Enforce journal deadlines independently of conversation actors - #1746
Conversation
BinaryBourbon
left a comment
There was a problem hiding this comment.
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: []
endThe 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/1returns[], 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.exskey regardless, so it can be turned off in production without a deploy. That also needs a row indocs/configuration.mdorconfig_reference_testwill 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}
endThat 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 submitted → uncertain, 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
@providersmaps all four providers, butExecutionTransportin #1748 refuses anything but"sprites"and #1749 rolls back:provider_not_supportedfor 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/3doesEnum.take(ids, @pool_size - used). It is safe today becauseusedcan 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 100against a guard oflimit in 1..100— fine, but exactly at the boundary. Worth a comment so nobody bumps one without the other.
7db7644 to
84cb45f
Compare
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>
5e6eb3e to
2462d42
Compare
|
Force-pushed It is off unless an operator asked for it. The child list defaulted to
Both variables have A failed termination is written off rather than fenced forever. This was the one I cared about: 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 Smaller items. One thing I had to restore. The re-cut of #1745 dropped Verification. Five new regressions. Reverting the age-out wiring fails 2; reverting the default-off fails 1. Moving on to #1747. |
…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>
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>
2462d42 to
033eb63
Compare
…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
left a comment
There was a problem hiding this comment.
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.
…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>
…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>
d87a801 to
a9865a1
Compare
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>
033eb63 to
bcb9023
Compare
…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>
a9865a1 to
70cb087
Compare
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>
bcb9023 to
26affb4
Compare
…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>
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>
26affb4 to
9db8714
Compare
…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>
…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>
…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>
Pull request was closed
…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>
…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>
…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>
…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>
…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>
…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>
…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>
…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>
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-releaseapplies.Part of #1732 — held. 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.