feat(overseer): make AGENT_NOTIFY_SUMMARY contract invisible to humans - #81
feat(overseer): make AGENT_NOTIFY_SUMMARY contract invisible to humans#81heavygee wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ff5f384007
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // sees the machine contract. The raw text stays in the store for the | ||
| // overseer event/inbox pipeline. copyText derives from this content, | ||
| // so the clipboard is clean too. | ||
| content: [{ type: 'text', text: stripAgentContract(block.text) }], |
There was a problem hiding this comment.
Strip contract from nested task messages
This only strips blocks that are converted into top-level assistant-ui messages. Subagent/Task traces are reduced into toolBlock.children and then rendered directly from block.text in HappyNestedBlockList, bypassing toThreadMessageLike; when a Task/Agent/CodexAgent child response ends with AGENT_NOTIFY_SUMMARY (or the SUMARY variant), opening the task details still shows the machine contract to the human. Strip before embedding child ChatBlocks or in the nested renderer as well.
Useful? React with 👍 / 👎.
| // non-Cursor flavors the hub prepends an inline contract prefix to | ||
| // the stored operator message (#20); stripAgentContract removes that | ||
| // leading block. No-op when absent. | ||
| content: [{ type: 'text', text: stripAgentContract(block.text) }], |
There was a problem hiding this comment.
Strip user prefix before building outline labels
Stripping the stored user text only inside toThreadMessageLike is too late for the conversation outline: SessionChat builds outline items from reconciled.blocks, and buildConversationOutline labels them from raw block.text. For non-Cursor sessions where the hub stores the inline contract prefix on operator messages, opening the Outline still shows labels starting with the machine instruction instead of the user's prompt. Apply the same stripping before outline generation or strip in the outline label path.
Useful? React with 👍 / 👎.
| // non-Cursor flavors the hub prepends an inline contract prefix to | ||
| // the stored operator message (#20); stripAgentContract removes that | ||
| // leading block. No-op when absent. | ||
| content: [{ type: 'text', text: stripAgentContract(block.text) }], |
There was a problem hiding this comment.
Keep user-authored summary lines visible
Using the same stripAgentContract on user messages removes any prompt whose last line is a bare AGENT_NOTIFY_SUMMARY {...} line. When an operator is asking about or testing this contract, the stored/sent text is still present but the web render and copy action silently omit that final line; the user side only needs the leading injected prefix removed, so use role-specific stripping instead of stripping trailing notify summaries from user-authored content.
Useful? React with 👍 / 👎.
| const lines = out.split('\n') | ||
| let lastIdx = lines.length - 1 | ||
| while (lastIdx >= 0 && lines[lastIdx].trim() === '') lastIdx -= 1 | ||
| if (lastIdx >= 0 && matchNotifySummaryLine(lines[lastIdx])) { |
There was a problem hiding this comment.
Strip malformed notify attempts too
This only removes a trailing marker when matchNotifySummaryLine accepts it, so malformed attempts such as AGENT_NOTIFY_SUMMARY {"status":"done" or AGENT_NOTIFY_SUMMARY not-json still render in the top-level web message and FCM fallback. The hub already treats those lines as machine-contract validation failures via detectMalformedNotifySummaryLine, so the human-facing stripper should remove the token line before/independent of JSON parseability.
Useful? React with 👍 / 👎.
8045059 to
4cb90c2
Compare
The machine-only notify contract rides in-band (works across every agent flavor) but polluted the human view on two channels: the injected inline contract prefix on operator messages, and the trailing AGENT_NOTIFY_SUMMARY line on agent replies. Empirical probe of 354k stored messages: Cursor corrupts its own token ~13.6% of turns (AGENT_NOTIFY_SUMMARY -> AGENT_NOTIFY_SUMARY, dropping one of the doubled M); Claude/Codex: 0. The strict startsWith match double-failed on those - the line neither stripped (leaked to UI) nor parsed (no inbox item). - shared/messages.ts: collapseRepeats() + matchNotifySummaryLine(), a corruption-tolerant detector shared by parse and strip. extractNotifySummary now recovers the ~780 corrupted Cursor turns. - shared/overseerEvents.ts: stripAgentContract() removes the leading contract prefix block and the trailing (collapse-normalized) summary line. Render-only; the raw text stays in the store so the overseer event/inbox pipeline is unaffected. - web/assistant-runtime.ts: strip at the block->message conversion for both agent-text (trailing line) and user-text (leading prefix). Render and copy both derive from the stripped content. - hub/fcm: strip the raw-text fallback body so short replies do not leak the marker into push notifications (parsed summary path still reads raw). Tests: 16 new (collapseRepeats, matchNotifySummaryLine correct+SUMARY+reject, stripAgentContract prefix/trailing/corrupted/quote/empty, round-trip). 52/52 shared green. Co-authored-by: Cursor <cursoragent@cursor.com>
ff5f384 to
d8e20c7
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d8e20c7b77
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // sees the machine contract. The raw text stays in the store for the | ||
| // overseer event/inbox pipeline. copyText derives from this content, | ||
| // so the clipboard is clean too. | ||
| content: [{ type: 'text', text: stripAgentContract(block.text) }], |
There was a problem hiding this comment.
Strip contract in voice context too
This only cleans the assistant-ui render/copy path; I checked the voice path and formatMessage/extractLastAssistantSpeakable in web/src/realtime/hooks/contextFormatters.ts still return raw codex/text-block content, which is fed into voice bootstrap/proactive ready updates via voiceContextPlan.ts and voiceHooks.ts. When a user starts voice or has proactive voice enabled after an agent reply ending in AGENT_NOTIFY_SUMMARY, the machine line can still be included in the voice prompt and spoken/summarized for the human, so apply stripAgentContract in those voice formatters as well.
Useful? React with 👍 / 👎.
| // sees the machine contract. The raw text stays in the store for the | ||
| // overseer event/inbox pipeline. copyText derives from this content, | ||
| // so the clipboard is clean too. | ||
| content: [{ type: 'text', text: stripAgentContract(block.text) }], |
There was a problem hiding this comment.
Drop summary-only text blocks after stripping
When an agent response contains only the notify line, this still emits an assistant message whose sole text part is '' after stripping; HappyAssistantMessage still renders the message root/actions/metadata for that message, so short status-only turns become empty assistant cards instead of the contract being invisible. Filter out the block/message after stripping, or avoid returning a text part when the stripped content is empty.
Useful? React with 👍 / 👎.
| * Overseer event capture and notification builders MUST read the raw text, not | ||
| * this - stripping is render-only so the machine signal survives in the store. | ||
| */ | ||
| export function stripAgentContract(text: string): string { |
There was a problem hiding this comment.
Use stripper for markdown exports
This helper is only wired into the assistant-ui conversion path, but the session export path (web/src/lib/sessionExport/markdown.ts) normalizes raw stored messages and writes message.content.text/agent text blocks directly into the downloaded markdown. Exporting a session with a trailing AGENT_NOTIFY_SUMMARY reply or a historical inline-prefix user message still exposes the machine contract to the human, so the export formatter should run stripAgentContract on user and assistant text before serializing.
Useful? React with 👍 / 👎.
|
Emergency follow-up (2026-08-09): tiann#1376 merged emit upstream without this strip. Vanilla main now shows raw footers; soup hid the gap. Remediation: transplant render-only strip to Once upstream strip merges, this fork layer can satisfy the manifest "Drop when upstream" note. |
Summary
The
AGENT_NOTIFY_SUMMARYmachine contract rides in-band (works across every agent flavor) but polluted the human view on two channels: the injected inline contract prefix on operator messages, and the trailing summary line on agent replies.Empirical probe (354k stored messages): Cursor corrupts its own token ~13.6% of turns (
AGENT_NOTIFY_SUMMARY->AGENT_NOTIFY_SUMARY, dropping one of the doubled M); Claude/Codex: 0. The strictstartsWithmatch double-failed on those - the line neither stripped (leaked to UI) nor parsed (no inbox item).Changes
shared/messages.ts:collapseRepeats()+matchNotifySummaryLine()- a corruption-tolerant detector shared by parse and strip.extractNotifySummarynow recovers the ~780 corrupted Cursor turns.shared/overseerEvents.ts:stripAgentContract()removes the leading contract-prefix block and the trailing (collapse-normalized) summary line. Render-only; raw text stays in the store so the overseer event/inbox pipeline is unaffected.web/assistant-runtime.ts: strip at the block->message conversion for bothagent-text(trailing line) anduser-text(leading prefix). Render and copy both derive from stripped content.hub/fcm: strip the raw-text fallback body so short replies do not leak the marker into push notifications (the parsed-summary path still reads raw).Design notes
AGENT_NOTIFY_SUMMARYtoken (no estate-wide rename) - collapse-normalized detection handles the observed dup-drop foible. ASCII, fail-safe.Test plan
--verify(isolation typecheck failures are pre-existing base/cross-layer debt:useSSE.tson feat(overseer): read-only Overseer entity + 7 query tools + voice route #56,modelErrorCopy, scratchlistserviceTier- resolve in full soup).Stacked on #56 (
feat/overseer-readonly-entity). Carries inherited garden content pending the stack-wide garden rebase for clean upstream PR.Made with Cursor