Skip to content

fix(voice-agent): forbid speaking [System: ...] framing aloud (closes #829) - #847

Merged
sonichi merged 1 commit into
mainfrom
fix/voice-system-bracket-leak
May 18, 2026
Merged

sonichi merged 1 commit into
mainfrom
fix/voice-system-bracket-leak

Conversation

@qingyun-wu

Copy link
Copy Markdown
Collaborator

Summary

Closes #829 — implements Lucy's proposed fix verbatim.

Adds a CRITICAL block to the voice-agent instructions array forbidding speech of [System: ...] framing. 5-line addition, prompt-only change.

Why

Lucy (Mac Studio bot) ran a new prompt-parrot detector against data/conversation.sqlite and found 9 anomalies across recent sessions — clustered in meeting-mode rehearsal, co-present demo, dismiss handling, and client-reconnect. Every code path that injects a bracketed [System: ...] control string into the conversation context can trigger it. The current system prompt has no explicit rule forbidding speech of bracketed framing, and Gemini native-audio treats text in context as speakable unless told otherwise.

Concrete examples from the issue:

2026-05-07 04:54:33  [System: The user said \"Thank you for watching,\" which clearly signals the end ...]
2026-05-07 04:13:12  [System: MEETING MODE — you are listening and taking notes. Only respond if someone says \"Sutando.\"]
2026-05-18 05:29:33  [System: Silence per co-presenter rules; Susan did not say CUE 1/2.]

Diff

 'IMPORTANT:',
 '- For high-stakes or irreversible actions (sending email, payments, deleting files),',
 '  confirm with the user before executing unless they have given standing approval.',
 '- When background tasks are running, stay present and responsive.',
 '- You earn your usefulness by doing, not explaining.',
+'',
+'CRITICAL — Never speak `[System: ...]` text aloud:',
+'- Any input string beginning with `[System:` is an internal directive, NOT content to read.',
+'- Treat the bracketed text as instructions to act on; emit ZERO audio referencing it.',
+'- If a `[System: ...]` chunk arrives mid-context, silently honor its directive — do not narrate it, do not summarize it, do not echo it back. Producing the literal bracket text is a bug.',

Verification (from #829)

  1. Restart voice-agent.
  2. Trigger a meeting-mode session + a co-present session + a dismiss flow.
  3. Re-run python3 skills/voice-self-repair/scripts/detect.py after 24h — prompt-parrot count should be 0 for sessions after the fix.

The detector lives on a separate WIP branch (feat/voice-self-repair-detector-v1) and isn't merged yet. Until it lands, verification can be "no bracketed text in conversation.sqlite for new sessions."

Scope note

This is intentionally a prompt-only fix. The CLAUDE.md guidance "When refactoring, do NOT change prompts or tool behavior" is about preserving tuned prompts during refactors — this is a deliberate prompt ADDITION based on detector evidence, with explicit author proposal in the issue.

Sibling not addressed

#829 also notes skills/discord-voice/scripts/discord-voice-server.ts will need the same rule once the multi-bot SUTANDO_INSTANCE_NAME prompt lands. That work isn't on main yet (Liu's WIP #791/#796); left for a followup.

Credits

Lucy (Mac Studio bot) filed the issue + proposed the diff. This PR just lands it.

@qingyun-wu
qingyun-wu force-pushed the fix/voice-system-bracket-leak branch 2 times, most recently from 9699030 to 2f10f9b Compare May 18, 2026 19:51

@sonichi sonichi left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — 5-line system-prompt addition, focused fix. The 3 sub-points (directive not content / honor silently / never echo) cover the failure mode cleanly. Closes #829 with the right shape — prompt-level rule rather than parsing-layer filter. Both checks green. Ship.

…829)

Lucy's detector found 9 prompt-parrot anomalies across recent sessions
where the voice-agent read `[System: ...]` control strings aloud as if
they were content. Clustered in meeting-mode rehearsal, co-present
demo, and dismiss handling — every code path that injects a bracketed
control string into the conversation context can trigger it.

Applies the proposed fix from #829 verbatim — adds a CRITICAL block to
the instructions array forbidding speech of `[System:` framing.

**Verification path (from #829):**

1. Restart voice-agent
2. Trigger a meeting-mode session + a co-present session + a dismiss flow
3. Re-run `python3 skills/voice-self-repair/scripts/detect.py` after 24h
   — prompt-parrot count should be 0 for sessions after the fix.

Note: the detector lives on a separate WIP branch (`feat/voice-self-repair-detector-v1`)
and isn't merged yet. Until it lands, verification is "no bracketed
text in conversation.sqlite for new sessions."

**Scope note:**

This is intentionally a prompt-only fix. The CLAUDE.md guidance "When
refactoring, do NOT change prompts or tool behavior" is about preserving
tuned prompts during refactors — this is a deliberate prompt ADDITION
based on detector evidence, with explicit author proposal in the issue.

**Sibling not addressed:**

#829 also notes `skills/discord-voice/scripts/discord-voice-server.ts`
will need the same rule once multi-bot SUTANDO_INSTANCE_NAME prompt
lands. That's not on main yet; left for a followup when it is.

Author: Lucy (Mac Studio bot) filed the issue + proposed the fix.
@qingyun-wu
qingyun-wu force-pushed the fix/voice-system-bracket-leak branch from 2f10f9b to 498854d Compare May 18, 2026 19:55
@sonichi
sonichi merged commit 68e648d into main May 18, 2026
2 checks passed
@sonichi
sonichi deleted the fix/voice-system-bracket-leak branch May 18, 2026 19:56
liususan091219 pushed a commit to liususan091219/sutando-fork that referenced this pull request May 28, 2026
…onichi#829) (sonichi#847)

Lucy's detector found 9 prompt-parrot anomalies across recent sessions
where the voice-agent read `[System: ...]` control strings aloud as if
they were content. Clustered in meeting-mode rehearsal, co-present
demo, and dismiss handling — every code path that injects a bracketed
control string into the conversation context can trigger it.

Applies the proposed fix from sonichi#829 verbatim — adds a CRITICAL block to
the instructions array forbidding speech of `[System:` framing.

**Verification path (from sonichi#829):**

1. Restart voice-agent
2. Trigger a meeting-mode session + a co-present session + a dismiss flow
3. Re-run `python3 skills/voice-self-repair/scripts/detect.py` after 24h
   — prompt-parrot count should be 0 for sessions after the fix.

Note: the detector lives on a separate WIP branch (`feat/voice-self-repair-detector-v1`)
and isn't merged yet. Until it lands, verification is "no bracketed
text in conversation.sqlite for new sessions."

**Scope note:**

This is intentionally a prompt-only fix. The CLAUDE.md guidance "When
refactoring, do NOT change prompts or tool behavior" is about preserving
tuned prompts during refactors — this is a deliberate prompt ADDITION
based on detector evidence, with explicit author proposal in the issue.

**Sibling not addressed:**

sonichi#829 also notes `skills/discord-voice/scripts/discord-voice-server.ts`
will need the same rule once multi-bot SUTANDO_INSTANCE_NAME prompt
lands. That's not on main yet; left for a followup when it is.

Author: Lucy (Mac Studio bot) filed the issue + proposed the fix.
qingyun-wu added a commit that referenced this pull request Sep 4, 2026
…ad table

Both from sonichi on #3850.

The comment said "Mirrors the client's hitlCategory.ts" and that file is not
in this repo — `git grep -l hitlCategory` returns only the comment itself. It
lives in ag2-space/cinny-webclient at src/app/components/message/hitlCategory.ts
(shipped as #847), so the comment now names the repo and the path. A reader
following it lands somewhere.

`_CATEGORY_HEADS[category_of(...)]` was a direct index, total only because
`HumanRequirement` coerces an UNRECOGNISED kind to `unknown`. It becomes
reachable the moment a kind already in `KINDS` is mapped to a third category
with no head, and it would raise inside the renderer that exists for clients
which cannot read the structured field — the least good place to raise. Now
`.get(..., blocked)`, which fails in the direction the mapping's own comment
argues for.

The test remaps `choice` rather than inventing a kind, because inventing one
measures the coercion instead of the renderer — sonichi's first probe did
exactly that and he said so. Control: restoring the direct index turns the
suite red (exit 1), the .get() form green. 10 hitl suites pass, ruff clean.

— sutando-qingyun-air (@qingyun-air.agent:ag2.space)
john-the-dev pushed a commit that referenced this pull request Sep 4, 2026
…3850)

* fix(hitl): the plain-text fallback follows the card's categorisation

cinny#847 colours a HITL card by kind — grey/light-blue for a decision, amber
for a real block. `fallback_body()` did not: it emitted
"⚠ Sutando needs your attention" for every kind, so on any client that ignores
the `space.ag2.hitl` field a mere choice or confirmation read as an alarm.

The mapping lives beside KINDS in schema.py, not in the projector, so the
engine has one owner for it and it mirrors the client's hitlCategory.ts
one-for-one. An unlisted kind is BLOCKED: under-stating a block strands the
user, over-stating a decision only over-warns.

Control: reverting the head line to its hardcoded form turns the suite red
(failures=1) and restoring it returns OK. All 10 hitl suites pass.

— sutando-qingyun-air (@qingyun-air.agent:ag2.space)

* fix(hitl): name where hitlCategory.ts lives, and stop indexing the head table

Both from sonichi on #3850.

The comment said "Mirrors the client's hitlCategory.ts" and that file is not
in this repo — `git grep -l hitlCategory` returns only the comment itself. It
lives in ag2-space/cinny-webclient at src/app/components/message/hitlCategory.ts
(shipped as #847), so the comment now names the repo and the path. A reader
following it lands somewhere.

`_CATEGORY_HEADS[category_of(...)]` was a direct index, total only because
`HumanRequirement` coerces an UNRECOGNISED kind to `unknown`. It becomes
reachable the moment a kind already in `KINDS` is mapped to a third category
with no head, and it would raise inside the renderer that exists for clients
which cannot read the structured field — the least good place to raise. Now
`.get(..., blocked)`, which fails in the direction the mapping's own comment
argues for.

The test remaps `choice` rather than inventing a kind, because inventing one
measures the coercion instead of the renderer — sonichi's first probe did
exactly that and he said so. Control: restoring the direct index turns the
suite red (exit 1), the .get() form green. 10 hitl suites pass, ruff clean.

— sutando-qingyun-air (@qingyun-air.agent:ag2.space)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

voice-agent: [System: ...] framing read out loud (prompt-parrot bug, affects meeting mode + co-present + dismiss)

2 participants