@sema-agent/client-core 0.12.2 → 0.14.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.
Files changed (131) hide show
  1. package/README.md +15 -2
  2. package/dist/abortableSleep.d.ts +29 -0
  3. package/dist/abortableSleep.js +43 -0
  4. package/dist/adapt/arms.d.ts +67 -0
  5. package/dist/adapt/arms.js +623 -0
  6. package/dist/adapt/ids.d.ts +22 -0
  7. package/dist/adapt/ids.js +34 -0
  8. package/dist/adapt/instanceLedger.d.ts +36 -0
  9. package/dist/adapt/instanceLedger.js +50 -0
  10. package/dist/adapt/panelTasks.d.ts +59 -0
  11. package/dist/adapt/panelTasks.js +193 -0
  12. package/dist/adapt/textStream.d.ts +63 -0
  13. package/dist/adapt/textStream.js +141 -0
  14. package/dist/adapt/toolCards.d.ts +65 -0
  15. package/dist/adapt/toolCards.js +100 -0
  16. package/dist/adapt/turnFlags.d.ts +37 -0
  17. package/dist/adapt/turnFlags.js +55 -0
  18. package/dist/adapt/wireShapes.d.ts +96 -0
  19. package/dist/adapt/wireShapes.js +167 -0
  20. package/dist/adapt.d.ts +33 -60
  21. package/dist/adapt.js +84 -1207
  22. package/dist/adapter/downstream/eventToSdkMessage.d.ts +57 -13
  23. package/dist/adapter/downstream/eventToSdkMessage.js +174 -106
  24. package/dist/adapter/downstream/terminalToSdkResult.js +151 -160
  25. package/dist/adapter/downstream/turnUsageToModelUsage.d.ts +36 -0
  26. package/dist/adapter/downstream/turnUsageToModelUsage.js +34 -6
  27. package/dist/adapter/runStream.d.ts +36 -4
  28. package/dist/adapter/runStream.js +195 -13
  29. package/dist/adapter/types.d.ts +28 -2
  30. package/dist/agentSession/backgroundView.js +4 -15
  31. package/dist/agentsWireCaps.d.ts +10 -6
  32. package/dist/agentsWireCaps.js +21 -7
  33. package/dist/argvFlagValue.d.ts +41 -0
  34. package/dist/argvFlagValue.js +69 -0
  35. package/dist/attachmentsWireCaps.d.ts +3 -2
  36. package/dist/attachmentsWireCaps.js +5 -3
  37. package/dist/classifierVerdictWire.d.ts +8 -0
  38. package/dist/classifierVerdictWire.js +8 -0
  39. package/dist/cloudConfigWireCaps.d.ts +28 -1
  40. package/dist/cloudConfigWireCaps.js +51 -11
  41. package/dist/controlRouter.d.ts +14 -8
  42. package/dist/controlRouter.js +15 -21
  43. package/dist/detachWire.d.ts +15 -5
  44. package/dist/detachWire.js +17 -7
  45. package/dist/effortWire.d.ts +0 -21
  46. package/dist/effortWire.js +6 -20
  47. package/dist/engineInlineTaskStats.d.ts +12 -6
  48. package/dist/engineWireSdk.d.ts +12 -0
  49. package/dist/env/localeGeo.js +2 -1
  50. package/dist/envFlag.d.ts +39 -0
  51. package/dist/envFlag.js +51 -0
  52. package/dist/finalVerifyWire.d.ts +7 -5
  53. package/dist/finalVerifyWire.js +6 -5
  54. package/dist/fleet/fleetLedger.d.ts +32 -9
  55. package/dist/fleet/fleetLedger.js +119 -34
  56. package/dist/fleet/fleetProjection.d.ts +44 -6
  57. package/dist/fleet/fleetProjection.js +52 -10
  58. package/dist/fleetAgentPanelProjection.d.ts +18 -1
  59. package/dist/fleetAgentPanelProjection.js +61 -14
  60. package/dist/forkWireCaps.d.ts +2 -1
  61. package/dist/forkWireCaps.js +5 -11
  62. package/dist/goalStopHook.d.ts +142 -0
  63. package/dist/goalStopHook.js +258 -0
  64. package/dist/headlessPermissionModeWire.d.ts +10 -0
  65. package/dist/headlessPermissionModeWire.js +24 -21
  66. package/dist/headlessReconnectWire.d.ts +7 -1
  67. package/dist/headlessReconnectWire.js +20 -2
  68. package/dist/hitl/approvalsFeed.d.ts +22 -1
  69. package/dist/hitl/approvalsFeed.js +103 -9
  70. package/dist/hitl/askGateWire.d.ts +27 -96
  71. package/dist/hitl/askGateWire.js +69 -618
  72. package/dist/hitl/frameRouter.d.ts +86 -0
  73. package/dist/hitl/frameRouter.js +383 -0
  74. package/dist/hitl/gateLedger.d.ts +119 -0
  75. package/dist/hitl/gateLedger.js +113 -0
  76. package/dist/hitl/hitlBridge.d.ts +75 -15
  77. package/dist/hitl/hitlBridge.js +94 -22
  78. package/dist/hitl/hitlHostSurface.d.ts +49 -0
  79. package/dist/hitl/hitlHostSurface.js +155 -0
  80. package/dist/hitl/parkResolver.d.ts +74 -0
  81. package/dist/hitl/parkResolver.js +250 -0
  82. package/dist/hitl/planReviewWire.d.ts +60 -2
  83. package/dist/hitl/planReviewWire.js +197 -91
  84. package/dist/hitl/toolApprovalWire.d.ts +67 -4
  85. package/dist/hitl/toolApprovalWire.js +129 -31
  86. package/dist/hooksWireCaps.d.ts +1 -82
  87. package/dist/hooksWireCaps.js +44 -237
  88. package/dist/host.d.ts +16 -5
  89. package/dist/index.d.ts +2 -0
  90. package/dist/index.js +15 -1
  91. package/dist/interactiveToolsWire.d.ts +15 -4
  92. package/dist/interactiveToolsWire.js +24 -26
  93. package/dist/limitsWire.js +7 -40
  94. package/dist/liveInitToolFace.d.ts +51 -6
  95. package/dist/liveQuestionStore.d.ts +18 -6
  96. package/dist/liveQuestionStore.js +15 -0
  97. package/dist/model/providerPresets.js +11 -1
  98. package/dist/notifications.d.ts +62 -2
  99. package/dist/notifications.js +306 -49
  100. package/dist/printToolResultFrame.d.ts +12 -4
  101. package/dist/printToolResultFrame.js +11 -21
  102. package/dist/retainBackgroundWireCaps.d.ts +3 -2
  103. package/dist/retainBackgroundWireCaps.js +5 -9
  104. package/dist/sandboxWire.d.ts +9 -31
  105. package/dist/sandboxWire.js +51 -50
  106. package/dist/scenarioWire.d.ts +1 -1
  107. package/dist/scenarioWire.js +25 -36
  108. package/dist/seam.d.ts +23 -6
  109. package/dist/seam.js +40 -30
  110. package/dist/seatContract.d.ts +369 -83
  111. package/dist/seatContract.js +585 -198
  112. package/dist/selfOrchestrationWireCaps.d.ts +6 -5
  113. package/dist/selfOrchestrationWireCaps.js +8 -12
  114. package/dist/sessionSlot.d.ts +8 -0
  115. package/dist/sessionSlot.js +1 -0
  116. package/dist/steering.js +2 -2
  117. package/dist/subagent/engineTaskHandleWire.d.ts +3 -0
  118. package/dist/subagent/engineTaskHandleWire.js +17 -2
  119. package/dist/subagentContentStore.d.ts +5 -5
  120. package/dist/toolResult.d.ts +89 -8
  121. package/dist/toolResult.js +99 -30
  122. package/dist/typePins.d.ts +17 -0
  123. package/dist/typePins.js +1 -0
  124. package/dist/ultracodeWireCaps.js +5 -6
  125. package/dist/unrefTimer.d.ts +30 -0
  126. package/dist/unrefTimer.js +5 -0
  127. package/dist/workflow.d.ts +3 -2
  128. package/dist/workflow.js +3 -2
  129. package/dist/workflowClient.d.ts +7 -0
  130. package/dist/workflowClient.js +47 -12
  131. package/package.json +3 -3
@@ -34,10 +34,12 @@
34
34
  * 关流封装),壳的 `engineBgProbe.ts:8-9` 自述「引擎 fleet 面只有 SSE,没有一次性口」已过期。
35
35
  * 本包出 `fleetSnapshotOptions()`;`engineBgProbe` 的手抄开流即断由壳侧换 verb(见交接报告)。
36
36
  *
37
- * ── 单实例纪律 ────────────────────────────────────────────────────────────────────────────────
38
- * `liveBgViews` 是 module 级 Set:写者 = 每个 `createFleetLedger()`,读者 = 引擎温切门
39
- * (`readEngineActiveBgTasks`)与代际让位读面(`isFleetSessionScoped`)。两份实例 = 温切门恒读空
40
- * respawn 会静默杀掉在飞 bg 子代。
37
+ * ── 单实例纪律(REF-CC-044,fleet2-06:per-sessionKey 化,W1/design/161 同款原语)────────────────
38
+ * `liveBgViews` 是 module 级 `Map<sessionKey, Set<LiveBgView>>`:写者 = 每个 `createFleetLedger()`
39
+ * (登记进自己的 `opts.sessionKey`,零参 = `DEFAULT_SESSION_KEY`),读者 = 引擎温切门
40
+ * (`readEngineActiveBgTasks[For]`)与代际让位读面(`isFleetSessionScoped[For]`)。同一 sessionKey
41
+ * 两份实例 = 温切门恒读空 ⇒ respawn 会静默杀掉在飞 bg 子代;per-session 化同时堵住了跨会话污染
42
+ * (A 会话的 `clearAllRetainedFleetRows` 不该清掉 B 会话的池,B 断线也不该把 A 标 fresh)。
41
43
  */
42
44
  import type { FleetFrame, FleetTaskRow } from '@sema-agent/sdk';
43
45
  import { type FleetTaskView, type FleetWorkflowView, type ProjectTasksOptions } from './fleetProjection.js';
@@ -63,14 +65,20 @@ export interface EngineActiveBgReading {
63
65
  * 仍广播,fail-closed 是 1.245+)。
64
66
  */
65
67
  export declare function isFleetSessionScoped(): boolean;
68
+ /** W1 keyed variant:只看**这一个会话**登记的视图(REF-CC-044)。 */
69
+ export declare function isFleetSessionScopedFor(sessionKey: string): boolean;
66
70
  /** 引擎侧在飞 bg 子代读数(union 所有未 dispose 的源,按 row.id 去重)。无 live 源 ⇒ `{[], false}`。 */
67
71
  export declare function readEngineActiveBgTasks(): EngineActiveBgReading;
72
+ /** W1 keyed variant(REF-CC-044):只 union **这一个会话**登记的视图,不跨会话泄漏。 */
73
+ export declare function readEngineActiveBgTasksFor(sessionKey: string): EngineActiveBgReading;
68
74
  /**
69
- * 清空所有 live 源的终态留存池 —— 会话切换(/clear、REPL 内 resume、fork)信号一到就调。
70
- * R7:留存池是渲染层宽限窗,快照臂刻意不清它(重连不该抽走行);但换了会话继续渲上一会话的
71
- * 终态行(名字 = objective 派生)就是跨会话残影。
75
+ * 清空**这一个会话**的所有 live 源的终态留存池 —— 会话切换(/clear、REPL 内 resume、fork)信号
76
+ * 一到就调。R7:留存池是渲染层宽限窗,快照臂刻意不清它(重连不该抽走行);但换了会话继续渲上一
77
+ * 会话的终态行(名字 = objective 派生)就是跨会话残影。
72
78
  */
73
79
  export declare function clearAllRetainedFleetRows(): void;
80
+ /** W1 keyed variant(REF-CC-044):只清**这一个会话**的池,不广播到别的会话。 */
81
+ export declare function clearAllRetainedFleetRowsFor(sessionKey: string): void;
74
82
  /** 测试钩:摘掉所有登记的视图(同进程多组断言之间互不污染)。 */
75
83
  export declare function __resetFleetLedgerRegistryForTests(): void;
76
84
  /**
@@ -87,12 +95,25 @@ export declare function fleetSnapshotOptions(opts?: {
87
95
  session?: string;
88
96
  timeoutMs?: number;
89
97
  };
98
+ /** REF-CC-047(fleet2-09):hook_notice 具名臂(B2 禁 unknown 出公开面;Extract 同款手法已在
99
+ * 文件内的 `BgNotificationWire` 示范过)。 */
100
+ export type HookNoticeFrame = Extract<FleetFrame, {
101
+ type: 'hook_notice';
102
+ }>;
90
103
  export interface FleetLedgerHooks {
91
104
  /**
92
105
  * hook_notice 帧的**写 store** 半场(判定在 `classifyHookNoticeFrame`,本台账只做分发)。
93
106
  * 返回值忽略;不给 = 该帧被静默丢弃(端没有承载面时的正确行为)。
94
107
  */
95
- onHookNotice?(frame: unknown, sessionScoped: boolean): void;
108
+ onHookNotice?(frame: HookNoticeFrame, sessionScoped: boolean): void;
109
+ }
110
+ /** REF-CC-044(fleet2-06)/REF-CC-039(fleet2-01):`createFleetLedger` 的可选装配位。 */
111
+ export interface CreateFleetLedgerOptions {
112
+ /** per-session 注册键(W1 同款;多会话宿主每会话一份,零参 = `DEFAULT_SESSION_KEY`)。 */
113
+ sessionKey?: string;
114
+ /** 本地接收时钟注入(测试用确定性时钟;默认 `Date.now`)。REF-CC-039:帧到达时刻的**单一铸点**,
115
+ * 绝不能用 wire 上的 server 时刻(`frame.ts`)顶替 —— 两个不同时钟域的算术是缺陷,不是特性。 */
116
+ nowFn?: () => number;
96
117
  }
97
118
  export interface FleetLedgerStatus {
98
119
  connected: boolean;
@@ -102,6 +123,8 @@ export interface FleetLedgerStatus {
102
123
  bgNotifyFailClosed: boolean;
103
124
  taskCount: number;
104
125
  workflowCount: number;
126
+ /** REF-CC-051(fleet2-13):畸形帧/通知早退的留痕计数(C5 静默丢数据零容忍)。 */
127
+ droppedMalformed: number;
105
128
  }
106
129
  export interface FleetLedger {
107
130
  /** 消费一帧(consumer-side active-set update-in-place / remove)。 */
@@ -129,4 +152,4 @@ export interface FleetLedger {
129
152
  * 建一个 fleet 帧台账。**不开流、不构造 client** —— 宿主的 consume 循环把解码后的帧喂进来。
130
153
  * (这条切缝就是设计稿 §2.5 给 fleetClient 划的那一刀:帧体归库、连接归端。)
131
154
  */
132
- export declare function createFleetLedger(hooks?: FleetLedgerHooks): FleetLedger;
155
+ export declare function createFleetLedger(hooks?: FleetLedgerHooks, opts?: CreateFleetLedgerOptions): FleetLedger;
@@ -7,10 +7,15 @@ import { enqueueBgChildNotification, isOwnWorkflowRun } from '../notifications.j
7
7
  import { isOwnEngineRun, recordBgParentRun, recordBgTerminalFacts, recordOwnEngineRun, } from '../subagentContentStore.js';
8
8
  import { rowIdTail } from '../workflow.js';
9
9
  import { recordEngineTranscriptId } from '../subagent/engineDelegatedPrompt.js';
10
- import { TERMINAL_FLEET_TASK_STATUSES, projectTasks, projectWorkflows, } from './fleetProjection.js';
10
+ import { DEFAULT_SESSION_KEY } from '../sessionSlot.js';
11
+ import { TERMINAL_FLEET_TASK_STATUSES, projectTasks, projectWorkflows, wireParentId, } from './fleetProjection.js';
11
12
  /** 终态行留存宽限窗(187 的 agents 页把完成会话留在列表;窗过即净,防长会话堆行)。 */
12
13
  export const TERMINAL_RETAIN_MS = 60_000;
13
- const liveBgViews = new Set();
14
+ /** REF-CC-044(fleet2-06):per-sessionKey 注册表(W1 同款 `Map<sessionKey, T>` 形)。 */
15
+ const liveBgViews = new Map();
16
+ function viewsFor(sessionKey) {
17
+ return liveBgViews.get(sessionKey) ?? new Set();
18
+ }
14
19
  /**
15
20
  * server 代际证据读面:任一 live fleet 源的 meta 报过 `sessionScoped=true`。
16
21
  * true ⇒ 到达本连接的**行帧**已经过 server 会话过滤,端侧行级 own 判别可以让位。
@@ -18,7 +23,11 @@ const liveBgViews = new Set();
18
23
  * 仍广播,fail-closed 是 1.245+)。
19
24
  */
20
25
  export function isFleetSessionScoped() {
21
- for (const view of liveBgViews) {
26
+ return isFleetSessionScopedFor(DEFAULT_SESSION_KEY);
27
+ }
28
+ /** W1 keyed variant:只看**这一个会话**登记的视图(REF-CC-044)。 */
29
+ export function isFleetSessionScopedFor(sessionKey) {
30
+ for (const view of viewsFor(sessionKey)) {
22
31
  try {
23
32
  if (view.sessionScoped() === true)
24
33
  return true;
@@ -31,9 +40,13 @@ export function isFleetSessionScoped() {
31
40
  }
32
41
  /** 引擎侧在飞 bg 子代读数(union 所有未 dispose 的源,按 row.id 去重)。无 live 源 ⇒ `{[], false}`。 */
33
42
  export function readEngineActiveBgTasks() {
43
+ return readEngineActiveBgTasksFor(DEFAULT_SESSION_KEY);
44
+ }
45
+ /** W1 keyed variant(REF-CC-044):只 union **这一个会话**登记的视图,不跨会话泄漏。 */
46
+ export function readEngineActiveBgTasksFor(sessionKey) {
34
47
  const byId = new Map();
35
48
  let fresh = false;
36
- for (const view of liveBgViews) {
49
+ for (const view of viewsFor(sessionKey)) {
37
50
  try {
38
51
  const r = view.read();
39
52
  if (r.connected)
@@ -48,12 +61,16 @@ export function readEngineActiveBgTasks() {
48
61
  return { tasks: [...byId.values()], fresh };
49
62
  }
50
63
  /**
51
- * 清空所有 live 源的终态留存池 —— 会话切换(/clear、REPL 内 resume、fork)信号一到就调。
52
- * R7:留存池是渲染层宽限窗,快照臂刻意不清它(重连不该抽走行);但换了会话继续渲上一会话的
53
- * 终态行(名字 = objective 派生)就是跨会话残影。
64
+ * 清空**这一个会话**的所有 live 源的终态留存池 —— 会话切换(/clear、REPL 内 resume、fork)信号
65
+ * 一到就调。R7:留存池是渲染层宽限窗,快照臂刻意不清它(重连不该抽走行);但换了会话继续渲上一
66
+ * 会话的终态行(名字 = objective 派生)就是跨会话残影。
54
67
  */
55
68
  export function clearAllRetainedFleetRows() {
56
- for (const view of liveBgViews) {
69
+ clearAllRetainedFleetRowsFor(DEFAULT_SESSION_KEY);
70
+ }
71
+ /** W1 keyed variant(REF-CC-044):只清**这一个会话**的池,不广播到别的会话。 */
72
+ export function clearAllRetainedFleetRowsFor(sessionKey) {
73
+ for (const view of viewsFor(sessionKey)) {
57
74
  try {
58
75
  view.clearRetained();
59
76
  }
@@ -86,13 +103,18 @@ export function fleetSnapshotOptions(opts) {
86
103
  * 建一个 fleet 帧台账。**不开流、不构造 client** —— 宿主的 consume 循环把解码后的帧喂进来。
87
104
  * (这条切缝就是设计稿 §2.5 给 fleetClient 划的那一刀:帧体归库、连接归端。)
88
105
  */
89
- export function createFleetLedger(hooks = {}) {
106
+ export function createFleetLedger(hooks = {}, opts = {}) {
107
+ const sessionKey = opts.sessionKey ?? DEFAULT_SESSION_KEY;
108
+ const nowFn = opts.nowFn ?? Date.now;
90
109
  const taskMap = new Map();
91
110
  const wfMap = new Map();
92
- /** [1481]①「elapsed 恒 0s」补偿:记每行最后一帧到达时刻(引擎只在活动边界重发行)。 */
111
+ /** [1481]①「elapsed 恒 0s」补偿:记每行**本地接收**时刻(引擎只在活动边界重发行)。
112
+ * REF-CC-039:字段标域改名 `receivedAtMs` —— 装的是 `nowFn()`,绝不是 wire 上的 `frame.ts`。 */
93
113
  const rowMeta = new Map();
94
- /** [1481]②「完成即消失」补偿:终态行进留存池,宽限窗内继续投影。 */
114
+ /** [1481]②「完成即消失」补偿:终态行进留存池,宽限窗内继续投影。`expireAt` 同样锚本地钟。 */
95
115
  const retained = new Map();
116
+ /** REF-CC-051(fleet2-13):畸形帧/通知早退计数(与同文件的策略性丢弃 debug 行对称留痕)。 */
117
+ let droppedMalformed = 0;
96
118
  let connected = false;
97
119
  let version = null;
98
120
  let scoped = null;
@@ -108,19 +130,35 @@ export function createFleetLedger(hooks = {}) {
108
130
  connected,
109
131
  rows: [...taskMap.values()]
110
132
  .filter(r => !TERMINAL_FLEET_TASK_STATUSES.has(r.status ?? ''))
111
- .map(r => ({
112
- id: r.id,
113
- name: r.name,
114
- status: r.status ?? 'running',
115
- ...(r.parentId ? { parentId: r.parentId } : {}),
116
- })),
133
+ .map(r => {
134
+ const parentId = wireParentId(r);
135
+ return {
136
+ id: r.id,
137
+ // REF-CC-046(fleet2-08,含 TYPESHAPE-06):与 parentId 同款条件展开 —— `name` 是三态位
138
+ // (server ≥3.5.1 起真的会缺席),无条件落键会让「键在不在」判三态在序列化边界翻面。
139
+ ...(typeof r.name === 'string' && r.name.length > 0 ? { name: r.name } : {}),
140
+ // REF-CC-052(fleet2-14):`status` 是 SDK 必填闭集,但脏值/缺席仍可能到达 wire —— 不知道
141
+ // 就说不知道,`'unknown'` 是自描述哨兵,绝不编造一个具体的、看似活跃的 `'running'`。
142
+ status: r.status ?? 'unknown',
143
+ // REF-CC-049(fleet2-11):经 wireParentId 单一铸点(悬空父脏值空串按缺席处理)。eopt:
144
+ // 局部变量先取一次(两次裸调用之间 TS 不共享窄化,双调用会重新变宽成 string|undefined)。
145
+ ...(parentId !== undefined ? { parentId } : {}),
146
+ };
147
+ }),
117
148
  }),
118
149
  sessionScoped: () => sessionScopedMeta,
119
150
  clearRetained: () => {
120
151
  retained.clear();
121
152
  },
122
153
  };
123
- liveBgViews.add(bgView);
154
+ {
155
+ let views = liveBgViews.get(sessionKey);
156
+ if (!views) {
157
+ views = new Set();
158
+ liveBgViews.set(sessionKey, views);
159
+ }
160
+ views.add(bgView);
161
+ }
124
162
  function applyFrame(frame) {
125
163
  switch (frame.type) {
126
164
  case 'meta': {
@@ -143,10 +181,12 @@ export function createFleetLedger(hooks = {}) {
143
181
  taskMap.clear();
144
182
  wfMap.clear();
145
183
  rowMeta.clear();
146
- const at = frame.ts;
184
+ // REF-CC-039(fleet2-01):单一铸点 —— 整批快照行共享**同一次**本地接收时刻,绝不用
185
+ // wire 上的 `frame.ts`(server 时刻,server 省略该键时兜成 0,与本地钟不同域)。
186
+ const receivedAtMs = nowFn();
147
187
  for (const row of frame.tasks) {
148
188
  taskMap.set(row.id, row);
149
- rowMeta.set(row.id, { receivedAt: at });
189
+ rowMeta.set(row.id, { receivedAtMs });
150
190
  }
151
191
  for (const row of frame.workflows)
152
192
  wfMap.set(row.id, row);
@@ -154,10 +194,15 @@ export function createFleetLedger(hooks = {}) {
154
194
  }
155
195
  case 'task': {
156
196
  const row = frame.row;
157
- if (!row?.id)
197
+ if (!row?.id) {
198
+ droppedMalformed++;
199
+ debug('[fleet-frame] MALFORMED task frame dropped(row 或 row.id 缺席)');
158
200
  break;
201
+ }
202
+ // REF-CC-039(fleet2-01):同一个字段只有一个铸点 —— 本地接收时刻,不是 `frame.ts`。
203
+ const receivedAtMs = nowFn();
159
204
  taskMap.set(row.id, row); // POST-MERGE FULL row → replace by id
160
- rowMeta.set(row.id, { receivedAt: frame.ts });
205
+ rowMeta.set(row.id, { receivedAtMs });
161
206
  debug(`[fleet-row] id=${row.id} parent=${row.parentId ?? '-'} agentType=${row.agentType ?? '-'}` +
162
207
  ` agentName=${row.agentName ?? '-'} name=${row.name ?? '-'} status=${row.status ?? '-'}` +
163
208
  ` tokens=${row.tokens ?? 0} parentToolCallId=${row.parentToolCallId ?? '-'}`);
@@ -169,9 +214,10 @@ export function createFleetLedger(hooks = {}) {
169
214
  // (或无标行)→ 登映射**不登台账**(台账只收硬证据,防无标外来行污染台账后反过来给
170
215
  // 通知臂放行);这也保住「壳重启+resume 接回会话」的终报 hydrate;
171
216
  // · 两者都不是(老代际广播窗)→ 不登映射(隔离优先)。
172
- if (row.parentId) {
217
+ const parentId = wireParentId(row);
218
+ if (parentId !== undefined) {
173
219
  const childTid = rowIdTail(row.id);
174
- const parentTid = rowIdTail(row.parentId);
220
+ const parentTid = rowIdTail(parentId);
175
221
  const parentOwned = isOwnEngineRun(parentTid);
176
222
  if (parentOwned)
177
223
  recordOwnEngineRun(childTid);
@@ -188,8 +234,14 @@ export function createFleetLedger(hooks = {}) {
188
234
  recordEngineTranscriptId(rowIdTail(row.id), row.transcriptId);
189
235
  }
190
236
  // 终态帧 → 留存池(remove 同毫秒尾随,probe 实证;留存让 completed band 数得到它)。
237
+ // REF-CC-040(fleet2-02):非终态分支必须显式 delete —— 否则 bg 复活(SendMessage 后
238
+ // seq+1)时,`project()` 因 `taskMap.has(id)` 恒 continue 而永不清理过期留存条目,belt
239
+ // 的 `!retained.has()` 前提失守,第二周期的终态被首周期的陈旧快照挡在门外。
191
240
  if (TERMINAL_FLEET_TASK_STATUSES.has(row.status ?? '')) {
192
- retained.set(row.id, { row, expireAt: frame.ts + TERMINAL_RETAIN_MS });
241
+ retained.set(row.id, { row, expireAt: receivedAtMs + TERMINAL_RETAIN_MS });
242
+ }
243
+ else {
244
+ retained.delete(row.id);
193
245
  }
194
246
  break;
195
247
  }
@@ -198,8 +250,13 @@ export function createFleetLedger(hooks = {}) {
198
250
  rowMeta.delete(frame.id);
199
251
  break;
200
252
  case 'workflow':
201
- if (frame.row?.id)
253
+ if (frame.row?.id) {
202
254
  wfMap.set(frame.row.id, frame.row);
255
+ }
256
+ else {
257
+ droppedMalformed++;
258
+ debug('[fleet-frame] MALFORMED workflow frame dropped(row 或 row.id 缺席)');
259
+ }
203
260
  break;
204
261
  case 'workflow_remove':
205
262
  wfMap.delete(frame.id);
@@ -221,10 +278,16 @@ export function createFleetLedger(hooks = {}) {
221
278
  break;
222
279
  }
223
280
  }
224
- /** bg 子代终态通知帧([446] 帧契约,service 1.119):常开 lane 即时到达 → 转注入链。 */
281
+ /** bg 子代终态通知帧([446] 帧契约,service 1.119):常开 lane 即时到达 → 转注入链。
282
+ * REF-CC-048(fleet2-10):参数类型此前写成 `FleetFrame extends never ? never : BgNotificationWire`
283
+ * —— 条件类型只对**裸类型参数**分发,`FleetFrame` 是具体 union,整个表达式恒等于
284
+ * `BgNotificationWire`,伪装成了一个从不成立的守卫。直写目标类型,不留误导性的仪式感。 */
225
285
  function applyBgNotification(n) {
226
- if (typeof n?.taskId !== 'string' || typeof n?.status !== 'string')
286
+ if (typeof n?.taskId !== 'string' || typeof n?.status !== 'string') {
287
+ droppedMalformed++;
288
+ debug('[fleet-frame] MALFORMED bg_notification dropped(taskId 或 status 缺席/非串)');
227
289
  return;
290
+ }
228
291
  // 🔴 own/foreign 判别(双开泄漏案帧级防线;[1498]④ 换锚重写):payload `sessionId` 是**子代
229
292
  // 自身** session uuid,宿主会话在 ownerSessionId 上且出 wire 前被剥 —— 所以判别锚
230
293
  // `parentTaskId`(spawn 该子代的 leader run id)对 own-run 台账:
@@ -282,11 +345,24 @@ export function createFleetLedger(hooks = {}) {
282
345
  : {}),
283
346
  });
284
347
  // belt:终态**行帧**丢失(kill 竞态等)时,已知行也随通知转终态进留存池。
348
+ // REF-CC-040(fleet2-02):无条件覆盖(不再靠 `!retained.has()` 判「池里没有它」)—— 「谁写得
349
+ // 对」不该依赖清理时序;'task' 非终态分支现在会显式 delete,belt 这里覆盖同样安全。
350
+ // REF-CC-039:`expireAt` 同一铸点(`nowFn()`),不用 `Date.now()` 另开一个时钟域。
351
+ //
352
+ // 🔴 取舍写死(P3 wave2 回炉,复审 advisory①:finding proposal 两种形都允许,本处取的是
353
+ // 「无条件覆盖」而不是「按 seq 择新」,两个方向的后果各钉了一条常驻断言):
354
+ // · **通知晚于终态行帧** ⇒ 覆盖。通知是 settle 权威(下方 `recordBgTerminalFacts` 就是
355
+ // 无条件按 `n.status` 记的账),留存池若不跟着覆盖,面板行渲 `completed` 而详情页写
356
+ // `killed` = 同一帧两写分叉。钉:pure 门「留存池行的 status 与终态事实台账不分叉」。
357
+ // · **非终态行帧晚于 belt** ⇒ 抹掉 belt 结果(`case 'task'` 的 `retained.delete`)。这正是
358
+ // 复活语义:行又活了,首周期的终态快照本就不该继续投影。钉:pure 门「复活(非终态)
359
+ // 一到就清留存条目 ⇒ 首周期陈旧终态行不得复现为幽灵」。
360
+ // 要改成 seq 择新形,先把这两条断言的期望值一起改 —— 它们是这个取舍的可执行形。
285
361
  const known = taskMap.get(n.taskId);
286
- if (known && !retained.has(n.taskId)) {
362
+ if (known) {
287
363
  retained.set(n.taskId, {
288
364
  row: { ...known, status: n.status },
289
- expireAt: Date.now() + TERMINAL_RETAIN_MS,
365
+ expireAt: nowFn() + TERMINAL_RETAIN_MS,
290
366
  });
291
367
  }
292
368
  }
@@ -313,7 +389,7 @@ export function createFleetLedger(hooks = {}) {
313
389
  const meta = rowMeta.get(r.id);
314
390
  if (!meta)
315
391
  return r;
316
- const idle = Math.max(0, nowMs - meta.receivedAt);
392
+ const idle = Math.max(0, nowMs - meta.receivedAtMs);
317
393
  return idle > 0 ? { ...r, elapsedMs: (r.elapsedMs ?? 0) + idle } : r;
318
394
  });
319
395
  // ② 终态留存池:live 集没有的行在宽限窗内继续投影;被查看中的行不过期。
@@ -344,15 +420,18 @@ export function createFleetLedger(hooks = {}) {
344
420
  return true;
345
421
  // 祖链闭包:孙代行 parentId=直接父(a… handle)而非 spawn run([1502] wire 事实)——
346
422
  // 沿 parentId 链上溯,任一环(或环的 parentId)∈ 本端 run 台账即 own。
423
+ // REF-CC-049(fleet2-11):`wireParentId` 单一铸点(空串按缺席),不再裸读 `cur.parentId`
424
+ // 做 `!== undefined` 判断 —— 那种写法对空串脏值判「有父」,与 case 'task' 那半支相反。
347
425
  const seen = new Set();
348
426
  let cur = r;
349
427
  while (cur && !seen.has(cur.id)) {
350
428
  seen.add(cur.id);
351
429
  if (isOwnEngineRun(cur.id))
352
430
  return true;
353
- if (cur.parentId !== undefined && isOwnEngineRun(cur.parentId))
431
+ const parentId = wireParentId(cur);
432
+ if (parentId !== undefined && isOwnEngineRun(parentId))
354
433
  return true;
355
- cur = cur.parentId !== undefined ? allRowsById.get(cur.parentId) : undefined;
434
+ cur = parentId !== undefined ? allRowsById.get(parentId) : undefined;
356
435
  }
357
436
  return false;
358
437
  };
@@ -383,10 +462,16 @@ export function createFleetLedger(hooks = {}) {
383
462
  bgNotifyFailClosed: bgNotifyFailClosedMeta,
384
463
  taskCount: taskMap.size,
385
464
  workflowCount: wfMap.size,
465
+ droppedMalformed,
386
466
  }),
387
467
  dispose() {
388
468
  disposed = true;
389
- liveBgViews.delete(bgView);
469
+ const views = liveBgViews.get(sessionKey);
470
+ if (views) {
471
+ views.delete(bgView);
472
+ if (views.size === 0)
473
+ liveBgViews.delete(sessionKey);
474
+ }
390
475
  },
391
476
  __rowsForTest: () => [...taskMap.values()],
392
477
  };
@@ -50,9 +50,13 @@ export interface FleetTaskView {
50
50
  tokens: number;
51
51
  tokenDir: 'down' | 'up';
52
52
  queuedCount: number;
53
- awaitingPlanApproval?: boolean;
54
- descendantCount?: number;
55
- viewed?: boolean;
53
+ /** REF-CC-050(fleet2-12):投影器无条件赋值(`?? false`),门 `ALWAYS_EMITTED_TASK_VIEW_KEYS`
54
+ * 已把它钉成「恒发」——类型必填而不是留一条测不到的缺席分支。 */
55
+ awaitingPlanApproval: boolean;
56
+ /** REF-CC-050:同上,`childCount.get(r.id) ?? 0` 恒赋值。 */
57
+ descendantCount: number;
58
+ /** REF-CC-050:同上,`viewingTaskId !== undefined && …` 恒赋值(布尔运算永不产 undefined)。 */
59
+ viewed: boolean;
56
60
  /** 🔴 三态:有键 ⇒ 真值(含真 0);缺键 ⇒ 「不知道」。绝不 `?? 0`。 */
57
61
  toolUses?: number;
58
62
  transcriptId?: string;
@@ -133,8 +137,15 @@ export declare const TERMINAL_FLEET_TASK_STATUSES: ReadonlySet<string>;
133
137
  * design/153 §7.1),server 只是尚未投影 ⇒ 这不是「猜测层」,是**预挂**。
134
138
  * 删的收益 = 一行;删的代价 = server 补上透传那天静默说谎。
135
139
  * ⇒ 保留 + 把「候 server 预挂」的注释换成上面这条可复核的直证。定谳权交回 PM。
140
+ *
141
+ * 🔴 REF-CC-043(fleet2-05,P2 裁 GO,宪法三问已过):`default` 分支此前渲 `'running'` ——
142
+ * 未知未来状态被谎报成「活跃」,后果方向是**伤害型**(用户会以为一个已经僵住/退场的子代还在跑)。
143
+ * 改渲 `'idle'`(闭集里本就存在的「非活跃」词,187 用它表示「非活跃、非终态、非阻塞」)—— 门
144
+ * `ALWAYS_EMITTED_TASK_VIEW_KEYS` 不受影响,只是同一个已知渲染词的语义更诚实。
145
+ * `s` 的参数类型同批收窄:`| undefined` 半支从未被真实调用点使用过(唯一生产调用点
146
+ * `projectTasks` 传的是 SDK 必填闭集 `FleetTaskRow['status']`),继续留着是死分支。
136
147
  */
137
- export declare function coerceTaskStatus(s: string | undefined): FleetTaskStatus;
148
+ export declare function coerceTaskStatus(s: string): FleetTaskStatus;
138
149
  /** 中性 workflow run 状态(running|completed|failed)→ 渲染词汇。 */
139
150
  export declare function coerceWorkflowStatus(s: string | undefined): FleetTaskStatus;
140
151
  /**
@@ -183,6 +194,13 @@ export declare function wireCurrentTool(r: Pick<FleetTaskRow, 'currentTool'>): {
183
194
  export declare function wireStartedCount(r: Pick<FleetWorkflowRow, 'startedCount'>): number | undefined;
184
195
  /** `parentToolCallId`(P1-2)—— 归属直读键。空串按缺席处理(引擎不该发,发了也别当真)。 */
185
196
  export declare function wireParentToolCallId(r: Pick<FleetTaskRow, 'parentToolCallId'>): string | undefined;
197
+ /**
198
+ * `parentId` —— 悬空父的存在性判定唯一铸点(REF-CC-049,fleet2-11)。B10 禁真值判定当存在性判定:
199
+ * 空串是脏值形(SDK 只承诺「悬空不发」,那是 server 当下行为不是类型保证),按缺席处理。
200
+ * `fleetLedger.ts` 与本文件的 `projectTasks` 都调它 —— 一个谓词一个铸点,防止 `!== undefined`
201
+ * 与真值判定两种写法在同一字段上判读相反(悬空父脏值 `""` 时曾经真的翻过面)。
202
+ */
203
+ export declare function wireParentId(r: Pick<FleetTaskRow, 'parentId'>): string | undefined;
186
204
  /** 终态四键之 `usage`(数值 verbatim,只挑数字位;全脏 ⇒ 整键缺席,不造空对象)。 */
187
205
  export declare function wireRowUsage(r: Pick<FleetTaskRow, 'usage'>): FleetRowUsage | undefined;
188
206
  /** 终态四键之 `editedFiles`(bg 子代专属;脏项逐条丢,不整键丢)。 */
@@ -220,6 +238,26 @@ export declare const FLEET_WORKFLOW_VIEW_KEYS: readonly string[];
220
238
  export declare const FLEET_TASK_ROW_WIRE_KEYS: readonly string[];
221
239
  /** SDK `FleetWorkflowRow` 的入口键名清单(同上)。 */
222
240
  export declare const FLEET_WORKFLOW_ROW_WIRE_KEYS: readonly string[];
241
+ /**
242
+ * 不投影登记表的一条条目(REF-CC-053,fleet2-15,P2 裁 GO)。
243
+ *
244
+ * 🔴 表原先只要求「写够 20 字理由」,而一条自认的**已知缺口**(`currentAction`,补它=真行为改动)
245
+ * 与三条**设计取舍**(折进别的键 / 归属权威在别处)共用同一条放行通道 —— 门只能证明「有人表过态」,
246
+ * 证明不了这态是「已裁定不做」还是「欠着没人管」。`kind` 把两者拆成两条不同的路:
247
+ * · `by-design`:裁定不投影,不需要到期日(它不会自己变成待办)。
248
+ * · `known-gap`:真缺口,`dueDate` 到期未清 ⇒ 门红(与规范「具名豁免带属主与到期日,过期即红」
249
+ * 同款,`run-fleet-view-keys-test.mjs` 的 D 段做校验)。
250
+ */
251
+ export interface NotProjectedEntry {
252
+ /** 为什么不投影(≥20 字,原「理由」字段原样保留)。 */
253
+ reason: string;
254
+ /** `by-design` = 裁定的架构取舍;`known-gap` = 自认的缺口,不是取舍。 */
255
+ kind: 'by-design' | 'known-gap';
256
+ /** 谁负责这条判定 / 谁负责把 known-gap 补上。 */
257
+ owner: string;
258
+ /** 仅 `known-gap` 必填:ISO 日期,过期未清 = 门红(禁无期限豁免)。 */
259
+ dueDate?: string;
260
+ }
223
261
  /**
224
262
  * **不投影登记表**:wire 上有、`FleetTaskView` 上**故意没有**的键,每条必须写清理由。
225
263
  *
@@ -228,7 +266,7 @@ export declare const FLEET_WORKFLOW_ROW_WIRE_KEYS: readonly string[];
228
266
  * 每个 wire 键只有两条出路:**要么投影出去,要么在这里写下不投的理由**;新 wire 键在覆盖账那条
229
267
  * 腿上当场红,不给「悄悄没人管」留位置。
230
268
  */
231
- export declare const FLEET_TASK_ROW_KEYS_NOT_PROJECTED: Readonly<Record<string, string>>;
269
+ export declare const FLEET_TASK_ROW_KEYS_NOT_PROJECTED: Readonly<Record<string, NotProjectedEntry>>;
232
270
  /** workflow 行:wire 十键与视图十键一一同名同义 ⇒ 本表**空**(空不是「忘了填」,是覆盖账那条腿
233
271
  * 每跑一次都在证明它该空:少投一个键就红)。 */
234
- export declare const FLEET_WORKFLOW_ROW_KEYS_NOT_PROJECTED: Readonly<Record<string, string>>;
272
+ export declare const FLEET_WORKFLOW_ROW_KEYS_NOT_PROJECTED: Readonly<Record<string, NotProjectedEntry>>;
@@ -24,6 +24,13 @@ export const TERMINAL_FLEET_TASK_STATUSES = new Set([
24
24
  * design/153 §7.1),server 只是尚未投影 ⇒ 这不是「猜测层」,是**预挂**。
25
25
  * 删的收益 = 一行;删的代价 = server 补上透传那天静默说谎。
26
26
  * ⇒ 保留 + 把「候 server 预挂」的注释换成上面这条可复核的直证。定谳权交回 PM。
27
+ *
28
+ * 🔴 REF-CC-043(fleet2-05,P2 裁 GO,宪法三问已过):`default` 分支此前渲 `'running'` ——
29
+ * 未知未来状态被谎报成「活跃」,后果方向是**伤害型**(用户会以为一个已经僵住/退场的子代还在跑)。
30
+ * 改渲 `'idle'`(闭集里本就存在的「非活跃」词,187 用它表示「非活跃、非终态、非阻塞」)—— 门
31
+ * `ALWAYS_EMITTED_TASK_VIEW_KEYS` 不受影响,只是同一个已知渲染词的语义更诚实。
32
+ * `s` 的参数类型同批收窄:`| undefined` 半支从未被真实调用点使用过(唯一生产调用点
33
+ * `projectTasks` 传的是 SDK 必填闭集 `FleetTaskRow['status']`),继续留着是死分支。
27
34
  */
28
35
  export function coerceTaskStatus(s) {
29
36
  switch (s) {
@@ -40,7 +47,8 @@ export function coerceTaskStatus(s) {
40
47
  case 'parked':
41
48
  return 'awaiting approval';
42
49
  default:
43
- return 'running'; // unknown future status render as a generic live row (open-set fallback)
50
+ // 未知未来状态渲成非活跃中性词(开集回落;绝不谎报活跃,REF-CC-043)
51
+ return 'idle';
44
52
  }
45
53
  }
46
54
  /** 中性 workflow run 状态(running|completed|failed)→ 渲染词汇。 */
@@ -145,6 +153,16 @@ export function wireParentToolCallId(r) {
145
153
  const v = r.parentToolCallId;
146
154
  return typeof v === 'string' && v.length > 0 ? v : undefined;
147
155
  }
156
+ /**
157
+ * `parentId` —— 悬空父的存在性判定唯一铸点(REF-CC-049,fleet2-11)。B10 禁真值判定当存在性判定:
158
+ * 空串是脏值形(SDK 只承诺「悬空不发」,那是 server 当下行为不是类型保证),按缺席处理。
159
+ * `fleetLedger.ts` 与本文件的 `projectTasks` 都调它 —— 一个谓词一个铸点,防止 `!== undefined`
160
+ * 与真值判定两种写法在同一字段上判读相反(悬空父脏值 `""` 时曾经真的翻过面)。
161
+ */
162
+ export function wireParentId(r) {
163
+ const v = r.parentId;
164
+ return typeof v === 'string' && v.length > 0 ? v : undefined;
165
+ }
148
166
  /** 终态四键之 `usage`(数值 verbatim,只挑数字位;全脏 ⇒ 整键缺席,不造空对象)。 */
149
167
  export function wireRowUsage(r) {
150
168
  const u = r.usage;
@@ -211,6 +229,7 @@ export function projectTasks(allRows, opts) {
211
229
  const toolUses = wireToolUses(r);
212
230
  const transcriptId = wireTranscriptId(r);
213
231
  const currentTool = wireCurrentTool(r);
232
+ const parentId = wireParentId(r);
214
233
  const parentToolCallId = wireParentToolCallId(r);
215
234
  const stoppedBy = wireStoppedBy(r);
216
235
  const usage = wireRowUsage(r);
@@ -222,7 +241,7 @@ export function projectTasks(allRows, opts) {
222
241
  // 子行帧无 description → 从 name 复原任务描述(CC 锚:行正文恒显 description,不留空列)。
223
242
  description: projectDescription(r.description) || taskDescFromName(r.name, r.agentType, r.agentName),
224
243
  depth: depthOf(r),
225
- ...(r.parentId !== undefined ? { parentId: r.parentId } : {}),
244
+ ...(parentId !== undefined ? { parentId } : {}),
226
245
  status: coerceTaskStatus(r.status),
227
246
  elapsedMs: r.elapsedMs ?? 0,
228
247
  tokens: r.tokens ?? 0,
@@ -368,16 +387,39 @@ export const FLEET_WORKFLOW_ROW_WIRE_KEYS = FLEET_WORKFLOW_ROW_WIRE_KEY_TUPLE;
368
387
  * 腿上当场红,不给「悄悄没人管」留位置。
369
388
  */
370
389
  export const FLEET_TASK_ROW_KEYS_NOT_PROJECTED = {
371
- sourceLane: 'SDK 3.0.0 新键([2070]① 让位半场:前台 delegation 复合 id 行带 "run-leg" 标)。两仓现零消费面 ——' +
372
- '#95 纪律:没人读的键不盲投。已知潜在消费方=壳 wf-panel 的双生行过滤(现走 taskId 台账补偿),' +
373
- '若迁到本键判别则届时投影并删本条(那是行为改动,单独批)。',
374
- agentType: '折进 `name`(deriveAgentLabel:agentName ?? agentType,都缺才退 objective 派生)——渲染契约只有一个身份列,187 tjl 同形。',
375
- agentName: ' agentType:两位在本层合成一个身份 label,分开带出去等于把 187 tjl 规则推给每个消费端各写一遍。',
376
- workflowRunId: 'workflow 归属**不走行字段**:权威判定在 adapt 的 task_progress 臂(recordWorkflowAgentTaskId,判据④),端按 taskId 尾段查台账过滤双生行。行上再带一份 = 第二真源。',
377
- currentAction: '结构化同源体 `currentTool` 已投影(SDK 注:currentAction 是它的**文本回落**)。🔴 诚实记账:旧端(server <1.288)只发 currentAction ⇒ 那一档上本视图今天确实拿不到活动文本,这是**已知缺口**不是设计取舍;补它 = 加 `currentAction?: string` 进本视图 + 端侧回落渲染。',
390
+ sourceLane: {
391
+ kind: 'by-design',
392
+ owner: 'client-core 规范重构轮',
393
+ reason: 'SDK 3.0.0 新键([2070]① 让位半场:前台 delegation 复合 id 行带 "run-leg" 标)。两仓现零消费面 ——' +
394
+ '#95 纪律:没人读的键不盲投。已知潜在消费方=壳 wf-panel 的双生行过滤(现走 taskId 台账补偿),' +
395
+ '若迁到本键判别则届时投影并删本条(那是行为改动,单独批)',
396
+ },
397
+ agentType: {
398
+ kind: 'by-design',
399
+ owner: 'client-core 规范重构轮',
400
+ reason: '折进 `name`(deriveAgentLabel:agentName ?? agentType,都缺才退 objective 派生)——渲染契约只有一个身份列,187 的 tjl 同形。',
401
+ },
402
+ agentName: {
403
+ kind: 'by-design',
404
+ owner: 'client-core 规范重构轮',
405
+ reason: '同 agentType:两位在本层合成一个身份 label,分开带出去等于把 187 的 tjl 规则推给每个消费端各写一遍。',
406
+ },
407
+ workflowRunId: {
408
+ kind: 'by-design',
409
+ owner: 'client-core 规范重构轮',
410
+ reason: 'workflow 归属**不走行字段**:权威判定在 adapt 的 task_progress 臂(recordWorkflowAgentTaskId,判据④),端按 taskId 尾段查台账过滤双生行。行上再带一份 = 第二真源。',
411
+ },
412
+ currentAction: {
413
+ kind: 'known-gap',
414
+ owner: 'client-core 规范重构轮',
415
+ dueDate: '2027-02-01',
416
+ reason: '结构化同源体 `currentTool` 已投影(SDK 注:currentAction 是它的**文本回落**)。🔴 诚实记账:旧端(server <1.288)只发 currentAction ⇒ 那一档上本视图今天确实拿不到活动文本,这是**已知缺口**不是设计取舍;补它 = 加 `currentAction?: string` 进本视图 + 端侧回落渲染。',
417
+ },
378
418
  };
379
419
  /** workflow 行:wire 十键与视图十键一一同名同义 ⇒ 本表**空**(空不是「忘了填」,是覆盖账那条腿
380
420
  * 每跑一次都在证明它该空:少投一个键就红)。 */
381
421
  export const FLEET_WORKFLOW_ROW_KEYS_NOT_PROJECTED = {};
382
- const _fleetViewKeyPins = [true, true, true, true, true, true, true, true];
422
+ // `Covers`(REF-CC-145/dup-12,P3 wave1 卡C-argv)改吃共用叶 ../typePins.js——四份逐字重复钉之一
423
+ // (`Exclude<A,B> extends never` = 「A 的每个成员都在 B 里」;不成立时类型是 `never`,赋 `true` 即编译红)。
424
+ const _fleetViewKeyPins = [true, true, true, true, true, true, true, true, true, true];
383
425
  void _fleetViewKeyPins;
@@ -39,12 +39,29 @@ export interface FleetAgentRowLike {
39
39
  }
40
40
  /** 行从投影集消失多久之后按 completed 兜底 settle(重连空窗 / snapshot 清表远小于此)。 */
41
41
  export declare const ABSENT_SETTLE_MS = 30000;
42
- /** 测试钩:清投影台账。 */
42
+ /**
43
+ * 测试钩:清投影台账 —— **全部 sessionKey**,不是只清 default 桶。
44
+ *
45
+ * 🔴 回炉定谳(P3 wave2 复审 blocker④):per-session 化那一批一度把它收窄成
46
+ * `resetFleetAgentPanelProjectionFor(DEFAULT_SESSION_KEY)`。它的契约是「清投影台账」,不是一个
47
+ * 会话语义位 —— 收窄等于给多会话宿主的用例之间留一条跨用例串扰通道(用例 A 先
48
+ * `projectFleetAgentRowsFor('sess-b', …)`,用例间调零参 reset,`sess-b` 桶存活 ⇒ 下个用例首次
49
+ * 投影同 taskId 时 `prev` 非空且 `settled:true`,`fleet-row`/`end` 被去重门吞掉)。
50
+ * 对位实现同形:`fleet/fleetLedger.__resetFleetLedgerRegistryForTests()` = `liveBgViews.clear()`;
51
+ * W1 段五个槽位的现成规约也逐个钉的是「reset 清全部键」。
52
+ * 要只清一个会话请用 `resetFleetAgentPanelProjectionFor(sessionKey)`。
53
+ */
43
54
  export declare function resetFleetAgentPanelProjection(): void;
55
+ /** W1 keyed variant(REF-CC-045):只清**这一个会话**的桶。 */
56
+ export declare function resetFleetAgentPanelProjectionFor(sessionKey: string): void;
44
57
  /** 台账读面(测试/诊断):当前记着的行数。 */
45
58
  export declare function fleetAgentProjectionSize(): number;
59
+ /** W1 keyed variant。 */
60
+ export declare function fleetAgentProjectionSizeFor(sessionKey: string): number;
46
61
  /**
47
62
  * 把一次 fleet 投影(已经过 own 判别 + workflow belt 的 FleetTask[])喂给面板事件流。
48
63
  * 幂等、无副作用地重复调用安全 —— 每秒被 footer 调用两次就是常态。
49
64
  */
50
65
  export declare function projectFleetAgentRows(rows: readonly FleetAgentRowLike[], nowMs?: number): void;
66
+ /** W1 keyed variant(REF-CC-045):多会话宿主每会话一份台账,互不顶盖。 */
67
+ export declare function projectFleetAgentRowsFor(sessionKey: string, rows: readonly FleetAgentRowLike[], nowMs?: number): void;