chapterhouse 0.3.12 → 0.3.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +2 -69
  2. package/dist/api/server.js +15 -157
  3. package/dist/api/server.test.js +1 -1
  4. package/dist/api/turn-sse.integration.test.js +36 -0
  5. package/dist/cli.js +0 -30
  6. package/dist/config.js +0 -3
  7. package/dist/copilot/agent-event-bus.js +41 -0
  8. package/dist/copilot/agent-event-bus.test.js +23 -0
  9. package/dist/copilot/agents.js +4 -59
  10. package/dist/copilot/orchestrator.js +60 -65
  11. package/dist/copilot/orchestrator.test.js +73 -158
  12. package/dist/copilot/task-event-log.js +5 -5
  13. package/dist/copilot/task-event-log.test.js +68 -142
  14. package/dist/copilot/tools.js +9 -85
  15. package/dist/daemon.js +0 -22
  16. package/dist/store/db.js +2 -50
  17. package/dist/store/db.test.js +0 -45
  18. package/package.json +1 -3
  19. package/web/dist/assets/index-BlIWCM11.js +217 -0
  20. package/web/dist/assets/index-BlIWCM11.js.map +1 -0
  21. package/web/dist/assets/{index-BtAcw3EP.css → index-lvHFM_ut.css} +1 -1
  22. package/web/dist/index.html +2 -2
  23. package/dist/api/ralph.js +0 -153
  24. package/dist/api/ralph.test.js +0 -101
  25. package/dist/copilot/agents.squad.test.js +0 -72
  26. package/dist/copilot/hooks.js +0 -157
  27. package/dist/copilot/hooks.test.js +0 -315
  28. package/dist/copilot/squad-event-bus.js +0 -27
  29. package/dist/copilot/tools.squad.test.js +0 -168
  30. package/dist/squad/charter.js +0 -125
  31. package/dist/squad/charter.test.js +0 -89
  32. package/dist/squad/context.js +0 -48
  33. package/dist/squad/context.test.js +0 -59
  34. package/dist/squad/discovery.js +0 -268
  35. package/dist/squad/discovery.test.js +0 -154
  36. package/dist/squad/index.js +0 -9
  37. package/dist/squad/init-cli.js +0 -109
  38. package/dist/squad/init.js +0 -395
  39. package/dist/squad/init.test.js +0 -351
  40. package/dist/squad/mirror.js +0 -83
  41. package/dist/squad/mirror.scheduler.js +0 -80
  42. package/dist/squad/mirror.scheduler.test.js +0 -197
  43. package/dist/squad/mirror.test.js +0 -172
  44. package/dist/squad/registry.js +0 -162
  45. package/dist/squad/registry.test.js +0 -31
  46. package/dist/squad/squad-coordinator-system-message.test.js +0 -190
  47. package/dist/squad/squad-session-routing.test.js +0 -260
  48. package/dist/squad/types.js +0 -4
  49. package/dist/squad/worktree.js +0 -295
  50. package/dist/squad/worktree.test.js +0 -189
  51. package/dist/store/squad-sessions.test.js +0 -341
  52. package/web/dist/assets/index-BR2cks94.js +0 -219
  53. package/web/dist/assets/index-BR2cks94.js.map +0 -1
@@ -68,7 +68,6 @@ async function loadOrchestratorModule(t, overrides = {}) {
68
68
  config: {
69
69
  copilotModel: "missing-model",
70
70
  selfEditEnabled: true,
71
- squadEnabled: false,
72
71
  },
73
72
  routeResults: [],
74
73
  routerArgs: [],
@@ -85,8 +84,6 @@ async function loadOrchestratorModule(t, overrides = {}) {
85
84
  ensureDefaultAgentsCalls: 0,
86
85
  loadAgentsCalls: 0,
87
86
  setActiveAgentCalls: [],
88
- setChannelProjectCalls: [],
89
- channelProjects: new Map(),
90
87
  parseMentionArgs: [],
91
88
  buildAgentRosterArgs: [],
92
89
  clearActiveTasksCalls: 0,
@@ -142,22 +139,6 @@ async function loadOrchestratorModule(t, overrides = {}) {
142
139
  resetClient: async () => client,
143
140
  },
144
141
  });
145
- t.mock.module("../squad/context.js", {
146
- namedExports: {
147
- setChannelProject: (channelKey, projectRoot) => {
148
- state.setChannelProjectCalls.push({ channelKey, projectRoot });
149
- state.channelProjects.set(channelKey, projectRoot);
150
- },
151
- getChannelProject: (channelKey) => state.channelProjects.get(channelKey),
152
- normalizeProjectPath: (projectPath) => `normalized:${projectPath}`,
153
- },
154
- });
155
- t.mock.module("../squad/charter.js", {
156
- namedExports: {
157
- buildSquadSystemPrefix: async (_projectRoot) => "# Squad context\n",
158
- getSquadCoordinatorSystemMessage: async (_projectRoot) => "# Squad Coordinator\nYou are the Squad coordinator.\n",
159
- },
160
- });
161
142
  t.mock.module("../store/db.js", {
162
143
  namedExports: {
163
144
  logConversation: (role, content, source) => {
@@ -184,7 +165,6 @@ async function loadOrchestratorModule(t, overrides = {}) {
184
165
  }),
185
166
  transaction: (fn) => fn,
186
167
  }),
187
- bumpProjectLastUsed: (_projectRoot) => { },
188
168
  appendTaskEvent: (taskId, kind, toolName, summary) => {
189
169
  const seq = (state.taskEvents.get(taskId)?.length ?? 0) + 1;
190
170
  const ev = { id: seq, taskId, seq, ts: Date.now(), kind, toolName, summary };
@@ -281,7 +261,6 @@ test("sendToOrchestrator logs both sides, remembers web auth context, and record
281
261
  config: {
282
262
  copilotModel: "claude-sonnet-4.6",
283
263
  selfEditEnabled: true,
284
- squadEnabled: false,
285
264
  },
286
265
  routeResults: [
287
266
  {
@@ -337,7 +316,6 @@ test("@mentions route through the orchestrator session without invoking the mode
337
316
  config: {
338
317
  copilotModel: "claude-sonnet-4.6",
339
318
  selfEditEnabled: true,
340
- squadEnabled: false,
341
319
  },
342
320
  parseMentionResult: { agentSlug: "designer", message: "polish the landing page" },
343
321
  sendResult: "Delegated",
@@ -355,48 +333,11 @@ test("@mentions route through the orchestrator session without invoking the mode
355
333
  assert.deepEqual(state.routerArgs, []);
356
334
  assert.deepEqual(state.sessionPrompts, [{ prompt: "[via web] polish the landing page" }]);
357
335
  });
358
- test("web projectPath activates squad context, rebuilds the orchestrator roster, and routes mentions per connection", async (t) => {
359
- const { orchestrator, state, client } = await loadOrchestratorModule(t, {
360
- config: {
361
- copilotModel: "claude-sonnet-4.6",
362
- selfEditEnabled: true,
363
- squadEnabled: true,
364
- },
365
- parseMentionResult: { agentSlug: "ripley", message: "audit the squad routing" },
366
- sendResult: "Squad handled it",
367
- });
368
- await orchestrator.initOrchestrator(client);
369
- const final = await new Promise((resolve) => {
370
- orchestrator.sendToOrchestrator("@ripley audit the squad routing", { type: "web", connectionId: "conn-squad", projectPath: "~/workspace/mock-squad" }, (text, done) => {
371
- if (done) {
372
- resolve(text);
373
- }
374
- });
375
- });
376
- assert.equal(final, "Squad handled it");
377
- assert.deepEqual(state.setChannelProjectCalls, [{
378
- channelKey: "conn-squad",
379
- projectRoot: "normalized:~/workspace/mock-squad",
380
- }]);
381
- assert.deepEqual(state.parseMentionArgs.at(-1), {
382
- text: "@ripley audit the squad routing",
383
- projectRoot: "normalized:~/workspace/mock-squad",
384
- });
385
- assert.deepEqual(state.setActiveAgentCalls, [{ channelKey: "conn-squad", agentSlug: "ripley" }]);
386
- assert.deepEqual(state.sessionPrompts.at(-1), [{ prompt: "[via web] audit the squad routing" }][0]);
387
- // Project sessions use getSquadCoordinatorSystemMessage (not buildAgentRoster) — only the default
388
- // session init triggers a buildAgentRoster call.
389
- assert.deepEqual(state.buildAgentRosterArgs, [undefined]);
390
- assert.equal(state.createSessionCalls.length, 2);
391
- assert.equal(typeof orchestrator.getCurrentChannelKey, "function");
392
- assert.equal(orchestrator.getCurrentChannelKey(), undefined);
393
- });
394
336
  test("feedAgentResult injects a background completion turn and proactively notifies listeners", async (t) => {
395
337
  const { orchestrator, state, client } = await loadOrchestratorModule(t, {
396
338
  config: {
397
339
  copilotModel: "claude-sonnet-4.6",
398
340
  selfEditEnabled: true,
399
- squadEnabled: false,
400
341
  },
401
342
  sendResult: "Agent complete",
402
343
  });
@@ -427,7 +368,6 @@ test("cancelCurrentMessage aborts the active request and agent helpers expose ru
427
368
  config: {
428
369
  copilotModel: "claude-sonnet-4.6",
429
370
  selfEditEnabled: true,
430
- squadEnabled: false,
431
371
  },
432
372
  sendResult: "__PENDING__",
433
373
  });
@@ -449,85 +389,99 @@ test("cancelCurrentMessage aborts the active request and agent helpers expose ru
449
389
  await orchestrator.shutdownAgents();
450
390
  assert.equal(state.clearActiveTasksCalls, 1);
451
391
  });
452
- test("feedAgentResult routes to project session when task session_key is a project key", async (t) => {
392
+ // ---------------------------------------------------------------------------
393
+ // REGRESSION: #35 — per-session isolation
394
+ // This test would have caught the original bug. With a global shared queue,
395
+ // session B's message would queue behind session A's blocking turn and the
396
+ // Promise.race would time out. With per-session queues, session B completes
397
+ // independently.
398
+ // ---------------------------------------------------------------------------
399
+ test("regression #35: session A blocking does not delay session B (concurrent sessions)", async (t) => {
453
400
  const { orchestrator, state, client } = await loadOrchestratorModule(t, {
454
- config: {
455
- copilotModel: "claude-sonnet-4.6",
456
- selfEditEnabled: true,
457
- squadEnabled: false,
458
- },
459
- sendResult: "Routed correctly",
460
- taskSessionKeys: new Map([["proj-task-1", "project:/repo/squad"]]),
401
+ config: { copilotModel: "claude-sonnet-4.6", selfEditEnabled: false },
402
+ sendResult: "__PENDING__", // session A will block in sendAndWait
461
403
  });
462
404
  await orchestrator.initOrchestrator(client);
463
- // initOrchestrator creates the default sessionrecord that baseline
464
- const sessionsAfterInit = state.createSessionCalls.length;
465
- const notified = new Promise((resolve) => {
466
- orchestrator.setProactiveNotify(resolve);
405
+ // Send to session A (explicit sessionKey "chat:a") parks because sendResult is __PENDING__
406
+ const sessionACallbacks = [];
407
+ orchestrator.sendToOrchestrator("slow request to session A", { type: "background", sessionKey: "chat:a" }, (text, done) => sessionACallbacks.push({ text, done }));
408
+ // Yield to event loop so session A's drain loop runs and parks in sendAndWait
409
+ await new Promise((resolve) => setTimeout(resolve, 20));
410
+ // Now session A is blocked inside sendAndWait. Switch sendResult so session B
411
+ // gets a fast response. Session A's pending reject is still captured in its closure.
412
+ state.sendResult = "session B complete";
413
+ // Send to session B (different sessionKey) — must complete without waiting for session A
414
+ const sessionBDone = new Promise((resolve) => {
415
+ orchestrator.sendToOrchestrator("quick request to session B", { type: "background", sessionKey: "chat:b" }, (text, done) => {
416
+ if (done)
417
+ resolve(text);
418
+ });
467
419
  });
468
- // Task belongs to the project session; feedAgentResult must open that session
469
- orchestrator.feedAgentResult("proj-task-1", "coder", "Squad feature done");
470
- assert.equal(await notified, "Routed correctly");
471
- // A second createSession call proves the orchestrator opened a fresh project session
472
- // rather than reusing the already-open default session.
473
- assert.equal(state.createSessionCalls.length, sessionsAfterInit + 1, "feedAgentResult should spin up a project session, not recycle the default one");
474
- // The prompt must reference the task and agent
475
- const prompt = state.sessionPrompts.at(-1);
476
- assert.ok(prompt?.prompt.includes("proj-task-1"), "prompt should reference the task id");
477
- assert.ok(prompt?.prompt.includes("coder"), "prompt should reference the agent slug");
478
- assert.ok(prompt?.prompt.includes("Squad feature done"), "prompt should include the result text");
420
+ // Session B must respond before the deadline (session A is still blocked indefinitely)
421
+ const result = await Promise.race([
422
+ sessionBDone,
423
+ new Promise((_, reject) => setTimeout(() => reject(new Error("session B was blocked by session A — global queue bug reproduced")), 300)),
424
+ ]);
425
+ assert.equal(result, "session B complete", "session B must resolve independently of blocked session A");
426
+ assert.equal(sessionACallbacks.length, 0, "session A must still be pending (no response yet)");
427
+ // Clean up: reject session A's pending promise so the test can finish
428
+ state.pendingReject?.(new Error("test teardown"));
429
+ await new Promise((resolve) => setTimeout(resolve, 10));
479
430
  });
480
431
  // ---------------------------------------------------------------------------
481
- // Sprint 3, Item 5 orchestrator lifecycle: project-session cancel/shutdown
432
+ // Orchestrator lifecycle: shutdown clears all sessions
482
433
  // ---------------------------------------------------------------------------
483
- test("cancelCurrentMessage targets the active project session, not the default session", async (t) => {
484
- const { orchestrator, state, client } = await loadOrchestratorModule(t, {
485
- config: {
486
- copilotModel: "claude-sonnet-4.6",
487
- selfEditEnabled: false,
488
- // squadEnabled true so sendToOrchestrator derives a "project:…" session key
489
- squadEnabled: true,
490
- },
491
- sendResult: "__PENDING__",
492
- });
493
- await orchestrator.initOrchestrator(client);
494
- // Default session was created during init
495
- const sessionsAfterInit = state.createSessionCalls.length;
496
- // Send to a project path — this should open a separate project session
497
- orchestrator.sendToOrchestrator("scaffold the new API", { type: "web", connectionId: "conn-project", projectPath: "/repo/squad-proj" }, () => { });
498
- // Yield to the event loop so the project session is created and the
499
- // sendAndWait promise is parked in PENDING state.
500
- await new Promise((resolve) => setTimeout(resolve, 0));
501
- assert.equal(state.createSessionCalls.length, sessionsAfterInit + 1, "a distinct project session must be created, separate from the default session");
502
- // Cancel should abort the active project session, not the default one
503
- const cancelled = await orchestrator.cancelCurrentMessage();
504
- assert.equal(cancelled, true, "cancelCurrentMessage should return true when an active request was aborted");
505
- assert.equal(state.abortCalls, 1, "exactly one abort — on the in-flight project session");
506
- });
507
434
  test("shutdownAgents disconnects all sessions, clears maps, and clears active tasks", async (t) => {
508
435
  const { orchestrator, state, client } = await loadOrchestratorModule(t, {
509
- config: { copilotModel: "claude-sonnet-4.6", selfEditEnabled: false, squadEnabled: true },
436
+ config: { copilotModel: "claude-sonnet-4.6", selfEditEnabled: false },
510
437
  sendResult: "ok",
511
438
  });
512
439
  // Init default session
513
440
  await orchestrator.initOrchestrator(client);
514
441
  const sessionsAfterInit = state.createSessionCalls.length;
515
- // Trigger a project session by sending a message with a projectPath
516
- orchestrator.sendToOrchestrator("hello from project", { type: "web", connectionId: "conn-1", projectPath: "/fake/project" }, (_text, _done) => { });
442
+ // Trigger a second session by sending a background message with a distinct sessionKey
443
+ orchestrator.sendToOrchestrator("hello from session chat:1", { type: "background", sessionKey: "chat:1" }, (_text, _done) => { });
517
444
  // Yield so the async session-creation IIFE runs
518
445
  await new Promise((resolve) => setTimeout(resolve, 10));
519
- assert.ok(state.createSessionCalls.length > sessionsAfterInit, "a project session should have been created");
446
+ assert.ok(state.createSessionCalls.length > sessionsAfterInit, "a second session should have been created");
520
447
  // Shutdown — both sessions should be disconnected
521
448
  await orchestrator.shutdownAgents();
522
- assert.equal(state.disconnectCalls, 2, "disconnect must be called once per session (default + project)");
449
+ assert.equal(state.disconnectCalls, 2, "disconnect must be called once per session (default + chat:1)");
523
450
  assert.equal(state.clearActiveTasksCalls, 1, "clearActiveTasks must be called exactly once");
524
451
  // Re-init after shutdown must create a fresh session (proves sessionMap was cleared)
525
452
  await orchestrator.initOrchestrator(client);
526
453
  assert.ok(state.createSessionCalls.length > sessionsAfterInit + 1, "re-init after shutdown must create a new session (not reuse stale sessionMap entry)");
527
454
  });
455
+ // ---------------------------------------------------------------------------
456
+ // feedAgentResult routes to the correct non-default session
457
+ // ---------------------------------------------------------------------------
458
+ test("feedAgentResult routes to a non-default session when the task's session_key is non-default", async (t) => {
459
+ const { orchestrator, state, client } = await loadOrchestratorModule(t, {
460
+ config: { copilotModel: "claude-sonnet-4.6", selfEditEnabled: false },
461
+ sendResult: "Routed correctly",
462
+ taskSessionKeys: new Map([["chat-task-1", "chat:abc"]]),
463
+ });
464
+ await orchestrator.initOrchestrator(client);
465
+ // initOrchestrator creates the default session — record that baseline
466
+ const sessionsAfterInit = state.createSessionCalls.length;
467
+ const notified = new Promise((resolve) => {
468
+ orchestrator.setProactiveNotify(resolve);
469
+ });
470
+ // Task belongs to "chat:abc" session; feedAgentResult must open that session
471
+ orchestrator.feedAgentResult("chat-task-1", "coder", "Feature done");
472
+ assert.equal(await notified, "Routed correctly");
473
+ // A second createSession call proves the orchestrator opened a fresh non-default session
474
+ // rather than reusing the already-open default session.
475
+ assert.equal(state.createSessionCalls.length, sessionsAfterInit + 1, "feedAgentResult should spin up a non-default session, not recycle the default one");
476
+ // The prompt must reference the task and agent
477
+ const prompt = state.sessionPrompts.at(-1);
478
+ assert.ok(prompt?.prompt.includes("chat-task-1"), "prompt should reference the task id");
479
+ assert.ok(prompt?.prompt.includes("coder"), "prompt should reference the agent slug");
480
+ assert.ok(prompt?.prompt.includes("Feature done"), "prompt should include the result text");
481
+ });
528
482
  test("ensureOrchestratorSession cleans up in-flight promise on session creation failure", async (t) => {
529
483
  const { orchestrator, state, client } = await loadOrchestratorModule(t, {
530
- config: { copilotModel: "claude-sonnet-4.6", selfEditEnabled: false, squadEnabled: false },
484
+ config: { copilotModel: "claude-sonnet-4.6", selfEditEnabled: false },
531
485
  // Non-recoverable error so the retry loop exits immediately
532
486
  createSessionError: "fatal: SDK host permanently unavailable",
533
487
  sendResult: "unreachable",
@@ -552,13 +506,13 @@ test("ensureOrchestratorSession cleans up in-flight promise on session creation
552
506
  assert.ok(state.createSessionCalls.length > countAfterFirst, "a second message must trigger a new createSession attempt, proving sessionCreatePromises was cleaned up");
553
507
  });
554
508
  // ---------------------------------------------------------------------------
555
- // S5-01: SDK subagent events persist to agent_tasks (squad dispatch tracking)
509
+ // S5-01: SDK subagent events persist to agent_tasks (agent dispatch tracking)
556
510
  // Root cause: executeOnSession subscribed to subagent.* events for the UI
557
511
  // activity feed but never wrote rows to agent_tasks. Workers tab only reads
558
- // agent_tasks, so squad coordinator dispatches were invisible.
512
+ // agent_tasks, so agent dispatches were invisible.
559
513
  // Fix: unconditional DB subscriptions in executeOnSession write/update rows.
560
514
  // ---------------------------------------------------------------------------
561
- test("S5-01: subagent.started event inserts a squad row into agent_tasks", async (t) => {
515
+ test("S5-01: subagent.started event inserts an adhoc row into agent_tasks", async (t) => {
562
516
  const { orchestrator, state, client } = await loadOrchestratorModule(t, {
563
517
  sendResult: "__PENDING__",
564
518
  });
@@ -571,7 +525,7 @@ test("S5-01: subagent.started event inserts a squad row into agent_tasks", async
571
525
  // Yield so the async IIFE reaches sendAndWait (which is pending)
572
526
  await new Promise((resolve) => setTimeout(resolve, 10));
573
527
  assert.ok(state.lastSession, "FakeSession should have been created");
574
- // Fire a subagent.started event — simulates the SDK's task tool starting a squad dispatch
528
+ // Fire a subagent.started event — simulates the SDK's task tool starting an agent dispatch
575
529
  state.lastSession.emit("subagent.started", {
576
530
  toolCallId: "subagent-call-001",
577
531
  agentName: "Kaylee",
@@ -581,7 +535,7 @@ test("S5-01: subagent.started event inserts a squad row into agent_tasks", async
581
535
  // The handler is synchronous — DB write should be in state.dbWrites immediately
582
536
  const insertWrite = state.dbWrites.find((w) => w.sql.includes("INSERT") && w.sql.includes("agent_tasks"));
583
537
  assert.ok(insertWrite, "subagent.started must INSERT a row into agent_tasks");
584
- assert.ok((insertWrite.sql + JSON.stringify(insertWrite.args)).includes("squad"), "inserted row must carry source='squad'");
538
+ assert.ok((insertWrite.sql + JSON.stringify(insertWrite.args)).includes("adhoc"), "inserted row must carry source='adhoc'");
585
539
  assert.ok(JSON.stringify(insertWrite.args).includes("subagent-call-001"), "task_id must equal the toolCallId from the event");
586
540
  // Resolve the pending sendAndWait so the test can clean up
587
541
  state.pendingReject?.(new Error("test teardown"));
@@ -637,45 +591,6 @@ test("S5-01: subagent.failed event updates agent_tasks status to error", async (
637
591
  state.pendingReject?.(new Error("test teardown"));
638
592
  });
639
593
  // ---------------------------------------------------------------------------
640
- // REGRESSION: #35 — per-session isolation
641
- // This test would have caught the original bug. With a global shared queue,
642
- // session B's message would queue behind session A's blocking turn and the
643
- // Promise.race would time out. With per-session queues, session B completes
644
- // independently.
645
- // ---------------------------------------------------------------------------
646
- test("regression #35: session A blocking does not delay session B (concurrent sessions)", async (t) => {
647
- const { orchestrator, state, client } = await loadOrchestratorModule(t, {
648
- config: { copilotModel: "claude-sonnet-4.6", selfEditEnabled: false, squadEnabled: true },
649
- sendResult: "__PENDING__", // session A will block in sendAndWait
650
- });
651
- await orchestrator.initOrchestrator(client);
652
- // Send to session A (project /project/a) — parks because sendResult is __PENDING__
653
- const sessionACallbacks = [];
654
- orchestrator.sendToOrchestrator("slow request to project A", { type: "web", connectionId: "conn-a", projectPath: "/project/a" }, (text, done) => sessionACallbacks.push({ text, done }));
655
- // Yield to event loop so session A's drain loop runs and parks in sendAndWait
656
- await new Promise((resolve) => setTimeout(resolve, 20));
657
- // Now session A is blocked inside sendAndWait. Switch sendResult so session B
658
- // gets a fast response. session A's pending reject is still captured in its closure.
659
- state.sendResult = "session B complete";
660
- // Send to session B (different project) — must complete without waiting for session A
661
- const sessionBDone = new Promise((resolve) => {
662
- orchestrator.sendToOrchestrator("quick request to project B", { type: "web", connectionId: "conn-b", projectPath: "/project/b" }, (text, done) => {
663
- if (done)
664
- resolve(text);
665
- });
666
- });
667
- // Session B must respond before the deadline (session A is still blocked indefinitely)
668
- const result = await Promise.race([
669
- sessionBDone,
670
- new Promise((_, reject) => setTimeout(() => reject(new Error("session B was blocked by session A — global queue bug reproduced")), 300)),
671
- ]);
672
- assert.equal(result, "session B complete", "session B must resolve independently of blocked session A");
673
- assert.equal(sessionACallbacks.length, 0, "session A must still be pending (no response yet)");
674
- // Clean up: reject session A's pending promise so the test can finish
675
- state.pendingReject?.(new Error("test teardown"));
676
- await new Promise((resolve) => setTimeout(resolve, 10));
677
- });
678
- // ---------------------------------------------------------------------------
679
594
  // #81 — task spawn args (name/description) must win over SDK agent_type fields
680
595
  // Root cause: subagent.started only carries agent_type boilerplate. The actual
681
596
  // spawn params (name, description) arrive earlier via tool.execution_start for
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Maintains a `Map<taskId, RingBuffer<TaskEvent>>` in the daemon process,
5
5
  * capped at 500 events per task. Subscribes to the process-wide
6
- * `squadEventBus` for `session:tool_call` events so the ring buffer stays
6
+ * `agentEventBus` for `session:tool_call` events so the ring buffer stays
7
7
  * in sync without the caller needing to wire anything extra.
8
8
  *
9
9
  * Consumers:
@@ -21,7 +21,7 @@
21
21
  *
22
22
  * @module copilot/task-event-log
23
23
  */
24
- import { squadEventBus } from "./squad-event-bus.js";
24
+ import { agentEventBus } from "./agent-event-bus.js";
25
25
  import { RingBuffer } from "./ring-buffer.js";
26
26
  // ---------------------------------------------------------------------------
27
27
  // Ring buffer — re-export so existing imports stay compatible
@@ -59,11 +59,11 @@ function notifyListeners(taskId, event) {
59
59
  // Public API
60
60
  // ---------------------------------------------------------------------------
61
61
  /**
62
- * Subscribe to the squadEventBus and maintain the ring buffer.
62
+ * Subscribe to the agent event bus and maintain the ring buffer.
63
63
  * Call once from initOrchestrator(). Returns an unsub / cleanup function.
64
64
  */
65
65
  export function initTaskEventLog() {
66
- const unsubToolCall = squadEventBus.subscribe("session:tool_call", (event) => {
66
+ const unsubToolCall = agentEventBus.subscribe("session:tool_call", (event) => {
67
67
  const taskId = event.sessionId;
68
68
  if (!taskId)
69
69
  return;
@@ -81,7 +81,7 @@ export function initTaskEventLog() {
81
81
  buf.push(taskEvent);
82
82
  notifyListeners(taskId, taskEvent);
83
83
  });
84
- const unsubDestroyed = squadEventBus.subscribe("session:destroyed", (event) => {
84
+ const unsubDestroyed = agentEventBus.subscribe("session:destroyed", (event) => {
85
85
  if (event.sessionId)
86
86
  clearTaskLog(event.sessionId);
87
87
  });