OBS-11 — stop/resume killed pure-native runs over a phantom external pin.
Role templates' preferred_external_agent leaked into execution identity even
when the user requested native and execution actually ran native; on resume
the availability gate trusted the pin and failed every non-terminal item.
Root fixes across the whole chain:
- Staffing card per-role defaults are now the RESOLVED backend (explicit
session agent choice > runnable template preference > native), never a
hardcoded external default; seat enrichment and the dispatch selector's
locked branch downgrade provably unavailable externals to native and
record the wish in execution_agent_unavailable.
- The resume availability gate fails closed only when a resumable external
session actually exists; a bare pin heals to native (snapshot AND task
durable identity) and the run resumes — mirroring dispatch fallback.
- Suspend-checkpoint replies: force_resume (chat/headless spelling) is
recognized alongside ui_force_resume, and bare continuation tokens
(English and Chinese spellings) take the plain-resume path instead of
being routed to the final decider as content, which reopened the
already-approved intake card.
OBS-5 — failed runs never closed and dropped new input. The dispatcher's
convergence exit now settles terminally-failed runs (status=failed,
lifecycle=closed_failed, run_failure metadata) and emits a
company_run_failure_review card whose replies never swallow messages:
dismiss acknowledges, content falls through so normal routing starts a
fresh run. _maybe_resume_existing_company_runtime no longer re-executes a
terminally-failed tree: control replies get an honest closed status,
content-bearing input starts a new run.
OBS-6 — provider quota exhaustion terminally failed work items. Rate-limit
rejections are classified (LLMProvider.is_rate_limit_error, covering
status codes, exception types, and English/Chinese provider error text),
the agent runtime raises typed ProviderQuotaExhaustedError instead of
burning conversation-feedback retries, and the company dispatcher parks:
the item returns to READY (attempt interrupted, no terminal failure), the
member session idles, and claiming backs off exponentially (60s doubling
to a 900s cap; a quiet 30min resets the streak) before resuming
automatically.
Verified end-to-end on the real minimax-m3 campaign: same goal, same 300s
stop point, same run shape that previously killed the whole tree within
90s now resumes cleanly and completes with all items approved; staffing
defaults native for all 11 roles.
Tests: test_stop_resume_native_pin (10), test_run_failure_settlement (6),
test_provider_quota_park (9); attempt-ledger, recruiter, and
suspend-resume suites updated to the new contracts (their old assertions
pinned the defective behaviors).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Work items whose execution kept dying without a durable verdict (crash
mid-dispatch, kill -9, cancel-before-harvest) were re-dispatched forever:
every exit path was responsible for remembering to write a terminal
phase, and any path that forgot left the card RUNNING and eligible again.
Replace that with structural accounting:
- claim CAS opens an attempt in the same UPDATE (attempt_seq+1,
attempt_settled=false) so no dispatch can start unaccounted (store.py)
- transition_work_item becomes the settlement authority: every
non-RUNNING transition settles the open attempt in the same write;
crashed/interrupted outcomes accumulate streaks, clean outcomes reset
them; claim release folds into the same write; settlement still lands
when the phase write loses a race (work_item_transition.py)
- dispatcher refuses cards over the streak limits (crash>=3,
interrupted>=5) in both is_dispatchable and _work_item_is_runnable,
and a per-tick reconcile pass back-fills dead attempts as interrupted
and terminalizes over-limit cards to FAILED with a visible
blocked_reason (dispatch_hold quarantine if even that write fails)
(phase.py, company_mode.py)
- crash exits now settle: cancellation unwind harvests coroutines that
died on a real exception before discarding them, the crashed-item
handler releases the claim and settles as crashed with a quarantine
fallback, and the timeout path settles as crashed (company_mode.py)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>