mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-15 04:18:44 +00:00
fix(gateway): scoped group writer prompt for system runs + media forwarding gaps (#104)
* fix(gateway): extract media forwarding helper and fix group writer cron/media gaps - Extract appendMediaToOutbound() helper to deduplicate media attachment conversion across all outbound paths - Add media forwarding to cron job handler, handoff announce, and teammate message flows - Fix silent reply suppression to allow media-only responses (no text content) - Skip group writer refusal prompt for system-initiated runs (cron, delegate, subagent) with empty senderID * fix(agent): protect identity files in system-initiated group writer runs - Move writer list lookup before senderID check so cron/delegate/subagent runs still load group writer config - Inject file protection prompt for system-initiated runs instead of skipping entirely, preventing modification of SOUL.md, IDENTITY.md, AGENTS.md, and USER.md - Retain fail-open behavior when writer list is unavailable --------- Co-authored-by: Nam Nguyen Ngoc <namnn.0911@gmail.com>
This commit is contained in:
1 parent
885814ddd1
commit
5f7f4eb204
3 files changed
+42
-31
No files matched your search
+30
-31
@@ -360,19 +360,7 @@ func consumeInboundMessages(ctx context.Context, msgBus *bus.MessageBus, agents
|
||||
Metadata: meta,
|
||||
}
|
||||
|
||||
// Convert media results from agent run to outbound media attachments
|
||||
for _, mr := range outcome.Result.Media {
|
||||
outMsg.Media = append(outMsg.Media, bus.MediaAttachment{
|
||||
URL: mr.Path,
|
||||
ContentType: mr.ContentType,
|
||||
})
|
||||
if mr.AsVoice {
|
||||
if outMsg.Metadata == nil {
|
||||
outMsg.Metadata = make(map[string]string)
|
||||
}
|
||||
outMsg.Metadata["audio_as_voice"] = "true"
|
||||
}
|
||||
}
|
||||
appendMediaToOutbound(&outMsg, outcome.Result.Media)
|
||||
|
||||
msgBus.PublishOutbound(outMsg)
|
||||
}(msg.Channel, msg.ChatID, sessionKey, runID, outMeta, blockReply)
|
||||
@@ -535,12 +523,7 @@ func consumeInboundMessages(ctx context.Context, msgBus *bus.MessageBus, agents
|
||||
Content: announceContent,
|
||||
Metadata: meta,
|
||||
}
|
||||
for _, mr := range outcome.Result.Media {
|
||||
outMsg.Media = append(outMsg.Media, bus.MediaAttachment{
|
||||
URL: mr.Path,
|
||||
ContentType: mr.ContentType,
|
||||
})
|
||||
}
|
||||
appendMediaToOutbound(&outMsg, outcome.Result.Media)
|
||||
msgBus.PublishOutbound(outMsg)
|
||||
}(sessionKey, origChannel, msg.ChatID, msg.SenderID, msg.Metadata["subagent_label"], outMeta, announceReq)
|
||||
continue
|
||||
@@ -664,12 +647,7 @@ func consumeInboundMessages(ctx context.Context, msgBus *bus.MessageBus, agents
|
||||
Content: announceContent,
|
||||
Metadata: meta,
|
||||
}
|
||||
for _, mr := range outcome.Result.Media {
|
||||
outMsg.Media = append(outMsg.Media, bus.MediaAttachment{
|
||||
URL: mr.Path,
|
||||
ContentType: mr.ContentType,
|
||||
})
|
||||
}
|
||||
appendMediaToOutbound(&outMsg, outcome.Result.Media)
|
||||
msgBus.PublishOutbound(outMsg)
|
||||
}(sessionKey, origChannel, msg.ChatID, msg.SenderID, outMeta, announceReq)
|
||||
continue
|
||||
@@ -730,15 +708,17 @@ func consumeInboundMessages(ctx context.Context, msgBus *bus.MessageBus, agents
|
||||
slog.Error("handoff announce: agent run failed", "error", outcome.Err)
|
||||
return
|
||||
}
|
||||
if outcome.Result.Content == "" || agent.IsSilentReply(outcome.Result.Content) {
|
||||
if (outcome.Result.Content == "" && len(outcome.Result.Media) == 0) || agent.IsSilentReply(outcome.Result.Content) {
|
||||
return
|
||||
}
|
||||
msgBus.PublishOutbound(bus.OutboundMessage{
|
||||
outMsg := bus.OutboundMessage{
|
||||
Channel: origCh,
|
||||
ChatID: chatID,
|
||||
Content: outcome.Result.Content,
|
||||
Metadata: meta,
|
||||
})
|
||||
}
|
||||
appendMediaToOutbound(&outMsg, outcome.Result.Media)
|
||||
msgBus.PublishOutbound(outMsg)
|
||||
}(origChannel, msg.ChatID, outMeta)
|
||||
continue
|
||||
}
|
||||
@@ -798,18 +778,20 @@ func consumeInboundMessages(ctx context.Context, msgBus *bus.MessageBus, agents
|
||||
slog.Error("teammate message: agent run failed", "error", outcome.Err)
|
||||
return
|
||||
}
|
||||
if outcome.Result.Content == "" || agent.IsSilentReply(outcome.Result.Content) {
|
||||
if (outcome.Result.Content == "" && len(outcome.Result.Media) == 0) || agent.IsSilentReply(outcome.Result.Content) {
|
||||
slog.Info("teammate message: suppressed silent/empty reply", "from", senderID)
|
||||
return
|
||||
}
|
||||
// Deliver response to origin channel (same as delegate/subagent announce).
|
||||
// This allows the lead to respond to users after receiving teammate updates.
|
||||
msgBus.PublishOutbound(bus.OutboundMessage{
|
||||
outMsg := bus.OutboundMessage{
|
||||
Channel: origCh,
|
||||
ChatID: chatID,
|
||||
Content: outcome.Result.Content,
|
||||
Metadata: meta,
|
||||
})
|
||||
}
|
||||
appendMediaToOutbound(&outMsg, outcome.Result.Media)
|
||||
msgBus.PublishOutbound(outMsg)
|
||||
}(origChannel, msg.ChatID, msg.SenderID, outMeta)
|
||||
continue
|
||||
}
|
||||
@@ -885,3 +867,20 @@ func consumeInboundMessages(ctx context.Context, msgBus *bus.MessageBus, agents
|
||||
debouncer.Push(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// appendMediaToOutbound converts agent MediaResults to outbound MediaAttachments
|
||||
// on the given OutboundMessage. Handles voice annotation when applicable.
|
||||
func appendMediaToOutbound(msg *bus.OutboundMessage, media []agent.MediaResult) {
|
||||
for _, mr := range media {
|
||||
msg.Media = append(msg.Media, bus.MediaAttachment{
|
||||
URL: mr.Path,
|
||||
ContentType: mr.ContentType,
|
||||
})
|
||||
if mr.AsVoice {
|
||||
if msg.Metadata == nil {
|
||||
msg.Metadata = make(map[string]string)
|
||||
}
|
||||
msg.Metadata["audio_as_voice"] = "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,7 @@ func makeCronJobHandler(sched *scheduler.Scheduler, msgBus *bus.MessageBus, cfg
|
||||
if peerKind == "group" {
|
||||
outMsg.Metadata = map[string]string{"group_id": job.Payload.To}
|
||||
}
|
||||
appendMediaToOutbound(&outMsg, result.Media)
|
||||
msgBus.PublishOutbound(outMsg)
|
||||
}
|
||||
|
||||
|
||||
@@ -459,6 +459,17 @@ func (l *Loop) buildGroupWriterPrompt(ctx context.Context, groupID, senderID str
|
||||
return "", files // fail-open
|
||||
}
|
||||
|
||||
// System-initiated runs (cron, delegate, subagent) have no sender ID.
|
||||
// Allow reading, messaging, and tool use freely, but still protect
|
||||
// identity files (SOUL.md, IDENTITY.md, etc.) from modification.
|
||||
if senderID == "" {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("## Group File Permissions\n\n")
|
||||
sb.WriteString("This is a system-initiated run (cron/scheduled task). You may read files, send messages, and use tools freely.\n")
|
||||
sb.WriteString("However, do NOT modify protected identity files (SOUL.md, IDENTITY.md, AGENTS.md, USER.md) unless explicitly instructed by the task.\n")
|
||||
return sb.String(), files
|
||||
}
|
||||
|
||||
numericID := strings.SplitN(senderID, "|", 2)[0]
|
||||
isWriter := false
|
||||
for _, w := range writers {
|
||||
|
||||
Reference in new issue
Block a user