@sema-agent/client-core 0.17.0 → 0.18.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.
package/README.md CHANGED
@@ -23,7 +23,7 @@ Renamed from **`@sema-agent/wire-cc-adapter`** (0.1.x, deprecated — see *Migra
23
23
 
24
24
  ## Scope
25
25
 
26
- **Version:** 0.17.0
26
+ **Version:** 0.18.0
27
27
 
28
28
  - **Today** — the adapter seam, the whole `adapt()` pipeline (all 14 A-layer arms plus the
29
29
  B/D/E tool-card layers), the notification/caps/model families, the adapter kernel (stream driver
@@ -53,7 +53,7 @@ Renamed from **`@sema-agent/wire-cc-adapter`** (0.1.x, deprecated — see *Migra
53
53
  `SseIdleError`, `probeHealth`, `APIError` and `TaskStopConflictError` are imported as values in
54
54
  five modules, and the browser bundle really bundles the SDK through (the portability guard would
55
55
  exit 3 rather than quietly mark it external).
56
- - The declared floor is `>=6.2.0`, and it is *witnessed*: the guard checks that an actually
56
+ - The declared floor is `>=6.3.0`, and it is *witnessed*: the guard checks that an actually
57
57
  installed SDK at that line still exports every value-level symbol this package imports and still
58
58
  declares `TaskStats.costMicroUsd` (the key `costOrNull` reads). A floor nobody ever ran is a
59
59
  promise, not a contract.
@@ -231,7 +231,7 @@ const toolEndResultArm = function* (m, { ctx, idOf, cards, panel }) {
231
231
  // ② MF-10 **关卡 settle**(子代从不发终态 tick,父卡的 tool_end 就是完成信号;卡的 output
232
232
  // 文本 = 子代最终报告,串到行 end 事件上)③ 卡台账出栈 + response-id 复位(S1)。
233
233
  // 🆕 B5:④ **B/D/E 层** —— 铸 `tool_result` 卡本体 + 三个侧产物
234
- // (Remember→memory_saved / task 族 structured 台账 / #117a bg Bash 桥)。
234
+ // (Remember→memory_saved / task 族 structured 台账 / #117a+#158 bg Bash+Monitor 桥)。
235
235
  const callId = typeof m.toolCallId === 'string' ? m.toolCallId : undefined;
236
236
  // 🔴 时序留钉(矩阵 §2.1①):同 assistant 臂 —— label 登记恒在任何 yield 之前。
237
237
  recordEngineToolLabel(callId, m.label);
@@ -482,8 +482,10 @@ const taskNotificationArm = function* (m, { ctx, idOf, panel, inst }) {
482
482
  const rawStatus = fields.status;
483
483
  const terminal = rawStatus === 'completed' || rawStatus === 'failed' || rawStatus === 'killed';
484
484
  // #117a settle 半场 —— **故意在渲染去重之前**:settle 幂等,重放帧仍须 settle 一条
485
- // 首投之后才注册的行。#47 矩阵 #7:只有 bash 行(或老引擎无 task_type)能被 settle
486
- const settleAllowed = n.task_type === undefined || n.task_type === 'background_bash';
485
+ // 首投之后才注册的行。#47 矩阵 #7:只有 bash/monitor 行(或老引擎无 task_type)能被 settle
486
+ // (#158:monitor 行注册进面板后,它的终局通知也得能关行;事件帧 status="event" 非终态,
487
+ // 由下方 terminal 门挡——事件流绝不错关行)。
488
+ const settleAllowed = n.task_type === undefined || n.task_type === 'background_bash' || n.task_type === 'monitor';
487
489
  if (settleAllowed && terminal) {
488
490
  yield chrome({
489
491
  kind: 'bgshell_settle',
@@ -521,7 +523,19 @@ const taskNotificationArm = function* (m, { ctx, idOf, panel, inst }) {
521
523
  // 🔴 B4 起由**适配器自持**(B2 已把队列端口搬进本包):此前挂在 `notification_terminal`
522
524
  // 的宿主义务①,是「说得通但没人做」的典型 —— 宿主没接线就静默双投。
523
525
  dropQueuedNotificationsForRun(taskId);
524
- if (n.task_type !== 'background_bash') {
526
+ // 🔴 这道门与上面的 `settleAllowed` 配对的是**显式的 bash/monitor 两族**:它们被
527
+ // bgshell_settle 关过一次的行,就不该再被**子代面板**的 settleFromNotification 关第二次。
528
+ // ⚠️ 两半**不是**字面同一个谓词,别顺手写成 `if (!settleAllowed)`:settleAllowed 还放行
529
+ // `task_type === undefined`(老引擎缺类型位的兼容腿),那一支是**有意**的双投兼容 ——
530
+ // 不知道它是哪族,两个面板都关一次比漏关一个面板好。只有显式说了自己是 bash/monitor 的
531
+ // 帧才配对排除。#158 复审车B 修:放行 monitor 那一批漏了这半 ——
532
+ // monitor 终局帧同时发 `bgshell_settle` 与 `panel_task{kind:'end'}` 两条,同一个 taskId
533
+ // 在两个面板语义上各关一次。壳侧只是**碰巧**没出事(bgshell_settle 先 yield 把行翻成终态,
534
+ // 随后 useEngineAgentPanelBridge 的 settle() 撞上 `status !== 'running'` 早退 —— 但那个
535
+ // settle() 根本没有行类型门,靠的是 yield 顺序而不是判据);sema-web 那端是**真错**:
536
+ // turn-mapping.ts panel_task/end 臂固定发 `status: isError ? 'failed' : 'completed'`,
537
+ // 于是 killed 的 monitor 行先被 bgshell_settle 正确记成 killed,再被这条覆盖成 failed。
538
+ if (n.task_type !== 'background_bash' && n.task_type !== 'monitor') {
525
539
  // ⟨帧序耦合 5/7⟩ #6 通知-settle 边(推送半场)整段让位给 M3 —— 拆分前这里直写
526
540
  // endedPanelTasks 又直读实例台账的 firedSubagentStartHookTaskIds,是矩阵点名的两条跨模块面。
527
541
  yield* panel.settleFromNotification(taskId, rawStatus !== 'completed');
@@ -21,9 +21,10 @@ export function createToolCardLedger(ctx, idOf) {
21
21
  const body = end && end.output !== undefined
22
22
  ? wireOutputToBody(p.name, end.output, isError, end.truncated === true, flattenWireOutput, end.structured)
23
23
  : degradedToolResultBody(isError);
24
- // ── E 层 ③:#117a 模型发起的后台 Bash 回执 → ctrl+B 面板行(判定在库、执行留宿主)。
25
- if (p.name === 'Bash' && !isError && end?.output !== undefined) {
26
- const reg = detectEngineBgShellReceipt(p.rawInput, flattenWireOutput(end.output));
24
+ // ── E 层 ③:#117a 模型发起的后台 Bash/Monitor 回执 → ctrl+B 面板行(判定在库、执行留宿主;
25
+ // #158:Monitor 此前被工具名门拒 面板盲区,现同臂分 source)
26
+ if ((p.name === 'Bash' || p.name === 'Monitor') && !isError && end?.output !== undefined) {
27
+ const reg = detectEngineBgShellReceipt(p.name, p.rawInput, flattenWireOutput(end.output));
27
28
  if (reg) {
28
29
  yield chrome({ kind: 'bgshell_register', laneProof: MAIN, taskId: reg.taskId, registration: reg });
29
30
  }
package/dist/adapt.d.ts CHANGED
@@ -58,7 +58,7 @@ export declare const ADAPTER_COVERAGE: {
58
58
  /** B4(0.6.0)新落码的臂/半场 —— A 层收官批,同样别再在 todo 里留同名条目。 */
59
59
  readonly b4: readonly ["A 层第 14 臂 task_progress:workflow lane 三级门 + 判据④ belt 回植 + C1 alias + bindTaskCard(显式父/FIFO/含糊即 unbound)+ inline stats + T50 双形读 + SubagentStart + #6 resident 台账 + 面板 tick", "settlePanelTasks 三处 sweep 全接线(tool_end 显式关卡带报告 / result 防御 sweep / turn 末 + abort sweep)", "P0-2 tool label 侧信道(sanitizeToolUseBlock 摘 label + engineToolLabelStore 登记;0.5.0 真缺口,label 会随 tool_use block 落转录 → /compact 直发 provider 400)", "task_notification 的两件宿主义务收回库内自持:dropQueuedNotificationsForRun + clearEnginePanelTaskResident;stop hook 的 if-started 门由库解", "T8 flattenWireOutput 提为独立导出(A 层 settle 报告与 B 层卡体同一份)"];
60
60
  /** B5(0.7.0)新落码的臂/半场 —— B/D/E 层收批,同样别再在 todo 里留同名条目。 */
61
- readonly b5: readonly ["B 层 structuredToToolUseResult 14 case(ask-user-question/bash/edit/create/update/text/grep/glob/notebook-edit/cron-create/cron-delete/cron-list/agent/task-output)+ workflow-run 登记副作用", "D 层 wireOutputToBody 3 臂(bash / taskoutput / 泛化);structured 白名单在场时 T11+T14 正则退位,缺席保回落", "E 层 4 臂:#117a bg Bash 回执(判定在库→chrome bgshell_register)· TodoWrite oldTodos 富卡 · ReportFindings 按名认领 · Remember→memory_saved 系统消息", "tool_end_result → user tool_result 正常路径(卡本体);result 与 turn 末两处开卡兜底关闭(abort 不兜底,cli 同)", "task/task-list structured → chrome task_ledger_sync{source:\"structured\"}(判定归一在库、落盘留宿主)", "T7 诚实缺席(§8-4:不搬 mock 合成器,改 degraded 标记)· T20 diff hunks 进包(src/diff/patch.ts)"];
61
+ readonly b5: readonly ["B 层 structuredToToolUseResult 14 case(ask-user-question/bash/edit/create/update/text/grep/glob/notebook-edit/cron-create/cron-delete/cron-list/agent/task-output)+ workflow-run 登记副作用", "D 层 wireOutputToBody 3 臂(bash / taskoutput / 泛化);structured 白名单在场时 T11+T14 正则退位,缺席保回落", "E 层 4 臂:#117a bg Bash 回执 + #158 Monitor 回执(同臂按工具名分,registration.source 判别;判定在库→chrome bgshell_register)· TodoWrite oldTodos 富卡 · ReportFindings 按名认领 · Remember→memory_saved 系统消息", "tool_end_result → user tool_result 正常路径(卡本体);result 与 turn 末两处开卡兜底关闭(abort 不兜底,cli 同)", "task/task-list structured → chrome task_ledger_sync{source:\"structured\"}(判定归一在库、落盘留宿主)", "T7 诚实缺席(§8-4:不搬 mock 合成器,改 degraded 标记)· T20 diff hunks 进包(src/diff/patch.ts)"];
62
62
  /** 如实留白——写不动/依赖未搬运资产的臂,#52b 起分批接。 */
63
63
  readonly todo: readonly ["T13 的 **D 层** 半场(taskoutput block content)未随 structured 退位:退位会改 block content 形状(cli 那里是裸对象 = 件1 隐患),属行为面改动,无真实需求驱动 ⇒ 见 DIVERGENCE-8", "macrotask 让渡(cli 在提交前让出宏任务给 Ink;宿主渲染策略,不进库)"];
64
64
  };
package/dist/adapt.js CHANGED
@@ -69,7 +69,7 @@ export const ADAPTER_COVERAGE = {
69
69
  b5: [
70
70
  'B 层 structuredToToolUseResult 14 case(ask-user-question/bash/edit/create/update/text/grep/glob/notebook-edit/cron-create/cron-delete/cron-list/agent/task-output)+ workflow-run 登记副作用',
71
71
  'D 层 wireOutputToBody 3 臂(bash / taskoutput / 泛化);structured 白名单在场时 T11+T14 正则退位,缺席保回落',
72
- 'E 层 4 臂:#117a bg Bash 回执(判定在库→chrome bgshell_register)· TodoWrite oldTodos 富卡 · ReportFindings 按名认领 · Remember→memory_saved 系统消息',
72
+ 'E 层 4 臂:#117a bg Bash 回执 + #158 Monitor 回执(同臂按工具名分,registration.source 判别;判定在库→chrome bgshell_register)· TodoWrite oldTodos 富卡 · ReportFindings 按名认领 · Remember→memory_saved 系统消息',
73
73
  'tool_end_result → user tool_result 正常路径(卡本体);result 与 turn 末两处开卡兜底关闭(abort 不兜底,cli 同)',
74
74
  'task/task-list structured → chrome task_ledger_sync{source:"structured"}(判定归一在库、落盘留宿主)',
75
75
  'T7 诚实缺席(§8-4:不搬 mock 合成器,改 degraded 标记)· T20 diff hunks 进包(src/diff/patch.ts)',
@@ -17,20 +17,46 @@
17
17
  * 值域 = 壳既有 wire permissionMode 词表(permissionWireCaps.PermissionMode:default / plan /
18
18
  * acceptEdits / bypassPermissions / auto——分类器批3 [907] server 五模式表,server ≥1.209 认
19
19
  * "auto")。非法 env 值 ⇒ 一次性 warn + 回落现状,绝不 fail 启动。
20
- * stamp 规则:
21
- * · flag 来源:非 default 的 wire 词直接 stamp(auto 自分类器批3 起入表);`default` stamp
22
- * (= 今天 flag 的实际效果,零行为变化);wire 词表外的 flag 值(dontAsk/manual…是壳内部
23
- * 模式,server 不认)不 stamp。
20
+ * stamp 规则([2713] 最小刀改形,2026-08-05;双复审补 dontAsk 归一同日):
21
+ * · flag 来源:显式表态一律显式 stamp——wire 五词直 stamp,`default` stampDefault 显式垫
22
+ * (旧形 default stamp server 侧 `settings.permissions.defaultMode` 恒缺席 ⇒
23
+ * `deriveSettingsPolicy` 早退返回 `{}` ⇒ **按模式派生的那套门一个不铸**,print 车道 ask 门
24
+ * 断链根因。⚠️ 本模块只对**这一个量**负责:body 里有没有 `permissionMode`。黑板 [2713] 写的
25
+ * 「applyTaskSettings 整跳」是那条链的简写,它成不成立还要看 body.settings 里有没有别的
26
+ * parsed 字段 —— 那是 server 的控制流,会随 server 改版漂,别在本注释里复述第二遍,要查就去
27
+ * 读 server task-settings.ts);壳内部词(`manual`=CC default 别名、`dontAsk`=壳侧最严
28
+ * 「ask⇒deny」模式)经 SHELL_INTERNAL_MODE_TO_WIRE 归一到 wire 词 `default` 后 stamp
29
+ * (方向纪律与三条残余记账见该常量 doc);归一表外的值仍不 stamp = 回落部署基线。
30
+ * ⚠️ 版本界(逐值核过 server CHANGELOG 与 cli scripts/lib/engine-pin.mjs,2026-08-05):
31
+ * `shellGate` 与 fs 写 ask 门曾同吃这道断链;server #153([2713] 分单②)把
32
+ * MANUAL_MODE_SHELL_GATE 升到 `applyRuntimeGovernance` 无条件施加,该修**发在 server 7.1.0**,
33
+ * 而当日 `ENGINE_PIN = '7.0.0'` —— **发货引擎上两门仍同断**,本 stamp 两门都决定。等 pin 抬到
34
+ * ≥7.1.0,shellGate 就与本字段在不在场无关,本 stamp 只再决定 fs 写 ask 门。
35
+ * 别把这段读成「现在只剩 fsWrite」,也别读成「shellGate 永远归我管」——它是随 pin 移动的界。
24
36
  * · env 来源:五词全部显式 stamp(含 `default`——env 是"显式垫层"旋钮,设了就是要显式语义,
25
37
  * 不再依赖 server absent-mode 缺省解释;server 1.191 对显式 "default" 走 manual ask 门解释)。
26
- * 车道纪律:本模块只被 seamQueryEngine.ask(headless `-p`)import;交互 REPL(seamQuery)一根毛
27
- * 不动——单测有 source-level lane proof(detachWire 同款)。
38
+ * 车道纪律:`resolveHeadlessPermissionMode`(本模块的 stamp 决策口)只被 seamQueryEngine.ask
39
+ * (headless `-p`)调用;交互 REPL(seamQuery)一根毛不动——单测的 lane proof 锚的正是**这个符号
40
+ * 名**,不是模块名。⚠️ 「本模块只被 seamQueryEngine import」是**错的**(旧注释遗留,双复审查出):
41
+ * `interactiveToolsWire.ts:45` 也 import 本模块的 `permissionModeFromArgv`(REF-CC-dup-10 收编,
42
+ * 两个 headless 旋钮共用同一份 argv 读法)——那是纯 argv 解析器,不碰 stamp 决策,车道纪律不破。
28
43
  */
29
44
  import { type PermissionMode, type PermissionModeField } from './permissionWireCaps.js';
30
45
  import { type EnvLike } from './hostEnv.js';
31
46
  /** 部署旋钮 env 键名(settings.json env 块 → 1a-envseed → process.env 同车道)。 */
32
47
  export declare const HEADLESS_PERMISSION_MODE_ENV = "SEMA_HEADLESS_PERMISSION_MODE";
33
48
  export declare const HEADLESS_PERMISSION_WIRE_MODES: readonly PermissionMode[];
49
+ /**
50
+ * 归一表的**唯一取值口**(#164 [2760]② wire 面三面同拍,2026-08-06):壳内部模式词 → wire 词;
51
+ * 表外词与 undefined 恒等透传(未表态不制造表态)。两个消费方共用同一张表(单表纪律,禁抄写):
52
+ * · headless flag 支(本模块 resolveHeadlessPermissionMode);
53
+ * · 壳交互车道(seamQuery 在 permissionModeField stamp 前归一——修前交互车道 `dontAsk` 落
54
+ * permissionModeField 表外支不 stamp ⇒ 引擎侧 absent-mode 一个门不铸,与底栏「don't ask on」
55
+ * 指示相反;归一后 stamp `default` ⇒ 引擎铸 ask 门,ask 到壳后由壳侧 dontAsk 决策臂
56
+ * (ask⇒deny)落地,与 CC 单进程语义对位)。
57
+ * 方向纪律与三条残余记账见上方表 doc,本函数不另立口径。
58
+ */
59
+ export declare function normalizeShellPermissionModeForWire(mode: string | undefined): string | undefined;
34
60
  export type HeadlessPermissionResolution = {
35
61
  /** 直接展开进 TaskRequest 的字段({} = 不 stamp,现状 absent-mode)。 */
36
62
  fields: PermissionModeField;
@@ -17,14 +17,29 @@
17
17
  * 值域 = 壳既有 wire permissionMode 词表(permissionWireCaps.PermissionMode:default / plan /
18
18
  * acceptEdits / bypassPermissions / auto——分类器批3 [907] server 五模式表,server ≥1.209 认
19
19
  * "auto")。非法 env 值 ⇒ 一次性 warn + 回落现状,绝不 fail 启动。
20
- * stamp 规则:
21
- * · flag 来源:非 default 的 wire 词直接 stamp(auto 自分类器批3 起入表);`default` stamp
22
- * (= 今天 flag 的实际效果,零行为变化);wire 词表外的 flag 值(dontAsk/manual…是壳内部
23
- * 模式,server 不认)不 stamp。
20
+ * stamp 规则([2713] 最小刀改形,2026-08-05;双复审补 dontAsk 归一同日):
21
+ * · flag 来源:显式表态一律显式 stamp——wire 五词直 stamp,`default` stampDefault 显式垫
22
+ * (旧形 default stamp server 侧 `settings.permissions.defaultMode` 恒缺席 ⇒
23
+ * `deriveSettingsPolicy` 早退返回 `{}` ⇒ **按模式派生的那套门一个不铸**,print 车道 ask 门
24
+ * 断链根因。⚠️ 本模块只对**这一个量**负责:body 里有没有 `permissionMode`。黑板 [2713] 写的
25
+ * 「applyTaskSettings 整跳」是那条链的简写,它成不成立还要看 body.settings 里有没有别的
26
+ * parsed 字段 —— 那是 server 的控制流,会随 server 改版漂,别在本注释里复述第二遍,要查就去
27
+ * 读 server task-settings.ts);壳内部词(`manual`=CC default 别名、`dontAsk`=壳侧最严
28
+ * 「ask⇒deny」模式)经 SHELL_INTERNAL_MODE_TO_WIRE 归一到 wire 词 `default` 后 stamp
29
+ * (方向纪律与三条残余记账见该常量 doc);归一表外的值仍不 stamp = 回落部署基线。
30
+ * ⚠️ 版本界(逐值核过 server CHANGELOG 与 cli scripts/lib/engine-pin.mjs,2026-08-05):
31
+ * `shellGate` 与 fs 写 ask 门曾同吃这道断链;server #153([2713] 分单②)把
32
+ * MANUAL_MODE_SHELL_GATE 升到 `applyRuntimeGovernance` 无条件施加,该修**发在 server 7.1.0**,
33
+ * 而当日 `ENGINE_PIN = '7.0.0'` —— **发货引擎上两门仍同断**,本 stamp 两门都决定。等 pin 抬到
34
+ * ≥7.1.0,shellGate 就与本字段在不在场无关,本 stamp 只再决定 fs 写 ask 门。
35
+ * 别把这段读成「现在只剩 fsWrite」,也别读成「shellGate 永远归我管」——它是随 pin 移动的界。
24
36
  * · env 来源:五词全部显式 stamp(含 `default`——env 是"显式垫层"旋钮,设了就是要显式语义,
25
37
  * 不再依赖 server absent-mode 缺省解释;server 1.191 对显式 "default" 走 manual ask 门解释)。
26
- * 车道纪律:本模块只被 seamQueryEngine.ask(headless `-p`)import;交互 REPL(seamQuery)一根毛
27
- * 不动——单测有 source-level lane proof(detachWire 同款)。
38
+ * 车道纪律:`resolveHeadlessPermissionMode`(本模块的 stamp 决策口)只被 seamQueryEngine.ask
39
+ * (headless `-p`)调用;交互 REPL(seamQuery)一根毛不动——单测的 lane proof 锚的正是**这个符号
40
+ * 名**,不是模块名。⚠️ 「本模块只被 seamQueryEngine import」是**错的**(旧注释遗留,双复审查出):
41
+ * `interactiveToolsWire.ts:45` 也 import 本模块的 `permissionModeFromArgv`(REF-CC-dup-10 收编,
42
+ * 两个 headless 旋钮共用同一份 argv 读法)——那是纯 argv 解析器,不碰 stamp 决策,车道纪律不破。
28
43
  */
29
44
  import { permissionModeField } from './permissionWireCaps.js';
30
45
  import { hostEnv } from './hostEnv.js';
@@ -44,6 +59,79 @@ const HEADLESS_PERMISSION_WIRE_MODE_TUPLE = ['default', 'plan', 'acceptEdits', '
44
59
  const _headlessPermissionModePins = [true, true];
45
60
  void _headlessPermissionModePins;
46
61
  export const HEADLESS_PERMISSION_WIRE_MODES = HEADLESS_PERMISSION_WIRE_MODE_TUPLE;
62
+ /**
63
+ * 壳内部模式词 → wire 词的归一表(**壳内映射,勿改 server 五词表**)。大小写敏感,与 commander
64
+ * `--permission-mode` 的 choices 同口径;表外词照原样交给 permissionModeField 双支自处置。
65
+ *
66
+ * · `manual` = CC 2.1.207 对内部 `default` 的**输入别名**(壳 `normalizePermissionModeAlias`
67
+ * 同一张表:`mode === 'manual' ? 'default' : mode`,枚举本身不含 manual 成员)。
68
+ * · `dontAsk` = 壳侧**最严**模式:`permissions.ts` 把 `behavior==='ask'` 整个改判成 `'deny'`
69
+ * (DONT_ASK_REJECT_MESSAGE),且它在 `INTERNAL_PERMISSION_MODES` 里 ⇒ `--permission-mode dontAsk`
70
+ * 是合法 CLI 取值、`-p` 车道真可达。wire 五词没有对应词;**不归一的后果是方向错的**——落
71
+ * permissionModeField 表外支 ⇒ 不 stamp ⇒ server `deriveSettingsPolicy` 因 `!perms` 返回 `{}`
72
+ * ⇒ **客户端表态派生的门一个都不铸**(部署基线 approvalRequire 与 runtime governance 仍在,
73
+ * 不是「什么都不拦」;但按模式该有的那道门没了),观测上与最松的 `bypassPermissions` 同形
74
+ * —— 用户要最严、拿到的是最松那一档的形。归一到 `default`(fs 写 ask 门)。
75
+ * 🔴 **这是近似投影,不是等价映射;别把它读成「已经修好了」**,三条残余按坐标记账:
76
+ * ① `default` 不是 wire 词表里最严的词(`plan` 才是 handsReadOnly 只读手)。选它是因为 `plan`
77
+ * 会连带改变工作流形态(present_plan + 只读手),把「别问我、直接拒」翻成「进规划模式」是
78
+ * 另一种谎报。`default` 是**不改工作流形态的前提下最接近**的落点。
79
+ * ② `dontAsk` 要的是即刻 deny,`default` 是 ask/park —— 有活体审批面时那条 ask **可以被批准**,
80
+ * 而 dontAsk 语义禁止这条路径。所以「只紧不松」这句话的比较基准**只能是不 stamp 那个落点**
81
+ * (归一前:该模式的门一个不铸),不是 dontAsk 本义 —— 相对 dontAsk 本义它是松的。要精确
82
+ * 投影必须 server 词表补一个 deny 语义的 wire 词(另票)。
83
+ * ③ 非 host lane(e2b/k8s/ssh)上 server 的 `fsWriteGate` wiring 恒为 undefined
84
+ * (resolve-spec `hostSemanticsLane ? … : undefined`),`deriveSettingsPolicy` 的 default 臂
85
+ * 只剩 workflow ask 政策 ⇒ **Write/Edit 在沙箱 lane 上仍不挂门**。
86
+ * ⚠️ 别写成「壳侧关不掉」——那是过度声称。壳侧**有**一条路:`settings.permissions.ask:
87
+ * ['Write','Edit','NotebookEdit']` 走 `createAskListPolicy`,那条政策是**名字键控、与 lane
88
+ * 无关**的(task-settings.ts:494-495,不吃 fsWriteGate wiring)。**这条路真的能关掉这个洞**,
89
+ * 本批刻意不走,理由如下(前两条曾被写错、双复审驳回,这里是订正后的版本):
90
+ * (a) 代价 = **host lane 上 fs 门自己的放行腿被盖掉**。名字键控 ask 与其它政策 deny/ask
91
+ * 恒赢地折叠,所以 ask-list 一挂,host lane 上现在被 scratchpad 豁免/acceptDirs 放行的
92
+ * 写也变成 ask —— 丢的是精度。
93
+ * (⚠️ 这条只说到这里就够了。两次复审都栽在替这个「没走的选项」推演更细的后果上:
94
+ * 先写成「两 lane 更不一致」被驳,再写成「两 lane 都凡写必问」又被驳——敏感路径 deny 腿
95
+ * 在 host 上仍然 deny,两 lane 并不同形。真要选这条路,后果得当场重新取证,别信这段。)
96
+ * (b) 代价 = 在 headless 上「凡写必问」而**没有应答者**(#165 = -p 车道无客户端审批面)⇒
97
+ * 沙箱 lane 的每一次 Write 都停在门上 —— 停成 park 还是直接 deny 由部署定(park 要
98
+ * `durableApproval` 与 checkpoint 存储**两个都在**,缺一就不是 park;精确条件在 server
99
+ * approval.ts 的 durable 政策 doc 里,本注释不复述以免漂)。两种都是行为面大改,要过
100
+ * 宪法三问,不是复审顺手能落的。
101
+ * (⚠️ 也不要写成「server 修了这层补偿自然作废」—— 已核实为假:server #153/7.1.0 只把
102
+ * `MANUAL_MODE_SHELL_GATE` 升到 governance 层,**非 host lane 的 fsWriteGate 仍不建**,
103
+ * [2713] 那条 @server 分单到今天没兑现,所以这层补偿不会因为 #153 而自动过时。)
104
+ * (c) 越过「壳带意图、service 解释」的分层——壳替 server 决定了拦哪几个工具名。
105
+ * ⇒ 记账在此,决定权在仓主(要不要用 ask-list 换掉精度、并同时推 #165);本表只负责
106
+ * 「让 server 有表态可读」。
107
+ * ⚠️ 判据方向(比较基准写死,别读成别的):本表允许的落点 = **相对「不 stamp」严格更紧或等紧**。
108
+ * 加词前先问「这个词 stamp 出去之后,server 铸出来的门比不 stamp 时更紧吗」——更松就不许进表
109
+ * (宁可不 stamp 回落部署基线,也不许客户端表态制造放行)。
110
+ * 注意这**不是**「落点 ≥ 壳内语义」:`dontAsk → default` 就不满足后者(见上方残余②),它满足的是
111
+ * 前者。写判据时用错基准会把这张表读成「已经等价了」。
112
+ * **刻意用 Map 而不是对象字面量**(与 adapt/arms.ts ARMS 表同一条纪律):键来自原始 argv,是开集;
113
+ * 对象表上 `tbl['__proto__']`/`tbl['constructor']` 会顺着原型链取回一个**非字符串**值,把它当
114
+ * `PermissionMode` 往下传是类型谎报(tsc 因 `Record<string,…>` 的索引签名全盲)。Map 无原型链取值。
115
+ */
116
+ const SHELL_INTERNAL_MODE_TO_WIRE = new Map([
117
+ ['manual', 'default'],
118
+ ['dontAsk', 'default'],
119
+ ]);
120
+ /**
121
+ * 归一表的**唯一取值口**(#164 [2760]② wire 面三面同拍,2026-08-06):壳内部模式词 → wire 词;
122
+ * 表外词与 undefined 恒等透传(未表态不制造表态)。两个消费方共用同一张表(单表纪律,禁抄写):
123
+ * · headless flag 支(本模块 resolveHeadlessPermissionMode);
124
+ * · 壳交互车道(seamQuery 在 permissionModeField stamp 前归一——修前交互车道 `dontAsk` 落
125
+ * permissionModeField 表外支不 stamp ⇒ 引擎侧 absent-mode 一个门不铸,与底栏「don't ask on」
126
+ * 指示相反;归一后 stamp `default` ⇒ 引擎铸 ask 门,ask 到壳后由壳侧 dontAsk 决策臂
127
+ * (ask⇒deny)落地,与 CC 单进程语义对位)。
128
+ * 方向纪律与三条残余记账见上方表 doc,本函数不另立口径。
129
+ */
130
+ export function normalizeShellPermissionModeForWire(mode) {
131
+ if (mode === undefined)
132
+ return undefined;
133
+ return SHELL_INTERNAL_MODE_TO_WIRE.get(mode) ?? mode;
134
+ }
47
135
  /**
48
136
  * argv 里的 `--permission-mode <v>` / `--permission-mode=<v>`(后者/后出现者赢,commander 同语义)与
49
137
  * `--dangerously-skip-permissions`(= bypassPermissions;`--permission-mode` 在场时让位)。
@@ -82,8 +170,12 @@ export function resolveHeadlessPermissionMode(argv, env = hostEnv()) {
82
170
  // 1) CLI flag 恒赢:flag 在场(含 wire 词表外的壳内部模式)⇒ env 完全不看。
83
171
  const flagMode = permissionModeFromArgv(argv);
84
172
  if (flagMode !== undefined) {
85
- // permissionModeField 只放行 wire 已知的非 default 词;default/壳内部词 {}(现状,零变化)。
86
- return { fields: permissionModeField(flagMode), source: 'flag' };
173
+ // [2713] 最小刀:flag 显式表态 显式 stamp(含 default——旧形 flag=default stamp 让 server
174
+ // deriveSettingsPolicy settings.permissions 缺席返回 {},按模式派生的门不铸)。壳内部词经 SHELL_INTERNAL_MODE_TO_WIRE
175
+ // 归一到 wire 词(manual/dontAsk → default;归一表的方向纪律见该常量 doc),勿改 server 词表;
176
+ // 表外词(未来新增的壳内部模式)经 permissionModeField 双支仍不 stamp = 回落部署基线。
177
+ const normalized = normalizeShellPermissionModeForWire(flagMode);
178
+ return { fields: permissionModeField(normalized, { stampDefault: true }), source: 'flag' };
87
179
  }
88
180
  // 2) env 垫层。
89
181
  const raw = env[HEADLESS_PERMISSION_MODE_ENV]?.trim();
@@ -27,6 +27,17 @@ export declare const HITL_REJECT_MESSAGE = "The user doesn't want to proceed wit
27
27
  /** core 对被 gate/连坐 abort 的 call 铸的 tool_end 载体(逐字;desktop session-host 真引擎实测
28
28
  * 同款)——HOLD 谓词锚它做**精确等值**,普通工具错的输出是各自错误文案,永不进 HOLD。 */
29
29
  export declare const ENGINE_ABORT_TOOL_RESULT = "Operation aborted";
30
+ /**
31
+ * tool_end.output 的文本归一(P0 案B,2026-08-03,server 6.0.0 wire 实测):sync leg 的毒化帧
32
+ * `output` 不再是裸 string,而是 content-block 数组 `[{"type":"text","text":"Operation aborted"}]`
33
+ * (8790 裸引擎 POST /v1/tasks/stream 逐帧记录字节形)。abort 标记的**等值锚**必须先归一再比,
34
+ * 否则 6.0.0 下 hold-poison 臂全盲:毒化帧当场上屏 + generic-close 记 ended ⇒ decide 后重放的
35
+ * 真收口帧被 isEnded 早退去重 —— 「按 Yes 屏幕永远停在 Error: Operation aborted」整案复发。
36
+ * 只归一**判定**,帧本身一个字节不改;等值语义不放宽([2084]①-b 收窄仍然成立,负控见 gate ⑩)。
37
+ * 🆕 [C93] 公面导出(0.17.1):web/desktop 被点名「两端务必接」的同款归一各自手抄=漂移温床,
38
+ * 导出做单一真源;签名不变,加导出=additive。
39
+ */
40
+ export declare function toolEndOutputText(output: unknown): string | undefined;
30
41
  /** 本桥消费的 wire 面(@sema-agent/sdk AgentClient 的结构切片,mock 可注入)。 */
31
42
  export interface AskGateWireDeps {
32
43
  /** approvals.list/decide + assistant(HitlBridge 的 client 切片)。 */
@@ -21,8 +21,10 @@ export const ENGINE_ABORT_TOOL_RESULT = 'Operation aborted';
21
21
  * 否则 6.0.0 下 hold-poison 臂全盲:毒化帧当场上屏 + generic-close 记 ended ⇒ decide 后重放的
22
22
  * 真收口帧被 isEnded 早退去重 —— 「按 Yes 屏幕永远停在 Error: Operation aborted」整案复发。
23
23
  * 只归一**判定**,帧本身一个字节不改;等值语义不放宽([2084]①-b 收窄仍然成立,负控见 gate ⑩)。
24
+ * 🆕 [C93] 公面导出(0.17.1):web/desktop 被点名「两端务必接」的同款归一各自手抄=漂移温床,
25
+ * 导出做单一真源;签名不变,加导出=additive。
24
26
  */
25
- function toolEndOutputText(output) {
27
+ export function toolEndOutputText(output) {
26
28
  if (typeof output === 'string')
27
29
  return output;
28
30
  if (!Array.isArray(output))
@@ -50,7 +50,10 @@ export type GateOutcome = {
50
50
  * 🔴 方向没变:这条判**错**的后果不对称 —— 误判成「已解决」最多让流多读一轮(hop 预算兜底),
51
51
  * 误判成「真失败」会把一个已经 completed 的 run 谎报成失败并烧掉整条会话转录。所以宁可宽。
52
52
  */
53
- /** #114/[C68] 单源化提升:desktop 照抄件(durable 双实现审计 P0-1 修)到货换包导入删抄件。 */
53
+ /** #114/[C68] 单源化提升:desktop 照抄件(durable 双实现审计 P0-1 修)到货换包导入删抄件。
54
+ * [C106] 收编 `not found` 形(2026-08-05):引擎对已批门的 deny 回 `approval not found`
55
+ * (desktop 实测——parked 刚批完、decide 未返回的竞态窗内 Stop 走 parked-deny 路),旧词表
56
+ * 判不中 ⇒ 按真失败 log-only 放弃 ⇒ runs.cancel 永不发出=Stop 哑弹。按上方「宁可宽」方向收编。 */
54
57
  export declare function isAlreadyResolvedGateReason(reason: unknown): boolean;
55
58
  /** 把 wire 答案({answers:[{header,selected,note?}]})折回 CC 卡片的 Record<question,string> 形状
56
59
  * (multiSelect 与对话框同款 ", " lossy join;note → annotations.notes)。 */
@@ -20,12 +20,15 @@ function isGateFailureCode(v) {
20
20
  * 🔴 方向没变:这条判**错**的后果不对称 —— 误判成「已解决」最多让流多读一轮(hop 预算兜底),
21
21
  * 误判成「真失败」会把一个已经 completed 的 run 谎报成失败并烧掉整条会话转录。所以宁可宽。
22
22
  */
23
- /** #114/[C68] 单源化提升:desktop 照抄件(durable 双实现审计 P0-1 修)到货换包导入删抄件。 */
23
+ /** #114/[C68] 单源化提升:desktop 照抄件(durable 双实现审计 P0-1 修)到货换包导入删抄件。
24
+ * [C106] 收编 `not found` 形(2026-08-05):引擎对已批门的 deny 回 `approval not found`
25
+ * (desktop 实测——parked 刚批完、decide 未返回的竞态窗内 Stop 走 parked-deny 路),旧词表
26
+ * 判不中 ⇒ 按真失败 log-only 放弃 ⇒ runs.cancel 永不发出=Stop 哑弹。按上方「宁可宽」方向收编。 */
24
27
  export function isAlreadyResolvedGateReason(reason) {
25
28
  if (typeof reason !== 'string')
26
29
  return false;
27
30
  const r = reason.toLowerCase();
28
- return r.includes('no pending checkpoint') || r.includes('resolved') || r.includes('already');
31
+ return r.includes('no pending checkpoint') || r.includes('resolved') || r.includes('already') || r.includes('not found');
29
32
  }
30
33
  /** 已解决判据(REF-CC-033):code 在场就只信 code,不在场才落回文案兜底(fs 审批腿的
31
34
  * `FsApprovalOutcome` 今天没有 code 字段——C-bridge 卡域,退役条件 = 那边也补上 code)。
package/dist/index.d.ts CHANGED
@@ -213,6 +213,8 @@ export * from './hooksWireCaps.js';
213
213
  export * from './liveInitToolFace.js';
214
214
  export * from './model/providerPresets.js';
215
215
  export * from './hitl/hitlBridge.js';
216
+ export { toolEndOutputText, ENGINE_ABORT_TOOL_RESULT } from './hitl/frameRouter.js';
217
+ export { isAskTool } from './hitl/frameRouter.js';
216
218
  export * from './hitl/hitlHostSurface.js';
217
219
  export * from './hitl/toolApprovalWire.js';
218
220
  export * from './hitl/askGateWire.js';
package/dist/index.js CHANGED
@@ -294,6 +294,13 @@ export * from './model/providerPresets.js';
294
294
  // 共享宿主通知面 + cancel-by-deny 有界观察 —— `askGateWire.ts` 已 import `toolApprovalWire.ts`,
295
295
  // 反向 import 会成环,所以两条腿共用的那一组挪进一个两边都能到达的独立模块。
296
296
  export * from './hitl/hitlBridge.js';
297
+ // [C93] P0 直讨(0.17.1):toolEndOutputText 单一真源导出——web/desktop 被点名「两端务必接」的
298
+ // 6.0.0 output content-block 归一,此前 frameRouter 内私有,两端手抄=漂移温床。frameRouter 只
299
+ // 挑名导出这一件(整星导出会把路由内部面全泄进公面)。
300
+ export { toolEndOutputText, ENGINE_ABORT_TOOL_RESULT } from './hitl/frameRouter.js';
301
+ // 0.18.0:isAskTool 规范判据导出(壳 askParkReopen 抄件收编——toolName 是 UNTRUSTED wire 位,
302
+ // approvals.list 行不经 normalizeToolNames,判据必须单源;壳到货换 import 删抄件)。
303
+ export { isAskTool } from './hitl/frameRouter.js';
297
304
  export * from './hitl/hitlHostSurface.js';
298
305
  export * from './hitl/toolApprovalWire.js';
299
306
  export * from './hitl/askGateWire.js';
@@ -7,7 +7,11 @@
7
7
  * · `maxWalltimeMs`(**毫秒**;5.7 的 `limits.timeoutSec` 已删,单位一并变)
8
8
  * · `maxTurns`
9
9
  * · `maxOutputTokens`(5.8.0 的**主限**——time 退出任务轴之后,token 是首选的预算旋钮;
10
- * wire 键名自 5.7 起一直是 maxOutputTokens,core 内部才叫 maxTokens——[2569] 案订正)
10
+ * wire 键名自 5.7 起一直是 maxOutputTokens,core 内部才叫 maxTokens——[2569] 案订正)
11
+ * 🔴 到限形订正([2592]/[2593] 案):这是**单请求输出上限**不是累计预算——撞线=生成中被
12
+ * 截断(工具调用参数途中被砍以 provider 截断披露上浮,core 3dfdb8f 起披露点名 cap),
13
+ * **不出** `limits.max_tokens_exceeded`(那是 core 内部累计预算键 `maxTokens` 的码,本 wire
14
+ * 从不发那个键 ⇒ 消费端别拿那个码当 --max-tokens 的到限判据)。
11
15
  * 到限语义也换了:**没有配速机器了**。5.8.0 删掉了 deadlineNudge / callCapByDeadline /
12
16
  * gracefulFinalize 那一套(以及引擎侧 1000 轮安全网、fork 200、session-bg 30min 暗注等全部隐式
13
17
  * 默认),走到 `maxWalltimeMs` 不再是「引擎自己减速然后优雅收尾」,而是**响亮终局**:
@@ -7,7 +7,11 @@
7
7
  * · `maxWalltimeMs`(**毫秒**;5.7 的 `limits.timeoutSec` 已删,单位一并变)
8
8
  * · `maxTurns`
9
9
  * · `maxOutputTokens`(5.8.0 的**主限**——time 退出任务轴之后,token 是首选的预算旋钮;
10
- * wire 键名自 5.7 起一直是 maxOutputTokens,core 内部才叫 maxTokens——[2569] 案订正)
10
+ * wire 键名自 5.7 起一直是 maxOutputTokens,core 内部才叫 maxTokens——[2569] 案订正)
11
+ * 🔴 到限形订正([2592]/[2593] 案):这是**单请求输出上限**不是累计预算——撞线=生成中被
12
+ * 截断(工具调用参数途中被砍以 provider 截断披露上浮,core 3dfdb8f 起披露点名 cap),
13
+ * **不出** `limits.max_tokens_exceeded`(那是 core 内部累计预算键 `maxTokens` 的码,本 wire
14
+ * 从不发那个键 ⇒ 消费端别拿那个码当 --max-tokens 的到限判据)。
11
15
  * 到限语义也换了:**没有配速机器了**。5.8.0 删掉了 deadlineNudge / callCapByDeadline /
12
16
  * gracefulFinalize 那一套(以及引擎侧 1000 轮安全网、fork 200、session-bg 30min 暗注等全部隐式
13
17
  * 默认),走到 `maxWalltimeMs` 不再是「引擎自己减速然后优雅收尾」,而是**响亮终局**:
@@ -102,9 +106,11 @@ const MAX_TURNS_USAGE = 'sema: --max-turns expects a positive whole number of tu
102
106
  ' enforced engine-side on the sema seam)\n' +
103
107
  ' (no flag) no turn cap — today\'s behaviour';
104
108
  const MAX_TOKENS_USAGE = 'sema: --max-tokens expects a positive whole number of tokens.\n' +
105
- ' --max-tokens <n> token budget for this run (sema superset — no upstream equivalent;\n' +
106
- ' the engine\'s primary run-level budget knob since core 5.8.0)\n' +
107
- ' (no flag) no token budget today\'s behaviour';
109
+ ' --max-tokens <n> per-request output-token cap (sema superset — no upstream equivalent):\n' +
110
+ ' caps each model response, NOT a cumulative run budget; hitting it cuts\n' +
111
+ ' generation mid-stream (a cut inside tool-call arguments surfaces as a\n' +
112
+ ' provider truncation error)\n' +
113
+ ' (no flag) no output-token cap — today\'s behaviour';
108
114
  /** STRICT integer shape for explicit flag values: ASCII digits only (no sign/decimal/exponent/space). */
109
115
  const INT_RE = /^\d+$/;
110
116
  // `lastFlagValue` (the `--name <v>`/`--name=<v>`/repeated-flag-last-wins scan) now lives in
@@ -27,10 +27,17 @@ export interface PermissionModeField {
27
27
  * service interprets); `default`/unknown/undefined ⇒ no stamp (the deployment baseline governs). No policy baked
28
28
  * here — relevance + tighten-only governance is the service's job.
29
29
  *
30
- * `stampDefault`([830]① server 1.191 五模式解释层):server 侧 absent-mode 不挂门(存量 headless
31
- * 零回归),manual(=CC default)要挂 fs 写 ask 门必须显式 stamp `permissionMode:"default"`。只有
32
- * 三选卡真能弹出来的调用方(交互 REPL,leader ToolUseConfirm 队列已注册)才传 true —— print/headless
33
- * 不 stamp;老 server 对 "default" 走 basePolicy(与 absent 等价),version-safe。
30
+ * `stampDefault`([830]① server 1.191 五模式解释层):server 侧 absent-mode 不挂门,manual(=CC
31
+ * default)要挂 fs 写 ask 门必须显式 stamp `permissionMode:"default"`。老 server 对 "default" 走
32
+ * basePolicy( absent 等价),version-safe。
33
+ *
34
+ * 🔴 现有三个 `stampDefault:true` 调用点(2026-08-05 起,别再按「只有交互 REPL 传 true」读这段——
35
+ * 那句旧话在 [2713] 之前就已被 env 支推翻,照它去「恢复」旧形 = 静默重开 P0 断链):
36
+ * · 交互 REPL(seamQuery,leader ToolUseConfirm 队列已注册 ⇒ 三选卡真弹得出来);
37
+ * · headless env 支(headlessPermissionModeWire,SEMA_HEADLESS_PERMISSION_MODE = 显式垫层旋钮);
38
+ * · headless flag 支([2713] 根修:`--permission-mode default|manual|dontAsk` 显式表态必须 stamp,
39
+ * 否则 server `deriveSettingsPolicy` 因 `!perms` 返回 `{}` ⇒ print 车道**全无客户端门**)。
40
+ * 不传 `stampDefault` 的调用方(缺省 false)= 「没有表态」,继续吃部署基线,行为逐字不变。
34
41
  */
35
42
  export declare function permissionModeField(mode: string | undefined, opts?: {
36
43
  stampDefault?: boolean;
@@ -30,10 +30,17 @@ const KNOWN = new Set([
30
30
  * service interprets); `default`/unknown/undefined ⇒ no stamp (the deployment baseline governs). No policy baked
31
31
  * here — relevance + tighten-only governance is the service's job.
32
32
  *
33
- * `stampDefault`([830]① server 1.191 五模式解释层):server 侧 absent-mode 不挂门(存量 headless
34
- * 零回归),manual(=CC default)要挂 fs 写 ask 门必须显式 stamp `permissionMode:"default"`。只有
35
- * 三选卡真能弹出来的调用方(交互 REPL,leader ToolUseConfirm 队列已注册)才传 true —— print/headless
36
- * 不 stamp;老 server 对 "default" 走 basePolicy(与 absent 等价),version-safe。
33
+ * `stampDefault`([830]① server 1.191 五模式解释层):server 侧 absent-mode 不挂门,manual(=CC
34
+ * default)要挂 fs 写 ask 门必须显式 stamp `permissionMode:"default"`。老 server 对 "default" 走
35
+ * basePolicy( absent 等价),version-safe。
36
+ *
37
+ * 🔴 现有三个 `stampDefault:true` 调用点(2026-08-05 起,别再按「只有交互 REPL 传 true」读这段——
38
+ * 那句旧话在 [2713] 之前就已被 env 支推翻,照它去「恢复」旧形 = 静默重开 P0 断链):
39
+ * · 交互 REPL(seamQuery,leader ToolUseConfirm 队列已注册 ⇒ 三选卡真弹得出来);
40
+ * · headless env 支(headlessPermissionModeWire,SEMA_HEADLESS_PERMISSION_MODE = 显式垫层旋钮);
41
+ * · headless flag 支([2713] 根修:`--permission-mode default|manual|dontAsk` 显式表态必须 stamp,
42
+ * 否则 server `deriveSettingsPolicy` 因 `!perms` 返回 `{}` ⇒ print 车道**全无客户端门**)。
43
+ * 不传 `stampDefault` 的调用方(缺省 false)= 「没有表态」,继续吃部署基线,行为逐字不变。
37
44
  */
38
45
  export function permissionModeField(mode, opts) {
39
46
  if (typeof mode === 'string' && mode !== 'default' && KNOWN.has(mode)) {
package/dist/seam.d.ts CHANGED
@@ -120,12 +120,18 @@ export type ChromeEvent = {
120
120
  outputPath?: string;
121
121
  }
122
122
  /**
123
- * B5 新臂(#117a,E 层 Bash 分臂的**执行半场**)—— 模型发起的后台 Bash 回执被认出来了。
124
- * 判定在库(`detectEngineBgShellReceipt`,纯文案锚定 + `run_in_background` 门);执行留宿主:
123
+ * B5 新臂(#117a,E 层 Bash/Monitor 分臂的**执行半场**)—— 模型发起的后台回执被认出来了。
124
+ * 判定在库(`detectEngineBgShellReceipt`,纯文案锚定,**按工具名正向枚举分臂**:
125
+ * `Bash` 臂 = 三形回执首句 + `run_in_background === true` 门;`Monitor` 臂(#158)= 第四形
126
+ * 回执首句 `Monitoring in background; task_id=…`,**没有** `run_in_background` 门
127
+ * (Monitor 天然后台,参数里根本没这个键);其它工具名一律不铸行);执行留宿主:
125
128
  * 宿主消费义务 = ① 把 registration 发给自己的后台 shell 面板 store(壳 =
126
129
  * `publishEngineBgShellPanelEvent`)② 登记「句柄 → 宿主 run」映射(壳 = `recordBgParentRun(
127
130
  * taskId, getActiveEngineTaskId())`,[1501] A/B 寻址的硬证据)。
128
- * 缺席 = ctrl+B 面板看不到这条后台命令(不是报错,是这一面在该宿主上哑掉)。
131
+ * 🔴 载荷 = 本包 `EngineBgShellRegistration`,**0.18.0 起带必填 `source: 'bash' | 'monitor'`**
132
+ * 行家族判别位(闭集)—— 消费端要分流行形/行文案就读它,别按 command 文本猜;把 monitor 行
133
+ * 一律贴成 bash 行是产者/消费者字段谎报(名义口径不符)。
134
+ * 缺席 = ctrl+B 面板看不到这条后台命令/监视(不是报错,是这一面在该宿主上哑掉)。
129
135
  */
130
136
  | {
131
137
  kind: 'bgshell_register';
@@ -161,27 +161,39 @@ modelText?: string): {
161
161
  isError?: boolean;
162
162
  } | null;
163
163
  /**
164
- * #117a —— **模型发起**的后台 Bash 回执探测器(cli `engineBgShellPanelStore.detectEngineBgShellReceipt`
165
- * 逐字)。引擎侧秒关这张卡、从不进壳的 LocalShellTask registry,只能从回执文案桥过去。
166
- * 🔴 **刻意**要求 INPUT `run_in_background === true`:ctrl+B 的 detach lane
167
- * (「Command moved to background」)归 engineToolDetach 管(它的行生命周期是有意的 settle-on-detach),
168
- * 在这里重复登记会长出双行。
164
+ * #117a / #158 —— **模型发起**的后台回执探测器。引擎侧秒关这张卡、从不进壳的 LocalShellTask
165
+ * registry,只能从回执文案桥过去。
166
+ * 🔴 **按 `toolName` 正向枚举分臂**(#158;开集入参,认不出的工具名一律 `null` = 不铸行,
167
+ * 安全方向是漏检不是误检):
168
+ * · `Bash` 臂 —— 三形回执首句 + **刻意**要求 INPUT 上 `run_in_background === true`:ctrl+B 的
169
+ * detach lane(「Command moved to background」)归 engineToolDetach 管(它的行生命周期是有意的
170
+ * settle-on-detach),在这里重复登记会长出双行。
171
+ * · `Monitor` 臂 —— 第四形回执首句 `Monitoring in background; task_id=…`,**没有**
172
+ * run_in_background 门:Monitor 天然后台,它的参数里根本没有这个键(此前三重门全拒 ⇒
173
+ * 引擎 Monitor 任务在 ctrl+B 面板零行)。
174
+ * 产物上的 `source: 'bash' | 'monitor'` 是行家族判别位(闭集),消费端按它分流行形,别按 command
175
+ * 文本猜;新工具要进面板必须显式加臂 + 扩这个闭集。
169
176
  * 判定在库、**执行留宿主**(面板 store 写入 + 宿主 run 映射)—— 走 chrome `bgshell_register` 臂。
170
- * 🔴 正则复用 `THREE_GEN_BG_RECEIPT_RE`(见上方 T14 `parseBackgroundReceipt` 头注,REF-CC-域词表-04
171
- * dedupe)—— 此前这里另抄了一份逐字节相同的字面量,两份手抄件靠"碰巧没人改坏"对齐。
177
+ * 🔴 Bash 臂正则复用 `THREE_GEN_BG_RECEIPT_RE`(见上方 T14 `parseBackgroundReceipt` 头注,
178
+ * REF-CC-域词表-04 dedupe)—— 此前这里另抄了一份逐字节相同的字面量,两份手抄件靠"碰巧没人改坏"
179
+ * 对齐;Monitor 臂用**分开的** `MONITOR_BG_RECEIPT_RE`(职责不交叉:THREE_GEN 还被
180
+ * `parseBackgroundReceipt` 的 bash structured 回落共用,Monitor 不属那条链)。
172
181
  */
173
182
  export interface EngineBgShellRegistration {
174
183
  kind: 'register';
175
184
  /** 回执上的**引擎** task id(`task_id=…`,TaskOutput/TaskStop 的句柄)。 */
176
185
  taskId: string;
177
- /** Bash tool_use input 的 command(面板行的命令行)。 */
186
+ /** tool_use input 的 command(面板行的命令行;Bash 与 Monitor 同键)。 */
178
187
  command: string;
179
- /** 模型给了 description 才有。 */
188
+ /** 模型给了 description 才有(Monitor 的 schema 里必填,恒在)。 */
180
189
  description?: string;
181
190
  /** 引擎模型面回执原文,逐字 —— 详情视图显示它(里面的 output-file 路径因此可见可复制)。 */
182
191
  receipt: string;
192
+ /** #158 行家族判别:'bash'=run_in_background Bash;'monitor'=Monitor 长跑监视(事件流语义,
193
+ * 终局才 settle)。消费端按此分流行形;闭集,新工具要进面板必须显式加臂。 */
194
+ source: 'bash' | 'monitor';
183
195
  }
184
- export declare function detectEngineBgShellReceipt(rawInput: unknown, outputText: string | undefined): EngineBgShellRegistration | null;
196
+ export declare function detectEngineBgShellReceipt(toolName: string, rawInput: unknown, outputText: string | undefined): EngineBgShellRegistration | null;
185
197
  /**
186
198
  * TodoWrite 清单卡:引擎不发 todo structured,全量新表骑在 tool_use INPUT 上 ⇒ 从它合成 typed 结果。
187
199
  * `oldTodos` = 上一次的活体清单(调用方持:一个 session 一份,正是划删线 diff 要的粒度)。
@@ -6,7 +6,7 @@
6
6
  * · **B 层**(`:608 structuredToToolUseResult`,按 `structured.type` 14 case)—— 结构化细节 → 逐工具
7
7
  * typed `toolUseResult`(卡的 outputSchema safeParse 过了才渲富卡);
8
8
  * · **E 层**(按工具名分臂)—— TodoWrite / ReportFindings 的富卡片补位(本文件),
9
- * Remember→memory_saved 与 #117a bg Bash 桥在 adapt.ts 的臂上(它们产的是消息/chrome,不是卡体);
9
+ * Remember→memory_saved 与 #117a/#158 bg Bash/Monitor 桥在 adapt.ts 的臂上(它们产的是消息/chrome,不是卡体);
10
10
  * · **T7**(`:314 synthesizeToolResultBody`)—— §8-4 已裁:**不搬合成器**,改「诚实缺席 + degraded 标记」。
11
11
  *
12
12
  * ── 本批的三条实质改动(不是纯搬运,逐条有出处)────────────────────────────────────────────────
@@ -260,6 +260,10 @@ export function parseModelFacingTaskOutput(text) {
260
260
  /** T14 三代文案的正则(单源;REF-CC-域词表-04 dedupe:此前 `parseBackgroundReceipt` 与下方
261
261
  * #117a 的 `detectEngineBgShellReceipt` 探测器各抄一份完全相同的字面量)。 */
262
262
  const THREE_GEN_BG_RECEIPT_RE = /^(?:Started in background|Command running in background|Command moved to background); task_id=([A-Za-z0-9_-]+)/;
263
+ /** #158 —— Monitor 回执首句锚(core tools/monitor.ts 两变体同首句;第四形与上面三形**分锚**,
264
+ * 职责不交叉:THREE_GEN 还被 parseBackgroundReceipt(bash structured 回落)共用,Monitor 不属
265
+ * 那条链。与三形同住「后台回执锚块」这一个登记的家(域词表-09 普查窗口)。 */
266
+ const MONITOR_BG_RECEIPT_RE = /^Monitoring in background; task_id=([A-Za-z0-9_-]+)/;
263
267
  /**
264
268
  * 🔴 **后台回执正则调用计数器**(REF-CC-域词表-04 仪器,与 `modelFacingParseCount` 同款):
265
269
  * 只在测试里读,生产零成本。
@@ -831,13 +835,30 @@ modelText) {
831
835
  return null;
832
836
  }
833
837
  }
834
- export function detectEngineBgShellReceipt(rawInput, outputText) {
838
+ export function detectEngineBgShellReceipt(toolName, rawInput, outputText) {
835
839
  if (typeof outputText !== 'string')
836
840
  return null;
837
841
  const input = (rawInput ?? {});
838
- if (input.run_in_background !== true)
842
+ // 正向枚举按工具分臂(#158:新工具默认不铸行=安全方向)
843
+ let source;
844
+ let m;
845
+ if (toolName === 'Bash') {
846
+ // 🔴 Bash 臂**刻意**要求 run_in_background===true:detach lane(Command moved…)归
847
+ // engineToolDetach,重复登记会长双行(原 #117a 纪律,逐字保留)。
848
+ if (input.run_in_background !== true)
849
+ return null;
850
+ m = THREE_GEN_BG_RECEIPT_RE.exec(outputText);
851
+ source = 'bash';
852
+ }
853
+ else if (toolName === 'Monitor') {
854
+ // #158:Monitor 天然后台(参数无 run_in_background 键),锚它自己的第四形回执首句——
855
+ // 此前三重门全拒 ⇒ 引擎 Monitor 任务面板零行(不可见/不可 tail)。
856
+ m = MONITOR_BG_RECEIPT_RE.exec(outputText);
857
+ source = 'monitor';
858
+ }
859
+ else {
839
860
  return null;
840
- const m = THREE_GEN_BG_RECEIPT_RE.exec(outputText);
861
+ }
841
862
  if (!m?.[1])
842
863
  return null;
843
864
  const command = typeof input.command === 'string' ? input.command : '(engine bash)';
@@ -848,6 +869,7 @@ export function detectEngineBgShellReceipt(rawInput, outputText) {
848
869
  command,
849
870
  ...(description !== undefined ? { description } : {}),
850
871
  receipt: outputText,
872
+ source,
851
873
  };
852
874
  }
853
875
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/client-core",
3
- "version": "0.17.0",
3
+ "version": "0.18.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",
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "peerDependencies": {
30
30
  "@sema-agent/agent-types": ">=0.2.0",
31
- "@sema-agent/sdk": ">=6.2.0"
31
+ "@sema-agent/sdk": ">=6.3.0"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@sema-agent/agent-types": "^0.2.0",