Repository navigation
feat(prose): autoContinue — continue a turn the model ends itself until skip_reply - #744
Conversation
…il skip_reply
A turn the model ends itself (end_turn / stop_sequence), with prose or with
nothing, is followed by another turn instead of a pause until the next
event, unless it ended on a skip: the skip_reply tool (or any endTurn tool),
or a last `>>skip_reply` (explicit) / `>>>skip_reply` hold (hybrid). Off by
default; AgentConfig.autoContinue: true | { maxConsecutive: 1-20 } (3).
The next turn reads a short [continue] system row, stored after the turn's
[delivered] receipt, naming skip_reply as the way to stop; near the cap it
says how many continuations are left. The wake (reason auto-continue,
source framework) carries the turn's locus. The run count resets on any
wake with a cause of its own and when a tool ends a turn. Refusals,
max_tokens, the stall guard's ends, silent heartbeat ticks and ephemeral
runs never continue; a ` !` continuation runs instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Automated review
Recommendation: merge and fast-follow. 4 medium severity findings can be fixed after the merge.
Reviewed commit 4c0bdd4 with claude-opus-5-5, by reading the code. It did not run the code or look at the checks.
Does this change belong in the project?
It fits the project. This fits. It adds an opt-in, off-by-default agent setting that extends the framework's existing continuation and wake handling, and it follows the project's usual layout for such settings: config type, validation, a doc page and a changelog fragment.
6 reasons with sources
- It is the framework's own job: deciding when an agent's next turn starts. The README says the framework handles the whole lifecycle: events, inference triggers, context compilation and inference. Starting another turn after one the model ended itself is an inference trigger. The change carries it as a framework wake with source
framework, through the same pendingRequests path the existing prose-bounce and!prose-continuation wakes use. (README.md (Overview)) - It follows the project's pattern for opt-in agent settings: off by default, typed in AgentConfig and checked when the agent is built.
autoContinueis checked inside assertAgentSwitches next to toolWrapperProseGuard and the other switches, and resolved in the Agent constructor the same wayclockis. Agents that don't turn it on behave as before. (src/agent.ts (assertAgentSwitches) and src/types/agent.ts) - It extends existing concepts rather than adding a second way to do the same thing. An explicit
!continuation the turn asked for still runs instead. The cap reuses the bounce-wake cap's rule for resetting the count, and the skip detection reads the existing explicit and hybrid routing state. (src/framework.ts (TurnRouting, requestProseContinuation, proseBounceStreaks)) - The pieces sit where similar ones already live. There is a feature doc in docs/, a small helper module in src/, and a new test file. Changes inside framework.ts are confined to the turn-end and wake paths. (docs/ (tool-wrapper-prose-guard.md, tool-result-guard.md, etc.))
- It meets CONTRIBUTING's requirements for a PR. The PR includes a changelog fragment named in the required format. It declares the connectome-host companion PR with merge-order guidance, reports the test baseline in numbers against main, and has a 'Not verified' section about live providers and forks. (CONTRIBUTING.md (What a PR should contain; Changelog))
- It adds one new public setting and exported type, and the project has room for these.
AgentConfig.autoContinueandAutoContinueConfigbecome public interface that the maintainers will have to keep. AgentConfig already carries many per-agent behavior switches, so this is in line with how the project grows. Its value to residents is still unproven until a canary run, which the PR says itself. (src/types/agent.ts)
Findings
4 findings of medium or high severity, see the inline comments:
- Medium severity. The auto-continue wake's channelId can win over a newer message's channel in a batched turn.
- Medium severity, test coverage. The stop-instruction choice for agents without skip_reply is untested at the framework level.
- Medium severity, test coverage. Several exclusions and the stop_sequence case have no tests.
- Medium severity. The [continue] notice is stored before the wake is known to run, so a dropped wake leaves a false notice behind.
4 more findings, less severe
- Low severity, documentation,
src/agent.ts:155-159. resolveAutoContinue docstring saystrueis refused. The comment reads 'Anything else is refused:true, or an object whosemaxConsecutiveis left out or a whole number from 1 to 20.' That sentence lists the accepted values as if they were refused.trueis accepted and maps to the default cap. Reword it to say what is accepted and what is refused. - Low severity, documentation,
docs/auto-continue.md:30-32. Docs say the notice follows the [delivered] receipt, but explicit mode writes no receipt. The complete path writes the receipt only whenturnProseRouting !== 'explicit'(framework.ts line 19873), because explicit envelopes acknowledge themselves. For explicit agents there is therefore no[delivered]row for the[continue]row to follow. The same claim appears in the AgentConfig.autoContinue JSDoc (src/types/agent.ts) and in the changelog fragment. Say that the receipt exists only outside explicit mode. - Low severity, documentation,
src/agent.ts:161-164. The refusal message for a non-object autoContinue does not mention thattrueis valid. ForautoContinue: "yes"or1, the error comes from holdsSettings: 'Agent "x" autoContinue must be an object, such as { maxConsecutive: 3 }, not a string'. The setting also acceptstrue(and false or null), and that is the most common form in the docs. An operator who wrote"true"as a string is told to write an object. Check for that case and say 'true, false, or an object'. - Low severity, test coverage,
src/framework.ts:16636-16641. The notice-storage failure path (no continuation, count rolled back) is untested. If addMessage throws, the code logs the error, sets the streak back to count - 1 and skips the wake. No test makes the context manager throw to check that no continuation is queued and that the count is restored. A regression here could either loop without notices or lose the cap accounting.
…channel outranked, guards tested - The [continue] notice is written by the scheduler as the turn that takes the auto-continue wake starts (autoContinuePending → writeAutoContinueNotice), and only when the continuation is that turn's whole cause, so it never claims a turn that doesn't run. A wake the inference policy drops writes no notice and gives back its count (dropAutoContinue); a notice that can't be stored starts no continuation and gives back its count. - The continuation's channel is the batch's fallback: any other channel-bearing wake (ambient included) outranks it, though queued first. - resolveAutoContinue: docstring says what it accepts; a string or number is told it can be true, false or an object (strings: unquoted). - Docs/JSDoc/changelog: the receipt precedes the notice only outside explicit mode; a batched message takes the turn with no notice. - Tests (26): batched message (channel, no notice, fresh run), policy drop, notice store failure, stop instruction for explicit/hybrid/locus agents without skip_reply, stop_sequence continues, no_progress/round_limit don't, ephemeral/stopping/unregistered guards, the config message. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Review addressed in 900fc6d — the four medium findings are answered in their threads; the low ones:
|
There was a problem hiding this comment.
Automated review
Recommendation: merge and fast-follow. 1 medium severity finding can be fixed after the merge.
Reviewed commit 900fc6d with claude-opus-5-5, by reading the code. It did not run the code or look at the checks.
Does this change belong in the project?
It fits the project. This fits. It adds an opt-in, off-by-default per-agent turn-continuation setting, inside the scheduler that already owns wakes, ! continuations and bounce caps.
5 reasons with sources
It reuses those mechanisms rather than adding a second way to do the same thing.
- It extends the framework's own job of driving agent turns and inference triggers, and adds no new subsystem. The README describes the framework as running the whole loop, from event processing to inference trigger to speech delivery. Auto-continue is one more framework-sourced wake, like heartbeat, prose-bounce and prose-continuation. (README.md (Overview))
- It builds on existing mechanisms instead of duplicating them. The streak cap resets on the same non-framework-source rule as proseBounceStreaks. An explicit
!continuation takes precedence over it. The wake carries the locus the way other framework wakes name a channel. The pure helpers live in a small new module, src/auto-continue.ts, and config resolution sits beside the other agent switch checks in assertAgentSwitches. (src/framework.ts (proseBounceStreaks, requestProseContinuation, FRAMEWORK_WAKE_SOURCES)) - The cost to maintainers is small and opt-in: one optional AgentConfig key and one exported type, with no new dependency or service. The setting is off by default and checked when the agent is built. The companion connectome-host PR only passes the key through, and the body says it is safe to merge in either order, as CONTRIBUTING.md asks. (src/types/agent.ts)
- The PR has the shape CONTRIBUTING.md asks for. It has Problem/Changes/Tests sections, numeric test baselines against main, a Not verified section, a changelog fragment in changelog.d/, a docs page, and tests the author checked against six mutations. (CONTRIBUTING.md (What a PR should contain; Changelog))
- One part departs from a stated project rule: the maxConsecutive refusal echoes the configured value, which config-checks forbids. config-checks says each refusal names the setting and the value's kind, never the value, because it could be a secret. resolveAutoContinue's maxConsecutive message uses JSON.stringify(max). This is a defect the code review already covers, not a reason to doubt that the feature belongs. (src/config-checks.ts (header comment))
Findings
1 finding of medium or high severity, see the inline comment:
- Medium severity. maxConsecutive refusal echoes the configured value, against the config-checks rule.
3 more findings, less severe
- Low severity, maintainability,
src/framework.ts:18924-18927. Unconditional autoContinueStreaks.delete breaks the prototype-harness convention the change follows elsewhere. The turn_ended path now callsthis.autoContinueStreaks.delete(agent.name)for every agent, autoContinue or not, without optional chaining. The same change usesthis.autoContinueStreaks?.deletein processInferenceRequests, under the existing comment that "prototype-built harnesses leave the map unset". Many tests build the framework with Object.create(AgentFramework.prototype). Any harness that reaches a tool-ended turn will now throw a TypeError in this handler, which runs before requestProseContinuation. Use?.here too, for consistency. - Low severity, documentation,
src/auto-continue.ts:8-27. AUTO_CONTINUE_STOP_REASONS doc comment is attached to the wrong constant. The JSDoc that explains which stop reasons continue ("Stop reasons of a turn the model ended itself…") sits directly above another JSDoc and AUTO_CONTINUE_REASON. AUTO_CONTINUE_STOP_REASONS itself is declared further down, after PendingAutoContinue, with no comment. Editors and generated docs therefore show the stop-reason explanation on nothing, and the set has no documentation. Move the block down so it sits above AUTO_CONTINUE_STOP_REASONS. - Low severity, test coverage,
test/auto-continue.test.ts:336-360. Batched-message test accepts either trigger reason, so it doesn't pin which request owns the turn. The test 'a message that arrives during the turn takes the next turn' assertstriggers[1].reason === 'channel-message' || triggers[1].reason === 'auto-continue'. That holds no matter which request becomes the trigger. The behavior the docs promise (the turn belongs to the message) is only checked indirectly, through channelId and the notice count. The silent-heartbeat test also injects its wake withsource: 'framework', but real silent heartbeats use the MCPL serverId as source (framework.ts handlePushEvent), so the batching and reset behavior of a real heartbeat next to a continuation is never exercised.
… harness-safe reset; doc placement
- resolveAutoContinue's maxConsecutive refusal names the value's kind (a
string, a number below 1, above 20, not whole), never the value: a ${VAR}
landing there could be a credential (config-checks rule). Tested.
- turn_ended's run reset uses optional chaining, like the scheduler's, for
prototype-built harnesses.
- AUTO_CONTINUE_STOP_REASONS carries its own doc comment.
- Tests: the batched-message turn's trigger is pinned to the message; the
silent tick arrives from a server, as real ticks do.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Re-review of 900fc6d addressed in 89a8413:
auto-continue 27/0; skip-reply-wake-capability 18/0; explicit-prose-routing 107/0. |
There was a problem hiding this comment.
Automated review
Recommendation: merge and fast-follow. 1 medium severity finding can be fixed after the merge.
Reviewed commit 89a8413 with claude-opus-5-5, by reading the code. It did not run the code or look at the checks.
Does this change belong in the project?
It fits the project. This fits. It adds an off-by-default per-agent setting that extends the framework's existing turn scheduling and ! continuation to locus mode, and it puts the code where similar settings and wake caps already live.
5 reasons with sources
It follows the project's PR conventions.
- Inference triggering and turn lifecycle are the framework's core job, and this changes when the next turn is triggered. The README says the framework handles the full lifecycle: event processing, inference triggers, tool dispatch and persistence. Starting another turn after one the model ends itself is a trigger policy inside that lifecycle. It is not a host or module concern. (README.md (Overview))
- It follows the existing pattern for agent settings: optional, off by default, and checked when the agent is built.
toolWrapperProseGuard,clockand others are optional AgentConfig fields, resolved in the Agent constructor and refused by assertAgentSwitches. The newautoContinueis resolved the same way, using the existing config-checks helpers (holdsSettings,kindOf). (src/agent.ts (assertAgentSwitches) and src/types/agent.ts (AgentConfig)) - It reuses the framework's existing continuation and wake machinery rather than adding a second mechanism. The continuation is queued as a
framework-source wake inpendingRequests. Its cap is modeled on the bounce-wake cap (proseBounceStreaks) and resets the same way. It also defers to the existing!continuation instead of competing with it. The review found that a batched silent heartbeat can reset the cap, so the part that resets the count only on a cause of its own does not yet work as described; that is a defect for the code review, not a question of fit. (src/framework.ts (proseBounceStreaks, requestProseContinuation, FRAMEWORK_WAKE_SOURCES)) - The only new public surface is one config key and one exported type, and the feature is off unless a host opts in. It adds
AutoContinueConfigandAgentConfig.autoContinue, with no new dependency or service. The pure logic lives in a smallsrc/auto-continue.tsmodule, and the feature is documented indocs/auto-continue.md, next to other per-feature docs such as docs/tool-result-guard.md. (CONTRIBUTING.md (How changes land)) - The PR follows the contributing guide: a declared companion PR, numeric test baseline, a Not verified section and a changelog fragment. It names connectome-host#433 and says the two can merge in either order. It reports pass/fail counts against a
mainbaseline, lists what it did not verify (no live provider, forks), and addschangelog.d/feat-continue-after-prose.added.md. (CONTRIBUTING.md (What a PR should contain; Changelog))
Findings
1 finding of medium or high severity, see the inline comment:
3 more findings, less severe
- Low severity, test coverage,
test/auto-continue.test.ts:535-551. Refusing a bad autoContinue is only tested on resolveAutoContinue, not where agents are built. The config tests callresolveAutoContinuedirectly. The PR's claim is thatassertAgentSwitchesrefuses a malformed setting before anything opens, for bothAgentFramework.create()andcreateEphemeralAgent(which reserves the agent's name). Neither path is tested. test/config-switches.test.ts already checks those entry points for the other switches, such as toolWrapperProseGuard at lines 109-122, and was not extended for autoContinue. If theresolveAutoContinue(config)call were dropped fromassertAgentSwitches, no test would fail. - Low severity, test coverage,
test/auto-continue.test.ts:498-512. The ' !' test's "no count" assertion cannot fail. The test assertsautoContinueStreaks.has('assistant') === falseafter the second turn. That turn ends on>>skip_reply done, and autoContinueAfterTurn's skip branch always deletes the streak. So the assertion holds even if the!turn had incremented the count. To check that a!continuation counts toward nothing, it needs to read the streak after the first turn, or end the second turn in a way that doesn't clear it. - Low severity, test coverage,
test/auto-continue.test.ts:281-296. No test covers a silent tick or another framework wake batched with a continuation. The heartbeat test only covers a silent tick that is a turn's whole cause. Two batching cases have no test. First, a silent tick batched with anauto-continuewake, which resets the streak and de-silences the turn. Second, a continuation batched with another framework wake (for exampleprose-bounce), which writes the notice and keeps the count. These are the batching rules in processInferenceRequests (lines 14796-14829) that the cap depends on.
…usal tested where agents are built - hasOwnCauseForRun(): a silent heartbeat tick isn't a cause of its own for an autoContinue run. Batched with a continuation it no longer resets the count (a resident whose ticks come faster than its turns would never reach the cap) and no longer suppresses the continuation's notice. The bounce streak's reset is unchanged. - Tests: a silent tick, and another framework wake, batched with a continuation keep the cap and the notice; create() and createEphemeralAgent refuse a bad autoContinue (the latter before registering the name); the ' !' test reads the count as the continuation's turn starts. - Docs/JSDoc: silent ticks don't restart a run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Review of 89a8413 addressed in a676cf8:
auto-continue 29/0, config-switches 8/0, explicit-prose-routing 107/0, skip-reply-wake-capability 18/0, present-while-acting 83/0, mcpl-coalescing 188/0. |
|
@antra-tess Our review of this PR at Off changes nothing. We scripted 14 cases through the built framework with the test suite's mock membrane, across locus, explicit and hybrid mode: prose and empty ends, What we'd change, with it on (runs on Linux, Node 24.22.1, context-manager 0.20.0, membrane 0.7.1):
Smaller points:
A question, by reading only: a host quiesce parks an The two-resident run (point 5)Two residents in one framework, only Written by Hamish-2699 (Claude Opus 5.5). — Fiona-2757 |
…med and neutral; stop must be offered; quiesce drops - The [continue] row is written in startAgentStream, past every gate that can still stop the turn (stopping framework, provider park/requeue, stated wait), as the newest of its start inputs; the trigger then goes on unmarked. The second per-agent map is gone: the wake's mark (InferenceRequest.autoContinueRun: stop, heldAfterSend; sole set by the scheduler) carries what the row says, the count is the run's. - Wording (antra's pick of nissa's): '[continue] <agent>, another turn starts now. If you're done, call skip_reply; anything you write without it is sent as usual.' The cap is mentioned only on the last continuation. The row names the agent: residents share one message slot. - A turn that sent and then closed in prose still continues (antra: held words can't be told from unfinished work), and its row says those words were held, not sent. - Continues only an agent offered a way to stop: skip_reply offered by the channel registry and allowed, or a skip envelope in explicit/hybrid mode. - A quiesce drops a queued continuation and gives back its count (antra). - No continuation on top of a queued bounce wake; none for conversation forks; a turn with no channel passes its unresolved channel on, so the continuation takes no fallback either. - Docs: cost per answer, hybrid pins vs locus, JSON examples without comments, the above. agent.ts: spacing. - Tests (36): each of the above, plus a continuation batched first with a message; every new guard mutation-checked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks, nissa-seru, a careful review. Addressed in be1ecee, with antra's calls on the three that touched the spec:
Smaller points:
Also from the earlier rounds still in place: a silent tick batched with a continuation neither restarts the run nor takes its row.
|
|
@antra-tess One follow-up on What we ran, on The resident from point 1 of our review, run with #716 merged in, shows the same in explicit mode: 5 turns and four posts of "Done.", against 2 turns and one post with the setting off. Written by Hamish-2699 (Claude Opus 5.5). — Fiona-2757 |
There was a problem hiding this comment.
Automated review
Recommendation: merge and fast-follow. 4 medium severity findings can be fixed after the merge.
Reviewed commit be1ecee with claude-opus-5-5, by reading the code. It did not run the code or look at the checks.
Does this change belong in the project?
It fits the project. This fits. It adds a default-off, per-agent turn-scheduling setting to the framework that owns inference triggering, and it follows the pattern of the project's other opt-in agent switches, with their docs, validation and changelog fragment.
5 reasons with sources
- Deciding when an agent's next inference runs is a core job of the framework the README describes. The README says the framework handles the full lifecycle of event processing, inference triggering and tool dispatch for agents. autoContinue adds a new framework-sourced wake reason to that same scheduler (processInferenceRequests), next to the existing prose-bounce and
!prose continuations. (README.md (Overview)) - It follows the existing pattern for opt-in agent settings: an AgentConfig field, construction-time validation and its own doc page. resolveAutoContinue is called from assertAgentSwitches, alongside toolWrapperProseGuard and toolResultGuard. Like those, it names the kind of a bad value and never the value itself. docs/auto-continue.md sits next to docs/tool-result-guard.md and docs/tool-wrapper-prose-guard.md. (src/agent.ts (assertAgentSwitches); docs/)
- It extends the prose-continuation idea the project already has instead of adding a parallel way to do it. The explicit and hybrid
!continuation still runs first and is preferred. Auto-continue reuses the existing wake sources, the turn-routing state and the bounce-streak style of cap. The pure logic lives in a small new module, src/auto-continue.ts. (src/framework.ts (requestProseContinuation, FRAMEWORK_WAKE_SOURCES)) - It meets the contribution conventions: a changelog fragment, a declared companion PR with merge-order guidance, and tests that the author mutated to show they fail on unfixed code. The PR adds changelog.d/feat-continue-after-prose.added.md. It declares connectome-host#433 and says the two can merge in either order. It reports the suite as numeric pass/fail counts against main. (CONTRIBUTING.md (What a PR should contain; How changes land))
- The new public surface is small and opt-in: one config key and two exported types. The additions are AgentConfig.autoContinue, AutoContinueConfig, AutoContinueRun and InferenceRequest.autoContinueRun, which is marked as set by the framework only. The setting is off by default, so existing agents are unaffected. The code review's findings, such as the shared resident slot and the store-failure edge, are implementation defects, not a departure from any stated design. (src/types/agent.ts)
Findings
4 findings of medium or high severity, see the inline comments:
- Medium severity. [continue] row is written into a shared resident slot while another resident's turn may be alive.
- Medium severity. A notice write that throws after its row landed still withdraws the turn, leaving a false [continue] row.
- Medium severity, test coverage. Quiesce branch that strips the mark from a requeued trigger is untested.
- Medium severity, test coverage. Store failure for a non-sole continuation (turn proceeds without row) is untested.
1 more finding, less severe
- Low severity, maintainability,
src/framework.ts:14820-14825. Continuation batched with a turn-resuming request keeps its count but writes no row. continuationMark is computed for any batch with no own cause, including one that also holds a context_budget_restart or tool_results_ready request. startAgentStream then skips the row because resumesTurn is set, so neither writeAutoContinueNotice nor giveBackAutoContinue runs. The run's count stays raised for a continuation that never told the agent anything. This is rare, but the count no longer matches the rows the agent read.
…red slot waits; a landed row counts - A turn whose closing prose bounced (explicit or hybrid) isn't continued: the bounce and its cap answer it, and continuing once the cap is spent restarted the loop the cap breaks (nissa-seru's run: 6 turns where off takes 3). TurnRouting.lastEnvelopeBounced, set by bounceProse, reset per envelope; replaces the queued-bounce check. - The row isn't written into the residents' shared slot while another reader's turn is alive: the scheduler requeues the continuation until it ends (as a clock row waits); a race at the write point counts as not stored. - A row whose write threw after it landed counts as written (read back with storedSince, as the clock row does), so no turn is withdrawn under a row. - A continuation a resumed turn takes in gives back its count. - Tests (40): bounce loop past the cap, shared-slot wait, landed-but-thrown, non-sole store failure (turn runs, count back), quiesce strip-and-park of a requeued marked trigger; each mutation-checked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@nissa-seru agreed: a turn whose closing prose bounced is now left to the bounce. |
|
Review of be1ecee addressed in e05a311 (the four mediums are answered in their threads). The low one, a continuation batched with a turn-resuming request keeps its count: the scheduler now gives back the count and drops the mark when the batch holds a
|
main brings anima-research#710 (two servers' channels with one id told apart), anima-research#727 (a batched wake writes its line only when it says something), anima-research#743, anima-research#744 and the 0.26.0 release. The one conflict is test/event-gate.test.ts, where anima-research#727's new describe block sits where this branch's resolver block does; both are kept, in that order. src/gate/event-gate.ts and src/gate/types.ts merge without conflict, and the merged code carries this branch's change as it was: the resolver anima-research#710 leaves in place still returns the registered channel id (derivePushEventChannel's channelId), which compiledMatches and resolveKey test and key on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in anima-research#743 (CI), anima-research#744 (autoContinue), anima-research#755 (script references) and anima-research#759 (a batched wake's line placed at its turn's start). One conflict, in runInferencePass's inference-policy branch: anima-research#744 gives back a dropped autoContinue wake's count there, and this branch ends a provider hold's pending primary only when nothing of the agent's is queued; both stay. Both anima-research#744 and anima-research#759 leave their start inputs (a `[continue]` row, a wake's line) to a start that doesn't resume its logical turn; with a batch's continuation now starting alone, a wake carrying either starts a turn of its own and writes them there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…qualified channels meet the registry, tune-out and the reply anchor What main brings since e4d1e46, the sixth catch-up's base: 0.25.0 and 0.26.0, and anima-research#709, anima-research#710, anima-research#719, anima-research#722, anima-research#724, anima-research#728, anima-research#727, anima-research#743, anima-research#744, anima-research#755, anima-research#759, anima-research#761, anima-research#763, anima-research#767, anima-research#707, anima-research#740, anima-research#760, anima-research#726, anima-research#371, anima-research#452, anima-research#768, anima-research#465, anima-research#770, anima-research#771, anima-research#723, anima-research#777, anima-research#776, anima-research#773, anima-research#775, anima-research#778, anima-research#774, anima-research#779 and anima-research#781. Most of the conflicts are where anima-research#710 met the branch: in main a channel the framework acts on is a ChannelRef, { serverId, channelId }, since two MCPL servers can register one id. Conflicts in 14 files (61 hunks), resolved by keeping both sides' intent; where the two had different designs for one thing, the branch's stays, moved onto main's types: - src/framework.ts: main's conversationAgentHomes map stays gone. A fork's home is the registry record's homeChannel, which homeOf reads wherever main read the map (isForkBound, mayTakeGateRoute, canCloseChannels, holdsChannelTraffic, agentForPrimary, the drafts resend, the channel tools' guard, the registry's homeChannelResolver, stop()'s unbinding, releaseAgentRuntime's). The record still keeps a bare id, read as main reads a wake that names its channel without its server (channelNamed), or, where it names no one registered channel, as a home on no server ({ serverId: '' }), which a path reading the pair finds unregistered and one reading the name finds no channel or two for. main's dropContinuations goes into releaseAgentRuntime, so every way a live non-ephemeral agent ends drops a continuation of its turn, and beside the registry's end of an ephemeral run. Arrival carries main's channel ref and the branch's addressed; the addressed-elsewhere notice names channels with main's channelShown and keys its once-per-channel set by destinationKey. isAddressedMessage stays tag-only, with main's ambient marks. publishInputWithTurnLocus gives the locus with its server, and only to a call made inside the turn (callingTurnOf); outside it, a conversation fork's call takes its home, as its dispatched calls do, and anyone else's the registry's default. The tune_out handler keeps the branch's shape (serverId, scope, check, peek) and resolves through ChannelRegistry.resolveChannel, now resolveToolChannelEntry with the calling turn's own channels, check's peek included; its answers name the channel with nameShown. holdForProviderWait holds nothing for an agent a listener of the failure it reports ended (stopAgent), where main's report-before-hold (anima-research#774) meets the branch's stopAgent at a conversation agent's hold after its compile. handleSubconsciousTool gets main's own channels, then the branch's caller. - src/tune-out/coordinator.ts: main's shown() names beside the branch's reader helpers; deliver_summary, cancel_tuneout and speak_in_channel resolve as main's do, for the subconscious and a reader fork alike; cancel_tuneout keeps the branch's serverId and all-except fallback, and a name that picks no channel (one two servers share, or a turn's own channel ownChannelFor refuses) doesn't keep a call naming the scope from ending it; findByChannel goes, as on main. focusChannel() returns a ref. - src/mcpl/channel-registry.ts: markAccepted records no anchor (admission does), and handleIncoming's arrival count of removals, which main's guard there read, goes with it; noteDelivered takes the channel with its server and sets nothing for a channel its server no longer registers, read at admission where main's guard compared removal counts at arrival and acceptance (a message admitted after its server registers the channel again sets the anchor here). buildChannelContext is main's, per server, with the branch's sharedFallback; incomingFor reads the anchor itself, not the speech fallback. globalFallbackFor, sharedFallback, fallbackLocus and focusChannelResolver are on refs. - src/index.ts: main's DefaultErrorPolicy beside the branch's exports. - Tests: the branch's registry stubs take main's shapes; main's appendInTwoParts in batched-wake-locus's rig. Beyond the conflicts: the all-except announcement goes to the excepted channel on its server (publishForAgent refuses a shared bare id since anima-research#710), and tune_out's all-except answer names its channel with nameShown; the tune_out schema's serverId description says channelId takes serverId:channelId too; main's refusals for a tuned-out channel no name picks alone give the tune_out cancel by id and server, since this branch's tune_out takes a serverId; a stale comment naming conversationAgentHomes in channel-registry.ts names the registry; docs/dendrite.md says where a publish naming no channel goes outside the turn, and no longer lists ChannelRef as unbuilt. Test adaptations: main's tests that set conversationAgentHomes give the fork its home through the registry. main's tests written to the latest-inbound rule run under it (reply-server-routes: speech LEGACY; shared-channel-id's registry fixture: latestInboundLocus, and noteDelivered as admission records). Two reply-server-routes tests of an in-turn publish naming no channel now check the branch's rule, the turn's locus through its server. tune_out's shared-channel-id tests wait for the handler's answer, and its shared-id refusal offers serverId. all-except- unit's stub registry has nameShown; present-while-acting and deferred-window-only build routing state in main's shapes. main's marker and removed-server anchor tests record the anchor at admission (noteDelivered), where this branch records it. At this commit alone, the tests where a fork's home has to name its server fail (two servers' channels with one id, or a home the registry doesn't register at that moment); the pull request lists them. The next commit stores the home with its server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem
A resident whose turn ends in plain prose (or ends with nothing) pauses until the next event — a message, a heartbeat, a timer — even when it was mid-task ("let me check that…" then end_turn). Explicit and hybrid modes have a per-message
!continuation, but locus mode (most of the fleet) has no way to say "keep going", and nothing lets a host make "keep going until the agent says it's done" the default.Changes
New per-agent setting, off by default:
AgentConfig.autoContinue: true | { maxConsecutive?: 1–20 }(default cap 3). With it on:end_turnorstop_sequence), with prose or with no text, is followed by another turn straight away (wake reasonauto-continue, sourceframework).skip_replytool or any other endTurn tool (sleep…), which never reach thecompletepath; a last>>skip_replyenvelope in explicit mode (newTurnRouting.lastEnvelopeSkipped); or a>>>skip_replyhold still standing in hybrid mode (hybridHold === 'private').startAgentStream, past every gate that can still stop it), a short system row joins the window as the newest start input (append-only, KV-stable), after the last turn's[delivered]receipt outside explicit mode:[continue] <agent>, another turn starts now. If you're done, call skip_reply; anything you write without it is sent as usual.It names the agent (residents share one message slot). The last continuation of a run adds
This is the last continuation in a row: after this turn you wait for the next event either way.If the last turn's closing words were held because it had already sent, it addsYour closing words last turn were held, not sent: that turn had already sent with a send tool.Explicit/hybrid agents withoutskip_replyare told their skip envelope. The row is written only when the continuation is the turn's whole cause, never for a wake that doesn't run (policy drop, quiesce: count given back; a row that can't be stored withdraws a turn the continuation alone caused). The wake carries what the row says (InferenceRequest.autoContinueRun).channelId+channelServerId), so locus/hybrid prose keeps landing where the last turn's did instead of the global last-inbound channel. It is the batch's fallback: a message that arrived meanwhile (ambient included) names the turn's channel, takes the turn without a[continue]row, and restarts the run.maxConsecutivecontinuations in a row. The count resets when a turn starts with a cause of its own — from outside the framework and not a silent heartbeat tick (hasOwnCauseForRun) — and when a tool ends a turn.max_tokenscut-offs, the stall guard'sno_progress/round_limit, silent heartbeat ticks, a turn whose prose bounced (the bounce wakes it), ephemeral runs, conversation forks, a stopping framework, an agent offered no way to stop (locus/disabled without an offeredskip_reply); a quiesce drops a queued continuation. A!continuation the turn asked for runs instead (no notice, counts toward nothing).resolveAutoContinuerefuses a malformed setting at construction (assertAgentSwitches, socreate()andcreateEphemeralAgentrefuse it before anything opens), naming the value's kind, never the value. New exported typesAutoContinueConfig,AutoContinueRun. Docs:docs/auto-continue.md. Changelog:changelog.d/feat-continue-after-prose.added.md.Companion PR: anima-research/connectome-host — recipe key
agent.autoContinuepassed through toAgentConfig.autoContinue(anima-research/connectome-host#433). Safe to merge in either order: the host only passes the key through, and an agent-framework without this PR ignores it.Tests
test/auto-continue.test.ts, 36 tests (15 at opening; review rounds 900fc6d, 89a8413, a676cf8 and nissa-seru's (be1ecee) added batching with a message / a silent tick / a framework wake, policy drop, notice store failure, no-skip_reply stop instructions per mode, stop_sequence / no_progress / round_limit, the ephemeral/stopping/unregistered guards, refusal messages), plus create()/createEphemeralAgent refusals intest/config-switches.test.ts. Opening set: prose end continues after the receipt in the same locus (server + id) until skip_reply; empty end continues; cap + last-continuation wording; a message after the cap starts a fresh run; off by default;max_tokensandrefusaldon't continue; silent heartbeat doesn't; a tool-ended turn clears the run; explicit>>skip_replyending waits while a delivered ending continues; hybrid>>>skip_replywaits (and keeps the hybrid locus); a!continuation runs instead; config validation; notice text.main8843507 (after fix(gate): a batched wake writes its line only when it says something #727, feat(retry): the framework is the one retry layer for its turns #728): auto-continue 15/0, default-error-policy 11/0, explicit-prose-routing 107/0, present-while-acting 83/0, skip-reply-wake-capability 18/0.npm test(built, Node 22.23.4, macOS, the branch on 9fdc3a6 run concurrently with amain9fdc3a6 baseline on the same loaded machine): branch 6426 pass / 9 fail / 11 cancelled;main6513 pass / 8 fail / 10 cancelled. The failure sets differ (timing tests under load). Every file holding a branch-only failure — discord-awareness-delivery, discord-awareness-outbox, mcpl-coalescing, pack-installs-alone, route-failure-targeting, stop-ends-awareness-drains, stop-with-host-operations, stop-without-stream, surgery-marks-receipt, code-execution-isolate-scripts, turn-id, maintenance-timer, host-quiesce — passes in full when run alone, on both the branch andmain.Not verified
[continue]row (and whether it reaches for skip_reply rather than riding the cap) is the thing to watch on a first canary — preferably not a classifier-sensitive (Fable-family) resident first.autoContinuecontinues too (connectome-host doesn't expose forks).changelog.d/feat-continue-after-prose.added.md.🤖 Generated with Claude Code