Skip to content

Commit 46280da

Browse files
authored
Merge pull request #4978 from loopx-project/codex/managed-result-local-readback-20260924
Return accepted managed reports to Goal Files and source manager conversation
2 parents c088d63 + c837315 commit 46280da

38 files changed

Lines changed: 1153 additions & 28 deletions

‎apps/presentation/dashboard/src/data/chat.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -923,6 +923,28 @@ export function readLoopXTeamWork(sessionId: string, operationId: string) {
923923
method: "POST", body: JSON.stringify({operation: "read", operation_id: operationId}),
924924
});
925925
}
926+
export type ManagedGoalResultRow = {
927+
todo_id: string; title: string; producer_agent_id: string; sha256: string;
928+
content_type: string; size_bytes: number; completed_at?: string | null;
929+
};
930+
export type ManagedGoalResultPage = {
931+
ok: true; items: ManagedGoalResultRow[]; total: number; next_cursor: string | null;
932+
unavailable_count: number; unavailable_todo_ids: string[];
933+
};
934+
export type ManagedGoalResultRead = {
935+
ok: true; goal_id: string; todo_id: string; text: string;
936+
result: {sha256: string; content_type: string; producer_agent_id: string};
937+
};
938+
export function fetchManagedGoalResults(goalId: string, cursor?: string) {
939+
const params = new URLSearchParams({goal_id: goalId});
940+
if (cursor) params.set("cursor", cursor);
941+
return requestJson<ManagedGoalResultPage>(`/api/chat/goal-results?${params}`);
942+
}
943+
export function readManagedGoalResult(goalId: string, todoId: string) {
944+
return requestJson<ManagedGoalResultRead>(
945+
`/api/chat/goal-results/${encodeURIComponent(todoId)}?goal_id=${encodeURIComponent(goalId)}`,
946+
);
947+
}
926948
// Keep inventory and selected-operation labels consistent; unknown states stay unknown.
927949
export function delegationStateLabel(row: {status: string; worker_active?: boolean; recovery_required: boolean | null}, zh: boolean) {
928950
if (row.status === "unavailable") return zh ? "无法核验" : "Unavailable";

‎apps/presentation/dashboard/src/features/personal-workspace/goal-loopx-mode.css‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
.goal-team-results p { font-size: 12px; line-height: 1.7; color: var(--pw-muted); }
102102
.goal-team-results [role="alert"] { color: var(--pw-red, #b42318); }
103103
.goal-team-results button, .goal-team-results select { min-height: 44px; padding: 8px 12px; border: 1px solid var(--pw-line, #ebebeb); border-radius: 6px; background: var(--pw-surface, #fff); color: inherit; font: inherit; cursor: pointer; }
104+
.goal-managed-results > header button { flex-shrink: 0; white-space: nowrap; }
104105
.goal-team-results button[aria-pressed="true"] { border-color: var(--pw-text); }
105106
.goal-team-results button:disabled { opacity: .5; cursor: default; }
106107
.goal-team-results summary { cursor: pointer; padding: 12px 0; font-size: 12px; min-height: 44px; }
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import {useEffect, useRef, useState} from "react";
2+
import {FileText, RefreshCw} from "lucide-react";
3+
import {
4+
fetchManagedGoalResults, readManagedGoalResult,
5+
type ManagedGoalResultPage, type ManagedGoalResultRow, type ManagedGoalResultRead,
6+
} from "../../data/chat";
7+
import {TeamArtifactReport, managedReportArtifact} from "./team-artifact-content";
8+
9+
/** Goal-scoped local reports; an inventory row never stands in for exact acceptance readback. */
10+
export function GoalManagedResults({goalId, zh}: {goalId: string; zh: boolean}) {
11+
const [page, setPage] = useState<ManagedGoalResultPage | null>(null);
12+
const [selected, setSelected] = useState<{row: ManagedGoalResultRow; read: ManagedGoalResultRead} | null>(null);
13+
const [busy, setBusy] = useState(false);
14+
const [error, setError] = useState("");
15+
const generation = useRef(0);
16+
const chosen = useRef<{todoId: string; sha256: string} | null>(null);
17+
const reader = useRef<HTMLDivElement>(null);
18+
19+
useEffect(() => {
20+
chosen.current = null;
21+
void load();
22+
return () => {generation.current++;};
23+
}, [goalId]);
24+
25+
async function read(row: ManagedGoalResultRow, current: number, focus = false) {
26+
const result = await readManagedGoalResult(goalId, row.todo_id);
27+
if (current !== generation.current) return;
28+
if (result.todo_id !== row.todo_id || result.goal_id !== goalId ||
29+
result.result.sha256 !== row.sha256 || result.result.producer_agent_id !== row.producer_agent_id) {
30+
throw new Error(zh ? "报告版本或验收已变化" : "Report version or acceptance changed");
31+
}
32+
chosen.current = {todoId: row.todo_id, sha256: row.sha256};
33+
setSelected({row, read: result});
34+
if (focus) window.requestAnimationFrame(() => reader.current?.focus());
35+
}
36+
37+
async function load(cursor?: string) {
38+
const current = ++generation.current;
39+
if (cursor) chosen.current = null;
40+
setBusy(true); setError(""); setSelected(null);
41+
try {
42+
const next = await fetchManagedGoalResults(goalId, cursor);
43+
if (current !== generation.current) return;
44+
if (!Array.isArray(next.items) || !Number.isInteger(next.total) ||
45+
!Number.isInteger(next.unavailable_count) ||
46+
!Array.isArray(next.unavailable_todo_ids) ||
47+
next.unavailable_count !== next.unavailable_todo_ids.length ||
48+
(next.next_cursor !== null && typeof next.next_cursor !== "string")) {
49+
throw new Error(zh ? "报告列表响应不完整" : "Report inventory response is incomplete");
50+
}
51+
setPage(next);
52+
const previous = chosen.current;
53+
const row = previous
54+
? next.items.find(item => item.todo_id === previous.todoId && item.sha256 === previous.sha256)
55+
: next.items[0];
56+
if (previous && !row) {
57+
setError(zh ? "上次报告已不在当前验收结果中。" : "The previous report is no longer in current accepted results.");
58+
} else if (row) {
59+
await read(row, current);
60+
}
61+
} catch (failure) {
62+
if (current === generation.current) {
63+
setPage(null);
64+
setError(`${zh ? "无法核验报告;旧内容已清除。" : "Cannot verify report; previous content was cleared."} ${String(failure)}`);
65+
}
66+
} finally {
67+
if (current === generation.current) setBusy(false);
68+
}
69+
}
70+
71+
async function select(row: ManagedGoalResultRow) {
72+
const current = ++generation.current;
73+
chosen.current = {todoId: row.todo_id, sha256: row.sha256};
74+
setBusy(true); setError(""); setSelected(null);
75+
try {await read(row, current, true);}
76+
catch (failure) {
77+
if (current === generation.current) setError(`${zh ? "报告或验收已变化;旧内容已清除。" : "Report or acceptance changed; previous content was cleared."} ${String(failure)}`);
78+
} finally {if (current === generation.current) setBusy(false);}
79+
}
80+
81+
const artifact = selected ? managedReportArtifact(
82+
selected.row.content_type, selected.row.sha256, selected.read.text) : null;
83+
return <section className="goal-team-results goal-managed-results" aria-label={zh ? "已验收的团队报告" : "Accepted team reports"} aria-busy={busy}>
84+
<header><div><h3>{zh ? "团队报告" : "Team reports"}</h3>
85+
<p>{zh ? "只有仍能通过当前验收的报告会出现在这里。" : "Only reports that still pass current acceptance appear here."}</p></div>
86+
<button type="button" disabled={busy} onClick={() => void load()}><RefreshCw size={14} aria-hidden="true"/>{zh ? "刷新" : "Refresh"}</button></header>
87+
{busy ? <p role="status">{zh ? "正在核验报告…" : "Verifying reports…"}</p> : null}
88+
{error ? <p role="alert">{error}</p> : null}
89+
{page && page.unavailable_count > 0 ? <p role="status">{zh
90+
? `本页有 ${page.unavailable_count} 份报告已无法通过当前核验。`
91+
: `${page.unavailable_count} report(s) on this page cannot pass current verification.`}</p> : null}
92+
{page && !busy && !page.items.length ? <p>{page.next_cursor
93+
? (zh ? "本页没有可核验的报告,可继续下一页。" : "No verifiable reports on this page; continue to the next page.")
94+
: (zh ? "暂无可核验的团队报告。" : "No verifiable team reports yet.")}</p> : null}
95+
{page?.next_cursor && !page.items.length ? <button type="button" disabled={busy} onClick={() => void load(page.next_cursor!)}>
96+
{zh ? "下一页" : "Next page"}
97+
</button> : null}
98+
{page && page.items.length > 0 ? <div className="goal-team-results-layout">
99+
<nav className="goal-team-result-list" aria-label={zh ? "选择团队报告" : "Choose a team report"}>
100+
{page.items.map(row => <button type="button" key={row.todo_id} disabled={busy}
101+
aria-pressed={selected?.row.todo_id === row.todo_id} onClick={() => void select(row)}>
102+
<FileText size={16} aria-hidden="true"/><span><strong>{row.title}</strong><small>{row.producer_agent_id}</small></span>
103+
</button>)}
104+
{page.next_cursor ? <button type="button" disabled={busy} onClick={() => void load(page.next_cursor!)}>
105+
{zh ? "下一页" : "Next page"}
106+
</button> : null}
107+
</nav>
108+
{artifact && selected ? <div ref={reader} tabIndex={-1} className="goal-team-result-reader">
109+
<TeamArtifactReport key={`${selected.row.todo_id}:${artifact.sha256}`} artifact={artifact}
110+
zh={zh} heading={selected.row.title}/>
111+
<p>{zh ? "验收任务" : "Accepted Todo"}: <code>{selected.row.todo_id}</code></p>
112+
</div> : null}
113+
</div> : null}
114+
</section>;
115+
}

‎apps/presentation/dashboard/src/features/personal-workspace/manager-team-result.tsx‎

Lines changed: 100 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import {useEffect, useState} from "react";
2-
import {ChatApiError, fetchChatSessions, fetchLoopXMode, fetchLoopXTeamWork, readLoopXTeamWork} from "../../data/chat";
3-
import {TeamArtifactReport, isMarkdownArtifact, type TeamArtifact} from "./team-artifact-content";
2+
import {ChatApiError, fetchChatSessions, fetchLoopXMode, fetchLoopXTeamWork, fetchManagedGoalResults, readLoopXTeamWork, readManagedGoalResult, type ManagedGoalResultRow} from "../../data/chat";
3+
import {TeamArtifactReport, isMarkdownArtifact, managedReportArtifact, type TeamArtifact} from "./team-artifact-content";
44

55
type Readback = {kind: "waiting" | "unavailable" | "multiple"} | {
66
kind: "adopted"; artifact: TeamArtifact; agentId: string;
77
};
8+
type ManagedReadback = {kind: "waiting" | "unavailable" | "multiple"} | {
9+
kind: "accepted"; artifact: TeamArtifact; agentId: string; title: string;
10+
};
811

912
/**
1013
* The Todo identities a Goal conversation itself reports work for, or the
@@ -21,6 +24,11 @@ const WORK_INDEX_WINDOW_MS = 30_000;
2124
const RELATED_SESSION_LIMIT = 8;
2225
const workIndexes = new Map<string, GoalWorkIndex>();
2326
const workIndexReads = new Map<string, Promise<GoalWorkIndex>>();
27+
/** Goal-wide inventory plus the exact Todo ids the server could not verify, so a
28+
* plan scopes its readback to its own ids instead of the whole Goal's health. */
29+
type ManagedGoalIndex = {readAt: number; rows: ManagedGoalResultRow[]; unavailableTodoIds: Set<string>};
30+
const managedIndexes = new Map<string, ManagedGoalIndex>();
31+
const managedIndexReads = new Map<string, Promise<ManagedGoalIndex>>();
2432

2533
/** A 4xx is the server declining this conversation's team readback: without a
2634
* coordinator identity it cannot own delegation work, so it is unrelated rather
@@ -154,11 +162,87 @@ async function readAdoptedResult(goalId: string, todoIds: Set<string>, force: bo
154162
return adopted.values().next().value ?? {kind: "waiting"};
155163
}
156164

165+
async function collectManagedGoalIndex(goalId: string): Promise<ManagedGoalIndex> {
166+
let cursor: string | undefined;
167+
let total: number | undefined;
168+
const rows: ManagedGoalResultRow[] = [];
169+
const unavailableTodoIds = new Set<string>();
170+
// Page until the snapshot ends. The previous fixed eight-page budget hid a
171+
// matching report that happened to sort later in the Goal's history.
172+
for (;;) {
173+
const page = await fetchManagedGoalResults(goalId, cursor);
174+
if (!Array.isArray(page.items) || !Number.isInteger(page.total) || page.total < 0 ||
175+
(total !== undefined && page.total !== total) ||
176+
!Number.isInteger(page.unavailable_count) || page.unavailable_count < 0 ||
177+
!Array.isArray(page.unavailable_todo_ids) ||
178+
page.unavailable_count !== page.unavailable_todo_ids.length ||
179+
(page.next_cursor !== null && (!page.next_cursor || typeof page.next_cursor !== "string"))) {
180+
throw new Error("managed inventory incomplete");
181+
}
182+
total = page.total;
183+
rows.push(...page.items);
184+
for (const todoId of page.unavailable_todo_ids) {
185+
if (typeof todoId !== "string" || !todoId) throw new Error("managed inventory incomplete");
186+
unavailableTodoIds.add(todoId);
187+
}
188+
if (!page.next_cursor) return {readAt: Date.now(), rows, unavailableTodoIds};
189+
if (page.next_cursor === cursor) throw new Error("managed cursor repeated");
190+
if (page.items.length === 0 && page.unavailable_count === 0) {
191+
throw new Error("managed inventory made no progress");
192+
}
193+
cursor = page.next_cursor;
194+
}
195+
}
196+
197+
/** Share the Goal inventory across plan cards; manual refresh always bypasses the short cache. */
198+
async function readManagedGoalIndex(goalId: string, force: boolean): Promise<ManagedGoalIndex> {
199+
const running = managedIndexReads.get(goalId);
200+
if (running) return running;
201+
const cached = managedIndexes.get(goalId);
202+
if (!force && cached && Date.now() - cached.readAt < WORK_INDEX_WINDOW_MS) return cached;
203+
const read = collectManagedGoalIndex(goalId)
204+
.then(index => {managedIndexes.set(goalId, index); return index;})
205+
.finally(() => {managedIndexReads.delete(goalId);});
206+
managedIndexReads.set(goalId, read);
207+
return read;
208+
}
209+
210+
/** A confirmed plan supplies the only Todo ids that may return to its source conversation. */
211+
async function readManagedPlanResult(goalId: string, todoIds: Set<string>, force: boolean): Promise<ManagedReadback> {
212+
try {
213+
const index = await readManagedGoalIndex(goalId, force);
214+
// Only a plan-owned unreadable row can hide this plan's report; unrelated
215+
// historical rows stay the server's and the Files view's concern.
216+
for (const todoId of todoIds) {
217+
if (index.unavailableTodoIds.has(todoId)) return {kind: "unavailable"};
218+
}
219+
const matched = new Map<string, ManagedGoalResultRow>();
220+
for (const row of index.rows) {
221+
if (!todoIds.has(row.todo_id)) continue;
222+
if (!row.sha256 || !row.producer_agent_id || !row.title) return {kind: "unavailable"};
223+
const previous = matched.get(row.todo_id);
224+
if (previous && previous.sha256 !== row.sha256) return {kind: "unavailable"};
225+
matched.set(row.todo_id, row);
226+
}
227+
if (matched.size > 1) return {kind: "multiple"};
228+
const row = matched.values().next().value;
229+
if (!row) return {kind: "waiting"};
230+
const read = await readManagedGoalResult(goalId, row.todo_id);
231+
if (read.goal_id !== goalId || read.todo_id !== row.todo_id ||
232+
read.result.sha256 !== row.sha256 || read.result.producer_agent_id !== row.producer_agent_id ||
233+
read.result.content_type !== row.content_type || typeof read.text !== "string") return {kind: "unavailable"};
234+
return {kind: "accepted", artifact: managedReportArtifact(row.content_type, row.sha256, read.text),
235+
agentId: row.producer_agent_id, title: row.title};
236+
} catch { /* A failed inventory or exact read cannot keep an earlier report visible. */ }
237+
return {kind: "unavailable"};
238+
}
239+
157240
/** Return only an accepted, currently adopted report to the manager conversation. */
158241
export function ManagerTeamResult({goalId, todoIds, zh, onOpenGoalEvidence}: {
159242
goalId: string; todoIds: string[]; zh: boolean; onOpenGoalEvidence: (goalId: string) => void;
160243
}) {
161244
const [result, setResult] = useState<{key: string; readback: Readback} | null>(null);
245+
const [managed, setManaged] = useState<{key: string; readback: ManagedReadback} | null>(null);
162246
const [request, setRequest] = useState({count: 0, force: false});
163247
const todoKey = [...todoIds].sort().join(",");
164248
// A new read must withdraw the previous accepted report immediately. The
@@ -170,6 +254,9 @@ export function ManagerTeamResult({goalId, todoIds, zh, onOpenGoalEvidence}: {
170254
void readAdoptedResult(goalId, new Set(todoKey.split(",")), request.force)
171255
.then(value => {if (!cancelled) setResult({key, readback: value});})
172256
.catch(() => {if (!cancelled) setResult({key, readback: {kind: "unavailable"}});});
257+
void readManagedPlanResult(goalId, new Set(todoKey.split(",")), request.force)
258+
.then(value => {if (!cancelled) setManaged({key, readback: value});})
259+
.catch(() => {if (!cancelled) setManaged({key, readback: {kind: "unavailable"}});});
173260
return () => {cancelled = true;};
174261
}, [goalId, todoKey, key, request.force]);
175262
useEffect(() => {
@@ -179,16 +266,25 @@ export function ManagerTeamResult({goalId, todoIds, zh, onOpenGoalEvidence}: {
179266
return () => window.clearInterval(timer);
180267
}, []);
181268
const readback = result?.key === key ? result.readback : null;
269+
const managedReadback = managed?.key === key ? managed.readback : null;
182270
if (!goalId || !todoKey) return null;
183-
return <section className={`personal-manager-team-result is-${readback?.kind ?? "loading"}`} aria-label={zh ? "团队结果回到管家" : "Team result returned to manager"} aria-busy={!readback}>
271+
return <section className={`personal-manager-team-result is-${readback?.kind ?? "loading"}`} aria-label={zh ? "团队结果回到管家" : "Team result returned to manager"} aria-busy={!readback || !managedReadback}>
184272
{!readback ? <p role="status">{zh ? "正在核验团队结果…" : "Verifying team result…"}</p> : readback.kind === "adopted" ? <>
185273
<header><strong>{zh ? "团队验收结果" : "Team result"}</strong><small>{goalId} · {readback.agentId}</small></header>
186274
<TeamArtifactReport artifact={readback.artifact} zh={zh} heading={zh ? "依据已采用 · 结果已验收" : "Source adopted · Result accepted"}/>
187-
</> : <p role="status">{readback.kind === "unavailable"
275+
</> : readback.kind === "waiting" && managedReadback?.kind === "accepted" ? null : <p role="status">{readback.kind === "unavailable"
188276
? (zh ? "团队结果或采用证据无法核验,请到 Goal 查看版本关系。" : "Team result or adoption evidence cannot be verified; inspect versions in the Goal.")
189277
: readback.kind === "multiple"
190278
? (zh ? "有多个已验收的下游结果,请到 Goal 选择要采用的结论。" : "Multiple downstream results are accepted; choose the conclusion in the Goal.")
191279
: (zh ? "团队任务已分配,尚无可核验的已采用结果。" : "Team work is assigned; no verifiable adopted result yet.")}</p>}
280+
{managedReadback?.kind === "accepted" ? <div className="personal-manager-managed-report">
281+
<header><strong>{zh ? "托管团队报告 · 已验收,采用尚未核验" : "Managed report · Accepted, adoption not verified"}</strong>
282+
<small>{managedReadback.agentId}</small></header>
283+
<TeamArtifactReport artifact={managedReadback.artifact} zh={zh} heading={managedReadback.title}/>
284+
</div> : managedReadback?.kind === "multiple" ? <p role="status">{zh
285+
? "这次分配已有多份托管报告;请到 Goal 选择结论。" : "This assignment has multiple managed reports; choose a conclusion in the Goal."}</p>
286+
: managedReadback?.kind === "unavailable" ? <p role="status">{zh
287+
? "托管报告无法核验;旧内容已撤回。" : "Managed report cannot be verified; previous content was withdrawn."}</p> : null}
192288
<div className="personal-manager-team-result-actions">
193289
<button type="button" onClick={() => onOpenGoalEvidence(goalId)}>{zh ? "查看证据与任务" : "Inspect evidence and tasks"}</button>
194290
<button type="button" disabled={!readback} onClick={() => setRequest(previous => ({count: previous.count + 1, force: true}))}>{zh ? "刷新结果" : "Refresh result"}</button>

0 commit comments

Comments
 (0)