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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ try {
await page.getByRole("button", {name: "团队执行情况", exact: true}).click();
const dialog = page.getByRole("dialog", {name: "团队执行情况"});
const openEvidence = dialog.getByRole("button", {name: "查看证据与反馈", exact: true});
const firstRead = page.waitForResponse(response => response.request().method() === "POST"
&& response.request().postDataJSON()?.operation === "read"
&& response.request().postDataJSON()?.operation_id === "accepted-analysis");
await openEvidence.first().click();
const legacyReadback = await (await firstRead).json();
const evidence = dialog.getByRole("region", {name: "执行证据"});
const original = () => evidence.getByLabel("证据内容: report.json");
await original().waitFor();
Expand All @@ -43,6 +47,67 @@ try {
// Older readbacks remain readable without invented provenance.
await evidence.getByText("本次验收依据", {exact: true}).click();
await evidence.getByText("此运行时未提供验收依据标识。", {exact: true}).waitFor();
// Transport fixtures exercise presentation only; real File/SQLite checks are
// qualified by test_local_delegation, never inferred from these synthetic rows.
const checkTime = "2026-01-02T03:04:05.123456Z";
const outputVersions = [{ref: "report.json", sha256: "d".repeat(64)}, {ref: "report.md", sha256: "9".repeat(64)}];
const currentValidation = {source: "goal_acceptance", basis_sha256: "7".repeat(64), check_count: 1,
pinned_file_count: 3, checked_at: checkTime, output_versions: outputVersions};
let validation = {...currentValidation, checked_at: undefined, output_versions: undefined};
let checkUnavailable = false;
const checkedRead = route => {
const body = route.request().postDataJSON();
if (body?.operation !== "read" || body.operation_id !== "accepted-analysis") return route.fallback();
api.loopxModeRequests.push({sessionId: configured.sessionId, ...body});
return checkUnavailable ? route.fulfill({status: 409, json: {error: "delegation output changed during validation"}})
: route.fulfill({json: {...legacyReadback, validation}});
};
await page.route("**/api/chat/sessions/*/loopx", checkedRead);
const recheck = evidence.getByRole("button", {name: "重新读取证据", exact: true});
await recheck.click();
await original().waitFor();
await evidence.getByText("本次验收依据", {exact: true}).click();
await evidence.getByText("未提供与当前产物匹配的检查时间及版本记录。", {exact: true}).waitFor();
assert.equal(await evidence.locator("time").count(), 0, "A legacy basis does not invent check records");
validation = currentValidation;
await recheck.click();
await original().waitFor();
await evidence.getByText("本次验收依据", {exact: true}).click();
const checkObservation = evidence.locator("details").filter({has: page.getByText("本次验收依据", {exact: true})});
await checkObservation.getByText(checkTime, {exact: true}).waitFor();
for (const version of outputVersions) await checkObservation.getByText(version.sha256, {exact: true}).waitFor();
assert.equal(await checkObservation.getByText("未提供与当前产物匹配的检查时间及版本记录。", {exact: true}).count(), 0);
await mkdir(outputDir, {recursive: true});
await checkObservation.scrollIntoViewIfNeeded();
await page.screenshot({path: resolve(outputDir, "team-checked-output-desktop.png"), animations: "disabled"});
await page.setViewportSize({width: 390, height: 844});
await page.emulateMedia({reducedMotion: "reduce"});
assert.ok(await dialog.evaluate(el => el.scrollWidth <= el.clientWidth));
await checkObservation.getByText(outputVersions[1].sha256, {exact: true}).scrollIntoViewIfNeeded();
await page.screenshot({path: resolve(outputDir, "team-checked-output-mobile.png"), animations: "disabled"});
await page.setViewportSize({width: 1512, height: 980});
// Another version's witness must never appear as a check of this report.
validation = {...validation, output_versions: [{ref: "report.json", sha256: "f".repeat(64)}, outputVersions[1]]};
await recheck.click();
await original().waitFor();
await evidence.getByText("本次验收依据", {exact: true}).click();
await evidence.getByText("未提供与当前产物匹配的检查时间及版本记录。", {exact: true}).waitFor();
assert.equal(await checkObservation.locator("time").count(), 0);
assert.equal(await checkObservation.getByText("f".repeat(64), {exact: true}).count(), 0);
checkUnavailable = true;
await recheck.click();
await evidence.getByRole("alert").filter({hasText: "已清除上次证据"}).waitFor();
assert.equal(await original().count(), 0);
assert.equal(await evidence.getByText("本次验收依据", {exact: true}).count(), 0);
await page.screenshot({path: resolve(outputDir, "team-checked-output-unavailable.png"), animations: "disabled"});
checkUnavailable = false;
validation = {...validation, output_versions: outputVersions};
await recheck.focus();
await page.keyboard.press("Enter");
await original().waitFor();
await evidence.getByText("本次验收依据", {exact: true}).click();
await checkObservation.getByText(checkTime, {exact: true}).waitFor();
await page.unroute("**/api/chat/sessions/*/loopx", checkedRead);
await evidence.getByRole("button", {name: "核验关联执行", exact: true}).click();
const verificationGap = evidence.getByText("当前读回未提供独立验收者与指定版本回执。", {exact: true});
await verificationGap.waitFor({timeout: 3000});
Expand Down Expand Up @@ -189,7 +254,7 @@ try {
assert.ok(await openEvidence.first().evaluate(el => el === document.activeElement));
assert.equal(api.turnRequests.length, 0);
assert.equal(api.loopxModeRequests.filter(row => row.operation === "message").length, 0);
console.log("team-evidence-return: passed (packaged navigation, downstream loss/revocation/restoration, core loss, keyboard return, fresh evidence/list, pagination, mobile and no execution)");
console.log("team-evidence-return: passed (packaged checked outputs, navigation, downstream loss/revocation/restoration, core loss, keyboard return, fresh evidence/list, pagination, mobile and no execution)");
} finally {
await workspace?.close();
await browser?.close();
Expand Down
3 changes: 2 additions & 1 deletion apps/presentation/dashboard/src/data/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1144,7 +1144,8 @@ export type DelegationReadback = {
status: string; worker_active: boolean; recovery_required: boolean;
artifacts?: Array<{ref: string; sha256: string; text: string}>; error?: string;
validation?: {source: "goal_acceptance" | "todo_validation"; basis_sha256: string;
check_count: number; pinned_file_count: number};
check_count: number; pinned_file_count: number; checked_at?: string;
output_versions?: Array<{ref: string; sha256: string}>};
dependencies?: DelegationDependency[]; adoptions?: DelegationAdoption[];
};
export function readLoopXTeamWork(sessionId: string, operationId: string) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,11 @@ export function ContextDrawer({ agents, attentionHistory = [], onSelectAttention
<section className="personal-detail-card" role="status">
<p>{t("drawer.taskReadbackUnavailable")}</p>
</section>
) : selection.item.detailMode === "request_only" ? (
<section className="personal-task-inspector-summary">
<p className="personal-proposal-explainer">{t("drawer.taskRequestOnly")}</p>
<TaskRequest todo={selection.item} local={!readOnly} />
</section>
) : (
<>
<section className="personal-task-inspector-summary">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,10 @@ export function DeliveryReview({ goal, items, userTodos, onSelect, active }: Del
const item = scopedAttention.find(todo => todo.todoId === id);
return item ? { kind: "attention", item } : null;
}
// History outside the status window still opens with the projected facts only.
// The status window is bounded; its absence cannot revoke exact request reading.
const todo = goal.agentTodos.find(item => item.todoId === id) ?? {
todoId: id, text: node.title, done: node.state === "done", claimedBy: node.owner_agent ?? null,
todoId: id, sourceTodoId: id, detailMode: "request_only" as const,
text: node.title, done: node.state === "done", claimedBy: node.owner_agent ?? null,
status: node.state === "waiting" ? "deferred" : node.state === "blocked" || node.state === "done" ? node.state : "open",
taskDomain: node.task_domain ?? null, taskClass: node.kind === "monitor" ? "continuous_monitor" : null,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ export function GoalTeamEvidence({sessionId, operationId, zh, canMessage, ingres
: (zh ? "投递状态待核实" : "Delivery requires reconciliation");
const hasCorrectionPath = Boolean(result?.dependencies?.some(link => link.relation === "revises")
&& result?.dependencies?.some(link => link.relation === "responds_to"));
// Defensive transport readback: never present a witness for another report version.
const checkedOutputs = result?.validation?.output_versions;
const checkedAt = result?.validation?.checked_at;
const hasCheckedOutputs = Boolean(checkedAt && Number.isFinite(Date.parse(checkedAt))
&& 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)));
return <section className="goal-team-evidence" aria-label={zh ? "执行证据" : "Execution evidence"} aria-busy={busy}>
<div className="goal-team-work-actions"><h3>{zh ? "执行证据" : "Execution evidence"}</h3>
<button type="button" disabled={busy} onClick={() => void read()}>{zh ? "重新读取证据" : "Recheck evidence"}</button></div>
Expand All @@ -84,6 +91,11 @@ export function GoalTeamEvidence({sessionId, operationId, zh, canMessage, ingres
{result.validation.pinned_file_count} {zh ? "项文件版本固定" : "file pins"}</p>
<p>{zh ? "本次读取重跑了当前规则,并核对产物版本。规则标识不证明独立复核者或异议已解决。"
: "This read reran the current rules and checked output versions. The rule identity does not attest an independent reviewer or resolve an objection."}</p>
{hasCheckedOutputs ? <>
<p>{zh ? "本次检查完成于" : "Checks completed at"}{" "}<time dateTime={checkedAt}>{checkedAt}</time></p>
<p>{zh ? "检查前后保持一致的产物版本" : "Output versions unchanged across these checks"}</p>
{checkedOutputs!.map(row => <p key={row.ref}>{row.ref}{" · "}<code>{row.sha256}</code></p>)}
</> : <p>{zh ? "未提供与当前产物匹配的检查时间及版本记录。" : "Check time and version records matching this report are unavailable."}</p>}
<code>{result.validation.basis_sha256}</code>
</> : <p>{zh ? "此运行时未提供验收依据标识。" : "This runtime did not provide the validation basis identity."}</p>}
</details> : null}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,7 @@ const en = {
"drawer.blockedReason": "Why it is blocked",
"drawer.blockedReasonMissing": "No reason was recorded when this task was blocked. Ask the responsible Agent in the conversation.",
"drawer.taskReadbackUnavailable": "Current task state is unavailable. Refresh or open its retained history before acting.",
"drawer.taskRequestOnly": "This task is outside the current summary. Read its original request here; current state and actions remain unavailable.",
"drawer.taskOrdinary": "Task",
"drawer.taskStatusBlocked": "Blocked",
"drawer.taskStatusDeferred": "Deferred",
Expand Down Expand Up @@ -1751,6 +1752,7 @@ const zhCN: Record<WorkspaceMessageKey, string> = {
"drawer.blockedReason": "为何受阻",
"drawer.blockedReasonMissing": "标记受阻时未记录原因,可在对话中询问负责的 Agent。",
"drawer.taskReadbackUnavailable": "当前任务状态不可用。请刷新或打开保留的历史记录,再决定后续操作。",
"drawer.taskRequestOnly": "此事项不在当前摘要中。可在这里读取原始请求;当前状态与操作仍不可用。",
"drawer.taskOrdinary": "普通任务",
"drawer.taskStatusBlocked": "受阻",
"drawer.taskStatusDeferred": "已延期",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ export type WorkspaceAgentTodo = {
requestText?: string;
/** The authority id, absent when a legacy projection needs a display-only id. */
sourceTodoId?: string | null;
/** Local map fallback: exact request reading without current-state actions. */
detailMode?: "request_only";
text: string;
todoId: string;
validationDigest?: string | null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1876,7 +1876,7 @@ export function PersonalWorkspacePage({
notice={serviceNotice}
drawer={drawerSelection ? <ContextDrawer agents={agents} attentionHistory={model.attentionHistory ?? model.userTodos} onSelectAttention={(item) => setSelection({ kind: "attention", item })} callbacks={effectiveDrawerCallbacks} goalNotifications={model.goalNotifications ?? []} goals={workspaceGoals} inspectorExpanded={taskInspectorExpanded} larkConnections={readOnly ? [] : larkConnections}
todoReadbackUnavailable={drawerSelection.kind === "todo" && !workspaceGoals.some((goal) =>
goal.goalId === drawerSelection.item.goalId && (drawerSelection.item.done
goal.goalId === drawerSelection.item.goalId && (drawerSelection.item.detailMode === "request_only" || drawerSelection.item.done
|| goal.agentTodos.some((todo) => todo.todoId === drawerSelection.item.todoId)))}
proposalReadbackUnavailable={actionReadback.isError || !actionReadback.data
|| (drawerSelection.kind === "proposal" && !actionReadback.data.some(proposal => proposal.proposal_id === drawerSelection.item.previewId))}
Expand Down
10 changes: 9 additions & 1 deletion docs/architecture/rfcs/live-team-workspace-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,8 @@ Implementation checkpoint ([#5587](https://github.com/loopx-project/loopx/pull/5

This remains a presentation/readback slice. `responds_to` does not certify an objection. Current native acceptance validates the canonical task and configured validators, but its readback does not expose an independent verifier identity and exact-version receipt. The revision therefore shows that missing evidence explicitly, even beside a valid requester adoption. The evidence reader separately exposes the current validation source, definition digest and check/file-pin counts from the existing typed validation plan. CLI and HTTP readback carry the same path-free observation after the original checks pass; failed validation or changed pins withdraw it with the report. This rule identity is not a persisted success or independent-verifier receipt. The frontend must consume such evidence from the existing acceptance owner when available; it cannot infer it from the reviewer name, relationship or artifact hash.

The current host-check observation additionally names its UTC completion time and the declared output versions that remained unchanged before and after those checks. A changed output withdraws even the first return; the evidence detail displays matching records and leaves older or mismatched records unavailable. Stability reuses the acceptance owner's task work digest plus current claim/lifecycle and selected rules/pins/workspace: unrelated canonical commits and observation metadata do not revoke a stable check, while changed work, authorization or validators still reject it. This replaces whole-Goal revision comparison only on this read path; mutation CAS remains intact. The delegation TS owner and read-only Chat/CLI projection add no verifier configuration or independent semantic verdict, and do not close Section 9's independent exact-version acceptance gap.

Packaged desktop, 390px, keyboard/reduced-motion and pagination checks cover these returns and recovery. An isolated production SQLite/HTTP/CLI fixture also rejects changed output, withdraws its list acceptance and restores the original version without launching additional work. [Public-safe fixture views](../../reference/local-delegation.md#inspect-accepted-evidence-and-return) make these states reviewable. These checks do not establish a real objection, independent semantic acceptance, installed native behavior or L1 completion. The existing real correction episode must still be exercised through the packaged UI and independently read back with Section 9's missing/lost-observation cases.
Task inspectors read the original request on demand through the existing Todo
authority. Status/list summaries and thin reads retain their bounded budget;
Expand All @@ -402,7 +404,13 @@ offers retry, and discards stale selection responses. A failed read never
reuses an earlier full body. File/SQLite CLI and loopback HTTP checks cover
active/retained requests and recovery; packaged desktop/390px checks cover a
988-character request, mismatched identity, late response and keyboard return.
These read-only checks do not certify semantic acceptance or L1 completion.
Map entries outside the bounded status summary now retain their exact Todo reference
for this request-only read. The drawer does not treat graph metadata as current
state or expose mutation actions; a current status entry restores the ordinary
inspector. Packaged desktop/390px checks use the actual isolated SQLite/HTTP
reader, remove its registry to withdraw the earlier body, and restore it for an
explicit retry. Canonical task state remains unchanged. These read-only checks
do not certify semantic acceptance, installed native behavior or L1 completion.
Motion is retained
only when it clarifies these transitions;
remove effects that obscure absent execution, absent acceptance or source loss.
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading