agent-relay 2.0.36 → 2.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 (81) hide show
  1. package/dist/index.cjs +31903 -33913
  2. package/dist/src/cli/index.js +38 -51
  3. package/dist/src/cli/index.js.map +1 -1
  4. package/package.json +18 -19
  5. package/packages/api-types/package.json +1 -1
  6. package/packages/benchmark/package.json +4 -4
  7. package/packages/bridge/package.json +8 -8
  8. package/packages/cli-tester/package.json +1 -1
  9. package/packages/config/dist/project-namespace.d.ts +28 -0
  10. package/packages/config/dist/project-namespace.d.ts.map +1 -1
  11. package/packages/config/dist/project-namespace.js +42 -0
  12. package/packages/config/dist/project-namespace.js.map +1 -1
  13. package/packages/config/package.json +2 -2
  14. package/packages/config/src/project-namespace.ts +65 -0
  15. package/packages/continuity/dist/formatter.d.ts +8 -2
  16. package/packages/continuity/dist/formatter.d.ts.map +1 -1
  17. package/packages/continuity/dist/formatter.js +142 -7
  18. package/packages/continuity/dist/formatter.js.map +1 -1
  19. package/packages/continuity/dist/index.d.ts +1 -0
  20. package/packages/continuity/dist/index.d.ts.map +1 -1
  21. package/packages/continuity/dist/index.js +2 -0
  22. package/packages/continuity/dist/index.js.map +1 -1
  23. package/packages/continuity/package.json +4 -1
  24. package/packages/continuity/src/formatter.ts +175 -10
  25. package/packages/continuity/src/index.ts +3 -0
  26. package/packages/daemon/dist/enhanced-features.d.ts +2 -3
  27. package/packages/daemon/dist/enhanced-features.d.ts.map +1 -1
  28. package/packages/daemon/dist/enhanced-features.js +1 -0
  29. package/packages/daemon/dist/enhanced-features.js.map +1 -1
  30. package/packages/daemon/dist/index.d.ts +0 -2
  31. package/packages/daemon/dist/index.d.ts.map +1 -1
  32. package/packages/daemon/dist/index.js +0 -3
  33. package/packages/daemon/dist/index.js.map +1 -1
  34. package/packages/daemon/dist/server.d.ts +0 -6
  35. package/packages/daemon/dist/server.d.ts.map +1 -1
  36. package/packages/daemon/dist/server.js +20 -119
  37. package/packages/daemon/dist/server.js.map +1 -1
  38. package/packages/daemon/package.json +12 -14
  39. package/packages/daemon/src/enhanced-features.ts +4 -4
  40. package/packages/daemon/src/index.ts +0 -4
  41. package/packages/daemon/src/server.ts +19 -127
  42. package/packages/daemon/vitest.config.ts +9 -0
  43. package/packages/hooks/package.json +4 -4
  44. package/packages/mcp/package.json +3 -3
  45. package/packages/memory/package.json +2 -2
  46. package/packages/policy/package.json +2 -2
  47. package/packages/protocol/package.json +1 -1
  48. package/packages/resiliency/package.json +1 -1
  49. package/packages/sdk/package.json +2 -2
  50. package/packages/spawner/package.json +1 -1
  51. package/packages/state/package.json +1 -1
  52. package/packages/storage/dist/adapter.d.ts +5 -5
  53. package/packages/storage/dist/adapter.js +9 -9
  54. package/packages/storage/dist/adapter.js.map +1 -1
  55. package/packages/storage/package.json +2 -2
  56. package/packages/storage/src/adapter.ts +9 -9
  57. package/packages/telemetry/package.json +1 -1
  58. package/packages/trajectory/package.json +2 -2
  59. package/packages/user-directory/package.json +2 -2
  60. package/packages/utils/package.json +2 -2
  61. package/packages/wrapper/package.json +6 -6
  62. package/scripts/build-cjs.mjs +2 -0
  63. package/packages/daemon/dist/migrations/index.d.ts +0 -73
  64. package/packages/daemon/dist/migrations/index.d.ts.map +0 -1
  65. package/packages/daemon/dist/migrations/index.js +0 -241
  66. package/packages/daemon/dist/migrations/index.js.map +0 -1
  67. package/packages/daemon/dist/relay-ledger.d.ts +0 -263
  68. package/packages/daemon/dist/relay-ledger.d.ts.map +0 -1
  69. package/packages/daemon/dist/relay-ledger.js +0 -538
  70. package/packages/daemon/dist/relay-ledger.js.map +0 -1
  71. package/packages/daemon/dist/relay-watchdog.d.ts +0 -125
  72. package/packages/daemon/dist/relay-watchdog.d.ts.map +0 -1
  73. package/packages/daemon/dist/relay-watchdog.js +0 -611
  74. package/packages/daemon/dist/relay-watchdog.js.map +0 -1
  75. package/packages/daemon/src/migrations/0001_initial.sql +0 -72
  76. package/packages/daemon/src/migrations/index.test.ts +0 -195
  77. package/packages/daemon/src/migrations/index.ts +0 -286
  78. package/packages/daemon/src/relay-ledger.test.ts +0 -358
  79. package/packages/daemon/src/relay-ledger.ts +0 -713
  80. package/packages/daemon/src/relay-watchdog.test.ts +0 -881
  81. package/packages/daemon/src/relay-watchdog.ts +0 -785
@@ -1,713 +0,0 @@
1
- /**
2
- * Relay Ledger - SQLite-based tracking of relay file processing
3
- *
4
- * Tracks files discovered in agent outboxes through their lifecycle:
5
- * pending -> processing -> delivered/failed -> archived
6
- *
7
- * Features:
8
- * - Atomic claims with status WHERE clause
9
- * - Crash recovery (reset processing -> pending on startup)
10
- * - Archive path tracking for auditing
11
- * - Configurable retention
12
- * - Schema migrations for version upgrades
13
- */
14
-
15
- import Database from 'better-sqlite3';
16
- import path from 'node:path';
17
- import fs from 'node:fs';
18
- import crypto from 'node:crypto';
19
- import { runMigrations, getPendingMigrations, verifyMigrations, type MigrationResult } from './migrations/index.js';
20
-
21
- // ============================================================================
22
- // Types
23
- // ============================================================================
24
-
25
- export type RelayFileStatus = 'pending' | 'processing' | 'delivered' | 'failed' | 'archived';
26
-
27
- export interface RelayFileRecord {
28
- id: number;
29
- fileId: string;
30
- /** Canonical/resolved path (symlinks resolved via realpath) */
31
- sourcePath: string;
32
- /** Original path that may have been a symlink (for debugging) */
33
- symlinkPath: string | null;
34
- archivePath: string | null;
35
- agentName: string;
36
- messageType: string;
37
- status: RelayFileStatus;
38
- retries: number;
39
- maxRetries: number;
40
- discoveredAt: number;
41
- processedAt: number | null;
42
- archivedAt: number | null;
43
- error: string | null;
44
- contentHash: string | null;
45
- fileSize: number;
46
- /** File modification time in nanoseconds (for change detection) */
47
- fileMtimeNs: number | null;
48
- /** File inode number (for change detection on Unix) */
49
- fileInode: number | null;
50
- }
51
-
52
- export interface LedgerAgentRecord {
53
- id: number;
54
- agentName: string;
55
- createdAt: number;
56
- lastSeenAt: number;
57
- status: 'active' | 'inactive';
58
- metadata: Record<string, unknown> | null;
59
- }
60
-
61
- export interface OrchestratorState {
62
- key: string;
63
- value: string;
64
- updatedAt: number;
65
- }
66
-
67
- export interface PendingOperation {
68
- id: number;
69
- operationType: 'process' | 'archive' | 'cleanup';
70
- targetId: string;
71
- payload: string | null;
72
- createdAt: number;
73
- attempts: number;
74
- lastAttemptAt: number | null;
75
- error: string | null;
76
- }
77
-
78
- export interface LedgerConfig {
79
- /** Path to SQLite database */
80
- dbPath: string;
81
- /** Maximum retries before marking as failed (default: 3) */
82
- maxRetries?: number;
83
- /** Archive retention in milliseconds (default: 7 days) */
84
- archiveRetentionMs?: number;
85
- /** Busy timeout for concurrent access (default: 5000ms) */
86
- busyTimeout?: number;
87
- }
88
-
89
- export interface ClaimResult {
90
- success: boolean;
91
- record?: RelayFileRecord;
92
- reason?: string;
93
- }
94
-
95
- // ============================================================================
96
- // Constants
97
- // ============================================================================
98
-
99
- const DEFAULT_MAX_RETRIES = 3;
100
- const DEFAULT_ARCHIVE_RETENTION_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
101
- const DEFAULT_BUSY_TIMEOUT = 5000;
102
-
103
- // Reserved agent names for special routing
104
- const RESERVED_AGENT_NAMES = new Set(['Lead', 'System', 'Broadcast', '*']);
105
-
106
- // ============================================================================
107
- // RelayLedger Class
108
- // ============================================================================
109
-
110
- export class RelayLedger {
111
- private db: Database.Database;
112
- private config: Required<LedgerConfig>;
113
-
114
- // Prepared statements for performance
115
- private stmtInsert!: Database.Statement;
116
- private stmtClaim!: Database.Statement;
117
- private stmtUpdateStatus!: Database.Statement;
118
- private stmtMarkDelivered!: Database.Statement;
119
- private stmtMarkFailed!: Database.Statement;
120
- private stmtMarkArchived!: Database.Statement;
121
- private stmtGetPending!: Database.Statement;
122
- private stmtGetByPath!: Database.Statement;
123
- private stmtIsActivePath!: Database.Statement;
124
- private stmtGetById!: Database.Statement;
125
- private stmtResetProcessing!: Database.Statement;
126
- private stmtCleanupArchived!: Database.Statement;
127
- private stmtGetStats!: Database.Statement;
128
-
129
- constructor(config: LedgerConfig) {
130
- this.config = {
131
- dbPath: config.dbPath,
132
- maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES,
133
- archiveRetentionMs: config.archiveRetentionMs ?? DEFAULT_ARCHIVE_RETENTION_MS,
134
- busyTimeout: config.busyTimeout ?? DEFAULT_BUSY_TIMEOUT,
135
- };
136
-
137
- // Ensure directory exists
138
- const dbDir = path.dirname(this.config.dbPath);
139
- if (!fs.existsSync(dbDir)) {
140
- fs.mkdirSync(dbDir, { recursive: true });
141
- }
142
-
143
- // Open database with recommended settings
144
- this.db = new Database(this.config.dbPath);
145
- this.db.pragma('journal_mode = WAL');
146
- this.db.pragma(`busy_timeout = ${this.config.busyTimeout}`);
147
- this.db.pragma('synchronous = NORMAL');
148
- this.db.pragma('foreign_keys = ON');
149
-
150
- this.initSchema();
151
- this.prepareStatements();
152
- }
153
-
154
- /** Last migration result (for debugging) */
155
- private lastMigrationResult?: MigrationResult;
156
-
157
- /**
158
- * Initialize database schema using migrations
159
- */
160
- private initSchema(): void {
161
- // Run migrations (creates tables if they don't exist, or upgrades existing schema)
162
- this.lastMigrationResult = runMigrations(this.db);
163
-
164
- // Log migration results for debugging
165
- if (this.lastMigrationResult.applied.length > 0) {
166
- console.log(`[relay-ledger] Applied migrations: ${this.lastMigrationResult.applied.join(', ')}`);
167
- }
168
- if (this.lastMigrationResult.errors.length > 0) {
169
- console.error(`[relay-ledger] Migration errors:`, this.lastMigrationResult.errors);
170
- }
171
- }
172
-
173
- /**
174
- * Get the last migration result
175
- */
176
- getMigrationResult(): MigrationResult | undefined {
177
- return this.lastMigrationResult;
178
- }
179
-
180
- /**
181
- * Get pending migrations that haven't been applied
182
- */
183
- getPendingMigrations(): Array<{ name: string; sql: string; checksum: string }> {
184
- return getPendingMigrations(this.db);
185
- }
186
-
187
- /**
188
- * Verify migration checksums match what was originally applied
189
- */
190
- verifyMigrationIntegrity(): Array<{ name: string; expected: string; actual: string }> {
191
- return verifyMigrations(this.db);
192
- }
193
-
194
- /**
195
- * Prepare frequently-used statements
196
- */
197
- private prepareStatements(): void {
198
- this.stmtInsert = this.db.prepare(`
199
- INSERT INTO relay_files (file_id, source_path, symlink_path, agent_name, message_type, discovered_at, file_size, content_hash, file_mtime_ns, file_inode)
200
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
201
- ON CONFLICT(file_id) DO NOTHING
202
- `);
203
-
204
- this.stmtClaim = this.db.prepare(`
205
- UPDATE relay_files
206
- SET status = 'processing', retries = retries + 1
207
- WHERE file_id = ? AND status = 'pending' AND retries < max_retries
208
- RETURNING *
209
- `);
210
-
211
- this.stmtUpdateStatus = this.db.prepare(`
212
- UPDATE relay_files SET status = ? WHERE file_id = ?
213
- `);
214
-
215
- this.stmtMarkDelivered = this.db.prepare(`
216
- UPDATE relay_files
217
- SET status = 'delivered', processed_at = ?
218
- WHERE file_id = ?
219
- `);
220
-
221
- this.stmtMarkFailed = this.db.prepare(`
222
- UPDATE relay_files
223
- SET status = 'failed', processed_at = ?, error = ?
224
- WHERE file_id = ?
225
- `);
226
-
227
- this.stmtMarkArchived = this.db.prepare(`
228
- UPDATE relay_files
229
- SET status = 'archived', archive_path = ?, archived_at = ?
230
- WHERE file_id = ?
231
- `);
232
-
233
- this.stmtGetPending = this.db.prepare(`
234
- SELECT * FROM relay_files
235
- WHERE status = 'pending' AND retries < max_retries
236
- ORDER BY id ASC
237
- LIMIT ?
238
- `);
239
-
240
- this.stmtGetByPath = this.db.prepare(`
241
- SELECT * FROM relay_files WHERE source_path = ?
242
- `);
243
-
244
- // Only check for pending/processing files (not archived/delivered/failed)
245
- this.stmtIsActivePath = this.db.prepare(`
246
- SELECT 1 FROM relay_files WHERE source_path = ? AND status IN ('pending', 'processing')
247
- `);
248
-
249
- this.stmtGetById = this.db.prepare(`
250
- SELECT * FROM relay_files WHERE file_id = ?
251
- `);
252
-
253
- this.stmtResetProcessing = this.db.prepare(`
254
- UPDATE relay_files
255
- SET status = 'pending'
256
- WHERE status = 'processing'
257
- `);
258
-
259
- this.stmtCleanupArchived = this.db.prepare(`
260
- DELETE FROM relay_files
261
- WHERE status = 'archived' AND archived_at < ?
262
- `);
263
-
264
- this.stmtGetStats = this.db.prepare(`
265
- SELECT status, COUNT(*) as count FROM relay_files GROUP BY status
266
- `);
267
- }
268
-
269
- /**
270
- * Generate a unique file ID (12-char hex for ~281 trillion combinations)
271
- */
272
- generateFileId(): string {
273
- return crypto.randomBytes(6).toString('hex');
274
- }
275
-
276
- /**
277
- * Check if an agent name is reserved
278
- */
279
- isReservedAgentName(name: string): boolean {
280
- return RESERVED_AGENT_NAMES.has(name);
281
- }
282
-
283
- /**
284
- * Register a newly discovered file
285
- * @param sourcePath - Canonical path (symlinks resolved)
286
- * @param agentName - Agent that owns the file
287
- * @param messageType - Type of message (msg, spawn, release, etc.)
288
- * @param fileSize - Size in bytes
289
- * @param contentHash - Optional hash for deduplication
290
- * @param fileMtimeNs - Optional modification time in nanoseconds
291
- * @param fileInode - Optional inode number
292
- * @param symlinkPath - Original path if it was a symlink (for debugging)
293
- */
294
- registerFile(
295
- sourcePath: string,
296
- agentName: string,
297
- messageType: string,
298
- fileSize: number,
299
- contentHash?: string,
300
- fileMtimeNs?: number,
301
- fileInode?: number,
302
- symlinkPath?: string
303
- ): string {
304
- const fileId = this.generateFileId();
305
- const now = Date.now();
306
-
307
- this.stmtInsert.run(
308
- fileId,
309
- sourcePath,
310
- symlinkPath ?? null,
311
- agentName,
312
- messageType,
313
- now,
314
- fileSize,
315
- contentHash ?? null,
316
- fileMtimeNs ?? null,
317
- fileInode ?? null
318
- );
319
- return fileId;
320
- }
321
-
322
- /**
323
- * Check if a file is actively being processed (pending or processing).
324
- * Returns false for archived/delivered/failed files so new files at the same path can be registered.
325
- */
326
- isFileRegistered(sourcePath: string): boolean {
327
- const row = this.stmtIsActivePath.get(sourcePath);
328
- return row !== undefined;
329
- }
330
-
331
- /**
332
- * Atomically claim a file for processing
333
- * Returns the record if claim succeeded, null otherwise
334
- */
335
- claimFile(fileId: string): ClaimResult {
336
- const row = this.stmtClaim.get(fileId) as any;
337
-
338
- if (!row) {
339
- // Check why claim failed
340
- const existing = this.stmtGetById.get(fileId) as any;
341
- if (!existing) {
342
- return { success: false, reason: 'File not found' };
343
- }
344
- if (existing.status !== 'pending') {
345
- return { success: false, reason: `File status is ${existing.status}, not pending` };
346
- }
347
- if (existing.retries >= existing.max_retries) {
348
- return { success: false, reason: `Max retries (${existing.max_retries}) exceeded` };
349
- }
350
- return { success: false, reason: 'Unknown claim failure' };
351
- }
352
-
353
- return {
354
- success: true,
355
- record: this.rowToRecord(row),
356
- };
357
- }
358
-
359
- /**
360
- * Mark a file as successfully delivered
361
- */
362
- markDelivered(fileId: string): void {
363
- this.stmtMarkDelivered.run(Date.now(), fileId);
364
- }
365
-
366
- /**
367
- * Mark a file as failed (will retry if under max retries)
368
- */
369
- markFailed(fileId: string, error: string): void {
370
- const record = this.getById(fileId);
371
- if (!record) return;
372
-
373
- if (record.retries >= record.maxRetries) {
374
- // Permanent failure
375
- this.stmtMarkFailed.run(Date.now(), error, fileId);
376
- } else {
377
- // Reset to pending for retry
378
- this.stmtUpdateStatus.run('pending', fileId);
379
- }
380
- }
381
-
382
- /**
383
- * Mark a file as archived (moved to archive location)
384
- */
385
- markArchived(fileId: string, archivePath: string): void {
386
- this.stmtMarkArchived.run(archivePath, Date.now(), fileId);
387
- }
388
-
389
- /**
390
- * Get pending files ready for processing
391
- */
392
- getPendingFiles(limit = 100): RelayFileRecord[] {
393
- const rows = this.stmtGetPending.all(limit) as any[];
394
- return rows.map(row => this.rowToRecord(row));
395
- }
396
-
397
- /**
398
- * Get a file record by ID
399
- */
400
- getById(fileId: string): RelayFileRecord | null {
401
- const row = this.stmtGetById.get(fileId) as any;
402
- return row ? this.rowToRecord(row) : null;
403
- }
404
-
405
- /**
406
- * Get a file record by source path
407
- */
408
- getByPath(sourcePath: string): RelayFileRecord | null {
409
- const row = this.stmtGetByPath.get(sourcePath) as any;
410
- return row ? this.rowToRecord(row) : null;
411
- }
412
-
413
- /**
414
- * Reset all 'processing' files to 'pending' (crash recovery)
415
- */
416
- resetProcessingFiles(): number {
417
- const result = this.stmtResetProcessing.run();
418
- return result.changes;
419
- }
420
-
421
- /**
422
- * Mark files as failed if their source file no longer exists
423
- */
424
- reconcileWithFilesystem(): { reset: number; failed: number } {
425
- let reset = 0;
426
- let failed = 0;
427
-
428
- // Get all processing files
429
- const processingFiles = this.db
430
- .prepare(`SELECT * FROM relay_files WHERE status = 'processing'`)
431
- .all() as any[];
432
-
433
- for (const row of processingFiles) {
434
- if (!fs.existsSync(row.source_path)) {
435
- // File was deleted while processing - mark as failed
436
- this.stmtMarkFailed.run(Date.now(), 'Source file deleted during processing', row.file_id);
437
- failed++;
438
- } else {
439
- // Reset to pending
440
- this.stmtUpdateStatus.run('pending', row.file_id);
441
- reset++;
442
- }
443
- }
444
-
445
- return { reset, failed };
446
- }
447
-
448
- /**
449
- * Clean up old archived records
450
- */
451
- cleanupArchivedRecords(): number {
452
- const cutoff = Date.now() - this.config.archiveRetentionMs;
453
- const result = this.stmtCleanupArchived.run(cutoff);
454
- return result.changes;
455
- }
456
-
457
- /**
458
- * Get statistics about file processing
459
- */
460
- getStats(): Record<RelayFileStatus, number> {
461
- const rows = this.stmtGetStats.all() as Array<{ status: string; count: number }>;
462
- const stats: Record<string, number> = {
463
- pending: 0,
464
- processing: 0,
465
- delivered: 0,
466
- failed: 0,
467
- archived: 0,
468
- };
469
-
470
- for (const row of rows) {
471
- stats[row.status] = row.count;
472
- }
473
-
474
- return stats as Record<RelayFileStatus, number>;
475
- }
476
-
477
- /**
478
- * Convert database row to RelayFileRecord
479
- */
480
- private rowToRecord(row: any): RelayFileRecord {
481
- return {
482
- id: row.id,
483
- fileId: row.file_id,
484
- sourcePath: row.source_path,
485
- symlinkPath: row.symlink_path,
486
- archivePath: row.archive_path,
487
- agentName: row.agent_name,
488
- messageType: row.message_type,
489
- status: row.status as RelayFileStatus,
490
- retries: row.retries,
491
- maxRetries: row.max_retries,
492
- discoveredAt: row.discovered_at,
493
- processedAt: row.processed_at,
494
- archivedAt: row.archived_at,
495
- error: row.error,
496
- contentHash: row.content_hash,
497
- fileSize: row.file_size,
498
- fileMtimeNs: row.file_mtime_ns,
499
- fileInode: row.file_inode,
500
- };
501
- }
502
-
503
- // ==========================================================================
504
- // Agent Management
505
- // ==========================================================================
506
-
507
- /**
508
- * Register or update an agent
509
- */
510
- registerAgent(agentName: string, metadata?: Record<string, unknown>): void {
511
- const now = Date.now();
512
- const metadataJson = metadata ? JSON.stringify(metadata) : null;
513
-
514
- this.db.prepare(`
515
- INSERT INTO agents (agent_name, created_at, last_seen_at, status, metadata)
516
- VALUES (?, ?, ?, 'active', ?)
517
- ON CONFLICT(agent_name) DO UPDATE SET
518
- last_seen_at = excluded.last_seen_at,
519
- status = 'active',
520
- metadata = COALESCE(excluded.metadata, agents.metadata)
521
- `).run(agentName, now, now, metadataJson);
522
- }
523
-
524
- /**
525
- * Update agent last seen time
526
- */
527
- updateAgentLastSeen(agentName: string): void {
528
- this.db.prepare(`
529
- UPDATE agents SET last_seen_at = ? WHERE agent_name = ?
530
- `).run(Date.now(), agentName);
531
- }
532
-
533
- /**
534
- * Mark agent as inactive
535
- */
536
- markAgentInactive(agentName: string): void {
537
- this.db.prepare(`
538
- UPDATE agents SET status = 'inactive' WHERE agent_name = ?
539
- `).run(agentName);
540
- }
541
-
542
- /**
543
- * Get all active agents
544
- */
545
- getActiveAgents(): LedgerAgentRecord[] {
546
- const rows = this.db.prepare(`
547
- SELECT * FROM agents WHERE status = 'active' ORDER BY last_seen_at DESC
548
- `).all() as any[];
549
-
550
- return rows.map(row => ({
551
- id: row.id,
552
- agentName: row.agent_name,
553
- createdAt: row.created_at,
554
- lastSeenAt: row.last_seen_at,
555
- status: row.status,
556
- metadata: row.metadata ? JSON.parse(row.metadata) : null,
557
- }));
558
- }
559
-
560
- /**
561
- * Get agent by name
562
- */
563
- getAgent(agentName: string): LedgerAgentRecord | null {
564
- const row = this.db.prepare(`
565
- SELECT * FROM agents WHERE agent_name = ?
566
- `).get(agentName) as any;
567
-
568
- if (!row) return null;
569
-
570
- return {
571
- id: row.id,
572
- agentName: row.agent_name,
573
- createdAt: row.created_at,
574
- lastSeenAt: row.last_seen_at,
575
- status: row.status,
576
- metadata: row.metadata ? JSON.parse(row.metadata) : null,
577
- };
578
- }
579
-
580
- // ==========================================================================
581
- // Orchestrator State
582
- // ==========================================================================
583
-
584
- /**
585
- * Save orchestrator state
586
- */
587
- saveState(key: string, value: string): void {
588
- this.db.prepare(`
589
- INSERT INTO orchestrator_state (key, value, updated_at)
590
- VALUES (?, ?, ?)
591
- ON CONFLICT(key) DO UPDATE SET
592
- value = excluded.value,
593
- updated_at = excluded.updated_at
594
- `).run(key, value, Date.now());
595
- }
596
-
597
- /**
598
- * Get orchestrator state
599
- */
600
- getState(key: string): string | null {
601
- const row = this.db.prepare(`
602
- SELECT value FROM orchestrator_state WHERE key = ?
603
- `).get(key) as { value: string } | undefined;
604
-
605
- return row?.value ?? null;
606
- }
607
-
608
- /**
609
- * Delete orchestrator state
610
- */
611
- deleteState(key: string): void {
612
- this.db.prepare(`
613
- DELETE FROM orchestrator_state WHERE key = ?
614
- `).run(key);
615
- }
616
-
617
- /**
618
- * Get all orchestrator state
619
- */
620
- getAllState(): OrchestratorState[] {
621
- const rows = this.db.prepare(`
622
- SELECT * FROM orchestrator_state ORDER BY key
623
- `).all() as any[];
624
-
625
- return rows.map(row => ({
626
- key: row.key,
627
- value: row.value,
628
- updatedAt: row.updated_at,
629
- }));
630
- }
631
-
632
- // ==========================================================================
633
- // Pending Operations (for crash recovery)
634
- // ==========================================================================
635
-
636
- /**
637
- * Add a pending operation
638
- */
639
- addPendingOperation(
640
- operationType: 'process' | 'archive' | 'cleanup',
641
- targetId: string,
642
- payload?: string
643
- ): number {
644
- const result = this.db.prepare(`
645
- INSERT INTO pending_operations (operation_type, target_id, payload, created_at)
646
- VALUES (?, ?, ?, ?)
647
- `).run(operationType, targetId, payload ?? null, Date.now());
648
-
649
- return result.lastInsertRowid as number;
650
- }
651
-
652
- /**
653
- * Complete a pending operation (remove it)
654
- */
655
- completePendingOperation(id: number): void {
656
- this.db.prepare(`
657
- DELETE FROM pending_operations WHERE id = ?
658
- `).run(id);
659
- }
660
-
661
- /**
662
- * Fail a pending operation (increment attempts, record error)
663
- */
664
- failPendingOperation(id: number, error: string): void {
665
- this.db.prepare(`
666
- UPDATE pending_operations
667
- SET attempts = attempts + 1, last_attempt_at = ?, error = ?
668
- WHERE id = ?
669
- `).run(Date.now(), error, id);
670
- }
671
-
672
- /**
673
- * Get pending operations for recovery
674
- */
675
- getPendingOperations(maxAttempts = 5): PendingOperation[] {
676
- const rows = this.db.prepare(`
677
- SELECT * FROM pending_operations
678
- WHERE attempts < ?
679
- ORDER BY created_at ASC
680
- `).all(maxAttempts) as any[];
681
-
682
- return rows.map(row => ({
683
- id: row.id,
684
- operationType: row.operation_type,
685
- targetId: row.target_id,
686
- payload: row.payload,
687
- createdAt: row.created_at,
688
- attempts: row.attempts,
689
- lastAttemptAt: row.last_attempt_at,
690
- error: row.error,
691
- }));
692
- }
693
-
694
- /**
695
- * Cleanup old failed operations
696
- */
697
- cleanupFailedOperations(maxAge = 24 * 60 * 60 * 1000): number {
698
- const cutoff = Date.now() - maxAge;
699
- const result = this.db.prepare(`
700
- DELETE FROM pending_operations
701
- WHERE created_at < ? AND attempts >= 5
702
- `).run(cutoff);
703
-
704
- return result.changes;
705
- }
706
-
707
- /**
708
- * Close the database connection
709
- */
710
- close(): void {
711
- this.db.close();
712
- }
713
- }