diff --git a/apps/daemon/src/server.ts b/apps/daemon/src/server.ts index 98da40cd3..580150592 100644 --- a/apps/daemon/src/server.ts +++ b/apps/daemon/src/server.ts @@ -10276,13 +10276,22 @@ export async function startServer({ clientSystemPrompt: clientInstructionPrompt, finalPromptOverride: codexImagegenOverride, }); + // Some models (notably claude-opus-4-7 with --include-partial-messages) + // start their reply by echoing the top of the user message verbatim, + // so the rendered chat shows a "# Instructions ..." block ahead of the + // real answer. Closing every Instructions block with an explicit + // "do not echo" line cuts the regression in practice without changing + // the turn-shape every agent CLI expects (user message carrying both + // instructions and request) — see server.ts:9920 composer notes. + const ECHO_GUARD = + '\n\n(Do not quote, restate, or echo the # Instructions block above in your reply. Begin your response with the answer to the # User request below.)'; const composed = [ instructionPrompt - ? `# Instructions (read first)\n\n${instructionPrompt}${cwdHint}${linkedDirsHint}\n\n---\n` + ? `# Instructions (read first)\n\n${instructionPrompt}${cwdHint}${linkedDirsHint}${ECHO_GUARD}\n\n---\n` : cwdHint - ? `# Instructions${cwdHint}${linkedDirsHint}\n\n---\n` + ? `# Instructions${cwdHint}${linkedDirsHint}${ECHO_GUARD}\n\n---\n` : linkedDirsHint - ? `# Instructions${linkedDirsHint}\n\n---\n` + ? `# Instructions${linkedDirsHint}${ECHO_GUARD}\n\n---\n` : '', `# User request\n\n${userRequestPrompt}${attachmentHint}${commentHint}`, safeImages.length diff --git a/apps/daemon/tests/chat-route.test.ts b/apps/daemon/tests/chat-route.test.ts index 57579ce52..13d23829a 100644 --- a/apps/daemon/tests/chat-route.test.ts +++ b/apps/daemon/tests/chat-route.test.ts @@ -214,6 +214,51 @@ process.exit(0); ); }); + it('closes the # Instructions block with an explicit "do not echo" guard so models do not parrot the prompt back', async () => { + // claude-opus-4-7 (and a few other instruction-tuned models) start + // their reply by echoing the # Instructions block verbatim, which + // shows up to users as the system prompt leading the visible + // answer. server.ts:9934 closes every Instructions block with a + // trailing guard line; this test pins the literal so a future + // refactor cannot silently drop it. + await withFakeAgent( + 'opencode', + ` +let prompt = ''; +process.stdin.setEncoding('utf8'); +process.stdin.on('data', (chunk) => { + prompt += chunk; +}); +process.stdin.on('end', () => { + const checks = [ + prompt.includes('Do not quote, restate, or echo the # Instructions block above') + ? 'has-echo-guard' + : 'missing-echo-guard', + ]; + console.log(JSON.stringify({ type: 'step_start' })); + console.log(JSON.stringify({ type: 'text', part: { text: checks.join('\\n') } })); + console.log(JSON.stringify({ type: 'step_finish', part: { tokens: { input: 1, output: 1 } } })); + process.exit(0); +}); +`, + async () => { + const response = await fetch(`${baseUrl}/api/chat`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + agentId: 'opencode', + message: 'hello', + }), + }); + const body = await response.text(); + + expect(response.ok).toBe(true); + expect(body).toContain('has-echo-guard'); + expect(body).not.toContain('missing-echo-guard'); + }, + ); + }); + it('injects @-mention skillIds into the composed system prompt', async () => { await withFakeAgent( 'opencode',