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.
- package/dist/agent/agent-manager.d.ts.map +1 -1
- package/dist/agent/agent-manager.js +23 -5
- package/dist/agent/agent-manager.js.map +1 -1
- package/dist/map/adapter/acp-over-map.d.ts +10 -0
- package/dist/map/adapter/acp-over-map.d.ts.map +1 -1
- package/dist/map/adapter/acp-over-map.js +157 -5
- package/dist/map/adapter/acp-over-map.js.map +1 -1
- package/dist/store/event-store.d.ts.map +1 -1
- package/dist/store/event-store.js +92 -53
- package/dist/store/event-store.js.map +1 -1
- package/dist/store/instance.d.ts +0 -2
- package/dist/store/instance.d.ts.map +1 -1
- package/dist/store/instance.js +1 -24
- package/dist/store/instance.js.map +1 -1
- package/package.json +3 -3
- package/references/acp-factory-ref/package-lock.json +2 -2
- package/references/acp-factory-ref/package.json +2 -2
- package/references/claude-code-acp/package-lock.json +2 -2
- package/references/claude-code-acp/package.json +1 -1
- package/references/claude-code-acp/src/acp-agent.ts +3 -6
- package/src/agent/__tests__/agent-manager.test.ts +4 -6
- package/src/agent/agent-manager.ts +24 -5
- package/src/map/adapter/__tests__/acp-over-map-history.test.ts +664 -0
- package/src/map/adapter/__tests__/acp-over-map-persistence.e2e.test.ts +440 -0
- package/src/map/adapter/acp-over-map.ts +189 -5
- package/src/store/__tests__/event-store.test.ts +4 -12
- package/src/store/__tests__/instance.test.ts +5 -7
- package/src/store/event-store.ts +115 -57
- package/src/store/instance.ts +1 -29
|
@@ -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
|
-
|
|
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
|
|
989
|
+
it('should throw when using removed legacy path option', async () => {
|
|
991
990
|
const legacyPath = path.join(testDir, 'legacy-store.json');
|
|
992
|
-
|
|
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
|
|
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
|
-
|
|
151
|
-
expect(resolved.backendType).toBe(
|
|
152
|
-
expect(resolved.
|
|
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', () => {
|
package/src/store/event-store.ts
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { createStore, Store } from 'tinybase';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
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
|
-
//
|
|
68
|
+
// Tabular better-sqlite3 Persister
|
|
69
69
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
70
70
|
|
|
71
71
|
/**
|
|
72
|
-
*
|
|
73
|
-
* TinyBase
|
|
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
|
|
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
|
-
//
|
|
82
|
-
|
|
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
|
|
122
|
+
return createCustomSqlitePersister(
|
|
85
123
|
store,
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
//
|
|
97
|
-
(
|
|
98
|
-
//
|
|
99
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
|
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
|
|
package/src/store/instance.ts
CHANGED
|
@@ -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
|
-
//
|
|
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
|
// ─────────────────────────────────────────────────────────────────────────────
|