fix(daemon): close # Instructions block with an explicit do-not-echo guard (#2827)

The composed chat prompt prepends a '# Instructions (read first)'
block in front of '# User request' so a single user message carries
both the system rules and the actual request — the shape every agent
CLI (Claude, Codex, OpenCode, Gemini) expects on stdin.

In practice claude-opus-4-7 (and a few other instruction-tuned
models, particularly with --include-partial-messages on the stream)
start their reply by echoing the top of that user message verbatim.
The chat UI then shows the system prompt as a literal block leading
the visible answer, e.g.:

  Instructions
  Always respond in Korean. Use Korean for all explanations…
  …Maintain full orthographic correctness…
  ).네, 완료했습니다. 전달하신 4가지 보강 포인트를 …

(The closing token of the instructions block runs straight into the
real answer without a newline — the telltale of a model-side echo
rather than a UI render bug.)

Close every Instructions block with one trailing line:

  (Do not quote, restate, or echo the # Instructions block above in
  your reply. Begin your response with the answer to the # User
  request below.)

This kills the regression in practice without changing the turn
shape (still one user message), so no agent CLI plumbing has to move.

Tested via tests/chat-route.test.ts — pins the literal guard string
so a future refactor cannot silently drop it.

Co-authored-by: nicejames <nicejames@gmail.com>
This commit is contained in:
leessju 2026-05-24 23:30:58 +09:00 committed by GitHub
parent 53dfb8808c
commit 8e3d1360bd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 57 additions and 3 deletions

View file

@ -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

View file

@ -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',