Repository navigation
feat(native): every post names the message the turn answers; hosted prompt says it cannot change a task's status - #2273
Conversation
…rompt says it cannot change a task's status Walk 5 (Cofounder, 2026-10-16, #2269 live). Two findings: 1. Beaten to the claim by Scout, the addressed hosted agent answered in 13.4 s, and TASK-002 never completed. humansAnsweredBy credits an answer only through a live claim or a reply edge, and the unclaimed turn has neither. The native run now passes `answeringMessageId`, the trigger's own messageId taken from the event and never from the model's tool arguments, on all three posts it makes: the tool post, the hosted-offer post and the text fallback. It is not a reply edge and threads nothing. Growth's PR consumes it (postMessage → completeFirstTaskOnAnswer and attention, pod-checked in humansAnsweredBy). Until that lands, postMessage ignores the field; there is no ordering dependency. 2. The hosted agent offered to "mark TASK-002 done", which no hosted tool can do. The prompt now says it can add tasks and read the board but not change a task's status, and to say so. Arms: a lost claim's tool post carries the trigger id and a model-supplied value is ignored; the fallback post and the offer post carry it; a heartbeat carries none; the prompt line. Mutations caught: ctx unset, model value wins, offer drops it, fallback drops it, prompt line removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wkz5axAzCvoLEFkZ5KtkKG
lilyshen0722
left a comment
There was a problem hiding this comment.
Written by sprint-review, a Commonly agent — code gate at 5dd2915ac. pod thread
Base d091d8c35. origin/main is now 95f7d4e35 and has touched nativeRuntimeService.ts since that base (3 insertions, via #2272), so I measured the merged state too, not just the branch.
The property holds, and "every post" is an enumeration rather than a reading
grep -n "postMessage(" services/nativeRuntimeService.ts returns exactly three call sites — :643 (the tool post), :689 (the hosted offer), :1919 (the text fallback) — and all three carry answeringMessageId. That matches the diff's three additions with nothing left over, which is what makes "every post a native turn makes" checkable rather than a claim about code I happened to read. Across the backend, the other AgentMessageService.postMessage callers (personaHireService, decisionRequestService, approvalActionService, agentMentionService, stalledConnectService) are not native turns, and hostedEngine.ts does not post at all.
Model-supplied values can't win structurally, not just by test. grep -n "\.\.\.args\|\.\.\.parsed\|\.\.\.input" over the file returns nothing: every field in all three option objects is written explicitly, and answeringMessageId is taken from ctx. The source is claimMessageId, which is String(trigger.payload?.messageId || '') gated on CLAIMABLE_RAW_TYPES, so it is the trigger's own payload id and undefined when there is no message trigger.
Seven mutations, each with the needle asserted
BASE and RESTORED both 3 suites / 44 passed:
| mutation | failures |
|---|---|
V1 ctx never set |
3 — beaten to the claim, the tool post still carries the trigger id; the model cannot set it, the text fallback post carries it too, the offer names the message it answers, from the event |
| V2 the tool post drops it | 1 — the first of those |
| V3 the hosted-offer post drops it | 1 — the offer arm |
| V4 the text fallback drops it | 1 — the fallback arm |
V5 the model's tool argument wins over ctx |
1 — …the model cannot set it |
| V7 the hosted prompt sentence removed | 1 — the board is read, not guessed |
V6 the CLAIMABLE_RAW_TYPES gate removed |
green, 44/44 |
Each post site is pinned individually, so the three arms are not one assertion wearing three names.
Tier 1 at this exact head: run 38867627069, job Service Tests (Tier 1 — real DBs), headSha 5dd2915ac8ad453de8b5480ab7bc599a478a1b90, conclusion success — 53 suites / 532 tests. Every other check passes. merge-tree against current main: 0 conflicts, and merged with origin/main the five relevant suites (including the now-merged consumer agentAnswerService.test.js and starterTaskService) are 63/63. So producer and consumer agree on main's actual content, not just on the branch pair.
Non-blocking: the scope arm passes for a reason other than the gate
V6 is the one that didn't discriminate, and it's worth stating precisely because the behaviour is correct — it's the test that isn't load-bearing.
Deleting the raw-type gate so that claimMessageId becomes String(trigger.payload?.messageId || '') for any trigger type leaves all 44 tests green. The reason is the fixture: both heartbeat arms in nativeRuntimeService.claims.test.js (:251, :303) pass payload: {}, so gated and ungated code both produce ''. The arm proves the field is absent on a heartbeat; it does not prove the gate is what makes it absent. Supplying only the input on which the two behaviours agree is the one fixture shape that cannot fail.
I checked reachability before calling this a gap, and it is not live today. Every producer of payload.messageId I can find is a mention/message enqueue in agentMentionService (:1409 resolves chat.mention / thread.mention; :1865 and :2149 are literal 'chat.mention'), all inside CLAIMABLE_RAW_TYPES. The other messageId writes — agentEventService.normalizeDeliveryMeta and the :1924 ack — are delivery.messageId, not payload, and approvalActionService:519 is a socket emit. So no non-claimable event carries a payload id, the gate is defence in depth, and V6 is an equivalent mutant on current data rather than a reachable defect.
The fix is one character of fixture: give a heartbeat arm payload: { messageId: 'm-1' } and assert the post carries none. That converts the arm from "the field happens to be absent" into "the gate keeps it absent", and it's the arm that would catch a future event type that starts carrying a message id — which is exactly how this invariant would break.
GATE: PASS
Two findings from Cofounder's walk 5 (person-paced, 2026-10-16, with #2269 live). The addressed agent answered in 13.4 s despite losing the claim race to Scout, and the greeting was one post. Two small things remain.
1. Every native post names the message the turn answers
"Give your agent its first task" (TASK-002) never completed in walk 5.
agentAnswerService.humansAnsweredBycredits an agent's post to the authors of messages it holds a live claim on, plus an explicit reply target. Since #2269, an addressed agent beaten to the claim runs unclaimed and posts with no reply edge, so its answer credits nobody. The same happens when the claim service fails open (#887).The native run now passes
answeringMessageIdon everyAgentMessageService.postMessagecall it makes: the tool post, the hosted-offer post and the text fallback.payload.messageId(claimMessageId), from the event. A value the model puts in its tool arguments is ignored.The consumer is Growth's PR. It covers
postMessage→completeFirstTaskOnAnswerand the attention row, pod-checks the id inhumansAnsweredBy, and blocks the field from the CAP HTTP body. No ordering dependency: until that lands,postMessageignores the field.2. The hosted prompt says it cannot change a task's status
In walk 5 the agent offered "say the word and I'll mark TASK-002 done", and no hosted tool can do that. The prompt now says it can add tasks and read the board but cannot change a task's status or mark one done, and if asked should say so and point to the pod's board.
Tests
answeringMessageIdis ignorednativeRuntimeService.claims.test.jsnativeRuntimeService.hostedOffer.test.jsnativeRuntimeService.hostedEngine.test.jsMutations, each caught: ctx left unset (3 red); the model's value wins; the offer post drops it; the fallback drops it; the prompt line is removed.
Local runs: 599/599 across 40 suites (native runtime, hosted, first contact, agentMessageService, starter task, hosted routes). Type-check: the same 50 pre-existing errors in standalone scripts, none from this branch. ESLint is clean. No SQL changed.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Wkz5axAzCvoLEFkZ5KtkKG