Skip to content

message: name the trigger, and announce a waiting message at session start - #244

Merged
kevintseng merged 2 commits into
mainfrom
feat/message-first-class
Aug 29, 2026
Merged

message: name the trigger, and announce a waiting message at session start#244
kevintseng merged 2 commits into
mainfrom
feat/message-first-class

Conversation

@kevintseng

Copy link
Copy Markdown
Contributor

Why

An agent working in this repo spent a whole session beside two other local agents and never used message to reach them — it used the host's own push tool every time, because that tool named itself in the output the agent was reading, and nothing said "there is a durable inbox, and something in it is for you." Four written rules said to use MeMesh; none were in front of the agent at the moment it chose a tool.

The fix is structural, not another reminder: put the fact where the agent already looks.

What changes

  1. The message description names its trigger (MCP handler, schema export, AGENTS.md, SKILL.md, three READMEs, API_REFERENCE): contact another local agent — hand off, ask, report back — and send here first; the durable inbox is the record, host push is only delivery. AGENTS.md's loop gains the step. Doc contract recertified (11 tools / 7 documents).

  2. briefing and the SessionStart hook announce unread deliveries beside the stated goal/next/blocked lines: N messages waiting for "<project>" — fetch them with the message tool; fetching does not acknowledge. "Waiting" = a delivery with no intake receipt. One leaf (src/core/agent-message-inbox.ts) mirrored into the hook by the generator, exactly like task-state.ts, so the two surfaces cannot disagree. Read-only; returns 0 on a pre-4.8.0 schema.

    Verified end to end on an empty real schema: one real send → the hook injects the line; after intake → it stops. (A hand-rolled fixture had suggested the empty-graph path swallowed it — the fixture was violating the deliveries→messages FK and proved nothing. Tests now seed through the real send action.)

What this is not

Not a Claude Code push adapter. Host push for Claude Code remains a separate boundary; this ships the durable half — on any host, the agent learns at session start that something is waiting.

Verification

  • eg check → PASS 4/4 CODE_COMPLETE; each claim's failure_path recorded by eg (trigger text removed → parity red; unread line removed from briefing → red; from the hook → red); eg replay 11/11
  • node scripts/run-tests-isolated.mjs → exit 0, 2986 passed / 11 skipped
  • npm run verify:release → exit 0, 5/5
  • typecheck, lint, check:surface-parity, check-doc-claims → exit 0

An agent working in this repository spent a whole session beside two other
local agents and never once used `message` to reach them. It used the
host's own push tool every time, because that tool named itself in the
output the agent was reading, and nothing the agent read ever said "there
is a durable inbox, and something in it is for you." Four rules told it to
use MeMesh; none were in front of it at the moment it chose a tool.

So the fact goes where the agent is already looking, and the tool says
when it is the right one:

- The `message` description — MCP handler, schema export, and every
  documented surface — now opens with its trigger: contact another local
  agent (hand off work, ask for a result, report a disposition), send here
  FIRST; the durable inbox is the record, host push is only delivery.
  AGENTS.md's loop gains that step.

- `briefing` and the SessionStart hook add, beside the stated goal / next /
  blocked lines, `N messages waiting for "<project>" — fetch them with the
  message tool; fetching does not acknowledge.` "Waiting" is the inbox's
  own definition: a delivery with no intake receipt. Both surfaces use one
  leaf, src/core/agent-message-inbox.ts, mirrored into the hook by the
  generator exactly like task-state.ts, so they cannot disagree. It reads
  the graph read-only and returns 0 on a pre-4.8.0 schema.

  The line appears on a graph with NO memories at all — a probe on an empty
  real schema plus one real send injected it, and after intake it stopped.
  An earlier hand-rolled fixture had suggested the empty-graph path swallowed
  it; the fixture was violating the deliveries→messages foreign key, so it
  proved nothing. The tests now seed through the real send action.

This is not a Claude Code push adapter. Host push for Claude Code stays a
separate boundary; what this ships is the durable half — the agent learns
at session start, on any host, that something is waiting.

[Verified-By: eg check -> exit=0, 4/4 claims CODE_COMPLETE; each claim's
 failure_path recorded by eg (trigger text removed -> parity red; unread
 line removed from briefing -> tests red; removed from the hook -> tests
 red); eg replay 11/11]
[Verified-By: node scripts/run-tests-isolated.mjs -> exit=0,
 Tests 2986 passed | 11 skipped (2997)]
[Verified-By: end-to-end probe: empty real schema + executeAgentMessageAction
 send -> hook injects '1 message waiting for "testproj-ad567c5c"'; after
 intake -> no such line]
[Verified-By: npm run typecheck -> exit=0; lint -> exit=0;
 check:surface-parity -> PASS 11 tools / 7 documents; check-doc-claims -> exit=0]
Two conflicts, both mechanical. tests/hooks/session-start.test.ts: each
branch inserted two regression tests at the same anchor (#243's two for
the global-namespace window, this branch's two for the unread-inbox line);
all four are kept. dist/skills-manifest.json: a generated artifact,
regenerated by `npm run build` rather than hand-merged.

[Verified-By: npm run typecheck -> exit=0; npm run lint -> exit=0;
 node scripts/run-tests-isolated.mjs on the four test files both branches
 touched -> exit=0, Tests 63 passed (63); eg replay 11/11, eg check PASS]
@kevintseng
kevintseng merged commit 4298b6b into main Aug 29, 2026
13 checks passed
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