From ce1563fc493ea0c88f83ede6b4e09c338bf9f994 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 2 Nov 2026 12:02:14 +0530 Subject: [PATCH] fix(collaboration): retain original handoff proof failures in return readback Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../dashboard/src/data/chat-model.ts | 2 +- .../personal-workspace/collaboration-card.tsx | 6 +- .../src/features/personal-workspace/i18n.tsx | 2 + .../return-delivery-status.tsx | 10 ++- .../chat-recovery.mjs | 17 ++++ .../collaboration/return_delivery.ts | 7 +- .../manager_return_transaction.test.ts | 58 +++++++++++++ tests/test_collaboration_goal_instance.py | 82 ++++++++++++++++++- 8 files changed, 176 insertions(+), 8 deletions(-) diff --git a/apps/presentation/dashboard/src/data/chat-model.ts b/apps/presentation/dashboard/src/data/chat-model.ts index 0bafe53d7a..751887fb8f 100644 --- a/apps/presentation/dashboard/src/data/chat-model.ts +++ b/apps/presentation/dashboard/src/data/chat-model.ts @@ -101,7 +101,7 @@ export type CollaborationReadback = { }; read_status: string; decision: string; - returns: { phase: string; status: string; request_revision?: number | null }[]; + returns: { phase: string; status: string; error?: string | null; request_revision?: number | null }[]; }; export type ChatRouteCandidate = { diff --git a/apps/presentation/dashboard/src/features/personal-workspace/collaboration-card.tsx b/apps/presentation/dashboard/src/features/personal-workspace/collaboration-card.tsx index ea03a39c53..c67facf8b1 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/collaboration-card.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/collaboration-card.tsx @@ -4,6 +4,7 @@ import {useEffect, useState} from "react"; import type {RequestControlView} from "../../data/request-control"; import {RequestControl} from "./request-control"; import { useWorkspaceI18n } from "./i18n"; +import { needsOriginalDeliveryProof } from "./return-delivery-status"; import "./collaboration-card.css"; const copy = { @@ -26,7 +27,7 @@ const copy = { }; export function CollaborationCard({ request }: { request?: CollaborationReadback }) { - const { locale } = useWorkspaceI18n(); + const { locale, t } = useWorkspaceI18n(); const [observed, setObserved] = useState(null); const [unavailable, setUnavailable] = useState(false); const [editing, setEditing] = useState(false); @@ -63,7 +64,8 @@ export function CollaborationCard({ request }: { request?: CollaborationReadback {revision !== undefined ? {locale === "zh-CN" ? "请求版本" : "Request revision"} {revision} : null} {readStatus === "supplied" ? c.supplied : readStatus === "unavailable" ? c.unavailable : c.pending} {c.decision}: {decision} - {conclusion ? {conclusion.status === "delivered" ? c.delivered + {conclusion ? {needsOriginalDeliveryProof(conclusion) ? t("returnDelivery.originalHandoff") + : conclusion.status === "delivered" ? c.delivered : conclusion.status === "queued" ? c.result : c.unverified} : null}

{decisionReason && !unavailable ?

{c.reason}: {decisionReason}

: null} diff --git a/apps/presentation/dashboard/src/features/personal-workspace/i18n.tsx b/apps/presentation/dashboard/src/features/personal-workspace/i18n.tsx index 7cbc2dd122..1f19d0ae9d 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/i18n.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/i18n.tsx @@ -1624,6 +1624,7 @@ const en = { "timeline.gateHistory": "{count} historical Gates", "timeline.pending": "Working…", "returnDelivery.queued": "Return queued", + "returnDelivery.originalHandoff": "Original handoff needs verification", "returnDelivery.verifying": "Verifying delivery without resending", "returnDelivery.delivered": "Delivered to the original audience", "returnDelivery.reconciled": "Delivery verified after recovery", @@ -3285,6 +3286,7 @@ const zhCN: Record = { "timeline.gateHistory": "{count} 项历史 Gate", "timeline.pending": "正在整理…", "returnDelivery.queued": "等待回传", + "returnDelivery.originalHandoff": "原始交接待核验", "returnDelivery.verifying": "正在核验送达,不会重复发送", "returnDelivery.delivered": "已送达原受众", "returnDelivery.reconciled": "恢复后已核验送达", diff --git a/apps/presentation/dashboard/src/features/personal-workspace/return-delivery-status.tsx b/apps/presentation/dashboard/src/features/personal-workspace/return-delivery-status.tsx index 00da89919b..ca68cd99d4 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/return-delivery-status.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/return-delivery-status.tsx @@ -1,10 +1,16 @@ import { useWorkspaceI18n } from "./i18n"; import type { WorkspaceReturnDelivery } from "./personal-workspace-model"; +export function needsOriginalDeliveryProof(delivery?: Pick) { + return delivery?.error === "initial_delivery_receipt_unavailable" + && delivery.status !== "delivered" && delivery.status !== "superseded"; +} + export function ReturnDeliveryStatus({ delivery }: { delivery?: WorkspaceReturnDelivery }) { const { t, locale } = useWorkspaceI18n(); if (!delivery) return null; - const label = delivery.status === "delivered" + const needsProof = needsOriginalDeliveryProof(delivery); + const label = needsProof ? t("returnDelivery.originalHandoff") : delivery.status === "delivered" ? delivery.verification === "reconciled_after_restart" ? t("returnDelivery.reconciled") : t("returnDelivery.delivered") @@ -13,7 +19,7 @@ export function ReturnDeliveryStatus({ delivery }: { delivery?: WorkspaceReturnD : delivery.status === "explicit_unverified" ? t("returnDelivery.unverified") : t("returnDelivery.queued"); - const tone = delivery.status === "delivered" + const tone = needsProof ? "explicit_unverified" : delivery.status === "delivered" ? "delivered" : delivery.status === "verification_required" ? "verification_required" diff --git a/examples/personal-workspace-browser/chat-recovery.mjs b/examples/personal-workspace-browser/chat-recovery.mjs index 8bf00fe222..171e79b838 100644 --- a/examples/personal-workspace-browser/chat-recovery.mjs +++ b/examples/personal-workspace-browser/chat-recovery.mjs @@ -156,6 +156,16 @@ export const chatRecoveryScenario = { await page.screenshot({ path: resolve(outputDir, "collaboration-unreadable-mobile.png"), fullPage: false, animations: "disabled" }); await page.setViewportSize({ width: 1512, height: 982 }); if (api.turnRequests.length !== turnsBeforeReturn) throw new Error("Disposition readback started another model turn"); + delegatedMessage.collaboration.returns = [{ phase: "conclusion", status: "retry_pending", error: "initial_delivery_receipt_unavailable_extra" }]; + await collaboration.getByText("回复送达尚未核验", { exact: true }).waitFor({ state: "visible" }); + delegatedMessage.collaboration.returns[0].error = "initial_delivery_receipt_unavailable"; + await collaboration.getByText("原始交接待核验", { exact: true }).waitFor({ state: "visible" }); + await collaboration.evaluate((node) => node.scrollIntoView({ block: "start" })); + await page.screenshot({ path: resolve(outputDir, "collaboration-original-handoff-desktop.png"), fullPage: false, animations: "disabled" }); + await page.setViewportSize({ width: 390, height: 844 }); + if (await collaboration.evaluate((node) => node.scrollWidth > node.clientWidth + 1)) throw new Error("Original handoff explanation overflows on mobile"); + await page.screenshot({ path: resolve(outputDir, "collaboration-original-handoff-mobile.png"), fullPage: false, animations: "disabled" }); + await page.setViewportSize({ width: 1512, height: 982 }); pass("collaboration-brief", "Original conversation preserves context, constraints, inputs and receiver decision without a new turn"); const returnText = "处理结论:已核验新约束并关联现有计划,无需再次追问。"; @@ -189,13 +199,20 @@ export const chatRecoveryScenario = { await page.screenshot({ path: resolve(outputDir, "manager-automatic-conclusion.png"), fullPage: false, animations: "disabled" }); const returnedMessage = page.__loopxRuntime.messages.get(returnSessionId) .find((message) => message.message_id === "handoff.browser-fixture"); + returnedMessage.return_delivery = { schema_version: "manager_return_delivery_status_v0", phase: "conclusion", + status: "retry_pending", error: "initial_delivery_receipt_unavailable_extra" }; + await richConclusion.getByText("等待回传", { exact: true }).waitFor({ state: "visible" }); + returnedMessage.return_delivery.error = "initial_delivery_receipt_unavailable"; + await richConclusion.getByText("原始交接待核验", { exact: true }).waitFor({ state: "visible" }); returnedMessage.return_delivery = { schema_version: "manager_return_delivery_status_v0", phase: "conclusion", status: "delivered", verification: "reconciled_after_restart", + error: "initial_delivery_receipt_unavailable", }; await page.getByText("恢复后已核验送达", { exact: true }).waitFor({ state: "visible", timeout: 10_000 }); + if ((await richConclusion.innerText()).includes("原始交接待核验")) throw new Error("A stale error cannot override verified delivery"); await page.setViewportSize({ width: 390, height: 844 }); await page.getByText(returnText, { exact: true }).waitFor({ state: "visible" }); await richConclusion.scrollIntoViewIfNeeded(); diff --git a/loopx/control_plane/collaboration/return_delivery.ts b/loopx/control_plane/collaboration/return_delivery.ts index fb26ebb6b5..b7bb5a2e98 100644 --- a/loopx/control_plane/collaboration/return_delivery.ts +++ b/loopx/control_plane/collaboration/return_delivery.ts @@ -294,7 +294,9 @@ function admitReturnLifetime(command: ReturnCommand, operation: string): void { initial_delivery: facts.initial_delivery, }); if (decision.kind === "reject" || decision.kind === "omit") { - throw new EffectRuntimeRequestError("return Goal lifetime admission rejected"); + throw new EffectRuntimeRequestError("return Goal lifetime admission rejected", + decision.kind === "reject" && decision.code === "initial_delivery_unproved" + ? "initial_delivery_receipt_unavailable" : "invalid_request"); } } @@ -396,6 +398,9 @@ async function prepareReturn(command: ReturnCommand, state: JsonObject): Promise if (requireJsonObject(command.facts.route, "source route").kind === "peer") return returnResponse(); admitReturnLifetime(command, "original_return_admit"); } catch (error) { + if (error instanceof EffectRuntimeRequestError && error.code === "initial_delivery_receipt_unavailable") { + return sourceFailure(command, state, error.code); + } if (error instanceof EffectRuntimeRequestError || error instanceof SyntaxError || (error as NodeJS.ErrnoException).code) return sourceFailure(command, state); throw error; diff --git a/tests/control_plane_ts/manager_return_transaction.test.ts b/tests/control_plane_ts/manager_return_transaction.test.ts index 564cea791f..f34fc1edbd 100644 --- a/tests/control_plane_ts/manager_return_transaction.test.ts +++ b/tests/control_plane_ts/manager_return_transaction.test.ts @@ -356,3 +356,61 @@ for (const [clock, retry] of [ } finally { await f.close(); } }); } + +for (const exact of [false, true]) { + for (const accepted of [false, true]) { + test(`initial receipt conflict preserves its cause and original return (${exact}, accepted=${accepted})`, async () => { + const f = await fixture(exact, true); + try { + if (accepted) { + const plan = await f.run("prepare"); + await f.run("attempt", { ...plan, attempt }); + } + const sourcePaths = [ + f.entryPath, join(f.folder, "roundtrips", id + ".json"), + join(f.folder, "replies", id, "conclusion.json"), + ]; + const original = await Promise.all(sourcePaths.map(path => readFile(path))); + const before = accepted ? await readFile(f.statePath) : null; + const initial = f.facts.initial_delivery as JsonObject; + const conflicting = { ...f.facts, initial_delivery: { ...initial, + turn: { ...(initial.turn as JsonObject), context_handoff_receipt: { + request_id: "f".repeat(64), goal_id: "research", agent_id: "another-worker", + } }, + } }; + const result = await f.run("prepare", { facts: conflicting, + now: "2026-01-02T03:06:05.123456+00:00" }); + assert.equal(result.action, "skip", "a different receiver's receipt cannot authorize a return"); + if (exact) { + assert.equal(result.source_unavailable, true); + if (before) assert.deepEqual(await readFile(f.statePath), before); + else await assert.rejects(readFile(f.statePath)); + } else { + const held = await f.state(); + assert.equal(held.error, "initial_delivery_receipt_unavailable"); + assert.equal(held.status, accepted ? "explicit_unverified" : "retry_pending"); + if (accepted) { + assert.deepEqual(held.attempt, attempt, "an accepted write is never reset into a fresh send"); + assert.equal((await f.run("prepare", { now: "2026-01-03T03:06:05Z" })).processed, 0); + assert.deepEqual(await f.state(), held); + } else { + const bytes = await readFile(f.statePath); + assert.equal((await f.run("prepare", { now: "2026-01-02T03:06:06Z" })).processed, 0); + assert.deepEqual(await readFile(f.statePath), bytes, "unchanged source honors the original backoff"); + const restored = { ...f.facts, initial_delivery: { ...initial, + turn: { ...(initial.turn as JsonObject), context_handoff_receipt: initial.request }, + } }; + const plan = await f.run("prepare", { facts: restored, now: "2026-01-02T03:06:16Z" }); + assert.equal(plan.action, "send"); + await f.run("settle", { ...plan, facts: restored, now: "2026-01-02T03:06:16Z", + outcome: { kind: "sent", reply_verified: true, idempotency_key: attempt.provider_receipt } }); + assert.equal((await f.state()).message_id, plan.message_id); + assert.equal((await f.run("prepare", { facts: restored, now: "2026-01-03T03:06:16Z" })).processed, 0); + } + } + const retained = await Promise.all(sourcePaths.map(path => readFile(path))); + assert.deepEqual(retained, original, "diagnosis never rewrites the request, source route or result"); + } finally { await f.close(); } + }); + } +} diff --git a/tests/test_collaboration_goal_instance.py b/tests/test_collaboration_goal_instance.py index d55ad90dac..7374e471e9 100644 --- a/tests/test_collaboration_goal_instance.py +++ b/tests/test_collaboration_goal_instance.py @@ -281,7 +281,7 @@ def _external_manager_request(root: Path, registry: Path): "adopt", "Work in A.", registry=registry, - caller_goal_ref=receipt["goal_ref"], + caller_goal_ref=receipt.get("goal_ref"), ) report( root, @@ -291,11 +291,89 @@ def _external_manager_request(root: Path, registry: Path): "conclusion", "A completed its bounded review.", registry=registry, - caller_goal_ref=receipt["goal_ref"], + caller_goal_ref=receipt.get("goal_ref"), ) return store, session, receipt +@pytest.mark.parametrize("exact", [False, True]) +@pytest.mark.parametrize("external", [False, True], ids=["private", "external"]) +def test_original_receipt_conflict_readback_recovers_the_same_result(tmp_path, exact, external): + if exact: + registry = _create_source_registry(tmp_path) + else: + registry = tmp_path / "registry.json" + registry.write_text(json.dumps({"goals": [{ + "id": "delivery", "repo": str(tmp_path), "status": "active", + "coordination": {"registered_agents": ["builder", "reviewer"]}, + }]}), encoding="utf-8") + if external: + store, session, receipt = _external_manager_request(tmp_path, registry) + else: + store, session, receipt = _manager_request(tmp_path, registry) + acknowledge(tmp_path, "delivery", "builder", receipt["request_id"], "adopt", + "Checking the public input.", registry=registry, + caller_goal_ref=receipt.get("goal_ref")) + report(tmp_path, "delivery", "builder", receipt["request_id"], "conclusion", + "A completed its bounded review.", registry=registry, + caller_goal_ref=receipt.get("goal_ref")) + rid = receipt["request_id"] + original = store.turn_for_client(session["session_id"], "owner-request") + store.append_message(session["session_id"], role="user", text=original["message"], + turn_id=original["turn_id"], message_id="original-owner-request") + original_response = original["response"] + store.update_turn(session["session_id"], original["turn_id"], response={ + **original_response, "context_handoff_receipt": { + **receipt, "request_id": "f" * 64, "agent_id": "reviewer", + }, + }) + result_path = _root(tmp_path) / "replies" / rid / "conclusion.json" + result_bytes = result_path.read_bytes() + sends = [] + + def sender(route, source_session, source_turn, text): + sends.append((route, source_session, source_turn, text)) + return {"reply_verified": True, "idempotency_key": "sha256:" + "a" * 64} + + admitted_at = datetime(2026, 9, 28, tzinfo=timezone.utc) + assert drain(tmp_path, registry, store, sender, now=admitted_at) == (0 if exact else 1) + assert sends == [] + state_path = result_path.with_name("conclusion.delivery.json") + if exact: + assert not state_path.exists(), "an unproved exact request grants no receipt mutation" + else: + state = json.loads(state_path.read_text()) + assert state["status"] == "retry_pending" + assert state["error"] == "initial_delivery_receipt_unavailable" + before = state_path.read_bytes() + assert drain(tmp_path, registry, store, sender, + now=admitted_at + timedelta(seconds=1)) == 0 + assert state_path.read_bytes() == before + assert not any(m.get("origin") == "manager_followup" + for m in store.session_snapshot(session["session_id"])["messages"]) + # Only this disposable host fixture restores its original committed source. + store.update_turn(session["session_id"], original["turn_id"], response=original_response) + if not exact and not external: + snapshot = _http_snapshot(tmp_path, registry, session["session_id"]) + request = next(m["collaboration"] for m in snapshot["messages"] + if m.get("collaboration", {}).get("request_id") == rid) + assert request["returns"][0]["error"] == "initial_delivery_receipt_unavailable" + assert drain(tmp_path, registry, store, sender, + now=admitted_at + timedelta(seconds=11)) == 1 + assert len(sends) == int(external) + if external: + assert sends[0][0]["request_id"] == rid + assert sends[0][1]["session_id"] == session["session_id"] + assert sends[0][2]["turn_id"] == original["turn_id"] + assert result_path.read_bytes() == result_bytes + assert drain(tmp_path, registry, store, sender, + now=admitted_at + timedelta(days=1)) == 0 + messages = [m for m in _http_snapshot(tmp_path, registry, session["session_id"])["messages"] + if m.get("origin") == "manager_followup"] + assert len(messages) == 1 and messages[0]["turn_id"] == original["turn_id"] + assert messages[0]["return_delivery"]["status"] == "delivered" + + def test_recreated_goal_cannot_observe_or_mutate_prior_instance_requests( tmp_path: Path, ) -> None: