Skip to content

The release sweep has never once closed a retained child's slot: 0 released records in 6,916 runs, and 154 of 601 children never settle #1301

Description

@drewstone

Summary

The released-terminal-record path added in 0.233.0 for #1247 has never executed once in production. Across 6,916 run journals on this operator host: 7,589 settled events, 0 with retainedExecution: 'released', and 0 environment-teardown receipts on any child id. Every retained-pending child still ends as reconciled with an open cursor slot, which fleetYield reports as neverSettled.

The rate is not improving. On the 108 runs that carry a fleetYield (0.233.0+): 154 of 601 spawned children never settled (25.6%). Today, verified-agency-20260919a (0.240.0, provider-tangle 1.6.0) ran 10.0 hours, spawned 4 children and lost 4 of 4.

I reproduced the gate locally on v0.240.0 (63c8f5a4) through the real providerAsExecutor retained path. Test output below.

The mechanism

A retained child that cannot be observed settles down with recoveryPending: true. scope.ts:3210-3218 writes a reconciled floor instead of a terminal record and defers the real one to the release sweep. That is correct and deliberate.

The sweep (scope.ts:2008-2110) only writes the deferred record behind child.cleanupConfirmed (scope.ts:2085), and cleanupConfirmed is set only when every teardown receipt reports destroyed: true and confirmTeardown() then succeeds (scope.ts:2037-2041).

For the failure that actually kills these children, that condition is unsatisfiable by construction. The child died because its sandbox stopped; environment.destroy() on a stopped sandbox answers HTTP 409, so providerAsExecutor.releaseRetained (environment-provider.ts:851-905) returns [receipt(false, …)], the gate stays shut, and the slot never closes.

The same fact — the environment is gone — is both the reason the child failed and the reason its settlement is withheld.

Downstream: spawn-journal.ts:501 puts the node in inDoubt, supervisor.ts:1313 counts it as fleetYield.neverSettled, spendGaps marks it never-settled, its tokens and USD are unattributed, and its transcript is unreachable.

Local reproduction (v0.240.0, 63c8f5a4)

A retained provider whose event stream and result() both raise HTTP 409: Sandbox is not running (status: stopped) — the production error verbatim — driven through createScope + providerAsExecutor + releaseRetainedEnvironments. Only environment.destroy() differs between the two arms.

[ok]    settled fields = {"retainedExecution":"pending","retainedPendingCause":"unobservable","reason":"retained provider execution requires reconciliation before replacement: caused by AggregateError: retained event observa…"}
[ok]    journal BEFORE sweep = {"spawned":1,"execution-input":1,"execution-admitted":3,"reconciled":1}
[ok]    executor.releaseRetained bound = function
[ok]    journal AFTER sweep  = {…,"reconciled":1,"environment-teardown":1,"settled":1}
[ok]      environment-teardown: destroyed=true
[ok]      settled: status=down, retainedExecution=released          <-- slot closes

[throw] settled fields = {"retainedExecution":"pending","retainedPendingCause":"unobservable","reason":"retained provider execution requires reconciliation before replacement: caused by AggregateError: retained event observa…"}
[throw] journal BEFORE sweep = {"spawned":1,"execution-input":1,"execution-admitted":3,"reconciled":1}
[throw] executor.releaseRetained bound = function
[throw] journal AFTER sweep  = {…,"reconciled":1,"environment-teardown":1}
[throw]   environment-teardown: destroyed=false,
            detail="providerAsExecutor(stopped-sandbox): environment.destroy() failed — HTTP 409: Sandbox is not running (status: stopped)"
[throw]   (no settled record)                                        <-- slot stays open forever

The [throw] arm is the production shape. It is deterministic: the child is neverSettled for the life of the run and of every reader after it.

A second red test shows the gate also shuts on a contract-violating executor that answers destroyed: true from releaseRetained but still destroyed: false from teardown — because confirmTeardown is teardownExecutor(…), which throws on a destroyed: false receipt (deadline.ts:116-120) and the throw is swallowed at scope.ts:2041. That contract is stated at types.ts:205-213 but nothing enforces or tests it.

Why this shipped untested

releaseRetained appears in exactly five source files and zero test files:

src/runtime/environment-provider.ts
src/runtime/supervise/scope.ts
src/runtime/supervise/types.ts
src/runtime/supervise/supervisor.ts
src/runtime/supervise/driver-executor.ts

No test drives releaseRetainedEnvironments through an executor that implements releaseRetained, so the release-written terminal record has no end-to-end coverage at all. observer-projection.test.ts asserts the projection of a released record that nothing produces.

A second, latent instance of the same class

Two wrappers rebuild Executor from an explicit allowlist and silently drop optional methods, releaseRetained among them:

  • gateOnDeliverable — completion-gate.ts:78-141
  • retryPreSpawnRefusals — worker-retry.ts:255-273, whose own doc at worker-retry.ts:179-180 claims it "delegates every other surface to it"

Both forward runtime, budgetExempt, deliver, progress, traceSource, metered, harnessTranscript, execute, teardown, resultArtifact and drop releaseRetained, recover, cancel, accounting, interactiveReady, teardownTimeoutMs. A worker behind either wrapper can never be released (so never settles), never recovered on resume, and never cancelled with provider acknowledgement — silently, because undefined is the legitimate "not implemented" answer. Neither wrapper is on the path the runs above took (no deliverable, no workerRetry), so this is a latent defect rather than the measured one, but it is the same omission and it produces exactly the zero-receipt shape.

Population

154 of 601 spawned children never settled across the 108 runs carrying a fleetYield. All 154 have the identical journal shape: reconciled present, environment-teardown absent, teardown-unconfirmed present, node status failed (147) or cancelled (7) with a settledAt already stamped.

run date runtime provider spawned done down neverSettled
diu-proj00716-20260916-glm-v1-wide-f 09-16 0.236.0 1.5.0 7 0 0 7
diu-proj00716-20260916-kimi-v1-wide-f 09-16 0.236.0 1.5.0 7 0 0 7
mech-interp-foundations-sandbox-a-20260917d 09-17 0.237.0 1.5.0 12 4 3 5
mech-interp-foundations-sandbox-a-20260917g 09-17 0.237.0 1.5.0 14 7 2 5
diu-proj00716-20260916-long-horizon-j 09-18 0.238.0 1.5.0 25 7 0 18
diu-proj00716-20260916-release-manager-j 09-18 0.238.0 1.5.0 26 4 1 21
diu-proj00716-20260916-model-steward-j 09-18 0.238.0 1.5.0 15 6 0 9
zkp-frontier-prover-a-20260918a 09-18 0.239.0 1.6.0 8 2 0 6
verified-agency-20260918e 09-18 0.240.0 1.6.0 107 86 11 10
continual-capability-20260918e 09-18 0.240.0 1.6.0 118 99 8 11
verified-agency-20260919a (withdrawn: 81% host sleep) 09-19 0.240.0 1.6.0 4 0 0 4

(18 further runs with 1–4 each.) By version: 0.236.0 19/22, 0.237.0 12/60, 0.238.0 90/246, 0.239.0 8/40, 0.240.0 25/229.

Host sleep is controlled for, and it does not explain the population. I built the sleep timeline from pmset -g log on this host (Sleep -> next DarkWake/Wake; the log covers 2026-09-13T05:54Z..2026-09-19T23:32Z UTC). verified-agency-20260919a is sleep-confounded and withdrawn as an exhibit: the host was asleep 486.7 of its 600.6 minutes (81.0%) across 69 windows, all four children lived through 75-86% host sleep, and three of the four had their last observation land while the host was asleep. A laptop-resident supervisor cannot observe a retained run while the laptop is in Deep Idle, so that run explains itself.

The rest of the population does not. Within pmset coverage there are 154 never-settled and 447 settled children from the same runs in the same window. 133 of the 154 never-settled children (86%), across 26 of 31 runs, had ZERO host sleep over their entire spawn-to-last-observation span. Cohort comparison: host asleep over the child's lifetime, mean 10.6% never-settled vs 2.0% settled, median 0.0% for both; spawn instant inside a sleep window 0/154 and 0/447. The sleep signal is real but confined to a minority.

The distinction that matters: host sleep can explain why a child enters the reconciled state. It cannot explain why it can never leave it. The reproduction below runs offline, with no network and no provider, and shows the promotion gate failing on its own.

Ask

  1. A retained-pending child whose environment cannot be destroyed must still reach a terminal state. An unreachable environment is stronger evidence that no later process will recover it, not weaker. scope.ts:2085 currently reads "we proved we destroyed it"; on the no-resume path (supervisePursuit forces retainedAtSettlement: 'release' and refuses re-entry) the correct condition is "no process will resume this run", which is already known at the call site. The unconfirmed environment is separately reported as teardown-unconfirmed, so closing the slot loses no leak evidence. If the distinction between "gone" and "alive but undeletable" matters, the receipt's detail (or a typed field) can carry it, rather than conflating both into "stay open forever".

  2. Cover the release sweep end to end. One test with an executor that implements releaseRetained, on each of: destroyed: true, destroyed: false, [], and a throwing confirmTeardown. Today all four behave the same in production, and three of them are silent.

  3. Stop rebuilding Executor from an allowlist. gateOnDeliverable and retryPreSpawnRefusals should delegate every unlisted surface (or spread the inner executor and override only what they change), so a new optional method cannot be silently dropped by a wrapper that claims to delegate it.

Open

Production is one step worse than the reproduction and I could not separate the last branch from records alone: the runs above have no environment-teardown receipt at all for any child, whereas the reproduced [throw] arm writes a destroyed: false one. releaseRetained returns [] only at environment-provider.ts:855 (!pending) and :860 (environmentId === undefined), or the sweep's own catch at scope.ts:2046. The records cannot tell these apart.

The measurement that separates them: log which of those three the sweep took, per child, at scope.ts:2037-2048 — the branch taken, typeof child.releaseRetained, and receipts.length — then press one short pursuit. One line, one run.


Filed from the discovery program; reproduction sources are src/runtime/supervise/never-settled-e2e.test.ts and never-settled-repro.test.ts in a worktree at v0.240.0, available on request.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions