@sema-agent/client-core 0.11.21 → 0.11.23

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.11.21
26
+ **Version:** 0.11.23
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
@@ -215,6 +215,7 @@ itself (FAILED names + skipped names + arithmetic reconciliation). All-SKIP repo
215
215
  | `scripts/run-engine-vocab-floor-test.mjs` | Engine-mirrored vocabularies (structured card whitelist, self-reported tool face, control verbs, recogniser sets) against the *installed* `@sema-agent/core` |
216
216
  | `scripts/run-streamjson-timing-honesty-test.mjs` | Stream timing & terminal honesty ([2084]): held errored fs-write results release on model progress; wall-clock timeout maps to `error_during_execution` with a truthful salvage note; the synthetic API-error assistant row carries the `<synthetic>` in-message sentinel |
217
217
  | `scripts/run-background-view-test.mjs` | `createBackgroundView` lifecycle: polling/notify pairing, per-source degrade (`501 → not-configured` vs `unavailable`), the capabilities `scheduler` probe, and dispose really aborting the in-flight fleet snapshot (pure projection lives in the pure suite's W-A segment) |
218
+ | `scripts/run-fleet-view-keys-test.mjs` | The fleet projection views, **both directions**: `FleetTaskView`/`FleetWorkflowView` ⇄ their key lists (compile-pinned) ⇄ what a maximal/minimal row really projects, plus a wire-key coverage ledger (every `FleetTaskRow` key is either projected or carries a written reason why not) and a drift ledger against the shell's render contract. A one-directional assignability check is blind to optional keys — which is how `startedAt` was silently dropped |
218
219
  | `scripts/run-public-surface-test.mjs` | The outward promises: the npm export surface baseline, the peer floor witness, and this README's claims |
219
220
 
220
221
  Each suite carries a floor that only moves up — a refactor that stops executing a group of
package/dist/adapt.js CHANGED
@@ -1081,6 +1081,21 @@ class WireToCcAdapterImpl {
1081
1081
  ...(description !== undefined ? { description } : {}),
1082
1082
  ...(prompt !== undefined ? { prompt } : {}),
1083
1083
  ...(currentAction !== undefined ? { currentAction } : {}),
1084
+ // 🔴 这里的 `: 0` 与 `fleetAgentPanelProjection` 那句「绝不在本层 `?? 0`」**不矛盾**,
1085
+ // 因为不是同一条 lane 的同一种供给形(两层的分层实情写在 `engineAgentPanelStore.ts`
1086
+ // 的 `PANEL_TOOLUSES_LANE_POLICY` 上,两条 lane 的可选性由编译钉锁住):
1087
+ // · **tick lane(本处)**:`usage` 是 core `task_progress` 臂的**必填**字段
1088
+ // (core 2.12.0 `dist/core/types.d.ts:606-610` 无 `?`),两处发帧点
1089
+ // (`runner/runtask.js:1080` 每轮边界 / `:2925` 终态 settle 拍)**无条件**构造
1090
+ // `{ totalTokens, toolUses: stats.toolCalls, durationMs }`,server 白名单整键透传
1091
+ // (`trace/project.js:118`)⇒ 缺席在这条 lane 上不是「不知道」,是**没有这种帧**。
1092
+ // 值本身是**累计**计数(`stats.toolCalls`,从 0 起算),所以真值域含真 0。
1093
+ // · **fleet-row lane(那处)**:同名键是 server 1.278.0 才**新加**的可选位,老引擎
1094
+ // 真的不发 ⇒ 缺席就是「不知道」,兜 0 会把它冒充成「跑了 0 个工具」,而且会把
1095
+ // 台账里已知的累计值**擦回 0**。
1096
+ // ⚠️ 别把两层「统一」:把本处改成传播缺席要先把事件形改成可选,而那正是本 lane
1097
+ // 刻意不做的事(`engineAgentPanelStore.ts:54-57` 记的就是这条 —— 复用同一个可选形
1098
+ // 会让 fleet-row lane 每秒把前台子代由真 tick 写进去的进度数字清一次)。
1084
1099
  toolUses: typeof usage.toolUses === 'number' ? usage.toolUses : 0,
1085
1100
  totalTokens: typeof usage.totalTokens === 'number' ? usage.totalTokens : 0,
1086
1101
  },
@@ -129,21 +129,36 @@ async function* runStreamInner(events, ctx, handle = {}) {
129
129
  // errorMessage 'session already has an active run'(+ result.activeTaskId)骑在
130
130
  // done{status:'failed'} 帧上(ai-agent-service server.ts:1526 同型)——裸 "API Error:" 行看着
131
131
  // 像 provider 故障,是误导。识别该终帧 → 一行专属文案(告知在等什么 + 出路);其它错误保持
132
- // 原 API Error 行(未知错误 fallback 不变)。引擎侧解锁腿([866] server cancel suspended 改
133
- // 语义 + reapSuspended TTL)到货前,这是壳侧最诚实的呈现。
132
+ // 原 API Error 行(未知错误 fallback 不变)。引擎侧解锁腿已到货([868]:cancel
133
+ // suspended/needs_review 就地终态化 claim 释放),所以文案指的就是那个端点;
134
+ // 会自动执行「cancel + 重发」的自愈腿在宿主侧(cli: src/sema/activeRunSelfHeal.ts),
135
+ // 本层只负责:识别 + 说真话。
134
136
  const activeTaskId = failedResult?.activeTaskId ??
135
137
  (ev.type === 'failed' ? ev.activeTaskId : undefined);
136
- const isActiveRunBusy = errText === 'session already has an active run' || typeof activeTaskId === 'string';
138
+ // 🔴 2026-07-31:锚必须是 `includes` 不是全等 —— 引擎发的原文是
139
+ // 「session already has an active run — POST /v1/runs/{activeTaskId}/cancel stops it …」
140
+ // (server dist/http/routes/tasks.js,3.10.0/3.18.0 逐字同形),比锚长。原来的全等式在
141
+ // 今天的任何引擎上**一次都命中不了**,整条判别一直只靠 activeTaskId 那一半在兜。
142
+ const isActiveRunBusy = errText.includes('session already has an active run') || typeof activeTaskId === 'string';
137
143
  // 件14 rev2 (2026-07-14): provider max_tokens cap rejection (e.g. glm-5.2 preset 131072 >
138
144
  // a third-party gateway's 128000) → append the way out. Keyword match on the upstream text,
139
145
  // supplement never mask (raw provider words stay on the row).
140
146
  const maxTokHint = /max_tokens/i.test(errText)
141
147
  ? '\nyour provider caps max_tokens lower — lower it in /model (press m, or M to type a value)'
142
148
  : '';
149
+ // 🔴 2026-07-31 假承诺修复:这里原本写的是「Press Esc to cancel it, or retry shortly.」——
150
+ // 两条出路**都是假的**,而且不是「某个客户端没接」,是没有任何客户端能靠它们脱困:
151
+ // · Esc:全 bundle 里 `/v1/runs/{id}/cancel` 在交互车道一处调用都没有,而且这一行上屏
152
+ // 时那条 turn 已经结束、取消键位根本没注册;
153
+ // · "retry shortly":park(suspended/needs_review)会**永久**留住 session claim ——
154
+ // 重启引擎捞不回来(boot 期孤儿回收只捞 running)、时间型 reap 挂在
155
+ // APPROVAL_TIMEOUT_SEC(默认 0 ⇒ 整条腿不跑),唯一兜底窗是 30 天。
156
+ // 换成真话 + 真的存在的两条动作(引擎的 cancel 端点 / 换一个会话)。
157
+ const busyHandle = typeof activeTaskId === 'string' && activeTaskId.length > 0 ? activeTaskId : null;
143
158
  const rowText = isActiveRunBusy
144
- ? `Previous turn is still pending an answer${typeof activeTaskId === 'string' && activeTaskId.length > 0
145
- ? ` (run ${activeTaskId})`
146
- : ''}. Press Esc to cancel it, or retry shortly.`
159
+ ? `This session is locked by an earlier run${busyHandle ? ` (run ${busyHandle})` : ''} that ` +
160
+ `has not been released, so this message was NOT sent. Nothing releases it on its own — ` +
161
+ `cancel that run (POST /v1/runs/${busyHandle ?? '<id>'}/cancel), or start a new session.`
147
162
  : `API Error: ${errText}${maxTokHint}`;
148
163
  yield {
149
164
  session_id: ctx.sessionId ?? '',
@@ -28,9 +28,10 @@
28
28
  *
29
29
  * 3. **cancel** (IH-8, §5.3, catalog L84). HARD-STOP an async run: `runs.cancel(taskId)` → 202 `CancelAck`
30
30
  * (`status:"cancelling"` or a terminal no-op). The run then SETTLES to `failed` + `errorCode:"cancelled"`
31
- * (NOT a new status — the UI shows "cancelled", not an error). A 409 means the run is SUSPENDED → it must
32
- * be cancelled by DENYING its approval (`hitlBridge.decideTool({decision:"deny"})`), never by `cancel`;
33
- * the router surfaces a `ControlSafetyError('cancel_suspended')` telling the shell to route to deny. 404 =
31
+ * (NOT a new status — the UI shows "cancelled", not an error). 🔴 A 409 no longer means "suspended"
32
+ * (server [868] cancels a suspended/needs_review run in place — that IS the unlock handle); it now only
33
+ * means the pending gate was decided/expired concurrently, surfaced as
34
+ * `ControlSafetyError('cancel_lost_race')` = re-read state and retry. 404 =
34
35
  * non-owner/unknown (no existence leak). Server-idempotent; not a submit → no SDK retry.
35
36
  *
36
37
  * 4. **queued commands** (§5.2, catalog L82 `contract-extension`). CC enqueues next turns with a per-message
@@ -61,7 +62,8 @@ export interface RunsResourceLike {
61
62
  signal?: AbortSignal;
62
63
  }): Promise<unknown>;
63
64
  /** POST /v1/runs/:id/cancel → 202 CancelAck. Hard-stop; server-idempotent; not a submit → no retry
64
- * (runs.ts:34). 409 if suspended (deny instead); 404 non-owner. */
65
+ * (runs.ts:34). 🔴 suspended/needs_review 也走这里就地终态化([868]);409 只剩「挂起的 gate
66
+ * 并发决定/过期」这一种(重读状态后重试);404 non-owner。 */
65
67
  cancel(taskId: string, opts?: {
66
68
  signal?: AbortSignal;
67
69
  }): Promise<CancelAck>;
@@ -71,12 +73,12 @@ export interface ControlClientLike {
71
73
  }
72
74
  /** A supervision-verb stop the shell must HANDLE, not retry (contract/04 §9.1). The `code` is stable so the
73
75
  * shell can branch: `not_running` (steer a non-running run) | `invalid_content` (steer carried an escape) |
74
- * `cancel_suspended` (cancel a suspended runroute to DENY instead) | `not_found` (non-owner/unknown). */
76
+ * `cancel_lost_race` (409: 挂起的 gate 被并发决定/过期重读状态后重试 cancel) | `not_found`。 */
75
77
  export declare class ControlSafetyError extends Error {
76
- readonly code: 'not_running' | 'invalid_content' | 'cancel_suspended' | 'not_found';
78
+ readonly code: 'not_running' | 'invalid_content' | 'cancel_lost_race' | 'not_found';
77
79
  /** The original SDK error, for logging (never re-thrown blind). */
78
80
  readonly cause?: unknown | undefined;
79
- constructor(message: string, code: 'not_running' | 'invalid_content' | 'cancel_suspended' | 'not_found',
81
+ constructor(message: string, code: 'not_running' | 'invalid_content' | 'cancel_lost_race' | 'not_found',
80
82
  /** The original SDK error, for logging (never re-thrown blind). */
81
83
  cause?: unknown | undefined);
82
84
  }
@@ -157,9 +159,14 @@ export declare class ControlRouter {
157
159
  * NOT an error (the router does not synthesize that terminal; the downstream stream / `terminalToSdkResult`
158
160
  * does). Branching (contract/04 §9.1):
159
161
  *
160
- * - **409 = the run is SUSPENDED.** A suspended run is cancelled by DENYING its approval, NOT by `cancel`
161
- * (which 409s). Surfaced as `ControlSafetyError('cancel_suspended')` so the shell routes the kill to
162
- * `hitlBridge.decideTool({decision:"deny"})` instead.
162
+ * - **409 = a LOST CAS RACE, not "suspended".** 🔴 2026-07-31 更正:这段原本写的是「409 = run
163
+ * suspended,要改走 deny」—— 那是 **[868] 之前**的世界。服务端自 [868] 起对
164
+ * suspended/needs_review 的 run **就地取消**( CAS 结掉挂起的 checkpoint,再 setTerminal
165
+ * 释放 session claim),`cancel` 就是那种 run 的恢复把手,不再 409。今天的 409 只剩一种成因:
166
+ * 挂起的那个 gate 被并发决定/过期了(server `conflict.approval_settled`)。正确处置是**重读
167
+ * run 状态后重试 cancel**,而不是去 deny 一个已经不存在的审批 —— 照旧文指路只会指进空处。
168
+ * Surfaced as `ControlSafetyError('cancel_lost_race')`(干净切:旧码名 `cancel_suspended` 已
169
+ * 退役,本仓/壳/web/桌面均无行为消费方,只有一条注释引用,同批改)。
163
170
  * - **404 = non-owner / unknown.** No existence oracle (404, not 403) → `ControlSafetyError('not_found')`.
164
171
  *
165
172
  * Server-idempotent and NOT a submit, so a successful cancel is safe to repeat (a terminal run no-ops);
@@ -5,7 +5,7 @@
5
5
  // router catches the SDK's typed errors and re-raises a stable, shell-branchable code.
6
6
  /** A supervision-verb stop the shell must HANDLE, not retry (contract/04 §9.1). The `code` is stable so the
7
7
  * shell can branch: `not_running` (steer a non-running run) | `invalid_content` (steer carried an escape) |
8
- * `cancel_suspended` (cancel a suspended runroute to DENY instead) | `not_found` (non-owner/unknown). */
8
+ * `cancel_lost_race` (409: 挂起的 gate 被并发决定/过期重读状态后重试 cancel) | `not_found`。 */
9
9
  export class ControlSafetyError extends Error {
10
10
  code;
11
11
  cause;
@@ -129,9 +129,14 @@ export class ControlRouter {
129
129
  * NOT an error (the router does not synthesize that terminal; the downstream stream / `terminalToSdkResult`
130
130
  * does). Branching (contract/04 §9.1):
131
131
  *
132
- * - **409 = the run is SUSPENDED.** A suspended run is cancelled by DENYING its approval, NOT by `cancel`
133
- * (which 409s). Surfaced as `ControlSafetyError('cancel_suspended')` so the shell routes the kill to
134
- * `hitlBridge.decideTool({decision:"deny"})` instead.
132
+ * - **409 = a LOST CAS RACE, not "suspended".** 🔴 2026-07-31 更正:这段原本写的是「409 = run
133
+ * suspended,要改走 deny」—— 那是 **[868] 之前**的世界。服务端自 [868] 起对
134
+ * suspended/needs_review 的 run **就地取消**( CAS 结掉挂起的 checkpoint,再 setTerminal
135
+ * 释放 session claim),`cancel` 就是那种 run 的恢复把手,不再 409。今天的 409 只剩一种成因:
136
+ * 挂起的那个 gate 被并发决定/过期了(server `conflict.approval_settled`)。正确处置是**重读
137
+ * run 状态后重试 cancel**,而不是去 deny 一个已经不存在的审批 —— 照旧文指路只会指进空处。
138
+ * Surfaced as `ControlSafetyError('cancel_lost_race')`(干净切:旧码名 `cancel_suspended` 已
139
+ * 退役,本仓/壳/web/桌面均无行为消费方,只有一条注释引用,同批改)。
135
140
  * - **404 = non-owner / unknown.** No existence oracle (404, not 403) → `ControlSafetyError('not_found')`.
136
141
  *
137
142
  * Server-idempotent and NOT a submit, so a successful cancel is safe to repeat (a terminal run no-ops);
@@ -146,7 +151,8 @@ export class ControlRouter {
146
151
  }
147
152
  catch (e) {
148
153
  if (isCancelSuspendedConflict(e)) {
149
- throw new ControlSafetyError('cancel on a SUSPENDED run (409) cancel it by DENYING its approval (decide), not by runs.cancel', 'cancel_suspended', e);
154
+ throw new ControlSafetyError('runs.cancel 409 the pending gate was decided or expired concurrently (lost CAS race); ' +
155
+ 're-read the run state and retry cancel if it is still active', 'cancel_lost_race', e);
150
156
  }
151
157
  if (isNotFound(e)) {
152
158
  throw new ControlSafetyError('runs.cancel 404 — non-owner / unknown run (no existence leak)', 'not_found', e);
@@ -234,7 +240,9 @@ function isSteeringInvalidContent(e) {
234
240
  const { code, name } = errCodes(e);
235
241
  return code === 'steering.invalid_content' || name === 'SteeringInvalidContentError';
236
242
  }
237
- /** A 409 on `cancel` means the run is SUSPENDED cancel-by-deny instead (runs.ts:36; openapi:371-372).
243
+ /** A 409 on `cancel` = the pending gate was decided/expired concurrently (lost CAS race; server
244
+ * `conflict.approval_settled`). 🔴 它**不再**表示「run 是 suspended」——[868] 起 suspended/needs_review
245
+ * 的 run 由 cancel 就地终态化。
238
246
  * The SDK raises a generic `ConflictError` (status 409) for this case — there is no dedicated subclass. */
239
247
  function isCancelSuspendedConflict(e) {
240
248
  const { name, status } = errCodes(e);
@@ -107,6 +107,12 @@ export type EngineAgentPanelEvent = {
107
107
  * the consumer settles ALL rows it owns that are still running. Idempotent. */
108
108
  kind: 'sweep';
109
109
  };
110
+ /**
111
+ * 两条 lane 对 `toolUses` 的口径(运行期读面,给门用;上面的编译钉保证它没在撒谎)。
112
+ * `required-engine-always-emits` = 缺席不可达 ⇒ 兜底值合法;
113
+ * `optional-tolerate-absent` = 缺席即「不知道」⇒ 必须传播缺席(键不落)。
114
+ */
115
+ export declare const PANEL_TOOLUSES_LANE_POLICY: Readonly<Record<'tick' | 'fleet-row', string>>;
110
116
  export declare function markEnginePanelTaskResident(taskId: string): void;
111
117
  export declare function clearEnginePanelTaskResident(taskId: string): void;
112
118
  export declare function isEnginePanelTaskResident(taskId: string): boolean;
@@ -22,6 +22,17 @@
22
22
  * Buffering: ticks published before the panel mounts are buffered (bounded) and replayed to the first
23
23
  * subscriber, so early frames in a fast turn aren't lost to mount timing.
24
24
  */
25
+ const _panelToolUsesLanePins = [true, true];
26
+ void _panelToolUsesLanePins;
27
+ /**
28
+ * 两条 lane 对 `toolUses` 的口径(运行期读面,给门用;上面的编译钉保证它没在撒谎)。
29
+ * `required-engine-always-emits` = 缺席不可达 ⇒ 兜底值合法;
30
+ * `optional-tolerate-absent` = 缺席即「不知道」⇒ 必须传播缺席(键不落)。
31
+ */
32
+ export const PANEL_TOOLUSES_LANE_POLICY = {
33
+ tick: 'required-engine-always-emits',
34
+ 'fleet-row': 'optional-tolerate-absent',
35
+ };
25
36
  // ── #6(frame-lane-matrix)session 常驻行台账 ─────────────────────────────────────────────────
26
37
  // 行寿命 ≠ turn 寿命:跨 turn 存活的 bg 子代行(tick 绑不到本 turn 开着的卡 = inert/unbound)不该被
27
38
  // turn 末防御 sweep(bridge settlePanelTasks(null) / seamQuery 'sweep' 事件)结成假 completed——一旦
@@ -21,9 +21,21 @@ import type { FleetTaskRow, FleetWorkflowRow } from '@sema-agent/sdk';
21
21
  /** 渲染面的 fleet 状态词汇(cli `overrides/fleet-tree.tsx` 的 `FleetTaskStatus` 同集同序)。 */
22
22
  export type FleetTaskStatus = 'queued' | 'running' | 'waiting' | 'stopping' | 'awaiting approval' | 'idle' | 'completed' | 'failed' | 'killed';
23
23
  /**
24
- * 投影后的一条 task 行。
25
- * 🔴 与 cli `overrides/fleet-tree.tsx` 的 `FleetTask` **结构等同**(字段名/可选性/类型逐一对齐)——
26
- * 渲染器类型留在端上(它是渲染契约),这里是产出侧类型。两侧漂移由 pure 门的键集断言钉死。
24
+ * 投影后的一条 task 行。渲染器类型(cli `overrides/fleet-tree.tsx` 的 `FleetTask`)留在端上——
25
+ * 它是**渲染契约**,本接口是**产出侧**类型,两者同族但不是同一个物。
26
+ *
27
+ * ⚠️ **两侧漂移到底被什么看住 —— 精确版(2026-07-31 更正)**:此处原写「两侧漂移由 pure 门的
28
+ * 键集断言钉死」,那是**假话**:pure 门里从来没有键集断言(只有逐字段的取值断言,
29
+ * `run-client-core-pure-test.mjs:2411-2452`)。当时真正在跑的看守只有一条 ——
30
+ * · **编译期、壳侧、单向**:cli `src/sema/fleetClient.ts` 的 `projectTasksForTest` 把
31
+ * `projectTasks(...)`(`FleetTaskView[]`)当 `FleetTask[]` 返回,于是 tsc 校
32
+ * `FleetTaskView` **可赋给** `FleetTask`。它只拦一件事:渲染契约要一个**必填**键而投影没产。
33
+ * 它拦不住的两个方向:①「投影多产一个渲染契约没声明的键」(非字面量赋值不做多余属性检查)、
34
+ * ②「渲染契约多声明一个**可选**键而投影永不产」。而本接口的键**几乎全是可选的** ⇒ 那道
35
+ * 检查对本接口绝大多数键零判别力。`startedAt` 就是从这个缝里漏出去的(见该键的注)。
36
+ * 现在补上的是运行期、本仓、**双向**的那半边:`scripts/run-fleet-view-keys-test.mjs` ——
37
+ * 本接口 ⇄ `FLEET_TASK_VIEW_KEYS`(文件末 `_fleetViewKeyPins`,编译期两个方向)⇄ 最大行/最小行
38
+ * 的**真产出键集**(运行期逐元素相等),外加 wire 键投影覆盖账 + 壳渲染契约漂移账。
27
39
  */
28
40
  export interface FleetTaskView {
29
41
  id: string;
@@ -199,3 +211,24 @@ export interface ProjectTasksOptions {
199
211
  export declare function projectTasks(allRows: readonly FleetTaskRow[], opts: ProjectTasksOptions): FleetTaskView[];
200
212
  /** wire workflow 行 → 渲染行。 */
201
213
  export declare function projectWorkflows(rows: readonly FleetWorkflowRow[]): FleetWorkflowView[];
214
+ /** `FleetTaskView` 的键名清单(运行期物;编译期与 `keyof FleetTaskView` 双向等值)。 */
215
+ export declare const FLEET_TASK_VIEW_KEYS: readonly string[];
216
+ /** `FleetWorkflowView` 的键名清单(同上)。 */
217
+ export declare const FLEET_WORKFLOW_VIEW_KEYS: readonly string[];
218
+ /** SDK `FleetTaskRow` 的**入口**键名清单(编译期与 `keyof FleetTaskRow` 双向等值 ⇒ SDK 加一个
219
+ * wire 键,本元组不跟就编译红;跟了之后覆盖账那条腿再逼你表态「投不投」)。 */
220
+ export declare const FLEET_TASK_ROW_WIRE_KEYS: readonly string[];
221
+ /** SDK `FleetWorkflowRow` 的入口键名清单(同上)。 */
222
+ export declare const FLEET_WORKFLOW_ROW_WIRE_KEYS: readonly string[];
223
+ /**
224
+ * **不投影登记表**:wire 上有、`FleetTaskView` 上**故意没有**的键,每条必须写清理由。
225
+ *
226
+ * 🔴 这张表是 `startedAt` 那类事故的正面看守:那次不是「谁反对投影它」,是**没有人被逼着表态**——
227
+ * wire 有、ledger 读到了、另外两个投影面都补过,唯独这一层静默丢弃,编译期一声不吭。有了本表,
228
+ * 每个 wire 键只有两条出路:**要么投影出去,要么在这里写下不投的理由**;新 wire 键在覆盖账那条
229
+ * 腿上当场红,不给「悄悄没人管」留位置。
230
+ */
231
+ export declare const FLEET_TASK_ROW_KEYS_NOT_PROJECTED: Readonly<Record<string, string>>;
232
+ /** workflow 行:wire 十键与视图十键一一同名同义 ⇒ 本表**空**(空不是「忘了填」,是覆盖账那条腿
233
+ * 每跑一次都在证明它该空:少投一个键就红)。 */
234
+ export declare const FLEET_WORKFLOW_ROW_KEYS_NOT_PROJECTED: Readonly<Record<string, string>>;
@@ -268,3 +268,112 @@ export function projectWorkflows(rows) {
268
268
  };
269
269
  });
270
270
  }
271
+ // ── 键集契约(`run-fleet-view-keys-test.mjs` 的运行期锚)─────────────────────────────────────
272
+ //
273
+ // 两跳链条,一跳都断不了(形与 `seatContract.ts` 的座位门同款):
274
+ // interface ──类型钉(本文件末 `_fleetViewKeyPins`,编译期,**两个方向**)──> 键名元组
275
+ // ──双向键集门(运行期,对最大行/最小行的真产出逐元素相等)──> 实装投影
276
+ //
277
+ // 为什么钉在**内部 const 元组**而不是导出的 `readonly string[]`:后者的 `[number]` 塌回 `string`,
278
+ // `Covers` 恒真 = 零判别力(seatContract `:838-843` 同一个坑的记录)。公开面给 `readonly string[]`,
279
+ // 消费者的 `includes(someString)` 不受影响。
280
+ const FLEET_TASK_VIEW_KEY_TUPLE = [
281
+ 'id',
282
+ 'name',
283
+ 'description',
284
+ 'depth',
285
+ 'parentId',
286
+ 'status',
287
+ 'elapsedMs',
288
+ 'tokens',
289
+ 'tokenDir',
290
+ 'queuedCount',
291
+ 'awaitingPlanApproval',
292
+ 'descendantCount',
293
+ 'viewed',
294
+ 'toolUses',
295
+ 'transcriptId',
296
+ 'currentTool',
297
+ 'startedAt',
298
+ 'parentToolCallId',
299
+ 'stoppedBy',
300
+ 'usage',
301
+ 'resumable',
302
+ 'editedFiles',
303
+ ];
304
+ /** `FleetTaskView` 的键名清单(运行期物;编译期与 `keyof FleetTaskView` 双向等值)。 */
305
+ export const FLEET_TASK_VIEW_KEYS = FLEET_TASK_VIEW_KEY_TUPLE;
306
+ const FLEET_WORKFLOW_VIEW_KEY_TUPLE = [
307
+ 'id',
308
+ 'name',
309
+ 'description',
310
+ 'status',
311
+ 'doneCount',
312
+ 'totalCount',
313
+ 'elapsedMs',
314
+ 'tokens',
315
+ 'failedCount',
316
+ 'startedCount',
317
+ ];
318
+ /** `FleetWorkflowView` 的键名清单(同上)。 */
319
+ export const FLEET_WORKFLOW_VIEW_KEYS = FLEET_WORKFLOW_VIEW_KEY_TUPLE;
320
+ const FLEET_TASK_ROW_WIRE_KEY_TUPLE = [
321
+ 'id',
322
+ 'name',
323
+ 'description',
324
+ 'agentType',
325
+ 'agentName',
326
+ 'parentId',
327
+ 'parentToolCallId',
328
+ 'workflowRunId',
329
+ 'status',
330
+ 'startedAt',
331
+ 'elapsedMs',
332
+ 'tokens',
333
+ 'queuedCount',
334
+ 'awaitingPlanApproval',
335
+ 'currentAction',
336
+ 'currentTool',
337
+ 'toolUses',
338
+ 'transcriptId',
339
+ 'stoppedBy',
340
+ 'usage',
341
+ 'resumable',
342
+ 'editedFiles',
343
+ ];
344
+ /** SDK `FleetTaskRow` 的**入口**键名清单(编译期与 `keyof FleetTaskRow` 双向等值 ⇒ SDK 加一个
345
+ * wire 键,本元组不跟就编译红;跟了之后覆盖账那条腿再逼你表态「投不投」)。 */
346
+ export const FLEET_TASK_ROW_WIRE_KEYS = FLEET_TASK_ROW_WIRE_KEY_TUPLE;
347
+ const FLEET_WORKFLOW_ROW_WIRE_KEY_TUPLE = [
348
+ 'id',
349
+ 'name',
350
+ 'description',
351
+ 'status',
352
+ 'doneCount',
353
+ 'totalCount',
354
+ 'failedCount',
355
+ 'startedCount',
356
+ 'elapsedMs',
357
+ 'tokens',
358
+ ];
359
+ /** SDK `FleetWorkflowRow` 的入口键名清单(同上)。 */
360
+ export const FLEET_WORKFLOW_ROW_WIRE_KEYS = FLEET_WORKFLOW_ROW_WIRE_KEY_TUPLE;
361
+ /**
362
+ * **不投影登记表**:wire 上有、`FleetTaskView` 上**故意没有**的键,每条必须写清理由。
363
+ *
364
+ * 🔴 这张表是 `startedAt` 那类事故的正面看守:那次不是「谁反对投影它」,是**没有人被逼着表态**——
365
+ * wire 有、ledger 读到了、另外两个投影面都补过,唯独这一层静默丢弃,编译期一声不吭。有了本表,
366
+ * 每个 wire 键只有两条出路:**要么投影出去,要么在这里写下不投的理由**;新 wire 键在覆盖账那条
367
+ * 腿上当场红,不给「悄悄没人管」留位置。
368
+ */
369
+ export const FLEET_TASK_ROW_KEYS_NOT_PROJECTED = {
370
+ agentType: '折进 `name`(deriveAgentLabel:agentName ?? agentType,都缺才退 objective 派生)——渲染契约只有一个身份列,187 的 tjl 同形。',
371
+ agentName: '同 agentType:两位在本层合成一个身份 label,分开带出去等于把 187 的 tjl 规则推给每个消费端各写一遍。',
372
+ workflowRunId: 'workflow 归属**不走行字段**:权威判定在 adapt 的 task_progress 臂(recordWorkflowAgentTaskId,判据④),端按 taskId 尾段查台账过滤双生行。行上再带一份 = 第二真源。',
373
+ currentAction: '结构化同源体 `currentTool` 已投影(SDK 注:currentAction 是它的**文本回落**)。🔴 诚实记账:旧端(server <1.288)只发 currentAction ⇒ 那一档上本视图今天确实拿不到活动文本,这是**已知缺口**不是设计取舍;补它 = 加 `currentAction?: string` 进本视图 + 端侧回落渲染。',
374
+ };
375
+ /** workflow 行:wire 十键与视图十键一一同名同义 ⇒ 本表**空**(空不是「忘了填」,是覆盖账那条腿
376
+ * 每跑一次都在证明它该空:少投一个键就红)。 */
377
+ export const FLEET_WORKFLOW_ROW_KEYS_NOT_PROJECTED = {};
378
+ const _fleetViewKeyPins = [true, true, true, true, true, true, true, true];
379
+ void _fleetViewKeyPins;
@@ -7,8 +7,24 @@ export interface FleetAgentRowLike {
7
7
  description?: string;
8
8
  status?: string;
9
9
  tokens?: number;
10
- /** server 1.278.0 `toolUses` —— 子代**累计**工具调用数。🔴 三态:有键真值(含真 0)/无键
11
- * `undefined`=老引擎不知道。绝不在本层 `?? 0`(那等于把「不知道」冒充成「跑了 0 个」)。 */
10
+ /**
11
+ * server 1.278.0 `toolUses` —— 子代**累计**工具调用数。🔴 三态:有键真值(含真 0)/
12
+ * 无键 `undefined` = 老引擎不知道。**本层绝不 `?? 0`**(那等于把「不知道」冒充成「跑了 0 个」,
13
+ * 而且会把台账里已知的累计值擦回 0)。
14
+ *
15
+ * ⚠️ **这句话只对本层成立 —— 分层实情**(2026-07-31 更正:原文写成绝对句「绝不在本层 `?? 0`」,
16
+ * 读起来像是全包禁令,于是与 `adapt.ts` tick 臂那句 `: 0` 构成一处**包内自相矛盾**;真相是两条
17
+ * lane 的供给形不同,判据见 `engineAgentPanelStore.ts` 的 `PANEL_TOOLUSES_LANE_POLICY`):
18
+ * · **本 lane(fleet 行帧)**:该键是 server 1.278.0 **新加**的可选位,老引擎真的不发 ⇒
19
+ * 缺席 = 不知道 ⇒ 必须传播缺席(键不落)。
20
+ * · **tick lane(`task_progress`)**:`usage` 在 core 的事件类型里是**必填**
21
+ * (core 2.12.0 `core/types.d.ts:606-610`),两处发帧点无条件构造它
22
+ * (`core/runner/runtask.js:1080` / `:2925`),server 白名单整键透传
23
+ * (`trace/project.js:118`)⇒ 那条 lane 上「缺席」不是「不知道」,是没有这种帧;那里的
24
+ * `: 0` 是对一个**恒发的累计计数**兜底,不是编造一个不知道的值。
25
+ * 两条 lane 的可选性差异由 `engineAgentPanelStore.ts` 的编译钉锁住:谁想把两层「统一」成
26
+ * 同一种形,先在那里撞红。
27
+ */
12
28
  toolUses?: number;
13
29
  /** server 1.278.0 `transcriptId`(子代 childSessionId)—— 委派 prompt 的取件锚,不是 prompt 本身。 */
14
30
  transcriptId?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/client-core",
3
- "version": "0.11.21",
3
+ "version": "0.11.23",
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",