@gotgenes/pi-permission-system 8.3.1 → 9.0.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.
@@ -8,6 +8,7 @@ import {
8
8
  } from "node:fs";
9
9
  import { homedir, tmpdir } from "node:os";
10
10
  import { dirname, join, resolve } from "node:path";
11
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
11
12
  import { expect, test } from "vitest";
12
13
  import {
13
14
  createActiveToolsCacheKey,
@@ -46,23 +47,16 @@ import {
46
47
  canResolveAskPermissionRequest,
47
48
  shouldAutoApprovePermissionState,
48
49
  } from "#src/yolo-mode";
50
+ import { type FakePi, makeFakePi } from "#test/helpers/make-fake-pi";
49
51
  import {
50
52
  type CreateManagerOptions,
51
53
  createManager,
52
54
  } from "#test/helpers/manager-harness";
53
55
 
54
- type MockHandler = (
55
- event: Record<string, unknown>,
56
- ctx: Record<string, unknown>,
57
- ) =>
58
- | Promise<Record<string, unknown> | undefined>
59
- | Record<string, unknown>
60
- | undefined;
61
-
62
56
  type ExtensionHarness = {
63
57
  baseDir: string;
64
58
  cwd: string;
65
- handlers: Record<string, MockHandler>;
59
+ pi: FakePi;
66
60
  prompts: string[];
67
61
  cleanup: () => Promise<void>;
68
62
  };
@@ -112,7 +106,6 @@ function createToolCallHarness(
112
106
  const baseDir = mkdtempSync(join(tmpdir(), "pi-permission-system-runtime-"));
113
107
  const cwd = options.cwd ?? baseDir;
114
108
  const prompts: string[] = [];
115
- const handlers: Record<string, MockHandler> = {};
116
109
  const originalAgentDir = process.env.PI_CODING_AGENT_DIR;
117
110
  const globalConfigPath = getGlobalConfigPath(baseDir);
118
111
  mkdirSync(join(baseDir, "agents"), { recursive: true });
@@ -124,22 +117,10 @@ function createToolCallHarness(
124
117
  "utf8",
125
118
  );
126
119
 
120
+ const pi = makeFakePi({ toolNames });
127
121
  process.env.PI_CODING_AGENT_DIR = baseDir;
128
122
  try {
129
- piPermissionSystemExtension({
130
- on: (name: string, handler: MockHandler): void => {
131
- handlers[name] = handler;
132
- },
133
- registerCommand: (): void => {},
134
- getAllTools: (): Array<{ name: string }> =>
135
- toolNames.map((name) => ({ name })),
136
- setActiveTools: (): void => {},
137
- registerProvider: (): void => {},
138
- events: {
139
- emit: (): void => {},
140
- on: (): (() => void) => () => undefined,
141
- },
142
- } as never);
123
+ piPermissionSystemExtension(pi as unknown as ExtensionAPI);
143
124
  } finally {
144
125
  if (originalAgentDir === undefined) {
145
126
  delete process.env.PI_CODING_AGENT_DIR;
@@ -151,11 +132,13 @@ function createToolCallHarness(
151
132
  return {
152
133
  baseDir,
153
134
  cwd,
154
- handlers,
135
+ pi,
155
136
  prompts,
156
137
  cleanup: async (): Promise<void> => {
157
- await Promise.resolve(
158
- handlers.session_shutdown({}, createMockContext(cwd, prompts, options)),
138
+ await pi.fire(
139
+ "session_shutdown",
140
+ {},
141
+ createMockContext(cwd, prompts, options),
159
142
  );
160
143
  rmSync(baseDir, { recursive: true, force: true });
161
144
  },
@@ -192,15 +175,14 @@ async function runToolCall(
192
175
  event: Record<string, unknown>,
193
176
  options: ExtensionHarnessOptions = {},
194
177
  ): Promise<Record<string, unknown>> {
195
- const handler = harness.handlers.tool_call;
196
- expect(handler).toBeTypeOf("function");
197
-
198
178
  const result = await withIsolatedSubagentEnv(async () =>
199
- Promise.resolve(
200
- handler(event, createMockContext(harness.cwd, harness.prompts, options)),
179
+ harness.pi.fire(
180
+ "tool_call",
181
+ event,
182
+ createMockContext(harness.cwd, harness.prompts, options),
201
183
  ),
202
184
  );
203
- return result ?? {};
185
+ return (result as Record<string, unknown> | undefined) ?? {};
204
186
  }
205
187
 
206
188
  test("Yolo mode only auto-approves ask-state permissions", () => {
@@ -2335,7 +2317,7 @@ test("session approval: session_shutdown clears session approvals", async () =>
2335
2317
  hasUI: true,
2336
2318
  selectResponse: "Yes",
2337
2319
  });
2338
- await Promise.resolve(harness.handlers.session_shutdown({}, shutdownCtx));
2320
+ await harness.pi.fire("session_shutdown", {}, shutdownCtx);
2339
2321
 
2340
2322
  // Access same path again — should prompt because cache was cleared
2341
2323
  const result = await runToolCall(
@@ -26,7 +26,10 @@ function makeService(
26
26
 
27
27
  describe("globalThis accessor", () => {
28
28
  afterEach(() => {
29
- unpublishPermissionsService();
29
+ const current = getPermissionsService();
30
+ if (current) {
31
+ unpublishPermissionsService(current);
32
+ }
30
33
  });
31
34
 
32
35
  it("returns undefined when nothing has been published", () => {
@@ -47,15 +50,25 @@ describe("globalThis accessor", () => {
47
50
  expect(getPermissionsService()).toBe(second);
48
51
  });
49
52
 
50
- it("returns undefined after unpublish", () => {
53
+ it("removes the slot when it still holds the given service", () => {
51
54
  const service = makeService();
52
55
  publishPermissionsService(service);
53
- unpublishPermissionsService();
56
+ unpublishPermissionsService(service);
54
57
  expect(getPermissionsService()).toBeUndefined();
55
58
  });
56
59
 
60
+ it("does not remove the slot when a different service occupies it", () => {
61
+ const parent = makeService();
62
+ const child = makeService();
63
+ publishPermissionsService(parent);
64
+ // A child instance never published `parent`; unpublishing its own service
65
+ // must be a no-op that leaves the parent's slot intact.
66
+ unpublishPermissionsService(child);
67
+ expect(getPermissionsService()).toBe(parent);
68
+ });
69
+
57
70
  it("unpublish is safe to call when nothing was published", () => {
58
- expect(() => unpublishPermissionsService()).not.toThrow();
71
+ expect(() => unpublishPermissionsService(makeService())).not.toThrow();
59
72
  expect(getPermissionsService()).toBeUndefined();
60
73
  });
61
74
  });
@@ -64,7 +77,10 @@ describe("globalThis accessor", () => {
64
77
 
65
78
  describe("service adapter delegation", () => {
66
79
  afterEach(() => {
67
- unpublishPermissionsService();
80
+ const current = getPermissionsService();
81
+ if (current) {
82
+ unpublishPermissionsService(current);
83
+ }
68
84
  });
69
85
 
70
86
  const fakeResult: PermissionCheckResult = {
@@ -191,7 +207,10 @@ describe("service adapter delegation", () => {
191
207
 
192
208
  describe("registerToolInputFormatter delegation", () => {
193
209
  afterEach(() => {
194
- unpublishPermissionsService();
210
+ const current = getPermissionsService();
211
+ if (current) {
212
+ unpublishPermissionsService(current);
213
+ }
195
214
  });
196
215
 
197
216
  it("delegates to the registry and returns its disposer", () => {
@@ -2,6 +2,7 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { afterEach, describe, expect, test, vi } from "vitest";
3
3
  import { SUBAGENT_ENV_HINT_KEYS } from "#src/permission-forwarding";
4
4
  import {
5
+ isRegisteredSubagentChild,
5
6
  isSubagentExecutionContext,
6
7
  normalizeFilesystemPath,
7
8
  } from "#src/subagent-context";
@@ -12,14 +13,57 @@ afterEach(() => {
12
13
  vi.restoreAllMocks();
13
14
  });
14
15
 
15
- function makeCtx(sessionDir: string | null): ExtensionContext {
16
+ function makeCtx(
17
+ sessionDir: string | null,
18
+ sessionId: string = "",
19
+ ): ExtensionContext {
16
20
  return {
17
21
  sessionManager: {
18
22
  getSessionDir: vi.fn(() => sessionDir),
23
+ getSessionId: vi.fn(() => sessionId),
19
24
  },
20
25
  } as unknown as ExtensionContext;
21
26
  }
22
27
 
28
+ describe("isRegisteredSubagentChild", () => {
29
+ const childSessionId = "child-session-abc";
30
+
31
+ test("returns true when the session id is registered", () => {
32
+ const registry = new SubagentSessionRegistry();
33
+ registry.register(childSessionId, {});
34
+ expect(
35
+ isRegisteredSubagentChild(makeCtx(null, childSessionId), registry),
36
+ ).toBe(true);
37
+ });
38
+
39
+ test("returns false when the session id is not registered", () => {
40
+ const registry = new SubagentSessionRegistry();
41
+ expect(
42
+ isRegisteredSubagentChild(makeCtx(null, childSessionId), registry),
43
+ ).toBe(false);
44
+ });
45
+
46
+ test("returns false when the session id is empty", () => {
47
+ const registry = new SubagentSessionRegistry();
48
+ registry.register("", {});
49
+ expect(isRegisteredSubagentChild(makeCtx(null, ""), registry)).toBe(false);
50
+ });
51
+
52
+ test("returns false when getSessionId throws", () => {
53
+ const registry = new SubagentSessionRegistry();
54
+ registry.register(childSessionId, {});
55
+ const ctx = {
56
+ sessionManager: {
57
+ getSessionDir: vi.fn(() => null),
58
+ getSessionId: vi.fn(() => {
59
+ throw new Error("session id unavailable");
60
+ }),
61
+ },
62
+ } as unknown as ExtensionContext;
63
+ expect(isRegisteredSubagentChild(ctx, registry)).toBe(false);
64
+ });
65
+ });
66
+
23
67
  describe("normalizeFilesystemPath", () => {
24
68
  test("normalizes a simple absolute path", () => {
25
69
  expect(normalizeFilesystemPath("/projects/my-app")).toBe(
@@ -203,57 +247,75 @@ describe("isSubagentExecutionContext — registry detection", () => {
203
247
  const subagentRoot = "/home/user/.pi/agent/sessions/subagents";
204
248
  const outsideDir =
205
249
  "/home/user/projects/my-app/.pi/agent/sessions/parent/tasks";
250
+ const childSessionId = "child-session-abc";
206
251
 
207
- test("returns true when session dir is registered (no env vars, outside filesystem root)", () => {
252
+ test("returns true when session id is registered (no env vars, dir outside filesystem root)", () => {
208
253
  const registry = new SubagentSessionRegistry();
209
- registry.register(outsideDir, { agentName: "Explore" });
254
+ registry.register(childSessionId, {});
210
255
  expect(
211
- isSubagentExecutionContext(makeCtx(outsideDir), subagentRoot, registry),
256
+ isSubagentExecutionContext(
257
+ makeCtx(outsideDir, childSessionId),
258
+ subagentRoot,
259
+ registry,
260
+ ),
212
261
  ).toBe(true);
213
262
  });
214
263
 
215
264
  test("returns true when registered session has a parentSessionId", () => {
216
265
  const registry = new SubagentSessionRegistry();
217
- registry.register(outsideDir, {
218
- agentName: "Plan",
219
- parentSessionId: "parent-123",
220
- });
266
+ registry.register(childSessionId, { parentSessionId: "parent-123" });
221
267
  expect(
222
- isSubagentExecutionContext(makeCtx(outsideDir), subagentRoot, registry),
268
+ isSubagentExecutionContext(
269
+ makeCtx(outsideDir, childSessionId),
270
+ subagentRoot,
271
+ registry,
272
+ ),
223
273
  ).toBe(true);
224
274
  });
225
275
 
226
- test("returns false when registry is provided but session dir is not registered", () => {
276
+ test("returns false when registry is provided but session id is not registered", () => {
227
277
  const registry = new SubagentSessionRegistry();
228
278
  expect(
229
- isSubagentExecutionContext(makeCtx(outsideDir), subagentRoot, registry),
279
+ isSubagentExecutionContext(
280
+ makeCtx(outsideDir, childSessionId),
281
+ subagentRoot,
282
+ registry,
283
+ ),
230
284
  ).toBe(false);
231
285
  });
232
286
 
233
- test("returns false when session dir is null and registry has no matching entry", () => {
287
+ test("returns false when session id is empty and registry has no matching entry", () => {
234
288
  const registry = new SubagentSessionRegistry();
235
289
  expect(
236
- isSubagentExecutionContext(makeCtx(null), subagentRoot, registry),
290
+ isSubagentExecutionContext(makeCtx(null, ""), subagentRoot, registry),
237
291
  ).toBe(false);
238
292
  });
239
293
 
240
294
  test("registry check takes priority over env var detection", () => {
241
295
  // Registry says registered; env var not set — should still return true.
242
296
  const registry = new SubagentSessionRegistry();
243
- registry.register(outsideDir, { agentName: "Explore" });
297
+ registry.register(childSessionId, {});
244
298
  // Confirm no env var is set
245
299
  expect(process.env.PI_IS_SUBAGENT).toBeUndefined();
246
300
  expect(
247
- isSubagentExecutionContext(makeCtx(outsideDir), subagentRoot, registry),
301
+ isSubagentExecutionContext(
302
+ makeCtx(outsideDir, childSessionId),
303
+ subagentRoot,
304
+ registry,
305
+ ),
248
306
  ).toBe(true);
249
307
  });
250
308
 
251
309
  test("unregistered session falls through to env var detection", () => {
252
310
  vi.stubEnv("PI_IS_SUBAGENT", "true");
253
- const registry = new SubagentSessionRegistry(); // empty — outsideDir not registered
311
+ const registry = new SubagentSessionRegistry(); // empty — childSessionId not registered
254
312
  // Env var present → still true even without registry entry
255
313
  expect(
256
- isSubagentExecutionContext(makeCtx(outsideDir), subagentRoot, registry),
314
+ isSubagentExecutionContext(
315
+ makeCtx(outsideDir, childSessionId),
316
+ subagentRoot,
317
+ registry,
318
+ ),
257
319
  ).toBe(true);
258
320
  });
259
321
 
@@ -19,13 +19,11 @@ describe("subscribeSubagentLifecycle", () => {
19
19
  subscribeSubagentLifecycle(bus, registry);
20
20
 
21
21
  bus.emit(SUBAGENT_CHILD_SESSION_CREATED, {
22
- sessionDir: "/sessions/child-abc",
23
- agentName: "Explore",
22
+ sessionId: "child-session-abc",
24
23
  parentSessionId: "parent-42",
25
24
  });
26
25
 
27
- expect(registry.get("/sessions/child-abc")).toEqual({
28
- agentName: "Explore",
26
+ expect(registry.get("child-session-abc")).toEqual({
29
27
  parentSessionId: "parent-42",
30
28
  });
31
29
  });
@@ -40,12 +38,11 @@ describe("subscribeSubagentLifecycle", () => {
40
38
  subscribeSubagentLifecycle(bus, registry);
41
39
 
42
40
  bus.emit(SUBAGENT_CHILD_SESSION_CREATED, {
43
- sessionDir: "/sessions/child-sync",
44
- agentName: "Explore",
41
+ sessionId: "child-session-sync",
45
42
  });
46
43
 
47
44
  // No await between emit and this assertion.
48
- expect(registry.has("/sessions/child-sync")).toBe(true);
45
+ expect(registry.has("child-session-sync")).toBe(true);
49
46
  });
50
47
 
51
48
  it("omits parentSessionId when the event does not carry one", () => {
@@ -53,12 +50,10 @@ describe("subscribeSubagentLifecycle", () => {
53
50
  subscribeSubagentLifecycle(bus, registry);
54
51
 
55
52
  bus.emit(SUBAGENT_CHILD_SESSION_CREATED, {
56
- sessionDir: "/sessions/child-xyz",
57
- agentName: "general-purpose",
53
+ sessionId: "child-session-xyz",
58
54
  });
59
55
 
60
- expect(registry.get("/sessions/child-xyz")).toEqual({
61
- agentName: "general-purpose",
56
+ expect(registry.get("child-session-xyz")).toEqual({
62
57
  parentSessionId: undefined,
63
58
  });
64
59
  });
@@ -66,11 +61,11 @@ describe("subscribeSubagentLifecycle", () => {
66
61
  it("unregisters a child session on disposed", () => {
67
62
  const bus = createEventBus();
68
63
  subscribeSubagentLifecycle(bus, registry);
69
- registry.register("/sessions/child-abc", { agentName: "Explore" });
64
+ registry.register("child-session-abc", { parentSessionId: "parent-42" });
70
65
 
71
- bus.emit(SUBAGENT_CHILD_DISPOSED, { sessionDir: "/sessions/child-abc" });
66
+ bus.emit(SUBAGENT_CHILD_DISPOSED, { sessionId: "child-session-abc" });
72
67
 
73
- expect(registry.has("/sessions/child-abc")).toBe(false);
68
+ expect(registry.has("child-session-abc")).toBe(false);
74
69
  });
75
70
 
76
71
  it("detaches both handlers when the returned unsubscribe is called", () => {
@@ -80,12 +75,11 @@ describe("subscribeSubagentLifecycle", () => {
80
75
  unsubscribe();
81
76
 
82
77
  bus.emit(SUBAGENT_CHILD_SESSION_CREATED, {
83
- sessionDir: "/sessions/child-abc",
84
- agentName: "Explore",
78
+ sessionId: "child-session-abc",
85
79
  });
86
- bus.emit(SUBAGENT_CHILD_DISPOSED, { sessionDir: "/sessions/child-abc" });
80
+ bus.emit(SUBAGENT_CHILD_DISPOSED, { sessionId: "child-session-abc" });
87
81
 
88
- expect(registry.has("/sessions/child-abc")).toBe(false);
82
+ expect(registry.has("child-session-abc")).toBe(false);
89
83
  });
90
84
 
91
85
  it("subscribes to a fake bus on the exact channel names", () => {
@@ -110,4 +104,29 @@ describe("subscribeSubagentLifecycle", () => {
110
104
  );
111
105
  expect(SUBAGENT_CHILD_DISPOSED).toBe("subagents:child:disposed");
112
106
  });
107
+
108
+ // ── #298 regression: concurrent siblings must be independent ──────────────
109
+
110
+ it("disposing one sibling does not evict the other (collision regression)", () => {
111
+ const bus = createEventBus();
112
+ subscribeSubagentLifecycle(bus, registry);
113
+
114
+ // Two concurrent children of the same parent register under distinct ids.
115
+ bus.emit(SUBAGENT_CHILD_SESSION_CREATED, {
116
+ sessionId: "child-A",
117
+ parentSessionId: "parent-P",
118
+ });
119
+ bus.emit(SUBAGENT_CHILD_SESSION_CREATED, {
120
+ sessionId: "child-B",
121
+ parentSessionId: "parent-P",
122
+ });
123
+
124
+ // Sibling A finishes first.
125
+ bus.emit(SUBAGENT_CHILD_DISPOSED, { sessionId: "child-A" });
126
+
127
+ // B must still be detected as a registered subagent.
128
+ expect(registry.has("child-A")).toBe(false);
129
+ expect(registry.has("child-B")).toBe(true);
130
+ expect(registry.get("child-B")?.parentSessionId).toBe("parent-P");
131
+ });
113
132
  });
@@ -12,89 +12,99 @@ const REGISTRY_KEY = Symbol.for(
12
12
  function makeInfo(
13
13
  overrides: Partial<SubagentSessionInfo> = {},
14
14
  ): SubagentSessionInfo {
15
- return {
16
- agentName: "Explore",
17
- ...overrides,
18
- };
15
+ return { ...overrides };
19
16
  }
20
17
 
21
18
  describe("SubagentSessionRegistry", () => {
22
19
  test("has() returns false for an unregistered key", () => {
23
20
  const registry = new SubagentSessionRegistry();
24
- expect(registry.has("/sessions/task-abc")).toBe(false);
21
+ expect(registry.has("session-abc")).toBe(false);
25
22
  });
26
23
 
27
24
  test("get() returns undefined for an unregistered key", () => {
28
25
  const registry = new SubagentSessionRegistry();
29
- expect(registry.get("/sessions/task-abc")).toBeUndefined();
26
+ expect(registry.get("session-abc")).toBeUndefined();
30
27
  });
31
28
 
32
29
  test("has() returns true after register()", () => {
33
30
  const registry = new SubagentSessionRegistry();
34
- registry.register("/sessions/task-abc", makeInfo());
35
- expect(registry.has("/sessions/task-abc")).toBe(true);
31
+ registry.register("session-abc", makeInfo());
32
+ expect(registry.has("session-abc")).toBe(true);
36
33
  });
37
34
 
38
35
  test("get() returns the registered info after register()", () => {
39
36
  const registry = new SubagentSessionRegistry();
40
37
  const info = makeInfo({ parentSessionId: "parent-123" });
41
- registry.register("/sessions/task-abc", info);
42
- expect(registry.get("/sessions/task-abc")).toEqual(info);
38
+ registry.register("session-abc", info);
39
+ expect(registry.get("session-abc")).toEqual(info);
43
40
  });
44
41
 
45
- test("register() stores agentName without parentSessionId", () => {
42
+ test("register() stores entry without parentSessionId", () => {
46
43
  const registry = new SubagentSessionRegistry();
47
- registry.register("/sessions/task-abc", makeInfo());
48
- expect(registry.get("/sessions/task-abc")).toEqual({
49
- agentName: "Explore",
50
- });
44
+ registry.register("session-abc", makeInfo());
45
+ expect(registry.get("session-abc")).toEqual({});
51
46
  });
52
47
 
53
48
  test("has() returns false after unregister()", () => {
54
49
  const registry = new SubagentSessionRegistry();
55
- registry.register("/sessions/task-abc", makeInfo());
56
- registry.unregister("/sessions/task-abc");
57
- expect(registry.has("/sessions/task-abc")).toBe(false);
50
+ registry.register("session-abc", makeInfo());
51
+ registry.unregister("session-abc");
52
+ expect(registry.has("session-abc")).toBe(false);
58
53
  });
59
54
 
60
55
  test("get() returns undefined after unregister()", () => {
61
56
  const registry = new SubagentSessionRegistry();
62
- registry.register("/sessions/task-abc", makeInfo());
63
- registry.unregister("/sessions/task-abc");
64
- expect(registry.get("/sessions/task-abc")).toBeUndefined();
57
+ registry.register("session-abc", makeInfo());
58
+ registry.unregister("session-abc");
59
+ expect(registry.get("session-abc")).toBeUndefined();
65
60
  });
66
61
 
67
62
  test("unregister() is a no-op for an unknown key", () => {
68
63
  const registry = new SubagentSessionRegistry();
69
- expect(() => registry.unregister("/sessions/nonexistent")).not.toThrow();
64
+ expect(() => registry.unregister("session-nonexistent")).not.toThrow();
70
65
  });
71
66
 
72
67
  test("register() overwrites a previous entry for the same key", () => {
68
+ const registry = new SubagentSessionRegistry();
69
+ registry.register("session-abc", makeInfo({ parentSessionId: "parent-1" }));
70
+ registry.register("session-abc", makeInfo({ parentSessionId: "parent-2" }));
71
+ expect(registry.get("session-abc")?.parentSessionId).toBe("parent-2");
72
+ });
73
+
74
+ // ── #298 regression: concurrent siblings must be independent ──────────────
75
+
76
+ test("two sibling session ids are registered independently", () => {
73
77
  const registry = new SubagentSessionRegistry();
74
78
  registry.register(
75
- "/sessions/task-abc",
76
- makeInfo({ parentSessionId: "parent-1" }),
79
+ "child-session-A",
80
+ makeInfo({ parentSessionId: "parent-P" }),
77
81
  );
78
82
  registry.register(
79
- "/sessions/task-abc",
80
- makeInfo({ parentSessionId: "parent-2" }),
81
- );
82
- expect(registry.get("/sessions/task-abc")?.parentSessionId).toBe(
83
- "parent-2",
83
+ "child-session-B",
84
+ makeInfo({ parentSessionId: "parent-P" }),
84
85
  );
86
+
87
+ expect(registry.has("child-session-A")).toBe(true);
88
+ expect(registry.has("child-session-B")).toBe(true);
85
89
  });
86
90
 
87
- test("multiple keys are independent", () => {
91
+ test("disposing one sibling does not evict the other (collision regression)", () => {
88
92
  const registry = new SubagentSessionRegistry();
89
- registry.register("/sessions/task-1", makeInfo({ agentName: "Explore" }));
90
- registry.register("/sessions/task-2", makeInfo({ agentName: "Plan" }));
93
+ registry.register(
94
+ "child-session-A",
95
+ makeInfo({ parentSessionId: "parent-P" }),
96
+ );
97
+ registry.register(
98
+ "child-session-B",
99
+ makeInfo({ parentSessionId: "parent-P" }),
100
+ );
91
101
 
92
- expect(registry.get("/sessions/task-1")?.agentName).toBe("Explore");
93
- expect(registry.get("/sessions/task-2")?.agentName).toBe("Plan");
102
+ // Sibling A finishes — should not affect B.
103
+ registry.unregister("child-session-A");
94
104
 
95
- registry.unregister("/sessions/task-1");
96
- expect(registry.has("/sessions/task-1")).toBe(false);
97
- expect(registry.has("/sessions/task-2")).toBe(true);
105
+ expect(registry.has("child-session-A")).toBe(false);
106
+ expect(registry.has("child-session-B")).toBe(true);
107
+ expect(registry.get("child-session-B")?.parentSessionId).toBe("parent-P");
98
108
  });
99
109
  });
100
110
 
@@ -119,20 +129,17 @@ describe("getSubagentSessionRegistry (process-global accessor)", () => {
119
129
 
120
130
  test("state registered through one call is visible through another call", () => {
121
131
  const writer = getSubagentSessionRegistry();
122
- writer.register("/sessions/child-tasks", {
123
- agentName: "Explore",
132
+ writer.register("child-session-xyz", {
124
133
  parentSessionId: "parent-abc",
125
134
  });
126
135
 
127
136
  const reader = getSubagentSessionRegistry();
128
- expect(reader.has("/sessions/child-tasks")).toBe(true);
129
- expect(reader.get("/sessions/child-tasks")?.parentSessionId).toBe(
130
- "parent-abc",
131
- );
137
+ expect(reader.has("child-session-xyz")).toBe(true);
138
+ expect(reader.get("child-session-xyz")?.parentSessionId).toBe("parent-abc");
132
139
  });
133
140
 
134
141
  test("starts empty on first call", () => {
135
142
  const registry = getSubagentSessionRegistry();
136
- expect(registry.has("/sessions/any-key")).toBe(false);
143
+ expect(registry.has("any-session-id")).toBe(false);
137
144
  });
138
145
  });