Skip to content

refactor(conversations): Launch, part b: fresh start_conversation, the reservation and one child spec builder - #2243

Merged
BinaryBourbon merged 1 commit into
mainfrom
stack/2175-7b-launch-fresh
Sep 15, 2026
Merged

BinaryBourbon merged 1 commit into
mainfrom
stack/2175-7b-launch-fresh

Conversation

@jhgaylor

Copy link
Copy Markdown
Collaborator

Stage 7b of #2175 (one owner per conversation lifecycle verb). Depends on 7a
(#2225, merged) and stage 2 (#2238, merged), which already gave Wake the
sandbox probe and start_conversation_server/4. Re-anchored against
origin/main at 15dd3550, well past 4224258d: stages 1, 2, 3b, 6 and 7a
of #2175 had all landed by then, so several helpers this stage needs were
already opened as @doc false doors (with_sandbox_lock, resolve_vault_id,
resolve_environment_id, resolve_inference_credential_id,
resolve_permission_policy, resolve_sandbox_provider,
broadcast_sidebar_update, check_execution_limits,
resolve_saved_inference). Stage 3 (#2211) has not landed yet, so
require_provider_commit_boundary/0 and mint_machine_name/3 were still
private; opened them here with the same @doc false shape stage 3 will use,
so that merge is a no-op collision at worst.

What moved (conversations.ex 5,277 → 4,972; launch.ex 352 → 783)

Into Fountain.Conversations.Launch, cut with their comments, docstrings and
lock-order notes:

  • start_conversation/2 (all three clauses, with its doc) — the sandbox_id
    attach clause, the fresh clause, and the {:home, sandbox} /
    unique-index-race recovery in its else.
  • fail_initial_start/2 and its sole helper pending_initial_binding?/4
    the delayed-start compensation, machine → parent → sandbox lock order kept
    exactly.
  • reserve_initial_conversation/4 — the tenant FOR SHARE waits, the
    channel-rotation unbind, and the with_sandbox_reservation/3-nested insert
    of sandbox + conversation + allowance. InferenceBinding.reserve/2 (via
    reserve_inference/1, now a door) still runs inside this same transaction.
  • home_or_new/5 (both clauses).
  • resolve_admission_inference/5 — made def (not just moved) because
    attach_conversation, which stays in Conversations until 7c, calls it
    too; it's the one function stage 7c will need a door for in reverse once
    attach_conversation moves.

Calls that now cross the module boundary gained a Conversations. prefix
(e.g. Conversations.require_provider_commit_boundary()); calls that
stopped crossing it (Launch.unbind_rotated_channel/2,
Launch.restore_rotated_channel/2, which the moved code used to reach from
conversations.ex) lost theirs, since reserve_initial_conversation and the
fresh clause now live beside them.

One child spec builder

Launch.child_spec/4 (three required args, one optional extra keyword —
still callable as child_spec/3) replaces the Horde child spec literal that
was written three times:

  • start_conversation's fresh clause (moved here) now calls it directly.
  • Wake.start_conversation_server/4 (conversations/wake.ex) calls
    Launch.child_spec/3.
  • Conversations.Rehydrator's boot sweep (conversations/rehydrator.ex)
    calls Launch.child_spec(conv.id, conv.sandbox_id, runtime_module, initial_prompt: nil) — the same initial_prompt: nil it always sent,
    now appended onto the shared three keys instead of duplicating them.

All three produce the identical term as before: {conversation_id:, sandbox_id:, runtime_module:} for the first two,
{conversation_id:, sandbox_id:, runtime_module:, initial_prompt: nil} for
the rehydrator. test/fountain/conversations/prompt_replay_test.exs pins the
first two shapes exactly (Enum.sort(Keyword.keys(spec_args())) == [:conversation_id, :runtime_module, :sandbox_id]), unedited and still
green.

What stayed in conversations.ex, and why

attach_conversation/3 stays until stage 7c. It went from defp to a
@doc false def (one comment naming #2217) because the moved
start_conversation's sandbox_id clause and its {:home, ...} /
unique-index-race branches call it — three call sites, all now
Conversations.attach_conversation/3.

Every other private helper the moved code depends on stayed put and got the
same @doc false door treatment already established by stages 7a/6/2
(comment naming #2217, defpdef, no logic touched):
resolve_sandbox_mode/2, resolve_sandbox_api_access/2,
check_sandbox_api_name/2, resolve_parent_id/2 (all their clauses),
resolve_admission_limits/2, insert_conversation_row/1,
after_conversation_created/1, record_execution_allowance_created/3,
get_root_conversation_id/1, broadcast_graph_update/1,
record_sandbox_usage/2 (all five clauses), stamp_terminated_at/1,
reserve_inference/1, require_provider_commit_boundary/0 and
mint_machine_name/3 (all five clauses, see the stage-3 note above).
check_sandbox_api_attach/2, create_attached_conversation/3 and
deliver_attach_prompt/3 are untouched — only attach_conversation reaches
them, and it didn't move.

Ownership comments

Four _unsafe_* calls became remote once their callers moved into Launch;
each carries a # ownership: ... comment within credo's ten-line lookback:
Agents._unsafe_current_version_id/1 (comment moved with the code, already
present), Conversations._unsafe_get_conversation!/1 (conv is the row
reserve_initial_conversation just created, same launch),
Conversations._unsafe_find_home/4 (twice — in home_or_new and in the
unique-index-race recovery — both note the scoped get_agent that ran
first), and Conversations._unsafe_running_turns_elsewhere/2 in
fail_initial_start (the FOR UPDATE re-read two lines above it). mix credo is clean.

Invariants carried

  • Audit (conversation.created) still fires after the insert transaction
    commits, never inside it; start_conversation stays in
    audit_guardrail_test.exs's @must_audit and the guardrail (which calls
    Conversations.start_conversation, now a delegate) still passes.
  • Billing.check_spend/1 moved once, with the fresh clause; none added.
  • The queue's @transient_errors vocabulary: untouched, nothing in this move
    renamed an error atom.
  • No Task.async. No server change — conversation_server.ex untouched.
  • The size pin (conversation_server_size_test.exs) is unaffected; this
    file isn't the server.

Delegate left

Conversations.start_conversation(attrs, opts \\ []), do: Launch.start_conversation(attrs, opts) for team.ex:389/:764,
team/schedules.ex:303, and the ~60 test call sites across
conversations_start_test.exs and friends — grepped the whole repo, no
other caller needed a second door.

Tests run unchanged, by explicit path from apps/fountain (357 total, 0 failures)

test/fountain/conversations_start_test.exs,
conversations/launch_commit_boundary_test.exs,
conversations/launch_credential_set_test.exs,
conversations/sandbox_mode_test.exs,
conversations/initial_start_failure_test.exs,
conversations/admission_lock_isolation_test.exs,
conversations/prompt_replay_test.exs, conversations/rehydrator_test.exs,
audit_guardrail_test.exs, team_test.exs,
fountain_web/controllers/conversation_controller_test.exs — run together,
357 tests, 0 failures. ../../ee/test/fountain/credits_enforcement_test.exs
run separately from apps/fountain: 11 tests, 0 failures.

mix format --check-formatted and mix credo clean on all four touched
files.

mise exec -- mix precommit --full from the worktree root, DATABASE_URL
set: PASSED (10 stages, 332s).

No behaviour change, no changelog fragment.

Closes #2217

🤖 Generated with Claude Code

@jhgaylor jhgaylor added stack:root The base PR of a stack; merge before its descendants. stack:2175 labels Sep 15, 2026
@jhgaylor
jhgaylor enabled auto-merge September 15, 2026 06:54

@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.

Verdict: APPROVED

Reviewed the complete diff at 34d45e3176cd9331f1a1686d594dd1529d6d450c against 15dd3550ea765615b85cc8a8eb0c9f1ec2b940f4. No actionable defects found.

Coverage: apps/fountain/lib/fountain/conversations.ex, apps/fountain/lib/fountain/conversations/launch.ex, apps/fountain/lib/fountain/conversations/wake.ex, and apps/fountain/lib/fountain/conversations/rehydrator.ex.

I challenged the extraction across tenant-scoped admission, inference selection, persistent-home fallback, transaction/lock ordering, failed-start compensation, audit/activation timing, public delegates, and prompt replay. The moved reservation, failure cleanup, home selection, and inference-resolution functions have identical parsed Elixir syntax trees after accounting for module qualification and visibility. Existing Launch functions are unchanged. Fresh launch retains its validation and side-effect sequence; all three child-spec call sites construct the same options as before, including the rehydrator's explicit initial_prompt: nil.

Validation: parsed all four files at both revisions with pinned Elixir 1.19.2; normalized function comparisons and git diff --check passed. Inspected the existing prompt-replay, launch commit-boundary, admission-lock isolation, rehydrator, and conversation-creation regression assertions. GitHub's required CI check and secret scan succeeded for this head. I did not run the application test suite locally (rl-env is unavailable); this is an independent source review, not a Review Loop verification attestation.

@jhgaylor
jhgaylor added this pull request to the merge queue Sep 15, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Sep 15, 2026
…e reservation and one child spec builder

Stage 7b of #2175 (one owner per conversation lifecycle verb).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Jake Gaylor <jhgaylor@gmail.com>
@jhgaylor
jhgaylor force-pushed the stack/2175-7b-launch-fresh branch from 34d45e3 to 4e6f2cf Compare September 15, 2026 07:12
@BinaryBourbon
BinaryBourbon added this pull request to the merge queue Sep 15, 2026
Merged via the queue into main with commit 72e753d Sep 15, 2026
28 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stack:root The base PR of a stack; merge before its descendants.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor(conversations): Launch, part b: fresh start_conversation, the reservation and one child spec builder

2 participants