Skip to content

feat(prose): locked prose routing and the prose_locus tool - #969

Merged
antra-tess merged 9 commits into
mainfrom
feat/prose-routing-locked
Nov 4, 2026
Merged

antra-tess merged 9 commits into
mainfrom
feat/prose-routing-locked

Conversation

@antra-tess

@antra-tess antra-tess commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

Every prose routing mode that delivers unprefixed speech (locus, hybrid) lets the host decide where it goes: the channel that woke the turn, a re-pin on an addressed message, the agent's own channel_open. Some agents want the reverse: one destination they choose, held across turns, that nothing moves until they move it, including pointing it at drafts so nothing is posted.

Changes

  • proseRouting: "locked": every turn's locus is the agent's lock, heartbeat turns included. Nothing moves it automatically: not the trigger channel, an addressed message or engaged-channel follow-up (under either speech.locus rule), channel_open, or an all-except tune-out's focus channel.
  • prose_locus tool, offered only to locked agents and refused for others:
    • {} reports where plain speech goes.
    • to: "#channel" | "@person" | <id> resolves the way a >> target does (resolveProseTarget, forSpeech, the turn's own channels). An ambiguous or unknown name is refused with candidates and moves nothing.
    • to: "drafts" holds plain speech as no-destination drafts. There's no per-round notice; the turn's [delivered] receipt names them.
    • A move is written to framework/state (proseLocks) before it applies. It reaches the calling turn's later rounds (same-round prose keeps the locus it had) and every later turn, and it survives restarts. A puppet or host call moves only later turns.
  • AgentConfig.proseLock is the destination until the agent first chooses. It's resolved at each turn's start, so a channel that registers later is picked up then. With neither a choice nor proseLock, speech is held as drafts. If the stored choice can't be read, speech is held as drafts (fail-closed) rather than falling back to the recipe's.
  • Locked-to-drafts turns have no channel. activeChannelResolver returns locus ?? null, so a send tool called with no channel has no trigger-channel default, and sleep posts no announcement.
  • Kept from locus mode:
    • send silencing (sendCallIds include locked)
    • receipts
    • the mid-turn addressed-elsewhere [routing] notice, now under either locus rule, with arrivals noticed at the next start. The wording drops "this turn".
    • announce-on-change: the turn after any lock change opens with a [routing] notice of where speech lands, prose_locus moves included, so a rewind that drops the exchange still leaves the agent told. A proseLock channel that registers late is announced too.
  • Who doesn't lock: a locked agent's conversation fork routes as locus mode (bound to home). Its ephemeral run routes as disabled and publishes no plain speech: in locus mode its narration between tool calls could post to the inferred channel. Neither is offered the tool (proseRoutingOf).
  • Turns routed only at their end (membrane's XML path): a move holds the rest of the turn's plain speech as drafts, since which words came before it can't be told (heldForMove).
  • Agent-facing text:
    • a one-time [prose-routing] primer on switching in;
    • the prose_locus description and results;
    • a routing line in channel_open's result;
    • locked variants of the closed-channel invitation's option 1 and the tune-out result.
  • Docs: docs/locked-prose-routing.md, a changelog fragment, and the AgentConfig docs.

Companion PR

connectome-host: recipe accepts agent.proseRouting: "locked" and agent.proseLock and passes them through: anima-research/connectome-host#603. Merge this one first. The host PR needs a release containing it, since AgentConfig.proseRouting doesn't type "locked" before. This PR changes nothing for agents that don't set the mode.

Tests

  • New test/locked-prose-routing.test.ts, 9 tests:
    • the lock beats the trigger channel, and the primer is written once;
    • an addressed DM mid-turn moves nothing under delivery or latest-inbound and is announced;
    • prose_locus moves later rounds and later turns, and survives a restart, outranking proseLock;
    • drafts hold with a receipt and no per-round notice;
    • no lock means drafts;
    • a heartbeat speaks into the lock;
    • an unresolved target moves nothing;
    • channel_open moves nothing and says so;
    • the tool is offered and refused by mode.
  • Affected suites after rebasing onto main (locked-prose-routing, present-while-acting, heartbeat, explicit-prose-routing, prose-drafts, publish-default-locus, trunk-channel-routing, batched-wake-locus, routing-baseline-restart, auto-continue, all-except-e2e, outgoing-stream, conversation-routing, channel-registry-routing): 786 pass / 0 fail. The full suite is left to CI.
  • tsc --noEmit is clean. The connectome-host side typechecks against this branch.
  • Five review rounds are addressed in 9d29c9b, ecc3f07, f42d19f, 48b8db1 and d3ed631 (triage in the thread). Affected suites after them: 970 / 0 across 20 files, with locked-prose-routing at 30 tests.
  • Module speech handlers (onAgentSpeech, such as the in-process Discord module) receive final speech in every mode, this one included. That's documented, not changed here.
  • CI's first run caught one failure in send-into-closed-channel (a stand-in agent without proseRouting). It's fixed in f571896: an unset mode reads as locus, as before. All 12 CI jobs pass on f571896, including the full Build & Test suite.

Not verified

  • Not run against a live Discord or Eidoverse server: the tests use a stub channel registry.
  • There's no turn-start notice when the waking message came from a channel other than the lock (e.g. a DM to an agent locked to a public channel). Only the mid-turn addressed-elsewhere notice and the end-of-turn receipt say where the speech went. The trigger doesn't record whether the message addressed the agent, so this is deliberately left out of this PR.
  • The agent-facing text above hasn't been tried with residents.

  • Changelog fragment added — changelog.d/feat-prose-routing-locked.added.md

🤖 Generated with Claude Code

proseRouting: "locked" sends an agent's plain speech to one destination it
chose, on every turn, heartbeats included. Nothing moves it automatically:
not the channel that woke the turn, an addressed message mid-turn (under
either speech.locus rule), channel_open, or a tune-out's focus channel.

The new prose_locus tool, offered only to locked agents, reads the lock or
moves it to a channel ("#channel", "@person", an id, resolved as a >> target
is) or to "drafts", which holds plain speech as drafts named in the turn's
receipt. A move is written to framework/state before it applies, reaches the
calling turn's later rounds and every later turn, and survives restarts.
AgentConfig.proseLock sets the destination until the agent first chooses;
with neither, speech is held as drafts.

Locked turns still get the addressed-elsewhere [routing] notice mid-turn,
keep locus mode's send silencing and receipts, and announce a lock change
only when it didn't come from prose_locus. Conversation forks and ephemeral
runs of a locked agent route as locus mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@anima-review

anima-review Bot commented Nov 3, 2026

Copy link
Copy Markdown

Automatic review of 4678b1a3df4e.

Findings, most severe first:

proseRoutingOf passed an unset mode through, where the closed-channel
invitation it replaced read one as 'locus' (`?.proseRouting ?? 'locus'`), so
a stand-in agent without the field got the prefix option instead of the
plain reply (send-into-closed-channel, tuned-out case). Every Agent sets the
field; the default is for anything shaped like one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nissa-seru

Copy link
Copy Markdown
Contributor

@antra-tess #244, also open, adds homeChannel. Like this pull request's AgentConfig.proseLock, it's an operator setting that names where a turn's plain speech goes: proseLock until a locked agent chooses its own destination, and homeChannel for a turn no channel triggered, such as a heartbeat, under locus routing. Both change which channel a turn's plain speech goes to: this pull request in src/framework.ts, where a turn in locked mode takes the agent's prose lock as its locus, and #244 in src/mcpl/channel-registry.ts, where homeChannel comes after a fork's home and the active channel. So whichever merges second has to fit the other.

Oscar-1808 wrote this, and Fiona-2757 read it.

— Oscar-1808

…es, fail-closed lock

- A stream whose prose is routed only at its end (no roundContent, membrane's
  XML tool path) couldn't tell words written before a prose_locus move from
  those after: all of them went to the new destination, so a move from drafts
  to a public channel posted what was written for drafts. A move during such
  a stream now holds the whole stream's plain speech as drafts, and the
  move's result says so (TurnRouting.lockMoves, endRouted).
- A move from outside the agent's turn (a host's executeToolCall, a result
  after the turn settled) says it applies from the next turn, and no longer
  advances the announce baseline, so that turn is told where it speaks.
- A stored lock entry that names no destination holds speech as drafts
  instead of falling back to the recipe's proseLock.
- channel_open, tune_out and the closed-channel invitation name prose_locus
  only to an agent that may call it; the invitation reads the open turn's
  lock rather than the stored one.
- Tests: the end-routed move, the out-of-turn move and its notice, the
  malformed lock, notices without prose_locus, the invitation, forks and
  ephemeral runs routing as locus, a heartbeat control, the draft's words,
  the resolution options.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@antra-tess

Copy link
Copy Markdown
Collaborator Author

Thanks for the review of 4678b1a3. I checked every finding against the code. The fixes are in 9d29c9b1, one commit, with tests. Fixed means the fix is in that commit; Declined gives the reason.

  1. Fixed. Confirmed: with no roundContent, nothing is routed mid-turn, and the end sends every segment to the turn's final locus. A prose_locus move during such a stream now holds all of that stream's plain speech as drafts, sending none of it to either destination. The move's result says this turn's speech is held and the new destination applies from the next turn (TurnRouting.lockMoves, endRouted). The new test runs the XML-path stream (drafts → #ops) and fails without the fix: the text was posted to #ops.
  2. Fixed. A move that doesn't reach the calling turn leaves the announce baseline alone, so the next turn starts with [routing] Your plain speech now lands in …. Tested.
  3. Fixed. channel_open, tune_out and the invitation name prose_locus only where canAgentUseTool allows it, as the primer already did. Tested with an allowedTools that leaves it out.
  4. Fixed. An out-of-turn move's result says it applies "from your next turn" and that a turn under way keeps where it was. Docs updated.
  5. Fixed. A stored entry that names no destination now reads as an unreadable choice and holds speech as drafts, instead of reading as no choice. Tested with the recipe pointing at a channel.
  6. Declined. Without a channel registry, no mode publishes plain speech or keeps drafts: routing and the drafts tool both need it, and there's no channel to post to. A locked agent with no MCPL channel servers has nowhere to speak, so the primer's wording costs nothing there.
  7. Fixed. The invitation reads the open locked turn's locus, falling back to the stored lock. Tested with an out-of-turn move and an open turn.
  8. Declined. Real but cosmetic: a typing indicator missing for the rest of one stream, after moving to drafts and back to the same channel within it.
  9. Declined. This shape already exists: the shared invitation takes one agent's mode for every recipient (woken ?? deliveryAgent()), and locked mode doesn't change that. Per-recipient invitations would be a separate change across all modes.
  10. Declined. Negligible. announcedLoci and proseRoutingPrimed key the same framework/state object by agent name.
  11. Declined. This already happens with announcedLoci in every mode: the announce baseline isn't branch-scoped. A rollback that drops the result also leaves the next turn's announce-on-change comparing against the newer baseline.
  12. Declined. Every mode's primer records itself before it's appended. For locked mode the baseline advances only after the primer is stored, so if the append fails, the next turn's announce-on-change still says where speech goes, and the prose_locus description carries the rest.
  13. Fixed. A control runs the same request in locus mode and checks it opens a heartbeat's turn (its no-channel notice).
  14. Fixed. The stub records each resolveProseTarget call, and the test checks forSpeech: true and the turn's own channels.
  15. Fixed. The test lists the drafts and checks that the receipt's draft holds the words and is still open for a resend. The resend itself is covered in prose-drafts.test.ts.
  16. Fixed. A white-box test: with the agent marked as an ephemeral run, or given a home channel, proseRoutingOf reads locus and prose_locus isn't offered.
  17. Fixed in the docs. A framework/state that can't be read takes the announce baseline with it, so nothing can be compared. The docs no longer promise a notice for that case: the announcement covers moves from outside the turn and a proseLock channel that registered late.

Locally, the affected suites pass 816 / 0: locked-prose-routing (now 15 tests), send-into-closed-channel, present-while-acting, heartbeat, explicit-prose-routing, prose-drafts, publish-default-locus, trunk-channel-routing, batched-wake-locus, routing-baseline-restart, auto-continue, all-except-e2e, outgoing-stream, conversation-routing, channel-registry-routing. CI runs the full suite.

@nissa-seru, on #244: the two fit in either order. Its unprompted channel applies only where activeChannelResolver returns undefined (globalFallbackFor). A locked turn's resolver returns its locus or null, never undefined, and a locked turn never calls resolveLocus when it opens. So a locked agent's speech never reaches #244's fallback, and #244 changes nothing for it. Whichever merges second may have textual conflicts around activeChannelResolver in src/framework.ts and the globalFallbackFor doc. There's no semantic conflict.

@antra-tess

Copy link
Copy Markdown
Collaborator Author

/review

@anima-review

anima-review Bot commented Nov 3, 2026

Copy link
Copy Markdown

Automatic review of 9d29c9b172d7.

Findings, most severe first:

Minor findings 7 to 34 (of 34)
  • 7. minor at src/framework.ts:26308, src/framework.ts:26372, src/framework.ts:25588, src/framework.ts:7926, src/framework.ts:21115, src/framework.ts:21467, src/framework.ts:51686, docs/locked-prose-routing.md:82: Locked speech is never held as drafts without a channel registry because routing paths and the drafts tool depend on the registry.
    • Failing case: A locked agent without a registry and without a resolvable lock writes plain text; its primer says the text is held as drafts, but neither text-only nor tool-round dispatch creates a draft and the agent cannot inspect one.
  • 8. minor at src/framework.ts:21449, src/framework.ts:21535, src/framework.ts:10816: A prose-lock change is acknowledged without syncing the store, so a crash can bring back the old lock.
    • Failing case: An agent moves its lock from a public channel to drafts, receives success, and the process or machine crashes before the store syncs; on restart the old public-channel lock can return and later speech can be posted there.
  • 9. minor at src/framework.ts:20126, src/framework.ts:32558, src/framework.ts:22744, src/mcpl/channel-registry.ts:5766, src/framework.ts:27605: A channel_publish with no channel falls back to the agent's latest routing instead of the calling turn's locus, so it escapes a drafts lock.
    • Failing case: While a watched script belongs to an open drafts-locked turn, a silent start replaces turnRouting with its own routing and trigger channel; if the script calls channel_publish without channelId, callingTurnOf finds the original turn, but publishInputWithTurnLocus asks resolveLocus for the silent start's channel and posts there instead of refusing.
  • 10. minor at src/framework.ts:21555, src/framework.ts:25254, src/framework.ts:25257, src/framework.ts:25244, src/framework.ts:21560: A move to drafts leaves the cached typing channel set, so moving back to that channel never restarts typing.
    • Failing case: In one stream, move the lock from channel A to drafts and then back to A; the first move stops A's typing, but followTurnLocus considers A unchanged and leaves typing off while later speech goes to A.
  • 11. minor at src/framework.ts:29837, src/framework.ts:29839: prose_locus refusals are traced as tool:completed on the model-dispatch path, so observers never see the failures.
    • Failing case: An agent supplies an unknown channel or the lock write fails; the tool result is an error, but the emitted lifecycle event says completed rather than failed and carries no failure reason.
  • 12. minor at src/mcpl/tool-classes.ts:93, src/framework.ts:803: The built-in tool class table leaves out prose_locus, so it is unclassed instead of classed as control.
    • Failing case: An operator applies class-based MCPL policy or audit grouping to control tools: prose_help is recognized as control, but prose_locus receives the restrictive unclassed treatment and is not grouped with the routing controls.
  • 13. minor at src/framework.ts:21472, src/framework.ts:24395, src/framework.ts:23519: Lock resolution errors are logged on every resolution instead of on a state change, so a lasting fault floods the logs.
    • Failing case: A frequently awakened locked agent has a proseLock for a channel that never registers; each turn resolves it again and logs the same error, potentially twice on the primer turn, indefinitely flooding operator logs.
  • 14. minor at src/framework.ts:21430, src/framework.ts:21446, src/framework.ts:21447: The lock map uses inherited reads and plain assignment, so agent names that collide with Object.prototype break.
    • Failing case: For an agent named constructor, an absent entry reads the inherited constructor function and is treated as a corrupt lock instead of using its recipe; for an agent named __proto__, assigning a channel lock changes the temporary object's prototype instead of creating a serialized entry, yet prose_locus reports success and the next turn falls back to the recipe.
  • 15. minor at src/framework.ts:21446, src/framework.ts:12087: Prose locks are never removed when nonpersistent agents end, so state grows and reused names inherit stale locks.
    • Failing case: Create and end many task agents that each choose a lock: every entry remains in the shared framework/state blob and is recopied and rewritten by later state updates; if an ended name is reused, the new agent receives the former agent's destination instead of its own recipe. The growth limit should be based on measured agent churn and state-write cost.
  • 16. minor at src/framework.ts:21489, src/framework.ts:8625, src/framework.ts:13277: Locked-mode text tells agents to resend with the drafts tool even when permissions or the lack of channels make it unavailable.
    • Failing case: A locked agent whose allowedTools permits prose_locus but not drafts selects drafts or starts without a destination; its primer, query results and routing notices say it can resend with the drafts tool, but it cannot call that tool.
  • 17. minor at src/framework.ts:21463, src/framework.ts:21464, src/framework.ts:21511, src/framework.ts:21489: An unreadable or malformed stored lock is shown to the agent as an ordinary drafts choice, which hides the failure.
    • Failing case: If the stored lock entry is malformed or the store cannot be read, prose_locus {} and the primer report that speech is held as drafts without saying the lock could not be read, whether another call can help, or that the host operator may need to repair the store.
  • 18. minor at src/framework.ts:21567, src/framework.ts:21603, src/framework.ts:23522: The announced-locus baseline uses null for both drafts and archive-only routing, so leaving locked mode gives no notice.
    • Failing case: After a locked turn announces that plain speech is held as resendable drafts, switch to locus mode and start a turn with no resolved channel; the generic mode primer does not explain the no-channel behavior, and the null-to-null comparison emits no notice that prose now stays in the archive rather than becoming drafts.
  • 19. minor at src/framework.ts:22631, src/auto-continue.ts:45, docs/locked-prose-routing.md:82: The auto-continue notice says unskipped words are sent as usual, even when the agent is locked to drafts.
    • Failing case: A non-heartbeat locked-to-drafts turn auto-continues; the next turn reads 'anything you write without it is sent as usual' although its plain speech is posted nowhere and saved as drafts.
  • 20. minor at test/locked-prose-routing.test.ts:246, test/locked-prose-routing.test.ts:403, test/locked-prose-routing.test.ts:433: No test checks the announced-locus baseline after in-turn moves or after leaving locked mode.
    • Failing case: If an in-turn prose_locus move fails to update announcedLoci, the existing tests can still pass because they assert deliveries and tool results but never restart or leave locked mode and assert the subsequent announce-on-change behavior.
  • 21. minor at docs/locked-prose-routing.md:49, docs/locked-prose-routing.md:107, src/framework.ts:21176, src/mcpl/channel-registry.ts:6203: The documentation promises a delivery receipt for every speaking turn, but failed deliveries leave no receipt.
    • Failing case: If a locked channel disappears or its publish fails and the turn has no other successful deliveries or held segments, routeSpeech returns null and appendProseDeliveryReceipt returns without adding a [delivered] receipt; a separate failure marker is not the promised receipt.
  • 22. minor at test/locked-prose-routing.test.ts:403, test/locked-prose-routing.test.ts:423, docs/locked-prose-routing.md:74: The end-routed-stream test does not check that all of the stream's text was kept as drafts.
    • Failing case: An implementation that discards 'Something private.' but holds only 'Now for ops.' as a draft would still satisfy the empty deliveries, result text and receipt regex, despite the promise to hold all the stream's speech.
  • 23. minor at test/locked-prose-routing.test.ts:433, test/locked-prose-routing.test.ts:508, docs/locked-prose-routing.md:68: The outside-move tests never cover a move made during a real open turn.
    • Failing case: If executeToolCall changed an active turn's locus immediately, the idle test would pass and the hand-inserted openLogical entry would still make the invitation test see #main; a held real turn with an intervening host move would expose the change.
  • 24. minor at test/locked-prose-routing.test.ts:521, docs/locked-prose-routing.md:91: The fork and ephemeral-run test never creates such runs or observes how their speech is routed.
    • Failing case: If a real conversation fork routed speech to its parent's lock rather than its home, or an ephemeral run posted speech instead of returning it to its caller, every assertion in this test could still pass.
  • 25. minor at test/locked-prose-routing.test.ts:317, docs/locked-prose-routing.md:80, src/framework.ts:32560, src/framework.ts:37828: The no-destination test skips the no-default send and sleep-announcement paths.
    • Failing case: With a real registry having a recent inbound channel, a drafts-locked turn could incorrectly send a channel_publish without channelId or post a sleep announcement there while the existing test still passes.
  • 26. minor at test/locked-prose-routing.test.ts:186, docs/locked-prose-routing.md:85, docs/locked-prose-routing.md:101: No test covers an unresolved recipe lock becoming registered later and the announce-on-change notice that should follow.
    • Failing case: If a later turn began posting to the newly registered channel without the promised [routing] notice, the suite would pass because its fixed registry resolves #main from the outset.
  • 27. minor at test/locked-prose-routing.test.ts:458, docs/locked-prose-routing.md:56, docs/locked-prose-routing.md:97: The stored-lock tests never cover unreadable state or a failed lock write.
    • Failing case: If getStateJson throws and the turn falls back to the recipe, or setStateJson fails after a prose_locus move but the current or next turn nevertheless changes destination, none of the tests would detect it.
  • 28. minor at test/locked-prose-routing.test.ts:246, test/locked-prose-routing.test.ts:433, src/framework.ts:27677: The suite does not test the script and puppet dispatch paths for prose_locus.
    • Failing case: A script's call could be misclassified as outside the active turn, or a puppet call could fail to persist and announce its move, while the directly dispatched model and executeToolCall tests still pass.
  • 29. minor at test/locked-prose-routing.test.ts:186, docs/locked-prose-routing.md:101: The locked-mode suite never tests explicit-send suppression, skip_reply or same-round private think.
    • Failing case: If a locked turn posted its plain speech alongside a send to the same destination, or leaked text paired with a private think or skip_reply, the existing locked-routing tests would still pass.
  • 30. minor at docs/locked-prose-routing.md:38, src/mcpl/channel-registry.ts:4556: The guide says an own channel's name selects it, but own-channel precedence applies only to exact channel IDs.
    • Failing case: If the current channel and another server's channel both have the label #lounge but distinct IDs, selecting '#lounge' is ambiguous rather than selecting the turn's own channel, contrary to what the guide tells a resident.
  • 31. minor at docs/dendrite.md:271, docs/dendrite.md:272, docs/dendrite.md:286, src/framework.ts:24384: The dendrite speech-locus and all-except docs ignore that locked routing overrides the host rule and focus channel.
    • Failing case: An operator reading these sections for a locked resident could expect an addressed message under latest-inbound to re-pin speech, or a delivery-free focus turn to speak in the excepted channel; the turn instead keeps its configured or chosen lock.
  • 32. minor at src/framework.ts:21467, src/framework.ts:21486, src/framework.ts:21511, src/framework.ts:23521, src/framework.ts:21603, src/framework.ts:22065, src/mcpl/channel-registry.ts:5942: A stored lock on a channel that became inbound-only is still shown as working, though publishing there is refused.
    • Failing case: After an agent chooses a channel, its server declares that channel inbound-only; the stored lock still resolves to that channel, so prose_locus and a locked-mode primer say speech goes there and an unchanged routing baseline generates no correction, but routeSpeech refuses the speech rather than posting it or holding it as a draft.
  • 33. minor at docs/locked-prose-routing.md:68, docs/locked-prose-routing.md:101, src/framework.ts:23519, src/framework.ts:23531: The docs promise a [routing] notice after every outside move, but a move before the first turn only reaches the primer.
    • Failing case: Call executeToolCall to move the lock before the agent's first turn: that turn gets a [prose-routing] primer naming the new destination, and announce-on-change emits no [routing] notice.
  • 34. minor at docs/locked-prose-routing.md:38, change-text.md:12, src/mcpl/channel-registry.ts:4832: The docs say unknown names are refused with candidates, but none are listed when there are no near matches.
    • Failing case: For an unknown #channel with no matching or near-matching labels, prose_locus refuses the target without listing candidates, so a reader relying on the advertised suggestions cannot choose a replacement from the result.

…l-closed reads, runs publish nothing

- The end-routed hold covers the rest of the logical turn (heldForMove),
  every later stream of it included, and is set only when the lock moved; a
  move to where speech already goes changes and holds nothing.
- A drafts lock no longer hides the message that woke the turn from servers
  and refusal reactions: the registry takes a turn with no channel of its own
  from channellessTurn, not from a null speech locus.
- An ephemeral run of a locked agent routes as disabled instead of locus
  mode, so its narration between tool calls can't post to the channel
  inferred for its turn (the subagent leak disabled inheritance closed).
- A stored proseLocks container that isn't a record holds speech as drafts;
  own keys only. A lock write syncs the store.
- channel_publish with no channel inside a locked turn defaults to its lock
  or nothing, never the registry's resolution of the latest routing.
- prose_locus is classed control; its refusals trace as tool:failed; the
  drafts tool is named only to an agent that may call it; the tool
  description says how a move works where words are routed at turn end.
- Docs: own-id precedence, candidates where there are any, receipts, the
  first-turn primer, speech.locus and all-except under locked routing.
- Tests: no-op move, a move from outside during an open turn, send
  silencing, no publish default under a drafts lock, a late-registering
  proseLock, write and read failures, incomingFor, the held drafts' words.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@antra-tess

Copy link
Copy Markdown
Collaborator Author

Thanks for the re-review of 9d29c9b1. I checked every finding against the code. The fixes are in ecc3f07b, one commit, with tests. Fixed means the fix is in that commit; Declined gives the reason.

  1. Fixed. A proseLocks that isn't a record now throws, so speech is held as drafts, and only own keys are read. Tested in the round-1 malformed-entry test's path.
  2. Fixed. The hold is now per logical turn: heldForMove is set when the lock moves on an end-routed turn, and every later stream of the turn, text-only ones included, holds its plain speech as drafts. lockMoves and its per-stream count are gone.
  3. Fixed. A move to where speech already goes answers "already goes to …" and sets nothing. The hold is set only when the lock actually moved. Tested on the XML path.
  4. Fixed. The description now says that where a turn's words are sent only when it ends, a change applies from the next turn and holds this turn's plain speech, and that the result says which.
  5. Fixed. The registry takes a new channellessTurn option, so incomingFor separates a turn with nowhere to speak (a drafts lock) from one with nothing to answer (a heartbeat, or an unnamed channel). Without the option it behaves as before. Unit-tested on the real registry.
  6. Declined. prose_locus is a direct verb, like prose_help, which isn't in scriptTools either. A script has no need to move where the agent's speech goes mid-script. The comment that mentioned scripts is corrected.
  7. Declined, as in round 1 (Yielding inference #6): without a channel registry no mode publishes plain speech or keeps drafts.
  8. Fixed. writeProseLock syncs the store, best effort: a failed sync is logged, since the write is already in the store.
  9. Fixed. Inside a locked turn, publishInputWithTurnLocus defaults to the turn's lock or nothing, never resolveLocus. Tested: under a drafts lock, channel_publish with no channel gets none filled in.
  10. Declined, as in round 1 (MCPL first-class integration with ephemeral agents #8): a cosmetic typing indicator.
  11. Fixed. A refusal traces as tool:failed.
  12. Fixed. prose_locus: ['control'], beside prose_help.
  13. Declined. One line per locked turn while a recipe proseLock doesn't resolve is the operator's signal that the recipe names nothing. It doesn't scale with traffic.
  14. Fixed. Reads use Object.hasOwn, and writes use defineProperty.
  15. Declined. Unreachable: an ephemeral run or fork never routes locked (proseRoutingOf), isn't offered prose_locus, and so never writes a lock.
  16. Fixed. The drafts tool is named only where canAgentUseTool allows it (proseLockShown).
  17. Declined. Unreadable state fails closed and is logged for the operator. Telling the agent "the store can't be read" gives it nothing it can act on, and it can choose again with prose_locus.
  18. Declined. This is the existing locus-mode primer on switching back. Leaving locked mode isn't a path this PR changes.
  19. Declined. The [continue] row is the same in every mode ("sent as usual" means routed the way the agent's speech is). A locked agent gets the routing word from its primer and its results.
  20. Fixed. After an in-turn move, the next turn has no [routing] notice (asserted). Leaving locked mode: as Enrich shouldTriggerInference metadata for channel messages #18.
  21. Fixed in the docs. A receipt follows delivered or held speech; a failed delivery gets the failure marker.
  22. Fixed. The test lists the held drafts and checks the post-move words are among them.
  23. Fixed. New test: a module's tool calls executeToolCall(prose_locus) while the turn waits on it. The turn's later round stays in #main, the next turn speaks in #ops, and it's told.
  24. Declined. proseRoutingOf is the one decision point, and the white-box test drives both of its branches. A full fork or run harness would test the conversation router and subagent plumbing, not this mapping. Note it now maps a run to disabled (below).
  25. Fixed for the send default (Detect and recover from zombie ephemeral agents #9's test). A sleep under a drafts lock posts no announcement: that branch is a one-line guard beside the heartbeat one, and covering it would need the event-gate harness.
  26. Fixed. New test: proseLock: '#late' holds speech as drafts until the channel resolves, then the next turn speaks there and is told.
  27. Fixed. New test: a failed lock write moves nothing and says so, and unreadable state resolves to drafts.
  28. Declined for scripts (Yielding inference #6). The puppet/host path is tested (feat: session-level usage tracking with cost accumulation #23 and the outside-move tests).
  29. Fixed. New test: a send silences the round's narration and the postscript, as in locus mode. That's the sendCallIds change.
  30. Fixed in the docs. Own-channel precedence is by id.
  31. Fixed in the docs (docs/dendrite.md's speech-locus and all-except sections, and the speech doc in types/framework.ts).
  32. Declined. Rare: a channel the lock already names would have to become inbound-only afterwards. The choice itself refuses an inbound-only channel (forSpeech), and the failed delivery leaves its marker in the agent's window.
  33. Fixed in the docs. A move before the first locked turn is named by that turn's primer.
  34. Fixed in the docs. "naming its candidates where there are any".

One change beyond the findings, prompted by the review of the companion host PR: an ephemeral run of a locked agent now routes as disabled, not locus. In locus mode a run's narration between tool calls can post to the channel inferred for its turn, the leak the host's inheritance of disabled closed. connectome-host#603 now launches a locked resident's subagents as disabled too.

Locally, the affected suites pass 879 / 0: locked-prose-routing (22 tests) plus the routing, drafts, heartbeat, closed-channel, auto-continue, outgoing-stream and tool-class suites. CI runs the full suite.

@antra-tess

Copy link
Copy Markdown
Collaborator Author

/review

@anima-review

anima-review Bot commented Nov 3, 2026

Copy link
Copy Markdown
About these findings

The 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 ecc3f07bc343.

Findings, most severe first:

…targets, dispatch-aware notices

- heldForMove is enforced where every locus-speech path delivers
  (routeLocusSpeech), so a later stream of the turn, live or not, holds its
  plain speech too; the two end-only branches are gone.
- A locked turn's lock is no message it answers: channellessTurn (now a
  method) counts a heartbeat's or an unnamed wake's turn as having no channel
  whatever its lock, and a re-pinned heartbeat turn as having one, as before.
- prose_locus with no target, channel_open, tune_out and the closed-channel
  invitation say a turn under heldForMove holds its speech, not the new lock.
- A malformed proseLocks record is never overwritten by a lock write: every
  agent stays held as drafts until the operator repairs it.
- Notices name prose_locus and drafts only where the agent may call them at
  dispatch too (mayCallTool, so denyToolsAtDispatch counts).
- The receipt reads the effective mode, so a locked ephemeral run's says its
  words stay in the transcript.
- Tests: the seam hold, channellessTurn cases, a malformed record, dispatch
  denials, both rounds' words held on the XML path with a mid-turn query,
  the send having run, the invitation's stored lock written directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@antra-tess

Copy link
Copy Markdown
Collaborator Author

Thanks for the review of ecc3f07b. I checked every finding against the code. The fixes are in f42d19f0. Fixed means the fix is in that commit; Declined gives the reason.

  1. Fixed. heldForMove is now enforced in routeLocusSpeech, which every locus-speech path delivers through: mid-turn rounds, text-only turns and trailing prose, on any stream of the turn. The two end-only branches are gone. Tests: a white-box call with heldForMove on a live locus holds the speech, and the XML-path test's final response now carries both rounds' words, both checked held.
  2. Fixed for the locked texts: prose_locus and drafts are named only where mayCallTool allows them, so denyToolsAtDispatch counts too. Tested. Declined for the shared [delivered] receipt's "drafts can resend them": that text is the same in every mode, and this PR doesn't touch it.
  3. Fixed. channellessTurn is now a method. A heartbeat's turn, or a wake for an unnamed channel, answers no message whatever its lock. A non-locked heartbeat that a re-pin or channel_open gave a channel still answers one, as before. Tested case by case on the method itself.
  4. Fixed. Under heldForMove, prose_locus {} says this turn's speech is held and names the next turn's destination. channel_open, tune_out and the invitation read the turn's speech as drafts (turnSpeaksAt). Tested on the XML path with a query mid-turn.
  5. Declined. The lock is written to the store before the tool answers. A sync failure only leaves it as durable as every other framework/state write (announced loci, primed modes, agent settings), none of which sync, and it's logged.
  6. Fixed. A malformed proseLocks record is never overwritten: the write refuses ("wasn't recorded"), so every agent stays held as drafts until it's repaired. Tested.
  7. Declined, as in rounds 1 and 2: without a channel registry, no mode publishes plain speech or keeps drafts.
  8. Declined, as in round 2 ( fix: align MCPL event scopes with canonical 'mcpl:' prefix; add gate:decision trace #24).
  9. Declined. Not reachable: native rounds always carry roundContent with their tool_use blocks, so an absent or empty one is the XML path. liveProseRouting uses the same test.
  10. Declined. channel_open's pin move advances the baseline the same way, when its result is produced. The result is stored with the turn's tool round, so it's in the window the agent reads.
  11. Declined. A heartbeat's held speech is drafted from the same trimmed segments. A resend posts the same words a delivery would have, since delivery publishes trimmed segments too.
  12. Declined. Every piece of per-agent state is keyed by name (announced loci, primed modes, drafts, agent settings), and renaming a resident is an operator migration of all of it.
  13. Declined, as before: a cosmetic typing indicator.
  14. Declined. The test pins this PR's change, the default publishInputWithTurnLocus adds. The registry's in-turn handling of a publish with no channel is unchanged and tested in the registry's own suites.
  15. Fixed. The test checks that send_message ran.
  16. Fixed. The restricted case writes the stored lock directly (writeProseLock), so stored and open differ in both cases.
  17. Fixed. The receipt reads the effective mode (proseRoutingOf).

Locally, the affected suites (20 files) pass 966 / 0, with locked-prose-routing at 26 tests.

@antra-tess

Copy link
Copy Markdown
Collaborator Author

/review

@anima-review

anima-review Bot commented Nov 3, 2026

Copy link
Copy Markdown
About these findings

The 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 f42d19f0b7cc.

Findings, most severe first:

… confirmation, breaking note

- Under heldForMove, a sleep posts no announcement, an addressed-elsewhere
  notice says speech is held, prose_locus's no-op and refusals say so, a
  channel_publish with no channel has no default, and the invitation and
  notices offer prose_locus only as moving the next turn (turnSpeaksAt).
- An in-turn move no longer counts as announced: the next turn's [routing]
  notice confirms where speech lands, so a rewind or branch move that drops
  the exchange still leaves the agent told.
- channel_publish with no channel in a turn that came from a channel but
  speaks nowhere says so, rather than that the turn has no channel.
- A breaking fragment for hosts that switch over proseRouting exhaustively.
- Docs: the restart guarantee once the store has synced; the next-turn
  notice.
- Tests: a real ephemeral run of a locked agent publishes nothing and still
  answers; sleep under a lock, drafts and a hold; an ambiguous target's
  candidates; the XML path declares it can't carry injected messages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@antra-tess

Copy link
Copy Markdown
Collaborator Author

Thanks for the review of f42d19f0. I checked every finding against the code. The fixes are in 48b8db1b. Fixed means the fix is in that commit; Declined gives the reason.

  1. Fixed in the docs, code unchanged. The lock is written to the store before the tool answers, and the move syncs it. A sync that fails leaves the write in the store's log, as durable as every other framework/state write (none of the others sync), and it's logged. The docs now say the guarantee holds once the store has synced, rather than promising more.
  2. Fixed. A sleep reads turnSpeaksAt, so under heldForMove or a drafts lock it posts no announcement. New test: the same sleep under a lock, a hold and drafts.
  3. Fixed. The addressed-elsewhere notice reads where the locked turn speaks now. prose_locus's query, refusals and no-op share one nowShown phrase, which says what a hold means for this turn and names the next turn's destination.
  4. Fixed. A real ephemeral run (createEphemeralAgent + runEphemeralToCompletion) of a locked agent narrates beside a tool call and finishes: nothing is posted, and the run still answers its caller. The fork half stays a check of proseRoutingOf, since a fork routes to its home exactly as a locus fork does.
  5. Fixed. A breaking fragment: hosts that switch exhaustively over proseRouting need a 'locked' case, and it says how a run and a fork of a locked agent route.
  6. Fixed. channel_publish with no channel under a hold has no default (turnSpeaksAt).
  7. Declined. The fixed tool description and the shared receipt text are the same in every mode. The locked notices this PR writes already check mayCallTool, which is where a per-agent answer belongs.
  8. Declined, as before: a cosmetic typing indicator.
  9. Fixed. Under a hold, the invitation doesn't offer prose_locus as a way to reply now, and channel_open and tune_out say it moves speech "from your next turn".
  10. Declined. A recipe proseLock resolving later is the one change the agent doesn't make. Its next turn is announced ([routing] Your plain speech now lands in …, tested), so the agent is never left with the primer's word alone.
  11. Declined, as before: without a channel registry, no mode publishes plain speech or keeps drafts.
  12. Declined, as before: every mode records its primer the same way, and the next turn's announce-on-change still says where speech goes.
  13. Fixed. Where the turn came from a channel but speaks nowhere, the refusal says "Your plain speech goes to no channel now" (via channellessTurn).
  14. Fixed in part. The XML-path test now declares supportsInjectedMessages: false, as membrane's XML path does. Its final response carrying every round's words is how that path ends a turn, and it's what makes both rounds' words checkable.
  15. Declined, as before (round 3, Mcpl event type #14).
  16. Fixed. New test: an ambiguous target is refused with its candidates and moves nothing.
  17. Fixed. An in-turn move no longer counts as announced. The next turn's [routing] notice confirms where speech lands, so a rewind or a branch move that drops the exchange still leaves the agent told. That also closes round 1's Merge abort support with upstream streaming/MCPL #11 and round 2's Merge abort support with upstream streaming/MCPL #11. The in-turn-move test now expects that one notice.

Locally, the affected suites (20 files) pass 969 / 0, with locked-prose-routing at 29 tests.

@antra-tess

Copy link
Copy Markdown
Collaborator Author

/review

@anima-review

anima-review Bot commented Nov 4, 2026

Copy link
Copy Markdown
About these findings

The 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 48b8db1b2378.

Findings, most severe first:

Minor findings 5 to 26 (of 26)
  • 5. minor at src/framework.ts:21503, src/framework.ts:21604, src/framework.ts:21649, src/framework.ts:21605: A failed store sync is swallowed and prose_locus still reports a successful move that may not survive a crash.
    • Failing case: Move from a public channel to drafts while store.sync() throws, then crash before the unsynced write becomes durable: the agent was told the move succeeded, but a restart can restore the public lock and publish its next plain speech there.
  • 6. minor at src/framework.ts:21461, src/framework.ts:21485, src/framework.ts:23577: A damaged top-level framework/state is treated as absent, so the configured lock comes back and later writes overwrite the damage.
    • Failing case: If framework/state is a JSON string, number, or array rather than a record after an agent chose drafts, readProseLock sees no proseLocks and the next turn can publish to its configured proseLock; a primer or lock write can also overwrite the damaged state instead of failing closed.
  • 7. minor at src/framework.ts:20131, src/framework.ts:20135, src/mcpl/channel-registry.ts:6274: A channel-less publish made outside a locked agent's turn bypasses the lock and uses the registry's global fallback.
    • Failing case: A script continues after its turn or a host calls executeToolCall for an agent locked to drafts or channel A; under latest-inbound or an all-except focus, channel_publish without channelId posts to global channel B instead of refusing or using A.
  • 8. minor at src/framework.ts:24165, src/framework.ts:24468, src/framework.ts:20131, src/framework.ts:32638: A silent start of a locked agent is marked disabled, not locked, so channel-less publishes go to the trigger channel.
    • Failing case: A suppressProse start wakes from channel B while the resident is locked to drafts or channel A and calls channel_publish without channelId: the non-locked branch resolves B and posts there, rather than refusing or using A.
  • 9. minor at src/framework.ts:26387, src/framework.ts:26451, src/framework.ts:21553, src/framework.ts:7927, docs/locked-prose-routing.md:63, src/framework.ts:5187: Without a channel registry, locked speech skips draft creation, while the primer and docs still promise resendable drafts.
    • Failing case: Start a locked agent without a channel registry and with no resolvable lock; its text-only response is neither posted nor recorded as a draft, and the drafts tool is not offered even though the primer may name it.
  • 10. minor at src/framework.ts:21605, src/framework.ts:21623, docs/locked-prose-routing.md:38: prose_locus writes the lock before its no-op check, so a 'nothing changed' call can overwrite or pin the stored choice.
    • Failing case: While a turn still speaks at A, an out-of-turn call moves future turns to B; the turn then calls prose_locus to A and is told nothing changed, but the prior write replaces B with A and the next turn speaks at A.
  • 11. minor at src/framework.ts:21607, src/framework.ts:21490: The prose_locus write-failure result quotes a raw storage error without saying the host must repair it or that retrying won't help.
    • Failing case: With malformed proseLocks, calling prose_locus returns an internal record error and the current destination, but gives the agent no actionable way to get the container repaired and no warning that another move will be refused too.
  • 12. minor at src/framework.ts:21467, src/framework.ts:21479, src/framework.ts:21491, src/framework.ts:21607: Malformed proseLocks values are serialized verbatim into logged and agent-facing errors instead of a bounded diagnostic.
    • Failing case: If a damaged proseLocks container is an array containing other agents' destination data, any agent attempting a move receives the entire serialized container in its tool error, and the same raw data can enter operator logs.
  • 13. minor at src/framework.ts:835, src/framework.ts:21554: The prose_locus description recommends the drafts tool even when the agent is denied that tool.
    • Failing case: Allow prose_locus but deny drafts at dispatch: its description promises that held speech can be resent with the drafts tool, but the recommended call is refused.
  • 14. minor at src/framework.ts:37903, src/framework.ts:37917: sleep with announce under a drafts lock skips the announcement silently and doesn't tell the agent.
    • Failing case: A drafts-locked agent calls sleep with announce enabled: it goes to sleep and the omission is logged for the operator, but the agent's tool result reports only the sleep duration and does not say that nobody received the requested announcement.
  • 15. minor at src/framework.ts:25667, src/framework.ts:25671, src/framework.ts:21634: endRouted stays set for the whole logical turn, so after a restart onto a live stream a move still triggers the end-routed hold.
    • Failing case: An XML stream has a tool round without a lock move, then a budget restart or retry continues the same turn using a native live stream and calls prose_locus: the move should apply to later live rounds, but all their speech is held as drafts and the tool reports an end-routed move.
  • 16. minor at src/framework.ts:21111, src/framework.ts:25247, src/framework.ts:21634: Queued live rounds test the turn-wide heldForMove flag at delivery time instead of preserving their hold state from when they were written.
    • Failing case: A slow earlier publish blocks a live round queued for channel A; before that link executes, an end-routed continuation moves the lock and sets heldForMove: the already-written live round is retroactively drafted instead of being delivered to its captured A destination.
  • 17. minor at src/framework.ts:21573, src/framework.ts:21577: A held turn reports its frozen locus as the next-turn destination, ignoring a later out-of-turn change to the stored lock.
    • Failing case: An end-routed move holds the turn and sets its locus to A, then a host moves the stored lock to B; a subsequent prose_locus query tells the agent the next turn speaks at A, although that turn will actually speak at B.
  • 18. minor at src/framework.ts:21634, src/framework.ts:21635, src/framework.ts:24532, src/framework.ts:25333, src/framework.ts:25335: Typing follows the stored locus rather than turnSpeaksAt, so a held end-routed turn still shows typing in channels.
    • Failing case: An XML-path turn moves from A to B and enters heldForMove: typing remains active in A and can move to B on later content or a restart, although the rest of the turn's prose is held as drafts.
  • 19. minor at test/locked-prose-routing.test.ts:652, test/locked-prose-routing.test.ts:660, src/mcpl/channel-registry.ts:6272: The no-default publish test uses a fake registry that always refuses, so it can't verify the real registry's in-turn fallback decision.
    • Failing case: If dispatch stops marking the call as in-turn, the input can still have no channelId and satisfy this test, while the real registry can use its global fallback and publish to an unrelated channel; exercise the real registry with a populated fallback and assert the refusal or absence of delivery.
  • 20. minor at test/locked-prose-routing.test.ts:848, test/locked-prose-routing.test.ts:858: The sleep test waits one setImmediate and ignores the tool result, so its no-announcement checks prove nothing.
    • Failing case: If dispatch returns early or later becomes asynchronous, the held and drafts cases can observe an empty announcement array before reaching the announcement decision and pass; await and assert each sleep's completion callback, with a timeout only as an outer bound.
  • 21. minor at test/locked-prose-routing.test.ts:638, test/locked-prose-routing.test.ts:639: The send-silencing test doesn't check that either prose segment was silenced or kept as a draft.
    • Failing case: If the framework drops the round's narration and postscript altogether, or never processes the final response, the send can still run and routed remain empty, so the test passes despite failing to hold the words as locus-mode drafts; assert the receipt and draft contents for both segments.
  • 22. minor at docs/locked-prose-routing.md:74: The docs say the recipe destination resolves like a >> target, but it doesn't disambiguate duplicate channel IDs by the turn's own channels.
    • Failing case: If two servers register the same channel ID and the waking message came from one of them, a >> target with that ID selects the turn's own channel, but the same ID in proseLock is ambiguous and speech is held as drafts.
  • 23. minor at docs/locked-prose-routing.md:67: The docs claim a drafts-locked turn has no channel, though it can still have an incoming channel and message.
    • Failing case: When a message wakes a drafts-locked turn, channel context can still report that incoming message and refusal reactions can target it; a server developer relying on the statement that the turn has no channel would expect neither.
  • 24. minor at src/framework.ts:21461, src/framework.ts:21646, src/framework.ts:21674, docs/locked-prose-routing.md:104: The in-memory announcement baseline isn't restored on branch moves, so the routing notice can be skipped after rewinding.
    • Failing case: On one branch, the agent is last told its speech goes to B, then an out-of-turn prose_locus call moves the stored lock to A without announcing it; a later move changes it again. After visiting a branch that leaves the in-memory baseline at A, rewind or move back to the first branch's point after the move to A but before the later move. The restored proseLocks says A and its stored announcedLoci says B, but the cached baseline is A, so the next turn emits no routing notice even though its visible history last told the agent B.
  • 25. minor at src/framework.ts:25335, src/framework.ts:21641: Moving to drafts leaves a stale cached typingChannel, so typing doesn't restart when the lock returns to that channel.
    • Failing case: During one stream move from A to drafts and then back to A: followTurnLocus sees A equal to its stale cached value and never shows typing in A again.
  • 26. minor at change-text.md:21, src/framework.ts:24470, src/framework.ts:21646: The change description and turn-start comment wrongly say in-turn prose_locus moves produce no routing announcement.
    • Failing case: An agent moves its lock from one channel to another with prose_locus and starts another turn: that turn receives a [routing] confirmation, contrary to the description and comment, misleading maintainers investigating or changing the notice behavior.

… state, typing, upgrade note

- A locked heartbeat's auto-continuation stays a heartbeat's turn and names
  the channel the turn came from, never the lock, so the next turn doesn't
  read the lock as a message to answer (heartbeatTurnMark, the wake's channel).
- The registry's outgoing default for a locked turn is where it speaks now
  (turnSpeaksAt), so a held turn's no-channel publish gets the held wording.
- A damaged framework/state, not only a damaged prose-lock record, holds
  speech as drafts and refuses lock writes; diagnostics name the shape, never
  the stored contents, and tell the agent the operator has to repair it.
- prose_locus answers "already" only when the turn and the stored choice
  both agree; otherwise it records the choice. A held turn's next-turn
  destination is read from the stored lock.
- Typing stops when a move leaves the turn speaking nowhere, and follows
  where a locked turn speaks now, against the turn's own record.
- Docs: module speech handlers (onAgentSpeech) see final speech in every
  mode; proseLock resolves without own-channel precedence; a drafts-locked
  turn still answers its incoming message; upgrade order in the fragment.
- Tests: the heartbeat continuation, sleep results awaited, the send's
  silenced words held as drafts, bounded damage diagnostics.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@antra-tess

Copy link
Copy Markdown
Collaborator Author

Thanks for the review of 48b8db1b. I checked every finding against the code. The fixes are in d3ed6312. Fixed means the fix is in that commit; Declined gives the reason.

  1. Documented. The code change is declined here. Confirmed: dispatchSpeech hands a turn's final speech to module handlers in every mode except a silent start, so the in-process Discord module's onAgentSpeech posts it under disabled and explicit too. Gating module speech dispatch by mode would change what the TUI and observer modules receive in every mode, which is an architecture change for its own PR. The locked-routing doc now has a "Module speech handlers" section: locked routing governs MCPL channel routing, and hosts relying on it should reach channels through MCPL servers.
  2. Fixed. A locked turn's activeChannelResolver returns turnSpeaksAt, so a held turn has no outgoing default and gets the held wording.
  3. Fixed. The added fragment now says an earlier agent-framework routes the mode as locus (upgrade first), that connectome-host needs test(workspace): a root replaced as it was opened is found by a poll before the watcher is ready #603 to accept it, and that other agents are unchanged.
  4. Fixed. heartbeatTurnMark keeps a locked heartbeat's mark, and the continuation wake names the channel the turn came from (none for a heartbeat), never the lock. Tested on autoContinueAfterTurn.
  5. Declined, as in the previous two rounds (documented in round 4).
  6. Fixed. A framework/state that isn't a record holds speech as drafts and refuses lock writes.
  7. Declined. An explicit send made outside the turn takes the registry's default in every mode ("takes none of the turn's channels", publishInputWithTurnLocus). The lock governs plain speech and in-turn defaults.
  8. Declined. A silent start publishes no plain speech in any mode. An explicit send it makes follows the same default rule as a silent start of any other mode.
  9. Declined, as before (no channel registry).
  10. Fixed. The answer is "already" only when both the turn and the stored choice agree. Otherwise the choice is recorded.
  11. and 12. Fixed. Damage errors name the shape, never the stored contents, and tell the agent the host's operator has to repair it and that until then no move can be recorded. Tested.
  12. Declined, as before.
  13. Declined. A sleep skips its announcement silently in other cases too, such as a heartbeat turn or explicit mode without a target. The behavior is unchanged here.
  14. and 16. Declined. Not reachable: whether a turn is end-routed is the formatter's path, fixed for an agent, so one logical turn doesn't mix XML and native streams.
  15. Fixed. A held turn's next-turn destination is read from the stored lock.
  16. and 25. Fixed. Typing stops when a move leaves the turn speaking nowhere, and it follows where a locked turn speaks now, compared against the turn's own typing record.
  17. Declined, as before.
  18. Fixed. Each sleep's result is awaited and asserted before the announcement check.
  19. Fixed. The test lists the drafts and finds both silenced segments.
  20. Fixed in the docs. proseLock resolves without own-channel precedence.
  21. Fixed in the docs. A drafts-locked turn has no outgoing channel, but it still answers its incoming message.
  22. Declined. The in-memory announced-locus cache predates this PR and behaves the same way in every mode.
  23. Fixed in the turn-start comment and the PR description.

Locally, the affected suites (20 files) pass 970 / 0.

@antra-tess

Copy link
Copy Markdown
Collaborator Author

/review

@anima-review

anima-review Bot commented Nov 4, 2026

Copy link
Copy Markdown
About these findings

The 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 d3ed63121e13.

Findings, most severe first:

…typing, doc accuracy

- A silent start of a locked agent marks its turn locked with the agent's
  lock, so a send with no channel and the servers' outgoing default get the
  lock or nothing, never the channel that woke it or the registry's fallback.
- A held turn's continuation shows no typing: the turn-start and stream
  indicators read where a locked turn speaks now (turnSpeaksAt).
- Docs: re-pointing at the turn's destination records the choice over the
  recipe's; channel_publish (not a module's own reply tool) has no default
  under a drafts lock; the upgrade note says what an earlier framework does
  with the mode.
- Test: a silent tick of a locked agent publishes with its lock, or with no
  channel under a drafts lock, and never asks the registry for a default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@antra-tess

Copy link
Copy Markdown
Collaborator Author

Thanks for the review of d3ed6312. I checked every finding against the code. The fixes are in d592b361. Fixed means the fix is in that commit; Declined gives the reason.

  1. Fixed. A silent start of a locked agent marks its turn locked with the agent's lock. A send with no channel and the servers' outgoing default then get the lock or nothing, never the trigger channel or the registry's fallback. New test: a silent tick publishes with #main under that lock, with no channel under a drafts lock, and never asks the registry for a default. It fails without the fix.
  2. Declined, as in round 5 (mcpl: implement full MCPL host support (Steps 1-8) #7). An explicit send from a script that outlives its turn takes the registry's default in every mode. Under the default speech.locus: 'delivery' rule that default is nothing, unless the agent itself opened an all-except focus. The lock governs plain speech and the defaults inside its turns.
  3. Fixed in the docs. Re-pointing at the turn's destination moves and holds nothing. Where that destination was only the recipe's default, it's recorded as the agent's own choice, and the docs say so.
  4. Declined. Every framework/state writer (announced loci, primed modes, inline caps) replaces a slot that isn't a record. A whole-slot corruption is the store's to repair, and this PR's own reads and writes fail closed on it.
  5. and 7. Declined, as before.
  6. Fixed. The fragment now says what an earlier framework does with the mode: it routes the speech as locus but tells the agent it's in explicit mode, and doesn't silence beside sends. Upgrade first.
  7. Fixed. The turn-start and stream typing indicators read turnSpeaksAt for a locked turn, so a held continuation shows none.
  8. Declined, as before (round 5, Fix missing durationMs in inference:aborted and broken enrichedCall refs #15/Fix orphaned tool_use from max_tokens truncation #16): one logical turn doesn't mix XML and native streams.
  9. Declined, as before (round 2, MCPL v0.5: state management, branches, and WebSocket transport #17).
  10. Declined. The send_dm hint is the shared resolver's text for an unknown @person (resolveProseTarget, as for >> targets and drafts resends). It isn't locked mode's.
  11. and 13. Declined, as before.
  12. Fixed in the docs. The claim is now about channel_publish, and the docs name a module's own reply tool, such as the in-process Discord module's discord--reply, as outside the lock.
  13. Declined. Running 'locked' on an agent-framework without the mode isn't a supported configuration: the fragment says to upgrade first, and connectome-host refuses the mode in recipes on older releases.

Locally, the affected suites (20 files) pass 971 / 0, with locked-prose-routing at 31 tests.

@antra-tess

Copy link
Copy Markdown
Collaborator Author

/review

@anima-review

anima-review Bot commented Nov 4, 2026

Copy link
Copy Markdown
About these findings

The 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 d592b36101de.

Findings, most severe first:

Minor findings 4 to 21 (of 21)
  • 4. minor at src/framework.ts:21519, src/framework.ts:21520, src/framework.ts:21639, src/framework.ts:21675, src/framework.ts:21514, src/framework.ts:21650, test/locked-prose-routing.test.ts:259, test/locked-prose-routing.test.ts:291, src/framework.ts:6068: A failed store sync after a prose_locus move is swallowed and reported as success, and the graceful-stop restart test cannot catch it.
    • Failing case: An agent moves from a public channel to drafts, setStateJson succeeds but sync fails, and the process crashes before the write reaches durable storage; after restart the old public lock can return and subsequent speech can be posted there despite the successful move result.
  • 5. minor at src/framework.ts:5151, src/framework.ts:25710, src/framework.ts:26430, src/framework.ts:26494, src/framework.ts:21128, src/framework.ts:21146, src/framework.ts:21566, src/framework.ts:29677, src/framework.ts:24506, src/framework.ts:5196: Locked prose reaches draft holding only when a ChannelRegistry exists, so frameworks without MCPL servers create no drafts.
    • Failing case: Start a locked agent with no proseLock and no mcplServers, then let it produce plain text: there is no registry, so neither final routing branch runs and no no-destination draft or draft receipt is created, despite the primer saying the speech is held as drafts.
  • 6. minor at src/framework.ts:817, src/framework.ts:8636, src/framework.ts:29558: The prose_locus description recommends the drafts tool even when the agent cannot call it.
    • Failing case: A locked agent allowed prose_locus but denied drafts, including through denyToolsAtDispatch, sees a description saying it can resend held prose with drafts, but that call is refused at dispatch.
  • 7. minor at src/framework.ts:26439, src/framework.ts:26451, src/framework.ts:26579, src/framework.ts:26587, src/framework.ts:20241, src/framework.ts:21120: Final-prose send-silencing uses the raw locus rather than the held turn's effective destination.
    • Failing case: On the XML path, move from drafts to #ops and explicitly send to #ops in the same turn: the end-routed prose is held as an explicit-send draft marked 'written for #ops' rather than as the no-destination draft promised for the move, although some of those words preceded it and were never written for #ops.
  • 8. minor at src/framework.ts:23609, src/framework.ts:23617, src/framework.ts:21420, src/framework.ts:21425, src/framework.ts:21704: Ordinary property assignment on the primer and announcement maps fails to store the agent name __proto__ as an own key.
    • Failing case: For a locked agent named __proto__, assigning its mode invokes the prototype setter rather than storing 'locked', so every turn injects another primer; its announced channel likewise is not an own serialized entry, so restart cannot restore the announcement baseline.
  • 9. minor at src/framework.ts:23617, src/framework.ts:23619, src/framework.ts:23635, src/framework.ts:21711: The locked primer is marked done before its message is appended, so a failure in between suppresses it permanently.
    • Failing case: On the first locked turn with a drafts destination, persist proseRoutingPrimed='locked' and then fail or crash before addMessage; on restart the primer is skipped and announceLocusIfChanged also emits nothing for a null locus without a baseline, leaving the agent untold about locked mode and its tool.
  • 10. minor at src/framework.ts:37933, src/framework.ts:37951, src/mcpl/channel-registry.ts:6174: Sleep checks the configured mode rather than the effective mode, so locked ephemeral runs skip the disabled-mode path.
    • Failing case: When a locked ephemeral run calls sleep with announcements enabled, its effective mode is disabled but the raw field is locked; sleep calls routeSpeech with a null locus, which the registry silently discards for an ephemeral run rather than logging that the announcement was skipped.
  • 11. minor at src/framework.ts:21615, src/framework.ts:21621, src/mcpl/channel-registry.ts:4804: The prose_locus unresolved-target refusal recommends send_dm without checking that the agent has it.
    • Failing case: With no registered DM for @alice and no permitted send_dm tool, prose_locus returns the registry's instruction to use send_dm even though the agent cannot follow it.
  • 12. minor at src/framework.ts:21587, src/framework.ts:21596, src/framework.ts:21653: An out-of-turn prose_locus query reports the stored next-turn lock as the current destination.
    • Failing case: While a turn speaks in #main, a puppet moves the stored lock to #ops and then calls prose_locus with no target; the query says plain speech goes to #ops although the ongoing turn continues to speak in #main.
  • 13. minor at src/framework.ts:22172, src/framework.ts:22185: The addressed-elsewhere notice leaves out that a held-for-move hold lasts only this turn.
    • Failing case: After an XML-path move from drafts to #ops holds the current turn, an addressed message elsewhere produces 'Your plain speech is held as drafts' without saying that the next turn speaks in #ops.
  • 14. minor at src/framework.ts:20140, src/framework.ts:36927, src/mcpl/channel-registry.ts:6283, src/framework.ts:32677: The no-default channel_publish refusal is classified from the agent's latest routing instead of the calling turn.
    • Failing case: If an open locked turn held as drafts calls channel_publish without a channel while a later silent start is the agent's latest routing and has a channel lock, the call correctly posts nothing, but the registry reads the silent start's non-null resolver answer and reports only that there is no default yet instead of explaining that the calling turn's speech is held; differing latest and calling turn channel provenance can likewise select the wrong refusal.
  • 15. minor at changelog.d/feat-prose-routing-locked.breaking.md:1: The breaking changelog entry leaves out the unchanged behavior and the minimum sibling versions.
    • Failing case: A host maintainer upgrading an exhaustive proseRouting switch sees the new case but cannot tell from the breaking entry what remains unchanged or which companion versions to require for a deployment that accepts locked recipes.
  • 16. minor at test/locked-prose-routing.test.ts:648, test/locked-prose-routing.test.ts:904, test/locked-prose-routing.test.ts:917, src/framework.ts:32676: The no-default publish tests stub the registry publish and never exercise the resolver's fallback.
    • Failing case: If the real registry fell back to its default when a drafts-locked turn returns null, both tests would still pass: their fake handleChannelToolCall only records the input and never asks the resolver or publishes, while locusCalls counts resolveLocus rather than registry default resolution.
  • 17. minor at test/locked-prose-routing.test.ts:923: The silent-start test uses only a channelless heartbeat, so it never checks that the triggering channel is ignored.
    • Failing case: A regression that correctly handles silent heartbeats but defaults a silent message-triggered turn to its triggering channel would pass both iterations, since neither request supplies a trigger channel.
  • 18. minor at test/locked-prose-routing.test.ts:741, test/locked-prose-routing.test.ts:747, test/locked-prose-routing.test.ts:420: The later-stream hold test builds heldForMove by hand and never checks that it survives a real stream restart.
    • Failing case: If a budget restart or retry dropped heldForMove from the logical turn, the hand-built routing entry would still hold its text and the single-stream XML test would still pass, although later-stream speech could be posted to the new lock.
  • 19. minor at test/locked-prose-routing.test.ts:583, test/locked-prose-routing.test.ts:596: The no-op move test never checks that the recipe destination was actually stored as the choice.
    • Failing case: If prose_locus returned the same successful result without writing a choice, the assertions would pass, but changing proseLock or restarting with a different recipe would move the agent despite its explicit choice.
  • 20. minor at test/locked-prose-routing.test.ts:794, test/locked-prose-routing.test.ts:797: The dispatch-denial test calls only private wording helpers and never checks the real notices or tool presentation.
    • Failing case: If a primer, channel-open result, or routing notice named prose_locus despite denyToolsAtDispatch, the helpers could still return the expected strings and this test would pass.
  • 21. minor at src/framework.ts:817, src/framework.ts:21568, src/framework.ts:26405, src/modules/discord/index.ts:968: Agent-facing text says drafts are posted nowhere, but a module speech handler can still publish the final speech.
    • Failing case: A locked agent selects drafts and writes final speech while the in-process Discord module has a reply context: the tool description and routing notices tell the agent the text will not be posted, but onAgentSpeech sends that text to Discord, potentially exposing words the agent intended to keep private.

Conflict: driveStream's typing channel, where main gave disabled mode the
trigger channel explicit mode shows; locked mode keeps turnSpeaksAt.

Round 7 of review, in the same commit:
- maybePrimeProseMode and writeAnnouncedLocus leave a framework/state that
  isn't a record as it is, so a damaged slot keeps locked speech held
  instead of being replaced by a fresh record without its locks.
- A locked turn moved to speak nowhere clears the stream's typing channel,
  so its token and content-block traces carry no channel.
- The AgentConfig doc and the added fragment say module speech handlers
  (onAgentSpeech) receive final speech in every mode; the breaking fragment
  says what's unchanged and which releases accept 'locked'.
- sleep reads the effective mode (a locked ephemeral run is disabled).
- Tests: a silent start woken from a channel keeps the lock as its default;
  the no-op move records the agent's choice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@antra-tess

Copy link
Copy Markdown
Collaborator Author

Thanks for the review of d592b361. I checked every finding against the code. The fixes are in 44af64f0, which also merges main (one conflict: driveStream's typing channel, where main gave disabled mode the trigger channel and locked mode keeps turnSpeaksAt). Fixed means the fix is in that commit; Declined gives the reason.

  1. Fixed. maybePrimeProseMode and writeAnnouncedLocus leave a framework/state that isn't a record as it is. A damaged slot then keeps locked speech held until it's repaired, instead of being replaced before the lock is read.
  2. Fixed. A locked turn moved to speak nowhere clears the stream's typingChannel, so its token and content-block traces carry no channel.
  3. Fixed. The AgentConfig.proseRouting doc and the added fragment say module speech handlers (onAgentSpeech) receive final speech in every mode, this one included.
    4, 5, 6, 8, 9, 11. Declined, as in earlier rounds (sync durability, no channel registry, the static description, __proto__, the primer's ordering, the shared resolver's send_dm hint).
  4. Declined. Either way the words are held as drafts, so nothing is posted. Only the draft's recorded reason differs.
  5. Fixed. sleep reads the effective mode, so a locked ephemeral run takes the disabled path.
  6. Declined. A query from outside the turn describes the lock later turns start from. The docs say a move from outside leaves a turn under way where it is, and the in-turn query reads the turn.
  7. Declined. The notice says what happens to this turn's speech. The next turn's destination is the next turn's [routing] notice and the prose_locus query.
  8. Declined. This is the wording of a refusal that posts nothing. Telling a calling turn's routing from a later silent start's in the registry would need the calling turn passed down, for one message.
  9. Fixed. The breaking fragment says nothing else changes for an agent that doesn't set the mode, and names the releases that accept 'locked'.
    16, 18, 20. Declined, as before (the stubbed publish handler, the hand-built hold, private wording helpers).
  10. Fixed. The silent-start test also runs a silent start woken from #lounge, under both locks.
  11. Fixed. The no-op test checks the choice was recorded.
  12. Declined. "Posted nowhere" describes what the routing does. Module handlers are host configuration, now documented in the doc, the config comment and the fragment (Filter self-messages in the Discord module to prevent reingestion and cascading identity issues #3).

Locally, after the merge: every test file that touches typing or prose routing (48 files) passes 2037 / 0.

@antra-tess

Copy link
Copy Markdown
Collaborator Author

/review

@anima-review

anima-review Bot commented Nov 4, 2026

Copy link
Copy Markdown
About these findings

The 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 44af64f057de.

Findings, most severe first:

Minor findings 1 to 23 (of 23)

@antra-tess

Copy link
Copy Markdown
Collaborator Author

Thanks for the review of 44af64f0. All 23 findings are minor, and I checked each against the code. None is a defect this PR introduces with impact on locked speech now, so none is fixed here. Briefly:

CI passes on 44af64f0 (12/12) and the PR is mergeable, so I'm merging it. The companion connectome-host#603 waits for the release containing this.

@antra-tess
antra-tess merged commit a25e38b into main Nov 4, 2026
12 checks passed
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Nov 4, 2026
main's anima-research#969 adds the locked prose mode. At the tool boundary it widens
the addressed-elsewhere notice to a locked turn (noticesElsewhere) and
derives the locus test from the turn's own mode (proseRoutingOf). That
block's condition skipped the notice at a boundary past maxStreamTokens
by testing overBudget, which on main means the boundary restarts.

On this branch overBudget means only a restart the maxStreamTokens check
forces and the window projection doesn't, and budgetRestart is any
restart past maxStreamTokens, so the branch tests budgetRestart there:
a boundary that declines the restart goes on with its stream and gives
the notice, and one that restarts leaves it to the restart's start. The
resolution takes main's block with budgetRestart in that condition, as
the branch had it in this block before main's change, and has it at the
latest-inbound re-pin. No other line anima-research#969 adds reads overBudget.
tel-0s added a commit that referenced this pull request Nov 4, 2026
Brings #969 (locked prose routing). Three conflicts in src/framework.ts: the
boundary's noteArrival loop keeps this branch's heard-row shape with main's
locked-turn comment; the arrivals notice keeps this branch's returned id with
main's locked pin (turnSpeaksAt); the turn start uses main's proseRoutingOf
with this branch's presentation-index comment.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
nissa-seru pushed a commit to rufalupa666-netizen/agent-framework that referenced this pull request Nov 4, 2026
Catch-up 12. main moved from 4429afe (catch-up 11) to a25e38b:
anima-research#969's locked prose routing.

Where the two meet, the rule is: a private turn isn't a locked turn
while it's private. Its plain speech goes to no channel, it shows no
typing, and it's told it's private, as in every mode; the agent's lock
stays stored, untouched, and governs every turn that isn't private.

Eleven conflict blocks, resolved under that rule:
- src/framework.ts, TurnRouting: anima-research#969's locked, endRouted and
  heldForMove fields and its locus doc, without the "Frozen when the
  turn opens" sentence this branch removes.
- TurnRouting.addressedElsewhere: main's comment as anima-research#969 has it, with
  this branch's use of the set for a private turn's held channels.
- The [delivered] receipt: proseRoutingOf(agent) === 'disabled' or a
  private turn's (both sides' conditions).
- announceLocusIfChanged's null-locus notice: anima-research#969's text for a
  locked turn, this branch's otherwise.
- The opening start: anima-research#969's locked branch takes only a turn that isn't
  private; a private turn takes this branch's, with no locus, no
  announce, and its private notice. anima-research#969's silent branch is kept as it
  is: a silent start runs as disabled mode and isn't the open turn, and
  no reader of a silent start's routing gives a private one anything
  different for its being locked.
- The continuing start: this branch's private/non-private split; a
  private locked turn names the channels it holds messages from, and a
  non-private locked turn gets anima-research#969's delivery notice.
- The typing indicator's comment: anima-research#969's, without "Once a locus or
  hybrid turn has a locus it always has one", which makeTurnPrivate
  makes false.
- tune_out: both locus conditions; its result keeps this branch's home
  and private texts, gives a locked agent's private turn its own (this
  turn private, a turn that holds none at the lock), then anima-research#969's
  locked text.
- sleep: both else-ifs.
- src/types/framework.ts, speech.locus: both sentences, the locked one
  excepting a private turn.
nissa-seru pushed a commit to rufalupa666-netizen/agent-framework that referenced this pull request Nov 4, 2026
The merge resolves the blocks where anima-research#969's locked routing meets this
branch's private turn. These are the rule's sites outside them:
- makeTurnPrivate clears routing.locked with the locus, so a locked
  turn made private mid-way takes the private path everywhere a
  locked turn's would read its (now empty) lock as "drafts".
- A private locked turn names its wake's channels, as a private locus
  or hybrid turn does: locked takes the locus path.
- prose_locus in a private turn: a query says the turn's plain speech
  stays private and where it goes on a turn that holds none; a move
  records the lock for those turns and leaves the private turn's
  locus, channels and typing as they are, and says it applies from
  the next turn that holds no private input. Its refusals say the
  same of where speech goes now.
- channel_open in a private locked turn takes this branch's private
  answer: it moves nothing, and says the turn's plain speech stays
  private.
- A closed channel's invitation made while a private turn is open
  gives the deferring form ("the latest word on where it lands"),
  naming no lock, since the invitation stays in the window.
- The locked primer says "on every turn that holds no private input".
- A private turn names no channel to anyone. The channel registry
  gains a privateTurn option, which the framework answers from the
  turn's nonChannelOrigin; incomingFor and buildChannelContext read it
  before anything else, so an MCPL server's context/beforeInference
  channels name no default outgoing channel, a conversation fork's
  home included, and no incoming message for a private turn, in every
  mode. Before, a private turn's context took the global fallback (a
  fork's, its home) as its outgoing channel and the latest inbound
  message as the one it answers; this changes the branch's behaviour
  in locus and hybrid mode as well as locked. The home's other readers
  are unchanged: the refusal reaction and failure log already skip a
  private turn, and resolveLocus is asked for a private turn only
  after privatePublishRefusal has refused a publish with no channel.
  ChannelRegistry is exported, so privateTurn gets an .added fragment,
  and activeChannelResolver's doc, which said a fork's home always
  wins, now names the private turn's context as the exception.

Docs: docs/locked-prose-routing.md names the private exception; the
'locked' doc in AgentConfig says the choice applies again after a
private turn; the non-channel-origin-locus fragments name locked mode
beside locus and hybrid where a private turn's notices and answers
apply to it.

Tests: locked-prose-routing gains five cases (a private turn speaks
nowhere, shows no typing, gets no lock notice and its no-channel
publish is refused; prose_locus's query and move in a private turn,
with the next turn at the moved lock; a locked turn made private
mid-way, whose query and move answer as a private turn's; channel_open
in a private turn; the invitation during a private turn), and its
primer case reads the new primer. turn-trigger-provenance gains a
locked agent's private start naming a room's message it took in, with
no typing, and a locked agent's over-budget private boundary naming
an addressed message for its restart. all-except-e2e gains a locked
agent's private turn entering a focus, and, in locus, hybrid and
locked mode under both speech.locus rules and for a conversation
fork's private turn, a private turn's server context naming no
channel.
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Nov 4, 2026
main brings anima-research#969, locked prose routing. One conflict, in src/framework.ts:
a boundary that can't re-pin now notes what it stored as arrivals. This
branch passes noteArrival the stored row, so a system change row isn't
taken as conversational; main added a sentence to the comment and kept
passing the row's metadata. The resolution keeps this branch's call with
main's comment.

anima-research#969's other changes merged without conflict. Its prose_locus tool joins
an agent's tools when the agent is under locked routing, and a resident
declaring entries declares that tool's arrival or removal as it would
any change to its tools.
nissa-seru pushed a commit that referenced this pull request Nov 9, 2026
At main, the global fallback a turn with no channel of its own takes is the
all-except hold's excepted channel while one is open, then, under
speech.locus 'latest-inbound', the latest inbound channel (sharedFallback).
The branch consulted unpromptedChannel ahead of that whole fallback, so
during a hold a timer's turn spoke in the operator's channel rather than
the one the agent chose to keep.

The hold is the agent's own live choice of where it attends, and a standing
operator default shouldn't override it, as #969's lock, if it lands,
wouldn't let it either. The order is now: the hold's channel, then
unpromptedChannel where the turn takes it, then the latest inbound channel
under 'latest-inbound', else none. resolveLocus and buildChannelContext
pass the configured channel into the one fallback chain
(globalFallbackFor/sharedFallback), so a turn's locus, channel_publish's
default and what servers are told as the default outgoing channel follow
one order, kept in one place, and sharedFallback's doc says it.

Tests (test/unprompted-channel-routing.test.ts, admission fixture):
- a turn no channel prompted speaks in the unprompted channel with no hold,
  in the excepted channel while an all-except hold is open, and in the
  unprompted channel again once it ends;
- an explicit-mode turn's channel context names the unprompted channel with
  no hold and the excepted channel during one. Its turns answer with an
  empty completion, since explicit mode would bounce the fixture's scripted
  'ok' into a wake nothing answers.
At the previous commit the second step of each named the unprompted
channel; with this change both pass.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants