fix(company): make delegation review lifecycle durable
This commit is contained in:
+151
-17
@@ -76,7 +76,7 @@ from opc.layer2_organization.phase import (
|
||||
InvalidPhaseTransition,
|
||||
TODO_PHASES,
|
||||
coerce_phase,
|
||||
is_resumable_after_claim_release,
|
||||
is_stale_claim_releasable,
|
||||
is_terminal,
|
||||
kanban_column,
|
||||
on_phase_transition,
|
||||
@@ -4600,9 +4600,9 @@ class OPCStore:
|
||||
We only touch in-flight phases (RUNNING / WAITING_FOR_* /
|
||||
PAUSED / NEEDS_ATTENTION / AWAITING_*) so we never disturb
|
||||
terminal cards. The phase itself is left alone; the sweep only
|
||||
clears the claim. The dispatcher's ``is_dispatchable`` check
|
||||
recognises a non-terminal card with no claim as eligible for
|
||||
re-pick on the next tick.
|
||||
clears the claim. Active execution can subsequently be re-picked;
|
||||
passive AWAITING_* parents remain non-dispatchable while their
|
||||
report/review chain (or human) advances them.
|
||||
"""
|
||||
if self._db is None:
|
||||
return 0
|
||||
@@ -4619,7 +4619,7 @@ class OPCStore:
|
||||
phase = coerce_phase(phase_str)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if not is_resumable_after_claim_release(phase):
|
||||
if not is_stale_claim_releasable(phase):
|
||||
continue
|
||||
metadata = _json_loads(metadata_json, {})
|
||||
metadata["claimed_by_role_session_id"] = ""
|
||||
@@ -5102,13 +5102,26 @@ class OPCStore:
|
||||
|
||||
return total
|
||||
|
||||
async def save_delegation_work_item(self, item: DelegationWorkItem) -> None:
|
||||
async def save_delegation_work_item(
|
||||
self,
|
||||
item: DelegationWorkItem,
|
||||
) -> None:
|
||||
await self._write_delegation_work_item(item, if_absent=False)
|
||||
|
||||
async def _write_delegation_work_item(
|
||||
self,
|
||||
item: DelegationWorkItem,
|
||||
*,
|
||||
if_absent: bool,
|
||||
) -> bool:
|
||||
# Single-source-of-truth gate: every write — whether it goes through
|
||||
# update_delegation_work_item or directly mutates `item.phase` and
|
||||
# then calls save — passes through validate_transition. Skipping the
|
||||
# validation requires a separate code path; there is no way to write
|
||||
# an invalid phase by accident.
|
||||
existing = await self.get_delegation_work_item(item.work_item_id)
|
||||
if if_absent and existing is not None:
|
||||
return False
|
||||
previous_phase = existing.phase if existing is not None else None
|
||||
validate_transition(previous_phase, item.phase)
|
||||
item.metadata = dict(item.metadata or {})
|
||||
@@ -5127,16 +5140,10 @@ class OPCStore:
|
||||
# may want to know "what changed".
|
||||
target_phase = item.phase
|
||||
db = self._require_db()
|
||||
await db.execute(
|
||||
"""INSERT INTO delegation_work_items
|
||||
(work_item_id, run_id, cell_id, team_instance_id, team_id, role_id, seat_id, seat_state_id,
|
||||
role_runtime_session_id, parent_work_item_id, source_role_id, source_seat_id, title, summary,
|
||||
kind, projection_id, phase, batch_id, batch_index,
|
||||
deliverable_summary, blocked_reason, handoff_status, continuation_source, manager_role_id,
|
||||
manager_seat_id, claimed_by_role_runtime_session_id, claimed_by_seat_id, metadata,
|
||||
created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(work_item_id) DO UPDATE SET
|
||||
conflict_action = (
|
||||
"DO NOTHING"
|
||||
if if_absent
|
||||
else """DO UPDATE SET
|
||||
run_id=excluded.run_id,
|
||||
cell_id=excluded.cell_id,
|
||||
team_instance_id=excluded.team_instance_id,
|
||||
@@ -5165,7 +5172,18 @@ class OPCStore:
|
||||
claimed_by_seat_id=excluded.claimed_by_seat_id,
|
||||
metadata=excluded.metadata,
|
||||
created_at=excluded.created_at,
|
||||
updated_at=excluded.updated_at""",
|
||||
updated_at=excluded.updated_at"""
|
||||
)
|
||||
cursor = await db.execute(
|
||||
f"""INSERT INTO delegation_work_items
|
||||
(work_item_id, run_id, cell_id, team_instance_id, team_id, role_id, seat_id, seat_state_id,
|
||||
role_runtime_session_id, parent_work_item_id, source_role_id, source_seat_id, title, summary,
|
||||
kind, projection_id, phase, batch_id, batch_index,
|
||||
deliverable_summary, blocked_reason, handoff_status, continuation_source, manager_role_id,
|
||||
manager_seat_id, claimed_by_role_runtime_session_id, claimed_by_seat_id, metadata,
|
||||
created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(work_item_id) {conflict_action}""",
|
||||
(
|
||||
item.work_item_id,
|
||||
item.run_id,
|
||||
@@ -5200,6 +5218,8 @@ class OPCStore:
|
||||
),
|
||||
)
|
||||
await db.commit()
|
||||
if if_absent and not (getattr(cursor, "rowcount", 0) or 0):
|
||||
return False
|
||||
# D2 hook fire — propagate phase change to dependent layers
|
||||
# (task.status, role_session.status, dispatcher wake, etc.). All
|
||||
# writes to delegation_work_items pass through here, so this is
|
||||
@@ -5208,6 +5228,20 @@ class OPCStore:
|
||||
await on_phase_transition(previous_phase, target_phase, item, store=self)
|
||||
except Exception: # never let hook failures break the write
|
||||
logger.opt(exception=True).debug("on_phase_transition raised at top level")
|
||||
return True
|
||||
|
||||
async def insert_delegation_work_item_if_absent(
|
||||
self,
|
||||
item: DelegationWorkItem,
|
||||
) -> bool:
|
||||
"""Atomically create a WorkItem without overwriting a concurrent claim.
|
||||
|
||||
Auxiliary report/review IDs are deterministic. A read-before-write
|
||||
check is insufficient because another dispatcher can create and claim
|
||||
the same card between those operations; the conflict decision must be
|
||||
made by SQLite in the insert statement itself.
|
||||
"""
|
||||
return await self._write_delegation_work_item(item, if_absent=True)
|
||||
|
||||
async def list_delegation_work_items(
|
||||
self,
|
||||
@@ -5475,6 +5509,106 @@ class OPCStore:
|
||||
await self.save_delegation_work_item(item)
|
||||
return item
|
||||
|
||||
async def apply_delegation_review_resolution(
|
||||
self,
|
||||
work_item_id: str,
|
||||
*,
|
||||
source_report_work_item_id: str,
|
||||
target_phase: Phase | str,
|
||||
blocked_reason: str,
|
||||
metadata_updates: dict[str, Any],
|
||||
) -> DelegationWorkItem | None:
|
||||
"""Atomically apply a manager verdict to its exact report generation.
|
||||
|
||||
The phase predicate and latest-applied-report predicate live in the
|
||||
same SQLite UPDATE as the child phase + applied-stamp write. This
|
||||
prevents a late manager turn from crossing an AWAITING_HUMAN
|
||||
transition or approving an older report after a newer report landed.
|
||||
``updated_at`` provides optimistic metadata concurrency; a few retries
|
||||
preserve unrelated same-phase updates without weakening either guard.
|
||||
"""
|
||||
target = coerce_phase(target_phase)
|
||||
validate_transition(Phase.AWAITING_MANAGER_REVIEW, target)
|
||||
expected_source = str(source_report_work_item_id or "").strip()
|
||||
db = self._require_db()
|
||||
|
||||
for _attempt in range(3):
|
||||
item = await self.get_delegation_work_item(work_item_id)
|
||||
if item is None or item.phase != Phase.AWAITING_MANAGER_REVIEW:
|
||||
return None
|
||||
metadata = dict(item.metadata or {})
|
||||
metadata.update(dict(metadata_updates or {}))
|
||||
if (
|
||||
self._metadata_has_work_item_projection_identity(metadata)
|
||||
or str(item.projection_id or "").strip()
|
||||
or str(item.kind or "").strip()
|
||||
):
|
||||
metadata, _ = migrate_work_item_projection_metadata(
|
||||
metadata,
|
||||
projection_id_fallback=str(
|
||||
item.projection_id or item.work_item_id or ""
|
||||
).strip(),
|
||||
turn_type_fallback=str(item.kind or "").strip(),
|
||||
)
|
||||
previous_updated_at = item.updated_at.isoformat()
|
||||
updated_at = datetime.now()
|
||||
cursor = await db.execute(
|
||||
"""UPDATE delegation_work_items
|
||||
SET phase = ?, blocked_reason = ?, metadata = ?, updated_at = ?
|
||||
WHERE work_item_id = ?
|
||||
AND phase = ?
|
||||
AND updated_at = ?
|
||||
AND COALESCE((
|
||||
SELECT report.work_item_id
|
||||
FROM delegation_work_items AS report
|
||||
WHERE report.parent_work_item_id = ?
|
||||
AND report.kind = 'report'
|
||||
AND json_extract(
|
||||
report.metadata,
|
||||
'$.report_target_work_item_id'
|
||||
) = ?
|
||||
AND json_extract(
|
||||
report.metadata,
|
||||
'$.report_card_outcome'
|
||||
) = 'applied'
|
||||
ORDER BY report.batch_index DESC,
|
||||
report.created_at DESC,
|
||||
report.work_item_id DESC
|
||||
LIMIT 1
|
||||
), '') = ?""",
|
||||
(
|
||||
target.value,
|
||||
str(blocked_reason or ""),
|
||||
_json_dumps(metadata),
|
||||
updated_at.isoformat(),
|
||||
work_item_id,
|
||||
Phase.AWAITING_MANAGER_REVIEW.value,
|
||||
previous_updated_at,
|
||||
work_item_id,
|
||||
work_item_id,
|
||||
expected_source,
|
||||
),
|
||||
)
|
||||
await db.commit()
|
||||
if not (getattr(cursor, "rowcount", 0) or 0):
|
||||
continue
|
||||
persisted = await self.get_delegation_work_item(work_item_id)
|
||||
if persisted is None:
|
||||
return None
|
||||
try:
|
||||
await on_phase_transition(
|
||||
Phase.AWAITING_MANAGER_REVIEW,
|
||||
target,
|
||||
persisted,
|
||||
store=self,
|
||||
)
|
||||
except Exception:
|
||||
logger.opt(exception=True).debug(
|
||||
"on_phase_transition raised after review-resolution CAS"
|
||||
)
|
||||
return persisted
|
||||
return None
|
||||
|
||||
async def reopen_approved_delegation_work_item_for_rework(
|
||||
self,
|
||||
work_item_id: str,
|
||||
|
||||
+3
-15
@@ -117,6 +117,7 @@ from opc.layer2_organization.session_scoping import (
|
||||
is_top_level_company_session,
|
||||
task_session_scope_id,
|
||||
)
|
||||
from opc.layer2_organization.turn_mode import reset_manager_dispatch_turn_metadata
|
||||
from opc.layer2_organization.seat_executor import EngineSeatExecutor
|
||||
from opc.layer2_organization.work_item_runtime import (
|
||||
is_work_item_runtime_metadata,
|
||||
@@ -6945,11 +6946,7 @@ class OPCEngine:
|
||||
task.metadata.pop("delegation_pending_work_item_ids", None)
|
||||
task.metadata.pop("delegated_children_pending", None)
|
||||
task.metadata.pop("delegation_wait_for_work_item_ids", None)
|
||||
task.metadata.pop("manager_board_mutation_performed", None)
|
||||
task.metadata.pop("manager_board_modified_work_item_ids", None)
|
||||
task.metadata.pop("manager_board_deleted_work_item_ids", None)
|
||||
task.metadata.pop("manager_no_delegation_justification", None)
|
||||
task.metadata.pop("no_delegation_justification", None)
|
||||
task.metadata = reset_manager_dispatch_turn_metadata(task.metadata)
|
||||
progress = list(task.metadata.get("progress_log", []) or [])
|
||||
progress.append(f"Company follow-up routed to final decider ({resume_source}): {reply}")
|
||||
task.metadata["progress_log"] = progress[-20:]
|
||||
@@ -10211,16 +10208,7 @@ class OPCEngine:
|
||||
return True
|
||||
if str(task_metadata.get("manager_no_delegation_justification", "") or "").strip():
|
||||
return True
|
||||
work_item_id = linked_work_item_id_for_task(task)
|
||||
if not work_item_id or not hasattr(self.store, "get_delegation_work_item"):
|
||||
return False
|
||||
work_item = await self.store.get_delegation_work_item(work_item_id)
|
||||
if work_item is None:
|
||||
return False
|
||||
metadata = dict(getattr(work_item, "metadata", {}) or {})
|
||||
if bool(metadata.get("manager_board_mutation_performed", False)):
|
||||
return True
|
||||
if str(metadata.get("manager_no_delegation_justification", "") or "").strip():
|
||||
if str(task_metadata.get("manager_dispatch_guard_unresolved", "") or "").strip():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
+1254
-909
File diff suppressed because it is too large
Load Diff
@@ -131,6 +131,10 @@ _WORK_ITEM_FIELDS: tuple[MetadataFieldSpec, ...] = (
|
||||
_spec("review_attempt", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
|
||||
_spec("review_attempt_count", MetadataOwner.WORK_ITEM, allowed_locations=("work_item",), legacy_fallback=True, migration_policy="work_item_wins"),
|
||||
_spec("review_target_work_item_id", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
|
||||
_spec("review_source_report_work_item_id", MetadataOwner.WORK_ITEM, allowed_locations=("work_item",), migration_policy="work_item_wins"),
|
||||
_spec("review_resolution", MetadataOwner.WORK_ITEM, allowed_locations=("work_item",), migration_policy="work_item_wins"),
|
||||
_spec("review_resolution_state", MetadataOwner.WORK_ITEM, allowed_locations=("work_item",), migration_policy="work_item_wins"),
|
||||
_spec("review_resolution_applied_work_item_id", MetadataOwner.WORK_ITEM, allowed_locations=("work_item",), migration_policy="work_item_wins"),
|
||||
_spec("review_target_worker_task_id", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
|
||||
_spec("review_target_worker_role_id", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
|
||||
_spec("review_target_worker_seat_id", MetadataOwner.WORK_ITEM, allowed_locations=("work_item", "task_execution_copy"), legacy_fallback=True, migration_policy="work_item_wins"),
|
||||
|
||||
@@ -49,7 +49,9 @@ __all__ = [
|
||||
"phase_for_task_status",
|
||||
"coerce_phase",
|
||||
"is_review_execution_work_item_metadata",
|
||||
"is_runtime_auxiliary_work_item",
|
||||
"should_hide_work_item_from_company_kanban",
|
||||
"is_stale_claim_releasable",
|
||||
"is_resumable_after_claim_release",
|
||||
"is_orphaned",
|
||||
"is_dispatchable",
|
||||
@@ -146,6 +148,27 @@ def is_report_execution_work_item_metadata(metadata: Mapping[str, Any] | None) -
|
||||
return work_kind == "report" and bool(str(data.get("report_target_work_item_id", "") or "").strip())
|
||||
|
||||
|
||||
def is_runtime_auxiliary_work_item(value_or_metadata: Any) -> bool:
|
||||
"""True for attention, report, and review runtime helper cards.
|
||||
|
||||
Accepts either a work-item-like object, a serialized work item containing
|
||||
a ``metadata`` mapping, or the metadata mapping itself. Keeping this
|
||||
identity check below the company runtime gives lifecycle and board code a
|
||||
single dependency-free definition of a non-business child.
|
||||
"""
|
||||
if isinstance(value_or_metadata, Mapping):
|
||||
nested = value_or_metadata.get("metadata")
|
||||
metadata = nested if isinstance(nested, Mapping) else value_or_metadata
|
||||
else:
|
||||
metadata = getattr(value_or_metadata, "metadata", None)
|
||||
data = dict(metadata or {}) if isinstance(metadata, Mapping) else {}
|
||||
return (
|
||||
bool(data.get("attention_work_item", False))
|
||||
or is_report_execution_work_item_metadata(data)
|
||||
or is_review_execution_work_item_metadata(data)
|
||||
)
|
||||
|
||||
|
||||
def should_hide_work_item_from_company_kanban(metadata: Mapping[str, Any] | None) -> bool:
|
||||
"""True when the kanban UI should not display this work item."""
|
||||
data = dict(metadata or {})
|
||||
@@ -316,40 +339,45 @@ def is_runnable(phase: Phase) -> bool:
|
||||
return phase in RUNNABLE_PHASES
|
||||
|
||||
|
||||
# Phases whose runtime claim, when released as stale (process restart, crashed
|
||||
# session), allow the dispatcher to re-pick the card. Without this set, a card
|
||||
# that was actively running when the process died becomes a zombie: phase still
|
||||
# says RUNNING / WAITING_FOR_*, but no session is alive to make progress.
|
||||
_RESUMABLE_AFTER_STALE_CLAIM: frozenset[Phase] = frozenset({
|
||||
Phase.RUNNING,
|
||||
Phase.WAITING_FOR_PEER,
|
||||
Phase.WAITING_FOR_CHILDREN,
|
||||
Phase.PAUSED,
|
||||
Phase.NEEDS_ATTENTION,
|
||||
Phase.AWAITING_MANAGER_REVIEW,
|
||||
Phase.AWAITING_HUMAN,
|
||||
})
|
||||
# A process restart invalidates every persisted runtime claim, including claims
|
||||
# on passive review states. Releasing a dead claim and allowing the original
|
||||
# worker to execute again are intentionally separate decisions: review parents
|
||||
# must lose their dead claim but remain passive while their report/review
|
||||
# auxiliaries resume.
|
||||
_STALE_CLAIM_RELEASABLE_PHASES: frozenset[Phase] = (
|
||||
IN_PROGRESS_PHASES | IN_REVIEW_PHASES
|
||||
)
|
||||
_RESUMABLE_AFTER_CLAIM_RELEASE_PHASES: frozenset[Phase] = IN_PROGRESS_PHASES
|
||||
|
||||
|
||||
def is_stale_claim_releasable(phase: Phase) -> bool:
|
||||
"""Whether startup recovery may clear a dead runtime claim.
|
||||
|
||||
This includes passive review/human-wait states so a crashed resident
|
||||
session cannot retain ownership forever. It does *not* imply that the
|
||||
original work item may be dispatched again.
|
||||
"""
|
||||
return phase in _STALE_CLAIM_RELEASABLE_PHASES
|
||||
|
||||
|
||||
def is_resumable_after_claim_release(phase: Phase) -> bool:
|
||||
"""True iff a stale claim on this phase can be released and the card
|
||||
re-picked up by the dispatcher (rather than left as a zombie).
|
||||
"""Whether the original worker may resume after its claim is released.
|
||||
|
||||
Used by the periodic stale-claim sweeper. Every in-flight phase must
|
||||
return True here — the invariant test in
|
||||
test_phase_state_machine_invariants.py enforces this.
|
||||
Active execution phases can be re-picked after a crash. Passive
|
||||
``AWAITING_*`` parents cannot: their report/review chain (or human) owns
|
||||
forward progress.
|
||||
"""
|
||||
return phase in _RESUMABLE_AFTER_STALE_CLAIM
|
||||
return phase in _RESUMABLE_AFTER_CLAIM_RELEASE_PHASES
|
||||
|
||||
|
||||
def is_orphaned(item: Any) -> bool:
|
||||
"""A work item is orphaned when its phase says 'in flight' but no
|
||||
runtime session currently holds a claim on it.
|
||||
|
||||
Typical scenario: the process that owned the claim died (restart,
|
||||
crash). On startup the stale-claim sweeper clears the claim
|
||||
metadata; this function then lets the dispatcher re-pick the card
|
||||
on the next tick, eliminating zombie work items (Bug C).
|
||||
Typical scenario: the process that owned an execution claim died. On
|
||||
startup the stale-claim sweeper clears the claim metadata; this function
|
||||
then lets the dispatcher re-pick active execution, but never a passive
|
||||
review parent.
|
||||
"""
|
||||
if not is_resumable_after_claim_release(item.phase):
|
||||
return False
|
||||
|
||||
@@ -31,6 +31,7 @@ from opc.core.models import Phase, TaskStatus
|
||||
from opc.layer2_organization.phase import (
|
||||
DONE_PHASES,
|
||||
RUNNABLE_PHASES,
|
||||
is_stale_claim_releasable,
|
||||
register_phase_transition_hook,
|
||||
task_status_for_phase,
|
||||
)
|
||||
@@ -365,6 +366,17 @@ def _active_focus_id(session: Any, work_item_by_id: dict[str, Any]) -> str:
|
||||
return ""
|
||||
if getattr(focused_item, "phase", None) in DONE_PHASES:
|
||||
return ""
|
||||
phase = getattr(focused_item, "phase", None)
|
||||
if is_stale_claim_releasable(phase):
|
||||
claim = str(
|
||||
getattr(focused_item, "claimed_by_role_runtime_session_id", "") or ""
|
||||
).strip()
|
||||
if not claim:
|
||||
# On a live transition the focused item retains its claim. After
|
||||
# restart the startup sweep removes that dead-process claim; the
|
||||
# persisted session focus must stop blocking the orphaned item (or
|
||||
# a passive parent's report/review helper) from being dispatched.
|
||||
return ""
|
||||
return focused
|
||||
|
||||
|
||||
|
||||
@@ -30,6 +30,29 @@ from typing import Any, Mapping
|
||||
from opc.core.models import Phase
|
||||
|
||||
|
||||
MANAGER_DISPATCH_TURN_METADATA_KEYS: tuple[str, ...] = (
|
||||
"manager_board_mutation_performed",
|
||||
"manager_board_modified_work_item_ids",
|
||||
"manager_board_deleted_work_item_ids",
|
||||
"manager_no_delegation_justification",
|
||||
"no_delegation_justification",
|
||||
"manager_dispatch_guard_unresolved",
|
||||
)
|
||||
|
||||
|
||||
def reset_manager_dispatch_turn_metadata(metadata: Mapping[str, Any] | None) -> dict[str, Any]:
|
||||
"""Return mutable metadata with prior manager-turn outcomes removed.
|
||||
|
||||
These keys describe what happened in one agent turn. They must not be
|
||||
carried into a retry, rework turn, or user follow-up; durable board state
|
||||
(dependencies and child mutation revisions) is intentionally untouched.
|
||||
"""
|
||||
result = dict(metadata or {})
|
||||
for key in MANAGER_DISPATCH_TURN_METADATA_KEYS:
|
||||
result.pop(key, None)
|
||||
return result
|
||||
|
||||
|
||||
class TurnMode(str, Enum):
|
||||
EXECUTE = "execute"
|
||||
DELEGATE = "delegate"
|
||||
|
||||
@@ -155,25 +155,13 @@ def is_delivery_turn(value_or_metadata: Any) -> bool:
|
||||
|
||||
|
||||
def is_manager_reviewable_turn(value_or_metadata: Any) -> bool:
|
||||
"""Return True when a finished WorkItem should enter manager review flow.
|
||||
"""Return the turn type's default manager-review policy.
|
||||
|
||||
An explicit persisted ``turn_output_kind`` wins over the turn-type
|
||||
default: delegation-kind turns (dispatch/intake/plan) are review-exempt
|
||||
only because their deliverable is normally a child card set that gets
|
||||
reviewed per child. When such a turn completed with the manager's own
|
||||
work product instead, the done-transition stamps
|
||||
``turn_output_kind=self_produced`` on the WorkItem and that output is
|
||||
reviewable like any execute turn — every consumer of this predicate
|
||||
(DONE routing, report spawn, report completion, recovery scans) follows
|
||||
automatically.
|
||||
Dispatch/intake/plan are normally board-producing turns and therefore
|
||||
exempt. The executor handles the one dynamic exception — a current turn
|
||||
that produced no board mutation — before it writes the authoritative
|
||||
WorkItem phase. Downstream review plumbing follows that phase directly.
|
||||
"""
|
||||
metadata: Mapping[str, Any] | None = None
|
||||
if isinstance(value_or_metadata, Mapping):
|
||||
metadata = value_or_metadata
|
||||
elif hasattr(value_or_metadata, "metadata"):
|
||||
metadata = getattr(value_or_metadata, "metadata", None) or {}
|
||||
if metadata and str(metadata.get("turn_output_kind", "") or "").strip().lower() == "self_produced":
|
||||
return True
|
||||
turn_type = _turn_type_for_value(value_or_metadata, fallback="")
|
||||
if not turn_type:
|
||||
return False
|
||||
|
||||
@@ -2230,6 +2230,11 @@ def create_collaboration_tools(
|
||||
task.metadata = dict(task.metadata)
|
||||
task.metadata["delegation_wait_for_work_item_ids"] = parent_dependency_ids
|
||||
task.metadata["manager_board_parent_work_item_id"] = parent_work_item_id
|
||||
# Reusing an existing scope-key match is not a current-turn board
|
||||
# mutation. Only newly persisted business children make this
|
||||
# attempt a delegated-board completion.
|
||||
if resolved_pending_items:
|
||||
task.metadata["manager_board_mutation_performed"] = True
|
||||
if attention_work_item_id:
|
||||
task.metadata["attention_business_parent_work_item_id"] = parent_work_item_id
|
||||
task.metadata["attention_work_item_id"] = attention_work_item_id
|
||||
|
||||
Reference in New Issue
Block a user