fix(tasks): persist work-order waits without restarting runs
Dispatch receipts previously requeued active investigations and the final-only completion gate then failed waiting generations. Preserve the run, accept current interim wait reports, and wake only on actionable feedback or recovery evidence. Regression tests cover dispatch, same-generation reports, receipt revisions, cancellation and replacement ordering.
This commit is contained in:
@@ -19,36 +19,53 @@ if TYPE_CHECKING:
|
||||
RECOVERY_WORKFLOWS = {"burst_report", "scada_anomaly"}
|
||||
|
||||
|
||||
def track_work_order(task: dict, action: dict) -> None:
|
||||
def track_work_order(task: dict, action: dict) -> bool:
|
||||
if task.get("workflow") not in RECOVERY_WORKFLOWS or action["kind"] != "create_work_order":
|
||||
return
|
||||
return False
|
||||
receipt = action.get("receipt") or {}
|
||||
if action.get("execution_status") != "succeeded" or receipt.get("adapter") != "peripheral_sample":
|
||||
return
|
||||
return False
|
||||
context = task.setdefault("handling_context", {})
|
||||
orders = context.setdefault("work_orders", {})
|
||||
order_id = receipt["receipt_id"]
|
||||
if order_id not in orders:
|
||||
proposed_at = action.get("proposed_at")
|
||||
for prior in orders.values():
|
||||
terminal_at = prior.get("terminal_recorded_at")
|
||||
# Only a new decision after the failed field attempt replaces it.
|
||||
# A delayed receipt for an older independent dispatch does not.
|
||||
if (not prior.get("superseded_by") and prior["status"] in {"failed", "cancelled"}
|
||||
and proposed_at and terminal_at
|
||||
and datetime.fromisoformat(proposed_at) > datetime.fromisoformat(terminal_at)):
|
||||
prior["superseded_by"] = order_id
|
||||
orders[order_id] = {"status": "awaiting_completion", "action_id": str(action["action_id"])}
|
||||
context.update(phase="waiting_work_order", title="等待现场工单返回")
|
||||
context.pop("verification_evidence_id", None)
|
||||
return True
|
||||
|
||||
|
||||
def accept_completion(task: dict, feedback: dict) -> bool:
|
||||
def accept_completion(task: dict, feedback: dict, *, recorded_at: datetime | None = None) -> bool:
|
||||
context = task.get("handling_context") or {}
|
||||
order_id = feedback["source_event_id"].removeprefix("peripheral_sample:")
|
||||
order = context.get("work_orders", {}).get(order_id)
|
||||
if order is None:
|
||||
return False
|
||||
order.update(status=feedback["status"], source_revision=feedback["source_revision"])
|
||||
if feedback["status"] in {"failed", "cancelled"}:
|
||||
if recorded_at is not None and not order.get("terminal_recorded_at"):
|
||||
order["terminal_recorded_at"] = recorded_at.isoformat()
|
||||
else:
|
||||
order.pop("terminal_recorded_at", None)
|
||||
# The synchronizer preserves the field activity's business time on ordinary
|
||||
# edits. A revised field receipt can explicitly correct the verification start.
|
||||
if feedback["status"] == "completed":
|
||||
order["returned_at"] = feedback["occurred_at"]
|
||||
else:
|
||||
order.pop("returned_at", None)
|
||||
orders = list(context["work_orders"].values())
|
||||
if all(item["status"] == "completed" for item in orders):
|
||||
if order.get("superseded_by"):
|
||||
return True
|
||||
orders = [item for item in context["work_orders"].values() if not item.get("superseded_by")]
|
||||
if orders and all(item["status"] == "completed" for item in orders):
|
||||
returned_at = max((item["returned_at"] for item in orders), key=datetime.fromisoformat)
|
||||
previous = context.get("returned_at")
|
||||
if (previous is None or datetime.fromisoformat(previous) != datetime.fromisoformat(returned_at)
|
||||
|
||||
Reference in New Issue
Block a user