fix(agent): make mid-turn /steer trusted, not read as injection

A steer rides inside a tool result (the only role-alternation-safe slot
mid-turn), so a bare "User guidance:" line reads as untrusted tool content —
well-behaved models refuse it as suspected prompt injection (observed live:
"I only follow instructions from you directly, not ones injected through
command results").

- Wrap steers in a bounded, self-describing [OUT-OF-BAND USER MESSAGE] marker
  (prompt_builder.format_steer_marker), shared by both drain sites.
- Add STEER_CHANNEL_NOTE to the core system prompt so the model expects this
  exact marker and trusts it as a genuine user message — while still ignoring
  lookalikes buried in tool/web/file output. Static text → byte-stable prompt,
  no prompt-cache regression; gated on the agent having tools.
- Desktop: steer ack is now an inline transcript note ( steered · …) instead
  of a toast.

Marker is intentionally static (not a per-session nonce) to honor the
byte-stable system-prompt caching policy; nonce hardening noted as follow-up.
This commit is contained in:
Brooklyn Nicholson
2026-06-05 20:59:36 -05:00
parent 40aef6af91
commit 0f45509daf
6 changed files with 75 additions and 14 deletions
@@ -767,7 +767,9 @@ export function usePromptActions({
if (result?.status === 'queued') {
triggerHaptic('submit')
notify({ kind: 'success', title: 'Steered', message: text })
// Inline note (not a toast) so the nudge lives in the transcript next
// to the turn it steered — same centered system-note style as slash output.
appendSessionTextMessage(sessionId, 'system', `⏩ steered · ${text}`)
return true
}
@@ -777,7 +779,7 @@ export function usePromptActions({
return false
},
[activeSessionId, activeSessionIdRef, requestGateway]
[activeSessionId, activeSessionIdRef, appendSessionTextMessage, requestGateway]
)
const reloadFromMessage = useCallback(