@sema-agent/client-core 0.39.0 → 0.41.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.
@@ -44,6 +44,7 @@
44
44
  * 绝不用「子代的首条输出」或行描述冒充委派 prompt(那是把一个看起来合理的错答案摆上屏)。
45
45
  * 一切失败 fail-soft,绝不 throw。
46
46
  */
47
+ import { stripSystemReminderBlocks } from '../systemReminderTag.js';
47
48
  import { hostLog } from '../host.js';
48
49
  import { makeEngineWireClient } from '../engineWireSdk.js';
49
50
  import { engineWireTarget } from '../engineWireTarget.js';
@@ -139,10 +140,16 @@ export function delegatedPromptText(content) {
139
140
  *
140
141
  * 信封是**成对标签**,所以用配对正则整段剥;不配对(被截断等)时保守不动,宁可多渲也不吃掉正文。
141
142
  */
142
- const PROMPT_ENVELOPE_TAGS = ['system-reminder', 'fork-boilerplate'];
143
+ // 🔴 件⑦([4974],2026-08-22):`system-reminder` 的**开标签带属性**(core ≥5.46 恒带
144
+ // `mark="<22 位 base64url>"`),精确形 `<system-reminder>` 整段剥不掉 ⇒ 上面那条 2026-08-01
145
+ // 修过的 bug 原样复发(详情页 Prompt 又变回技能清单)。剥壳口径收进包内单一闸口
146
+ // `systemReminderTag`(同根因的另一座位是 `classifierVerdictWire`,见该模块头注)。
147
+ // `fork-boilerplate` 是 core `agents/subagent.js` 里的**裸**标签(dist 直证),保持精确形 ——
148
+ // 没有属性的标签不需要属性容忍,给它加 = 给伪造面白让一寸。
149
+ const PROMPT_ENVELOPE_EXACT_TAGS = ['fork-boilerplate'];
143
150
  function stripEnvelopes(raw) {
144
- let text = raw;
145
- for (const tag of PROMPT_ENVELOPE_TAGS) {
151
+ let text = stripSystemReminderBlocks(raw);
152
+ for (const tag of PROMPT_ENVELOPE_EXACT_TAGS) {
146
153
  text = text.replace(new RegExp(`<${tag}>[\\s\\S]*?</${tag}>`, 'g'), '');
147
154
  }
148
155
  return text.trim() || undefined;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * systemReminderTag — `<system-reminder>` 包裹的**单一识别闸口**(件⑥⑦,2026-08-22,[4974])。
3
+ *
4
+ * ## 为什么要有这个文件
5
+ *
6
+ * core ≥5.46 起,引擎铸的每一个 `<system-reminder>` **开标签**都带一个 `mark` 属性。真字节直证
7
+ * (core 5.52.0 `dist/core/reminder-mint.js`,逐行):
8
+ * ```js
9
+ * const MARK_SHAPE = /^[A-Za-z0-9_-]{22}$/ // randomBytes(16).toString("base64url")
10
+ * export function openSystemReminder(mark) {
11
+ * return mark === undefined ? "<system-reminder>" : `<system-reminder mark="${mark}">`
12
+ * }
13
+ * export function mintSystemReminder(body, mark) {
14
+ * return `${openSystemReminder(mark)}\n${body}\n</system-reminder>`
15
+ * }
16
+ * ```
17
+ * 闭标签**没有**属性,只有开标签变了。而包内两处判定当时都锚着**裸开标签**,于是同一个根因
18
+ * 长出两个座位([4974] 双 CONFIRMED):
19
+ * · `classifierVerdictWire.unwrapSystemReminder` —— 剥不掉壳 ⇒ 裁决原文前面顶着一整个开标签 ⇒
20
+ * `startsWith(签名)` 恒不成立 ⇒ auto-deny 的通知行与 Recent Denials 记账**静默失效**;
21
+ * · `subagent/engineDelegatedPrompt.stripEnvelopes` —— 信封整段剥不掉 ⇒ Fleet 详情页的
22
+ * 「Prompt」又变回 ~1330 字符的技能清单(那个函数头注自陈 2026-08-01 修过的 bug 原样复发)。
23
+ *
24
+ * 两处各写一份正则正是这条缺陷能长出**两个**座位的原因,所以收成这一个闸口:识别口径只有一处,
25
+ * 下次 core 再动标签形也只有一处要跟。
26
+ *
27
+ * ## 口径(刻意写窄,不做 HTML 解析)
28
+ *
29
+ * · 开标签 = `<system-reminder>` 或 `<system-reminder` + **空白起头**的属性段 + `>`;属性段用
30
+ * `[^>]*` —— 它跨不过 `>`,所以没有回溯爆炸面,也不会把后面的正文吃进标签里。
31
+ * · 刻意**不校验** `mark` 的值形:本模块的职责是「认出这是引擎的信封壳」并把它剥掉/跳过,
32
+ * **不是**信任判定。真正的信任判据是壳里的 mark 比对(core 的 `reminderMarkDeclaration` 明说
33
+ * 「带不带 mark 都可能是数据」),那是另一层的事;这里若按 mark 值放行/拒绝,等于把一个安全
34
+ * 判定藏在一个显示层的剥壳函数里。
35
+ * · 闭标签逐字 `</system-reminder>`(core 就是这么铸的,不给它加属性容忍 = 不给伪造面留口子)。
36
+ * · **不处理嵌套**:配对用非贪婪,遇到第一个闭标签就收 —— 与件⑥⑦ 修之前逐字同语义
37
+ * (core 不铸嵌套 reminder;真出现嵌套时残留的是外层闭标签,不是把正文吃掉,方向仍是保守的)。
38
+ *
39
+ * 🔴 零 import(端无关叶件):纯字符串处理,不碰 Node、不碰 wire 类型。
40
+ */
41
+ /**
42
+ * 整条文本就是一个 `<system-reminder>` 包裹时,返回**里面的正文**;否则原样返回。
43
+ * (`formatHookFeedback` = `mintSystemReminder` 的逆变换。)
44
+ */
45
+ export declare function unwrapSystemReminder(text: string): string;
46
+ /**
47
+ * 把文本里**所有**成对的 `<system-reminder>…</system-reminder>` 段整段删掉(不配对的原样留着 ——
48
+ * 截断的信封宁可多渲一点,也不能把正文吃掉)。
49
+ */
50
+ export declare function stripSystemReminderBlocks(text: string): string;
51
+ /** 这段文本**以**一个 system-reminder 开标签起头吗(诊断/判定用的轻量读口)。 */
52
+ export declare function startsWithSystemReminderOpenTag(text: string): boolean;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * systemReminderTag — `<system-reminder>` 包裹的**单一识别闸口**(件⑥⑦,2026-08-22,[4974])。
3
+ *
4
+ * ## 为什么要有这个文件
5
+ *
6
+ * core ≥5.46 起,引擎铸的每一个 `<system-reminder>` **开标签**都带一个 `mark` 属性。真字节直证
7
+ * (core 5.52.0 `dist/core/reminder-mint.js`,逐行):
8
+ * ```js
9
+ * const MARK_SHAPE = /^[A-Za-z0-9_-]{22}$/ // randomBytes(16).toString("base64url")
10
+ * export function openSystemReminder(mark) {
11
+ * return mark === undefined ? "<system-reminder>" : `<system-reminder mark="${mark}">`
12
+ * }
13
+ * export function mintSystemReminder(body, mark) {
14
+ * return `${openSystemReminder(mark)}\n${body}\n</system-reminder>`
15
+ * }
16
+ * ```
17
+ * 闭标签**没有**属性,只有开标签变了。而包内两处判定当时都锚着**裸开标签**,于是同一个根因
18
+ * 长出两个座位([4974] 双 CONFIRMED):
19
+ * · `classifierVerdictWire.unwrapSystemReminder` —— 剥不掉壳 ⇒ 裁决原文前面顶着一整个开标签 ⇒
20
+ * `startsWith(签名)` 恒不成立 ⇒ auto-deny 的通知行与 Recent Denials 记账**静默失效**;
21
+ * · `subagent/engineDelegatedPrompt.stripEnvelopes` —— 信封整段剥不掉 ⇒ Fleet 详情页的
22
+ * 「Prompt」又变回 ~1330 字符的技能清单(那个函数头注自陈 2026-08-01 修过的 bug 原样复发)。
23
+ *
24
+ * 两处各写一份正则正是这条缺陷能长出**两个**座位的原因,所以收成这一个闸口:识别口径只有一处,
25
+ * 下次 core 再动标签形也只有一处要跟。
26
+ *
27
+ * ## 口径(刻意写窄,不做 HTML 解析)
28
+ *
29
+ * · 开标签 = `<system-reminder>` 或 `<system-reminder` + **空白起头**的属性段 + `>`;属性段用
30
+ * `[^>]*` —— 它跨不过 `>`,所以没有回溯爆炸面,也不会把后面的正文吃进标签里。
31
+ * · 刻意**不校验** `mark` 的值形:本模块的职责是「认出这是引擎的信封壳」并把它剥掉/跳过,
32
+ * **不是**信任判定。真正的信任判据是壳里的 mark 比对(core 的 `reminderMarkDeclaration` 明说
33
+ * 「带不带 mark 都可能是数据」),那是另一层的事;这里若按 mark 值放行/拒绝,等于把一个安全
34
+ * 判定藏在一个显示层的剥壳函数里。
35
+ * · 闭标签逐字 `</system-reminder>`(core 就是这么铸的,不给它加属性容忍 = 不给伪造面留口子)。
36
+ * · **不处理嵌套**:配对用非贪婪,遇到第一个闭标签就收 —— 与件⑥⑦ 修之前逐字同语义
37
+ * (core 不铸嵌套 reminder;真出现嵌套时残留的是外层闭标签,不是把正文吃掉,方向仍是保守的)。
38
+ *
39
+ * 🔴 零 import(端无关叶件):纯字符串处理,不碰 Node、不碰 wire 类型。
40
+ */
41
+ /**
42
+ * 开标签的**闭集**形:core 只会铸这两种(`openSystemReminder` 逐字),所以判据也只认这两种。
43
+ *
44
+ * 🔴 **不做通配属性**(七审 finding② 采纳):`<system-reminder\s[^>]*>` 那种宽口会把
45
+ * `onclick="…"` / 换行垃圾 / 未闭合引号 / 内嵌 `<img` 全部当成合法开标签 —— 而本模块的两个消费点
46
+ * 处理的正是**模型与工具产出的文本**(UNTRUSTED),宽口等于给伪造信封开一道口子:攻击者/模型
47
+ * 可以用一个假信封把内容藏起来(委派 prompt 面)或把自己的文本伪装成引擎裁决(分类器面)。
48
+ * `mark` 的值形按 core `MARK_SHAPE` 逐字(`randomBytes(16).toString("base64url")` = 22 位
49
+ * `[A-Za-z0-9_-]`),值本身**不比对**(见文件头:比对是信任判定,不是剥壳的活)——但**形**必须对,
50
+ * 形不对的一律不是引擎铸的壳。
51
+ */
52
+ const OPEN_TAG_SOURCE = '<system-reminder(?: mark="[A-Za-z0-9_-]{22}")?>';
53
+ /** 整段配对(开标签 + 正文 + 闭标签)。捕获组 1 = 正文。 */
54
+ const WRAPPED_RE = new RegExp(`^\\s*${OPEN_TAG_SOURCE}\\s*\\n?([\\s\\S]*?)\\n?\\s*<\\/system-reminder>\\s*$`);
55
+ /**
56
+ * 整条文本就是一个 `<system-reminder>` 包裹时,返回**里面的正文**;否则原样返回。
57
+ * (`formatHookFeedback` = `mintSystemReminder` 的逆变换。)
58
+ */
59
+ export function unwrapSystemReminder(text) {
60
+ const m = WRAPPED_RE.exec(text);
61
+ return m ? m[1] : text;
62
+ }
63
+ /**
64
+ * 把文本里**所有**成对的 `<system-reminder>…</system-reminder>` 段整段删掉(不配对的原样留着 ——
65
+ * 截断的信封宁可多渲一点,也不能把正文吃掉)。
66
+ */
67
+ export function stripSystemReminderBlocks(text) {
68
+ return text.replace(new RegExp(`${OPEN_TAG_SOURCE}[\\s\\S]*?<\\/system-reminder>`, 'g'), '');
69
+ }
70
+ /** 这段文本**以**一个 system-reminder 开标签起头吗(诊断/判定用的轻量读口)。 */
71
+ export function startsWithSystemReminderOpenTag(text) {
72
+ return new RegExp(`^\\s*${OPEN_TAG_SOURCE}`).test(text);
73
+ }
@@ -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
- | 公开导出面 | **766** 个运行期符号(+ 39 个测试钩;= 未发 `0.39.0` 的值,npm `0.38.0` 是 **764**;`0.37.0` 是 **753**,见 `CHANGELOG.md`) | `scripts/public-export-baseline.json` 的 `count` / `testHookCount` —— **别手抄进别处,以该文件为准** |
26
+ | 公开导出面 | **767** 个运行期符号(+ 39 个测试钩;= 未发 `0.40.0` 的值,npm `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`(**766** 项)。
104
+ > 全集真源 = `scripts/public-export-baseline.json` 的 `names`(**767** 项)。
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
- 实测:766 项 **100% 是运行期导出,零 type-only**。
114
+ 实测:767 项 **100% 是运行期导出,零 type-only**。
115
115
 
116
116
  **推论(端必须知道)**:
117
117
  - barrel 导出的**类型**面比 707 大得多,且**不被这道门看守** —— `AdapterContext` / `SeamEvent` /
@@ -120,18 +120,18 @@ SDK 核对「本包 import 的每个值级符号仍然导出」「`TaskStats.cos
120
120
  端依赖这些类型是合法的,但**不要**拿基线 diff 当"类型面没变"的证据。
121
121
  - `src/agentSession/contract.ts` 对基线贡献 **0** 项(纯类型模块,`export *` 在 dist 里是空转发)。
122
122
 
123
- 766 项的内部构成(帮助端估读表大小):**226** 项是 `SCREAMING_SNAKE` 常量数据表/词汇表
123
+ 767 项的内部构成(帮助端估读表大小):**227** 项是 `SCREAMING_SNAKE` 常量数据表/词汇表
124
124
  (矩阵、键集、env 名、锚串)而非可调用物;**5** 项是 PascalCase 运行期值
125
125
  (`ControlRouter` / `ControlSafetyError` / `HitlBridge` / `HitlSafetyError` / `DecideTransportRetryExhaustedError`);
126
126
  **39** 项是 `*For(sessionKey, …)` 的 per-session 变体(§6;其中 `engineNamespaceKeyFor` 是命名巧合 —— 参数是 baseUrl 不是 sessionKey,见域 14)。
127
127
 
128
- ### 2b. 域图(16 域,逐域计数之和 = 764)
128
+ ### 2b. 域图(16 域,逐域计数之和 = 767)
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
- | 3 | **HITL 决断卡链**(§4/§5 主战场) | 120 | `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` 注入) · `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`)、`parkRowBirthWait.ts`、`approvalDecisionNoteAudit.ts`、`askParkRowRouting.ts`、`resumeRunningCard.ts`(#265 上收的判定层)、`persistedRulesWire.ts`、`localAllowRule.ts`(#244 F2 规则侧) |
134
+ | 3 | **HITL 决断卡链**(§4/§5 主战场) | 121 | `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` 注入) · `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`)、`parkRowBirthWait.ts`、`approvalDecisionNoteAudit.ts`、`askParkRowRouting.ts`、`resumeRunningCard.ts`(#265 上收的判定层)、`persistedRulesWire.ts`、`localAllowRule.ts`(#244 F2 规则侧) |
135
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` |
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` |
@@ -304,6 +304,100 @@ lane 归属改用 id 形状 / `workflowRunId` 启发式判(`src/adapt/arms.ts`)
304
304
  7.38 到)。**对本包与端零影响** —— 正因为消费面按开集写,白名单是 server 的投递判定,不是消费判据。
305
305
  已按接入文档宪法回报 server。
306
306
 
307
+ ### 3z. 🆕 `_sema_collateral_abort` —— 连坐 abort 机读位(0.40.0,#324 / [4907])
308
+
309
+ **病形**:同 turn 多个 call 在飞 + 一个撞 gate ⇒ 引擎对**整批**在飞 call 铸同一串
310
+ `Operation aborted`。本包 HITL 桥的 HOLD 表按 `isGatedToolEnd`(名字腿 ∪ abort 标记腿)
311
+ 把它们**全部**收进同一张表,任一 flush 出口一次倾泻 ⇒ 端上满屏同文红行,而其中只有一条是
312
+ 用户刚决断的那个 call,其余是**连坐**的旁观 call。
313
+
314
+ **本版做的事**(additive,`isError` / `output` / 帧序 / 帧数一律不动):
315
+
316
+ | 位置 | 行为 |
317
+ |---|---|
318
+ | `gateLedger.noteParkGate(...)` / `noteGateSubject(callId)` | 登记「本批那张 park」与「它决断出的主角」。判据只收 **park** 与 wire **身份**,绝不收工具名、也不收 `tool_approval` 帧 |
319
+ | `gateLedger.flushHeld()` | 在**这一拍**求出身:已知不是主角的帧 stamp `_sema_collateral_abort: true`。**一帧不丢**的不变量不变 —— 丢帧的后果是永久假排队(dim 圆点等不到结果行),比多渲一行更坏 |
320
+ | `eventToSdkMessage` 的 `case 'tool_end'` | 严格 `true` 才原样上内部 `tool_end_result` 臂;缺席不铸键 |
321
+
322
+ **判别子为什么在 flush 当拍求值(而不是入表当拍)**:durable park 腿的帧序是
323
+ `tool_start → tool_end(毒化) → done{suspended}` —— **gate 身份晚于毒化帧到达**。入表那一拍包层
324
+ 还不知道谁是主角,任何判词都是猜的。按工具名猜(「名字腿认得 = 可能是主角」)双向都错:一等
325
+ `kind==='tool_approval'` gate 的主角(名字腿认不得的 toolName,如 `WebFetch`)被判成连坐 ——
326
+ **假阳打在主角上**;同族兄弟(两个 `Bash` 只有一个撞 gate)两个都被判成主角 —— 连坐帧拿不到位。
327
+ 五个 flush 出口**全部**晚于 park 被看见,所以判词搬到那一拍才有真判据。
328
+
329
+ **判别子只认 park 供证 —— 归属必须可结构性证明**。判别子真正要回答的是「**这一批** abort 帧
330
+ 属于哪一张 gate」,而 wire 上**没有任何位**做这个关联,包层能算的都是推断。四轮异源对抗复审的
331
+ 定谳:只有 **park** 具备结构性归属 —— `flushHeld` 的出口就长在那张 park 自己的决断/回退路径上
332
+ (`parkResolver` fail-soft / 决断成功后的续流放行 / 终帧),「这一批是这张 park 的 abort 批」是
333
+ 结构事实,不是猜的。
334
+
335
+ 判据构成(**全部批级**,一批一套):
336
+ 1. **本批恰好一张 park**(硬门)。零张 ⇒ 归属无从证明;两张以上 ⇒ 两次 gate 的 abort 混进同一张
337
+ 扣留表,谁属谁无解。两种情形一律**整批不标**。
338
+ · durable **重放同一张 park** 不算新的一张 —— 判据是「自上次登记以来有没有**新的 call** 进过
339
+ 扣留表」(按 call 不按帧:re-attach 会把仍被扣留的毒化帧连同 park 一起重放)。
340
+ 🔴 走这条去重分支时**一个字节都不写**:该判据证明得了「这一批的组成没变」,证明**不了**
341
+ 「这张 park 就是上一张」—— 一张真的新 park 同样可以零毒化帧。顺手刷新族属就等于让一张与
342
+ 这批帧无关的 gate 改写本批判据。真是重放时族属本就相同,不写零损失。
343
+ · ask park 臂同样要登记:它自己用不到兜底腿,但它是本批的第二张 park,漏登会让上一张 fs park
344
+ 的判据被拿来给这一批派判词(连 ask 主角自己都会被 stamp)。
345
+ 2. **主角 = 本批(扣留帧 ∪ 本批已出批的帧)里唯一属该族的 call**;该族 = 这张 park 的 gate
346
+ **本体** `toolName` 所属的 fs 写 / shell 执行族(准入证;族外 park 不发证 ⇒ 整批不标)。
347
+ · 并上「已出批的帧」是因为决断成功那一下会把主角的毒化帧 `dropHeld` 掉;不并上就等于主角
348
+ 「不在这一批」,整批判据当场失效。
349
+ · 前置:**每一条扣留帧的 `tool_start` 都得是本连接亲眼见过的**。族成员判据取自 tool_start 当拍
350
+ 记的集合,没见过 start 的 call 在「唯一」这件事上是隐形的;而续流只消费 `runs.events` 的一段
351
+ (re-attach 从 `lastEventId` 起),主角的 start 完全可能落在这次连接之外 —— 那时表里剩下的
352
+ 唯一族成员是**旁观者**。信息不全就不许声称「唯一」。
353
+ · 🔴 **pending 队列一概不参与判别**:`durable checkpointGate` 不带 callId,
354
+ `findPendingForTask` 取的是「同 taskId + 谓词命中的**第一行**」(谓词不命中时还退「任意行」)
355
+ —— 那是给**卡面渲染**用的宽口,**不是身份**。同族两行排队时它可能给出旁观者,拿它当主角
356
+ 就会把真主角挤成「非主角」而 stamp(异源复审实测)。所以决断结果只用于 gate 生命周期
357
+ (`markDecided`/`dropHeld`),judgment 一步都不碰它。
358
+
359
+ 批级判据的**退休点两处**:`flushHeld` drain 完、以及 `dropHeld` 之后**扣留表为空**那一下。
360
+ 后者必须写成「无条件检查表空」而不是「摘中了才检查」—— 「park 决断成功但本批压根没有毒化帧」
361
+ 的批每次 delete 都落空,写成后者就等于那种批**永远不退休**,判据活到下一批派错判词。
362
+ drain 那条拦的是「判完之后又来一批背后没有 park 的 abort 帧」。
363
+
364
+ 🔴 **缺席语义(端必须按这条读)**:`_sema_collateral_abort` 缺席 = **「不是已知的连坐帧」**,它同时
365
+ 覆盖两种情况 —— (a) 这一帧是 gate 主角,(b) **本包判别不出来**(判据没到手)。**缺席不等于
366
+ 「这是主角」**,更不能据此渲「这条才是你刚决断的那个」。要断言主角身份,端自己按 `toolCallId`
367
+ 与手上的审批卡对账,别拿本键的缺席反推([honest-absence-not-fabricated-zero])。
368
+
369
+ 🔴 **端怎么读(唯一正道)**:从 **`tool_end_result` 臂**上读(`errorCode` / `settledBy` /
370
+ `approver` 同款位置),按 `toolCallId` 归集,自己决定呈现(建议:连坐簇按 repeated-error 族
371
+ 软化折叠,头卡留 + 计数尾)。**判别取机读位,不猜文案** —— `Operation aborted` 这串是
372
+ 引擎词汇,会随 core 版本变。键常量从本包取:`import { SEMA_COLLATERAL_ABORT_KEY }`
373
+ (单一真源,三端别各自手抄)。
374
+
375
+ 🔴 **这一位刻意不在转录 `tool_result` block 上,也不在转录消息顶层**(两向都有常驻断言钉着,
376
+ pure 门 062⑥/⑦):
377
+ - **block 上不行** —— 转录卡会随 `/compact` 直发 provider,往 block 上加自铸键正是 0.5.0
378
+ `label` 那个真缺口的形状(**provider 400**);
379
+ - **消息顶层不行** —— 壳的 `createUserMessage` 是闭形重建,顶层键在到达渲染层之前就没了
380
+ (`_sema_degraded` 至今壳侧零消费面,根因就是这个),放那儿等于假接线。
381
+
382
+ ⚠️ **已知局限一(在册,不当已办)**:**同族多兄弟分不出主角**。两个 `Bash` 同时在飞、其中一个
383
+ 撞 gate 时,wire 上没有任何位能说「这两个里哪个撞的」—— `lastFsOrShellGatedCallId()` 指向**最后
384
+ 一个** tool_start,pending 队列给的是「第一行」,两者都不是身份。**决断成功也不例外**(见判据②
385
+ 最后一条)。此格按诚实缺席处置:**整表一个都不 stamp**,端侧行为逐字节退回本键出现之前。
386
+ 🔴 出路在 server:`durable checkpointGate` 带上 `toolCallId`。常驻门 `F8-S3/S8/S33` 把这一格钉成
387
+ 「全 ABSENT」—— server 补位后这些条**当场翻红**,逼下一棒把新身份接进判据,而不是让局限长住。
388
+
389
+ ⚠️ **已知局限二(刻意为之)**:**同步 `tool_approval` 帧腿整条不标**。帧只证明「本 turn 出现过
390
+ 一张 gate」,证明不了「**这一批** abort 帧属于它」—— 那张 gate 完全可能早已正常收口(零毒化帧
391
+ ⇒ 零扣留表 ⇒ 一次 flush 都不会跑),它的证却会活到下一批去派判词。异源对抗复审四轮里,**全部**
392
+ 「假阳打在 gate 主角身上」的残留路径都出自帧腿取证(旧 server 同批双主角 / 零 HOLD 审批证跨批 /
393
+ 历史身份跨批)。取舍是明确的:**宁可少标,绝不误标主角**(误标主角 = 把用户真正该看的那条红行
394
+ 折叠掉,比多渲 N 行更坏)。常驻门 `F8-S4/S5/S7/S11/S14/S17/S21/S22` 把这一格钉成「全 ABSENT」。
395
+ 🔴 出路同样在 server:abort 批带 **gate 关联 id**(不只是主角 `toolCallId`),把归属从推断变成读数。
396
+
397
+ ⚠️ **@core 对表位(在册,不当已办)**:core 5.51 的 `[DEFERRED]` / `gate.batch_halted` 词汇
398
+ ([4826]③)到货时与本键**对表**。若 core 给出更强的官方形(例如 `errorCode` 上的专码),
399
+ 端侧换锚、本 additive 键**保留兼容**,不塌方。在那之前本键是端能拿到的唯一机读判别子。
400
+
307
401
  ---
308
402
 
309
403
  ## §4 回执消费义务(ack contract)—— 🔴 本档重点
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/client-core",
3
- "version": "0.39.0",
3
+ "version": "0.41.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",