Repository navigation
fix(code-execution): separate script execution from bounded observation - #155
Meganeuridae wants to merge 3 commits into
Conversation
Reserve interpreter startup, isolate late replies, serialize wake delivery, retain execution results, and support ending an observation with completion wake armed. Co-Authored-By: Codex (GPT-6) <noreply@openai.com>
slimepriestess
left a comment
There was a problem hiding this comment.
Reviewed head 7130ac8, and as a trial merge onto main@67991cf (0.18.0). Second read by Opus 5.5 in a separate session; the ephemeral finding and the test-gap list are its catches, re-verified here.
Verdict: CHANGES REQUESTED. For primary agents the mechanism is right and the ordering in ScriptRun holds up. Three things stand in the way: ephemerals lose results and are told they won't, four of the changelog's fixes have no test that goes red on revert, and the branch doesn't typecheck against current main.
Blocking
-
Ephemerals are promised a wake they can't receive, and their script is killed when they believe it. An ephemeral's foreground call yields after 10s with the note "Still running. Completion will notify you"; the tool description says the same. When the ephemeral's run ends, the
finallyinrunEphemeralToCompletioncallsdisposeAgentScripts, which setscancelledand so suppresses the completion notice insettleCodeExecution. An ephemeral that ends its turn on that note never sees its output. Before this PR a 30s script in an ephemeral blocked and returned. The PR already knows ephemerals can't be woken, which is whyon_timeout=end_turnis refused for them (runCodeExecution); the default path breaks the same promise silently. No test touches ephemerals on the foreground path. Fix is either blocking semantics for ephemerals (observe until the script deadline) or an honest note ("you must keep waiting; this execution ends with your run") plus a test for whichever you pick. -
Four fixes in the changelog have no revert-goes-red test. Reverting any of these leaves the suite green:
- Inner-tool
endTurnscoped to its execution ("keep deferred end-turn effects scoped"). Background runners now pass noonEndTurn, so a background innerendTurnis dropped instead of landing on an unrelated foreground result. Only the pre-existing foreground case attest/code-execution.test.ts:508touchesendTurn. abortAgentScriptaborting only an observed run ("unrelated later stream failures leave yielded work alone"). Nothing exercises a stream error against an observed run, or against a yielded one. Item 4 below depends on this behavior, so it needs the test.- Stale
wake_ackafter interpreter replacement. The test at:299coverstool_result; thewakebranch has the same guard and no test. wakeAbortcutting a rate-floor wait short when the run ends.
- Inner-tool
-
Doesn't typecheck on main.
getHostModeStatus(src/framework.ts:2741on main, from #122) readsthis.backgroundScripts, which this PR renames. Git merges everything but the first import line cleanly, thentscfails with TS2551 on that property.[...this.codeExecutionScripts.values()].filter(r => r.status === 'running').lengthfixes it; see item 4 for what the field should mean now.
For antra to rule
-
Yielded foreground runs change a guarantee of #122's quiesce. Before: a foreground script held a turn token, so drain waited for it and abandon killed it. Now:
drained: truecan be reported while model-authored foreground work is mid-flight (up toscriptTimeoutMs, 600s by default), and abandon no longer stops it, becauseabortAgentScriptskips a run nobody is observing. Background daemons were already exempt by design; this extends the exemption to work that came out of a turn. Either abandon also aborts yielded foreground runs, or the quiesce doc says drained is not quiet. Whichever way, the status field should count both modes and probably stop being calledbackgroundScripts. -
The
code_executiondescription is rewritten, and the guidance flips.tool-definition.tsdrops the header comment antra wrote on 7/31 explaining that the tool's name, input shape and description deliberately track Anthropic's server-side programmatic tool calling so models' trained priors transfer. The new text replaces "call tools directly when a single call answers the question" with "choose direct calls or code as useful, including for a single call," drops the sanitized-name example, and drops the "arm a wake rule as backup" caution. It also never says that a detached foreground run still dies atscriptTimeoutMs, so an agent will start a 30-minute job in the foreground and get a DIED wake at ten minutes. The lifecycle sentences need adding, no question. The rest is a decision about how residents are steered, riding inside a lifecycle PR.
Notes
- A deferred inner-tool
endTurnfrom a yielded run is applied to whichever laterwaitretrieves the result: the agent wakes on completion, callswait, and the retrieval ends its turn. Defensible; say so in the doc, or clear the flag on yield. - Script wakes are now
system: true, soisConversationalInjectionreturns false for them: a wake no longer clears explicit-send suppression and never influences routing. Reads right for completion notices, less obviously right forwake_agent(payload), which is the script speaking to its owner. Intentional? ScriptRundrops the old belt-and-suspenders catch. With the newpy-runnerthe completion can't reject by construction (theensureChildchain settles its own failures), so this is defense in depth, not a bug. If it ever did reject, the record would sit atrunningwith the context reporting busy;cancelrecovers it.injectScriptWake's return value is ignored insidesettleCodeExecution, so a completion whose notice fails to land is lost to an agent that already ended its turn. The doc admits this under limits; fine for now, worth a line in the notice-failure log at least.
Verified
runtime-py.tson main already emitsexec_idontool_callandwake, so the new "reply must match the pending execution" filter is live-correct, not a silent drop.wait_ms: integer 0–60000, defaultcodeExecution.foregroundWaitMsthen 10_000;end_turnrefused for ephemerals and unregistered agents.- Busy foreground context returns the running id.
cancelof a yielded run callsPyRunner.abort(), which reclaims the interpreter (the runner object survives, the Python child and its globals don't); the next run gets a fresh child. The test at:727prints a literal, so it doesn't actually show freshness; asserting a prior global is gone would. - Startup reserve:
pendingis set before the first await; abort and dispose during startup settle; latetool_resultfrom a replaced interpreter is dropped (tests). - Wake serialization under
asyncio.gatherrespects floor and cap; delivery failure surfaces in Python asRuntimeError(tests). - Completion path:
addMessage(..., { forAgent })plus apendingRequestsentry with reasonscript:wake; parked during quiesce, admitted otherwise; not an EventGate event, so "bypasses ambient gating" is accurate. "Queued even if it arrives during turn teardown" is read, not tested: the release tests sleep 0.4s, so completion lands after teardown. ScriptRunordering: result set, waiters finished,onCompleterun synchronously in the same.then; the observer's continuation runs after, sorecord.statusis final when read. A timer firing before completion arms one notice; completion before the timer delivers to the observer; no path gives both.- Head:
script-run+code-executiontests 42/42. Trial merge with item 3 patched: 974 tests, 970 pass, 0 fail, 4 skipped (workspace-mount re-attach cases, platform-gated, unrelated). - Companion connectome-host#130: calls
releaseCodeExecutionWaitbehind a feature check, gates/release-waiton operator authority, and validatesforegroundWaitMsas an integer ≤ 60000. Not reviewed beyond that.
For the #architecture thread: the ten seconds isn't hardcoded. Default 10_000, recipe codeExecution.foregroundWaitMs (host main already forwards the block whole), per-call wait_ms up to 60_000.
|
| const endTurn = observed.endTurn || record.endTurn; | ||
| record.endTurn = false; |
There was a problem hiding this comment.
Late end-turn affects another turn When a foreground script outlasts its observation budget and later calls an inner tool such as
skip_reply, that tool sets record.endTurn after the original tool result has returned. A later action=wait, even after the script finishes, consumes the flag and ends that unrelated turn prematurely.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/framework.ts
Line: 8200-8201
Comment:
**Late end-turn affects another turn** When a foreground script outlasts its observation budget and later calls an inner tool such as `skip_reply`, that tool sets `record.endTurn` after the original tool result has returned. A later `action=wait`, even after the script finishes, consumes the flag and ends that unrelated turn prematurely.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| messageId, | ||
| source: `background-script:${record.id}`, | ||
| }); | ||
| return null; |
There was a problem hiding this comment.
Deferred wake acknowledged too early When
wake_agent() fires during an active turn, the envelope is deferred rather than stored, but this path immediately acknowledges delivery to Python. If the later flush fails, it discards the envelope while the queued wake can still start inference, so the agent never receives the payload Python was told it delivered.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/framework.ts
Line: 8488
Comment:
**Deferred wake acknowledged too early** When `wake_agent()` fires during an active turn, the envelope is deferred rather than stored, but this path immediately acknowledges delivery to Python. If the later flush fails, it discards the envelope while the queued wake can still start inference, so the agent never receives the payload Python was told it delivered.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| const waitMs = input.wait_ms ?? this.codeExecutionConfig?.foregroundWaitMs ?? 10_000; | ||
| if (typeof waitMs !== 'number' || !Number.isInteger(waitMs) || waitMs < 0 || waitMs > 60_000) { | ||
| return { success: false, error: 'wait_ms must be an integer from 0 to 60000', isError: true }; | ||
| } |
There was a problem hiding this comment.
Invalid default blocks script management If the host sets
foregroundWaitMs outside 0–60000, validation rejects every code_execution call without an explicit wait_ms. That includes action=list, action=cancel, and immediate background starts, none of which needs a foreground observation budget. An invalid default therefore prevents agents from managing existing scripts.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/framework.ts
Line: 8095-8098
Comment:
**Invalid default blocks script management** If the host sets `foregroundWaitMs` outside 0–60000, validation rejects every `code_execution` call without an explicit `wait_ms`. That includes `action=list`, `action=cancel`, and immediate background starts, none of which needs a foreground observation budget. An invalid default therefore prevents agents from managing existing scripts.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| `[background script ${record.id}] Your background script DIED ${elapsedMin}m after start ` + | ||
| `(it did not call wake_agent and is no longer watching).\n` + | ||
| `[${record.mode} script ${record.id}] Script ${record.status === 'died' ? 'DIED' : 'finished'} ${elapsedMin}m after start ` + | ||
| `(return_code=${exec.returnCode}). Retrieve the retained result with code_execution action=wait, script_id=${record.id}.\n` + |
There was a problem hiding this comment.
Completion notice promises evicted result The notice tells the agent to retrieve the result by
script_id, but if five other executions settle before the agent acts on the queued wake, this result is evicted. action=wait then returns no script, leaving a foreground script’s full output unavailable beyond the notice’s short tail.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/framework.ts
Line: 8442
Comment:
**Completion notice promises evicted result** The notice tells the agent to retrieve the result by `script_id`, but if five other executions settle before the agent acts on the queued wake, this result is evicted. `action=wait` then returns `no script`, leaving a foreground script’s full output unavailable beyond the notice’s short tail.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.anima-research#155 separates how long the agent watches a script (wait_ms, on_timeout) from how long the script may run. "timeout" there names the watching side, so a per-call `timeout_ms` that stops the script would read as the same thing. `time_limit_ms` pairs with `wait_ms` instead. The description also says that stopping a script does not stop tools it already called. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UCEhG5Sap3gQCiaNzWEXqb
… and the integration fixes Brings the branch onto current main (0.19.0 plus anima-research#203's per-call time_limit_ms). Conflicts in PyRunner.exec, the tool description and runCodeExecution are resolved as on the local execution integration branch: time_limit_ms bounds the Python run and wait_ms bounds the observation, the time-limit cap note survives a yield and a later wait. Also carries the fixes found while integrating (without anima-research#181's structured results, which stay in their own PR): - a script notification accepted into a live tool continuation retires its queued wake; a turn that ends keeps it armed - shutdown resolves pending inner-tool waiters; late tool results after the queue closed are dropped instead of raising - host status counts running background interpreters only, fixing the typecheck against anima-research#122's getHostModeStatus Co-Authored-By: Codex (GPT-6) <noreply@openai.com> Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Greptile: - An inner end-turn ends only a turn still waiting on its script; a late request after the call returned "running" is dropped instead of ending the turn that later retrieves the result. - codeExecution.foregroundWaitMs is validated when the framework is created, so an invalid default can no longer refuse list/cancel. - A result announced by a completion notice is kept until retrieved (at most 20 per agent); other settled results stay five deep. The notice says so. - wake_agent's ack contract is stated precisely: stored, or queued behind the agent's live turn like every deferred writer. Review: - Ephemeral agents wait for their foreground scripts to end instead of being promised a completion wake they can never receive. - Quiesce keeps anima-research#122's guarantee: drain waits for foreground scripts that outlived their turn and abandon stops them; status adds foregroundScripts. Background watchers stay exempt. - The code_execution description and header comment are restored, with only the waiting and retention sentences added. - wake_agent messages are conversational again; completion notices stay system. - ScriptRun turns a rejected completion into a failed result again; an undeliverable completion notice is logged. - Revert-red tests for each fix and for the four untested changelog fixes (background end-turn, observed-only abort, stale wake_ack, wakeAbort). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Updated to Greptile
@slimepriestess
The cancel test now also asserts that a prior global is gone after cancelling. @antra-tess: items 4, 5 and 7 were yours to rule on. In each case I kept the old behaviour, and any of them can be changed. #181 is separate and not included here. |
…d carries no thinking
Context-manager 0.13's SelectOptions.dryRun is a non-committing compile: no
fold resolutions persisted, no produce/merge work enqueued, no transition
bookkeeping ("every slider drag would rewrite the fold plan and spend LLM
tokens"). With context-manager #160 (#155's thinking binding), a compile
without dryRun also commits stamps and joins its branch's list of compiles
awaiting acceptance; one that is never sent fences the walk when its chain
at its head isn't the walk's, and the replies stored after it then lose
their thinking once a later compile is accepted (Oscar-1005 confirmed this
reading, room-475 #67755). Between turns that costs nothing; mid-stream it
costs the stream's remaining replies.
agent-framework's compiles that are never sent:
- previewActivation (both paths), which undo, rollback and hiding messages
run for their "last visible" line (lastVisiblePreview). It passes the new
ActivationRequestOptions.dryRun to buildActivationRequest, which passes it
to compileWithInjections.
- ApiServer cmdMessageList and cmdAgentContext (the latter at an optional
hypothetical budget).
- A conversation fork's seed: the template's compile, which can run while
the template streams, and the new namespace's emptiness check.
Agent-side, compileWithInjections now skips, for a dry run, the two things it
commits after a compile: the RFC-006 consumed watermark (nobody read what a
dry run assembled) and settleRuntimeSettingsTransition (a prepared budget
change waits for the compile that is sent). A default preview used to
settle a converging budget change; previewActivation's own doc already
promised "no trace on the system", and its comment said only a budgeted
preview suppressed settling. The doc claims that ContextManager.compile is
side-effect-free (buildActivationRequest, previewActivation) are corrected.
setToolDefinitions(compressionTools) still runs for a preview, as before;
the doc now says so.
The fork seed also leaves out the template's thinking and redacted_thinking
(withoutThinking; a message left empty is dropped, any other is kept as it
was). Each block was signed under the template's prefix, and the fork's
system, tools and scoped injections don't promise to match it, so on an
account that enforces binding the fork's first request could be refused
(inferred from Oscar-1005's probes, not observed); once the fork's own pass
engages, unstamped copies never render anyway. If the template is mid-turn,
the seed can end on a tool round whose thinking is now gone: Oscar-1005's
probe found a stripped in-flight round accepted on Opus 5.5 and Haiku 5.5
(not probed: Sonnet 5.5, Fable 5.1).
connectome-host has the same kind of compiles; Agnes-1009 took them (#234,
and a follow-up stacked on its #213).
Tests: test/unsent-compiles.test.ts (4: previews dry with and without
injections and at a budget; a preview leaves the watermark; a preview leaves
a prepared budget change, with a sent compile settling it as the control;
the API's message list and context dry) and conversation-routing (+1: the
fork seed's compiles are dry, by instance, and the seed has no thinking).
Red at 02adf27 with only these test files: 5 fail, 15 pass.
…messages are read Context-manager #155 (thinking binding): a provider that binds signed thinking to the prefix it was minted under refuses a reply whose stored prefix holds a message its request didn't. Context-manager #160 stamps each reply from the store as it stands when the reply is stored, and the host's side of that contract (acceptRound's doc) is that what's stored after a compile, up to a reply, is what the round's request carried. Three agent-framework paths broke it. 1. Window-only deferred messages. At a tool boundary every message deferred for the agent was stored, and only the injectable ones were injected: one carrying tool blocks (the membrane's tool cycle) or named as the agent (a prefill) was stored between the turn's rounds without being injected. Sources: the built-in Discord module's backfill stores the bot's own messages under the agent's name; any module's addMessage as the agent or with tool blocks; the puppet pair's defensive queueing. Oscar-1005 showed the direction (room-203 #67671): the reply stored after such a message is stamped with it, later compiles render it, the stamp matches, and the thinking goes out bound to a request without it. Fix: drainDeferredFor takes an optional stopAt; the tool boundary drains the agent's queue only up to its first message that isInjectableMidTurn rejects. That message and everything queued behind it stay queued for the turn-end flush (driveStream's finally), where no reply follows them in the stream. A cut rather than a skip: the queue's order is kept everywhere else, the Discord backfill depends on chronology, and a pair must stay adjacent. Cost: messages queued behind a held one wait for the turn's end. A budget restart doesn't drain at its turn start (continuingTurn), so they wait through it too. 2. Re-deferral split a pair. addMessage's deferral skipped any write carrying tool_result. A turn-end flush can run after a successor turn has taken the agent (the 2026-07-31 interleave) and re-adds drained entries through addMessage, so a queued pair's tool_use re-deferred while its tool_result was stored alone. (1) sends every mid-turn pair through that flush. Now only a fresh tool_result write keeps the bypass; one coming back from the queue (deferredWriteId) re-defers like anything else. resume() clears quiesced before its flush and boot recovery runs with no turn alive, so neither changes. This also covers a flush while another agent's tool cycle is pending (the default path's global midToolCycle). The post-surgery flush (the surgery hold's release) was the one re-add from the queue that passed no deferredWriteId, so its re-adds counted as fresh, and boot recovery couldn't dedupe its writes after a crash; it now passes the id like the others (Petra-1012 found it reading the PR text). 3. RFC-006 watermark. The consumed watermark moved only at a compile (and boot, branch switch, a new fork). A message injected at a tool boundary is stored above the turn compile's watermark, so isUnreadStoredMessage called it unread and a coalesced replacement for its subject could remove it (push-coalescer acceptPlain/acceptDeferred/retract -> removePlacement -> cm.removeMessage), mid-turn or before the next compile: a message the agent heard and answered, and, under #160, either a stripped reply or a refused one depending on whether the removal came after or before the reply was stored (Oscar-1005, room-475 #67755). Fix: after provideToolResults hands injected messages to the stream, agent.markContextConsumed(). Store head, not the last injected sequence: conservative for other slots' messages stored since the compile (they read as read, so a replacement appends). Only when something was injected; endTurn, restart and non-streaming boundaries inject nothing and leave the watermark alone. Tests: test/deferred-window-only.test.ts, 7. Red at 02adf27: 6 fail; the control (a message stored at an endTurn boundary stays unread) passes before and after. operator-surgery, +1: a write deferred behind a surgery lands with its durable id (fails with the post-surgery flush passing none).
…them Context-manager #155 (thinking binding): a provider that binds signed thinking to the prefix it was minted under refuses that thinking once a later request changes anything before it. Two agent-framework request-only constructs changed the prefix between the request that minted a reply and every later request. 1. Silent heartbeat: the tick's instruction rode an ephemeral system-position injection (InferenceRequest.ephemeralSystemPrompt -> ContextInjection 'framework:silent-heartbeat'), so every heartbeat request's system differed from every other request's. That request missed the prompt cache from the system on, every retained thinking block failed its binding there, and the tick reply's own thinking failed at the next ordinary turn. Now the instruction is the separator's text (SILENT_HEARTBEAT_SEPARATOR), which every compile already renders byte-identically before the tick's first surviving row and which the tick's own request ends on. Every past tick reads it too, telling the agent those turns were private self-checks. Steady-state cost: 199 characters where the separator was 16, at each tick whose rows survive in the window (roughly 50 tokens a tick). One-time cost: the first compile after upgrading renders past ticks with the new text, one prompt-cache rewrite (and no strip if this ships before agent-framework engages context-manager #160's pass). ephemeralSystemPrompt is removed from InferenceRequest (types/agent.ts), the push-event wake, the batched wake, the budget restart and the system injection: the heartbeat was its only producer; connectome-host's main neither sets nor reads it (git grep), nor does anything else in the anima checkouts here. It is an exported interface, so the removal has its own breaking fragment naming who would need to act; the behaviour is in the changed fragment. 2. [Continue]: a compile that ends on the agent's own message gets a trailing request-only [Continue] turn, and later compiles didn't render it, so the reply it prompted sat straight after the previous assistant message: the formatter merged them (signed thinking from two responses in one message, the "cannot be modified" 400 when it is the latest), and the reply's binding failed from the next request. Context-manager's chain never sees request-only turns (it chains its own compiled messages), so it would send that thinking. Now the reply's first stored row is stamped metadata.promptedBy: 'continue' and request builds render [Continue] immediately before it, found the way tick rows are found (tool_use ids, thinking signatures; text never). Same known limit as ticks: a reply with neither isn't found, and carries no thinking to fail. Mechanics: - silent-heartbeat.ts is now the request-only-rows module. indexRequestOnlyRows returns ticks (identity key -> eventId, this agent's tick rows) and continued (the identity keys of stamped rows), independently, so a row can be both. renderRequestOnlyRows puts a tick's separator before the tick's first surviving row and [Continue] immediately before each stamped row, after any separator, as the reply's request had them. A stamped row is one stored row of the agent's own reply, which a compile never splits, so [Continue] needs no once-per-group check. (Basil-1007 asked about a row carrying both stamps: no path found today opens a stream inside a tick on [Continue] — every same-turn restart or retry compiles past stored tool results or an unchanged opening — but the first version would have rendered only the separator for such a row.) Internal names only; the public exports (SILENT_HEARTBEAT_SEPARATOR, silentHeartbeatOf, the tick types) keep their names. - Agent.buildActivationRequest's body is now assembleActivationRequest (private), which also reports endsOnContinue; buildActivationRequest wraps it for previews. startStreamWithInjections sets continuePromptPending from it; addAssistantResponse stamps the first row it stores while that's set. Every stream start resets it (a budget restart, guard retry or error retry compiles afresh); the other addAssistantResponse callers store the stream's own rows. The direct Agent API stores through contextManager.addMessage and never adds [Continue]. Tests: silent-heartbeat-rows (+4: the tick's system is the agent's own; the reply to [Continue] is re-rendered byte-stable; only its first row is marked; a row with both stamps gets both turns, separator first, also when it is the tick's first surviving row) and silent-heartbeat (the instruction is the request's last turn and the system is unchanged). The separator literal in the rows test is now the exported constant. Red at 02adf27 with only these test files: 4 of the integration tests fail (the both-stamps unit test needs the new exports). Reviewed by Basil-1007.
|
@Meganeuridae #608 replaced the Python |
Problem
A Python
awaitcurrently holds the model-facing tool call open until the whole script exits. The runner also accepts overlapping cold starts, and an outstanding tool response from a killed interpreter can satisfy a replacement interpreter's reused call ID. Background wake requests can exceed the configured rate/cap when concurrent and falsely acknowledge a context delivery failure.Changes
script_idafter 10 seconds by default (wait_ms, maximum 60 seconds);action=waitretrieves or rejoins the same run. Ephemeral agents wait for the script to end, since they cannot receive a completion notice. Persistent Python state stays serial and busy responses identify the active run.on_timeout=end_turnarms completion delivery before returning an end-turn result. An active observer consumes completion directly; otherwise a yielded execution sends a bounded completion notice and wake. A result a notice announced stays retrievable until the agent retrieves it (at most 20 per owner); other settled results, the five most recent. An inner end-turn ends only a turn still waiting on its script.abandonstops them; host status addsforegroundScripts. Background watchers stay exempt.releaseCodeExecutionWait(agentName, scriptId?)for the host's admin-only command. This resolves observations through the normal tool-result boundary without killing Python. It is not an agent tool and is not exposed on the generic framework socket API.The agent-facing description keeps its original guidance and adds the waiting behavior;
docs/code-execution-lifecycle.mddescribes the exact behavior and the further design work. A breaking-change fragment calls out the new foreground return contract.Tests
Current head (merged with
mainate45c1a1, which includes #203):npm test: 1138 pass / 0 fail / 4 skipped (1142 total).Original submission: baseline
8b612f5786 pass; branch 802 pass; three regressions replayed red against the original source.Not verified / limits
No live provider, resident deployment, host restart recovery, or cross-platform run was performed. Scripts/results remain process-local. Cancelling Python does not cancel an already dispatched inner tool. A
wake_agentqueued behind a live turn is acknowledged once queued; a store rejection when it is later flushed is logged and dropped, as for every deferred write. Durable outcomes, paged journals, and delivery receipts are explicitly deferred in the design note.Companion PRs
Companion: anima-research/connectome-host#130. It adds admin-only
/release-wait [script_id]and recipe validation. Publish/install a framework release containing this primitive before expecting that command to work; the companion fails explicitly on older framework versions. No merge, package publication, dependency rollout, or deployment is included here.Generated with OpenAI Codex.
Review fixes: 🤖 Generated with Claude Code