fix(company): stop/resume identity truth, failure-path closure, quota park
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>
This commit is contained in:
@@ -411,7 +411,19 @@ class CompanyRecruiterFlowTests(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertEqual(checkpoint.payload["recommended_action"], "auto_recruit")
|
||||
self.assertEqual(checkpoint.payload["staffing_defaults"]["source"], "system")
|
||||
self.assertTrue(all(role["default_selection"]["kind"] == "fallback" for role in checkpoint.payload["staffing_roles"]))
|
||||
self.assertTrue(all(role["selected_agent"] == "codex" for role in checkpoint.payload["staffing_roles"]))
|
||||
# Card defaults become per-role overrides on approve, so they must
|
||||
# reflect what will actually run (OBS-11): the role template's
|
||||
# external preference when it can run, native otherwise.
|
||||
agents_by_role = {
|
||||
role["role_id"]: role["selected_agent"]
|
||||
for role in checkpoint.payload["staffing_roles"]
|
||||
}
|
||||
self.assertTrue(all(
|
||||
role["selected_agent"] == role["default_agent"]
|
||||
for role in checkpoint.payload["staffing_roles"]
|
||||
))
|
||||
self.assertEqual(agents_by_role.get("ceo"), "native")
|
||||
self.assertEqual(agents_by_role.get("senior_engineer"), "codex")
|
||||
self.assertEqual(llm.calls, [])
|
||||
self.assertEqual(tasks, [])
|
||||
await store.close()
|
||||
@@ -634,7 +646,21 @@ class CompanyRecruiterFlowTests(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertEqual(checkpoint.payload["staffing_pool"]["employees"], [])
|
||||
self.assertGreater(len(checkpoint.payload["staffing_roles"]), 0)
|
||||
self.assertTrue(all(role["default_selection"]["kind"] == "fallback" for role in checkpoint.payload["staffing_roles"]))
|
||||
self.assertTrue(all(role["selected_agent"] == "codex" for role in checkpoint.payload["staffing_roles"]))
|
||||
# Card defaults become per-role overrides on approve, so they must
|
||||
# reflect what will actually run (OBS-11): the role template's
|
||||
# external preference when it can run, native otherwise.
|
||||
agents_by_role = {
|
||||
role["role_id"]: role["selected_agent"]
|
||||
for role in checkpoint.payload["staffing_roles"]
|
||||
}
|
||||
self.assertTrue(all(
|
||||
role["selected_agent"] == role["default_agent"]
|
||||
for role in checkpoint.payload["staffing_roles"]
|
||||
))
|
||||
# decision.preferred_agent="opencode" is an explicit session-level
|
||||
# choice and outranks role template preferences.
|
||||
self.assertEqual(agents_by_role.get("ceo"), "opencode")
|
||||
self.assertEqual(agents_by_role.get("senior_engineer"), "opencode")
|
||||
template_ids = {item["template_id"] for item in checkpoint.payload["staffing_pool"]["templates"]}
|
||||
self.assertIn("engineering-frontend-developer", template_ids)
|
||||
self.assertEqual(llm.calls, [])
|
||||
@@ -744,7 +770,7 @@ class CompanyRecruiterFlowTests(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertEqual(senior_role["selected_agent"], "opencode")
|
||||
ceo_role = next(role for role in payload["staffing_roles"] if role["role_id"] == "ceo")
|
||||
self.assertEqual(ceo_role["default_selection"], {"kind": "fallback", "id": ""})
|
||||
self.assertEqual(ceo_role["selected_agent"], "codex")
|
||||
self.assertEqual(ceo_role["selected_agent"], "native")
|
||||
await store.close()
|
||||
|
||||
async def test_confirmed_session_reuses_staffing_defaults_without_recruiter_llm(self) -> None:
|
||||
@@ -1476,7 +1502,19 @@ class CompanyRecruiterFlowTests(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertIsNotNone(checkpoint)
|
||||
self.assertEqual(checkpoint.checkpoint_type, "company_staffing_selection")
|
||||
self.assertEqual(checkpoint.payload["staffing_defaults"]["source"], "system")
|
||||
self.assertTrue(all(role["selected_agent"] == "codex" for role in checkpoint.payload["staffing_roles"]))
|
||||
# Card defaults become per-role overrides on approve, so they must
|
||||
# reflect what will actually run (OBS-11): the role template's
|
||||
# external preference when it can run, native otherwise.
|
||||
agents_by_role = {
|
||||
role["role_id"]: role["selected_agent"]
|
||||
for role in checkpoint.payload["staffing_roles"]
|
||||
}
|
||||
self.assertTrue(all(
|
||||
role["selected_agent"] == role["default_agent"]
|
||||
for role in checkpoint.payload["staffing_roles"]
|
||||
))
|
||||
self.assertEqual(agents_by_role.get("ceo"), "native")
|
||||
self.assertEqual(agents_by_role.get("senior_engineer"), "codex")
|
||||
await store.close()
|
||||
|
||||
async def test_deny_recruitment_cancels_execution(self) -> None:
|
||||
|
||||
Reference in New Issue
Block a user