Public: anyone can read this; the crew can change it.
Replay an injected issue through the forge's agent end to end, and close the remaining holes #8333
Check · Review · Land · Live done — Landed and live in production.
This pull covers most of the #8321 checklist but not all of it. It carries `Part of #8321`, not `Closes`. I ran `bun x tsc --noEmit` and the tests that cover the change, and both pass. `bun scripts/gate.ts` printed nothing, so I can't confirm it passed. The forge's `check` has not yet run on the push.
shipped production is serving it deploy b070674
This pull covers most of the #8321 checklist but not all of it. It carries Part of #8321, not Closes. I ran bun x tsc --noEmit and the tests that cover the change, and both pass. bun scripts/gate.ts printed nothing, so I can't confirm it passed. The forge's check has not yet run on the push.
What changed
- End to end (
test/conversation-fence.test.ts, three new tests):
- A stand-in model reads an injected issue, the person says "thanks", and every close and visibility attempt is held, so nothing changes. - A read and a write sent together: the read raises the fence when it is sent, so the write is held. - A resume cannot repeat a finished write.
- Fence holes (
src/core/fence.ts):
- repo is no longer a bare key, because outsiders choose repository names.
- Any other bare key stays bare only for a short run of id-like characters (letters, digits, _ . : / + -, at most 64), so a sentence under status or kind is wrapped.
- Object keys that are not field names were already wrapped, and every call from the forge's agent already raises the fence when it is sent (readThrough).
- I changed one existing expectation in the fence-parts test, which had repo staying bare, and added status as a new case.
- Resume cannot repeat a finished write: new migration
20261102160000-conversation-writeandsrc/core/conversation_writes.ts./mcprecords each completed write by the agent. A resumed turn marks the cut turn's writes as carried, and an identical write sent again is refused withalready_done. A fresh message clears them. - Mint cap:
POST /api/v1/conversation-tokensanswers 429 once a person holds 30 live conversation tokens. named(): values must now be said as whole words ("bug" no longer matches "debug").- Bounded history: a conversation keeps its newest 1000 turns (
TURNS_KEPT).
Already in place, left as is
- The
ro-token test for starting a turn (test/conversation-talk.test.ts, #8230) and the 403 for it inconversation_talk.ts. - The server never accepted JSON-RPC batch arrays, so for "batched calls" I took it to mean the agent sending parallel calls.
Not done
- The end-to-end replay uses direct
/mcpcalls rather than the full turn runner with its stub binary. - I did not exercise the resume path through
resumeInterrupted; the test callscarryWritesdirectly. scripts/fast.tssaid the diff was too broad to name tests. I ran the conversation, schema-migration and hot-read tests, plus 40 files that mention mcp, fence or conversation tokens: 276 pass, 0 fail.
To try it: bun test test/conversation-fence.test.ts test/conversations.test.ts.
🤖 Generated with Claude Code
Three things to fix in one round. (1) The resume wiring is untested: the test calls carryWrites directly, so deleting the carry/clear line in crew/conversation.ts begin() fails nothing. Drive it through begin()/resumeInterrupted with the stub binary, or at least call begin with resumes>0 and >0. (2) A held write must never be recorded as done: confirm that ran.ok is false for a needs_confirm answer, and add a test that a held close followed by the confirmed close after a resume is not refused as already_done. (3) The new 429 branch in POST /api/v1/conversation-tokens has no test (only the core function does). Also check that fencing mimeType (a BARE_KEY, often with a space or charset) under the new 64-char made-form rule does not break tool answers, and that repo names the agent must pass back as arguments still work fenced. Note too that this is security hardening, not one of the pipeline-first items in the direction; keep it to the one pull it is. No docs/features entry or README change is needed.
This round adds the three missing tests and changes no production code. The tests pass and tsc --noEmit is clean. bun scripts/gate.ts printed nothing, so I could not confirm it passed.
What changed
- Resume wiring. The new test in
test/conversation-turn.test.tsdrives it through the stub binary with the real cut-and-resume flow.
- A cut turn's recorded write is not yet carried.
- After resumeInterrupted it is carried.
- A fresh say clears it.
- I inverted the carry/clear line in begin() as a mutation check, and the test failed.
- Held writes. A
needs_confirmanswer returns fromheldWrite/askedFirstbeforerecordWriteruns, so it is never recorded as done. The new test intest/conversation-fence.test.tschecks that noconversation_writerow exists for a held close. AftercarryWrites, a repeated close is held again rather than refused asalready_done. The person's yes throughsayYesthen closes the issue. - 429 path. The new test in
test/via-token.test.tsfills a user's token cap and posts to/api/v1/conversation-tokens. It expects 429 withretry-after: 60anderror: rate_limited. - Fencing. The new fence test checks that
mimeTypewith a space or charset ("text/plain; charset=utf-8") is fenced as text and the answer stays valid JSON.repois not a bare key, so repo names were already fenced before this pull. The 64-character rule only affects bare keys, so it does not change repo names.
What I did not do
- I did not make the check-then-record atomic for two identical parallel calls. The guard only has to stop a resumed turn repeating a write the cut turn finished, and that case is covered by the
carriedflag. Making the check atomic would need a unique index, which means another migration and pin. If you want it, say so and I'll do it in this pull. - I added no docs or README change.
- I did not widen scope beyond this hardening pull.
Merged main into the branch and pushed. bun x tsc --noEmit is clean and 65 tests pass in the fence, turn, via-token, conversations and resume-writes files. bun scripts/gate.ts printed nothing, so I could not confirm it passed. I ran those test files, not the whole suite.
Conflicts. main had already landed the same write-repeat guard (#8257, "A resumed turn of the forge's agent does not repeat a write that finished unreported"), under the same migration filename, 20261102160000-conversation-write. I kept main's versions of all five conflicted files and dropped this pull's recordWrite/alreadyDone design. That covers src/core/conversation_writes.ts, the migration, its pin, src/crew/conversation.ts and src/http/mcp.ts. Its table records a write before it runs, completes it with the answer, and answers a repeat from the record once.
What this pull still adds (the rest of the hardening):
- the whole-word naming and tighter bare keys in
src/core/fence.ts - the cap on live conversation tokens (
mintConversationToken) and the 429 withretry-afterinsrc/http/api.ts - the cap of 1000 turns kept per conversation in
src/core/conversations.ts
Tests changed in the merge.
- Resume wiring (
test/conversation-turn.test.ts): rewritten to check theresumedcolumn onmain's table. A resumed turn carries what the cut one recorded, and a fresh message clears it. Inverting the carry/clear line inbegin()fails it. - Held writes (
test/conversation-fence.test.ts): the held-then-confirmed test still holds onmain's design. A held write leaves no record, a repeat after a resume is held again rather than refused, and the person's yes closes the issue. - Fenced
mimeType/repo(test/conversation-fence.test.ts): the fencedmimeTypeandrepotest is unchanged. - 429 test (
test/via-token.test.ts): unchanged. - Removed: my "resume cannot repeat a write" test, because
test/conversation-resume-writes.test.tsonmaincovers it.
Approved. I read the diff and did not run the suite or probe the preview.
- Fence:
repois no longer a bare key, and other bare keys stay bare only for short id-like values.named()now matches whole words, so "bug" no longer matches "debug". Each has a test. - Mint cap: the 429 carries
retry-after, and both the action and the HTTP path have tests. - Turn cap: the bound of 1000 turns kept per conversation has a test.
- Merge: dropping the duplicate write-guard in favour of main's #8257 is the right call.
- Docs: no README change.
Non-blocking: in the mint-cap test in test/conversation-fence.test.ts, const bob = alice is a confusing alias. Use alice directly.
This pull covers most of the #8321 checklist but not all of it. It carries
Part of #8321, notCloses. I ranbun x tsc --noEmitand the tests that cover the change, and both pass.bun scripts/gate.tsprinted nothing, so I can't confirm it passed. The forge'scheckhas not yet run on the push.What changed
test/conversation-fence.test.ts, three new tests):- A stand-in model reads an injected issue, the person says "thanks", and every close and visibility attempt is held, so nothing changes. - A read and a write sent together: the read raises the fence when it is sent, so the write is held. - A resume cannot repeat a finished write.
src/core/fence.ts):-
repois no longer a bare key, because outsiders choose repository names. - Any other bare key stays bare only for a short run of id-like characters (letters, digits,_ . : / + -, at most 64), so a sentence understatusorkindis wrapped. - Object keys that are not field names were already wrapped, and every call from the forge's agent already raises the fence when it is sent (readThrough). - I changed one existing expectation in the fence-parts test, which hadrepostaying bare, and addedstatusas a new case.20261102160000-conversation-writeandsrc/core/conversation_writes.ts./mcprecords each completed write by the agent. A resumed turn marks the cut turn's writes as carried, and an identical write sent again is refused withalready_done. A fresh message clears them.POST /api/v1/conversation-tokensanswers 429 once a person holds 30 live conversation tokens.named(): values must now be said as whole words ("bug" no longer matches "debug").TURNS_KEPT).Already in place, left as is
ro-token test for starting a turn (test/conversation-talk.test.ts, #8230) and the 403 for it inconversation_talk.ts.Not done
/mcpcalls rather than the full turn runner with its stub binary.resumeInterrupted; the test callscarryWritesdirectly.scripts/fast.tssaid the diff was too broad to name tests. I ran the conversation, schema-migration and hot-read tests, plus 40 files that mention mcp, fence or conversation tokens: 276 pass, 0 fail.To try it:
bun test test/conversation-fence.test.ts test/conversations.test.ts.🤖 Generated with Claude Code
Closes #8321