@sema-agent/client-core 0.74.5 → 0.75.1

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/CHANGELOG.md CHANGED
@@ -49,6 +49,51 @@
49
49
  > 挡住 ⇒ 本批把它机械化——④a0 对 `pending` 行**要求段头已是日期形**(`(未发布)` 直接红),阶段一
50
50
  > commit 漏转在发布前就红,不再靠人记。
51
51
 
52
+ ## 0.75.1(2026-09-20)
53
+
54
+ > 主题:**面板反序缓行的残洞**(外部复验读数:短命子代的最终用量在一种到达序下永远交不出)+ **缓行遇同代 running 帧的三向定形** + **`spawnName` 透传与 `agentType` 改读诚实来源**(#969 提货,CC-95)。**patch**:型面 additive(tick 臂 +1 可选位),公面值导出零增;行为面三笔(见下)。接入面 §75。
55
+
56
+ ### Fixed
57
+
58
+ - **反序缓行永远放不出(0.75.0 的残洞;外部复验 D1)**:`tick{UUID, cardBound}` 先上屏、终态行随后被缓住、`end{UUID}` 最后到时,漏斗「三把可能的键」**全查不到** —— 因为后两把只在行帧**真发布过**时才登记,而缓行的定义就是「还没发布」。结果:消费端只得到一条空的 ended 行,那一行唯一带最终用量的帧被吞。现在 `end` 归一加**第四条腿**:按缓行**自带的** wire 侧键(`transcriptId` 主 / 委派卡 id 退路)反查缓行,放行仍走「代际三向判 → 同键核 → 经原漏斗归一」,且**整条腿永不 `return`** —— 原 wire 键那一行自己的 `end` 不会被它吞掉。🔴 候选**多于一条**、或候选放行后会与那条 `end` 分流到两把键时,本包**一条都不放**(宁可晚交用量,也不让一只子代分成两行);那几条行的用量仍缺席,记 KNOWN-LIMITS。
59
+ - **缓着终态行时后到的 `running` 帧被无条件当复活(外部复验 B4)**:最终用量会被陈旧快照顶成小数字。现在三向 —— **可证同代** = 陈旧快照(整帧不落地、零事件、缓行留着、台账保持 settled)/ **可证更新** = 复活(作废缓行)/ **判不出** = 维持 0.75.0(KL-26)。可比性只看**帧自带的**两键(沿用记住的值会把「缺席」变成「相等」);`startedAt` 的形核补齐 `Number.isFinite`,与周期判据同源(族扫同批改齐 fleet 投影与后台视图两处)。
60
+ - **`subagent_lifecycle.agentType` 此前发的是委派卡的 `description`**(不是类型):tick 臂的 `name` 是上游由 `description` 派生的**展示名**,喂进 `agentType` 位后宿主的 SubagentStart hook 收到的是任务描述 —— 一个看起来合理的错答案。现在按**诚实来源**三档取:tick 上的结构读类型键(亲核 sdk 9.8.1:`task_progress` 臂**没有** `agentType` / `spawnAgentType`,今天恒不命中)→ fleet 行帧的 `agentType`(在被 `deriveAgentLabel` 折进 `name` **之前**留一份,新包内叶 `src/fleet/fleetRowAgentType.ts`)→ 都没有 ⇒ **键缺席**。🔴 缺席是新的可能形,端的 hook 侧要有 absent 兜底。
61
+
62
+ ### Added
63
+
64
+ - **`panel_task` tick 臂 `spawnName?`**(#969 提货,additive):结构读 wire `task_progress.spawnName`,非空串才带;**缺席 = 产生者还没有这一位**(旧 core / 旧 server),不是「没有名字」,也绝不用展示名 `name` 顶替。与 `name` 是两位:一个是展示名,一个是派生这只子代时被声明的名字。
65
+
66
+ ### Changed
67
+
68
+ - 0.75.0 发车文案三处更正(已发段是冻结面,一字不动,更正记在本段):① 「`.d.ts` 改五只」实测**四只**(重跑请求要由两版 dist 逐文件 diff 反推,不按族概括);② `panelRunningHistory.d.ts` 的读口改名属**包内叶面**,不出公面 —— 「型面 additive」那句只覆盖公面,叶面这一笔当时没写;③ 「三把可能的键」的真实射程 = 行**没发布过**时只有第一把有效(本版修掉后头注已改成实情,并加了第四条腿)。
69
+
70
+ ### Guards
71
+
72
+ - panel-cycle 门 96 → **170 checks**(D1 两序与退路键 / 不张冠李戴 / B4 三向 / 多候选一条都不放 / 缓行真作废 / `spawnName` 与 `agentType` 三档);变异 19 枚(18 见红 + 1 可证等价);异源对抗复审三轮每轮一条 [high] 全采修(多缓行别名覆盖 / 递归派发吞掉 wire 行的 end / 候选与 end 分流到两把键)。diff 门加一只壳侧版本时差归一器(带反空转与退役条件,**刻意不进** `ADAPTER_DIVERGENCES` —— 它不是有意偏离基线)。
73
+
74
+ ## 0.75.0(2026-09-20)
75
+
76
+ > 主题:**压缩的非 compacted 结局上 chrome 面**(CC-89;此前三端都看不见压缩失败)+ **peer sdk 地板 `>=9.8.1`**(CC-81)+ **短命子代的最终用量不再丢**(0.74.5 首见即终态零事件的代价面,外部复验读数)。**minor**:① 新内部臂 `compaction_outcome` 进 `INTERNAL_SDK_ARM_TYPES` —— 自建 print / stream-json 出口的端**必须**先给它表态(吞或转),否则它会顺着兜底透传成越界帧;② peer 地板抬升。型面 additive(`ChromeEvent` +1 臂 `CompactionOutcomeChromeEvent`;值导出零增)。接入面 §74。
77
+
78
+ ### Added
79
+
80
+ - **chrome 臂 `compaction_outcome`(CC-89)**:引擎的 `compaction_outcome` 帧(一次压缩**没有**以 compacted 收尾:被任务终态抢先而作废、熔断开着而失败等)此前在投影口落 `not_in_slice`,包内与三端零消费者 —— 压缩失败对用户不可见。现在:投影口出内部臂 `{type:'compaction_outcome', outcome, trigger?, reason?, …identity}`,适配层出 `ChromeEvent{kind:'compaction_outcome', laneProof, outcome, trigger?, reason?, eventId?}`,转录面零行(CC 转录里只有成功压缩的 `compact_boundary`,不替它编消息)。`outcome` / `trigger` 是引擎开集原词,逐字不折;`trigger` / `reason` 非空串才在场;`outcome` 缺席 / 非串 / 空串 ⇒ malformed。lane 如实:`parentToolCallId` 在场 ⇒ `laneProof.lane === 'subagent'`;只能靠 `sourceTaskId` / `bgAgentId` 归因的帧不发(没有诚实的 lane 可挂,不落到主会话名下;KNOWN-LIMITS KL-22)。臂表 `required:false`。
81
+
82
+ ### Fixed
83
+
84
+ - **短命子代的最终用量不再被「首见即终态零事件」吞掉(0.74.5 CC-86 的代价面;外部复验 W2 读数)**:子代在面板两拍之间起步并结束、从没投影过 running 时,通知车道的 `end` 已递达(消费端建了行、状态 ended),终态 `fleet-row` 却被当成历史行零事件 ⇒ 行在、`totalTokens` / `toolUses` 两位空。「首见」的判据从「本进程没发布过 running」改成「消费端没有这一行」= 没发布过 running **也没递达过 end**:历史叶加第二本账(漏斗里任何 lane 的当代 `end` 递达后记 ended{周期身份},有界 4096)。结过的行再来终态帧:同代 / 判不出代际 ⇒ 恰一条 `fleet-row` 交最终用量、**不再第二次 `end`**;可证更新的一代(结过之后复活又结束)⇒ 照发 `fleet-row` + `end`。反序(终态 `fleet-row` 先到、`end` 后到)⇒ 终态行先缓住(有界 4096),`end` 到漏斗时**先于一切身份归一**放出(放出的行帧登记的钥匙 / 所有权,后面的 `end` 归一必须看到)、按代际 peek 后再放(`end` 可证更旧 ⇒ 缓行留着;可证更新 ⇒ 作废;同代 / 判不出 ⇒ 放);复活为 running 作废缓行;真历史行永远等不到 `end`,不上屏。结过的行再来的终态帧按**方向**判:可证更旧 ⇒ 整帧丢(不倒写用量、不第二次 `end`);ended 账单调写、`end` 的陈旧判据同时参考行帧水位与 ended 账(短命子代常没有行帧水位)。0.74.5 的两格「零事件」改口为「零 `end`、至多一条 `fleet-row`」。
85
+ - `compacted.freedTokens` 负值 / 非有限在本包按缺席(不夹到 0):注释此前写「夹 ≥ 0」与代码不符,改注释不改代码 —— 缺席比编一个 0 诚实(外部复验读数)。
86
+
87
+ ### Changed
88
+
89
+ - **peer `@sema-agent/sdk` 地板 `>=9.7.1` → `>=9.8.1`(CC-81)**:9.8.x 给 `Capabilities.deviceExecutor` 的对象臂声明了 `management?: boolean`(本包 0.74.2 起的读器此前按结构读),包现在对 9.8.1 编译,消费端实装全在 9.8.1 ⇒ 9.7.1 失去物料见证。读器运行时行为零变化(入参仍按不可信结构防御读);device 读器门 +7 格把 sdk 声明形(可选键 / false 臂 / `management` 可选布尔;readonly / `| undefined` 算兼容形)钉住,声明删除 / 行注释伪证 / string 退化 / 改必填 各一枚负控;读器本身仍按不可信结构读,关联只在门。
90
+ - 两只测试钩(`__resetEngineAgentPanelIdentityForTests` / `__resetPanelRunningHistoryForTests`)的注释逐表列出清什么、不清什么,并写明连续场景中途别调(外部验收方的请求)。
91
+
92
+ ### Guards
93
+
94
+ - panel-cycle 门 +19 格(S1v / S1w / S1x / S1y / S1z 族:两序 / 真历史行 / 复活新一代 / 前代 end 不放当代缓行 / 前代迟到帧不作废缓行;S1α–S1δ 族:异源对抗复审第三轮四条 [high] 反例 —— 缓行放出后重新归一 / 迟到前代终态帧整帧丢 / ended 账不倒写 / 前代 end 不取走当代缓行)、pure 门与身份归一门两格改口;singleton +2 high(ended 账 / 缓行表,上限 116 → 118)。
95
+ - 新门 `scripts/run-compaction-outcome-projection-test.mjs` 18 格(投影口 10 / 适配层 8;异源对抗复审 R1 [medium] 采修 —— 投影器不浅拷帧:展开会擦掉原型链上的身份键、还执行无关的可枚举 getter,与兄弟臂同律逐键读);device 读器门 +7(T1–T7);sdk 地板门 FLOOR 9.8.1,负控锚同批。新门 A3 改成同序列有 / 无结局帧的转录面逐字对拍(异源对抗复审 R2 [medium]:只排除同名 type 的行守不住「零新增转录行」)。
96
+
52
97
  ## 0.74.5(2026-09-19)
53
98
 
54
99
  > 主题:**core 7.24.0 提货**(CC-85 `compacted.freedTokens?`)+ **首见即终态的行零事件**(CC-86;cli L-415 幽灵完成行)+ **`isNewEngineAgentPanelCycle` 上公面**(CC-87)。**patch**:型面 additive(+2 常量 +2 函数,公面 1061 → 1065;`compact_metadata._sema_freed_tokens?` 超集键;零 wire 新键 —— `freedTokens` 由 core 7.24.0 铸;peer 不动)。接入面 §73。
package/README.md CHANGED
@@ -35,7 +35,7 @@ Renamed from **`@sema-agent/wire-cc-adapter`** (0.1.x, deprecated — see *Migra
35
35
 
36
36
  ## Scope
37
37
 
38
- **Version:** 0.74.5
38
+ **Version:** 0.75.1
39
39
 
40
40
  - **Today** — the adapter seam, the whole `adapt()` pipeline (all 14 A-layer arms plus the
41
41
  B/D/E tool-card layers), the notification/caps/model families, the adapter kernel (stream driver
@@ -67,7 +67,7 @@ Renamed from **`@sema-agent/wire-cc-adapter`** (0.1.x, deprecated — see *Migra
67
67
  against — the tables live upstream precisely so this package does not keep a second copy that can
68
68
  fall behind. The browser bundle really bundles the SDK through (the portability guard would
69
69
  exit 3 rather than quietly mark it external).
70
- - The declared floor is `>=9.7.1` (raised from `>=9.6.0` in 0.74.0: `Capabilities.approvalsStreamLive` / `.executionLane`, `LivePendingRow.frame`, the `live_*` approval-stream events and `gates[].toolCallId` are typed from 9.7.x on, and no consumer ships 9.6.0 any more, so the older floor lost its witness; before that raised from `>=9.4.0` in 0.71.0: the `tool_disclosure` / `tool_progress` frames and `ToolApprovalFrame.readRootCandidate` are typed there; earlier: raised from `>=8.8.0` in 0.69.0: the `reasoning_end` frame and `McpStatusPanel.lastLegMcp` are typed from 9.4.0 on), and it is *witnessed*: the guard checks that an actually
70
+ - The declared floor is `>=9.8.1` (raised from `>=9.7.1` in 0.75.0: `Capabilities.deviceExecutor.management` is typed from 9.8.x on, the package now compiles against 9.8.1, and no consumer ships 9.7.x any more, so the older floor lost its witness; before that raised from `>=9.6.0` in 0.74.0: `Capabilities.approvalsStreamLive` / `.executionLane`, `LivePendingRow.frame`, the `live_*` approval-stream events and `gates[].toolCallId` are typed from 9.7.x on, and no consumer ships 9.6.0 any more, so the older floor lost its witness; before that raised from `>=9.4.0` in 0.71.0: the `tool_disclosure` / `tool_progress` frames and `ToolApprovalFrame.readRootCandidate` are typed there; earlier: raised from `>=8.8.0` in 0.69.0: the `reasoning_end` frame and `McpStatusPanel.lastLegMcp` are typed from 9.4.0 on), and it is *witnessed*: the guard checks that an actually
71
71
  installed SDK at that line still exports every value-level symbol this package imports and still
72
72
  declares `TaskStats.costMicroUsd` (the key `costOrNull` reads). A floor nobody ever ran is a
73
73
  promise, not a contract.
@@ -317,8 +317,9 @@ public-surface guard checks that last one).
317
317
  | `scripts/run-run-cancel-context-test.mjs` | The run record's `cancelContext` side-note (engine ≥7.87.3) read structurally, and the cause of a `turn_aborted{engine_error}` classified from machine-readable evidence only: `cancelled` (code `cancelled`, with the cancel-time context when present) / `engine_error` (any other failure code, passed through verbatim) / `run_still_live` (the record is not terminal — a dropped stream is a client-side fact, not the run's cause) / `unknown` (never guessed). An absent `cancelContext` reads as *not reported*, never as "not cancelled"; `elapsedMs` is never folded to 0. |
318
318
  | `scripts/run-suspended-reopen-projection-test.mjs` | The durable `suspended` event's `reopened` key read as three distinct states — `reopened` (with the engine's code, verbatim), `not_reopened` (an explicit `null`), `unstated` (key absent or unreadable) — and carried on the HITL bridge's active gate (`currentGateReopen()`), re-read on every `suspended` and cleared with the gate. |
319
319
  | `scripts/run-panel-identity-normalization-test.mjs` | One background subagent has two ids on the wire — the fleet row id tail and the `task_progress` task id (its transcript id). Every panel event goes through one funnel that rewrites the `tick` / `end` task id onto the fleet row's id once a `fleet-row` has registered the key (`transcriptId` first, `parentToolCallId` as the fallback), carrying the original as `wireTaskId` and marking `taskIdOrigin`; an unbound tick whose row has not arrived yet waits one beat (bounded) and is released verbatim on the next tick / `end`, when the buffer is full, or after `MAX_HELD_WIRE_TICK_BEATS` other fleet-row / end / sweep events (a `sweep` itself leaves it alone: there is no row to settle yet); a normalized `end` that carries no cycle identity borrows the registering row's, so a late close of a revived task is recognized as stale; the key table is an LRU (a task that keeps ticking is never evicted by newer registrations); the residency mark migrates with the id and both keys are cleared on settle — except that a stale (previous-cycle) terminal never clears the revived row's mark — so the notification lane can clear it. Once a tick has been delivered verbatim under its UUID, that UUID is the subagent's key: later fleet rows and fleet-side ends are rewritten onto it (the tail kept in `wireTaskId`), so a consumer sees one row in every arrival order; a late tick from a previous cycle is dropped rather than folded into the revived row. |
320
+ | `scripts/run-compaction-outcome-projection-test.mjs` | The `compaction_outcome` frame (a compaction that did **not** end as compacted: mooted by the task ending, failed, …) projected to an internal arm and then to the additive `compaction_outcome` chrome event — `outcome` required and verbatim (open set), `trigger` / `reason` present only when the engine sent a non-empty string, malformed frames dropped, zero transcript rows, the lane told honestly (`parentToolCallId` ⇒ subagent lane; a frame attributable only by `sourceTaskId` / `bgAgentId` is not surfaced on the main lane). |
320
321
  | `scripts/run-approval-card-retract-test.mjs` | The approval card's **decision-free retraction** and the in-stream frame leg's **outcome hand-back**: a host that must withdraw a card that no longer has a decision channel (session switch, engine switch, a tracker reporting the ask gone) answers `{ kind: 'retracted' }` and the package sends nothing on any of the three legs (in-stream frame, suspended ask, durable park), reporting `decision: 'unresolved'` with a `retracted` flag; `aborted` / `failed` / `deny` keep their meaning (a real deny is still posted), and `onToolApprovalOutcome` hands every in-stream outcome back to the host exactly once, tolerating a throwing or rejecting callback Also the single source for the host-side approval-outcome note (`approvalOutcomeNoteOf`): `settled` is whether the decision was delivered, `retracted` is an independent key present only when the card was retracted, and `detail` is the retraction / edit-refused sentence or the refusal code and message — never a fabricated sentence. |
321
- | `scripts/run-panel-cycle-identity-test.mjs` | The **cycle identity** on agent-panel events and the fleet ledger's **departure read-out**: a background agent may be revived under the same id, so `fleet-row` and `end` events now carry the wire's own `cycleSeq` / `startedAt` when present (absent means the row has no notion of generations, never "generation one"), `isStaleEngineAgentPanelEnd` is the single rule for ignoring a late `end` from a previous cycle (only when both sides carry a comparable identity; absence never drops a real terminal), a changed `cycleSeq` is a new cycle for usage stickiness and buffer coalescing, the notification lane carries `cycleSeq` only when the wire really sent `seq`, and `task_remove` frames reach the host through `onTaskRemoved` with `removeReason` / `cycleSeq` verbatim, a stale previous-generation removal leaving the newer row in place |
322
+ | `scripts/run-panel-cycle-identity-test.mjs` | The **cycle identity** on agent-panel events and the fleet ledger's **departure read-out**: a background agent may be revived under the same id, so `fleet-row` and `end` events now carry the wire's own `cycleSeq` / `startedAt` when present (absent means the row has no notion of generations, never "generation one"), `isStaleEngineAgentPanelEnd` is the single rule for ignoring a late `end` from a previous cycle (only when both sides carry a comparable identity; absence never drops a real terminal), a changed `cycleSeq` is a new cycle for usage stickiness and buffer coalescing, the notification lane carries `cycleSeq` only when the wire really sent `seq`, and `task_remove` frames reach the host through `onTaskRemoved` with `removeReason` / `cycleSeq` verbatim, a stale previous-generation removal leaving the newer row in place. A terminal row held back because the consumer has no such row yet is also released by the keys it carries itself (its transcript id, or the delegating call id of a subagent already on screen under its wire id), since the key tables are only written once a row has actually been published — the release still goes through the one funnel, so the subagent stays one row; a running frame arriving after such a held terminal row is a stale snapshot when both sides carry a comparable generation and it matches (no event, the held row keeps its final usage), a revival when the frame is provably newer (the held row is dropped), and is treated as a revival when neither side can be compared. A subagent lifecycle event carries `agentType` only from an honest source — the fleet row's own agent type, recorded before it is folded into the row label — and omits the key when there is none, never substituting the display name |
322
323
  | `scripts/run-workflow-size-warning-test.mjs` | The **workflow size warning** verdict shared by every host footer / panel: a three-state result (`warn` / `ok` / `unknown`) read off the optional fleet view keys, where an unknown size is never reported as a normal one (absent `totalCount` / `tokens` without positive over-cap evidence is `unknown`, naming the missing keys), positive evidence on either axis wins regardless of absent keys, the per-agent denominator uses the engine's started count only when it is not below done+failed (a smaller value is a stale reading), otherwise falls back to the done+failed lower bound only when both keys are present — and a lower-bound denominator only yields an upper bound of the projection, which can prove *within cap* (`ok`, flagged) but never *over cap* (`unknown`, with the upper bound exposed) — and the prior is used only when the engine itself reports zero started agents; cap precedence env > explicit guideline > default, prototype keys never act as a guideline, the env reader is pure and does not fall through to the second name on a bad first value; caps, guideline table, env names and the three copy variants are single-sourced |
323
324
  | `scripts/run-approvals-stream-live-capability-test.mjs` | The engine's live-approval-push self-description (`capabilities.approvalsStreamLive`, engine ≥7.87.1), read the same four-state way as its four sibling capability readers: an absent key is reported as not reported (never folded into `false`), the value must be a strict boolean, and the one decision the feed consumer needs — whether it must keep pulling suspended asks itself — is answered by `livePendingNeedsReconcile`, which only says no when the engine explicitly says it pushes. |
324
325
  | `scripts/run-model-facing-anchors-test.mjs` | Five places where model-facing engine text is parsed back into card slots: the task-output cursor semantics machine-readable key wins over the spool marker text (and its absence is never read as `full`), the `--- result (…) ---` line accepts a parenthesised note, the bash header accepts the engine's exit-note and image variants and carries the note into the same benign-exit slot the structured path uses, the notification seed scan treats both producers' terminal words as terminal, and the parked background-task phrase is recognised. |
@@ -11,6 +11,8 @@ import { isWorkflowAgentTaskId, recordWorkflowAgentTaskId } from '../workflow.js
11
11
  // L-215+(0.65.0;core [6908]):终局状态词的单铸谓词(三处同形一次改齐)。
12
12
  import { isTerminalStatus } from '../runTerminal.js';
13
13
  import { resolveEnginePanelTaskId } from '../engineAgentPanelStore.js';
14
+ // 0.75.1 CC-95(b):子代类型的诚实来源台账(fleet 行帧折进 name 之前留的那一份;包内叶)。
15
+ import { fleetRowAgentType } from '../fleet/fleetRowAgentType.js';
14
16
  import { chrome, MAIN, transcript, messageIdentityOf } from './ids.js';
15
17
  import { CANCEL_MESSAGE, decisionOf, flattenWireOutput, REJECT_MESSAGE, sanitizeToolUseBlock, shortTaskLabel, TASK_TOOL_NAMES, WORKFLOW_TOOL_NAMES, } from './wireShapes.js';
16
18
  // ══ M2/M1 —— assistant(tool_use 半场 + durable 文本/思考半场)══════════════════════════════════
@@ -557,6 +559,35 @@ const contextUsageArm = function* (m) {
557
559
  ...(typeof m.eventId === 'string' && m.eventId.length > 0 ? { eventId: m.eventId } : {}),
558
560
  });
559
561
  };
562
+ /**
563
+ * CC-89(0.75.0):`compaction_outcome` 内部臂 → chrome `compaction_outcome`。每帧一条;转录面零行。
564
+ * 🔴 lane 如实:`parentToolCallId` 非空串 ⇒ subagent;只有 `sourceTaskId` / `bgAgentId`(或父键空串)的帧没有诚实的 lane 可挂 ⇒ 不发,
565
+ * 绝不落到主会话名下。`outcome` 非空串才发(非投影口喂进来的坏帧兜底)。
566
+ */
567
+ const compactionOutcomeArm = function* (m) {
568
+ const str = (v) => (typeof v === 'string' && v.length > 0 ? v : undefined);
569
+ const outcome = str(m.outcome);
570
+ if (outcome === undefined)
571
+ return;
572
+ let laneProof = MAIN;
573
+ if (isSubFlowSegmentEnd(m)) {
574
+ const parent = str(m.parentToolCallId);
575
+ if (parent === undefined)
576
+ return;
577
+ laneProof = { lane: 'subagent', parentToolCallId: parent };
578
+ }
579
+ const trigger = str(m.trigger);
580
+ const reason = str(m.reason);
581
+ const eventId = str(m.eventId);
582
+ yield chrome({
583
+ kind: 'compaction_outcome',
584
+ laneProof,
585
+ outcome,
586
+ ...(trigger !== undefined ? { trigger } : {}),
587
+ ...(reason !== undefined ? { reason } : {}),
588
+ ...(eventId !== undefined ? { eventId } : {}),
589
+ });
590
+ };
560
591
  const textSegmentEndArm = function* (m, { text }) {
561
592
  // 🔴 断闸按「**键在不在**」判,不按「是不是串」判(异源对抗复审第三轮 [medium] 采纳)。
562
593
  // 投影层已对坏 lane 位整帧 fail-closed;这一道是给**非投影口**喂进来的帧(宿主自建管线 /
@@ -794,6 +825,9 @@ const taskProgressArm = function* (m, { ctx, cards, panel, inst }) {
794
825
  // ("Bash npm test"),service 已 redact。PROGRESS 语义(喂状态/摘要显示槽),不是诊断。
795
826
  // 老引擎缺席 ⇒ 两个消费面维持今天的显示。
796
827
  const currentAction = typeof m.currentAction === 'string' && m.currentAction.length > 0 ? m.currentAction : undefined;
828
+ // 0.75.1 CC-95(a)—— `spawnName`:sdk 9.8.1 的 `task_progress` 臂上还没有这一位(新形才铸),所以按
829
+ // **结构读**取(不靠型面),非空串才算数;缺席 = 旧形,不编。
830
+ const spawnName = m.spawnName;
797
831
  // inline 群组行孪生:**绑定了**的 tick 同时喂转录里那条行的统计(按委派卡 id 键)。
798
832
  if (cardId !== undefined) {
799
833
  yield chrome({
@@ -825,15 +859,22 @@ const taskProgressArm = function* (m, { ctx, cards, panel, inst }) {
825
859
  : typeof input.task === 'string'
826
860
  ? input.task
827
861
  : undefined;
862
+ // #6 常驻台账的两个写口整臂让位给 M3(module store 不许两个模块各写各的)。
863
+ // 0.74.3 CC-70:常驻标记在**归一后的键**下(fleet 尾段 = 通知车道清标用的那把);此前 UUID 记、17 位清 ⇒ 永不清。
864
+ // 🔴 0.75.1 CC-95(b):归一必须在 `panel.start` **之前**算 —— 子代类型的诚实来源是 fleet 行帧的 `agentType`,
865
+ // 而那本账按**行 id 尾段**记,拿 wire 的 UUID 是查不到的。
866
+ const identity = resolveEnginePanelTaskId(taskId, explicitParent);
828
867
  // ⟨帧序耦合 5/7⟩ 首 tick 才 fire Start,而通知臂的 if-started 门读的就是它。
829
868
  if (!panel.isLive(taskId)) {
830
869
  // 这条引擎子代在壳侧**可观测的起点** = 本 turn 的首 tick。
831
- const agentType = typeof m.name === 'string' && m.name.length > 0 ? m.name : 'subagent';
832
- yield* panel.start(taskId, agentType);
870
+ // 🔴 0.75.1 CC-95(b)子代**类型**的诚实来源(此前取的是 `m.name` = 上游由委派卡 description 派生的**展示名**
871
+ // ⇒ 宿主的子代启动 hook 收到的是任务描述,一个看起来合理的错答案):
872
+ // ① wire `task_progress` 上的结构读类型键 —— 亲核 sdk 9.8.1 `dist/events.d.ts` 的 `task_progress` 臂:
873
+ // **没有** `agentType`、也没有 `spawnAgentType`,这条 lane 今天不报类型 ⇒ **有出处才读**,所以不读;
874
+ // ② fleet 行帧的 `agentType`(sdk `FleetTaskRow.agentType`,§K-7 短类型 label)—— 按归一后的尾段查;
875
+ // ③ 都没有 ⇒ **键缺席**(宿主 hook 侧按 absent 兜底,不编 `subagent`)。
876
+ yield* panel.start(taskId, fleetRowAgentType(identity.taskId));
833
877
  }
834
- // #6 常驻台账的两个写口整臂让位给 M3(module store 不许两个模块各写各的)。
835
- // 0.74.3 CC-70:常驻标记在**归一后的键**下(fleet 尾段 = 通知车道清标用的那把);此前 UUID 记、17 位清 ⇒ 永不清。
836
- const identity = resolveEnginePanelTaskId(taskId, explicitParent);
837
878
  panel.noteResidency(taskId, identity.taskId);
838
879
  panel.markLive(taskId);
839
880
  yield chrome({
@@ -843,6 +884,9 @@ const taskProgressArm = function* (m, { ctx, cards, panel, inst }) {
843
884
  kind: 'tick',
844
885
  taskId,
845
886
  ...(typeof m.name === 'string' && m.name.length > 0 ? { name: m.name } : {}),
887
+ // 0.75.1 CC-95(a):派生这只子代时被声明的名字(wire `task_progress.spawnName`)。结构读、非空串才带;
888
+ // 缺席 = 旧形的产生者 ⇒ 键不落(不编、不拿展示名 `name` 冒充 —— 两位是两位)。
889
+ ...(typeof spawnName === 'string' && spawnName.length > 0 ? { spawnName } : {}),
846
890
  ...(description !== undefined ? { description } : {}),
847
891
  ...(prompt !== undefined ? { prompt } : {}),
848
892
  ...(currentAction !== undefined ? { currentAction } : {}),
@@ -1269,6 +1313,8 @@ export const ARMS = new Map([
1269
1313
  ['tool_progress', toolProgressArm],
1270
1314
  ['tool_roster_delta', toolRosterDeltaArm],
1271
1315
  ['context_usage', contextUsageArm],
1316
+ // CC-89(0.75.0):压缩的非 compacted 结局(additive;不接 = 压缩失败对用户不可见)。
1317
+ ['compaction_outcome', compactionOutcomeArm],
1272
1318
  ['wiring_manifest', wiringManifestArm],
1273
1319
  // B-078 / L-208(0.65.0):design/172 流内审批两帧 —— 修前它们在投影层就 `dropped`,
1274
1320
  // 壳自己另接一份 ⇒ desktop/web 拿不到卡集与撤卡(归层违例)。
@@ -61,8 +61,12 @@ export interface PanelTaskLedger {
61
61
  /** #6 常驻台账:live-bound ⇒ 清常驻;inert/unbound ⇒ 记常驻(module store 的**唯一**写口之一)。 */
62
62
  /** 0.74.3 CC-70:`residentKey` = 常驻标该记在哪把键下(归一后的 fleet 尾段);缺省 = taskId。绑卡判定仍按 tick 的 taskId。 */
63
63
  noteResidency(taskId: string, residentKey?: string): void;
64
- /** SubagentStart(类型恒记,fire 一生一次)。 */
65
- start(taskId: string, agentType: string): Generator<AdapterOutput>;
64
+ /**
65
+ * SubagentStart(类型恒记,fire 一生一次)。
66
+ * 🔴 0.75.1 CC-95(b):`agentType` **可缺席** —— 调用方查不到诚实来源时就不传,本臂让键缺席,
67
+ * 绝不在这里编一个占位词(此前传的是 tick 的展示名,子代启动 hook 因此收到的是委派卡的 description)。
68
+ */
69
+ start(taskId: string, agentType?: string): Generator<AdapterOutput>;
66
70
  /** 关卡 settle(cardId)或 turn 末/abort 防御 sweep(cardId===null)。 */
67
71
  settle(cardId: string | null, isError: boolean, report?: string): Generator<AdapterOutput>;
68
72
  /** #6 通知-settle 边(推送半场):真终态在此落行,session 常驻行不再被 turn sweep 假结。 */
@@ -108,13 +108,16 @@ export function createPanelTaskLedger(ctx, cards, inst) {
108
108
  },
109
109
  /** SubagentStart(cli fireSubagentStartHook 的臂化):类型恒记,fire 一生一次。 */
110
110
  *start(taskId, agentType) {
111
- subagentTypeById.set(taskId, agentType); // Stop 的类型查找恒记(fire 与否无关)
111
+ // 0.75.1 CC-95(b):类型**在场才记**(缺席不写空串进表 —— stop 那一半读的是同一份记忆,写了空串
112
+ // 就等于把「不知道」渲成了一个词)。Stop 的类型查找恒记(fire 与否无关)。
113
+ if (agentType !== undefined)
114
+ subagentTypeById.set(taskId, agentType);
112
115
  // 🔴 判重 + 记号都在**唯一那条 yield 之前**(矩阵 §2.1②:零残余窗,别挪)。
113
116
  if (inst.hasFiredSubagentStart(taskId))
114
117
  return;
115
118
  inst.markFiredSubagentStart(taskId);
116
- ctx.probe?.('hook', `start ${taskId} type=${agentType}`);
117
- yield chrome({ kind: 'subagent_lifecycle', laneProof: laneOf(taskId), phase: 'start', taskId, agentType });
119
+ ctx.probe?.('hook', `start ${taskId} type=${agentType ?? '-'}`);
120
+ yield chrome({ kind: 'subagent_lifecycle', laneProof: laneOf(taskId), phase: 'start', taskId, ...(agentType !== undefined ? { agentType } : {}) });
118
121
  },
119
122
  /**
120
123
  * 结掉一张关闭中的卡所绑的面板行(`cardId`),或 turn 末/abort 结掉**本 turn 活绑**的全部行
@@ -44,6 +44,8 @@ export const INTERNAL_SDK_ARM_TYPES = new Set([
44
44
  'tool_progress',
45
45
  'tool_roster_delta',
46
46
  'context_usage',
47
+ // CC-89(0.75.0):压缩的非 compacted 结局 → 中性内部臂(print 车道同律:吞或转,裸透传 = 越界帧)。
48
+ 'compaction_outcome',
47
49
  'workflow_complete',
48
50
  'tool_end_result',
49
51
  'retry_status',
@@ -338,8 +340,8 @@ export function eventToSdkMessage(ev, ctx) {
338
340
  // sdk 补上该位的当天,`run-compaction-boundary-projection-test.mjs` F10 行当场红逼复核。
339
341
  case 'compacted': {
340
342
  const raw = ev;
341
- // 0.74.5 CC-85(core 7.24.0 #946):引擎自己测的结构性差值,夹 ≥ 0;旧的 tokensBefore / tokensAfter **不是可相减的一对**。
342
- // 缺席 = 没测出来 ⇒ 键不落(渲「未知」不是 0);在场才带超集键。
343
+ // 0.74.5 CC-85(core 7.24.0 #946):引擎自己测的结构性差值(core 侧夹 ≥ 0);旧的 tokensBefore / tokensAfter **不是可相减的一对**。
344
+ // 缺席 = 没测出来 ⇒ 键不落(渲「未知」不是 0);在场才带超集键。**负值 / 非有限在本包同样按缺席**(不夹到 0 —— 缺席比编一个 0 诚实;test 0.74.5 复验读数)。
343
345
  const freedTokens = typeof raw.freedTokens === 'number' && Number.isFinite(raw.freedTokens) && raw.freedTokens >= 0 ? raw.freedTokens : undefined;
344
346
  const attachedFiles = raw.attachedFiles;
345
347
  const seg = raw.preserved_segment;
@@ -826,8 +828,8 @@ export function eventToSdkMessage(ev, ctx) {
826
828
  case 'failed':
827
829
  return nothing('terminal_delegated');
828
830
  // ── 已入 SDK union、本切片**有意**不投影的臂(REF-CC-060:进 switch 才受穷举保护)────────
829
- // `meta`:流首帧的 taskId/sessionId 一等信号 —— 身份面由 SDK/传输层自己消费(runStream 只按
830
- // `eventSeq` 去重),没有可渲染内容。
831
+ // `meta`:流首帧的 taskId/sessionId 一等信号 —— 本切片无可渲染内容(runStream 只按 `eventSeq` 去重)。
832
+ // ⚠️ 活性普查(2026-09-19)更正:传输层**不**替宿主消费它;需要 sessionId / taskId 的端(desktop / web-client)各自直读本帧。
831
833
  case 'meta':
832
834
  // `file_link` / `prompt_assembled` / `model_usage` / `context_usage` / `config_assembled`:
833
835
  // 引擎的可观测/审计面帧,本切片(CC transcript 投影)无对位渲染物;
@@ -840,12 +842,8 @@ export function eventToSdkMessage(ev, ctx) {
840
842
  case 'config_assembled':
841
843
  // `needs_review`:plan-review park 的**事件**形;壳消费的是 done{status:'needs_review'} 终帧
842
844
  // (runStream 的 plan_review_park chrome 臂),事件形在本切片重复且更早,故不投影。
845
+ // ⚠️ 活性普查(2026-09-19):web-client 仍直读事件形;本切片不投的理由不变(终帧是权威),但「没人消费」这句不成立。
843
846
  case 'needs_review':
844
- // `compaction_outcome`(sdk 4.0.1 新臂,[2373]A-2 三腿同源):压缩**非 compacted 结局**报告
845
- // (mooted/failed 等)。CC transcript 无对位物(compact_boundary 只对成功压缩,由 `compacted`
846
- // 臂投)——本切片不投影。⚠️ 「压缩失败让用户看见」是 chrome/HUD 面的活,今天两端都还没接
847
- // (adapt 臂表同样无此臂),缺口记 0.13.0 候选,不许由本切片顺手编一个假 transcript 形。
848
- case 'compaction_outcome':
849
847
  // ── sdk 6.9.0 提货批(2026-08-08)新入 union 的四条臂 ─────────────────────────────────────
850
848
  // `tool_approval` / `tool_approval_complete`:HITL 审批帧。**#185a 起它们是 `AgentEvent` 的臂**
851
849
  // (此前只是 named SSE frame),但本切片不是它们的消费口 —— 它们由
@@ -918,6 +916,10 @@ export function eventToSdkMessage(ev, ctx) {
918
916
  // 由哪些段组成、哪一段大」,fleet/HUD 面要渲;三个标量走 finite 门(非有限即缺席,永不 null)。
919
917
  case 'context_usage':
920
918
  return contextUsageProjection(ev, ctx);
919
+ // CC-89(0.75.0):`compaction_outcome` 从 not_in_slice 组里拿出来 —— 压缩的**非 compacted 结局**(被终态抢先 / 失败等)此前
920
+ // 没有任何一端看得见。转录里没有对位物(`compact_boundary` 只对成功压缩)⇒ 走内部臂 → chrome 臂,不编消息。
921
+ case 'compaction_outcome':
922
+ return compactionOutcomeProjection(ev, ctx);
921
923
  case 'tool_progress':
922
924
  return toolProgressProjection(ev, ctx);
923
925
  default:
@@ -1294,6 +1296,8 @@ function projectLspSection(raw) {
1294
1296
  return undefined;
1295
1297
  return { mounted: o.mounted, ...(typeof o.lane === 'string' && o.lane.length > 0 ? { lane: o.lane } : {}) };
1296
1298
  }
1299
+ /** 内部臂的唯一铸出口(内部臂不在 CC 的 SDKMessage 联合里,出境只此一处铸;新臂走这里,不再各写一处铸链)。 */
1300
+ const internalArmExit = (body) => body;
1297
1301
  function contextUsageProjection(ev, ctx) {
1298
1302
  const { identity, bad } = identityOrMalformed(ev, 'context_usage');
1299
1303
  if (bad !== null)
@@ -1319,14 +1323,39 @@ function contextUsageProjection(ev, ctx) {
1319
1323
  }
1320
1324
  if (usedTokens === undefined && windowTokens === undefined && compactAtTokens === undefined && sections === undefined)
1321
1325
  return nothing('empty_payload');
1322
- return projected(stamp(ctx, armBody({
1326
+ return projected(internalArmExit(stamp(ctx, armBody({
1323
1327
  type: 'context_usage',
1324
1328
  ...(usedTokens !== undefined ? { usedTokens } : {}),
1325
1329
  ...(windowTokens !== undefined ? { windowTokens } : {}),
1326
1330
  ...(compactAtTokens !== undefined ? { compactAtTokens } : {}),
1327
1331
  ...(sections !== undefined ? { sections } : {}),
1328
1332
  ...identity,
1329
- })));
1333
+ }))));
1334
+ }
1335
+ /**
1336
+ * CC-89(0.75.0):`outcome` 是主事实,非空串必在(否则 malformed —— 不投一条没有结局的结局帧),原词逐字(引擎开集,不按成员过滤);
1337
+ * `trigger` / `reason` 非空串才在场,缺席 / 坏形 ⇒ 键不在场(不编 'auto'、不编空串;主事实不因旁键丢)。
1338
+ */
1339
+ function compactionOutcomeProjection(frame, ctx) {
1340
+ // 🔴 不浅拷(异源复审 R1 [medium]):展开会擦掉原型链上的身份键(进程内对象把子代帧洗成主会话)、还会执行无关的可枚举 getter;
1341
+ // 与兄弟臂同律 —— 就在同一份记录上逐键各读一次。
1342
+ const ev = frame;
1343
+ const { identity, bad } = identityOrMalformed(ev, 'compaction_outcome');
1344
+ if (bad !== null)
1345
+ return bad;
1346
+ const str = (v) => (typeof v === 'string' && v.length > 0 ? v : undefined);
1347
+ const outcome = str(ev.outcome);
1348
+ if (outcome === undefined)
1349
+ return dropped('malformed', 'compaction_outcome');
1350
+ const trigger = str(ev.trigger);
1351
+ const reason = str(ev.reason);
1352
+ return projected(internalArmExit(stamp(ctx, armBody({
1353
+ type: 'compaction_outcome',
1354
+ outcome,
1355
+ ...(trigger !== undefined ? { trigger } : {}),
1356
+ ...(reason !== undefined ? { reason } : {}),
1357
+ ...identity,
1358
+ }))));
1330
1359
  }
1331
1360
  function engineNoticeProjection(ev, ctx) {
1332
1361
  const code = typeof ev.code === 'string' && ev.code.length > 0 ? ev.code : undefined;
@@ -99,7 +99,8 @@ export function fleetTaskToBackgroundRow(r) {
99
99
  // [C33]③.2 / server [C36]:launch 时刻 epoch ms(revive 行=本 revive 周期 wake 时刻),
100
100
  // ADDITIVE / tolerate-absent —— 排队中的行本就没有,缺席即键缺席(0 不是「1970 年启动」)。
101
101
  // 两腿同一个 mapper:`snapshot` 首帧行与 `task` 增量帧行(POST-MERGE FULL row)都过这里。
102
- const startedAtMs = typeof r.startedAt === 'number' && r.startedAt > 0 ? r.startedAt : undefined;
102
+ // 与 fleet 投影同律:只有**有限正数**算 launch 锚(非有限值渲不出时长,只会变成 `null`)。
103
+ const startedAtMs = typeof r.startedAt === 'number' && Number.isFinite(r.startedAt) && r.startedAt > 0 ? r.startedAt : undefined;
103
104
  return {
104
105
  key: { source: 'fleet', rowId: r.id },
105
106
  status: backgroundStatusFromFleet(r.status),
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * deviceExecutorManagementCapability — `GET /v1/capabilities.deviceExecutor.management`(server ≥7.88.0;契约附录 B.1
3
3
  * `deviceExecutor` 行 + §14)的三端共用读面(0.74.2;与 sql / writeProtection / webSearch.backend / executionLane /
4
- * approvalsStreamLive 五只兄弟同一套四态词汇)。sdk 对该位无型(9.7.1)⇒ 按契约结构读,sdk 补型当拍对表。
4
+ * approvalsStreamLive 五只兄弟同一套四态词汇)。sdk 9.7.1 对该位无型 ⇒ 按契约结构读;9.8.x 起声明 `management?: boolean`(0.75.0 CC-81 对表:读器不改,门钉声明形)。
5
5
  *
6
6
  * ── 这一位答的是哪一个问题 ────────────────────────────────────────────────────────────────
7
7
  * 「`/v1/devices/*` 的管理动词在这台 server 上在不在」(present-iff:与该族 501 判据逐字同源)。它挂在 `deviceExecutor`
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * deviceExecutorManagementCapability — `GET /v1/capabilities.deviceExecutor.management`(server ≥7.88.0;契约附录 B.1
3
3
  * `deviceExecutor` 行 + §14)的三端共用读面(0.74.2;与 sql / writeProtection / webSearch.backend / executionLane /
4
- * approvalsStreamLive 五只兄弟同一套四态词汇)。sdk 对该位无型(9.7.1)⇒ 按契约结构读,sdk 补型当拍对表。
4
+ * approvalsStreamLive 五只兄弟同一套四态词汇)。sdk 9.7.1 对该位无型 ⇒ 按契约结构读;9.8.x 起声明 `management?: boolean`(0.75.0 CC-81 对表:读器不改,门钉声明形)。
5
5
  *
6
6
  * ── 这一位答的是哪一个问题 ────────────────────────────────────────────────────────────────
7
7
  * 「`/v1/devices/*` 的管理动词在这台 server 上在不在」(present-iff:与该族 501 判据逐字同源)。它挂在 `deviceExecutor`
@@ -37,6 +37,12 @@ export type EngineAgentPanelEvent = {
37
37
  currentAction?: string;
38
38
  toolUses: number;
39
39
  totalTokens: number;
40
+ /**
41
+ * 0.75.1 —— 派生这只子代时**被调用方声明的名字**(wire `task_progress.spawnName`,engine 侧新位)。
42
+ * 🔴 与 `name` 是两位,别合流:`name` 是**展示名**(上游由委派卡的 description 派生),本位是 spawn 时的名字。
43
+ * 🔴 缺席 = **旧形**(产生者还没有这一位)⇒ 键不落,绝不拿展示名冒充。
44
+ */
45
+ spawnName?: string;
40
46
  /**
41
47
  * 0.74.3 CC-70:tick 上带的委派卡 id(wire `task_progress.parentToolCallId`,在场才带)—— 身份归一的**退路键**
42
48
  * (`transcriptId` 查不到时按它反查 fleet 尾段)。
@@ -209,7 +215,12 @@ export declare function resolveEnginePanelTaskId(wireTaskId: string, parentToolC
209
215
  * `cycle`(0.74.4):调用方(通知车道)带得出这条终态的周期时传进来 —— 尾段归某把 UUID 所有而通知比所有权旧 ⇒ 不清那把 UUID 的常驻标。
210
216
  */
211
217
  export declare function clearEnginePanelTaskResidentByWire(wireTaskId: string, cycle?: EngineAgentPanelCycleIdentity): void;
212
- /** 测试钩:清两张钥匙表、缓一拍与周期水位。 */
218
+ /**
219
+ * 测试钩。逐表清单(每加一张表同批更新这里):① 转录钥匙表(UUID → 行尾段)② 父调用钥匙表(parentToolCallId → 行尾段)
220
+ * ③ 缓着的未绑卡 tick ④ 每行最近一次行帧的周期水位 ⑤ 「先发布的键」集合(UUID)⑥ 同上按父调用记的那一份 ⑦ 反向所有权表(行尾段 → UUID)。
221
+ * 🔴 不清:订阅者 / 回放缓冲、常驻标、running 历史(那一本由 `__resetPanelRunningHistoryForTests` 清)。
222
+ * 🔴 连续场景(同一只子代跨多步的用例)**中途别调**:清掉 ④–⑦ 之后,陈旧判据与「先发布的键为准」都会从零开始,前后两半不再是同一段历史。
223
+ */
213
224
  export declare function __resetEngineAgentPanelIdentityForTests(): void;
214
225
  /** 宿主的唯一发布口(三条 lane 都经这里):先归一身份,再进主通道(订阅者 / 缓冲)。 */
215
226
  export declare function publishEngineAgentPanelEvent(ev: EngineAgentPanelEvent): void;