From c4c975571af831131f3e79585fc9a777bc16ba3d Mon Sep 17 00:00:00 2001
From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
Date: Wed, 7 Oct 2026 20:29:42 +0800
Subject: [PATCH 1/3] feat(app): confirm requester adoption from accepted
reports
Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
---
.../smoke/team-adoption-http-fixture.py | 97 ++++++++++
.../smoke/team-adoption-recovery-smoke.mjs | 112 ++++++++++++
apps/presentation/dashboard/src/data/chat.ts | 6 +
.../personal-workspace/goal-loopx-mode.css | 4 +
.../personal-workspace/goal-team-adoption.tsx | 168 ++++++++++++++++++
.../personal-workspace/goal-team-evidence.tsx | 13 +-
.../personal-workspace/goal-team-lineage.tsx | 5 +-
.../personal-workspace/goal-team-results.tsx | 9 +-
.../personal-workspace-browser/fixture.mjs | 10 +-
.../team-evidence.mjs | 76 ++++++++
loopx/chat_loopx_mode.py | 20 +++
.../control_plane/collaboration/chat_mode.ts | 11 +-
tests/control_plane_ts/chat_mode.test.ts | 15 ++
tests/test_chat_team_evidence.py | 103 +++++++++++
14 files changed, 637 insertions(+), 12 deletions(-)
create mode 100644 apps/presentation/dashboard/smoke/team-adoption-http-fixture.py
create mode 100644 apps/presentation/dashboard/smoke/team-adoption-recovery-smoke.mjs
create mode 100644 apps/presentation/dashboard/src/features/personal-workspace/goal-team-adoption.tsx
diff --git a/apps/presentation/dashboard/smoke/team-adoption-http-fixture.py b/apps/presentation/dashboard/smoke/team-adoption-http-fixture.py
new file mode 100644
index 0000000000..5bb987872a
--- /dev/null
+++ b/apps/presentation/dashboard/smoke/team-adoption-http-fixture.py
@@ -0,0 +1,97 @@
+"""Disposable real delegation/HTTP/SQLite owner; scripted host, no paid model."""
+from __future__ import annotations
+
+import json
+from pathlib import Path
+import sys
+import tempfile
+import threading
+from types import SimpleNamespace
+
+import pytest
+
+REPO = Path(__file__).resolve().parents[4]
+sys.path.insert(0, str(REPO))
+sys.path.insert(0, str(REPO / "tests"))
+
+def main() -> None:
+ from loopx.chat_runtime import ChatRuntimeController
+ from loopx.chat_server import ChatHTTPServer, ChatRequestHandler
+ from loopx.chat_store import ChatSessionStore
+ from test_local_delegation import brief, service, wait
+
+ with tempfile.TemporaryDirectory(prefix="loopx-team-adoption-") as directory:
+ patches = pytest.MonkeyPatch()
+ finalizers = []
+ server = None
+ try:
+ root, runner = service.__wrapped__(Path(directory), SimpleNamespace(
+ param="sqlite", addfinalizer=finalizers.append), patches)
+ config = json.loads(runner.config.read_text())
+ config["bindings"].append({**config["bindings"][0], "id": "synthesis", "agent_id": "reviewer",
+ "todo_id": "todo_reviewer-corrected", "workspace": str(root / "reviewer/corrected"),
+ "output_refs": ["output.json", "report.md"]})
+ runner.config.write_text(json.dumps(config))
+ runner.start("analysis", "analysis-1", brief())
+ source = wait(runner)
+ artifact = source["artifacts"][0]
+ input_file = root / "reviewer/corrected/accepted-input.json"
+ input_file.write_text(artifact["text"])
+ # Authored synthetic companion text; the real acceptance rule still
+ # checks JSON semantics, while artifact readback checks both versions.
+ (root / "reviewer/corrected/report.md").write_text(
+ "# Corrected evidence\n\nThe current-period normalized value is 25. "
+ "Different periods remain incomparable; the repost adds no independent source.\n")
+ runner.start("synthesis", "synthesis-1", {**brief(), "inputs": [{
+ "ref": "accepted-input.json", "description": "Accepted input", "sha256": artifact["sha256"],
+ "delegation": {"operation_id": "analysis-1", "ref": artifact["ref"], "relation": "uses"}}]})
+ assert wait(runner, "synthesis-1")["status"] == "accepted"
+ repo = root / "project"
+ executor = repo / ".loopx/config/delegations.json"
+ executor.parent.mkdir(parents=True)
+ executor.write_bytes(runner.config.read_bytes())
+ registry = json.loads(runner.registry.read_text())
+ registry["goals"][0]["spawn_policy"] = {"mode": "multi_subagent", "allowed": True,
+ "max_children": 2, "execution_config": ".loopx/config/delegations.json"}
+ runner.registry.write_text(json.dumps(registry))
+ store = ChatSessionStore(runner.root)
+ runtime = ChatRuntimeController(store=store, codex_bin="unused", registry_path=runner.registry)
+ store.create_session(goal_id=runner.goal_id, agent_id="codex", channel_id="goal." + runner.goal_id,
+ session_id="adoption", upstream_thread_id="fixture", upstream_mode="chat", adapter_kind="codex_app_server")
+ runtime.loopx_mode.apply("adoption", {"operation": "configure", "settings": {
+ "agent_id": "lead", "token_budget": 1000}}, work_dir=repo, objective="Read accepted work")
+ initial_session = store.load_session("adoption")
+ server = ChatHTTPServer(("127.0.0.1", 0), ChatRequestHandler)
+ server.verbose = False
+ server.registry_path, server.chat_store, server.runtime_controller = runner.registry, store, runtime
+ server.runtime_root_override, server.scan_roots, server.limit = str(runner.root), [], 20
+ thread = threading.Thread(target=server.serve_forever, daemon=True)
+ thread.start()
+ print(json.dumps({"origin": f"http://127.0.0.1:{server.server_port}"}), flush=True)
+ for command in sys.stdin:
+ operation = command.strip()
+ if operation == "withdraw":
+ input_file.write_text("{}")
+ print(json.dumps({"withdrawn": True}), flush=True)
+ elif operation == "restore":
+ input_file.write_text(artifact["text"])
+ print(json.dumps({"restored": True}), flush=True)
+ elif operation == "inspect":
+ observed = runner.read("analysis-1")
+ print(json.dumps({"result": observed, "session_unchanged": store.load_session("adoption") == initial_session,
+ "host_invocations": [(root / workspace / "host-invocations").read_text()
+ for workspace in ("analyst/initial", "reviewer/corrected")]}), flush=True)
+ else:
+ raise ValueError("Unknown fixture command")
+ finally:
+ if server:
+ server.shutdown()
+ server.server_close()
+ thread.join(timeout=5)
+ for cleanup in finalizers:
+ cleanup()
+ patches.undo()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/apps/presentation/dashboard/smoke/team-adoption-recovery-smoke.mjs b/apps/presentation/dashboard/smoke/team-adoption-recovery-smoke.mjs
new file mode 100644
index 0000000000..662ab8e2cc
--- /dev/null
+++ b/apps/presentation/dashboard/smoke/team-adoption-recovery-smoke.mjs
@@ -0,0 +1,112 @@
+// Packaged report interaction + real scoped HTTP/delegation/SQLite adoption.
+// Navigation is synthetic; no acceptance/adoption rule or result is mocked.
+import assert from "node:assert/strict";
+import {spawn} from "node:child_process";
+import {once} from "node:events";
+import {createInterface} from "node:readline";
+import {mkdir} from "node:fs/promises";
+import {resolve} from "node:path";
+import {resolveTestPython} from "../../../../scripts/test-python.mjs";
+import {launchBrowser, loadPlaywright, waitForHttp} from "../../../../examples/dashboard-browser-smoke-support.mjs";
+
+process.env.LOOPX_PERSONAL_WORKSPACE_PACKAGED = "1";
+const {repoRoot, outputDir, port, startServer} = await import("../../../../examples/personal-workspace-browser/fixture.mjs");
+const {openWorkspacePage} = await import("../../../../examples/personal-workspace-browser/scenario-context.mjs");
+const fixture = spawn(resolveTestPython({repoRoot}), ["-u", "apps/presentation/dashboard/smoke/team-adoption-http-fixture.py"],
+ {cwd: repoRoot, stdio: ["pipe", "pipe", "pipe"]});
+const exited = once(fixture, "exit");
+const lines = createInterface({input: fixture.stdout});
+const iterator = lines[Symbol.asyncIterator]();
+let stderr = "";
+fixture.stderr.on("data", chunk => {stderr += String(chunk);});
+async function next() {const line = await iterator.next(); assert.equal(line.done, false, stderr); return JSON.parse(line.value);}
+async function command(value) {fixture.stdin.write(`${value}\n`); return next();}
+let server, browser, workspace;
+try {
+ const {origin} = await next();
+ server = await startServer();
+ const url = `http://127.0.0.1:${port}/chat/?statusUrl=/status.json`;
+ await waitForHttp(url);
+ browser = await launchBrowser(loadPlaywright().chromium);
+ workspace = await openWorkspacePage({newPage: options => browser.newPage({locale: "zh-CN", ...options})}, url);
+ const {page, api} = workspace;
+ await page.locator(".personal-goal-link", {hasText: "Product Release"}).click();
+ await page.getByRole("navigation", {name: "Goal 视图"}).getByRole("button", {name: "对话", exact: true}).click();
+ await page.getByRole("button", {name: "开启 LoopX 模式", exact: true}).click();
+ await page.getByLabel("已注册的协调身份").selectOption("lead");
+ await page.getByLabel("协调员总 token 额度").fill("100000");
+ await page.getByRole("button", {name: "保存设置", exact: true}).click();
+ const configured = api.loopxModeRequests.findLast(row => row.operation === "configure");
+ const mode = page.__loopxRuntime.loopxModes.get(configured.sessionId);
+ const writes = [];
+ let loseAck = true;
+ await page.route("**/api/chat/sessions/*/loopx", async route => {
+ const body = route.request().method() === "POST" ? route.request().postDataJSON() : {};
+ if (!["read", "operations", "adopt"].includes(body.operation)) return route.fallback();
+ const response = await route.fetch({url: `${origin}/api/chat/sessions/adoption/loopx`,
+ headers: {...route.request().headers(), origin}});
+ if (body.operation === "adopt") {
+ writes.push(body);
+ if (loseAck) {
+ assert.equal(response.status(), 200, await response.text());
+ loseAck = false;
+ return route.fulfill({status: 503, json: {error: "Synthetic acknowledgement loss after real commit"}});
+ }
+ }
+ return route.fulfill({response});
+ });
+ Object.assign(mode, {enabled: true, paused: true, active_turn_id: null, native: {status: "paused", tokenBudget: 100000}});
+ const results = page.getByRole("region", {name: "团队成果", exact: true});
+ await results.getByRole("button", {name: "analyst · output.json", exact: true}).click();
+ await results.locator(".goal-team-result-reader > details > summary").click();
+ const adoption = results.locator(".goal-team-adoption");
+ await adoption.locator("summary").click();
+ await adoption.getByRole("button", {name: "查找使用此版本的后续结果", exact: true}).click();
+ await adoption.getByLabel("后续结果", {exact: true}).waitFor();
+ assert.equal(writes.length, 0);
+ assert.equal((await command("inspect")).result.adoptions, undefined);
+ await adoption.getByRole("button", {name: "阅读后续结果", exact: true}).click();
+ await adoption.getByLabel("证据内容: report.md").waitFor();
+ assert.equal(writes.length, 0);
+ const confirm = adoption.getByRole("button", {name: "确认采用于此结果", exact: true});
+ await confirm.focus(); await page.keyboard.press("Enter");
+ await adoption.getByText("采用已记录,指定版本与后续结果当前有效。", {exact: true}).waitFor();
+ assert.equal(writes.length, 1, "Lost acknowledgement reconciles the actual committed receipt without another write");
+ assert.deepEqual(writes[0], {operation: "adopt", operation_id: "analysis-1", consumer_operation_id: "synthesis-1"});
+ const recorded = await command("inspect");
+ assert.equal(recorded.result.adoptions.length, 1);
+ assert.equal(recorded.result.adoptions[0].state, "current");
+ assert.ok(recorded.session_unchanged, "No native lifecycle, mode, wake or extra coordinator Turn changed");
+ assert.deepEqual(recorded.host_invocations, ["1", "1"]);
+ await mkdir(outputDir, {recursive: true});
+ await adoption.getByText("采用已记录,指定版本与后续结果当前有效。", {exact: true}).scrollIntoViewIfNeeded();
+ await page.screenshot({path: resolve(outputDir, "team-adoption-owner-desktop.png"), animations: "disabled"});
+ await page.setViewportSize({width: 390, height: 844}); await page.emulateMedia({reducedMotion: "reduce"});
+ assert.ok(await adoption.evaluate(el => el.scrollWidth <= el.clientWidth));
+ await adoption.getByText("采用已记录,指定版本与后续结果当前有效。", {exact: true}).scrollIntoViewIfNeeded();
+ await page.screenshot({path: resolve(outputDir, "team-adoption-owner-mobile.png"), animations: "disabled"});
+ // Current source still renders, while a real changed receiver input revokes adoption.
+ await command("withdraw");
+ await results.getByRole("button", {name: "刷新成果", exact: true}).click();
+ await results.locator(".goal-team-result-reader > details > summary").click();
+ await results.getByText("采用证据已失效或无法核验", {exact: true}).waitFor();
+ assert.equal(await adoption.getByText("采用已记录,指定版本与后续结果当前有效。", {exact: true}).count(), 0);
+ await adoption.locator("summary").click();
+ await adoption.getByRole("button", {name: "查找使用此版本的后续结果", exact: true}).click();
+ await adoption.getByText("已检查的工作中没有可核验的使用结果。", {exact: true}).waitFor();
+ const refused = await fetch(`${origin}/api/chat/sessions/adoption/loopx`, {method: "POST", headers: {"Content-Type": "application/json", Origin: origin}, body: JSON.stringify(writes[0])});
+ assert.equal(refused.status, 409, await refused.text());
+ await command("restore");
+ await results.getByRole("button", {name: "刷新成果", exact: true}).click();
+ await results.locator(".goal-team-result-reader > details > summary").click();
+ await results.getByText("已记录采用 · 后续结果验收有效", {exact: true}).waitFor();
+ assert.equal(writes.length, 1, "Readback recovery never records a new adoption");
+ assert.equal(api.turnRequests.length, 0);
+ console.log("team-adoption-recovery: passed (packaged report, real HTTP/SQLite/checks/receipts, explicit decision, lost ACK, exact use withdrawal/recovery, no dispatch, mobile/keyboard)");
+} catch (error) {
+ if (workspace) console.error({body: (await workspace.page.locator("body").innerText()).slice(-3000), errors: workspace.errors});
+ throw error;
+} finally {
+ await workspace?.close(); await browser?.close(); server?.kill("SIGTERM");
+ fixture.stdin.end(); if (fixture.exitCode === null) await exited; lines.close();
+}
diff --git a/apps/presentation/dashboard/src/data/chat.ts b/apps/presentation/dashboard/src/data/chat.ts
index d466fb71b8..8f3c5aa4e2 100644
--- a/apps/presentation/dashboard/src/data/chat.ts
+++ b/apps/presentation/dashboard/src/data/chat.ts
@@ -1153,6 +1153,12 @@ export function readLoopXTeamWork(sessionId: string, operationId: string) {
method: "POST", body: JSON.stringify({operation: "read", operation_id: operationId}),
});
}
+export function adoptLoopXTeamWork(sessionId: string, operationId: string, consumerOperationId: string) {
+ return requestJson(`/api/chat/sessions/${sessionId}/loopx`, {
+ method: "POST", body: JSON.stringify({operation: "adopt", operation_id: operationId,
+ consumer_operation_id: consumerOperationId}),
+ });
+}
export type ManagedGoalResultRow = {
todo_id: string; title: string; producer_agent_id: string; sha256: string;
content_type: string; size_bytes: number; completed_at?: string | null;
diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-loopx-mode.css b/apps/presentation/dashboard/src/features/personal-workspace/goal-loopx-mode.css
index 6c414cd891..0f1489d022 100644
--- a/apps/presentation/dashboard/src/features/personal-workspace/goal-loopx-mode.css
+++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-loopx-mode.css
@@ -91,6 +91,10 @@ li[data-check="blocked"] .goal-team-check-pill { border-color: transparent; back
.goal-team-lineage ul { list-style: none; padding: 0; margin: 12px 0; }
.goal-team-lineage li { display: grid; gap: 8px; padding: 0 0 16px 16px; border-left: 2px solid var(--pw-line, #ebebeb); overflow-wrap: anywhere; }
.goal-team-lineage button { justify-self: start; max-width: 100%; overflow-wrap: anywhere; text-align: start; }
+.goal-team-adoption { margin-top: 16px; }
+.goal-team-adoption label { display: block; margin-block: 12px; }
+.goal-team-adoption select { display: block; max-width: 100%; margin-top: 6px; }
+.goal-team-adoption > button { margin-block: 6px; }
.goal-team-episode { margin-block: 20px; border: 1px solid var(--pw-line, #ebebeb); border-radius: 10px; padding: 16px; background: var(--pw-bg, #fafafa); }
.goal-team-episode-heading { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.goal-team-episode > p { color: var(--pw-muted, #666); line-height: 1.5; }
diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-team-adoption.tsx b/apps/presentation/dashboard/src/features/personal-workspace/goal-team-adoption.tsx
new file mode 100644
index 0000000000..d8661a6071
--- /dev/null
+++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-team-adoption.tsx
@@ -0,0 +1,168 @@
+import {useEffect, useRef, useState} from "react";
+import {adoptLoopXTeamWork, fetchLoopXTeamWork, readLoopXTeamWork, type DelegationReadback} from "../../data/chat";
+import {isMarkdownArtifact, TeamArtifactReport} from "./team-artifact-content";
+
+const accepted = (row: DelegationReadback) => row.status === "accepted"
+ && !row.error && !row.recovery_required && Boolean(row.artifacts?.length);
+const sameResult = (expected: DelegationReadback, observed: DelegationReadback) => accepted(observed)
+ && expected.operation_id === observed.operation_id && expected.request_id === observed.request_id
+ && expected.agent_id === observed.agent_id && expected.todo_id === observed.todo_id
+ && expected.artifacts?.length === observed.artifacts?.length
+ && expected.artifacts?.every(row => observed.artifacts?.some(item => item.ref === row.ref && item.sha256 === row.sha256));
+const uses = (source: DelegationReadback, consumer: DelegationReadback) => accepted(consumer)
+ && consumer.operation_id !== source.operation_id && consumer.dependencies?.every(link => link.state === "current")
+ && consumer.dependencies?.some(link =>
+ link.relation === "uses" && link.state === "current" && link.operation_id === source.operation_id
+ && source.artifacts?.some(row => row.ref === link.ref && row.sha256 === link.sha256));
+const hasReceipt = (source: DelegationReadback, consumer: DelegationReadback) => source.adoptions?.some(row =>
+ row.state === "current" && row.consumer_operation_id === consumer.operation_id
+ && row.consumer_request_id === consumer.request_id && row.consumer_agent_id === consumer.agent_id
+ && row.consumer_todo_id === consumer.todo_id && row.source_artifacts.length > 0
+ && row.source_artifacts.every(item => source.artifacts?.some(artifact => item.ref === artifact.ref && item.sha256 === artifact.sha256)
+ && consumer.dependencies?.some(link => link.relation === "uses" && link.state === "current"
+ && link.operation_id === source.operation_id && link.ref === item.ref && link.sha256 === item.sha256))
+ && row.consumer_artifacts.length === consumer.artifacts?.length
+ && consumer.artifacts?.every(item => row.consumer_artifacts.some(artifact => item.ref === artifact.ref && item.sha256 === artifact.sha256)));
+
+/** Explicit owner decision through the existing requester-scoped adoption owner.
+ * Discovery/read/reload never writes; the backend rechecks every confirmation. */
+export function GoalTeamAdoption({sessionId, result, zh, onObservation}: {
+ sessionId: string; result: DelegationReadback; zh: boolean;
+ onObservation: (result: DelegationReadback | null) => void;
+}) {
+ const [candidates, setCandidates] = useState([]);
+ const [selected, setSelected] = useState("");
+ const [consumer, setConsumer] = useState(null);
+ const [artifactRef, setArtifactRef] = useState("");
+ const [cursor, setCursor] = useState(null);
+ const [searched, setSearched] = useState(false);
+ const [busy, setBusy] = useState(false);
+ const [error, setError] = useState("");
+ const [incomplete, setIncomplete] = useState(false);
+ const [uncertain, setUncertain] = useState(false);
+ const generation = useRef(0);
+ const inspectedPages = useRef(0);
+ const versionKey = JSON.stringify(result.artifacts?.map(({ref, sha256}) => ({ref, sha256})));
+ useEffect(() => {
+ generation.current++; inspectedPages.current = 0;
+ setCandidates([]); setSelected(""); setConsumer(null); setArtifactRef(""); setCursor(null);
+ setSearched(false); setBusy(false); setError(""); setIncomplete(false); setUncertain(false);
+ return () => {generation.current++;};
+ }, [sessionId, result.operation_id, result.request_id, versionKey]);
+ const unavailable = zh ? "指定版本或后续结果无法核验;请重新读取证据。" : "The referenced version or downstream result cannot be verified; recheck the evidence.";
+ async function discover(next = false) {
+ const current = ++generation.current;
+ const previous = next ? candidates : [];
+ if (!next) inspectedPages.current = 0;
+ setBusy(true); setError(""); setConsumer(null); setUncertain(false);
+ if (!next) {setCandidates([]); setSelected(""); setSearched(false); setIncomplete(false);}
+ try {
+ const page = await fetchLoopXTeamWork(sessionId, next && cursor ? cursor : undefined);
+ if (current !== generation.current) return;
+ if (page.has_more && (!page.next_cursor || (next && cursor && page.next_cursor <= cursor))) {
+ throw new Error(zh ? "执行分页无法继续核验。" : "Execution paging cannot be verified.");
+ }
+ const matches: DelegationReadback[] = [];
+ let missing = !page.page_readback_complete;
+ // One bounded page at a time. A newer scope cancels queued reads.
+ for (const row of page.items) {
+ if (current !== generation.current) return;
+ if (!row.operation_id || row.operation_id === result.operation_id || row.status !== "accepted" || row.recovery_required) continue;
+ try {
+ const value = await readLoopXTeamWork(sessionId, row.operation_id);
+ if (value.operation_id === row.operation_id && uses(result, value)) matches.push(value);
+ } catch {missing = true;}
+ }
+ if (current !== generation.current) return;
+ const values = [...new Map([...previous, ...matches].map(row => [row.operation_id, row])).values()];
+ inspectedPages.current++;
+ setCandidates(values); setSelected(values[0]?.operation_id ?? ""); setSearched(true);
+ setCursor(page.has_more ? page.next_cursor : null); setIncomplete(old => old || missing);
+ } catch (failure) {
+ if (current === generation.current) {
+ setCandidates([]); setSelected(""); setCursor(null); setSearched(false);
+ setError(failure instanceof Error ? failure.message : String(failure));
+ }
+ } finally {if (current === generation.current) setBusy(false);}
+ }
+ async function read() {
+ const current = ++generation.current;
+ const expected = candidates.find(row => row.operation_id === selected);
+ setBusy(true); setError(""); setConsumer(null); setUncertain(false);
+ try {
+ const source = await readLoopXTeamWork(sessionId, result.operation_id).catch(() => null);
+ if (current !== generation.current) return;
+ if (!source || !sameResult(result, source)) {onObservation(null); return;}
+ onObservation(source);
+ const downstream = await readLoopXTeamWork(sessionId, selected);
+ if (current !== generation.current) return;
+ if (!expected || !sameResult(expected, downstream) || !uses(source, downstream)) throw new Error(unavailable);
+ setConsumer(downstream);
+ } catch (failure) {
+ if (current === generation.current) setError(failure instanceof Error ? failure.message : String(failure));
+ } finally {if (current === generation.current) setBusy(false);}
+ }
+ async function confirm() {
+ if (!consumer) return;
+ const current = ++generation.current;
+ setBusy(true); setError("");
+ try {
+ let source: DelegationReadback;
+ try {source = await adoptLoopXTeamWork(sessionId, result.operation_id, consumer.operation_id);}
+ catch (failure) {
+ // An acknowledgement may be lost after commit. Reconcile once without
+ // another write; a later retry retains this same source/consumer pair.
+ source = await readLoopXTeamWork(sessionId, result.operation_id);
+ if (!hasReceipt(source, consumer)) throw failure;
+ }
+ if (current !== generation.current) return;
+ if (!sameResult(result, source)) {onObservation(null); return;}
+ onObservation(source);
+ if (!hasReceipt(source, consumer)) throw new Error(unavailable);
+ setUncertain(false);
+ } catch (failure) {
+ if (current === generation.current) {
+ setUncertain(true);
+ setError((zh ? "采用结果尚未核实。重试将核验并记录同一版本与后续结果,不会重新派工。"
+ : "Adoption remains unconfirmed. Retry checks and records the same version and downstream result without dispatching work.")
+ + " " + (failure instanceof Error ? failure.message : String(failure)));
+ }
+ } finally {if (current === generation.current) setBusy(false);}
+ }
+ if (!accepted(result)) return null;
+ const recorded = consumer && hasReceipt(result, consumer) && !uncertain;
+ const artifact = consumer?.artifacts?.find(row => row.ref === artifactRef)
+ ?? consumer?.artifacts?.find(row => isMarkdownArtifact(row.ref)) ?? consumer?.artifacts?.[0];
+ return
+ {zh ? "确认采用于后续结果" : "Confirm adoption into a downstream result"}
+ {zh ? "先阅读使用此版本的已验收后续结果,再由此对话的协调身份记录采用。不会启动任务或补足独立验收证据。"
+ : "Read an accepted downstream result using this version, then record adoption as this conversation's coordinator. This starts no work and supplies no independent-verifier evidence."}
+ void discover()}>{zh ? "查找使用此版本的后续结果" : "Find downstream results using this version"}
+ {busy ? {zh ? "正在核验指定版本与后续结果…" : "Checking exact versions and downstream results…"}
: null}
+ {incomplete ? {zh ? "部分执行无法核验;列表不代表全部后续结果。" : "Some executions cannot be verified; this list does not represent all downstream results."}
: null}
+ {searched && !candidates.length ? {zh ? "已检查的工作中没有可核验的使用结果。" : "No verifiable uses result was found in the inspected work."}
: null}
+ {cursor && inspectedPages.current < 10 ? void discover(true)}>{zh ? "继续查找下一页" : "Search the next page"} : null}
+ {cursor && inspectedPages.current >= 10 ? {zh ? "本次已检查十页;仍有未检查工作,请核实原请求。" : "Ten pages inspected; more work remains uninspected. Reconcile the original request."}
: null}
+ {candidates.length ? <>
+ {zh ? "后续结果" : "Downstream result"}
+ {setSelected(event.target.value); setConsumer(null); setError("");}}>
+ {candidates.map(row => {row.agent_id} · {row.operation_id} )}
+
+
+ void read()}>{zh ? "阅读后续结果" : "Read downstream result"}
+ > : null}
+ {consumer ? <>
+ {artifact ? : null}
+ {consumer.artifacts && consumer.artifacts.length > 1 ? {zh ? "后续结果的其他产物" : "Other downstream artifacts"}
+ setArtifactRef(event.target.value)}>
+ {consumer.artifacts.map(row => {row.ref} )}
+
+ : null}
+ {recorded ? {zh ? "采用已记录,指定版本与后续结果当前有效。" : "Adoption recorded; the referenced version and downstream result are current."}
+ : void confirm()}>{uncertain
+ ? (zh ? "重试同一采用决定" : "Retry this adoption decision") : (zh ? "确认采用于此结果" : "Confirm adoption into this result")} }
+ > : null}
+ {error ? {error}
: null}
+ ;
+}
diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-team-evidence.tsx b/apps/presentation/dashboard/src/features/personal-workspace/goal-team-evidence.tsx
index 1f1b928182..b0b7ba6a13 100644
--- a/apps/presentation/dashboard/src/features/personal-workspace/goal-team-evidence.tsx
+++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-team-evidence.tsx
@@ -105,6 +105,10 @@ export function GoalTeamEvidence({sessionId, operationId, zh, canMessage, ingres
&& checkedOutputs?.length && checkedOutputs.length === result?.artifacts?.length
&& new Set(checkedOutputs.map(row => row.ref)).size === checkedOutputs.length
&& checkedOutputs.every(row => result?.artifacts?.some(artifact => artifact.ref === row.ref && artifact.sha256 === row.sha256)));
+ function observe(value: DelegationReadback | null) {
+ setResult(value); setObservedAt(value ? new Date().toLocaleTimeString() : "");
+ setError(value ? "" : (zh ? "关联执行或版本已变化;请重新读取证据。" : "A linked execution or version changed; recheck the evidence."));
+ }
return
{zh ? "执行证据" : "Execution evidence"}
void read()}>{zh ? "重新读取证据" : "Recheck evidence"}
@@ -113,14 +117,11 @@ export function GoalTeamEvidence({sessionId, operationId, zh, canMessage, ingres
{result ? <>
{result.agent_id} · {delegationStateLabel(result, zh)} {" · "}{observedAt}
{zh ? "按需读取的当前观察,不是持续在线状态;验收不代表协调员已采用。" : "An on-demand observation, not continuous liveness; acceptance does not establish coordinator adoption."}
- {
- setResult(value); setObservedAt(value ? new Date().toLocaleTimeString() : "");
- setError(value ? "" : (zh ? "关联执行或版本已变化;请重新读取证据。" : "A linked execution or version changed; recheck the evidence."));
- }}/>
+
{hasCorrectionPath ? {zh ? "版本与采用关系详情" : "Version and adoption details"}
-
- : }
+
+ : }
{result.error ? {result.error}
: null}
{result.status === "accepted" && !result.error && !result.recovery_required ?
{zh ? "本次验收依据" : "Current validation basis"}
diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-team-lineage.tsx b/apps/presentation/dashboard/src/features/personal-workspace/goal-team-lineage.tsx
index 8cf9ecdc56..42e4029a35 100644
--- a/apps/presentation/dashboard/src/features/personal-workspace/goal-team-lineage.tsx
+++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-team-lineage.tsx
@@ -1,8 +1,10 @@
import type {DelegationReadback} from "../../data/chat";
+import {GoalTeamAdoption} from "./goal-team-adoption";
/** Request intent, accepted output and requester decision retain distinct strength. */
-export function GoalTeamLineage({result, zh, onInspect}: {
+export function GoalTeamLineage({result, zh, onInspect, sessionId, onObservation}: {
result: DelegationReadback; zh: boolean; onInspect: (operationId: string) => void;
+ sessionId: string; onObservation: (result: DelegationReadback | null) => void;
}) {
const relations = zh ? {responds_to: "回应此版本", revises: "修订此版本", uses: "使用此版本"}
: {responds_to: "Respond to this version", revises: "Revise this version", uses: "Use this version"};
@@ -31,5 +33,6 @@ export function GoalTeamLineage({result, zh, onInspect}: {
{row.consumer_artifacts.map(artifact => {zh ? "结果" : "Result"}: {artifact.ref} · sha256:{artifact.sha256})}
)} : {zh ? "尚无请求方采用记录。" : "No requester adoption is recorded."}
}
+
;
}
diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-team-results.tsx b/apps/presentation/dashboard/src/features/personal-workspace/goal-team-results.tsx
index dc2ab35fa1..337619b0fd 100644
--- a/apps/presentation/dashboard/src/features/personal-workspace/goal-team-results.tsx
+++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-team-results.tsx
@@ -10,7 +10,7 @@ const readableRows = (page: DelegationInventory) => page.items.filter(row =>
const AUTO_DISCOVERY_PAGES = 3;
const MAX_INSPECTED_PAGES = 10;
-/** Read-only entry in the original conversation, using the same scoped delegation API. */
+/** Original-conversation result reader and explicit requester adoption. */
export function GoalTeamResults({sessionId, zh, refreshKey}: {sessionId: string; zh: boolean; refreshKey: string}) {
const [pages, setPages] = useState([]);
const [selection, setSelection] = useState<{result: DelegationReadback; artifact: TeamArtifact} | null>(null);
@@ -121,7 +121,12 @@ export function GoalTeamResults({sessionId, zh, refreshKey}: {sessionId: string;
: (zh ? `已检查 ${pages.length} 页;还有未检查的工作。` : `${pages.length} pages inspected; more work remains unseen.`)} : null}
: null}
{selection ?
-
{adoptionSummary} void read(operationId)}/>
+
{adoptionSummary} void read(operationId)}
+ sessionId={sessionId} onObservation={value => {
+ const artifact = value?.artifacts?.find(row => row.ref === selection.artifact.ref && row.sha256 === selection.artifact.sha256);
+ if (value && artifact) setSelection({result: value, artifact});
+ else {setSelection(null); setError(changed);}
+ }}/>
{selection.result.artifacts && selection.result.artifacts.length > 1 ?
{zh ? "其他产物" : "Other artifacts"} {const artifact = selection.result.artifacts!.find(row => row.ref === event.target.value); if (artifact) {
diff --git a/examples/personal-workspace-browser/fixture.mjs b/examples/personal-workspace-browser/fixture.mjs
index 629e6e796c..7709266dda 100644
--- a/examples/personal-workspace-browser/fixture.mjs
+++ b/examples/personal-workspace-browser/fixture.mjs
@@ -1644,7 +1644,13 @@ export async function installApi(page, { goalSubagentConfigurationEnabled = true
await route.fulfill({json: paused});
return;
}
- if (body.operation === "read") {
+ if (body.operation === "adopt") {
+ if (body.operation_id !== "accepted-analysis" || body.consumer_operation_id !== "accepted-synthesis") {
+ return route.fulfill({status: 409, json: {error: "Fixture adoption target unavailable"}});
+ }
+ current.fixtureAdoptionState = "current";
+ }
+ if (body.operation === "read" || body.operation === "adopt") {
if (current.fixtureTeamReadDelayMs) await new Promise(resolveWait => setTimeout(resolveWait, current.fixtureTeamReadDelayMs));
if (current.fixtureCorrectionEpisode && body.operation_id === "original-analysis") {
await route.fulfill({json: {ok: true, operation_id: body.operation_id, request_id: "request-original",
@@ -1679,7 +1685,7 @@ export async function installApi(page, { goalSubagentConfigurationEnabled = true
consumer_request_id: "request-synthesis", consumer_agent_id: "synthesizer", consumer_todo_id: "todo_synthesis",
source_artifacts: [{ref: "report.json", sha256: "d".repeat(64)}],
consumer_artifacts: [{ref: "synthesis.json", sha256: "e".repeat(64)},
- ...(current.fixturePlanTodoId ? [{ref: "report.md", sha256: "8".repeat(64)}] : [])], state: current.fixtureAdoptionState}]} : {}),
+ {ref: "report.md", sha256: "8".repeat(64)}], state: current.fixtureAdoptionState}]} : {}),
artifacts: [{ref: "report.json", sha256: "d".repeat(64),
text: '{"cash_flow":75,"note":""}'},
{ref: "report.md", sha256: "9".repeat(64), text: "# Cash allocation\n\n| Measure | Value |\n|---|---:|\n| Free cash | 75 |\n\n[Source](https://example.org/report)\n"}]}});
diff --git a/examples/personal-workspace-browser/team-evidence.mjs b/examples/personal-workspace-browser/team-evidence.mjs
index 2efa27dab2..509f83d925 100644
--- a/examples/personal-workspace-browser/team-evidence.mjs
+++ b/examples/personal-workspace-browser/team-evidence.mjs
@@ -269,6 +269,82 @@ export const teamEvidenceScenario = {
await content.waitFor();
assert.equal(api.turnRequests.length, 0, "Evidence reading must not start a model");
await page.screenshot({path: resolve(outputDir, "team-evidence-desktop.png"), animations: "disabled"});
+ // Reading a consumer is not adoption. Only the explicit owner decision
+ // writes; a lost post-commit acknowledgement reconciles without rewriting.
+ delete mode.fixtureAdoptionState;
+ await evidence.getByRole("button", {name: "重新读取证据", exact: true}).click();
+ await content.waitFor();
+ await evidence.locator(".goal-team-lineage-detail > summary").click();
+ const adoption = evidence.locator(".goal-team-adoption");
+ await adoption.locator("summary").click();
+ const consumerInventory = async route => {
+ const body = route.request().method() === "POST" ? route.request().postDataJSON() : {};
+ if (body.operation !== "operations") return route.fallback();
+ return route.fulfill({json: {items: [{record_id: "b".repeat(64), operation_id: "accepted-synthesis",
+ agent_id: "synthesizer", status: "accepted", recovery_required: false}],
+ has_more: false, next_cursor: null, page_readback_complete: true}});
+ };
+ await page.route("**/api/chat/sessions/*/loopx", consumerInventory);
+ await adoption.getByRole("button", {name: "查找使用此版本的后续结果", exact: true}).click();
+ await adoption.getByLabel("后续结果", {exact: true}).waitFor();
+ assert.equal(api.loopxModeRequests.filter(row => row.operation === "adopt").length, 0);
+ await adoption.getByRole("button", {name: "阅读后续结果", exact: true}).click();
+ await adoption.getByLabel("证据内容: report.md", {exact: true}).waitFor();
+ assert.equal(api.loopxModeRequests.filter(row => row.operation === "adopt").length, 0);
+ const lostAdoptionAck = async route => {
+ const body = route.request().method() === "POST" ? route.request().postDataJSON() : {};
+ if (body.operation !== "adopt") return route.fallback();
+ api.loopxModeRequests.push({sessionId: configured.sessionId, ...body});
+ mode.fixtureAdoptionState = "current";
+ return route.fulfill({status: 503, json: {error: "Fixture acknowledgement lost after commit"}});
+ };
+ await page.route("**/api/chat/sessions/*/loopx", lostAdoptionAck);
+ const confirmAdoption = adoption.getByRole("button", {name: "确认采用于此结果", exact: true});
+ await confirmAdoption.focus(); await page.keyboard.press("Enter");
+ await adoption.getByText("采用已记录,指定版本与后续结果当前有效。", {exact: true}).waitFor();
+ const decisions = api.loopxModeRequests.filter(row => row.operation === "adopt");
+ assert.equal(decisions.length, 1, "Post-commit reconciliation sends no duplicate decision");
+ assert.deepEqual({operation_id: decisions[0].operation_id, consumer_operation_id: decisions[0].consumer_operation_id},
+ {operation_id: "accepted-analysis", consumer_operation_id: "accepted-synthesis"});
+ await adoption.scrollIntoViewIfNeeded();
+ await page.screenshot({path: resolve(outputDir, "team-adoption-desktop.png"), animations: "disabled"});
+ await page.setViewportSize({width: 390, height: 844});
+ await page.emulateMedia({reducedMotion: "reduce"});
+ assert(await adoption.evaluate(el => el.scrollWidth <= el.clientWidth), "Adoption stays readable at 390px");
+ await page.screenshot({path: resolve(outputDir, "team-adoption-mobile.png"), animations: "disabled"});
+ await page.setViewportSize({width: 1512, height: 982});
+ await page.unroute("**/api/chat/sessions/*/loopx", lostAdoptionAck);
+ // An unavailable reconciliation keeps the reviewed pair and requires an
+ // explicit retry; it cannot silently select or dispatch different work.
+ delete mode.fixtureAdoptionState;
+ await adoption.getByRole("button", {name: "查找使用此版本的后续结果", exact: true}).click();
+ await adoption.getByRole("button", {name: "阅读后续结果", exact: true}).click();
+ await adoption.getByLabel("证据内容: report.md", {exact: true}).waitFor();
+ const unavailableReconciliation = async route => {
+ const body = route.request().method() === "POST" ? route.request().postDataJSON() : {};
+ if (body.operation !== "read" || body.operation_id !== "accepted-analysis") return route.fallback();
+ return route.fulfill({status: 503, json: {error: "Fixture readback unavailable"}});
+ };
+ await page.route("**/api/chat/sessions/*/loopx", lostAdoptionAck);
+ await page.route("**/api/chat/sessions/*/loopx", unavailableReconciliation);
+ await adoption.getByRole("button", {name: "确认采用于此结果", exact: true}).click();
+ await adoption.getByRole("button", {name: "重试同一采用决定", exact: true}).waitFor();
+ assert.equal(await adoption.getByLabel("后续结果", {exact: true}).isDisabled(), true);
+ assert.equal(await adoption.getByRole("button", {name: "查找使用此版本的后续结果", exact: true}).isDisabled(), true);
+ assert.equal(api.loopxModeRequests.filter(row => row.operation === "adopt").length, 2);
+ await page.unroute("**/api/chat/sessions/*/loopx", unavailableReconciliation);
+ await page.unroute("**/api/chat/sessions/*/loopx", lostAdoptionAck);
+ await adoption.getByRole("button", {name: "重试同一采用决定", exact: true}).click();
+ await adoption.getByText("采用已记录,指定版本与后续结果当前有效。", {exact: true}).waitFor();
+ const retriedDecisions = api.loopxModeRequests.filter(row => row.operation === "adopt");
+ assert.equal(retriedDecisions.length, 3);
+ assert.deepEqual(retriedDecisions[2], retriedDecisions[1], "Retry retains the exact reviewed adoption pair");
+ mode.fixtureAdoptionState = "unavailable";
+ await adoption.getByRole("button", {name: "查找使用此版本的后续结果", exact: true}).click();
+ await adoption.getByText("已检查的工作中没有可核验的使用结果。", {exact: true}).waitFor();
+ assert.equal(await adoption.getByRole("button", {name: "确认采用于此结果", exact: true}).count(), 0);
+ assert.equal(api.loopxModeRequests.filter(row => row.operation === "adopt").length, 3);
+ await page.unroute("**/api/chat/sessions/*/loopx", consumerInventory);
// Lose the first acknowledgement; retry must preserve identity and content.
let first = true;
await page.route("**/api/chat/sessions/*/loopx", async route => {
diff --git a/loopx/chat_loopx_mode.py b/loopx/chat_loopx_mode.py
index b9d3ef1202..b6e9a717a2 100644
--- a/loopx/chat_loopx_mode.py
+++ b/loopx/chat_loopx_mode.py
@@ -304,6 +304,26 @@ def read_team(self, session_id, body):
def apply(self, session_id, body, *, work_dir, objective):
if body.get("operation") in {"inspect", "operations", "read"}:
return self.read_team(session_id, body)
+ if body.get("operation") == "adopt":
+ if set(body) != {"operation", "operation_id", "consumer_operation_id"}:
+ raise ValueError("adopt requires source and consumer operation ids only")
+ from .control_plane.collaboration.peers import require_operation_id
+
+ operation_id = require_operation_id(body["operation_id"])
+ consumer_id = require_operation_id(body["consumer_operation_id"])
+ with self._lock(session_id):
+ session = self._session(session_id)
+ settings = (session.get("loopx_mode") or {}).get("settings") or {}
+ service, _, _, _ = self._execution(session, settings)
+ goal = self._goal(session)
+ effect_runtime_result("collaboration.chat_mode", {
+ "session": session, "origin": "web", "operation": "adopt",
+ "settings": settings,
+ "registered_agents": registered_agent_ids_for_goal(goal),
+ "goal_active": goal_accepts_collaboration(goal),
+ "execution_binding_valid": True,
+ })
+ return {"ok": True, **service.adopt_result(operation_id, consumer_id)}
if set(body) - {
"operation",
"settings",
diff --git a/loopx/control_plane/collaboration/chat_mode.ts b/loopx/control_plane/collaboration/chat_mode.ts
index b29be6a356..25c4d69550 100644
--- a/loopx/control_plane/collaboration/chat_mode.ts
+++ b/loopx/control_plane/collaboration/chat_mode.ts
@@ -14,7 +14,7 @@ function requireThat(ok: unknown, message: string): asserts ok {
export function planChatMode(input: JsonObject): JsonObject {
const session = requireJsonObject(input.session, "conversation session");
const operation = input.operation;
- requireThat(["configure", "start", "resume", "pause", "exit", "message", "wake"].includes(String(operation)), "unsupported conversation operation");
+ requireThat(["configure", "start", "resume", "pause", "exit", "message", "wake", "adopt"].includes(String(operation)), "unsupported conversation operation");
const localOwner = resolveConversationScope(session).kind === "owner_goal"
&& session.session_mode !== "attached_host" && session.agent_id === "codex";
// External inbox returns have their own exact audience owner. A recorded
@@ -43,6 +43,15 @@ export function planChatMode(input: JsonObject): JsonObject {
return {operation, enabled: operation !== "exit"};
}
requireThat(input.goal_active === true, "Goal is stopped or unavailable");
+ if (operation === "adopt") {
+ requireThat(typeof settings.agent_id === "string" && Array.isArray(input.registered_agents)
+ && input.registered_agents.includes(settings.agent_id), "select a registered coordinator identity");
+ requireThat(input.execution_binding_valid === true, "configure the coordinator's authorized execution bindings first");
+ // An owner decision over completed work does not resume execution, consume
+ // a model allowance or observe a wake. The delegation owner rechecks both
+ // results and their exact uses input before recording the decision.
+ return {operation};
+ }
requireThat(!session.active_turn_id, "wait for the current conversation turn before changing execution");
requireThat(Number.isSafeInteger(settings.token_budget) && Number(settings.token_budget) > 0
&& Number(settings.token_budget) <= 2147483647, "set a positive coordinator token allowance");
diff --git a/tests/control_plane_ts/chat_mode.test.ts b/tests/control_plane_ts/chat_mode.test.ts
index 0b6d46a1df..639543b499 100644
--- a/tests/control_plane_ts/chat_mode.test.ts
+++ b/tests/control_plane_ts/chat_mode.test.ts
@@ -22,6 +22,21 @@ test("execution is explicit and preserves native lifecycle/allowance", () => {
assert.throws(() => planChatMode({...input, operation: "resume", native: {status: "active"}}), /paused/);
});
+test("owner adoption records completed work without execution or allowance changes", () => {
+ const adoption = {...input, operation: "adopt", settings: {agent_id: "lead"}};
+ for (const loopx_mode of [{enabled: false}, {enabled: true, paused: true}, {enabled: true}]) {
+ assert.deepEqual(planChatMode({...adoption, session: {...session, loopx_mode, active_turn_id: "running"}}),
+ {operation: "adopt"});
+ }
+ assert.throws(() => planChatMode({...adoption, goal_active: false}), /stopped/);
+ assert.throws(() => planChatMode({...adoption, execution_binding_valid: false}), /bindings/);
+ assert.throws(() => planChatMode({...adoption, registered_agents: []}), /registered/);
+ for (const change of [{channel_id: "manager"}, {session_mode: "attached_host"}, {agent_id: "other"}]) {
+ assert.throws(() => planChatMode({...adoption, session: {...session, ...change}}), /local managed/);
+ }
+ assert.throws(() => planChatMode({...adoption, origin: "host"}), /local managed/);
+});
+
test("neither registration nor a role name grants execution", () => {
for (const changes of [
{origin: "external"}, {goal_active: false}, {execution_binding_valid: false},
diff --git a/tests/test_chat_team_evidence.py b/tests/test_chat_team_evidence.py
index 5c7248f7fb..75afb10ad7 100644
--- a/tests/test_chat_team_evidence.py
+++ b/tests/test_chat_team_evidence.py
@@ -3,6 +3,8 @@
import shutil
import subprocess
import sys
+import http.client
+import threading
import pytest
@@ -64,6 +66,7 @@ def read(body=None):
assert observed["artifacts"][0]["text"] == (root / "analyst/initial/output.json").read_text()
assert store.load_session(sid) == before
assert (root / "analyst/initial/host-invocations").read_text() == "1"
+
for body in [
{"operation": "read"}, {"operation": "read", "operation_id": ["analysis-1"]},
{"operation": "read", "operation_id": "../analysis-1"},
@@ -82,3 +85,103 @@ def read(body=None):
with pytest.raises(ValueError, match="bindings changed"):
read()
assert (root / "analyst/initial/host-invocations").read_text() == "1"
+
+
+def test_owner_http_adopts_exact_result_without_starting_or_resuming_work(service):
+ from loopx.chat_server import ChatHTTPServer, ChatRequestHandler
+
+ root, runner = service
+ config = json.loads(runner.config.read_text())
+ config["bindings"].append({**config["bindings"][0], "id": "synthesis", "agent_id": "reviewer",
+ "todo_id": "todo_reviewer-corrected", "workspace": str(root / "reviewer/corrected")})
+ runner.config.write_text(json.dumps(config))
+ runner.start("analysis", "analysis-1", brief())
+ source = wait(runner)
+ artifact = source["artifacts"][0]
+ input_file = root / "reviewer/corrected/accepted-input.json"
+ input_file.write_text(artifact["text"])
+ runner.start("synthesis", "synthesis-1", {**brief(), "inputs": [{
+ "ref": "accepted-input.json", "description": "Accepted input", "sha256": artifact["sha256"],
+ "delegation": {"operation_id": "analysis-1", "ref": artifact["ref"], "relation": "uses"}}]})
+ consumer = wait(runner, "synthesis-1")
+ assert consumer["status"] == "accepted"
+ repo = root / "project"
+ executor = repo / ".loopx/config/delegations.json"
+ executor.parent.mkdir(parents=True)
+ executor.write_bytes(runner.config.read_bytes())
+ registry = json.loads(runner.registry.read_text())
+ registry["goals"][0]["spawn_policy"] = {"mode": "multi_subagent", "allowed": True,
+ "max_children": 2, "execution_config": ".loopx/config/delegations.json"}
+ runner.registry.write_text(json.dumps(registry))
+ store = ChatSessionStore(runner.root)
+ controller = ChatRuntimeController(store=store, codex_bin="unused", registry_path=runner.registry)
+ sid = store.create_session(goal_id=runner.goal_id, agent_id="codex",
+ channel_id="goal." + runner.goal_id, upstream_thread_id="fixture",
+ upstream_mode="chat", adapter_kind="codex_app_server")["session_id"]
+ controller.loopx_mode.apply(sid, {"operation": "configure", "settings": {
+ "agent_id": "lead", "token_budget": 1000}}, work_dir=repo, objective="Read completed work")
+ # Pausing execution does not prevent this explicit decision on completed work.
+ mode = store.load_session(sid)["loopx_mode"]
+ store.update_session(sid, loopx_mode={**mode, "enabled": True, "paused": True})
+ before = store.load_session(sid)
+ server = ChatHTTPServer(("127.0.0.1", 0), ChatRequestHandler)
+ server.verbose = False
+ server.registry_path, server.chat_store, server.runtime_controller = runner.registry, store, controller
+ server.runtime_root_override, server.scan_roots, server.limit = str(runner.root), [], 20
+ thread = threading.Thread(target=server.serve_forever, daemon=True)
+ thread.start()
+
+ def request(body, *, origin=None):
+ connection = http.client.HTTPConnection(*server.server_address, timeout=60)
+ try:
+ connection.request("POST", f"/api/chat/sessions/{sid}/loopx", json.dumps(body), {
+ "Content-Type": "application/json",
+ "Origin": origin or f"http://127.0.0.1:{server.server_port}"})
+ response = connection.getresponse()
+ return response.status, json.loads(response.read())
+ finally:
+ connection.close()
+
+ adoption = {"operation": "adopt", "operation_id": "analysis-1", "consumer_operation_id": "synthesis-1"}
+ try:
+ assert "adoptions" not in request({"operation": "read", "operation_id": "analysis-1"})[1]
+ for body in [
+ {**adoption, "agent_id": "reviewer"}, {**adoption, "settings": {"agent_id": "reviewer"}},
+ {**adoption, "consumer_operation_id": "analysis-1"},
+ {**adoption, "consumer_operation_id": "../synthesis-1"},
+ {"operation": "adopt", "operation_id": "analysis-1"},
+ ]:
+ assert request(body)[0] == 409
+ assert request(adoption, origin="https://untrusted.example")[0] == 403
+ input_file.write_text("{}")
+ assert request(adoption)[0] == 409
+ assert "adoptions" not in request({"operation": "read", "operation_id": "analysis-1"})[1]
+ input_file.write_text(artifact["text"])
+ registry["goals"][0]["status"] = "stopped"
+ runner.registry.write_text(json.dumps(registry))
+ assert request(adoption)[0] == 409
+ registry["goals"][0].pop("status")
+ runner.registry.write_text(json.dumps(registry))
+ status, recorded = request(adoption)
+ assert status == 200, recorded
+ assert len(recorded["adoptions"]) == 1
+ receipt = recorded["adoptions"][0]
+ assert receipt["state"] == "current" and receipt["requester_agent_id"] == "lead"
+ assert receipt["source_artifacts"] == [{"ref": artifact["ref"], "sha256": artifact["sha256"]}]
+ assert receipt["consumer_artifacts"] == [{"ref": row["ref"], "sha256": row["sha256"]}
+ for row in consumer["artifacts"]]
+ assert request(adoption)[1]["adoptions"] == recorded["adoptions"]
+ cli = subprocess.run([sys.executable, "-m", "loopx.cli", "--format", "json", "--registry", str(runner.registry),
+ "--runtime-root", str(runner.root), "delegation", "read", "--goal-id", runner.goal_id,
+ "--agent-id", "lead", "--execution-config", str(executor), "--operation-id", "analysis-1"],
+ capture_output=True, text=True, check=True)
+ assert json.loads(cli.stdout)["adoptions"] == recorded["adoptions"]
+ assert store.load_session(sid) == before
+ for workspace in ("analyst/initial", "reviewer/corrected"):
+ assert (root / workspace / "host-invocations").read_text() == "1"
+ executor.write_text(json.dumps({"schema_version": "loopx_local_delegation_v0", "bindings": []}))
+ assert request(adoption)[0] == 409
+ finally:
+ server.shutdown()
+ server.server_close()
+ thread.join(timeout=10)
From 4c1a812c299c09e4c1da6b87764b9abc57b83640 Mon Sep 17 00:00:00 2001
From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
Date: Wed, 7 Oct 2026 20:29:54 +0800
Subject: [PATCH 2/3] docs(collaboration): explain explicit report adoption and
recovery
Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
---
.../rfcs/live-team-workspace-v0.md | 8 ++++++
.../team-adoption-confirmed-desktop.png | Bin 0 -> 109691 bytes
.../team-adoption-confirmed-mobile.png | Bin 0 -> 57366 bytes
docs/reference/local-delegation.md | 26 ++++++++++++++++++
4 files changed, 34 insertions(+)
create mode 100644 docs/assets/personal-workspace/team-adoption-confirmed-desktop.png
create mode 100644 docs/assets/personal-workspace/team-adoption-confirmed-mobile.png
diff --git a/docs/architecture/rfcs/live-team-workspace-v0.md b/docs/architecture/rfcs/live-team-workspace-v0.md
index 5ec4731a70..1646a2015a 100644
--- a/docs/architecture/rfcs/live-team-workspace-v0.md
+++ b/docs/architecture/rfcs/live-team-workspace-v0.md
@@ -316,6 +316,14 @@ records native Codex MCP execution and a real-model correction/acceptance/adopti
qualification. This retires the earlier host-approval blocker; it does not prove
the released first-use journey, two continuing cycles or observer comprehension.
Reuse the existing version-bound readback, feedback and scoped pause owners.
+The report details now expose the existing requester adoption operation: inspect
+an accepted downstream result using the exact displayed version, then explicitly
+record the decision through the configured coordinator. Discovery and reading
+remain read-only; lost acknowledgement is reconciled against the durable receipt,
+and changed receiver inputs withdraw current adoption. This completes an App
+operation over already accepted work, not the live L1 qualification or an
+independent-verifier receipt. The real correction, verifier and requester-use
+acceptance above remains required.
The presentation slice in #4828 returns readable reports to the original Goal
conversation and compares an explicitly selected dependency with the accepted
output, preferring the same artifact name/type. The referenced artifact and hash
diff --git a/docs/assets/personal-workspace/team-adoption-confirmed-desktop.png b/docs/assets/personal-workspace/team-adoption-confirmed-desktop.png
new file mode 100644
index 0000000000000000000000000000000000000000..b53473b4a1a072b8576dbca86476cc91eda324e7
GIT binary patch
literal 109691
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 6wp&3G)J-
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