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."}

+ + {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 ? : null} + {cursor && inspectedPages.current >= 10 ?

{zh ? "本次已检查十页;仍有未检查工作,请核实原请求。" : "Ten pages inspected; more work remains uninspected. Reconcile the original request."}

: null} + {candidates.length ? <> + + + : null} + {consumer ? <> + {artifact ? : null} + {consumer.artifacts && consumer.artifacts.length > 1 ? : null} + {recorded ?

{zh ? "采用已记录,指定版本与后续结果当前有效。" : "Adoption recorded; the referenced version and downstream result are current."}

+ : } + : 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"}

@@ -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 ?