Skip to content

fix(subagent): surface primary error when fallback retry masks it + journal failure reason (#59) - #69

Merged
rz1989s merged 2 commits into
mainfrom
fix/59-primary-error-masking
Aug 29, 2026
Merged

rz1989s merged 2 commits into
mainfrom
fix/59-primary-error-masking

Conversation

@rz1989s

@rz1989s rz1989s commented Aug 29, 2026

Copy link
Copy Markdown
Member

Problem (#59)

Dispatching a subagent with an explicit model string appeared to fail where the inherited session model succeeded — the surfaced error named the fallback model only. Two real defects hid behind that symptom:

  1. Error masking: when the subagent: auto-retry with a fallback model on provider rate-limit / auth failure (Ollama-Cloud primary → OpenRouter fallback) #39 fallback retry also failed, only the fallback's error was returned. The primary's failure text was dropped entirely.
  2. Journal gap: the run:ended journal event carried no error field — the archived failing runs (fl-msbwt1jj, fl-msbwwqpg) have empty resultSummary and zero diagnostic content, making post-hoc diagnosis impossible.

Investigation (closes the "explicit vs inherited divergence" theory)

  • resolveAgentModel builds the inherit path's model as ${parentModel.provider}/${parentModel.id} from the live session model — so both paths resolve via the identical getModel(provider, id) call.
  • Empirical probe of ModelRuntime.create() in this environment: getModel("Ollama", "glm-5.2:cloud") resolves cleanly (local models.json provider key, exact case). No runtime error.
  • Conclusion: there is no code divergence between explicit and inherited model resolution; the observed asymmetry was transient provider failure (Ollama per-minute limits, 2026-08-08) made undiagnosable by the masking. The fix is diagnosability.

Changes

  • tools/subagent.ts — after a failed fallback retry, compose the surfaced error with both attempts: primary '<model>' failed: <err>; fallback '<model>' failed: <err>.
  • engine/retry-fallback.ts — same composed-error contract in withModelFallbackRetry (lifecycle/bg spawn sites).
  • engine/spawnSubagent.tsfinishRun journals the failure reason on run:ended (conditionally, so successful runs stay unchanged).

Tests

  • Composed error asserted on both retry paths (direct tool + lifecycle/bg wrapper): names both models, includes both failure texts.
  • run:ended carries a meaningful error on failed runs.
  • 702/702 pass (699 → 702), pnpm typecheck clean.

Closes #59

…ournal failure reason

#59 dogfood finding: a failed fallback retry returned ONLY the fallback's
error, masking why the primary (e.g. an explicit model string) failed at
all. Investigation showed explicit vs inherited model resolution are the
same code path (both strings -> getModel(provider, id)); the observed
asymmetry was transient provider failure + this masking, so the fix is
diagnosability:

- tools/subagent.ts: when the retry also fails, compose the surfaced
  error with both attempts (primary '<model>': <err>; fallback: <err>).
- engine/retry-fallback.ts: same composed-error contract for the
  lifecycle/bg spawn wrapper (withModelFallbackRetry).
- engine/spawnSubagent.ts finishRun: journal the failure reason on the
  run:ended event (archived failing runs had empty resultSummary and no
  error field — post-hoc diagnosis from the journal was impossible).

Tests: composed-error on both retry paths; run:ended carries error.
…guard in retry wrapper

Review findings on PR #69: the journaled run:ended.error rode on a TS
spread quirk (excess-property bypass) — declare it on the RunEndedEvent
interface so the journal schema is the contract; and mirror the direct
path's identical-error dedup guard in withModelFallbackRetry so both
composition sites share one contract.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

subagent: explicit model string fails where inherited session model succeeds (same model)

1 participant