macro-agent 0.0.16 → 0.1.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.
Files changed (45) hide show
  1. package/dist/acp/macro-agent.d.ts +2 -0
  2. package/dist/acp/macro-agent.d.ts.map +1 -1
  3. package/dist/acp/macro-agent.js +52 -20
  4. package/dist/acp/macro-agent.js.map +1 -1
  5. package/dist/agent/agent-manager.d.ts.map +1 -1
  6. package/dist/agent/agent-manager.js +23 -5
  7. package/dist/agent/agent-manager.js.map +1 -1
  8. package/dist/map/adapter/acp-over-map.d.ts +16 -0
  9. package/dist/map/adapter/acp-over-map.d.ts.map +1 -1
  10. package/dist/map/adapter/acp-over-map.js +242 -24
  11. package/dist/map/adapter/acp-over-map.js.map +1 -1
  12. package/dist/map/adapter/map-adapter.d.ts +1 -0
  13. package/dist/map/adapter/map-adapter.d.ts.map +1 -1
  14. package/dist/map/adapter/map-adapter.js +57 -8
  15. package/dist/map/adapter/map-adapter.js.map +1 -1
  16. package/dist/store/event-store.d.ts +5 -0
  17. package/dist/store/event-store.d.ts.map +1 -1
  18. package/dist/store/event-store.js +126 -53
  19. package/dist/store/event-store.js.map +1 -1
  20. package/dist/store/instance.d.ts +0 -2
  21. package/dist/store/instance.d.ts.map +1 -1
  22. package/dist/store/instance.js +1 -24
  23. package/dist/store/instance.js.map +1 -1
  24. package/dist/store/types/agents.d.ts +5 -0
  25. package/dist/store/types/agents.d.ts.map +1 -1
  26. package/package.json +3 -3
  27. package/references/acp-factory-ref/package-lock.json +2 -2
  28. package/references/acp-factory-ref/package.json +2 -2
  29. package/references/claude-code-acp/package-lock.json +2 -2
  30. package/references/claude-code-acp/package.json +1 -1
  31. package/references/claude-code-acp/src/acp-agent.ts +3 -6
  32. package/src/acp/__tests__/history.test.ts +8 -4
  33. package/src/acp/macro-agent.ts +60 -26
  34. package/src/agent/__tests__/agent-manager.test.ts +4 -6
  35. package/src/agent/agent-manager.ts +24 -5
  36. package/src/map/adapter/__tests__/acp-over-map-cancel.test.ts +802 -0
  37. package/src/map/adapter/__tests__/acp-over-map-history.test.ts +1123 -0
  38. package/src/map/adapter/__tests__/acp-over-map-persistence.e2e.test.ts +440 -0
  39. package/src/map/adapter/acp-over-map.ts +282 -25
  40. package/src/map/adapter/map-adapter.ts +79 -9
  41. package/src/store/__tests__/event-store.test.ts +44 -12
  42. package/src/store/__tests__/instance.test.ts +5 -7
  43. package/src/store/event-store.ts +157 -57
  44. package/src/store/instance.ts +1 -29
  45. package/src/store/types/agents.ts +1 -0
@@ -15,6 +15,22 @@ import type { AgentId } from "../../store/types/index.js";
15
15
  import { SessionMapper } from "../../acp/session-mapper.js";
16
16
  import type { ACPSessionId } from "../../acp/types.js";
17
17
 
18
+ // ─────────────────────────────────────────────────────────────────
19
+ // Helpers
20
+ // ─────────────────────────────────────────────────────────────────
21
+
22
+ /** Extract a plain-text output string from `rawOutput` (string | ContentBlock[] | undefined). */
23
+ function extractToolOutput(rawOutput: unknown): string | undefined {
24
+ if (typeof rawOutput === "string") return rawOutput;
25
+ if (Array.isArray(rawOutput)) {
26
+ return rawOutput
27
+ .filter((item: any) => item.type === "text" && typeof item.text === "string")
28
+ .map((item: any) => item.text as string)
29
+ .join("\n") || undefined;
30
+ }
31
+ return undefined;
32
+ }
33
+
18
34
  // ─────────────────────────────────────────────────────────────────
19
35
  // Types
20
36
  // ─────────────────────────────────────────────────────────────────
@@ -79,6 +95,20 @@ export class ACPOverMAPHandler {
79
95
  this.defaultCwd = config.defaultCwd ?? process.cwd();
80
96
  }
81
97
 
98
+ /**
99
+ * Abort all active streams targeting a specific agent.
100
+ * Called when an agent is stopped via MAP protocol to interrupt
101
+ * any in-progress ACP prompt streaming.
102
+ */
103
+ abortStreamsForAgent(agentId: AgentId): void {
104
+ for (const [streamId, streamState] of this.streams) {
105
+ if (streamState.agentId === agentId) {
106
+ console.error(`[ACP-over-MAP] Aborting stream ${streamId} for stopped agent ${agentId}`);
107
+ streamState.abortController.abort();
108
+ }
109
+ }
110
+ }
111
+
82
112
  /**
83
113
  * Process an ACP request and return the response.
84
114
  * @param targetAgentId - Target agent for the request
@@ -277,7 +307,7 @@ export class ACPOverMAPHandler {
277
307
  streamState.agentId = existing.id;
278
308
  this.sessionMapper.createMapping(sessionId as ACPSessionId, existing.id);
279
309
  this.emitSessionInfo(streamState, sessionId, emitNotification);
280
- return {};
310
+ return { sessionId };
281
311
  }
282
312
 
283
313
  // Agent exists but no active session - resume it
@@ -287,7 +317,7 @@ export class ACPOverMAPHandler {
287
317
  streamState.agentId = spawned.id;
288
318
  this.sessionMapper.createMapping(sessionId as ACPSessionId, spawned.id);
289
319
  this.emitSessionInfo(streamState, sessionId, emitNotification);
290
- return {};
320
+ return { sessionId };
291
321
  }
292
322
 
293
323
  // No existing agent found - create new with the specified session ID
@@ -302,7 +332,7 @@ export class ACPOverMAPHandler {
302
332
  this.sessionMapper.createMapping(sessionId as ACPSessionId, spawned.id);
303
333
  this.emitSessionInfo(streamState, sessionId, emitNotification);
304
334
 
305
- return {};
335
+ return { sessionId };
306
336
  }
307
337
 
308
338
  private async handleAuthenticate(_params: unknown): Promise<unknown> {
@@ -322,7 +352,9 @@ export class ACPOverMAPHandler {
322
352
  messages?: Array<{ role: string; content: string }>;
323
353
  }) ?? {};
324
354
 
325
- const sessionId = paramSessionId ?? sessionIdFromContext ?? streamState.sessionId;
355
+ // Prefer the server's resolved session ID (set during loadSession) over the
356
+ // client's acpContext.sessionId which may be stale (e.g., "_resolve_" sentinel)
357
+ const sessionId = streamState.sessionId ?? paramSessionId ?? sessionIdFromContext;
326
358
  if (!sessionId) {
327
359
  throw new Error("No session - call newSession or loadSession first");
328
360
  }
@@ -380,6 +412,21 @@ export class ACPOverMAPHandler {
380
412
  emitNotification(notification);
381
413
  };
382
414
 
415
+ // Ensure conversation exists in EventStore for history persistence
416
+ this.ensureConversation(sessionId as ACPSessionId, agentId);
417
+
418
+ // Accumulate response content for history recording
419
+ const buffer: { parts: Array<{ type: "text"; text: string } | ({ type: "tool" } & Record<string, unknown>)> } = {
420
+ parts: [],
421
+ };
422
+
423
+ // Track latest plan for persistence
424
+ let latestPlan: Array<{ content: string; priority: string; status: string }> | null = null;
425
+
426
+ // Track tool info from initial tool_call events (title, name, input)
427
+ // so we can merge them when tool_call_update arrives with status "completed"
428
+ const toolInfoCache = new Map<string, { title?: string; name?: string; input?: unknown }>();
429
+
383
430
  try {
384
431
  // Stream responses from the agent
385
432
  let updateCount = 0;
@@ -389,17 +436,76 @@ export class ACPOverMAPHandler {
389
436
  return { stopReason: "cancelled" };
390
437
  }
391
438
 
439
+ // Accumulate content for history persistence (preserving text/tool interleaving order)
440
+ const u = update as Record<string, unknown>;
441
+ const updateType = u.sessionUpdate as string ?? u.type as string;
442
+ if (updateType === "agent_message_chunk") {
443
+ const content = u.content as { type?: string; text?: string } | undefined;
444
+ if (content?.text) {
445
+ const last = buffer.parts[buffer.parts.length - 1];
446
+ if (last && last.type === "text") {
447
+ last.text += content.text;
448
+ } else {
449
+ buffer.parts.push({ type: "text", text: content.text });
450
+ }
451
+ }
452
+ } else if (updateType === "plan") {
453
+ const entries = (u as { entries?: Array<{ content: string; priority: string; status: string }> }).entries;
454
+ if (entries) {
455
+ latestPlan = entries;
456
+ }
457
+ } else if (updateType === "tool_call" || updateType === "tool_call_update") {
458
+ const toolCallId = u.toolCallId as string | undefined;
459
+ const status = u.status as string | undefined;
460
+ const meta = u._meta as { claudeCode?: { toolName?: string } } | undefined;
461
+
462
+ // Cache tool info from initial tool_call events
463
+ if (updateType === "tool_call" && toolCallId) {
464
+ toolInfoCache.set(toolCallId, {
465
+ title: u.title as string | undefined,
466
+ name: meta?.claudeCode?.toolName,
467
+ input: u.rawInput,
468
+ });
469
+ }
470
+
471
+ if (status === "completed" || status === "failed") {
472
+ // Merge cached info for tool_call_update events that lack title/input
473
+ const cached = toolCallId ? toolInfoCache.get(toolCallId) : undefined;
474
+ buffer.parts.push({
475
+ type: "tool",
476
+ toolCallId,
477
+ title: u.title ?? cached?.title,
478
+ name: meta?.claudeCode?.toolName ?? cached?.name,
479
+ status: u.status,
480
+ input: u.rawInput ?? cached?.input,
481
+ output: extractToolOutput(u.rawOutput),
482
+ });
483
+ }
484
+ }
485
+
392
486
  // Stream the update back to the client
393
487
  emitSessionUpdate(update);
394
488
  updateCount++;
395
489
  }
396
490
 
397
- console.error(`[ACP-over-MAP] Prompt completed for agent ${agentId}, ${updateCount} updates`);
491
+ // Check if the loop ended because of cancellation
492
+ const wasCancelled = streamState.abortController.signal.aborted;
493
+ const stopReason = wasCancelled ? "cancelled" : "end_turn";
494
+
495
+ console.error(`[ACP-over-MAP] Prompt completed for agent ${agentId}, ${updateCount} updates, stopReason=${stopReason}`);
496
+
497
+ // Persist conversation turns for history
498
+ this.recordPromptTurns(sessionId as ACPSessionId, agentId, messageContent, buffer);
499
+
500
+ // Persist latest plan in EventStore for history loading across restarts
501
+ if (latestPlan) {
502
+ this.eventStore.updateAgentPlan(agentId as AgentId, latestPlan);
503
+ }
398
504
 
399
505
  // Emit updated session info after prompt completes
400
506
  this.emitSessionInfo(streamState, sessionId, emitNotification);
401
507
 
402
- return { stopReason: "end_turn" };
508
+ return { stopReason };
403
509
  } catch (error) {
404
510
  console.error(`[ACP-over-MAP] Prompt error:`, error);
405
511
  return {
@@ -417,31 +523,36 @@ export class ACPOverMAPHandler {
417
523
  sessionIdFromContext?: string,
418
524
  ): Promise<unknown> {
419
525
  const { sessionId: paramSessionId } = (params as { sessionId?: string }) ?? {};
420
- const sessionId = paramSessionId ?? sessionIdFromContext ?? streamState.sessionId;
526
+ // Prefer server's resolved session ID over client's potentially stale one
527
+ const sessionId = streamState.sessionId ?? paramSessionId ?? sessionIdFromContext;
421
528
 
422
- // Signal cancellation
423
- streamState.abortController.abort();
529
+ const agentId = sessionId
530
+ ? this.sessionMapper.getAgentId(sessionId as ACPSessionId)
531
+ : streamState.agentId;
424
532
 
425
- if (!sessionId) {
426
- return { cancelled: true };
427
- }
533
+ console.error(
534
+ `[ACP-over-MAP] Cancel - streamId=${streamState.streamId} sessionId=${sessionId} agentId=${agentId}`,
535
+ );
428
536
 
429
- // Get the mapped agent
430
- const agentId = this.sessionMapper.getAgentId(sessionId as ACPSessionId);
431
- if (!agentId) {
432
- return { cancelled: true };
433
- }
537
+ // 1. Abort the for-await loop in handlePrompt so it stops yielding updates
538
+ streamState.abortController.abort();
434
539
 
435
- // Terminate the agent
436
- try {
437
- await this.agentManager.terminate(agentId, "cancelled");
438
- } catch (error) {
439
- console.warn(`[ACP-over-MAP] Error terminating agent ${agentId}:`, error);
540
+ // 2. Cancel the agent's active session (sends session/cancel to the subprocess)
541
+ // This does NOT terminate the agent — it only interrupts the current prompt.
542
+ // The subprocess stays alive and can accept new prompts.
543
+ // Use map/agents/stop for full agent termination.
544
+ if (agentId) {
545
+ const session = this.agentManager.getSession(agentId);
546
+ if (session) {
547
+ try {
548
+ await session.cancel();
549
+ console.error(`[ACP-over-MAP] Session cancelled for agent ${agentId}`);
550
+ } catch (error) {
551
+ console.warn(`[ACP-over-MAP] session.cancel() failed for ${agentId}:`, error);
552
+ }
553
+ }
440
554
  }
441
555
 
442
- // Clean up
443
- this.sessionMapper.removeMapping(sessionId as ACPSessionId);
444
-
445
556
  return { cancelled: true };
446
557
  }
447
558
 
@@ -538,11 +649,157 @@ export class ACPOverMAPHandler {
538
649
  };
539
650
  }
540
651
 
652
+ case "_macro/getHistory": {
653
+ const { sessionId, agentId: historyAgentId, limit } = methodParams as {
654
+ sessionId?: string;
655
+ agentId?: string;
656
+ limit?: number;
657
+ };
658
+
659
+ // Resolve conversationId: prefer agentId lookup (resolves to the
660
+ // original session_id where turns were recorded), fall back to
661
+ // explicit sessionId. This allows history to survive across server
662
+ // restarts even when the ACP session ID changes (e.g., resume()
663
+ // fails → TUI creates new session with different ID).
664
+ let conversationId: string | undefined;
665
+ if (historyAgentId) {
666
+ const agent = this.eventStore.getAgent(historyAgentId as AgentId);
667
+ if (agent) {
668
+ conversationId = agent.session_id;
669
+ }
670
+ }
671
+ if (!conversationId) {
672
+ conversationId = sessionId;
673
+ }
674
+ if (!conversationId) {
675
+ return { turns: [] };
676
+ }
677
+
678
+ const turns = this.eventStore.listTurns({
679
+ conversationId,
680
+ order: "asc",
681
+ limit: limit ?? 200,
682
+ });
683
+
684
+ // Include persisted plan and agent cwd if available
685
+ const agent = historyAgentId ? this.eventStore.getAgent(historyAgentId as AgentId) : undefined;
686
+ const plan = agent?.plan ?? [];
687
+
688
+ return {
689
+ turns: turns.map((turn) => ({
690
+ role:
691
+ turn.contentType === "user_prompt"
692
+ ? ("user" as const)
693
+ : ("assistant" as const),
694
+ timestamp: turn.timestamp,
695
+ content: turn.content,
696
+ })),
697
+ plan,
698
+ cwd: agent?.cwd ?? null,
699
+ };
700
+ }
701
+
541
702
  default:
542
703
  throw new Error(`Unknown extension method: ${method}`);
543
704
  }
544
705
  }
545
706
 
707
+ // ─────────────────────────────────────────────────────────────────
708
+ // History Persistence
709
+ // ─────────────────────────────────────────────────────────────────
710
+
711
+ /**
712
+ * Ensure a conversation exists in the EventStore for a given session.
713
+ * This must be called before recording turns.
714
+ */
715
+ private ensureConversation(
716
+ acpSessionId: ACPSessionId,
717
+ agentId: AgentId,
718
+ ): void {
719
+ if (typeof this.eventStore.getConversation !== "function") {
720
+ return;
721
+ }
722
+
723
+ const existing = this.eventStore.getConversation(acpSessionId);
724
+ if (existing) return;
725
+
726
+ try {
727
+ this.eventStore.emit({
728
+ type: "conversation",
729
+ source: { agent_id: agentId },
730
+ payload: {
731
+ action: "created",
732
+ conversation_id: acpSessionId,
733
+ conversation_type: "session",
734
+ subject: `ACP session ${acpSessionId}`,
735
+ },
736
+ });
737
+ } catch (error) {
738
+ console.warn(
739
+ `[ACP-over-MAP] Failed to create conversation for session ${acpSessionId}:`,
740
+ error instanceof Error ? error.message : String(error),
741
+ );
742
+ }
743
+ }
744
+
745
+ /**
746
+ * Record user and assistant turns after a prompt completes.
747
+ * Mirrors MacroAgent.recordPromptTurns() for the ACP-over-MAP path.
748
+ */
749
+ private recordPromptTurns(
750
+ acpSessionId: ACPSessionId,
751
+ agentId: AgentId,
752
+ userMessage: string,
753
+ buffer: { parts: Array<{ type: "text"; text: string } | ({ type: "tool" } & Record<string, unknown>)> },
754
+ ): void {
755
+ const now = Date.now();
756
+
757
+ try {
758
+ // Record user turn
759
+ if (userMessage) {
760
+ this.eventStore.emit({
761
+ type: "turn",
762
+ source: { agent_id: agentId },
763
+ payload: {
764
+ action: "recorded",
765
+ turn_id: `turn_user_${now}_${Math.random().toString(36).slice(2, 8)}`,
766
+ conversation_id: acpSessionId,
767
+ participant: "user",
768
+ timestamp: now,
769
+ content_type: "user_prompt",
770
+ content: userMessage,
771
+ source_type: "explicit",
772
+ },
773
+ });
774
+ }
775
+
776
+ // Record assistant turn with accumulated content (parts already in order)
777
+ const parts = buffer.parts;
778
+
779
+ if (parts.length > 0) {
780
+ this.eventStore.emit({
781
+ type: "turn",
782
+ source: { agent_id: agentId },
783
+ payload: {
784
+ action: "recorded",
785
+ turn_id: `turn_asst_${now}_${Math.random().toString(36).slice(2, 8)}`,
786
+ conversation_id: acpSessionId,
787
+ participant: agentId,
788
+ timestamp: now + 1,
789
+ content_type: "assistant_response",
790
+ content: { parts },
791
+ source_type: "explicit",
792
+ },
793
+ });
794
+ }
795
+ } catch (error) {
796
+ console.warn(
797
+ `[ACP-over-MAP] Failed to record turns for session ${acpSessionId}:`,
798
+ error instanceof Error ? error.message : String(error),
799
+ );
800
+ }
801
+ }
802
+
546
803
  // ─────────────────────────────────────────────────────────────────
547
804
  // Session Info
548
805
  // ─────────────────────────────────────────────────────────────────
@@ -65,6 +65,7 @@ import {
65
65
  type ScopeId,
66
66
  } from "../types.js";
67
67
  import type { AgentId } from "../../store/types/index.js";
68
+ import type { AgentStopReason } from "../../agent/types.js";
68
69
  import {
69
70
  createConnectionManager,
70
71
  type ConnectionManager,
@@ -712,6 +713,10 @@ export class MAPAdapterImpl implements MAPAdapter {
712
713
  "map/agents/get": async (params) =>
713
714
  this.handleGetAgent(participantId, params),
714
715
 
716
+ // Agent lifecycle
717
+ "map/agents/stop": async (params) =>
718
+ this.handleStopAgent(participantId, params),
719
+
715
720
  // Scope queries
716
721
  "map/scopes/list": async () => this.handleListScopes(participantId),
717
722
  "map/scopes/get": async (params) =>
@@ -764,6 +769,7 @@ export class MAPAdapterImpl implements MAPAdapter {
764
769
  "map/send": "canMessage",
765
770
  "map/agents/list": "canQuery",
766
771
  "map/agents/get": "canQuery",
772
+ "map/agents/stop": "canStop",
767
773
  "map/scopes/list": "canQuery",
768
774
  "map/scopes/get": "canQuery",
769
775
  "map/scopes/create": "canManageScopes",
@@ -808,16 +814,27 @@ export class MAPAdapterImpl implements MAPAdapter {
808
814
  break;
809
815
  }
810
816
 
811
- const result = await session.rpcHandler.process(value, {
812
- participantId: session.participantId,
813
- capabilities: participant.capabilities,
814
- signal: session.abortController.signal,
817
+ // Process messages concurrently (same pattern as toad reference).
818
+ // The message loop must not block on any handler, otherwise
819
+ // session/cancel can't be delivered while session/prompt is running.
820
+ // The stream writer queues writes safely, so concurrent responses
821
+ // don't interleave.
822
+ const processAndRespond = async () => {
823
+ const result = await session.rpcHandler.process(value, {
824
+ participantId: session.participantId,
825
+ capabilities: participant.capabilities,
826
+ signal: session.abortController.signal,
827
+ });
828
+
829
+ if (result.type === "response") {
830
+ await this.sendToSession(session, result.response);
831
+ }
832
+ };
833
+ processAndRespond().catch((err) => {
834
+ if (!session.abortController.signal.aborted) {
835
+ console.error(`[MAPAdapter] Error processing message:`, err);
836
+ }
815
837
  });
816
-
817
- // Send response if needed
818
- if (result.type === "response") {
819
- await this.sendToSession(session, result.response);
820
- }
821
838
  }
822
839
  } catch (error) {
823
840
  if (!session.abortController.signal.aborted) {
@@ -1181,6 +1198,59 @@ export class MAPAdapterImpl implements MAPAdapter {
1181
1198
  return { agent: agent ?? null };
1182
1199
  }
1183
1200
 
1201
+ private async handleStopAgent(
1202
+ _participantId: ParticipantId,
1203
+ params: unknown,
1204
+ ): Promise<{ stopping: boolean; agent?: AgentInfo }> {
1205
+ const { agentId, reason, force } = (params as {
1206
+ agentId?: AgentId;
1207
+ reason?: string;
1208
+ force?: boolean;
1209
+ }) ?? {};
1210
+
1211
+ if (!agentId) {
1212
+ throw RPCError.invalidParams("agentId required");
1213
+ }
1214
+
1215
+ if (!this.services.agentManager) {
1216
+ throw RPCError.internalError("Agent manager not available");
1217
+ }
1218
+
1219
+ // Abort any active ACP streams for this agent
1220
+ if (this.acpOverMapHandler) {
1221
+ this.acpOverMapHandler.abortStreamsForAgent(agentId);
1222
+ }
1223
+
1224
+ // Terminate the agent
1225
+ try {
1226
+ await this.services.agentManager.terminate(
1227
+ agentId,
1228
+ (reason ?? "cancelled") as AgentStopReason,
1229
+ );
1230
+ } catch (error) {
1231
+ console.error(`[MAPAdapter] Error stopping agent ${agentId}:`, error);
1232
+ throw RPCError.internalError(
1233
+ `Failed to stop agent: ${error instanceof Error ? error.message : String(error)}`,
1234
+ );
1235
+ }
1236
+
1237
+ // Emit agent state changed event for subscribers
1238
+ this.emitEvent({
1239
+ eventId: `stop-${Date.now()}`,
1240
+ type: "agent.state.changed" as MAPEventType,
1241
+ timestamp: Date.now(),
1242
+ agentId,
1243
+ data: {
1244
+ agentId,
1245
+ current: "stopped",
1246
+ previous: "running",
1247
+ reason: reason ?? "cancelled",
1248
+ },
1249
+ });
1250
+
1251
+ return { stopping: true };
1252
+ }
1253
+
1184
1254
  private async handleListScopes(
1185
1255
  participantId: ParticipantId,
1186
1256
  ): Promise<{ scopes: ScopeInfo[] }> {
@@ -674,8 +674,7 @@ describe('Event Archival', () => {
674
674
  beforeEach(async () => {
675
675
  // Create a temporary directory for testing
676
676
  testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'event-store-test-'));
677
- const storePath = path.join(testDir, 'store.json');
678
- store = await createEventStore({ path: storePath });
677
+ store = await createEventStore({ baseDir: testDir, instanceId: 'archive-test' });
679
678
  });
680
679
 
681
680
  afterEach(async () => {
@@ -987,18 +986,11 @@ describe('Event Archival', () => {
987
986
  }
988
987
  });
989
988
 
990
- it('should emit deprecation warning when using legacy path option', async () => {
989
+ it('should throw when using removed legacy path option', async () => {
991
990
  const legacyPath = path.join(testDir, 'legacy-store.json');
992
- const legacyStore = await createEventStore({ path: legacyPath });
993
-
994
- expect(consoleWarnSpy).toHaveBeenCalledWith(
995
- expect.stringContaining('DEPRECATION WARNING')
996
- );
997
- expect(consoleWarnSpy).toHaveBeenCalledWith(
998
- expect.stringContaining('path` option is deprecated')
991
+ await expect(createEventStore({ path: legacyPath })).rejects.toThrow(
992
+ 'The `path` option has been removed'
999
993
  );
1000
-
1001
- await legacyStore.close();
1002
994
  });
1003
995
 
1004
996
  it('should not emit deprecation warning for new-style configuration', async () => {
@@ -1064,6 +1056,46 @@ describe('Event Archival', () => {
1064
1056
  await store1.close();
1065
1057
  await store2.close();
1066
1058
  });
1059
+
1060
+ it('should preserve agent plan across reload (rebuildViews)', async () => {
1061
+ const planStore = await createEventStore({
1062
+ baseDir: testDir,
1063
+ instanceId: 'plan-reload-test',
1064
+ });
1065
+
1066
+ // Spawn an agent
1067
+ planStore.emit({
1068
+ type: 'spawn',
1069
+ source: { agent_id: 'agent_plan' },
1070
+ payload: {
1071
+ agent_id: 'agent_plan',
1072
+ session_id: 'sess_plan',
1073
+ task: 'test plan persistence',
1074
+ },
1075
+ });
1076
+
1077
+ // Set plan via updateAgentPlan
1078
+ const plan = [
1079
+ { content: 'Read files', priority: 'high', status: 'completed' },
1080
+ { content: 'Write code', priority: 'medium', status: 'in_progress' },
1081
+ ];
1082
+ planStore.updateAgentPlan('agent_plan' as AgentId, plan);
1083
+
1084
+ // Verify plan is set
1085
+ const before = planStore.getAgent('agent_plan' as AgentId);
1086
+ expect(before?.plan).toEqual(plan);
1087
+
1088
+ // Persist, then reload (simulates server restart: load from SQLite + rebuildViews)
1089
+ await planStore.persist();
1090
+ await planStore.reload();
1091
+
1092
+ // Plan should survive the reload
1093
+ const after = planStore.getAgent('agent_plan' as AgentId);
1094
+ expect(after).toBeDefined();
1095
+ expect(after?.plan).toEqual(plan);
1096
+
1097
+ await planStore.close();
1098
+ });
1067
1099
  });
1068
1100
  });
1069
1101
 
@@ -90,7 +90,6 @@ describe('Path Resolution', () => {
90
90
 
91
91
  expect(resolved.instancePath).toBe(':memory:');
92
92
  expect(resolved.isNew).toBe(true);
93
- expect(resolved.isLegacy).toBe(false);
94
93
  expect(resolved.backendType).toBe('memory');
95
94
  expect(resolved.instanceId).toMatch(/^inst_/);
96
95
  });
@@ -142,15 +141,14 @@ describe('Path Resolution', () => {
142
141
  expect(resolved.namespace).toBe('my-project');
143
142
  });
144
143
 
145
- it('should handle legacy path option', () => {
144
+ it('should ignore legacy path option and use defaults', () => {
146
145
  const legacyPath = path.join(testBaseDir, 'legacy-store.json');
147
- const config: StoreConfig = { path: legacyPath };
146
+ const config: StoreConfig = { path: legacyPath, baseDir: testBaseDir };
148
147
  const resolved = resolveInstancePath(config);
149
148
 
150
- expect(resolved.isLegacy).toBe(true);
151
- expect(resolved.backendType).toBe('json');
152
- expect(resolved.instancePath).toBe(legacyPath);
153
- expect(resolved.instanceId).toBe('legacy-store');
149
+ // path option is no longer handled — resolveInstancePath ignores it
150
+ expect(resolved.backendType).toBe(DEFAULT_BACKEND_TYPE);
151
+ expect(resolved.instanceId).toMatch(/^inst_/);
154
152
  });
155
153
 
156
154
  it('should throw on invalid instance ID', () => {