fix: release comms-blocking parks when replies arrive and give every role file authoring tools

Project 3333 forensics: env_engineer sent a blocking question to the CTO,
got a full reply 96s later, and still deadlocked the whole run — the
park half (`_park_for_blocking_comms`) had no wired consumer, so
WAITING_FOR_PEER work items could never be released.

Unpark path (root fix):
- The dispatcher loop now calls `_try_unpark_blocking_comms` each tick
  for parked, non-in-flight tasks; blocking replies land as durable
  inbox files, so the check is read-only until all replies are present.
- `_try_unpark_blocking_comms` accepts orphaned waits (peer_wait
  stripped by the legacy resolver while the work item stayed parked)
  and falls back to the park predicate itself: an empty unresolved
  blocking outbox releases the task, anything pending keeps it parked.
- `resolve_task_peer_wait` no longer touches comms_blocking waits (it
  flipped task.status without the work-item phase and stripped the
  peer_wait evidence); `_resume_peer_checkpoint` re-enters the company
  runtime for comms/orphaned waits and lets the dispatcher converge.

File tools (defaults changed at their declaration sites, honoring the
"empty tools = everything, explicit list = exactly that" contract):
- corporate builtin groups gain file_write/file_edit for coordination,
  QA, and data-acquisition roles.
- all shipped org YAML role tool lists gain the missing
  file_write/file_edit entries.
- coordination turn modes no longer strip file_write/file_edit at
  runtime — in-context content (briefs, matrices) must be persistable
  instead of getting trapped in blocking DM hand-offs.

Also includes the pending office_ui ws_handler change from the working
tree.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
LZH-YS1998
2026-07-08 21:06:39 +08:00
parent 4b29b89371
commit 5aa57e69ee
12 changed files with 110 additions and 10 deletions
+5
View File
@@ -1989,6 +1989,11 @@ class CommunicationManager:
wait = dict(task.metadata.get("peer_wait", {}))
if not wait:
return False
if str(wait.get("kind") or "") == "comms_blocking":
# File-comms blocking waits are owned by the company
# dispatcher's per-tick unpark; resolving them here would flip
# task.status without the work-item phase and strand the run.
return False
if wait.get("kind") != "meeting":
msg_id = wait.get("message_id")
reply = await self.store.get_latest_reply(msg_id) if msg_id else None
+27 -2
View File
@@ -4236,6 +4236,21 @@ class CompanyWorkItemExecutor:
)
] or list(self._active_tasks)
self._active_tasks = tasks
# Consumer half of `_park_for_blocking_comms`: blocking
# replies arrive as durable inbox files, so each tick checks
# parked tasks and releases the ones whose replies are all
# present. In-flight tasks are skipped — their coroutine
# still owns the Task object and a late save_task would
# clobber the transition.
in_flight_task_ids = {
claimed.id for _member, claimed in active_work_item_tasks.values()
}
for parked in tasks:
if (
parked.status == TaskStatus.AWAITING_PEER
and parked.id not in in_flight_task_ids
):
await self._try_unpark_blocking_comms(parked)
await self.runtime.refresh_inbox_state(tasks)
work_items = await self._load_delegation_work_items(tasks)
work_items = await self._refresh_ready_work_items(work_items, tasks=tasks)
@@ -9225,7 +9240,13 @@ class CompanyWorkItemExecutor:
if task.status != TaskStatus.AWAITING_PEER:
return False
peer_wait = dict(task.metadata.get("peer_wait", {}) or {})
if str(peer_wait.get("kind") or "") != "comms_blocking":
wait_kind = str(peer_wait.get("kind") or "")
# An empty kind is an orphaned wait (e.g. a legacy resolver popped
# `peer_wait` while the work item stayed WAITING_FOR_PEER); those
# are recoverable from the durable comms state below. Waits with a
# different explicit kind (meeting, message-id) have their own
# resolvers.
if wait_kind and wait_kind != "comms_blocking":
return False
try:
from opc.layer2_organization import comms as _comms
@@ -9240,7 +9261,11 @@ class CompanyWorkItemExecutor:
return False
blocking_ids = list(peer_wait.get("blocking_message_ids", []) or [])
if not blocking_ids:
# Nothing to wait for — defensively unpark.
# No recorded ids (orphaned or empty wait): fall back to the
# park predicate itself — any unanswered blocking outbox
# message keeps the task parked, none means release.
if _comms.find_unresolved_blocking_outbox(layout, role_id):
return False
task.metadata = dict(task.metadata)
task.metadata.pop("peer_wait", None)
await transition_work_item_from_task(
@@ -40,6 +40,8 @@ _BROWSER_EXECUTION_TOOLS = [
_CORPORATE_COORDINATION_TOOLS = [
"file_read",
"file_write",
"file_edit",
"file_search",
"list_dir",
"todo_write",
@@ -49,8 +51,6 @@ _CORPORATE_COORDINATION_TOOLS = [
_CORPORATE_BOOTSTRAP_TOOLS = [
*_CORPORATE_COORDINATION_TOOLS,
"shell_exec",
"file_write",
"file_edit",
]
_CORPORATE_WEB_COORDINATION_TOOLS = [
@@ -76,6 +76,7 @@ _CORPORATE_EXECUTION_TOOLS = [
_CORPORATE_QA_TOOLS = [
"file_read",
"file_write",
"file_edit",
"file_search",
"list_dir",
"shell_exec",
@@ -105,6 +106,7 @@ _CORPORATE_DATA_ACQUISITION_TOOLS = [
"shell_exec",
"file_read",
"file_write",
"file_edit",
"file_search",
"list_dir",
"web_search",