macro-agent 0.0.16 → 0.0.17

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.
@@ -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 () => {
@@ -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', () => {
@@ -9,8 +9,8 @@
9
9
  */
10
10
 
11
11
  import { createStore, Store } from 'tinybase';
12
- import { createFilePersister } from 'tinybase/persisters/persister-file';
13
- import { createCustomPersister } from 'tinybase/persisters';
12
+ import { createCustomPersister, createCustomSqlitePersister } from 'tinybase/persisters';
13
+ import type { DatabasePersisterConfig } from 'tinybase/persisters';
14
14
  import Database from 'better-sqlite3';
15
15
  import { nanoid } from 'nanoid';
16
16
  import * as path from 'path';
@@ -65,38 +65,80 @@ import type { StorageBackend, ExportedEvent } from './backends/types.js';
65
65
  import { createTinyBaseBackend } from './backends/tinybase-backend.js';
66
66
 
67
67
  // ─────────────────────────────────────────────────────────────────────────────
68
- // Custom better-sqlite3 Persister
68
+ // Tabular better-sqlite3 Persister
69
69
  // ─────────────────────────────────────────────────────────────────────────────
70
70
 
71
71
  /**
72
- * Creates a custom TinyBase persister for better-sqlite3.
73
- * TinyBase's built-in createSqlite3Persister expects node-sqlite3 (async/callback API),
74
- * but we use better-sqlite3 (sync API). This custom persister bridges the gap.
72
+ * Build tabular config for the 10 TinyBase tables.
73
+ * Identity mapping: TinyBase table name === SQLite table name.
75
74
  */
76
- function createBetterSqlite3Persister(
75
+ function getTabularConfig(): DatabasePersisterConfig {
76
+ const tableNames = [
77
+ 'events', 'agents', 'tasks', 'messages',
78
+ 'sessions', 'conversations', 'turns',
79
+ 'threads', 'subscriptions', 'participants',
80
+ ];
81
+
82
+ const load: Record<string, string> = {};
83
+ const save: Record<string, string> = {};
84
+ for (const t of tableNames) {
85
+ load[t] = t; // SQLite table -> TinyBase table (same name)
86
+ save[t] = t; // TinyBase table -> SQLite table (same name)
87
+ }
88
+
89
+ return {
90
+ mode: 'tabular',
91
+ tables: { load, save },
92
+ autoLoadIntervalSeconds: 1,
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Creates a tabular TinyBase persister backed by better-sqlite3.
98
+ *
99
+ * Unlike the old JSON blob approach (entire store as one JSON string in a
100
+ * single row), tabular mode maps each TinyBase table to a real SQLite table
101
+ * and only writes changed rows on autoSave — making persistence O(delta)
102
+ * instead of O(total).
103
+ */
104
+ function createTabularBetterSqlite3Persister(
77
105
  store: Store,
78
106
  db: ReturnType<typeof Database>,
79
- tableName: string = 'tinybase_store'
80
107
  ) {
81
- // Create table if not exists
82
- db.exec(`CREATE TABLE IF NOT EXISTS ${tableName} (_id TEXT PRIMARY KEY, store TEXT)`);
108
+ // Wrap better-sqlite3's sync API as the async DatabaseExecuteCommand.
109
+ // TinyBase generates SQL with $1, $2, ... placeholders (PostgreSQL-style),
110
+ // but better-sqlite3 uses ? for positional array binding. Convert them.
111
+ const executeCommand = async (sql: string, params?: any[]): Promise<Record<string, any>[]> => {
112
+ const convertedSql = sql.replace(/\$\d+/g, '?');
113
+ const trimmed = convertedSql.trimStart().toUpperCase();
114
+ const stmt = db.prepare(convertedSql);
115
+ if (trimmed.startsWith('SELECT') || trimmed.startsWith('PRAGMA')) {
116
+ return (params ? stmt.all(...params) : stmt.all()) as Record<string, any>[];
117
+ }
118
+ params ? stmt.run(...params) : stmt.run();
119
+ return [];
120
+ };
83
121
 
84
- return createCustomPersister(
122
+ return createCustomSqlitePersister(
85
123
  store,
86
- // getPersisted - load from SQLite
87
- async () => {
88
- const row = db.prepare(`SELECT store FROM ${tableName} WHERE _id = '_'`).get() as { store: string } | undefined;
89
- return row ? JSON.parse(row.store) : undefined;
90
- },
91
- // setPersisted - save to SQLite
92
- async (getContent) => {
93
- const json = JSON.stringify(getContent());
94
- db.prepare(`INSERT OR REPLACE INTO ${tableName} (_id, store) VALUES ('_', ?)`).run(json);
95
- },
96
- // addPersisterListener - poll for external changes (cross-process)
97
- (listener) => setInterval(listener, 1000),
98
- // delPersisterListener - cleanup polling
99
- (interval: ReturnType<typeof setInterval>) => clearInterval(interval),
124
+ getTabularConfig(),
125
+ executeCommand,
126
+ // addChangeListener better-sqlite3 has no native change events
127
+ (_listener: (tableName: string) => void) => null as any,
128
+ // delChangeListener — no-op
129
+ (_handle: any) => {},
130
+ // onSqlCommand
131
+ undefined,
132
+ // onIgnoredError
133
+ (error: any) => console.warn('[EventStore] Persister error:', error),
134
+ // destroy
135
+ () => db.close(),
136
+ // persist mode (1 = StoreOnly)
137
+ 1 as any,
138
+ // thing (the db instance)
139
+ db,
140
+ // getThing accessor name
141
+ 'getDb',
100
142
  );
101
143
  }
102
144
 
@@ -252,7 +294,15 @@ export function parseDuration(duration: string): number {
252
294
  export async function createEventStore(config: StoreConfig = {}): Promise<EventStore> {
253
295
  // Resolve instance configuration
254
296
  const resolved = resolveInstancePath(config);
255
- const { instanceId, instancePath, namespace, isNew, isLegacy, backendType } = resolved;
297
+ const { instanceId, instancePath, namespace, isNew, backendType } = resolved;
298
+
299
+ // Reject deprecated legacy path option
300
+ if (config.path) {
301
+ throw new Error(
302
+ '[macro-agent] The `path` option has been removed. ' +
303
+ 'Use `instanceId` and `baseDir` instead for per-instance isolation.'
304
+ );
305
+ }
256
306
 
257
307
  // Track baseDir for MCP subprocess communication
258
308
  const baseDir = config.baseDir ?? path.join(os.homedir(), '.multiagent');
@@ -261,15 +311,6 @@ export async function createEventStore(config: StoreConfig = {}): Promise<EventS
261
311
  const peerVisibility: PeerVisibilityConfig =
262
312
  config.peerVisibility ?? DEFAULT_PEER_VISIBILITY;
263
313
 
264
- // Emit deprecation warning for legacy path option
265
- if (isLegacy) {
266
- console.warn(
267
- '[macro-agent] DEPRECATION WARNING: The `path` option is deprecated and will be removed in a future version. ' +
268
- 'Use `instanceId` and `baseDir` instead for per-instance isolation. ' +
269
- 'See documentation for migration guide.'
270
- );
271
- }
272
-
273
314
  // Emit warning for in-memory mode (only in non-test environments)
274
315
  if (config.inMemory && process.env.NODE_ENV !== 'test') {
275
316
  console.warn(
@@ -282,31 +323,53 @@ export async function createEventStore(config: StoreConfig = {}): Promise<EventS
282
323
  const store = createStore();
283
324
 
284
325
  // Set up persister based on backend type
285
- let persister: ReturnType<typeof createFilePersister> | ReturnType<typeof createBetterSqlite3Persister> | null = null;
326
+ let persister: ReturnType<typeof createTabularBetterSqlite3Persister> | null = null;
286
327
  let db: ReturnType<typeof Database> | null = null;
287
328
 
288
329
  if (!config.inMemory) {
289
- if (isLegacy) {
290
- // Legacy mode: Use JSON file persister at the specified path
291
- const dir = path.dirname(instancePath);
292
- if (!fs.existsSync(dir)) {
293
- fs.mkdirSync(dir, { recursive: true });
294
- }
295
- persister = createFilePersister(store, instancePath);
296
- } else {
297
- // New instances: Use SQLite with custom better-sqlite3 persister
298
- ensureInstanceDir(instancePath);
299
- const dbPath = path.join(instancePath, 'store.sqlite');
300
- db = new Database(dbPath);
301
- db.pragma('journal_mode = WAL');
302
- db.pragma('busy_timeout = 5000');
303
- persister = createBetterSqlite3Persister(store, db);
330
+ ensureInstanceDir(instancePath);
331
+ const dbPath = path.join(instancePath, 'store.sqlite');
332
+ db = new Database(dbPath);
333
+ db.pragma('journal_mode = WAL');
334
+ db.pragma('busy_timeout = 5000');
335
+
336
+ // Migration: if old JSON blob table exists, load data from it first
337
+ const oldTableExists = db.prepare(
338
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='tinybase_store'"
339
+ ).get();
340
+
341
+ if (oldTableExists) {
342
+ const oldPersister = createCustomPersister(
343
+ store,
344
+ async () => {
345
+ const row = db!.prepare("SELECT store FROM tinybase_store WHERE _id = '_'").get() as { store: string } | undefined;
346
+ return row ? JSON.parse(row.store) : undefined;
347
+ },
348
+ async () => {},
349
+ (listener) => setInterval(listener, 1000),
350
+ (interval: ReturnType<typeof setInterval>) => clearInterval(interval),
351
+ );
352
+ await oldPersister.load();
353
+ oldPersister.destroy();
304
354
  }
355
+
356
+ persister = createTabularBetterSqlite3Persister(store, db);
357
+
358
+ if (oldTableExists) {
359
+ // Save migrated data to new tabular format, then drop old table
360
+ await persister.save();
361
+ db.exec('DROP TABLE IF EXISTS tinybase_store');
362
+ }
363
+
305
364
  await persister.load();
365
+ // Auto-save: persist to disk whenever the in-memory store changes.
366
+ // Without this, emit() only writes to TinyBase's in-memory store and
367
+ // data is lost if the server is killed before an explicit persist().
368
+ await persister.startAutoSave();
306
369
  }
307
370
 
308
371
  // Initialize/update instance metadata
309
- if (!config.inMemory && !isLegacy) {
372
+ if (!config.inMemory) {
310
373
  if (isNew) {
311
374
  const meta = createInstanceMeta(resolved, config);
312
375
  writeInstanceMeta(instancePath, meta);
@@ -876,11 +939,6 @@ export async function createEventStore(config: StoreConfig = {}): Promise<EventS
876
939
  * Get archives directory path
877
940
  */
878
941
  function getArchivesDir(): string {
879
- // For legacy mode, use parent of the file path
880
- // For new mode, use the instance directory
881
- if (isLegacy) {
882
- return path.join(path.dirname(instancePath), 'archives');
883
- }
884
942
  return path.join(instancePath, 'archives');
885
943
  }
886
944
 
@@ -213,9 +213,6 @@ export interface ResolvedInstance {
213
213
  /** Whether this is a new instance */
214
214
  isNew: boolean;
215
215
 
216
- /** Whether this is using legacy path mode */
217
- isLegacy: boolean;
218
-
219
216
  /** Backend type to use */
220
217
  backendType: string;
221
218
  }
@@ -300,25 +297,11 @@ export function resolveInstancePath(config: StoreConfig): ResolvedInstance {
300
297
  instancePath: ':memory:',
301
298
  namespace,
302
299
  isNew: true,
303
- isLegacy: false,
304
300
  backendType: 'memory',
305
301
  };
306
302
  }
307
303
 
308
- // Legacy path takes precedence for backward compat
309
- if (config.path) {
310
- const legacyInstanceId = deriveInstanceIdFromPath(config.path);
311
- return {
312
- instanceId: legacyInstanceId,
313
- instancePath: config.path,
314
- namespace,
315
- isNew: !fs.existsSync(config.path),
316
- isLegacy: true,
317
- backendType: 'json',
318
- };
319
- }
320
-
321
- // New instance resolution
304
+ // Instance resolution
322
305
  const instanceId = config.instanceId ?? generateInstanceId();
323
306
 
324
307
  if (!isValidInstanceId(instanceId)) {
@@ -339,21 +322,10 @@ export function resolveInstancePath(config: StoreConfig): ResolvedInstance {
339
322
  instancePath,
340
323
  namespace,
341
324
  isNew,
342
- isLegacy: false,
343
325
  backendType,
344
326
  };
345
327
  }
346
328
 
347
- /**
348
- * Derive an instance ID from a legacy path
349
- */
350
- function deriveInstanceIdFromPath(legacyPath: string): string {
351
- // Use the filename without extension as the instance ID
352
- const basename = path.basename(legacyPath, path.extname(legacyPath));
353
- // Sanitize to valid instance ID
354
- return basename.replace(/[^a-zA-Z0-9_-]/g, '_');
355
- }
356
-
357
329
  // ─────────────────────────────────────────────────────────────────────────────
358
330
  // Instance Directory Management
359
331
  // ─────────────────────────────────────────────────────────────────────────────