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
9 changes: 9 additions & 0 deletions apps/presentation/dashboard/src/data/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,15 @@ export type ChatVisibleMessage = {
role: string;
text: string;
created_at: string;
return_delivery?: {
schema_version: "manager_return_delivery_status_v0";
phase: "decision" | "conclusion";
status: string;
created_at?: string | null;
delivered_at?: string | null;
error?: string | null;
verification?: "reconciled_after_restart";
};
};

export type ChatImageAttachment = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { OutputRow } from "./cards/output-row";
import { RunRow } from "./cards/run-row";
import { ScheduleRow } from "./cards/schedule-row";
import { useWorkspaceI18n } from "./i18n";
import { ReturnDeliveryStatus } from "./return-delivery-status";
import type { WorkspaceDrawerSelection, WorkspaceGoal, WorkspaceTimelineItem } from "./personal-workspace-model";

export function ChannelTimeline({
Expand Down Expand Up @@ -76,6 +77,7 @@ export function ChannelTimeline({
{item.message.attachments?.length ? <div className="personal-message-images">{item.message.attachments.map((attachment) => <img alt={attachment.name} key={attachment.id} src={attachment.dataUrl} />)}</div> : null}
{item.message.role === "user" ? <p>{item.message.text}</p> : <MarkdownText text={item.message.text} />}
{item.message.pending ? <span className="personal-message-pending">{t("timeline.pending")}</span> : null}
<ReturnDeliveryStatus delivery={item.message.returnDelivery} />
</div>
</article>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -999,6 +999,11 @@ const en = {
"timeline.emptyWorkspaceDescription": "Describe a Goal to the LoopX Manager, or ask what deserves attention today.",
"timeline.gateHistory": "{count} historical Gates",
"timeline.pending": "Working…",
"returnDelivery.queued": "Return queued",
"returnDelivery.verifying": "Verifying delivery without resending",
"returnDelivery.delivered": "Delivered to the original audience",
"returnDelivery.reconciled": "Delivery verified after recovery",
"returnDelivery.unverified": "Delivery remains explicitly unverified",
"timeline.runCompleted": "{run}: completed",
"timeline.review": "Review",
"timeline.reviewAndConfirm": "Review and confirm",
Expand Down Expand Up @@ -1994,6 +1999,11 @@ const zhCN: Record<WorkspaceMessageKey, string> = {
"timeline.emptyWorkspaceDescription": "向 LoopX 管家描述一个 Goal,或询问今天最值得关注的事情。",
"timeline.gateHistory": "{count} 项历史 Gate",
"timeline.pending": "正在整理…",
"returnDelivery.queued": "结论等待回传",
"returnDelivery.verifying": "正在核验送达,不会重复发送",
"returnDelivery.delivered": "已送达原受众",
"returnDelivery.reconciled": "恢复后已核验送达",
"returnDelivery.unverified": "送达仍明确未核验",
"timeline.runCompleted": "{run}:已完成",
"timeline.review": "查看处理方式",
"timeline.reviewAndConfirm": "查看并确认",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const page = source("./personal-workspace-page.tsx");
const router = source("./personal-workspace-router.ts");
const shell = source("./workspace-shell.tsx");
const timeline = source("./channel-timeline.tsx");
const returnDelivery = source("./return-delivery-status.tsx");
const runRow = source("./cards/run-row.tsx");
const larkSettings = source("./lark-settings-page.tsx");
const machineSettings = source("./machine-configuration-settings.tsx");
Expand Down Expand Up @@ -98,6 +99,13 @@ assert.match(page, /function operationProposalFields/, "Operation details have a
assert.doesNotMatch(page.match(/function operationProposalFields[\s\S]*?\n\}/)?.[0] ?? "", /authorized_principals|payload_digest|parameters\.payload/, "Operation details do not expose private authority or inline payloads");
assert.match(page, /t\("proposal\.primary\.operationGroup"\)/, "Operation confirmation routes users to the bound group");
assert.match(chatData, /result_delivery:/, "Dashboard retains operation result-delivery readback");
assert.match(chatData, /return_delivery\??:/, "Chat messages retain manager return-delivery readback");
assert.match(dashboard, /deliveryByMessage/, "Manager return polling refreshes delivery state after the message arrives");
assert.match(timeline + page, /ReturnDeliveryStatus/, "Both manager conversation surfaces render return delivery state");
for (const state of ["delivered", "verification_required", "explicit_unverified"]) {
assert.match(returnDelivery, new RegExp(state), `Return delivery renders ${state}`);
}
assert.doesNotMatch(returnDelivery, /message_ref|provider_receipt|intent_digest/, "Provider-private locator facts never enter the return status badge");
assert.match(actionReview, /proposal\.action_kind !== "operation\.execute" \|\| objectValue\(objectValue\(proposal\.operation\)\?\.result_delivery\) !== null/, "An operation is not complete in the Dashboard until result delivery is verified");
assert.match(page, /reviewPlan\.operationFrame/, "Dashboard operation details consume the shared TS review frame");
assert.match(page, /operation\.execute" && proposal\.status === "applied"/, "Dashboard restores terminal operation receipts from the canonical action store");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,11 +241,18 @@ export type WorkspaceMessage = {
attachments?: WorkspaceImageAttachment[];
id: string;
pending?: boolean;
returnDelivery?: WorkspaceReturnDelivery;
role: "assistant" | "user" | "system";
text: string;
time?: string;
};

export type WorkspaceReturnDelivery = {
error?: string | null;
status: string;
verification?: "reconciled_after_restart";
};

export type WorkspaceImageAttachment = {
dataUrl: string;
id: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { GoalSidebar } from "./goal-sidebar";
import { GoalTasksView } from "./goal-tasks-view";
import { localizedGoalState, localizedSessionStatus, useWorkspaceI18n, type WorkspaceTranslate } from "./i18n";
import { MarkdownText } from "./markdown";
import { ReturnDeliveryStatus } from "./return-delivery-status";
import type {
PersonalWorkspaceCallbacks,
WorkspaceAgentOption,
Expand Down Expand Up @@ -289,6 +290,7 @@ function ManagerConversationTray({
<div className="personal-manager-conversation-bubble">
{message.role === "user" ? <p>{message.text}</p> : <MarkdownText text={message.text} />}
{message.pending ? <small>{t("conversation.agentPending")}</small> : null}
<ReturnDeliveryStatus delivery={message.returnDelivery} />
</div>
</article>
))}
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useWorkspaceI18n } from "./i18n";
import type { WorkspaceReturnDelivery } from "./personal-workspace-model";

export function ReturnDeliveryStatus({ delivery }: { delivery?: WorkspaceReturnDelivery }) {
const { t } = useWorkspaceI18n();
if (!delivery) return null;
const label = delivery.status === "delivered"
? delivery.verification === "reconciled_after_restart"
? t("returnDelivery.reconciled")
: t("returnDelivery.delivered")
: delivery.status === "verification_required"
? t("returnDelivery.verifying")
: delivery.status === "explicit_unverified"
? t("returnDelivery.unverified")
: t("returnDelivery.queued");
const tone = delivery.status === "delivered"
? "delivered"
: delivery.status === "verification_required"
? "verification_required"
: delivery.status === "explicit_unverified"
? "explicit_unverified"
: "queued";
return (
<small className={`personal-return-delivery is-${tone}`} role="status">
{label}
</small>
);
}
21 changes: 19 additions & 2 deletions apps/presentation/dashboard/src/views/dashboard-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import {
type ChatSessionSnapshot,
type ChatSessionSummary,
type ChatImageAttachment,
type ChatVisibleMessage,
type ManagerRuntimeSessionReadback,
type ProtectedActionProposal,
type TodoProposal,
Expand Down Expand Up @@ -506,6 +507,7 @@ type PersonalManagerMessage = {
id: number;
lines: string[];
pending?: boolean;
returnDelivery?: ChatVisibleMessage["return_delivery"];
reconnect?: boolean;
role: "assistant" | "user";
sourceLabel?: string;
Expand Down Expand Up @@ -1588,11 +1590,24 @@ function PersonalGoalHome({
const previous = current[contextId] ?? [];
const seen = new Set(previous.map((row) => row.sourceMessageId));
const fresh = replies.filter((row) => !seen.has(row.message_id));
if (!fresh.length) return current;
return { ...current, [contextId]: [...previous, ...fresh.map((row) => ({
const deliveryByMessage = new Map(
replies.map((row) => [row.message_id, row.return_delivery]),
);
let deliveryChanged = false;
const updated = previous.map((row) => {
const delivery = row.sourceMessageId
? deliveryByMessage.get(row.sourceMessageId)
: undefined;
if (JSON.stringify(delivery) === JSON.stringify(row.returnDelivery)) return row;
deliveryChanged = true;
return { ...row, returnDelivery: delivery };
});
if (!fresh.length && !deliveryChanged) return current;
return { ...current, [contextId]: [...updated, ...fresh.map((row) => ({
id: managerMessageId.current++, sourceMessageId: row.message_id,
role: "assistant" as const, agentLabel: selectedAgent.label,
sourceLabel: "管家交接回执", text: visibleAgentMessage(row.text), lines: [],
returnDelivery: row.return_delivery,
}))] };
});
} catch {
Expand Down Expand Up @@ -1689,6 +1704,7 @@ function PersonalGoalHome({
id: managerMessageId.current++,
lines: [],
role: message.role === "user" ? "user" : "assistant",
returnDelivery: message.return_delivery,
sourceLabel: message.role === "user"
? undefined
: message.role === "error"
Expand Down Expand Up @@ -2554,6 +2570,7 @@ function PersonalGoalHome({
attachments: message.attachments,
id: String(message.id),
pending: message.pending,
returnDelivery: message.returnDelivery,
role: message.role,
text: message.text || (message.pending ? "Agent 正在处理…" : message.lines.join("\n")),
},
Expand Down
16 changes: 16 additions & 0 deletions examples/personal-workspace-browser/chat-recovery.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,15 @@ export const chatRecoveryScenario = {
message_id: "handoff.browser-fixture", turn_id: "original-delegation",
role: "agent", origin: "manager_followup", text: `${returnText}\n\n- **已完成**:核验新约束\n- 下一步:继续现有计划\n\n1. 核对证据\n2. 汇报结论`,
created_at: "2026-08-13T01:00:03Z",
return_delivery: {
schema_version: "manager_return_delivery_status_v0",
phase: "conclusion",
status: "verification_required",
error: "provider_delivery_unverified",
},
});
await page.getByText(returnText, { exact: true }).waitFor({ state: "visible", timeout: 10_000 });
await page.getByText("正在核验送达,不会重复发送", { exact: true }).waitFor({ state: "visible" });
const richConclusion = page.locator(".personal-channel-timeline .personal-message").filter({ hasText: returnText });
if (await richConclusion.locator("ul > li").count() !== 2
|| await richConclusion.locator(".personal-md strong").innerText() !== "已完成") {
Expand All @@ -100,6 +107,15 @@ export const chatRecoveryScenario = {
}
await richConclusion.scrollIntoViewIfNeeded();
await page.screenshot({ path: resolve(outputDir, "manager-automatic-conclusion.png"), fullPage: false, animations: "disabled" });
const returnedMessage = page.__loopxRuntime.messages.get(returnSessionId)
.find((message) => message.message_id === "handoff.browser-fixture");
returnedMessage.return_delivery = {
schema_version: "manager_return_delivery_status_v0",
phase: "conclusion",
status: "delivered",
verification: "reconciled_after_restart",
};
await page.getByText("恢复后已核验送达", { exact: true }).waitFor({ state: "visible", timeout: 10_000 });
await page.setViewportSize({ width: 390, height: 844 });
await page.getByText(returnText, { exact: true }).waitFor({ state: "visible" });
await richConclusion.scrollIntoViewIfNeeded();
Expand Down
13 changes: 13 additions & 0 deletions loopx/capabilities/manager_context/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,19 @@ sending, and never retargets a closed/replaced conversation. An offline transpor
retries the persisted answer rather than rerunning the worker. Ambiguous external
writes remain `verification_required` and are not blindly resent.

When the provider returned a trustworthy message locator before readback failed,
the same background pump persists that private attempt and later performs a
read-only verification. A matching message advances the original delivery to
`delivered` without sending again. Provider outages retain
`verification_required`; a missing legacy locator, changed intent, missing
message, or verified mismatch becomes `explicit_unverified`. CLI, Manager read,
and Chat expose the same public-safe state and reason without returning the
provider locator. The Lark adapter keeps locator interpretation and provider
readback; `manager-context` remains the sole result/delivery writer. The typed
`control_plane/collaboration/return_delivery.ts` boundary owns provider-neutral
attempt validation and verification classification; Python retains file-lock,
persistence and adapter orchestration only.

New handoffs persist their exact original return route. Legacy requests remain
queryable; a receiver can explicitly report one only when its exact persisted
Chat receipt uniquely recovers the route. Historical timestamps stay unknown.
Expand Down
Loading