@sema-agent/client-core 0.8.0 → 0.9.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.
@@ -49,6 +49,46 @@ export declare function taskNotificationDedupKey(n: {
49
49
  seq?: number | string;
50
50
  taskType?: string;
51
51
  }): string;
52
+ /**
53
+ * ⇄ B7(T2):同一把键的 **裸 wire 载荷** 入口 —— cli `upstreamBridge.engineTaskNotificationDedupKey`
54
+ * 的等价物(入参是引擎 `task_notification` 帧的原始 payload:snake_case `task_id` / `task_type`)。
55
+ *
56
+ * 🔴 **不是第二份算法**:字段归一之后直接委托给上面那一个 `taskNotificationDedupKey`。
57
+ * 两份手写算法正是这条键在 0.1.1 被 codex 对抗复审抓到过的形(无 seq 追不追段的差异会让
58
+ * 「重放去重」整条失效而两边代码看着都对);pure 门 B7 段对两个入口做**同键对拍**。
59
+ *
60
+ * 键形:`[external:]taskId:status[:seq]`。缺 `task_id` ⇒ 空 taskId 段(与 cli 逐字一致 ——
61
+ * 空 id 的通知本就在 `normalizeTaskNotification` 那一层被丢弃,这里不额外加语义)。
62
+ *
63
+ * ⚠️ **P1-3(完成事件单一权威)未到货 ⇒ 只搬不删**:引擎给出 `completionId` 之后,这把
64
+ * 「客户端按 taskId:status[:seq] 自己重算」的键才能换成「读引擎发的 id」。退役条件见
65
+ * `compensations.ts` 的 T2 行。
66
+ */
67
+ export declare function taskNotificationDedupKeyFromWire(n: Record<string, unknown>): string;
68
+ /**
69
+ * ⇄ B7(T3):从**落盘转录**里的 `<task-notification>` 块反解去重键 —— #63 resume 回植的
70
+ * **纯解析半场**(适配器实例的 `importLedgerFromTranscript` 与宿主的 resume 装载腿共用这一份)。
71
+ *
72
+ * 为什么要单独导出:cli 里这段解析有**两份**(`upstreamBridge.seedEngineTaskNotificationDedupFromTranscript`
73
+ * 与包内 `WireToCcAdapterImpl.importLedgerFromTranscript`),两份的差异会让 resume 回植在某一侧
74
+ * 静默失效。B7 把解析收成一份纯函数,两个消费口都调它;壳侧那份 shim 化后直接调
75
+ * `bridgeAdapter().importLedgerFromTranscript(...)`。
76
+ *
77
+ * 🔴 键形必须是**无 seq** 的:落盘 XML 不带 seq,Monitor 的 `:seq` 批键形与之不同 —— 这正是
78
+ * 「绝不误伤 Monitor 多批流」的判据(cli `taskNotificationResumeSeed` 单测双向钉的就是它)。
79
+ *
80
+ * ⚠️ **P1-3 到货后**:仍需回植(crash 窗口是客观存在的),但不必再解 XML —— 转录里会直接带
81
+ * `completionId`。退役条件见 `compensations.ts` 的 T3 行。
82
+ */
83
+ export interface TranscriptNotificationSeeds {
84
+ /** 渲染去重键(`[external:]taskId:status`,**无 seq 段**)。 */
85
+ renderedKeys: string[];
86
+ /** 跨通道记账键(裸 taskId;仅**非 external** 的终态 completed/failed/killed)。 */
87
+ notifiedKeys: string[];
88
+ /** 解出的块数(= cli `seeded` 返回值)。 */
89
+ seeded: number;
90
+ }
91
+ export declare function parseTranscriptNotificationSeeds(messages: ReadonlyArray<unknown>): TranscriptNotificationSeeds;
52
92
  /** 宿主队列条目的最小结构形(壳 QueuedCommand 的子集;本包只读这三个键)。 */
53
93
  export interface QueuedCommandLike {
54
94
  mode?: unknown;
@@ -63,6 +103,23 @@ export interface NotificationQueuePort {
63
103
  }): void;
64
104
  /** 壳 = messageQueueManager.removeByFilter;返回被摘掉的条目(数量是判据)。 */
65
105
  removeByFilter(predicate: (cmd: QueuedCommandLike) => boolean): QueuedCommandLike[];
106
+ /**
107
+ * B7:`mode:'prompt'` 的 **isMeta** 回植(今天唯一的消费者 = plan_review 结局,
108
+ * `hitl/planReviewWire.ts`)。与上面那条是**不同的队列语义**(task-notification 走
109
+ * `<task-notification>` 通道;这条是一个 isMeta 的 prompt turn),所以是第二个动词而不是
110
+ * 给第一个加参数。
111
+ *
112
+ * 🔴 **可选**(`?`):既有宿主(壳 B2 起装的那份)不实现它照样满足接口 —— 加必填方法会让所有
113
+ * 已装配的宿主在类型层一起红,而这条动词只有一个消费者。缺席 ⇒ 计 miss + 该结局不回植
114
+ * (模型在 approve 之后原地不动),**不是**静默可接受的面,所以计 miss 而不是静默。
115
+ */
116
+ enqueueMetaPrompt?(command: {
117
+ value: string;
118
+ mode: 'prompt';
119
+ priority: 'later';
120
+ isMeta: true;
121
+ workload: string;
122
+ }): void;
66
123
  }
67
124
  /** 宿主装配:壳侧 shim / web 宿主在**模块加载期**装(装之前的调用会计入 miss)。 */
68
125
  export declare function installNotificationQueuePort(port: NotificationQueuePort | null): void;
@@ -81,6 +138,16 @@ export declare function _resetNotificationQueuePortForTest(): void;
81
138
  * (enqueue 两族与 bridge 渲染同用 escapeXml,键形字节对齐)。仅主线程 task-notification
82
139
  * 条目(agentId undefined);用户输入/其他模式绝不触碰。
83
140
  */
141
+ /**
142
+ * B7:plan_review 结局回植(`hitl/planReviewWire.ts` 的唯一调用点)。走队列口的
143
+ * `enqueueMetaPrompt` 可选动词 —— 口或动词缺席 ⇒ 计 miss(与 `port()` 同一个计数器,宿主
144
+ * 只看 `hostPortMisses()` 一个数)并**返回 false**,调用方 fail-soft。
145
+ *
146
+ * 🔴 为什么不复用 `enqueuePendingNotification`:那条走的是 `<task-notification>` 通道(队列
147
+ * 的 `task-notification` 模式,drain 时按通知语义处理);plan_review 结局是一个 **isMeta 的
148
+ * prompt turn**。混用 = 结局被当成后台任务完成通知渲染,是「看起来送到了」的假绑定。
149
+ */
150
+ export declare function enqueuePlanReviewOutcome(value: string): boolean;
84
151
  export declare function dropQueuedNotificationsForRun(taskId: string): number;
85
152
  /**
86
153
  * Pre-seed the runId dedup WITHOUT enqueueing — the completion already reached the model in-band
@@ -99,6 +99,80 @@ export function taskNotificationDedupKey(n) {
99
99
  const lane = n.taskType === 'external' ? 'external:' : '';
100
100
  return `${lane}${n.taskId}:${n.status}${seq ? `:${seq}` : ''}`;
101
101
  }
102
+ /**
103
+ * ⇄ B7(T2):同一把键的 **裸 wire 载荷** 入口 —— cli `upstreamBridge.engineTaskNotificationDedupKey`
104
+ * 的等价物(入参是引擎 `task_notification` 帧的原始 payload:snake_case `task_id` / `task_type`)。
105
+ *
106
+ * 🔴 **不是第二份算法**:字段归一之后直接委托给上面那一个 `taskNotificationDedupKey`。
107
+ * 两份手写算法正是这条键在 0.1.1 被 codex 对抗复审抓到过的形(无 seq 追不追段的差异会让
108
+ * 「重放去重」整条失效而两边代码看着都对);pure 门 B7 段对两个入口做**同键对拍**。
109
+ *
110
+ * 键形:`[external:]taskId:status[:seq]`。缺 `task_id` ⇒ 空 taskId 段(与 cli 逐字一致 ——
111
+ * 空 id 的通知本就在 `normalizeTaskNotification` 那一层被丢弃,这里不额外加语义)。
112
+ *
113
+ * ⚠️ **P1-3(完成事件单一权威)未到货 ⇒ 只搬不删**:引擎给出 `completionId` 之后,这把
114
+ * 「客户端按 taskId:status[:seq] 自己重算」的键才能换成「读引擎发的 id」。退役条件见
115
+ * `compensations.ts` 的 T2 行。
116
+ */
117
+ export function taskNotificationDedupKeyFromWire(n) {
118
+ return taskNotificationDedupKey({
119
+ taskId: typeof n.task_id === 'string' ? n.task_id : '',
120
+ status: typeof n.status === 'string' ? n.status : 'completed',
121
+ ...(typeof n.seq === 'number' || typeof n.seq === 'string' ? { seq: n.seq } : {}),
122
+ ...(typeof n.task_type === 'string' ? { taskType: n.task_type } : {}),
123
+ });
124
+ }
125
+ export function parseTranscriptNotificationSeeds(messages) {
126
+ const unescape = (s) => s
127
+ .replace(/&lt;/g, '<')
128
+ .replace(/&gt;/g, '>')
129
+ .replace(/&quot;/g, '"')
130
+ .replace(/&#39;/g, "'")
131
+ .replace(/&amp;/g, '&');
132
+ const renderedKeys = [];
133
+ const notifiedKeys = [];
134
+ let seeded = 0;
135
+ for (const m of messages) {
136
+ const rec = m;
137
+ if (rec?.type !== 'user')
138
+ continue;
139
+ const content = rec.message?.content;
140
+ const texts = [];
141
+ if (typeof content === 'string')
142
+ texts.push(content);
143
+ else if (Array.isArray(content)) {
144
+ for (const b of content) {
145
+ const bb = b;
146
+ if (bb?.type === 'text' && typeof bb.text === 'string')
147
+ texts.push(bb.text);
148
+ }
149
+ }
150
+ for (const text of texts) {
151
+ if (!text.includes(`<${TASK_NOTIFICATION_TAG}`))
152
+ continue;
153
+ const blockRe = new RegExp(`<${TASK_NOTIFICATION_TAG}(\\s[^>]*)?>([\\s\\S]*?)</${TASK_NOTIFICATION_TAG}>`, 'g');
154
+ for (const bm of text.matchAll(blockRe)) {
155
+ const attrs = bm[1] ?? '';
156
+ const body = bm[2] ?? '';
157
+ const idM = body.match(new RegExp(`<${TASK_ID_TAG}>([\\s\\S]*?)</${TASK_ID_TAG}>`));
158
+ const stM = body.match(new RegExp(`<${STATUS_TAG}>([\\s\\S]*?)</${STATUS_TAG}>`));
159
+ if (!idM || !stM)
160
+ continue;
161
+ const taskId = unescape(idM[1] ?? '');
162
+ const status = unescape(stM[1] ?? '');
163
+ if (taskId.length === 0 || status.length === 0)
164
+ continue;
165
+ const external = /\btype="external"/.test(attrs);
166
+ renderedKeys.push(`${external ? 'external:' : ''}${taskId}:${status}`);
167
+ if (!external && (status === 'completed' || status === 'failed' || status === 'killed')) {
168
+ notifiedKeys.push(taskId);
169
+ }
170
+ seeded++;
171
+ }
172
+ }
173
+ }
174
+ return { renderedKeys, notifiedKeys, seeded };
175
+ }
102
176
  // ══════════════════════════════════════════════════════════════════════════════════════════════
103
177
  // ⇄ B2 批搬迁(2026-07-27,多端改造设计稿 §3 B2「通知族合并」):cli 三个文件并入本模块
104
178
  // ① src/sema/engineTaskNotification.ts(347 行,台账 + watcher + 入队合成)
@@ -180,6 +254,26 @@ const notifiedRunIds = new Set();
180
254
  * (enqueue 两族与 bridge 渲染同用 escapeXml,键形字节对齐)。仅主线程 task-notification
181
255
  * 条目(agentId undefined);用户输入/其他模式绝不触碰。
182
256
  */
257
+ /**
258
+ * B7:plan_review 结局回植(`hitl/planReviewWire.ts` 的唯一调用点)。走队列口的
259
+ * `enqueueMetaPrompt` 可选动词 —— 口或动词缺席 ⇒ 计 miss(与 `port()` 同一个计数器,宿主
260
+ * 只看 `hostPortMisses()` 一个数)并**返回 false**,调用方 fail-soft。
261
+ *
262
+ * 🔴 为什么不复用 `enqueuePendingNotification`:那条走的是 `<task-notification>` 通道(队列
263
+ * 的 `task-notification` 模式,drain 时按通知语义处理);plan_review 结局是一个 **isMeta 的
264
+ * prompt turn**。混用 = 结局被当成后台任务完成通知渲染,是「看起来送到了」的假绑定。
265
+ */
266
+ export function enqueuePlanReviewOutcome(value) {
267
+ const p = port();
268
+ if (!p)
269
+ return false;
270
+ if (typeof p.enqueueMetaPrompt !== 'function') {
271
+ queuePortMisses++;
272
+ return false;
273
+ }
274
+ p.enqueueMetaPrompt({ value, mode: 'prompt', priority: 'later', isMeta: true, workload: 'plan-review' });
275
+ return true;
276
+ }
183
277
  export function dropQueuedNotificationsForRun(taskId) {
184
278
  const needle = `<${TASK_ID_TAG}>${escapeXml(taskId)}</${TASK_ID_TAG}>`;
185
279
  const p = port();
@@ -1,4 +1,3 @@
1
- export { engineTaskHandlesCapable } from './engineRowStopGate.js';
2
1
  export interface EngineTaskOutputResult {
3
2
  /** 累积后的输出正文(游标两形兼容,见头注)。 */
4
3
  content: string;
@@ -44,9 +44,10 @@ import { engineWireDebugEnabled, engineWireTarget } from '../engineWireTarget.js
44
44
  import { activeEngineRunId, engineSessionParamSpread } from '../engineSessionParam.js';
45
45
  import { getBgParentRun } from '../subagentContentStore.js';
46
46
  import { engineTaskHandlesCapable } from './engineRowStopGate.js';
47
- // 能力门同步读口:#98 起真源在零依赖叶 `engineRowStopGate`(批量停要同步分类,不能拉本模块的
48
- // SDK 图),这里原样 re-export,消费方不动。
49
- export { engineTaskHandlesCapable } from './engineRowStopGate.js';
47
+ // 能力门同步读口:真源在零依赖叶 `engineRowStopGate`(batch-stop 同步分类不拉本模块 SDK 图)。
48
+ // ⚠️ 此处**不再 re-export**——它与原定义同进 index.ts 的 `export *` barrel 会构成双出口,
49
+ // esbuild 对 star-export 歧义直接 build 失败(tsc 同源 symbol 不报=假绿;0.8.0 壳收批实撞,
50
+ // idle-terminal-settle 套逮住)。包内消费者直接 import 原叶。
50
51
  /** 宿主 run 解析:fleet 行/通知帧喂的 parent 映射优先(行 id=`${runId} ${handle}` 的 parentId
51
52
  * 投影),缺席退当前交互 run(register 时刻的活跃 run 即宿主——engineSide bash 行的出生形)。 */
52
53
  function resolveHostRun(handle) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/client-core",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Client-side session runtime shared by every sema human client (TUI / web / desktop): sema wire frames (AgentEvent) -> CC session vocabulary (SDKMessage) with dual-plane output (transcript/chrome), deterministic transcript ids, lane discipline as a type, and the notification/dedup ledgers. Every CC-skin shape is collected here so the wire itself stays neutral. Blackboard [1832] design axioms; [1651]/[1652]/[1653] signed seam design. Renamed from @sema-agent/wire-cc-adapter (0.1.x).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "devDependencies": {
27
27
  "@sema-agent/agent-types": "^0.2.0",
28
- "@sema-agent/sdk": "^0.0.118",
28
+ "@sema-agent/sdk": "^0.0.125",
29
29
  "esbuild": "^0.27.4",
30
30
  "typescript": "^6.0.2"
31
31
  }