Skip to content

Refuse conversation release while remote execution is unresolved - #1752

Merged
jhgaylor merged 2 commits into
mainfrom
fix/bounded-release-fence
Sep 12, 2026
Merged

jhgaylor merged 2 commits into
mainfrom
fix/bounded-release-fence

Conversation

@jhgaylor

@jhgaylor jhgaylor commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Release could succeed with no actor even while its cancelled spawn remained unresolved. Both live and absent-actor paths now check persisted turns and execution journals under the parent lock before changing state or closing anything. Unresolved work returns busy without interruption; confirmed cleanup permits release. Unrelated co-tenants keep running on the unchanged sandbox.

Stacked on #1751; draft, public execution controls disabled. Tracks #1732. Machine transfer/park/reset/rehome/deletion, provider incarnation and live acceptance remain separate gates.

Full precommit passes 4,765 tests + 6 doctests, zero failures. The focused suite passes 103 tests, including ten new regressions and a real-actor refusal. Sixty independent PostgreSQL races cover release/admission, release/recovery and release/cleanup, plus twenty earlier deadline races. Provider identities and acknowledgments are synthetic; no provider operations occurred. Evidence: decisions/evidence/release-fence.json.

Known CI blocker inherited from #1751: the EEF feed for CVE-2026-32686 currently flags Decimal 3.1.1, while the maintainer advisory identifies 3.0.0 as patched. The dependency audit has not been waived.

CI at the exact release head finished: 18 checks pass, four skip, and the dependency audit plus required rollup fail. Test partitions, coverage, SDKs and release boot pass. The only root failure is EEF-CVE-2026-32686 on Decimal 3.1.1: the maintainer advisory identifies 3.0.0 as patched, but the current EEF feed omits that fixed boundary. The audit has not been waived.

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.

@jhgaylor jhgaylor left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Posted under the wrong account by mistake (this repo has two authenticated identities and the CLI reverted mid-run). The review itself is on this PR from @BinaryBourbon — read that one; this placeholder carries no findings.

@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 fence is right, but it takes away a release that used to always work

Commenting, with one thing I would like changed before this merges.

_unsafe_release_parent/2 is well built — lock the parent, check running turns and open executions under that lock, let the caller supply the writer, refuse without retiring or interrupting anything. Threading both the live-server and no-server paths through it so the same durable check applies either way is exactly right, and it fixes a real gap: the old handle_call(:release_conv, ...) dropped the connection and terminated the row before checking anything.

The regression: the previous no-server path was unconditional.

conv ->
  {:ok, _} = Conversations.update_conversation(conv, %{status: "terminated"})
  :ok

Now it refuses with :busy whenever a turn row says running — and a conversation with no server and a running turn is a normal state in this system, not a pathological one. wake_for_interrupt/1's own docstring says so: "The process can have exited (deploy, Horde rebalance, a plain {:stop, :normal, _}) while a turn was still marked running." Those are precisely the conversations an owner reaches for release on, and after this PR release is refused until something else reconciles the orphan. The docstring change ("{:error, :busy} while a turn runs or remote execution remains unresolved... The same durable check applies with no server alive") states the new behaviour accurately but does not flag that it is a change.

Layered on the open_execution?/1 half, and on the fact that uncertain and awaiting_identity never resolve (see #1744), release joins reset_sandbox/2 as a second escape hatch that can be closed permanently. I would rather see the running-turn check stay soft on the no-server path (wake and reconcile, then release, as wake_for_interrupt/1 already does), with the hard refusal reserved for the durable execution fence.

Smaller: notify_parent_change/1 correctly falls through on the error tuple, so the |> case do after it reads a little oddly — {:ok, _} -> :ok discards the conversation the broadcast just used. Not wrong, just worth a shape that makes the discard obvious.

Same rebase and the same red hex-audit-gate as #1751 — neither is this PR's doing.

@jhgaylor

Copy link
Copy Markdown
Collaborator Author

Force-pushed 2df81ab4. Rebased onto the re-cut #1751, and the regression fixed by splitting the two things that were conflated.

A running turn row now refuses release only when a live server makes it authoritative. _unsafe_release_parent/3 takes actor_alive?, and release_conversation/2 passes false on the branch where whereis/1 already returned nil. A conversation with no server and a running turn is a normal state — your own wake_for_interrupt/1 docstring says so — and it is exactly the state an owner reaches for release on.

An unresolved bounded execution refuses either way, as :execution_fenced rather than :busy. That one is a durable fact rather than an inference from a row, and it is bounded: the coordinator writes an obligation off once nothing can resolve it. Worth the distinct name on its own — :busy is a turn a live actor is running and ends by itself; :execution_fenced is remote work Fountain cannot yet account for. Those deserve different answers at the API.

I rewrote your test rather than deleting it. "an absent actor does not authorize release of a running legacy turn" is now "an orphaned running turn does not block release", with a comment saying why the opposite was wrong — that is a direct reversal of stated intent and it should be arguable from the test, not just from a PR comment. Two companions: a live actor's running turn still refuses :busy, and the durable fence refuses with and without an actor.

The notify_parent_change/1 shape you had is fine as-is — it already falls through on the error tuple, and I left it alone.

On the size pin. The docstring and comments this needed pushed conversation_server.ex to 2739 against #1751's 2733, and the pin only ratchets down. I trimmed the prose rather than raising it; it lands exactly at 2733. Worth knowing that this stack has now spent the room the TurnLaunch extraction bought, so the next thing added to that actor needs its own extraction.

Verification. 13 release/size tests pass. Full core suite: 6 doctests, 5,092 tests, 0 failures, 2 skipped. Format, credo --strict (7,434 mods/funs, no issues), okf validate decisions valid. ADR 0046 records why release refuses on a fact and not an inference.

That is the whole stack — #1744 through #1752 are all rebased onto each other in order and each has its own green suite. #1754 I have left alone; it is still labelled a draft reference and my review on it stands.

@jhgaylor
jhgaylor force-pushed the fix/bounded-parent-fences branch from 62969ec to 37e3965 Compare September 11, 2026 07:51
jhgaylor added a commit that referenced this pull request Sep 11, 2026
Review of #1752 found that the release fence took away a release that always
worked. The no-server path was unconditional before this branch; it now refused
`:busy` whenever a turn row said `running` — and a conversation with no server
and a `running` turn is a normal state, not a pathological one.
`wake_for_interrupt/1`'s own docstring says so: "the process can have exited
(deploy, Horde rebalance, a plain `{:stop, :normal, _}`) while a turn was still
marked `running`". Those are exactly the conversations an owner reaches for
release on, and refusing there fenced them out of their own recovery with
nothing able to un-fence it — the same shape as the permanent reset refusal on
#1744.

Split into the two things that were conflated:

  * a `running` turn row refuses only when a **live** server makes it
    authoritative. `_unsafe_release_parent/3` takes `actor_alive?`, and
    `release_conversation/2` passes `false` on the branch where `whereis/1`
    already returned nil.
  * an unresolved bounded execution refuses either way, as
    `:execution_fenced` rather than `:busy`. That is a durable fact rather
    than an inference from a row, and it is bounded: the coordinator writes an
    obligation off once nothing can resolve it.

The distinct name is worth having on its own. `:busy` means a turn a live actor
is running and ends by itself; `:execution_fenced` means remote work Fountain
cannot yet account for.

The test that asserted the old behaviour is rewritten rather than deleted, and
says why the opposite was wrong. Two companions: a live actor's running turn
still refuses, and the durable fence refuses with and without an actor.

Size pin held at 2733 by trimming the two comments this added rather than
raising it. Full core suite: 6 doctests, 5,092 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 fix/bounded-release-fence branch from 2df81ab to 30618f8 Compare September 11, 2026 07:51

@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: release is a fact again, and the two things it was conflating are now named separately

Re-reviewed at 30618f8. This is exactly the split I asked for.

_unsafe_release_parent/3 takes actor_alive?, release_conversation/2 passes false on the branch where whereis/1 already returned nil, and the durable fence stays unconditional under its own name. A running turn row is evidence only when something is there to run it; without a server it is as likely the orphan wake_for_interrupt/1 exists for, and that is the state an owner reaches for release in. The comment says so at the site, which is where the next person will be standing.

Rewriting the old test rather than deleting it, with the reason the opposite was wrong, is the right way to retire an assertion — and the two companions (a live actor's running turn still refuses; the durable fence refuses with and without an actor) pin both halves of the split instead of just the new one. :execution_fenced as a distinct atom from :busy is worth having on its own: one means a turn that ends by itself, the other means remote work Fountain cannot yet account for, and they call for different things from the caller.

One thing to pick up

:execution_fenced has no FallbackController clause. Team.rotate/4 returns it straight out of ConversationServer.release_conversation/2, and team_controller.ex passes anything that is not :busy through untouched — so it lands on the terminal safety net: 422 {"error": "execution_fenced"} with no message, plus a fallback: unmapped error atom warning logged on every refusal. Same for the copy of it that admission raises in #1749.

The shape is already #1431-compliant, so this is polish rather than a defect: one clause gives it a message worth reading ("a remote command from an earlier turn is still unaccounted for; it clears on its own") and stops the warning treating a designed refusal as a miss.

Still open from last time and still trivial: _unsafe_release_conversation/2's |> notify_parent_change() |> case do {:ok, _} -> :ok discards the conversation the broadcast just used, without the shape making the discard obvious.

Approving — checks are green on this head, and this one closes the last of the "a fence with no way out" family across the stack.

@jhgaylor
jhgaylor force-pushed the fix/bounded-parent-fences branch from 37e3965 to b5534e7 Compare September 11, 2026 09:35
jhgaylor added a commit that referenced this pull request Sep 11, 2026
Review of #1752 found that the release fence took away a release that always
worked. The no-server path was unconditional before this branch; it now refused
`:busy` whenever a turn row said `running` — and a conversation with no server
and a `running` turn is a normal state, not a pathological one.
`wake_for_interrupt/1`'s own docstring says so: "the process can have exited
(deploy, Horde rebalance, a plain `{:stop, :normal, _}`) while a turn was still
marked `running`". Those are exactly the conversations an owner reaches for
release on, and refusing there fenced them out of their own recovery with
nothing able to un-fence it — the same shape as the permanent reset refusal on
#1744.

Split into the two things that were conflated:

  * a `running` turn row refuses only when a **live** server makes it
    authoritative. `_unsafe_release_parent/3` takes `actor_alive?`, and
    `release_conversation/2` passes `false` on the branch where `whereis/1`
    already returned nil.
  * an unresolved bounded execution refuses either way, as
    `:execution_fenced` rather than `:busy`. That is a durable fact rather
    than an inference from a row, and it is bounded: the coordinator writes an
    obligation off once nothing can resolve it.

The distinct name is worth having on its own. `:busy` means a turn a live actor
is running and ends by itself; `:execution_fenced` means remote work Fountain
cannot yet account for.

The test that asserted the old behaviour is rewritten rather than deleted, and
says why the opposite was wrong. Two companions: a live actor's running turn
still refuses, and the durable fence refuses with and without an actor.

Size pin held at 2733 by trimming the two comments this added rather than
raising it. Full core suite: 6 doctests, 5,092 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 fix/bounded-release-fence branch from 30618f8 to 06345ed Compare September 11, 2026 09:35
@jhgaylor
jhgaylor force-pushed the fix/bounded-parent-fences branch from b5534e7 to cad1e44 Compare September 11, 2026 10:06
jhgaylor added a commit that referenced this pull request Sep 11, 2026
Review of #1752 found that the release fence took away a release that always
worked. The no-server path was unconditional before this branch; it now refused
`:busy` whenever a turn row said `running` — and a conversation with no server
and a `running` turn is a normal state, not a pathological one.
`wake_for_interrupt/1`'s own docstring says so: "the process can have exited
(deploy, Horde rebalance, a plain `{:stop, :normal, _}`) while a turn was still
marked `running`". Those are exactly the conversations an owner reaches for
release on, and refusing there fenced them out of their own recovery with
nothing able to un-fence it — the same shape as the permanent reset refusal on
#1744.

Split into the two things that were conflated:

  * a `running` turn row refuses only when a **live** server makes it
    authoritative. `_unsafe_release_parent/3` takes `actor_alive?`, and
    `release_conversation/2` passes `false` on the branch where `whereis/1`
    already returned nil.
  * an unresolved bounded execution refuses either way, as
    `:execution_fenced` rather than `:busy`. That is a durable fact rather
    than an inference from a row, and it is bounded: the coordinator writes an
    obligation off once nothing can resolve it.

The distinct name is worth having on its own. `:busy` means a turn a live actor
is running and ends by itself; `:execution_fenced` means remote work Fountain
cannot yet account for.

The test that asserted the old behaviour is rewritten rather than deleted, and
says why the opposite was wrong. Two companions: a live actor's running turn
still refuses, and the durable fence refuses with and without an actor.

Size pin held at 2733 by trimming the two comments this added rather than
raising it. Full core suite: 6 doctests, 5,092 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 fix/bounded-release-fence branch from 06345ed to e5581a5 Compare September 11, 2026 10:06
@jhgaylor
jhgaylor marked this pull request as ready for review September 11, 2026 10:23
@jhgaylor
jhgaylor force-pushed the fix/bounded-parent-fences branch from cad1e44 to 1e18e2f Compare September 11, 2026 11:17
jhgaylor added a commit that referenced this pull request Sep 11, 2026
Review of #1752 found that the release fence took away a release that always
worked. The no-server path was unconditional before this branch; it now refused
`:busy` whenever a turn row said `running` — and a conversation with no server
and a `running` turn is a normal state, not a pathological one.
`wake_for_interrupt/1`'s own docstring says so: "the process can have exited
(deploy, Horde rebalance, a plain `{:stop, :normal, _}`) while a turn was still
marked `running`". Those are exactly the conversations an owner reaches for
release on, and refusing there fenced them out of their own recovery with
nothing able to un-fence it — the same shape as the permanent reset refusal on
#1744.

Split into the two things that were conflated:

  * a `running` turn row refuses only when a **live** server makes it
    authoritative. `_unsafe_release_parent/3` takes `actor_alive?`, and
    `release_conversation/2` passes `false` on the branch where `whereis/1`
    already returned nil.
  * an unresolved bounded execution refuses either way, as
    `:execution_fenced` rather than `:busy`. That is a durable fact rather
    than an inference from a row, and it is bounded: the coordinator writes an
    obligation off once nothing can resolve it.

The distinct name is worth having on its own. `:busy` means a turn a live actor
is running and ends by itself; `:execution_fenced` means remote work Fountain
cannot yet account for.

The test that asserted the old behaviour is rewritten rather than deleted, and
says why the opposite was wrong. Two companions: a live actor's running turn
still refuses, and the durable fence refuses with and without an actor.

Size pin held at 2733 by trimming the two comments this added rather than
raising it. Full core suite: 6 doctests, 5,092 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 fix/bounded-release-fence branch from e5581a5 to b763ad0 Compare September 11, 2026 11:17
@jhgaylor
jhgaylor force-pushed the fix/bounded-parent-fences branch from 1e18e2f to a01e6c9 Compare September 12, 2026 00:06
jhgaylor added a commit that referenced this pull request Sep 12, 2026
Review of #1752 found that the release fence took away a release that always
worked. The no-server path was unconditional before this branch; it now refused
`:busy` whenever a turn row said `running` — and a conversation with no server
and a `running` turn is a normal state, not a pathological one.
`wake_for_interrupt/1`'s own docstring says so: "the process can have exited
(deploy, Horde rebalance, a plain `{:stop, :normal, _}`) while a turn was still
marked `running`". Those are exactly the conversations an owner reaches for
release on, and refusing there fenced them out of their own recovery with
nothing able to un-fence it — the same shape as the permanent reset refusal on
#1744.

Split into the two things that were conflated:

  * a `running` turn row refuses only when a **live** server makes it
    authoritative. `_unsafe_release_parent/3` takes `actor_alive?`, and
    `release_conversation/2` passes `false` on the branch where `whereis/1`
    already returned nil.
  * an unresolved bounded execution refuses either way, as
    `:execution_fenced` rather than `:busy`. That is a durable fact rather
    than an inference from a row, and it is bounded: the coordinator writes an
    obligation off once nothing can resolve it.

The distinct name is worth having on its own. `:busy` means a turn a live actor
is running and ends by itself; `:execution_fenced` means remote work Fountain
cannot yet account for.

The test that asserted the old behaviour is rewritten rather than deleted, and
says why the opposite was wrong. Two companions: a live actor's running turn
still refuses, and the durable fence refuses with and without an actor.

Size pin held at 2733 by trimming the two comments this added rather than
raising it. Full core suite: 6 doctests, 5,092 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 fix/bounded-release-fence branch from 99efa82 to 0a58be2 Compare September 12, 2026 02:00
@jhgaylor
jhgaylor force-pushed the fix/bounded-parent-fences branch from e861173 to 30fa08c Compare September 12, 2026 04:20
jhgaylor added a commit that referenced this pull request Sep 12, 2026
Review of #1752 found that the release fence took away a release that always
worked. The no-server path was unconditional before this branch; it now refused
`:busy` whenever a turn row said `running` — and a conversation with no server
and a `running` turn is a normal state, not a pathological one.
`wake_for_interrupt/1`'s own docstring says so: "the process can have exited
(deploy, Horde rebalance, a plain `{:stop, :normal, _}`) while a turn was still
marked `running`". Those are exactly the conversations an owner reaches for
release on, and refusing there fenced them out of their own recovery with
nothing able to un-fence it — the same shape as the permanent reset refusal on
#1744.

Split into the two things that were conflated:

  * a `running` turn row refuses only when a **live** server makes it
    authoritative. `_unsafe_release_parent/3` takes `actor_alive?`, and
    `release_conversation/2` passes `false` on the branch where `whereis/1`
    already returned nil.
  * an unresolved bounded execution refuses either way, as
    `:execution_fenced` rather than `:busy`. That is a durable fact rather
    than an inference from a row, and it is bounded: the coordinator writes an
    obligation off once nothing can resolve it.

The distinct name is worth having on its own. `:busy` means a turn a live actor
is running and ends by itself; `:execution_fenced` means remote work Fountain
cannot yet account for.

The test that asserted the old behaviour is rewritten rather than deleted, and
says why the opposite was wrong. Two companions: a live actor's running turn
still refuses, and the durable fence refuses with and without an actor.

Size pin held at 2733 by trimming the two comments this added rather than
raising it. Full core suite: 6 doctests, 5,092 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 fix/bounded-release-fence branch from 0a58be2 to e35d657 Compare September 12, 2026 04:20
@jhgaylor
jhgaylor force-pushed the fix/bounded-parent-fences branch from 30fa08c to 815f38f Compare September 12, 2026 04:37
jhgaylor added a commit that referenced this pull request Sep 12, 2026
Review of #1752 found that the release fence took away a release that always
worked. The no-server path was unconditional before this branch; it now refused
`:busy` whenever a turn row said `running` — and a conversation with no server
and a `running` turn is a normal state, not a pathological one.
`wake_for_interrupt/1`'s own docstring says so: "the process can have exited
(deploy, Horde rebalance, a plain `{:stop, :normal, _}`) while a turn was still
marked `running`". Those are exactly the conversations an owner reaches for
release on, and refusing there fenced them out of their own recovery with
nothing able to un-fence it — the same shape as the permanent reset refusal on
#1744.

Split into the two things that were conflated:

  * a `running` turn row refuses only when a **live** server makes it
    authoritative. `_unsafe_release_parent/3` takes `actor_alive?`, and
    `release_conversation/2` passes `false` on the branch where `whereis/1`
    already returned nil.
  * an unresolved bounded execution refuses either way, as
    `:execution_fenced` rather than `:busy`. That is a durable fact rather
    than an inference from a row, and it is bounded: the coordinator writes an
    obligation off once nothing can resolve it.

The distinct name is worth having on its own. `:busy` means a turn a live actor
is running and ends by itself; `:execution_fenced` means remote work Fountain
cannot yet account for.

The test that asserted the old behaviour is rewritten rather than deleted, and
says why the opposite was wrong. Two companions: a live actor's running turn
still refuses, and the durable fence refuses with and without an actor.

Size pin held at 2733 by trimming the two comments this added rather than
raising it. Full core suite: 6 doctests, 5,092 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 fix/bounded-release-fence branch from e35d657 to a244b67 Compare September 12, 2026 04:37
@jhgaylor
jhgaylor force-pushed the fix/bounded-parent-fences branch from 815f38f to 91b414a Compare September 12, 2026 04:38
jhgaylor added a commit that referenced this pull request Sep 12, 2026
Review of #1752 found that the release fence took away a release that always
worked. The no-server path was unconditional before this branch; it now refused
`:busy` whenever a turn row said `running` — and a conversation with no server
and a `running` turn is a normal state, not a pathological one.
`wake_for_interrupt/1`'s own docstring says so: "the process can have exited
(deploy, Horde rebalance, a plain `{:stop, :normal, _}`) while a turn was still
marked `running`". Those are exactly the conversations an owner reaches for
release on, and refusing there fenced them out of their own recovery with
nothing able to un-fence it — the same shape as the permanent reset refusal on
#1744.

Split into the two things that were conflated:

  * a `running` turn row refuses only when a **live** server makes it
    authoritative. `_unsafe_release_parent/3` takes `actor_alive?`, and
    `release_conversation/2` passes `false` on the branch where `whereis/1`
    already returned nil.
  * an unresolved bounded execution refuses either way, as
    `:execution_fenced` rather than `:busy`. That is a durable fact rather
    than an inference from a row, and it is bounded: the coordinator writes an
    obligation off once nothing can resolve it.

The distinct name is worth having on its own. `:busy` means a turn a live actor
is running and ends by itself; `:execution_fenced` means remote work Fountain
cannot yet account for.

The test that asserted the old behaviour is rewritten rather than deleted, and
says why the opposite was wrong. Two companions: a live actor's running turn
still refuses, and the durable fence refuses with and without an actor.

Size pin held at 2733 by trimming the two comments this added rather than
raising it. Full core suite: 6 doctests, 5,092 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 fix/bounded-release-fence branch from a244b67 to 2614dd9 Compare September 12, 2026 04:38
@jhgaylor
jhgaylor force-pushed the fix/bounded-parent-fences branch from 91b414a to 26931d4 Compare September 12, 2026 05:14
jhgaylor added a commit that referenced this pull request Sep 12, 2026
Review of #1752 found that the release fence took away a release that always
worked. The no-server path was unconditional before this branch; it now refused
`:busy` whenever a turn row said `running` — and a conversation with no server
and a `running` turn is a normal state, not a pathological one.
`wake_for_interrupt/1`'s own docstring says so: "the process can have exited
(deploy, Horde rebalance, a plain `{:stop, :normal, _}`) while a turn was still
marked `running`". Those are exactly the conversations an owner reaches for
release on, and refusing there fenced them out of their own recovery with
nothing able to un-fence it — the same shape as the permanent reset refusal on
#1744.

Split into the two things that were conflated:

  * a `running` turn row refuses only when a **live** server makes it
    authoritative. `_unsafe_release_parent/3` takes `actor_alive?`, and
    `release_conversation/2` passes `false` on the branch where `whereis/1`
    already returned nil.
  * an unresolved bounded execution refuses either way, as
    `:execution_fenced` rather than `:busy`. That is a durable fact rather
    than an inference from a row, and it is bounded: the coordinator writes an
    obligation off once nothing can resolve it.

The distinct name is worth having on its own. `:busy` means a turn a live actor
is running and ends by itself; `:execution_fenced` means remote work Fountain
cannot yet account for.

The test that asserted the old behaviour is rewritten rather than deleted, and
says why the opposite was wrong. Two companions: a live actor's running turn
still refuses, and the durable fence refuses with and without an actor.

Size pin held at 2733 by trimming the two comments this added rather than
raising it. Full core suite: 6 doctests, 5,092 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 fix/bounded-release-fence branch from 2614dd9 to 5abec69 Compare September 12, 2026 05:14
@jhgaylor
jhgaylor force-pushed the fix/bounded-parent-fences branch from 26931d4 to ca72e26 Compare September 12, 2026 05:32
jhgaylor added a commit that referenced this pull request Sep 12, 2026
Review of #1752 found that the release fence took away a release that always
worked. The no-server path was unconditional before this branch; it now refused
`:busy` whenever a turn row said `running` — and a conversation with no server
and a `running` turn is a normal state, not a pathological one.
`wake_for_interrupt/1`'s own docstring says so: "the process can have exited
(deploy, Horde rebalance, a plain `{:stop, :normal, _}`) while a turn was still
marked `running`". Those are exactly the conversations an owner reaches for
release on, and refusing there fenced them out of their own recovery with
nothing able to un-fence it — the same shape as the permanent reset refusal on
#1744.

Split into the two things that were conflated:

  * a `running` turn row refuses only when a **live** server makes it
    authoritative. `_unsafe_release_parent/3` takes `actor_alive?`, and
    `release_conversation/2` passes `false` on the branch where `whereis/1`
    already returned nil.
  * an unresolved bounded execution refuses either way, as
    `:execution_fenced` rather than `:busy`. That is a durable fact rather
    than an inference from a row, and it is bounded: the coordinator writes an
    obligation off once nothing can resolve it.

The distinct name is worth having on its own. `:busy` means a turn a live actor
is running and ends by itself; `:execution_fenced` means remote work Fountain
cannot yet account for.

The test that asserted the old behaviour is rewritten rather than deleted, and
says why the opposite was wrong. Two companions: a live actor's running turn
still refuses, and the durable fence refuses with and without an actor.

Size pin held at 2733 by trimming the two comments this added rather than
raising it. Full core suite: 6 doctests, 5,092 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 fix/bounded-release-fence branch from 5abec69 to 5bb64d4 Compare September 12, 2026 05:32
@jhgaylor
jhgaylor force-pushed the fix/bounded-parent-fences branch from ca72e26 to ec3453b Compare September 12, 2026 05:52
jhgaylor added a commit that referenced this pull request Sep 12, 2026
Review of #1752 found that the release fence took away a release that always
worked. The no-server path was unconditional before this branch; it now refused
`:busy` whenever a turn row said `running` — and a conversation with no server
and a `running` turn is a normal state, not a pathological one.
`wake_for_interrupt/1`'s own docstring says so: "the process can have exited
(deploy, Horde rebalance, a plain `{:stop, :normal, _}`) while a turn was still
marked `running`". Those are exactly the conversations an owner reaches for
release on, and refusing there fenced them out of their own recovery with
nothing able to un-fence it — the same shape as the permanent reset refusal on
#1744.

Split into the two things that were conflated:

  * a `running` turn row refuses only when a **live** server makes it
    authoritative. `_unsafe_release_parent/3` takes `actor_alive?`, and
    `release_conversation/2` passes `false` on the branch where `whereis/1`
    already returned nil.
  * an unresolved bounded execution refuses either way, as
    `:execution_fenced` rather than `:busy`. That is a durable fact rather
    than an inference from a row, and it is bounded: the coordinator writes an
    obligation off once nothing can resolve it.

The distinct name is worth having on its own. `:busy` means a turn a live actor
is running and ends by itself; `:execution_fenced` means remote work Fountain
cannot yet account for.

The test that asserted the old behaviour is rewritten rather than deleted, and
says why the opposite was wrong. Two companions: a live actor's running turn
still refuses, and the durable fence refuses with and without an actor.

Size pin held at 2733 by trimming the two comments this added rather than
raising it. Full core suite: 6 doctests, 5,092 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 fix/bounded-release-fence branch from 5bb64d4 to 44919bc Compare September 12, 2026 05:52
@BinaryBourbon
BinaryBourbon changed the base branch from fix/bounded-parent-fences to main September 12, 2026 06:08
jhgaylor and others added 2 commits September 12, 2026 02:08
Signed-off-by: Jake Gaylor <jhgaylor@gmail.com>
Review of #1752 found that the release fence took away a release that always
worked. The no-server path was unconditional before this branch; it now refused
`:busy` whenever a turn row said `running` — and a conversation with no server
and a `running` turn is a normal state, not a pathological one.
`wake_for_interrupt/1`'s own docstring says so: "the process can have exited
(deploy, Horde rebalance, a plain `{:stop, :normal, _}`) while a turn was still
marked `running`". Those are exactly the conversations an owner reaches for
release on, and refusing there fenced them out of their own recovery with
nothing able to un-fence it — the same shape as the permanent reset refusal on
#1744.

Split into the two things that were conflated:

  * a `running` turn row refuses only when a **live** server makes it
    authoritative. `_unsafe_release_parent/3` takes `actor_alive?`, and
    `release_conversation/2` passes `false` on the branch where `whereis/1`
    already returned nil.
  * an unresolved bounded execution refuses either way, as
    `:execution_fenced` rather than `:busy`. That is a durable fact rather
    than an inference from a row, and it is bounded: the coordinator writes an
    obligation off once nothing can resolve it.

The distinct name is worth having on its own. `:busy` means a turn a live actor
is running and ends by itself; `:execution_fenced` means remote work Fountain
cannot yet account for.

The test that asserted the old behaviour is rewritten rather than deleted, and
says why the opposite was wrong. Two companions: a live actor's running turn
still refuses, and the durable fence refuses with and without an actor.

Size pin held at 2733 by trimming the two comments this added rather than
raising it. Full core suite: 6 doctests, 5,092 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 fix/bounded-release-fence branch from 44919bc to 27ddae3 Compare September 12, 2026 06:09
@jhgaylor
jhgaylor added this pull request to the merge queue Sep 12, 2026
Merged via the queue into main with commit 653af87 Sep 12, 2026
24 checks passed
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