@sema-agent/client-core 0.29.0 → 0.30.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +455 -0
  2. package/README.md +19 -3
  3. package/dist/adapt/arms.js +24 -1
  4. package/dist/adapt/wireShapes.d.ts +7 -0
  5. package/dist/adapt/wireShapes.js +7 -0
  6. package/dist/adapter/activeRunSelfHeal.d.ts +285 -48
  7. package/dist/adapter/activeRunSelfHeal.js +553 -19
  8. package/dist/adapter/runStream.js +13 -3
  9. package/dist/engineWireSdk.d.ts +10 -2
  10. package/dist/engineWireSdk.js +7 -3
  11. package/dist/hitl/approvalDecisionNoteAudit.d.ts +58 -0
  12. package/dist/hitl/approvalDecisionNoteAudit.js +91 -0
  13. package/dist/hitl/armedGateRegistry.d.ts +62 -4
  14. package/dist/hitl/armedGateRegistry.js +226 -14
  15. package/dist/hitl/askParkRowRouting.d.ts +150 -0
  16. package/dist/hitl/askParkRowRouting.js +183 -0
  17. package/dist/hitl/gateIdentity.d.ts +8 -0
  18. package/dist/hitl/gateIdentity.js +8 -0
  19. package/dist/hitl/hitlBridge.d.ts +7 -0
  20. package/dist/hitl/hitlBridge.js +11 -2
  21. package/dist/hitl/parkOwnership.d.ts +2 -1
  22. package/dist/hitl/parkOwnership.js +11 -3
  23. package/dist/hitl/parkRowBirthWait.d.ts +63 -0
  24. package/dist/hitl/parkRowBirthWait.js +192 -0
  25. package/dist/hitl/planReviewWire.d.ts +31 -1
  26. package/dist/hitl/planReviewWire.js +69 -30
  27. package/dist/hitl/resumeRunningCard.d.ts +134 -0
  28. package/dist/hitl/resumeRunningCard.js +177 -0
  29. package/dist/hitl/toolApprovalWire.d.ts +49 -9
  30. package/dist/hitl/toolApprovalWire.js +9 -0
  31. package/dist/index.d.ts +5 -0
  32. package/dist/index.js +14 -0
  33. package/dist/seatContract.d.ts +27 -0
  34. package/dist/seatContract.js +42 -0
  35. package/dist/subagent/engineSubagentTail.d.ts +0 -2
  36. package/dist/subagent/engineSubagentTail.js +7 -15
  37. package/dist/subagentContentStore.d.ts +58 -2
  38. package/dist/subagentContentStore.js +95 -6
  39. package/dist/toolResult.d.ts +26 -0
  40. package/dist/toolResult.js +38 -6
  41. package/dist/workflowClient.d.ts +6 -1
  42. package/docs/INTEGRATION-CLIENTS.md +844 -0
  43. package/docs/REFACTOR-LEDGER.md +392 -0
  44. package/package.json +7 -4
@@ -68,6 +68,13 @@ export declare function estimateCjkTokens(s: string): number;
68
68
  * B4 把它从 B 层(`wireOutputToBody`)提到独立导出:A 层的 `settlePanelTasks(report)` 也要用它
69
69
  * (关卡时把子代最终报告串到面板行 end 事件上),两处必须同一份实现。
70
70
  *
71
+ * 🔴 **包内另有一份块数组展平**(0.30.0 发包扫描登记,2026-08-14):`subagentContentStore.ts` 的
72
+ * `coerceOutput`(子代查看态结果栏那一位)。它**不能** import 本文件 —— 那个 store 在 `runStream.ts`
73
+ * 内核可移植闭包(`MAX_CLOSURE_FILES`,只许降)之内且以 dep-free 立身。两份由 **pure 门 ⑨b 段的
74
+ * 等值断言**逐形对拍钉住(六形 + 逐字面值);改本函数的块数组语义 ⇒ 同批改那一份并看那条断言。
75
+ * ⚠️ 两者只在**块数组与裸串**这两形上等值:非串非数组时本函数按「转录卡是文本粒度」返回 `''`,
76
+ * 而 `coerceOutput` 走 `JSON.stringify`(它服务的是诚实缺席/原型不保的对象形),这一分叉是刻意的。
77
+ *
71
78
  * 公面。
72
79
  */
73
80
  export declare function flattenWireOutput(output: unknown): string;
@@ -68,6 +68,13 @@ export function estimateCjkTokens(s) {
68
68
  * B4 把它从 B 层(`wireOutputToBody`)提到独立导出:A 层的 `settlePanelTasks(report)` 也要用它
69
69
  * (关卡时把子代最终报告串到面板行 end 事件上),两处必须同一份实现。
70
70
  *
71
+ * 🔴 **包内另有一份块数组展平**(0.30.0 发包扫描登记,2026-08-14):`subagentContentStore.ts` 的
72
+ * `coerceOutput`(子代查看态结果栏那一位)。它**不能** import 本文件 —— 那个 store 在 `runStream.ts`
73
+ * 内核可移植闭包(`MAX_CLOSURE_FILES`,只许降)之内且以 dep-free 立身。两份由 **pure 门 ⑨b 段的
74
+ * 等值断言**逐形对拍钉住(六形 + 逐字面值);改本函数的块数组语义 ⇒ 同批改那一份并看那条断言。
75
+ * ⚠️ 两者只在**块数组与裸串**这两形上等值:非串非数组时本函数按「转录卡是文本粒度」返回 `''`,
76
+ * 而 `coerceOutput` 走 `JSON.stringify`(它服务的是诚实缺席/原型不保的对象形),这一分叉是刻意的。
77
+ *
71
78
  * 公面。
72
79
  */
73
80
  export function flattenWireOutput(output) {
@@ -1,52 +1,41 @@
1
+ import type { ActiveRunBusySignal } from './runStream.js';
2
+ /** wire gate kind → plan_review 重开臂的 canonical 成员(core CheckpointGate 词表之一)。 */
3
+ export declare const PLAN_REVIEW_GATE_KIND = "plan_review";
1
4
  /**
2
- * adapter/activeRunSelfHeal.ts 「会话被 active-run claim 锁死」的自愈决断层(A-028.1,
3
- * #244 族A 包半场,2026-08-12;源形 = cli `src/sema/activeRunSelfHeal.ts` 的四路分诊树 +
4
- * 结局文案层,判别半场 `activeRunBusySignal` 早已在隔壁 `runStream.ts` —— 本文件是它的
5
- * 消费侧,两半从此同居包内单源)。
6
- *
7
- * ── 事故形态 ────────────────────────────────────────────────────────────────────────────────
8
- * 引擎的 session claim 是会话级锁(createRun EEXIST ⇒ 409)。终态释放它;而 park 态
9
- * (`suspended` / `needs_review`)保留它,且重启引擎救不回来(boot 期孤儿回收只捞 running)、
10
- * 时间型 reap 默认整条腿不跑。于是「上一轮 turn 在审批/提问处 park 了,用户走开」之后,这个
11
- * 会话的每一条新消息恒被 409 拒收。
5
+ * wire gate kind plan 重开臂的**全表**(#265,2026-08-14 补齐 `dry_run_review`)。
12
6
  *
13
- * ── 🔴 处置是**分诊**,不是「见 409 就取消」────────────────────────────────────────────────
14
- * 分诊真源两级(A-028.1 目标形):**`pendingGate.kind` 优先,status 表回退**。
15
- * · kind wire 指名的 gate 身份(引擎只在 parked 且真有待决 checkpoint 时带它),比 status
16
- * 粒度准:run status `needs_review` ≠ gate kind `plan_review` —— dry-run 复核门(gate kind
17
- * `needs_review`)同样落 status needs_review,按 status 猜臂会把它错路成 plan 审批卡。
18
- * · kind 缺席(旧引擎 / 非 park / store 降级)才回退 status 表。
19
- * 四路结局:
20
- * ① plan_review 门 ⇒ **重开审批卡**,绝不 cancel(cancel = 替用户 reject 掉整个 plan)。
21
- * ② 审批/提问门(human / irreversible_ask / tool_approval)⇒ **重开那张卡**(把决定权还给
22
- * 用户;cancel+自动重发臂已整退役 —— 实证它是「新消息→cancel→新 run 同 ask 再 park」的
23
- * 循环病根;重开失败也只如实告知,绝不回退 cancel)。
24
- * ③ `running`(或任何非 park 态)⇒ **什么都不做**。持有 claim 的可能是用户背景化的 durable
25
- * run,正在替他干活;见 409 就 cancel = 销毁他明确要求的工作。
26
- * ④ kind/status 两级都判不出(含今天还不存在的新门/新状态)⇒ 也什么都不做,如实告知。
27
- * 不确定时不做破坏性动作;新词到货时最坏结果是保守,而不是拿没想过的词做破坏性动作。
7
+ * server 侧的 kind→决议入口映射(`resumeEntryForGate`)把 `plan_review` 与 `dry_run_review` 送去
8
+ * **同一个**入口 `/v1/assistant/tasks/{taskId}/plan_review`,两者 park 也都落 `needs_review`
9
+ * 同臂。此前包内只认 canonical 那一个词,于是一条停在 dry-run 复核门上的 run 走结局④「不动 +
10
+ * 如实说」——话是诚实的,但那张本来可以重开的卡没被重开,会话仍旧锁着。
28
11
  *
29
- * ── 出路文案纪律 ────────────────────────────────────────────────────────────────────────────
30
- * 本模块产出的每一句「你可以做什么」都必须是**真的接了线**的动作。默认串承诺的是 cli
31
- * `/clear`(regenerateSessionId ⇒ 全新引擎会话)与引擎自己的 cancel/decide 端点;没有 `/clear`
32
- * 概念的宿主**必须**经 {@link ActiveRunSelfHealCopy} 换成自己的真出路 —— 绝不出现「按某个键」
33
- * 而那个键没有注册。
34
- *
35
- * 🔴 portability:本文件是零值级 import 的纯函数叶(类型全部 type-only 借 `runStream.ts`),
36
- * 不进内核/A 层闭包;index 闭包 +1 已登记。
12
+ * ⚠️ 别把它和 run status 词 `needs_review` 混起来:那是 **status**(两跳间接量),gate kind 叫
13
+ * `dry_run_review`。gate kind `needs_review` 仍是表外词(结局④),这条边界由常驻门逐词钉。
37
14
  */
38
- import type { ActiveRunBusySignal } from './runStream.js';
39
- /** wire gate kind → plan_review 重开臂(core CheckpointGate 六 kind 词表之一)。 */
40
- export declare const PLAN_REVIEW_GATE_KIND = "plan_review";
15
+ export declare const PLAN_REVIEW_GATE_KINDS: readonly string[];
41
16
  /**
42
- * wire gate kind → ask 重开臂的三个成员:`human` / `irreversible_ask` 是 core CheckpointGate
43
- * 的审批/提问族(有 approvals.list pending 行可铸卡);`tool_approval` 是 park 判定
17
+ * wire gate kind → ask 重开臂的成员:`human` / `irreversible_ask` / `policy_ask` 是 core
18
+ * CheckpointGate 的审批/提问族(有 approvals.list pending 行可铸卡);`tool_approval` 是 park 判定
44
19
  * (`toolApprovalWire.isToolApprovalGate`)已承认的一等 kind —— 两张表必须同集合
45
20
  * ([paired-mechanisms-must-share-premise]:park 认得出、分诊路不进去 = 恒 reopen-failed)。
46
21
  * 表外 kind(`resource_limit` / `needs_review` / `task_done` / 未来新词)一律走结局④:不动 +
47
22
  * 如实说(wire 带的 decidePath 由文案层原样交给用户)。
23
+ *
24
+ * `policy_ask` 是 #265 补齐的第四个成员(server 全表逐名对读):它与 `human` 同族同入口
25
+ * (`/v1/approvals/{sessionId}/decide`),漏一个词的代价 = 那一类门恒走结局④,卡不重开。
48
26
  */
49
27
  export declare const ASK_PARK_GATE_KINDS: readonly string[];
28
+ /**
29
+ * 行上在场且**不属 ask 门族**的门种(`plan_review` / `resource_limit` / 未来新词);`null` = 该行可入
30
+ * ask/审批臂。kind 缺席(pre-`gate_kind` 历史行)与空串也归 `null` —— 旧行没有更强信号,各臂维持
31
+ * 自己的原判据(toolName 等),门种闸只拦「明说了自己不归这族」的行。
32
+ * 🔴 单源:`classifyAskParkRows` 的门种闸与 `findPendingForTask` 的行过滤必须共用这一只 ——
33
+ * 各写各的 includes 必漂(P-30:`findPendingForTask` 侧漏了这道闸,同 task 停着 plan_review 行时
34
+ * 「任意行」回落把它递给工具审批 wire,弹 toolName 空的卡、decide 撞 409 gate_not_tool_approval)。
35
+ */
36
+ export declare function askParkForeignGateKind(row: {
37
+ gateKind?: string | null;
38
+ }): string | null;
50
39
  /**
51
40
  * status 回退表(kind 缺席时的粗粒度代理):plan_review park 的引擎 run status。
52
41
  * 🔴 动作**不按状态名硬编码枚举**:两张表之外的一切状态(含今天还不存在的)一律「不动它 +
@@ -55,6 +44,41 @@ export declare const ASK_PARK_GATE_KINDS: readonly string[];
55
44
  export declare const PLAN_REVIEW_STATES: readonly string[];
56
45
  /** status 回退表:审批/提问 park(**只有 `suspended`**)。 */
57
46
  export declare const ASK_PARK_STATES: readonly string[];
47
+ /**
48
+ * 走三选卡臂的状态 —— **只有 `running`**(引擎 RunStatus 里「正在干活」的那一个词)。
49
+ *
50
+ * 🔴 为什么是白名单而不是「park 词表之外的一切」:三选卡上的两条动作路(steer / cancel)都只对
51
+ * 一条**真在跑**的 run 成立 —— steer 的语义是「注入在跑的那一轮」(非 running 会落 `queued` /
52
+ * `parked_for_wake` / 409),cancel 对 park 态则是替用户否掉待决项(禁区)。今天还不存在的新状态
53
+ * 落在表外 ⇒ 照旧「不动它 + 如实说」:不确定时不给用户递一把语义不明的把手。
54
+ */
55
+ export declare const RUNNING_STATES: readonly string[];
56
+ /**
57
+ * 「引擎确认这条 run **不再占着会话**」的状态词 —— 也就是 cancel 之后允许重发那条消息的**唯一**
58
+ * 判据。
59
+ *
60
+ * 🔴 为什么是白名单而不是「running 之外的一切」:这条会话锁的语义是 **park 态(`suspended` /
61
+ * `needs_review`)保留 claim**。用 `!RUNNING_STATES.includes(status)` 当判据时,一条正在收尾的 run
62
+ * 只要在这一拍被读成 park(cancel 是异步的,run 完全可能先走到一个 checkpoint),就被判成「已释放」
63
+ * ⇒ 上屏说「引擎确认它不再占着这个会话」(假话)+ 立刻重发 ⇒ 那条消息一头撞进还锁着的会话,再吃
64
+ * 一个 409。没想过的新状态词同理落在白名单外:我们不知道那个词是不是「释放了」,就不能替引擎下这个
65
+ * 断言(等到点如实说「还占着」是可收敛的诚实结局,零破坏性)。
66
+ *
67
+ * 词表锚 = SDK `RunStatus`(`running|completed|failed|suspended|needs_review|blocked`)的三个终态
68
+ * + `timeout`。真·404(引擎不认得这条 run 了)另有一条腿,不走这里(见 {@link waitForClaimRelease})。
69
+ */
70
+ export declare const CLAIM_RELEASED_STATES: readonly string[];
71
+ /**
72
+ * 「这条 run **确实还占着**会话」的状态词 —— 也就是文案敢说「that run still held this session」的
73
+ * **唯一**判据(二次评审 R5 [medium])。
74
+ *
75
+ * 🔴 为什么又是白名单:上面那张表管的是「敢不敢重发」,这张表管的是「敢不敢断言它还占着」,两件事
76
+ * **都**只能由正面证据得出,而它们**不是互补的** —— 两张表之外还有一整片「读到了一个我们不认识的
77
+ * 词」的地带(server 加一个新终态 `cancelled` 就是现成的例子)。用「lastStatus 非空」当持锁判据,
78
+ * 那个新词会被读成「还占着」并渲上屏,而会话其实早就释放了。表外非空词 ⇒ 既不算释放也不算持锁,
79
+ * 收口成「确认不了」。
80
+ */
81
+ export declare const CLAIM_HELD_STATES: readonly string[];
58
82
  /**
59
83
  * durable 控制面的**鸭子类型**视图。live 客户端有 get/cancel,mock 车道没有 —— 两个都是可选,
60
84
  * `events` 是必填只为让这个接口不是「全可选属性」(全可选会触发 TS 的弱类型检查,把宽客户端形
@@ -63,14 +87,32 @@ export declare const ASK_PARK_STATES: readonly string[];
63
87
  */
64
88
  export interface DurableRunVerbs {
65
89
  events: unknown;
66
- get?: (taskId: string, opts?: {
67
- signal?: AbortSignal;
68
- }) => Promise<{
90
+ get?: (taskId: string, opts?: DurableRunCallOpts) => Promise<{
69
91
  status?: unknown;
70
92
  } | null>;
71
- cancel?: (taskId: string, opts?: {
72
- signal?: AbortSignal;
73
- }) => Promise<unknown>;
93
+ cancel?: (taskId: string, opts?: DurableRunCallOpts) => Promise<unknown>;
94
+ /** 三选卡的 steer 腿(`POST /v1/runs/:id/steer`)。🔴 **AT-MOST-ONCE**:steer 非幂等,契约逐字
95
+ * 「it is NEVER retried」(本包 `controlRouter` 同律)—— 本层调它恰一次,失败如实上屏由人重发。
96
+ * live 客户端有,mock 车道恒缺席 ⇒ 三选卡上的 steer 选项整个不渲(假 affordance 禁令)。 */
97
+ steer?: (taskId: string, steer: {
98
+ text: string;
99
+ mode?: 'all' | 'one-at-a-time';
100
+ }, opts?: DurableRunCallOpts) => Promise<unknown>;
101
+ }
102
+ /**
103
+ * durable run 动词的每次调用选项。
104
+ *
105
+ * 🔴 `session` 不是可有可无的礼貌位:SDK 头注逐字 —— 这些 run 面是**会话作用域**的,今天一个
106
+ * **缺席**的 session 只让 server 记一条迁移告警就放行,而「the enforcement train will 404 absence
107
+ * too — so new code should always send it」。等那班车到站,不带 session 的 `get`/`cancel`/`steer`
108
+ * 会**整体** 404:三选卡照样渲着 canSteer/canCancel(动词都在场),用户按下去却必然失败,cancel
109
+ * 之后的释放确认也永远读不到 —— 一张按了没用的卡,正是本文件到处在禁的那一形。所以本层把宿主
110
+ * 注入的 `sessionId` 逐次透传;宿主没给就退化成今天的行为(告警放行),但那是宿主的选择,不是
111
+ * 本层替它省掉的一步。
112
+ */
113
+ export interface DurableRunCallOpts {
114
+ signal?: AbortSignal;
115
+ session?: string;
74
116
  }
75
117
  /**
76
118
  * 重开口的判决形(契约,宿主与包内重开腿共同遵守)。
@@ -94,16 +136,72 @@ export type ReopenCardVerdict = {
94
136
  firstSight: boolean;
95
137
  presented?: boolean;
96
138
  };
139
+ /**
140
+ * `running` 三选卡的呈现请求。🔴 `canSteer` / `canCancel` 是**供给位**(那条路今天兑现得了吗),
141
+ * 呈现层据此渲选项 —— 渲一个按了没用的选项比没有这个选项更坏。
142
+ */
143
+ export interface RunningChoiceRequest {
144
+ taskId: string;
145
+ /** 引擎报的状态(如实转述,不加工)。 */
146
+ status: string;
147
+ /** steer 动词在场 **且** 有正文可送。 */
148
+ canSteer: boolean;
149
+ /** cancel 动词在场 **且** 有 `get` 可以确认 claim 真的释放。 */
150
+ canCancel: boolean;
151
+ }
97
152
  /** 分诊树的注入口(重开腿/待决卡探询都是宿主生命周期资产,经 deps 进来,本层保持纯)。 */
98
153
  export interface ActiveRunSelfHealDeps {
99
154
  /** 屏幕上是否已有一张待决卡(有 ⇒ 结局 = decision-pending,绝不重开第二张)。 */
100
155
  hasPendingDecision?: () => boolean;
156
+ /**
157
+ * [3892]-[3899] P0 假死锁防御(#244 F1 上收,语义照 cli 现实现):`hasPendingDecision` 的真值
158
+ * 常来自渲染队列长度的**本地镜像**,它在 `--resume` 后可能卡成脏值(队列里躺着一张从未渲上屏
159
+ * 的幽灵卡)—— 布尔真 + 屏上零真卡 ⇒ 每条消息被 decision-pending 臂闸死,而引擎侧那个 run 早已
160
+ * completed。本读口 = **引擎侧属主 pending 行数**(宿主装配点带归属过滤:引擎全局队列可躺着
161
+ * 别家会话的陈年行,裸计数恒非零 = 防御恒不触发,等于没修)。
162
+ * 判据:镜像说有卡时复核一次,**属主行数恰 0 = 镜像脏值的正面证据** ⇒ 不 stand down,继续真
163
+ * 分诊(放行重呈);>0 / 抛错 / 读口缺席 / 窗尽 / 调用方中止 ⇒ 保守维持 stand down(分不出
164
+ * 真卡与幽灵时,旧行为对真卡是对的;存量装配不注入时行为逐字不变)。
165
+ * 🔴 `opts.signal` = 本层合流的截止/中止口(4s 有界窗 × `deps.signal`),装配方**必须**把它
166
+ * 透传进真实读面(approvals.list)—— 只掐调用方的 await 不掐底层请求,「有界」就只是判决面
167
+ * 的说法(忽略该参的存量实现照常工作,只是失去被掐能力)。
168
+ */
169
+ listOwnedPendingApprovals?: (opts?: {
170
+ signal?: AbortSignal;
171
+ }) => Promise<number>;
101
172
  /** plan_review park 的重开口(生产 = 包 `hitl/planReviewWire.reopenPlanReviewCard`);
102
- * 判决形见 {@link ReopenCardVerdict}。 */
103
- reopenPlanReview?: (taskId: string) => ReopenCardVerdict;
104
- /** ask park 的重开口(生产 = 宿主 ask 重开编排,纯判据半场在包 `hitl/parkOwnership.ts`)。
105
- * async:待决行(=卡的内容)必须先到手才有资格说「重开了」。 */
173
+ * 判决形见 {@link ReopenCardVerdict}。同步/异步两式通吃(端的生产口可能要等呈现回执)。 */
174
+ reopenPlanReview?: (taskId: string) => ReopenCardVerdict | Promise<ReopenCardVerdict>;
175
+ /** ask park 的重开口(生产 = 宿主 ask 重开编排,纯判据半场在包 `hitl/parkOwnership.ts`
176
+ * `hitl/askParkRowRouting.ts`)。async:待决行(=卡的内容)必须先到手才有资格说「重开了」。 */
106
177
  reopenAskPark?: (taskId: string) => Promise<ReopenCardVerdict>;
178
+ /**
179
+ * `running` 三选卡的呈现口(#265 上收件)。**缺席 ⇒ 本臂整个不走**(退回 `not-parked` 现状行)
180
+ * —— 卡呈不出来时零动作是唯一诚实的收口。
181
+ * 返回值三态:`steer`(把这条消息排进在跑的那一轮)/ `cancel`(停掉它再重发)/ `wait`(什么都
182
+ * 不做)。🔴 Esc / 空答 / 读不出的答案一律由呈现层收口成 `wait`。
183
+ */
184
+ offerRunningChoice?: (req: RunningChoiceRequest) => Promise<'steer' | 'cancel' | 'wait'>;
185
+ /**
186
+ * 被 409 拒收的那条消息原文 = steer 的正文(端**原样**送,不预处理控制串 —— 契约逐字:
187
+ * 「The text is untrusted DATA — the client sends it RAW and the SERVER fences it」)。
188
+ * 缺席/空串 ⇒ steer 选项不渲(没有正文可送的「排队」是假 affordance)。
189
+ * 🔴 **只有正文**:wire 的 steer body 没有 images/attachments 位,所以同一次提交里的附件**不随行**
190
+ * —— 回执文案必须如实说这件事。
191
+ */
192
+ deniedMessage?: string;
193
+ /**
194
+ * 本 turn 的中断口(Esc)—— 自愈腿里任何**有界轮询/等待**都必须吃它。
195
+ * 🔴 cancel 落地之后这条腿要等引擎确认 claim 真的交出来(有界轮询)。那段等待期用户按 Esc,
196
+ * 若这里没有 signal,轮询照转到自己走完 ——「UI 显示已停、后台还在问」正是明令禁掉的那一形。
197
+ * 🔴 缺席 ⇒ 退回不可中断的有界轮询,不改任何既有调用方。
198
+ */
199
+ signal?: AbortSignal;
200
+ /** cancel 之后等 claim 释放的窗口(ms,缺省 {@link CANCEL_RELEASE_WAIT_MS});测试用它把窗调小。 */
201
+ cancelReleaseWaitMs?: number;
202
+ /** 本会话的引擎 sessionId —— durable run 动词逐次透传(见 {@link DurableRunCallOpts.session})。
203
+ * 缺席 = 退化成今天的「server 记一条迁移告警后放行」,enforcement 到站后那条腿会整体 404。 */
204
+ sessionId?: string;
107
205
  }
108
206
  export type SelfHealOutcome =
109
207
  /** ask park ⇒ 审批/提问卡已呈上/重开,决定权还给用户;**不 cancel、不重发**。
@@ -154,7 +252,141 @@ export type SelfHealOutcome =
154
252
  kind: 'ask-reopen-failed';
155
253
  taskId: string;
156
254
  decidePath: string | null;
255
+ }
256
+ /**
257
+ * 三选卡①:用户选了 steer,消息交给了那条持锁的 run。`delivery` = SDK `SteerReceipt.delivery`
258
+ * (`applied` / `queued` / `parked_for_wake`,读不出即 null)—— 三种投递语义**完全不同**,文案
259
+ * 必须分形(共用一句固定包裹时,`parked_for_wake` 那一格会在同一句里既说 run「is already
260
+ * working」又说它「had already finished」)。
261
+ *
262
+ * `status` = `SteerReceipt.status`(服务端投递那一刻读到的行状态词)。它不是修辞:
263
+ * `delivery:'queued'` 按契约意味着**那条 run 根本不在跑**(durably suspended / needs_review),
264
+ * 即 wire 正在纠正本层的分诊输入(进这一臂的前提是 status 读作 `running`)。
265
+ *
266
+ * `reopened` = queued 臂按 `status` 重开那张门卡的判决(其余 delivery 恒 null)—— 门没被答,
267
+ * 那条 run 就不会 resume,消息也就永远注入不进去。
268
+ */
269
+ | {
270
+ kind: 'running-steered';
271
+ taskId: string;
272
+ delivery: string | null;
273
+ status: string | null;
274
+ reopened: ReopenCardVerdict | null;
275
+ }
276
+ /**
277
+ * 三选卡①的失败半场:steer 这一枪没得到成功回执。🔴 **零重试**(非幂等),也**绝不回退到 cancel**。
278
+ *
279
+ * `delivery` 是**两类失败的分界**,不是修辞:
280
+ * · `'rejected'` = 服务端答了且明确拒(4xx:`steering.invalid_content` 422 /
281
+ * `steering.not_running` 409 / 404 non-owner / 413 too large …)⇒ 「没送到」是**已证事实**,
282
+ * 可以放心让用户重发。
283
+ * · `'unknown'` = 送达未知(5xx,或连接在回执路上断了)⇒ 请求可能**已经被受理并注入**。
284
+ * 对这一类说「你的消息没发出去,再发一遍」= 诱导用户把一条非幂等指令注入第二次,而重复的
285
+ * 副作用客户端全程看不见。诚实做法是说清「无法确认」,让用户先看那条 run 的动向。
286
+ */
287
+ | {
288
+ kind: 'running-steer-failed';
289
+ taskId: string;
290
+ detail: string;
291
+ delivery: 'rejected' | 'unknown';
292
+ }
293
+ /** 三选卡②:用户显式选了 cancel,cancel 已落地且引擎确认那条 run 不再占着会话 ⇒ 调用方把被拒的
294
+ * 这条消息**重发一次**(用户已显式授权;与「零授权自动 cancel 循环」不是一回事)。 */
295
+ | {
296
+ kind: 'running-cancelled';
297
+ taskId: string;
298
+ }
299
+ /** 三选卡②的有界收口:cancel 请求发出去了,但等到窗口到点(或用户中止了等待)那条 run 仍占着
300
+ * 会话 ⇒ **不重发**,如实说。`waitedMs` = **真等了多久**(不是预算值:预算是打算等多久,用户
301
+ * 读到的那句话说的是已经发生的事);`aborted` = 收口原因是用户中止而不是窗口到点。 */
302
+ | {
303
+ kind: 'running-cancel-timeout';
304
+ taskId: string;
305
+ waitedMs: number;
306
+ aborted: boolean;
307
+ /** 🔴 最近一次探测**读到了一个确实代表持锁的词**吗(判据 = {@link CLAIM_HELD_STATES},不是
308
+ * 「lastStatus 非空」—— 表外的新状态词非空却证明不了持锁)。false = 读不出来 / 读到一个不认识
309
+ * 的词 ⇒ 文案只许说「确认不了」,绝不说「它还占着」。 */
310
+ confirmedHeld: boolean;
311
+ }
312
+ /**
313
+ * 三选卡②的失败半场:cancel 那一枪没成,且**有界确认腿也没看到会话被交出来**。
314
+ * `delivery` 与 steer 同义(见 running-steer-failed):`rejected` = 服务端明确拒了那一枪(4xx,
315
+ * 确定没生效);`unknown` = 送达未知(5xx / 连接断)—— 那条 run **可能已经在停了**,文案不许
316
+ * 断言「它还占着会话」。
317
+ */
318
+ | {
319
+ kind: 'running-cancel-failed';
320
+ taskId: string;
321
+ detail: string;
322
+ delivery: 'rejected' | 'unknown';
157
323
  };
324
+ /**
325
+ * 一次**至多一次**(non-idempotent)POST 失败之后:到底是「服务端明确拒了」还是「不知道有没有
326
+ * 落地」。steer 与 cancel 两条腿共用这一把尺 —— 它们同属「这一枪不能盲发第二次」的族,而两类
327
+ * 失败对用户的处置**完全不同**,各写一份必漂。
328
+ *
329
+ * 判据 = HTTP 状态码在不在、是不是 4xx:**4xx = 服务端收到了、解析了、明确拒绝**(steering 族的
330
+ * 拒收码全在 4xx:422 invalid_content / 409 not_running|queue_full|duplicate_input_id / 404 / 413),
331
+ * 那一枪确定没有产生效果;5xx 与「压根没有状态码」(fetch failed / 连接断 / 超时)都是**送达未知**
332
+ * —— 请求可能已经被受理,只是回执没回来。
333
+ * 🔴 方向:不确定时归 `unknown`(保守面在「不对用户断言一件证不出的事」这一侧)。
334
+ */
335
+ export declare function atMostOnceFailureClass(e: unknown): 'rejected' | 'unknown';
336
+ /** SDK `SteerReceipt.delivery`(`applied` / `queued` / `parked_for_wake`)。读不出 = null —— 不编造
337
+ * 一个投递语义:三种 delivery 对用户的意思完全不同,猜错就是假承诺。 */
338
+ export declare function readSteerDelivery(receipt: unknown): string | null;
339
+ /** SDK `SteerReceipt.status` —— 服务端在投递那一刻读到的**行状态词**(`running` /「park 词」/终态)。
340
+ * `delivery:'queued'` 时它是**唯一**能分出「在等哪一类门」的材料(契约逐字:`"suspended"` OR
341
+ * `"needs_review"`),读不出即 null(不猜一个门种)。 */
342
+ export declare function readSteerReceiptStatus(receipt: unknown): string | null;
343
+ /** cancel 之后**有界**等那条 run 交出会话的缺省窗(`POST …/cancel` 是 202 异步 —— 收下 ≠ 已停)。 */
344
+ export declare const CANCEL_RELEASE_WAIT_MS = 10000;
345
+ /** {@link waitForClaimRelease} 的收口:`released` = 那条 run 真交出了会话;`waitedMs` = **真等了
346
+ * 多久**;`aborted` = 收口原因是调用方中止(用户 Esc)而不是窗口到点。 */
347
+ export interface ClaimReleaseVerdict {
348
+ released: boolean;
349
+ waitedMs: number;
350
+ aborted: boolean;
351
+ /**
352
+ * **最近一次**探测读到的状态词;那一次读不出来(404 / 传输错 / 奇形记录)⇒ null。
353
+ *
354
+ * 🔴 它是文案层分「还占着」与「读不出来」的唯一判据:`released:false` 有两种出身 —— 读到了 park
355
+ * 词(那句「那条 run 还占着这个会话」是**可证的**)与读不出来(此时说「它还占着」就是替引擎下
356
+ * 一个证不出的断言)。收口成同一句话 = 又一次把「不知道」渲成事实。
357
+ * 🔴 **只记最近那一次,不记「窗内曾经读到过」**(二次评审 R2 [medium]):先读到 running、此后到
358
+ * 窗尽全是 404 的时序里,最新证据是「读不出来」;留着那个陈旧的 running 会让收口行断言一件它
359
+ * 已经不知道的事,恰好塌回本批要修的那一格。
360
+ */
361
+ lastStatus: string | null;
362
+ }
363
+ /** {@link waitForClaimRelease} 的注入口(时钟/等待/中止/窗口全部可注入 —— 门要能在零墙钟下判)。 */
364
+ export interface ClaimReleaseWaitDeps {
365
+ /** run 详情读口(生产 = durable `runs.get`)。 */
366
+ get: NonNullable<DurableRunVerbs['get']>;
367
+ /** 总窗上限(ms)。 */
368
+ budgetMs: number;
369
+ /** 时钟(缺省 Date.now)。 */
370
+ now?: () => number;
371
+ /** 退避等待(缺省 = 可中断 setTimeout)。 */
372
+ sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
373
+ /** 调用方中止口(Esc)。 */
374
+ signal?: AbortSignal;
375
+ /** 本会话的引擎 sessionId(逐次透传进 `get`;见 {@link DurableRunCallOpts.session})。 */
376
+ session?: string;
377
+ }
378
+ /**
379
+ * 等到那条 run 不再占着会话 ⇒ `released`;窗口到点仍占着 / 读不到 / 调用方中止 ⇒ 不 released
380
+ * (如实说,**绝不重发**那条被拒的消息)。
381
+ *
382
+ * 🔴 判据锚在**决定量**本身:`runs.get` 读回的 status 在不在 {@link CLAIM_RELEASED_STATES} 白名单里
383
+ * (而不是「cancel 回了 202」那个前置条件,也不是「不是 running」那个更弱的反面)。
384
+ * 🔴 读失败(传输错)**不**当成「释放了」:那是「不知道」,而把不知道当成释放 = 把消息喂进一个可能
385
+ * 还锁着的会话(下一次 409 就是它的代价)。只有 404(引擎不认得这条 run 了)才算释放 —— 那种情况
386
+ * 下它连 claim 都不可能还占着。
387
+ * 🔴 有界性由**每一发都带截止 signal**保证(见 {@link claimProbeSignal}),不是靠循环顶那一次减法。
388
+ */
389
+ export declare function waitForClaimRelease(taskId: string, deps: ClaimReleaseWaitDeps): Promise<ClaimReleaseVerdict>;
158
390
  /**
159
391
  * 自愈一次:定门(kind 优先 → status 表回退)→ 按门类重开对应的卡 → 回报结局。**绝不抛**
160
392
  * (turn 收尾路径)。kind 在场时不再回查 runs.get —— wire 已指名门身份,省一次往返。
@@ -175,6 +407,11 @@ export interface ActiveRunSelfHealCopy {
175
407
  /** 整行覆盖(headless 面):返回 undefined = 落回默认串。 */
176
408
  headlessRowFor?: (signal: ActiveRunBusySignal) => string | undefined;
177
409
  }
410
+ /** cli 的唯一真出路(`/clear` = 新会话 id ⇒ 引擎侧全新 session ⇒ 不受旧 claim 影响)。
411
+ * 导出(#244 F1):宿主的 resume 冷启动对账腿等失败半场要说**同一句**真出路 —— 各写一份必漂
412
+ * (一句「按某个键」而那个键没注册,正是本文件头在骂的形)。覆盖语义照旧走
413
+ * {@link ActiveRunSelfHealCopy.wayOut},本常量只是默认串的单源。 */
414
+ export declare const INTERACTIVE_WAY_OUT = "run /clear to keep working in a fresh session";
178
415
  /**
179
416
  * 每种结局的上屏整行。**每一句出路都真的接了线**;没有真出路的分支就直说「这个会话暂时无法
180
417
  * 继续」。重开成功那条也要上屏 —— 卡是替用户重新打开的,他得知道去答哪张、答完做什么。