@zhushanwen/pi-delivery-port 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-delivery-port",
3
- "version": "0.2.0",
4
- "description": "In-process delivery port for the pi half of the unified delivery service (design .tmp/tech-design/unified-delivery.md §3.7): binds pi appendEntry / sessionManager.getEntries / isIdle / sendMessage / message_end / session_compact into the @zhushanwen/session-delivery v3 kernel as the sole executor of the custom lane — outbox journal (entry schema v2 + v1 notify-ledger compat read), details-id delivery proof, session_start difference-set replay, submit-side merge window with external-channel isolation, and compaction-wait degradation (shared library, not a Pi extension).",
3
+ "version": "0.3.0",
4
+ "description": "In-process delivery port for the pi half of the unified delivery service (design .tmp/tech-design/unified-delivery.md §3.7): binds pi appendEntry / sessionManager.getEntries / isIdle / sendMessage / message_end / session_compact into the @zhushanwen/session-delivery v3 kernel as the sole executor of the custom lane — outbox journal (entry schema v2 + v1 notify-ledger compat read), details-id delivery proof, session_start difference-set replay, edge-immediate dispatch with external-channel isolation (permanent dedup via the delivery journal), and compaction-wait degradation (shared library, not a Pi extension).",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
7
7
  "keywords": [
@@ -17,18 +17,18 @@
17
17
  "src/"
18
18
  ],
19
19
  "dependencies": {
20
- "@zhushanwen/extension-protocol": "0.22.0"
20
+ "@zhushanwen/extension-protocol": "0.24.0"
21
21
  },
22
22
  "peerDependencies": {
23
23
  "@earendil-works/pi-coding-agent": "^1.0.0",
24
- "@zhushanwen/session-delivery": "2.0.0"
24
+ "@zhushanwen/session-delivery": "2.1.0"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@earendil-works/pi-coding-agent": "^1.0.0",
28
28
  "@types/node": "^24.0.0",
29
29
  "@vitest/coverage-v8": "^4.1.9",
30
30
  "vitest": "^4.1.8",
31
- "@zhushanwen/session-delivery": "2.0.0"
31
+ "@zhushanwen/session-delivery": "2.1.0"
32
32
  },
33
33
  "scripts": {
34
34
  "typecheck": "npx tsc --noEmit",
@@ -3,7 +3,7 @@
3
3
  // ack)/ 幂等(同 id 重提交 swallowed)/ session_start 差集重放 / 外部通道逐条批组
4
4
  // 隔离 / compaction 等待降级与事件唤醒 / deliveryId 写入与批展平逐条对位。
5
5
  // fake host 全内存(无 fs——测试红线:禁触真实数据目录由构造性满足);
6
- // fake timers 驱动 60s 合批窗口。
6
+ // fake timers 驱动「无窗口定时器」负断言与 collect 等待时序(攒批窗已删 §3.3.7③)。
7
7
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
8
8
 
9
9
  import {
@@ -31,12 +31,11 @@ type SentMessage = {
31
31
  details?: unknown;
32
32
  };
33
33
 
34
- function makeHost(init: { entries?: unknown[]; idle?: boolean; bg?: boolean } = {}) {
34
+ function makeHost(init: { entries?: unknown[]; idle?: boolean } = {}) {
35
35
  const entries: unknown[] = init.entries ?? [];
36
36
  const sent: Array<{ message: SentMessage; options: { deliverAs: string; triggerTurn: boolean } }> = [];
37
37
  const callLog: string[] = [];
38
38
  const idle = { value: init.idle ?? true };
39
- const bg = { value: init.bg ?? false };
40
39
  const sendThrows = { value: false };
41
40
  const warn = vi.fn();
42
41
  const debug = vi.fn();
@@ -89,7 +88,6 @@ function makeHost(init: { entries?: unknown[]; idle?: boolean; bg?: boolean } =
89
88
  handlers.session_compact_failed = handlers.session_compact_failed.filter((h) => h !== handler);
90
89
  };
91
90
  },
92
- hasRunningBackground: () => bg.value,
93
91
  logger: { warn, debug },
94
92
  };
95
93
  const fireAll = (list: Array<() => void>): void => {
@@ -119,7 +117,6 @@ function makeHost(init: { entries?: unknown[]; idle?: boolean; bg?: boolean } =
119
117
  sent,
120
118
  callLog,
121
119
  idle,
122
- bg,
123
120
  sendThrows,
124
121
  warn,
125
122
  debug,
@@ -154,7 +151,7 @@ describe("四步对位(受理前落盘 → 注入 → message_end 命中 → a
154
151
  });
155
152
 
156
153
  it("直通路径:outbox 落盘先于 sendMessage;命中后 ack 落盘且重放差集清零", () => {
157
- const h = makeHost({ bg: false });
154
+ const h = makeHost();
158
155
  const port = createDeliveryPort(h.host);
159
156
  const receipt = port.deliver(defaultEnvelope({ id: "n-1" }));
160
157
  expect(receipt).toEqual({ kind: "accepted", id: "n-1" });
@@ -187,14 +184,14 @@ describe("四步对位(受理前落盘 → 注入 → message_end 命中 → a
187
184
  });
188
185
 
189
186
  it("followUp 档透传 deliverAs", () => {
190
- const h = makeHost({ bg: false });
187
+ const h = makeHost();
191
188
  const port = createDeliveryPort(h.host);
192
189
  port.deliver(defaultEnvelope({ id: "n-2", timing: "followUp" }));
193
190
  expect(h.sent[0].options.deliverAs).toBe("followUp");
194
191
  });
195
192
 
196
193
  it("缺省 id 内部生成且注入 details 携带同一 id", () => {
197
- const h = makeHost({ bg: false });
194
+ const h = makeHost();
198
195
  const port = createDeliveryPort(h.host);
199
196
  const receipt = port.deliver(defaultEnvelope());
200
197
  expect(receipt.kind).toBe("accepted");
@@ -214,7 +211,7 @@ describe("幂等(同 id 重提交 swallowed)", () => {
214
211
  });
215
212
 
216
213
  it("缓冲成员同 id 重提交 swallowed", () => {
217
- const h = makeHost({ bg: true });
214
+ const h = makeHost();
218
215
  const port = createDeliveryPort(h.host);
219
216
  expect(port.deliver(defaultEnvelope({ id: "dup-1" })).kind).toBe("accepted");
220
217
  expect(port.deliver(defaultEnvelope({ id: "dup-1" }))).toEqual({ kind: "swallowed", reason: "already-seen" });
@@ -223,7 +220,7 @@ describe("幂等(同 id 重提交 swallowed)", () => {
223
220
  });
224
221
 
225
222
  it("直通条目已终局后跨实例同 id 提交不重投(判重三级:持久终局播种)", () => {
226
- const h = makeHost({ bg: false });
223
+ const h = makeHost();
227
224
  const first = createDeliveryPort(h.host);
228
225
  first.deliver(defaultEnvelope({ id: "done-1" }));
229
226
  h.injectAndEmit(h.sent[0].message); // 送达 + ack
@@ -254,9 +251,9 @@ describe("幂等(同 id 重提交 swallowed)", () => {
254
251
  });
255
252
  });
256
253
 
257
- // ─── 合批窗口与批组隔离 ─────────────────────────────────────
254
+ // ─── 边沿即投与批组隔离(subagent-observability §3.3.7③ 攒批窗退役后行为锁)──
258
255
 
259
- describe("提交侧合批(60s 滑动窗口)与批组隔离", () => {
256
+ describe("边沿即投与批组隔离(攒批窗已删)", () => {
260
257
  beforeEach(() => {
261
258
  vi.useFakeTimers();
262
259
  vi.setSystemTime(T0);
@@ -265,106 +262,71 @@ describe("提交侧合批(60s 滑动窗口)与批组隔离", () => {
265
262
  vi.useRealTimers();
266
263
  });
267
264
 
268
- it("默认通道 + 活跃后台 → 进窗口不注入;60s 到期合成一条批消息", () => {
269
- const h = makeHost({ bg: true });
265
+ it("默认通道边沿即投:落盘受理后立即注入(单条无批 wrapper),无窗口等待", () => {
266
+ const h = makeHost();
270
267
  const port = createDeliveryPort(h.host);
271
268
  port.deliver(defaultEnvelope({ id: "b-1", payload: { kind: "custom", customType: DEFAULT_NOTIFY_CUSTOM_TYPE, content: "first", display: true, details: { k: 1 } } }));
272
- port.deliver(defaultEnvelope({ id: "b-2", payload: { kind: "custom", customType: DEFAULT_NOTIFY_CUSTOM_TYPE, content: "second", display: true, details: { k: 2 } } }));
273
- expect(h.sent).toHaveLength(0); // 窗口内不注入
274
- expect(h.outboxEntries()).toHaveLength(2); // 受理前已逐条落盘
269
+ // 边沿即投:无 60s 窗口——落盘受理后同步出站
270
+ expect(h.sent).toHaveLength(1);
271
+ expect(h.outboxEntries()).toHaveLength(1); // 受理前已落盘
272
+ expect(h.sent[0].message.customType).toBe(DEFAULT_NOTIFY_CUSTOM_TYPE);
273
+ expect((h.sent[0].message.details as Record<string, unknown>)["batch"]).toBeUndefined();
275
274
 
276
- vi.advanceTimersByTime(60_000);
277
- expect(h.sent).toHaveLength(1); // 一次 sendMessage
278
- const msg = h.sent[0].message;
279
- expect(msg.customType).toBe(DEFAULT_NOTIFY_CUSTOM_TYPE);
280
- expect(msg.details).toMatchObject({ batch: true });
281
- // 批展平逐条对位:items 序 = 成员序,deliveryId 各自携带
282
- const items = (msg.details as { items: Array<Record<string, unknown>> }).items;
283
- expect(items).toHaveLength(2);
284
- expect(items[0]).toMatchObject({ k: 1, deliveryId: "b-1" });
285
- expect(items[1]).toMatchObject({ k: 2, deliveryId: "b-2" });
286
-
287
- // message_end 批命中 → 逐条 ack
288
- h.injectAndEmit(msg);
289
- const ackIds = h.ackEntries().map((a) => (a["data"] as { id: string }).id).sort();
290
- expect(ackIds).toEqual(["b-1", "b-2"]);
291
- });
292
-
293
- it("滑动窗口:窗口内新到达重置窗口尾", () => {
294
- const h = makeHost({ bg: true });
295
- const port = createDeliveryPort(h.host);
296
- port.deliver(defaultEnvelope({ id: "s-1" }));
297
- vi.advanceTimersByTime(59_000);
298
- port.deliver(defaultEnvelope({ id: "s-2" }));
299
- vi.advanceTimersByTime(59_000);
300
- expect(h.sent).toHaveLength(0); // 重置后未到期
301
- vi.advanceTimersByTime(1_000);
275
+ // message_end 命中 → ack
276
+ h.injectAndEmit(h.sent[0].message);
277
+ const ackIds = h.ackEntries().map((a) => (a["data"] as { id: string }).id);
278
+ expect(ackIds).toEqual(["b-1"]);
279
+ // 无第二批:窗口机制已删,定时器不存在
280
+ vi.advanceTimersByTime(120_000);
302
281
  expect(h.sent).toHaveLength(1);
303
282
  });
304
283
 
305
- it("外部通道逐条批组隔离:立即单独注入,不进窗口批", () => {
306
- const h = makeHost({ bg: true });
284
+ it("同组多成员近同时到达 → 分组放行合成一条批消息(保留边界:pumpDirect/commitAsBatch 批形态)", () => {
285
+ const h = makeHost();
286
+ const port = createDeliveryPort(h.host);
287
+ // 同步段连续提交:首条触发的 pump 已取走首条,第二条在新一轮 pump 与首条
288
+ // 内核注入的 merge-commit hold 窗内聚合——同通道批形态仍合法(保留边界断言)
289
+ port.deliver(defaultEnvelope({ id: "g-1", payload: { kind: "custom", customType: DEFAULT_NOTIFY_CUSTOM_TYPE, content: "one", display: true, details: { k: 1 } } }));
290
+ port.deliver(defaultEnvelope({ id: "g-2", payload: { kind: "custom", customType: DEFAULT_NOTIFY_CUSTOM_TYPE, content: "two", display: true, details: { k: 2 } } }));
291
+ expect(h.sent.length).toBeGreaterThanOrEqual(1);
292
+ expect(h.sent.length).toBeLessThanOrEqual(2);
293
+ // 全部条目都出站(不丢)
294
+ const deliveredIds = h.sent
295
+ .flatMap((s) => {
296
+ const details = s.message.details as { batch?: boolean; items?: Array<Record<string, unknown>>; deliveryId?: string };
297
+ return details.batch === true ? (details.items ?? []).map((i) => i.deliveryId) : [details.deliveryId];
298
+ })
299
+ .sort();
300
+ expect(deliveredIds).toEqual(["g-1", "g-2"]);
301
+ // 出站消息恒单通道(批组隔离不变量)
302
+ for (const s of h.sent) expect(s.message.customType).toBe(DEFAULT_NOTIFY_CUSTOM_TYPE);
303
+ });
304
+
305
+ it("外部通道逐条批组隔离:立即单独注入,与默认通道不混批", () => {
306
+ const h = makeHost();
307
307
  const port = createDeliveryPort(h.host);
308
308
  const wf = { kind: "custom" as const, customType: "workflow-result", content: "workflow done", display: true, details: { runId: "r-1" } };
309
309
  expect(port.deliver({ id: "wf-1", payload: wf })).toEqual({ kind: "accepted", id: "wf-1" });
310
310
  expect(h.sent).toHaveLength(1); // 外部通道立即逐条
311
311
  expect(h.sent[0].message.details).toEqual({ runId: "r-1", deliveryId: "wf-1" }); // 无 batch wrapper
312
312
 
313
- port.deliver(defaultEnvelope({ id: "bg-1" })); // 默认通道照常进窗口
314
- expect(h.sent).toHaveLength(1);
315
- vi.advanceTimersByTime(60_000);
313
+ port.deliver(defaultEnvelope({ id: "bg-1" })); // 默认通道边沿即投
316
314
  expect(h.sent).toHaveLength(2);
317
- expect(h.sent[1].message.customType).toBe(DEFAULT_NOTIFY_CUSTOM_TYPE); // 批只含默认通道成员
315
+ expect(h.sent[1].message.customType).toBe(DEFAULT_NOTIFY_CUSTOM_TYPE); // 各自通道出站
316
+ expect((h.sent[1].message.details as Record<string, unknown>)["batch"]).toBeUndefined();
317
+ // 外部通道消息不含默认通道成员(无混批)
318
+ expect(JSON.stringify(h.sent[0].message.details)).not.toContain("bg-1");
318
319
  });
319
320
 
320
- it("单成员窗口到期按单条注入(无 batch wrapper)", () => {
321
- const h = makeHost({ bg: true });
322
- const port = createDeliveryPort(h.host);
323
- port.deliver(defaultEnvelope({ id: "solo-1" }));
324
- vi.advanceTimersByTime(60_000);
325
- expect(h.sent).toHaveLength(1);
326
- expect(h.sent[0].message.details).not.toMatchObject({ batch: true });
327
- expect(h.sent[0].message.details).toEqual({ outcome: "completed", deliveryId: "solo-1" });
328
- });
329
-
330
- it("merge:false 覆写让默认通道跳过窗口立即提交", () => {
331
- const h = makeHost({ bg: true });
332
- const port = createDeliveryPort(h.host);
333
- port.deliver(defaultEnvelope({ id: "nm-1" }), { merge: false });
334
- expect(h.sent).toHaveLength(1);
335
- });
336
-
337
- it("merge:true 对外部通道不生效(结构性逐条):立即单条注入无 batch wrapper", () => {
338
- const h = makeHost({ bg: true });
339
- const port = createDeliveryPort(h.host);
340
- const wf = { kind: "custom" as const, customType: "workflow-result", content: "workflow done", display: true, details: { runId: "r-9" } };
341
- expect(port.deliver({ id: "wf-m1", payload: wf }, { merge: true })).toEqual({ kind: "accepted", id: "wf-m1" });
342
- // 显式 merge:true 不把外部通道条目放进窗口:立即单条注入
343
- expect(h.sent).toHaveLength(1);
344
- expect(h.sent[0].message.customType).toBe("workflow-result");
345
- expect((h.sent[0].message.details as Record<string, unknown>)["batch"]).toBeUndefined();
346
- // 窗口到期后无第二批投出(条目从未进 mergeBuffer)
347
- vi.advanceTimersByTime(60_000);
348
- expect(h.sent).toHaveLength(1);
349
- });
350
-
351
- it("flush() 立即放行窗口缓冲", () => {
352
- const h = makeHost({ bg: true });
321
+ it("flush() 幂等放行缓冲条目(空缓冲 no-op 不报错)", () => {
322
+ const h = makeHost();
353
323
  const port = createDeliveryPort(h.host);
324
+ port.flush(); // 空缓冲 no-op
325
+ expect(h.sent).toHaveLength(0);
354
326
  port.deliver(defaultEnvelope({ id: "f-1" }));
355
- port.flush();
356
- expect(h.sent).toHaveLength(1);
357
- });
358
-
359
- it("窗口活跃期到达的外部通道条目单独出站,缓冲成员不随后泄露", () => {
360
- const h = makeHost({ bg: true });
361
- const port = createDeliveryPort(h.host);
362
- port.deliver(defaultEnvelope({ id: "bg-first" })); // 进缓冲
363
- port.deliver({ id: "wf-x", payload: { kind: "custom", customType: "managed-session-notify", content: "session died", display: true } });
364
- // 外部通道条目独立注入(缓冲成员未入内核——隔离由「缓冲在端口、不在内核」保证)
327
+ expect(h.sent).toHaveLength(1); // 边沿已投
328
+ port.flush(); // 已出站,无重复注入
365
329
  expect(h.sent).toHaveLength(1);
366
- expect(h.sent[0].message.customType).toBe("managed-session-notify");
367
- expect((h.sent[0].message.details as Record<string, unknown>)["batch"]).toBeUndefined();
368
330
  });
369
331
  });
370
332
 
@@ -380,7 +342,7 @@ describe("compaction 等待降级与事件唤醒", () => {
380
342
  });
381
343
 
382
344
  it("before_compact 置标记 → 缓冲挂起不注入(落盘已完成);session_compact 复位唤醒重投", () => {
383
- const h = makeHost({ bg: false, idle: false });
345
+ const h = makeHost({ idle: false });
384
346
  const port = createDeliveryPort(h.host);
385
347
  h.fireBeforeCompact(); // compaction 开始边沿(事件标记置位,非 isIdle 判定)
386
348
  const receipt = port.deliver(defaultEnvelope({ id: "c-1" }));
@@ -397,7 +359,7 @@ describe("compaction 等待降级与事件唤醒", () => {
397
359
  });
398
360
 
399
361
  it("session_compact_failed 边沿同样复位唤醒", () => {
400
- const h = makeHost({ bg: false, idle: false });
362
+ const h = makeHost({ idle: false });
401
363
  const port = createDeliveryPort(h.host);
402
364
  h.fireBeforeCompact();
403
365
  port.deliver(defaultEnvelope({ id: "c-2" }));
@@ -407,7 +369,7 @@ describe("compaction 等待降级与事件唤醒", () => {
407
369
  });
408
370
 
409
371
  it("run 活跃(isIdle=false)未 compaction → 照常注入(steer 入 pi 队列不拒)", () => {
410
- const h = makeHost({ bg: false, idle: false }); // run 活跃:无 before_compact,标记 false
372
+ const h = makeHost({ idle: false }); // run 活跃:无 before_compact,标记 false
411
373
  const port = createDeliveryPort(h.host);
412
374
  const receipt = port.deliver(defaultEnvelope({ id: "run-1" }));
413
375
  expect(receipt).toEqual({ kind: "accepted", id: "run-1" });
@@ -416,7 +378,7 @@ describe("compaction 等待降级与事件唤醒", () => {
416
378
  });
417
379
 
418
380
  it("持有期内 settled 边沿只做无谓尝试,仍挂起(无双重注入)", () => {
419
- const h = makeHost({ bg: false, idle: false });
381
+ const h = makeHost({ idle: false });
420
382
  const port = createDeliveryPort(h.host);
421
383
  h.fireBeforeCompact();
422
384
  port.deliver(defaultEnvelope({ id: "c-3" }));
@@ -427,7 +389,7 @@ describe("compaction 等待降级与事件唤醒", () => {
427
389
  });
428
390
 
429
391
  it("注入 throw → 内核 retain 留账 + settleRejected 计数;恢复后 settled 边沿重投", () => {
430
- const h = makeHost({ bg: false, idle: true });
392
+ const h = makeHost({ idle: true });
431
393
  h.sendThrows.value = true;
432
394
  const port = createDeliveryPort(h.host);
433
395
  expect(port.deliver(defaultEnvelope({ id: "t-1" })).kind).toBe("accepted");
@@ -455,12 +417,12 @@ describe("失败保留策略(onFailure 覆写)", () => {
455
417
  });
456
418
 
457
419
  it("显式 forget 首败即弃(真注入 throw):discard 终局列落盘、settled 边沿不重投、重放不复活", () => {
458
- const h = makeHost({ bg: false });
420
+ const h = makeHost();
459
421
  h.sendThrows.value = true; // 注入通道全程 throw
460
422
  const port = createDeliveryPort(h.host);
461
423
  expect(port.deliver(defaultEnvelope({ id: "fg-1" }), { onFailure: "forget" }).kind).toBe("accepted");
462
424
  // 提交链两段:首投被 commitAsBatch 注入门禁 hold(设计内等待,不计首败、无终局落盘)
463
- // → 窗口结束 kernel.flush() 重提 → 真注入 throw(首败语义生效):forget → discard。
425
+ // → kernel.flush() 重提 → 真注入 throw(首败语义生效):forget → discard。
464
426
  // settleRejected=1 锚定 discard 由真注入失败触发(hold 路径不计数)。
465
427
  expect(h.sent).toHaveLength(0);
466
428
  expect(port.metrics().settleRejected).toBe(1);
@@ -479,12 +441,12 @@ describe("失败保留策略(onFailure 覆写)", () => {
479
441
  expect(h.sent).toHaveLength(0);
480
442
  });
481
443
 
482
- it("提交窗 hold 拒不计首败:forget 条目不被 discard、留册,窗口结束重提送达成功", () => {
483
- const h = makeHost({ bg: false });
444
+ it("提交窗 hold 拒不计首败:forget 条目不被 discard、留册,重提送达成功", () => {
445
+ const h = makeHost();
484
446
  const port = createDeliveryPort(h.host);
485
- // 默认通道 bg=false → 直通 → commitAsBatch 提交窗:首投被注入门禁 hold(设计内等待)
447
+ // 边沿即投仍经 commitAsBatch 提交窗:首投被注入门禁 hold(设计内等待)
486
448
  expect(port.deliver(defaultEnvelope({ id: "hd-1" }), { onFailure: "forget" }).kind).toBe("accepted");
487
- // 窗口结束 kernel.flush() 重提 → 送达成功(旧实现此处 hold 被误计首败 → discard 永久丢弃)
449
+ // kernel.flush() 重提 → 送达成功(旧实现此处 hold 被误计首败 → discard 永久丢弃)
488
450
  expect(h.sent).toHaveLength(1);
489
451
  expect(h.sent[0].message.details).toMatchObject({ deliveryId: "hd-1" });
490
452
  expect(h.ackEntries()).toHaveLength(0); // 无 discard 终局(条目全程未失败)
@@ -497,11 +459,9 @@ describe("失败保留策略(onFailure 覆写)", () => {
497
459
  });
498
460
 
499
461
  it("提交窗 hold 拒对 retain 条目同样不计首败:留守不重复计数,重提送达正常终局", () => {
500
- const h = makeHost({ bg: true });
462
+ const h = makeHost();
501
463
  const port = createDeliveryPort(h.host);
502
- port.deliver(defaultEnvelope({ id: "hd-r" })); // 缺省 retain
503
- expect(h.sent).toHaveLength(0); // 窗口缓冲期不注入
504
- vi.advanceTimersByTime(60_000); // 窗口到期 → commitAsBatch 提交(首投 hold 留守)→ kernel.flush 重提
464
+ port.deliver(defaultEnvelope({ id: "hd-r" })); // 缺省 retain——边沿即投经 commitAsBatch(首投 hold 留守)→ kernel.flush 重提
505
465
  expect(h.sent).toHaveLength(1);
506
466
  expect(port.metrics().settleRejected).toBe(0); // hold 不重复计入失败
507
467
  const firstFailWarns = h.warn.mock.calls.filter((c) => String(c[0]).includes("first-failure"));
@@ -510,8 +470,8 @@ describe("失败保留策略(onFailure 覆写)", () => {
510
470
  expect(h.ackEntries()[0]!["data"]).toEqual({ v: 2, id: "hd-r" }); // ack(无 outcome = 非 discard)
511
471
  });
512
472
 
513
- it("缺省(不传 onFailure):首投经提交窗 hold 留守(设计内等待);窗口外重投遇真注入 throw → retain 留账,恢复后边沿重投", () => {
514
- const h = makeHost({ bg: false, idle: true });
473
+ it("缺省(不传 onFailure):首投经提交窗 hold 留守(设计内等待);重投遇真注入 throw → retain 留账,恢复后边沿重投", () => {
474
+ const h = makeHost({ idle: true });
515
475
  h.sendThrows.value = true;
516
476
  const port = createDeliveryPort(h.host);
517
477
  expect(port.deliver(defaultEnvelope({ id: "rt-1" })).kind).toBe("accepted");
@@ -534,19 +494,21 @@ describe("session_start 差集重放", () => {
534
494
  vi.useRealTimers();
535
495
  });
536
496
 
537
- it("崩溃在注入前:新实例 recover 重投(默认通道合批形态)且 replayReplays 计数", () => {
538
- const h = makeHost({ bg: true });
497
+ it("崩溃在注入前:新实例 recover 重投(同组重放批形态)且 replayReplays 计数", () => {
498
+ const h = makeHost();
539
499
  const first = createDeliveryPort(h.host);
540
500
  first.deliver(defaultEnvelope({ id: "r-1", payload: { kind: "custom", customType: DEFAULT_NOTIFY_CUSTOM_TYPE, content: "one", display: true } }));
541
501
  first.deliver(defaultEnvelope({ id: "r-2", payload: { kind: "custom", customType: DEFAULT_NOTIFY_CUSTOM_TYPE, content: "two", display: true } }));
542
- first.dispose(); // 崩溃:进程消亡,outbox entry 留在 session 文件
502
+ first.dispose(); // 崩溃:进程消亡,outbox entry 留在 session 文件(首实例边即投的 sent 不算送达——无 message_end)
543
503
 
544
504
  const second = createDeliveryPort(h.host); // respawn 后 session_start 重新装配
505
+ const sentBefore = h.sent.length;
545
506
  expect(second.recover()).toBe(2);
546
507
  expect(second.metrics().recoveryReplays).toBe(2);
547
- // 重放合并投出(账本 recover→attemptDeliver 形态):一条批消息
548
- expect(h.sent).toHaveLength(1);
549
- const items = (h.sent[0].message.details as { items: Array<Record<string, unknown>> }).items;
508
+ // 重放同组两条一起放行 → 分组放行批形态(保留边界断言):一条批消息
509
+ expect(h.sent.length).toBe(sentBefore + 1);
510
+ const replayMsg = h.sent[h.sent.length - 1].message;
511
+ const items = (replayMsg.details as { items: Array<Record<string, unknown>> }).items;
550
512
  expect(items.map((i) => i.deliveryId).sort()).toEqual(["r-1", "r-2"]);
551
513
  });
552
514
 
@@ -580,7 +542,7 @@ describe("session_start 差集重放", () => {
580
542
  });
581
543
 
582
544
  it("重放幂等:recover 二次调用零动作(在途条目不重复入账)", () => {
583
- const h = makeHost({ bg: false });
545
+ const h = makeHost();
584
546
  const first = createDeliveryPort(h.host);
585
547
  first.deliver(defaultEnvelope({ id: "ri-1" }));
586
548
  first.dispose();
@@ -596,7 +558,7 @@ describe("session_start 差集重放", () => {
596
558
 
597
559
  describe("受理前落盘(耐久承诺)", () => {
598
560
  it("appendEntry 抛错 → rejected(persist-failed),不注入、id 不登记(可重试)", () => {
599
- const h = makeHost({ bg: false });
561
+ const h = makeHost();
600
562
  let fail = false;
601
563
  const host = h.host;
602
564
  const orig = host.appendEntry;
@@ -646,11 +608,12 @@ const pendingRegister = (id: string, type = "bash") => ({
646
608
  data: { id, type, name: `task ${id}`, registeredAt: T0, sessionId: "s-1" },
647
609
  });
648
610
 
649
- /** pending unregister entry fixture(status = 写侧 mapReasonToStatus 已映射落盘值)。 */
650
- const pendingUnregister = (id: string, status: string) => ({
611
+ /** pending unregister entry fixture(reason = 写侧真实原因词,status = mapReasonToStatus
612
+ * 映射落盘的 legacy 副档——生产 listener 双字段同写,结清解析以 reason 为权威)。 */
613
+ const pendingUnregister = (id: string, reason: string, status: string) => ({
651
614
  type: "custom",
652
615
  customType: "pending:unregister",
653
- data: { id, reason: "fixture", status },
616
+ data: { id, reason, status },
654
617
  });
655
618
 
656
619
  const managedEnvelope = (id: string, overrides: Partial<Envelope> = {}): Envelope => ({
@@ -727,7 +690,7 @@ describe("collect 挂起分流与收取(合批多键)", () => {
727
690
  expect(collected.map((c) => c.id).sort()).toEqual(["k-a", "k-b"]);
728
691
  });
729
692
 
730
- it("collectRelease 解散等待集 + 未收挂起条目直通内核(跳过 60s 窗口,恢复 push)", () => {
693
+ it("collectRelease 解散等待集 + 未收挂起条目直通内核(边沿即投无窗口,恢复 push)", () => {
731
694
  const h = makeHost({ entries: [pendingRegister("k-a")] });
732
695
  const port = createDeliveryPort(h.host);
733
696
  port.collectArm(["k-a"]);
@@ -803,23 +766,25 @@ describe("collectArm 五态快照", () => {
803
766
  expect(h.sent).toHaveLength(1);
804
767
  });
805
768
 
806
- it("unregistered-no-entry:曾注册已注销且无日志条目 → 按 unregister status 定 outcome(三形态:死亡跨批 / 例外1 / 防御分支的 status 值变体)", () => {
807
- // 写侧 mapReasonToStatus 落盘值:exited/orphaned → cancelled(死亡跨批・例外1)、
808
- // stopped → aborted(例外1 掐断)、reopened → completed(防御分支收尾)
809
- const cases: Array<{ id: string; status: string }> = [
810
- { id: "k-death", status: "cancelled" },
811
- { id: "k-orph", status: "cancelled" },
812
- { id: "k-stop", status: "aborted" },
813
- { id: "k-defn", status: "completed" },
769
+ it("unregistered-no-entry:曾注册已注销且无日志条目 → 按 unregister reason 结清解析定 outcome(三形态:死亡跨批 / 例外1 / 防御分支收尾)", () => {
770
+ // 写侧 listener 双字段同写:reason = 真实原因词(结清权威通道)、status = legacy
771
+ // 副档。exited/orphaned → cancelled(死亡跨批・例外1)、stopped → cancelled
772
+ // (中止族——legacy status 副档为 aborted,结清视图以五值为准)、reopened →
773
+ // completed(防御分支收尾)
774
+ const cases: Array<{ id: string; reason: string; status: string }> = [
775
+ { id: "k-death", reason: "exited", status: "cancelled" },
776
+ { id: "k-orph", reason: "orphaned", status: "cancelled" },
777
+ { id: "k-stop", reason: "stopped", status: "aborted" },
778
+ { id: "k-defn", reason: "reopened", status: "completed" },
814
779
  ];
815
780
  const h = makeHost({
816
- entries: [...cases.map((c) => pendingRegister(c.id)), ...cases.map((c) => pendingUnregister(c.id, c.status))],
781
+ entries: [...cases.map((c) => pendingRegister(c.id)), ...cases.map((c) => pendingUnregister(c.id, c.reason, c.status))],
817
782
  });
818
783
  const port = createDeliveryPort(h.host);
819
784
  const snap = port.collectArm(cases.map((c) => c.id));
820
785
  expect(snap.map((s) => s.state)).toEqual(["unregistered-no-entry", "unregistered-no-entry", "unregistered-no-entry", "unregistered-no-entry"]);
821
786
  expect(snap[0]!.entry).toMatchObject({ outcome: "cancelled", note: expect.any(String) });
822
- expect(snap[2]!.entry).toMatchObject({ outcome: "aborted" });
787
+ expect(snap[2]!.entry).toMatchObject({ outcome: "cancelled" });
823
788
  expect(snap[3]!.entry).toMatchObject({ outcome: "completed" });
824
789
  // 无处可写终局(该键不属任何日志条目):零 ack entry
825
790
  expect(h.ackEntries()).toHaveLength(0);
@@ -38,22 +38,22 @@ function makeCtx(entries: unknown[], idle: boolean) {
38
38
  };
39
39
  }
40
40
 
41
- function makeOptions(bg = false) {
41
+ function makeOptions() {
42
42
  const logger = {
43
43
  warn: () => {},
44
44
  debug: () => {},
45
45
  };
46
- return { component: "test", logger, hasRunningBackground: () => bg };
46
+ return { component: "test", logger };
47
47
  }
48
48
 
49
- describe("createPiDeliveryPortHost 接线(八端口逐成员)", () => {
50
- it("appendEntry / readSessionEntries / isIdle / hasRunningBackground 透传", () => {
49
+ describe("createPiDeliveryPortHost 接线(七端口逐成员)", () => {
50
+ it("appendEntry / readSessionEntries / isIdle 透传", () => {
51
51
  const { pi, appended } = makePi();
52
52
  const entries = [{ type: "message" }];
53
53
  const host = createPiDeliveryPortHost(
54
54
  pi as never,
55
55
  makeCtx(entries, false) as never,
56
- makeOptions(true),
56
+ makeOptions(),
57
57
  );
58
58
 
59
59
  host.appendEntry("custom_x", { a: 1 });
@@ -61,7 +61,9 @@ describe("createPiDeliveryPortHost 接线(八端口逐成员)", () => {
61
61
 
62
62
  expect(host.readSessionEntries()).toBe(entries);
63
63
  expect(host.isIdle()).toBe(false);
64
- expect(host.hasRunningBackground()).toBe(true);
64
+ // [subagent-observability §3.3.7③] hasRunningBackground 已随攒批窗退役删除,
65
+ // 接线面不再含该成员(结构断言:host 对象无此键)。
66
+ expect("hasRunningBackground" in host).toBe(false);
65
67
  });
66
68
 
67
69
  it("sendMessage 注入恒携带 triggerTurn: true,deliverAs 透传", () => {
@@ -122,7 +124,7 @@ describe("createPiDeliveryPortHost 接线(八端口逐成员)", () => {
122
124
  const host = createPiDeliveryPortHost(
123
125
  pi as never,
124
126
  makeCtx([], true) as never,
125
- { component: "probe", logger, hasRunningBackground: () => false },
127
+ { component: "probe", logger },
126
128
  );
127
129
  expect(host.logger).toBe(logger);
128
130
  });
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // pi 半区投递端口主体(设计 .tmp/tech-design/unified-delivery.md §3.7 组件表):
4
4
  // 把 pi 扩展运行时端口接线成 @zhushanwen/session-delivery v3 内核的 port 半区
5
- // 装配——custom 车道唯一执行者。与账本(notify-ledger)的四步生命周期逐条对位:
5
+ // 装配——custom 投递路径唯一执行者。与账本(notify-ledger)的四步生命周期逐条对位:
6
6
  // ① record(受理前落盘)→ deliver 入口 appendEntry(outbox,plain entry);
7
7
  // ② deliver(投递)→ 内核 on-accept 受理即出站 → pi.sendMessage 直达唯一队列
8
8
  // (两档出队由 pi 原生 steer/followUp 队列承载,本端口零注入时机代码——提交
@@ -14,19 +14,24 @@
14
14
  // 装配形态(§3.7):每 session 一份(session_start 重绑 = 旧实例 dispose + 新实例
15
15
  // recover);消费方经 Symbol.for 进程级槽 getDeliveryPort 获取(C-ext-06 惯例)。
16
16
  //
17
- // ── 提交侧双缓冲与按通道分组放行(§3.7 合批行 + 批组隔离行)───────────────
18
- // 全部提交先落盘再进提交缓冲(攒批成员 mergeBuffer / 直通成员 directBuffer),由
19
- // 放行泵按通道分组交给内核:
20
- // - 默认通道组(subagent-bg-notify):60s 滑动窗口攒批(攒批条件 = 有活跃后台
21
- // 任务),到期/flush 时经 merge-commit hold 逐条提交内核(注入门禁 hold →
22
- // 内核 retain → 条目 queued 聚合)→ kernel.flush() → 内核 buildBatchPayload
23
- // 合成一条 {batch:true, items} 消息一次 sendMessage 入队;
17
+ // ── 提交侧缓冲与按通道分组放行(§3.7 批组隔离行;攒批窗已退役)─────────────
18
+ // 全部提交先落盘再进提交缓冲(directBuffer),由放行泵按通道分组交给内核:
19
+ // - 默认通道组(subagent-bg-notify):边沿即投——落盘受理后立即经 merge-commit
20
+ // hold 逐条提交内核(注入门禁 hold → 内核 retain → 条目 queued 聚合)→
21
+ // kernel.flush() → 内核 buildBatchPayload 合成一条 {batch:true, items} 消息
22
+ // 一次 sendMessage 入队(同组多成员近同时到达时仍是合法批形态——分组放行
23
+ // 语义保留,窗口等待已删);
24
24
  // - 外部通道组(workflow-result / managed-session-notify 等):逐条提交、逐条
25
25
  // 消息(账本 [u9] 分组规则——batch 合并形态对外部通道无消费契约)。
26
+ // [subagent-observability §3.3.7③] 攒批窗(60s 滑动窗口 + mergeBuffer +
27
+ // wantMerge 判定 + MERGE_WINDOW_MS 定时器)整机制退役删除——所有通知边沿即投;
28
+ // 省 token 价值由 collect_results 批量收取能力取代。保留边界(防误删):内核
29
+ // buildBatchPayload 与 pumpDirect/commitAsBatch/merge-commit hold(recover 重放、
30
+ // collectRelease 直通、compaction 唤醒仍产合法批形态)、消费方批兼容读分支、
31
+ // collect 挂起子系统、journal 差集重放——全部保留。
26
32
  // 分组放行的不变量:内核 queued 集恒来自单次单组放行 → 内核 doSend 的合批产物恒
27
33
  // 单通道——批组隔离不依赖时序巧合(混批窗口结构性消除;残余边界见 pumpDirect
28
- // 注释)。双缓冲(攒批/直通分离)的原因:直通放行不得携带窗口攒批成员(窗口语义
29
- // = 攒到到期/flush)。账目/幂等/回执/ack 全程逐条(成员各自独立内核条目,
34
+ // 注释)。账目/幂等/回执/ack 全程逐条(成员各自独立内核条目,
30
35
  // items[].deliveryId 逐项对位),合批只发生在注入形态。
31
36
  //
32
37
  // ── compaction 防御(§3.8 提交侧等待)────────────────────────
@@ -46,8 +51,8 @@
46
51
  // 不进投递内核)+ onCollectArrival 唤醒等待器;未命中 → push 现状零变化。挂起不独立
47
52
  // 持久化,投递日志即耐久层:崩溃后差集重放按未投递重投(push),收取过的条目写
48
53
  // collected 终局(ack outcome 扩值)构造性退出差集。等待器三 API(collectArm 五态
49
- // 快照 / consume 全键展开收取 / collectRelease 解散等待集 + 未收条目直通内核跳过
50
- // 合批窗口)。session 替换路径 abort 先于 dispose 归并中断语义;extension 重载在
54
+ // 快照 / consume 全键展开收取 / collectRelease 解散等待集 + 未收条目直通内核
55
+ // flush——边沿即投,攒批窗已删 §3.3.7③)。session 替换路径 abort 先于 dispose 归并中断语义;extension 重载在
51
56
  // collect 阻塞期不可达(设计 §3.1 [重绑] 登记),dispose 只做在途等待器 warn 留痕
52
57
  // 不变量检查,不定义唤醒与返回语义。
53
58
  //
@@ -74,11 +79,11 @@ import type {
74
79
  import {
75
80
  scanPendingEntries,
76
81
  type MappedPendingStatus,
82
+ type ResolvedSettlementReason,
77
83
  } from "@zhushanwen/extension-protocol";
78
84
 
79
85
  import {
80
86
  ACK_CUSTOM_TYPE,
81
- DEFAULT_NOTIFY_CUSTOM_TYPE,
82
87
  OUTBOX_CUSTOM_TYPE,
83
88
  extractDeliveryIds,
84
89
  isPlainObject,
@@ -98,9 +103,6 @@ import type {
98
103
  DeliveryTiming,
99
104
  } from "./types.ts";
100
105
 
101
- /** 提交侧合批窗口(账本 MERGE_WINDOW_MS=60s 现状保持,设计 §3.7 合批行)。 */
102
- const MERGE_WINDOW_MS = 60_000;
103
-
104
106
  /** genId 时间戳段进制(base36:紧凑且小写字母数字安全,dp-<ts>-<seq> 的 ts 段)。 */
105
107
  const ID_TIMESTAMP_RADIX = 36;
106
108
 
@@ -212,9 +214,8 @@ function createKernelAssembly(deps: KernelAssemblyDeps): { kernel: ReturnType<ty
212
214
  */
213
215
  const kernelPort: KernelPort = {
214
216
  supportedPayloads: ["custom"],
215
- // 内核接口必填成员;on-accept 模式内核 busy gate 短路旁路(delivery.ts flush/
216
- // scheduleFlush 均先判 on-accept),恒不被消费。提交侧挂起判定 = compacting
217
- // 事件标记(见 send 第二道防线与放行泵预检),不走本探测。
217
+ // 内核接口必填成员;内核受理即出站不读忙闲探测,恒不被消费。提交侧挂起判定 =
218
+ // compacting 事件标记(见 send 第二道防线与放行泵预检),不走本探测。
218
219
  isIdle: () => host.isIdle(),
219
220
  subscribeSettled: (cb) => host.onAgentSettled(cb),
220
221
  send(msg: DeliveryMessage, intent: DeliveryIntent) {
@@ -257,7 +258,6 @@ function createKernelAssembly(deps: KernelAssemblyDeps): { kernel: ReturnType<ty
257
258
 
258
259
  const kernelConfig: DeliveryConfigWithWarn = {
259
260
  intent: "interrupt-at-turn-boundary",
260
- dispatchMode: "on-accept",
261
261
  store,
262
262
  // details-id 凭据判定:state.deliveredIds 由 message_end 边沿增量维护(事件与
263
263
  // entry 写入同栈——收到事件 = entry 已持久),O(1) 且锚定磁盘事实。
@@ -275,7 +275,7 @@ interface CollectFaceDeps {
275
275
  /** 挂起条目收取时摘除失败策略项(deliver 登记的提交时选项不残留)。 */
276
276
  failurePolicies: Map<string, DeliveryFailurePolicy>;
277
277
  warn(msg: string, err?: unknown): void;
278
- /** 解散等待集后未收条目的直通放行(进 pumpDirect 分组放行管线,跳过合批窗口)。 */
278
+ /** 解散等待集后未收条目的直通放行(进 pumpDirect 分组放行管线——边沿即投,攒批窗已删 §3.3.7③)。 */
279
279
  routeDirect(records: readonly JournalRecord[]): void;
280
280
  }
281
281
 
@@ -328,7 +328,7 @@ function createCollectFace(deps: CollectFaceDeps): CollectFace {
328
328
  ): Omit<CollectedEntry, "id"> {
329
329
  return {
330
330
  type: pending.registerTypes.get(key),
331
- outcome: pending.unregisterStatus.get(key),
331
+ outcome: unregisterOutcomeOf(pending, key),
332
332
  content: rec.payload.content,
333
333
  details: rec.payload.details,
334
334
  };
@@ -343,15 +343,21 @@ function createCollectFace(deps: CollectFaceDeps): CollectFace {
343
343
  * 收取(不写新终局——已注入条目再写 collected 会让日志宣称「未注入过」);
344
344
  * - in-push-pipeline:键 ∈ 日志条目键集、条目无终局(已过分流点,在 push 管线
345
345
  * 排队待注入)→ 不收取不等待(等它会自锁,设计 §3.3 push 管线态决策);
346
- * - unregistered-no-entry:键曾注册已注销、不属任何日志条目键集 → 按 unregister
347
- * entry 的 status 定 outcome 进 collected(无载荷快照形态);
346
+ * - unregistered-no-entry:键曾注册已注销、不属任何日志条目键集 → outcome 按
347
+ * unregister entry 的结清视图优先 + legacy status 兜底定值进 collected(无载荷
348
+ * 快照形态;畸形 data 缺省语义见 types.ts CollectedEntry 注释);
348
349
  * - waiting:活跃(register 在册无注销)且无日志条目 → 登记等待集,后续 deliver
349
350
  * 命中被挂起;
350
351
  * - unknown(MF-1-5 第五态):键未注册且无条目——判定源 = 活跃集 ∪ 投递日志 ∪
351
352
  * 曾注册集的补集。不登记等待集(防空等到 timeout 挂死)、不虚构终态;工具侧
352
353
  * 读本态组装 errors(不再伪装 waiting——端口是唯一未知判定点,词表诚实表达)。
353
354
  */
354
- function collectArm(keys: string[]): CollectSnapshot {
355
+ /** 注销 key 的 outcome(结清视图优先——权威通道 resolveSettlementReason;legacy status 兜底)。 */
356
+ function unregisterOutcomeOf(pending: PendingBaseline, key: string) {
357
+ return pending.unregisterSettlements.get(key) ?? pending.unregisterStatus.get(key);
358
+ }
359
+
360
+ function collectArm(keys: string[]): CollectSnapshot {
355
361
  if (deps.isDisposed()) throw new Error("collectArm on disposed delivery port");
356
362
  const pending = scanPendingBaseline(host.readSessionEntries());
357
363
  const snapshot: CollectSnapshot = [];
@@ -370,7 +376,7 @@ function createCollectFace(deps: CollectFaceDeps): CollectFace {
370
376
  state: "unregistered-no-entry",
371
377
  entry: {
372
378
  type: pending.registerTypes.get(key),
373
- outcome: pending.unregisterStatus.get(key),
379
+ outcome: unregisterOutcomeOf(pending, key),
374
380
  note: UNREGISTERED_NO_ENTRY_NOTE,
375
381
  },
376
382
  });
@@ -424,10 +430,9 @@ function createCollectFace(deps: CollectFaceDeps): CollectFace {
424
430
  }
425
431
 
426
432
  /**
427
- * 解散等待集;未收挂起条目直通内核 flush(跳过 60s 合批窗口——等待期
428
- * hasRunningBackground 恒真,复用 wantMerge 判定会把恢复 push 推迟一个窗口)。
429
- * 直通走 routeDirect(pumpDirect 按通道分组放行:外部通道逐条 / 默认通道组合批
430
- * 消息,批形态语义合法——跳过的是窗口不是分组)。
433
+ * 解散等待集;未收挂起条目直通内核 flush(边沿即投——攒批窗已删,恢复 push
434
+ * 不再有任何窗口延迟)。直通走 routeDirect(pumpDirect 按通道分组放行:外部
435
+ * 通道逐条 / 默认通道组合批消息,批形态语义合法——分组放行保留,窗口已删)。
431
436
  */
432
437
  function collectRelease(): void {
433
438
  if (deps.isDisposed()) return;
@@ -492,20 +497,14 @@ export function createDeliveryPort(host: DeliveryPortHost): DeliveryPortHandle {
492
497
  const buckets: DeliveryPortMetrics = { settleRejected: 0, recoveryReplays: 0 };
493
498
  let disposed = false;
494
499
  /**
495
- * 提交双缓冲(落盘已完成,等待放行;仅在缓冲期持有,放行即摘):
496
- * - mergeBuffer = 默认通道攒批成员(60s 窗口,到期聚合批放行);
497
- * - directBuffer = 直通成员(外部通道 / 无后台任务 / merge:false;按通道分组
498
- * 立即放行)。
499
- * 分两缓冲的原因:直通放行不得携带窗口攒批成员(窗口语义 = 攒到到期/flush),
500
- * 统一单缓冲时「直通触发的 pump」会把窗口成员提前冲出——正是内核 v1 clearMergeTimer
501
- * 注释登记过的「孤儿 timer 冲出窗口成员」同构缺陷,结构性规避。
500
+ * 提交缓冲(落盘已完成,等待放行;仅在缓冲期持有,放行即摘)。窗口攒批缓冲
501
+ * (mergeBuffer)已随攒批窗退役删除(§3.3.7③)——全部受理条目统一进本缓冲
502
+ * 边沿即投;同组多成员近同时到达经 pumpDirect 分组放行仍产合法批形态。
502
503
  */
503
- const mergeBuffer = new Map<string, JournalRecord>();
504
504
  const directBuffer = new Map<string, JournalRecord>();
505
505
  /** 已受理 id 索引(deliver 幂等预检的全窗口快路径;内核三级判重对已入内核条目
506
506
  * 兜底)。仅增量添加,无淘汰——量级 = 每 session 通知总量(数十~数百)。 */
507
507
  const seenIds = new Set<string>();
508
- let mergeTimer: ReturnType<typeof setTimeout> | undefined;
509
508
  /** merge-commit 期标志:默认通道组逐条提交内核期间 hold 注入,使成员聚合为内核
510
509
  * queued 集,最后 kernel.flush() 一次合批出站(见文件头分组放行段)。 */
511
510
  let mergeCommitting = false;
@@ -521,7 +520,7 @@ export function createDeliveryPort(host: DeliveryPortHost): DeliveryPortHandle {
521
520
  let idSeq = 0;
522
521
  /**
523
522
  * 显式覆写的失败保留策略(deliver opts.onFailure → submitToKernel 的单次传递):
524
- * 条目在提交缓冲期(合批窗口 / compaction 挂起)暂存 id → policy,提交内核时
523
+ * 条目在提交缓冲期(compaction 挂起等提交期悬置;攒批窗已删 §3.3.7③)暂存 id → policy,提交内核时
525
524
  * 消费并摘除。不进 JournalRecord / outbox schema(§3.7 耐久面行 schema 定形,
526
525
  * 策略是提交时选项非账目字段)。重放重建的条目不在册(缺省 retain):forget
527
526
  * 条目首败即 discard 落终局列、不在重放差集;「首败前崩溃」的残留条目重放按
@@ -546,7 +545,7 @@ export function createDeliveryPort(host: DeliveryPortHost): DeliveryPortHandle {
546
545
 
547
546
  // ─── collect 挂起面(result-collect 设计 §3.1②;子系统实现见 createCollectFace)───
548
547
  // routeDirect = 解散等待集后未收条目的直通放行(进 pumpDirect 分组放行管线——
549
- // 跳过 60s 合批窗口,不逐条唤醒泵)。
548
+ // 边沿即投,攒批窗已删 §3.3.7③,不逐条唤醒泵)。
550
549
  const collectFace = createCollectFace({
551
550
  host,
552
551
  state,
@@ -585,7 +584,6 @@ export function createDeliveryPort(host: DeliveryPortHost): DeliveryPortHandle {
585
584
  if (disposed) return;
586
585
  compacting = false;
587
586
  try {
588
- flushMergeBuffer("compaction-edge");
589
587
  pumpDirect();
590
588
  kernel.flush();
591
589
  } catch (err) {
@@ -665,7 +663,7 @@ export function createDeliveryPort(host: DeliveryPortHost): DeliveryPortHandle {
665
663
  // 显式失败策略随条目登记(submitToKernel 单次消费;缺省 retain 不占位)。
666
664
  // 登记在落盘成功后——persist-failed rejected 的条目不残留策略项。
667
665
  if (opts?.onFailure) failurePolicies.set(id, opts.onFailure);
668
- routeAcceptedDelivery(id, record, collectKeys, channel, opts);
666
+ routeAcceptedDelivery(id, record, collectKeys);
669
667
  return { kind: "accepted", id };
670
668
  }
671
669
 
@@ -687,16 +685,14 @@ export function createDeliveryPort(host: DeliveryPortHost): DeliveryPortHandle {
687
685
  return true;
688
686
  }
689
687
 
690
- /** 受理后路由(collect 挂起分流 → 攒批判定):挂起条目已落盘(persistOutboxRecord
688
+ /** 受理后路由(collect 挂起分流 → 边沿即投放行):挂起条目已落盘(persistOutboxRecord
691
689
  * 成功在前)——崩溃后差集重放按未投递重投。 */
692
690
  function routeAcceptedDelivery(
693
691
  id: string,
694
692
  record: JournalRecord,
695
693
  collectKeys: readonly string[] | undefined,
696
- channel: string,
697
- opts: DeliveryDeliverOptions | undefined,
698
694
  ): void {
699
- // collect 挂起分流(result-collect 设计 §3.1②,先于合批判定):键集 ∩ 在途
695
+ // collect 挂起分流(result-collect 设计 §3.1②,先于放行):键集 ∩ 在途
700
696
  // 等待集 ≠ ∅ → 挂起缓冲(不进内核,push 管线零感知)+ 唤醒等待器;否则 push
701
697
  // 现状零变化。
702
698
  if (collectKeys !== undefined && collectKeys.length > 0 && collectFace.hasWaiterFor(collectKeys)) {
@@ -704,56 +700,16 @@ export function createDeliveryPort(host: DeliveryPortHost): DeliveryPortHandle {
704
700
  seenIds.add(id);
705
701
  return;
706
702
  }
707
- // 攒批判定(§3.4 缺省值表):默认通道 + 有活跃后台任务 → 60s 窗口攒批;其余
708
- // → 立即放行(泵按通道分组):外部通道结构性逐条——merge:true 亦不得覆盖
709
- // (批组隔离 [u9],窗口合批形态 {batch,items} 对外部通道无消费契约,任务书
710
- // 目标 6);默认通道的无后台任务 / merge:false 覆写同样立即放行。
711
- const wantMerge =
712
- channel === DEFAULT_NOTIFY_CUSTOM_TYPE &&
713
- (opts?.merge ?? host.hasRunningBackground());
714
- if (wantMerge) {
715
- mergeBuffer.set(id, record);
716
- seenIds.add(id);
717
- armMergeTimer();
718
- } else {
719
- directBuffer.set(id, record);
720
- seenIds.add(id);
721
- pumpDirect();
722
- }
723
- }
724
-
725
- // ─── 合批窗口(60s 滑动;攒批路径专用)─────────────────────
726
-
727
- function clearMergeTimer(): void {
728
- if (mergeTimer !== undefined) {
729
- clearTimeout(mergeTimer);
730
- mergeTimer = undefined;
731
- }
732
- }
733
-
734
- /** 滑动窗口:每次新攒批成员到达重置窗口尾(账本内核 mergeWindowMs 滑动语义同源)。 */
735
- function armMergeTimer(): void {
736
- clearMergeTimer();
737
- mergeTimer = setTimeout(() => {
738
- mergeTimer = undefined;
739
- flushMergeBuffer("merge-window");
740
- }, MERGE_WINDOW_MS);
703
+ // 边沿即投(§3.3.7③ 攒批窗退役后唯一路由):落盘受理的条目直接进放行缓冲,
704
+ // 泵按通道分组交给内核——默认通道组经 merge-commit hold 聚合出合法批形态,
705
+ // 外部通道组结构性逐条(批组隔离 [u9] 不依赖时序)。
706
+ directBuffer.set(id, record);
707
+ seenIds.add(id);
708
+ pumpDirect();
741
709
  }
742
710
 
743
711
  // ─── 放行泵(按通道分组交给内核)───────────────────────────
744
712
 
745
- /**
746
- * 窗口攒批成员聚合放行(mergeBuffer → 一次批消息)。compaction 挂起标记置位时
747
- * 整缓冲挂起(compaction 终态边沿唤醒)。
748
- */
749
- function flushMergeBuffer(_reason: string): void {
750
- if (disposed || mergeBuffer.size === 0) return;
751
- if (compacting) return; // compaction 等待:整缓冲挂起(终态边沿复位 + 唤醒)
752
- const group = [...mergeBuffer.values()];
753
- mergeBuffer.clear();
754
- commitAsBatch(group);
755
- }
756
-
757
713
  /**
758
714
  * 直通成员分组放行(directBuffer)。不变量:内核 queued 集恒来自单次单组放行
759
715
  * → 内核 doSend 的合批产物恒单通道——批组隔离不依赖时序巧合。
@@ -835,8 +791,6 @@ export function createDeliveryPort(host: DeliveryPortHost): DeliveryPortHandle {
835
791
 
836
792
  function flush(): void {
837
793
  if (disposed) return;
838
- clearMergeTimer();
839
- flushMergeBuffer("flush");
840
794
  pumpDirect();
841
795
  kernel.flush();
842
796
  }
@@ -853,18 +807,14 @@ export function createDeliveryPort(host: DeliveryPortHost): DeliveryPortHandle {
853
807
  if (seenIds.has(rec.id)) continue;
854
808
  if (state.ackedIds.has(rec.id) || state.deliveredIds.has(rec.id)) continue;
855
809
  seenIds.add(rec.id);
856
- if (rec.payload.customType === DEFAULT_NOTIFY_CUSTOM_TYPE) {
857
- mergeBuffer.set(rec.id, rec);
858
- } else {
859
- directBuffer.set(rec.id, rec);
860
- }
810
+ directBuffer.set(rec.id, rec);
861
811
  replayed += 1;
862
812
  }
863
813
  if (replayed > 0) {
864
814
  buckets.recoveryReplays += replayed;
865
815
  emitBucketLog(host, "recoveryReplays", buckets.recoveryReplays);
866
- // 立即尝试投递(重放合并形态:攒批成员合并批、直通成员分组放行)。
867
- flushMergeBuffer("replay");
816
+ // 立即尝试投递(重放合并形态:直通成员分组放行——同组多成员仍产合法批
817
+ // 形态,保留边界 pumpDirect/commitAsBatch 未删)。
868
818
  pumpDirect();
869
819
  }
870
820
  return replayed;
@@ -888,8 +838,6 @@ export function createDeliveryPort(host: DeliveryPortHost): DeliveryPortHandle {
888
838
  `(waiting=${inFlight.waiting}, suspended=${inFlight.suspended})`,
889
839
  );
890
840
  }
891
- clearMergeTimer();
892
- mergeBuffer.clear();
893
841
  directBuffer.clear();
894
842
  unsubMessageEnd();
895
843
  unsubBeforeCompact();
@@ -942,6 +890,10 @@ interface PendingBaseline {
942
890
  unregisteredIds: Set<string>;
943
891
  /** 注销条目 id → status(写侧经 mapReasonToStatus 已映射落盘;畸形不收)。 */
944
892
  unregisterStatus: Map<string, MappedPendingStatus>;
893
+ /** 注销条目 id → 结清五值(§3.3.6 权威通道 resolveSettlementReason——快照收取与
894
+ * 工具侧回填两通道同规则,结清视图优先取值;legacy status 视图承担对账副档与
895
+ * 本视图缺位(畸形 data:reason 非 string)时的兜底)。 */
896
+ unregisterSettlements: Map<string, ResolvedSettlementReason>;
945
897
  }
946
898
 
947
899
  function scanPendingBaseline(entries: readonly unknown[]): PendingBaseline {
@@ -955,6 +907,7 @@ function scanPendingBaseline(entries: readonly unknown[]): PendingBaseline {
955
907
  registerTypes: scan.registerTypes,
956
908
  unregisteredIds: scan.unregisteredIds,
957
909
  unregisterStatus: scan.unregisterStatuses,
910
+ unregisterSettlements: scan.unregisterSettlements,
958
911
  };
959
912
  }
960
913
 
package/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/index.ts
2
2
  //
3
3
  // @zhushanwen/pi-delivery-port 公共出口:统一消息投递服务的 pi 半区投递端口
4
- // (custom 车道唯一执行者;设计 .tmp/tech-design/unified-delivery.md §3.7)。
4
+ // (custom 投递路径唯一执行者;设计 .tmp/tech-design/unified-delivery.md §3.7)。
5
5
  //
6
6
  // 装配形态(§3.7):共享库(非 pi extension)+ 装配点 = @zhushanwen/pi-base-tool-
7
7
  // enhance 的 session_start(infrastructure 档必载)。消费方(notifier / session-
package/src/pi-host.ts CHANGED
@@ -23,8 +23,6 @@ export interface CreatePiDeliveryPortHostOptions { // oe-exempt:20261017:framewo
23
23
  component: string;
24
24
  /** 降级与诊断出口(@zhushanwen/pi-extension-logger 的 ExtensionLogger)。 */
25
25
  logger: DeliveryPortHost["logger"];
26
- /** 攒批条件谓词(有活跃后台任务;base-tool-enhance 注入 task-store 计数)。 */
27
- hasRunningBackground(): boolean;
28
26
  }
29
27
 
30
28
  /** 接线 pi 扩展运行时端口为 DeliveryPortHost(纯端口面,无业务状态)。 */
@@ -62,7 +60,6 @@ export function createPiDeliveryPortHost(
62
60
  pi.on("session_compact_failed", () => {
63
61
  handler();
64
62
  }),
65
- hasRunningBackground: () => options.hasRunningBackground(),
66
63
  logger: options.logger,
67
64
  };
68
65
  }
package/src/types.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  // §3.7 pi 半区组件表 / §3.4 提交选项缺省值表)。
5
5
  //
6
6
  // 本包是「知 pi 又知内核」的适配层:把 pi 扩展运行时
7
- // 端口接线成 @zhushanwen/session-delivery v3 内核的 port 半区——custom 车道唯一
7
+ // 端口接线成 @zhushanwen/session-delivery v3 内核的 port 半区——custom 投递路径唯一
8
8
  // 执行者(outbox 投递日志 / details-id 送达凭据 / session_start 差集重放 / 提交侧
9
9
  // 合批与批组隔离 / compaction 等待)。消费方(notifier / session-manager / bash)
10
10
  // 经 Symbol.for 进程级槽 getDeliveryPort() 获取实例(C-ext-06 惯例)。包依赖分层:
@@ -16,7 +16,7 @@
16
16
  // 的 extensions 侧同位形态);内核包零 pi 词汇的纪律由 session-delivery 自守。
17
17
 
18
18
  import type { CustomPayload, DeliveryFailurePolicy } from "@zhushanwen/session-delivery";
19
- import type { MappedPendingStatus } from "@zhushanwen/extension-protocol";
19
+ import type { MappedPendingStatus, SettlementReason } from "@zhushanwen/extension-protocol";
20
20
 
21
21
  // ─── envelope ───────────────────────────────────────────────
22
22
 
@@ -29,7 +29,7 @@ import type { MappedPendingStatus } from "@zhushanwen/extension-protocol";
29
29
  export type DeliveryTiming = "steer" | "followUp";
30
30
 
31
31
  /**
32
- * 投递信封(进程内 custom 车道):text 载荷属 runtime 半区端点,不出现在本端口面。
32
+ * 投递信封(进程内 custom 投递路径):text 载荷属 runtime 半区端点,不出现在本端口面。
33
33
  *
34
34
  * to 字段刻意缺席:port 每 session 装配一份(session_start 重绑),投递目标恒为
35
35
  * 当前 session——进程内端口无跨 session 寻址语义(跨会话互投是设计 §9 非目标)。
@@ -43,15 +43,8 @@ export interface DeliveryEnvelope { // oe-exempt:20261017:framework:统一投递
43
43
  id?: string;
44
44
  }
45
45
 
46
- /** deliver 提交选项(缺省值表 §3.4:通知域 = steer + retain + 合批 + details-id)。 */
46
+ /** deliver 提交选项(§3.4:通知域 = steer + retain + details-id;攒批窗已删,无合批选项)。 */
47
47
  export interface DeliveryDeliverOptions { // oe-exempt:20261017:framework:统一投递对外 API 契约(deliver 提交选项面)
48
- /**
49
- * 合批窗口判定覆盖。缺省 = 「默认通道(subagent-bg-notify)且宿主有活跃后台任务」
50
- * 时进 60s 滑动窗口攒批;外部通道(workflow-result / managed-session-notify 等)
51
- * 恒逐条(批组隔离 [u9] 规则,merge:true 不得覆盖——窗口合批形态 {batch,items}
52
- * 对外部通道无消费契约)。显式 merge:false 可让默认通道条目跳过窗口立即提交。
53
- */
54
- merge?: boolean;
55
48
  /**
56
49
  * 失败保留策略(内核首败处置)。缺省 "retain"(留账:下一边沿 / compaction 唤醒 /
57
50
  * session_start 重放重投——通知域无人值守语义);显式 "forget" = 首败即弃(discard
@@ -84,17 +77,21 @@ export type DeliveryDeliverResult =
84
77
  /**
85
78
  * 收取条目(collect 面返回给等待器的单条结果;字段来源设计 §3.1③ 字段来源表):
86
79
  * id = 收取展开键(单债权条目 = 生产方申报的操作 id;managed 合批条目逐键展开,
87
- * 每键一条、载荷共享);type 与 outcome 与 pending 词表对齐(protocol
88
- * mapReasonToStatus 单一权威点,不建第三套终态词表)——两字段的两条填充路径:
89
- * collectArm 快照收取(ready / unregistered-no-entry,端口按日志基线填)与消费方
90
- * 终局回填(consume 条目,等待结束后按 session entries 现算填,见 consume 时序
91
- * 契约);content/details = 通知载荷原样;note 标记特殊态说明(如 managed 域「注销
80
+ * 每键一条、载荷共享);type 与 pending 词表对齐;outcome 是不透明终态标注值
81
+ * (消费方按原文呈现,不做词表分支判断)。快照与回填两通道统一填充规则:
82
+ * 注销结清视图(SettlementReason ∪ 'unknown'——reason 事实经 resolveSettlementReason
83
+ * 结清解析,subagent-observability §3.3.6 权威通道)优先取值;结清视图缺位时
84
+ * legacy status 副档(expired / time_limited / aborted 等历史终态,
85
+ * mapReasonToStatus 映射产物)兜底。缺位仅发生于畸形 data(status 合法但 reason
86
+ * 非 string——注销 entry 不入结清视图),此时两通道同取 status 值保持一致;
87
+ * 两视图皆缺位保持 undefined(诚实缺省,不虚构终态)。
88
+ * content/details = 通知载荷原样;note 标记特殊态说明(如 managed 域「注销
92
89
  * 无条目分支完结,无独立载荷快照」)。
93
90
  */
94
91
  export interface CollectedEntry { // oe-exempt:20261017:framework:collect 批量收取对外契约(收取条目形态)
95
92
  id: string;
96
93
  type?: string;
97
- outcome?: MappedPendingStatus;
94
+ outcome?: MappedPendingStatus | SettlementReason;
98
95
  content?: string;
99
96
  details?: unknown;
100
97
  note?: string;
@@ -106,8 +103,9 @@ export interface CollectedEntry { // oe-exempt:20261017:framework:collect 批量
106
103
  * discarded)→ 可快照收取;
107
104
  * - in-push-pipeline:键 ∈ 某日志条目键集、条目无终局(已过分流点,在 push 管线
108
105
  * 排队待注入)→ 不收取不等待;
109
- * - unregistered-no-entry:键曾注册且已注销、不属任何日志条目键集 → 按 unregister
110
- * 的 status 定 outcome 进 collected(无载荷快照形态);
106
+ * - unregistered-no-entry:键曾注册且已注销、不属任何日志条目键集 → outcome 按
107
+ * unregister 的结清视图优先 + legacy status 兜底定值进 collected(无载荷快照
108
+ * 形态,畸形 data 缺省语义见 CollectedEntry 注释);
111
109
  * - waiting:活跃且无日志条目 → 登记等待,后续 deliver 命中等待集被挂起;
112
110
  * - unknown:键未注册且无条目(活跃集 ∪ 投递日志键集 ∪ 曾注册集的补集)→ 不登记
113
111
  * 等待集、不虚构终态——工具侧读本态组装 errors(未知判定单点在端口)。
@@ -158,8 +156,8 @@ export interface DeliveryPortHost { // oe-exempt:20261017:framework:pi 能力注
158
156
  readSessionEntries(): readonly unknown[];
159
157
  /**
160
158
  * 主 agent 是否空闲(ctx.isIdle()——含 run 活跃与 compaction 两态)。仅作内核
161
- * port.isIdle 供给(内核接口必填成员;on-accept 模式内核 busy gate 旁路,恒不
162
- * 消费)。提交侧挂起判定不用本端口——compaction 等待由 compaction 事件标记承载
159
+ * port.isIdle 供给(内核接口必填成员;内核受理即出站不读忙闲探测,恒不消费)。
160
+ * 提交侧挂起判定不用本端口——compaction 等待由 compaction 事件标记承载
163
161
  * (见 onSessionBeforeCompact),run 活跃必须放行(sendMessage 入 pi steer 队列
164
162
  * 不拒),isIdle 的复合语义不能承载该区分。
165
163
  */
@@ -186,8 +184,6 @@ export interface DeliveryPortHost { // oe-exempt:20261017:framework:pi 能力注
186
184
  onSessionCompact(handler: () => void): () => void;
187
185
  /** compaction 失败/中止边沿(标记同样复位 + 唤醒——失败也要重投尝试,失败可见性归日志)。 */
188
186
  onSessionCompactFailed(handler: () => void): () => void;
189
- /** 攒批条件谓词(有活跃后台任务;账本 mergeHoldActive = hasRunningBackground 同源)。 */
190
- hasRunningBackground(): boolean;
191
187
  /** 分桶诊断与降级 warn 出口(extensionLogger——落 session JSONL 不进 LLM +
192
188
  * TAIJI_AGENT_DEBUG 落日志盘)。 */
193
189
  logger: DeliveryPortLogger;
@@ -196,30 +192,29 @@ export interface DeliveryPortHost { // oe-exempt:20261017:framework:pi 能力注
196
192
  // ─── handle(进程级槽暴露面)────────────────────────────────
197
193
 
198
194
  /**
199
- * 投递端口句柄(custom 车道唯一执行者的对外形态)。
195
+ * 投递端口句柄(custom 投递路径唯一执行者的对外形态)。
200
196
  * 同 session 必须单例(session_start 重绑 = 旧实例 dispose + 新实例 recover)。
201
197
  */
202
198
  export interface DeliveryPortHandle { // oe-exempt:20261017:framework:进程级槽交付的端口句柄契约(消费方读槽面)
203
199
  /**
204
- * 唯一提交入口(§3.4 端点形态):受理前 appendEntry 落投递日志 → 缓冲(攒批路径)
205
- * 或内核 send(直通路径)→ 受理结果同步返回。fire-and-forget 语义:调用方忽略
206
- * 返回值不受影响;rejected 条目由调用方裁决重试(幂等键未登记,重提合法)。
200
+ * 唯一提交入口(§3.4 端点形态):受理前 appendEntry 落投递日志 → 提交缓冲 →
201
+ * 边沿即投放行(攒批窗已删 §3.3.7③)→ 受理结果同步返回。fire-and-forget 语义:
202
+ * 调用方忽略返回值不受影响;rejected 条目由调用方裁决重试(幂等键未登记,重提合法)。
207
203
  */
208
204
  deliver(envelope: DeliveryEnvelope, opts?: DeliveryDeliverOptions): DeliveryDeliverResult;
209
205
  /**
210
206
  * session_start 差集重放:扫投递日志 −
211
- * (对话记录已送达 ∪ ack)差集 → 重入队 → 立即尝试投递。重放条目若满足攒批
212
- * 条件经窗口合并投出(合并形态与直通路径一致)。幂等:可
213
- * 重复调用,差集为空零动作。
207
+ * (对话记录已送达 ∪ ack)差集 → 重入队 → 边沿即投。重放同组多成员经分组放行
208
+ * 仍产合法批形态(保留边界)。幂等:可重复调用,差集为空零动作。
214
209
  *
215
210
  * @returns 重放条数。
216
211
  */
217
212
  recover(): number;
218
213
  /** 分桶计数快照(副本)。 */
219
214
  metrics(): DeliveryPortMetrics;
220
- /** 强制投递尝试(窗口缓冲提前出站 + 内核 flush——外部重触发口)。 */
215
+ /** 强制投递尝试(缓冲条目出站 + 内核 flush——外部重触发口)。 */
221
216
  flush(): void;
222
- /** 销毁:清窗口 timer + 内核 dispose + 退订全部事件订阅 + 摘进程级槽绑定。 */
217
+ /** 销毁:内核 dispose + 退订全部事件订阅 + 摘进程级槽绑定。 */
223
218
  dispose(): void;
224
219
  /**
225
220
  * collect 面(result-collect 设计 §3.1② 端口挂起面,方法语义以该节为准)。
@@ -239,7 +234,7 @@ export interface DeliveryPortHandle { // oe-exempt:20261017:framework:进程级
239
234
  * 以等待结束后的最终回填为准(诚实缺省,不虚构终态)。
240
235
  */
241
236
  consume(): CollectedEntry[];
242
- /** 解散等待集;未收挂起条目直通内核 flush(跳过 60s 合批窗口,恢复 push)。 */
237
+ /** 解散等待集;未收挂起条目直通内核 flush(边沿即投,恢复 push)。 */
243
238
  collectRelease(): void;
244
239
  /** 挂起条目到达回调订阅(事件驱动唤醒等待器,无轮询);返回退订函数。 */
245
240
  onCollectArrival(cb: () => void): () => void;