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
25 changes: 24 additions & 1 deletion apps/presentation/dashboard/smoke/todo-ownership-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import { CompletedTaskLane } from "../src/features/personal-workspace/completed-
import { todoItemSchema } from "../src/data/status.ts";
import { workspaceAgentTodoFromItem } from "../src/features/personal-workspace/personal-workspace-model.ts";
import { ContextDrawer } from "../src/features/personal-workspace/context-drawer.tsx";
import { WorkspaceI18nProvider } from "../src/features/personal-workspace/i18n.tsx";
import { WorkspaceI18nProvider, useWorkspaceI18n } from "../src/features/personal-workspace/i18n.tsx";
import { ChatApiError } from "../src/data/chat.ts";
import { todoCompletionPreviewFeedback } from "../src/features/personal-workspace/todo-completion-feedback.ts";

// A Goal's default executor is not evidence that a Todo has been claimed.
// Exercise the real consumers, including retained history and stale labels.
Expand All @@ -17,6 +19,27 @@ try {
const render = (component, props) => renderToStaticMarkup(createElement(
WorkspaceI18nProvider, null, createElement(component, props),
));
const completionRequest = {actionKind: "todo.update", normalizedParameters: {operation: "complete"}};
function CompletionFeedback({error, request}) {
return todoCompletionPreviewFeedback(error, request, useWorkspaceI18n().t);
}
for (const code of ["handoff_mode_requires_lease", "lease_fence_required"]) {
const error = new ChatApiError("Canonical refusal", {error_code: code, http_status: 400});
const feedback = render(CompletionFeedback, {error, request: completionRequest});
assert.ok(feedback.includes(locale === "en" ? "This page cannot complete" : "此页面无法代为完成"));
assert.ok(feedback.includes(locale === "en" ? "has not been marked complete" : "尚未标记完成"));
assert.equal(error.payload.error_code, code, "Presentation preserves the canonical diagnostic");
for (const request of [
{...completionRequest, actionKind: "gate.resolve"},
{...completionRequest, normalizedParameters: {operation: "edit"}},
]) assert.equal(render(CompletionFeedback, {error, request}), "", "Unrelated actions retain their original errors");
}
for (const error of [new Error("lease_fence_required"),
new ChatApiError("lease_fence_required", {error_code: "provider_unavailable"}),
new ChatApiError("Foreign execution", {error_code: "lease_cas_mismatch"})]) {
assert.equal(render(CompletionFeedback, {error, request: completionRequest}), "",
"Neither error prose nor other refusals establish this recovery route");
}
const notesLabel = locale === "en" ? "Notes" : "备注";
const evidenceLabel = locale === "en" ? "Evidence" : "证据";
for (const invalid of [null, "dependency_cycle"]) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ const en = {
"feedback.goalRefreshFailed": "The Goal opened, but its latest state could not be refreshed. Use Refresh to try again.",
"feedback.preparingPreview": "Preparing confirmation preview: {title}",
"feedback.previewFailed": "Could not prepare the confirmation preview: {error}",
"feedback.taskCompletionUnavailable": "This task requires the responsible Agent's current execution lease proof. This page cannot complete it on the Agent's behalf. Ask the responsible Agent to complete it from its execution, then refresh the task. The task has not been marked complete.",
"feedback.sendFailed": "Send failed: {error}",
"feedback.proposalDraftFailed": "The reply suggested a task, but its draft could not be prepared. Ask again to retry.",
"history.loading": "Reading conversation history…",
Expand Down Expand Up @@ -1575,6 +1576,7 @@ const zhCN: Record<WorkspaceMessageKey, string> = {
"feedback.goalRefreshFailed": "已打开 Goal,但暂时无法刷新最新状态。请点击刷新重试。",
"feedback.preparingPreview": "正在准备确认预览:{title}",
"feedback.previewFailed": "无法准备确认预览:{error}",
"feedback.taskCompletionUnavailable": "此任务需要负责 Agent 提交当前执行租约证明,此页面无法代为完成。请让负责 Agent 从原执行中完成,再刷新任务状态;任务尚未标记完成。",
"feedback.sendFailed": "发送失败:{error}",
"feedback.proposalDraftFailed": "回答里提出了任务建议,但草稿未能生成。可以再问一次重试。",
"history.loading": "正在读取会话记录…",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { goalCreateRequest } from "./goal-create-request";
import { todoCompletionPreviewFeedback } from "./todo-completion-feedback";
import { readSteeringRequest, retainSteeringRequest, retireSteeringRequest } from "./steering-recovery";
import { useConversationInputState } from "./use-conversation-input-state";
import type { ConversationHistoryStatus } from "../../data/use-conversation-history";
Expand Down Expand Up @@ -1277,6 +1278,12 @@ export function PersonalWorkspacePage({
? await callbacks.onPreviewAction(request)
: workspaceProposal(await previewTypedAction(request), t);
} catch (error) {
const completionFeedback = todoCompletionPreviewFeedback(error, request, t);
if (completionFeedback !== null && error instanceof ChatApiError) {
// All Task entrypoints use this preview route. Keep the owner's code
// and diagnostics while explaining why this caller cannot complete.
throw new ChatApiError(completionFeedback, error.payload);
}
if (!(error instanceof ChatApiError) || error.payload.error_code !== "action_preview_gate") throw error;
const rawGate = error.payload.gate && typeof error.payload.gate === "object"
? error.payload.gate as Record<string, unknown>
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,16 @@
import { ChatApiError } from "../../data/chat";
import type { WorkspaceTranslate } from "./i18n";
import type { WorkspaceActionPreviewRequest } from "./personal-workspace-model";

/** Present the terminal owner's refusal; neither inspect nor acquire a lease. */
export function todoCompletionPreviewFeedback(
error: unknown,
request: WorkspaceActionPreviewRequest,
t: WorkspaceTranslate,
): string | null {
if (request.actionKind !== "todo.update" || request.normalizedParameters.operation !== "complete"
|| !(error instanceof ChatApiError)) return null;
if (error.payload.error_code !== "handoff_mode_requires_lease"
&& error.payload.error_code !== "lease_fence_required") return null;
return t("feedback.taskCompletionUnavailable");
}
15 changes: 15 additions & 0 deletions docs/reference/handoff-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ recovery; new requests cannot select it, even as a no-op. Existing Goals are not
silently changed, and creation/Host defaults remain unchanged in this stage.
This is an ownership policy, not a capability grant or a storage provider.

The App's Task list and Task details use the same reviewed completion entry.
When the terminal owner requires a lease or its current execution proof, the
App explains that completion is unavailable from this caller. The responsible
Agent must complete from its execution with the current proof; refresh the Task
to read back that result. A recorded claim or a lease existing in the store is
not a proof supplied by this caller. The App does not borrow a key, acquire a
lease or change ownership policy to make the completion succeed. Other preview
failures retain their own diagnostics. This explains an unavailable operation;
it does not qualify App completion under `hard_lease`.

## Choose the policy independently of storage

Use `soft_claim` for an explicitly single executor without parallel takeover.
Expand Down Expand Up @@ -128,6 +138,11 @@ migration/recovery boundary.
历史回执恢复输入,不能再作为新请求的目标;已有 Goal 和创建/Host 默认值不被
静默改写。soft 用于明确的单执行者,hard 用于本机并行 Agent/automation 和云端
共享;默认策略切换前还需验证 Host 的 acquire、renew、release 和阻塞结算。
App 的任务列表和详情共用审核完成入口。终态 owner 要求租约或当前执行证明时,
页面明确说明此调用方无法代为完成;负责 Agent 须从原执行提交当前证明,之后刷新
任务读回结果。已有 claim 或 store 中存在租约不等于页面持有执行证明;App 不借用
key、自动 acquire 或更改策略绕过拒绝。其他预览失败保留原诊断。此项只完善不可用
状态说明,不代表 `hard_lease` 下 App 手动完成已经合格。
存储晋升与所有权分开:新 CLI promote 默认 `preserve`,显式 hard 才审核策略升级;
旧 v0 保存计划仍按原合同恢复。

Expand Down
Loading