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 @@ -36,6 +36,9 @@ equal(deferMissingCondition.missingFields.join(","), "resume_when", "todo defer
const deferUntilPr = routeWorkspaceInput("把 todo-1 暂缓到pr_merged:huangruiteng/loopx#3399", goalContext);
equal(deferUntilPr.route, "typed_action", "todo defer with condition routes to typed action");
equal(deferUntilPr.normalizedParameters.resume_when, "pr_merged:huangruiteng/loopx#3399", "todo defer preserves supported condition");
const deferUntilDate = routeWorkspaceInput("把 todo-1 暂缓到 resume_at:2026-09-14T09:00:00+08:00", goalContext);
equal(deferUntilDate.route, "typed_action", "todo defer accepts a timezone-aware resume_at condition");
equal(deferUntilDate.normalizedParameters.resume_when, "resume_at:2026-09-14t09:00:00+08:00", "todo defer preserves an explicit timezone");
equal(routeWorkspaceInput("帮我修复 MR 冲突,跑测试,然后 push", goalContext).actionKind, "todo.create", "execution task");
equal(routeWorkspaceInput("创建任务并设置 Heartbeat", goalContext).route, "clarify", "compound intent");
equal(routeWorkspaceInput("创建任务并设置 Heartbeat", goalContext).missingFields.join(","), "single_intent", "compound missing field");
Expand Down
2 changes: 2 additions & 0 deletions apps/presentation/dashboard/src/data/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ export const todoItemSchema = z.object({
role: z.string().optional().nullable(),
status: z.string().optional().nullable(),
resume_when: z.string().optional().nullable(),
resume_ready: z.boolean().optional().nullable(),
resume_condition: z.record(z.string(), z.unknown()).optional().nullable(),
priority: z.string().optional().nullable(),
title: z.string().optional().nullable(),
archive_state: z.string().optional().nullable(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -558,8 +558,10 @@
<div><dt>{t("common.status")}</dt><dd>{selection.item.done ? t("drawer.taskStatusCompleted") : selection.item.status === "deferred" ? t("drawer.taskStatusDeferred") : selection.item.status === "blocked" ? t("drawer.taskStatusBlocked") : t("drawer.taskStatusOpen")}</dd></div>
<div><dt>{t("drawer.priority")}</dt><dd>{selection.item.priority ?? t("drawer.notSet")}</dd></div>
<div><dt>{t("drawer.dependencies")}</dt><dd>{selection.item.dependencies?.join(" · ") || t("common.none")}</dd></div>
{selection.item.status === "deferred" ? <div><dt>{t("drawer.resumeWhen")}</dt><dd>{selection.item.resumeWhen || t("drawer.notSet")}</dd></div> : null}
<div><dt>{t("drawer.nextTransition")}</dt><dd>{selection.item.nextTransition ?? (selection.item.done ? t("drawer.taskNextCompleted") : selection.item.status === "deferred" ? t("drawer.taskNextDeferred") : t("drawer.taskNextOpen"))}</dd></div>
{selection.item.status === "deferred" || selection.item.resumeWhen ? <div><dt>{t("drawer.resumeWhen")}</dt><dd>{selection.item.resumeWhen || t("drawer.notSet")}</dd></div> : null}
{selection.item.resumeWhen ? <div><dt>{t("drawer.resumeState")}</dt><dd>{selection.item.resumeReady ? t("drawer.resumeReady") : t("drawer.resumePending")}</dd></div> : null}
{selection.item.resumeReceiptId ? <div><dt>{t("drawer.resumeReceipt")}</dt><dd>{selection.item.resumeReceiptId}</dd></div> : null}
<div><dt>{t("drawer.nextTransition")}</dt><dd>{selection.item.nextTransition ?? (selection.item.done ? t("drawer.taskNextCompleted") : selection.item.resumeReady ? t("drawer.taskNextResumeReady") : selection.item.status === "deferred" ? t("drawer.taskNextDeferred") : t("drawer.taskNextOpen"))}</dd></div>

Check warning on line 564 in apps/presentation/dashboard/src/features/personal-workspace/context-drawer.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=huangruiteng_loopx&issues=AaCbtLmqkhlzGgA41uHu&open=AaCbtLmqkhlzGgA41uHu&pullRequest=4347

Check warning on line 564 in apps/presentation/dashboard/src/features/personal-workspace/context-drawer.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=huangruiteng_loopx&issues=AaCbtLmqkhlzGgA41uHt&open=AaCbtLmqkhlzGgA41uHt&pullRequest=4347
</dl>
</section>
{!readOnly && !selection.item.done ? <div className="personal-task-inspector-actions" aria-label={t("drawer.taskActions")}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ const en = {
"composer.agentProgress": "Ask Agent for a progress report",
"composer.agentProgressPrompt": "Give me a progress report for this Goal: completed, running, blocked, and next steps.",
"composer.attachImageHint": "Choose, paste, or drag an image",
"composer.clarifyDefer": "Deferring a Todo requires a deterministic resume condition. Add todo_done:<todo_id>, pr_merged:[owner/repo]#<number>, or capacity_available:<capability>.",
"composer.clarifyDefer": "Deferring a Todo requires a deterministic resume condition. Add todo_done:<todo_id>, pr_merged:[owner/repo]#<number>, capacity_available:<capability>, or resume_at:<timezone-aware RFC3339 timestamp>.",
"composer.clarifySingleAction": "This message contains multiple operations that may change state. Describe one operation at a time so each confirmation preview can be reviewed separately.",
"composer.createGoal": "Create Goal",
"composer.createGoalDraft": "Goal draft",
Expand Down Expand Up @@ -340,10 +340,10 @@ const en = {
"drawer.taskCompletedTitle": "Task completed",
"drawer.taskDefer": "Defer",
"drawer.taskDeferCondition": "Todo defer resume condition",
"drawer.taskDeferInvalid": "Unsupported condition format; use one of the three deterministic conditions below.",
"drawer.taskDeferPlaceholder": "For example, pr_merged:owner/repo#123",
"drawer.taskDeferInvalid": "Unsupported condition format; use one of the deterministic conditions below.",
"drawer.taskDeferPlaceholder": "For example, resume_at:2026-09-14T09:00:00+08:00",
"drawer.taskDeferReview": "Review defer",
"drawer.taskDeferSupported": "Supports todo_done, pr_merged, and capacity_available conditions.",
"drawer.taskDeferSupported": "Supports todo_done, pr_merged, capacity_available, and timezone-aware resume_at conditions.",
"drawer.taskDeferUntil": "Defer until",
"drawer.taskDetails": "Todo details",
"drawer.taskInfo": "Task information",
Expand All @@ -354,7 +354,12 @@ const en = {
"drawer.taskStatusBlocked": "Blocked",
"drawer.taskStatusDeferred": "Deferred",
"drawer.resumeWhen": "Resume condition",
"drawer.resumeState": "Resume state",
"drawer.resumePending": "Waiting for condition",
"drawer.resumeReady": "Ready to resume",
"drawer.resumeReceipt": "Resume receipt",
"drawer.taskNextDeferred": "Wait for the resume condition, then reassess",
"drawer.taskNextResumeReady": "Resume condition met; awaiting lifecycle replan",
"drawer.taskStatusCompleted": "Completed",
"drawer.taskStatusOpen": "Ready",
"drawer.taskSuccessor": "Create follow-up Todo",
Expand Down Expand Up @@ -1037,7 +1042,7 @@ const zhCN: Record<WorkspaceMessageKey, string> = {
"composer.agentProgress": "向 Agent 获取进度报告",
"composer.agentProgressPrompt": "请给我一份当前 Goal 的进度报告:已完成、执行中、阻塞和下一步。",
"composer.attachImageHint": "选择、粘贴或拖入图片",
"composer.clarifyDefer": "暂缓 Todo 需要可自动判断的恢复条件。请补充 todo_done:<todo_id>、pr_merged:[owner/repo]#<number> 或 capacity_available:<capability>。",
"composer.clarifyDefer": "暂缓 Todo 需要可自动判断的恢复条件。请补充 todo_done:<todo_id>、pr_merged:[owner/repo]#<number>、capacity_available:<capability> 或 resume_at:<带时区 RFC3339 时间>。",
"composer.clarifySingleAction": "这句话包含多个可能修改状态的操作。请一次描述一项操作,我会逐项展示确认预览。",
"composer.createGoal": "创建新 Goal",
"composer.createGoalDraft": "Goal 草稿",
Expand Down Expand Up @@ -1309,10 +1314,10 @@ const zhCN: Record<WorkspaceMessageKey, string> = {
"drawer.taskCompletedTitle": "任务已完成",
"drawer.taskDefer": "暂缓",
"drawer.taskDeferCondition": "Todo 暂缓恢复条件",
"drawer.taskDeferInvalid": "条件格式不受支持;请使用下列三种可判定条件。",
"drawer.taskDeferPlaceholder": "例如 pr_merged:owner/repo#123",
"drawer.taskDeferInvalid": "条件格式不受支持;请使用下列可判定条件。",
"drawer.taskDeferPlaceholder": "例如 resume_at:2026-09-14T09:00:00+08:00",
"drawer.taskDeferReview": "检查暂缓",
"drawer.taskDeferSupported": "支持 todo_done、pr_merged、capacity_available 条件。",
"drawer.taskDeferSupported": "支持 todo_done、pr_merged、capacity_available 与带时区的 resume_at 条件。",
"drawer.taskDeferUntil": "暂缓至",
"drawer.taskDetails": "Todo 详情",
"drawer.taskInfo": "任务信息",
Expand All @@ -1323,7 +1328,12 @@ const zhCN: Record<WorkspaceMessageKey, string> = {
"drawer.taskStatusBlocked": "受阻",
"drawer.taskStatusDeferred": "已延期",
"drawer.resumeWhen": "恢复条件",
"drawer.resumeState": "恢复状态",
"drawer.resumePending": "等待条件满足",
"drawer.resumeReady": "可恢复",
"drawer.resumeReceipt": "恢复回执",
"drawer.taskNextDeferred": "等待恢复条件满足后重新评估",
"drawer.taskNextResumeReady": "恢复条件已满足,等待生命周期重新规划",
"drawer.taskStatusCompleted": "已完成",
"drawer.taskStatusOpen": "待执行",
"drawer.taskSuccessor": "创建后续 Todo",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export type WorkspaceHomeLane =

export type WorkspaceAgentTodo = {
resumeWhen?: string | null;
resumeReady?: boolean | null;
resumeReceiptId?: string | null;
claimedBy?: string | null;
dependencies?: string[];
done: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
export function todoResumeWhenFromMessage(rawMessage: string) {
const message = normalizedMessage(rawMessage).toLowerCase();
const match = message.match(
/(?:^|[\s,,;;:((:]|到|至)(?<condition>todo_done:todo_[a-z0-9_-]{3,64}|pr_merged:(?:(?:[a-z0-9_.-]{1,80})\/(?:[a-z0-9_.-]{1,100}))?#[1-9][0-9]{0,8}|capacity_available:[a-z][a-z0-9_:-]{0,63})(?=$|[\s,,。;;))])/iu,
/(?:^|[\s,,;;:((:]|到|至)(?<condition>todo_done:todo_[a-z0-9_-]{3,64}|pr_merged:(?:(?:[a-z0-9_.-]{1,80})\/(?:[a-z0-9_.-]{1,100}))?#[1-9][0-9]{0,8}|capacity_available:[a-z][a-z0-9_:-]{0,63}|resume_at:[1-9][0-9]{3}-[0-9]{2}-[0-9]{2}t[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]{1,3})?(?:z|[+-][0-9]{2}:[0-9]{2}))(?=$|[\s,,。;;))])/iu,

Check warning on line 57 in apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-router.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use concise character class syntax '\d' instead of '[0-9]'.

See more on https://sonarcloud.io/project/issues?id=huangruiteng_loopx&issues=AaCbtLntkhlzGgA41uH4&open=AaCbtLntkhlzGgA41uH4&pullRequest=4347

Check warning on line 57 in apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-router.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use concise character class syntax '\d' instead of '[0-9]'.

See more on https://sonarcloud.io/project/issues?id=huangruiteng_loopx&issues=AaCbtLntkhlzGgA41uH1&open=AaCbtLntkhlzGgA41uH1&pullRequest=4347

Check warning on line 57 in apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-router.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use concise character class syntax '\d' instead of '[0-9]'.

See more on https://sonarcloud.io/project/issues?id=huangruiteng_loopx&issues=AaCbtLntkhlzGgA41uH3&open=AaCbtLntkhlzGgA41uH3&pullRequest=4347

Check warning on line 57 in apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-router.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Simplify this regular expression to reduce its complexity from 68 to the 20 allowed.

See more on https://sonarcloud.io/project/issues?id=huangruiteng_loopx&issues=AaCbtLntkhlzGgA41uHv&open=AaCbtLntkhlzGgA41uHv&pullRequest=4347

Check warning on line 57 in apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-router.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use concise character class syntax '\d' instead of '[0-9]'.

See more on https://sonarcloud.io/project/issues?id=huangruiteng_loopx&issues=AaCbtLntkhlzGgA41uHx&open=AaCbtLntkhlzGgA41uHx&pullRequest=4347

Check warning on line 57 in apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-router.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use concise character class syntax '\d' instead of '[0-9]'.

See more on https://sonarcloud.io/project/issues?id=huangruiteng_loopx&issues=AaCbtLntkhlzGgA41uHz&open=AaCbtLntkhlzGgA41uHz&pullRequest=4347

Check warning on line 57 in apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-router.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use concise character class syntax '\d' instead of '[0-9]'.

See more on https://sonarcloud.io/project/issues?id=huangruiteng_loopx&issues=AaCbtLntkhlzGgA41uH0&open=AaCbtLntkhlzGgA41uH0&pullRequest=4347

Check warning on line 57 in apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-router.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use concise character class syntax '\d' instead of '[0-9]'.

See more on https://sonarcloud.io/project/issues?id=huangruiteng_loopx&issues=AaCbtLntkhlzGgA41uH2&open=AaCbtLntkhlzGgA41uH2&pullRequest=4347

Check warning on line 57 in apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-router.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use concise character class syntax '\d' instead of '[0-9]'.

See more on https://sonarcloud.io/project/issues?id=huangruiteng_loopx&issues=AaCbtLntkhlzGgA41uHy&open=AaCbtLntkhlzGgA41uHy&pullRequest=4347

Check warning on line 57 in apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-router.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use concise character class syntax '\d' instead of '[0-9]'.

See more on https://sonarcloud.io/project/issues?id=huangruiteng_loopx&issues=AaCbtLntkhlzGgA41uHw&open=AaCbtLntkhlzGgA41uHw&pullRequest=4347
);
return match?.groups?.condition ?? null;
}
Expand Down
11 changes: 11 additions & 0 deletions apps/presentation/dashboard/src/views/dashboard-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@ type TodoExplorerItem = {

type PersonalAgentTodoItem = {
resumeWhen?: string | null;
resumeReady?: boolean | null;
resumeReceiptId?: string | null;
claimedBy?: string | null;
done: boolean;
evidence?: string | null;
Expand Down Expand Up @@ -704,9 +706,18 @@ function personalTodoText(todo: TodoItem) {
return compactShareText(todo.title ?? todo.text, 112);
}

function personalTodoResumeReceiptId(todo: TodoItem) {
const receipt = todo.resume_condition?.resume_receipt;
if (!receipt || typeof receipt !== "object" || Array.isArray(receipt)) return null;
const receiptId = (receipt as Record<string, unknown>).receipt_id;
return typeof receiptId === "string" && receiptId.trim() ? receiptId.trim() : null;
}

function personalAgentTodoFromItem(todo: TodoItem, row: GoalDirectoryRow): PersonalAgentTodoItem {
return {
resumeWhen: todo.resume_when ?? null,
resumeReady: todo.resume_ready ?? null,
resumeReceiptId: personalTodoResumeReceiptId(todo),
claimedBy: todo.claimed_by ?? null,
// Legacy summaries mark deferred entries checked; they are not completed work.
done: todo.status === "deferred" ? false : todo.done,
Expand Down
13 changes: 13 additions & 0 deletions docs/project-agent-todo-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,19 @@ Deferred todos may carry a machine-readable resume condition with
material-change generation. The transition binds the monitor's current
generation as a baseline, so unchanged polls, note edits, and replay of the
same material result do not wake the todo.
- `resume_when=resume_at:<timezone-aware-rfc3339-timestamp>`: the todo becomes
ready at or after one exact instant. A timezone is mandatory; authoring
normalizes equivalent offsets to UTC. Before that instant the todo remains a
typed wait. At and after it, the projection exposes generation `1` and the
same content-addressed `todo_resume_receipt_v0` across repeated reads and
process restarts.

`resume_at` is a one-shot Todo condition, not a recurring scheduler. Natural
language such as `tomorrow morning` is rejected instead of being interpreted
relative to a host locale. CLI, heartbeat quota reads, and managed Turn consume
the same Todo projection and runtime-clock snapshot. A due receipt makes the
lifecycle replan observable, but it does not reopen the Todo or grant execution
authority by itself.

The monitor and the delivery it discovers are separate work items. A
`continuous_monitor` is observe-only; it never becomes the runnable delivery.
Expand Down
8 changes: 8 additions & 0 deletions docs/quota-allocation.md
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,14 @@ higher-priority typed wait visible as `availability_reason=resume_condition_pend
while making the runnable fallback and its bounded continuation context the
default model-facing action.

For `resume_at:<timezone-aware-rfc3339-timestamp>`, the active-state read takes
one runtime-clock snapshot and passes it through the shared Todo reducer. A
future timestamp therefore produces the same `agent_scope_wait` or fallback
selection in CLI and heartbeat paths. Once due, quota selects the deferred Todo
through `successor_replan_required`; the managed Turn sees the same stable
resume receipt and must perform the normal explicit lifecycle update. Repeated
ticks do not create additional material-change generations or receipts.

If an active per-agent vision has no other selectable advancement and its
existing current-agent or unclaimed successor is blocked by an exact supported
`resume_when`, quota projects `vision_wait_state.state=waiting` and
Expand Down
2 changes: 2 additions & 0 deletions docs/reference/protocols/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ scanning a chronological list.
- [`quota_planning_horizon_v0`](quota-planning-horizon-v0.md): Bounded agent planning horizon v0
- [`rollback_packet_v0`](rollback-packet-v0.md): Rollback packet v0
- [`task_graph_projection_v0`](task-graph-projection-v0.md): Task graph projection v0
- [`typed_date_resume_trigger_v0`](typed-date-resume-trigger-v0.md): timezone-aware one-shot Todo resume and stable due receipt v0
- [`typed_date_resume_trigger_v0`](typed-date-resume-trigger-v0.zh-CN.md): timezone-aware one-shot Todo resume and stable due receipt v0 (中文)
- [`todo_detail_cold_path_v0`](todo-detail-cold-path-v0.md): Todo detail cold path v0
- [`todo_suggestion_prompt_v0`](todo-suggestion-prompt-v0.md): Todo suggestion prompt v0
- [`turn_envelope_v0`](turn-envelope-v0.md): Turn envelope v0
Expand Down
80 changes: 80 additions & 0 deletions docs/reference/protocols/typed-date-resume-trigger-v0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Typed Date Resume Trigger v0

Status: implemented contract

## Purpose

`resume_at` gives a Todo one exact, machine-readable wakeup instant without
creating a second scheduler or message queue. It is part of the existing
`resume_when` owner and uses the same active-state, quota, managed Turn, frontend,
and Lark projection path as the other typed resume conditions.

## Authoring

The token is:

```text
resume_at:<timezone-aware-rfc3339-timestamp>
```

Examples:

```text
resume_at:2026-09-14T09:30:00+08:00
resume_at:2026-09-14T01:30:00Z
```

The boundary requires a complete calendar date, seconds, and either `Z` or an
explicit offset no larger than `14:00`. It rejects invalid dates and naive local
times. Equivalent instants are normalized to UTC before persistence. Natural
language is never inferred.

## Evaluation and receipt

One active-state projection uses one `evaluated_at` runtime-clock snapshot for
all date-bound Todos. Before the scheduled instant, the condition contains:

- `satisfied=false`;
- `material_change=false` and `material_change_generation=0`;
- `clock_provider=runtime_clock`;
- no resume receipt.

At or after the instant, it contains:

- `satisfied=true`;
- `material_change=true` and `material_change_generation=1`;
- `generation_fence=once_at_or_after_scheduled_for`;
- a `todo_resume_receipt_v0` whose identity is derived from the Todo id and
canonical condition.

The receipt's `triggered_at` is the scheduled instant, not the observation
time. Repeated ticks, later reads, and process restarts therefore reproduce the
same receipt and never increment the generation beyond `1`.

## Consumer behavior

- CLI authoring validates and reads back the canonical UTC token.
- Status and quota keep a future Todo outside executable lanes. Other eligible
work may continue according to the existing fallback policy.
- Once due, quota returns the existing `successor_replan_required` lifecycle
action. A receipt is proof of the condition transition, not execution
authority and not an implicit reopen.
- Managed Turn interprets the same quota packet. Heartbeat runs do not own a
separate timer or readiness cache.
- The frontend extends the existing Todo defer/details surface with pending,
ready, and receipt information. It does not add another configuration owner.
- Lark requires no condition-specific state owner: the Goal Channel consumes
the same status/interaction projection and may render the receipt through its
normal presentation sink.

This protocol does not alter periodic-report policy, scheduler cadence, or the
user's notification preference.

## Acceptance

1. Offset input round-trips as the equivalent UTC token; naive or invalid input
fails before writeback.
2. Before due, CLI/quota/managed Turn agree that the Todo is waiting.
3. At due, they agree on `successor_replan_required` and the receipt identity.
4. A later tick and a fresh process reproduce that receipt exactly.
5. Frontend typecheck, route smoke, and packaged dashboard build pass.
71 changes: 71 additions & 0 deletions docs/reference/protocols/typed-date-resume-trigger-v0.zh-CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# 类型化日期恢复触发器 v0

状态:已实现契约

## 目的

`resume_at` 为 Todo 提供一个精确、机器可读的一次性恢复时刻,而不新建第二套
调度器或消息队列。它归属现有 `resume_when`,并与其他类型化恢复条件共用
active-state、quota、托管 Turn、前端和 Lark 投影链路。

## 写入

Token 格式为:

```text
resume_at:<timezone-aware-rfc3339-timestamp>
```

例如:

```text
resume_at:2026-09-14T09:30:00+08:00
resume_at:2026-09-14T01:30:00Z
```

边界要求完整日历日期、秒,以及 `Z` 或不大于 `14:00` 的显式时区偏移;无时区
本地时间和非法日期都会被拒绝。等价时刻在持久化前统一规范为 UTC,不推断
“明早”之类自然语言。

## 求值与回执

一次 active-state 投影为所有日期 Todo 复用同一个 `evaluated_at` 运行时时钟快照。
在计划时刻之前,条件包含:

- `satisfied=false`;
- `material_change=false`、`material_change_generation=0`;
- `clock_provider=runtime_clock`;
- 无恢复回执。

到达或超过计划时刻后,条件包含:

- `satisfied=true`;
- `material_change=true`、`material_change_generation=1`;
- `generation_fence=once_at_or_after_scheduled_for`;
- 根据 Todo id 和规范化条件生成的 `todo_resume_receipt_v0`。

回执的 `triggered_at` 是计划时刻,而非本次观察时刻。因此重复 tick、后续读取和
进程重启都会得到同一回执,generation 不会超过 `1`。

## 消费方行为

- CLI 在写入前校验,并回读规范化的 UTC token。
- Status 与 quota 在未来时刻到来前不把 Todo 放入可执行队列;是否继续其他工作
仍由既有 fallback 策略决定。
- 到期后 quota 返回既有 `successor_replan_required` 生命周期动作。回执只证明
条件发生转换,不授予执行权限,也不会隐式 reopen Todo。
- 托管 Turn 解释同一个 quota packet;heartbeat 不拥有独立定时器或 readiness
缓存。
- 前端在既有 Todo 延后/详情入口展示 pending、ready 和回执,不新增配置真相源。
- Lark 无需条件专属状态 owner;Goal Channel 消费同一 status/interaction 投影,
并可通过既有 presentation sink 展示回执。

本协议不改变定期周报策略、调度频率或用户的通知偏好。

## 验收

1. 带 offset 的输入回读为等价 UTC token;无时区或非法输入在写入前失败。
2. 到期前,CLI、quota 与托管 Turn 一致判定为等待。
3. 到期时,各入口一致得到 `successor_replan_required` 和同一回执身份。
4. 后续 tick 与新进程精确复现该回执。
5. 前端 typecheck、route smoke 与打包构建通过。
Loading