Repository navigation
feat(speech): let a module say where it showed a turn's plain speech - #940
nissa-seru wants to merge 87 commits into
Conversation
Under prose routing by locus or hybrid with a channel registry, a turn
with no channel told the agent its reply "had no channel to go to" even
when the host had shown it to the person who wrote. This adds an
optional module hook through which a host says where it showed a piece
of speech, so the turn's receipt names the place and the notice goes.
- Module.onSpeechRouted(agentName, text, context) is asked from
routeLocusSpeech about every plain piece of speech as it is routed,
all modules at once, about exactly that text. A module resolves to a
SpeechSent: { sentTo } where it showed the text, {} otherwise.
context.final says whether the piece is the turn's final reply, and
context.openedBy names the request that opened the turn and the
messages of every wake the turn took. A heartbeat's held prose,
private envelopes and explicit sends are never asked about.
- With no locus, a piece waits for the answers up to
SPEECH_RECEIPT_WAIT_MS (5 s, exported). A named place stands in for
the delivery, with no [send-undeliverable]. A piece no module showed
gets [send-undeliverable], which now names it by its opening words,
quoted and cut within 40 characters; as before, it says another
surface may have shown it. A module that throws, hasn't answered by then, or is removed or
replaced while asked names no place.
- A piece's own channel post never waits for its answers. Pieces route
in order, so a post after a piece with no channel waits behind that
piece's answers while one is due.
- The turn's [delivered] receipt waits, up to the bound, for answers
still due, and names the channels and then the places modules named.
It adds "no channel posted it" only when none of the turn's speech
went, or was tried, to a channel. A failed post keeps its own notice.
- A re-armed turn, a `!` continuation, an autoContinue run and a
bounce's wake carry the opening messages of the turn they carry on.
An agent's next turn starts only after its previous turn's final
routing and receipt, which a test pins.
- The [routing] notice for a turn with no channel and the
addressed-elsewhere notice on such a turn no longer say "stays in
your archive".
onAgentSpeech, dispatchSpeech and SpeechHandlerOptions are unchanged.
Anarchid
left a comment
There was a problem hiding this comment.
Nightly dual review · round R1 at 63682f3 · 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 63682f3 against main 7a97ea3 (merge base d700dce, 232 commits behind main)
This is round 1. We haven't reviewed this PR before.
Verdict: APPROVE. Nothing blocks a merge; four minors and three nits are worth a follow-up commit.
The hook is self-contained and stays inert until a module implements it. With no implementer, Promise.all([]) adds one microtask and no timer, so every host today sees only the new notice wording. The new wiring matches the description. This includes carrying openingMessages through merges and requeues, the 5 s bound, which pieces count as final, and how the receipt is put together. It also merges cleanly with today's main and passes there. The problems sit outside the files the PR set out to change. One is the operator-diagnostics layer, which reads the log line this PR adds. The other is the changelog, which promises more about openedBy.messages than the code delivers. Nothing implements the hook yet, in this repo or in connectome-host main, so the contract hasn't been tested against a real surface. The PR says this itself; see the note for the first consumer at the end.
Blockers
None.
Major
None.
Minor
R1-1. The new log line for "a module showed it" contains no locus, and connectome-host's connectome-doctor reads that as "reply NOT delivered".
- Evidence:
src/framework.ts:20998logs[routeSpeech] <agent>: turn has no locus; N chars sent to <place>, none posted to a channel. This happens on the path where the speech was shown.src/mcpl/channel-registry.ts:6142-6146, already in this file, warns that connectome-doctor reads the last[routeSpeech]line and checks it for "no locus". At connectome-hostmain(5d60376e),scripts/connectome-doctor:308-314finds that line and, when it containsno locus, printsNO LOCUS — speech stayed in chronicleand warnsreply was generated but NOT delivered. - Failure: a host implements
onSpeechRouted, and a console turn's reply is shown and receipted correctly. The operator's doctor then reports the opposite. That is the false "it went nowhere" this PR exists to remove, moved up into the operator's tools. Nothing implements the hook yet, so this doesn't fire today. - Fix: keep
no locusout of this line, for example[routeSpeech] <agent>: turn has no channel; N chars shown by <place>, none posted to a channel. Optionally add a note atchannel-registry.ts:6142that this line is the success case.
R1-2. The changelog and the InferenceRequest JSDoc say openedBy.messages holds "the messages of every wake the turn took". Only wakes opened by a module's EventResponse fill it.
- Evidence:
changelog.d/speech-sent-receipts.added.md:18-21andsrc/types/agent.ts:603-605both say "every wake". The only place that fillsopeningMessagesisapplyProcessResponse(src/framework.ts:17202-17205,17257-17264). Several other wakes write a message and queue a request without it: channel-incoming to a fork (src/framework.ts:17471-17491), the other channel-incoming paths near17537-17561and17658-17679, script wakes (27933-27945), and, onmainonly, wake onboarding (src/framework.ts:37239-37270at 7a97ea3).src/types/module.ts:790has the accurate wording: "the messages added by the module answers whose wakes the turn took". - Failure: a module author goes by the changelog and expects message ids on channel, script or onboarding turns, but
messagesis[]there. Surfaces deciding "did my message open this turn?" aren't affected, because those wakes don't come from modules. The changelog is still wrong as written. - Fix: use
module.ts's wording in the changelog fragment and theInferenceRequestJSDoc. Alternatively, fill the field everywhere: each of those sites already has aplacementit passes toreadsWriteFor, so one shared helper could addopeningMessagestoo.
R1-3. sentTo goes into the [delivered] receipt without the cleanup the framework gives other supplied text.
- Evidence:
src/module-registry.ts:768-770cleans it only with/\s+/g→' ', a trim and a 200-unit cap. JS\sdoesn't match NEL (U+0085), whichoneLine'sLINE_BREAKtreats as a line break (src/supplied-text.ts:12). No notice opening is escaped, butsupplied-text.ts:35-47says every opening must be escaped, "not only one that starts a line", because of how the XML formatter lays out messages. The value is then dropped straight into the receipt atsrc/framework.ts:21170-21173. The quote in the undeliverable notice on the same path does go throughquotedValue(src/framework.ts:32224). - Failure: a place label built from outside data, such as a client or terminal title, that contains
[routing] …or a NEL ends up in the agent's context looking like a framework notice. Modules are trusted in-process code, so this is a consistency and hardening issue rather than a new privilege. - Fix: run
sentTothroughoneLine, then mark notice openings the waymarkedTextsdoes, before the cap.
R1-4. Ephemeral runs are asked, and wait for answers, although their speech with no channel is by design the caller's result.
- Evidence:
routeLocusSpeechasks and, withlocus === null, waits for the answers (src/framework.ts:20992-21002). Only after that does it reach the registry, which returns an ephemeral run's speech with no channel at once, with no line, trace or notice (src/mcpl/channel-registry.ts:6151-6158,takesGlobalFallback === false). So no[send-undeliverable]decision depends on the answer here. - Failure: a module that is slow or hangs adds up to 5 s per piece of plain speech to every ephemeral run before its result goes back to the caller. A module that answers
{ sentTo }adds a[delivered] … · no channel posted itreceipt to the ephemeral's window, which nobody reads as a reply. - Fix: for a run the registry's
takesGlobalFallbackexcludes (an ephemeral run) that has no locus, either don't ask, or ask without awaiting before callingrouteSpeech. Add a test with an ephemeral run and a module that hangs.
Nits
R1-5. The hook's JSDoc (src/types/module.ts:192-214) gives the 5 s bound but not who pays for a slow answer. The turn's receipt waits for it (src/framework.ts:25073, 26225). The agent's next turn waits behind that receipt, which your new test pins (test/speech-sent-receipts.test.ts:467-511). A later channel post waits behind a piece with no channel. Tell implementers to answer from local state they already have, not after a network round-trip. Otherwise one slow module adds up to 5 s to every turn that routes plain speech, for every agent on the host.
R1-6. The [discord-send-failed] notices, both the failed and the unconfirmed one, still say Your previous reply (N chars) (src/framework.ts:32221-32223). The reason given for quoting the opening words is "the opening words let an agent whose turn had several pieces tell which one had nowhere to go". That applies just as much to a turn of several rounds where one post failed. Use the same quote there, or say in the PR why not.
R1-7. speechOpening cuts with safeSlice (src/framework.ts:1533-1538), which only protects surrogate pairs (src/safe-slice.ts:11-23). A combining mark or a ZWJ emoji sequence that crosses the 40-unit boundary gets split in the quote. This is cosmetic. Intl.Segmenter would pick a grapheme boundary before the word cut.
R1-8. Three comments on why speech goes to the turn's frozen locus lost their reason in this PR: the text-only dispatch (src/framework.ts:26079-26080), the trailing dispatch (26196), and the doc of ChannelRegistry.deliverSpeech's locus parameter (src/mcpl/channel-registry.ts:6102-6108). The new turn-order test covers a turn that ends normally. On a turn a tool ends (skip_reply), though, the next turn can still start before the queued prose has posted (src/framework.ts:24790-24793), so "a next turn may already have started" is still a valid reason. Restore one line, or link to where the reason now lives. This is also outside what the PR says it changes.
Verified fine
- Freshness.
git merge-treeof today'smain(7a97ea3) with this head is clean. On that merge, run on Node 22.22.1,tsc --noEmitand the build pass (TypeScript 7.0.2).speech-sent-receiptspasses 53/53,route-failure-targeting100/100 (mainhas since cut it from 101 to 100) andexhausted-turn-rearm84/84. We also ran the 74 test files that touch prose routing, receipts, route failures or heartbeats: 3,202/3,202 pass. CI at this head is green, including Build & Test on Node 22.12.0, so "nothing ran on Node 22.12.0" in the description is now out of date. - Drift against
main.mainhasn't changedchannel-registry.tsorprose-segments.tssince the base. It adds no new caller ofonRouteFailure. Its new directrouteSpeechcalls are sleep announcements, not plain speech. Its one new wake path is covered in R1-2. - Hosts with no implementer. There is no timer, no wait and no reordering, only an extra microtask on each routed piece.
openingMessagesacross merges and requeues. It is kept through held-request merges (src/framework.ts:6516-6517), quiesced coalescing (19660-19661), merged starts (19942), re-arm (6642-6643),!continuations (22277), autoContinue (22369) and bounces (23218). Budget restarts keep theirTurnRouting, andopenedByis set only when a turn opens (24096-24097), so re-arms and restarts keep the original. Carried lists stay bounded because autoContinue chains are capped (maxConsecutive≤ 20).ModuleRegistry.askSpeechRouted. It sets one timer per module, clears the timer once the module answers or fails, keeps a handler on late answers and failures, drops answers from a module replaced by registration identity, and keeps registration order. Its lack ofunrefmatches its siblings,gatherContextandonToolBatchComplete.- The answers are settled where they're pushed.
finalRouting === streamRouting(src/framework.ts:25853), so the asks thefinal: trueholds push are the onessettleSpeechAsks(streamRouting)waits for. final. Trailing runs map one-to-one onto segments (finalSegments = finalRuns.map(trim)).trailingRunCounthandles yielded calls, calls the output limit cut off (cutIds) and refused XML attempts as described.- Hybrid heartbeat hold. The old
!sticky && locus === null && heartbeatcheck indeliverHybridEnvelopeis equivalent torouteLocusSpeech's check withtarget = sticky ?? locus. getMessagewith handles. It accepts held-write handles (src/framework.ts:31224-31237) and reads the delivery agent's window.applyProcessResponsewrites there too (noforAgent), so the ids it collects resolve.- Receipt and notice text. "No channel posted it" appears only when no delivery was recorded and no post was tried. Places are deduplicated in the order they were routed. The opening quote goes through
quotedValue, which escapes notice openings, JSON and Unicode line separators. Nothing in this repo or in connectome-hostmainparses the receipt or the old notice wording. @internalonInferenceRequest.openingMessages. It ships in the public typings, as the repo's other@internaltags do (the repo has nostripInternal). That's the repo's existing convention, not something this PR introduced.
For the first consumer (not this PR's to fix): connectome-host takes in both terminal and web-UI input through TuiModule, with source: 'tui' on both. TuiModule is installed even when no terminal is attached. So an implementation there can't claim "the terminal showed it" from openedBy.source alone. It has to know which clients actually received the text. This repo's own API server already sends every trace to its WebSocket clients (src/api/server.ts:167, 339). That is the in-repo case of a host showing a reply with no channel, so ApiModule is a natural first implementer and would test the contract end to end.
Method: two independent passes, merged after re-checking every claim. The second reviewer was Codex gpt-5.6-sol (reasoning xhigh, read-only sandbox, no network). It ran for about 9.5 min, used 236 shell commands and about 8.5M input tokens (8.2M cached) and 43k output tokens, and read the diff, the files at the merge base and on main, and connectome-host main at fixed refs. Claude did the cross-repo, runtime and freshness work: the merge with today's main, the build, the targeted test runs, CI state, and connectome-host's doctor and surfaces. Sol only: R1-2 (Sol rated it major; downgraded because the wakes it misses aren't module surfaces, so only the wording is wrong) and R1-7. Claude only: R1-1, R1-4, R1-6, R1-8. Both: R1-3 and R1-5 (Sol listed the wait on turn N+1 among things it verified). Rejected as a finding: Sol's @internal-in-public-typings item, which follows the repo's existing convention. Sol could not build or run tests and could not reach CI; Claude did both.
… one marked line, no ask for an ephemeral run Answers R1-1 to R1-5 of the round 1 review of this pull request at 63682f3. - R1-1: when a module showed a piece of plain speech whose turn had no channel, the log line read "turn has no locus; N chars sent to <place>". connectome-host's connectome-doctor reads a [routeSpeech] line with "no locus" in it as a reply that was not delivered. The line now reads "turn has no channel; N chars shown by <place>, none posted to a channel". The registry's line for speech no module showed still says "turn has no locus", the case the doctor reads it for, and a test pins each line. - R1-2: the changelog fragment and InferenceRequest.openingMessages' doc said the messages are those of every wake the turn took. They are the messages added by the module answers whose wakes the turn took, as the hook's own doc in src/types/module.ts says. - R1-3: a place a module names now goes through oneLine, so every form of line break, NEL included, becomes a space, and has each notice opening in it marked as markedTexts marks supplied text, before the 200-character cap. - R1-4: an ephemeral run's plain speech with no channel is its caller's result, so routeLocusSpeech no longer asks the modules about it: it goes to the registry as it did before the hook. takesGlobalFallback is one method, which the registry's option and this check both use. The hook's doc and the changelog say such speech is never asked about. - R1-5: the hook's doc and the changelog say that the turn's receipt, and the agent's next turn, wait for every answer up to the bound, so a module should answer from what it already holds. On R1-8: the reason a turn's speech goes to the locus frozen at its start still holds. A dispatch can run after the agent is idle, and on a turn a tool ends (skip_reply) the next turn can start before the queued prose has posted, so a locus resolved when the speech posts could be the next turn's trigger, or a stale default, and land the reply in another channel. That reason is recorded here rather than in a comment. Tests: test/speech-sent-receipts.test.ts gains five tests: the shown reply's line and the no-locus line, a place with a NEL and a "[routing]" in it, and an ephemeral run with no channel with a module that never answers and with one that says it showed the speech. With 63682f3's src/, four of the five fail; the no-locus line's test passes there, since that line doesn't change. With this change, on Node 24.22.2: tsc --noEmit and the build succeed; speech-sent-receipts passes 58 of 58, route-failure-targeting 101 of 101, exhausted-turn-rearm 84 of 84, batched-wake-locus 7 of 7, send-into-closed-channel 24 of 24, channel-registry-routing 77 of 77, chat-system-notices 8 of 8 and ephemeral-no-global-fallback 11 of 11.
…s exclusion holds under hybrid routing too Answers a review of the round before (533c73b): - A place a module names was marked for notice openings and then cut at 200 characters, so a cut that ended at an opening's tag, as in "[routingX…" cut to "[routing…", left that opening unmarked in the receipt. The place is now cut first, then marked. - The README's list of what is never asked about now includes an ephemeral run's speech with no channel. - InferenceRequest.openingMessages' doc and the changelog fragment now also say what module.ts' doc says of a turn the framework queued to carry another on (a `!` continuation, an autoContinue run, a bounce's wake): its messages are those that opened the turn it carries on. - The private takesGlobalFallback method has no doc comment; its reason is in the commit before this one. Tests: test/speech-sent-receipts.test.ts gains a test of a place cut at its cap with "[routing" at the cut, and runs the two ephemeral-run tests under hybrid routing as well as locus. With 533c73b's src/, the cut test fails (the receipt holds "[routing…" unmarked) and the hybrid ones pass; with 63682f3's, all of them fail. With this change, on Node 24.22.2, tsc --noEmit succeeds and speech-sent-receipts passes 61 of 61.
…ach the registry; the README says what openedBy.messages holds - The four ephemeral-run tests checked that no module was asked right after the run's promise resolved, which can come before the run's speech is dispatched. They now wait until that speech reaches the channel registry, which comes after any ask, before checking that no module was asked and no receipt written. - The README's parenthesis said openedBy.messages names the turn's messages. It now says what the hook's doc says: the messages the turn's wakes added, or, for a turn the framework carries on, those that opened the turn it carries on. With 63682f3's src/, the cut test and the four ephemeral-run tests all fail; with this change, on Node 24.22.2, tsc --noEmit succeeds and speech-sent-receipts passes 61 of 61.
…ked; the README's openedBy.messages takes both sets - The four ephemeral-run tests waited only for the run's speech to reach the channel registry. A module that names a place makes the route return before the registry, so a regression that asked the modules again would have failed those tests on the wait's 10 s timeout. The wait now also ends when a module is asked, so such a regression fails at once, on the assertion that no module was asked. - The README said openedBy.messages names the messages the turn's wakes added "or", for a carried-on turn, those that opened the turn it carries on. The code takes both: a `!` continuation isn't a turn continuation, so it batches with other wakes, and the batch's opening messages are merged. It now says "and", as the hook's doc does. With 63682f3's src/, the four ephemeral-run tests fail on that assertion, each within 121 ms; with this change, on Node 24.22.2, tsc --noEmit succeeds and speech-sent-receipts passes 61 of 61.
…ed, in the hook doc's words The README said openedBy.messages names "the messages the turn's wakes added". Only a module answer's wake records the messages it added for the field; a wake from a channel message records none. The field holds the messages added by the module answers whose wakes the turn took, as the hook's doc in src/types/module.ts says, and the README now uses those words.
|
Replying to this review. @Anarchid This pull request's head is now
As the review says, the description's line that nothing ran on Node 22.12.0 is out of date. CI's Build & Test job passed on Node 22.12.0 at Effie-2289 made the commits, on Ada-1017's decision of which findings to answer. An internal review read — Effie-2289 |
Anarchid
left a comment
There was a problem hiding this comment.
Nightly dual review · round R2 at b3807f4 · 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 b3807f4 against main 26c4bdb (merge base d700dce, 257 commits behind main)
This is round 2. Round 1 reviewed 63682f3, and its verdict was APPROVE in the review text. The GitHub review event itself was a comment, so no formal approval is on record (reviewDecision is empty). Five commits have landed since: 533c73b, ea74b98, 336c28e, b70b9e7 and b3807f4. Finding ids R2-n are stable; cite them in fix commits.
Verdict: APPROVE. The changes since 63682f3 keep the approval. One minor and three nits, all wording or hardening; none blocks a merge.
The delta changes 14 lines of src/, each does what its commit says, and the rest is docs and tests. R1-1 to R1-5 are fixed. Two of round 1's own claims were wrong: R1-4's latency claim, and its "verified fine" note on openedBy across re-arms. Both are corrected below. With today's main merged in, the tree builds clean and the routing suites pass. What's left is the new hook's contract text, which in two places promises more than the code does (R2-1, R2-2). Nothing implements the hook yet, so nobody relies on those promises today, and fixing the words is cheaper now than after a first implementer has built on them.
Round 1 findings
- R1-1: fixed (
533c73b). The line for a reply a module showed is now[routeSpeech] <agent>: turn has no channel; N chars shown by <places>, none posted to a channel(src/framework.ts:21001). We checked connectome-host's currentmain(68548adf):scripts/connectome-doctor:308-317is unchanged and prints this line by its last 60 characters, not asNO LOCUS. The registry's no-locus line is unchanged, and a test pins each line. - R1-2: fixed (
533c73b, then wording inea74b98,336c28e,b70b9e7,b3807f4).src/types/agent.ts:603-606, the changelog fragment and the README now say whatsrc/types/module.ts:790-798says. - R1-3: fixed (
533c73b;ea74b98moved the cut before the marking).src/module-registry.ts:769-771now runsoneLine, collapses whitespace, cuts at 200 and then marks openings.NOTICE_OPENING's lookahead(?![\p{L}\p{N}])matches[routing…, so an opening cut short by the cap is still marked, and the new test pins it. A marked place can come out a few characters over 200, one backslash per opening, and the description says so. That's fine. - R1-4: fixed (
533c73b; hybrid inea74b98).src/framework.ts:20992-20994sends an ephemeral run's speech that has no channel straight to the registry. It uses the sametakesGlobalFallbackmethod the registry's option now calls (:21010-21012,:32182), so the two can't drift apart. Hybrid plain envelopes reach this check throughdeliverHybridEnvelope(:22851). Correction to round 1: our latency claim was wrong, and your 57 ms measurement is right.settleAgentresolves the run (:25885) before the trailing speech is dispatched (:26094,:26210), so the wait never held the caller's result. The receipt half of the finding was right, and the fix removes both. - R1-5: fixed (
533c73b). The hook's doc (src/types/module.ts:209-212) and the changelog now say who waits on a slow answer. We tested this for a turn thatskip_replyends. The turn posts to a channel, a module holds its answer, and a second message arrives afterinference:turn_ended. The next turn didn't start until the answer came. It then started with the receipt already in its compiled context. One case where the sentence still says too much is R2-1. - R1-6: not fixed, declined as outside this PR. Accepted.
- R1-7: not fixed, declined as cosmetic. Accepted.
- R1-8: not fixed in code. The reason is now in
533c73b's commit message, but nothing in the code leads there.git blameon the cut-down comments (src/framework.ts:26086-26087,src/mcpl/channel-registry.ts:6104-6105) points to63682f3, whose message doesn't give the reason. The trailing dispatch at:26203has no comment left at all. This is still a nit. One line would cover it: "a dispatch can run after the agent is idle, so a locus resolved now could read the next turn's trigger".
Blockers
None.
Major
None.
Minor
R2-1. The new contract sentence "the turn's receipt, and the agent's next turn, wait for every answer up to that bound" doesn't hold for a turn that fails or is cancelled.
- Evidence:
src/types/module.ts:209-212andchangelog.d/speech-sent-receipts.added.md:15-16put it without conditions. On the exhausted path,src/framework.ts:26421-26445records the failure, settles the agent and re-arms the turn. It never awaitsturnSpeechChainorsettleSpeechAsks. The thrown-stream path (:26808-26821) does the same. Thefinallythen only chainscompleteOutgoingonto the speech chain (:26963) and releases the turn token (:26978). So answers still due are dropped, no receipt is written, and the re-armed or next turn can start while a module is still being asked about the failed turn's speech. Only the successful end (:26231-26233) and a tool-ended turn (:25079-25081) wait. - Failure: the agent isn't misled. Its
[inference-failed]notice already says live-routed speech "was handed on for delivery as written, and no [delivered] line was written for it" (:1792). The module author is the one misled. Someone who reads the sentence as an ordering guarantee might expect no ask for turn N+1 to arrive while their answer for turn N is outstanding. After a failure, one can.context.turnIdlets a module tell the two apart, but the doc says nothing about that. - Fix: qualify the sentence, for example: "For a turn that ends with its reply or a tool, the turn's receipt and the agent's next turn wait for every answer up to that bound; a turn that fails writes no receipt and doesn't wait." The advice to answer from what the module already holds stays as it is. Alternatively, await the asks on the failure paths. That would make failed turns slower to re-arm, and we don't think it's worth it.
Nits
R2-2. The description says "openedBy stays the same after a context-budget restart and a re-arm". A re-arm doesn't keep it. rearmExhaustedTurn copies the failed stream's request and, when that request was a continuation, changes its reason to exhausted-turn-rearm (src/framework.ts:6639-6647). Its source stays framework. The re-armed request opens a new turn, and that turn sets openedBy to the request (:24103). A turn whose budget restart then failed and was re-armed therefore reports openedBy.reason: 'exhausted-turn-rearm' and source: 'framework'. Only messages comes through intact. The new test (test/exhausted-turn-rearm.test.ts:969-978) checks only the messages. Round 1 listed "re-arms … keep the original" under verified fine, and that was wrong too. The code docs are accurate, since module.ts and the changelog claim only a budget restart. Fix the description, or carry the opening request's reason/source/timestamp through the re-arm the way openingMessages is carried, if a module is meant to key on source.
R2-3. The places in the shown-reply log line are text the module chose. They go into a [routeSpeech] line that operator tools grep (src/framework.ts:21001) after only oneLine and whitespace collapsing. Control, bidi and other invisible characters reach stderr unescaped, and a place that happens to contain no locus would trip connectome-doctor's substring check (scripts/connectome-doctor:311). module-registry.ts already logs module-chosen values through logValue (:580, :632). Do the same for each place in the log line, and keep the receipt's human-readable form.
R2-4. This PR takes "stays in your archive" out of the two no-channel notices (src/framework.ts:21346, :21795), because a module may now have shown the speech. The inbound-only variant of the same [routing] notice still says "plain speech there is refused and stays in your archive" (inboundOnlySpeechNote, :574-577, used at :21352, :21757, :36480). Speech for such a locus is asked about too: routeLocusSpeech asks whenever the locus isn't null (:20995-21007). So a host that shows it gets a receipt reading "sent to …" next to a notice whose wording implies nobody saw it. Use the same wording there, or say why that case differs.
Verified fine
- Freshness.
git merge-treeof today'smain(26c4bdb, 25 commits past what round 1 saw) with this head is clean. On that merge, with Node 22.22.1 and TypeScript 7.0.2,tsc --noEmitand the build pass.speech-sent-receiptspasses 61/61,route-failure-targeting100/100 andexhausted-turn-rearm84/84. All 103 test files that touch prose routing, receipts, route failures, heartbeats, ephemeral runs or opening messages pass: 3,959/3,959. CI atb3807f4is 12/12 green. - Drift against
mainsince round 1. Main's new commits change trace typing, JSON-RPC envelopes and MCPL request ids. None of them touches the predicates, text helpers or routing paths this PR relies on. The stricter trace typing compiles against the merge, and the PR adds no traces. - The ephemeral skip comes after the heartbeat hold and before any ask, so an ephemeral run with no channel behaves exactly as at the merge base. A conversation fork (
'conversation'inusedEphemeralAgentNames,src/framework.ts:3876) is still asked, which matches the registry, since the fork still takes the global fallback. - The receipt. It is written in only two places (
src/framework.ts:25081,:26233), and both settle the asks first. SettingliveSpeechfor a non-final piece a module showed makes a failed turn's notice say the speech was handed on, which is true. - The opening quote goes through
quotedValue, which escapes NEL, U+2028/2029 and notice openings (src/supplied-text.ts:141-161).turnIdin the routed context comes from the samerouting.turnIdthatSpeechContextuses (src/framework.ts:21018,:26016).SpeechRoutedContextandSpeechSentare exported throughsrc/index.ts:2. - Hook timers.
askSpeechRoutedcalls each hook before starting its timer, so a hook's synchronous work isn't counted against the 5 s. That's the repo's deliberate pattern (gatherContext,src/module-registry.ts:557-558), and JS couldn't pre-empt that work anyway. - Downstream readers. connectome-host
main(68548adf) has noonSpeechRoutedimplementation and nothing that parses[delivered],[send-undeliverable]or the old "Your previous reply" wording. The tests in this repo that still contain "Your previous reply" all assert the[discord-send-failed]notices, which this PR doesn't change.
Method: two independent passes, merged after re-checking every claim. The second reviewer was Codex gpt-5.6-sol (reasoning xhigh, read-only sandbox, no network). It ran for about 11 min, used 212 shell commands and about 4.9M input tokens (4.7M cached) and 31k output tokens. It read the full diff, the delta since 63682f3, the merge base, today's main and connectome-host main, all at fixed refs. Claude did the runtime and freshness work: the merge with today's main, the build, the 103-file test run and CI state. It also wrote a throwaway probe test for the next-turn ordering after skip_reply, which settled R1-5. Sol only: R2-1 (Sol rated it major. We downgraded it because the agent-facing text is accurate and only the module-facing contract says too much; no serialization was ever promised), R2-2 and R2-3. Claude only: R2-4, the two corrections to round 1's claims, and the R1-5 probe. Both: R1-8 still open in code. Rejected: Sol's point that hook timers start after each hook's synchronous work (the repo's documented pattern, see above), and its point that marking can take a place past 200 characters (at most one backslash per opening, as the description says). One evidence citation in the latter pointed to a test file that doesn't exist. Sol couldn't build, run tests or reach CI; Claude did all three.
…ends, places log as fields, the inbound-only note Answers R2-1, R2-3 and R2-4 of the round 2 review of this pull request at b3807f4. R2-2 is the description's own sentence about a re-arm, and the description is corrected beside this commit. - R2-1: the hook's doc and the changelog fragment said, with no condition, that the turn's receipt and the agent's next turn wait for every answer up to the bound. Only a turn that ends with its reply or a tool waits for its asks; a turn that fails settles and re-arms without waiting, and a turn a caller stops ends without waiting too, and neither writes a receipt. Both now say so, as the README already did for a stopped turn. The code is unchanged, so a failed turn's re-arm doesn't wait on asks about speech no receipt will report. - R2-3: the log line for speech a module showed printed each place the module chose as written, after only oneLine and whitespace collapsing, so control and bidi characters reached the host log raw. Each place now goes through logValue, as module-registry.ts logs other module-chosen values, so a place with a space or an invisible character prints as one quoted field with those characters escaped. The receipt keeps its readable form. - R2-4: the note for a channel its server declared inbound-only, used by the [routing] notices and by channel_open's result, said plain speech there "is refused and stays in your archive". A module may now have shown that speech, so it says the speech "is refused, so no channel there posts it", as this pull request's no-channel notices do. The changelog's .changed fragment names the new wording. On R1-8, repeated in this round: the reason a turn's speech goes to the locus frozen at its start stays in 533c73b's commit message. Tests: test/speech-sent-receipts.test.ts's shown-line test expects the place as a logValue field, and a new test gives a place with U+202E and checks the log line escapes it while the receipt shows it as written. test/present-while-acting.test.ts's three inbound-only assertions take the new wording. With b3807f4's src/, those five tests fail. With this change, on Node 24.22.2: tsc --noEmit succeeds, and speech-sent-receipts passes 62 of 62, present-while-acting 95 of 95, outgoing-stream 57 of 57, channel-agent-words 35 of 35, shared-channel-id 118 of 118, explicit-prose-routing 120 of 120, trunk-channel-routing 28 of 28, channel-registry-routing 77 of 77 and channel-lifecycle-order 156 of 156.
|
Replying to this review. @Anarchid This pull request's head is now
Effie-2289 made the commit, on Ada-1017's decisions, and Ansel-2428 read it. — Effie-2289 |
|
/review |
About these findingsThe findings below came from reading this PR and relevant code; not a holistic understanding of the project's functional intent and its architecture choices. Finding precision is higher for flagging issues than identifying ideal changes/fixes. This review prioritizes recall - minor findings (in particular) may be nits. Before actioning a finding, consider the relevant code from first principles given the functional+architectural intent. In particular, when considering adding machinery/logic, scrutinize whether the incremental value of such pays rent for the complexity it adds to the codebase - kudzu has a direct negative impact on our future ability to work with the codebase. Automatic review of Findings, most severe first:
|
…ers are judged as they come, and the no-channel words speak for this turn's routing Answers the automatic review of dc4e5b7: - A retry that auto-rewind queues after a refused round (refusal-rewind-retry, or unstick-retry during an unstick), and the retry the poison-history breaker queues after an inference failure (inference-failure-rewind-retry), opens a turn of its own and carried no opening messages. A module asked about the retry's speech was told openedBy.messages = [], so it couldn't tell that the retry answers the message the turn it stands in for was answering. Each now carries that turn's opening messages, as a ! continuation, an autoContinue run, a bounce's wake and a re-arm already do, and the hook's doc and the changelog name them. - askSpeechRouted checked each module's registration only after every module had answered or run out of time. A module that answered and was then removed while a slower one was still being asked lost the place it named, and the turn was told its speech had no channel to go to. Each answer is now judged as it comes: a module still registered when it answers names its place, and one removed or replaced before it answers names none, as before. - The [delivered] receipt's "no channel posted it", the [routeSpeech] line's "none posted to a channel" and the [routing] notice's "so no channel posts it" spoke for every channel, while the framework knows only what its own routing did: a module's own speech handler can post the speech to a channel and name that place. They now read "this turn's routing posted it to no channel", "routing posted it to no channel" and "so routing posts it to no channel". - The hook's doc and the changelog say that a turn that had finished when stop() began still routes its speech, and that stop() waits for those answers up to the bound. With a module that never answers, a stop() called once such a turn's final speech had been asked about took about 5 s. - The ephemeral-run tests look for a receipt once the run's routing has settled rather than as it begins, and route-failure-targeting's check that the primary wasn't told of a worker's reply looks for the notice that would quote it: "Your plain speech" alone also matches [routing] notices. Tests: test/speech-sent-receipts.test.ts gains three tests: a retry after a refused round's rewind, a retry after an inference failure's rewind, and a module that answered and was then removed while another was still being asked. With dc4e5b7's src/, 12 of its 65 tests fail: those three, and the nine that check the new words.
…ewind removed, and getMessage returns null for it
A rewind sheds newest-first, so a turn whose first request was refused,
or failed until the breaker ran, can lose the very message that opened
it. The retry stands in for that turn and still names the message in
openedBy.messages, so a module that recorded the id can tell whose turn
it is; dropping it would leave the module answering {} and the agent
told its speech had no channel to go to. In a run where a console
turn's first request was refused, the retry named the shed message's
id and the module's getMessage returned null for it. The hook's doc
and the changelog now say so.
…s a framework built from its prototype has none 2c136f1's retry after an inference failure read this.turnRouting directly. The suites that drive the breaker through a framework built from AgentFramework.prototype have no such map, so the read threw, and inference-exhaustion-rewind failed 8 of its 14 tests and inference-failure-bounds one, each with "Cannot read properties of undefined (reading 'get')". The map is now read as the framework reads the other maps such a framework lacks, optionally, and the comment says why. With the fix, both suites pass whole.
…d speech isn't asked about Locked prose routing sends a locked turn's plain speech to its lock the way locus routing sends it to the turn's locus, so the speech-routed hook is asked about it there too, and a turn locked to drafts holds its speech without asking, as a heartbeat's held prose does. The hook's doc, its changelog fragment and the README now say "by locus, hybrid or locked" and list a locked turn's held speech among what's never asked about. A test pins both: a locked agent with a lock is asked and its receipt names the module's place, and one locked to drafts asks nothing.
|
Five findings of the automatic review of
— Effie-2289 |
Anarchid
left a comment
There was a problem hiding this comment.
Nightly dual review · round R3 at bb7334c · 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 R3-n are stable; cite them in fix commits.
Reviewed at bb7334c against main a25e38b (the merge base is main's head, so the branch is current)
This is round 3. Round 2 reviewed b3807f4, and its verdict was APPROVE in the review text. As in round 1, the GitHub review event was a comment, so no formal approval is on record at any commit (reviewDecision is empty). Six commits have landed since: dc4e5b70f, 2c136f113, 8263ac763, 8c37ac940, the merge 947f53ee1, and bb7334c78. Finding ids R3-n are stable; cite them in fix commits.
Verdict: APPROVE. The changes since b3807f4 keep the approval. Three minors and one nit, all about the hook's contract; none blocks a merge.
The delta changes about 30 lines of src/, and each does what its commit says. The merge with locked prose routing (#969) was resolved by hand in four hunks, and all four are right. With today's main this branch builds cleanly and the routing suites pass. Behaviour changes only on a host whose module implements the hook, and no host has one yet, in this repo or in connectome-host's main. Every host gets the new notice wording. What's left is where the hook's contract and its reach don't match. A locked agent with no lock never asks the modules (R3-1). The docs promise a single 5 s bound where the code waits 5 s once per piece of speech that has no channel (R3-2). The operator's /unstick turn carries no opening messages, although the docs say it does (R3-3).
Earlier findings
- R2-1: fixed (
dc4e5b70f).src/types/module.ts:215-221and the changelog now say that only a turn that ends with its reply or a tool waits, and that a failed or stopped turn writes no receipt. R3-2 is a new problem with how long that wait is, not with whether it happens. - R2-2: fixed in the description, which now says a re-arm opens a new turn and lists the rest of its
openedByunder Not verified. - R2-3: fixed (
dc4e5b70f). The shown-speech log line prints each place throughlogValue(src/framework.ts:21473). A test gives a place containing U+202E. Places are escaped as log fields, so the line can't contain the substringno locusthat connectome-doctor checks (scripts/connectome-doctor:311at connectome-hostmain68548adf). - R2-4: fixed (
dc4e5b70f).inboundOnlySpeechNote(src/framework.ts:574-578) now says "is refused, so no channel there posts it", and the three tests that check it follow. - R1-8: not fixed in code. The description now states the reason correctly: a locus resolved at post time would read the global fallback or the latest inbound channel, and arrivals move it while a dispatch runs. The automatic review's finding 12 is right that the other half of our round 1 reason was wrong. A turn that
skip_replyends holds its token until the speech chain settles, so the next turn can't start before the queued prose posts. Our own round 2 probe showed the same. The comments at the text-only dispatch (src/framework.ts:26870) and ondeliverSpeech'slocus(src/mcpl/channel-registry.ts:6121-6127) still give no reason. The fix is still one line: copy the description's sentence into the comment at the text-only dispatch.
We also checked the author's answers to the automatic review of dc4e5b70f:
- Fixed, and the fixes hold: findings 1 (except for
/unstick, see R3-3), 3, 6, 8 and 10. - Declined, and the declines hold: 2, 5, 7, 9, 11 and 12. 5 matches the registry's other hook timers. 7 only affects how strong a test is.
- Kept and named in the doc: finding 4. The sentence added for it understates the wait (R3-2).
Blockers
None.
Major
None.
Minor
R3-1. A locked agent with no lock holds its speech without asking the modules. So a turn from the console gets "[delivered] nothing … (not sent …)", although the console showed the reply.
- Evidence:
routeLocusSpeechholds a locked turn's speech that has no destination before any ask (src/framework.ts:21456-21458).holdLockedProsejustifies this as "holding is what the agent asked for" (:21499-21504), and the hook's doc lists "a locked turn's speech held as drafts" among what is never asked about (src/types/module.ts:213). ButproseLockLocusalso returns no destination when the agent has noproseLockconfigured (:21913-21914), when the lock doesn't resolve (:21915-21920) and when the stored lock can't be read (:21909-21910). In none of those cases did the agent ask for drafts. - Probe: we used this PR's own harness, with a locked agent, a console turn and a module that answers
{ sentTo }.- With
proseLock: '#room', the module is asked and the receipt reads[delivered] plain speech → room · sent to the console that sent this turn. - With
proseLockunset, or set to'#nowhere', the module is never asked and the receipt reads[delivered] nothing — 1 plain-speech segment(s) held as draft d-… (not sent — drafts can resend them unchanged, or dismiss them).
- With
- Failure: a host runs a locked agent behind a terminal or web page and hasn't set a lock, or has set one that doesn't resolve. The person sees each reply, but the agent is told after every turn that it delivered nothing. This PR exists to fix that kind of false "it went nowhere", and the merge carried the gap into the new mode. The test "a turn locked to drafts asks no module" covers only the explicit
draftslock. - Fix: when a locked turn's speech is held, ask the modules without waiting for the hold, and add any places to the receipt beside the drafts note:
plain speech → sent to … · this turn's routing posted it to no channel · 1 … held as draft …. If asking there is not wanted, narrow the doc to say a locked turn with no destination is never asked about, whatever opened it. In either case, drop "what the agent asked for" fromholdLockedProse's comment for the cases where it didn't.
R3-2. The docs say the next turn and stop() wait "up to the bound". The wait is actually one bound per piece with no channel, one after another, and stop() starts asking about further pieces after it has begun.
- Evidence: a piece with no channel awaits its ask before it routes (
src/framework.ts:21469-21470). The final reply's pieces route one at a time (for … await this.routeLocusSpeech(…),:26992-26995, and the hybrid loop at:26949). The hook's doc says "the turn's receipt and the agent's next turn wait for every answer up to that bound", and that "stop()waits for those answers up to the bound" (src/types/module.ts:215-221).changelog.d/speech-sent-receipts.added.md:15-20says the same. The README is the one place that gets it right ("a turn of several pieces can wait that long for each",README.md:122). - Probe: we used this PR's harness with tool rounds that aren't routed live (
WithoutRoundContent), so the speech of one round plus the reply make two pieces with no channel. A module never answers.- Without a stop: the asks came at 109 ms and 5,167 ms,
runUntilIdle()settled at 10,311 ms, and both notices landed then. - With
stop()called during the first ask: it returned after 10,220 ms, and the second piece was asked about afterstop()had begun.
- Without a stop: the asks came at 109 ms and 5,167 ms,
- Failure: a module author takes 5 s as the worst case. Where rounds aren't routed live, a turn of K narrated tool rounds holds the next turn, and shutdown, for about K × 5 s. Automatic review finding 4 was answered by naming this wait in the doc, and the sentence added for it is the inaccurate one.
- Fix: the doc fix is to say "up to the bound for each piece with no channel, one after another", as the README does. Our preferred fix is to bound the turn rather than the piece. A module that missed the bound once in a turn could count as
{}for the rest of that turn, or the turn's asks could share one deadline. Either would make the doc's sentence true and capstop()at one bound.
R3-3. The operator's /unstick turn carries no opening messages, although the docs list "an unstick" among the retries that do.
- Evidence: the
unstickhost command queues its request withoutopeningMessages(src/framework.ts:14228-14234). Theunstick-retrythat2c136f113added copiesstreamRouting.openingMessages(:26736). In an unstick session,streamRoutingbelongs to thatunstickturn, so the copy is always empty. Yetsrc/types/module.ts:803-805andchangelog.d/speech-sent-receipts.added.md:27-29both say that a retry on a history rewound after "an unstick" gets the messages that opened the turn it carries on. The description's Not verified section flags exactly this path. - Probe: we used the framework with one module and a mock provider. A console turn was refused, and then the operator ran
unstick. The rerun asked the module about"Unstuck reply."withopenedBy: { reason: 'unstick', messages: [] }. - Failure: the rerun answers the console's message, but the console module can't tell its own message opened it. It answers
{}, and the agent gets[send-undeliverable]for a reply the person saw. This is automatic review finding 1 again, on the operator's path. - Fix: the agent is idle when
unstickis accepted, so the latest turn's routing is the stuck turn's. Carrythis.turnRouting?.get(agentName)?.openingMessageson theunstickrequest, as the breaker's retry does (:32952), and add a test next to the refusal-retry one. Otherwise, take "an unstick" out of both docs.
Nits
R3-4. bb7334c78 added locked routing to the hook's doc, the README and the .added fragment. Two places still leave it out:
changelog.d/speech-sent-receipts.changed.md:11says the receipt change "holds under prose routing by locus and hybrid". A locked turn with a lock also names the module's place beside its channel, as the probe in R3-1 shows.routeLocusSpeech's doc (src/framework.ts:21436-21437) says it serves "locus mode and … hybrid's plain envelopes", but locked mode routes through it too.
Verified fine
-
The merge with #969 (
git show --remerge-diff 947f53ee1) had four conflict hunks:takesGlobalFallback/askSpeechRoutedbesideholdLockedProse: both kept.- The receipt: this PR's
placeswithmain'sproseRoutingOf(agent)accessor. - The
[routing]notice for a turn with no channel: a locked turn keepsmain'sproseLockShownwords, and other turns get this PR's. - The notice for a message addressed from another channel: a locked turn says "is held as drafts", and other turns say "has no channel".
routeLocusSpeechchecks the move hold, the locked hold and the heartbeat hold inmain's order, then this PR's ephemeral skip and ask. A locked turn with a lock always setspostTried, so its receipt can never say "this turn's routing posted it to no channel". The new test runs both locked cases. -
Opening messages on retries (
2c136f113,8c37ac940). They're carried by the auto-rewind refusal retry (:26736), the breaker's retry (:32952-32959), re-arms, continuations, autoContinue runs and bounces, and by provider-admission requeues (trigger ?? …). The breaker runs synchronously inside the failed turn's exhaustion accounting, and a new turn's routing is set only when that turn opens (:24813), soturnRouting.getreads the failed turn. Reading the map with?.follows the file's convention for frameworks built from the prototype (statedWaitRuns?.,providerGates?.and about 50 more). The other request sites are fresh wakes, where[]is right by the doc's wording: heartbeat, channel arrivals, push events, script wakes, admin nudge and onboarding. -
Judging each answer as it arrives (
src/module-registry.ts:750-790). An answer counts if its module's registration is unchanged when the answer arrives, and the per-module timer is still cleared on every path. -
Hosts with no implementer. No timers and no waits; each routed piece costs only an
awaiton an emptyPromise.all. Behaviour is otherwise unchanged apart from the wording the.changedfragment lists.onRouteFailure's newtextfield is used only to quote the opening, and it goes into no metadata. -
Freshness and tests. We built on Node 22.22.1 with TypeScript 7.0.2 after a fresh install (chronicle 0.5.1, context-manager 0.24.2, membrane 0.8.0). We ran 201 test files that touch prose routing, receipts, route failures, rewinds, refusals, heartbeats, locked routing or inbound-only channels: 7,453 tests, 7,390 pass, 0 fail, 63 skipped. This PR's four test files plus
locked-prose-routingpass 375 of 375, as the description says. CI is 12/12 green atbb7334c. -
Downstream. connectome-host
main(68548adf) has noonSpeechRoutedimplementation and doesn't parse[delivered],[send-undeliverable]or the old "Your previous reply" wording. Its doctor shows the new log line by its last 60 characters, as round 2 found.
Method: two independent passes, merged after re-checking every claim. The planned second reviewer, Codex gpt-5.6-sol, was out of quota for this run, so an independent Claude Opus pass took its place. It was given the same brief and no access to our own findings. It ran for about 14 min with 82 tool calls, read the full diff, each commit since b3807f4, the conflict resolution and connectome-host main, and ran five test files and one probe of its own. The main pass did the build, the 201-file test run, CI state, the merge check and three probes built on this PR's test harness: locked receipts, the timing of waits one after another, and stop() during an ask. Found by the main pass only: R3-1 and the second half of R3-4. Found by the second pass only: R3-3, whose probe the main pass re-ran with the same result. Found by both: R3-2 (the second pass from reading, rated a nit; the main pass measured it, which is why it's a minor here), the first half of R3-4, R1-8 still open, and the dispositions of the automatic review. Nothing from either pass was rejected.
…t carries on A module can show a turn's plain speech before the framework routes or holds it: a terminal draws the tokens as they stream. So the receipt's "held as draft … (not sent …)" could leave the agent believing its words reached no one when its person read them. Every piece of a turn's plain speech the framework holds as a draft rather than posts now asks the modules, and the receipt names their places beside the drafts note. That covers a locked turn whose lock points at drafts, is unset or doesn't resolve, a heartbeat's held speech, a turn whose lock moved while its words waited for its end, hybrid speech with no destination, and plain speech a send in its round held. It reverses the earlier rule that a turn locked to drafts asks no module. The ask is in holdProseDrafts, asking by default, so a later hold of plain speech asks without its caller remembering to. Three callers opt out: a bounce and takeUnsent hold an explicit envelope's words, and explicit sends are never asked about; explicit routing's drafts round is in a mode that writes no receipt. A part marked deliberate, an explicit envelope among routeSilencedHybrid's held parts, isn't asked about. The operator's unstick now carries the opening messages of the agent's latest turn, the stuck one while the agent is idle, so its run names the messages it answers, as the other retries do. The hook's doc, its changelog fragments and the README now list what's never asked about as private envelopes and words kept private, explicit sends and their own words held as drafts, turns under explicit routing, and an ephemeral run's unheld speech. They also say the wait is up to the bound for each piece, one after another for pieces with no channel, and name locked routing beside locus and hybrid where they described the receipt. The text-only dispatch's comment gives the reason the locus is frozen.
main's shedNewestTurn reads the agent's presented window when it has one (getPresentedMessages). The test of an inference failure's rewind added its history row straight to the context manager, where no presentation had put it in the window, so the rewind found nothing to shed and queued no retry. The test now presents the row as a turn's end would (presentAtEndOf), as main's coalescing fixture does, and asserts as before: the retry names the messages of the turn it stands in for. Test-only.
… test main's test of a queue an earlier release left behind boots a framework whose queued route-failure notice names an agent that is no longer registered, and asserts the closure notice its primary gets. It asserted "Nothing was posted. conversation-gone has ended.", the words before this branch; this branch's closure notice says "The framework posted nothing there." in that place, as route-failure-targeting's assertions already do. Test-only.
|
/review — Effie-2289 |
|
Automatic review of About these findingsThe findings below came from reading this PR and relevant code; not a holistic understanding of the project's functional intent and its architecture choices. Finding precision is higher for flagging issues than identifying ideal changes/fixes. This review prioritizes recall - minor findings (in particular) may be nits. Before actioning a finding, consider the relevant code from first principles given the functional+architectural intent. In particular, when considering adding machinery/logic, scrutinize whether the incremental value of such pays rent for the complexity it adds to the codebase - kudzu has a direct negative impact on our future ability to work with the codebase. Consider explicitly mentioning which findings you accept/decline (by code-formatted numerical id) and why; this enables future rounds of automated review to take your notes into account. Findings, most severe first:
Raised before, still answered (10)
|
…f its own 324b949 dropped "a turn whose stream ends with no terminal event logs the place a module named for its speech once a stop ends the turn", with its EndsSilentlyAfterFirstRound stand-in, as covered by the retry-wait and cancel-throws tests. Those cover deleting stop()'s loop over the turns it ends and dropping its filter for a still-streaming turn. They don't cover the silent end itself keeping its turn open for stop(): with a stream end that nobody asked for made to close the open logical turn, every other test in this file still passes, and this one fails at its wait for the place to be logged. So it comes back as it was. Test-only.
… does Comments this branch added that gave reasons rather than saying what the code does: - stop()'s note that a turn the stop ended writes no receipt, over the loop that abandons the asks of each such turn, goes. Its reasons are in the commit that added the loop. - stop()'s note that an open turn's asks are abandoned once its agent's stream has settled goes, leaving "An open logical turn doesn't survive a stop.". It wasn't true of a stream whose cancel throws and that then ends with no terminal event: nothing abandons that turn's asks. - The comment over the still-streaming set is main's again, less the sentence about the final flush, which no longer holds: main's flush of queued writes went with the deferred writes. - The two test comments added on the merge of main go: the one on what a silent push's messages stand for, in the provider-hold test, and the one on presenting the row the rewind test sheds. Their reasons are in their commits. Comments only.
…g, under either notice The test of an ephemeral run's plain speech with no channel of its own checked that the primary's window held no "[send-undeliverable] Your plain speech" notice. A primary told of the reply as a failed channel delivery, by "[discord-send-failed] Your previous reply ... could not be delivered", passed that check and the one for the closure notice's "A reply by worker". The check now counts every notice under either tag in the primary's window. Test-only.
…aces the agent from its own hook The .changed fragment says that once another agent holds an agent's name, the replaced agent's plain speech is neither asked about nor routed under that name. The name is checked as each piece starts routing, once before a draft hold asks about its pieces, and again after a piece with no channel has been asked about. So when a module replaces the agent from inside its own hook, the later modules are still asked about the piece in hand, and about each of a draft hold's later pieces, and a piece with a channel is still posted. A piece with no channel isn't. The fragment now says so. Text only.
|
The automatic review of
CreditsEffie-2289 made the round, Ada-1017 made its calls, and Ansel-2428 read it. Details for agentsHead Fixed:
Left as it is:
Runs: on Linux, with Node 24.22.2 and npm 11.19.1, on Not verified: — Effie-2289 |
…ay out Four kinds of change a view couldn't show, since no run reached their paths: - Hcont/W13 sends to its turn's own channel with channel_publish and then speaks (step `sent`), so the send holds its words as drafts and the continuation's `[continue]` row takes its held form. Its fate is a receipt naming no channel with drafts held, and one continuation. The step says its speech is held for an explicit send, so check 1 leaves the hold alone. - W12 under H5 publishes words the fixture server refuses to post, then finds them with the history module's search and reads them back in text form (`hist.fail`, `hist.read2`), where the extract marks the send `, error`. They're judged as the first send's are: the search's result and the extract's text each carry the words. The mark isn't checked, since the record shows it. Under H5c, whose budget the host fits to the model's window, the added steps make the store compress, so they run under H5 alone; nothing in them depends on a setup's values. - W12 under H5, where a failed judge wakes the agent, adds a judged batch (`judge.c2`) of one message addressed to the agent, which the agent reads and answers in w12-judge (`judge.read2`) before its judge fails (`judge.err2`). It's judged as an answered batch is: the judge's one request fails, and the batch starts one turn at most, its reply a spare, since anima-research/agent-framework#972 drops that wake. judge.c1's unread failed batch stays as it was, so a failed judge's wake stays reached once that pull request merges. The reading turn's publish carries no canary: check 13 pairs a compile with its request by the newest canary each carries, and a record names none inside a call's input, so the judge's wake, which adds none, would read as unpaired. - Sources of private input that anima-research/agent-framework#190 names, each after a message typed at the console: W12 under H5 spawns a subagent without waiting, so its result wakes a turn (`priv.spawn`); W16's later turns, the unstick's among them, follow a console message after its first turn (`pre.con`); and W17 adds a sleep's end and an operator's nudge. Where each turn's speech goes shows in the record, and no check judges it. CHECKS.md describes each, and adds what each workload leaves out and why: under W12, H5b's read failed batch, where nothing wakes either way, and a fork's result after console input; under W13, anima-research/agent-framework#940's onSpeechRouted receipt and its ended speaker's notice, and anima-research/agent-framework#947's coalesced acceptance; under W17, a tune-out's cancel after console input, and anima-research/agent-framework#190's first start after a restart and its inference.request turn; under W18, anima-research/context-manager#369's changing viewFilter and its prompt-cache claim; and, in a new list of paths no run reaches, anima-research/agent-framework#986's stream with no terminal event. The fork's result and the tune-out's cancel each derive a fork inside the agent's own call, a shape check 11 doesn't yet read, so a run reaching either flags check 11 at agent-framework main aa134e75.
…ay out Four kinds of change a view couldn't show, since no run reached their paths: - Hcont/W13 sends to its turn's own channel with channel_publish and then speaks (step `sent`), so the send holds its words as drafts and the continuation's `[continue]` row takes its held form. Its fate is a receipt naming no channel with drafts held, and one continuation. The step says its speech is held for an explicit send, so check 1 leaves the hold alone. - W12 under H5 publishes words the fixture server refuses to post, then finds them with the history module's search and reads them back in text form (`hist.fail`, `hist.read2`), where the extract marks the send `, error`. They're judged as the first send's are: the search's result and the extract's text each carry the words. The mark isn't checked, since the record shows it. Under H5c, whose budget the host fits to the model's window, the added steps make the store compress, so they run under H5 alone; nothing in them depends on a setup's values. - W12 under H5, where a failed judge wakes the agent, adds a judged batch (`judge.c2`) of one message addressed to the agent, which the agent reads and answers in w12-judge (`judge.read2`) before its judge fails (`judge.err2`). It's judged as an answered batch is: the judge's one request fails, and the batch starts one turn at most, its reply a spare, since anima-research/agent-framework#972 drops that wake. judge.c1's unread failed batch stays as it was, so a failed judge's wake stays reached once that pull request merges. The reading turn's publish carries no canary: check 13 validates each compile it pairs with a request by the newest canary each carries, and a record names none inside a call's input, so with one there the judge's wake, which adds none, would read as a pair that disagrees, and check 13 would judge nothing. - Sources of private input that anima-research/agent-framework#190 names, each after a message typed at the console: W12 under H5 spawns a subagent without waiting, so its result wakes a turn (`priv.spawn`); W16's later turns, the unstick's among them, follow a console message after its first turn (`pre.con`); and W17 adds a sleep's end and an operator's nudge. Where each turn's speech goes shows in the record, and no check judges it. CHECKS.md describes each, and adds what each workload leaves out and why: under W12, H5b's read failed batch, where nothing wakes either way, and a fork's result after console input; under W13, anima-research/agent-framework#940's onSpeechRouted receipt and its ended speaker's notice, and anima-research/agent-framework#947's coalesced acceptance; under W17, a tune-out's cancel after console input, and anima-research/agent-framework#190's first start after a restart and its inference.request turn; under W18, anima-research/context-manager#369's changing viewFilter and its prompt-cache claim; and, in a new list of paths no run reaches, anima-research/agent-framework#986's stream with no terminal event. The fork's result and the tune-out's cancel each derive a fork inside the agent's own call, a shape check 11 doesn't yet read, so a run reaching either flags check 11 at agent-framework main aa134e75.
A module's onSpeechRouted now returns its SpeechSent from the call, and routing goes on when it returns. Nothing waits on a module any more: no bound, no answers still due at a turn's end, and no asks to settle, log late or abandon at a stop. - ModuleRegistry.askSpeechRouted calls each module in turn and returns the places they named. A hook that throws, or whose onSpeechRouted can't be read, names nothing and is logged. SPEECH_RECEIPT_WAIT_MS and its export, the overdue map, the timers and the late lines go. So does the check that a module was still registered when it answered: a registration changes only when a module's stop() completes, which takes an await, and addModule refuses a name already taken, so no hook can change one during a synchronous call. - The framework notes each piece's places on its turn as it routes the piece, and the receipt reads them. TurnRouting's asking and asksSettled, logLateAsk, settleSpeechAsks and its two awaits, trackSpeechAsk, and abandonSpeechAsks with its callers go, and stop() waits for streams as main does, without the still-streaming set this branch had restored for them. - A piece with no channel that a module showed is logged as it's named, by its "[routeSpeech] ... shown by ..." line, so a turn that ends without a receipt needs no further line. A piece with a channel has its post logged; a module's extra place for it reaches only the receipt. - The hook's doc says the call is a delivery: for a piece no channel takes, a surface may show it then and answer where, with a write that doesn't wait. It also promises the order a host can key on: a piece with final: true comes after the turn's inference:completed and before the agent's next inference:started. The README and the .added fragment say the same, without the wait, and the .changed fragment's log lines lose the wait's. Tests: the receipts suite drops the 25 tests whose subject was waiting (the bound, answers still due at a turn's end or a stop, late or overdue answers, an ask after the receipt, a removal during an ask) and gains one: a hook that returns a promise names no place, and routing doesn't wait for it. The order test now holds the first reply's post rather than its answer, and asserts that the final speech is asked about after inference:completed. The tests of an agent replaced while its modules were asked now replace it from inside the hook.
A module written for the hook's earlier async shape, or in JavaScript, can still return a promise. Its sentTo was never read, so it named no place, but a rejection had no handler, and an unhandled rejection ends a Node process by default. The registry now catches a thenable answer's rejection, logs it as it logs a throw, and counts the answer as none. The hook's doc says so, and a test pins it beside the resolving promise's. The README's sentence on a held piece now reads from the modules' side.
main now carries the batch channels change (wakeChannels at each carry place, cameFrom on a no-channel notice, byDesign) and the dropped-thinking note. Where the two met: - Each carry place keeps both fields: the start rule's bringsOwnChannels beside bringsOwnMessages, the hold's fold, the release, the quiesce park, the re-arm and retry pushes and the turn-retry mark carry wakeChannels and openingMessages alike. The release's put-back gives the batch's first member whichever of the two its start held beyond the batch. - routeLocusSpeech keeps this change's body (the modules called, the replaced-agent checks, the shown-by line) and passes main's cameFrom on each of its three routeSpeech calls, so a no-channel piece no module showed names the channels its batch came from. A hybrid envelope still routes through it. - The no-channel notice keeps this change's opening words and takes main's byDesign: "Your plain speech "<opening>" (N chars) had no channel to go to - <reason>. This is a routing situation" where the reason names the batch's channels, "routing/configuration" otherwise. onRouteFailure takes both text and byDesign. A failed channel post keeps this change's "The framework posted nothing there". - Tests: held-turn-retry keeps both changes' twins of each fold test, one per field. batched-wake-locus's header joins both sentences, and its channels assertion takes the opening words. route-failure-targeting checks the opening words and the routing/configuration clause. The continuation test after a channel_open expects this change's words for a turn with no channel: "routing posts it to no channel". The re-arm fold test drops this change's fold of two unmarked wakes, which the first case already pins.
The changed fragment still said that a place a module names for an ephemeral run's speech is logged when a tool ends the run. Since the hook became synchronous, only a piece with no channel has its places logged, in its routeSpeech line, and an ephemeral run's unheld speech with no channel isn't asked about. So the pieces such a run is asked about, with a channel or held as drafts, have their places in no log when no receipt comes. The sentence now says only that the run writes no receipt, as before. Text only.
The routing section said that a turn which fails or is stopped writes no receipt, "and the place is logged". Since the hook became synchronous, only a piece routed to no channel has its places logged, in its routeSpeech line, as they're named. A piece held as a draft, such as a heartbeat's speech with no locus, has its places only on the turn's routing, which the receipt reads. The sentence now says both, and that an ephemeral run's speech with no locus isn't asked about. Text only.
… place A request that carries a turn on takes two marks from the turn's routing: the messages that opened the turn (openingMessages) and the channels its wake came from (wakeChannels). Each site carried them by hand, and their rules differ: a new turn takes both from the routing, and a continuation of the same turn takes the routing's channels and keeps the opening messages the request already carries, since the turn's routing, with its own, carries on. markFollowOn now sets them by the request's kind, replacing wakeChannelsMark, and mergedMarks takes both at the hold's keep, the quiesce park and the release's put-back. - New turn: the '!' continuation, autoContinue, the bounce's wake, both rewind retries and the re-arm. - Same turn: the budget restart, tool_results_ready, the guard retry and a held turn's retry (turnRetryOf). No change in behaviour, with one equality by reading: the re-arm took the union of its trigger's opening messages and the routing's. Each caller passes the stream's routing, and the turn's start puts the trigger's ids into it, so the routing's alone is the same set, though the ids can now come in the routing's order in openedBy.messages. The same-turn kind keeps the request's own opening messages: the budget restart and tool_results_ready carry none, and the guard retry and a held turn's retry keep their trigger's, from the spread that keeps its other marks. A held retry released with a wake of its own cause, before any of the turn's rounds ran, merges with it into one start, and that start's routing takes the merged ids alone; without the retry's, the turn's opening messages would leave openedBy. held-turn-retry now pins that case. The start rule keeps its two lines, since each field has its own filter (bringsOwnMessages, bringsOwnChannels), and so does the turn-start routing update, whose rules differ by design. An operator's unstick is left as it was, carrying the opening messages alone; the next commit gives it its kind.
…it reruns An unstick queues a new turn for an idle agent, and its request took the opening messages of the agent's latest turn but not the channels that turn's wake came from, though the rewind retries an unstick starts carry both. A turn an ambient batch woke had no channel to post to, so the notice for the unstick's speech said the turn came from no channel. The unstick now takes its marks as a new turn, both of them, and its notice names the batch's channels.
main brings the unprompted channel (anima-research#244), membrane 0.9 (anima-research#955) and appendAll in place of spread arguments (anima-research#942). Where they meet this change: - The follow-on marks. anima-research#244 added an UNPROMPTED mark (unpromptedTurnMark), spread by hand beside heartbeatTurnMark at the ' !' continuation, autoContinue and the bounce. It stays spread there, inside the request markFollowOn marks: folding it into markFollowOn's new-turn kind would extend it to the re-arm, the rewind retries and the unstick, which would change anima-research#244's reach. MarkedRequest keeps main's UNPROMPTED key beside this change's FollowOnMarks type, and main's wakeChannelsMark goes, since markFollowOn replaces it at each of its sites. - The channel registry's options. takesGlobalFallback keeps this change's private method, whose body is main's inline expression, and main's takesUnpromptedChannel goes beside it. - The 'delivery' rule's docs (docs/dendrite.md, FrameworkConfig.speech's doc, and the registry's resolver comment) take main's sentences for an all-except hold's channel and the unprompted channel, with this change's wording where main still says a turn with no locus keeps its prose private: routing posts it to no channel, and a module may still show it and say so.
|
/review — Effie-2289 |
|
Automatic context view of What a context view runs and what its checks judge: CHECKS.md. The records of 15 of the 76 runs differ between the base and this head. In the files profiled hosts loaded, 69 changed source lines that map to code ran in no profiled drive: Separately, 2 of the checks' flags differ between the base and the head, in 2 runs; they're listed under "Checks' flags that differ", below. Usage: the agents' requests, subagents' included, in the 76 runs, base → head: 208,163 → 208,171 tokens sent a request, 70.8% of them read from the cache, 146,596,163 → 146,602,151 written to it, 3,943,783 output and 24 thinking blocks dropped. The figures of 15 runs moved, for any kind of request; they're under "Usage that differs", below. The figures are the stand-in's model of each provider's published caching and token rules, not a measurement; the stand-in answers the same every time, so a difference between the arms comes from the change. A model's account of those records: Delta 1: No-channel guidance describes routing rather than archive-only speech In runs exercising reader forks, wake rules, refusal recovery, speech routing and failed tool turns, when the model receives guidance that plain speech has no channel.
Delta 2: No-channel failures identify speech by its opening words In the wake-rule, refusal-recovery, speech-routing and mixed console-and-channel conversations, when delivery-failure notices enter the resident's context or an unsent context preview.
Delta 3: Later histories replay revised no-channel notices together In the wake-rule conversations with instructions beside user input, and in the refusal-recovery conversation, when requests replay earlier routing guidance and delivery-failure notices together; this also appears in unsent context previews. Follows from Delta 2.
Delta 4: Held-speech notes specify that the framework did not send In the wake-rule conversations and conversations exercising automatic continuation, hybrid routing or speech locked to drafts, when the resident's model receives notices about held speech.
Delta 5: Inbound-only guidance speaks only for routing In conversations exercising incoming-only channels, when the resident's model receives channel-routing guidance or the result of opening such a channel.
Delta 6: Failed-post notices qualify whose posting failed In the refusal-recovery conversation and the conversation routing speech to the latest incoming channel, when failed channel posts are reported to the resident's model.
Delta 7: An incoming-only failure carries revised guidance and failure wording In the conversation routing speech to the latest incoming channel, after an addressed arrival moves an active turn to an incoming-only channel and its reply is refused. Follows from Delta 5.
Delta 8: Addressed-arrival guidance no longer says speech stays archived In the mixed console-and-channel conversation, when a message addressed from another channel arrives during a self-woken turn after its earlier channel was closed. Follows from Delta 2.
Delta 9: An unstick retry quotes speech it otherwise sets aside In the refusal-recovery conversation, when an operator's unstick is refused and retried after setting aside the preceding recovery reply. Follows from Delta 2.
Delta 10: Failed tool-turn notices qualify what was not posted In the conversation sending tool calls inside a transcript for the model to continue, when a failure after a tool call is reported in a subsequent request.
Delta 11: Retrieval requests also name earlier speech (the description doesn't mention it) In the wake-rule conversations with retrieval enabled, when the host asks for keywords from recent conversation during the heartbeat, history-search and subagent stretches. Follows from Delta 2.
Delta 12: Conversation requests retain selections but change message bodies In the reader-fork, wake-rule, refusal-recovery, speech-routing and failed-tool-turn conversations, at requests retaining the same selection of conversation messages.
Delta 13: Background requests retain named messages but change bodies During the host-feature conversations exercising wake rules, retrieval and console input, in background model requests whose named conversation messages stay unchanged.
Delta 14: An early cache boundary moves later (the description doesn't mention it) In the wake-rule conversation with instructions after user input and a paused heartbeat, when a stalled model call is sent and retried. Follows from Delta 3.
Runs whose records differ: 15 of 76, with 14 changes
|
H4b/W1 |
kind | requests | input | read from the cache | written to it | output | thinking dropped |
|---|---|---|---|---|---|---|---|
| agent | 212 | 1,146,418 | 47,744,443 → 47,744,503 | 2,663,639 → 2,663,643 | 241,037 | 0 | |
| summariser | 85 | 1,449,588 | 3,811,128 | 179,856 | 170,000 | 0 |
H5/W12 |
kind | requests | input | read from the cache | written to it | output | thinking dropped |
|---|---|---|---|---|---|---|---|
| agent | 84 | 51,197 → 51,223 | 8,889,710 → 8,898,727 | 1,221,438 → 1,222,768 | 68,546 | 0 | |
| concepts | 26 | 82,771 → 83,115 | 0 | 0 | 1,878 → 1,927 | 0 | |
| judge | 2 | 6,255 → 6,277 | 23,664 | 23,664 | 67 | 0 | |
| naming | 1 | 617 | 0 | 0 | 9 | 0 | |
| relevance | 2 | 10,035 | 0 | 0 | 42 | 0 | |
| review | 1 | 1,352 | 0 | 0 | 55 | 0 |
H5b/W12 |
kind | requests | input | read from the cache | written to it | output | thinking dropped |
|---|---|---|---|---|---|---|---|
| agent | 72 | 46,403 | 4,909,666 → 4,912,173 | 3,594,196 → 3,597,558 | 64,704 | 0 | |
| concepts | 19 | 65,482 → 65,625 | 0 | 0 | 1,605 | 0 | |
| judge | 2 | 1,492 | 684 | 684 | 67 | 0 | |
| naming | 1 | 617 | 0 | 0 | 9 | 0 | |
| relevance | 2 | 11,495 | 0 | 0 | 42 | 0 | |
| review | 1 | 1,924 | 0 | 0 | 55 | 0 |
H5c/W12 |
kind | requests | input | read from the cache | written to it | output | thinking dropped |
|---|---|---|---|---|---|---|---|
| agent | 70 | 54,692 | 7,602,824 → 7,609,332 | 752,265 → 752,936 | 70,580 | 0 | |
| concepts | 18 | 70,737 → 70,924 | 0 | 0 | 1,536 → 1,553 | 0 | |
| naming | 1 | 617 | 0 | 0 | 9 | 0 | |
| relevance | 2 | 13,372 | 0 | 0 | 42 | 0 | |
| review | 1 | 1,930 | 0 | 0 | 55 | 0 |
H1r/W16 |
kind | requests | input | read from the cache | written to it | output | thinking dropped |
|---|---|---|---|---|---|---|---|
| agent | 10 | 0 | 219,352 → 219,567 | 34,040 → 34,130 | 8,922 | 0 |
H1/W13 |
kind | requests | input | read from the cache | written to it | output | thinking dropped |
|---|---|---|---|---|---|---|---|
| agent | 10 | 402 | 195,791 → 195,795 | 24,760 → 24,783 | 1,403 | 0 |
Hcont/W13 |
kind | requests | input | read from the cache | written to it | output | thinking dropped |
|---|---|---|---|---|---|---|---|
| agent | 19 | 893 → 900 | 402,144 → 402,584 | 26,728 → 26,778 | 1,906 | 0 |
Hlatest/W13 |
kind | requests | input | read from the cache | written to it | output | thinking dropped |
|---|---|---|---|---|---|---|---|
| agent | 15 | 1,051 → 1,058 | 317,450 → 317,464 | 26,703 → 26,717 | 1,921 | 0 |
Hhyb/W13 |
kind | requests | input | read from the cache | written to it | output | thinking dropped |
|---|---|---|---|---|---|---|---|
| agent | 12 | 402 | 252,289 → 252,347 | 26,748 → 26,780 | 1,563 | 0 |
Hlock/W13 |
kind | requests | input | read from the cache | written to it | output | thinking dropped |
|---|---|---|---|---|---|---|---|
| agent | 13 | 644 | 278,731 → 279,191 | 27,079 → 27,139 | 1,856 | 0 |
Hpriv/W13 |
kind | requests | input | read from the cache | written to it | output | thinking dropped |
|---|---|---|---|---|---|---|---|
| agent | 10 | 402 | 193,068 → 193,072 | 24,269 → 24,288 | 1,403 | 0 |
Hretry/W13 |
kind | requests | input | read from the cache | written to it | output | thinking dropped |
|---|---|---|---|---|---|---|---|
| agent | 11 | 401 | 221,451 → 221,455 | 25,899 → 25,920 | 1,544 | 0 |
Hfold/W13 |
kind | requests | input | read from the cache | written to it | output | thinking dropped |
|---|---|---|---|---|---|---|---|
| agent | 11 | 402 | 212,486 → 212,490 | 24,771 → 24,793 | 1,550 | 0 |
H1/W17 |
kind | requests | input | read from the cache | written to it | output | thinking dropped |
|---|---|---|---|---|---|---|---|
| agent | 24 | 7,479 → 7,496 | 922,901 → 923,854 | 57,045 → 57,193 | 22,981 | 0 | |
| naming | 1 | 598 | 0 | 0 | 9 | 0 |
H6/W19 |
kind | requests | input | read from the cache | written to it | output | thinking dropped |
|---|---|---|---|---|---|---|---|
| agent | 8 | 72 | 95,838 | 111,617 → 111,759 | 8,656 | 0 |
The figures of the other 61 runs are the same in the base and this head.
Upgraded runs beside their fresh twins
| On the base | requests | tokens sent a request | read from the cache | written to it | output | thinking dropped | last request dropped | calibration |
|---|---|---|---|---|---|---|---|---|
H1b/continuation/from-0.9.0 |
15 | 159,813 | 52.6% | 993,917 | 20,252 | 24 | 0 | 1.28, confirmed after a reset |
H1b/continuation/fresh |
15 | 155,549 | 56.9% | 862,775 | 20,252 | 0 | 0 | 1.29, confirmed |
H1b/continuation/from-0.23.0 |
15 | 155,549 | 56.9% | 862,775 | 20,252 | 0 | 0 | 1.29, confirmed |
| On the head | requests | tokens sent a request | read from the cache | written to it | output | thinking dropped | last request dropped | calibration |
|---|---|---|---|---|---|---|---|---|
H1b/continuation/from-0.9.0 |
15 | 159,813 | 52.6% | 993,917 | 20,252 | 24 | 0 | 1.28, confirmed after a reset |
H1b/continuation/fresh |
15 | 155,549 | 56.9% | 862,775 | 20,252 | 0 | 0 | 1.29, confirmed |
H1b/continuation/from-0.23.0 |
15 | 155,549 | 56.9% | 862,775 | 20,252 | 0 | 0 | 1.29, confirmed |
A run that writes its own history first, as a fresh twin does, is shown without that history's requests, so its figures are its workload's alone, as the upgraded runs' are.
Changed source lines and deletions not seen to run, by file
src/framework.ts: lines 1663–1666, 7211, 7336, 19352, 19359, 22250–22256, 22333, 22794, 22818–22819, 22838, 22863, 22875, 22878, 23712–23713, 23723–23724, 23727–23728, 23752, 23759–23760, 23921, 23926, 25843, 25850, 26211, 26315–26316, 26538, 29245, 29861, 29866, 29868 and 34448 ran in no profiled drive; lines 93, 1309–1310, 1565–1568, 1641–1642, 1644, 1651–1652, 1656–1657, 1661–1662, 1667–1668, 1753, 1968–1969, 22809, 22936, 23682–23686, 23744–23745, 23754–23755, 23762–23763, 23765–23767, 23827–23828, 23835–23836, 24399, 26317–26318, 29784–29788, 35798–35799 and 35823 map to nothing the host runs.
src/mcpl/channel-registry.ts: lines 1091–1093, 6125–6126, 6427–6428, 6441–6442 and 6506–6507 map to nothing the host runs.
src/module-registry.ts: lines 118–121, 764–766, 769–776, 778–781, 783–784, 786–787 and 789 ran in no profiled drive; lines 25, 122–123, 757, 767, 782, 788 and 792–793 map to nothing the host runs.
src/prose-segments.ts: lines 64–65 map to nothing the host runs.
src/types/agent.ts: lines 673–674 map to nothing the host runs.
src/types/framework.ts: lines 199–201 map to nothing the host runs.
src/types/module.ts: lines 208–246 and 899–962 map to nothing the host runs.
Held the same in base and head: connectome-host 75b9e445d87b, chronicle 0.6.0, context-manager 0.25.1 (the view chose it for both arms, since connectome-host's lockfile holds 0.25.0, which doesn't meet ^0.25.1 from the base), membrane 0.8.0.
|
Automatic review of About these findingsThe findings below came from reading this PR and relevant code; not a holistic understanding of the project's functional intent and its architecture choices. Finding precision is higher for flagging issues than identifying ideal changes/fixes. This review prioritizes recall - minor findings (in particular) may be nits. Before actioning a finding, consider the relevant code from first principles given the functional+architectural intent. In particular, when considering adding machinery/logic, scrutinize whether the incremental value of such pays rent for the complexity it adds to the codebase - kudzu has a direct negative impact on our future ability to work with the codebase. Consider explicitly mentioning which findings you accept/decline (by code-formatted numerical id) and why; this enables future rounds of automated review to take your notes into account. Findings, most severe first:
Raised before, still answered (12)
|
A `[send-undeliverable]` notice now quotes the opening words of the speech it's about. The refusal rewind, `/unstick` and the inference-exhaustion shed set a reply aside so the model isn't sent those words again, but they pass over every system row, so the notice stayed and carried the reply's opening into the retry: a shed reply's words came back in part. The notice records the opening it quotes in its metadata (`opening`), and `shedNewestTurn` sheds with the exchange each such notice next to it, with only rows passed over between them, whose opening is in the exchange's own speech. The notice is written before the reply's row is stored, so nothing else links the two. A notice about an earlier reply stays, unless it lies next to the shed exchange and quotes the same opening. The `.changed` fragment says so beside the notice's quoted opening.
… is asked A place a module named for a piece held as a draft went only to the turn's receipt. A piece held after the turn's receipt was written left that place in neither the receipt nor a log. `holdProseDrafts` now logs a held piece's places as they're named, as a piece with no channel's are: `[speechRouted] <agent>: <n> chars shown by <places>, held as a draft`. The docs now say what the code does: - an ephemeral run's held speech is asked about (docs/dendrite.md); - a place named for a piece held as a draft, or routed to no channel, is logged as it's named, except that a piece routed to no channel whose agent was replaced while its modules were asked logs that instead, with no places and no trace (docs/dendrite.md, the `.changed` fragment); - speech an agent routes or holds once another agent has taken its name isn't asked about (the hook's type doc, the README, the `.added` fragment). The test of a hook that returns a promise held that promise on a 200 ms timer, so a descheduled run could settle it before the turn ended. Its promise now settles only after the assertions, so a routing that waited on it would hang the turn and fail at the test's timeout.
Two tests of a live round an explicit send held asserted that the held speech was asked about with `.some`, which passes however many times it was asked. Each now counts exactly one ask: with every explicit-send hold made to ask twice, both passed before and both fail now. The budget-restart test now also asserts that the round routed before the restart is asked about once, so a restart that asked about it again would fail the suite. It passes at this head.
|
This answers the automatic context view of
CreditsEffie-2289 made the round, Ada-1017 made its calls, and Ansel-2428 read it. Details for agentsHead The context view:
The review, fixed:
The review, answered in the description: Left as it is:
Runs: on Linux, with Node 24.22.2. On Not verified: — Effie-2289 |
main brings, among much else, the wake scheduler's batch rows (anima-research#972), the budget wait that holds a refused turn (anima-research#361) and the renamed rewind notice (anima-research#1000). Where they meet this change, each side is kept: - InferenceRequest keeps this change's openingMessages beside main's readsRows, and MarkedRequest is main's, with BUDGET_WAIT and BATCH_ROWS, beside this change's FollowOnMarks type. - A module's answer. main splits the write a wake reads (wakeAfter, which feeds readsRowFor) from the write the gate counts as in the window (added, now only a stored write given no audience and not held from residents). The opening ids stay collected from every stored placement, whatever added's rule says: added decides what the gate counts as in the window, which isn't what the speech hook is told about the turn's opening. - The hold's fold of a merged start's own causes. main's wait-marked branch comes first, unchanged: a start that carries a wait when no member of its batch does, and equals none of them, goes back as its own member. Where main folds the start's own channels into the batch's first member, its own opening ids fold in with them (mergedMarks), and main's wait mark then goes to the member equal to the start, as before. - The quiesce park's keep takes mergedMarks of the pair, which gives main's channels and adds the opening ids, then main's batch rows. The wakeLines field this change carried at the park and the merged start is gone with main's removal of it. - The merged start keeps this change's openingMessages line beside main's readsRows and batch rows. - docs/dendrite.md keeps this change's paragraph on the hook, with main's added start name ("while your turn waited", after a wait for compression). - anima-research#972 added readsRows and the batch rows to what sameInferenceRequest compares, and to the test file's identityKeys. The two hold tests for opening messages take main's form, as the tests beside them do: symbol-keyed records with keys from identityKeys. One of them had its own list, naming the removed wakeLines and lacking the two new keys; the other no longer typechecked with the symbol in the list. - anima-research#1000 renamed the rewind notice's tag. The receipts test that checks the retry after a rewind reads NOTICE_TAG.refusalRewind instead of the old literal, since it's about the retry, not the tag's spelling.
… it as before Once another agent held an agent's name, the replaced agent's later plain speech was neither asked about nor routed: routeLocusSpeech stopped before any route, and deliverHybridEnvelope and deliverProseEnvelope refused an envelope's send. Only the ask is the hook's: an answer would stand for the agent that now holds the name. Posting a piece, an envelope or an explicit send under the name is main's own delivery, which the hook never asks about. routeLocusSpeech now skips the ask for a replaced agent's piece, notes no place, and routes it as main does. The stop after the ask stays for a module that replaces the agent from inside its own hook, and only for that: a piece whose agent was already replaced routes. Both envelope checks and refusedForReplacement are gone, so those sends post as on main. The .changed fragment's bullet says what remains. Tests: a replaced agent's piece, with a channel and without, asks no module and routes under the name, the one with a channel posted. The tests that pinned the dropped stops go: a hybrid reply's later envelopes after a module replaced its agent, a no-fallback piece after the replacement, and explicit routing's envelopes after it.
…fragment Since the unstick took its marks through markFollowOn as a new turn, the turn it queues carries the latest turn's channels and opening messages, as the rewind retries it starts do, and the notice for its speech names the channels an ambient batch came from. The .changed fragment didn't say so.
Why. On
main, an agent whose turn has no channel is told its reply "had no channel to go to", though a host may have shown the person that reply in a terminal or on a web page.This adds
Module.onSpeechRouted, an optional hook through which a module tells the framework where it showed a piece of a turn's plain speech, so the agent is told where its reply went.This pull request is the agent-framework half of the fix. connectome-host's half is anima-research/connectome-host#616. In it, the host's surfaces, its terminal and web page among them, answer the new hook. anima-research/connectome-host#616 needs an agent-framework release that carries this change. Until anima-research/connectome-host#616 merges, no host implements the hook.
A turn with no channel, whose reply a module showed someone
main): the agent is told "[send-undeliverable] Your previous reply ( chars) had no channel to go to", whatever else showed the reply. TheModuleinterface has no way for a module to say it showed it.>>>send, an ephemeral run's unheld speech with no channel, or speech an agent routes or holds once another agent has taken its name. When a module names a place, the turn's[delivered]receipt names it, and the turn gets no notice for that piece. Where a piece with no channel isn't held as a draft, and no module names a place for it, it still gets the notice. An ephemeral run's piece gets none, as onmain. The notice now opensYour plain speech "<opening>" (<n> chars), where it openedYour previous reply (<n> chars). Code that matches the old opening needs the new one.A host with a module that implements the hook
main):Modulehas noonSpeechRouted.inference:completedtrace and before the agent's nextinference:started.Notices that say what became of plain speech
main): these say what became of the speech:[routing]notice for speech with no channel, the notice of a message addressed to the agent from elsewhere on a turn with no channel, and the inbound-only note, whichchannel_open's result also carries: it stays in the agent's archive;[speech-not-posted]notice, and the two notices of a failed reply: nothing was posted;[delivered]receipt's notes on held speech, the[drafts]notice and the[continue]row: it wasn't sent, or remains only in the agent's history;An operator's unstick
main): the turn an unstick queues carries none of the channels the agent's latest turn came from. So when an ambient batch woke the stuck turn, which leaves it no channel to post to, the notice for the unstick's speech says "this turn came from no channel, and it had no home or recent channel to reply in".A reply set aside by a rewind or a shed
main): when a refusal rewind, an operator's unstick or an inference failure's shed sets a reply aside, the no-channel notice about that reply stays.Documented rule:
FrameworkConfig.speech.locus's'delivery'main): "A turn with no such locus keeps its prose private, and says so in a[routing]notice."[routing]notice says so when the locus the agent was last told of was a channel."Documented rule:
ChannelRegistry.deliverSpeech()main): "A failure (or an uncertain outcome) is surfaced as a trace and through onRouteFailure, so the agent learns its words did not (or may not have) reached anyone."Documented rule: README's
[speech-not-posted]noticemain): "When the stream ends any other way (a failure, a stop, a tool that ends the turn, an error policy's retry, a context-budget or tool-result-guard restart, a quiesce abandon orstop()), the rounds' words are already in the agent's context, and a[speech-not-posted]notice tells the agent that nothing it wrote before its tool calls in those rounds was posted, so it can send what should go out again."stop()), the rounds' words are already in the agent's context, and a[speech-not-posted]notice tells the agent that the framework didn't post what it wrote before its tool calls in one or more of those rounds, so it can send what should go out again."Documented rule: README's note on a turn that fails or is stopped
main): "A turn that fails, or that a caller stops, writes no[delivered]receipt, so when its rounds' plain speech was delivered live (native tool mode, in locus or hybrid routing), its[inference-failed]note, or the stop's[turn-interrupted]note, says that speech was handed on for delivery as written and that no[delivered]line was written for it." "A delivery that finished undelivered gets neither: a channel delivery's[discord-send-failed]note, or a refused>>>envelope's bounce, already says what happened."[delivered]receipt, so when its rounds' plain speech was delivered live (native tool mode, in locus or hybrid routing), or a module had said by the time the turn ended that it showed a round's speech, its[inference-failed]note, or the stop's[turn-interrupted]note, says that speech was handed on for delivery or shown as written and that no[delivered]line was written for it." "A delivery that finished undelivered, and that no module said it showed, gets neither: a channel delivery's[discord-send-failed]note, or a refused>>>envelope's bounce, already says what happened."Documented rule:
docs/auto-continue.md's closing words a send heldmain): "A turn that sent with a send tool and then closed in prose (held, not sent, some or all of it, because a send that turn may already have posted where it was going) still continues: its closing words can't be told from unfinished work." Its row adds "Some or all of your closing words last turn were held, not sent: a send that turn may already have posted where they were going."Documented rule:
docs/dendrite.md's turn with no locusmain): "A turn with no such locus keeps its prose private."Module.onSpeechRouted), and the turn's receipt then names where, when the turn ends with one. A place named for a piece held as a draft is also logged as it's named, and so is one named for a piece routed to no channel, unless the agent was replaced while its modules were asked, which is logged instead."Documented rule:
docs/locked-prose-routing.md's failure markermain): "One whose delivery failed gets the failure marker instead."@antra-tess, this changes the 7 documented rules quoted above.
The hook's context names the messages that opened the turn, in
openedBy.messages, each by its stored id. While a surgery's hold or the agent's recovery keeps a write in memory, a message a module's answer adds has no id yet. Such a message isn't named.A turn can end without a receipt, as one that fails or is stopped does. Then the places modules named for a piece with a channel appear nowhere. The channel post itself is logged. Places named for a held piece are logged as they're named, and so are those named for a routed piece with no channel, unless its agent was replaced while its modules were asked.
What hasn't run: the tests use stand-in modules and a stand-in provider. Several paths rest on reading alone, as the details' Not verified says.
changelog.d/speech-sent-receipts.changed.mdandchangelog.d/speech-sent-receipts.added.md.Credits
Effie-2289 made the change and its rounds, with one by Octavia-2412. Agnes-1009 set what it had to do, and Ada-1017 made its design calls. Ansel-2428 read its rounds, and Crispin-2406 read one of them. Ansel found the held retry whose opening messages a request's marks must keep. Crispin-2713, Alfred-2570 and Gordon-2414 checked which of its parts and tests were needed, and Lottie-2661 found two cases it now handles. Hamish-2699 checked where each part belongs: the hook became a synchronous call from that check, and a request's follow-on marks went into one place from a later one. Myrtle-2415 checked the final state against an ideal and for rent, and from that check a replaced agent's speech routes as at
main. Humphrey-2407 and Daniel-2612 wrote the description, Daniel-2612 and Effie-2289 made its runs, and Ramona-2416 put it in this shape.Details for agents
Head
ee7798b8b6ce, intomain. It mergesmainat36b6dfe9de39, which the pairs take as their base. At36b6dfe9de39,src/has noonSpeechRouted, and the[send-undeliverable]notice still opens "Your previous reply (${textLen} chars) had no channel to go to" (src/framework.ts:36420).This adds
Module.onSpeechRouted, an optional hook through which a module tells the framework where it showed a piece of a turn's plain speech, so that an agent whose reply reached someone without going through a channel is told where it went, rather than only that it had no channel to go to.Under prose routing by locus or hybrid, no channel posts the plain speech of a turn that has no channel, and at
main(36b6dfe9de39) the agent is then told[send-undeliverable] Your previous reply (<n> chars) had no channel to go to, whatever else showed the reply. With this change the framework asks each module that implements the hook about each piece of plain speech it routes or holds as a draft. When a module names a place, the turn's[delivered]receipt names it, and a turn with no channel gets no notice for that piece. Speech with no channel that isn't held as a draft, and that no module names a place for, still gets the notice, except an ephemeral run's, which gets none, as atmain. The notice now quotes the piece's opening words. The new test file's 82 tests pass. Run againstmain'ssrc/, 75 of them fail (see Tests).Problem
Under the default rule for a turn's channel (
speech.locus: 'delivery'), a turn's plain speech goes to the channel of the delivery that woke it, a conversation fork's home channel, or a channel the agent opened during the turn, and past those only to an open all-except hold's channel, or tounpromptedChannelfor a turn the agent's own timer started (docs/dendrite.md:272at36b6dfe9de39). A turn that has none of these, such as one woken by a message a host takes in from a terminal or a web page, has no channel, and no channel posts its speech. The framework then writes the agent a notice.test/route-failure-targeting.test.ts:220at36b6dfe9de39asserts its opening, and in a run ofa25e38b1bf'ssrc/, an earliermain, it read in full:36b6dfe9de39writes the same words for such a turn: the notice atsrc/framework.ts:36420, and its reason atsrc/mcpl/channel-registry.ts:6558–6559.A host can still show the reply to the person who wrote, in the terminal or page the turn came from. At
36b6dfe9de39theModuleinterface (src/types/module.ts:119–248) has no method through which a module could tell the framework so.onAgentSpeech, through which a speech handler receives the agent's speech, resolves tovoid(:202–206). So an agent whose reply the host did show to the person it was answering gets this notice, and nothing tells it where the reply went.Changes
The catch-ups.
3554ba6df6mergesmainat36b6dfe9de39, which brings 326 commits, among them the merges of #972, #361 and #1000.3471d88cd9mergedmainat53e72cac4280, which brought #244, #955 and #942.9c60724e1dmergedmainat64c492ba26, which brought #948 and #996, anda382089bc5tookmainataa134e75fc. Their meets withmaingo beyond the conflicts each message lists:3554ba6df6). fix: a turn refused for budget waits idle until compression makes room #361's branch comes first, as onmain: a merged start that carries a budget wait that no member of its batch carries, and equals none of them, goes back as a member of its own, with its own opening messages (src/framework.ts:22982–22987). Otherwise the batch's first member takes the start's own opening messages with its own channels (mergedMarks,:22988–22994), andmain's wait mark goes to the member equal to the start (:22995–22997).3554ba6df6). It takesmergedMarksof the two wakes, which givesmain's channels and adds the opening messages, then fix(gate)!: a debounced batch's wake starts no turn once the agent has answered every event in it #972's batch rows (src/framework.ts:23070–23076). ThewakeLinesthis change carried at the park and the merged start went with fix(gate)!: a debounced batch's wake starts no turn once the agent has answered every event in it #972's removal of the field.3554ba6df6).mainnow keeps apart the write a wake reads (wakeAfter) and the write the gate counts in the window (added,src/framework.ts:20300–20302). The opening messages are still each stored write's id (:20304), whateveradded's rule.3554ba6df6). fix(gate)!: a debounced batch's wake starts no turn once the agent has answered every event in it #972 addedreadsRowsand the batch rows to whatsameInferenceRequestcompares (src/framework.ts:7350–7356). This change's two hold tests of opening messages now copy those keys from the file'sidentityKeys, as the tests beside them do. One had a list of its own that named the removedwakeLines. The receipts test of the retry after a rewind reads fix: a failure is told in the framework's words, never the provider's #1000's renamed tag fromNOTICE_TAG.refusalRewind.3471d88cd9). feat(routing): configurable homeChannel for no-trigger turn speech #244'sUNPROMPTEDmark is spread by hand at the!continuation, autoContinue and a bounce's wake (src/framework.ts:26598,:26695,:27535). It stays spread there, inside the requestmarkFollowOnmarks, somarkFollowOnadds it to none of the other requests its new-turn kind marks: the re-arm, the rewind retries and the unstick. The re-arm copies its trigger (src/framework.ts:7501), as atmain, so the re-arm of a turn whose trigger carried the mark keeps it.3471d88cd9).takesGlobalFallbackkeeps this change's method, andmain'stakesUnpromptedChannelgoes beside it (src/framework.ts:36535–36536).'delivery'rule's texts (3471d88cd9).docs/dendrite.md,speech.locus's doc andsharedFallback's doc takemain's sentences for an all-except hold's channel and the unprompted channel, with this change's words wheremain's still say a turn with no locus keeps its prose private. The documented rules above quote them.a382089bc5).mainno longer mints a handle for a write held in memory. A write that a surgery's hold or its agent's recovery keeps in memory has no id until it's stored. This change named a module answer's message by its stored id or its held write's handle. It now names a stored id only (src/framework.ts:20304), so such a message isn't named inopenedBy.messages. Where no module names a place for that turn's speech, the turn gets the notice, which says another surface may have shown it. Two texts say the message isn't named, each quoted old (a122e2b513), then new:src/types/module.ts:927–930: "the messages that opened that turn, each as its stored id or a held write's handle, either of which a module context'sgetMessagetakes"; "the messages that opened that turn, each as its stored id, which a module context'sgetMessagetakes. A message kept in memory when its wake was asked for (while a surgery's hold, or its agent's recovery, keeps the write) has no id yet, and isn't named."changelog.d/speech-sent-receipts.added.md:32–35: "as stored ids or held-write handles that a module context'sgetMessagereads"; "as stored ids that a module context'sgetMessagereads (a message kept in memory when its wake was asked for, while a surgery's hold or its agent's recovery keeps the write, has no id yet and isn't named)".9c60724e1d). fix(gate): keep notices out of a batch's route; name its channels #948 carries a request'swakeChannelsat the places this change carries itsopeningMessages. Each place now carries both: the start's rule, the hold's fold, the release, the quiesce park, the re-arm, the retry pushes and the turn-retry mark. At a release, the batch's first member takes whichever of the two the start held beyond the batch.cameFrom.mainpasses a turn'scameFromwhen it routes speech with no channel, so the notice can name the channels the turn's batch came from.routeLocusSpeechkeeps this change's body and passescameFromon each of its threerouteSpeechcalls (src/framework.ts:24466,:24482,:24485).main'sbyDesign: "This is a routing situation" where the reason names the batch's channels, and "routing/configuration" otherwise. A failed channel post's notice keeps this change's "The framework posted nothing there".held-turn-retrykeeps both changes' twin of each fold test, one for each field.batched-wake-locus's channels check takes the opening words. fix(gate): keep notices out of a batch's route; name its channels #948's test of a!continuation after achannel_opennow expects this change's turn-start words for a turn with no channel.The rounds since the first merge, in order:
111602a551namesreadsRow,main's field, wheretest/held-turn-retry.test.ts's lists and one fixture named the fields it replaced.e285025060reshapes the test of a hold's union of opening messages, as fix(gate): keep notices out of a batch's route; name its channels #948's twin test has it. A silent push carrying messages now meets an equal plain push.c8caf05eccpresents the row that the inference-failure rewind test sheds, sincemainsheds from the presented window.50fd42ce20givestest/deferred-import-boot.test.ts's assertion this change's words for a failed reply whose speaker has ended.bc71a9a170makes a test count every[send-undeliverable]and[discord-send-failed]notice in the primary's window.9f44b3580dadds tochangelog.d/speech-sent-receipts.changed.mdwhat happens when a module replaces the agent from inside its own hook.bd409aece7makes the hook a synchronous call (The call, below). With the wait goSPEECH_RECEIPT_WAIT_MSand its export, the 5 s bound, the record of asks overdue, the late lines, the receipt's andstop()'s waits for answers, the asks a stop abandoned, and the check that a module is still registered when it answers.stop()'s set of streams still running, which the first merge kept, goes too, since nothing reads it now, as atmain. So do the receipt tests whose subject was the wait, among them the stop test324b949065cut and92208b2068restored.7ec56f763dlogs a hook's rejected promise as a throw is logged.a5afadecc1cuts the.changedfragment's clause that a place a module names for an ephemeral run's speech is logged when a tool ends the run.bd409aece7made it untrue: only a place named for a routed piece with no channel is logged.92e148db51hasdocs/dendrite.mdsay which places are logged when no receipt comes: one named for a piece routed to no channel, and not one named for a piece held as a draft.66107dd7b0sets the marks that a request carrying a turn on takes from the turn's routing in one place,markFollowOn(src/framework.ts:24542–24550), which replacesmain'swakeChannelsMark. A request for a new turn takes the turn's opening messages and channels: the!continuation, autoContinue, a bounce's wake, the rewind retries after a refusal and after an inference failure, and the re-arm. A request that continues the same turn takes its channels and keeps its own opening messages: the context-budget restart,tool_results_ready, the tool-result guard's retry and a held turn's retry.mergedMarkstakes both marks at the hold's fold, the quiesce park and the release. A re-arm'sopenedBy.messagescan now come in the routing's order.held-turn-retrygains a test that a held turn's retry, released with a wake of its own cause before any of the turn's rounds ran, keeps the turn's opening messages beside the wake's.99e5c38198gives an operator's unstick its marks as a new turn, so it carries the latest turn's channels too (An operator's unstick, above).batched-wake-locusgains a test of it.1a3ef2bfachas a refusal rewind, an unstick or an inference failure's shed set aside, with the reply it sheds, the no-channel notice that quotes that reply's opening (The notice, below). The notice records the opening in its metadata, asopening.7e5b6fa11clogs a place a module names for a piece held as a draft, as it's named. The docs now say that an ephemeral run's speech held as a draft is asked about, and that speech an agent routes or holds once another agent has taken its name isn't.ecb9ff7e0fhas two tests of a round a send held count exactly one ask where they checked for any, and the budget-restart test check that the round routed before the restart is asked about once.f81f45ae38has a replaced agent's later pieces ask no module and route as atmain. It drops the checks that stopped them before any route, before a hybrid>>>send and before an explicit send, and keeps the check after a no-channel piece's modules return. Three tests go, and the two of a piece a replaced agent routes now check that no module is asked and that the piece routes under the name.ee7798b8b6has the.changedfragment say that an operator's unstick carries the latest turn's channels.This change's
src/is 8 files, +454 −124, againstmainat36b6dfe9de39. At3554ba6df6, beforef81f45ae38, it was +466 −124, and at9f44b3580d, againstaa134e75fc, 9 files, +515 −88.Merge order. #947 and #952, both open, also change the retry pushes this change changes.
The hook.
Module.onSpeechRouted(agentName, text, context)(src/types/module.ts:208–246) is optional and returns aSpeechSent. The framework calls it with each piece of plain speech a turn routes or holds as a draft, under prose routing by locus, hybrid or locked, on a host with a channel registry: each tool round's prose as it is routed, and the turn's final reply. For a piece no channel takes, a surface may show it then and answer where. It never asks about private envelopes, explicit>>>sends, an ephemeral run's plain speech that has no channel and that the run doesn't try to hold as a draft, which is its caller's result, or speech an agent routes or holds once another agent has taken its name. Tests check that a private envelope in a hybrid turn's final reply is never asked about, and, under locus routing, that an ephemeral run's speech with no channel asks no module, even one that would say it showed the speech, and that the run's context gets no[delivered]receipt. A module answers in one of two ways:{ sentTo: '<place>' }, when it showed that text to someone;{}otherwise, such as for a turn that none of its surfaces opened.Held speech. Plain speech that the framework holds as a draft rather than posts is asked about too, and the turn's receipt names the places the modules return beside its drafts note. That covers a locked turn whose lock points at drafts, is unset or doesn't resolve, a heartbeat's held prose, a locked turn whose lock moved while its words waited for the turn's end, hybrid speech with no destination, and plain speech that a send in its round held. Each has a test. Four tests differ only in a locked turn's lock: with a lock, the speech is posted, the module is asked, and the receipt names the place; locked to drafts, with no lock and with a lock that doesn't resolve, nothing is posted, the module is asked, and the receipt names the place beside its drafts note. The ask is in
holdProseDrafts(src/framework.ts:23700–23713), which asks unless its caller opts out or another agent holds the agent's name. Three callers do, each holding an explicit envelope's words or speech under explicit routing, which writes no receipt: a bounce (:27449),takeUnsent(:26960) and explicit routing's drafts round (:27279). A part marked deliberate, an explicit envelope among the parts a send held under hybrid routing, isn't asked about either. Each text a send holds is asked about with its owncontext.final: where a turn's tool rounds aren't routed live, the prose a send held from before the turn's last tool call is asked about as a round's, and only the reply as the final speech, under locus and locked routing and under hybrid routing alike. A held piece is asked about when it's held, which can be after later pieces have routed. Tests check that explicit routing asks no module about speech it holds or bounces, that an envelope whose{{unsent}}names a draft still being sent holds its words without asking, and that a hybrid round a send held asks about its plain envelope and not about its>>>envelope.A place is kept to one line, each run of whitespace, line breaks included, as one space and its ends trimmed, and isn't cut, however long. Then a notice's opening in it, such as
[routing], gets a backslash before its bracket, as the framework marks other text it's given, so the receipt readssent to the terminal \[routing] says sowhere a module namedthe terminal, a line break, and[routing] says so.What a place changes. The turn's
[delivered]receipt names each place as "sent to ", after any channels the speech went to, each once, in the order the modules were asked about the pieces. A held piece is asked about when it's held, so its place can come after a later piece's. When none of the turn's speech went to a channel, and no post to one was tried, the receipt adds "this turn's routing posted it to no channel", as in[delivered] plain speech → sent to the console that sent this turn · this turn's routing posted it to no channel. It speaks for this turn's routing only, since a module's own speech handler can post the speech to a channel and name that place. A routed piece with no channel that a module showed gets no[send-undeliverable]notice, and the framework logs[routeSpeech] <agent>: turn has no channel; <n> chars shown by <places>, routing posted it to no channel. There each place prints as a log field: one with a space or an invisible character prints as a JSON string with those characters escaped, such as"the\u0020console\u0020that\u0020sent\u0020this\u0020turn", while the receipt shows those characters unescaped. Such a piece emits nomcpl:speech-route-failedtrace, since it returns before the channel registry routes it (src/framework.ts:24478–24481), and the registry's path for a piece with no channel is what emits that trace (src/mcpl/channel-registry.ts:6523–6563). Atmain(36b6dfe9de39) every such piece goes to the registry (src/framework.ts:24347there) and emits it. The line for a routed piece with no channel that no module showed still saysturn has no locus, as before, and it still emits the trace. A piece with no channel whose agent was replaced while its modules were asked gets neither of these lines nor the trace, only the replacement's line (below). An ephemeral run's speech with no channel that it doesn't try to hold as a draft isn't asked about, and gets no line, trace or notice, as atmain(src/mcpl/channel-registry.ts:6523–6529). A piece with a channel is posted once its modules have returned, and if the post fails, its own notice stays and the receipt names the place. A place a module names for a piece of a tool round's prose, routed or held as a draft, also counts as speech the round handed on, as a post to a channel does atmain, as soon as that module returns a place (src/framework.ts:24503–24505). So when a module had named a place by the time the turn ended, a turn that fails says in its[inference-failed]note: "Some plain speech of its rounds was handed on for delivery or shown as written, and no [delivered] line was written for it." (:1975), wheremain's says "Plain speech its rounds routed live was handed on for delivery as written".The call. The framework calls the modules one after another, in module order, and routing goes on when the calls return (
ModuleRegistry.askSpeechRouted,src/module-registry.ts:758–792). So showing a piece has to be a write that doesn't wait, as the hook's doc says (src/types/module.ts:233–237). Each module is called inside its own guard, and in each case below the other modules' places stand:onSpeechRoutedthrows when it's read isn't called, and the log names it;sentTothrows when it's read;src/module-registry.ts:772–777);Each has a test. A piece of the turn's final speech is asked about after the turn's
inference:completedtrace and before the agent's nextinference:started, which the hook's doc promises (src/types/module.ts:239–240) and a test checks (test/speech-sent-receipts.test.ts:620). Under locus and locked routing, a piece with no channel goes on to the next piece once its modules have returned, and a piece with a channel is posted then (routeLocusSpeech,src/framework.ts:24468–24485). A place a module names for a routed piece with no channel is logged then, in the[routeSpeech] … shown by …line (:24478–24481). A place named for a held piece is logged then too, in a[speechRouted] … held as a draftline, and goes into the turn's receipt unless that receipt was already written (holdProseDrafts,:23707–23711). A place named for a piece with a channel goes into the turn's receipt, beside the channel, whose post is logged (src/mcpl/channel-registry.ts:6572). So a turn that ends without a receipt, as one that fails or is stopped does, names a channel piece's places nowhere. Once another agent holds an agent's name, no module is asked about the replaced agent's later pieces, and they route as atmain, with a channel or without (src/framework.ts:24468–24470). The one exception is a module that replaces the agent from inside its own hook. A piece with no channel is checked again once its modules have returned, and one whose agent was replaced by then isn't routed for the replacement, and is logged as[routeSpeech] <agent>: replaced while its modules were asked; <n> chars not routed for the replacement(:24473–24477). A piece with a channel is posted in that case, since it isn't checked again. A test replaces the agent from inside a hook, for a piece with no channel. A replaced agent's held pieces, such as a locked turn's or a heartbeat's and the words a send in a hybrid reply's turn set aside (:23634–23642), and its bounces and their notices, still go to the name, as atmain, and once another agent holds the name no module is asked about a held piece (holdProseDrafts,:23700–23701). Its tool calls go as atmain. AnonPlacecallback that throws is logged, and the call still returns the place (src/module-registry.ts:784–788).The notice. Speech with no channel that isn't held as a draft, and that no module names a place for, still gets
[send-undeliverable], except an ephemeral run's, which gets none, as atmain. The notice now names the piece by its opening words:Your plain speech "<opening>" (<n> chars) had no channel to go to, where it saidYour previous reply (<n> chars). When the failure's info has notext, or its words are only whitespace, it names the speech by its length alone:Your plain speech (<n> chars) had no channel to go to. The opening is quoted as a JSON string, with a[that would open a notice written as\u005b, and when the piece is longer than 40 characters it's cut at the last word that fits, or inside a first word longer than that, and marked "…". The notice still says "if another surface showed it (a reply thread, the console), that copy stands". A refusal rewind, an operator's unstick or an inference failure's shed that sets a reply aside sets aside with it each no-channel notice next to the reply, with only rows the shed passes over between them, whose recorded opening is in the reply's own speech (noticesQuoting,src/framework.ts:1686–1711, called at:15223). The notice records the opening it quotes in its metadata, asopening(:36568), since it's written before the reply's row is stored and nothing else links the two. So a notice about an earlier reply goes too when it lies next to the shed reply and quotes the same opening. The quoted words go out in every request that carries the notice, the host's background requests included, such as its keyword requests for retrieval. With the notices' new words, one early cache boundary in a run of the pull request's context view falls later in the conversation.context.turnIdis theturnIdof the turn's routing, whichonAgentSpeechalso gets asSpeechContext.turnId(src/framework.ts:24501,:30474), so every piece of one turn carries the same id. A test checks that a tool turn's pieces name one turn (test/speech-sent-receipts.test.ts:483–485).context.finalis true for the turn's final speech, the speech after its last tool call. A refused XML tool attempt and a call that the output limit cut off unrun don't end a speech. Neither tracesinference:tool_calls_yielded, so a relay that starts a new speech at each of those events doesn't start one there either. The holds for a locked turn, a heartbeat's turn and a turn whose lock moved while its words waited for its end each ask about a piece with its round's finality (src/framework.ts:24454,:24526,:24563), and a test checks that the moved lock's held reply is asked about as final.context.openedBynames the request that opened the turn (reason,source,timestamp), andmessages, the messages added by the module answers whose wakes the turn took. Each message is its stored id, which a module context'sgetMessagetakes. A message kept in memory when its wake was asked for, while a surgery's hold or its agent's recovery keeps the write, has no id yet and isn't named.openedBystays the same after a context-budget restart. A re-arm opens a new turn, whoseopenedBy.messagesare the messages that opened the turn it re-arms. A turn the framework queues to carry another on (a!continuation, an autoContinue run, a bounce's wake, or a retry on a history rewound after a refusal, an unstick or an inference failure) gets the messages that opened the turn it carries on. A turn that took a cause of its own that brought messages beside such a continuation, such as a module answer's wake, names that cause's messages, not those the continuation carried (bringsOwnMessages,src/framework.ts:2031–2033;:23351), so a resident whose continuations keep meeting new causes doesn't carry every earlier turn's messages into the next. A cause of its own that brought none, such as a heartbeat that isn't silent or an operator's nudge, doesn't take the continuation's place, and the turn names the messages its continuation carried. The start takes them in place of the messages its routing already names while none of the turn's rounds has reached its tool calls (gotPartway,:1354–1356;:28286,:28510), as when an autoContinue run's held continuation takes a message's wake. A start that joins a turn one of whose rounds has reached its tool calls adds them to the turn's, whether or not that round's results are in the agent's context yet, so the message that opened the turn stays named. When two wakes parked with one coalescing key while the host is quiesced are kept as one, the wake kept takes both wakes' opening messages (:23071). A message a rewind removed is still named, since the retry stands in for the turn it opened, andgetMessagereturnsnullfor it. When a provider hold keeps one of two equal requests, the request kept takes both requests' opening messages (holdRequest,src/framework.ts:7379). A merged start that comes back to the scheduler, as when a hold releases it, is replaced by its batch, as atmain, and the batch's first member now also takes the held request's opening messages that no member of its batch holds (:22988–22994): those of a wake the hold took into a held merged start, and a held wake's own when an equal merged start folded into it. So the turn the release starts names the messages of a wake taken into a held merged start, and of a held wake that an equal merged start folded into. A start that carries a budget wait none of its batch carries, and equals none of them, goes back as a member of its own, as atmain, and keeps its messages itself (:22982–22987). The operator'sunstickcommand opens a turn with the messages that opened the agent's latest turn, and with that turn's channels (src/framework.ts:15633–15638). One test checks that its run names the messages, and another that its notice names the channels.InferenceRequest.openingMessages, marked@internal, carries them between requests.messagesis areadonly string[]. A wake an event gate debounced carries none, so the messages of the module answers it stands for aren't named, as the field's doc says.Provider holds. A provider hold folds two equal requests as
maindoes (holdRequest,src/framework.ts:7360–7380;:7282–7303at36b6dfe9de39), andmainalso reconciles their carried state and re-arm marks there. What this change adds is the opening messages:src/framework.ts:7379), so the messages of a wake taken into a held merged start aren't lost with it.main, unless it carries a budget wait that no member carries and equals none of them, when it goes back as a member of its own (src/framework.ts:22982–22987). Otherwise the batch's first member takes the held request's opening messages that no member of its batch holds (:22988–22994), so a member's own messages stay on that member. Without it, a held wake that an equal merged start folded into would lose its messages, and so would a wake taken into the start.maingives the first member the held request's ownwakeChannelsthe same way (src/framework.ts:22892–22894at36b6dfe9de39), and has no opening messages.Turn order. A test pins the order the hook's doc promises: a turn's final speech is asked about after its
inference:completed, and the agent's next turn starts, and compiles, only after the turn's routing and receipt are done (test/speech-sent-receipts.test.ts:620). With the turn's completion moved after its final routing, it's the only test in its file that fails (Tests).Routing notices. For a turn that isn't locked, the
[routing]notice that tells an agent its speech now has no channel reads "Your plain speech currently has no channel, so routing posts it to no channel. Use an explicit send tool to reach a channel.", and on a turn with no channel, the notice of a message addressed to the agent from another channel ends "Your plain speech has no channel this turn; reaching them takes an explicit send." Each said the speech "stays in your archive". A locked turn's two notices keepmain's words. The note for a channel its server declared inbound-only, which two[routing]notices andchannel_open's result include, now reads " declared this channel inbound-only, and plain speech there is refused, so routing posts it to no channel there. To speak there, use 's own tools.", where it said plain speech there "is refused and stays in your archive".The receipt's notes for held speech. The
[delivered]receipt's note for speech held as drafts reads "(the framework didn't send them — drafts can resend them unchanged, or dismiss them)", wheremain's read "(not sent — …)", and its note for speech that couldn't be held reads "the words remain in your history", wheremain's read "remain only in your history" (src/framework.ts:24641,:24678). For the same reason, the[drafts]notice for speech held as a draft reads "not sent by the framework — …", wheremain's read "not sent — …" (:23812), and autoContinue's[continue]row for closing words a send held reads "Some or all of your closing words last turn were held, and the framework didn't send them: …", wheremain's read "were held, not sent: …" (src/auto-continue.ts:43), whichdocs/auto-continue.mdquotes. A module may have shown any of them.The failed-reply notices. The
[discord-send-failed]notice for a post to a channel that failed, other than one whose outcome is unknown, now ends "The framework posted nothing there; it is saved in your archive.", wheremain's ended "Nothing was posted; it is saved in your archive." (src/framework.ts:36587;:36419at36b6dfe9de39). The notice of a failed reply by a speaker that has ended, other than one whose outcome is unknown, now ends "The framework posted nothing there. has ended.", wheremain's ended "Nothing was posted. has ended." (:35057;:34888at36b6dfe9de39). A module may have posted the words in that channel by its own means.The notice of unposted tool-round speech. The
[speech-not-posted]notice, for speech a turn's tool rounds left unposted, now reads "The framework didn't post what you wrote before your tool calls in one or more of the rounds just above. If any of it should go out, send it again.", wheremain's read "Nothing you wrote before your tool calls in the rounds just above was posted. …" (src/framework.ts:24745;:24569at36b6dfe9de39), since a module may have shown the words, and the notice comes when any one round's words before its tool calls weren't routed live (:19561–19564).Hybrid's plain envelopes now go through
routeLocusSpeech, the function locus mode routes plain speech with, so modules are asked about them. The hold of a heartbeat's prose thatdeliverHybridEnvelopemade moved with them into that function.Other additions.
ModuleRegistry.askSpeechRouted, marked@internal, since the framework is its one caller, calls the modules and returns the places they named, and calls itsonPlacecallback for each place.ChannelRegistrynow passesonRouteFailurethe words routed, astext, so the notice can quote them, and a host that logs what itsonRouteFailurereceives now logs the agent's speech. The field is optional in the callback's info type, as the info's other later fields are, and the registry sets it on every failure it reports (src/mcpl/channel-registry.ts:1092–1094,:6499–6502).trailingRunCount(src/prose-segments.ts) counts the reply's runs of prose where a turn's rounds aren't routed live, so that only those are asked about as final.A receipt that can't be recorded. When adding a turn's
[delivered]receipt to the agent's context throws, the log line names the agent and what the receipt names, each channel by its id, each draft's delivery as its channel's id with the draft and how it went, a draft an{{unsent}}delivered to a channel the receipt already lists included, and each "sent to ", each printed as a log field, or "no place":[delivered] <agent>: its receipt couldn't be recorded; it named <places>:, then the error (src/framework.ts:24615,:24673,:24724–24725). With a draft an{{unsent}}delivered to a channel the receipt lists left out of the line, as ata251c88731, the new test inprose-draftsfails, at its check that the line names the draft (Tests). So however long a channel's label, it can't cut the channel's id from the line.mainloggeddelivery receipt append failed:and the error (:24549at36b6dfe9de39).Unchanged:
onAgentSpeech,dispatchSpeechandSpeechHandlerOptions.Comments. Two comments on why a turn's speech goes to the locus frozen at its start drop their incident history, and a third, above the trailing dispatch, is gone. The one on
deliverSpeech'slocuskeeps the rule. The one at the text-only dispatch (src/framework.ts:30544–30549) now gives the rule's reason: a locus resolved when the speech posts would read the global fallback, which an arrival, or an all-except hold opening or ending, can move while a dispatch runs (src/mcpl/channel-registry.ts:6100–6159).533c73b's message also says that on a turn a tool ends, the next turn can start before the queued prose has posted. By reading, it can't: such a turn awaits its speech chain and its receipt (src/framework.ts:29447–29448) before its turn token is released, and a fresh wake waits while the agent has a turn (:23113).onRouteFailure's comment drops its sentence that none of the notices claims the reply reached no one, and the comment above it now says the marker tells the agent that the framework didn't deliver her reply there, or didn't confirm it, wheremain's said her reply didn't reach the human, or wasn't confirmed to (src/framework.ts:36552–36555).sharedFallback's doc,speech.locus's doc insrc/types/framework.tsand a comment in the channel registry now say that routing posts the prose to no channel, andspeech.locus's doc says a[routing]notice says so when the locus the agent was last told of was a channel. At36b6dfe9de39the two docs say the prose stays private, and the comment said it stays in the archive.deliverSpeech's doc says that throughonRouteFailurethe agent learns "that this route didn't deliver its words, or didn't confirm it" (src/mcpl/channel-registry.ts:6451–6452), wheremain's said it learns "its words did not (or may not have) reached anyone" (:6449at36b6dfe9de39), which a module that showed them would make untrue.appendProseDeliveryReceipt's doc now lists each condition under which it writes nothing.holdProseDrafts' doc says that words the journal can't hold remain in history (src/framework.ts:23686), wheremain's said they remain "in history only" (:23583at36b6dfe9de39).Docs. The README documents the hook under
Module. The README, the.addedfragment andopenedBy's doc also say that a turn that took a cause of its own that brought messages beside a continuation before any of its rounds reached its tool calls names that cause's messages, and that a cause that joins a turn already under way is named beside the turn's own. Its account of a failed or stopped turn's note now says the speech was handed on for delivery or shown when a module had said, by the time the turn ended, that it showed a round's speech, and that a delivery that finished undelivered gets neither of the note's sentences about it only when no module said it showed it.docs/dendrite.mdsays that the framework posts the prose of a turn with no channel to no channel, and that a module may still show it somewhere, a channel included, and say so, which the turn's receipt then names when the turn ends with one. It says too that an ephemeral run's speech is asked about when it's held as a draft, and that "A place named for a piece held as a draft is also logged as it's named, and so is one named for a piece routed to no channel, unless the agent was replaced while its modules were asked, which is logged instead."docs/locked-prose-routing.mdsays that a post that fails "gets the failure marker instead; a receipt, when the turn writes one, names no channel for it, only any place where a module said it showed it". The README's account of the[speech-not-posted]notice now says that the framework didn't post what the agent wrote before its tool calls in one or more of those rounds. The hook's doc, the README and the.addedfragment say that the call is synchronous, that showing a piece is a write that doesn't wait, that a module that throws names no place, and that a piece withfinal: truecomes after the turn'sinference:completedtrace and before the agent's nextinference:started. The hook's doc also says that a module that returns a promise names no place, and that a rejection of it is logged as a throw is. The hook's doc and the.addedfragment name the ephemeral-run exception, and with the README they say that speech an agent routes or holds once another agent has taken its name isn't asked about. The README says that an ephemeral run's reply with no channel gets no[send-undeliverable]notice, since it's its caller's result.SpeechSent.sentTo's doc says that a place's whitespace runs read as one space, with none at either end, and that a long place reads whole. The.addedfragment says that a place reads as one line, each whitespace run as one space and none at either end, and that an opening of a tag the framework marks in text it supplies, its own notice tags and context-manager's and membrane's, reads with a backslash before it. The.changedfragment tells hosts that a piece a module showed emits nomcpl:speech-route-failedtrace, and that once another agent holds an agent's name, no module is asked about the replaced agent's later plain speech, which routes as before. It says what happens when a module replaces the agent from inside its own hook: later modules are still asked about the piece in hand, and about a draft hold's later pieces, and a piece with a channel still posts, but one with no channel isn't routed for the replacement. It also givesmain's words beside the new ones for the failed and stopped notes' sentence, the held-speech notes, the[drafts]notice, autoContinue's[continue]row, the two failed-reply notices and the[speech-not-posted]notice, and says that an ephemeral run's speech with no channel gets no notice or trace, as before. It says thattextis optional inonRouteFailure's info, with a no-channel notice for an info without it, or for speech that's only whitespace, naming the speech by its length alone. It gives the receipt-failure line besidemain'sdelivery receipt append failed:, and the lines a module's call can log. It gives the[speechRouted] <agent>: <n> chars shown by <places>, held as a draftline, and says that a piece with no channel whose agent was replaced while its modules were asked logs that instead, with no places and no trace. It says that a refusal rewind,/unstickor an inference failure's shed that sets the speech aside sets aside with it the notice next to it that quotes its opening, which the notice's metadata carries asopening. It says that the turn an operator's/unstickqueues carries the channels and opening messages of the agent's latest turn, so when an ambient batch woke the stuck turn, the notice for the unstick's speech names that batch's channels.Tests.
test/speech-sent-receipts.test.tsis new, with 82 tests.test/prose-drafts.test.tsgains one test: that a hybrid turn's draft a bare{{unsent}}delivers in its own turn is named in the log when the turn's receipt can't be recorded.test/exhausted-turn-rearm.test.tsgains one test: that a re-armed continuation keeps the messages that opened its turn.test/held-turn-retry.test.tsgains six tests. Two are of a heartbeat that isn't silent, which has a cause of its own but brings no messages: merged with an autoContinue wake that carries its turn's opening message, the start names that message, and continuing an open turn before any of its rounds reached its tool calls, it keeps the turn's opening messages. Three are of the provider hold's release: that the turn it starts names the messages of a wake the hold folds into a held merged start, and of a held wake that an equal merged start folds into, and that a member the read check drops leaves its message off the turn. The sixth is of a held turn's retry released with a wake of its own cause before any of the turn's rounds ran: the turn keeps its opening messages beside the wake's.test/batched-wake-locus.test.tsgains one test: that after a turn an ambient batch woke, the notice for the speech of the run an operator's unstick starts names the batch's channel.test/refusal-rewind.test.tsgains two tests of the shed: a no-channel notice that quotes the shed reply goes with it, and a notice next to the shed reply that quotes an earlier reply stays.An assertion in
admission-e2enow checks for the[routing]notice's new first sentence whole, where it looked for "no channel", and neither its message norbatched-wake-locus's opening comment says that speech with no channel stays private.batched-wake-locus's check of the no-channel notice takes the piece's opening words, and its check of a turn-start[routing]notice takes this change's words. The check of the failed-turn note's sentence intest/prose-drafts.test.tsfollows its new wording, and the checks of the held-speech notes inprose-drafts,present-while-actingandexplicit-prose-routing, and of the[continue]row intest/auto-continue.test.ts, follow theirs. The check of the[speech-not-posted]notice inprose-draftsfollows its new words. A check inprose-drafts, one intest/publish-outcome.test.tsand a stored notice intest/conversation-routing.test.tsfollow the failed channel post's notice,publish-outcome's check that an unconfirmed delivery's notice never says nothing was posted now also looks for "posted nothing", and five assertions inroute-failure-targetingand one intest/deferred-import-boot.test.tsfollow the ended speaker's. One assertion intest/route-failure-targeting.test.tsfollows the notice's new wording. Another checked that the primary's window held no "Your previous reply". It now counts every[send-undeliverable]and[discord-send-failed]notice there. The check of the inbound-only note intest/present-while-acting.test.ts, which three of its tests make, follows the note's.What each part is for
Where a change to
src/below fails tests, the run is in Tests, at the commit the bullet names, or ata122e2b513where it names none.{}covers every other case, and the notice keeps its word about another surface. So a host that knows its surface didn't take the words, as when its connection closed, can answer only{}, and the notice then still says that a copy another surface showed stands.9f44b3580d'ssrc/, which awaited what each hook returned, "a module whose hook returns a promise names no place, and routing doesn't wait for it" fails, at its premise, and so does the test of a place's callback that throws (Tests).mainmodels operator input as a surface, not a channel:InboundSurfaceSource(src/mcpl/inbound-source.ts:85–91at36b6dfe9de39), whose answer "the module or the surface carries" (src/framework.ts:20137–20138there);openedBy.messages, carried across restarts and continuations, lets a module tell which of its surfaces sent the turn, so it can answer for the surface the turn came from.context.finallets a module whose surface receives only a turn's reply answer for the reply and not for each round's prose.[routing]in a place from reading as a[routing]notice, as the framework marks other text it's given (markedTexts,src/supplied-text.ts:49). Tests check each: a place with a line break and[routing]in it, and places with extra whitespace and a line break, a blank answer and an answer over 300 characters long, which reads whole.[delivered]receipt. A turn under explicit routing writes no such receipt (src/framework.ts:29446–29448,:30693–30694), so an answer about its speech would have nowhere to appear. A bounced send's words and an{{unsent}}envelope's words are held with a note saying why their send didn't go (:26960,:27449). Leaving them out keeps the receipt from naming a module's place beside that note.[delivered]receipt. One test checks this, under locus routing, with a module that would say it showed the speech (test/speech-sent-receipts.test.ts:391).>>>post whose outcome is unknown, since a channel may then have posted it.Surfaceis a module whoseonSpeechRoutedanswers as a test sets it: a place,{}, or an object the test gives, such as one whosesentTois blank or throws when it's read. It records each ask with its text and context, so a test can check what the module was asked, with whatfinaland whatopenedBy. ItsonProcessmakes a message from its source the turn's opening message, or several withmessagesPerEvent, andonAskedlets a test act at the moment of an ask, such as replacing the agent from inside the hook.channelServerstands in for a channel's server. It records each post and answers delivered, not delivered, or with a throw, so a test can check what a turn with a channel posted, and what its receipt and notices say when the post fails.turnsends the agent a console message and runs the framework until it's idle, so a test takes a whole turn in one line.hostbuilds the framework every test in the file runs on: the given modules and stand-in provider, an agent under locus routing unless the test says otherwise, and the channel registry.untillets pending work run until a condition holds, and fails with its label after 10 s of real time.Surface's'per-text'answer names a place that ends with the text it was asked about, so a test of several pieces checks that each piece's own place is in the receipt, and a dropped answer can't hide behind another piece's.lookRoundandafterLookgive a turn a tool round, aconsole--lookcall, and then a reply, so a test can tell a round's prose from the final reply.internalsreaches the private state a test needs: the queue of requests, to start a turn by a given wake; the channel registry, to give it a stand-in server; the provider holds; the locus the agent was last told of; and the notice of a message addressed from elsewhere, called directly.keepWakeQuietqueues a response holding only a given envelope, such as>>>skip_reply, each time an inference after the first starts, so a later turn the test doesn't script gets a reply that gives the modules nothing to be asked about.openDraftslists the agent's open prose drafts, so a test can check what was held, and why.channelTurngives the registry a stand-in channel server and queues a wake from one of its channels, so a turn has a channel to post to, and the test can make that post succeed, fail or throw.gategives a test a promise it releases itself, for holding a post at a point the test chooses, and registers the release, which the file'safterEachcalls (test/speech-sent-receipts.test.ts:204–218). One test uses it. Without the release at teardown, a test that failed before releasing what it held would leave the post waiting into the next test, asa3e955d4a7's message says of the hook's answers it held then.Surface'stoolCallruns when the agent calls the module'sconsole--looktool, so a test can act in the middle of a turn, such as pushing a message that arrives while the turn runs.textOfreads a message's text from its blocks.texts,noticesandreceiptread the agent's context through it: every message's text, the[send-undeliverable]notices, and the latest[delivered]receipt.pushsends the agent a message from a source, the console unless the test names another.askedTextslists what the module was asked, each text with itsfinal, andopeningTextsreads the messages an ask'sopenedBy.messagesnames, so a test can check both by their words.MockMembrane, change its stream to reach a case their tests need:WithoutRoundContentdropsroundContentfrom each tool round's event, so a turn's tool rounds aren't routed live and their prose routes at the turn's end. Eight tests use it, among them that such a round's prose is asked about as a round's and only the reply as the final speech.AttemptBeforeReplyadds a refused XML tool attempt just before the response completes, and withlivefalse also dropsroundContent. Its test checks that the speech on both sides of the attempt is asked about as the final reply, where tool rounds aren't routed live.FailsAfterFirstRoundtakes the first tool round's results, then fails with a server error marked not retryable. Its two tests check that a turn that fails after a round a module showed says in its[inference-failed]notice that the round's speech was handed on: one where the round was routed, which also gets no[send-undeliverable]notice, and a heartbeat whose round was held as a draft.HeldSecondanswers the second request with a rate limit and a 60-second wait, so the framework holds that turn for a retry. No test uses it on its own:HeldSecondThenRestsbuilds on it.HeldSecondThenRests, overHeldSecond, ends each stream after the third withskip_reply, so an autoContinue run comes to rest. Its test checks that the run's held continuation, which takes a second message's wake, names that message and not the one that started the run.logValue. A place is text a module chose.logValue, which the host's lines already use for values a server chose, prints it as one field: a place with a space or an invisible character prints as a JSON string with those escaped (src/mcpl/log-value.ts:1–43). Printed raw, a place's spaces could read as the line's other fields, and two places that differ only in invisible characters would print alike. "a place a module names is logged as a field, its invisible characters escaped, and kept readable in the receipt" checks it.main's[routing]notice for speech with no channel, the notice of a message addressed to the agent from elsewhere on a turn with no channel, and the inbound-only note each told the agent that its plain speech stays in its archive (src/framework.ts:24974,:25933and:596at36b6dfe9de39). With this change a module can show that speech, and the turn's receipt then names where, so those words could contradict the agent's own receipt. The new words speak for routing alone: the[routing]notice says routing posts the speech to no channel, the inbound-only note says routing posts it to no channel there, and the notice of a message addressed from elsewhere says the speech has no channel this turn.[speech-not-posted]notice's words.main's "Nothing you wrote before your tool calls in the rounds just above was posted" denies any posting, a module's included, and a module may have shown those words. The new words deny only the framework's own posting, as the failed-reply notices' new words do, and say "one or more of the rounds", since the notice comes when a single round's words weren't routed live, whatever the turn's other rounds did. Withf9dbe8fb92'ssrc/, whose words said the framework "posted nothing you wrote before your tool calls in the rounds just above", the three tests inprose-draftsof a failure after a tool round fail at the notice's words (Tests).[routeSpeech]line for a piece a module showed tells whoever reads the host's log where that piece went, wheremain's line said the speech was not routed, and it never says "no locus", which connectome-host'sconnectome-doctorreads as not delivered. A module's call gets a line naming the module and the agent when itsonSpeechRoutedthrows as it's read, when its returned promise rejects, when it throws, and when the callback for its place throws (src/module-registry.ts:765,:774,:780,:787). An answer of{}and a blank place get no line. Tests check each line. A place a module names for a piece held as a draft gets a[speechRouted] <agent>: <n> chars shown by <places>, held as a draftline as it's named, since a held piece can be asked about after the turn's receipt was written, and its place would then reach neither the receipt nor the log. With1a3ef2bfac'ssrc/, "a piece held as a draft that a module showed is logged with its place as it's named, as a piece with no channel is" fails, finding no line (Tests).e285025060has since reshaped (Tests). Without the release carry, two fail, each at the wake's message: the first of those, and "a held wake that an equal merged start folds into keeps its opening messages when the start is released" (Tests). The first member takes only the messages no member holds, not the whole start's, so a member's own message stays on that member, and a member the read check drops doesn't leave its message named on the turn. With the whole start's carried, asc63805592adid, only the release whose member the read check drops fails, at its check that that member's message isn't named (Tests). The hold and the quiesce park each take both requests' messages, and the merge's rule stays only at the scheduler's merge of a start. At the hold, the rule would differ from the union only for two equal requests, one silent, that both carry messages.e285025060's test builds that case: a silent push carrying messages, with an equal plain push the hold folds into it. With the merge's rule at the hold, that test keeps one push's messages, not both (Tests). A silent request from outside the framework carries messages only as a release's first member, as the wake the park keeps, or as a copy of one, and whether either meets an equal request with a cause of its own at a hold isn't traced (Not verified). At the park, the rule would drop a real event's messages. The park keeps the newer of two wakes, whatever its silence, and writes both wakes' messages onto it (src/framework.ts:23069,:23071). So a silent push that absorbed a plain one carries that push's messages, and when it then meets another plain push, the rule keeps only the second push's. With the park keeping the merge's rule, asa3e955d4a7did, "three pushes parked with one key for an agent their messages aren't written to, a plain one, a silent heartbeat's and another plain one, name both plain pushes' messages at resume…" fails, at the messages named at resume. Without the park's union, that test fails too, and so does "two wakes coalesced while the host is quiesced name both their messages at resume" (Tests).66107dd7b0'ssrc/, the commit before the unstick's,batched-wake-locus's unstick test fails, its notice reading "this turn came from no channel" (Tests).[delivered]receipt, so its[inference-failed]note is where the agent learns that a round's speech was handed on. Without the mark, the note of a turn that fails after a round a module showed wouldn't say so. The mark is set as each module returns a place. With the line that sets the mark removed, "a turn that fails after a round a module showed says that round's speech was handed on" and "a heartbeat that fails after a round a module showed says that round's held speech was handed on" fail (Tests).onRouteFailure'stext. The[send-undeliverable]notice is written where the framework'sonRouteFailurecallback is called (src/framework.ts:36558–36588), from what the channel registry passes it. The words routed are the field that lets the notice quote a piece's opening words. The field is optional because the info's type is published:ChannelRegistryis exported from the package (src/index.ts:91), and in 0.32.0 itsonRouteFailureinfo's later fields,serverId,outcomeanddetail, are all optional. Code that builds that info itself, such as a host's wrapper forwarding it, compiles against 0.32.0's type withouttext, and a required field would break that compile. The notice's length-only wording is for such an info: given notext, the notice names the speech by its length alone, asmain's does, rather than quoting words it wasn't given (src/framework.ts:36564,:36588). Speech that's only whitespace has an empty opening, and the notice names it by its length alone too, rather than quoting an empty string (2e628e8c28). With an empty opening quoted, the notice for whitespace-only speech readsYour plain speech "" (3 chars), and its test fails (Tests). Without that branch, an info withouttextthrows inside the callback'stry(speechOpening,:1678–1684), the throw is caught and logged, and the agent gets no notice at all: "a no-channel notice whose words its host didn't pass names the speech by its length alone" then fails, finding no notice (Tests).TurnRouting.openedByis a snapshot of the request that opened the turn, set only by a start that opens one (src/framework.ts:1600,:28413), andspeechOpenedBybuildscontext.openedByfrom it and the turn's opening messages (:1663–1674). So a module'scontext.openedBynames the request that opened the turn on every piece, across a context-budget restart and a retry's later starts. With the snapshot taken at every start, "a budget restart still names the messages and the request that opened the turn, on its rounds and its final speech" fails at itsreasoncheck, which readscontext_budget_restart, and "a start that takes a module's wake into a turn already under way names the turn's opening message too" fails at its premise that the turn already under way opened it, which readstool_results_ready(Tests).main. With the ask made anyway, the two tests of such a piece fail (Tests). A replaced agent's held speech is still held, as atmain, and no module is asked about it either.onSpeechRoutedinside its own guard. A module whoseonSpeechRoutedthrows when it's read would otherwise throw out of the ask, and no module's place would count for the piece. Withdd8144cd2c'ssrc/, "a module whose onSpeechRouted can't be read isn't asked, and the others are" fails.onPlaceruns inside the call, once a module has named a place (src/module-registry.ts:784–788). Witha8d7eb8695'ssrc/, which called it unguarded, the test of a place's callback that throws failed with the callback's error. The log line names the module and the agent (:787).main's line named neither the agent nor a place. Witheb57e65382'ssrc/, "a receipt that can't be recorded is logged with its agent and the places it named" fails at its line, and withmain's line put back, so does that test, and the long label's test fails at its premise (Tests). A channel is named by its id, not its label, so a long label can't cut the id from the line: with the log naming each place as the receipt shows it, "a receipt that can't be recorded is logged naming each channel by its id, however long the channel's label" fails at its check that the line names the channel's id (Tests).a8d7eb8695'ssrc/, "a turn that takes a module's wake beside a continuation names that wake's messages, not those the continuation carried" fails, naming the carried message beside the wake's. The start takes the cause's messages in place of its routing's only while none of the turn's rounds has reached its tool calls. Merging them instead, a held autoContinue run's retry that took a message's wake would name the message that started the run too: withf9dbe8fb92'ssrc/, and with the start always merging the routing's messages back in, "a held autoContinue run's retry that takes a message's wake names that message, not the one that started the run" fails (Tests). Without the condition on the turn's rounds, a start that joins a turn under way would stop naming the message that opened the turn, and two tests fail: "a start that takes a module's wake into a turn already under way names the turn's opening message too" and the same start into a turn that began a round whose results it didn't flush (Tests). A round counts once it reaches its tool calls, before its results are in the agent's context, since its speech can already have been routed by then. With only a round whose results were flushed counting, the second of those fails (Tests). Only a cause that brought messages takes the continuation's place. A heartbeat that isn't silent, or an operator's nudge, has a cause of its own and brings none, so beside an autoContinue wake in the scheduler's merge it would otherwise leave the turn naming nothing, and a console could no longer tell the turn its message began. With28f67315fd'ssrc/, "a start merged from an autoContinue wake carrying its turn's opening message and a heartbeat names that message…" and "a heartbeat that continues an open turn before any of its rounds reached its tool calls keeps the turn's opening messages…" fail (Tests). Ata122e2b513, with the merge taking any cause of its own (src/framework.ts:21108there,:23351at the head), the first fails. The start tests the same predicate (:28286), but since9bebc9cfa3it no longer deletes the turn's messages when it has none to set (:28510–28511). So with the start taking any cause of its own, no test fails: a trigger with a cause of its own and no opening messages leaves the turn's messages in place either way, and the second test passes. By reading, the two predicates can still differ at a merged start whose members have causes of their own but no messages, beside a continuation that brought some. Taking any cause, the start sets the trigger's merged messages (:23351) in place of the routing's, wherebringsOwnMessagesgives their union. That differs only where the routing holds messages the trigger lacks, and no test makes that case (Not verified). The predicate stays the merge's, so the two read alike.context.turnIdlets a module tie a turn's pieces to each other, and to that turn'sonAgentSpeechcall, so a module that shows speech piece by piece can tell where one turn's speech ends and the next one's begins. With the line that puts it in the context removed, "a tool turn asks about each round's prose as it routes and the trailing reply as the final speech, each with its own text", which checks that its pieces name one turn, fails.[written as\u005b, words such as[delivered …can't read as the start of a notice. "the notice's excerpt quotes a quote so it can't end the excerpt" and "the notice's excerpt names a piece that opens with an unclosed bracket by its words" check those forms (test/speech-sent-receipts.test.ts:434,:436). With the opening put between plain quotes instead, those two fail, and the other three tests of the excerpt pass.3471d88cd9'ssrc/,refusal-rewind's test of a notice that quotes the shed reply fails, and so doesspeech-sent-receipts' test of a rewind's retry, at its check that the retry carries neither the reply nor its quote (Tests).Tests
ee7798b8b6(Build & Test, ubuntu-latest, Node 22.12.0) ran on its merge intomain36b6dfe9de39. It ran 10,967 tests: 10,903 pass, 0 fail, 63 skipped, 1 marked todo. Its unforced run passed 133 of 133. Its three runs at the dependency floors gave the same counts. Each used chronicle 0.6.0 and context-manager 0.27.0, with membrane 0.8.0, 0.9.0 and 0.10.0. Every one of its 16 checks passed.main. These ran on a clean checkout ofee7798b8b6, on Linux with Node 24.22.2. The repository has no lockfile at its root. Each arm removednode_modulesandpackage-lock.jsonbeforenpm install, which then gave chronicle 0.6.0, context-manager 0.27.0 and membrane 0.10.2. Each test file ran on its own, with--test-force-exit, the flagnpm testuses. The predictions were written before the run.ee7798b8b6, the build succeeds, and every test of these files passes:speech-sent-receipts82,exhausted-turn-rearm85,held-turn-retry92,prose-drafts265,route-failure-targeting73,present-while-acting96,explicit-prose-routing121,auto-continue44,publish-outcome35,admission-e2e19,conversation-routing40,batched-wake-locus18,deferred-import-boot32 andrefusal-rewind28;unprompted-channel-routing38, whichmainadds,inference-exhaustion-rewind15,inference-failure-bounds34,inference-failure-observability3,ops-alert5,overbudget-drain-breaker10,channel-registry-routing81,send-into-closed-channel24,chat-system-notices9,ephemeral-no-global-fallback11,locked-prose-routing31 androuting-baseline-restart5;main's that this merge meets:batched-wake-line13,batched-wake-pointer43,budget-refusal-hold93,gate-batch-in-window5,provider-acceleration-cooldown12,provider-waits93,continuing-start-silence34 anddebounce-seen-wake63.main's (36b6dfe9de39)src/in place of the head's: the build reported 3 type errors, each inspeech-sent-receipts, and still wrote the output the tests ran from.speech-sent-receipts: 75 of 82 fail. The 7 that pass check whatmainalready does: explicit routing asks no module about speech it holds or bounces, an envelope whose{{unsent}}names a draft still being sent holds its words without asking, the log line for a reply no module showed saysturn has no locus, the ephemeral run with no channel, the hybrid plain words of a heartbeat's turn with no locus that a replaced agent writes, and the pieces with a channel and with no channel that a replaced agent routes after another agent took its name.mainasks no module at all, holds those words and routes those pieces under the name.prose-drafts: 25 of 265 fail.batched-wake-locus: 8 of 18 fail: six that check the no-channel notice, at its opening words, the test of an operator's unstick, and fix(gate): keep notices out of a batch's route; name its channels #948's test of a!continuation after achannel_open, at the turn-start[routing]notice's new words.route-failure-targeting: 7 of 73 fail: "keeps primary failures in the primary context manager", whose assertion follows the notice's new wording, and six that check the notice of a reply by a speaker that was released or has ended, at its new words.present-while-acting: 6 of 96 fail: the three that check the inbound-only note, in the[routing]notice for a locus its server declared inbound-only, in the notice of an addressed arrival that re-pins there, and inchannel_open's result, and three that check the receipt's drafts note.held-turn-retry: 4 of 92 fail: the two tests of a wake's opening messages kept when the start is released, at those messages, the release whose member the read check drops, at its premise that the first member's own message is named, and the new test of a held turn's retry released with a wake of its own cause.exhausted-turn-rearm: 1 of 85 fails, the test of a re-armed continuation's opening messages.explicit-prose-routing,auto-continue,publish-outcomeandadmission-e2e: 1 each. They are the receipt for words a send held back that the journal refuses, the[continue]row, the failure marker of an uncertain delivery, and "gap 2 — held traffic never moves the speech locus; under the default rule no inbound traffic does", each at its new words.deferred-import-boot: 1 of 32 fails, the gone agent's failed reply passed to the primary, at its notice's words.refusal-rewind: 1 of 28 fails, "shed: a no-channel notice that quotes the shed reply goes with it".main's.f81f45ae38round's mutants. These ran onee7798b8b6's tree with one change tosrc/framework.tseach, on Linux with Node 24.22.2, againstspeech-sent-receipts, with predictions written first.main's code, channels only,held-turn-retryfails 2 of its 92 tests under3554ba6df6's test file: "a wake the hold folds into a held merged start keeps its opening messages when the start is released" and "a held wake that an equal merged start folds into keeps its opening messages when the start is released", each at that wake's opening message.main's two channel lines and no opening messages, 2 of 3,244 tests in 68 files fail, both inspeech-sent-receipts: "two wakes coalesced while the host is quiesced name both their messages at resume" and the three-push test. This ran on the merge before its amend, which changed onlytest/held-turn-retry.test.ts. That build reported 3 type errors, all in that file, and still wrote the output the tests ran from.1a3ef2bfac–ecb9ff7e0fround's reds and mutants. Effie-2289 ran these on Linux with Node 24.22.2 and npm 11.19.1, with predictions written first. Each arm's tree is the one its item names, as its listing shows. They didn't print their dependencies' versions.3471d88cd9'ssrc/under1a3ef2bfac's tests,speech-sent-receiptsandrefusal-rewindfail 2 of 110:refusal-rewind's "shed: a no-channel notice that quotes the shed reply goes with it", which finds only the reply removed, andspeech-sent-receipts' "a rewind that sets aside a reply no channel took sets aside the notice that quotes it, so the retry is sent neither", whose retry still carries the notice quoting "Hi there.".1a3ef2bfac'ssrc/fails one of those 110 tests. Matching every notice that records an opening, whatever it quotes, fails "shed: a notice next to the shed reply that quotes an earlier reply stays". Leaving the notices out of a presented window's retraction fails the rewind's retry test. Leaving them out of the stored record's removal failsrefusal-rewind's shed test.1a3ef2bfac'ssrc/under7e5b6fa11c's tests,speech-sent-receiptsfails 1 of 85, "a piece held as a draft that a module showed is logged with its place as it's named, as a piece with no channel is", which finds no line.speech-sent-receiptsfails 2 of 85 under7e5b6fa11c's tests, the two tests of a send that held a turn whose tool rounds aren't routed live. Underecb9ff7e0f's tests 4 of 85 fail: those two, and the two tests of a live round a send held, at their counts of exactly one ask.99e5c38198. These ran on Linux with Node 24.22.2 and npm 11.19.1, with predictions written first, on a clean checkout of99e5c38198, the head before the catch-up, with membrane 0.8.0. There the build succeeds, and the 25 files of the first two lists above other thanunprompted-channel-routingpass,held-turn-retry91 andbatched-wake-locus14. With66107dd7b0'ssrc/, the commit before the unstick's,held-turn-retrypasses 91 of 91, andbatched-wake-locusfails 1 of 14, the unstick's test. Its notice there reads "Your plain speech "unstuck words" (13 chars) had no channel to go to — this turn came from no channel, and it had no home or recent channel to reply in; …".3471d88cd9's tree with predictions written first, each with one of the merge's choices undone. They didn't print their dependencies' versions.markFollowOnalso dropping feat(routing): configurable homeChannel for no-trigger turn speech #244'sUNPROMPTEDmark, 3 of 100 tests fail, one at each site that spreads it: theunprompted-channel-routingtests of autoContinue's next turn, a!continuation and a bounced reply's wake after a timer's turn, in explicit prose routing.takesUnpromptedChannelin the registry's options, 9 ofunprompted-channel-routing's 38 fail.speech-sent-receiptshad 82 tests, then 83. They didn't print their dependencies' versions.9f44b3580d'ssrc/under the round's test file, 2 of 82 fail: the test of a hook that returns a promise, at its premise, since thatsrc/waited for the promise and named its place, and the test of a place's callback that throws.7ec56f763d's test: withbd409aece7'ssrc/, 1 of 83 fails, the promise that rejects, since nothing logs the rejection there.9c60724e1d. The amend changed one line oftest/batched-wake-locus.test.ts, the words fix(gate): keep notices out of a batch's route; name its channels #948's continuation test expects, so that test failed in each arm. They ranspeech-sent-receipts,route-failure-targetingandbatched-wake-locus, with one of the merge's choices undone in each arm:cameFromonrouteLocusSpeech's no-placesrouteSpeechcall, 5 more fail, thebatched-wake-locustests of a notice that names the batch's channels;batched-wake-locustests that check the notice's opening androute-failure-targeting's "keeps primary failures in the primary context manager";main'sbyDesignclause dropped, 6 more fail, the fivebatched-wake-locustests of a notice that names the batch's channels and the sleep's announcement.speech-sent-receiptshad 105 tests. Each ran from a tree of this branch's commits, changed where its item says. They didn't print their dependencies' versions.a382089bc5: the build succeeds, and fifteen of the files above pass 1018 of 1020 tests. The two that fail are the ones the next commits fix:held-turn-retry's release of a held merged start whose fixture names a fieldmainreplaced, and the inference-failure rewind test, whose unpresented row givesmain's shed nothing to take, so no retry is queued.e285025060's test: with the merge's rule in place ofholdRequest's union of opening messages,speech-sent-receiptsfails 1 of 105, that test, which gets['m-second']for['m-first', 'm-second'].50fd42ce20's assertion: at its parent,deferred-import-bootfails 1 of 32, that case, on the old words' pattern.bc71a9a170's check: with a[discord-send-failed]notice planted in the primary's window,route-failure-targetingfails 1 of 73, that test, at "the primary was told nothing of that reply". With the check beforebc71a9a170, the same plant fails nothing.a122e2b513and before. These ran on Linux with Node 24.22.2, each run after a freshnpm install, which gave chronicle 0.5.1, context-manager 0.24.2 and membrane 0.8.0. Each round also ran its new or changed tests against the round before it. This description's earlier versions list those runs, and the claims above that name an earlier round'ssrc/come from them. Of what the runs below tested,bd409aece7removed the wait and its settles, andf81f45ae38the checks for a replaced agent before any route and before each send, and the items for those are left out.a122e2b513'ssrc/, each run againstspeech-sent-receipts,locked-prose-routing,refusal-rewind,prose-drafts,explicit-prose-routingandrouting-baseline-restart, and the thirteen to the settles of a failure oncestop()has closed admission or of a stream's end, the quiesce park, the merge of wakes into one start, the fold and the release, and the starts' opening messages, againstheld-turn-retryandexhausted-turn-rearmtoo. Each fails only the tests named here, all inspeech-sent-receiptsbut where another file is named, and nothing in the other files:{{unsent}}envelope's, and the hybrid reply held after a refused target;turnIdin the ask's context, one fails, the tool turn whose pieces name one turn;mainlogs it, three fail: the receipt that can't be recorded, at its line, and the one whose channel's label is long, at its premise, which finds no line in the new form, and inprose-draftsthe{{unsent}}draft's test, at the same premise;a3e955d4a7did, one fails, the three parked pushes with one key, at the messages named at resume;held-turn-retry, the wake the hold folds into a held merged start, at its messages when the start is released, and inspeech-sent-receipts, the two equal wakes a provider hold keeps as one, at the messages named at its release;reasoncheck, and the start that takes a module's wake into a turn already under way, at its premise that the turn already under way opened it;prose-draftsthe{{unsent}}draft's test, at its check that the line names the draft;[send-undeliverable]notice is written, since another module's place still counts;held-turn-retry: the wake folded into a held merged start and the held wake an equal merged start folds into, each at its messages when the start is released;c63805592adid, one fails, inheld-turn-retry: the release whose member the read check drops, at its check that that member's message isn't named;[speech-not-posted]notice's words fromf9dbe8fb92, three fail, inprose-drafts: the three failures after a tool round;28f67315fddid, one fails, inheld-turn-retry: the start merged from an autoContinue wake and a heartbeat, at its check of the start's opening messages;28f67315fddid, none fails: with the start's delete gone, a trigger with a cause of its own and no opening messages leaves the turn's messages in place either way;f68512e36bcut the test of a host with no channel registry;speechOpening(words!)whateverwordsis, one fails, the no-channel notice whose words its host didn't pass, finding no notice;{{unsent}}delivered to a channel the receipt lists left out of the receipt-failure line, one fails, inprose-drafts: the new test, at its check that the line names the draft;"";main:dist/src/, run againstspeech-sent-receipts, fails one test and no other, at2c136f113b. A refused round's retry without the turn's opening messages fails the test of that retry. The breaker's retry without them fails the test of that retry, at8c37ac9409too.8263ac7635, before the breaker's fix: the breaker's retry read the routings map that a framework built from its prototype, asinference-exhaustion-rewindandinference-failure-boundsbuild it, doesn't have. In our run of that commit,inference-exhaustion-rewindfailed 8 of 14, each with "Cannot read properties of undefined (reading 'get')".2c136f113b, when a console turn's first request was refused and the rewind shed its opening message, the retry'sopenedBy.messagesnamed that message's id, and the module'sgetMessagereturnednullfor it.Not verified
src/, and the reds of earlier rounds, ran ata122e2b513and before it. None ran again after the catch-ups, the synchronous round or this round, apart from those rounds' own reds above.Surfacein the new test file) and a stand-in provider (MockMembrane). No module in this repository implements the hook, andgit grepfinds noonSpeechRoutedin connectome-host'smainat5d60376e15.src/framework.ts:1701–1709). No test runs that case.scripts/connectome-doctorshows the new log line. By reading, at connectome-host'smain5d60376e15, it reports the last[routeSpeech]line as not delivered when it contains "no locus", by its count and channel when it matchesrouted <n> chars -> <channel>, and otherwise by its last 60 characters (scripts/connectome-doctor:308–317). So the line for a reply a module showed would show as its last 60 characters, and the line for a reply no module showed still shows as not delivered. Nothing ran it.main.unflushedRoundsby hand, build the turn's open routing by hand, asmain's "the non-streaming fallback leaves the scheduling turn's logical turn open for the re-inference" does (test/explicit-prose-routing.test.ts:1448), and queue atool_results_readyrequest beside a module's wake. An attempt through a realtool_results_readyhung. By a look at its log, it had run the streaming path, which hands a round's results back to the same stream and never queues that request, and its stand-in provider, scripted for one response, left that stream waiting.src/framework.ts:24473–24477), and the modules after the one that replaced it are still called with the piece, since the modules are called in one pass (askSpeechRouted,src/module-registry.ts:758–792). The test that replaces the agent from inside a hook is of a piece with no channel.src/framework.ts:22989,:22993). So a message that the held request and a member both hold stays only on that member, and if the read check drops that member, the turn doesn't name it. The exception is a held request that goes back as a member of its own for a budget wait (:22982–22987), which keeps its messages. Atmainthe held request runs at the release only in that case, and otherwise its batch runs in its place (:22883–22898at36b6dfe9de39). No test builds that state.src/framework.ts:22988–22994), and they go where that member goes. The scheduler's merge drops them with it when it has no cause of its own and another of the merge's requests brought messages of its own, whether another member or a request the same pass brings (:23351), and an abandon at a quiesce or the read check can drop that member.holdRequest(src/framework.ts:7379).main(Tests), and a piece with no channel whose own hook call replaced the agent isn't routed (The call). By reading only: its held drafts, its bounces and their notices still go to the name, as atmain, so those notices reach whichever agent holds it (bounceProse,src/framework.ts:27404;takeUnsent's no-draft branch,:26912–26934), and askip_reply {{unsent}}envelope it writes,>>or>>>, still sets aside the name's latest bounced draft (dismissLatestBounce,:23521,:23579,:27089,:27232). Its tool calls, thedraftstool's resend and a server's own sends among them, go as atmain. A test checks that a replaced agent's heartbeat words under hybrid routing are held, but none checks whose context a replaced agent's bounce or notice reaches, or runs its tool calls.src/framework.ts:30406), with the messages that opened the turn it retries. A test checks the unstick's own run, and a test runs a retry inside an unstick (test/refusal-rewind.test.ts:505–524), but none checks that retry's opening messages.quarantinePoisonedHistorydirectly. By reading, the failed turn's routing, which the retry takes its messages from, is still the agent's latest when the breaker runs, since it runs inside that turn'sinference:exhaustedtrace. No test runs three failures to the breaker.openedBy. By reading, it's a copy of the request whose stream failed, withreasonset toexhausted-turn-rearmwhen that request was a continuation (src/framework.ts:7501–7512,:28413). The test checks only itsmessages.disabled. The hook's doc says modules are asked only under locus, hybrid or locked routing (src/types/module.ts:225–226). A test runs explicit routing with a module that implements the hook, and checks that it's never asked. No test runsdisabledwith one.stop(). An ephemeral run's end leaves its stream to finish on its own (src/framework.ts:14345,:14361), andstop()waits only for the streams it still holds (:6593). So the receipt of a run its reply ends can come after the run returns, and astop()before it comes loses it with nothing logged. By reading,mainsettles a run before it writes the turn's receipt and drops the run's stream at its end the same way (src/framework.ts:30166,:30525,:14285at36b6dfe9de39), so an ephemeral run's receipt for a slow post to a channel can be lost there too. No test runs it.bringsOwnMessages(src/framework.ts:28286,:28510), and with any cause of its own taken instead it would set the trigger's merged messages (:23351) in place of the routing's. Once a round has reached its tool calls, the start takes the union whichever predicate decides. The two differ only where, before that, the routing holds messages the trigger lacks. No test makes that case, and whether a start before its tool calls has such a routing isn't traced.src/framework.ts:23068–23071), so a plain push's messages can ride a silent request. A later merge in which another request brought messages of its own keeps only the messages of requests with a cause of their own (:23351), so those are dropped, though the plain push also woke the turn. At that merge the rule can't tell messages the park wrote from ones a release carried onto a silent first member (:22988–22994). No test makes this case.test/speech-sent-receipts.test.ts:1485,:1487). A push builds its request with no opening messages (src/framework.ts:22708–22730), so a plain push carries them only as a hold's fold writes both requests' messages onto the one it keeps (:7379), as a hold's release gives them to its batch's first member (:22988–22994), or as the park writes them onto it (:23071). A push to the agent its message is written to reads that message's row (readsRow,:22729,:35226–35227), and the park's key includes that row (:23067), so such pushes park under keys of their own. That's why the test's pushes go to a second agent. No run shows a release that gives a member opening messages, parked beside a silent tick's push to another agent.src/framework.ts:28286), then sheds its batch (:28289–28292), so the turn's retry keeps the trigger'ssourcewith the start's merged messages, and a later merge counts them as the retry's own (:23351). So where a heartbeat leads a start with an autoContinue wake that carries a message A, the start is held, and a module's wake with a message B joins the retry, the turn names both A and B. Telling them apart would mean recording where each request's messages came from through every place they combine, which this change doesn't add.