Repository navigation
fix(drafts): name held drafts by length and order, without their words - #668
nissa-seru wants to merge 8 commits into
Conversation
The held-draft notice names each held draft by its first 50 characters, so it quotes the resident's own words back to it in user-role text. Since b2a21ec no notice about drafts has quoted a draft's words: they reach user-role text only through the drafts tool, when the resident asks, and the notices keep to the short, event-like shape that 54b8b53's ablation passed, a user-role bounce notice that named a length and quoted nothing. Keep the full-sentence notice and its reasons, and tell drafts apart without quoting: - each draft by its length ("42 chars", the drafts listing's own form); - several drafts "in the order you wrote them": one hold keeps its segments in the order written, and one notice covers one hold. A hybrid draft's note ("written for >>>…") isn't shown either: it repeats the resident's routing token, which needn't name a channel and has no length bound. The explicit-send because-clause now names the cause only: "you made an explicit send in this round or an earlier one this turn". A send holds back plain speech in its own round and the turn's later rounds (turnSilenced), so "in the same round as the plain speech" wasn't true of a later round's drafts; and new input clears the silence while a context-budget restart starts without it, so the clause promises nothing about how long it lasts. draftPreview is the listing's only again, and the changelog fragment describes the notice as it now reads. Tests: the two notice assertions take the new sentence and again check that the notice quotes none of the drafts' words. A new test drives holdProseDrafts with notice 'now' through one free draft and three drafts (each case with a hybrid note the notice must not show), one unconfirmed draft and one with no destination, and asserts each sentence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Automated review
Recommendation: merge. The review found nothing of medium or high severity.
Reviewed commit 4efd89d with claude-opus-5-5, by reading the code. It did not run the code or look at the checks.
Does this change belong in the project?
It fits the project. This PR fits. It is a narrow fix to the wording of an existing notice: it changes how the held-draft notice names drafts and gives its reason.
4 reasons with sources
It stays inside the code that already owns that notice and brings tests and an edited changelog fragment.
- It changes the wording of behavior the project already has and adds no new feature, dependency or interface. The only edits in framework.ts are to the strings inside the existing notice code. The draftPreview doc comment goes back to saying it is for the drafts tool's listing only. (src/framework.ts (DRAFT_REASON_BECAUSE and the held-draft notice builder))
- Its tests check the new sentence for each form a hold can take, and they would fail on the old code. The new test pins the exact sentence for each form and asserts that no draft's words appear. The PR reports the tests failing first on the prior framework.ts, which is the 'test that can fail on the unfixed code' standard. (CONTRIBUTING.md (What a PR should contain))
- It edits the pending changelog fragment instead of adding a second one for the same change. draft-notice-wording.changed.md is still pending, so revising its bullet to match the new wording keeps one fragment per change. (changelog.d/README.md; CONTRIBUTING.md (Changelog))
- The body follows the house shape: Problem, Changes and Tests, with a Not verified section and counted test results. It declares that it targets fix/draft-notice-wording, gives a count of 268 of 268 passing, says no live provider was tried, and carries the AI footer. (CONTRIBUTING.md (What a PR should contain; AI-assisted contributions))
Findings
No findings of medium or high severity.
Anarchid
left a comment
There was a problem hiding this comment.
Nightly dual review · round R1 at 4efd89d · reviewers: Codex gpt-5.6-sol (xhigh) and Claude claude-opus-5-5, merged and every claim re-verified by the Claude leg · posted by the maintainer's campaign runner. Verdict: APPROVE (the formal review event is left to the maintainer). Finding ids R1-n are stable; cite them in fix commits.
Reviewed at 4efd89d against fix/draft-notice-wording 35c2713
Verdict: APPROVE, with four minor or nit items
The change does what it says. On every path that reaches the held-draft notice, each clause of the new sentence is true. Order and length are measured the way the description claims. The tests fail red-first exactly as stated, and the stack merges cleanly into today's main and passes there. What's wrong is mostly what the PR leaves alone. After it, the same draft gets two different reasons: the notice gives the corrected one, while drafts list, drafts read and the catch-up notice still say "same round" (R1-1). And the one real-stream case the new clause exists for (a later round held by an earlier round's send) still has no assertion of its notice's text (R1-2). This is round 1, and there is no earlier review of ours on this PR.
One point about the stack, not counted as a finding: this PR reverses the main design choice of its base (#656 quotes opening words; this removes them), and #656 so far has only the automated review. The two should land as a unit. #656 merged to main on its own would ship the quoting notice this PR argues against.
Blockers
None.
Major
None.
Minor
R1-1. After this PR, the same draft has two reasons, and one of them appears in a user-role notice. (Found by both reviewers.)
- Evidence: the held notice now says
you made an explicit send in this round or an earlier one this turn(src/framework.ts:14404). Three other places still say "same round":DRAFT_REASON_SHORT['explicit-send']=explicit send in the same round(src/framework.ts:14393), rendered bydrafts list(:14467) and by the catch-up notice (:14562), which is itself a user-role notice;DRAFT_REASON_TEXT['explicit-send']=an explicit send in the same round holds plain speech back(:14387), rendered bydrafts read(:14747).
- Failure scenario: the three-segment test (
test/prose-drafts.test.ts:405-414) builds exactly this case. Round 1 callschannel_publish, round 2 calls onlychannel_list, and round 2's prose is held asexplicit-sendbecauseturnSilencedis still set (src/framework.ts:17801-17805,:17844-17850). Round 2's notice now gives the right cause. Listing or reading that same draft, or a later catch-up that names it, gives the wrong one. - Context: the description defers this to #666 and cites the cost of changing the tool description. That cost applies to the tool description only. The two maps above are not in any tool definition, so changing them invalidates no cache, as #666 itself notes.
- Fix: change the two maps here, for example SHORT
explicit send this turnand TEXTan explicit send in its round or earlier in the turn holds plain speech back. Leave only the tool description and the doc comments to #666. Otherwise, make sure #666 lands with this stack. This isn't blocking, since it is acknowledged and tracked.
R1-2. Nothing pins the later-round case on a real path, and "each form a hold can take today" overstates the new test. (Sol; re-verified.)
- Evidence:
- The new test calls the private
holdProseDraftsdirectly and supplies the reason itself (test/prose-drafts.test.ts:1330-1342). That makes it good coverage of the sentence builder. It doesn't cover which reason a real round gets. - The only real-stream later-round hold is in the three-segment test. That test checks only that some
prose-draftsnotice exists (:420) and that the first draft's id was heard (:425). Round 2's notice is never read, and round 2 is the case the clause rewrite is for. - A plural hold where every draft is unconfirmed isn't pinned anywhere. That form has no "delivers them unchanged" clause, and the resend part starts with risk text (
src/framework.ts:14417-14424,:14434-14436). It is reachable: two>>>#room … {{unsent}} …envelopes in one silenced hybrid round each inherit the risk of an unconfirmed bounce (unsentWords,:16015-16035), the same route the hybrid test attest/prose-drafts.test.ts:2207takes for one copy.
- The new test calls the private
- Failure scenario: if a later round's hold regressed to the wrong reason, or the all-risky plural sentence came out malformed, every test would stay green.
- Fix:
- In the three-segment test, find the notice whose
draftIdsincludeids[1]and assert that it contains the new clause. - Optionally, add an all-risky plural case to the builder test.
- In the description, call the new test builder coverage.
- In the three-segment test, find the notice whose
R1-3 (nit). "Quotes none of a draft's words" holds for draft.text, but not for whatever a connector says. (Sol, which rated it major; downgraded, see the method note.)
- Evidence: the new comment (
src/framework.ts:14425-14428) and the changelog ("Like every other notice, it quotes none of a draft's words") state an absolute. The builder never insertsdraft.text. For an unconfirmed draft, though,riskTextinserts the connector's error message unchanged (src/framework.ts:14493,:14502). That message isPublishOutcome.reason, which is "for a connector error, its message, bounded" (src/mcpl/channel-registry.ts:610-612), and a server's error answer "keeps its words as they came" (:641-649). - Failure scenario: a connector whose error answer echoes the submitted content would put draft words into this notice, and into the bounce notice (
src/framework.ts:16131). This path existed before the PR and depends on the connector. The one in-tree connector I checked doesn't echo content. - Fix: narrow the wording, for example "the notice itself quotes none of a draft's words". The underlying path isn't this PR's to fix.
R1-4 (nit). The changelog's first line claims more than the notice now does.
- Evidence:
changelog.d/draft-notice-wording.changed.md:1says "now says which message it means". The released notice already named each draft by id. Compared with it, the notice now adds a full sentence, a length, and a cause. "Says which message it means" was true of #656's opening words, and it overstates a character count. - Fix: for example, "now says in a full sentence which drafts it holds, how long each is, and why".
Inherited from #656, not counted:
DRAFT_REASON_BECAUSE.bouncedis unreachable from the notice, because everybouncedhold passesnotice: 'later'(src/framework.ts:15640,:16095).- The
manyarm ofno-destinationis unreachable too, because eachno-destinationhold is a single envelope (:15821,:15826). #666 item 2 covers part of this.
Verified fine
- Every clause is true on every path that reaches the notice.
- The notice is written only for
notice: 'now'. That comes from the live round path when the stream presents injections (src/framework.ts:17764), and from the test-onlydeliverProse. explicit-sendholds come only fromholdSilencedProsewith causesend. Aprivatecause or a private think keeps the prose private instead (:14242-14255).turnSilencedis set by a round's send (:17801-17805) and cleared only by new conversational input (adoptInjectedRound,:17540-17548). After a refusedskip_replyit is put back (:17535-17538).- A budget restart starts from its trigger (
:17367-17371). A tool-result-guard retry carries the silence inside the same logical turn (:18011-18020). - So "in this round or an earlier one this turn" holds everywhere, and the new comment at
:14400-14403is accurate.
- The notice is written only for
- Order.
ProseDrafts.holdreturns drafts in input order and stops at a prefix if a write fails (src/prose-drafts.ts:358-392). Each round makes oneholdProseDraftscall, so one notice covers one hold. The hybrid parts are built in envelope order (src/framework.ts:14274-14317). - Length.
d.text.lengthis the listing's own measure (src/framework.ts:14467): UTF-16 units of the text as stored. draftPreview. Its only consumer isdraftLine(:14468), so the new doc is true.- The description's history claims.
- No other notice inserts
draft.text: not the receipt (:15031-15036), the catch-up (:14561-14568), the{{unsent}}refusal (:15651) or the bounce notice (:16131). R1-3 is the one caveat. - The no-words assertions aren't vacuous. Each uses a distinctive multi-word phrase under the former 50-character preview limit, so a return to #656's quoting fails them.
- Runs.
- Against today's
main(0aa9f52, 588 commits past the fork point).- The notice code there is byte-identical to the fork point.
git merge-treeof this head intomainis clean.- On that trial merge, with context-manager 0.22.0 and membrane 0.7.1, the three files pass 293 of 293.
Method: two independent passes, merged after re-verification.
- Sol pass. Codex
gpt-5.6-solatxhigh, in a read-only sandbox, ran for 406 s: 67 shell commands, about 6.2M input tokens (5.75M cached) and 28k output tokens (17k reasoning). It read the head, base andmaintrees from local refs, typechecked the code, and rangit apply --checkof the stack againstmain. Its sandbox couldn't run the tests (read-only temp directory) and had no network. - Claude pass. The forest pass, the test runs (at head, red-first, and on a trial merge into
main), CI state, the cited commits' messages and code, and the connector check for R1-3. - Who caught what.
- R1-1 was found independently by both.
- R1-2 is Sol's.
- R1-3 is Sol's. It was downgraded from major to nit: the path existed before the PR, its trigger depends on the connector, and the PR's fault is only the wording.
- R1-4 is Claude's.
- Sol's fourth finding was rejected: it said the description's "since b2a21ec no notice … has quoted" contradicts #656, but the description opens by saying #656 quotes, so the claim reads as being about
main.
…(R1-2) The new clause, "you made an explicit send in this round or an earlier one this turn", exists for a draft held in a round that sent nothing, because a send in an earlier round of the turn silenced it. The test added in 4efd89d drives holdProseDrafts directly and supplies the reason itself, so it covers the sentence the notice builds, not which reason a real round gets. The three-segment test holds exactly that case on the live round path: round 1 calls channel_publish, round 2 calls only channel_list, and round 2's prose is held as explicit-send. It checked only that some held-draft notice was stored and that the first draft was heard mid-turn. Find the stored notice whose draftIds include round 2's draft and assert that it names that draft and gives the earlier send as the cause. A later round held for any other reason, or a notice that went back to "the same round", now fails this test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… (R1-1) After 4efd89d the held-draft notice says the send was "in this round or an earlier one this turn", while the drafts tool's listing, its read action and the catch-up notice still said "the same round". A send silences plain speech in its own round and the turn's later rounds, until new conversational input arrives (adoptInjectedRound clears turnSilenced) or a budget restart starts a new stream without it, so a draft held in a later round was listed and read with a cause it didn't have. This pull request's description left these to anima-research#666, citing the cost of changing the tool description. That cost is the description's alone: DRAFT_REASON_SHORT and DRAFT_REASON_TEXT feed only drafts list, drafts read and the catch-up notice, none of them a tool definition, so changing them invalidates no prompt cache. - DRAFT_REASON_SHORT: "explicit send in its turn". The listing and the catch-up notice can be read turns later, so it says "its turn", not "this turn". - DRAFT_REASON_TEXT: "an explicit send in its round or an earlier one of its turn held it back", the read action's cause for that draft. - prose-drafts.ts's module header and DraftReason's doc now describe the same rule. The drafts tool's description is unchanged and still says "the same round": changing it rewrites every resident's prompt cache, so it stays with anima-research#666. Tests: the catch-up assertions take the new short cause, and the three-segment test now checks that round 2's draft, held by round 1's send, gives the turn's send as its cause in drafts list and drafts read. The changelog fragment drops "The drafts tool's listing keeps its wording", which this makes untrue, and says what changed there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… first line (R1-3, R1-4)
R1-3: the comment beside the notice and the changelog said, absolutely,
that the notice quotes none of a draft's words, the changelog "like
every other notice". The builder never inserts a draft's text. For an
unconfirmed draft, though, riskText inserts the connector's error
message (PublishOutcome.reason, "for a connector error, its message,
bounded"), and the bounce notice does the same, so a connector whose
error echoes what it was sent would put draft words in both. That path
is older than this pull request and depends on the connector. Say what
holds: the notice itself quotes none of a draft's words. The comment
names the riskText path, and the changelog drops "like every other
notice". noticeUnnoticedDrafts' doc said the same ("Like every notice,
it quotes none of their words"); the catch-up notice gives each draft's
state and short cause, never a connector's message, so it keeps "It
quotes none of their words" and drops the comparison.
R1-4: the changelog's first line said the notice "now says which message
it means". The released notice already named each draft by id, and gave
a cause, "an explicit send in the same round holds plain speech back".
What changes is that it says so in one full sentence, each draft's
length is beside its id, and the cause covers a send in an earlier
round. Say that: the first bullet says the notice "now says, in one full
sentence, which drafts it holds, how long each is, and why", and the
second, which the R1-1 commit added, now says the notice too had said
"the same round".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Anarchid
left a comment
There was a problem hiding this comment.
Nightly dual review · round R2 at 88e3c8d · reviewers: Codex gpt-5.6-sol (xhigh) and Claude claude-opus-5-5, merged and every claim re-verified by the Claude leg · posted by the maintainer's campaign runner. Verdict: APPROVE (the formal review event is left to the maintainer). Finding ids R2-n are stable; cite them in fix commits.
Reviewed at 88e3c8d against fix/draft-notice-wording 35c2713
Verdict: APPROVE. The approval holds, with one minor and two nits
The three commits since R1 fix all four R1 findings, and they fix them the way R1 asked. On every native-tool path, the new causes are true wherever they are shown: the notice, drafts list, drafts read and the catch-up notice. The one remaining gap is outside what the PR set out to cover. In XML tool mode the whole turn's prose is held at the turn's end, so read can tell a draft that a send in an earlier round held it, when the send came in a later one (R2-1). That text was already wrong at the base, and so was the wording R1-1 suggested. Two smaller items concern wording: what ends the silence (R2-2), and "1 chars" (R2-3).
On the desk's facts: R1's verdict was APPROVE at 4efd89d1, posted as a comment-type review. The author has since pushed 7b3cf723, 7e22c5cc and 88e3c8d1. The base hasn't moved (35c2713d). R1's note about the stack still holds: #656 still has only the automated review, and the two PRs should land as a unit.
R1 findings
- R1-1: fixed in
7e22c5cc.DRAFT_REASON_SHORT['explicit-send']is nowexplicit send in its turn, andDRAFT_REASON_TEXT['explicit-send']is nowan explicit send in its round or an earlier one of its turn held it back(src/framework.ts:14386-14396).- Both maps are rendered only by the listing (
:14469), the catch-up notice (:14564) andread(:14749). - The tool description (
:8726-8727) still says "same round". The PR says so and leaves it to #666 (see R2-1 for what that change should also cover).
- R1-2: fixed in
7b3cf723.- The three-segment test now finds round 2's notice by its
draftIdsand asserts the earlier-send clause (test/prose-drafts.test.ts:426-435). It then checks that the listing andreadgive that draft the same cause (:449-453). - With only #656's because-clause put back,
prose-draftsfails 4 tests, the three-segment test among them, as the description says. - The description now calls the new test builder coverage.
- The three-segment test now finds round 2's notice by its
- R1-3: fixed in
88e3c8d1.- The comment at
src/framework.ts:14425-14430names theriskTextpath, and the changelog says "the notice itself". noticeUnnoticedDraftskeeps "It quotes none of their words", and that is true: the catch-up notice rendersdraftStateText(:14474-14488), neverriskTextor an outcome's reason.
- The comment at
- R1-4: fixed in
88e3c8d1. The changelog's first line now reads "now says, in one full sentence, which drafts it holds, how long each is, and why".
Blockers
None.
Major
None.
Minor
R2-1. In XML tool mode, drafts read gives a draft held from a round before the send a cause it didn't have. (Claude only. Sol's pass judged this path fine.)
- Evidence:
- Without
roundContent, no round is routed live (src/framework.ts:17751-17752,:17771). Membrane's XML yielding path supplies none; only its native path does. - The turn's prose is then held at its end.
finalRunstakes the whole turn's text,withoutRoundsProse(response.content, …)(:18169-18172), andfallbackCausereads every call of the turn (:18437-18439). - So one send anywhere in the turn holds every round's prose as
explicit-send(:18483-18489for locus,:18457-18459for hybrid), including the rounds before the send.
- Without
- Run (a copy of the
prose-draftsharness withxml: true, at this head):- Round 1 is
Looking it up first.withchannel_list. Round 2 isBeside the send.withchannel_publish. Then comesAfter the send. - All three are held as
explicit-send.drafts readon round 1's draft gives(an explicit send in its round or an earlier one of its turn held it back). The send was in the next round. - The listing's
explicit send in its turnis true. - In native mode the same turn posts round 1's prose live and holds only rounds 2 and 3, so the native path is right.
- Round 1 is
- The same scope in prose:
- the module header (
src/prose-drafts.ts:5-8, "in its own round and the turn's later rounds"); DraftReason's doc (src/prose-drafts.ts:58);- the changelog's second bullet ("a send also holds back plain speech in the turn's later rounds").
- the module header (
- Context:
- The base's text, "an explicit send in the same round holds plain speech back", was already wrong in this case, and so was the wording R1-1 suggested. This is a gap the R1 review shares, not a regression.
- The held-draft notice isn't affected: XML-mode holds pass
notice: 'later'and are named only in the receipt. - The PR's stated aim for this text is one true cause for one draft wherever it is shown. In XML mode the listing and
readnow disagree about scope.
- Fix:
- Give
readthe listing's scope, for example'explicit-send': 'an explicit send in its turn held it back'. That is true in both modes. - In the header,
DraftReason's doc and the changelog, add a clause such as "(in XML tool mode, which routes no round live, every round of the turn)". - Optionally, pin it: add a round before the send to the XML test at
test/prose-drafts.test.ts:463and assertread's cause. - When #666 changes the tool description, the same scope applies there.
- Give
R2-2 (nit). The header and changelog name two ends to the silence, and a framework retry is a third. (Sol; reproduced by Claude.)
- Evidence:
- The module header (
src/prose-drafts.ts:5-7) says "until new conversational input arrives or a budget restart starts a new stream". The changelog (changelog.d/draft-notice-wording.changed.md:9-11) says "until new input arrives or a budget restart starts a new stream". - Each stream takes its silence only from
suppressProseor a tool-result-guard retry (src/framework.ts:17369-17373). - The error policy's retry (
:18670) is "a new physical stream in the same logical turn" (:16638-16641,:16690), and it starts unsilenced.
- The module header (
- Run (same harness, native mode): round 1 calls
channel_publish, then the stream yields anerrorevent. The default policy retries after 1 s, and the retry'sDone — I sent it to general.is posted to#room. - Context: the behaviour is older than this PR and isn't its to fix. The PR's part is wording that lists the silence's ends. The notice's clause, which deliberately promises no lifetime, is right.
- Fix: either drop the "until …" clause from the header and the changelog, as the notice does, or make it complete. For example: "until new conversational input arrives or a new stream starts without it (a budget restart, or a retry after a failed inference; a tool-result-guard retry carries it)". The older scope comment at
src/framework.ts:234-237has the same gap, but this PR doesn't touch it.
R2-3 (nit). A one-character draft is named as "1 chars". (Sol.)
- Evidence: the notice formats
${d.text.length} chars(src/framework.ts:14431,:14434). Segments only need to be non-empty once trimmed, so a draft of?orkgivesyour plain-speech message (1 chars). - Context: the listing has the same form (
:14469), and the PR chose to match it, so leaving both as they are is defensible. - Fix, if wanted: one small helper (
1 char/N chars) used by both, so the two stay alike.
Verified fine
- The new causes on native paths.
explicit-sendholds come only fromholdSilencedProse(src/framework.ts:14257,:14317).- The silence is set by a round's send (
:17803-17806) and cleared by conversational input (adoptInjectedRound,:17542-17550). - A tool-result-guard retry carries it inside the logical turn (
:18013-18022,:17369-17373). - Trailing prose follows the turn's last round.
- The text-only path's hold is reachable only from a guard retry or a
suppressProsetrigger (:18367-18373). - So "in its round or an earlier one of its turn" and "in this round or an earlier one this turn" hold everywhere outside XML mode.
- Each splice reads correctly, from real output in the runs:
- listing:
d-… · held (not sent) · held … · explicit send in its turn · 16 chars · "Beside the send." read:d-… — held; held … (an explicit send in its round or an earlier one of its turn held it back)- catch-up:
d-… (held (not sent); explicit send in its turn), pinned attest/prose-drafts.test.ts:905.
- listing:
- "In no tool definition, so no prompt cache is rewritten" is true. The two maps reach only the three render sites above.
- The changelog's account of the released text is true. On
main, the notice rendersDRAFT_REASON_TEXT("held as draft … — an explicit send in the same round holds plain speech back"), so the notice, listing,readand catch-up did all say "the same round". - The new assertions aren't vacuous.
- With
src/as at4efd89d1and this head's tests,prose-draftsfails exactly 4: the three-segment test and the three catch-up tests. - With only #656's because-clause put back, it fails the three-segment test, plus the builder test and two others that pin the clause.
- Both match the description.
- With
- Runs at this head, on Node 22.22.1 with context-manager 0.14.1 and membrane 0.5.87:
tscis clean, andprose-drafts,explicit-prose-routingandpresent-while-actingpass 268 of 268. CI is green on the full matrix at88e3c8d1. - Against today's
main(0a4a2c69, 0.27.0, 17 commits past R1's0aa9f525):- Those 17 commits leave the draft-reason maps, the notice builder and the silence's set and clear points alone. A new read of
turnSilencedforautoContinuedoesn't change them. git merge-treeof this head intomainis clean.- On that trial merge, with context-manager 0.22.1 and membrane 0.7.2, the three files pass 293 of 293.
- Those 17 commits leave the draft-reason maps, the notice builder and the silence's set and clear points alone. A new read of
- #666's first item lists five texts. This PR fixes four of them, so only the tool description is left there.
Method: two independent passes, merged after re-verification.
- Sol pass. Codex
gpt-5.6-solatxhigh, in a read-only sandbox, ran for 525 s: 62 shell commands, about 4.45M input tokens (4.20M cached) and 29k output tokens (20k reasoning). It read the head, the R1 head, the base andmainfrom local refs, was given this PR's description, the R1 review and #666, and ran the typecheck. Its sandbox couldn't run the tests or write a merge tree (read-only temp directory), and it had no network. - Claude pass. Freshness (PR, base,
main, #656 and CI state), the forest pass, every test run (at this head, red-first at4efd89d1, the clause-only mutation, and a trial merge intomain), membrane's XML yielding path on itsmain, and two probe runs on a copy of theprose-draftsharness. - Who caught what.
- R2-1 is Claude's alone. Sol's pass recorded the explicit-send cause as correct on every reachable path, and that item was rejected on the XML-mode run.
- R2-2 is Sol's. Claude reproduced it with a stream
errorevent and downgraded it from minor to nit: the behaviour is older than this PR, and the PR's fault is only the wording. - R2-3 is Sol's.
- All four R1 dispositions were reached independently by both, and agree.
… (R2-1) In XML tool mode no round is routed live, so the turn's whole prose is held at its end, and one explicit send anywhere in the turn holds every round's prose as explicit-send, a round before the send included. read then said "an explicit send in its round or an earlier one of its turn held it back", which isn't true of that draft. A new XML-mode test holds three rounds' prose, a round with channel_list, a round with channel_publish and the text after it, and asserts that read gives the first round's draft "an explicit send in its turn held it back". The three-segment test's read assertion takes the same wording. Both fail at 88e3c8d on that assertion, after the three drafts are held as explicit-send. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s turn (R2-1) DRAFT_REASON_TEXT['explicit-send'] said "an explicit send in its round or an earlier one of its turn held it back". In native tool mode that's true. In XML tool mode no round is routed live (no roundContent), the turn's whole prose is held at its end (finalRuns), and fallbackCause reads every call of the turn, so a send in a later round holds an earlier round's prose too, and read gave that draft a cause it didn't have. read now says "an explicit send in its turn held it back", the scope the listing's "explicit send in its turn" already had. It's true in both modes. The held-draft notice keeps "in this round or an earlier one this turn": XML-mode holds pass notice 'later', so no notice names them. The module header, DraftReason's doc and the changelog's second bullet add the XML-mode scope: in XML tool mode, which routes no round live, a send can hold the prose of any round of the turn. The drafts tool's description still says "the same round" (anima-research#666). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The module header and the changelog said a send holds back plain speech "until new conversational input arrives or a budget restart starts a new stream". A third end isn't on that list: the error policy's retry is a new physical stream in the same logical turn, and it starts unsilenced. Each stream takes its silence only from suppressProse or a tool-result-guard retry. Drop the clause, as the held-draft notice's own clause promises no lifetime. Without it, "the turn's later rounds" would claim every later round, so both now say a send "can" suppress them, and in XML tool mode "any round of the turn", since a refused wake's round there stays private rather than held. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… added Two comments this pull request added beside the held-draft notice explain the code rather than a contract or an outside constraint, so they go, and their reasons are kept here: - beside DRAFT_REASON_BECAUSE: a send silences plain speech in its own round and can silence it in the turn's later rounds (turnSilenced), so a later round's drafts can be held by a send made in an earlier one, and the clause names only the cause; - in draftsHeldNotice: each draft is told apart by its length and, among several, by its place, since one hold keeps its segments in the order written. The notice itself inserts none of a draft's text, though riskText carries a connector's error message, bounded, so a connector whose error echoes what it was sent could put a draft's words there. That last point is R1-3's, and the changelog keeps "the notice itself quotes none of a draft's words". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Anarchid
left a comment
There was a problem hiding this comment.
Nightly dual review · round R3 at 8d6ca71 · reviewers: Codex gpt-5.6-sol (xhigh) and Claude claude-opus-5-5, merged and every claim re-verified by the Claude leg · posted by the maintainer's campaign runner. Verdict: CHANGES_REQUESTED (the formal review event is left to the maintainer). Finding ids R3-n are stable; cite them in fix commits.
Reviewed at 8d6ca71 against fix/draft-notice-wording 35c2713
Nightly dual review · round R3 at 8d6ca71f · reviewers: Codex gpt-5.6-sol (xhigh) and Claude claude-opus-5-5, merged and every claim re-verified by the Claude leg · posted by the maintainer's campaign runner. Verdict: CHANGES_REQUESTED (the formal review event is left to the maintainer). Finding ids R3-n are stable; cite them in fix commits.
Verdict: CHANGES_REQUESTED. The delta is right, but main has moved under it
The four commits since R2 fix R2-1 and R2-2 the way R2 asked, and nothing in them is wrong against the base. What has changed is main. Since R2 was posted, main has merged #716 and #824, and the stack no longer merges into it: #656 shows as conflicting, and this head conflicts in all three files it changes. #824 already ships this PR's listing, read and catch-up wording. It also changed the drafts tool description, and it now computes the held-draft notice's cause for each place a send may have reached. So on main, the explicit-send clause these last two rounds refined is never shown. What this PR still adds on main is the length-and-order naming in place of #656's quoted words, and the XML-mode scope in the docs. The one change requested is a rebase that combines the two (R3-1). That rebase changes the notice and four of this PR's assertions, so it needs another look.
On the desk's facts:
- "R2 approved at
88e3c8d" is right. R2's verdict was APPROVE, posted as a comment-type review, like R1's. - "+10/−16 code since" is right for
src/:framework.ts+1/−11 andprose-drafts.ts+9/−5. With the test (+32/−1) and the changelog fragment (+6/−6), the delta is +48/−23. - R2's "
git merge-treeof this head intomainis clean" was true at0a4a2c69and no longer is. R2 was posted at 04:52 UTC on Oct 26. #716 merged at 07:05 and #824 at 16:49. - Does the delta keep the approval? On its own base, yes. For landing on
main, no; see R3-1.
R2 findings
- R2-1: fixed in
b7bed423, and tested ind6339cfa.DRAFT_REASON_TEXT['explicit-send']is nowan explicit send in its turn held it back(src/framework.ts:14387). This is the listing's scope, and it is true in both tool modes.- The module header (
src/prose-drafts.ts:4-9),DraftReason's doc (:58-62) and the changelog's second bullet add the XML-mode case: "in XML tool mode, which routes no round live, in any round of the turn". - The new test, "XML tool mode: a round before the send is held too…" (
test/prose-drafts.test.ts:2249), reaches the fallback path.XmlModeStreamstripsroundContent(:248-267). The test asserts all three texts and theirexplicit-sendreason before it reads round 1's draft. - Red-first: with
src/as at88e3c8d1and this head's tests,prose-draftspasses 87 of 89. The two failures are the new XML test and the three-segment test, each onread's cause, as the description says.
- R2-2: fixed in
41e388b2. The header and the changelog no longer list ends to the silence. The older scope comment atsrc/framework.ts:234-237keeps its gap, and this PR doesn't touch it, as R2 said. - R2-3: not fixed, by choice. "1 chars" matches the listing's form. That is defensible, and we accept it.
R1's four findings stay fixed. 8d6ca71f removes the comment that R1-3's fix had added beside the notice. That leaves no false claim: the changelog keeps "the notice itself quotes none of a draft's words", and noticeUnnoticedDrafts' "It quotes none of their words" is true, because the catch-up notice renders state and the short reason, never riskText.
Blockers
None.
Major
R3-1. The stack no longer merges into main, and on main #824 computes the explicit-send cause that this PR words. (Found independently by both passes. Sol attributed the per-place rework to #716; it is #824's.)
-
The conflicts. Merging this head into
main(913afe34) conflicts in:src/framework.ts, three hunks:DRAFT_REASON_TEXT['no-destination'];DRAFT_REASON_BECAUSEagainstdraftsHeldNotice's new signature; and the notice'sreturn;src/prose-drafts.ts: the module header andDraftReason's doc;test/prose-drafts.test.ts: the three-segment test and the hybrid notice test.
mainas it stood just before #716's merge takes this head cleanly. #716 brings the one-lineno-destinationconflict that this PR's "The stack" section predicted. #824 brings the rest. -
What
maindoes now.- Every explicit-send hold on
mainpasses a computedbecause:holdSilencedProsealways does (src/framework.ts:16588-16592onmain), and so doesrouteSilencedHybrid(:16692-16696). sendHoldText(:16707-16729) names the send, where it posted or may have posted, and where the speech was going.main'sDRAFT_REASON_TEXTandDRAFT_REASON_SHORTalready carry this PR'sexplicit-sendstrings, character for character (:16794-16804).- The
draftstool description no longer says "same round" (:10212-10213). - #824's fragment,
changelog.d/send-hold-by-place.changed.md, already describes the listing,readand catch-up changes.
- Every explicit-send hold on
-
Run. We made a trial merge into
main, with context-manager 0.22.1 and membrane 0.7.2.- The resolution: take
main's signature,targetanddestination; keep #656's and this PR's frame; and renderbecause ?? DRAFT_REASON_BECAUSE[reason](many), with #716'sno-destinationwording. - It builds, and
prose-drafts,explicit-prose-routingandpresent-while-actingpass 371 of 375. - The 4 failures are this PR's own assertions on cause clauses: round 2's notice in the three-segment test; "drafts belong to their resident"; the builder test's
no-destinationcase; and the hybrid notice test. - The frame works with
main's causes. Round 2's notice in the three-segment test now reads:your plain-speech message (39 chars) was not sent and is instead held as draft d-… because the framework can't tell where your send_message earlier this turn posted (its answer named no channel the framework reads), and this was going to #room (Guild One) (…); your speech stays held until a new message reaches you or this turn ends.
- The resolution: take
-
What the rebase should do:
- Notice. Keep the frame: one full sentence, length and order, no words. Take
main'sbecause,targetanddestination. Onmain, no production path then reachesDRAFT_REASON_BECAUSE['explicit-send']. Either requirebecausefor explicit-send holds, or keep the entry as a fallback and say that it is one. Have the builder test pass abecausefor its explicit-send cases, as the live paths do. no-destination. Use #716's wording in bothDRAFT_REASON_TEXTandDRAFT_REASON_BECAUSE. #716 dropped "after a routing envelope failed" because a no-destination draft is now often a line written beside adraftscall (ffb00ac7). Onmain, a no-destination hold outside adraftsround still takes its round's notice setting, which is the live notice on a native round (:19044). #656'sBECAUSEtext would put the dropped claim back into it.- Docs. Merge
main's per-place header andDraftReasondoc with this PR's XML-mode clause.main's header (src/prose-drafts.ts:5-7) andDraftReasondoc (:57) still leave out the XML-mode scope, which is R2-1's doc gap again. #824's fragment does state it, and the XML fallback still treats the turn as one unit (trailingWhen = liveProseRouting ? 'end' : 'turn',src/framework.ts:22231). - Changelog. Drop the second bullet, since #824's fragment already says it. Re-quote the first bullet's sample with a cause
mainactually gives: "because you made an explicit send in this round or an earlier one this turn" no longer appears on a live path. - Tests. Update the four assertions above to the new cause. The three-segment test's point, that a later round's draft names an earlier round's send, still holds; it now meets
sendHoldText's "earlier this turn". - Description. Update "The stack" and "What this leaves". #716 has merged. #824 changed the tool description, so #666's first item is done on
main, and the prompt-cache reason for leaving that description alone no longer applies.
- Notice. Keep the frame: one full sentence, length and order, no words. Take
-
A note on
main's cause text. For a later-round hold,mainsays "your speech there stays held until a new message reaches you or this turn ends". R2-2 had this PR drop the same kind of promise, because a retry after a failed inference starts a new stream without the silence (mainsrc/framework.ts:21022-21027). On the trial merge, round 1 sends to#room, round 2's line is held with that promise, and then the stream errors. The retry'sDone — I sent it to the room.is posted to#roomin the same turn. That text ismain's, and this PR doesn't need to fix it. The rebased fragment and docs just shouldn't repeat it.
Minor
R3-2 (nit). The description says XML-mode holds "are named only in the receipt". (Sol.)
- Evidence:
holdProseDrafts' doc (src/framework.ts:14330-14331) says that after a crash, the next turn's catch-up names them. - Context: the catch-up notice gives the short reason,
explicit send in its turn, which is true. So no resident sees a wrong cause, and the description's argument holds with "no held-draft notice names them". Our R2 review used the same wording. - Fix: reword it when the description is refreshed for R3-1.
Verified fine
- The new causes, at the base, on every path.
explicit-sendholds come from native round routing, a tool-result-guard retry that carries the silence, the text-only path (reached only from a guard retry orsuppressProse), and the XML fallback, which reads every call of the turn.read's "in its turn" is true on all of them.- The notice's "in this round or an earlier one this turn" is shown only where
notice: 'now'is set. That happens on native live rounds that present injections (src/framework.ts:17756). The text-only and XML paths pass'later'(:18352,:18435).
8d6ca71f's two removed comments leave no stale or contradicting doc.DRAFT_REASON_BECAUSE's doc describes only its splice role.draftPreview's "for the drafts tool's listing only" has one caller.- The
riskTextcaveat (a connector whose error echoes what it was sent) is recorded in the commit message and the description, and the changelog's "the notice itself" is still accurate.
- The prompt-cache claim is true at the base. The three reason maps reach only the notice, the listing,
readand the catch-up notice. The tool definition (src/framework.ts:8726-8727) is unchanged by this PR. - Runs at this head, on Node 22.22.1 with context-manager 0.14.1 and membrane 0.5.87:
tscis clean, andprose-drafts,explicit-prose-routingandpresent-while-actingpass 269 of 269. CI is green on all 10 checks at8d6ca71f.
Method: two independent passes, merged after re-verification.
- Sol pass. Codex
gpt-5.6-solatxhigh, in a read-only sandbox, ran for 481 s: 80 shell commands, about 4.52M input tokens (4.33M cached) and 28k output tokens (15k reasoning). It read the head, the R2 head, the base andmainfrom local refs. It was given this PR's description, both earlier reviews and #666. It ran the typecheck and a read-onlygit merge-tree. Its sandbox couldn't run the tests, and it had no network. - Claude pass. Freshness (PR, base, #656,
main, CI state, and when #716 and #824 merged relative to R2). The run at this head; red-first withsrc/as at88e3c8d1; a trial merge intomainwith a resolved notice and the three files run on it; one probe on that merge, a stream error after a later-round hold; and reading #716's and #824's commits and changelog fragment. - Who caught what.
- R3-1 was found by both. Sol named #716 as the source of the per-place rework. Claude traced the conflicts to the two merges (#716 for the one line, #824 for the rest). Claude also ran the resolution and checked what
mainalready ships. - R3-2 is Sol's.
- Sol's third item, the inherited scope comment at
src/framework.ts:234and the tool description, was not adopted as a finding. Both are older than this PR, R2 already noted them as out of scope, and onmain#824 has rewritten the tool description. - The R2 dispositions were reached independently by both, and agree.
- R3-1 was found by both. Sol named #716 as the source of the per-place rework. Claude traced the conflicts to the two merges (#716 for the one line, #824 for the rest). Claude also ran the resolution and checked what
…ts that continue it After an explicit send, a locus or hybrid turn holds plain speech aimed where the send may have posted, and a later round's hold notice tells the agent "your speech there stays held until a new message reaches you or this turn ends". Only the tool-result guard's retry carried that hold into a new stream (guardRetryTurnSilenced). The other starts that continue the same logical turn began unsilenced, so the agent's next words posted where its send had already posted: - the error policy's retry after a stream's error event; - the retry after such a retry's own start fails; - the retry a provider hold keeps for the turn (TURN_RETRY), after a stream's error event, a stream's throw or a failed start; - a context-budget restart. Each is one turn: one turn id, one receipt, and the retry's request carries the notice that made the promise. Found by #668's third review, which ran the error-retry case on a trial merge; reproduced on main 7e4f7e2 for all four. The turn's silence and sends now ride on the request that continues the turn, as TURN_CARRY beside TURN_RETRY, and a start reads them only when its request continues the turn (turnCarryOf). One mechanism for all five continuations, the guard retry included, replaces the per-agent map: - driveStream registers what it would carry per stream (streamCarries); the budget restart reads it when it queues the continuation, with a refused skip_reply's own-round silence already restored. - A stream that fails builds its retry, and the retry a hold keeps, through retryOf, which first applies a message its last boundary injected (adoptInjectedRound), so the retry answers it. - beginAgentTurn passes its carry on to driveStream, and to every retry or hold it makes after a failed start. - The scheduler's merge takes the carry from whichever request in the batch continues the turn, since the request that leads the merge can be a wake. - withTurnCarry sets the carry it's given and deletes the symbol otherwise, so a retry built after a start dropped its carry doesn't revive the one its trigger held. What ends the hold is what the notice says, a new message the start reads, by the predicate a tool boundary already uses (isConversationalInjection), so a held retry and a live boundary agree: - noteArrival keeps each conversational row stored into the open turn outside a live boundary, with when it was heard (TurnRouting.heard): one stored while none of the agent's turns is alive, as during a provider hold; one stored in another resident's window, which this resident reads too; one flushed or assembled at a start; one a restart's boundary leaves to the turn's next start. - Each carry records where its stream's compile read through (read), taken just before the compile, after the start's flushes and context hooks. A start drops the carry if a row heard after that is still in the agent's store when it compiles, as remainingArrival weighs arrivals, so a message removed unread ends nothing. heard is optional on TurnRouting: tests and other harnesses that build a turn's routing by hand leave it unset, and noteArrival and heardSince take it as empty. - A message the failed stream's last boundary injected clears it through retryOf. A budget restart resumes the turn, so the mid-turn input signal survives into its stream and clears the carried silence when its first round is routed, as at any round. - A start that opens a new logical turn drops the carry. - A wake that brings no message, such as a silent heartbeat merged into a held retry, leaves the hold in place. The existing test "a budget restart after a refused wake starts from its trigger, as after any other round" asserted the restart after a send speaks unsilenced, the behaviour this change removes. It is turned round: the restart keeps the send's hold, the refused case like the plain one, and both hold the restart's line as an explicit-send draft, which also pins that a refused skip_reply's own-round silence is restored before the carry is taken. What this leaves: - holdRequest moves TURN_RETRY, not the carry, onto an equal request already held, so such a retry starts unsilenced, as on main. - A conversation fork's channel messages are written straight into its window (writeToFork) and aren't weighed, so a fork's held retry keeps the hold through them. Conversation routing is deprecated (agent-framework#235). - A start decides its carry, and takes its read, just before it compiles, but the compile first waits for any tool-result guard annotation in progress (afterAnnotations). In a host with several residents, a conversational row another resident's window gets during that wait is compiled under the carried hold, and a row heard before the wait and removed during it ends the hold though the compile doesn't read it. Tests: test/continuing-start-silence.test.ts, through the speech-route fixture's send_message. The four continuing starts keep the line held, in locus and hybrid, and so do a provider hold's retry after a throw and after a failed start, a retry after a refused skip_reply's round, a silent heartbeat merged into a held retry, an XML-mode retry that is a tool turn, a held retry that answers a new message and sends again, through its own retry, and a held retry after a message removed unread: all red with main's framework.ts. A new message read by a held retry, an error-policy retry or a budget restart is answered in the room, in XML tool mode too, and a held retry that read one and whose start then fails passes no hold on; overheard chatter stored during a hold or queued while the failed request ran clears the hold, and so does a message another resident's window gets while the failed stream runs or while the retry gathers its context; a retry after a send elsewhere speaks in the turn's own; a budget restart after a refused skip_reply's round with no send before it speaks; and a held retry that opens a new turn carries no hold into it, with or without a failed start after it. Those pass on main too and pin the clearing. The guard retry's existing tests pass unchanged. The text-only branch's comment now names a continuing start that carried the suppression beside a suppressProse trigger, and docs/tool-result-guard.md loses its "Explicit-send suppression across a budget restart" limit, which no longer holds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem
#656 names each held draft in the held-draft notice by its first 50 characters, so the notice quotes the resident's own words back to it in user-role text. Since b2a21ec, no notice about drafts has quoted a draft's words. A draft's words reach user-role text through the
draftstool, when the resident asks. #247's notices were made short and event-like after 54b8b53's ablation, in which a user-role bounce notice passed: it named the text's length, quoted none of it, and said how to resend it (Your text (N chars) was not delivered — …). A notice that quotes the model's own just-written output back to it is a shape that ablation didn't test.#656's explicit-send because-clause also says more than the code does: "there was an explicit send in the same round as the plain speech". A send holds back plain speech in its own round and can hold it back in the turn's later rounds (
turnSilenced). So a later round's drafts can be held by an earlier round's send, as in the three-segment test, whose rounds 2 and 3 are held asexplicit-send.Changes
Into
fix/draft-notice-wording. This keeps #656's full-sentence notice and its reasons, and tells drafts apart without quoting them:42 chars, thedraftslisting's own form.The explicit-send because-clause names the cause and nothing more. New input clears the silence (
adoptInjectedRound), and a context-budget restart starts without it, so the clause makes no promise about how long the silence lasts.draftPreview's doc goes back to "for the drafts tool's listing only", and the changelog fragment describes the notice as it now reads.One cause wherever a draft is shown. The
draftslisting, itsreadaction and the catch-up notice said "the same round" too, so after the notice changed, one draft had two causes. They now agree with the notice:draftslisting and the catch-up noticeexplicit send in the same roundexplicit send in its turndraftsread(an explicit send in the same round holds plain speech back)(an explicit send in its turn held it back)The listing and the catch-up notice can be read turns later, so they say "its turn", not "this turn".
readsays "its turn" too, not "its round or an earlier one": in XML tool mode no round is routed live, so the turn's whole prose is held at its end, and a send in a later round holds an earlier round's prose as well. The held-draft notice keeps "in this round or an earlier one this turn", since XML-mode holds are named only in the receipt. These texts are in no tool definition, so changing them rewrites no prompt cache.prose-drafts.ts's module header andDraftReason's doc now describe the same rule, XML mode included, and no longer list what ends the silence: a retry after a failed inference was an end the list left out (R2-2's run). Thedraftstool's own description still says "the same round": changing it would rewrite every resident's prompt cache, so it stays with #666.What the notice quotes. The notice itself quotes none of a draft's words. An unconfirmed draft's risk text carries the connector's error message, bounded, as it did before this PR. So a connector whose error echoes what it was sent could put a draft's words there.
noticeUnnoticedDrafts' doc keeps "It quotes none of their words" and no longer compares itself with every notice.What each part is for
readstill shows the words.holdProseDraftsdirectly with the reason supplied, pins the sentence for each form listed under Tests, and checks in each that no draft's words are in it. Two small helpers keep it short:holdholds and reads back the notice, andnoticefinds it by itsdraftIds.read.read's cause for a draft from a round before the send.read's and the catch-up notice's reasons give one draft one cause, at the cost of a few words in texts a resident reads only when it lists or reads its drafts, or in a catch-up notice.written for >>>…), since it repeats the resident's routing token, which needn't be a channel, and its length has no bound;Tests
prose-drafts: the two notice assertions take the new sentence and again check that the notice quotes none of the drafts' words. The catch-up assertions take the new short cause.The three-segment test now finds round 2's notice by its
draftIdsand asserts that it gives the earlier send as the cause, and that the listing andreadgive that draft the same cause.A new XML-mode test holds three rounds' prose (a round with
channel_list, a round withchannel_publish, then the text after it) and asserts thatreadgives the first round's draft "an explicit send in its turn held it back".A new test drives
holdProseDraftswithnotice: 'now'and asserts the sentence for each of these:Several drafts with a possibly-posted one among them stay in the hybrid case above.
Runs for the second review round, on the head
8d6ca71f's tree (5d3ecbd0), run before its last commit message was reworded, on Linux, Node 24.22.1, with context-manager 0.14.1 and membrane 0.5.87. Each rannpm run build, then each file fromdist/:prose-drafts,explicit-prose-routingandpresent-while-actingpass 269 of 269 (89, 103 and 77), with 0 failing.src/as at88e3c8d1,prose-draftsfails 2 tests: the new XML-mode test and the three-segment test, each onread's cause.mcpl-ws-transport, which fails at4efd89d1too, as below.Runs for the first review round, on Linux, Node 24.22.1, with context-manager 0.14.1 and membrane 0.5.87. Each ran
npm run build, then each file fromdist/:88e3c8d1: typecheck is clean.prose-drafts,explicit-prose-routingandpresent-while-actingpass 268 of 268 (88, 103 and 77), with 0 failing.src/as at4efd89d1,prose-draftsfails 4 tests: the three-segment test and the three catch-up tests whose assertions changed.88e3c8d1: 5,967 tests, 5,921 pass, 44 skipped and 2 fail, on a machine at a load average of 71 to 112.mcpl-ws-transport: the base's, as below.code-execution's "a call made while a run watches, that waits for room in the host until the run has yielded, …": it rests on wall-clock timing, 16 slow calls against a 2 s call timeout and a 3 s yield. Its comment records that a sweep of that timeout passed at 1.5 s and began to fail below it. It passes alone at88e3c8d1, as at4efd89d1, and it fails in the full suite at4efd89d1too.4efd89d1, on the same machine: 5,967 tests, 5,920 pass, 44 skipped and 3 fail. The three are the two above andcode-execution-isolate-scripts' "a left script that crashes or reaches its limit wakes its agent", which passes at88e3c8d1.code-executionpasses 94 of 94 andprovider-waits92 of 92, alone at both4efd89d1and88e3c8d1.mcpl-ws-transport, "under Bun, a failed dial's error leaves no credential in its message, cause or stack" fails alone at both4efd89d1and88e3c8d1, with 33 of 34 passing. It expects the errorWebSocket connection to 'ws://…' failed, and under this machine's Bun the error readsconnect ECONNREFUSED 127.0.0.1:1 (ws://…). It fails at the base, so it isn't this PR's. CI runs this test under Bun 1.3.14, where this machine has 1.4.4, and it passed at4efd89d1.Runs before the review round, at
4efd89d1from a clean tree, on Linux, Node 24.22.0, with context-manager 0.14.1 and membrane 0.5.87. Each rannpm run build, then each file fromdist/:prose-drafts,explicit-prose-routingandpresent-while-actingpass 268 of 268, with 0 failing. fix(drafts): the held-draft notice names the message and says why in a sentence #656's description reports 267 of 267 for these files, and this adds one test.src/framework.tsas at35c2713d, and again as atmain97ca7eba,prose-draftsfails 3 tests: the new one, and the two whose assertions changed.Review rounds
Anarchid's review at
4efd89d1(R1):7e22c5cc;7b3cf723;88e3c8d1.Anarchid's review at
88e3c8d1(R2):d6339cfaand fixed inb7bed423;41e388b2;8d6ca71fdrops two explanatory comments this PR had added beside the notice; their reasons are in its commit message.The stack
This PR changes #656's notice. #656 merged without it ships the notice that quotes each held draft's first 50 characters.
#716 also changes
DRAFT_REASON_TEXT: itsno-destinationentry, the line beside this PR'sexplicit-sendentry.What this leaves
#666 covers three things this PR leaves alone:
draftstool's description, whose wording would rewrite every resident's prompt cache, and which has the same XML-mode scope asread;Not verified
changelog.d/draft-notice-wording.changed.md, fix(drafts): the held-draft notice names the message and says why in a sentence #656's, edited.Written by Nigel-2405 (Claude Opus 5.5). Gladys-2413 (Claude Opus 5.5) read and approved the first version, and three internal reviews read it. She also read both review rounds by reading.
Description checked and posted by Lottie-2661 (Claude Opus 5.5), and this round's by Fiona-2757 (Claude Opus 5.5).
🤖 Written using Anthropic's Claude Opus 5.5 through the Rookery's tooling, and posted from the shared
nissa-seruaccount.