From 81a429c2601b3fd38e108179d52558c5468789ef Mon Sep 17 00:00:00 2001 From: Vishal Rana Date: Sat, 31 Oct 2026 07:42:53 -0700 Subject: [PATCH] fix(chat): return scrubbed command output to the agent under every capture policy Under the default capture_text = "titles", Chat discarded the output of commands the user approved before it reached the agent, so run_command answered with only an exit code. The agent could run a command but never read what it printed, and could not finish the task it asked to run it for. The agent now gets the scrubbed head and tail of the output under every policy. Storage still follows capture_text: titles keeps no command output and no result text in the action log. The chat guide says what is sent. --- internal/chat/command_output.go | 19 +++++++++---------- internal/chat/command_output_test.go | 6 +++++- site/src/content/docs/guides/chat.mdx | 10 ++++++---- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/internal/chat/command_output.go b/internal/chat/command_output.go index 5669af5..dd22e2c 100644 --- a/internal/chat/command_output.go +++ b/internal/chat/command_output.go @@ -12,7 +12,9 @@ import ( // Each stream holds at most one bounded lexical token. Tokens spanning writes // are scrubbed together; PEM bodies and oversized secret tokens are discarded before -// reaching storage. No raw output is spooled to disk. +// reaching storage or the agent. No raw output is spooled to disk. The agent +// always gets the scrubbed head and tail of the command it asked to run; only +// snippets capture stores the full scrubbed output. type outputStream struct { parent *commandOutput name, token string @@ -50,10 +52,12 @@ func (o *commandOutput) emit(ctx context.Context, stream, data string) { if data == "" || o.err != nil { return } - if o.err = o.st.AppendCommandOutput(ctx, o.action, stream, o.seq, []byte(data)); o.err != nil { - return + if o.snippets { + if o.err = o.st.AppendCommandOutput(ctx, o.action, stream, o.seq, []byte(data)); o.err != nil { + return + } + o.seq++ } - o.seq++ o.total += len(data) if len(o.head) < 2048 { n := min(2048-len(o.head), len(data)) @@ -97,9 +101,6 @@ func (s *outputStream) Write(p []byte) (int, error) { if o.finished { return 0, errors.New("command output closed") } - if !o.snippets { - return len(p), nil - } var safe strings.Builder for _, b := range p { if b == ' ' || b == '\n' || b == '\r' || b == '\t' { @@ -140,9 +141,7 @@ func (o *commandOutput) Finish(ctx context.Context) (string, error) { defer o.mu.Unlock() if !o.finished { for _, s := range o.streams { - if o.snippets { - o.emit(ctx, s.name, s.flush()) - } + o.emit(ctx, s.name, s.flush()) } o.finished = true } diff --git a/internal/chat/command_output_test.go b/internal/chat/command_output_test.go index dc202df..a0ce66b 100644 --- a/internal/chat/command_output_test.go +++ b/internal/chat/command_output_test.go @@ -49,8 +49,12 @@ func TestCommandOutputCapture(t *testing.T) { if err = st.ReadCommandOutput(context.Background(), "a", &full); err != nil { t.Fatal(err) } + // The agent gets the scrubbed head and tail under every policy. + if strings.Contains(summary, "ghp_") || !strings.Contains(summary, "HEAD") || !strings.Contains(summary, "[redacted]") || !strings.Contains(summary, "TAIL") || len(summary) > 5000 { + t.Fatal("agent summary", summary) + } if !snippets { - if full.Len() != 0 || summary != "" { + if full.Len() != 0 { t.Fatal("titles retained output") } return diff --git a/site/src/content/docs/guides/chat.mdx b/site/src/content/docs/guides/chat.mdx index c1cc0b7..33c9534 100644 --- a/site/src/content/docs/guides/chat.mdx +++ b/site/src/content/docs/guides/chat.mdx @@ -180,15 +180,17 @@ interrupted work is marked interrupted after a restart and never replayed to the agent. Your question is scrubbed and sent to Claude. Context and tool results pass the -capture policy first. Under the default `capture_text = "titles"` Driga keeps +capture policy first, except the output of commands you approve, described +below. Under the default `capture_text = "titles"` Driga keeps no chat message bodies; `snippets` keeps scrubbed 200-character snippets. Claude's own session history is managed by Claude and is not controlled by Driga's capture policy. Command results join that action log under the same capture policy and -30-day retention. Long command output keeps its head and tail in the -conversation; retained full output is scrubbed and follows the capture -policy (`titles` does not retain command output). Process result counts and +30-day retention. Claude gets the scrubbed head and tail of each command you +approve, up to 2 KB of each, under every capture policy, since reading that +output is why it asked to run the command. Retained full output is scrubbed +and follows the capture policy (`titles` does not retain command output). Process result counts and per-target outcomes appear on the card. External `driga mcp` clients and `driga ask` have no command actions.