@sema-agent/client-core 0.38.0 → 0.39.0

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.
@@ -91,7 +91,7 @@ export class HitlSafetyError extends Error {
91
91
  // {@link DecideTransportRetryExhaustedError} → **重呈臂**(re-attach ⇒ durable 流重放 park ⇒
92
92
  // 同一张卡重交用户),而不是把 turn 判死;若第一发其实已送达,重呈的下一步会撞
93
93
  // `isAlreadyResolvedGateReason` 的已解决判据被救回。
94
- // 🔴 请托半场(候黑板):SDK decide 若开 per-call timeoutMs(或对 HITL 面单列长缺省),本层的
94
+ // 🔴 请托半场(候协作板):SDK decide 若开 per-call timeoutMs(或对 HITL 面单列长缺省),本层的
95
95
  // 超时类重试环可整段收敛成一发长等待。
96
96
  /** 瞬断重试的起始退避(×2 递增,封顶 {@link DECIDE_RETRY_BACKOFF_MAX_MS};别把 decide 打成连发)。 */
97
97
  const DECIDE_TRANSPORT_RETRY_BACKOFF_MS = 750;
@@ -99,7 +99,7 @@ const DECIDE_RETRY_BACKOFF_MAX_MS = 5_000;
99
99
  /**
100
100
  * SDK 的 **per-attempt 超时帽**(`AbortSignal.timeout(timeoutMs)`)的**缺省值**。
101
101
  *
102
- * 🔴 **它是「观察到的缺省」,不是本包能保证的量**(codex 对抗复审 [medium] 采纳,2026-08-21)。
102
+ * 🔴 **它是「观察到的缺省」,不是本包能保证的量**(对抗复审 [medium] 采纳,2026-08-21)。
103
103
  * per-call opts 只有 `signal`(与之合流取先,只能收短不能放长),但 `timeoutMs` 是 **client
104
104
  * 构造级**旋钮,而本桥吃的是**宿主注入的** client(`HitlClientLike` 根本不暴露它)⇒ 一个 web/桌面
105
105
  * 宿主完全可以用 30s 或 120s 的 client 构造本桥。所以任何「总窗 ÷ 帽 = 发数」的推导都只在缺省值
@@ -133,13 +133,13 @@ const DECIDE_TIMEOUT_MIN_ATTEMPTS = 2;
133
133
  * (120s 恰等于 2 帽 + 退避,会让发数悬在退避时序的一根头发上)。
134
134
  * ⇒ 在缺省帽上可预算的最坏墙钟 ≈ 60s(首发)+ 0.75s(退避)+ 60s(重发)≈ 121s,而不是旧口径的 10 分钟。
135
135
  *
136
- * 🔴 **本窗只是墙钟上界,不承诺发数**(codex [medium] 采纳):宿主可以用非缺省 `timeoutMs` 构造
136
+ * 🔴 **本窗只是墙钟上界,不承诺发数**(复审 [medium] 采纳):宿主可以用非缺省 `timeoutMs` 构造
137
137
  * client(见 {@link DECIDE_ATTEMPT_TIMEOUT_CAP_MS}),那时「窗 ÷ 帽」得出的发数与这里写的不同。
138
138
  * 与宿主无关的那条性质(**至少重试一次**)由 {@link DECIDE_TIMEOUT_MIN_ATTEMPTS} 单独保证 ——
139
139
  * 两个界合取:**发数达标 ∧ 窗尽** 才停。所以在 120s client 上是「2 发、~240s」,在 30s client 上
140
140
  * 是「3 发、~92s」,在缺省 60s 上是「2 发、~121s」—— 三者都有界,且都拿得到那一次重试。
141
141
  *
142
- * 🔴 窗的语义是**不再起新发**,刻意不掐在飞那一发(codex 复审议题,驳回后成文):给一发可能已被
142
+ * 🔴 窗的语义是**不再起新发**,刻意不掐在飞那一发(对抗复审议题,驳回后成文):给一发可能已被
143
143
  * server 受理的 decide 塞截止 signal 换不来任何安全 —— server 侧照跑,客户端只多制造一个「送达
144
144
  * 未知」。
145
145
  * 🔴 **写成「帽 × 系数」而不是裸 90_000**:上面那段推导只有在两者绑在一起时才会随 SDK 改帽自动
@@ -516,7 +516,7 @@ export class HitlBridge {
516
516
  // **是真异常**,不再是「server 还在跑 resume」的常态 —— 所以总窗只留恰一次重试的量
517
517
  // (推导见 DECIDE_TIMEOUT_RETRY_TOTAL_BUDGET_MS)。重发仍然安全:重复 decide 由 server CAS
518
518
  // 保证不双跑,首发其实送达时下一发只会撞 4xx(conflict/not-found ⇒ 上抛,已解决判据接手)。
519
- // 🔴 **两个界合取**(codex [medium] 采纳):发数没达标就一定再发一次(与宿主的
519
+ // 🔴 **两个界合取**(复审 [medium] 采纳):发数没达标就一定再发一次(与宿主的
520
520
  // per-attempt timeoutMs 无关),达标之后才由墙钟窗决定还发不发。少了前半句,
521
521
  // 120s client 的宿主一次重试都拿不到;少了后半句,30s client 会一路重试到分钟级。
522
522
  if (attempts >= DECIDE_TIMEOUT_MIN_ATTEMPTS && Date.now() - startedAt >= decideTimeoutRetryBudgetMs()) {
@@ -123,7 +123,7 @@ export type ListAllPersistedRulesOutcome = {
123
123
  * 🔴 **游标绑 `(rev, principal, scope)`**:两页之间有人加/删了规则 ⇒ 第二页 400
124
124
  * `request.query_invalid`。处置 = **丢游标从头列一次**(静默重置成「接着上一页」会得到一份既漏行
125
125
  * 又重行的清单);从头再撞一次 ⇒ 如实报 cursor-stale,由调用方(人按 r 刷新)决定。
126
- * 🔴 **调用方 cursor 不收**(codex F2 对抗复审 [medium]):类型上剔掉 `cursor` 还不够 —— JS
126
+ * 🔴 **调用方 cursor 不收**(复审 F2 对抗复审 [medium]):类型上剔掉 `cursor` 还不够 —— JS
127
127
  * 调用方仍能塞进来,而首页的 `...params` 会把它原样送出 ⇒ 「列全」从**中途**开始却报 `ok:true`
128
128
  * 完整清单(治理面据此藏掉仍然生效的规则)。运行期显式剥除 + 留痕:drain 恒从第一页起,
129
129
  * 「接着别人的 keyset」证明不了完整性,与本函数的契约(rules 恒完整)结构性冲突。
@@ -58,7 +58,7 @@ export function classifyRulesFailure(e) {
58
58
  return { kind: 'forbidden', message };
59
59
  return { kind: 'error', message };
60
60
  }
61
- /** 一页要多少条。**必须显式给**(codex 对抗复审 [medium] 实撞):server 缺省是 **50**,而页帽
61
+ /** 一页要多少条。**必须显式给**(对抗复审 [medium] 实撞):server 缺省是 **50**,而页帽
62
62
  * 按「200/页」算 ⇒ 真实上界只有 1250 条,一位规则多于 1250 的 principal 会恒拿到「翻不完」的
63
63
  * 失败、整个治理面打不开,而注释还写着 5000。夹取语义在 server(非数/越界夹进 1..200),所以给
64
64
  * 上限最省往返、也让页帽的算术与现实一致。 */
@@ -73,7 +73,7 @@ const MAX_PAGES = 25;
73
73
  * 🔴 **游标绑 `(rev, principal, scope)`**:两页之间有人加/删了规则 ⇒ 第二页 400
74
74
  * `request.query_invalid`。处置 = **丢游标从头列一次**(静默重置成「接着上一页」会得到一份既漏行
75
75
  * 又重行的清单);从头再撞一次 ⇒ 如实报 cursor-stale,由调用方(人按 r 刷新)决定。
76
- * 🔴 **调用方 cursor 不收**(codex F2 对抗复审 [medium]):类型上剔掉 `cursor` 还不够 —— JS
76
+ * 🔴 **调用方 cursor 不收**(复审 F2 对抗复审 [medium]):类型上剔掉 `cursor` 还不够 —— JS
77
77
  * 调用方仍能塞进来,而首页的 `...params` 会把它原样送出 ⇒ 「列全」从**中途**开始却报 `ok:true`
78
78
  * 完整清单(治理面据此藏掉仍然生效的规则)。运行期显式剥除 + 留痕:drain 恒从第一页起,
79
79
  * 「接着别人的 keyset」证明不了完整性,与本函数的契约(rules 恒完整)结构性冲突。
@@ -104,7 +104,7 @@ export async function listAllPersistedRules(facade, params = {}, opts) {
104
104
  }
105
105
  return { ok: false, failure };
106
106
  }
107
- // 🔴 页体 fail-closed 窄化(codex F2 轮二 [high]):SDK 传输层只 JSON.parse,不做运行期
107
+ // 🔴 页体 fail-closed 窄化(复审 F2 轮二 [high]):SDK 传输层只 JSON.parse,不做运行期
108
108
  // schema 校验 —— 一个 2xx 的 `{rev:9}`(无 rules 数组)在旧读法下会被认证成「完整的空清单」
109
109
  // (`ok:true, rules:[]`),治理面据此宣称「没有持久规则」而活规则不可见、无法撤销
110
110
  // (版本偏斜/后端降级下静默发生)。坏形页 = 判不出,绝不当「读到了空的」:
@@ -124,7 +124,7 @@ export async function listAllPersistedRules(facade, params = {}, opts) {
124
124
  },
125
125
  };
126
126
  }
127
- // 🔴 跨页 rev 钉(codex F2 轮三 [high]):游标契约上绑 rev —— 清单变了,续页**该** 400
127
+ // 🔴 跨页 rev 钉(复审 F2 轮三 [high]):游标契约上绑 rev —— 清单变了,续页**该** 400
128
128
  // cursor-stale。一个 2xx 却换了 rev 的续页 = server 违约或降级形,拼起来是**混合快照**;
129
129
  // 认证它为完整清单,比 400 那条腿(丢游标重列)更坏 —— 这里不猜不修补,如实 failure。
130
130
  if (rev !== null && res.rev !== rev) {
@@ -296,6 +296,17 @@ export interface ApprovalCardRequest {
296
296
  * **行为面**改动(会改到人已按下的决断的字节),按宪法三问单独立项,不在本过境批内。
297
297
  */
298
298
  requiresRealApproval?: true;
299
+ /** [4851]:窗三键透传(server ≥7.34;壳 readAskDeadlineMs 消费渲倒计时)。只在
300
+ * `windowIsCurrent === true` 的 live 腿上过境(重放腿=旧余量,续窗即假倒计时);**全部**缺席
301
+ * =该帧无窗不渲,半套在场读器按「expiresAtMs 优先、expiresInMs 兜底」回落。 */
302
+ expiresInMs?: number;
303
+ /** 同门:窗绝对到点(server 时钟 epoch ms)。 */
304
+ expiresAtMs?: number;
305
+ /** 同门:server 出帧时刻(壳时钟校偏用)。 */
306
+ serverNowMs?: number;
307
+ /** 本地**收帧**时刻(`Date.now()`,与窗任一键同拍盖下)。壳读器明文优先这枚锚:卡口串行
308
+ * 阻塞下,排队靠后的卡若按「呈现那一刻」起算余量,等待的时间会被整段还给窗。 */
309
+ receivedAtMs?: number;
299
310
  }
300
311
  /**
301
312
  * 🔴 **拆缝口** —— 弹「三选卡」并等人的决断。壳 = vendored CC `PermissionRequest`;
@@ -494,6 +505,26 @@ export interface ToolApprovalFrame {
494
505
  * run-durable-card-display-keys-test.mjs ⑨ 段;上游补位后按 {@link probeCause} 的双源合流形跟批。
495
506
  */
496
507
  requiresRealApproval?: true;
508
+ /**
509
+ * server ≥7.34.0(#288/[4429]② —— cli 自己请托的窗三键,**ADDITIVE**,`"tool_approval"` only;
510
+ * 来源锚 = sema-server src/tool-approval.ts 的三键声明+emit 前赋值,字段注逐字「新壳借此给旧族帧
511
+ * 补倒计时」)。这只 ask 的**决策窗**:到点后引擎按 `UNATTENDED_APPROVAL_POLICY`(缺省 park)
512
+ * 处置,卡上的按键此后 404 `tool_approval.not_pending`([4845] Postgres 直读实证)。
513
+ *
514
+ * 🔴 三键**信封级**(不进 card 语义):壳的倒计时读器(`readAskDeadlineMs`)以
515
+ * `expiresAtMs`(+`serverNowMs` 时钟校偏)优先、`expiresInMs` 兜底 —— **半套在场时按此
516
+ * 优先序回落消化**(异源对抗复审纠偏:早注「任一缺席整段不渲」与真实读器不符);**全部**
517
+ * 缺席 = 旧 server / 该帧无窗,才是不渲。0.38.1 及更早的白名单投影把三键丢在包边界外
518
+ * ([4851] 头号请托的实体)——帧到了、壳读器预埋了、中间这一格空转,[4845]「卡挂着像活的」
519
+ * 的直接成因。缺席 ≠ 「永不过期」。
520
+ * 🔴 过境有闸:三键只在 `ToolApprovalFrameLaneOpts.windowIsCurrent === true`(live 流)时上卡
521
+ * —— durable 账本重放的历史帧带的是铸帧时刻的旧余量,续窗 = 假倒计时(见 stamp 段注)。
522
+ */
523
+ expiresInMs?: number;
524
+ /** 窗绝对到点时刻(server 时钟,epoch ms)。与 {@link expiresInMs} 同门在场。 */
525
+ expiresAtMs?: number;
526
+ /** server 出帧时刻(epoch ms)——壳用它对本地时钟校偏后再消费 expiresAtMs。 */
527
+ serverNowMs?: number;
497
528
  }
498
529
  /** {@link ToolApprovalFrame.delegation} 的形(命名形,不用内联匿名 —— typeshape 门 B4 棘轮口径)。 */
499
530
  export interface ToolApprovalDelegation {
@@ -509,7 +540,7 @@ export interface ToolApprovalDelegation {
509
540
  * `TOOL_APPROVAL_FRAME_KEYS` 比对——SDK additive 增键时对账当天红,不再人肉追平。
510
541
  * 下面两个类型钉保证镜像与 interface 本身不可能漂移(少键/多键都是编译错)。
511
542
  */
512
- export declare const TOOL_APPROVAL_FRAME_KEYS_MIRROR: readonly ["type", "approvalId", "toolCallId", "toolName", "sourceTaskId", "fromSubagent", "sourceAgentName", "message", "args", "argsOmitted", "governanceForced", "ruleSuggestions", "persistedRuleShadowed", "probeCause", "ruleEvidence", "requiresRealApproval", "delegation", "outcome"];
543
+ export declare const TOOL_APPROVAL_FRAME_KEYS_MIRROR: readonly ["type", "approvalId", "toolCallId", "toolName", "sourceTaskId", "fromSubagent", "sourceAgentName", "message", "args", "argsOmitted", "governanceForced", "ruleSuggestions", "persistedRuleShadowed", "probeCause", "ruleEvidence", "requiresRealApproval", "expiresInMs", "expiresAtMs", "serverNowMs", "delegation", "outcome"];
513
544
  /**
514
545
  * 子代帧判别:显式键 fromSubagent(core 1.378 RB-39②)优先;缺席退 sourceTaskId 在场性权宜式
515
546
  * (server 1.258 [1549]①3,旧代际兼容)。
@@ -599,5 +630,15 @@ export interface ToolApprovalFrameLaneOpts {
599
630
  * 缺席/false ⇒ 不发(fail-closed 到「不发」侧;决断本身照常送达,现状字节不变)。
600
631
  */
601
632
  approvalDecisionNoteCapable?: boolean;
633
+ /**
634
+ * 这条帧是不是**当下**从 live 流上收到的(异源对抗复审 P1 采纳;壳孪生帧族同名闸的
635
+ * 本包席位 —— 壳 `approvalStreamWire` 头注逐字:「账本重放的历史帧带的是铸帧时刻的旧余量,
636
+ * 拿它算『本地 now + 余量』等于给一只早就过期的 ask 续一个新窗」)。
637
+ * 🔴 窗三键**只在本位 === true 时**过境到卡:durable 账本 tail 会把旧 `tool_approval` 行连
638
+ * 冻结三键逐字重放,重放腿不设防 = 过期 ask 在屏上拿到一个精确但完全错误的倒计时,比
639
+ * 「卡挂着没倒计时」更坏。缺席 = 不知道来路 ⇒ **不 stamp**(fail-closed:退回无窗渲染,
640
+ * 老宿主不传本位时行为 = 0.38.1,零回归)。live 腿的宿主接线随各端提货批补 `true`。
641
+ */
642
+ windowIsCurrent?: boolean;
602
643
  }
603
644
  export declare function surfaceToolApprovalFrameAndRespond(frame: ToolApprovalFrame, respond: RespondToolApprovalFn, streamArgs: unknown | undefined, signal?: AbortSignal, lane?: ToolApprovalFrameLaneOpts): Promise<ToolApprovalFrameOutcome>;
@@ -389,6 +389,14 @@ export const TOOL_APPROVAL_FRAME_KEYS_MIRROR = [
389
389
  // sdk 7.1.0 的运行期锚**尚无**本键(node 直读实证:锚 17 项)⇒ 这是一次**领先**,进对账门的
390
390
  // AHEAD_OF_ANCHOR 带退出条件登记(SDK 追平当天那条登记自红逼删,回到逐元素相等)。
391
391
  'requiresRealApproval',
392
+ // [4851] 头号请托(client-core 0.38.2):server 7.34.0 起真发窗三键(#288/[4429]② cli 自己请托,
393
+ // server src/tool-approval.ts 三键声明+条件 stamp)。⚠️ 与 requiresRealApproval 同形=**领先** SDK
394
+ // 运行期锚(sdk 7.2.0 锚 17 项,node 直读实证无三键)⇒ 进对账门 AHEAD_OF_ANCHOR 带退出条件登记
395
+ // (SDK 追平当天登记自红逼删)。0.38.1 把三键丢在包边界外 = [4845]「卡到期屏幕零反应」的直接成因
396
+ // (壳读器 readAskDeadlineMs 早已预埋,缺的一直是包边界这一格——#283 逐字同形第四例)。
397
+ 'expiresInMs',
398
+ 'expiresAtMs',
399
+ 'serverNowMs',
392
400
  'delegation',
393
401
  'outcome',
394
402
  ];
@@ -532,6 +540,12 @@ function pathFromGateMessage(message) {
532
540
  * 🔴 **一把判词服务多键**是设计而非省事:两条决断腿(活帧 / durable 行)× 多个开集记录键共用同一个
533
541
  * 载体判据,判词绝不各写各的(各写一份 = 同一个问题有了两个属主,分叉那天没人发现)。
534
542
  */
543
+ /** 窗三键的坏形闸:三键定义域按 SDK 契约本就非负(`expiresInMs = max(0, …)`,0 合法=窗恰好
544
+ * 到点)。裸 `Number.isFinite` 会放行负数 —— 与铸卡注承诺的「负数=坏形降缺席」相反(同文件
545
+ * `delegation.depth` 曾同款教训,异源对抗复审第二次命中同形)。 */
546
+ function isNonNegativeFinite(v) {
547
+ return typeof v === 'number' && Number.isFinite(v) && v >= 0;
548
+ }
535
549
  function isWireRecordCarrier(v) {
536
550
  return typeof v === 'object' && v !== null && !Array.isArray(v);
537
551
  }
@@ -630,6 +644,26 @@ export async function surfaceToolApprovalFrameAndRespond(frame, respond, streamA
630
644
  ...(isFromSubagent(frame) ? { workerBadge: subagentBadgeFor(frame) } : {}),
631
645
  ...(wireNote !== undefined ? { wireNote } : {}),
632
646
  ...(frame.governanceForced === true ? { governanceForced: true } : {}),
647
+ // [4851]:窗三键透传,三闸合取(异源对抗复审 P1/P2 收编):
648
+ // ① `lane.windowIsCurrent === true` —— durable 账本重放的历史帧带的是铸帧时刻的旧余量,
649
+ // 给它续窗 = 过期 ask 渲出精确假倒计时(壳孪生族同名闸的本包席位,见 LaneOpts 注);
650
+ // ② **非负有限数**才 stamp —— `Number.isFinite` 单独放行负数,而三键定义域按 SDK 契约
651
+ // 本就非负(`expiresInMs = max(0, …)`;0 合法 = 窗恰好到点)。负数/NaN/Infinity =
652
+ // 坏形降缺席,绝不折 0 或猜(同文件 delegation.depth 曾同款教训);
653
+ // ③ 三键各自独立 stamp:server 承诺同门在场,但包边界不替上游把「半套在场」湮灭成全缺席
654
+ // —— 读器自己按「expiresAtMs 优先、expiresInMs 兜底」消化半套。
655
+ // 任一键 stamp 时同步盖 `receivedAtMs`(本地收帧锚):卡口串行阻塞下,排队靠后的卡若按
656
+ // 「呈现那一刻」起算余量,窗被整段还回去 —— 壳读器明文优先这枚锚。
657
+ ...(lane?.windowIsCurrent === true
658
+ ? {
659
+ ...(isNonNegativeFinite(frame.expiresInMs) ? { expiresInMs: frame.expiresInMs } : {}),
660
+ ...(isNonNegativeFinite(frame.expiresAtMs) ? { expiresAtMs: frame.expiresAtMs } : {}),
661
+ ...(isNonNegativeFinite(frame.serverNowMs) ? { serverNowMs: frame.serverNowMs } : {}),
662
+ ...(isNonNegativeFinite(frame.expiresInMs) || isNonNegativeFinite(frame.expiresAtMs)
663
+ ? { receivedAtMs: Date.now() }
664
+ : {}),
665
+ }
666
+ : {}),
633
667
  // 🔴 跨字段不变量(对抗复审命中):`delegation` 只在**子代 ask** 的门上在场(SDK 头注:
634
668
  // "Present on the same door as fromSubagent (child asks only)")。只校对象形不校这一条 ⇒
635
669
  // 一个**宿主自己**的破坏性工具审批也能带着 delegation 到卡口,被呈成「某层子代发起的」——
@@ -58,6 +58,15 @@ export interface ToolEndResultArmLike {
58
58
  /** 引擎 CC-shaped details(core 1.199+ AgentToolResult.details;Bash 时带 exitCode)。 */
59
59
  structured?: unknown;
60
60
  truncated?: unknown;
61
+ /**
62
+ * 这次调用失败的**机器码**(开集;`eventToSdkMessage` 的 `case 'tool_end'` 臂原样上臂,宿主从
63
+ * 臂上直读 —— 与 {@link settledBy}/{@link approver} 同款「臂带、卡不带」姿势)。已知族:中断
64
+ * 合成收口两码(`interrupted_never_started` / `interrupted_outcome_unknown`,core 5.10.0)、
65
+ * 委派席位到限两码(`delegation.*`)与 `mcp.server_revoked`(两族 core ≥5.50.0 孪生拼法起才
66
+ * 真到达 —— 更旧引擎恒缺席,缺席≠没拒过)。词表与判定单源 `engineErrorCodes.ts`
67
+ * (`delegationCapDispositionOf` 等);宿主只渲已知码的指路文案,未知码原样报码**不编因**。
68
+ */
69
+ errorCode?: unknown;
61
70
  /**
62
71
  * 这次调用**是怎么结算的**(core ≥5.18.1 #187;`human`/`timeout`/`aborted` + 开集读)与
63
72
  * **是谁结算的**(core ≥5.35.0 design/252 G-7 / server ≥7.23.0 #263;审批通道自报的身份串,
@@ -170,7 +170,7 @@ export type BuiltTaskRequest = TaskRequest;
170
170
  * `settings.<resolved>` 的车道(#292 P1 起 = **两条**)那份 resolver effective 快照是**开放集**,
171
171
  * 所以**表外**子键(permissions/env/model/… 不可枚举)放行。
172
172
  *
173
- * 🔴 开放集**不是**整只 `settings` 免检(#292 P1 codex 复审第二轮 [medium] 采纳):表里点名了、
173
+ * 🔴 开放集**不是**整只 `settings` 免检(#292 P1 对抗复审第二轮 [medium] 采纳):表里点名了、
174
174
  * 而 lanes 不含本车道的子键(如 print 车道的 `settings.ultracode`)照旧**点名** —— 那些键是
175
175
  * **可枚举的已知量**,放过它们等于让「ultracode 仅 interactive」这条登记在真 wire 面失效。
176
176
  * `buildTaskRequest` 内的 `resolvedSnapshotForWire` 只管住构造器自己那一份;端在构造之后仍能往请求体上
@@ -129,7 +129,7 @@ const shapeTag = (v) => {
129
129
  /**
130
130
  * plain object 判别 —— 按 **prototype** 判,不按 `Object.prototype.toString` 的标签判。
131
131
  *
132
- * 🔴 标签判法是**假的**(0.35.0 codex 对抗复审 [high] 采纳):`[object Object]` 对**普通类实例**同样
132
+ * 🔴 标签判法是**假的**(0.35.0 对抗复审 [high] 采纳):`[object Object]` 对**普通类实例**同样
133
133
  * 成立,而 `Symbol.toStringTag` 还能让任意载体自报这个标签。放它过去之后,下面的摊开用的是
134
134
  * `Object.entries`(**只取自有可枚举键**)—— 于是一个把 `permissions.deny/ask` 挂在**原型 getter**
135
135
  * 上的载体会摊出一个**空快照**,请求照发 = 权限静默变宽,正是本节要堵的那个 fail-open 形。
@@ -139,7 +139,7 @@ const shapeTag = (v) => {
139
139
  * `Map` / `Date` / boxed 包装对象 / 数组一律拒 —— 它们要么内容不在自有键上,要么摊开就是垃圾键;
140
140
  * 「上游产出坏了」比「悄悄发一个更宽的权限面」更该被人看见。
141
141
  *
142
- * 🔴 判据 **realm 无关**(0.35.0 codex 对抗复审第八轮 [medium] 采纳):不拿「**本** realm 的
142
+ * 🔴 判据 **realm 无关**(0.35.0 对抗复审第八轮 [medium] 采纳):不拿「**本** realm 的
143
143
  * `Object.prototype`」做身份比较 —— iframe / `node:vm` / 另一个渲染进程里的对象字面量各有**自己的**
144
144
  * `Object.prototype`,身份比较会把这些**完全合法、JSON 忠实**的载体误判成异形,在 web / desktop 宿主
145
145
  * 上变成提交前的硬失败。
@@ -242,7 +242,7 @@ const materializeJsonFaithful = (v, path, seen = new Set()) => {
242
242
  }
243
243
  return { value: items }; // 重建:原数组(可能是子类/带访问器/带覆盖方法)不出门
244
244
  }
245
- // 🔴 重建成**无原型**记录(0.35.0 codex 对抗复审第七轮 [high] 的可采半场):
245
+ // 🔴 重建成**无原型**记录(0.35.0 对抗复审第七轮 [high] 的可采半场):
246
246
  // ① 忠实 —— 源本来就允许 `Object.create(null)` 字典,重建成 `{}` 等于把载体形换掉了;
247
247
  // ② 少一条改写面 —— 无原型记录不会继承任何**事后**装到 `Object.prototype` 上的 `toJSON`。
248
248
  // ⚠️ 但这只关掉了 record 那一半:重建出来的**数组**必须是真数组(`Array.isArray` / 序列化成
@@ -264,7 +264,7 @@ const materializeJsonFaithful = (v, path, seen = new Set()) => {
264
264
  // `Object.prototype` 的 setter 上:键整个丢掉、还顺手换了目标对象的原型。
265
265
  Object.defineProperty(rec, k, { value: r.value, enumerable: true, writable: true, configurable: true });
266
266
  }
267
- // 🔴 判据的读法**一律排在捕获之后**(0.35.0 codex 对抗复审第十二轮 [high] 采纳):`toJSON` 走属性
267
+ // 🔴 判据的读法**一律排在捕获之后**(0.35.0 对抗复审第十二轮 [high] 采纳):`toJSON` 走属性
268
268
  // 查找、`isPlainRecord` 走 `getPrototypeOf`/`constructor` —— 这三种读法**都可以带副作用**
269
269
  // (`getPrototypeOf` 陷阱在被问的那一刻把 `deny` 清空),而**原生 `JSON.stringify` 从不问原型**。
270
270
  // 校在捕获之前 = 本层自己的读法成了丢内容的那一环(比被动撒谎更糟:那是我们引入的丢失面)。
@@ -305,7 +305,7 @@ const materializeJsonFaithful = (v, path, seen = new Set()) => {
305
305
  * `TypeError` 带形状描述(合法载体的判据见 {@link isPlainRecord} —— 「摊得全」是它的全部理由)。
306
306
  * (本包是发出去的 npm 公开面:JS 调用方与版本偏斜的宿主都到得了这里,型面拦不住。)
307
307
  *
308
- * ## ③ 权限面**子树**同样要摊得全(0.35.0 codex 对抗复审第二轮 [high] 采纳)
308
+ * ## ③ 权限面**子树**同样要摊得全(0.35.0 对抗复审第二轮 [high] 采纳)
309
309
  *
310
310
  * 只校最外层是不够的:`{ permissions: new Map([['deny',['Write']]]) }` 的外层是合法字面量,可
311
311
  * `JSON.stringify` 把那只 Map 序列化成 `"permissions":{}` —— deny 规则**静默消失**,与 ② 要堵的
@@ -335,7 +335,7 @@ const resolvedSnapshotForWire = (resolved) => {
335
335
  // 当场物化成脱钩副本),原件的形状判据留到最后;校不过就整体拒,拒绝面一字未变。
336
336
  const named = namedSettingsSubKeys();
337
337
  const out = {};
338
- // 🔴 **键先捕获、值逐个即读即处理**(0.35.0 codex 对抗复审第六轮 [high]):`Object.entries(resolved)`
338
+ // 🔴 **键先捕获、值逐个即读即处理**(0.35.0 对抗复审第六轮 [high]):`Object.entries(resolved)`
339
339
  // 会把**所有兄弟键**的值先读齐 —— 于是一个 `env`/`model` 位上的 getter 能在 `permissions` 被
340
340
  // 重建**之前**把它的 deny 数组清空(拿到的是同一个引用)。这与内层那条(见
341
341
  // {@link materializeJsonFaithful} 第五条)是同一个病、只是高一层:两层都必须按 stringify 的
@@ -365,7 +365,7 @@ const resolvedSnapshotForWire = (resolved) => {
365
365
  out[k] = checked.value;
366
366
  continue;
367
367
  }
368
- // 🔴 函数值一律不进(#292 P1 codex 复审第三轮 [medium] 采纳):JSON wire 上没有函数,而一个
368
+ // 🔴 函数值一律不进(#292 P1 对抗复审第三轮 [medium] 采纳):JSON wire 上没有函数,而一个
369
369
  // **自有 `toJSON`** 会在序列化那一刻整只改写 `settings` 的字节 —— 实测
370
370
  // `resolved={permissions:…, toJSON(){return {ultracode:true}}}` 能让 `JSON.stringify(req)` 出
371
371
  // `settings:{ultracode:true}`,把上面那道车道剥离(以及一切按对象查的判据)整体绕过。
@@ -550,7 +550,7 @@ export function applyLiveRequestDefaults(req, host) {
550
550
  * `settings.<resolved>` 的车道(#292 P1 起 = **两条**)那份 resolver effective 快照是**开放集**,
551
551
  * 所以**表外**子键(permissions/env/model/… 不可枚举)放行。
552
552
  *
553
- * 🔴 开放集**不是**整只 `settings` 免检(#292 P1 codex 复审第二轮 [medium] 采纳):表里点名了、
553
+ * 🔴 开放集**不是**整只 `settings` 免检(#292 P1 对抗复审第二轮 [medium] 采纳):表里点名了、
554
554
  * 而 lanes 不含本车道的子键(如 print 车道的 `settings.ultracode`)照旧**点名** —— 那些键是
555
555
  * **可枚举的已知量**,放过它们等于让「ultracode 仅 interactive」这条登记在真 wire 面失效。
556
556
  * `buildTaskRequest` 内的 `resolvedSnapshotForWire` 只管住构造器自己那一份;端在构造之后仍能往请求体上
@@ -149,7 +149,7 @@ export interface SubagentResumeContext {
149
149
  * 取运行 → 无则 no-run;makeEngineWireClient)—— 两条腿共用同一套装配,免得 steer 能到达而 resume
150
150
  * 到不了(或反过来),那种不对称在用户面上表现为「同一个 agent 有时能说话有时不能」。
151
151
  *
152
- * 🔴 `session` 的三态(#242 批 2 定口径,codex 复审 medium 收紧):
152
+ * 🔴 `session` 的三态(#242 批 2 定口径,对抗复审 medium 收紧):
153
153
  * · 台账有这一行**且**捕到了它的会话 ⇒ 用**行登记的**会话(resume 是 per-id 读面,配错的
154
154
  * `{runId, session}` 组合在 session-bound run 上 fail-closed 404);
155
155
  * · 台账**有这一行但没捕到会话**(fleet 行帧腿/通知腿今天不传 session)⇒ **不带 `?session=`**,
@@ -173,7 +173,7 @@ export declare function resumeSettledSubagent(facade: SubagentResumeFacade | nul
173
173
  signal?: AbortSignal;
174
174
  }): Promise<SubagentResumeOutcome>;
175
175
  /**
176
- * {@link auditRetainWithoutWake} 的判决(codex 复审 low 收紧的**返回契约**)。
176
+ * {@link auditRetainWithoutWake} 的判决(对抗复审 low 收紧的**返回契约**)。
177
177
  *
178
178
  * 两位分别回答两个不同的问题,**不许合成一个**:
179
179
  * · `message` = 组合成不成立(成立才有这条记录;`undefined` 返回 = 不成立);
@@ -103,7 +103,7 @@ export function resolveOwnerRunId(childTaskId, activeRunId = activeEngineRunId()
103
103
  * 取运行 → 无则 no-run;makeEngineWireClient)—— 两条腿共用同一套装配,免得 steer 能到达而 resume
104
104
  * 到不了(或反过来),那种不对称在用户面上表现为「同一个 agent 有时能说话有时不能」。
105
105
  *
106
- * 🔴 `session` 的三态(#242 批 2 定口径,codex 复审 medium 收紧):
106
+ * 🔴 `session` 的三态(#242 批 2 定口径,对抗复审 medium 收紧):
107
107
  * · 台账有这一行**且**捕到了它的会话 ⇒ 用**行登记的**会话(resume 是 per-id 读面,配错的
108
108
  * `{runId, session}` 组合在 session-bound run 上 fail-closed 404);
109
109
  * · 台账**有这一行但没捕到会话**(fleet 行帧腿/通知腿今天不传 session)⇒ **不带 `?session=`**,
@@ -20,7 +20,7 @@ export type SubagentSteerOutcome = {
20
20
  * 指名了行而台账缺席 ⇒ `no-run` + `noteBgOwnerAbsence` 留痕(绝不回落在飞 run);
21
21
  * **没指名行**(既有两参调用)⇒ 才回落在飞 run —— 老调用方逐字零行为变化,本参数是 additive。
22
22
  *
23
- * 🔴 会话参数**二态**(codex 复审 medium 采纳:「取了台账的 run 却配现势 session」是确定性 404):
23
+ * 🔴 会话参数**二态**(对抗复审 medium 采纳:「取了台账的 run 却配现势 session」是确定性 404):
24
24
  * · 行登记时**捕到了会话** ⇒ 用**行的会话**(与行的 runId 同源同拍,是唯一自洽的组合);
25
25
  * · 行**没有**随行会话(`recordBgParentRun` 只写 run —— 今天 bg 行多数是这一格)⇒ 退**现势会话**。
26
26
  * 刻意**不**跟 resume 腿的「没捕到就干脆不带」:本腿与 tail/subagentOutput 同族,遵 [1498]③
@@ -45,7 +45,7 @@ import { noteBgOwnerAbsence } from './subagentOwnerAbsence.js';
45
45
  * 指名了行而台账缺席 ⇒ `no-run` + `noteBgOwnerAbsence` 留痕(绝不回落在飞 run);
46
46
  * **没指名行**(既有两参调用)⇒ 才回落在飞 run —— 老调用方逐字零行为变化,本参数是 additive。
47
47
  *
48
- * 🔴 会话参数**二态**(codex 复审 medium 采纳:「取了台账的 run 却配现势 session」是确定性 404):
48
+ * 🔴 会话参数**二态**(对抗复审 medium 采纳:「取了台账的 run 却配现势 session」是确定性 404):
49
49
  * · 行登记时**捕到了会话** ⇒ 用**行的会话**(与行的 runId 同源同拍,是唯一自洽的组合);
50
50
  * · 行**没有**随行会话(`recordBgParentRun` 只写 run —— 今天 bg 行多数是这一格)⇒ 退**现势会话**。
51
51
  * 刻意**不**跟 resume 腿的「没捕到就干脆不带」:本腿与 tail/subagentOutput 同族,遵 [1498]③
@@ -1,5 +1,5 @@
1
1
  /**
2
- * subagentOwnerAbsence — #242 批 3(黑板 [4000] Q3=B 裁定落地):bg 子代宿主 run 台账缺席时的
2
+ * subagentOwnerAbsence — #242 批 3(协作板 [4000] Q3=B 裁定落地):bg 子代宿主 run 台账缺席时的
3
3
  * **响亮留痕**单源。
4
4
  *
5
5
  * 三条读面腿(`engineSubagentTail` / `engineTaskHandleWire` / `engineSubagentOutput`)此前一律
@@ -391,7 +391,7 @@ export function getBgTerminalFacts(taskId) {
391
391
  // 🔴 有界:键来自 wire(不可信),无界表 = 引擎乱发 taskId 就能吃掉宿主内存。上限取**两表
392
392
  // 上限之和**(壳表 256 + 本表原 `MAX_TASKS*2`=64)—— 合表**不许缩窄既有保障**:收编前两条
393
393
  // lane 各有各的池子,能同时记 256 条 tick + 64 条 fleet;若按 256 钉,「tick 池已满 + 再来一条
394
- // fleet 行」当场淘汰一条 tick,那条子代的 resume 入口凭空消失,而收编前它还在(codex 复审
394
+ // fleet 行」当场淘汰一条 tick,那条子代的 resume 入口凭空消失,而收编前它还在(对抗复审
395
395
  // medium 实证形)。两读口的 LRU touch 也会让高频 fleet 行持续挤压 tick 历史,同一条道理。
396
396
  // 满了按 LRU 淘汰,淘汰后果只是那一行退回「不知道宿主 run」= 诚实缺席,不是错值。
397
397
  const MAX_BG_PARENT_RUNS = 320;
@@ -50,7 +50,7 @@ export function classifyTurnWireError(err) {
50
50
  const message = typeof e.message === 'string' && e.message.length > 0 ? e.message : String(err);
51
51
  return { kind: 'http', status: e.status, ...(errorCode !== undefined ? { errorCode } : {}), message };
52
52
  }
53
- // park 判别按 `name` **结构读**,不附加 instanceof Error(codex F3 [medium] 真病修):壳源形的
53
+ // park 判别按 `name` **结构读**,不附加 instanceof Error(复审 F3 [medium] 真病修):壳源形的
54
54
  // `err instanceof Error &&` 合取在单进程壳里恒真无害,但本件是三端共用面 —— desktop IPC 序列化 /
55
55
  // web 跨 bundle 的同名错误是 plain object,instanceof 合取会把「候人决断」误诊成「客户端 bug +
56
56
  // 建议重跑」(重跑=重复 turn,且掩盖真待决态)。壳注释自陈的意图本就是「name 判别(跨包
@@ -2,7 +2,7 @@
2
2
 
3
3
  > **本档是什么**:`@sema-agent/client-core` 作为**上游包**,对它的三个宿主端(`sema-cli` TUI /
4
4
  > `sema-web` 聊天区 / `sema-desktop` session-host)的**正式接入文档**。按接入文档宪法(clay 08-12,
5
- > 黑板 [3680])立档:消费上游先要详细全接入文档,**文档报错可直接打回**。
5
+ > 协作板 [3680])立档:消费上游先要详细全接入文档,**文档报错可直接打回**。
6
6
  >
7
7
  > **本档不是什么**:它**不是契约源**。契约源 = `src/**` 的实现本身 + 各文件头注/JSDoc。
8
8
  > 本档的每一节都给**实现锚**(文件 + 符号名),读者据锚对账;对不上以真码为准,**当场改本档**。
@@ -23,7 +23,7 @@
23
23
  | peer:wire 契约 | `@sema-agent/sdk` **>=7.2.0**(value-level,非 type-only) | `package.json` `peerDependencies` |
24
24
  | peer:会话词汇表 | `@sema-agent/agent-types` **>=0.2.0**(type-only,零运行时) | 同上 |
25
25
  | runtime dep | `diff` ^9.0.0(**唯一**一条;portability 门按**等值**钉死) | `package.json` `dependencies` |
26
- | 公开导出面 | **764** 个运行期符号(+ 39 个测试钩;= npm `0.38.0` 的值;`0.37.0` 是 **753**,见 `CHANGELOG.md`) | `scripts/public-export-baseline.json` 的 `count` / `testHookCount` —— **别手抄进别处,以该文件为准** |
26
+ | 公开导出面 | **766** 个运行期符号(+ 39 个测试钩;= 未发 `0.39.0` 的值,npm `0.38.0` 是 **764**;`0.37.0` 是 **753**,见 `CHANGELOG.md`) | `scripts/public-export-baseline.json` 的 `count` / `testHookCount` —— **别手抄进别处,以该文件为准** |
27
27
  | 常驻门 | 以 `scripts/gates-manifest.json` 的 `suites` 长度为准(**本档不抄这个数**) | `scripts/gates-manifest.json`;`npm test` 的名单等值门与它逐名对账 |
28
28
  | 沿革档 | 0.29.0 起建 `CHANGELOG.md`;更早批次记账在 `src/index.ts` 文件头 + `docs/REFACTOR-LEDGER.md` | — |
29
29
 
@@ -101,7 +101,7 @@ SDK 核对「本包 import 的每个值级符号仍然导出」「`TaskStats.cos
101
101
 
102
102
  ## §2 公共导出面地图(按域)
103
103
 
104
- > 全集真源 = `scripts/public-export-baseline.json` 的 `names`(**764** 项)。
104
+ > 全集真源 = `scripts/public-export-baseline.json` 的 `names`(**766** 项)。
105
105
  > 本节**不逐名抄**,只给「域 → 承重导出 → 用途 → 实现锚」。承重导出 = 一个端为了让这个域干活
106
106
  > **必须**直接调到的那几个符号;其余是它们的类型、变体与辅助位。
107
107
  > 单一入口:`import { … } from '@sema-agent/client-core'`(`exports` 只有 `.` 一个;
@@ -111,7 +111,7 @@ SDK 核对「本包 import 的每个值级符号仍然导出」「`TaskStats.cos
111
111
 
112
112
  `public-export-baseline.json` 由 **`dist/index.js` 的运行期导出**生成(生成口径自述见
113
113
  `scripts/run-client-core-typeshape-test.mjs`,双向精确集合门在 `scripts/run-public-surface-test.mjs`)。
114
- 实测:764 项 **100% 是运行期导出,零 type-only**。
114
+ 实测:766 项 **100% 是运行期导出,零 type-only**。
115
115
 
116
116
  **推论(端必须知道)**:
117
117
  - barrel 导出的**类型**面比 707 大得多,且**不被这道门看守** —— `AdapterContext` / `SeamEvent` /
@@ -120,7 +120,7 @@ SDK 核对「本包 import 的每个值级符号仍然导出」「`TaskStats.cos
120
120
  端依赖这些类型是合法的,但**不要**拿基线 diff 当"类型面没变"的证据。
121
121
  - `src/agentSession/contract.ts` 对基线贡献 **0** 项(纯类型模块,`export *` 在 dist 里是空转发)。
122
122
 
123
- 764 项的内部构成(帮助端估读表大小):**225** 项是 `SCREAMING_SNAKE` 常量数据表/词汇表
123
+ 766 项的内部构成(帮助端估读表大小):**226** 项是 `SCREAMING_SNAKE` 常量数据表/词汇表
124
124
  (矩阵、键集、env 名、锚串)而非可调用物;**5** 项是 PascalCase 运行期值
125
125
  (`ControlRouter` / `ControlSafetyError` / `HitlBridge` / `HitlSafetyError` / `DecideTransportRetryExhaustedError`);
126
126
  **39** 项是 `*For(sessionKey, …)` 的 per-session 变体(§6;其中 `engineNamespaceKeyFor` 是命名巧合 —— 参数是 baseUrl 不是 sessionKey,见域 14)。
@@ -144,7 +144,7 @@ SDK 核对「本包 import 的每个值级符号仍然导出」「`TaskStats.cos
144
144
  | 13 | **座位 IPC 契约** | 33 | `LOCAL_SESSIONS_SPEC` · `SEAT_METHOD_NAMES` · `SEAT_EVENT_TYPES` · `isLocalSessionEvent` · `isToolPermissionRequest` · `toolPermissionRequestId` · `SEAT_VALIDATOR_KEY_COVERAGE` | desktop↔web 座位 IPC 契约的**单一真源**(此前两边各一份、名字零重合 ⇒ 编译器永远不会告诉你它们漂了)。🔴 加 verb 忘了加 `LOCAL_SESSIONS_SPEC` **不报错**:preload 不注册 channel、渲染端读到 `undefined` | `src/seatContract.ts`(**零 import**,纯类型 + 常量 + 纯谓词) |
145
145
  | 14 | **宿主端口与会话槽** | 26 | `installHost` · `installHostFor` · `hostPortMisses(For)` · `DEFAULT_SESSION_KEY` · `hostEnv` · `unrefTimer` · `parseLocaleTag` / `pickUiLanguage`(#244 F4 族D A-028.20:locale tag 手术单源 + UI 语言判定;与 `resolveRegionHint` 双出口成文 —— 语言偏好域 en/zh ≠ 地址可达域 cn/intl/unknown,`zh-Hant` 前者 zh 后者 intl 是设计)· `engineNamespaceKeyFor` / `mergeSessionMapRecord` / `mergeEngineEntry`(A-028.12:会话 id 映射单一键形 + merge 判定;存储经 `SessionMapStorePort` 归端 —— cli 文件锁/原子写,web localStorage)| 进程/端级装配层(settings/fs/queue/timers/session/log/probe),与 per-turn 的 `AdapterContext` **分层**。头注的判定规则:**这个能力每 turn 都会变吗?** 会 ⇒ `ctx`;不会 ⇒ `installHost` | `src/host.ts`、`src/hostEnv.ts`、`src/sessionSlot.ts`、`src/unrefTimer.ts`、`src/env/{localeGeo,localeTag,uiLanguage}.ts`、`src/sessionMap.ts` |
146
146
  | 15 | **控制面与传输** | 73 | `ControlRouter`(+ `ControlSafetyError`)· `makeEngineWireClient` + `resolveWireAuth` · `installEngineWireTarget` · `diagnoseSseIdleTear` / `isSseIdleError` · `attemptActiveRunSelfHeal` + `activeRunBusySignal` + `activeRunSelfHealRow` / `activeRunBusyHeadlessRow` · `kickEngineCapsProbe` / `engineCapTrue` / `invalidateEngineCaps(baseUrl, probe?)`(#307 S25:引擎温切后的 caps 生产失效口 —— kick 自带幂等闸,同 baseUrl 重启后不显式失效就永远读到旧引擎那一版能力位;调用方 = 壳的 respawn/restartEngine。🔴 **推荐两参形**:第二参给替代探测则「推进代际 + 注册新探测」在同一同步块内完成,失效与下一次 kick 之间那个「等待者读到未判」的窗按构造不存在;单参形保留给「只丢缓存、这一刻没有替代探测」的调用方,那种情形下读到未判是诚实结局) · `mapBrainStatusToRetry` · `waitForClaimRelease` + `CLAIM_RELEASED_STATES` / `CLAIM_HELD_STATES` · `atMostOnceFailureClass` / `readSteerDelivery` · `clearRunningChoiceOffer`(Inkglow-1085 P0b①:「Do nothing」登记的清口 —— 端的「重新打开操作菜单」入口;登记在场时 attemptActiveRunSelfHeal 不整卡重弹,not-parked 结局带 `alreadyOffered: true` 判别位,端据此降级渲一行)· `INTERACTIVE_WAY_OUT`(默认出路串单源)· `normalizeWirePrincipal`(A-028.10:principal 在场性 trim 原语 —— 全空白=缺席不发头,engineWireTarget 两臂/makeEngineWireClient/壳 livePrincipal 同尺)· `classifyTurnWireError` / `isWireTransportError` / `isPreStreamDrainingReject` / `isResumeAtRejection` / `drainingRetryDelayMs` / `scenarioDenyFromError` + `WIRE_NETWORK_ERROR_PATTERN`(A-028.11/.13:turn 错误分型判定半场,人话文案与渲染归端) | 上行通道的**监管**半场(submit / steer / kill / 队列命令定序)+ 传输构造、caps 探测、SSE 断流分诊、**409 active-run 自愈** | `src/controlRouter.ts`、`steering.ts`、`sseIdleTriage.ts`、`retryStatus.ts`、`diagnostics.ts`、`engineWireSdk.ts`、`engineWireTarget.ts`、`src/principalWire.ts`、`src/wireErrorTriage.ts`、`engineSessionParam.ts`、`engineCapsCache.ts`、`liveInitToolFace.ts`、`adapter/activeRunSelfHeal.ts` |
147
- | 16 | **引擎词汇表与包自检** | 46 | `CONFIG_REFUSAL_CODES` / `isConfigRefusalCode` · `STOP_CONFLICT_CODES` · `isInterruptedToolEndCode` · `isRewindFamilyCode` · `CLIENT_VERBS` · `compensationSplitViolations` · `DELEGATION_CAP_CODES` / `isDelegationCapCode` / `DELEGATION_CONCURRENCY_CAP` / `DELEGATION_SESSION_CAP`(0.38.0 #318 件④:core 5.48.0 design/323 委派席位到限**两码,处置不对称禁合并** —— 并发帽=**可等**(兄弟结束即有位)/ 会话累计帽=**等也没用**(这条会话的配额用尽))· `CONFIG_DELEGATION_ENTRY_CAPS`(同批入 `CONFIG_REFUSAL_CODES` 识别表) | 三端分臂共用的**去字面化** `errorCode` 词表(病根正是三端各抄一份字面);编译期 verb 闭合门;搬迁补偿登记表 | `src/engineErrorCodes.ts`(39 项;A-028.11/.13 补 `DRAINING_ERROR_CODE`/`SCENARIO_NOT_ALLOWED_ERROR_CODE`/`RESUME_AT_ERROR_CODE_PREFIX`;#318 件④ 补 `delegation.*` 族四位 + `config.delegation_entry_caps`)、`src/classifierVerdictWire.ts`、`src/compensations.ts`、`src/clientSlice.ts` |
147
+ | 16 | **引擎词汇表与包自检** | 48 | `CONFIG_REFUSAL_CODES` / `isConfigRefusalCode` · `STOP_CONFLICT_CODES` · `isInterruptedToolEndCode` · `isRewindFamilyCode` · `CLIENT_VERBS` · `compensationSplitViolations` · `DELEGATION_CAP_CODES` / `isDelegationCapCode` / `DELEGATION_CONCURRENCY_CAP` / `DELEGATION_SESSION_CAP`(0.38.0 #318 件④:core 5.48.0 design/323 委派席位到限**两码,处置不对称禁合并** —— 并发帽=**可等**(兄弟结束即有位)/ 会话累计帽=**等也没用**(这条会话的配额用尽))· `CONFIG_DELEGATION_ENTRY_CAPS`(同批入 `CONFIG_REFUSAL_CODES` 识别表)· `delegationCapDispositionOf` / `MCP_SERVER_REVOKED`(0.39.0 载体到货消费件:core 5.50.0 补 `{ error: code, code }` 孪生拼法后两码真上 `tool_end.errorCode`,0.38.0「先立词不落消费分支」的已知局限自此解除;处置轴 `wait-for-slot` / `reuse-existing-or-await-reap` 机器可读(累计帽=retained-window 帐,行回收配额即回,处置=SendMessage 复用,**非**「换会话/永久耗尽」——0.38.0 段该句系勘误),未知 `delegation.*` 码 ⇒ `undefined`;`mcp.server_revoked` = 操作员 mid-session 吊销 server 后的工具面本地闸(被吊销的 server **名**今天不过 wire 境:detail.server 是进程内位,抬升腿只 lift code——归因渲染候 core 补 typed detail,已点名);载体门 = engine-vocab G3 腿锚 core dist 铸点) | 三端分臂共用的**去字面化** `errorCode` 词表(病根正是三端各抄一份字面);编译期 verb 闭合门;搬迁补偿登记表 | `src/engineErrorCodes.ts`(计数以 `scripts/public-export-baseline.json` 为准,别手抄;A-028.11/.13 补 `DRAINING_ERROR_CODE`/`SCENARIO_NOT_ALLOWED_ERROR_CODE`/`RESUME_AT_ERROR_CODE_PREFIX`;#318 件④ 补 `delegation.*` 族四位 + `config.delegation_entry_caps`;0.39.0 补三新码消费件三位)、`src/classifierVerdictWire.ts`、`src/compensations.ts`、`src/clientSlice.ts` |
148
148
 
149
149
  🔴 **`engineErrorCodes` 的开集纪律**(该文件头注逐字):这些 `ReadonlySet` / 前缀谓词一律是**识别表**,
150
150
  回答的是「我认不认得这个码」,**绝不是**「合法码只有这些」。消费点 `switch` **必须留 `default`**,
@@ -769,13 +769,13 @@ CHANGELOG 0.29.0「已知局限」段与相应 JSDoc 都有成文。**别在读
769
769
  | **P-12** | low | canonical 重呈短路臂**沿用 arm responder** ⇒ `ReopenPlanReviewOpts.deliverDecision` 注入口**不生效**(成文例外 + debug 留痕) | `src/hitl/planReviewWire.ts`(`mintFreshQuestionId:false` 分支) | 任何**包装决断投递**的端(重试/退避/上屏定序,cli 的 `decideRetry` 是参照)必须走默认 `mintFreshQuestionId` 臂 —— 否则你的包装被静默旁路,跑的是裸 `decidePlanReview` 的 fire-and-forget |
770
770
  | **P-13** | 成文局限(不改行为) | 默认键下的 own-run 归属缺省腿是**进程级**证据,**不区分同一宿主进程内的会话代际** —— `/clear` 前登记的 run 在新会话语境下**仍判 owned**。最坏后果逐字:`用户看到自己旧会话的审批卡` | `src/hitl/parkOwnership.ts`(`ParkOwnershipDeps.isOwnRun` JSDoc);`docs/REFACTOR-LEDGER.md` 记为**驳为成文局限** | 多会话端必须**自注入**会话粒度的 `isOwnRun`;或传非默认 `sessionKey` 并接受缺省腿被整条跳过(代价 = 多一次诚实的 reopen-failed) |
771
771
  | **P-14** | high(打包面) | `activeReopenResponders` 的**单活纪律是 module 单例**:两份实例 ⇒ 各退各的,跨份的旧卡退役不掉 —— **退化回修复前的重复活卡形** | `src/hitl/planReviewWire.ts`(`activeReopenResponders`,singleton-manifest 在册) | 见 §8-G:必须保证 bundle 里只有**一份** `@sema-agent/client-core` |
772
- | **P-31** | med(0.32.0 未发布登记;codex 复审第二/三轮抓出) | **fleet 面的会话锚整条走默认槽,不跟 ledger 的 `sessionKey`** —— 两条腿都中招:①`bg_notification` 的 `ownByRoot` 判据用 `engineSessionParam()` = `hostSessionFor(DEFAULT_SESSION_KEY)`;②更强的一条 —— `serverFailClosed`(meta 两位)**根本不读会话端口**,它信的是「这条流是替谁开的」,而本包给出的开流参数 `fleetStreamOptions()` / `fleetSnapshotOptions()` 是 **module 级函数、只认默认槽会话**,连 `sessionKey` 都拿不到。⇒ keyed 多会话宿主上,一条按默认槽开的流接到**非默认键** ledger 时,属于默认槽会话的通知会被放行并以 `evidence:'server_fail_closed'`(🔴 **生产 meta 组合下命中的正是这一格**)或 `'own_root'` 交给 hook | `src/fleet/fleetLedger.ts`(`serverFailClosed` / `ownByRoot` / `fleetStreamOptions`);`src/host.ts`(`hostSession` = 默认键) | 单会话宿主(cli 及今天的三端)**不受影响**——默认槽就是它的会话,流与 ledger 恒同会话。**keyed 多会话宿主**:①必须自己保证「流按哪个会话开、就接给哪个 ledger」;②在①落实之前,不要把 `evidence` 的前两格当作「属于本 ledger 会话」的证明去做有副作用的事(落库/翻卡/跨会话搬运)。🔴 正位解 = fleet 面**整条**换 per-key 会话锚(开流参数 + 归属判据一起动);**只改判据这一半会自相矛盾**(按默认槽开流、按 keyed 槽判定 ⇒ 反而丢自己的通知),故本批**不动行为**,登记待裁 |
772
+ | **P-31** | med(0.32.0 未发布登记;对抗复审第二/三轮抓出) | **fleet 面的会话锚整条走默认槽,不跟 ledger 的 `sessionKey`** —— 两条腿都中招:①`bg_notification` 的 `ownByRoot` 判据用 `engineSessionParam()` = `hostSessionFor(DEFAULT_SESSION_KEY)`;②更强的一条 —— `serverFailClosed`(meta 两位)**根本不读会话端口**,它信的是「这条流是替谁开的」,而本包给出的开流参数 `fleetStreamOptions()` / `fleetSnapshotOptions()` 是 **module 级函数、只认默认槽会话**,连 `sessionKey` 都拿不到。⇒ keyed 多会话宿主上,一条按默认槽开的流接到**非默认键** ledger 时,属于默认槽会话的通知会被放行并以 `evidence:'server_fail_closed'`(🔴 **生产 meta 组合下命中的正是这一格**)或 `'own_root'` 交给 hook | `src/fleet/fleetLedger.ts`(`serverFailClosed` / `ownByRoot` / `fleetStreamOptions`);`src/host.ts`(`hostSession` = 默认键) | 单会话宿主(cli 及今天的三端)**不受影响**——默认槽就是它的会话,流与 ledger 恒同会话。**keyed 多会话宿主**:①必须自己保证「流按哪个会话开、就接给哪个 ledger」;②在①落实之前,不要把 `evidence` 的前两格当作「属于本 ledger 会话」的证明去做有副作用的事(落库/翻卡/跨会话搬运)。🔴 正位解 = fleet 面**整条**换 per-key 会话锚(开流参数 + 归属判据一起动);**只改判据这一半会自相矛盾**(按默认槽开流、按 keyed 槽判定 ⇒ 反而丢自己的通知),故本批**不动行为**,登记待裁 |
773
773
 
774
774
  ### 7d. 请求面与其它在册件
775
775
 
776
776
  | ID | 级别 | 一句话现状 | 实现锚 | 端**今天**必须怎么办 |
777
777
  |---|---|---|---|---|
778
- | **P-15** | med(6 → **5**,#292 P1 结清一条) | `REQUEST_FIELD_MATRIX` 有 **5 个字段登记为 `gap: true`**(表内 `gap:true` 的定义逐字 = 「这条差异**没有正当理由,是漏的**」),全部是 **print/headless 车道缺席**:`settings.ultracode` · `reasoningEffort` · `model` · `clientContext` · `scratchpadDir`。表内点名的后果:`clientContext` 缺席 ⇒ **引擎误标 (UTC)**;`scratchpadDir` 缺席 ⇒ **`-p` 的工具写不进 exemptDir**。**已结清**:`settings.<resolved>`(0.34.0 / #292 P1 —— 版本号与 CHANGELOG 段**同一个**,codex 复审 [low] 抓的正是两处不一致)—— 它的缺席是**权限方向**的(`-p` 上用户 settings 的 `permissions.deny/ask` 整体不被引擎求值,cli [4208] 实测),现两车道都 stamp | `src/request/taskRequest.ts`(`REQUEST_FIELD_MATRIX` 的 `gap` 列) | headless 车道上这五项**确实不上 wire**。端不要在 print 车道假设它们在场;补齐是**行为改动**,要单独一条测试,不许端侧偷加。`settings.<resolved>` 反过来:print 车道现在**会**摊开 resolver 快照 ⇒ 端必须把值放进 `input.settings.resolved`(端不给值仍是零 stamp,不会凭空出现),且该车道 `settings` 子键走**开放集**口径:`unregisteredRequestKeys` 只放行**表外**动态子键(快照自己的 permissions/env/model/… 不可枚举),**表内但不属于本车道**的子键(今天 = print 的 `settings.ultracode`)仍会被点名 —— 端不许把它白名单化,那条红是真的;另:`settings` 子键值为**函数**(如自有 `toJSON`,能在序列化时整只改写字节)恒被点名且构造器不 stamp。🔴 **0.35.0 起快照通道对具名通道让位**:见 P-15c |
778
+ | **P-15** | med(6 → **5**,#292 P1 结清一条) | `REQUEST_FIELD_MATRIX` 有 **5 个字段登记为 `gap: true`**(表内 `gap:true` 的定义逐字 = 「这条差异**没有正当理由,是漏的**」),全部是 **print/headless 车道缺席**:`settings.ultracode` · `reasoningEffort` · `model` · `clientContext` · `scratchpadDir`。表内点名的后果:`clientContext` 缺席 ⇒ **引擎误标 (UTC)**;`scratchpadDir` 缺席 ⇒ **`-p` 的工具写不进 exemptDir**。**已结清**:`settings.<resolved>`(0.34.0 / #292 P1 —— 版本号与 CHANGELOG 段**同一个**,对抗复审 [low] 抓的正是两处不一致)—— 它的缺席是**权限方向**的(`-p` 上用户 settings 的 `permissions.deny/ask` 整体不被引擎求值,cli [4208] 实测),现两车道都 stamp | `src/request/taskRequest.ts`(`REQUEST_FIELD_MATRIX` 的 `gap` 列) | headless 车道上这五项**确实不上 wire**。端不要在 print 车道假设它们在场;补齐是**行为改动**,要单独一条测试,不许端侧偷加。`settings.<resolved>` 反过来:print 车道现在**会**摊开 resolver 快照 ⇒ 端必须把值放进 `input.settings.resolved`(端不给值仍是零 stamp,不会凭空出现),且该车道 `settings` 子键走**开放集**口径:`unregisteredRequestKeys` 只放行**表外**动态子键(快照自己的 permissions/env/model/… 不可枚举),**表内但不属于本车道**的子键(今天 = print 的 `settings.ultracode`)仍会被点名 —— 端不许把它白名单化,那条红是真的;另:`settings` 子键值为**函数**(如自有 `toJSON`,能在序列化时整只改写字节)恒被点名且构造器不 stamp。🔴 **0.35.0 起快照通道对具名通道让位**:见 P-15c |
779
779
  | **P-15b** | 🔴 权限方向 | `REQUEST_FIELD_MATRIX` 的 stamp 门对**未登记键静默丢弃** —— 表里点名的真实危险形逐字:**「用户显式排除的工具被静默放回」(权限方向回归,类型层不报)**。`excludeTools` 是真 wire 键、早在 seatContract 的 `START_SESSION_OPTION_KEYS` 里,却曾长期在矩阵外;**今天只有 desktop 在发它** | `src/request/taskRequest.ts`(`excludeTools` 行)、`src/seatContract.ts`(`START_SESSION_OPTION_KEYS`) | 端自拼 taskReq 的键**必须**先进矩阵;上 wire 前跑 `unregisteredRequestKeys(req, lane)` 并**当红对待**,别当 lint |
780
780
  | **P-15c** | 🔴 治理方向(0.35.0 行为改动) | `settings.<resolved>` 快照是**开放集 spread**(子键即 wire 键)⇒ 它天生是一条**第二通道**。0.34.0 只剥「车道异名」子键,于是**两条车道都登记**的具名子键剥不到 —— 而它们各有治理门:`hooksForWire()` 是 fail-closed(无 `SettingsPort` / 工作区未受信 / 管理侧关停全部 hooks / 检查抛错 ⇒ 返 `undefined`),此时快照里那份**没过门**的 `hooks` 照样上 wire = 关停令等于没下。让位修前靠**合并序**(具名键覆盖快照),而合并序只在具名通道**有值**时管用,门否决时恰恰**没值**。0.35.0 改**结构剥离**:凡表里有 `settings.<sub>` 行的子键(`hooks`/`webSearch`/`ultracode`),快照一概不产;表外子键(`permissions`/`env`/`model`/…)原样摊开 | `src/request/taskRequest.ts`(`namedSettingsSubKeys` / `resolvedSnapshotForWire`) | ① 具名键**必须走具名位**:`input.settings.hooks` / `.webSearch` / `.ultracode` —— 只塞进 `input.settings.resolved` 的宿主从 0.35.0 起那两个键**不再上 wire**(两条车道对称,不是新差异面);② `input.settings.resolved` 只放**表外**的 resolver 产物;③ 该位为 `undefined`/`null` = 合法缺席(照旧降空照发),**合法载体只有对象字面量与 `null` 原型字典**;其余形(数组/原始值/boxed 包装对象/`Map`/**类实例**)抛 `TypeError`。判据锚 **prototype 层数(realm 无关 —— iframe/vm/另一渲染进程的字面量照过)不锚自报标签**:类实例与 `Symbol.toStringTag` 伪造都能自报 `[object Object]`,而摊开走 `Object.entries`(只取自有可枚举键)⇒ 权限面挂在原型 getter 上会摊出空快照、请求照发。这一位摊开的是已解析权限面,降空 = 带着被剥掉的 `deny/ask` 发出去,故 fail-closed;端别 catch 掉它当没事,那是上游产出坏了 —— 把快照**平摊成对象字面量**再传即可;④ `permissions` **子树**也递归校「序列化后还是同一份内容吗」:嵌套 `Map`/`Set`/类实例/boxed/环、**任何一层**上可 call 的 `toJSON`(自有/非枚举/原型链/数组子类)、**非有限数**(`NaN`/`±Infinity`)都 ⇒ `TypeError`,报路径如 `permissions.deny[0]`,不猜 schema;射程刻意只到 `permissions`(整体深净化 = 独立工单)。⚠️ 两条**成文边界**(各有判据钉住现状,不是漏):① 构造之后污染 `Array.prototype`(重建出来的数组必须是真数组);② `getPrototypeOf` 被 Proxy 陷阱撒谎的载体(同 realm 内无可移植的 Proxy 读法)——两者都**不新增丢失面**(发的字节 = 原生序列化那一份),要关得靠「受信 resolver 出口发烙印/已物化记录」那个结构 |
781
781
  | **P-15d** | 已知缺口(权限方向;0.35.0 登记,**刻意未在本批闭合**) | 0.35.0 把「序列化后还是同一份内容吗」这条不变量**只**落到 `settings.resolved.permissions` 子树(校 + 就地重建)。**同一条论证对其它带权限含义的位一样成立,而它们今天没有等价强制**:`permissionMode`(字符串位;非串载体的 `toJSON` 能自己决定 wire 上那个词)· `excludeTools`(丢一个元素 = **用户显式排除的工具被放回**,与 P-15b 同一方向)· `additionalDirectories` / `additionalReadDirectories`(读写边界根)· `agents` / `hooks` / `attachments`(对象位,同款 `toJSON` 改写面)。**为什么不在本批一起做**:逐个挑两三个字段补,只会造出下一个同样任意的边界;正解是**一次**把「wire 载荷 JSON-safe 规范化」做成包级闸口(该工单自 0.34.0 起在册),覆盖所有位并同批建判据 | `src/request/taskRequest.ts`(`materializeJsonFaithful` 的射程 = `permissions`;边界本身有一条判据钉着:非权限位的同款畸形**不拦**) | 端**不要**推断「本包会替我把请求体洗干净」——今天只有 `permissions` 子树有这个保证。上 wire 的值请自己保证是 JSON 原生形(字面量 / 数组 / 字符串 / 有限数):别拿类实例、`Map`、带 `toJSON` 的包装对象、访问器对象当载体。尤其 `permissionMode` / `excludeTools`:前者决定整会话的审批姿态,后者丢一个元素就是权限变宽 |
@@ -793,7 +793,7 @@ CHANGELOG 0.29.0「已知局限」段与相应 JSDoc 都有成文。**别在读
793
793
  | **P-27** | 立票设计件(web [C1] 疑点③,族A 二段票同批) | `ToolPermissionDecision` **无 note 席位**且座位宿主无 caps 缓存读口 ⇒ #229 回决备注在两座位端(web/desktop)**结构性无法供给**。修形二选一未裁:decision 形补 `note?` + 能力位随 `ToolPermissionRequest` 下发,或宿主侧统一判 | `src/seatContract.ts`(`ToolPermissionDecision`,682 行域) | 座位端今天**不要**渲 note 输入位(渲了也送不出去=假 affordance);候本条落地随提货单换 |
794
794
  | **P-28** | 🔴 med(浏览器面) | ⚠️ 2026-08-14 补记:除下面那 10 处外,**第二个未放宽的入参面** `EngineProbeOpts.authToken`(`src/engineWireSdk.ts`)另喂 2 处探针(`agentsWireCaps.ts` 的 `engineSupportsTaskAgents` ⇒ `undefined` / `liveInitToolFace.ts` 的 `probeScenarioTools` ⇒ `null`,均属 C 档静默),合计 **12** 处 —— 见 §5d 末尾那段。 **`same-origin-relay` 只放宽了 2 个入参面,装配入口没跟**:`EngineWireClientConfig.token` 与 `LiveWorkflowConfig.authToken` 收了 `\| { mode:'same-origin-relay' }`,而 `EngineWireTarget.token` 仍是 `string` —— `installEngineWireTarget()` 恰恰是**非 Node 宿主唯一**的装配入口。经 `engineWireTarget()` 取址再构造 client 的 **10 处**(`hitl/planReviewWire.ts` ×2 · `subagent/engineSubagentTail.ts` · `engineSubagentSteer.ts` · `engineSubagentOutput.ts` · `engineCompactWire.ts` ×2 · `engineTaskHandleWire.ts` ×2 · `engineDelegatedPrompt.ts`)在同源反代部署下**没有合法凭证形可传**。⚠️ 失效形**逐点不同**(§5d 末两表:A 响亮 + **有条件**用户可见 = `decidePlanReview`,可见性取决于通知队列口装没装 / B 结构化 reason = steer 与 taskStop / C 无条件静默 = 其余七处),但**构造失败一律吞成 null、从不抛异常** | `src/engineWireTarget.ts`(`EngineWireTarget.token`)· `src/engineWireSdk.ts`(`makeEngineWireClient` 的 catch 臂)· §5d 的两表 | 浏览器同源宿主今天**只能**走 §5d 上表那两条自带入参面的路径(直调 `makeEngineWireClient` / `createLiveWorkflowSource`);走 `engineWireTarget()` 的子代与 plan-review 动词**别指望在 relay 部署下发得出去**,也**不要**把「动词没反应」读成「引擎没这个能力」(⚠️ plan-review 那条**只在通知队列口装上时**才到达用户,见 §5d 末表 —— 队列口没装就退回零用户通道,端的兜底告知别急着撤)。🔴 正位解在包侧(放宽 `EngineWireTarget.token` + 10 处透传),**不许端侧侧路补救**(跨仓缺陷源头修复);要它落地按 [C162] 令④ 回 C 板 |
795
795
  | **P-29** | low(自检面) | **通知队列口没有存在性读口**:审批卡口有 `hasApprovalCardPort(For)`、HITL 面有 `hitlHostSurfaceFor`、宿主端口族有 `hostSettings()` 等无副作用读口(见 §5a 的 (a) 表),**唯独 `installNotificationQueuePort()` 没有对偶谓词**。而它的 `notificationQueuePortMisses()` 与同族几个 miss 计数一样**初值为 0**,只有真发生过一次「用到了但没装」才递增 ⇒ 「完全没装 + 还没有任何投递」照样是 0。拿它做**启动装配自检**必然假绿 —— §5a 此前正是这么写的(#252 复审 R3/R4 命中,已按端口拆成「存在性读口」与「回归探针」两类) | `src/notifications.ts`(`installNotificationQueuePort` 无对偶读口;`queuePortMisses` 初值与 `port()` 的 null 分支) | 队列口:按 §8-B 真调 `installNotificationQueuePort()`,miss 计数只当**跑过真流量之后**的回归探针用;其余端口按 §5a (a) 表用各自的存在性读口做启动校验。要队列口的读口按 [C162] 令④ 回 C 板提(正位解在包侧:补一个 `hasNotificationQueuePort()` 谓词) |
796
- | **P-30** | med(HITL 路由面) | **durable 审批腿不按 `gateKind` 路由,且取行有「同 taskId 任意行」回落** (0.30.0 发包扫描 codex 复审 finding① 坐实,**非本窗引入**):`findPendingForTask` 在工具名谓词无命中时走 `?? rows.find(r => r.taskId === taskId)`,而 `surfaceFsApprovalAndDecide` 拿到行之后**不校 `gateKind`** ⇒ 同一 task 上同时停着 `plan_review` / `resource_limit` 行时,会弹出一张 `toolName` 为空串的**工具审批卡**。⚠️ **不会误批**(server 侧 fail-closed):本腿打的是 `POST /v1/approvals/:sessionId/decide`,非工具门在该端点上回 **409 `gate_not_tool_approval`**(SDK `dist/errors.d.ts`;⚠️ **不是** `gate_not_resumable` / `gate_not_plan_review` —— 那两个分别是 `/resume` 与 plan-review 端点的守卫,2026-08-14 codex 复审 R2 订正本条初稿的错码)。🔴 **但后果不止「一次失败的决断」**:decide 抛错 ⇒ `surfaceFsApprovalAndDecide` 折成 `{kind:'failed'}` ⇒ `parkResolver` 走 fail-soft 结束**本次客户端 turn**;而 server 侧 checkpoint 因为 fail-closed **没被消费**,run/session 仍 suspended、仍持 claim ⇒ 重试还会再撞一次。⚠️ **终帧按入口分两形,排障别只等一个码**(2026-08-14 codex 复审 R3 订正本条初稿的单一描述):① **初始 park 入口**(`done{…park…}` 经 `frameRouter.routeDone` 进来,`park.pendingDone` **在场**)⇒ 先 `led.flushHeld()` 吐出 park 期被 HOLD 的**毒化帧**(`tool_end{isError:true, output:'Operation aborted'}`,`frameRouter.ENGINE_ABORT_TOOL_RESULT`),再原样回吐那条 `done` —— **没有**合成 `failed` 终帧、**没有** `hitl_unanswered` 错误码,可观察到的失败信号只有那条 isError 的 `tool_end`。⚠️ **它与「用户真按了拒绝」可以分辨,按 `output` 分**(2026-08-14 codex 复审 R5 订正本条初稿的「同形不可分」):fail-soft 这条是 `flushHeld()` 原样吐出的**毒化帧**,`output` 逐字是 `ENGINE_ABORT_TOOL_RESULT`(`'Operation aborted'`);真 deny 走 `frameRouter` 的 `denied-call` / `deny-stamp-next` 臂,`output` 被改写成 `HITL_REJECT_MESSAGE`(CC `REJECT_MESSAGE` 逐字)。端做归因按 `output` 判,别只看 `isError`;② **续流 / durable re-attach 入口**(`suspended` 进来,无 `pendingDone`)⇒ 才合成 `failed{errorCode:'hitl_unanswered'}`。⇒ 端做告警/埋点时**不要**只锚 `hitl_unanswered`,①那条路径上它根本不出现。⚠️ 定性要分清:这条 fail-soft 链是 durable 腿**通用**的失败路径(设计如此 —— 替代方案是谎报成功,更坏),**不是**本缺口独有;本缺口的**增量**是「弹了一张 `toolName` 为空的卡 + 发了一次注定 409 的 decide + 把用户的一次表态浪费掉」 | `src/hitl/hitlBridge.ts`(`findPendingForTask` 的第二条 `rows.find`)· `src/hitl/toolApprovalWire.ts`(`surfaceFsApprovalAndDecide` 全程零 `gateKind` 读)· 常驻登记见 `scripts/run-durable-card-display-keys-test.mjs` ⑦ 段 `gateKind` 那条未投影理由 | 端**不要**把「durable 卡弹出来了」读成「这一定是个工具门」;拿到 `toolName` 为空串的卡按异常处置、别渲成可决断卡。🔴 正位解在包侧(本腿按 `gateKind` 严格路由 + 回落收窄),要同批想好 pre-`gate_kind` 历史行 `gateKind` 缺席时的降级 —— 属独立设计件,按 [C162] 令④ 回 C 板提 |
796
+ | **P-30** | med(HITL 路由面) | **durable 审批腿不按 `gateKind` 路由,且取行有「同 taskId 任意行」回落** (0.30.0 发包扫描 对抗复审 finding① 坐实,**非本窗引入**):`findPendingForTask` 在工具名谓词无命中时走 `?? rows.find(r => r.taskId === taskId)`,而 `surfaceFsApprovalAndDecide` 拿到行之后**不校 `gateKind`** ⇒ 同一 task 上同时停着 `plan_review` / `resource_limit` 行时,会弹出一张 `toolName` 为空串的**工具审批卡**。⚠️ **不会误批**(server 侧 fail-closed):本腿打的是 `POST /v1/approvals/:sessionId/decide`,非工具门在该端点上回 **409 `gate_not_tool_approval`**(SDK `dist/errors.d.ts`;⚠️ **不是** `gate_not_resumable` / `gate_not_plan_review` —— 那两个分别是 `/resume` 与 plan-review 端点的守卫,2026-08-14 对抗复审 R2 订正本条初稿的错码)。🔴 **但后果不止「一次失败的决断」**:decide 抛错 ⇒ `surfaceFsApprovalAndDecide` 折成 `{kind:'failed'}` ⇒ `parkResolver` 走 fail-soft 结束**本次客户端 turn**;而 server 侧 checkpoint 因为 fail-closed **没被消费**,run/session 仍 suspended、仍持 claim ⇒ 重试还会再撞一次。⚠️ **终帧按入口分两形,排障别只等一个码**(2026-08-14 对抗复审 R3 订正本条初稿的单一描述):① **初始 park 入口**(`done{…park…}` 经 `frameRouter.routeDone` 进来,`park.pendingDone` **在场**)⇒ 先 `led.flushHeld()` 吐出 park 期被 HOLD 的**毒化帧**(`tool_end{isError:true, output:'Operation aborted'}`,`frameRouter.ENGINE_ABORT_TOOL_RESULT`),再原样回吐那条 `done` —— **没有**合成 `failed` 终帧、**没有** `hitl_unanswered` 错误码,可观察到的失败信号只有那条 isError 的 `tool_end`。⚠️ **它与「用户真按了拒绝」可以分辨,按 `output` 分**(2026-08-14 对抗复审 R5 订正本条初稿的「同形不可分」):fail-soft 这条是 `flushHeld()` 原样吐出的**毒化帧**,`output` 逐字是 `ENGINE_ABORT_TOOL_RESULT`(`'Operation aborted'`);真 deny 走 `frameRouter` 的 `denied-call` / `deny-stamp-next` 臂,`output` 被改写成 `HITL_REJECT_MESSAGE`(CC `REJECT_MESSAGE` 逐字)。端做归因按 `output` 判,别只看 `isError`;② **续流 / durable re-attach 入口**(`suspended` 进来,无 `pendingDone`)⇒ 才合成 `failed{errorCode:'hitl_unanswered'}`。⇒ 端做告警/埋点时**不要**只锚 `hitl_unanswered`,①那条路径上它根本不出现。⚠️ 定性要分清:这条 fail-soft 链是 durable 腿**通用**的失败路径(设计如此 —— 替代方案是谎报成功,更坏),**不是**本缺口独有;本缺口的**增量**是「弹了一张 `toolName` 为空的卡 + 发了一次注定 409 的 decide + 把用户的一次表态浪费掉」 | `src/hitl/hitlBridge.ts`(`findPendingForTask` 的第二条 `rows.find`)· `src/hitl/toolApprovalWire.ts`(`surfaceFsApprovalAndDecide` 全程零 `gateKind` 读)· 常驻登记见 `scripts/run-durable-card-display-keys-test.mjs` ⑦ 段 `gateKind` 那条未投影理由 | 端**不要**把「durable 卡弹出来了」读成「这一定是个工具门」;拿到 `toolName` 为空串的卡按异常处置、别渲成可决断卡。🔴 正位解在包侧(本腿按 `gateKind` 严格路由 + 回落收窄),要同批想好 pre-`gate_kind` 历史行 `gateKind` 缺席时的降级 —— 属独立设计件,按 [C162] 令④ 回 C 板提 |
797
797
 
798
798
  ### 7e. 缺口的共同形状(值得单独说)
799
799
 
@@ -3,13 +3,13 @@
3
3
  > **终态:167/167 全清**(F 族→B 族→wave1 五卡→G 族两车→wave2 六卡→收官批→A 族压轴双车)。
4
4
  > 收官判据:21 门全绿(merge 后主树 20/20 套含 SEMA_CLI_ROOT 真差分,另 _gate-lib 加载期自测)/
5
5
  > 导出基线 525 双向 / 六 strict 旋钮全开 / singleton manifest 双向 / typeshape 棘轮 b4=21·unknown 203(逐条登记)·裸返回 0。
6
- > 收货形=refactor-pipeline-form(sonnet·opus 车+codex×opus 双镜头+主会话亲审亲变异);A 族压轴双车三方判决+
6
+ > 收货形=refactor-pipeline-form(sonnet·opus 车+双镜头 双镜头+主会话亲审亲变异);A 族压轴双车三方判决+
7
7
  > 收货修两笔(args 快照/单读·快照·惰性三处)见 aaec8a7/c73fdf7 两 merge。残余台账=WAVE1-RESIDUALS.md+
8
8
  > 各族「P2 裁决」段 keep-with-reason 条目。本档由 P2 工作底稿原位收卷,逐条裁决与坐标全部保留如下。
9
9
 
10
10
  # (原)client-core 规范重构 P1→P2 裁决工作底稿
11
11
 
12
- 统一重排号 REF-CC-001 起连号,原镜头 id 括注保留。**裁决进度**:F 族(075-080)=GO 已落地(merge 2626369,四旋钮+20 红清零,变异实证红→绿);A 族=全 GO 但候 r2 修订稿(codex 复审四 verdict 折入,见 A-FAMILY-CODEX-REVIEW.md);B 族 notif-01/02/03=GO 紧急批在飞。并单已按 critic(agent5)点名 + 复核发现的同文件同行段同修法项执行(见文末统计的「并单前/后」)。P2裁决列留空,由裁决人填 GO/HOLD/REJECT + 备注。
12
+ 统一重排号 REF-CC-001 起连号,原镜头 id 括注保留。**裁决进度**:F 族(075-080)=GO 已落地(merge 2626369,四旋钮+20 红清零,变异实证红→绿);A 族=全 GO 但候 r2 修订稿(对抗复审四 verdict 折入,见 A-FAMILY-ADVERSARIAL-REVIEW.md);B 族 notif-01/02/03=GO 紧急批在飞。并单已按 critic(agent5)点名 + 复核发现的同文件同行段同修法项执行(见文末统计的「并单前/后」)。P2裁决列留空,由裁决人填 GO/HOLD/REJECT + 备注。
13
13
 
14
14
  来源镜头:agent0=dup-invariant(13) / agent1=giant-split(17) / agent2=type-shape(17) / agent3=gate-p4(38) / agent4=lexicon/域词表(21) / agent5=critic完备性(14) / sup0=midband(7) / sup1=xlate(17) / sup2=notif(14) / sup3=hitl2(17,实14条finding+3条coverage段) / sup4=fleet2(15)。
15
15
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/client-core",
3
- "version": "0.38.0",
3
+ "version": "0.39.0",
4
4
  "description": "Client-side session runtime shared by every sema human client (TUI / web / desktop): sema wire frames (AgentEvent) -> CC session vocabulary (SDKMessage) with dual-plane output (transcript/chrome), deterministic transcript ids, lane discipline as a type, and the notification/dedup ledgers. Every CC-skin shape is collected here so the wire itself stays neutral. Blackboard [1832] design axioms; [1651]/[1652]/[1653] signed seam design. Renamed from @sema-agent/wire-cc-adapter (0.1.x).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "devDependencies": {
37
37
  "@sema-agent/agent-types": "^0.2.0",
38
- "@sema-agent/core": "^5.43.0",
38
+ "@sema-agent/core": "^5.50.0",
39
39
  "@sema-agent/sdk": "^7.2.0",
40
40
  "esbuild": "^0.27.4",
41
41
  "typescript": "^6.0.2"