agent-relay 2.0.37 → 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.
- package/dist/index.cjs +31903 -33913
- package/dist/src/cli/index.js +36 -51
- package/dist/src/cli/index.js.map +1 -1
- package/package.json +18 -19
- package/packages/api-types/package.json +1 -1
- package/packages/benchmark/package.json +4 -4
- package/packages/bridge/package.json +8 -8
- package/packages/cli-tester/package.json +1 -1
- package/packages/config/dist/project-namespace.d.ts +28 -0
- package/packages/config/dist/project-namespace.d.ts.map +1 -1
- package/packages/config/dist/project-namespace.js +42 -0
- package/packages/config/dist/project-namespace.js.map +1 -1
- package/packages/config/package.json +2 -2
- package/packages/config/src/project-namespace.ts +65 -0
- package/packages/continuity/dist/formatter.d.ts +8 -2
- package/packages/continuity/dist/formatter.d.ts.map +1 -1
- package/packages/continuity/dist/formatter.js +142 -7
- package/packages/continuity/dist/formatter.js.map +1 -1
- package/packages/continuity/dist/index.d.ts +1 -0
- package/packages/continuity/dist/index.d.ts.map +1 -1
- package/packages/continuity/dist/index.js +2 -0
- package/packages/continuity/dist/index.js.map +1 -1
- package/packages/continuity/package.json +4 -1
- package/packages/continuity/src/formatter.ts +175 -10
- package/packages/continuity/src/index.ts +3 -0
- package/packages/daemon/dist/enhanced-features.d.ts +2 -3
- package/packages/daemon/dist/enhanced-features.d.ts.map +1 -1
- package/packages/daemon/dist/enhanced-features.js +1 -0
- package/packages/daemon/dist/enhanced-features.js.map +1 -1
- package/packages/daemon/dist/index.d.ts +0 -2
- package/packages/daemon/dist/index.d.ts.map +1 -1
- package/packages/daemon/dist/index.js +0 -3
- package/packages/daemon/dist/index.js.map +1 -1
- package/packages/daemon/dist/server.d.ts +0 -6
- package/packages/daemon/dist/server.d.ts.map +1 -1
- package/packages/daemon/dist/server.js +20 -119
- package/packages/daemon/dist/server.js.map +1 -1
- package/packages/daemon/package.json +12 -14
- package/packages/daemon/src/enhanced-features.ts +4 -4
- package/packages/daemon/src/index.ts +0 -4
- package/packages/daemon/src/server.ts +19 -127
- package/packages/daemon/vitest.config.ts +9 -0
- package/packages/hooks/package.json +4 -4
- package/packages/mcp/package.json +3 -3
- package/packages/memory/package.json +2 -2
- package/packages/policy/package.json +2 -2
- package/packages/protocol/package.json +1 -1
- package/packages/resiliency/package.json +1 -1
- package/packages/sdk/package.json +2 -2
- package/packages/spawner/package.json +1 -1
- package/packages/state/package.json +1 -1
- package/packages/storage/dist/adapter.d.ts +5 -5
- package/packages/storage/dist/adapter.js +9 -9
- package/packages/storage/dist/adapter.js.map +1 -1
- package/packages/storage/package.json +2 -2
- package/packages/storage/src/adapter.ts +9 -9
- package/packages/telemetry/package.json +1 -1
- package/packages/trajectory/package.json +2 -2
- package/packages/user-directory/package.json +2 -2
- package/packages/utils/package.json +2 -2
- package/packages/wrapper/package.json +6 -6
- package/scripts/build-cjs.mjs +2 -0
- package/packages/daemon/dist/migrations/index.d.ts +0 -73
- package/packages/daemon/dist/migrations/index.d.ts.map +0 -1
- package/packages/daemon/dist/migrations/index.js +0 -241
- package/packages/daemon/dist/migrations/index.js.map +0 -1
- package/packages/daemon/dist/relay-ledger.d.ts +0 -263
- package/packages/daemon/dist/relay-ledger.d.ts.map +0 -1
- package/packages/daemon/dist/relay-ledger.js +0 -538
- package/packages/daemon/dist/relay-ledger.js.map +0 -1
- package/packages/daemon/dist/relay-watchdog.d.ts +0 -125
- package/packages/daemon/dist/relay-watchdog.d.ts.map +0 -1
- package/packages/daemon/dist/relay-watchdog.js +0 -611
- package/packages/daemon/dist/relay-watchdog.js.map +0 -1
- package/packages/daemon/src/migrations/0001_initial.sql +0 -72
- package/packages/daemon/src/migrations/index.test.ts +0 -195
- package/packages/daemon/src/migrations/index.ts +0 -286
- package/packages/daemon/src/relay-ledger.test.ts +0 -358
- package/packages/daemon/src/relay-ledger.ts +0 -713
- package/packages/daemon/src/relay-watchdog.test.ts +0 -881
- 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
|
-
}
|