Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/presentation/dashboard/src/data/chat-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -26,7 +27,7 @@ const copy = {
};

export function CollaborationCard({ request }: { request?: CollaborationReadback }) {
const { locale } = useWorkspaceI18n();
const { locale, t } = useWorkspaceI18n();
const [observed, setObserved] = useState<RequestControlView | null>(null);
const [unavailable, setUnavailable] = useState(false);
const [editing, setEditing] = useState(false);
Expand Down Expand Up @@ -63,7 +64,8 @@ export function CollaborationCard({ request }: { request?: CollaborationReadback
{revision !== undefined ? <span>{locale === "zh-CN" ? "请求版本" : "Request revision"} {revision}</span> : null}
<span>{readStatus === "supplied" ? c.supplied : readStatus === "unavailable" ? c.unavailable : c.pending}</span>
<span>{c.decision}: {decision}</span>
{conclusion ? <span>{conclusion.status === "delivered" ? c.delivered
{conclusion ? <span>{needsOriginalDeliveryProof(conclusion) ? t("returnDelivery.originalHandoff")
: conclusion.status === "delivered" ? c.delivered
: conclusion.status === "queued" ? c.result : c.unverified}</span> : null}
</p>
{decisionReason && !unavailable ? <p>{c.reason}: {decisionReason}</p> : null}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -3285,6 +3286,7 @@ const zhCN: Record<WorkspaceMessageKey, string> = {
"timeline.gateHistory": "{count} 项历史 Gate",
"timeline.pending": "正在整理…",
"returnDelivery.queued": "等待回传",
"returnDelivery.originalHandoff": "原始交接待核验",
"returnDelivery.verifying": "正在核验送达,不会重复发送",
"returnDelivery.delivered": "已送达原受众",
"returnDelivery.reconciled": "恢复后已核验送达",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { useWorkspaceI18n } from "./i18n";
import type { WorkspaceReturnDelivery } from "./personal-workspace-model";

export function needsOriginalDeliveryProof(delivery?: Pick<WorkspaceReturnDelivery, "error" | "status">) {
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")
Expand All @@ -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"
Expand Down
17 changes: 17 additions & 0 deletions examples/personal-workspace-browser/chat-recovery.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "处理结论:已核验新约束并关联现有计划,无需再次追问。";
Expand Down Expand Up @@ -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();
Expand Down
7 changes: 6 additions & 1 deletion loopx/control_plane/collaboration/return_delivery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}

Expand Down Expand Up @@ -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;
Expand Down
58 changes: 58 additions & 0 deletions tests/control_plane_ts/manager_return_transaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(); }
});
}
}
82 changes: 80 additions & 2 deletions tests/test_collaboration_goal_instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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:
Expand Down
Loading