@zhushanwen/pi-session-manager 0.6.4 → 0.6.5

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-session-manager",
3
- "version": "0.6.4",
3
+ "version": "0.6.5",
4
4
  "description": "Pi extension for agent-managed sessions — create, message, monitor, abort, archive, and delete child sessions via ctx.ui.select channel.",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -30,10 +30,10 @@
30
30
  "README.md"
31
31
  ],
32
32
  "dependencies": {
33
- "@zhushanwen/extension-protocol": "0.26.0",
34
- "@zhushanwen/pi-delivery-port": "0.3.1",
35
- "@zhushanwen/pi-extension-logger": "0.6.2",
36
- "@zhushanwen/pi-ext-guards": "0.5.0"
33
+ "@zhushanwen/extension-protocol": "0.27.0",
34
+ "@zhushanwen/pi-delivery-port": "0.4.0",
35
+ "@zhushanwen/pi-ext-guards": "0.5.1",
36
+ "@zhushanwen/pi-extension-logger": "0.6.2"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/node": "^24.0.0",
@@ -9,7 +9,8 @@
9
9
  // 旧实例后缓存引用的 deliver 静默 rejected,对半概率丢全部通知的 D3⑤ 同律);
10
10
  // 通道 = managed-session-notify 外部通道(payload.customType,port 批组隔离恒
11
11
  // 逐条不合批);槽空降级(port 缺席 → warn + false,STANDARDS §11.1);port
12
- // rejected(persist-failed)→ warn + false(幂等键未登记,watch 重试合法)。
12
+ // rejected(persist-failed)→ warn + 立即单次重提(D8①:deliver 被调恰 2),重提
13
+ // 仍 rejected 才 false(幂等键未登记,恢复 = 对账边沿,见下方两用例)。
13
14
 
14
15
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
15
16
  import { createLoggerModuleMock, getLoggerMock } from "./helpers/logger-mock.ts";
@@ -38,8 +39,19 @@ type DeliverCall = {
38
39
  collectKey: string[] | undefined;
39
40
  };
40
41
 
41
- function plantPort(behavior: "accept" | "reject" = "accept"): Array<DeliverCall> {
42
+ /**
43
+ * 桩行为脚本项:accept(受理)| reject(拒收)。序列按 deliver 调用序消费(耗尽
44
+ * 后循环末项)——「首投 rejected、重提 accepted」的 disposed 窗口重绑恢复形态用
45
+ * ["reject", "accept"] 表达。reject 行为不进 calls(calls 只收受理)——重提单次
46
+ * 形态的计数经「deliver 被调次数」断言(attempt 封装在桩内,calls 长度 +
47
+ * rejected 次数还原总调次)。
48
+ */
49
+ type DeliverStep = "accept" | "reject";
50
+
51
+ function plantPort(behavior: DeliverStep | DeliverStep[] = "accept"): Array<DeliverCall> {
42
52
  const calls: Array<DeliverCall> = [];
53
+ const script = Array.isArray(behavior) ? behavior : [behavior];
54
+ let attempt = 0;
43
55
  Reflect.set(globalThis, PORT_SLOT_KEY, {
44
56
  current: {
45
57
  deliver: (
@@ -49,7 +61,9 @@ function plantPort(behavior: "accept" | "reject" = "accept"): Array<DeliverCall>
49
61
  },
50
62
  opts?: { collectKey?: string[] },
51
63
  ) => {
52
- if (behavior === "reject") return { kind: "rejected", reason: "persist-failed (fake)" };
64
+ const step = script[Math.min(attempt, script.length - 1)]!;
65
+ attempt += 1;
66
+ if (step === "reject") return { kind: "rejected", reason: "persist-failed (fake)" };
53
67
  calls.push({
54
68
  id: envelope.id,
55
69
  customType: envelope.payload.customType,
@@ -97,13 +111,34 @@ describe("recordManagedNotify 投递接入纪律(unified-delivery 波 2:port
97
111
  expect(delivered[0]!.collectKey).toEqual(["sm-a", "sm-b"]);
98
112
  });
99
113
 
100
- it("port rejected(persist-failed)→ warn + false(幂等键未登记,watch 重试合法;不静默吞)", () => {
114
+ it("port rejected(persist-failed)→ 立即单次重提(D8①):deliver 被调恰 2(原投 1 + 重提 1)、无第三次投递(RL10 单次形态同锁)、重提仍 rejected → false + warn 含对账恢复指引", () => {
101
115
  plantPort("reject");
102
116
  expect(recordManagedNotify("sm-rej", "content", makeDetails("sm-rej"), ["sm-rej"])).toBe(false);
103
117
  expect(loggerMock.warn).toHaveBeenCalledWith(
104
118
  expect.stringContaining("delivery port rejected notification"),
105
119
  expect.objectContaining({ notifyId: "sm-rej" }),
106
120
  );
121
+ // RL10:重提仍 rejected 即停(无重试循环无定时器)——warn 文案含单次语义与
122
+ // 对账边沿恢复指引(RL11 同族:持久成因不静默重试)
123
+ expect(loggerMock.warn).toHaveBeenCalledWith(
124
+ expect.stringContaining("notify retry rejected after the single re-acquire attempt"),
125
+ expect.objectContaining({ notifyId: "sm-rej" }),
126
+ );
127
+ });
128
+
129
+ it("rejected 后单次重提 accepted(disposed 窗口重绑恢复形态)→ true:重提原 envelope(同幂等键同 collectKey 申报)受理即恢复", () => {
130
+ const delivered = plantPort(["reject", "accept"]);
131
+ expect(recordManagedNotify("sm-retry", "hello", makeDetails("sm-retry"), ["sm-retry"])).toBe(true);
132
+ // 原投 rejected(不进 calls)+ 重提 accepted(进 calls)= 恰 1 条受理
133
+ expect(delivered).toHaveLength(1);
134
+ expect(delivered[0]!.id).toBe("sm-retry");
135
+ expect(delivered[0]!.content).toBe("hello");
136
+ expect(delivered[0]!.collectKey).toEqual(["sm-retry"]);
137
+ // 无第三次投递(重提受理即收尾——RL10 的收尾语义)
138
+ expect(loggerMock.warn).not.toHaveBeenCalledWith(
139
+ expect.stringContaining("retry rejected"),
140
+ expect.anything(),
141
+ );
107
142
  });
108
143
 
109
144
  it("投递异常(port.deliver 抛错)→ warn 留痕不外抛(不拖垮 watch 应答编排,unregister 照常)", () => {
@@ -122,4 +157,23 @@ describe("recordManagedNotify 投递接入纪律(unified-delivery 波 2:port
122
157
  expect.objectContaining({ notifyId: "sm-throw-2" }),
123
158
  );
124
159
  });
160
+
161
+ it("rejected 后重取缺席(重提前槽被 dispose)→ 即停转对账兜底 + warn(重提路径第二停止位,不扩直发降级)", () => {
162
+ Reflect.set(globalThis, PORT_SLOT_KEY, {
163
+ current: {
164
+ deliver: () => {
165
+ // 模拟转换窗竞态:主投 rejected 后旧实例被 dispose(槽摘除——
166
+ // 重提的 getDeliveryPort() 将取到 undefined)
167
+ Reflect.deleteProperty(globalThis, PORT_SLOT_KEY);
168
+ return { kind: "rejected", reason: "persist-failed (fake)" };
169
+ },
170
+ flush: () => {},
171
+ },
172
+ });
173
+ expect(recordManagedNotify("sm-absent", "content", makeDetails("sm-absent"), ["sm-absent"])).toBe(false);
174
+ expect(loggerMock.warn).toHaveBeenCalledWith(
175
+ expect.stringContaining("delivery port absent on re-acquire"),
176
+ expect.objectContaining({ notifyId: "sm-absent" }),
177
+ );
178
+ });
125
179
  });
@@ -0,0 +1,95 @@
1
+ // notify-reconcile-resume-edge.test.ts —— session-manager 域切回边沿订阅装配锁
2
+ // (completion-signal-delivery D8② 三入口同构的切回补强边沿,U2 修复回归):
3
+ // extension load 装配期经 pi-delivery-port 进程级注册槽订阅切回派发——漏挂 =
4
+ // 「进程存活 + rejected + 用户切走再切回」场景补投不触发的缺陷形态(入口①②
5
+ // 在该场景不可达:切回不重发 session_start、无新回合则 agent_settled 不达)。
6
+ //
7
+ // 观察点:mock ./notify-reconcile.ts 的 runNotifyReconcileMaintenance(编排本体
8
+ // 行为在 notify-reconcile.test.ts 直测权威,本文件只锁装配接线面)——default
9
+ // export 执行后 dispatchResumeReconcile(真实槽)应驱动本域闭包以 reason="resume"
10
+ // 调编排,且 deps 的 entries / sessionFile 从派发 ctx 现读。
11
+
12
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
13
+
14
+ const { reconcileMock } = vi.hoisted(() => ({
15
+ reconcileMock: vi.fn(() => ({
16
+ scanned: 0,
17
+ candidates: 0,
18
+ redelivered: [],
19
+ skippedDelivered: [],
20
+ skippedJournaled: [],
21
+ })),
22
+ }));
23
+
24
+ vi.mock("../notify-reconcile.ts", async (importOriginal) => {
25
+ const actual = await importOriginal<typeof import("../notify-reconcile.ts")>();
26
+ return { ...actual, runNotifyReconcileMaintenance: reconcileMock };
27
+ });
28
+
29
+ import {
30
+ _resetResumeReconcileForTest,
31
+ dispatchResumeReconcile,
32
+ type ResumeReconcileContext,
33
+ } from "@zhushanwen/pi-delivery-port";
34
+
35
+ import registerExtension from "../index.ts";
36
+
37
+ function makeMockPi() {
38
+ return {
39
+ registerTool: vi.fn(),
40
+ on: vi.fn(),
41
+ appendEntry: vi.fn(),
42
+ } as never;
43
+ }
44
+
45
+ function makeEdgeCtx(): ResumeReconcileContext {
46
+ return {
47
+ sessionManager: {
48
+ getEntries: () => [{ customType: "pending:register", data: { id: "sm-r1", type: "session", name: "worker" } }],
49
+ getSessionFile: () => "/sessions/edge.jsonl",
50
+ getSessionId: () => "sess-edge",
51
+ },
52
+ };
53
+ }
54
+
55
+ beforeEach(() => {
56
+ _resetResumeReconcileForTest();
57
+ reconcileMock.mockClear();
58
+ });
59
+
60
+ afterEach(() => {
61
+ _resetResumeReconcileForTest();
62
+ });
63
+
64
+ describe("session-manager 域切回边沿订阅(D8② 入口③,U2 修复)", () => {
65
+ it("extension load 后切回派发 → 本域对账闭包以 reason=resume 调编排,deps 从派发 ctx 现读", () => {
66
+ registerExtension(makeMockPi());
67
+ const ctx = makeEdgeCtx();
68
+
69
+ const failures = dispatchResumeReconcile(ctx);
70
+
71
+ expect(failures).toEqual([]);
72
+ expect(reconcileMock).toHaveBeenCalledTimes(1);
73
+ const deps = reconcileMock.mock.calls[0][0] as {
74
+ reason: string;
75
+ entries: readonly unknown[];
76
+ sessionFile: string | undefined;
77
+ };
78
+ expect(deps.reason).toBe("resume");
79
+ expect(deps.entries).toEqual(ctx.sessionManager.getEntries());
80
+ expect(deps.sessionFile).toBe("/sessions/edge.jsonl");
81
+ });
82
+
83
+ it("重复 load(同进程 session 替换形态)覆盖旧订阅:派发只打新闭包一次(无死 handler 双派发)", () => {
84
+ registerExtension(makeMockPi());
85
+ registerExtension(makeMockPi());
86
+
87
+ expect(dispatchResumeReconcile(makeEdgeCtx())).toEqual([]);
88
+ expect(reconcileMock).toHaveBeenCalledTimes(1);
89
+ });
90
+
91
+ it("未 load 本扩展时空槽派发零调用(扩展禁用形态,缺席语义非错误)", () => {
92
+ expect(dispatchResumeReconcile(makeEdgeCtx())).toEqual([]);
93
+ expect(reconcileMock).not.toHaveBeenCalled();
94
+ });
95
+ });