agy parity T7 — agy workers can't resume, be nudged, or auto-resume: implement splice_prompt/2 - #1966
Merged
Merged
Conversation
gemini/agy was the one provider Worker.inject_resume_argv/4 and
inject_nudge_argv/3 could not rewrite at all — no splice_prompt/2 meant
every resume attempt and gate nudge fell straight to :unsupported_provider
and parked, even though the underlying agy CLI supports `--conversation
<id>` for session resume.
splice_prompt/2 finds the "-p" prompt slot that default_argv/2 always
produces (both the agy and upstream-gemini branches share that shape) and:
- for a `["--resume", sid, prompt]` insert: on the agy branch, swaps in
the new prompt and inserts `--conversation <id>` right after it,
leaving --print-timeout/--model/--effort and everything else
untouched; on the upstream-gemini branch (no --conversation support)
returns {:error, :resume_unsupported} instead of a bogus invocation
- for a `[nudge]` insert: swaps only the prompt on either branch
- returns {:error, :no_print_slot} when there's no "-p" flag (fixtures)
Worker.inject_resume_argv/4 and inject_nudge_argv/3 already dynamically
dispatch to splice_prompt/2 via function_exported?/3, so no worker.ex
change was needed there beyond refreshing a stale comment. Updated the
respawn_provider_test.exs fixture test that asserted the old
:unsupported_provider park behavior for gemini.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…gy task's provider to Claude Post-merge verification of T7 found that both worker_resume (MCP tool, Dispatch.resume/2) and arb worker resume (Dispatch.resume_session/2) re-derived the provider from Routing.choose/2 on every resume with no memory of what the prior run actually used. An agy/gemini task's resume silently dispatched on Claude instead — spending the quota the operator routed to agy specifically to conserve, with no signal in the result that a provider switch happened. For resume_session/2 this was worse than a wrong choice of agent: a Claude spawn could receive an agy conversation UUID as --resume, a nonsensical invocation. Both functions now default :agent_type (unless the caller passes one explicitly) to the provider recorded on the task's most recent usage-ledger row, via a new Dispatch.latest_provider/1. A resume stays on the same provider by construction instead of by accident. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ty-t7-agy-workers-can # Conflicts: # apps/arbiter/test/arbiter/agents/gemini_test.exs
…er and session_id from different usage rows verification found the provider-preservation half of the earlier fix worked but conversation continuity did not: latest_provider/1 and latest_session_id/1 ran independent "newest row" queries, so a task whose most recent usage row recorded a different provider than the row holding the resumable session_id could pin :agent_type off one row while threading the OTHER row's conversation id into --conversation. latest_session_id/1 now returns the provider from the SAME row the session_id came from, and resume_session/2 pins :agent_type to that value instead of a separate lookup. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ty-t7-agy-workers-can Resolves conflicts in dispatch.ex/dispatch_test.exs: origin/main's resolve_resume_provider/2 (bd-2exkl0, PR #1926) generalized provider inheritance for resume/2 using the authoring run's provider with availability fallback + coordinator escalation. Keeps that for resume/2, and adds resolve_session_resume_provider/3 for resume_session/2 so the session-id/provider pairing fix from this branch's AC5 post-merge commits (04b8426, a8abf4d) survives: it still prefers the provider recorded on the exact usage-ledger row the session_id came from before falling back to the general authoring-provider resolution. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…e the regression test actually regress Finding 1: the mismatch regression test constructed the mismatch out of two usage-ledger rows, but the provider resolver reads worker_run rows first (Run.latest_authoring_provider/1) and only falls back to the usage ledger when no run carries a provider — so the fixture's extra usage row never influenced anything and the test passed even with the AC5 fix reverted. Rebuilt the mismatch as a newer failed Run row (what the resolver actually reads), stubbed claude on the test PATH so a future regression spawns the stub instead of the operator's real CLI, and corrected the comment's claim about which query the resolver uses. Verified: fails pre-fix (times out waiting for the agy argv file because dispatch goes to the claude stub instead), passes post-fix. Finding 2: resolve_session_resume_provider/3 can fall through to a DIFFERENT provider than the one that captured session_id (unknown/ unavailable session provider, or an explicit agent_type override), but resume_session/2 still threaded that foreign session_id straight into resume_opts regardless — producing a bogus invocation like `claude --resume <agy-conversation-uuid>` on the mismatched CLI. resume_opts now only carries :resume_session_id when the resolved provider matches the provider that owns it; otherwise it degrades to resume/2's context-based briefing instead of handing a foreign conversation id to another CLI. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…e_session_id/3 CI's mix audit failed dialyzer with `lib/arbiter/worker/dispatch.ex:1:pattern_match`: the `false` clause of the private, single-call-site maybe_put_resume_session_id(opts, provider == session_provider, session_id) is flagged unreachable because dialyzer's success typing narrows the boolean argument to the literal `true`. It's a known success-typing precision limit, not a real dead branch — resume_session/2's explicit agent_type override and the provider-unavailable fallback in resolve_session_resume_provider/3 both legitimately produce a provider that differs from session_provider at runtime, and skipping the :resume_session_id put in that case is the entire point of the guard added in 9b5ec6e. Added a regression test exercising the override path directly (resume with an explicit agent_type that differs from the provider that captured the prior session_id) to prove the `false` clause is reachable and correct. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ty-t7-agy-workers-can
…n-provider mismatch, drop the file-wide dialyzer suppression it required Finding 1: resume_session/2's provider-mismatch fallback dropped resume_session_id but never built resume/2's git-derived briefing it claimed to degrade to, so a resumed dispatch on a mismatched provider silently carried the original task prompt with no continuity — risking redone work. It now calls ResumeContext.build/3 (same as resume/2) and logs the dropped session id, or Logger.info-only degrades to no briefing if the worktree briefing itself can't be built. Finding 2: inlining the provider == session_provider branch directly into resume_session/2 (per reviewer's suggested restructure) instead of dispatching through a narrow two-clause private helper eliminates the dialyzer pattern_match false positive that needed the file-wide lib/arbiter/worker/dispatch.ex suppression; confirmed clean with mix dialyzer --format short after removing the ignore entry. Tightened the round-1 mismatch test to assert the briefing text is present in the resumed argv, not just the absence of --resume. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…efing outcome
Finding 1 (merge blocker): resolve_session_resume_provider/3's is_atom/1
guard on session_provider was a tautology (safe_provider_atom/1 never
returns anything but an atom or nil), making dialyzer's false-arm of the
andalso genuinely dead code. Swap to not is_nil/1, which is not a
tautology and keeps the nil-rejection explicit without re-adding the
removed .dialyzer_ignore.exs suppression.
Finding 2 (nit): the mismatch branch's Logger.info fired before
ResumeContext.build/3 and always claimed a briefing was attached, even
on the {:error, _} arm that proceeds with none. Move the log after the
case and split it: Logger.info on the real briefing, Logger.warning
(naming the build failure reason) when none was built.
…ty-t7-agy-workers-can
ryanrborn
marked this pull request as draft
September 22, 2026 15:12
…r-runs history
The 2026-09-22 04:26Z post-merge verification of AC5 misread an agy run as
having `session_id: NULL` / `provider: null`, and concluded the conversation
id was never captured. It was: `worker_runs.session_id` and `.provider` were
correctly populated in the DB for both the original and resumed run (and
`resumed_from_run_id` was correctly set on the resumed one) — but
`arb worker runs --json` (backed by `RunJSON.summary/1`) and the
`worker_runs` MCP tool (`serialize_worker_run_summary/1`) both silently
dropped `session_id`/`resumed_from_run_id` from their output, so there was no
way to observe them without querying the DB directly.
Surface all three fields on both surfaces so resume continuity (or its
absence) is directly observable from `arb worker runs <task> --json` and the
`worker_runs` MCP tool, without DB access.
The verified live defect was that the run was resumed via the `worker_resume`
MCP tool, which calls `Dispatch.resume/2` — documented and signed off
(2026-06-05) as the deliberately fresh-agent, git-briefing resume path that
carries no session-resume id for any provider. `Dispatch.resume_session/2`
(backing `arb worker resume` / `POST /api/workers/:task_id/resume`) is the
session-continuing path, and it already threads agy's conversation id
correctly per the existing dispatch_test.exs coverage
("resume_session/2 pins the provider to the SAME row the session_id came
from"). Flagged back to the coordinator for a scope decision on whether
`worker_resume`/`resume/2` should also gain opportunistic session
continuity — that would be a cross-provider behavior change reversing a
prior sign-off, out of this fix's authority to make unilaterally.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Follow-up fix to the earlier bd-b7e33c AC5 post-merge work. Coordinator
verification on 2026-09-19 (~05:32Z, on a real agy resume, bd-937r5u) found
the picture had split: the provider-preservation half was confirmed fixed
(a resumed agy task stayed on
gemini-3.1-pro-highinstead of silentlyfalling back to Claude), but conversation continuity itself was still
failing — the resumed run did not reuse the original
--conversationid;it created new conversation databases instead.
Root cause:
Dispatch.resume_session/2derived the provider it pins onto:agent_typeand thesession_idit threads into--resume/--conversationfrom two independent "latest usage-ledger row" queries
(
latest_provider/1andlatest_session_id/1). On a task with more than oneusage row — e.g. an earlier successful agy session plus a later attempt on a
different provider that errored before a session_id was ever captured — the
two queries can pick different rows. That lets
:agent_typeget pinned toone row's provider while
--conversationgets threaded with a differentrow's session id, which either dispatches the wrong CLI entirely or hands
that CLI a conversation id it has no way to honor. This was flagged as a
non-blocking observation in the prior review of this ticket and is exactly
the shape the coordinator's live observation surfaced.
Fix:
latest_session_id/1now returns the provider from the same usagerow the session_id came from, and
resume_session/2pins:agent_typetothat value instead of a separate
latest_provider/1lookup. The two valuescan no longer diverge by construction.
Round 2: the ReviewGate found the branch stale/conflicted against
origin/main—origin/mainhad independently landed PR #1926 (bd-2exkl0),which generalized resume-provider inheritance via
resolve_resume_provider/2(authoring-run provider + availability fallback + coordinator escalation)
and touched the exact same lines in
Dispatch.resume/2/resume_session/2this branch's AC5 fix touched. Merged
origin/mainand resolved theconflict by keeping
resolve_resume_provider/2as-is forresume/2(nosession-id constraint, so the general authoring-provider/fallback resolution
is correct there), and adding a session-aware
resolve_session_resume_provider/3forresume_session/2that stillprefers the provider recorded on the exact usage-ledger row the
session_idcame from — falling back to the general resolution only when the caller
forces a provider or that row's provider is unavailable. This preserves the
AC5 mismatch fix (and its regression test) on top of the newer, more general
provider-fallback machinery from main.
Round 2 review fixes (7a1e9a1): the mismatch fallback dropped
resume_session_idbut never builtresume/2's promised git-derivedbriefing, so a resumed dispatch on a mismatched provider silently carried
the original task prompt with no continuity.
resume_session/2now callsResumeContext.build/3(same asresume/2) on that path. Separately,inlining the
provider == session_providerbranch directly intoresume_session/2(instead of dispatching through a narrow private helper)eliminated a dialyzer
pattern_matchfalse positive that had required afile-wide
.dialyzer_ignore.exssuppression — the suppression was removed.Round 3 review fixes (8c575b1): the guard added to fix that dialyzer
false positive (
is_atom(session_provider) and ...) was itself a tautology—
safe_provider_atom/1never returns anything but an atom ornil— whichmade dialyzer flag the
andalso's false arm as dead code. Swapped tonot is_nil(session_provider) and ..., which is not a tautology, keeps thenil-rejection explicit, and needs no suppression. Also fixed a misleading
log: the mismatch-path
Logger.infoused to fire beforeResumeContext.build/3ran and always claimed a briefing was attached, evenon the
{:error, _}arm that proceeds with none — it now logs after thecase, split intoLogger.infowhen a real briefing was built andLogger.warning(naming the failure reason) when it wasn't.Test plan
New test
resume_session/2 pins the provider to the SAME row the session_id came from(dispatch_test.exs) — reproduces the mismatchdirectly (a newer no-session-id
"claude"row vs. an older"gemini"row carrying the real session_id), fails against the pre-fix code
(times out waiting for the agy stub, since resume dispatched Claude
instead) and passes after the fix, asserting the exact
--conversation agy-conv-mismatchvalue reaches the spawned argv.Tightened regression test asserts the
ResumeContext.build/3briefingtext (
"RESUMING work on task") is actually present in the resumedargv on the mismatch path, not just the absence of
--resume.mix test test/arbiter/worker/dispatch_test.exs test/arbiter/agents/gemini_test.exs test/arbiter/worker_resume_test.exs test/arbiter/worker/respawn_provider_test.exs— 234 tests, 0 failures (re-run on HEAD8c575b1c).mix dialyzer --format short(umbrella root,MIX_ENV=dev, matching the CIauditjob) —Total errors: 61, Skipped: 61, Unnecessary Skips: 0, exit 0. No.dialyzer_ignore.exssuppression re-added.mix precommit(format, compile --warnings-as-errors, full test suite) — 0 failures at round 2; re-validated at round 3 via the targeted suite above plus the umbrella dialyzer run.Post-merge, coordinator-owned (
verify_after_deploy, not a merge gate): re-run the same live check that surfaced this —arb worker resume <task>against a real agy/gemini task with a resumable prior session. A working result looks like: the resumed worker stays ongemini-...(notSonnet), AND the agy process's.gemini/antigravity-cli/conversations/<original-uuid>.dbfile (not a fresh one) keeps growing after the resume — i.e. the SAME conversation database from before the resume is the one that gets appended to, not a newly created one.References
bd-b7e33c
Closes #1813
🤖 Generated with Claude Code