@sema-agent/client-core 0.44.0 → 0.45.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.
@@ -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
- | 公开导出面 | **783** 个运行期符号(+ 40 个测试钩;= 未发 server 7.48.0 消费批的值,npm `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
+ | 公开导出面 | **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` —— **别手抄进别处,以该文件为准** |
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`(**783** 项)。
104
+ > 全集真源 = `scripts/public-export-baseline.json` 的 `names`(**787** 项)。
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
- 实测:783 项 **100% 是运行期导出,零 type-only**。
114
+ 实测:787 项 **100% 是运行期导出,零 type-only**。
115
115
 
116
116
  **推论(端必须知道)**:
117
117
  - barrel 导出的**类型**面比 707 大得多,且**不被这道门看守** —— `AdapterContext` / `SeamEvent` /
@@ -120,19 +120,19 @@ SDK 核对「本包 import 的每个值级符号仍然导出」「`TaskStats.cos
120
120
  端依赖这些类型是合法的,但**不要**拿基线 diff 当"类型面没变"的证据。
121
121
  - `src/agentSession/contract.ts` 对基线贡献 **0** 项(纯类型模块,`export *` 在 dist 里是空转发)。
122
122
 
123
- 783 项的内部构成(帮助端估读表大小):**232** 项是 `SCREAMING_SNAKE` 常量数据表/词汇表
123
+ 787 项的内部构成(帮助端估读表大小):**232** 项是 `SCREAMING_SNAKE` 常量数据表/词汇表
124
124
  (矩阵、键集、env 名、锚串)而非可调用物;**5** 项是 PascalCase 运行期值
125
125
  (`ControlRouter` / `ControlSafetyError` / `HitlBridge` / `HitlSafetyError` / `DecideTransportRetryExhaustedError`);
126
- **39** 项是 `*For(sessionKey, …)` 的 per-session 变体(§6;其中 `engineNamespaceKeyFor` 是命名巧合 —— 参数是 baseUrl 不是 sessionKey,见域 14)。
126
+ **41** 项是 `*For(sessionKey, …)` 的 per-session 变体(§6;其中 `engineNamespaceKeyFor` 是命名巧合 —— 参数是 baseUrl 不是 sessionKey,见域 14)。
127
127
 
128
- ### 2b. 域图(16 域,逐域计数之和 = 783)
128
+ ### 2b. 域图(16 域,逐域计数之和 = 787)
129
129
 
130
130
  | # | 域 | 名数 | 承重导出 | 用途 | 实现锚 |
131
131
  |---|---|---|---|---|---|
132
132
  | 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
133
  | 2 | **seam 公共契约** | 2(其余为 type-only) | `CHROME_ARMS` · `deriveTranscriptId` | 公共词汇 + **id 确定性不变量**(同一条流重放 ⇒ 同一串 id)。`CHROME_ARMS` = 端「我要消费哪些 chrome 臂」的对照清单 | `src/seam.ts` |
134
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 规则侧) |
135
- | 4 | **子代 wire + 面板侧信道台账** | 82 | `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` |
135
+ | 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
136
  | 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
137
  | 6 | **请求装配(上行唯一构造口)** | 8 | `buildTaskRequest` · `REQUEST_FIELD_MATRIX` · `unregisteredRequestKeys` · `applyLiveRequestDefaults` · `taskNotificationToPrintFrame` | 两条车道(`interactive`/`print`)出站请求的**唯一**构造器;`unregisteredRequestKeys` 是可执行门 —— 端偷带一个未登记键上 wire 就红 | `src/request/taskRequest.ts`、`src/request/printNotification.ts` |
138
138
  | 7 | **通知与 outstanding 台账** | 41 | `installNotificationQueuePort` · `normalizeTaskNotification` · `taskNotificationDedupKeyFromWire` · `registerOutstandingBgTask` / `registerOutstandingWorkflowRun` · `notificationQueuePortMisses` · `subscribeOutstandingWorkflows` · `outstandingDeliverableWorkflowCount` | `task_notification` 归一 + 去重 + 投递进宿主命令队列的**一把闸**;`outstandingDeliverableWorkflowCount()` 是 headless `-p` 的**退出门** | `src/notifications.ts`(11 个 module 台账) |
@@ -143,7 +143,7 @@ SDK 核对「本包 import 的每个值级符号仍然导出」「`TaskStats.cos
143
143
  | 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
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**,纯类型 + 常量 + 纯谓词)。🔴 **证据等级标注(0.42.0,test [5087] 的「语料**种类**缺口」/ cli [5088] 认领件)**:该文件里所有以「CC 如何如何」为形的断言(`212 methods` / `854-channel census` / 方法名逐字保留 / `fQe` 逐字段对照 / 一切 `.vite/build/index.chunk-*.js` 坐标)**证据等级 = 桌面 unpack,本地语料库不可复验** —— 本仓手边可复验的参照语料**只覆盖终端 CLI 形态**的静态产物,拿它去 grep 桌面壳里的符号只会零命中,而零命中在这里**既不证真也不证伪**。复核这些断言**不得**拿本仓语料当反证 |
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
- | 15 | **控制面与传输** | 74 | `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` |
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` |
147
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` / 前缀谓词一律是**识别表**,
@@ -1072,12 +1072,13 @@ HITL 面有 `hitlHostSurfaceFor` —— 见 §5a 的 (a) 表;队列口没有,登
1072
1072
  | 槽位 | `*For` API | 实现锚 |
1073
1073
  |---|---|---|
1074
1074
  | 宿主端口(settings/fs/timers/session/log/probe) | `installHostFor` · `hostSettingsFor` · `hostFsFor` · `hostTimersFor` · `hostSessionFor` · `hostPortMissesFor` | `src/host.ts` |
1075
+ | 会话参数派生(`?session=`) | `engineSessionParamFor` | `src/engineSessionParam.ts` |
1075
1076
  | 审批卡口 | `installApprovalCardPortFor` · `approvalCardPortFor` · `hasApprovalCardPortFor` · `approvalCardPortMissesFor` | `src/hitl/toolApprovalWire.ts` |
1076
1077
  | HITL 宿主面 | `installHitlHostSurfaceFor` · `hitlHostSurfaceFor` · `hitlHostSurfaceMissesFor` | `src/hitl/hitlHostSurface.ts` |
1077
1078
  | 问答 overlay | `publishQuestionFrameFor` · `hasQuestionOverlayFor` · `onQuestionFrameFor` | `src/liveQuestionStore.ts` |
1078
1079
  | 呈现台账(决断卡首见/复见) | `registerArmedGateFor` · `wasGateArmedFor` · `clearArmedGateFor` · `registerArmedGateFromQuestionIdFor` | `src/hitl/armedGateRegistry.ts` |
1079
1080
  | wire 目标 | `installEngineWireTargetFor` · `engineWireTargetFor` | `src/engineWireTarget.ts` |
1080
- | fleet 台账 | `isFleetSessionScopedFor` · `readEngineActiveBgTasksFor` · `clearAllRetainedFleetRowsFor` | `src/fleet/fleetLedger.ts` |
1081
+ | fleet 台账 | `isFleetSessionScopedFor` · `readEngineActiveBgTasksFor` · `clearAllRetainedFleetRowsFor` · 🆕 `ledger.issueStream()` / `ledger.issueSnapshot()`(**keyed 开流参数 + 帧入口的唯一正身**,§6d) | `src/fleet/fleetLedger.ts` |
1081
1082
  | fleet 面板投影 | `projectFleetAgentRowsFor` · `resetFleetAgentPanelProjectionFor` · `fleetAgentProjectionSizeFor` | `src/fleetAgentPanelProjection.ts` |
1082
1083
  | 归属判据 | `pendingRowIsOwnedByThisSession({… sessionKey})` | `src/hitl/parkOwnership.ts` |
1083
1084
  | plan 重开 | `reopenPlanReviewCard(taskId, { sessionKey })`(全链 `*For` 路由:overlay/帧/台账) | `src/hitl/planReviewWire.ts` |
@@ -1095,7 +1096,7 @@ HITL 面有 `hitlHostSurfaceFor` —— 见 §5a 的 (a) 表;队列口没有,登
1095
1096
  (fail-closed,跨会话不认领)。desktop 的 `sessionForEngineId` 单腿判定应收敛到本判据,
1096
1097
  **传 `sessionKey` + 自注入 `isOwnRun`**。
1097
1098
 
1098
- #### `onBgNotificationAccepted` 的 `evidence` 参数(0.32.0 未发布,additive 第三参)
1099
+ #### `onBgNotificationAccepted` 的 `evidence` 参数(0.32.0 起,additive 第三参)
1099
1100
 
1100
1101
  `createFleetLedger` 的 `FleetLedgerHooks.onBgNotificationAccepted(n, rowIds, evidence)` 在 `bg_notification`
1101
1102
  帧**通过归属判别之后**回调(被隔离丢弃/畸形早退的不回调)。0.32.0 起补第三参 `evidence`
@@ -1104,27 +1105,137 @@ HITL 面有 `hitlHostSurfaceFor` —— 见 §5a 的 (a) 表;队列口没有,登
1104
1105
 
1105
1106
  🔴 **每个词命名的是「放行臂」,不是「归属结论」**;三档强度,中间两格**不同档**。
1106
1107
 
1107
- 🔴 **前两格的会话级读法带一个前提:喂给这个 ledger fleet 流,是按这个 ledger 的会话开的。**
1108
- ledger **自己不开流**(帧体归库、连接归端),无从校验;而本包的开流参数 `fleetStreamOptions()` /
1109
- `fleetSnapshotOptions()` 是 **module 级、只认默认槽会话**,连 `sessionKey` 都拿不到。
1110
- **单会话宿主**(cli 及今天的三端)前提恒成立;⇒ **keyed 多会话宿主**必须自己保证流与 ledger 同会话,
1111
- 否则前两格会替**别的会话**作证 —— 在册局限 **P-31**(§7c)。
1108
+ 🔴 **可信谓词按臂拆分**(0.45.0 / design/285;此前是「非默认 `sessionKey` 一律封顶」那一刀切)。
1109
+ 两条会话级臂**不同命,因为自校能力不同**:
1110
+
1111
+ - `own_root` 是**内容自校臂** —— 拿通知里的 `rootSessionId` 与**本 ledger 的会话锚**逐值比对。
1112
+ 宿主就算把 A 会话的流喂进 B 的 ingress,A 的通知带 `rootSessionId = sid-A ≠ sid-B` ⇒ 值不等,自动拒。
1113
+ ⇒ 锚一换成 per-key,这一臂就**真的可信**:默认槽恒可信;keyed ledger 上帧经
1114
+ `ledger.issueStream()` 的 ingress 到达即可信,走兼容入口 `ledger.applyFrame` 仍封顶(没有通道证据)。
1115
+ - `server_fail_closed` **零内容自校** —— 只读 meta 两个布尔位,不读任何会话、不比对任何值,而
1116
+ **任何**一条 session-bound 流的 meta 都长这样。跨-key 误接流时它会替别的会话作证,而 ingress
1117
+ 证明不了帧的来源连接。⇒ **keyed ledger 上这一臂恒封顶**,直到上游在 fleet `meta` 帧上回显本连接的
1118
+ session id、本端校验相等为止 —— 在册局限 **P-31** 的**残余一族**(§7c;三格,见下),不许读成「已闭合」。
1119
+ ⚠️ **同一个残余另有两格**:①**行帧车道** —— `meta.sessionScoped === true` 让行级 own 判别整条让位
1120
+ (`ownTaskRow`/`ownWorkflowRow`,[1510] 既有裁定);②**`hook_notice` 分发** —— `onHookNotice` 的第二参
1121
+ 就是那一位,且回调不带锚也不带 evidence。两格同样是零内容自校的连接级信任位。本批**都不改行为**
1122
+ (非本批引入 / 根治同为上游 meta 回显 / 擅自封顶会让已正确接线的 keyed 宿主的行凭空消失),按残余
1123
+ 登记在 P-31 行上。端在 keyed 上做有副作用的事时,**三条通道都要按这条残余自裁**。
1124
+
1125
+ 🔴 **keyed 多会话宿主的开流姿势**(唯一正身):`fleetStreamOptions()` / `fleetSnapshotOptions()` 是
1126
+ **module 级、只认默认槽会话**的形,对单会话宿主正确、对 keyed 宿主错;本包**刻意不给它们加 `*For`
1127
+ 兄弟**(一个 module 级 keyed helper 与 ledger 的绑定状态互不可见,会造出「用 B 参数开了 B 流而 ledger
1128
+ 显示未绑定」「调了 helper 却把返回值丢掉而 ledger 显示已绑定」两条互斥假象)。keyed 宿主一律:
1129
+
1130
+ ```ts
1131
+ const ledger = createFleetLedger(hooks, { sessionKey })
1132
+ const ingress = ledger.issueStream() // 开流那一刻一次性捕获 {sessionKey, session, epoch}
1133
+ const stream = client.fleet.stream({ ...ingress.options, signal })
1134
+ for await (const frame of stream) ingress.applyFrame(frame) // 🔴 帧只经 ingress 进入 ledger
1135
+ ingress.close() // 收流;此后的迟到帧丢弃并计 droppedStaleIngress
1136
+ ```
1137
+
1138
+ 重连 = 再 `issueStream()`(epoch +1,旧 ingress 随即作废)。可观察面 = `status()` 的 `sessionAnchor` /
1139
+ `sessionAnchorEpoch`(**`0` = 不在流模式**:从未 `issueStream()`,或当代 stream 已 `close()`)/
1140
+ `droppedStaleIngress`。`sessionAnchor` 是**三态**:①有 live stream ⇒ 它开流那一刻的捕获;②无流但账里
1141
+ **有内容** ⇒ 内容归属锚(绝不改报一个还没被用过的现读值 —— 否则「`close()` 之后槽会话轮换」那一刻
1142
+ `status()` 就会报新会话而账里装的还是旧会话的行);③空账无流 ⇒ 兼容入口下一帧会现读的那个值。
1143
+
1144
+ 🔴 **空锚(`session === undefined`,principal 级流)只挡得住 `own_root`** —— 那一臂要拿 `rootSessionId`
1145
+ 与本锚**逐值比对**,没有锚就结构性比不了。`server_fail_closed` **不受空锚影响**:它的前提是「这条流是
1146
+ 替谁开的」,**与本端有没有装 `SessionPort` 无关**(端可能根本没装,而流是宿主自己按别的方式按会话开的)
1147
+ ⇒ 默认槽 + 空锚 + 生产 meta 仍报 `server_fail_closed`(与 0.32.0 逐字同);keyed 上它本来就恒封顶。
1148
+
1149
+ 一次性快照走 `ledger.issueSnapshot(opts)`:
1150
+
1151
+ ```ts
1152
+ const snap = ledger.issueSnapshot({ timeoutMs: 5000 })
1153
+ snap.applySnapshot(await client.fleet.snapshot({ ...snap.options, signal })) // 🔴 用 applySnapshot,不是 applyFrame
1154
+ snap.close()
1155
+ ```
1156
+
1157
+ 🔴 **为什么是 `applySnapshot` 而不是 `applyFrame`**:SDK 的 `client.fleet.snapshot()` 回的是
1158
+ **`{tasks, workflows}`,不是帧**(它内部开流→取 `snapshot` 帧→关流,`type`/`ts`/`meta` 都在封装里被吞)。
1159
+ 帧形在库内合成一处,端不再手抄一份会漂的形。⚠️ 一次性快照拿不到 `meta` 帧 ⇒ 它**不会**填上本 ledger 的
1160
+ `sessionScoped` / `bgNotifyFailClosed` 两位(那两位说的是「这条连接」的性质,而快照封装里的连接已经关了)
1161
+ —— 要那两位就走 stream。
1162
+
1163
+ 🔴 **单锚不变量**:一本 ledger 在**任一时刻只有一个会话锚,而且账里的内容属于那个锚**。落地四条:
1164
+
1165
+ 1. stream 模式下 `issueSnapshot()` **继承** stream 的捕获,不自己现读;
1166
+ 2. `issueStream()` **作废**在飞的 snapshot ingress;
1167
+ 3. **换会话必清内容**:`issueStream()`(以及无 live stream 时的 `issueSnapshot()`)发放前先判「账里的
1168
+ 内容还算不算数」——**经 ingress 落的**内容有确切归属锚,按「捕获值变没变」判;**经兼容入口
1169
+ `ledger.applyFrame()` 落的**内容归属**不可知**(兼容入口每帧现读槽会话,库里不留锚),不可知就
1170
+ 不能替它背书 ⇒ 一律清。判为不算数就把上一会话的行/留存清掉 —— 否则 `status().sessionAnchor` 报 B 而
1171
+ `project()` / 在飞读面还在渲 A 的行。⚠️ **同会话重开流(真正的重连)内容一行不清**:留存池是渲染层
1172
+ 宽限窗,重连不该抽走已完成行。判据是**捕获的会话变没变**,不是「有没有重开流」。
1173
+ 🔴 snapshot-only 车道同样要走这条 —— 快照的 REPLACE 只换活跃集、**刻意不清留存**,不清账的话
1174
+ 「A 流写完终态留存后 close → 换会话 → 只取快照」会让两代同账;
1175
+ 4. **换连接必清连接级 meta**:`version`/`scoped`/`sessionScoped`/`bgNotifyFailClosed` 是「**那条连接**」
1176
+ 的事实,不是会话内容,而且是 fail-**open** 方向的信任位(前者让行级 own 判别整条让位,后者让通知的
1177
+ 台账判别整条让位)。**每开一条新 stream ingress 就清**,不看会话变没变 —— 重连若落到老版本 / 降级
1178
+ 节点 / 忽略 `?session=` 的节点,或新 `meta` 缺席/迟到,沿用上一条连接的这两位 = 广播行被投影、
1179
+ foreign 通知绕过本地归属门去改状态并触发钩子。**保留行是对的,保留连接信任位不是。**
1180
+ 复位到新 `meta` 到达之间那一小段两边都更严(行级回本地台账 = 成文的「宁藏勿串」,通知回
1181
+ `parentTaskId` 判别);SDK 的发帧序是 `meta` 第一帧,所以这个窗在真实连接上就是「首帧之前」;
1182
+ 5. `ingress.close()` **真的退出流模式**(`sessionAnchorEpoch` 回 `0`;⚠️ 退出流模式**不等于**锚回落
1183
+ 现读 —— 账里还有内容时 `sessionAnchor` 照实报**内容归属锚**,见上面的三态)——
1184
+ 否则「关流 → 换会话 → 取快照」会继承一条已关的流的陈旧锚,拿旧会话去打服务器而 `status()` 还报旧的。
1185
+ 关一只**过期**的 ingress 不影响更新的那一只。
1186
+
1187
+ 没有这几条,「槽内会话轮换 + 宿主没重开流」会造出两个会话代际同写一本 `taskMap` 的混合账,
1188
+ 而 `status().sessionAnchor` 只报一个 ⇒ 混合态从读面上看不出来。**要切会话就先 `issueStream()`**。
1189
+
1190
+ 🔴 **快照落账的两道闸**(任一不满足即丢弃 + 计 `droppedStaleIngress`):①**发放那一刻有活流的快照一律不落账**
1191
+ (「活流」含两形:`issueStream()` 发过的 ingress 流,**以及**兼容入口消费、由 `setConnected(true)`
1192
+ 自报的流 —— ⚠️ 后一形**只对纯兼容入口宿主**成立:采用过 ingress 之后流的存活由 epoch 说了算,
1193
+ `close()` 不替宿主改 `connected`,拿一个陈旧的 true 当活流会把「开流 → close → 一次性快照兜底」这条
1194
+ **合法降级序列**的快照永久判废;判据固化在**发放**时,不是落账时现查 —— 否则「流活着时签发 → 快照在路上、流先 `close()`
1195
+ → 落账」会因 close 把流模式位清零而反而通过闸,而那只快照跳过过连接级 meta 复位、会沿用一条已关
1196
+ 连接的信任位);
1197
+ ②**账本写序号**——快照 ingress 发放之后,**任何入口**(ingress 或兼容 `ledger.applyFrame`)只要成功落过
1198
+ 一帧,这份快照即判过期。第二道是必需的:兼容入口消费活流时 `sessionAnchorEpoch` **恒为 0**,只看它的闸
1199
+ 在那条车道上整条落空。副产品(有意):一只快照 ingress 是**一次性**的 —— 要再取一次就再 `issueSnapshot()`。
1200
+
1201
+ 第一道闸的两条独立理由,同一个结论:
1202
+ ①快照与流是两条连接、两个写者,而 `applySnapshot` 是无条件 REPLACE —— 「服务端取样之后、落账之前」
1203
+ 流又推了一帧时,照落会把**更新的**行覆盖回旧值,而 fleet 是 latest-state 总线,没有任何后续帧保证修
1204
+ 回来(只能等那一行下次 transition);②快照连接的 `meta` 被 SDK 封装吞掉 ⇒ 它的行**自带零 scoping
1205
+ 断言**,落进一本正持着别条连接 `sessionScoped=true` 的账里,会被那条**不相干连接**的信任位无条件放行
1206
+ 投影。而 live stream 本身就是「snapshot 先行的 latest-state 总线」—— 它一开就带全量快照并持续刷新,
1207
+ 此时再落一份一次性快照**零收益**。一次性快照的正当场景(首屏 / 温切探针)本来就发生在**没有 live
1208
+ stream** 的时候。
1209
+
1210
+ 🔴 **snapshot-only 时快照车道复位连接级 meta**(即使同会话):理由同上②——快照连接自带不了 `meta`,
1211
+ 沿用上一条(**已关的**)stream 连接的信任位,等于让「快照落在降级 / 不按 `?session=` 过滤的节点上」时
1212
+ 带回的外来行被一条不相干连接背书,而且此后**没有任何 meta 帧**能纠正它。复位后快照行回到本地台账判别
1213
+ (fail-closed 方向)。⚠️ 推论:**只走快照的宿主拿不到 `sessionScoped` 让位** —— 要让位就得开 stream。
1214
+ 🔴 **反过来:活流期间签发的快照什么都不清**(内容与连接级 meta 都不动)—— 它自己永久判过期就够了,
1215
+ 不该拿一条**正活着**的连接陪葬(那条流的 `meta` 只在连接首帧出现过,清掉就是让两个信任位在剩余整个
1216
+ 连接生命期保持关闭 ⇒ 行投影退回本地判别而静默藏行)。
1112
1217
 
1113
1218
  | `evidence` | 放行臂 | 强度 | 端该怎么读 |
1114
1219
  |---|---|---|---|
1115
- | `server_fail_closed` | meta `bgNotifyFailClosed` ∧ 本连接 session-bound | **会话级证明**(带上述流前提) | 最强:server 侧注入路已 fail-closed,到达即**这条流的**会话。⚠️ 这一臂**完全不读会话端口**,信的就是「这条流替谁开的」;前提破了它不会报错,而它正是**生产 meta 组合**下命中的那一格 |
1116
- | `own_root` | 通知 `rootSessionId` === `engineSessionParam()`(固定点语义) | **会话级证明**(带上述流前提) | 判据读的是 **`DEFAULT_SESSION_KEY` 槽**的 `SessionPort`,**不是**本 ledger `sessionKey` —— 单会话宿主两者恒同 |
1117
- | `session_anchor_untrusted` | keyed ledger(非默认 `sessionKey`)上由**会话级臂**放行 | **非证据**(封顶词) | 🔴 会话级臂放行了,但本 ledger 无法信任那个会话锚(开流参数只认默认槽,前提无法成立也无法校验)—— 不冒充 `server_fail_closed`/`own_root`,也不谎标进程成员(帧的 `parentTaskId` 可能是 foreign)。端按非证据档自裁;P-31 正位解落地后此封顶撤销。单会话宿主(默认槽)结构性不出现此词 |
1220
+ | `server_fail_closed` | meta `bgNotifyFailClosed` ∧ 本连接 session-bound | **会话级证明** | 最强:server 侧注入路已 fail-closed,到达即**这条流的**会话。⚠️ 这一臂**完全不读会话端口**,信的就是「这条流替谁开的」;生产 meta 组合下它在默认槽恒成立。🔴 **只在默认槽出现** —— keyed ledger 上它恒被封顶(零内容自校,见上);keyed 上生产 meta 若同时满足 `own_root`,报的是 `own_root`(可信臂胜出,见臂序段) |
1221
+ | `own_root` | 通知 `rootSessionId` === **本 ledger 的会话锚**(固定点语义) | **会话级证明**(内容自校) | = ingress 到达时该 ingress 开流那一刻捕获的会话;走兼容入口时 = `engineSessionParamFor(sessionKey)` 现读。默认槽两条路都可信;keyed ledger **必须经 ingress** 才解封(兼容入口无通道证据 ⇒ 封顶) |
1222
+ | `session_anchor_untrusted` | 会话级臂放行了,但**那一臂在本 ledger 上不可自校** | **非证据**(封顶词) | 🔴 今天恰有两格产出它:①keyed ledger 上的 `server_fail_closed`(结构性,零内容自校,**待上游 meta 回显 session id 才可能解封**);②keyed ledger **未经 ingress**(宿主直喂 `ledger.applyFrame`)时的 `own_root`。不冒充前两格,也不谎标进程成员(帧的 `parentTaskId` 可能是 foreign)、不标 `absent_parent`(键可能在场)。端按非证据档自裁。**单会话宿主(默认槽)结构性不出现此词** |
1118
1223
  | `own_parent` | `parentTaskId`(spawn 该子代的 leader run)∈ own-run 台账 | **进程级成员证明** | 🔴 只证明「本进程曾亲手驱动过这条 run」,**不区分会话代际** —— own-run 台账是进程级 `Set`、按会话零分区,`/clear` 或换会话后**旧会话**的 run 仍命中(= 在册局限 **P-13** 在通知面的同一张脸)。**不得**当作「属于当前会话」;要按会话归属做事,按 P-13 的出路自注入会话粒度判据,或只认上面两格 |
1119
1224
  | `absent_parent` | 通知**没带** `parentTaskId` | **非证据** | 🔴 absent-放行姿势(老引擎/老帧形不带该键,fail-closed 会把自家通知整批吞掉)。端要拿归属做有副作用的事(落库/跨会话搬运/翻别人的卡)时,这一格应自裁为「未证明」 |
1120
1225
 
1121
- 臂序 = 包内放行判据的**求值序**,多臂同时成立报**实际过门的第一条**(不报「最强的那条理论上也成立」)。
1226
+ 臂序 = 包内放行判据的**求值序**,**但只在「成立 ∧ 可信」的会话级臂之间排序**(0.45.0 收紧)。
1227
+ 🔴 生产 meta 组合下两位恒 `true`,若无条件先报 `server_fail_closed`,keyed 车道就**永远走不到**
1228
+ `own_root` —— 哪怕 `rootSessionId` 与本锚精确相等、内容自校已经成立;那等于把一条**真的**会话级证明
1229
+ 扔掉换一个封顶的非证据词,本批的净收益在生产配置下整条不可达。规矩:第一条**成立且可信**的会话级臂
1230
+ 胜出;一条都没有而至少有一条成立 ⇒ 封顶词。🔴 **绝不因此下探到 `own_parent`** —— 会话级臂放行的帧
1231
+ 根本没验过 `parentTaskId ∈ own-run 台账`(放行门在会话级臂上短路),报进程成员就是谎报。
1122
1232
  词表开集:端对未知词按 `absent_parent` 一档兜底最安全。真源 = `src/fleet/fleetLedger.ts` 的
1123
1233
  `BgNotificationAcceptEvidence` 顶注。
1124
1234
 
1125
1235
  ### 6e. 🔴 已知局限(多会话端**接之前必读**)
1126
1236
 
1127
- 见 §7c 的 **P-10 ~ P-14 + P-31** 六条 —— 全部是 sessionKey 面的在册局限,
1237
+ 见 §7c 的 **P-10 ~ P-14 + P-31** 六条 —— 全部是 sessionKey 面的在册局限(**P-31 自 0.45.0 起是
1238
+ 🟡 部分已解**:fleet 会话锚已 per-key 化,残余 = **一族三格**(keyed 上 `server_fail_closed` 恒封顶 / 行帧车道 `sessionScoped` 让位 / `hook_notice` 第二参),同根因同根治,待上游 meta 回显本连接 session id),
1128
1239
  CHANGELOG 0.29.0「已知局限」段与相应 JSDoc 都有成文。**别在读完它们之前接卡口与 plan review。**
1129
1240
 
1130
1241
  其中两条会直接把多会话端坑到「装了但不生效」:
@@ -1179,7 +1290,7 @@ CHANGELOG 0.29.0「已知局限」段与相应 JSDoc 都有成文。**别在读
1179
1290
  | **P-12** | low | canonical 重呈短路臂**沿用 arm responder** ⇒ `ReopenPlanReviewOpts.deliverDecision` 注入口**不生效**(成文例外 + debug 留痕) | `src/hitl/planReviewWire.ts`(`mintFreshQuestionId:false` 分支) | 任何**包装决断投递**的端(重试/退避/上屏定序,cli 的 `decideRetry` 是参照)必须走默认 `mintFreshQuestionId` 臂 —— 否则你的包装被静默旁路,跑的是裸 `decidePlanReview` 的 fire-and-forget |
1180
1291
  | **P-13** | 成文局限(不改行为) | 默认键下的 own-run 归属缺省腿是**进程级**证据,**不区分同一宿主进程内的会话代际** —— `/clear` 前登记的 run 在新会话语境下**仍判 owned**。最坏后果逐字:`用户看到自己旧会话的审批卡` | `src/hitl/parkOwnership.ts`(`ParkOwnershipDeps.isOwnRun` JSDoc);`docs/REFACTOR-LEDGER.md` 记为**驳为成文局限** | 多会话端必须**自注入**会话粒度的 `isOwnRun`;或传非默认 `sessionKey` 并接受缺省腿被整条跳过(代价 = 多一次诚实的 reopen-failed) |
1181
1292
  | **P-14** | high(打包面) | `activeReopenResponders` 的**单活纪律是 module 单例**:两份实例 ⇒ 各退各的,跨份的旧卡退役不掉 —— **退化回修复前的重复活卡形** | `src/hitl/planReviewWire.ts`(`activeReopenResponders`,singleton-manifest 在册) | 见 §8-G:必须保证 bundle 里只有**一份** `@sema-agent/client-core` |
1182
- | **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 槽判定 ⇒ 反而丢自己的通知),故本批**不动行为**,登记待裁 |
1293
+ | **P-31** | 🟡 **部分已解**(0.45.0 / design/285 批 0+1+2+3 落地;残余**一族三格**如实留册) @cli @web @desktop | **已解的半场:fleet 面的会话锚整条换 per-key,而且从写入一直贯到读侧出站。**①开流参数:keyed 宿主的唯一正身 = `createFleetLedger(hooks,{sessionKey}).issueStream()` 发的 **ingress** —— 它在开流那一刻**一次性捕获** `{sessionKey, session, epoch}`,且**帧只能经它进入 ledger**(module 级 `fleetStreamOptions()` / `fleetSnapshotOptions()` 原样保留、语义不动,供单会话默认槽宿主继续用;**刻意不加 `*For` 兄弟**——module 级 keyed helper 与 ledger 的绑定状态互不可见,它测的是「你调了哪个 helper」而不是「流按哪个 key 开」)。重连 = 新 ingress + epoch+1,旧 ingress 的迟到帧丢弃并计 `droppedStaleIngress`;一次性快照走 `issueSnapshot()` + `applySnapshot(await client.fleet.snapshot(...))`(SDK 那只 verb 回的是 `{tasks, workflows}` **不是帧**,帧形在库内合成一处)。**单锚不变量**:一本 ledger 任一时刻只有一个会话锚、且账里的内容属于那个锚 —— stream 模式下 snapshot **继承** stream 的捕获、`issueStream()` **作废**在飞的 snapshot、**换会话必清内容**(stream 与 snapshot-only 两条车道同一套;同会话重连内容一行不清,R7 保住)、**换连接必清连接级 meta**(那四位是 fail-open 方向的连接信任位,沿用 = 重连落到降级节点时广播行被投影、foreign 通知绕过归属门)、`close()` 真的退出流模式;另有**快照顺序令牌**:快照发放后流又落过帧即判过期丢弃,绝不把更新的行 REPLACE 回旧值。②`ownByRoot` 的比对锚换成**本 ledger 的会话锚**(经 ingress ⇒ 该 ingress 的不可变捕获;兼容入口 ⇒ `engineSessionParamFor(sessionKey)` 现读)。⇒ keyed 宿主**自己的**通知不再被判 foreign 丢掉,且带**别人会话**锚的通知按方向判 foreign。③**owner 台账三值原子写**(批 2):两条 fleet 登记腿(行帧 / bg 通知)把 `{runId, sessionId, sessionKey}` **同条**写进 `SubagentOwnerRecord` —— 值取**本 ingress 开流那一刻的不可变捕获**,不是每帧现读某个可变槽(逐帧现读会把重连后旧流的迟到帧写成「旧 runId + 新会话」,那正是 `recordSubagentOwnerFromProgress` 头注明令禁止的错组合;ingress 的 epoch 闸让旧代际的帧根本进不来)。兼容入口 `ledger.applyFrame` 归属不可知 ⇒ 两个新位一个都不写(与 0.44.0 逐字同);同 `runId` 上**缺席不覆盖在场**、`runId` 真变了两位一起丢;同 `runId` 上换会话**不静默** —— 计数与四元组走读口 `subagentOwnerSessionConflicts()`(恒应为 0)。④**读侧全路径按槽取**(批 3):`resolveOwnerContext(childTaskId)` 一次读给出 `{record, runId, key}`,五个取址点(tail / subagentOutput / taskOutput / taskStop / delegatedPrompt)的 **baseUrl / token / principal 一律取 `engineWireTargetFor(owner.sessionKey ?? DEFAULT_SESSION_KEY)`**,不再是零参默认槽;caps 门(`engineTaskHandlesCapable(sessionKey)`)与 `?session=` 同槽。🔴 **BEHAVIOR CHANGE(默认槽也变)**:tail / subagentOutput / taskOutput / taskStop 四条读面的 `?session=` 改为**会话二态** —— 行登记时捕到了会话就用**行的**会话,缺才退本槽现势会话(与 steer 腿既有口径统一)。tick 腿**今天就在默认槽写 `sessionId`**,所以「会话轮换后读旧行」此前是拿现势会话打一条 session-bound 的旧 run = 确定性 404,现在自洽。⑤plan 决策链两处(`armPlanReviewApproval` / `decidePlanReview`)**显式豁免本批**:整条 plan_review HITL 链(卡注册 / 退役 / responder 台账)今天是默认槽单会话形,只换 wire target 会造出「按默认槽立卡、按 keyed 槽发决断」的半 keyed 形,比现状更坏 —— 整条链同批转 keyed 是 additive 公开面改动,归后续工单;现状由常驻钉 `B3-P31/G18b` 两向钉住。🔴 **豁免不等于没有后果,后果如实登记(异源对抗复审 [high] 采纳的半场)**:①**keyed-only 宿主**(只装了 `installEngineWireTargetFor(key, …)`、默认槽为空)上 `armPlanReviewApproval()` 恒返 `false` ⇒ 一条 park 在 `plan_review` 的 run **没有审批入口**,只能等窗口到期/走别的路;②**keyed + 默认槽都装**的宿主上,卡与决断都走**默认槽**的 baseUrl/token/principal —— 与那条 park 住的 run 所属的槽可能不是同一台。⇒ 多会话宿主在 plan-mode 上**今天不可用**,不是「有一点瑕疵」;要用就等整条链转 keyed 的那一批。<br>🔴 **残余(不许读成已闭合)—— 一族三格,同根因同根治**:根因 = **连接级的零内容自校信任位**。宿主把 A 流喂进 B 的 ingress 时,ingress **证明不了帧的来源连接**,而这两位只读 meta、不比对任何会话值:①**通知臂** `server_fail_closed`(meta `bgNotifyFailClosed` ∧ `sessionScoped`)⇒ 在 keyed ledger 上**恒封顶为** `session_anchor_untrusted`;②**行帧车道**的 `sessionScoped` 让位(`ownTaskRow`/`ownWorkflowRow` 命中即整条放行,[1510] 既有裁定)⇒ 同样会让一条被误接的流的行在本键 ledger 上无条件投影。③**`hook_notice` 分发**:`FleetLedgerHooks.onHookNotice(frame, sessionScoped)` 的第二参交出去的就是那一位,回调**既不带锚也不带 evidence** ⇒ 一条被误接的 A 流报 `sessionScoped=true` 时,A 的「本轮守卫未能评估」会带着「已按会话过滤」这句话进 B 的宿主面,端无从自裁( observe 帧,不改状态)。本批**刻意不封顶行帧车道、也不改 `onHookNotice` 签名**(①都非本批引入;②根治与通知臂同一个;③擅自封顶会让已正确接线的 keyed 宿主的行凭空消失,而 additive 第三参是另一批的公开面决定),现状由常驻钉 `B3-P31/R4a`(行帧)与 `B3-P31/R9a`(hook_notice)钉住。唯一真根治 = **上游在 fleet `meta` 帧上 additive 回显本连接的 session id**、本端校验相等 —— **一次修好三条通道**(按 Wire 能力显式表态制单独立项 @server @sdk;本包**不阻塞**) | `src/engineSessionParam.ts`(`engineSessionParamFor`)· `src/fleet/fleetLedger.ts`(`FleetIngress` / `issueStream` / `issueSnapshot` / `ownByRoot` 换锚 / 按臂拆分的可信谓词 / `FleetLedgerStatus` 的 `sessionAnchor`·`sessionAnchorEpoch`·`droppedStaleIngress` / 两条登记腿的三值原子写)· `src/subagentContentStore.ts`(`SubagentOwnerRecord.sessionKey` / `recordBgParentRun` 第四参 / `subagentOwnerSessionConflicts`)· `src/subagent/engineSubagentResume.ts`(`resolveOwnerContext`)· `src/subagent/{engineSubagentTail,engineSubagentOutput,engineSubagentSteer,engineTaskHandleWire,engineDelegatedPrompt,engineRowStopGate,engineCompactWire}.ts` · `docs/INTEGRATION-CLIENTS.md` §6d | 单会话宿主(cli 及今天的三端)**逐字节零受迫** —— 默认槽两条会话级臂与开流参数都与 0.32.0 同,兼容入口 `ledger.applyFrame` 语义不动。**keyed 多会话宿主**:①每键装齐 `installHostFor(key,{session})`(漏装 锚为空,两臂都不解封,`hostPortMissesFor(key)` 会点名);②`issueStream()` 开流、**帧只经 `ingress.applyFrame`**,重连即重开 ingress;③明白 `server_fail_closed` keyed 上**仍封顶**,拿它做有副作用的事(落库/翻卡/跨会话搬运)前按非证据档自裁 |
1183
1294
 
1184
1295
  ### 7d. 请求面与其它在册件
1185
1296
 
@@ -1277,6 +1388,26 @@ reason 里写明「枚举器盲区形」。已知两形:
1277
1388
  - [ ] `installEngineWireTarget(target)` —— **非 Node 宿主(web / desktop 渲染进程)必装**:它们没有 env,这是唯一入口
1278
1389
  - [ ] `kickEngineCapsProbe(baseUrl, probe)`(caps 门的数据源)
1279
1390
  - [ ] `installWorkflowStatusProbe` / `installBgTaskStatusProbe` —— 不装 = **workflow / 后台子代的完成通知永远不落地**
1391
+ - [ ] **多会话宿主接 fleet 台账时**(0.45.0 / P-31):`createFleetLedger(hooks, { sessionKey })` 之后**必须**用
1392
+ `ledger.issueStream()` 拿开流参数、并且**帧只经 `ingress.applyFrame` 进 ledger**(重连即重开 ingress;
1393
+ 一次性快照走 `ledger.issueSnapshot(opts)`)。直喂 `ledger.applyFrame` 是**默认槽兼容入口** ——
1394
+ keyed ledger 走它会让 `own_root` 一律封顶成 `session_anchor_untrusted`(没有通道证据)。
1395
+ 前置:该键的 `installHostFor(key, { session })` 必须已装,否则会话锚为空、两条会话级臂都不解封
1396
+ - [ ] **多会话宿主还要每键装 wire target**(0.45.0 / P-31 批 3):`installEngineWireTargetFor(key, target)` —— 子代读面
1397
+ (tail / subagentOutput / taskOutput / taskStop / delegatedPrompt)现在按**行登记的槽**取 baseUrl/token/principal;
1398
+ 漏装该键 ⇒ 那一槽的读面整条走**进程级 env 回落**(= 与默认槽同一条路径),行是 B 的、凭证是 A 的。
1399
+ 🔴 只有**经 ingress** 落账的行才带槽键 —— 走兼容入口 `ledger.applyFrame` 的 keyed 宿主拿不到这一位(归属不可知),
1400
+ 读侧会回落默认槽:那不是 bug,是「没给通道证据就不替它背书」的同一条纪律
1401
+ - [ ] 🔴 **多会话宿主:plan-mode 今天不可用**(0.45.0 / P-31 批 3 的显式豁免,见 §7c P-31 行)——
1402
+ `armPlanReviewApproval` / `decidePlanReview` 两处仍读**默认槽**的 wire target,而整条 plan_review
1403
+ HITL 链(卡注册 / 退役 / responder 台账)也还是默认槽单会话形。后果两形:**keyed-only 宿主**
1404
+ (默认槽为空)上 arm 恒返 `false` ⇒ 一条 park 在 `plan_review` 的 run **没有审批入口**;
1405
+ **keyed + 默认槽都装**的宿主上,卡与决断都走默认槽的 baseUrl/token/principal,与那条 park 住的
1406
+ run 所属的槽可能不是同一台。⇒ 多会话宿主要用 plan-mode,等整条链转 keyed 的那一批;
1407
+ **不要**自己在外面拼一个 keyed 的 decide(那会与库内默认槽的卡台账两头不一致)。
1408
+ 🔴 **运行时边界(不只是文档)**:`armPlanReviewApproval(result, sessionKey?)` 有 additive 第二参 ——
1409
+ keyed 宿主**应当**把这条 park 所属的槽传进来,库这边会**明确拒绝**(不 arm + `hostLog('error')`)
1410
+ 而不是静默按默认槽立卡。不传 / 传默认槽 = 与 0.44.0 逐字同
1280
1411
  - [ ] `installSubagentActivitySink`(可选:Subagent Progress 段的数据源;不装不是错误态)
1281
1412
  - [ ] `installSubagentTailMetaSink`(可选,0.30.4 #280 件2:tail meta 帧的 `contentFrames` 判别位数据源;不装不是错误态,但查看态会把「结构性无内容」渲成永远的「还没来」)
1282
1413
  - [ ] **启动校验用存在性读口**(§5a 的 (a) 表,🔴 **哨兵逐口不同,逐个照抄别推广**;多会话宿主用 `*For` 那一支):`hasApprovalCardPort() === true` · `hasApprovalCardPortFor(key) === true` · **`hitlHostSurfaceFor(key) !== null`**(哨兵是 `null`,写 `!== undefined` **恒真** = 假绿) · `hostSettings() !== undefined` · `hostSettingsFor(key) !== undefined` · `hostFs() !== undefined` · `hostFsFor(key) !== undefined` · `hostSession() !== undefined` · `hostSessionFor(key) !== undefined` · `engineWireTarget() !== null` · `engineWireTargetFor(key) !== null`(`hostTimers()` / `hostTimersFor(key)` 同为 `!== undefined`,按你真装的挑)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/client-core",
3
- "version": "0.44.0",
3
+ "version": "0.45.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",