Skip to content

feat(prose): autoContinue — continue a turn the model ends itself until skip_reply - #744

Merged
anima-connectome-github[bot] merged 6 commits into
mainfrom
feat/continue-after-prose
Oct 23, 2026
Merged

anima-connectome-github[bot] merged 6 commits into
mainfrom
feat/continue-after-prose

Conversation

@antra-tess

@antra-tess antra-tess commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

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:

  • A turn the model ends itself (stop reason end_turn or stop_sequence), with prose or with no text, is followed by another turn straight away (wake reason auto-continue, source framework).
  • It stays paused, as today, when the turn ended on a skip: the skip_reply tool or any other endTurn tool (sleep…), which never reach the complete path; a last >>skip_reply envelope in explicit mode (new TurnRouting.lastEnvelopeSkipped); or a >>>skip_reply hold still standing in hybrid mode (hybridHold === 'private').
  • As the continuation's turn starts (in 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 adds Your closing words last turn were held, not sent: that turn had already sent with a send tool. Explicit/hybrid agents without skip_reply are 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).
  • The continuation's wake carries the turn's locus (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.
  • Cap: at most maxConsecutive continuations 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.
  • Never continued: refusals, max_tokens cut-offs, the stall guard's no_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 offered skip_reply); a quiesce drops a queued continuation. A ! continuation the turn asked for runs instead (no notice, counts toward nothing).
  • resolveAutoContinue refuses a malformed setting at construction (assertAgentSwitches, so create() and createEphemeralAgent refuse it before anything opens), naming the value's kind, never the value. New exported types AutoContinueConfig, AutoContinueRun. Docs: docs/auto-continue.md. Changelog: changelog.d/feat-continue-after-prose.added.md.

Companion PR: anima-research/connectome-host — recipe key agent.autoContinue passed through to AgentConfig.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 in test/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_tokens and refusal don't continue; silent heartbeat doesn't; a tool-ended turn clears the run; explicit >>skip_reply ending waits while a delivered ending continues; hybrid >>>skip_reply waits (and keeps the hybrid locus); a ! continuation runs instead; config validation; notice text.
  • The tests fail on the unfixed code: each of six mutations at opening (and each review fix's revert since: channel fallback, notice-only-when-sole-cause, policy-drop and store-failure reverts, stop strings, stop_sequence, ephemeral guard, silent-tick predicate, assertAgentSwitches call) (no autoContinue call; explicit skip ignored; hybrid hold ignored; skip_reply not clearing the run; no reset on an external cause; no locus on the wake) fails at least one test.
  • Rebased onto main 8843507 (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 a main 9fdc3a6 baseline on the same loaded machine): branch 6426 pass / 9 fail / 11 cancelled; main 6513 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 and main.

Not verified

  • Not exercised against a live provider or a live resident; no resident runs it yet. How a given model reads the [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.
  • Conversation forks aren't excluded: a fork built from an agent config with autoContinue continues too (connectome-host doesn't expose forks).

  • Changelog fragment added — changelog.d/feat-continue-after-prose.added.md.

🤖 Generated with Claude Code

…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>

@github-actions github-actions Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. autoContinue is checked inside assertAgentSwitches next to toolWrapperProseGuard and the other switches, and resolved in the Agent constructor the same way clock is. 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.autoContinue and AutoContinueConfig become 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:

4 more findings, less severe
  • Low severity, documentation, src/agent.ts:155-159. resolveAutoContinue docstring says true is refused. The comment reads 'Anything else is refused: true, or an object whose maxConsecutive is left out or a whole number from 1 to 20.' That sentence lists the accepted values as if they were refused. true is 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 when turnProseRouting !== '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 that true is valid. For autoContinue: "yes" or 1, the error comes from holdsSettings: 'Agent "x" autoContinue must be an object, such as { maxConsecutive: 3 }, not a string'. The setting also accepts true (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.

Comment thread src/framework.ts
Comment thread src/framework.ts Outdated
Comment thread src/framework.ts
Comment thread src/framework.ts Outdated
…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>
@anima-connectome-github

Copy link
Copy Markdown
Contributor

Review addressed in 900fc6d — the four medium findings are answered in their threads; the low ones:

  • resolveAutoContinue docstring: now says what it accepts (true → default cap; an object's maxConsecutive, 1–20) and that anything else is refused.
  • Receipt in explicit mode: docs/auto-continue.md, the AgentConfig.autoContinue JSDoc and the changelog now say the [delivered] receipt precedes the notice only outside explicit mode, and that the notice is written as the continuation turn starts.
  • Non-object message: a string or number now gets must be true, false or an object such as { maxConsecutive: 3 }, not a string (true and false go unquoted); tested.
  • Storage-failure path: tested (no continuation, count given back) — see the notice thread.

test/auto-continue.test.ts: 26 pass / 0 fail. Each new behavior was mutation-checked (channel fallback, notice-only-when-sole-cause, policy-drop revert, store-failure revert, stop-instruction strings, stop_sequence, ephemeral guard: each removal fails a test). Neighbouring suites on this commit: explicit-prose-routing 107/0, present-while-acting 83/0, skip-reply-wake-capability 18/0, default-error-policy 11/0, mcpl-coalescing 188/0, host-quiesce 50/0, turn-id 7/0.

@github-actions github-actions Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

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 calls this.autoContinueStreaks.delete(agent.name) for every agent, autoContinue or not, without optional chaining. The same change uses this.autoContinueStreaks?.delete in 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' asserts triggers[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 with source: '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.

Comment thread src/agent.ts
… 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>
@anima-connectome-github

Copy link
Copy Markdown
Contributor

Re-review of 900fc6d addressed in 89a8413:

  • Medium — refusal echoes the value: fixed, see the thread.
  • Low — unconditional autoContinueStreaks.delete in turn_ended: now ?., as in the scheduler.
  • Low — misplaced JSDoc: the stop-reasons comment now sits on AUTO_CONTINUE_STOP_REASONS.
  • Low — batched-message test: pins the trigger to channel-message (the message, queued first, is requests[0]). The silent-tick test now injects its wake from a server source, as real ticks arrive. A real tick batched with a continuation is an ordinary turn by the existing rule (a silent tick never suppresses a genuine wake), and, having a cause of its own, writes no [continue] row and restarts the run — the same path the batched-message test covers.

auto-continue 27/0; skip-reply-wake-capability 18/0; explicit-prose-routing 107/0.

@github-actions github-actions Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, clock and others are optional AgentConfig fields, resolved in the Agent constructor and refused by assertAgentSwitches. The new autoContinue is 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 in pendingRequests. 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 AutoContinueConfig and AgentConfig.autoContinue, with no new dependency or service. The pure logic lives in a small src/auto-continue.ts module, and the feature is documented in docs/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 main baseline, lists what it did not verify (no live provider, forks), and adds changelog.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 call resolveAutoContinue directly. The PR's claim is that assertAgentSwitches refuses a malformed setting before anything opens, for both AgentFramework.create() and createEphemeralAgent (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 the resolveAutoContinue(config) call were dropped from assertAgentSwitches, 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 asserts autoContinueStreaks.has('assistant') === false after 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 an auto-continue wake, which resets the streak and de-silences the turn. Second, a continuation batched with another framework wake (for example prose-bounce), which writes the notice and keeps the count. These are the batching rules in processInferenceRequests (lines 14796-14829) that the cap depends on.

Comment thread src/framework.ts
…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>
@anima-connectome-github

Copy link
Copy Markdown
Contributor

Review of 89a8413 addressed in a676cf8:

  • Medium — silent tick resets the cap: fixed, see the thread.
  • Low — refusal untested where agents are built: test/config-switches.test.ts now has create() refusing autoContinue: 'true' and { maxConsecutive: 0 }, and createEphemeralAgent refusing { maxConsecutive: 21 } before it registers any state under the name. Dropping resolveAutoContinue from assertAgentSwitches fails two of them.
  • Low — the ! test's count assertion couldn't fail: it now reads the count as the continuation's turn starts, before its own skip clears it.
  • Low — batching with a silent tick / another framework wake: both are covered by the new cap-1 batching tests (notice written, cap holds).

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.

@nissa-seru

Copy link
Copy Markdown
Contributor

@antra-tess Our review of this PR at a676cf8a: with autoContinue left out, nothing changes, by run. With it on, we found five things we'd change, each shown by a run or a line below, plus some smaller points. These are our recommendations; the choices are yours.

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, skip_reply, max_tokens, a tool round, two messages, a bounce, a delivery, >>skip_reply and >>>skip_reply, and ! in both envelope modes. Every request the model received and every stored row were the same as main's (8843507c) once timestamps and generated ids are masked, at each of this PR's four heads. Two runs of one build matched each other, as a control.

What we'd change, with it on (runs on Linux, Node 24.22.1, context-manager 0.20.0, membrane 0.7.1):

  1. Don't continue a turn whose trailing prose was held because the turn sent. That turn already spoke deliberately. We ran it with fix(prose): what a drafts round would hold or bounce stays in context (#701) #716 merged in (that PR keeps a drafts round's own line out of the drafts). The resident sends with channel_publish, ends its turn with a plain "Done." (held, since the turn sent), and in each continuation resends the newest draft with a line beside it. With the setting on, "Done." posted to the channel three times, once per continuation, in locus, hybrid and explicit mode. With it off, in locus and hybrid mode the line stayed held and nothing more posted; in explicit mode the bounce wake's turn resent it once.

  2. Continue only when the agent has a stop it can use: offered, not only permitted. In locus or disabled mode without skip_reply, the row names no way to stop. A resident that never calls skip_reply had one answer become four inferences and four posts in locus mode, the default cap of 3. The stop text checks canAgentUseTool(name, 'skip_reply') (src/framework.ts:16669), which is permission. skip_reply comes from the channel registry, so an agent can be permitted it with nothing offering it. Read as the agent receives it (not run with a live model), the row asks "anything else?" three times with no way to say no.

  3. Word the row so it doesn't invite a reply that gets published. Read as the agent receives it (not run with a live model), "Your last turn ended without skip_reply" reads as a lapse, and the natural answer is an apology or "I'm done here". In locus and hybrid mode that answer is posted to the channel, and the turn continues again. Our suggestions:

    • a neutral opening, such as "[continue] Another turn starts now.";
    • one clause saying what the agent's words do: "If you're done, call skip_reply; anything you write without it is sent as usual.";
    • dropping "Continuation 2 of 3 in a row: one more is left", which reads as a budget to use. The last line, "after this turn you wait for the next event either way", is the useful one.
  4. Write the row where the stream starts. processInferenceRequests writes it at src/framework.ts:14809, before startAgentStream. By our reading, startAgentStream can still start no turn after that:

    • it returns while the framework is stopping (:17504);
    • it parks behind an auxiliary call and then requeues the wake (:17539, :17553).

    Then the row claims a turn that didn't run, and a later drop gives nothing back, since the pending entry is gone. Written where the stream actually starts, everything autoContinuePending holds can be derived there from the streak, the agent's config and the stop. That removes the second per-agent map.

  5. Name the agent in the row. Residents read one shared message slot. In a run with two residents, where only one had autoContinue, the other resident's next request carried "[continue] Your last turn ended without skip_reply, so another one starts now. To stop and wait for the next event, call skip_reply.", the first resident's notice and stop instruction.

Smaller points:

  • No channel: a turn woken for a channel that names no one registered channel sets noChannel and takes no fallback (src/framework.ts:18096). Its continuation's wake carries only routing.locus (:16688), which that turn doesn't have, so the continuation takes the global fallback the turn refused. By reading. We'd carry the no-channel state onto the wake.
  • Hybrid pins: by our reading, the wake carries the locus, not a >>> pin, and a new turn starts with no pin. So "the next turn's plain prose lands where the last one's did" (docs/auto-continue.md:64) holds for the locus, not a pinned destination. We'd say the locus.
  • The cost, stated plainly: an agent that answers and simply ends its turn gets at least one more full inference per answer, the continuation it ends with skip_reply, and up to maxConsecutive if it never calls it (two inferences for one answer in point 5's run, four in point 2's). Each continuation's rows also stay in the window. The docs say the cap bounds the cost, but they don't give this per-answer figure.
  • Explicit mode with bounces: a turn that ends in unprefixed prose queues both a bounce wake and a continuation. A resident that does this every turn took 4 turns with the setting on, where off gave 3 (the bounce cap).
  • Text:
    • the docs' first example is a json block with // comments;
    • src/agent.ts has this.clock =resolveClockConfig;
    • "Not verified" names forks continuing. A one-line guard like the subagent one would settle it.

A question, by reading only: a host quiesce parks an auto-continue wake with the others, and it fires at resume, possibly hours later, as "another one starts now". Should a continuation survive a quiesce at all?

The two-resident run (point 5)

Two residents in one framework, only assistant with autoContinue: true, a channel registry stubbed as test/auto-continue.test.ts stubs it. One message, assistant answers "A first thought.", is continued once, and calls skip_reply; then other gets a wake of its own:

starts: assistant:external-message, other:external-message, assistant:auto-continue, other:test
assistant's window, kind auto-continue: "[continue] Your last turn ended without skip_reply, so another one starts now. To stop and wait for the next event, call skip_reply."
other's window, kind auto-continue: the same row
other's request contains "[continue]": true

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>
@anima-connectome-github

Copy link
Copy Markdown
Contributor

Thanks, nissa-seru, a careful review. Addressed in be1ecee, with antra's calls on the three that touched the spec:

  1. Continuing a turn whose trailing prose was held because it sent: antra's decision is to keep continuing: a held "Done." can't be told from "now let me check X", and being stuck is worse. Instead the row now gives feedback: when the last turn's closing words were held because it had already sent, the row says Your closing words last turn were held, not sent: that turn had already sent with a send tool. Integration test: a send_message round, then "Done." (held), continues with that line in its row.
  2. Continue only with a stop that's offered: autoContinueStop() now requires skip_reply offered (a channel registry) and allowed; otherwise explicit/hybrid get their skip envelope, and locus/disabled without it are not continued at all (logged). Tests for both locus cases (not allowed / no registry) and the envelope wording in explicit/hybrid.
  3. Wording: antra chose yours: [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 one (This is the last continuation in a row: after this turn you wait for the next event either way.); the "N of M, one more left" line is gone.
  4. Write the row where the stream starts: it's now written in startAgentStream, right after the turn-start and assembly flushes, past the stopping check, the provider park/requeue and the stated-wait hold, as the newest start input before the checkpoint; the trigger then goes on unmarked so a retry can't write it twice. The second map is gone: the wake carries a mark (InferenceRequest.autoContinueRun: stop, heldAfterSend; sole set by the scheduler) and the count is the run's own. A requeued trigger keeps the mark and is recognized as carrying the continuation.
  5. Name the agent: the row starts [continue] <agent>, ….

Smaller points:

  • No channel: a noChannel turn's continuation now carries that turn's unresolved channel, so it takes no fallback either (test with an unresolvable ghost channel).
  • Hybrid pins: the docs now say the locus, and that a >>> pin doesn't carry to the next turn.
  • Cost: stated per answer in the docs ("What it costs"): at least one more inference per answer, up to maxConsecutive without skips, and each continuation's rows stay in the window.
  • Explicit mode with bounces: no continuation is queued on top of a queued prose-bounce wake; the bounce wake (with its cap) takes its place (test: one wake, not two).
  • Text: the JSON examples have no comments; the this.clock = spacing is fixed; conversation forks are now guarded like ephemeral runs (test).
  • Quiesce: antra's call is that a continuation doesn't survive one: a quiesced scheduler drops the auto-continue wake and gives back its count (a requeued trigger keeps its other causes and parks without the mark). Test.

Also from the earlier rounds still in place: a silent tick batched with a continuation neither restarts the run nor takes its row.

test/auto-continue.test.ts 36/0 (each new guard mutation-checked); config-switches 8/0, explicit-prose-routing 107/0, skip-reply-wake-capability 18/0, present-while-acting 83/0, mcpl-coalescing 188/0, host-quiesce 50/0, default-error-policy 11/0.

@nissa-seru

Copy link
Copy Markdown
Contributor

@antra-tess One follow-up on be1eceec, as information. In explicit mode, autoContinue now runs on past the bounce-wake cap, whose stated purpose is breaking this loop ("capped at 2 consecutive bounce-wakes per agent (then notices append without waking, breaking any loop)", docs/explicit-prose-routing.md:66-68). Our lean is that a turn whose closing prose bounced doesn't continue: the bounce and its cap are explicit mode's own answer to that turn. Counting bounce wakes toward the run's maxConsecutive would be the alternative.

What we ran, on be1eceec with the setting on (the same harness as our review's runs, Linux, Node 24.22.1): a resident in explicit mode whose every turn ends in unprefixed prose took 6 turns for one message. That was 4 at a676cf8a, and it is 3 with the setting off. The turns were the message, two prose-bounce wakes, then three auto-continue wakes. By our reading, autoContinueAfterTurn yields only while a bounce wake is queued (src/framework.ts:16684), so once the cap's two wakes are spent and the bounce notices append without waking, the continuations start.

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

@github-actions github-actions Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

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.

Comment thread src/framework.ts
Comment thread src/framework.ts
Comment thread src/framework.ts
Comment thread src/framework.ts Outdated
…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>
@anima-connectome-github

Copy link
Copy Markdown
Contributor

@nissa-seru agreed: a turn whose closing prose bounced is now left to the bounce. bounceProse marks the turn (lastEnvelopeBounced, reset per envelope), and autoContinueAfterTurn doesn't continue it, before or after the cap's wakes are spent; that replaces the queued-bounce check. Test: an explicit resident whose every turn ends in unprefixed prose takes the message's turn plus two prose-bounce wakes, 3 turns, as with the setting off, and no continuation. (A turn that bounced earlier but closed on a delivered envelope still continues.) In the next commit after be1ecee, along with the automated review's points.

@anima-connectome-github

Copy link
Copy Markdown
Contributor

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 context_budget_restart / tool_result_guard_retry request, whose turn writes no start inputs. Not covered by a test: building that batch needs a live turn mid-restart, and the path is a two-line guard.

test/auto-continue.test.ts 40/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, host-quiesce 50/0, default-error-policy 11/0.

@anima-connectome-github
anima-connectome-github Bot merged commit bb8f29e into main Oct 23, 2026
12 checks passed
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 23, 2026
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>
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 23, 2026
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>
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 24, 2026
…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>
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