Skip to content

Enforce journal deadlines independently of conversation actors - #1746

Merged
jhgaylor merged 3 commits into
mainfrom
feat/independent-execution-deadlines
Sep 12, 2026
Merged

jhgaylor merged 3 commits into
mainfrom
feat/independent-execution-deadlines

Conversation

@jhgaylor

@jhgaylor jhgaylor commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

A blocked conversation callback must not prevent a turn deadline from expiring. This adds a supervised coordinator with separate expiration and termination pools, task-local timeouts, and persisted recovery that never replays an uncertain provider write. It terminates only the journaled provider session after an ownership-checked claim.

Stacked on #1745. Draft; public bounded execution remains disabled. Trusted session identity, durable deadline events, and the remaining actor/sandbox lifecycle integration still block activation. Local task death does not prove remote termination or stopped billing.

Validation: full precommit passes 4,671 tests and 6 doctests, zero failures, including 38 worker/journal regressions. Tests cover blocked actors, saturated termination slots, competing coordinators, timeout/restart, shutdown, and the recorded provider target. No live provider operations occurred. ADR 0046 and the evidence artifact preserve prior shutdown failures and their corrections.

Tracks #1732. No SDK surface changes beyond the parent; sdk-no-release applies.

Part of #1732held. Superseded on main by #1773#1793; the residual hunks are being re-cut as focused PRs tracked by #1864, which keeps these frozen until each has a replacement or an explicitly linked deferral. Do not close: they are the reference for that mapping.

@BinaryBourbon BinaryBourbon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review: the coordinator shape is right; it should not be on by default, and it has no way out of uncertain

The separation this PR is built on is the correct one — a blocked provider call in the termination pool cannot starve expiry, each task carries its own :timer.kill_after, async_nolink under Fountain.TaskSupervisor is the supervised form this repo requires, and terminate/2 gives short journal writes a bounded grace before a brutal kill. _unsafe_due_deadlines/2 and _unsafe_ready_terminations/1 splitting the old _unsafe_due/2 is a clean way to express it.

Requesting changes on two points.

1. It runs by default, and there is no runtime switch

defp execution_deadline_children do
  if Application.get_env(:fountain, :execution_deadline_worker_enabled, true),
    do: [Fountain.Conversations.ExecutionDeadlineWorker],
    else: []
end

The default is true and only config/test.exs turns it off, so every prod and dev node starts a coordinator that runs two queries per second against turn_executions forever. Nothing in config/runtime.exs reads it, so an operator cannot turn it off without a rebuild.

Two asks:

  • Gate it on execution limits actually being configured — the rest of this stack is careful to ship inert (enforced_controls/1 returns [], ADR 0046 says "no API or scheduler activates bounded turns yet"), and a per-second poll on every replica is the one part that is not.
  • Give it a runtime.exs key regardless, so it can be turned off in production without a deploy. That also needs a row in docs/configuration.md or config_reference_test will fail.

A 1 s tick is also much tighter than anything comparable here (SandboxReaper, CreditExpirer). Deadlines are absolute and already durable, so a 5–15 s tick would cost nothing in correctness and a lot less in load. If 1 s is deliberate, a sentence saying why would help.

2. Every failed provider termination is permanent

defp call_terminator(terminator, attempt) do
  terminator.(attempt)
rescue
  _ -> {:error, :termination_unconfirmed}
catch
  _, _ -> {:error, :termination_unconfirmed}
end

That lands the row in uncertain, and _unsafe_ready_terminations/1 selects state == "ready" only, so nothing ever retries it. _unsafe_recover_submissions/2 only moves submitteduncertain, which makes the absorbing state easier to reach, not easier to leave. The 10 s :timer.kill_after does the same thing to any provider call that outlives it.

The downstream cost is on #1744, where I have written it up in full: a row in uncertain keeps _unsafe_fenced?/1 true, refuses every new turn on the conversation, and makes reset_sandbox/2 return :sandbox_mid_turn forever. So on this branch a single slow terminate_session bricks a conversation and its shared home with no operator lever. A bounded retry with backoff, or an age after which the obligation is written off, would close it; either way it belongs in this PR, because this is the component that manufactures the state.

Smaller

  • @providers maps all four providers, but ExecutionTransport in #1748 refuses anything but "sprites" and #1749 rolls back :provider_not_supported for the rest, so three of those four entries are unreachable. Either drop them or say in the moduledoc that they are staged for later — as written it reads like runner/E2B/Daytona are supported.
  • start_candidates/3 does Enum.take(ids, @pool_size - used). It is safe today because used can never exceed @pool_size, but a negative count takes from the end of the list rather than returning [], which is a nasty way to find out if that invariant ever changes. Enum.take(ids, max(@pool_size - used, 0)) costs nothing.
  • @batch_size 100 against a guard of limit in 1..100 — fine, but exactly at the boundary. Worth a comment so nobody bumps one without the other.

@jhgaylor
jhgaylor force-pushed the feat/typed-execution-limits branch from 7db7644 to 84cb45f Compare September 11, 2026 05:32
jhgaylor added a commit that referenced this pull request Sep 11, 2026
Review of #1746 found the coordinator manufacturing the state #1744's journal
could not leave, and running everywhere whether or not anyone asked for it.

**It is off unless an operator asked.** The child list defaulted to `true` with
only `config/test.exs` turning it off, so every prod and dev node polled
`turn_executions` twice a second forever, and nothing in `runtime.exs` read the
key — an operator could not stop it without a rebuild. It now defaults to off,
`runtime.exs` derives the default from whether `FOUNTAIN_EXECUTION_LIMITS` sets
a host ceiling, and `FOUNTAIN_EXECUTION_DEADLINE_WORKER` overrides either way.
The tick moved from 1s to 5s and is configurable: a deadline is absolute and
durable, so lateness costs precision, not safety. Both variables have rows in
`docs/configuration.md`, which `config_reference_test` requires.

**A failed termination is written off, not fenced forever.** `call_terminator/2`
rescues every failure to `{:error, :termination_unconfirmed}`, and
`_unsafe_ready_terminations/1` selects only `state == "ready"`, so nothing ever
looked at the row again. One slow `terminate_session` therefore fenced its
conversation and its shared home permanently, and took `reset_sandbox/2` with
it. The recovery tick now also calls `_unsafe_retire_unresolved/2`: past an hour
a row in `awaiting_identity` or `uncertain` retires to `stopped` with its
`last_error` intact.

A retry was the other candidate and is deliberately not what happens. One
persisted attempt authorizes exactly one provider write; re-arming a lost
attempt would replay an operation whose outcome is unknown, or require assuming
`terminate_session/3` is idempotent across a session that may already have been
replaced. Giving up on a cleanup Fountain cannot confirm is the smaller claim.
ADR 0046 records both the sweep and the rejected alternative.

**Smaller.** `@providers` listed four backends while `ExecutionTransport`
refuses everything but Sprites, so three entries were unreachable and the map
implied support that does not exist; it now says Sprites. `Enum.take/2` with a
negative count takes from the *end* of a list rather than returning `[]`, which
would start the wrong jobs — `max(_, 0)` so that `used <= @pool_size` staying
true is not load-bearing.

The dependency bump this needs is restored as its own commit below: the
re-cut of #1745 dropped it along with the rest of that branch, and
`Managoat.Sandbox.terminate_session/3` arrives in managoat_sandbox 0.3.0.
It stays a separate commit because it moves the ACP stack (acp 0.4.0,
runtimes 0.4.1, runner 0.2.2) and that is not the coordinator's change.

Full core suite on the bumped set: 6 doctests, 4,988 tests, 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R9reKpmXUUJf4eUMULogie
Signed-off-by: Jake Gaylor <jhgaylor@gmail.com>
@jhgaylor
jhgaylor force-pushed the feat/independent-execution-deadlines branch from 5e6eb3e to 2462d42 Compare September 11, 2026 05:57
@jhgaylor

Copy link
Copy Markdown
Collaborator Author

Force-pushed 2462d426. Rebased onto the re-cut #1745 and both blockers addressed.

It is off unless an operator asked for it. The child list defaulted to true, so every prod and dev node polled turn_executions twice a second forever, and nothing in runtime.exs read the key — it could not be stopped without a rebuild. Now:

  • default off, with runtime.exs deriving the default from whether FOUNTAIN_EXECUTION_LIMITS sets a host ceiling;
  • FOUNTAIN_EXECUTION_DEADLINE_WORKER overrides either way, so an incident can stop the poll without a deploy;
  • the tick is 5 s and configurable via FOUNTAIN_EXECUTION_DEADLINE_INTERVAL_MS. A deadline is absolute and durable, so lateness costs precision, not safety.

Both variables have docs/configuration.md rows — config_reference_test requires that, and it passes.

A failed termination is written off rather than fenced forever. This was the one I cared about: call_terminator/2 rescues every failure to {:error, :termination_unconfirmed} and _unsafe_ready_terminations/1 selects only "ready", so nothing looked at the row again. One slow terminate_session fenced its conversation and its shared home permanently and took reset_sandbox/2 with it. The recovery tick now also calls _unsafe_retire_unresolved/2 (added on #1744): past an hour, awaiting_identity and uncertain retire to stopped with last_error intact.

I did not add a retry, deliberately. One persisted attempt authorizes exactly one provider write. Re-arming a lost attempt would either replay an operation whose outcome is unknown, or require assuming terminate_session/3 is idempotent across a session that may already have been replaced. Giving up on a cleanup Fountain cannot confirm is the smaller claim, and ADR 0046 now records the rejected alternative alongside the sweep. If you want the retry instead, that is a real argument — but it needs the no-replay invariant relaxed explicitly rather than by accident.

Smaller items. @providers is now %{"sprites" => :sprites}: the other three were unreachable (ExecutionTransport refuses them, admission rolls back :provider_not_supported) and the map implied support that does not exist. Enum.take(ids, max(@pool_size - used, 0)), so used <= @pool_size staying true is not load-bearing.

One thing I had to restore. The re-cut of #1745 dropped 7db76444 with the rest of that branch, and this worker needs it — Managoat.Sandbox.terminate_session/3 arrives in managoat_sandbox 0.3.0, and managoat_runner 0.2.0 pins sandbox ~> 0.2.0, so the bump is the whole set. It is back as its own commit below the coordinator, because it moves the ACP stack (acp 0.4.0, runtimes 0.4.1, runner 0.2.2) and that is not the coordinator's change. Worth your eye: that is the largest thing in this PR by risk.

Verification. Five new regressions. Reverting the age-out wiring fails 2; reverting the default-off fails 1. Fountain.Application.execution_deadline_children/0 is @doc false public so the supervision test asserts on the list the supervisor actually reads rather than on the config it was just handed. Full core suite on the bumped dependency set: 6 doctests, 4,988 tests, 0 failures, 2 skipped. mix format --check-formatted, mix compile --warnings-as-errors, mix credo --strict (7,290 mods/funs, no issues), okf validate decisions valid, all three prose gates clean (docs-style.py's two findings are pre-existing in files this PR does not touch).

Moving on to #1747.

jhgaylor added a commit that referenced this pull request Sep 11, 2026
…s door

Rebase of #1751 onto the re-cut stack, plus its two review notes.

**The red CI was staleness and the rebase cleared it.** `hex-audit-gate.exs`
was failing on the `decimal` advisory `EEF-CVE-2026-32686`, acknowledged on
main in #1753 sixty commits after this branch forked. It exits 0 here. The
remaining line is advisory: #1746's dependency bump moved the locked Decimal
artifact, so the acknowledgment no longer matches anything and could be
removed — that is main's call, not this stack's.

**The evidence file no longer reads as if the dead-end were the design.**
`turn-parent-fences.json` said unknown spawn recovery "correctly returns
failed with awaiting_identity". True as an immediate outcome, and the
expectation was right to correct — but stated alone it reads as the final
state, which was the thing I could not find a way out of when reviewing #1744.
It now says `awaiting_identity` is an obligation with an age and names the
sweep that writes it off.

**`_unsafe_create_autonomous_turn/1` is dropped.** It admitted background work
through `_unsafe_create_turn/1`, which skips the sandbox binding #1764 added,
and called the `_unsafe_autonomous_turn/2` helper #1749's re-cut removed for
the same reason. Autonomous turns use the one admission path, so
`turn_parent_test.exs`'s "closed parents refuse user and background admission"
now exercises both doors as the same door.

**Two things this branch had that main's admission did not, now in it.** A
terminated or failed parent refuses a turn with `:not_running` — `attached?`
checks the machine, and a retired actor can still reach the conversation with
a queued prompt. And the parent goes `running` inside the admission
transaction, so a turn and the status explaining it commit together; a reader
could previously see a `running` turn under an `idle` parent for the width of
the launch, and a failed launch left the pair disagreeing. `update_all` rather
than `update_conversation/2`, because that one audits and an audit insert must
not run inside a transaction (ADR 0013).

Kept: the generation fencing that is the point of the PR —
`_unsafe_write_parent/3`, `_unsafe_recover_turn/2`,
`_unsafe_clear_idle_session/2`, and `session_plan/2` returning
`{:error, :execution_fenced}` so even the placeholder session write is tied to
the admitted turn.

Size pin 2739 -> 2733. Full core suite: 6 doctests, 5,080 tests, 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R9reKpmXUUJf4eUMULogie
Signed-off-by: Jake Gaylor <jhgaylor@gmail.com>
jhgaylor added a commit that referenced this pull request Sep 11, 2026
Review of #1746 found the coordinator manufacturing the state #1744's journal
could not leave, and running everywhere whether or not anyone asked for it.

**It is off unless an operator asked.** The child list defaulted to `true` with
only `config/test.exs` turning it off, so every prod and dev node polled
`turn_executions` twice a second forever, and nothing in `runtime.exs` read the
key — an operator could not stop it without a rebuild. It now defaults to off,
`runtime.exs` derives the default from whether `FOUNTAIN_EXECUTION_LIMITS` sets
a host ceiling, and `FOUNTAIN_EXECUTION_DEADLINE_WORKER` overrides either way.
The tick moved from 1s to 5s and is configurable: a deadline is absolute and
durable, so lateness costs precision, not safety. Both variables have rows in
`docs/configuration.md`, which `config_reference_test` requires.

**A failed termination is written off, not fenced forever.** `call_terminator/2`
rescues every failure to `{:error, :termination_unconfirmed}`, and
`_unsafe_ready_terminations/1` selects only `state == "ready"`, so nothing ever
looked at the row again. One slow `terminate_session` therefore fenced its
conversation and its shared home permanently, and took `reset_sandbox/2` with
it. The recovery tick now also calls `_unsafe_retire_unresolved/2`: past an hour
a row in `awaiting_identity` or `uncertain` retires to `stopped` with its
`last_error` intact.

A retry was the other candidate and is deliberately not what happens. One
persisted attempt authorizes exactly one provider write; re-arming a lost
attempt would replay an operation whose outcome is unknown, or require assuming
`terminate_session/3` is idempotent across a session that may already have been
replaced. Giving up on a cleanup Fountain cannot confirm is the smaller claim.
ADR 0046 records both the sweep and the rejected alternative.

**Smaller.** `@providers` listed four backends while `ExecutionTransport`
refuses everything but Sprites, so three entries were unreachable and the map
implied support that does not exist; it now says Sprites. `Enum.take/2` with a
negative count takes from the *end* of a list rather than returning `[]`, which
would start the wrong jobs — `max(_, 0)` so that `used <= @pool_size` staying
true is not load-bearing.

The dependency bump this needs is restored as its own commit below: the
re-cut of #1745 dropped it along with the rest of that branch, and
`Managoat.Sandbox.terminate_session/3` arrives in managoat_sandbox 0.3.0.
It stays a separate commit because it moves the ACP stack (acp 0.4.0,
runtimes 0.4.1, runner 0.2.2) and that is not the coordinator's change.

Full core suite on the bumped set: 6 doctests, 4,988 tests, 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R9reKpmXUUJf4eUMULogie
Signed-off-by: Jake Gaylor <jhgaylor@gmail.com>
@jhgaylor
jhgaylor force-pushed the feat/independent-execution-deadlines branch from 2462d42 to 033eb63 Compare September 11, 2026 07:51
jhgaylor added a commit that referenced this pull request Sep 11, 2026
…s door

Rebase of #1751 onto the re-cut stack, plus its two review notes.

**The red CI was staleness and the rebase cleared it.** `hex-audit-gate.exs`
was failing on the `decimal` advisory `EEF-CVE-2026-32686`, acknowledged on
main in #1753 sixty commits after this branch forked. It exits 0 here. The
remaining line is advisory: #1746's dependency bump moved the locked Decimal
artifact, so the acknowledgment no longer matches anything and could be
removed — that is main's call, not this stack's.

**The evidence file no longer reads as if the dead-end were the design.**
`turn-parent-fences.json` said unknown spawn recovery "correctly returns
failed with awaiting_identity". True as an immediate outcome, and the
expectation was right to correct — but stated alone it reads as the final
state, which was the thing I could not find a way out of when reviewing #1744.
It now says `awaiting_identity` is an obligation with an age and names the
sweep that writes it off.

**`_unsafe_create_autonomous_turn/1` is dropped.** It admitted background work
through `_unsafe_create_turn/1`, which skips the sandbox binding #1764 added,
and called the `_unsafe_autonomous_turn/2` helper #1749's re-cut removed for
the same reason. Autonomous turns use the one admission path, so
`turn_parent_test.exs`'s "closed parents refuse user and background admission"
now exercises both doors as the same door.

**Two things this branch had that main's admission did not, now in it.** A
terminated or failed parent refuses a turn with `:not_running` — `attached?`
checks the machine, and a retired actor can still reach the conversation with
a queued prompt. And the parent goes `running` inside the admission
transaction, so a turn and the status explaining it commit together; a reader
could previously see a `running` turn under an `idle` parent for the width of
the launch, and a failed launch left the pair disagreeing. `update_all` rather
than `update_conversation/2`, because that one audits and an audit insert must
not run inside a transaction (ADR 0013).

Kept: the generation fencing that is the point of the PR —
`_unsafe_write_parent/3`, `_unsafe_recover_turn/2`,
`_unsafe_clear_idle_session/2`, and `session_plan/2` returning
`{:error, :execution_fenced}` so even the placeholder session write is tied to
the admitted turn.

Size pin 2739 -> 2733. Full core suite: 6 doctests, 5,080 tests, 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R9reKpmXUUJf4eUMULogie
Signed-off-by: Jake Gaylor <jhgaylor@gmail.com>

@BinaryBourbon BinaryBourbon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review: both asks answered, and the ageing exit is the right shape

Re-reviewed at 033eb63. The two blocking points are closed and closed in the way I would have argued for.

It is off unless an operator asked. execution_deadline_children/0 defaults to false, runtime.exs derives the default from whether FOUNTAIN_EXECUTION_LIMITS gives a host ceiling, FOUNTAIN_EXECUTION_DEADLINE_WORKER overrides either way, and both variables have rows in docs/configuration.md. The tick is 5 s and configurable. The two supervision tests assert the child list itself rather than a flag, including the unset case, which is the assertion that would actually catch a default flipping back.

A failed termination is written off, not fenced forever. _unsafe_retire_unresolved/2 on the recovery tick, past an hour, stopped with last_error intact. The reasoning for ageing out rather than retrying — one persisted attempt authorizes exactly one provider write — is right, and it is now in ADR 0046 as a rejected alternative rather than as an omission. Of the three new regressions, "the same attempt is never submitted to the provider twice" is the one that earns its keep: it proves the no-retry rule instead of restating it.

@providers down to Sprites and Enum.take(max(_, 0)) both done.

One thing I would still like written down somewhere

The default derives from the host ceiling only, but users.execution_limits is a real per-account ceiling with its own writer (Accounts.update_execution_limits/3). An operator who gives one account a ceiling and sets no host ceiling gets bounded turns with no coordinator on any node: nothing expires a deadline, and nothing ages an obligation out — the dead-end this PR just closed, reachable by configuration instead of by a slow provider.

The docs row does say "Set true if you give an account a ceiling but no host ceiling", so the burden is entirely on the operator reading that sentence. Cheapest closure later is to make it not a sentence: refuse at admission (or warn at boot) when an allowance can resolve for an account and the worker is off. Not blocking — enforced_controls/1 is still [], so nothing can admit a bounded turn today — but it is the one path left where the fence outlives its sweep.

Smaller, take it or leave it: @batch_size 100 and _unsafe_retire_unresolved/2's limit in 1..100 still sit exactly on each other's boundary with nothing saying so.

Approving.

jhgaylor added a commit that referenced this pull request Sep 11, 2026
…s door

Rebase of #1751 onto the re-cut stack, plus its two review notes.

**The red CI was staleness and the rebase cleared it.** `hex-audit-gate.exs`
was failing on the `decimal` advisory `EEF-CVE-2026-32686`, acknowledged on
main in #1753 sixty commits after this branch forked. It exits 0 here. The
remaining line is advisory: #1746's dependency bump moved the locked Decimal
artifact, so the acknowledgment no longer matches anything and could be
removed — that is main's call, not this stack's.

**The evidence file no longer reads as if the dead-end were the design.**
`turn-parent-fences.json` said unknown spawn recovery "correctly returns
failed with awaiting_identity". True as an immediate outcome, and the
expectation was right to correct — but stated alone it reads as the final
state, which was the thing I could not find a way out of when reviewing #1744.
It now says `awaiting_identity` is an obligation with an age and names the
sweep that writes it off.

**`_unsafe_create_autonomous_turn/1` is dropped.** It admitted background work
through `_unsafe_create_turn/1`, which skips the sandbox binding #1764 added,
and called the `_unsafe_autonomous_turn/2` helper #1749's re-cut removed for
the same reason. Autonomous turns use the one admission path, so
`turn_parent_test.exs`'s "closed parents refuse user and background admission"
now exercises both doors as the same door.

**Two things this branch had that main's admission did not, now in it.** A
terminated or failed parent refuses a turn with `:not_running` — `attached?`
checks the machine, and a retired actor can still reach the conversation with
a queued prompt. And the parent goes `running` inside the admission
transaction, so a turn and the status explaining it commit together; a reader
could previously see a `running` turn under an `idle` parent for the width of
the launch, and a failed launch left the pair disagreeing. `update_all` rather
than `update_conversation/2`, because that one audits and an audit insert must
not run inside a transaction (ADR 0013).

Kept: the generation fencing that is the point of the PR —
`_unsafe_write_parent/3`, `_unsafe_recover_turn/2`,
`_unsafe_clear_idle_session/2`, and `session_plan/2` returning
`{:error, :execution_fenced}` so even the placeholder session write is tied to
the admitted turn.

Size pin 2739 -> 2733. Full core suite: 6 doctests, 5,080 tests, 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R9reKpmXUUJf4eUMULogie
Signed-off-by: Jake Gaylor <jhgaylor@gmail.com>
jhgaylor added a commit that referenced this pull request Sep 11, 2026
…s door

Rebase of #1751 onto the re-cut stack, plus its two review notes.

**The red CI was staleness and the rebase cleared it.** `hex-audit-gate.exs`
was failing on the `decimal` advisory `EEF-CVE-2026-32686`, acknowledged on
main in #1753 sixty commits after this branch forked. It exits 0 here. The
remaining line is advisory: #1746's dependency bump moved the locked Decimal
artifact, so the acknowledgment no longer matches anything and could be
removed — that is main's call, not this stack's.

**The evidence file no longer reads as if the dead-end were the design.**
`turn-parent-fences.json` said unknown spawn recovery "correctly returns
failed with awaiting_identity". True as an immediate outcome, and the
expectation was right to correct — but stated alone it reads as the final
state, which was the thing I could not find a way out of when reviewing #1744.
It now says `awaiting_identity` is an obligation with an age and names the
sweep that writes it off.

**`_unsafe_create_autonomous_turn/1` is dropped.** It admitted background work
through `_unsafe_create_turn/1`, which skips the sandbox binding #1764 added,
and called the `_unsafe_autonomous_turn/2` helper #1749's re-cut removed for
the same reason. Autonomous turns use the one admission path, so
`turn_parent_test.exs`'s "closed parents refuse user and background admission"
now exercises both doors as the same door.

**Two things this branch had that main's admission did not, now in it.** A
terminated or failed parent refuses a turn with `:not_running` — `attached?`
checks the machine, and a retired actor can still reach the conversation with
a queued prompt. And the parent goes `running` inside the admission
transaction, so a turn and the status explaining it commit together; a reader
could previously see a `running` turn under an `idle` parent for the width of
the launch, and a failed launch left the pair disagreeing. `update_all` rather
than `update_conversation/2`, because that one audits and an audit insert must
not run inside a transaction (ADR 0013).

Kept: the generation fencing that is the point of the PR —
`_unsafe_write_parent/3`, `_unsafe_recover_turn/2`,
`_unsafe_clear_idle_session/2`, and `session_plan/2` returning
`{:error, :execution_fenced}` so even the placeholder session write is tied to
the admitted turn.

Size pin 2739 -> 2733. Full core suite: 6 doctests, 5,080 tests, 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R9reKpmXUUJf4eUMULogie
Signed-off-by: Jake Gaylor <jhgaylor@gmail.com>
@jhgaylor
jhgaylor marked this pull request as ready for review September 11, 2026 10:23
@jhgaylor
jhgaylor force-pushed the feat/typed-execution-limits branch from d87a801 to a9865a1 Compare September 11, 2026 11:16
jhgaylor added a commit that referenced this pull request Sep 11, 2026
Review of #1746 found the coordinator manufacturing the state #1744's journal
could not leave, and running everywhere whether or not anyone asked for it.

**It is off unless an operator asked.** The child list defaulted to `true` with
only `config/test.exs` turning it off, so every prod and dev node polled
`turn_executions` twice a second forever, and nothing in `runtime.exs` read the
key — an operator could not stop it without a rebuild. It now defaults to off,
`runtime.exs` derives the default from whether `FOUNTAIN_EXECUTION_LIMITS` sets
a host ceiling, and `FOUNTAIN_EXECUTION_DEADLINE_WORKER` overrides either way.
The tick moved from 1s to 5s and is configurable: a deadline is absolute and
durable, so lateness costs precision, not safety. Both variables have rows in
`docs/configuration.md`, which `config_reference_test` requires.

**A failed termination is written off, not fenced forever.** `call_terminator/2`
rescues every failure to `{:error, :termination_unconfirmed}`, and
`_unsafe_ready_terminations/1` selects only `state == "ready"`, so nothing ever
looked at the row again. One slow `terminate_session` therefore fenced its
conversation and its shared home permanently, and took `reset_sandbox/2` with
it. The recovery tick now also calls `_unsafe_retire_unresolved/2`: past an hour
a row in `awaiting_identity` or `uncertain` retires to `stopped` with its
`last_error` intact.

A retry was the other candidate and is deliberately not what happens. One
persisted attempt authorizes exactly one provider write; re-arming a lost
attempt would replay an operation whose outcome is unknown, or require assuming
`terminate_session/3` is idempotent across a session that may already have been
replaced. Giving up on a cleanup Fountain cannot confirm is the smaller claim.
ADR 0046 records both the sweep and the rejected alternative.

**Smaller.** `@providers` listed four backends while `ExecutionTransport`
refuses everything but Sprites, so three entries were unreachable and the map
implied support that does not exist; it now says Sprites. `Enum.take/2` with a
negative count takes from the *end* of a list rather than returning `[]`, which
would start the wrong jobs — `max(_, 0)` so that `used <= @pool_size` staying
true is not load-bearing.

The dependency bump this needs is restored as its own commit below: the
re-cut of #1745 dropped it along with the rest of that branch, and
`Managoat.Sandbox.terminate_session/3` arrives in managoat_sandbox 0.3.0.
It stays a separate commit because it moves the ACP stack (acp 0.4.0,
runtimes 0.4.1, runner 0.2.2) and that is not the coordinator's change.

Full core suite on the bumped set: 6 doctests, 4,988 tests, 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R9reKpmXUUJf4eUMULogie
Signed-off-by: Jake Gaylor <jhgaylor@gmail.com>
@jhgaylor
jhgaylor force-pushed the feat/independent-execution-deadlines branch from 033eb63 to bcb9023 Compare September 11, 2026 11:16
jhgaylor added a commit that referenced this pull request Sep 11, 2026
…s door

Rebase of #1751 onto the re-cut stack, plus its two review notes.

**The red CI was staleness and the rebase cleared it.** `hex-audit-gate.exs`
was failing on the `decimal` advisory `EEF-CVE-2026-32686`, acknowledged on
main in #1753 sixty commits after this branch forked. It exits 0 here. The
remaining line is advisory: #1746's dependency bump moved the locked Decimal
artifact, so the acknowledgment no longer matches anything and could be
removed — that is main's call, not this stack's.

**The evidence file no longer reads as if the dead-end were the design.**
`turn-parent-fences.json` said unknown spawn recovery "correctly returns
failed with awaiting_identity". True as an immediate outcome, and the
expectation was right to correct — but stated alone it reads as the final
state, which was the thing I could not find a way out of when reviewing #1744.
It now says `awaiting_identity` is an obligation with an age and names the
sweep that writes it off.

**`_unsafe_create_autonomous_turn/1` is dropped.** It admitted background work
through `_unsafe_create_turn/1`, which skips the sandbox binding #1764 added,
and called the `_unsafe_autonomous_turn/2` helper #1749's re-cut removed for
the same reason. Autonomous turns use the one admission path, so
`turn_parent_test.exs`'s "closed parents refuse user and background admission"
now exercises both doors as the same door.

**Two things this branch had that main's admission did not, now in it.** A
terminated or failed parent refuses a turn with `:not_running` — `attached?`
checks the machine, and a retired actor can still reach the conversation with
a queued prompt. And the parent goes `running` inside the admission
transaction, so a turn and the status explaining it commit together; a reader
could previously see a `running` turn under an `idle` parent for the width of
the launch, and a failed launch left the pair disagreeing. `update_all` rather
than `update_conversation/2`, because that one audits and an audit insert must
not run inside a transaction (ADR 0013).

Kept: the generation fencing that is the point of the PR —
`_unsafe_write_parent/3`, `_unsafe_recover_turn/2`,
`_unsafe_clear_idle_session/2`, and `session_plan/2` returning
`{:error, :execution_fenced}` so even the placeholder session write is tied to
the admitted turn.

Size pin 2739 -> 2733. Full core suite: 6 doctests, 5,080 tests, 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R9reKpmXUUJf4eUMULogie
Signed-off-by: Jake Gaylor <jhgaylor@gmail.com>
@jhgaylor
jhgaylor force-pushed the feat/typed-execution-limits branch from a9865a1 to 70cb087 Compare September 11, 2026 23:59
jhgaylor added a commit that referenced this pull request Sep 12, 2026
Review of #1746 found the coordinator manufacturing the state #1744's journal
could not leave, and running everywhere whether or not anyone asked for it.

**It is off unless an operator asked.** The child list defaulted to `true` with
only `config/test.exs` turning it off, so every prod and dev node polled
`turn_executions` twice a second forever, and nothing in `runtime.exs` read the
key — an operator could not stop it without a rebuild. It now defaults to off,
`runtime.exs` derives the default from whether `FOUNTAIN_EXECUTION_LIMITS` sets
a host ceiling, and `FOUNTAIN_EXECUTION_DEADLINE_WORKER` overrides either way.
The tick moved from 1s to 5s and is configurable: a deadline is absolute and
durable, so lateness costs precision, not safety. Both variables have rows in
`docs/configuration.md`, which `config_reference_test` requires.

**A failed termination is written off, not fenced forever.** `call_terminator/2`
rescues every failure to `{:error, :termination_unconfirmed}`, and
`_unsafe_ready_terminations/1` selects only `state == "ready"`, so nothing ever
looked at the row again. One slow `terminate_session` therefore fenced its
conversation and its shared home permanently, and took `reset_sandbox/2` with
it. The recovery tick now also calls `_unsafe_retire_unresolved/2`: past an hour
a row in `awaiting_identity` or `uncertain` retires to `stopped` with its
`last_error` intact.

A retry was the other candidate and is deliberately not what happens. One
persisted attempt authorizes exactly one provider write; re-arming a lost
attempt would replay an operation whose outcome is unknown, or require assuming
`terminate_session/3` is idempotent across a session that may already have been
replaced. Giving up on a cleanup Fountain cannot confirm is the smaller claim.
ADR 0046 records both the sweep and the rejected alternative.

**Smaller.** `@providers` listed four backends while `ExecutionTransport`
refuses everything but Sprites, so three entries were unreachable and the map
implied support that does not exist; it now says Sprites. `Enum.take/2` with a
negative count takes from the *end* of a list rather than returning `[]`, which
would start the wrong jobs — `max(_, 0)` so that `used <= @pool_size` staying
true is not load-bearing.

The dependency bump this needs is restored as its own commit below: the
re-cut of #1745 dropped it along with the rest of that branch, and
`Managoat.Sandbox.terminate_session/3` arrives in managoat_sandbox 0.3.0.
It stays a separate commit because it moves the ACP stack (acp 0.4.0,
runtimes 0.4.1, runner 0.2.2) and that is not the coordinator's change.

Full core suite on the bumped set: 6 doctests, 4,988 tests, 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R9reKpmXUUJf4eUMULogie
Signed-off-by: Jake Gaylor <jhgaylor@gmail.com>
@jhgaylor
jhgaylor force-pushed the feat/independent-execution-deadlines branch from bcb9023 to 26affb4 Compare September 12, 2026 00:05
jhgaylor added a commit that referenced this pull request Sep 12, 2026
…s door

Rebase of #1751 onto the re-cut stack, plus its two review notes.

**The red CI was staleness and the rebase cleared it.** `hex-audit-gate.exs`
was failing on the `decimal` advisory `EEF-CVE-2026-32686`, acknowledged on
main in #1753 sixty commits after this branch forked. It exits 0 here. The
remaining line is advisory: #1746's dependency bump moved the locked Decimal
artifact, so the acknowledgment no longer matches anything and could be
removed — that is main's call, not this stack's.

**The evidence file no longer reads as if the dead-end were the design.**
`turn-parent-fences.json` said unknown spawn recovery "correctly returns
failed with awaiting_identity". True as an immediate outcome, and the
expectation was right to correct — but stated alone it reads as the final
state, which was the thing I could not find a way out of when reviewing #1744.
It now says `awaiting_identity` is an obligation with an age and names the
sweep that writes it off.

**`_unsafe_create_autonomous_turn/1` is dropped.** It admitted background work
through `_unsafe_create_turn/1`, which skips the sandbox binding #1764 added,
and called the `_unsafe_autonomous_turn/2` helper #1749's re-cut removed for
the same reason. Autonomous turns use the one admission path, so
`turn_parent_test.exs`'s "closed parents refuse user and background admission"
now exercises both doors as the same door.

**Two things this branch had that main's admission did not, now in it.** A
terminated or failed parent refuses a turn with `:not_running` — `attached?`
checks the machine, and a retired actor can still reach the conversation with
a queued prompt. And the parent goes `running` inside the admission
transaction, so a turn and the status explaining it commit together; a reader
could previously see a `running` turn under an `idle` parent for the width of
the launch, and a failed launch left the pair disagreeing. `update_all` rather
than `update_conversation/2`, because that one audits and an audit insert must
not run inside a transaction (ADR 0013).

Kept: the generation fencing that is the point of the PR —
`_unsafe_write_parent/3`, `_unsafe_recover_turn/2`,
`_unsafe_clear_idle_session/2`, and `session_plan/2` returning
`{:error, :execution_fenced}` so even the placeholder session write is tied to
the admitted turn.

Size pin 2739 -> 2733. Full core suite: 6 doctests, 5,080 tests, 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R9reKpmXUUJf4eUMULogie
Signed-off-by: Jake Gaylor <jhgaylor@gmail.com>
jhgaylor and others added 3 commits September 11, 2026 20:33
Signed-off-by: Jake Gaylor <jhgaylor@gmail.com>
Signed-off-by: Jake Gaylor <jhgaylor@gmail.com>
Review of #1746 found the coordinator manufacturing the state #1744's journal
could not leave, and running everywhere whether or not anyone asked for it.

**It is off unless an operator asked.** The child list defaulted to `true` with
only `config/test.exs` turning it off, so every prod and dev node polled
`turn_executions` twice a second forever, and nothing in `runtime.exs` read the
key — an operator could not stop it without a rebuild. It now defaults to off,
`runtime.exs` derives the default from whether `FOUNTAIN_EXECUTION_LIMITS` sets
a host ceiling, and `FOUNTAIN_EXECUTION_DEADLINE_WORKER` overrides either way.
The tick moved from 1s to 5s and is configurable: a deadline is absolute and
durable, so lateness costs precision, not safety. Both variables have rows in
`docs/configuration.md`, which `config_reference_test` requires.

**A failed termination is written off, not fenced forever.** `call_terminator/2`
rescues every failure to `{:error, :termination_unconfirmed}`, and
`_unsafe_ready_terminations/1` selects only `state == "ready"`, so nothing ever
looked at the row again. One slow `terminate_session` therefore fenced its
conversation and its shared home permanently, and took `reset_sandbox/2` with
it. The recovery tick now also calls `_unsafe_retire_unresolved/2`: past an hour
a row in `awaiting_identity` or `uncertain` retires to `stopped` with its
`last_error` intact.

A retry was the other candidate and is deliberately not what happens. One
persisted attempt authorizes exactly one provider write; re-arming a lost
attempt would replay an operation whose outcome is unknown, or require assuming
`terminate_session/3` is idempotent across a session that may already have been
replaced. Giving up on a cleanup Fountain cannot confirm is the smaller claim.
ADR 0046 records both the sweep and the rejected alternative.

**Smaller.** `@providers` listed four backends while `ExecutionTransport`
refuses everything but Sprites, so three entries were unreachable and the map
implied support that does not exist; it now says Sprites. `Enum.take/2` with a
negative count takes from the *end* of a list rather than returning `[]`, which
would start the wrong jobs — `max(_, 0)` so that `used <= @pool_size` staying
true is not load-bearing.

The dependency bump this needs is restored as its own commit below: the
re-cut of #1745 dropped it along with the rest of that branch, and
`Managoat.Sandbox.terminate_session/3` arrives in managoat_sandbox 0.3.0.
It stays a separate commit because it moves the ACP stack (acp 0.4.0,
runtimes 0.4.1, runner 0.2.2) and that is not the coordinator's change.

Full core suite on the bumped set: 6 doctests, 4,988 tests, 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R9reKpmXUUJf4eUMULogie
Signed-off-by: Jake Gaylor <jhgaylor@gmail.com>
@jhgaylor
jhgaylor force-pushed the feat/independent-execution-deadlines branch from 26affb4 to 9db8714 Compare September 12, 2026 00:33
jhgaylor added a commit that referenced this pull request Sep 12, 2026
…s door

Rebase of #1751 onto the re-cut stack, plus its two review notes.

**The red CI was staleness and the rebase cleared it.** `hex-audit-gate.exs`
was failing on the `decimal` advisory `EEF-CVE-2026-32686`, acknowledged on
main in #1753 sixty commits after this branch forked. It exits 0 here. The
remaining line is advisory: #1746's dependency bump moved the locked Decimal
artifact, so the acknowledgment no longer matches anything and could be
removed — that is main's call, not this stack's.

**The evidence file no longer reads as if the dead-end were the design.**
`turn-parent-fences.json` said unknown spawn recovery "correctly returns
failed with awaiting_identity". True as an immediate outcome, and the
expectation was right to correct — but stated alone it reads as the final
state, which was the thing I could not find a way out of when reviewing #1744.
It now says `awaiting_identity` is an obligation with an age and names the
sweep that writes it off.

**`_unsafe_create_autonomous_turn/1` is dropped.** It admitted background work
through `_unsafe_create_turn/1`, which skips the sandbox binding #1764 added,
and called the `_unsafe_autonomous_turn/2` helper #1749's re-cut removed for
the same reason. Autonomous turns use the one admission path, so
`turn_parent_test.exs`'s "closed parents refuse user and background admission"
now exercises both doors as the same door.

**Two things this branch had that main's admission did not, now in it.** A
terminated or failed parent refuses a turn with `:not_running` — `attached?`
checks the machine, and a retired actor can still reach the conversation with
a queued prompt. And the parent goes `running` inside the admission
transaction, so a turn and the status explaining it commit together; a reader
could previously see a `running` turn under an `idle` parent for the width of
the launch, and a failed launch left the pair disagreeing. `update_all` rather
than `update_conversation/2`, because that one audits and an audit insert must
not run inside a transaction (ADR 0013).

Kept: the generation fencing that is the point of the PR —
`_unsafe_write_parent/3`, `_unsafe_recover_turn/2`,
`_unsafe_clear_idle_session/2`, and `session_plan/2` returning
`{:error, :execution_fenced}` so even the placeholder session write is tied to
the admitted turn.

Size pin 2739 -> 2733. Full core suite: 6 doctests, 5,080 tests, 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R9reKpmXUUJf4eUMULogie
Signed-off-by: Jake Gaylor <jhgaylor@gmail.com>
@jhgaylor
jhgaylor changed the base branch from feat/typed-execution-limits to main September 12, 2026 00:34
jhgaylor added a commit that referenced this pull request Sep 12, 2026
…s door

Rebase of #1751 onto the re-cut stack, plus its two review notes.

**The red CI was staleness and the rebase cleared it.** `hex-audit-gate.exs`
was failing on the `decimal` advisory `EEF-CVE-2026-32686`, acknowledged on
main in #1753 sixty commits after this branch forked. It exits 0 here. The
remaining line is advisory: #1746's dependency bump moved the locked Decimal
artifact, so the acknowledgment no longer matches anything and could be
removed — that is main's call, not this stack's.

**The evidence file no longer reads as if the dead-end were the design.**
`turn-parent-fences.json` said unknown spawn recovery "correctly returns
failed with awaiting_identity". True as an immediate outcome, and the
expectation was right to correct — but stated alone it reads as the final
state, which was the thing I could not find a way out of when reviewing #1744.
It now says `awaiting_identity` is an obligation with an age and names the
sweep that writes it off.

**`_unsafe_create_autonomous_turn/1` is dropped.** It admitted background work
through `_unsafe_create_turn/1`, which skips the sandbox binding #1764 added,
and called the `_unsafe_autonomous_turn/2` helper #1749's re-cut removed for
the same reason. Autonomous turns use the one admission path, so
`turn_parent_test.exs`'s "closed parents refuse user and background admission"
now exercises both doors as the same door.

**Two things this branch had that main's admission did not, now in it.** A
terminated or failed parent refuses a turn with `:not_running` — `attached?`
checks the machine, and a retired actor can still reach the conversation with
a queued prompt. And the parent goes `running` inside the admission
transaction, so a turn and the status explaining it commit together; a reader
could previously see a `running` turn under an `idle` parent for the width of
the launch, and a failed launch left the pair disagreeing. `update_all` rather
than `update_conversation/2`, because that one audits and an audit insert must
not run inside a transaction (ADR 0013).

Kept: the generation fencing that is the point of the PR —
`_unsafe_write_parent/3`, `_unsafe_recover_turn/2`,
`_unsafe_clear_idle_session/2`, and `session_plan/2` returning
`{:error, :execution_fenced}` so even the placeholder session write is tied to
the admitted turn.

Size pin 2739 -> 2733. Full core suite: 6 doctests, 5,080 tests, 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R9reKpmXUUJf4eUMULogie
Signed-off-by: Jake Gaylor <jhgaylor@gmail.com>
jhgaylor added a commit that referenced this pull request Sep 12, 2026
…s door

Rebase of #1751 onto the re-cut stack, plus its two review notes.

**The red CI was staleness and the rebase cleared it.** `hex-audit-gate.exs`
was failing on the `decimal` advisory `EEF-CVE-2026-32686`, acknowledged on
main in #1753 sixty commits after this branch forked. It exits 0 here. The
remaining line is advisory: #1746's dependency bump moved the locked Decimal
artifact, so the acknowledgment no longer matches anything and could be
removed — that is main's call, not this stack's.

**The evidence file no longer reads as if the dead-end were the design.**
`turn-parent-fences.json` said unknown spawn recovery "correctly returns
failed with awaiting_identity". True as an immediate outcome, and the
expectation was right to correct — but stated alone it reads as the final
state, which was the thing I could not find a way out of when reviewing #1744.
It now says `awaiting_identity` is an obligation with an age and names the
sweep that writes it off.

**`_unsafe_create_autonomous_turn/1` is dropped.** It admitted background work
through `_unsafe_create_turn/1`, which skips the sandbox binding #1764 added,
and called the `_unsafe_autonomous_turn/2` helper #1749's re-cut removed for
the same reason. Autonomous turns use the one admission path, so
`turn_parent_test.exs`'s "closed parents refuse user and background admission"
now exercises both doors as the same door.

**Two things this branch had that main's admission did not, now in it.** A
terminated or failed parent refuses a turn with `:not_running` — `attached?`
checks the machine, and a retired actor can still reach the conversation with
a queued prompt. And the parent goes `running` inside the admission
transaction, so a turn and the status explaining it commit together; a reader
could previously see a `running` turn under an `idle` parent for the width of
the launch, and a failed launch left the pair disagreeing. `update_all` rather
than `update_conversation/2`, because that one audits and an audit insert must
not run inside a transaction (ADR 0013).

Kept: the generation fencing that is the point of the PR —
`_unsafe_write_parent/3`, `_unsafe_recover_turn/2`,
`_unsafe_clear_idle_session/2`, and `session_plan/2` returning
`{:error, :execution_fenced}` so even the placeholder session write is tied to
the admitted turn.

Size pin 2739 -> 2733. Full core suite: 6 doctests, 5,080 tests, 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R9reKpmXUUJf4eUMULogie
Signed-off-by: Jake Gaylor <jhgaylor@gmail.com>
@jhgaylor
jhgaylor enabled auto-merge September 12, 2026 00:49
@jhgaylor jhgaylor closed this Sep 12, 2026
auto-merge was automatically disabled September 12, 2026 00:56

Pull request was closed

@jhgaylor jhgaylor reopened this Sep 12, 2026
@jhgaylor
jhgaylor enabled auto-merge September 12, 2026 00:57
@jhgaylor
jhgaylor added this pull request to the merge queue Sep 12, 2026
Merged via the queue into main with commit 17b8e54 Sep 12, 2026
26 checks passed
jhgaylor added a commit that referenced this pull request Sep 12, 2026
…s door

Rebase of #1751 onto the re-cut stack, plus its two review notes.

**The red CI was staleness and the rebase cleared it.** `hex-audit-gate.exs`
was failing on the `decimal` advisory `EEF-CVE-2026-32686`, acknowledged on
main in #1753 sixty commits after this branch forked. It exits 0 here. The
remaining line is advisory: #1746's dependency bump moved the locked Decimal
artifact, so the acknowledgment no longer matches anything and could be
removed — that is main's call, not this stack's.

**The evidence file no longer reads as if the dead-end were the design.**
`turn-parent-fences.json` said unknown spawn recovery "correctly returns
failed with awaiting_identity". True as an immediate outcome, and the
expectation was right to correct — but stated alone it reads as the final
state, which was the thing I could not find a way out of when reviewing #1744.
It now says `awaiting_identity` is an obligation with an age and names the
sweep that writes it off.

**`_unsafe_create_autonomous_turn/1` is dropped.** It admitted background work
through `_unsafe_create_turn/1`, which skips the sandbox binding #1764 added,
and called the `_unsafe_autonomous_turn/2` helper #1749's re-cut removed for
the same reason. Autonomous turns use the one admission path, so
`turn_parent_test.exs`'s "closed parents refuse user and background admission"
now exercises both doors as the same door.

**Two things this branch had that main's admission did not, now in it.** A
terminated or failed parent refuses a turn with `:not_running` — `attached?`
checks the machine, and a retired actor can still reach the conversation with
a queued prompt. And the parent goes `running` inside the admission
transaction, so a turn and the status explaining it commit together; a reader
could previously see a `running` turn under an `idle` parent for the width of
the launch, and a failed launch left the pair disagreeing. `update_all` rather
than `update_conversation/2`, because that one audits and an audit insert must
not run inside a transaction (ADR 0013).

Kept: the generation fencing that is the point of the PR —
`_unsafe_write_parent/3`, `_unsafe_recover_turn/2`,
`_unsafe_clear_idle_session/2`, and `session_plan/2` returning
`{:error, :execution_fenced}` so even the placeholder session write is tied to
the admitted turn.

Size pin 2739 -> 2733. Full core suite: 6 doctests, 5,080 tests, 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R9reKpmXUUJf4eUMULogie
Signed-off-by: Jake Gaylor <jhgaylor@gmail.com>
jhgaylor added a commit that referenced this pull request Sep 12, 2026
…s door

Rebase of #1751 onto the re-cut stack, plus its two review notes.

**The red CI was staleness and the rebase cleared it.** `hex-audit-gate.exs`
was failing on the `decimal` advisory `EEF-CVE-2026-32686`, acknowledged on
main in #1753 sixty commits after this branch forked. It exits 0 here. The
remaining line is advisory: #1746's dependency bump moved the locked Decimal
artifact, so the acknowledgment no longer matches anything and could be
removed — that is main's call, not this stack's.

**The evidence file no longer reads as if the dead-end were the design.**
`turn-parent-fences.json` said unknown spawn recovery "correctly returns
failed with awaiting_identity". True as an immediate outcome, and the
expectation was right to correct — but stated alone it reads as the final
state, which was the thing I could not find a way out of when reviewing #1744.
It now says `awaiting_identity` is an obligation with an age and names the
sweep that writes it off.

**`_unsafe_create_autonomous_turn/1` is dropped.** It admitted background work
through `_unsafe_create_turn/1`, which skips the sandbox binding #1764 added,
and called the `_unsafe_autonomous_turn/2` helper #1749's re-cut removed for
the same reason. Autonomous turns use the one admission path, so
`turn_parent_test.exs`'s "closed parents refuse user and background admission"
now exercises both doors as the same door.

**Two things this branch had that main's admission did not, now in it.** A
terminated or failed parent refuses a turn with `:not_running` — `attached?`
checks the machine, and a retired actor can still reach the conversation with
a queued prompt. And the parent goes `running` inside the admission
transaction, so a turn and the status explaining it commit together; a reader
could previously see a `running` turn under an `idle` parent for the width of
the launch, and a failed launch left the pair disagreeing. `update_all` rather
than `update_conversation/2`, because that one audits and an audit insert must
not run inside a transaction (ADR 0013).

Kept: the generation fencing that is the point of the PR —
`_unsafe_write_parent/3`, `_unsafe_recover_turn/2`,
`_unsafe_clear_idle_session/2`, and `session_plan/2` returning
`{:error, :execution_fenced}` so even the placeholder session write is tied to
the admitted turn.

Size pin 2739 -> 2733. Full core suite: 6 doctests, 5,080 tests, 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R9reKpmXUUJf4eUMULogie
Signed-off-by: Jake Gaylor <jhgaylor@gmail.com>
jhgaylor added a commit that referenced this pull request Sep 12, 2026
…s door

Rebase of #1751 onto the re-cut stack, plus its two review notes.

**The red CI was staleness and the rebase cleared it.** `hex-audit-gate.exs`
was failing on the `decimal` advisory `EEF-CVE-2026-32686`, acknowledged on
main in #1753 sixty commits after this branch forked. It exits 0 here. The
remaining line is advisory: #1746's dependency bump moved the locked Decimal
artifact, so the acknowledgment no longer matches anything and could be
removed — that is main's call, not this stack's.

**The evidence file no longer reads as if the dead-end were the design.**
`turn-parent-fences.json` said unknown spawn recovery "correctly returns
failed with awaiting_identity". True as an immediate outcome, and the
expectation was right to correct — but stated alone it reads as the final
state, which was the thing I could not find a way out of when reviewing #1744.
It now says `awaiting_identity` is an obligation with an age and names the
sweep that writes it off.

**`_unsafe_create_autonomous_turn/1` is dropped.** It admitted background work
through `_unsafe_create_turn/1`, which skips the sandbox binding #1764 added,
and called the `_unsafe_autonomous_turn/2` helper #1749's re-cut removed for
the same reason. Autonomous turns use the one admission path, so
`turn_parent_test.exs`'s "closed parents refuse user and background admission"
now exercises both doors as the same door.

**Two things this branch had that main's admission did not, now in it.** A
terminated or failed parent refuses a turn with `:not_running` — `attached?`
checks the machine, and a retired actor can still reach the conversation with
a queued prompt. And the parent goes `running` inside the admission
transaction, so a turn and the status explaining it commit together; a reader
could previously see a `running` turn under an `idle` parent for the width of
the launch, and a failed launch left the pair disagreeing. `update_all` rather
than `update_conversation/2`, because that one audits and an audit insert must
not run inside a transaction (ADR 0013).

Kept: the generation fencing that is the point of the PR —
`_unsafe_write_parent/3`, `_unsafe_recover_turn/2`,
`_unsafe_clear_idle_session/2`, and `session_plan/2` returning
`{:error, :execution_fenced}` so even the placeholder session write is tied to
the admitted turn.

Size pin 2739 -> 2733. Full core suite: 6 doctests, 5,080 tests, 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R9reKpmXUUJf4eUMULogie
Signed-off-by: Jake Gaylor <jhgaylor@gmail.com>
jhgaylor added a commit that referenced this pull request Sep 12, 2026
…s door

Rebase of #1751 onto the re-cut stack, plus its two review notes.

**The red CI was staleness and the rebase cleared it.** `hex-audit-gate.exs`
was failing on the `decimal` advisory `EEF-CVE-2026-32686`, acknowledged on
main in #1753 sixty commits after this branch forked. It exits 0 here. The
remaining line is advisory: #1746's dependency bump moved the locked Decimal
artifact, so the acknowledgment no longer matches anything and could be
removed — that is main's call, not this stack's.

**The evidence file no longer reads as if the dead-end were the design.**
`turn-parent-fences.json` said unknown spawn recovery "correctly returns
failed with awaiting_identity". True as an immediate outcome, and the
expectation was right to correct — but stated alone it reads as the final
state, which was the thing I could not find a way out of when reviewing #1744.
It now says `awaiting_identity` is an obligation with an age and names the
sweep that writes it off.

**`_unsafe_create_autonomous_turn/1` is dropped.** It admitted background work
through `_unsafe_create_turn/1`, which skips the sandbox binding #1764 added,
and called the `_unsafe_autonomous_turn/2` helper #1749's re-cut removed for
the same reason. Autonomous turns use the one admission path, so
`turn_parent_test.exs`'s "closed parents refuse user and background admission"
now exercises both doors as the same door.

**Two things this branch had that main's admission did not, now in it.** A
terminated or failed parent refuses a turn with `:not_running` — `attached?`
checks the machine, and a retired actor can still reach the conversation with
a queued prompt. And the parent goes `running` inside the admission
transaction, so a turn and the status explaining it commit together; a reader
could previously see a `running` turn under an `idle` parent for the width of
the launch, and a failed launch left the pair disagreeing. `update_all` rather
than `update_conversation/2`, because that one audits and an audit insert must
not run inside a transaction (ADR 0013).

Kept: the generation fencing that is the point of the PR —
`_unsafe_write_parent/3`, `_unsafe_recover_turn/2`,
`_unsafe_clear_idle_session/2`, and `session_plan/2` returning
`{:error, :execution_fenced}` so even the placeholder session write is tied to
the admitted turn.

Size pin 2739 -> 2733. Full core suite: 6 doctests, 5,080 tests, 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R9reKpmXUUJf4eUMULogie
Signed-off-by: Jake Gaylor <jhgaylor@gmail.com>
jhgaylor added a commit that referenced this pull request Sep 12, 2026
…s door

Rebase of #1751 onto the re-cut stack, plus its two review notes.

**The red CI was staleness and the rebase cleared it.** `hex-audit-gate.exs`
was failing on the `decimal` advisory `EEF-CVE-2026-32686`, acknowledged on
main in #1753 sixty commits after this branch forked. It exits 0 here. The
remaining line is advisory: #1746's dependency bump moved the locked Decimal
artifact, so the acknowledgment no longer matches anything and could be
removed — that is main's call, not this stack's.

**The evidence file no longer reads as if the dead-end were the design.**
`turn-parent-fences.json` said unknown spawn recovery "correctly returns
failed with awaiting_identity". True as an immediate outcome, and the
expectation was right to correct — but stated alone it reads as the final
state, which was the thing I could not find a way out of when reviewing #1744.
It now says `awaiting_identity` is an obligation with an age and names the
sweep that writes it off.

**`_unsafe_create_autonomous_turn/1` is dropped.** It admitted background work
through `_unsafe_create_turn/1`, which skips the sandbox binding #1764 added,
and called the `_unsafe_autonomous_turn/2` helper #1749's re-cut removed for
the same reason. Autonomous turns use the one admission path, so
`turn_parent_test.exs`'s "closed parents refuse user and background admission"
now exercises both doors as the same door.

**Two things this branch had that main's admission did not, now in it.** A
terminated or failed parent refuses a turn with `:not_running` — `attached?`
checks the machine, and a retired actor can still reach the conversation with
a queued prompt. And the parent goes `running` inside the admission
transaction, so a turn and the status explaining it commit together; a reader
could previously see a `running` turn under an `idle` parent for the width of
the launch, and a failed launch left the pair disagreeing. `update_all` rather
than `update_conversation/2`, because that one audits and an audit insert must
not run inside a transaction (ADR 0013).

Kept: the generation fencing that is the point of the PR —
`_unsafe_write_parent/3`, `_unsafe_recover_turn/2`,
`_unsafe_clear_idle_session/2`, and `session_plan/2` returning
`{:error, :execution_fenced}` so even the placeholder session write is tied to
the admitted turn.

Size pin 2739 -> 2733. Full core suite: 6 doctests, 5,080 tests, 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R9reKpmXUUJf4eUMULogie
Signed-off-by: Jake Gaylor <jhgaylor@gmail.com>
jhgaylor added a commit that referenced this pull request Sep 12, 2026
…s door

Rebase of #1751 onto the re-cut stack, plus its two review notes.

**The red CI was staleness and the rebase cleared it.** `hex-audit-gate.exs`
was failing on the `decimal` advisory `EEF-CVE-2026-32686`, acknowledged on
main in #1753 sixty commits after this branch forked. It exits 0 here. The
remaining line is advisory: #1746's dependency bump moved the locked Decimal
artifact, so the acknowledgment no longer matches anything and could be
removed — that is main's call, not this stack's.

**The evidence file no longer reads as if the dead-end were the design.**
`turn-parent-fences.json` said unknown spawn recovery "correctly returns
failed with awaiting_identity". True as an immediate outcome, and the
expectation was right to correct — but stated alone it reads as the final
state, which was the thing I could not find a way out of when reviewing #1744.
It now says `awaiting_identity` is an obligation with an age and names the
sweep that writes it off.

**`_unsafe_create_autonomous_turn/1` is dropped.** It admitted background work
through `_unsafe_create_turn/1`, which skips the sandbox binding #1764 added,
and called the `_unsafe_autonomous_turn/2` helper #1749's re-cut removed for
the same reason. Autonomous turns use the one admission path, so
`turn_parent_test.exs`'s "closed parents refuse user and background admission"
now exercises both doors as the same door.

**Two things this branch had that main's admission did not, now in it.** A
terminated or failed parent refuses a turn with `:not_running` — `attached?`
checks the machine, and a retired actor can still reach the conversation with
a queued prompt. And the parent goes `running` inside the admission
transaction, so a turn and the status explaining it commit together; a reader
could previously see a `running` turn under an `idle` parent for the width of
the launch, and a failed launch left the pair disagreeing. `update_all` rather
than `update_conversation/2`, because that one audits and an audit insert must
not run inside a transaction (ADR 0013).

Kept: the generation fencing that is the point of the PR —
`_unsafe_write_parent/3`, `_unsafe_recover_turn/2`,
`_unsafe_clear_idle_session/2`, and `session_plan/2` returning
`{:error, :execution_fenced}` so even the placeholder session write is tied to
the admitted turn.

Size pin 2739 -> 2733. Full core suite: 6 doctests, 5,080 tests, 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R9reKpmXUUJf4eUMULogie
Signed-off-by: Jake Gaylor <jhgaylor@gmail.com>
jhgaylor added a commit that referenced this pull request Sep 12, 2026
…s door

Rebase of #1751 onto the re-cut stack, plus its two review notes.

**The red CI was staleness and the rebase cleared it.** `hex-audit-gate.exs`
was failing on the `decimal` advisory `EEF-CVE-2026-32686`, acknowledged on
main in #1753 sixty commits after this branch forked. It exits 0 here. The
remaining line is advisory: #1746's dependency bump moved the locked Decimal
artifact, so the acknowledgment no longer matches anything and could be
removed — that is main's call, not this stack's.

**The evidence file no longer reads as if the dead-end were the design.**
`turn-parent-fences.json` said unknown spawn recovery "correctly returns
failed with awaiting_identity". True as an immediate outcome, and the
expectation was right to correct — but stated alone it reads as the final
state, which was the thing I could not find a way out of when reviewing #1744.
It now says `awaiting_identity` is an obligation with an age and names the
sweep that writes it off.

**`_unsafe_create_autonomous_turn/1` is dropped.** It admitted background work
through `_unsafe_create_turn/1`, which skips the sandbox binding #1764 added,
and called the `_unsafe_autonomous_turn/2` helper #1749's re-cut removed for
the same reason. Autonomous turns use the one admission path, so
`turn_parent_test.exs`'s "closed parents refuse user and background admission"
now exercises both doors as the same door.

**Two things this branch had that main's admission did not, now in it.** A
terminated or failed parent refuses a turn with `:not_running` — `attached?`
checks the machine, and a retired actor can still reach the conversation with
a queued prompt. And the parent goes `running` inside the admission
transaction, so a turn and the status explaining it commit together; a reader
could previously see a `running` turn under an `idle` parent for the width of
the launch, and a failed launch left the pair disagreeing. `update_all` rather
than `update_conversation/2`, because that one audits and an audit insert must
not run inside a transaction (ADR 0013).

Kept: the generation fencing that is the point of the PR —
`_unsafe_write_parent/3`, `_unsafe_recover_turn/2`,
`_unsafe_clear_idle_session/2`, and `session_plan/2` returning
`{:error, :execution_fenced}` so even the placeholder session write is tied to
the admitted turn.

Size pin 2739 -> 2733. Full core suite: 6 doctests, 5,080 tests, 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R9reKpmXUUJf4eUMULogie
Signed-off-by: Jake Gaylor <jhgaylor@gmail.com>
jhgaylor added a commit that referenced this pull request Sep 12, 2026
…s door

Rebase of #1751 onto the re-cut stack, plus its two review notes.

**The red CI was staleness and the rebase cleared it.** `hex-audit-gate.exs`
was failing on the `decimal` advisory `EEF-CVE-2026-32686`, acknowledged on
main in #1753 sixty commits after this branch forked. It exits 0 here. The
remaining line is advisory: #1746's dependency bump moved the locked Decimal
artifact, so the acknowledgment no longer matches anything and could be
removed — that is main's call, not this stack's.

**The evidence file no longer reads as if the dead-end were the design.**
`turn-parent-fences.json` said unknown spawn recovery "correctly returns
failed with awaiting_identity". True as an immediate outcome, and the
expectation was right to correct — but stated alone it reads as the final
state, which was the thing I could not find a way out of when reviewing #1744.
It now says `awaiting_identity` is an obligation with an age and names the
sweep that writes it off.

**`_unsafe_create_autonomous_turn/1` is dropped.** It admitted background work
through `_unsafe_create_turn/1`, which skips the sandbox binding #1764 added,
and called the `_unsafe_autonomous_turn/2` helper #1749's re-cut removed for
the same reason. Autonomous turns use the one admission path, so
`turn_parent_test.exs`'s "closed parents refuse user and background admission"
now exercises both doors as the same door.

**Two things this branch had that main's admission did not, now in it.** A
terminated or failed parent refuses a turn with `:not_running` — `attached?`
checks the machine, and a retired actor can still reach the conversation with
a queued prompt. And the parent goes `running` inside the admission
transaction, so a turn and the status explaining it commit together; a reader
could previously see a `running` turn under an `idle` parent for the width of
the launch, and a failed launch left the pair disagreeing. `update_all` rather
than `update_conversation/2`, because that one audits and an audit insert must
not run inside a transaction (ADR 0013).

Kept: the generation fencing that is the point of the PR —
`_unsafe_write_parent/3`, `_unsafe_recover_turn/2`,
`_unsafe_clear_idle_session/2`, and `session_plan/2` returning
`{:error, :execution_fenced}` so even the placeholder session write is tied to
the admitted turn.

Size pin 2739 -> 2733. Full core suite: 6 doctests, 5,080 tests, 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R9reKpmXUUJf4eUMULogie
Signed-off-by: Jake Gaylor <jhgaylor@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release:skip-sdk Explicitly allows an SDK surface change without releasing a new package version.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants