@sema-agent/client-core 0.46.0 → 0.48.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.
@@ -15,24 +15,29 @@
15
15
 
16
16
  ## §0 版本锚与重扫纪律
17
17
 
18
- ### 0a. 版本锚(2026-08-21)
18
+ ### 0a. 版本锚(2026-09-01)
19
19
 
20
20
  | 项 | 值 | 真源 |
21
21
  |---|---|---|
22
- | 本包 | `@sema-agent/client-core` **0.38.0** | `package.json` `version` |
23
- | peer:wire 契约 | `@sema-agent/sdk` **>=7.2.0**(value-level,非 type-only) | `package.json` `peerDependencies` |
22
+ | 本包 | `@sema-agent/client-core` **0.48.0**(工作树;发布前 npm 最新是 `0.47.0`) | `package.json` `version` |
23
+ | peer:wire 契约 | `@sema-agent/sdk` **>=7.4.0**(value-level,非 type-only;0.48.0 抬版,四条硬理由见 `CHANGELOG.md` 0.48.0 段末的地板影响面账) | `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
- | 公开导出面 | **787** 个运行期符号(+ 41 个测试钩;= 未发 design/285 批 0+1+2+3 的值,npm `0.44.0` 是 **783**,`0.43.1`/`0.43.0` 是 **776**,`0.42.0` 是 **771**,`0.41.0` 是 **767**,`0.39.0` 是 **766**,`0.38.0` 是 **764**;`0.37.0` 是 **753**,见 `CHANGELOG.md`) | `scripts/public-export-baseline.json` 的 `count` / `testHookCount` —— **别手抄进别处,以该文件为准** |
26
+ | 公开导出面 | **794** 个运行期符号(+ 41 个测试钩;= 未发 0.48.0 的值,npm `0.47.0` 是 **790**,`0.46.0` 是 **787**,`0.44.0` 是 **783**,`0.43.1`/`0.43.0` 是 **776**,`0.42.0` 是 **771**,`0.41.0` 是 **767**,`0.39.0` 是 **766**,`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
 
30
- ⚠️ 0.38.0 已于 2026-08-21 发布,本表与 npm 最新版重新对齐(工作树 = npm)。装旧版(≤0.37.0)的
31
- 端注意:0.38.0 新增的 **11 个 additive 导出**(`engineCapState` + `EngineCapState`、
32
- `resolveEntryVision` / `computeDeleteBlockers` / `computeDeleteWarnings`、`CONFIG_DELEGATION_ENTRY_CAPS` /
33
- `DELEGATION_CONCURRENCY_CAP` / `DELEGATION_SESSION_CAP` / `DELEGATION_CAP_CODES` / `isDelegationCapCode`、
34
- `wireCycleSeq` / `wireRetiredBy`)在旧版上按名 import 会**在 ESM 实例化当场炸**(具名导出不存在)——
35
- 提货前先抬依赖。旧版对表以 `CHANGELOG.md` 对应版本段为准。
30
+ ⚠️ **工作树 ≠ npm**:本表记的是**工作树**的 0.48.0,npm 上最新仍是 0.47.0(冻结账里 0.48.0
31
+ `pending` 行)。装 ≤0.47.0 的端注意:0.48.0 新增的 **4 个 additive 导出**
32
+ (`readSessionMemoryStatus` / `classifyMemoryStatusFailure` / `readCaptureOptOut` / `readLastCapture`)
33
+ 在旧版上按名 import 会**在 ESM 实例化当场炸**(具名导出不存在)—— 提货前先抬依赖。
34
+ 🔴 **0.48.0 还抬了 peer 地板**(`@sema-agent/sdk >=7.4.0`),这是本版**唯一**的非 additive 面:
35
+ 端装 <7.4.0 的 SDK 会看到 peer 警告(运行期不因此变化)。同一条对 0.47.0 那 **3 个 additive 导出**
36
+ 成立(`planInteractiveHalt` / `RUN_LEVEL_STOP_ERROR_CODES` / `readDecideCurrentPending`)。同一条对 0.38.0 那 11 个
37
+ additive 导出成立(`engineCapState` + `EngineCapState`、`resolveEntryVision` /
38
+ `computeDeleteBlockers` / `computeDeleteWarnings`、`CONFIG_DELEGATION_ENTRY_CAPS` /
39
+ `DELEGATION_CONCURRENCY_CAP` / `DELEGATION_SESSION_CAP` / `DELEGATION_CAP_CODES` /
40
+ `isDelegationCapCode`、`wireCycleSeq` / `wireRetiredBy`)。旧版对表以 `CHANGELOG.md` 对应版本段为准。
36
41
 
37
42
  🔴 **本表里仍然手抄的数字都有门看着**(#252,2026-08-14):`scripts/run-integration-doc-freshness-test.mjs`
38
43
  ① 段把 638 / 32 / §2b 十六域名数之和 / 191 / 4 / 33 逐个对 `public-export-baseline.json` 算出来的值,
@@ -101,7 +106,7 @@ SDK 核对「本包 import 的每个值级符号仍然导出」「`TaskStats.cos
101
106
 
102
107
  ## §2 公共导出面地图(按域)
103
108
 
104
- > 全集真源 = `scripts/public-export-baseline.json` 的 `names`(**787** 项)。
109
+ > 全集真源 = `scripts/public-export-baseline.json` 的 `names`(**794** 项)。
105
110
  > 本节**不逐名抄**,只给「域 → 承重导出 → 用途 → 实现锚」。承重导出 = 一个端为了让这个域干活
106
111
  > **必须**直接调到的那几个符号;其余是它们的类型、变体与辅助位。
107
112
  > 单一入口:`import { … } from '@sema-agent/client-core'`(`exports` 只有 `.` 一个;
@@ -111,7 +116,7 @@ SDK 核对「本包 import 的每个值级符号仍然导出」「`TaskStats.cos
111
116
 
112
117
  `public-export-baseline.json` 由 **`dist/index.js` 的运行期导出**生成(生成口径自述见
113
118
  `scripts/run-client-core-typeshape-test.mjs`,双向精确集合门在 `scripts/run-public-surface-test.mjs`)。
114
- 实测:787 项 **100% 是运行期导出,零 type-only**。
119
+ 实测:794 项 **100% 是运行期导出,零 type-only**。
115
120
 
116
121
  **推论(端必须知道)**:
117
122
  - barrel 导出的**类型**面比 707 大得多,且**不被这道门看守** —— `AdapterContext` / `SeamEvent` /
@@ -120,18 +125,18 @@ SDK 核对「本包 import 的每个值级符号仍然导出」「`TaskStats.cos
120
125
  端依赖这些类型是合法的,但**不要**拿基线 diff 当"类型面没变"的证据。
121
126
  - `src/agentSession/contract.ts` 对基线贡献 **0** 项(纯类型模块,`export *` 在 dist 里是空转发)。
122
127
 
123
- 787 项的内部构成(帮助端估读表大小):**232** 项是 `SCREAMING_SNAKE` 常量数据表/词汇表
128
+ 794 项的内部构成(帮助端估读表大小):**233** 项是 `SCREAMING_SNAKE` 常量数据表/词汇表
124
129
  (矩阵、键集、env 名、锚串)而非可调用物;**5** 项是 PascalCase 运行期值
125
130
  (`ControlRouter` / `ControlSafetyError` / `HitlBridge` / `HitlSafetyError` / `DecideTransportRetryExhaustedError`);
126
131
  **41** 项是 `*For(sessionKey, …)` 的 per-session 变体(§6;其中 `engineNamespaceKeyFor` 是命名巧合 —— 参数是 baseUrl 不是 sessionKey,见域 14)。
127
132
 
128
- ### 2b. 域图(16 域,逐域计数之和 = 787)
133
+ ### 2b. 域图(16 域,逐域计数之和 = 794)
129
134
 
130
135
  | # | 域 | 名数 | 承重导出 | 用途 | 实现锚 |
131
136
  |---|---|---|---|---|---|
132
137
  | 1 | **适配内核(下行主链)** | 33 | `adapt` · `createWireToCcAdapter` · `runStream` · `eventToSdkMessage` · `terminalToSdkResult` · `turnUsageToModelUsage` · `isRunStreamActive` · `ADAPTER_DIVERGENCES` | 引擎 SSE `AgentEvent` → 端要渲的**双面输出**:transcript(`SDKMessage`)+ chrome(瞬态 `ChromeEvent`)。**本包存在的理由** | `src/adapt.ts`、`src/adapt/{arms,wireShapes,panelTasks}.ts`(经 `adapt.ts` 再导出)、`src/adapter/runStream.ts`、`src/adapter/downstream/*`、`src/adapter/types.ts` |
133
138
  | 2 | **seam 公共契约** | 2(其余为 type-only) | `CHROME_ARMS` · `deriveTranscriptId` | 公共词汇 + **id 确定性不变量**(同一条流重放 ⇒ 同一串 id)。`CHROME_ARMS` = 端「我要消费哪些 chrome 臂」的对照清单 | `src/seam.ts` |
134
- | 3 | **HITL 决断卡链**(§4/§5 主战场) | 131 | `makeHitlCanUseTool` · `HitlBridge` · `findPendingForTask` · `HitlSafetyError` · `bridgeAskUserQuestionGates` · `surfaceToolApprovalFrameAndRespond` / `surfaceFsApprovalAndDecide` · `readToolApprovalRespondAck` · `installApprovalCardPort(For)` · `installHitlHostSurface(For)` · `armPlanReviewApproval` · `reopenPlanReviewCard` · `decidePlanReview` · `startApprovalsFeed` · `pendingRowIsOwnedByThisSession` · `approvalCallKey`/`liveFrameCallKey`/`planReviewQuestionId` · `registerArmedGateFor`/`wasGateArmedFor`/`clearArmedGateFor` · `waitForGateArmed(For)`/`onGateArmed(For)`/`gateArmedWaitMs`(#244 F1 呈现回执事件源) · `planReviewArmedKey(For)`/`notePlanReviewAnswered(For)`/`notePlanReviewAnsweredIfDecisive(For)`(A-024.4 plan 呈现分代) · `toolEndOutputText` · `isAskTool` · `waitForParkRowBirth` · `classifyAskParkRows` / `askParkRowArm` / `classifyAskParkChainFailure` · `readDecisionNoteAudit` / `decisionNoteAuditLine` · `resumeRunningOptions` / `resumeChoiceFromLabels` · `persistedRulesLaneAvailable`/`persistedRulesGovernanceAvailable` · `classifyRulesFailure` · `listAllPersistedRules` · `classifySkippedReason` · `readRulePersistOutcome`(#244 F2:persist-ack 读口与 `readToolApprovalRespondAck` 合成一处) · `parseLocalAllowRule`(durable 腿本地落规则窄化骨架,谓词经 `LocalAllowRuleDeps` 注入) · `readToolApprovalRespondRefusal`(#225 件5,0.42.0:respond 抛错的结构化原文读口 —— 三位各自防御读、各自缺席不铸、**三位皆缺席时整只返 `undefined`**;原样交还零加工,UNTRUSTED-for-display)· `installEditedRuleTextPrechecker` / `hasEditedRuleTextPrechecker` / `precheckEditedRuleText`([5076] 转出口,0.42.0:core 5.57.0 `precheckEditedRuleText` 的**端口注入形** —— 类型面 + 注入口 + 诚实缺席读口。🔴 **不是** value 级 re-export,理由见 §7 缺口 **P-34**;未装 ⇒ 返 `undefined`,绝不编一个 `{ok:true}`)· `surfaceRuleArmNotSent` / `RULE_NOT_SENT_WARN_TEXT` + `surfaceRuleArmRejected` / `RULE_NOT_SENT_REJECTED_WARN_TEXT`(#334,0.43.0:人在卡上按下的「不再询问」被整条丢弃时的诚实告知——**两条刻意分开**:前者=**引擎能力位未确认**(换台引擎/等探测就好),后者=**这次选择没过包内表核/互斥核**(表外文本/坏下标/两臂同场,换引擎也不会变) —— 编辑臂 `respondFreeFormRules` 与批臂 `respondBatchRuleOffers` 两条同形存量共用一条,与 `surfaceRememberNotApplied` 同族纪律:决断照送、只是规则没存,静默丢掉用户明确意图 = 让人以为功能坏了)· `DecideTransportRetryExhaustedError`(Inkglow-1085 P0a:decide 出站瞬断重试耗尽的 typed 判别 —— HitlBridge 内建单次退避重试,耗尽走重呈臂不判死 turn;端一般只消费行为,不需要 instanceof) | suspended→decide→resume 环。🔴 **D-1 两元组 verbatim 回显**是字节级断言的安全不变量,端**不许重实现它的任何一段**。🔴 键空间边界(web [C1] d3 拦截):`gateIdentity` 四常量两函数只覆盖 HITL questionId/callKey 空间;seat 的 `TOOL_PERMISSION_REQUEST_ID_DOMAINS`(`plan:` 等)是另一键空间,**两者绝不合并**(合并=座位校验器静默拒全部 plan-review 卡) | `src/hitl/hitlBridge.ts`、`toolApprovalWire.ts`、`askGateWire.ts`、`planReviewWire.ts`、`hitlHostSurface.ts`、`gateIdentity.ts`、`armedGateRegistry.ts`、`parkOwnership.ts`、`parkResolver.ts`、`approvalsFeed.ts`、`frameRouter.ts`(**只挑名导出** `toolEndOutputText`/`ENGINE_ABORT_TOOL_RESULT`/`isAskTool`/`HITL_REJECT_MESSAGE`/`HITL_INTERRUPT_MESSAGE_FOR_TOOL_USE`)、`parkRowBirthWait.ts`、`approvalDecisionNoteAudit.ts`、`askParkRowRouting.ts`、`resumeRunningCard.ts`(#265 上收的判定层)、`persistedRulesWire.ts`、`localAllowRule.ts`(#244 F2 规则侧) |
139
+ | 3 | **HITL 决断卡链**(§4/§5 主战场) | 132 | `makeHitlCanUseTool` · `HitlBridge` · `findPendingForTask` · `HitlSafetyError` · `bridgeAskUserQuestionGates` · `surfaceToolApprovalFrameAndRespond` / `surfaceFsApprovalAndDecide` · `readToolApprovalRespondAck` · `installApprovalCardPort(For)` · `installHitlHostSurface(For)` · `armPlanReviewApproval` · `reopenPlanReviewCard` · `decidePlanReview` · `startApprovalsFeed` · `pendingRowIsOwnedByThisSession` · `approvalCallKey`/`liveFrameCallKey`/`planReviewQuestionId` · `registerArmedGateFor`/`wasGateArmedFor`/`clearArmedGateFor` · `waitForGateArmed(For)`/`onGateArmed(For)`/`gateArmedWaitMs`(#244 F1 呈现回执事件源) · `planReviewArmedKey(For)`/`notePlanReviewAnswered(For)`/`notePlanReviewAnsweredIfDecisive(For)`(A-024.4 plan 呈现分代) · `toolEndOutputText` · `isAskTool` · `waitForParkRowBirth` · `classifyAskParkRows` / `askParkRowArm` / `classifyAskParkChainFailure` · `readDecisionNoteAudit` / `decisionNoteAuditLine` · `resumeRunningOptions` / `resumeChoiceFromLabels` · `persistedRulesLaneAvailable`/`persistedRulesGovernanceAvailable` · `classifyRulesFailure` · `listAllPersistedRules` · `classifySkippedReason` · `readRulePersistOutcome`(#244 F2:persist-ack 读口与 `readToolApprovalRespondAck` 合成一处) · `parseLocalAllowRule`(durable 腿本地落规则窄化骨架,谓词经 `LocalAllowRuleDeps` 注入) · `readToolApprovalRespondRefusal`(#225 件5,0.42.0:respond 抛错的结构化原文读口 —— 三位各自防御读、各自缺席不铸、**三位皆缺席时整只返 `undefined`**;原样交还零加工,UNTRUSTED-for-display)· `installEditedRuleTextPrechecker` / `hasEditedRuleTextPrechecker` / `precheckEditedRuleText`([5076] 转出口,0.42.0:core 5.57.0 `precheckEditedRuleText` 的**端口注入形** —— 类型面 + 注入口 + 诚实缺席读口。🔴 **不是** value 级 re-export,理由见 §7 缺口 **P-34**;未装 ⇒ 返 `undefined`,绝不编一个 `{ok:true}`)· `surfaceRuleArmNotSent` / `RULE_NOT_SENT_WARN_TEXT` + `surfaceRuleArmRejected` / `RULE_NOT_SENT_REJECTED_WARN_TEXT`(#334,0.43.0:人在卡上按下的「不再询问」被整条丢弃时的诚实告知——**两条刻意分开**:前者=**引擎能力位未确认**(换台引擎/等探测就好),后者=**这次选择没过包内表核/互斥核**(表外文本/坏下标/两臂同场,换引擎也不会变) —— 编辑臂 `respondFreeFormRules` 与批臂 `respondBatchRuleOffers` 两条同形存量共用一条,与 `surfaceRememberNotApplied` 同族纪律:决断照送、只是规则没存,静默丢掉用户明确意图 = 让人以为功能坏了)· `DecideTransportRetryExhaustedError`(Inkglow-1085 P0a:decide 出站瞬断重试耗尽的 typed 判别 —— HitlBridge 内建单次退避重试,耗尽走重呈臂不判死 turn;端一般只消费行为,不需要 instanceof) | suspended→decide→resume 环。🔴 **D-1 两元组 verbatim 回显**是字节级断言的安全不变量,端**不许重实现它的任何一段**。🔴 键空间边界(web [C1] d3 拦截):`gateIdentity` 四常量两函数只覆盖 HITL questionId/callKey 空间;seat 的 `TOOL_PERMISSION_REQUEST_ID_DOMAINS`(`plan:` 等)是另一键空间,**两者绝不合并**(合并=座位校验器静默拒全部 plan-review 卡) | `src/hitl/hitlBridge.ts`、`toolApprovalWire.ts`、`askGateWire.ts`、`planReviewWire.ts`、`hitlHostSurface.ts`、`gateIdentity.ts`、`armedGateRegistry.ts`、`parkOwnership.ts`、`parkResolver.ts`、`approvalsFeed.ts`、`frameRouter.ts`(**只挑名导出** `toolEndOutputText`/`ENGINE_ABORT_TOOL_RESULT`/`isAskTool`/`HITL_REJECT_MESSAGE`/`HITL_INTERRUPT_MESSAGE_FOR_TOOL_USE`)、`parkRowBirthWait.ts`、`approvalDecisionNoteAudit.ts`、`askParkRowRouting.ts`、`resumeRunningCard.ts`(#265 上收的判定层)、`persistedRulesWire.ts`、`localAllowRule.ts`(#244 F2 规则侧) |
135
140
  | 4 | **子代 wire + 面板侧信道台账** | 84 | `tailEngineSubagent` · `installSubagentActivitySink` · `installSubagentTailMetaSink`(#280 件2:tail meta 帧发布口,`contentFrames` 判别位载体)· `stopEngineTask` + `classifyTaskStopConflict` · `fetchEngineSubagentReport` · `steerEngineSubagent`(0.32.0 未发布 #280 件A:additive 第三参 `childTaskId` —— 端有行上下文时**应当**传,传了就走「台账优先 / 缺席即诚实缺席 + `noteBgOwnerAbsence` 留痕」的 Q3 口径,与 tail·taskOutput·subagentOutput 三腿同姿势、与孪生 resume 腿共用同一个 `resolveOwnerRunId` 判据;**不传**则逐字维持旧行为=回落在飞 run)· `resumeSettledSubagent` + `resolveSubagentResumeContext` + `resolveOwnerRunId` + `classifySubagentResumeFailure` + `subagentResumeAvailable`(#242 批 2 A-028.7:resume 判定半场上收,与 steer 孪生同居;取址三态 = 台账有行用行值 / 指名了行但台账缺席则**诚实缺席绝不回落在飞 run** / 没指名行才回落。出路文案归端)· `recordSubagentOwnerFromProgress` + `getBgParentRunOwner`(A-028.6:「子代 → 宿主 run」**单表**,宿主 run 必须由持 stream-local 值的调用方显式传入,包内绝不从 `activeEngineRunId()` 推断)· `noteBgOwnerAbsence`(#242 批 3 [4000] Q3=B:tail/taskOutput·taskStop/subagentOutput 三腿台账缺席即诚实缺席**绝不回落在飞 run**,缺席 warn 留痕每 (腿,taskId) 一条)· `clearBgTerminalFacts`(#242 批 3 扫码修:复活=新周期,旧周期终态事实作废——fleetLedger 复活两腿按尾段清账,factsAccepted 方向核不再拿上周期终态当先例)· `auditRetainWithoutWake`([4000] Q5:引擎宣示 `subagentResume` + 本端在付 `retainSubagentSessions` + 端未实现 `wakeSubagent` ⇒ 响亮一条;`CLIENT_VERBS.wakeSubagent` 维持 fail-soft)· `subscribeSubagentContent` · `subscribeEngineAgentPanel` · `publishQuestionFrame` / `respondToQuestion` | 驱动与观测委派子代;经 module 级台账喂活体 agent/task 面板。全部**能力位 gate**(§5b) | `src/subagent/*.ts`、`src/subagentContentStore.ts`、`src/engineAgentPanelStore.ts`、`src/engineInlineTaskStats.ts`、`src/engineToolLabelStore.ts`、`src/liveQuestionStore.ts` |
136
141
  | 5 | **fleet 投影** | 46 | `createFleetLedger` · `projectTasks` · `projectWorkflows` · `projectFleetAgentRows` · `readEngineActiveBgTasks` · `FLEET_TASK_VIEW_KEYS` · `escapeDisplayControlChars`(不可见字符可见化,行标签/描述消毒的共享底座)· `wireCycleSeq` / `wireRetiredBy`(0.38.0 提货补投的 #261 §2 两位:代际号 = SendMessage 复活即 +1,**缺席 ≠ 第一代**;`retiredBy` 在场 = 这条终态是对账腿从 durable run 行投影出来的、**不是**发布方亲报 —— 幽灵行与正常收尾唯一的 wire 判据。两位都只在场才落键) | 老 `fleetClient` 那一刀的成品:**帧体归库、连接归端** —— 端持 SSE 连接,库做行投影 + 保留台账 | `src/fleet/fleetProjection.ts`、`src/fleet/fleetLedger.ts`、`src/fleetAgentPanelProjection.ts`、`src/fleetTaskDesc.ts` |
137
142
  | 6 | **请求装配(上行唯一构造口)** | 8 | `buildTaskRequest` · `REQUEST_FIELD_MATRIX` · `unregisteredRequestKeys` · `applyLiveRequestDefaults` · `taskNotificationToPrintFrame` | 两条车道(`interactive`/`print`)出站请求的**唯一**构造器;`unregisteredRequestKeys` 是可执行门 —— 端偷带一个未登记键上 wire 就红 | `src/request/taskRequest.ts`、`src/request/printNotification.ts` |
@@ -143,7 +148,7 @@ SDK 核对「本包 import 的每个值级符号仍然导出」「`TaskStats.cos
143
148
  | 12 | **workflow 与后台工作视图** | 19 | `projectWorkflowRun` · `createLiveWorkflowSource` · `ensureWorkflowActivityLedger` · `readWorkflowActivityLedger` · `stopWorkflowActivityLedger` · `resetWorkflowActivityLedgers` · `createBackgroundView` · `projectBackgroundView` · `recordWorkflowAgentTaskId` · `agentDisplayStatus` | 活过一个 turn 的长任务读面:workflow run + 跨 session 后台任务归一表(`assistant.tasks` 与 fleet SSE **两源独立降级**) | `src/workflow.ts`、`src/workflowClient.ts`、`src/workflowMonitor.ts`、`src/agentSession/backgroundView.ts`(+ 纯类型 `src/agentSession/contract.ts`) |
144
149
  | 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**,纯类型 + 常量 + 纯谓词)。🔴 **证据等级标注(0.42.0,test [5087] 的「语料**种类**缺口」/ cli [5088] 认领件)**:该文件里所有以「CC 如何如何」为形的断言(`212 methods` / `854-channel census` / 方法名逐字保留 / `fQe` 逐字段对照 / 一切 `.vite/build/index.chunk-*.js` 坐标)**证据等级 = 桌面 unpack,本地语料库不可复验** —— 本仓手边可复验的参照语料**只覆盖终端 CLI 形态**的静态产物,拿它去 grep 桌面壳里的符号只会零命中,而零命中在这里**既不证真也不证伪**。复核这些断言**不得**拿本仓语料当反证 |
145
150
  | 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
- | 15 | **控制面与传输** | 76 | `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`(默认出路串单源)· `engineSessionParamFor`(design/285 批 0:`?session=` 派生的 **per-key** 形 —— `hostSessionFor(sessionKey)?.currentSessionId()` + [1501] 空串归一;零参 `engineSessionParam()` = 默认槽兼容层,取值链逐字等价)· `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` |
151
+ | 15 | **控制面与传输** | 82 | `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`(默认出路串单源)· `engineSessionParamFor`(design/285 批 0:`?session=` 派生的 **per-key** 形 —— `hostSessionFor(sessionKey)?.currentSessionId()` + [1501] 空串归一;零参 `engineSessionParam()` = 默认槽兼容层,取值链逐字等价)· `normalizeWirePrincipal`(A-028.10:principal 在场性 trim 原语 —— 全空白=缺席不发头,engineWireTarget 两臂/makeEngineWireClient/壳 livePrincipal 同尺)· `readSessionMemoryStatus` / `classifyMemoryStatusFailure` / `readCaptureOptOut` / `readLastCapture`(S-53 会话记忆姿态读面,0.48.0:失败分诊**码优先**——两个 404 分道 `not_found.session` / `not_found.route`,无码 404 不猜落 failed;五键逐键缺席语义两个合读器,`lastCapture` 三态的判别材料是 `committedCount` 不是本键;IO 归宿主注入 `MemoryStatusClientLike`,详见 §11) · `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`、`src/sessionMemoryStatus.ts` |
147
152
  | 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
153
 
149
154
  🔴 **`engineErrorCodes` 的开集纪律**(该文件头注逐字):这些 `ReadonlySet` / 前缀谓词一律是**识别表**,
@@ -239,14 +244,36 @@ type EventProjection =
239
244
 
240
245
  ### 3d. 键级剥离(帧投影了,但键被投影边界剥掉)
241
246
 
242
- `case 'task_progress'` 的白名单**逐字**只 stamp 七键:
247
+ `case 'task_progress'` 的白名单**逐字**只 stamp 八键(0.47.0 起,#363 补 `model`):
243
248
 
244
249
  ```
245
- taskId · name · usage · currentAction · workflowRunId · workflowAgentLabel · parentToolCallId
250
+ taskId · name · usage · model · currentAction · workflowRunId · workflowAgentLabel · parentToolCallId
246
251
  ```
247
252
 
248
- **`taskType` / `status` / `parentTaskId` 三键在本层被整体剥掉** —— §7 缺口 **P-1**。
249
- lane 归属改用 id 形状 / `workflowRunId` 启发式判(`src/adapt/arms.ts`)。
253
+ 🆕 **`model`(0.47.0;server ≥7.52.1)** = 这条 tick 所属**子 run 的模型 id**(core 铸点
254
+ `prepared.model.id`;server `trace/project.js` `taskProgressEventData` 里条件 spread:
255
+ `string` 且非空才发)。本层同条件透传 —— **空串/缺席一律整键不铸**(空串既不是模型 id 也不是
256
+ 「不知道」)。补它之前是 `workflowRunId` 的**同形第二例**:上游真发、白名单剥掉、两边代码看着都对。
257
+ ✅ **SDK 锚已追平,cast 已退(0.48.0)**:0.47.0 这里写的是「sdk 7.3.0 的 `task_progress` 臂尚未
258
+ 声明这一位,故本层是**结构视图读**;SDK 补上当天那处 cast 可整条删」—— sdk **7.4.0 已声明**
259
+ `model?: string`,peer 地板同批抬到 `>=7.4.0` ⇒ 按那条退役条件兑现,改类型面直读。
260
+ 🔴 **退役的是 cast,不是运行期判**:`typeof` 门保留(旧 server 缺席 ⇒ 键不 stamp;wire 是 JSON,
261
+ 类型声明是上游承诺、不是本层前提)。端侧**行为逐字节不变**。
262
+
263
+ **仍然被本层剥掉的五键**(`seq` / `taskType` / `parentTaskId` / `status` / `eventId`):
264
+ - `taskType` / `status` / `parentTaskId` —— 在册的 §7 缺口 **P-1**;lane 归属今天改用 id 形状 /
265
+ `workflowRunId` 启发式判(`src/adapt/arms.ts`)。
266
+ - `seq` —— core #258 的 stop-cycle 代际号(复活即 +1)。fleet 面已有同轴的 `wireCycleSeq`(0.38.0),
267
+ **tick 这条腿今天没有消费方** ⇒ 照旧剥。
268
+ ⚠️ **就地订正(0.48.0)**:本条 0.47.0 的原文还写着「SDK 7.3.0 连声明都没有」—— sdk **7.4.0 已
269
+ 声明** `seq?: number`(与 `model` 同批)。**剥它的理由换了一条,但仍然剥**:准入条件从来是
270
+ 「说得出谁读它、读来干什么」,SDK 有没有声明只是当时顺带成立的第二个事实。
271
+ 🔴 **声明到货不是透传的理由** —— 否则这张白名单会随上游类型面自动变宽,准入条件形同虚设。
272
+ - `eventId` —— EventIdentity 的另一半;本臂只补了 `parentToolCallId`(lane 判据要它),`eventId` 至今无消费方。
273
+
274
+ 🔴 **透一位的前置条件**(键账的维护规矩,不是修辞):说得出**谁读它、读来干什么**,并同批更新
275
+ 本节的逐字表。这张表由常驻门 `scripts/run-additive-key-passthrough-test.mjs` 的 G1 段与**真产物**
276
+ 逐名对账(双向:多透一个红、少透一个也红;档与码不一致同样红)。
250
277
 
251
278
  **实现锚**:`src/adapter/downstream/eventToSdkMessage.ts` 的 `case 'task_progress'`。
252
279
 
@@ -321,17 +348,83 @@ in-process ask 拒绝时**在场。
321
348
  5. 🔴 **缺席不可反推** —— server 对非白名单码 / 缺 `sessionId` 的通告**如实不投**(宁缺席不串台),
322
349
  全族那一份只在 server 的运维日志里。「没收到通告」**不等于**「没发生」。
323
350
 
324
- ⚠️ **两条如实登记(2026-08-21 亲验)**:
325
- - **SDK 类型面还没到货**:`engine_notice` 尚未进已发布 SDK 的 `AgentEvent` union
326
- (sdk 仓 `3d6aebc` 已写,但 npm `@sema-agent/sdk@7.2.0` 的真 tarball `dist/` 全树零命中)。
327
- 故本包按 `workflow_complete` / `human_input` 当年的先例走 **raw 预分派**,并留了自退休钉:
328
- 臂一进 union,`assertNeverArm` 就编译期真红,逼下一棒把它搬进 switch。
351
+ #### 3f-2. 🆕 server 7.54.0 两个新码(0.48.0;`task.halt_unconsumed` / `task.late_approval`)
352
+
353
+ server 7.54.0`ENGINE_NOTICE_WIRE_CODES` 增至 **14 码**,新增两个 **task 域**的码
354
+ ([5899] / [5906];真源 = server `dist/trace/engine-notice-wire.js` 的常量数组直证):
355
+
356
+ | code | 语义(server 侧铸文;本包不改写一个字) |
357
+ |---|---|
358
+ | `task.halt_unconsumed` | 一次 halt 请求没有被消费掉(停止动词落在了「没有在飞的那一轮」那一格) |
359
+ | `task.late_approval` | 一次审批**迟到**了 —— 决断到达时它要结算的那只 ask 已经不在等了 |
360
+
361
+ 🔴 **本包的施工量 = 零,而这不是偷懒**:本臂按**开集**消费(消费纪律第 2 条:一个码都不硬编),
362
+ 所以这两个码**按构造**就到得了端 —— 不需要、也**不应该**为它们加任何识别分支。加一张码白名单
363
+ 才是这条腿唯一会坏的方式(新码当天静默蒸发,而两边代码看着都对)。
364
+ 常驻门 `scripts/run-additive-key-passthrough-test.mjs` **G3 段**把这条构造钉住:两个真码 **+ 一个
365
+ 编造的码**同时过投影,三者都必须原样出臂 —— 编造码那一条是**判别力的来源**(真码可能因为被加进
366
+ 某张白名单而仍然绿,编造码必红)。
367
+
368
+ 🔴 **端的消费点应当落在哪(本节写死,免得三端各找各的)**:
369
+ - 两码都是**会话级披露**,不是转录物 ⇒ 落**通知面 / 状态行**,**不要**合成 transcript 行
370
+ (与 §3f 主段同一条纪律)。
371
+ - `task.late_approval` 的呈现要与**审批卡面**联动而不是并列:它说的是「你刚才那一决断没落到东西
372
+ 上」,端若已经把卡收掉,应当据此把那张卡的终态从「已决断」订正为「未结算」,否则用户看到的是
373
+ 一次并不存在的成功。🔴 **它不是错误**,不要渲成失败态 —— 迟到是时序事实。
374
+ - `task.halt_unconsumed` 对应壳侧 Esc/停止腿(§10 `planInteractiveHalt` 的同一条语义轴):
375
+ 端据它把「已请求停止」的乐观态**收回**,而不是让那一行一直挂着。
376
+ ⚠️ 与 §10 的判定**不互替**:那一条是**发起前**的判定(该发 halt 还是 cancel),本码是**发起后**
377
+ 引擎回报的事实。两者都要,缺任一端都会在某一格谎报。
378
+ - 两码都遵守 §3f 的重放幂等序(`eventId` > `eventSeq` > `code+ts`)。
379
+ - **cli 认领**:壳侧接点在下一批(本批只保证「到得了端」+ 门 + 本节指引)。
380
+
381
+ ⚠️ **一条如实登记的订正(0.48.0)**:
382
+ - ✅ **SDK 类型面已到货,raw 预分派已退役**:0.47.0 这里登记的是「`engine_notice` 尚未进已发布
383
+ SDK 的 `AgentEvent` union(npm 7.2.0 真 tarball 全树零命中),故走 **raw 预分派** + 自退休钉」。
384
+ sdk **7.4.0 已声明该臂**(五键全必填、无 `& EventIdentity`)⇒ 那颗自退休钉**本批真的响了**:
385
+ devDep 抬到 7.4.0 的当拍 `tsc` 就报 `assertNeverArm` 收不下这条臂,逼着把它搬进 `case`。
386
+ 已按原定条款搬迁,**行为一字不改**(投影函数原样复用)。
387
+ 🔴 **投影仍走 raw `Record` 视图,刻意不改吃 SDK 收窄形**:SDK 把五键记成**全必填**,而本层对
388
+ 每一键都做诚实缺席处理(`code` 空 ⇒ malformed;`message`/`detail` 坏 ⇒ 降级但不丢帧;
389
+ `sessionId`/`ts` 非法 ⇒ 不 stamp)—— 这些分支在收窄形上会被类型面判成死码而**静默失效**。
390
+ 必填是 server 的承诺,不是本层的前提。且 `eventId` / `eventSeq` 两个重放身份键**根本不在**
391
+ SDK 臂声明里,收窄形上读它们是编译错。
329
392
  - **附录 D.3 的白名单表已失真**:档里仍写「起步白名单(server 7.36 三码)」,而 server main 的
330
393
  `ENGINE_NOTICE_WIRE_CODES` 已是**六码**(core 5.47/5.48 的 `NOTICE_AUDIENCE` 到货后
331
394
  `memory.hold_opened` / `hold_released` / `hold_disposed` 入册;v7.37.0 tag 上仍是三码 ⇒ 六码随
332
395
  7.38 到)。**对本包与端零影响** —— 正因为消费面按开集写,白名单是 server 的投递判定,不是消费判据。
333
396
  已按接入文档宪法回报 server。
334
397
 
398
+ ### 3g. 🆕 `status`(BrainStatus)臂的两个新键(0.48.0;core 7.0.x #506 ㋑ / server ≥7.53)
399
+
400
+ `RetryStatus` 上新增两个 additive 位。**病形与 §3d 的 `model` 逐字同族**:上游真发、本层闭形白名单
401
+ 剥掉、两边代码看着都对。⚠️ 这条腿上有**两层**白名单(`eventToSdkMessage` 的 `case 'status'` +
402
+ `adapt/arms.ts` 的 `retryStatusArm`),**两层同批修** —— 只修其中一层键仍到不了宿主。
403
+
404
+ | 键 | 语义 | 缺席读法 |
405
+ |---|---|---|
406
+ | `retryAtMs` | 本次退避**预计结束的墙钟时刻**(epoch ms),= 发帧那一刻的 `Date.now() + retryInMs`,**由产生者铸** | 不宣告等待的帧上必缺席(`recovered` / `gave_up` / output-cap 立即重发) |
407
+ | `errorStatus` | **刚刚失败那次尝试**的 HTTP 状态码;CC `system/api_retry.error_status` 是同一个数 | 缺席面**封闭**:传输层失败 / 流中断 / `circuit_open` / 终态帧上恒缺席 —— **禁**渲成 `0` 或「未知错误码」 |
408
+
409
+ 🔴 **`retryAtMs` 在场时端应当拿它渲倒计时,而不是拿 `deadline`**:`deadline` 是**本包**按
410
+ `nowMs + 剩余量`现算的,跨进程跳(core → server → 本包 → 端)的传输耗时已经把它推后了;而 core 对
411
+ >30s 的等待会每 30s **重播一帧并递减**,于是「自己算」的倒计时在每个重播片上**重新起跳**而不是收敛。
412
+ 产生者是唯一说得出那个时刻的人,所以它才铸这一位。
413
+ 🔴 **`deadline` 的语义与字节本批一字未改**(0.29.0 起已发布的行为面):两位**并存**,端自己选
414
+ (在场优先)。换算法 = 一次静默的行为改动,本包不做。常驻门 G4c 段是这条方向钉的反钉。
415
+ 🔴 **时钟域**:`retryAtMs` 是**墙钟**(`Date.now()`),不是单调钟。端不得拿它与自己的单调计时器比;
416
+ 跨机器 / 跨授时校正时按**近似值**处理 —— 权威的**相对**量始终是 `retryInMs`。
417
+ 🔴 **`errorStatus` 不许用来推断该不该重试**:该不该等由 `phase` / `errClass` 两个中性桶说了算;
418
+ 本位是给操作者看的**点名**(「谁失败了」),渲进人话行即可。
419
+
420
+ **端的消费点**:重试覆盖层那一行(`RetryStatus`)。CC parity 形 = 「API Error 529 · Retrying in Ns」。
421
+ **cli 认领**:壳侧渲染在下一批。
422
+ **实现锚**:`src/retryStatus.ts`(`BrainStatusPayload` / `RetryStatus` / `mapBrainStatusToRetry`)+
423
+ `src/adapter/downstream/eventToSdkMessage.ts` 的 `case 'status'` + `src/adapt/arms.ts` 的 `retryStatusArm`。
424
+ **常驻门**:`scripts/run-additive-key-passthrough-test.mjs` **G4 段**(驱**两段合成**的全链,
425
+ 只驱一层会在另一层漏修时假绿)+ `scripts/run-engine-vocab-floor-test.mjs` **G2-c**(键集镜像对
426
+ **实装 core** 逐键等值 —— 引擎再加键,那边先红)。
427
+
335
428
  ### 3z. 🆕 `_sema_collateral_abort` —— 连坐 abort 机读位(0.40.0,#324 / [4907])
336
429
 
337
430
  **病形**:同 turn 多个 call 在飞 + 一个撞 gate ⇒ 引擎对**整批**在飞 call 铸同一串
@@ -807,8 +900,93 @@ durable park 腿走 `HitlBridge.decideTool(outcome, toolUseID, opts, preResolved
807
900
  **实现锚**:`src/hitl/hitlBridge.ts`(`HitlSafetyError`)、`src/hitl/hitlHostSurface.ts`(`isNoPendingError`)、
808
901
  `src/hitl/parkOwnership.ts`(`isHitlSafetyErrorLike`)。
809
902
 
903
+ ### 4f. 🆕 decide 失败上的 `currentPending` 指路键(0.47.0;server S-02 ≥7.52)
904
+
905
+ server 的 409 `approval_stale` 拒体带一个 **additive 指路键** `currentPending` —— 本会话**当前**那条
906
+ pending 的三件 D-1 坐标。本包把它从 decide 失败原样搬到调用方拿得到的 outcome 上。
907
+
908
+ | 位 | 语义 |
909
+ |---|---|
910
+ | `toolName` | 当前 pending 被门住的工具名(与 pending 行同源同值)。**UNTRUSTED-for-display** |
911
+ | `boundCallId` | 当前 pending 的 `pendingAction.toolCallId` —— 重定位后 decide 要**逐字回显**的 D-1 锚 |
912
+ | `boundInputHash?` | server 铸的绑定 hash(行上有才带)。🔴 **逐字回显,绝不本地重算**(§4d 同一条铁律) |
913
+
914
+ **端怎么接**(三件,逐条是纪律不是建议):
915
+
916
+ 1. **读口**:`readDecideCurrentPending(err)` —— 从任意抛出物读出 `GateCurrentPending | undefined`。
917
+ 宿主若自己接 decide,用这一个读口,**别再铸第二份窄读器**。经本包 durable fs 审批腿
918
+ (`surfaceFsApprovalAndDecide`)时它已经落在 `FsApprovalOutcome.currentPending` 上,直接读。
919
+ 2. **它是指路,不是裁决**:server 自己的登记原话逐字 ——「纯指路/便利面,**不参与任何门/CAS/resume
920
+ 判定**」。合法用法只有一个:拿 `boundCallId`(+`boundInputHash`)**一跳重定位**到当前那条 pending,
921
+ 重新呈卡、重新 decide。🔴 **绝不**拿它当「可以自动重决」的凭据:上一张卡的答案是人对**另一件事**
922
+ 给的,自动搬过去 = 替人对他没看过的事按 Yes(与 `binding_mismatch` 同一条铁律,§4d/§4e)。
923
+ 3. **缺席什么都不证明**:老 server / 非工具门 / server 侧行读失败(它自己的 F 类留痕臂)/ 本次失败
924
+ 根本不是 stale 臂 —— 四种情形在 wire 上**同形**。⇒ 缺席时退回既有姿势(重拉 `GET /v1/approvals`
925
+ 自行重定位),**不许**把缺席读成「没有别的 pending 了」。
926
+ 4. 🔴 **`checkpointToken` 永不过境**:server 侧 resume 凭证不外发,本形也刻意没有那一位;
927
+ 读口对表外键一律不搬运(常驻门逐条钉)。
928
+
929
+ 5. **它同时是一道「绝不自动重决」闸**(#363,异源复审 [high] 采纳):`allowSession`(accept-session)
930
+ 那条腿会先发 `approve + remember:'session'`,失败时回退一发**纯 approve**。修前那条回退臂除两个
931
+ 本地错误类外**全吞** —— 引擎回的 stale(带 `currentPending`)会被当成「老 server 不识别
932
+ `remember`」,然后**用人对旧卡给的答案再发一次 decide**,正是 §4d/§4e 那条铁律禁的动作;
933
+ 顺带首发的指路键还会被第二发的错误顶掉。现在:**拒体带指路键 ⇒ 立即上抛**(只发一次 decide,
934
+ 指路键原样进 outcome),人重新决断。老 server 的 400 未知键**照旧回退**(既有兼容腿宽度不变)。
935
+
936
+ ⚠️ **可达性如实登记(端接之前必读)**:sdk 的 `ApprovalDecision` 自 1.0.0 起**刻意无**
937
+ `checkpointToken`,而 server 的 stale 臂**只在调用方回显该 token 时触发** ⇒ **经 SDK client 的
938
+ decide 今天拿不到这枚 409**。本位是给「注入自有传输层 / 读别人写的 wire」的宿主与将来上游放行准备的
939
+ 通路 —— 今天在标准 SDK 路径上它恒缺席。这不是缺陷,是如实的射程边界(见 §7b)。
940
+
941
+ ⚠️ **类型形自铸的记账**:sdk **7.3.0** 有逐字同形的 `ApprovalStaleCurrentPending` +
942
+ `ApprovalStaleError.currentPending`,但本包 peer 地板是 **>=7.2.0**(那一版上两个名字都不存在),
943
+ `import type` 会让装 7.2.0 的端**当场编不过**,而抬地板对所有消费方都是提要求、不是 additive。
944
+ ⇒ 与 `RuleOffer`(#334)同款处置:**自铸形 + 记账**,名字刻意**不同名**(`GateCurrentPending`)。
945
+ **退役条件**:peer 地板抬到 `>=7.3.0` 的那一批换成上游类型别名。
946
+
947
+ **实现锚**:`src/hitl/hitlBridge.ts`(`GateCurrentPending` / `readDecideCurrentPending`)、
948
+ `src/hitl/toolApprovalWire.ts`(`FsApprovalOutcome.currentPending`,allow + deny 两条 decide 失败臂)、
949
+ `src/hitl/parkResolver.ts`(ask 腿同形);常驻门 `scripts/run-additive-key-passthrough-test.mjs` G2 段。
950
+
951
+ ---
952
+
810
953
  ---
811
954
 
955
+ ### 4g. 🆕 durable park 行的 bidi 披露位 `hasBidiControls`(0.48.0;S-30①,server ≥7.53 / core 5.60.0 #438)
956
+
957
+ durable 审批行 `PendingCheckpoint.hasBidiControls` 随卡透传到 `ApprovalCardRequest.hasBidiControls`。
958
+ 语义:这条 park 行的**执行载荷**里含至少一个 DIRECTIONAL 格式控制符(Trojan Source —— 人眼读到的
959
+ 顺序 ≠ 真正执行的字节顺序)。
960
+
961
+ 🔴 **与活卡腿的 `inputHasBidi` 刻意分键不合流**(上游把两个名字取得不同,正是为了不让人合并):
962
+
963
+ | | 活卡帧腿 | durable park 行腿 |
964
+ |---|---|---|
965
+ | 卡上的键 | `inputHasBidi`(0.43.0 起) | `hasBidiControls`(**本批**) |
966
+ | 谁算的 | server 对**帧自身序列化后的 args** 现算(E-14) | **core** 在 park mint 时算(`PendingAction.hasBidiControls`),反范式成 durable 列;server `listPending` 读列 `=== 1` 才铸,**不重算** |
967
+
968
+ ⇒ 同一只 ask 的两条腿**在场性可以不一致**,这是设计不是缺陷。端要渲一个徽标的话,读**两位的并**
969
+ 是允许的(那是端的呈现决定),但两位在本层必须**各自到货**。合成一位 = 拿一个量冒充另一个。
970
+
971
+ 🔴 **缺席绝不折成 `false`**(类型是 `true`,与 `governanceForced` / `inputHasBidi` 同族):
972
+ 缺席 = **没检出**(干净 / core 有界扫描没够着 / 列诞生前 park 的老行),端**禁**读成「已确认干净」
973
+ —— 那是对用户下一个证不出的断言。
974
+ 🔴 **披露位,不是清洗位;本包字节零改**:清洗会改掉即将被执行的那串字节(卡上显示的与真跑的不是
975
+ 同一个东西),比不披露更坏。显形(转义 / 高亮 / 加标记)归端。
976
+ 🔴 **永不参与 resume / gate / CAS**(上游同款纪律):展示与分诊用。
977
+
978
+ **上游三条读面与本包的覆盖**(如实记账):
979
+ | 上游读面 | 本包 |
980
+ |---|---|
981
+ | `GET /v1/approvals` 行(`PendingCheckpoint`) | ✅ **原样透传**(`startApprovalsFeed` 把 `list()` 的行原样交给宿主,零重铸)+ 本批补上「行 → 卡」重铸处 |
982
+ | `/v1/approvals/stream` 的 `pending` 帧(`ApprovalStreamEvent`) | ⭕ **零施工(如实记)** —— 本包的 feed 只把 stream 事件当「变了」信号,权威列表**恒来自 `list()`**(见 `approvalsFeed.ts` 头注),所以该帧的键不经本包任何投影 |
983
+ | inbox 行(`InboxRow`) | ⭕ **零施工(如实记)** —— 本包**没有** inbox 投影面(全仓零 `InboxRow` 引用);端若自接 inbox,直接读 SDK 形 |
984
+
985
+ **端的消费点**:审批卡的 bidi 提示 / 徽标。**cli 认领**:壳侧审批卡在下一批接。
986
+ **实现锚**:`src/hitl/toolApprovalWire.ts`(`ApprovalCardRequest.hasBidiControls` + `surfaceFsApprovalAndDecide` 的卡入参)。
987
+ **常驻门**:`scripts/run-durable-card-display-keys-test.mjs` **⑪ 段**(正控 + 缺席 + 非 true 四形负控 +
988
+ 两腿分键反钉)与 **⑦ 段富行键集普查**(上游 additive 加展示键当天红 —— 本批正是被它抓出来的)。
989
+
812
990
  ## §5 能力位 gate 义务与端口缺席语义
813
991
 
814
992
  ### 5a. 端口(`install*`):缺席语义与"漏装会静默坏掉什么"
@@ -1256,7 +1434,7 @@ CHANGELOG 0.29.0「已知局限」段与相应 JSDoc 都有成文。**别在读
1256
1434
 
1257
1435
  | ID | 级别 | 一句话现状 | 实现锚 | 端**今天**必须怎么办 |
1258
1436
  |---|---|---|---|---|
1259
- | **P-1** | med | `task_progress` 白名单**只 stamp 七键**(`taskId`/`name`/`usage`/`currentAction`/`workflowRunId`/`workflowAgentLabel`/`parentToolCallId`);SDK `events.d.ts` `task_progress` 臂上声明的 **`taskType` / `status` / `parentTaskId` 三键被整体剥掉**(`eventId` 也从不 stamp)。⚠️ 码里的理由注释只覆盖 `status` + EventIdentity(「service wire whitelist strips `status` + EventIdentity」)—— **`taskType` 与 `parentTaskId` 被剥掉,码里没有任何说法**,而 SDK 明写 `parentTaskId` 正是同一张 server 白名单**产出**的 | `src/adapter/downstream/eventToSdkMessage.ts` 的 `case 'task_progress'`;lane 启发式在 `src/adapt/arms.ts`(`recordWorkflowAgentTaskId` 三级门) | 别指望从一条 progress tick 上拿到委派类别(`taskType`)或嵌套归属(`parentTaskId`)。嵌套要从 `bg_notification.parentTaskId` 经 `src/fleet/fleetLedger.ts` 的 `recordBgParentRun` 恢复;workflow lane 归属走 `src/adapt/arms.ts` 的三级门。🔴 **别在端侧自己从别处补进投影**(那是绕过唯一投影口) |
1437
+ | **P-1** | med | `task_progress` 白名单**只 stamp 八键**(`taskId`/`name`/`usage`/`model`/`currentAction`/`workflowRunId`/`workflowAgentLabel`/`parentToolCallId`;`model` 是 0.47.0 补的,#363)。server 投影**发 13 键**,**仍被剥掉五键**:`taskType` / `status` / `parentTaskId`(本条原本的三件)+ `seq`(core #258 stop-cycle 代际号,tick 这条腿今天无消费方;SDK 7.3.0 连声明都没有)+ `eventId`(EventIdentity 的另一半,至今无消费方)。✅ **0.47.0 销掉的那半**:原文说「`taskType` 与 `parentTaskId` 被剥掉,码里没有任何说法」—— 现在该臂头注有**逐条族扫账**(放行 8 / 剥离 5,各带「谁没在读它」),且档与码由常驻门 `scripts/run-additive-key-passthrough-test.mjs` G1 段**双向对账** | `src/adapter/downstream/eventToSdkMessage.ts` 的 `case 'task_progress'`;lane 启发式在 `src/adapt/arms.ts`(`recordWorkflowAgentTaskId` 三级门) | 别指望从一条 progress tick 上拿到委派类别(`taskType`)或嵌套归属(`parentTaskId`)。嵌套要从 `bg_notification.parentTaskId` 经 `src/fleet/fleetLedger.ts` 的 `recordBgParentRun` 恢复;workflow lane 归属走 `src/adapt/arms.ts` 的三级门。🔴 **别在端侧自己从别处补进投影**(那是绕过唯一投影口);要透一位先走 §3d 的前置条件 |
1260
1438
  | **P-2** | med | 包内注释断言「**子代从不发终态 tick**」(`src/adapt/arms.ts` 的 `toolEndResultArm` ② MF-10 段逐字;`src/adapt/panelTasks.ts` 与补偿 T36/T34 复述),而 pin 的 SDK 逐字说 **server ≥1.258 会转发 core 的 SETTLE 终态 tick**(`"completed"`/`"failed"`)。**且这个矛盾自我维持** —— P-1 的白名单删掉了 `status`,所以终态 tick 就算上了 wire,在包内也**观测不到**。源码里**没有任何一处**把它记为假断言 | `src/adapt/arms.ts`(MF-10 段)、`src/adapt/panelTasks.ts`、`src/compensations.ts`(T36 `retireOn: 'W6(引擎为每条子代发终态 tick)后…'` / T34 `retireOn: null`) | 把四处防御 sweep 当成子代行 settle 的**唯一**机制;**不要**在端侧建「等子代终态 tick」的状态机 —— 包永远不会交给你一条 |
1261
1439
  | **P-3** | med | `approval_request`(design/172 流内审批开卡帧)在本包**零消费口** ⇒ `dropped('unsupported_arm')`(有痕、没人接) | `src/adapter/downstream/eventToSdkMessage.ts` 的 `case 'approval_request'` | 存量路径今天仍能决断(legacy `tool_approval` 腿在,同一只 ask 出两帧,顺序钉死「先 legacy、后 `approval_request`」)。🔴 **但降级路径不保证**(逐字):legacy respond 是 **live-only + same-replica**(错副本 404),重连时 pending 卡靠 `approval_request` preamble 对账 —— 「收到 legacy 帧后断线、按 `Last-Event-ID` 重连只再看到 `approval_request`」以及**非粘性多副本部署**这两条路上,今天只能等窗口到期 → park/deny。**多副本 worker 上这是已知缺口不是环境问题** |
1262
1440
  | **P-4** | med | **`approval_revoke` 在 SDK union 里连成员都没有**(SDK 顶注:known asymmetry,`Registering it is an open item for the next batch`)⇒ 本包不可能有 case ⇒ 运行期落 `dropped('unknown_arm')`;审批链也看不见它(`isToolApprovalFrame` 只认两帧)。全仓 `grep -rn "revoke"` = **0** | `src/adapter/downstream/eventToSdkMessage.ts` 的 `default` 臂;`src/hitl/toolApprovalWire.ts` 的 `isToolApprovalFrame` | 🔴 **引擎撤卡时本包不会替你撤那张卡** —— 被撤的 ask 会一直留在屏上,直到它自己的 5 分钟 TTL / deny 路径触发。要 revoke 语义的端只能自己接 raw named-SSE 腿并撤自己的卡(`unknown_arm` 的 drop 至少留了一行痕) |
@@ -1281,6 +1459,10 @@ CHANGELOG 0.29.0「已知局限」段与相应 JSDoc 都有成文。**别在读
1281
1459
  | **P-36** | low | **中断文案归一只覆盖 `Operation aborted` 这一串**(#323 症状②,0.43.0;clay 裁定的**明确边界**,不是漏做):同一次用户中断里,**执行前被连坐**的旁观者拿的是 core 的另一串 `operation aborted before execution`,`interrupted_never_started` 族又是第三种;这两族**刻意不并入**中断改写臂 —— core 显式拒绝合并两串向([4973]),两串各承真语义(`Operation aborted` = 执行中被中止 / 该串 = 从未执行),而且它们**各有自己的文案与折叠腿**(端侧的 interrupted-batch 折叠 + `TOOL_END_INTERRUPTED_CODES` 词表)。⇒ 纯取消批里,那两族的 tool_end 今天仍按各自原文呈现 | `src/hitl/frameRouter.ts`(`isUserInterruptRewritable` 的判据①头注 + `isEngineAbortToolEnd` 的两串族说明)· 负控 = `scripts/run-hitl-gate-honesty-test.mjs` F13-d | 端**不要**假定「用户中断 ⇒ 这一批 tool_end 文案全是 CC 中断串」;两族按各自既有腿归因(机读码优先,文案兜底)。要不要并成一形是**语义裁定**不是实现细节,需 clay 先裁 |
1282
1460
  | **P-34** | low | **编辑臂预检判官在浏览器 lane 结构上装不了**(#225 / [5076],0.42.0):`precheckEditedRuleText` 的唯一合法实参是 core 5.57.0 那只纯函数,而 `@sema-agent/core` 的 barrel 值级拉 `node:crypto`/`node:fs`/`node:path` —— 本包**不能**做 value 级 re-export(portability 门 `EXPECTED_PACKAGES_INDEX` 等值门 + esbuild 浏览器腿双重否决,施工时实打验证) | `src/hitl/editedRuleTextPrecheck.ts`(模块头注的「为什么是端口注入」段) | Node 宿主(TUI / desktop 主进程)装上即得内联即时校验;**浏览器 lane 留缺席走「提交后才知道」的往返形**,这是设计不是漏装。🔴 缺席**不可**据以判断部署形态 |
1283
1461
 
1462
+ | **P-41** | low | 🆕 **decide 的 `currentPending` 指路键今天在标准 SDK 路径上恒缺席**(0.47.0 件②,如实登记的射程边界不是缺陷):server 的 409 `approval_stale` 臂**只在调用方回显 `checkpointToken` 时触发**(engine 7.52.1 `http/server.js` 的 `resumeCheckpoint`,`if (binding?.checkpointToken && …)` 真字节),而 sdk 的 `ApprovalDecision` 自 1.0.0 起**刻意删掉**了那一位 ⇒ 经 SDK client 的 decide 拿不到这枚 409。另:ask 腿(`GateOutcome.currentPending`)**包内无消费方** —— `GateOutcome` 是包内型、决断结局不出包;它在场的理由是**同形存量清剿**(两条 decide 失败腿一次改齐) | `src/hitl/hitlBridge.ts`(`readDecideCurrentPending`)、`src/hitl/toolApprovalWire.ts`、`src/hitl/parkResolver.ts` | 按 §4f 接:读得到就一跳重定位,**缺席时退回重拉 `GET /v1/approvals`**,绝不把缺席读成「没有别的 pending 了」。自己注入传输层(非 SDK client)的宿主今天就拿得到 |
1463
+
1464
+ | **P-43** | med(**存量、非本批引入** —— 0.42.0 基线上逐字相同) @cli @web @desktop | 🆕 **accept-session 回退臂的错误分类比它自己的注释宽**(#363 异源复审 [high] 的**未收窄那一半**,如实登记):`allowSession` 腿先发 `approve + remember:'session'`,失败时回退一发纯 approve;那条 catch 的注释写的是「**只**兜『老 server 不识别 remember ⇒ 400 未知键』这一形」,而实际形是 **catch-all 减去三条具名再抛**(`HitlSafetyError` / `DecideTransportRetryExhaustedError` / 0.47.0 新加的『拒体带 `currentPending`』)。⇒ 一个 **404 / 5xx / 宿主自抛的无 status 错误**今天仍会被当成「老 server 不识别 remember」并**自动重发**一次纯 approve。收窄成「只认 400」是**行为改动**,不属于 0.47.0 这个 additive 批的射程 | `src/hitl/toolApprovalWire.ts`(`case 'allow'` 的内层 catch) | 端今天不需要做什么(两发都是 approve,不构成跨门的 double-act);**属主批**:下一个愿意改老引擎兼容腿宽度的批把它收窄成精确的 legacy-400,并同批给回退臂补正控/负控 |
1465
+
1284
1466
  ### 7c. 多会话(sessionKey)面在册局限 —— 多会话端**接之前必读**
1285
1467
 
1286
1468
  | ID | 级别 | 一句话现状 | 实现锚 | 端**今天**必须怎么办 |
@@ -1316,6 +1498,8 @@ CHANGELOG 0.29.0「已知局限」段与相应 JSDoc 都有成文。**别在读
1316
1498
  | **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()` 谓词) |
1317
1499
  | **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 订正本条初稿的「同形不可分」;**0.43.0 起是三分不是两分**,见下):fail-soft 这条是 `flushHeld()` 吐出的**毒化帧**,`output` 逐字是 `ENGINE_ABORT_TOOL_RESULT`(`'Operation aborted'`);真 deny 走 `frameRouter` 的 `denied-call` / `deny-stamp-next` 臂,`output` 被改写成 `HITL_REJECT_MESSAGE`(CC `REJECT_MESSAGE` 逐字)。🆕 **0.43.0 新增第三形(#323 症状②)**:fail-soft 的原因若是**用户在门卡上中断**(`GateOutcome.kind === 'aborted'`,= 用户按 Esc/Ctrl+C),同一批毒化帧的 `output` 被归一成 `HITL_INTERRUPT_MESSAGE_FOR_TOOL_USE`(CC `[Request interrupted by user for tool use]` 逐字,公面导出)——`isError` / `errorCode`(含 `gate.parked`)/ `_sema_collateral_abort` 等机读位**一个都不改**。⇒ 端做归因的 `output` 三分:`'Operation aborted'` = 非中断原因的 fail-soft(卡面不可用 / no_pending / 传输失败)· CC 中断串 = 用户中断 · CC REJECT 串 = 用户真拒绝。端**不要**再假定「fail-soft ⇒ 必是引擎原文」;② **续流 / 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 板提 |
1318
1500
 
1501
+ | **P-42** | low @cli @web @desktop | 🆕 **Esc halt 只上收了判定,发射面仍在各端**(0.47.0 件③,刻意的分工不是半成品):`planInteractiveHalt` 给判据与升级码闭集,`POST /v1/runs/:id/interrupt` 的**发射**(以及 `?session=` 供给、超时窗、台账、留痕)仍归端。cli 侧那条「裸 fetch 直拨 interrupt」的网络面豁免,**退役条件就是端接上这个口子**(壳换装不在 0.47.0 批内) | `src/interactiveHalt.ts`;§10 | 按 §10b 的分支表接:判定用本包,发射用端自己的传输腿;🔴 halt 必须排在撕 SSE **之前**(§10c 第 1 条) |
1502
+
1319
1503
  ### 7e. 缺口的共同形状(值得单独说)
1320
1504
 
1321
1505
  **P-1 / P-2 / P-3 / P-4 / P-5 / P-6 / P-7 是同一类**:上游(server / SDK)已经把材料铸到 wire 上了,
@@ -1466,3 +1650,146 @@ reason 里写明「枚举器盲区形」。已知两形:
1466
1650
  1. 公面有没有**新导出 / 撤回**(→ §2 域图 + 基线 diff);
1467
1651
  2. 有没有**新回执键或缺席语义变更**(→ §4 三列表;这一类改动最容易被当成 additive 而漏接);
1468
1652
  3. 有没有**新端口 / 新能力位 gate**(→ §5 + §8 checklist)。
1653
+
1654
+ ---
1655
+
1656
+ ## §10 🆕 上行停止动词的判定层 —— 交互 Esc 的 halt(0.47.0)
1657
+
1658
+ 「用户按 **Esc** ⇒ 先发 **turn 级** halt;升级成 **run 级** cancel 恰有**两格**——
1659
+ ① 引擎自己回了升级闭集里的 **409**(它在说「这里没有在飞 turn 可切,run 级停止请用 cancel」),
1660
+ 或 ② 这一发**连判决都没拿到**(传输失败/超时/未武装)**且**屏上确实挂着审批卡;其余一律不升级」
1661
+ —— 三端(TUI / desktop / web)都会 Esc、都会撞同一个 parked 格,所以判定归本包。
1662
+ **发射**(裸 fetch / SDK verb)、台账、留痕、UI 反馈仍归各端。
1663
+ ⚠️ **别把①漏掉**:只按「拿不到判决且 parked 才升级」接线,会漏掉**引擎明确指路**那一格 ——
1664
+ parked run 的会话锁不放,用户下一条消息照样撞「Session busy」(正是本判定要消灭的病)。
1665
+
1666
+ ### 10a. 两个动词各自唯一能做到的格(**cancel 绝不删**)
1667
+
1668
+ | 动词 | 语义 | 它唯一能做到的事 |
1669
+ |---|---|---|
1670
+ | `POST /v1/runs/:id/interrupt`(**bare 体 `{}`**) | turn 级「切 + 停」(core `stream.halt()`) | 切掉在飞那一轮,run 在边界上终局、**同 session 下一 submit 照常续**。这是 Esc 的正题 |
1671
+ | `POST /v1/runs/:id/cancel` | run 级终局 | **唯一**能把一条停在审批门上的 `suspended`/`needs_review` run 就地终态化、把会话锁放开的动词(server [868] 语义) |
1672
+
1673
+ ⇒ 无条件把 cancel 换成 interrupt = 审批卡挂着按 Esc 的主场景当场回归成「Session busy」病;
1674
+ 无条件补 cancel = 同一条 run 上别的在飞工具(后台 bash 等)被**连坐**拆掉。本判定就是这两条之间那道闸。
1675
+ ⚠️ 带 `text` 的 interrupt 是 **steer** 语义(切 + 转向),**Esc 不用**;未知键的对象体 server 400
1676
+ `request.body_shape`(它刻意不把 malformed 折成换动词)。
1677
+
1678
+ ### 10b. 端怎么接
1679
+
1680
+ ```ts
1681
+ import { planInteractiveHalt, RUN_LEVEL_STOP_ERROR_CODES } from '@sema-agent/client-core'
1682
+
1683
+ // ① 还没打过 ⇒ 恒判 interrupt(这一格刻意不看 parked)
1684
+ planInteractiveHalt({}) // → { action: 'interrupt', reason: 'first-shot' }
1685
+ // ② 端自己发射,把结局折成 InteractiveHaltInterruptOutcome 再问第二次
1686
+ planInteractiveHalt({ parked, interruptOutcome })
1687
+ // → { action: 'escalate-cancel' | 'none', reason: <闭集机读词> }
1688
+ ```
1689
+
1690
+ | `interruptOutcome` | `parked` | 判决 | `reason` |
1691
+ |---|---|---|---|
1692
+ | 缺席 / `null` | 任意 | `interrupt` | `first-shot` |
1693
+ | `{kind:'halted'}` | 任意 | `none` | `halted` |
1694
+ | `{kind:'refused', status:409, errorCode ∈ 闭集}` | 任意 | `escalate-cancel` | `engine-says-run-level` |
1695
+ | `{kind:'refused', 其余}` | 任意 | `none` | `refused-no-escalation` |
1696
+ | `{kind:'transport'}` / `{kind:'unarmed'}` | `true` | `escalate-cancel` | `no-verdict-on-parked-card` |
1697
+ | `{kind:'transport'}` / `{kind:'unarmed'}` | 其余 | `none` | `no-verdict-not-parked` |
1698
+ | 认不得的形 | 任意 | `none` | `unknown-outcome` |
1699
+
1700
+ 🔴 **不对称是刻意的**:判**不**升级 = 用户退回「Session busy」卡再选一次(**可恢复**);判**错**升级
1701
+ = 拆掉一条其实还活着的 run 并连坐它身上的在飞工具(**不可恢复**)。凡「证不出来」一律落不升级侧。
1702
+ 🔴 **升级码闭集恰一员** `interrupt.nothing_in_flight`(`RUN_LEVEL_STOP_ERROR_CODES`)。
1703
+ `interrupt.not_held` **刻意在外** —— 它的语义是「**本副本**手上没有可切的 live turn face」,server
1704
+ 自己的原话之一是「run is live on **another replica**」:那不证明全局没有在飞 turn。
1705
+ `steering.not_running` 也在外(run 已终局,补枪是纯噪声)。
1706
+ 🔴 **码与 409 是合取**:只认码不认状态 ⇒ 一只 5xx 只要正文里带上那个码就能骗来一发破坏性 cancel。
1707
+ 🔴 `parked` 只在**没有判决**那两格被读,且**只认严格 `true`**;引擎给了判决时判决说了算(两个方向都是)。
1708
+
1709
+ ### 10c. 端仍然要自己做的三件(本包**不做**)
1710
+
1711
+ 1. **发射与顺序**:🔴 halt 必须排在「撕 SSE」**之前**。交互车道零 `x-detach-on-disconnect`,先撕流 =
1712
+ server 按断连语义当场收尾那条 run,随后落地的 interrupt 只会拿到 409 `steering.not_running`
1713
+ (cli L-11 真机实测:同步撕流形每一轮都是它)。
1714
+ 2. **不挡 UI**:Esc 的本地即时反馈一拍都不许被网络往返推迟;判定本身是同步纯函数,发射走
1715
+ fire-and-forget + 端侧本地兜底窗。
1716
+ 3. **台账与留痕**:「这条 run 被怎么处理了」(切一轮 vs 拆整条)是端的诊断面;`reason` 是**机读闭集词**,
1717
+ 人话文案归端(别把它当展示串)。
1718
+
1719
+ **实现锚**:`src/interactiveHalt.ts`;常驻门 `scripts/run-esc-halt-plan-test.mjs`。
1720
+
1721
+ ## §11 🆕 会话记忆姿态读面(S-53,0.48.0;server ≥7.53 / core 7.0.2 #511 件1)
1722
+
1723
+ `GET /v1/sessions/:id/memory-status` 的**三端公共读面**。本包提供**纯判定 + 薄封装**,
1724
+ IO 归宿主注入(`MemoryStatusClientLike`,与 `HitlClientLike` 同款 duck-type);
1725
+ 本件**一句面向用户的话都不铸** —— 措辞与是否上屏归端。
1726
+
1727
+ ### 11a. 为什么这件在库里(两处判定,三端各写一遍必然各错一遍)
1728
+
1729
+ **① 同 status 不同码。** 本路由的 **404 有两个互不相干的含义**:
1730
+
1731
+ | errorCode | 含义 | 端的处置 |
1732
+ |---|---|---|
1733
+ | `not_found.session` | 会话未知**或非本 principal 所有**(server 反枚举:两者同码同串,判不出更细的,**别猜**) | 会话面报「找不到这条会话」 |
1734
+ | `not_found.route` | 支持区间内 **<7.53 的老 server 没有这条路由**,答的是通用回退 | 按「**面不存在**」降级(与 501 同处置) |
1735
+
1736
+ 🔴 按 **status** 分诊必然把「你的部署没这个面」说成「你这个会话不存在」——
1737
+ 判据只能锚 `errorCode`([anchor-on-the-deciding-quantity])。
1738
+ 🔴 **无码的 404 落 `failed`(如实说判不出),绝不挑一个猜**:两个码的处置相反,猜错任一向都是
1739
+ 一句用户会照着去排错的假话。**501 才允许无码兜底**(本路由两条 501 臂都是「面不在/没开」,无歧义)。
1740
+ 🔴 `capability.*`(换部署形态)与 `feature.*`(叫管理员开开关)**分列不合流** ——
1741
+ SDK 顶注逐字:同为 501 而**处置相反**。
1742
+
1743
+ **② 五键缺席语义逐键不同。** `optOutSource` / `lastCaptureAt` 在**健康会话**上就合法缺席;
1744
+ 零历史会话的真形 = `{captureOptedOut:false, committedCount:0, foldedCount:0}`,**没有任何降级**。
1745
+ ⇒ 把缺席一律读成「没有 / 关着 / 0」就是对用户下一个证不出的断言。
1746
+
1747
+ ### 11b. 端怎么接
1748
+
1749
+ ```ts
1750
+ import { readSessionMemoryStatus, readCaptureOptOut, readLastCapture } from '@sema-agent/client-core'
1751
+
1752
+ const v = await readSessionMemoryStatus(client, engineCapturedSessionId, { signal })
1753
+ switch (v.kind) {
1754
+ case 'ok': /* readCaptureOptOut(v.facts) / readLastCapture(v.facts) */ break
1755
+ case 'unsupported': /* 🔴 别提供这个入口(不是报错,是诚实的能力缺席);reason 分 capability/route/feature */ break
1756
+ case 'not_found': /* 会话未知或非属主 */ break
1757
+ case 'failed': /* 分类不明,如实说;v.error 是原始抛出物 */ break
1758
+ }
1759
+ ```
1760
+
1761
+ 两个**合读器**(缺席语义就藏在这两格里,端**不要**自己读裸键):
1762
+
1763
+ | 读法 | 三态 | 判别材料 |
1764
+ |---|---|---|
1765
+ | `readCaptureOptOut` | `opted_out` / `active` / `indeterminate` | `captureOptedOut` × `optOutSource` **完整真值表**(不是「看布尔位 + 特判 fault」)。契约把两键**成对**定死,只有两个组合有定义:`true`×`"record"` ⇒ `opted_out`;`false`× **缺席** ⇒ `active`;缺席×`"fault"`(记录店失败)⇒ `indeterminate`。🔴 **其余组合在契约上不存在**(`false`×`record` / `true`× 缺席 / `true`×`fault` …),只可能来自版本斜差、畸形 200 体或中间层改写 ⇒ 一律 `indeterminate`。这是**隐私姿态**断言,两个方向都危险:读成 `active` 是向用户断言「你的对话正在被记忆」,读成 `opted_out` 是反向的同一种谎 |
1766
+ | `readLastCapture` | `known` / `none` / `indeterminate` | 🔴 判别材料是**另一键** `committedCount`,不是 `lastCaptureAt` 本身:本键缺席**同时**覆盖「台账不可读」与「真的没有贡献」两形 ⇒ **单读它判不出任何东西**。`committedCount === 0`(台账可读、真零)+ 本键缺席 ⇒ `none`;`committedCount` 缺席 ⇒ `indeterminate`。🔴 `known` 的条件是**合取**(时刻在场 ∧ 台账可读 ∧ `committedCount > 0`):两者本是**同一次台账读**,`{committedCount:0, lastCaptureAt:X}` 这种对不上的形是矛盾 ⇒ `indeterminate`,不产出确定时间 |
1767
+
1768
+ ### 11c. 端必读的三条
1769
+
1770
+ 1. 🔴 **`sessionId` 必须取引擎捕获值**(壳从 wire 上拿到的那个 id),不是宿主自铸/自选的串。
1771
+ server 侧记录与台账按**裸 sessionId** 键控且**活过会话** ⇒ 喂一个**被回收**的 id 会读到
1772
+ **上一代**的计数 / opt-out(元数据,无内容字节;server 侧成文的跨代注意)。
1773
+ 本包对空串**直接落 `failed` 且不发请求** —— 那一发必然是对某个不属于本会话的东西提问。
1774
+ 2. 🔴 **没有能力位**([5785]/[5786] 未定位名):直接调用,**501 就是本部署无此面的诚实答案**。
1775
+ 别为它去探一个不存在的 caps 位。
1776
+ 3. 🔴 **畸形键在本层降缺席、不采信**:降键的后果是两个合读器答 `indeterminate`(「不知道」),
1777
+ 采信坏形的后果是拿它当真值渲。两害相权,如实不知道。
1778
+ ⚠️ 但 **200 体整体非对象 ⇒ `failed`**,不洗成「全键缺席」的假 `ok` —— 那会把一次装配缺陷
1779
+ 渲成一个看起来很诚实的 `indeterminate`。
1780
+ 4. 🔴 **身份绑定:回声的 `sessionId` 必须与请求值逐字相等,否则整只落 `failed`**
1781
+ (`SessionMemoryStatusResponse` 契约上它是必填回显 ⇒ 缺席/非串同样是坏形)。
1782
+ 收下一个不相等的回声 = 把**另一条会话**的记忆元数据(计数 / opt-out 姿态)呈现在当前会话面板上
1783
+ —— 缓存错配、中间层串台、依赖故障都造得出。上游对同一件事的纪律是**宁缺席不串台**,本层照办。
1784
+ 端**不需要**自己再核一遍这一位。
1785
+ 5. 🔴 **「永不抛」对畸形宿主也成立,成功路与失败路都算**:本函数吃的是 duck-typed 注入 client,
1786
+ 它可以回一个带**抛错 getter** 的对象或敌意 `Proxy`,也可以**用**这种对象作拒因。
1787
+ 两条路都设了防:归一化整段在 `try` 内;`classifyMemoryStatusFailure` **自己**取属性时也带保护
1788
+ (它是在 `catch` **块内**被调用的 —— `catch` 里抛出的异常不会再被同一个 `try` 接住,分类器一抛
1789
+ 就会击穿这句承诺)。⇒ 调用点**不需要**给它套 `try`,顶注那句承诺是可依赖的。
1790
+ ⚠️ `classifyMemoryStatusFailure` 单独调用时同样永不抛,且**原抛出物原样带出**(端要看得到真因)。
1791
+
1792
+ **端的消费点**:cli [5902] 研判推荐挂 **`/status` 面**(会话级披露,不是转录物)。
1793
+ **cli 认领**:壳侧接点在下一批。
1794
+ **实现锚**:`src/sessionMemoryStatus.ts`。
1795
+ **常驻门**:`scripts/run-session-memory-status-test.mjs`。
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sema-agent/client-core",
3
- "version": "0.46.0",
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).",
3
+ "version": "0.48.0",
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. Renamed from @sema-agent/wire-cc-adapter (0.1.x).",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "./dist/index.js",
@@ -18,7 +18,7 @@
18
18
  "README.md",
19
19
  "CHANGELOG.md",
20
20
  "docs/INTEGRATION-CLIENTS.md",
21
- "docs/REFACTOR-LEDGER.md"
21
+ "LICENSE"
22
22
  ],
23
23
  "sideEffects": false,
24
24
  "scripts": {
@@ -31,12 +31,12 @@
31
31
  },
32
32
  "peerDependencies": {
33
33
  "@sema-agent/agent-types": ">=0.2.0",
34
- "@sema-agent/sdk": ">=7.2.0"
34
+ "@sema-agent/sdk": ">=7.4.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@sema-agent/agent-types": "^0.2.0",
38
- "@sema-agent/core": "^5.57.0",
39
- "@sema-agent/sdk": "^7.2.0",
38
+ "@sema-agent/core": "^7.1.0",
39
+ "@sema-agent/sdk": "^7.4.0",
40
40
  "esbuild": "^0.27.4",
41
41
  "typescript": "^6.0.2"
42
42
  }