Skip to content

feat(overseer): make AGENT_NOTIFY_SUMMARY contract invisible to humans - #81

Open
heavygee wants to merge 1 commit into
feat/overseer-readonly-entityfrom
feat/overseer-contract-invisible
Open

feat(overseer): make AGENT_NOTIFY_SUMMARY contract invisible to humans#81
heavygee wants to merge 1 commit into
feat/overseer-readonly-entityfrom
feat/overseer-contract-invisible

Conversation

@heavygee

Copy link
Copy Markdown
Owner

Summary

The AGENT_NOTIFY_SUMMARY machine 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 strict startsWith match 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. 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; 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 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

  • Kept the existing AGENT_NOTIFY_SUMMARY token (no estate-wide rename) - collapse-normalized detection handles the observed dup-drop foible. ASCII, fail-safe.
  • Dropped the input-side store/deliver decouple in favor of strip-at-egress (lower risk; no change to agent message delivery).
  • Local CLI/Ink terminal is the agent printing to its own tty - out of scope for the remote-render fix.

Test plan

  • 16 new shared tests (collapseRepeats, matchNotifySummaryLine correct+SUMARY+reject, stripAgentContract prefix/trailing/corrupted/quote/empty, round-trip). 52/52 shared green.
  • cli typecheck clean; web/hub changed files typecheck clean.
  • Soup rebuild --verify (isolation typecheck failures are pre-existing base/cross-layer debt: useSSE.ts on feat(overseer): read-only Overseer entity + 7 query tools + voice route #56, modelErrorCopy, scratchlist serviceTier - resolve in full soup).
  • Operator dogfood on :3006: agent reply clean, operator prompt clean, inbox still populates (incl. corrupted-Cursor turns now recovered).

Stacked on #56 (feat/overseer-readonly-entity). Carries inherited garden content pending the stack-wide garden rebase for clean upstream PR.

Made with Cursor

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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) }],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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) }],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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) }],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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])) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

@heavygee
heavygee force-pushed the feat/overseer-readonly-entity branch from 8045059 to 4cb90c2 Compare July 14, 2026 13:21
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>
@heavygee
heavygee force-pushed the feat/overseer-contract-invisible branch from ff5f384 to d8e20c7 Compare July 16, 2026 10:56

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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) }],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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) }],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

@heavygee

heavygee commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

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 tiann/hapi via tiann#1464 (peer worktree feat/notify-contract-strip). Do not wait on community pretty-print tiann#1462.

Once upstream strip merges, this fork layer can satisfy the manifest "Drop when upstream" note.

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.

1 participant