fix(company): terminalize report-chain failure into FAILED settlement; prompt-spill hygiene

Report chain: when consecutive report cards exceed the failure limit the
parent now fails through transition_work_item (settlement, dependents,
manager visibility) instead of parking forever in AWAITING_MANAGER_REVIEW
behind a metadata hold no reconcile path could clear. The hold stamp
survives only as the quarantine fallback when the FAILED write does not
land, mirroring the attempt ledger's terminalize pattern.

Cursor prompt spill: one stable file per task (retries overwrite instead
of accumulating) and a self-ignoring .gitignore so workspace git never
picks up .opc/external_prompts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
LZH-YS1998
2026-07-31 19:53:13 +08:00
parent 18ddc18948
commit 5f5fbf432e
4 changed files with 121 additions and 17 deletions
+11
View File
@@ -3612,6 +3612,17 @@ class ExternalAgentMonitoringTests(unittest.IsolatedAsyncioTestCase):
self.assertIn(str(prompt_file), str(cmd[-1]))
self.assertNotIn(prompt[:64], metadata["command"])
self.assertLess(len(cmd[-1].encode("utf-8")), CursorAdapter._ARGV_PROMPT_MAX_BYTES)
# Hygiene: one stable file per task (retries overwrite) and a
# self-ignoring directory so workspace git never picks it up.
self.assertEqual(prompt_file.name, "cursor_task-large.md")
_cmd2, metadata2 = adapter.build_interactive_invocation(
task, workspace_path=tmpdir
)
self.assertEqual(metadata2["prompt_file"], str(prompt_file))
self.assertEqual(
sorted(p.name for p in prompt_file.parent.iterdir()),
[".gitignore", "cursor_task-large.md"],
)
finally:
_cleanup_test_dir(tmpdir)
+34 -1
View File
@@ -13,7 +13,7 @@ from __future__ import annotations
import tempfile
import unittest
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock
from unittest.mock import AsyncMock, MagicMock, patch
from opc.core.config import OPCConfig, RoleConfig
from opc.core.events import EventBus
@@ -1503,6 +1503,39 @@ class ReportFailureStormBrakeTests(unittest.IsolatedAsyncioTestCase):
)
)
refreshed = await self.store.get_delegation_work_item("wi-child")
self.assertEqual(refreshed.phase, Phase.FAILED)
self.assertIn("report_chain_failure", str(refreshed.blocked_reason or ""))
self.assertFalse(
str((refreshed.metadata or {}).get("report_chain_hold", "") or "").strip()
)
async def test_terminalize_write_failure_quarantines_via_hold(self) -> None:
parent = _build_child_work_item()
parent.phase = Phase.AWAITING_MANAGER_REVIEW
parent.metadata = {
**dict(parent.metadata or {}),
"review_owner_role_id": "cto",
"review_owner_seat_id": "seat::team::cto::cto",
"max_consecutive_report_failures": 3,
}
await self.store.save_delegation_work_item(parent)
for attempt in (1, 2, 3):
await self.store.save_delegation_work_item(self._failed_report(attempt))
with patch(
"opc.layer2_organization.company_mode.transition_work_item",
side_effect=RuntimeError("db write lost"),
):
await self.executor._reconcile_missing_review_chain(
await self.store.list_delegation_work_items("run-1")
)
self.assertIsNone(
await self.store.get_delegation_work_item(
report_work_item_id_for_attempt("wi-child", 4)
)
)
refreshed = await self.store.get_delegation_work_item("wi-child")
self.assertEqual(refreshed.phase, Phase.AWAITING_MANAGER_REVIEW)
self.assertEqual(
refreshed.metadata.get("report_chain_hold"),
"consecutive_report_failures",