diff --git a/packages/daemon/src/turn.ts b/packages/daemon/src/turn.ts index d4a5fa6..0f21484 100644 --- a/packages/daemon/src/turn.ts +++ b/packages/daemon/src/turn.ts @@ -242,8 +242,8 @@ export function modelEnvSecrets(modelEnv: Record | undefined): s * redaction. Holding each partial line also keeps the daemon log readable when a process writes * one line in several chunks. * - * Lines are parsed only for model provenance. Agent activity is intentionally not copied into the - * daemon's operational log; the complete redacted stdout/stderr remains in the per-turn log file. + * What a line MEANS is the harness's business (`Harness.renderOutput`); a renderer that declines — + * or a harness with none at all — leaves the raw line, which is what non-JSON CLI output gets too. */ function liveOutputLogger( log: ((message: string) => void) | undefined, @@ -261,6 +261,7 @@ function liveOutputLogger( let effectiveModel = initialModel const emit = (stream: ContainerOutputStream, line: string): void => { if (line.length === 0) return + let messages = [line] if (stream === 'stdout' && render) { // Belt and braces: a renderer is contractually no-throw, but it parses untrusted container // output, and a throw here would escape into the runner's output callback mid-turn. @@ -282,8 +283,12 @@ function liveOutputLogger( (previous !== undefined ? ` (switched from ${redactOutput(previous, secrets)})` : ''), ) } + messages = rendered.lines } } + for (const message of messages) { + log?.(`agent ${profile} ${requestUri} ${stream}: ${redactOutput(message, secrets)}`) + } } const write = (stream: ContainerOutputStream, chunk: string): void => { pending[stream] += chunk diff --git a/packages/daemon/test/turn.test.mjs b/packages/daemon/test/turn.test.mjs index bd79207..f39aa15 100644 --- a/packages/daemon/test/turn.test.mjs +++ b/packages/daemon/test/turn.test.mjs @@ -358,7 +358,7 @@ it('crashed: a container that produces no socket observation is classified crash }) }) -it('keeps streamed agent output out of the daemon log', async () => { +it('streams tagged, redacted agent output while the turn is running', async () => { const { actor } = await makeActor('did:plc:agent-live-output') await withRunDir(async (runDir) => { const messages = [] @@ -375,11 +375,15 @@ it('keeps streamed agent output out of the daemon log', async () => { makeToken: () => 'secret-token', log: (message) => messages.push(message), }) - assert.deepEqual(messages.slice(1), [`turn ${BUNDLE.request.uri} exited without an artifact or question; marked crashed`]) + assert.deepEqual(messages.slice(1), [ + `agent planner ${BUNDLE.request.uri} stdout: working with [redacted]`, + `agent planner ${BUNDLE.request.uri} stderr: still running`, + `turn ${BUNDLE.request.uri} exited without an artifact or question; marked crashed`, + ]) }) }) -it('does not copy formatted Claude activity into the daemon log', async () => { +it('formats Claude stream-json activity for the daemon log', async () => { const { actor } = await makeActor('did:plc:agent-stream-json') await withRunDir(async (runDir) => { const messages = [] @@ -400,7 +404,12 @@ it('does not copy formatted Claude activity into the daemon log', async () => { checkout: noopCheckout, log: (message) => messages.push(message), }) - assert.deepEqual(messages.slice(1), [`turn ${BUNDLE.request.uri} exited without an artifact or question; marked crashed`]) + assert.deepEqual(messages.slice(1), [ + `agent planner ${BUNDLE.request.uri} stdout: tool Bash: npm test`, + `agent planner ${BUNDLE.request.uri} stdout: tool Write: /tmp/plan.md`, + `agent planner ${BUNDLE.request.uri} stdout: result: Done`, + `turn ${BUNDLE.request.uri} exited without an artifact or question; marked crashed`, + ]) }) }) @@ -427,8 +436,9 @@ it('stamps the latest model the agent reports, including a mid-turn switch', asy log: (message) => messages.push(message), }) assert.match(messages[0], /model opus$/) - assert.deepEqual(messages.slice(1, 3), [ + assert.deepEqual(messages.slice(1, 4), [ `agent planner ${BUNDLE.request.uri} running on model claude-opus-4-8-20251101`, + `agent planner ${BUNDLE.request.uri} stdout: assistant: thinking`, `agent planner ${BUNDLE.request.uri} running on model claude-sonnet-4-8-20251101 (switched from claude-opus-4-8-20251101)`, ]) // One line per change only — the repeat on the second event does not log again. @@ -1297,7 +1307,7 @@ it('a managed-auth codex turn mounts a scratch home, redacts tokens, and writes passwd, `radial:x:${process.getuid?.() ?? 1000}:${process.getgid?.() ?? 1000}:Radial turn:/home/radial:/bin/sh\n`, ) - assert.ok(messages.every((message) => !message.includes(' stderr: '))) + assert.ok(messages.some((message) => /agent .* stderr: \[redacted\]/.test(message))) assert.ok(messages.every((message) => !message.includes('managed-refresh-secret'))) assert.equal( JSON.parse(await readFile(join(authDir, 'auth.json'), 'utf8')).tokens.refresh_token, @@ -1365,7 +1375,12 @@ it('any provider credential the daemon holds reaches the container, and secrets modelEnv: { OPENROUTER_API_KEY: 'sk-or-v1-secret-value', CLOUDFLARE_ACCOUNT_ID: 'acct-1234' }, log: (message) => messages.push(message), }) - assert.ok(messages.every((message) => !message.includes('auth failed'))) + // Redaction is by NAME: blanket-redacting an account id (or a region, or a cache setting) + // would mangle every log line that happened to contain it. + assert.equal( + messages[1], + `agent planner ${BUNDLE.request.uri} stdout: auth failed for [redacted] on acct-1234`, + ) }) }) @@ -1413,6 +1428,11 @@ it("a harness's rendering never reaches another harness's output", async () => { checkout: noopCheckout, log: (message) => messages.push(message), }) - assert.deepEqual(messages.slice(1, -1), [`agent planner ${BUNDLE.request.uri} running on model openai/gpt-5.2`]) + assert.deepEqual(messages.slice(1, -1), [ + `agent planner ${BUNDLE.request.uri} running on model openai/gpt-5.2`, + `agent planner ${BUNDLE.request.uri} stdout: assistant: On it`, + `agent planner ${BUNDLE.request.uri} stdout: tool bash: pnpm test`, + `agent planner ${BUNDLE.request.uri} stdout: plain unstructured warning`, + ]) }) })