@sema-agent/client-core 0.72.11 → 0.72.13

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.
@@ -0,0 +1,125 @@
1
+ /**
2
+ * executionLaneCapability — **本部署的执行车道自述**的三端共用读面(0.72.13;server ≥7.86.0 / S-426 的
3
+ * `GET /v1/capabilities.executionLane`,契约附录 B.1;sdk 9.6.0 的 `Capabilities` 尚未声明该键)。
4
+ *
5
+ * ── 这一格答的是哪一个问题 ────────────────────────────────────────────────────────────────
6
+ * 「工具跑的那台机器就是这台 server 吗」(文件系统同一性)。它决定技能的 `baseDir` 能不能发:此前消费端只能拿
7
+ * 旁证三合取(本壳 spawn ∧ 无远端 URL ∧ `REMOTE_EXEC` ∈ {空, host})去猜,`-p` / mock 两条车道因此发不出 `baseDir`。
8
+ * 7.86.0 起 server 自己说:`{ provider, toolsOnThisHost }`。
9
+ *
10
+ * ── 🔴 三条契约读法(附录 B.1 逐字)────────────────────────────────────────────────────────
11
+ * ① **键缺席 ≠「不是本机车道」**:老 server(<7.86.0)不报这一位 ⇒ `not_reported`,只能按改前的旁证判。读成 false
12
+ * 会让老 server 上的 host 部署一律欠报 `baseDir`,还渲错车道。
13
+ * ② **蕴含单向,别读成等价**:`toolsOnThisHost:false` ⇒ `baseDir` 恒不发(本位的承诺);`true` 只是**必要条件** ——
14
+ * 扁平形技能与插件来源技能在任何车道上都不带,`baseDir` 一律按 present-iff 读。
15
+ * ③ **只出词,不出内情**:apiUrl / host / serial / 镜像 / 凭据 / `mountPath` 契约上一个字都不上 wire;真出现了也不是
16
+ * 本读面该带出去的东西 ⇒ 多余成员不过境。它也**不是**权限位(`cwdHonored` 才是),不许拿它顶替。
17
+ *
18
+ * ── 四态读数(与 sql / writeProtection / webSearch.backend 三只兄弟同一套词汇)──────────────
19
+ * `unobserved`(本进程一次 caps 都没观测到)/ `not_reported`(观测到了、回体无此键 = 老 server)/ `present`。
20
+ * 本位没有 `none` 档:铸点恒铸对象(`REMOTE_EXEC` 未设折 `"host"`),`null` / 缺座 / 坏形 ⇒ 畸形(删格 ⇒ `unobserved`)。
21
+ *
22
+ * ── 🔴 窄读域只许等于或宽于铸点域 ────────────────────────────────────────────────────────────
23
+ * `provider` 的闭集取型自 server 的 `RemoteExecProvider`(加一条车道那张表自动跟着长)。本包**不**手抄那几个词当判据
24
+ * (抄一份 = 给自己立第二个判官,上游加车道那天把新词判成畸形):按**非空串**读,上屏前按 UNTRUSTED-for-display 消毒封长。
25
+ */
26
+ import { capForDisplay } from './fleetTaskDesc.js';
27
+ import { engineWireTarget } from './engineWireTarget.js';
28
+ import { engineCapsGeneration } from './engineCapsCache.js';
29
+ /**
30
+ * caps 回体 → 本格读数;**畸形一律 `undefined`**(= 这一格不写 ⇒ 读口答 `unobserved`)。
31
+ * 键不在 / `undefined` ⇒ `not_reported`(老 server);`null` / 数组 / 非对象 / 缺任一座 / 座坏形 ⇒ 畸形。
32
+ */
33
+ export function projectExecutionLaneCapability(caps) {
34
+ if (caps === null || typeof caps !== 'object')
35
+ return undefined;
36
+ if (!('executionLane' in caps))
37
+ return { kind: 'not_reported' };
38
+ const lane = caps.executionLane;
39
+ if (lane === undefined)
40
+ return { kind: 'not_reported' };
41
+ if (lane === null || typeof lane !== 'object' || Array.isArray(lane))
42
+ return undefined;
43
+ const provider = lane.provider;
44
+ const toolsOnThisHost = lane.toolsOnThisHost;
45
+ if (typeof provider !== 'string' || provider === '')
46
+ return undefined;
47
+ if (typeof toolsOnThisHost !== 'boolean')
48
+ return undefined;
49
+ return { kind: 'present', view: { provider, toolsOnThisHost } };
50
+ }
51
+ const readingByBase = new Map();
52
+ /**
53
+ * 宿主 caps probe 的**读面 tee** 落点(与三只兄弟并列;宿主在同一处按序调)。绝不 throw。
54
+ * 投影 `undefined`(畸形)⇒ 这一格**被删**而不是留着上一台引擎的旧值;`opts.generation` 关掉「旧探测覆盖新读数」竞态。
55
+ */
56
+ export function noteEngineCapsForExecutionLane(baseUrl, caps, opts) {
57
+ try {
58
+ if (typeof baseUrl !== 'string' || baseUrl === '')
59
+ return;
60
+ if (opts?.generation !== undefined && opts.generation !== engineCapsGeneration(baseUrl))
61
+ return;
62
+ const reading = projectExecutionLaneCapability(caps);
63
+ if (reading === undefined) {
64
+ readingByBase.delete(baseUrl);
65
+ return;
66
+ }
67
+ readingByBase.set(baseUrl, reading);
68
+ }
69
+ catch {
70
+ /* fail-soft:本 tee 任何分支都不许打断 caps 探测链 */
71
+ }
72
+ }
73
+ /** 本进程观测到的执行车道;这一格空缺 ⇒ `{kind:'unobserved'}`(**绝不**编一条车道)。 */
74
+ export function observedExecutionLane(baseUrl = engineWireTarget()?.baseUrl) {
75
+ if (typeof baseUrl !== 'string' || baseUrl === '')
76
+ return { kind: 'unobserved' };
77
+ const hit = readingByBase.get(baseUrl);
78
+ if (hit !== undefined)
79
+ return hit;
80
+ return { kind: 'unobserved' };
81
+ }
82
+ /**
83
+ * 「工具是否跑在本机」的**判据单源**(此前三端各拼旁证三合取):读到位就用位 —— `true` 是发 `baseDir` 的必要条件、
84
+ * `false` 是「恒不发」的承诺;读不到(`not_reported` / `unobserved`)⇒ **原样**用调用方的旁证,绝不折 false
85
+ * (契约 🔴:老 server 上只能按改前的办法判)。
86
+ */
87
+ export function toolsRunHereFromExecutionLane(reading, legacyInference) {
88
+ if (reading.kind === 'present')
89
+ return reading.view.toolsOnThisHost;
90
+ return legacyInference;
91
+ }
92
+ /** 车道词的展示上限(UTF-16 单元;与三只兄弟同值同理由)。 */
93
+ const LANE_DETAIL_MAX = 40;
94
+ /**
95
+ * 三态(+未观测)→ doctor 那一行的 detail 串。**唯一措辞真源**(三端共用;端只拼接不另写)。
96
+ * 🔴 `not_reported` 那一句**不许**暗示「不是本机车道」;`present` 的 false 那一句把「技能 baseDir 不发」说出来
97
+ * (用户看到技能相对路径解析不了时,这就是原因)。
98
+ */
99
+ export function executionLaneDoctorDetail(reading) {
100
+ switch (reading.kind) {
101
+ case 'unobserved':
102
+ return ('execution lane not observed — the engine reports it on /v1/capabilities; ' +
103
+ 'this process has not received a capabilities response from the engine yet');
104
+ case 'not_reported':
105
+ return ('execution lane not reported by this engine — only newer engines advertise it; ' +
106
+ 'this does not say where tools run, so the client keeps inferring it the way it did before');
107
+ case 'present': {
108
+ const lane = capForDisplay(reading.view.provider, LANE_DETAIL_MAX);
109
+ if (reading.view.toolsOnThisHost) {
110
+ return `execution lane ${lane} — tools run on the engine host (same filesystem), so skills that have a directory of their own can carry a baseDir`;
111
+ }
112
+ return `execution lane ${lane} — tools run on a different machine than the engine, so skill baseDir is never sent on this deployment`;
113
+ }
114
+ }
115
+ }
116
+ /** **换代失效口**(由宿主在引擎温切成功后调):清成未观测,不是留旧值。空串 ⇒ no-op;绝不 throw。 */
117
+ export function forgetExecutionLaneReading(baseUrl) {
118
+ if (typeof baseUrl !== 'string' || baseUrl === '')
119
+ return;
120
+ readingByBase.delete(baseUrl);
121
+ }
122
+ /** 测试钩子。 */
123
+ export function __resetExecutionLaneReadingsForTests() {
124
+ readingByBase.clear();
125
+ }
@@ -97,8 +97,15 @@ export function coerceWorkflowStatus(s) {
97
97
  return 'killed';
98
98
  case 'parked':
99
99
  return 'awaiting approval';
100
- default:
100
+ case 'queued':
101
+ return 'queued';
102
+ case 'running':
101
103
  return 'running';
104
+ default:
105
+ // 0.72.12 CC-51(#64 同形):认不出的词不再谎报活跃 —— 与同文件 `coerceTaskStatus`(REF-CC-043)同律落
106
+ // 非活跃中性词 `idle`(FleetTaskStatus 是 wire 闭集,无 unknown 位;idle = 「活着但此刻没在推进」,
107
+ // 是闭集里唯一不断言方向的词)。
108
+ return 'idle';
102
109
  }
103
110
  }
104
111
  /**
@@ -37,7 +37,7 @@ export interface FleetAgentRowLike {
37
37
  * 详情页 elapsed 只能锚「壳建行时刻」——遗留行上渲出 1h+ 的假时长。缺席=老引擎,消费端保持现状。 */
38
38
  startedAt?: number;
39
39
  }
40
- /** 行从投影集消失多久之后按 completed 兜底 settle(重连空窗 / snapshot 清表远小于此)。 */
40
+ /** 行从投影集消失多久之后上报 `absent`(0.72.12 起不再 settle;重连空窗 / snapshot 清表远小于此)。名字沿用(公面导出)。 */
41
41
  export declare const ABSENT_SETTLE_MS = 30000;
42
42
  /**
43
43
  * 测试钩:清投影台账 —— **全部 sessionKey**,不是只清 default 桶。
@@ -36,20 +36,24 @@
36
36
  * setAppState 全树重渲)。
37
37
  * ② 终态行发一次 `end`(幂等台账),并摘除常驻标 —— 与 bg_notification 合成链同一个 settle 边,
38
38
  * 两边先到先得,消费端 settle 本身幂等(running 才动)。
39
- * ③ 缺席兜底:行从投影集里消失且**超过宽限窗**仍没见终态 ⇒ 按 completed settle。诚实边界:这是
40
- * 安全网不是真相源(真终态来自行帧终态 / bg_notification),宽限窗内的重连空窗(snapshot 清表)
41
- * 不会误结。lastSeen 恒**先**按本次在场行刷新、再判缺席,所以 footer 长时间卸载后重挂也不误结。
39
+ * ③ 缺席上报(0.72.12 CC-50 改形):行从投影集里消失且**超过宽限窗**仍没见终态 ⇒ 经**独立缺席口**(`publishEngineAgentPanelAbsence`,0.72.13;不上主通道)
40
+ * 上报一条(**不是** `end`、**不** settle)。此前这里「按 completed settle」= 把「引擎不再上报这一行」
41
+ * 编成「成功」—— 本包手上没有任何载体说明它完没完成(test [7541] 两条现场腿秒级实测:屏上
42
+ * ✔ Completed 的真实来源就是这一格;server [7545]:子行随父 run 离场被 `orphaned` 移除,真终局由
43
+ * 后续对账周期补发,结构性晚于这个窗)。真终态帧迟到照发那唯一一次 `end`;行回来照发 `fleet-row`。
44
+ * 宽限窗内的重连空窗(snapshot 清表)不上报;lastSeen 恒**先**按本次在场行刷新、再判缺席,所以
45
+ * footer 长时间卸载后重挂也不误报。
42
46
  * ④ 行 id 尾段判(`${runId} ${taskId}` 复合形取尾;真机 1.277.4 上子行 id 是裸 a+16hex,复合形是
43
47
  * run 发布器双生行的形)——REF-CC-042:与同包 `workflow.rowIdTail` 同一个函数(import,不再
44
48
  * 就地维护第二份;该函数出过等价性事故,0.1.1 修,见 `workflow.ts` 头注)。
45
49
  */
46
- import { publishEngineAgentPanelEvent } from './engineAgentPanelStore.js';
50
+ import { publishEngineAgentPanelAbsence, publishEngineAgentPanelEvent } from './engineAgentPanelStore.js';
47
51
  import { rowIdTail } from './workflow.js';
48
52
  import { TERMINAL_FLEET_TASK_STATUSES } from './fleet/fleetProjection.js';
49
53
  import { DEFAULT_SESSION_KEY } from './sessionSlot.js';
50
54
  // L-215+(0.65.0;core [6908]):「非成功终局」的单铸谓词(与 notifications 的同名位同源)。
51
55
  import { isTerminalNotSuccess } from './runTerminal.js';
52
- /** 行从投影集消失多久之后按 completed 兜底 settle(重连空窗 / snapshot 清表远小于此)。 */
56
+ /** 行从投影集消失多久之后上报 `absent`(0.72.12 起不再 settle;重连空窗 / snapshot 清表远小于此)。名字沿用(公面导出)。 */
53
57
  export const ABSENT_SETTLE_MS = 30_000;
54
58
  /** REF-CC-045(fleet2-07):settled 条目的回收期 —— settle 后这么久仍未再被投影 ⇒ 台账整条
55
59
  * delete,给 `fleetAgentProjectionSizeFor()` 一个上界(此前 `seen` 只增不减,长会话里每个
@@ -177,6 +181,7 @@ export function projectFleetAgentRowsFor(sessionKey, rows, nowMs = Date.now()) {
177
181
  settled: true,
178
182
  // notif-F13:settle 时刻只在**首次** settle 时铸(重复投影的终态行不许一路续命回收期)。
179
183
  settledAtMs: prev?.settled ? prev.settledAtMs : nowMs,
184
+ absentReportedAtMs: undefined,
180
185
  });
181
186
  continue;
182
187
  }
@@ -186,6 +191,7 @@ export function projectFleetAgentRowsFor(sessionKey, rows, nowMs = Date.now()) {
186
191
  // 值↔undefined 之间来回抖,每秒白发两次事件。
187
192
  if (!prev ||
188
193
  prev.settled ||
194
+ prev.absentReportedAtMs !== undefined || // 行回来了:即使值未变也发一条,消费端据此撤掉 absent 标
189
195
  prev.status !== status ||
190
196
  prev.tokens !== tokens ||
191
197
  (toolUses !== undefined && prev.toolUses !== toolUses) ||
@@ -216,29 +222,40 @@ export function projectFleetAgentRowsFor(sessionKey, rows, nowMs = Date.now()) {
216
222
  lastSeenAt: nowMs,
217
223
  settled: false,
218
224
  settledAtMs: undefined, // 又活了 ⇒ 回收钟归零(重新 settle 时重铸)
225
+ absentReportedAtMs: undefined, // 在场 ⇒ 缺席周期结束(下次再消失是新周期,会再上报一次)
219
226
  });
220
227
  }
221
- // ③ 缺席兜底 —— 必须在【本次在场行全部刷完 lastSeen 之后】判,否则 footer 长时间卸载后重挂
222
- // 会把仍在跑的行按「陈旧」误结。
228
+ // ③ 缺席上报 —— 必须在【本次在场行全部刷完 lastSeen 之后】判,否则 footer 长时间卸载后重挂
229
+ // 会把仍在跑的行按「陈旧」误报。🔴 0.72.12 CC-50:这里只发 `absent`,**不** settle、不铸 end ——
230
+ // 缺席不是终局,本包不替引擎下这个断言;真终态帧迟到照走 ② 发那唯一一次 end(去重记号不被缺席占用)。
223
231
  for (const [taskId, s] of seenMap) {
224
- if (present.has(taskId) || s.settled)
232
+ if (present.has(taskId) || s.settled || s.absentReportedAtMs !== undefined)
225
233
  continue;
226
- if (nowMs - s.lastSeenAt <= ABSENT_SETTLE_MS)
234
+ const absentForMs = nowMs - s.lastSeenAt;
235
+ if (absentForMs <= ABSENT_SETTLE_MS)
227
236
  continue;
228
- publishEngineAgentPanelEvent({ kind: 'end', taskId, isError: false });
229
- seenMap.set(taskId, { ...s, settled: true, settledAtMs: nowMs }); // notif-F13:settle 时刻按**现在**铸
237
+ // 🔴 0.72.13:走**独立缺席口**,不上主通道(老消费者的裸 else = sweep 体,会把新 kind 当 sweep;见 store 头注 / DS-07)。
238
+ publishEngineAgentPanelAbsence({ taskId, lastSeenAtMs: s.lastSeenAt, absentForMs });
239
+ seenMap.set(taskId, { ...s, absentReportedAtMs: nowMs });
230
240
  }
231
241
  // ④ REF-CC-045(fleet2-07):settled 条目回收 —— 给台账一个上界(此前只增不减)。settle 已发过
232
242
  // 事件,delete 对外无事件,消费端 settle 本身幂等。
233
243
  // [2393] notif-F13/sweep-F7 两处纠(判据见 SETTLED_RETENTION_MS 头注):
234
244
  // · 锚 `settledAtMs`(settle 那一刻)而不是 `lastSeenAt`(最后一次被投影);
235
245
  // · 只回收**已不在场**的条目 —— 仍在场的终态行删了会每拍重发 end。
246
+ // 0.72.12:已上报 absent 且始终不再在场的条目同样有上界 —— 锚 `absentReportedAtMs`(上报那一刻),
247
+ // 回收后迟到的终态帧仍会发恰一次 end(prev 缺席 ⇒ 首次 settle),行回来则按新行重建。
236
248
  for (const [taskId, s] of seenMap) {
237
- if (!s.settled || present.has(taskId))
249
+ if (present.has(taskId))
238
250
  continue;
239
- const settledAt = s.settledAtMs ?? s.lastSeenAt;
240
- if (nowMs - settledAt > SETTLED_RETENTION_MS)
251
+ if (s.settled) {
252
+ const settledAt = s.settledAtMs ?? s.lastSeenAt;
253
+ if (nowMs - settledAt > SETTLED_RETENTION_MS)
254
+ seenMap.delete(taskId);
255
+ }
256
+ else if (s.absentReportedAtMs !== undefined && nowMs - s.absentReportedAtMs > SETTLED_RETENTION_MS) {
241
257
  seenMap.delete(taskId);
258
+ }
242
259
  }
243
260
  if (seenMap.size === 0)
244
261
  seen.delete(sessionKey);
@@ -138,7 +138,18 @@ export interface PlanReviewOutcome {
138
138
  decision: 'approve' | 'edit' | 'reject';
139
139
  editedPlan?: string;
140
140
  reason?: string;
141
+ /**
142
+ * 0.72.13 CC-46(engine ≥7.86.0;契约 §4c):批准之后这条任务继续用的 permission mode。**只配 `approve`**;
143
+ * 缺席 = 键不落(老壳姿势,引擎按 `default`)。🔴 适用面读不到 wire:只有**提交这条任务的那一方**知道它是不是
144
+ * `permissionMode:"plan"` 只读起步的 —— 不是只读起步的任务送这一位会吃 400 `request.field_conflict`,那次批准整个送不出去。
145
+ */
146
+ permissionModeAfter?: PlanReviewModeAfter;
141
147
  }
148
+ /** 契约 §4c `permissionModeAfter` 的闭集两词(`acceptEdits` = 工作目录内的写不再逐次征询;`default` = 每次写照常征询)。 */
149
+ export declare const PLAN_REVIEW_MODE_AFTER_WORDS: readonly ["default", "acceptEdits"];
150
+ export type PlanReviewModeAfter = (typeof PLAN_REVIEW_MODE_AFTER_WORDS)[number];
151
+ /** 这个值是不是闭集里的词(词表外 —— 含 `plan` / `bypassPermissions` / `auto` —— 引擎 400:一次批准不许顺带关掉征询面)。 */
152
+ export declare function isPlanReviewModeAfter(v: string | undefined): v is PlanReviewModeAfter;
142
153
  /**
143
154
  * {@link HitlSafetyError.code} 的闭集(0.60.0 抽名,**成员一个没动**)。
144
155
  *
@@ -147,7 +158,7 @@ export interface PlanReviewOutcome {
147
158
  * ——抄一份就是给自己立第二个判官(B-025 的病形)。加员仍必须**同批**改齐
148
159
  * `parkResolver.GATE_FAILURE_CODES` 白名单(见下方原注)。
149
160
  */
150
- export type HitlSafetyCode = 'binding_mismatch' | 'no_pending' | 'wrong_gate' | 'bad_plan_edit' | 'empty_answer';
161
+ export type HitlSafetyCode = 'binding_mismatch' | 'no_pending' | 'wrong_gate' | 'bad_plan_edit' | 'bad_plan_mode' | 'empty_answer';
151
162
  /**
152
163
  * 一次决断链失败**停在哪一段**(0.60.0;异源对抗复审 [medium] 采纳,B-037 候包件 ② 的完整兑现)。
153
164
  *
@@ -35,6 +35,12 @@ export function denyReasonForWire(reason, tag) {
35
35
  `(server caps approval reason at ${MAX_DENY_REASON_CHARS}; over-cap is a 413 reason_too_large that would drop the DECISION, not just the reason)`);
36
36
  return cut;
37
37
  }
38
+ /** 契约 §4c `permissionModeAfter` 的闭集两词(`acceptEdits` = 工作目录内的写不再逐次征询;`default` = 每次写照常征询)。 */
39
+ export const PLAN_REVIEW_MODE_AFTER_WORDS = Object.freeze(['default', 'acceptEdits']);
40
+ /** 这个值是不是闭集里的词(词表外 —— 含 `plan` / `bypassPermissions` / `auto` —— 引擎 400:一次批准不许顺带关掉征询面)。 */
41
+ export function isPlanReviewModeAfter(v) {
42
+ return v !== undefined && PLAN_REVIEW_MODE_AFTER_WORDS.includes(v);
43
+ }
38
44
  /** A safety stop: a contract law was about to be violated (e.g. a binding mismatch, a wrong-gate route).
39
45
  * The caller MUST re-present to the human or surface the error — NEVER silently retry or auto-decide
40
46
  * (contract/04 §9.1 fail-closed law). */
@@ -603,7 +609,21 @@ export class HitlBridge {
603
609
  else if (outcome.editedPlan !== undefined) {
604
610
  throw new HitlSafetyError(`plan-review "${outcome.decision}" must NOT carry editedPlan`, 'bad_plan_edit');
605
611
  }
612
+ // 0.72.13 CC-46:permissionModeAfter invariant(契约 §4c)—— 只配 approve、闭集两词;坏形本地拦,一个字节都不出门
613
+ // (server 会 400 field_conflict / field_invalid,而那次决断就整个丢了)。拒文不回显调用方给的值。
614
+ const modeAfter = outcome.permissionModeAfter;
615
+ if (modeAfter !== undefined) {
616
+ if (outcome.decision !== 'approve') {
617
+ throw new HitlSafetyError(`plan-review "${outcome.decision}" must NOT carry permissionModeAfter (it only applies to an approval)`, 'bad_plan_mode');
618
+ }
619
+ if (!isPlanReviewModeAfter(modeAfter)) {
620
+ throw new HitlSafetyError('plan-review permissionModeAfter must be "default" or "acceptEdits"', 'bad_plan_mode');
621
+ }
622
+ }
623
+ // sdk 9.6.0 的 PlanReviewRequest 尚未声明 permissionModeAfter ⇒ 交集型加一可选位(不断言、不 any);缺席 = 键不落。
606
624
  const req = { decision: outcome.decision };
625
+ if (modeAfter !== undefined && isPlanReviewModeAfter(modeAfter))
626
+ req.permissionModeAfter = modeAfter;
607
627
  // eopt:上面的 invariant 检查已经保证 decision==='edit' ⇒ editedPlan 非空,但那道保证隔着一个
608
628
  // 已经关闭的 if 块,TS 不跨块记账——这里重新直接判一次 undefined,narrowing 与赋值同一表达式内完成
609
629
  // (PlanReviewRequest 是 SDK 外部 wire 型,不能改;诚实缺席 = 键不落,不传 `editedPlan: undefined`)。
@@ -45,7 +45,7 @@ export declare const MAX_TOTAL_PARKS = 64;
45
45
  * 双向钉住,任何一边加员而另一边不跟 ⇒ 编译红。此前两边靠人肉同步,而不同步的可观测后果是
46
46
  * 「新码被 `isGateFailureCode` 降成 undefined 后静默走文案兜底臂」—— 门里长得跟正常一模一样。
47
47
  */
48
- export declare const GATE_FAILURE_CODES: readonly ["no_pending", "binding_mismatch", "wrong_gate", "bad_plan_edit", "empty_answer"];
48
+ export declare const GATE_FAILURE_CODES: readonly ["no_pending", "binding_mismatch", "wrong_gate", "bad_plan_edit", "bad_plan_mode", "empty_answer"];
49
49
  export type GateFailureCode = (typeof GATE_FAILURE_CODES)[number];
50
50
  export type GateOutcome =
51
51
  /** `receipt`(B-070 / L-200,0.65.0):这次 decide 的 **200 回执读数** —— 与 fs 腿的
@@ -98,6 +98,8 @@ export const GATE_FAILURE_CODES = [
98
98
  'binding_mismatch',
99
99
  'wrong_gate',
100
100
  'bad_plan_edit',
101
+ /** 0.72.13 CC-46:plan-review 的 permissionModeAfter 坏形(非 approve 带这一位 / 词表外)被本地拦下。 */
102
+ 'bad_plan_mode',
101
103
  /** FIX②:空作答被本地拦下(空 answers[] / 空 selected[] / 空 header)。 */
102
104
  'empty_answer',
103
105
  ];
@@ -37,6 +37,7 @@
37
37
  * app-state seam 不在本模块可达面,记 rc.47 接 REPL 层;engine 侧 handsReadOnly 已由 resume 处理。
38
38
  */
39
39
  import { type QuestionAnswer } from '../liveQuestionStore.js';
40
+ import { type PlanReviewModeAfter } from './hitlBridge.js';
40
41
  import type { ReopenCardVerdict } from '../adapter/activeRunSelfHeal.js';
41
42
  /**
42
43
  * REF-CC-026:此前是 module-private 常量,唯一另一个消费者(cli `planReviewReopen.ts`)只能靠
@@ -46,6 +47,17 @@ import type { ReopenCardVerdict } from '../adapter/activeRunSelfHeal.js';
46
47
  */
47
48
  export declare const PLAN_REVIEW_APPROVE_LABEL = "Yes, approve and run the plan";
48
49
  export declare const PLAN_REVIEW_REJECT_LABEL = "No, reject it (keep planning)";
50
+ /**
51
+ * 0.72.13 CC-46(engine ≥7.86.0 S-433;契约 §4c):三选卡的两个 Yes —— 对齐 CC `ExitPlanMode` 卡
52
+ * 「Yes, and auto-accept edits」/「Yes, and manually approve edits」。只在**双闸**都过时才出三选卡(见 {@link planReviewCardOptions}
53
+ * 的调用点):宿主声明这条任务是它自己用 `permissionMode:"plan"` 提交的(适用面读不到 wire)∧ 引擎版本证据 ≥7.86.0。
54
+ */
55
+ export declare const PLAN_REVIEW_APPROVE_AUTO_LABEL = "Yes, and auto-accept edits";
56
+ export declare const PLAN_REVIEW_APPROVE_MANUAL_LABEL = "Yes, and manually approve edits";
57
+ /** `permissionModeAfter` 的引擎地板(server 7.86.0;更老的 server 静默丢这个键 = 承诺了却不兑现)。 */
58
+ export declare const PLAN_REVIEW_MODE_AFTER_MIN_ENGINE: readonly [number, number, number];
59
+ /** 引擎版本串够不够新:三态 —— 读不出(缺席 / 非 x.y.z)⇒ `unknown`,**不折 supported**(没证据不许出三选卡)。 */
60
+ export declare function versionSupportsPlanReviewModeAfter(v: string | undefined): 'supported' | 'unsupported' | 'unknown';
49
61
  /**
50
62
  * REF-CC-026:三值判决(此前 `selected === APPROVE_LABEL ? 'approve' : 'reject'` 把「没读懂/没
51
63
  * 选」的第三种输入静默塌缩进危险的 reject 臂——overlay 的 dismiss/拒答语义是「送空 answers」,
@@ -53,7 +65,28 @@ export declare const PLAN_REVIEW_REJECT_LABEL = "No, reject it (keep planning)";
53
65
  * (含 undefined)落 `'dismissed'`,由调用方决定是保持 park 还是显式当 reject 处理——本函数只
54
66
  * 负责如实分类,不替调用方做选择。
55
67
  */
56
- export declare function planReviewDecisionFromAnswer(answer: QuestionAnswer | null | undefined): 'approve' | 'reject' | 'dismissed';
68
+ export declare function planReviewDecisionFromAnswer(answer: QuestionAnswer | null | undefined,
69
+ /** 本次实际展示的那张卡的标签;不给 = 老两选(见 {@link planReviewChoiceFromAnswer})。 */
70
+ shownLabels?: readonly string[]): 'approve' | 'reject' | 'dismissed';
71
+ /** 0.72.13 CC-46:带批准后模式的判读(三选卡的两个 Yes 各带一档;老两选的 Yes 不带键)。标签显式等值比对,第三种输入 ⇒ dismissed。 */
72
+ export type PlanReviewChoice = {
73
+ decision: 'approve';
74
+ permissionModeAfter?: PlanReviewModeAfter;
75
+ } | {
76
+ decision: 'reject';
77
+ };
78
+ export declare function planReviewChoiceFromAnswer(answer: QuestionAnswer | null | undefined,
79
+ /**
80
+ * 本次**实际展示**的那张卡的标签(`planReviewCardOptions(offer).map(o => o.label)`);只认其中的标签。
81
+ * 🔴 **不给 = 老两选**:三选卡的两个 Yes 只有调用方显式说「我展示的是三选卡」才读得成批准 —— 缺省不许替老调用方
82
+ * 悄悄扩大批准的输入域(异源对抗复审:重开腿恒两选,缺省放行时一个没展示过的 auto 标签会被投递成 approve)。
83
+ */
84
+ shownLabels?: readonly string[]): PlanReviewChoice | 'dismissed';
85
+ /** 卡选项的**单源**:`offerModeAfter=false` ⇒ 老两选(逐字同旧);`true` ⇒ 三选(两个 Yes + No)。 */
86
+ export declare function planReviewCardOptions(offerModeAfter: boolean): Array<{
87
+ label: string;
88
+ description: string;
89
+ }>;
57
90
  /**
58
91
  * 推代的**门控口**(A-024.4;#244 F1 上收,语义照壳现实现):只有**决断性**作答(approve/
59
92
  * reject 标签命中 —— {@link planReviewDecisionFromAnswer} 的官方三值分类)才消费当代 plan 门。
@@ -85,13 +118,27 @@ export declare function _resetArmedPlanReviewsForTest(): void;
85
118
  * REF-CC-027 above); false = caller renders the terminal as usual. Fail-soft: any error inside must
86
119
  * never break the stream drain.
87
120
  */
88
- export declare function armPlanReviewApproval(result: unknown, sessionKey?: string): boolean;
121
+ export declare function armPlanReviewApproval(result: unknown, sessionKey?: string,
122
+ /**
123
+ * 0.72.13 CC-46:`submittedInPlanMode: true` = 宿主声明「这条任务是我用 `permissionMode:"plan"` 提交的」(只读起步)。
124
+ * 🔴 这件事**读不到 wire 上**(契约 §4c:两种任务产出同一张卡),所以只能由提交方声明;不声明 / false ⇒ 老两选卡、决断体不带键。
125
+ * 声明了还要过第二道闸:本进程 caps 缓存里的引擎版本 ≥7.86.0(更老的 server 静默丢这个键 = 卡上承诺了却不兑现)。
126
+ */
127
+ opts?: {
128
+ submittedInPlanMode?: boolean;
129
+ }): boolean;
89
130
  /** POST the decision; on settle enqueue an isMeta turn so the model reports the resume outcome.
90
131
  * SDK 宪法迁移批:SDK 0.0.52 assistant.planReview verb(同 wire `POST /v1/assistant/tasks/:id/
91
132
  * plan_review` 同 body {decision})——raw fetch 退役。错误模型:非 2xx 抛 APIError(message =
92
133
  * server body.error 原文)→ 同款「HTTP <status> <error>」outcome 文案;网络失败走原「could not
93
134
  * reach the engine」臂。 */
94
- export declare function decidePlanReview(taskId: string, decision: 'approve' | 'reject'): Promise<void>;
135
+ export declare function decidePlanReview(taskId: string, decision: 'approve' | 'reject',
136
+ /**
137
+ * 0.72.13 CC-46(engine ≥7.86.0;契约 §4c):批准之后用哪一档。缺席 ⇒ 体逐字节同旧 `{decision}`。只配 approve、闭集两词 ——
138
+ * 坏形本地拒(零 POST,outcome 如实说没送出去,不回显值)。引擎答 400 `request.field_conflict`(这条任务不是只读起步):
139
+ * `default` 与缺席同义 ⇒ 去键重发恰一次;`acceptEdits` **不**重发(不静默降成逐次征询),outcome 说批准没生效。
140
+ */
141
+ permissionModeAfter?: string): Promise<void>;
95
142
  /**
96
143
  * {@link reopenPlanReviewCard} 的宿主参数。
97
144
  */