agent-relay-server 0.15.0 → 0.16.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/package.json +1 -1
- package/src/automations.ts +17 -17
- package/src/bus-outbox.ts +5 -5
- package/src/bus.ts +1 -1
- package/src/commands-db.ts +5 -5
- package/src/config-store.ts +12 -12
- package/src/db.ts +294 -226
- package/src/insights-db.ts +6 -6
- package/src/lifecycle-manager.ts +3 -3
- package/src/maintenance.ts +25 -6
- package/src/memory-sqlite-broker.ts +12 -12
- package/src/provider-catalog-store.ts +2 -2
- package/src/recipe-db.ts +9 -9
- package/src/security.ts +1 -1
- package/src/token-db.ts +10 -10
package/package.json
CHANGED
package/src/automations.ts
CHANGED
|
@@ -284,7 +284,7 @@ export function createAutomation(input: CreateAutomationInput, now = Date.now())
|
|
|
284
284
|
if (!getOrchestrator(normalized.orchestratorId)) throw new ValidationError(`orchestrator ${normalized.orchestratorId} not found`);
|
|
285
285
|
const id = randomUUID();
|
|
286
286
|
const nextRunAt = normalized.enabled ? nextScheduledAt(normalized.schedule, normalized.timezone, now) : undefined;
|
|
287
|
-
db().
|
|
287
|
+
db().query(`
|
|
288
288
|
INSERT INTO automations (id, kind, name, description, enabled, schedule, timezone, next_run_at, catch_up_policy, concurrency_policy, orchestrator_id, target_policy, task_template, created_at, updated_at)
|
|
289
289
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
290
290
|
`).run(
|
|
@@ -327,7 +327,7 @@ export function updateAutomation(id: string, input: UpdateAutomationInput, now =
|
|
|
327
327
|
const normalized = normalizeCreateInput(next);
|
|
328
328
|
if (!getOrchestrator(normalized.orchestratorId)) throw new ValidationError(`orchestrator ${normalized.orchestratorId} not found`);
|
|
329
329
|
const nextRunAt = normalized.enabled ? nextScheduledAt(normalized.schedule, normalized.timezone, now) : undefined;
|
|
330
|
-
db().
|
|
330
|
+
db().query(`
|
|
331
331
|
UPDATE automations
|
|
332
332
|
SET name = ?, description = ?, enabled = ?, schedule = ?, timezone = ?, next_run_at = ?,
|
|
333
333
|
catch_up_policy = ?, concurrency_policy = ?, orchestrator_id = ?, target_policy = ?, task_template = ?, updated_at = ?
|
|
@@ -352,18 +352,18 @@ export function updateAutomation(id: string, input: UpdateAutomationInput, now =
|
|
|
352
352
|
|
|
353
353
|
export function deleteAutomation(id: string): boolean {
|
|
354
354
|
ensureAutomationTables();
|
|
355
|
-
return db().
|
|
355
|
+
return db().query("DELETE FROM automations WHERE id = ?").run(id).changes > 0;
|
|
356
356
|
}
|
|
357
357
|
|
|
358
358
|
export function getAutomation(id: string): Automation | null {
|
|
359
359
|
ensureAutomationTables();
|
|
360
|
-
const row = db().
|
|
360
|
+
const row = db().query("SELECT * FROM automations WHERE id = ?").get(id) as any;
|
|
361
361
|
return row ? rowToAutomation(row) : null;
|
|
362
362
|
}
|
|
363
363
|
|
|
364
364
|
export function listAutomations(): Automation[] {
|
|
365
365
|
ensureAutomationTables();
|
|
366
|
-
return (db().
|
|
366
|
+
return (db().query("SELECT * FROM automations ORDER BY enabled DESC, next_run_at IS NULL, next_run_at ASC, name COLLATE NOCASE").all() as any[])
|
|
367
367
|
.map(rowToAutomation);
|
|
368
368
|
}
|
|
369
369
|
|
|
@@ -381,20 +381,20 @@ export function listAutomationRuns(filter: { automationId?: string; status?: str
|
|
|
381
381
|
}
|
|
382
382
|
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
383
383
|
params.push(filter.limit ?? 100);
|
|
384
|
-
return (db().
|
|
384
|
+
return (db().query(`SELECT * FROM automation_runs ${where} ORDER BY created_at DESC LIMIT ?`).all(...params) as any[])
|
|
385
385
|
.map(rowToAutomationRun);
|
|
386
386
|
}
|
|
387
387
|
|
|
388
388
|
function getAutomationRun(id: string): AutomationRun | null {
|
|
389
389
|
ensureAutomationTables();
|
|
390
|
-
const row = db().
|
|
390
|
+
const row = db().query("SELECT * FROM automation_runs WHERE id = ?").get(id) as any;
|
|
391
391
|
return row ? rowToAutomationRun(row) : null;
|
|
392
392
|
}
|
|
393
393
|
|
|
394
394
|
export function runDueAutomations(now = Date.now()): AutomationDispatchResult[] {
|
|
395
395
|
ensureAutomationTables();
|
|
396
396
|
const results = dispatchQueuedAutomationRuns(now);
|
|
397
|
-
const due = (db().
|
|
397
|
+
const due = (db().query("SELECT * FROM automations WHERE enabled = 1 AND next_run_at IS NOT NULL AND next_run_at <= ? ORDER BY next_run_at ASC LIMIT 25").all(now) as any[])
|
|
398
398
|
.map(rowToAutomation);
|
|
399
399
|
for (const automation of due) {
|
|
400
400
|
if (hasActiveRun(automation.id)) {
|
|
@@ -421,7 +421,7 @@ export function runAutomationNow(id: string, now = Date.now()): AutomationDispat
|
|
|
421
421
|
|
|
422
422
|
export function reconcileAutomationRuns(now = Date.now()): AutomationReconcileResult[] {
|
|
423
423
|
ensureAutomationTables();
|
|
424
|
-
const rows = db().
|
|
424
|
+
const rows = db().query(`
|
|
425
425
|
SELECT * FROM automation_runs
|
|
426
426
|
WHERE status IN ('scheduled', 'dispatching', 'waiting_agent', 'running') AND task_id IS NOT NULL
|
|
427
427
|
ORDER BY created_at ASC
|
|
@@ -629,7 +629,7 @@ function createRunTask(
|
|
|
629
629
|
|
|
630
630
|
function insertRun(automation: Automation, scheduledFor: number, now: number): AutomationRun {
|
|
631
631
|
const id = randomUUID();
|
|
632
|
-
db().
|
|
632
|
+
db().query(`
|
|
633
633
|
INSERT INTO automation_runs (id, automation_id, status, scheduled_for, orchestrator_id, meta, created_at, updated_at)
|
|
634
634
|
VALUES (?, ?, 'dispatching', ?, ?, '{}', ?, ?)
|
|
635
635
|
`).run(id, automation.id, scheduledFor, automation.orchestratorId, now, now);
|
|
@@ -637,11 +637,11 @@ function insertRun(automation: Automation, scheduledFor: number, now: number): A
|
|
|
637
637
|
}
|
|
638
638
|
|
|
639
639
|
function enqueueAutomationRun(automation: Automation, scheduledFor: number, now: number): AutomationRun {
|
|
640
|
-
const existing = db().
|
|
640
|
+
const existing = db().query("SELECT * FROM automation_runs WHERE automation_id = ? AND status = 'scheduled' AND scheduled_for = ?")
|
|
641
641
|
.get(automation.id, scheduledFor) as any;
|
|
642
642
|
if (existing) return rowToAutomationRun(existing);
|
|
643
643
|
const id = randomUUID();
|
|
644
|
-
db().
|
|
644
|
+
db().query(`
|
|
645
645
|
INSERT INTO automation_runs (id, automation_id, status, scheduled_for, orchestrator_id, meta, created_at, updated_at)
|
|
646
646
|
VALUES (?, ?, 'scheduled', ?, ?, '{}', ?, ?)
|
|
647
647
|
`).run(id, automation.id, scheduledFor, automation.orchestratorId, now, now);
|
|
@@ -649,7 +649,7 @@ function enqueueAutomationRun(automation: Automation, scheduledFor: number, now:
|
|
|
649
649
|
}
|
|
650
650
|
|
|
651
651
|
function dispatchQueuedAutomationRuns(now: number): AutomationDispatchResult[] {
|
|
652
|
-
const queued = (db().
|
|
652
|
+
const queued = (db().query(`
|
|
653
653
|
SELECT r.* FROM automation_runs r
|
|
654
654
|
JOIN automations a ON a.id = r.automation_id
|
|
655
655
|
WHERE r.status = 'scheduled' AND a.enabled = 1
|
|
@@ -680,7 +680,7 @@ function updateRun(id: string, input: {
|
|
|
680
680
|
}, now: number): void {
|
|
681
681
|
const current = getAutomationRun(id);
|
|
682
682
|
const meta = input.meta !== undefined ? input.meta : current?.meta;
|
|
683
|
-
db().
|
|
683
|
+
db().query(`
|
|
684
684
|
UPDATE automation_runs
|
|
685
685
|
SET status = COALESCE(?, status),
|
|
686
686
|
started_at = COALESCE(?, started_at),
|
|
@@ -715,20 +715,20 @@ function updateRun(id: string, input: {
|
|
|
715
715
|
|
|
716
716
|
function hasActiveRun(automationId: string): boolean {
|
|
717
717
|
const placeholders = [...BLOCKING_RUN_STATUSES].map(() => "?").join(",");
|
|
718
|
-
const row = db().
|
|
718
|
+
const row = db().query(`SELECT id FROM automation_runs WHERE automation_id = ? AND status IN (${placeholders}) LIMIT 1`)
|
|
719
719
|
.get(automationId, ...BLOCKING_RUN_STATUSES) as any;
|
|
720
720
|
return Boolean(row);
|
|
721
721
|
}
|
|
722
722
|
|
|
723
723
|
function cancelActiveRuns(automationId: string, now: number, reason: string): void {
|
|
724
724
|
const placeholders = [...OPEN_RUN_STATUSES].map(() => "?").join(",");
|
|
725
|
-
db().
|
|
725
|
+
db().query(`UPDATE automation_runs SET status = 'canceled', finished_at = ?, error = ?, updated_at = ? WHERE automation_id = ? AND status IN (${placeholders})`)
|
|
726
726
|
.run(now, reason, now, automationId, ...OPEN_RUN_STATUSES);
|
|
727
727
|
}
|
|
728
728
|
|
|
729
729
|
function rescheduleAutomation(automation: Automation, now: number): void {
|
|
730
730
|
const next = automation.enabled ? nextScheduledAt(automation.schedule, automation.timezone, now) : undefined;
|
|
731
|
-
db().
|
|
731
|
+
db().query("UPDATE automations SET next_run_at = ?, updated_at = ? WHERE id = ?").run(next ?? null, now, automation.id);
|
|
732
732
|
}
|
|
733
733
|
|
|
734
734
|
function requireOnlineOrchestrator(orchestratorId: string): Orchestrator {
|
package/src/bus-outbox.ts
CHANGED
|
@@ -20,7 +20,7 @@ interface OutboxRow {
|
|
|
20
20
|
|
|
21
21
|
export function appendEvent(type: string, source: string, data: unknown, subject?: string): number {
|
|
22
22
|
const timestamp = Date.now();
|
|
23
|
-
const result = getDb().
|
|
23
|
+
const result = getDb().query(`
|
|
24
24
|
INSERT INTO bus_outbox (event_type, source, subject, data, timestamp)
|
|
25
25
|
VALUES (?, ?, ?, ?, ?)
|
|
26
26
|
`).run(type, source, subject ?? null, JSON.stringify(data ?? {}), timestamp);
|
|
@@ -28,7 +28,7 @@ export function appendEvent(type: string, source: string, data: unknown, subject
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export function replayEvents(since: number, limit = 500): BusEvent[] {
|
|
31
|
-
const rows = getDb().
|
|
31
|
+
const rows = getDb().query(`
|
|
32
32
|
SELECT seq, event_type, source, subject, data, timestamp
|
|
33
33
|
FROM bus_outbox
|
|
34
34
|
WHERE seq > ?
|
|
@@ -40,17 +40,17 @@ export function replayEvents(since: number, limit = 500): BusEvent[] {
|
|
|
40
40
|
|
|
41
41
|
export function pruneOutbox(retentionMs = 60 * 60 * 1000): number {
|
|
42
42
|
const threshold = Date.now() - retentionMs;
|
|
43
|
-
const result = getDb().
|
|
43
|
+
const result = getDb().query("DELETE FROM bus_outbox WHERE timestamp < ?").run(threshold);
|
|
44
44
|
return result.changes;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
export function getOutboxCursor(): number {
|
|
48
|
-
const row = getDb().
|
|
48
|
+
const row = getDb().query("SELECT coalesce(max(seq), 0) AS cursor FROM bus_outbox").get() as { cursor: number };
|
|
49
49
|
return row.cursor;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
export function getOldestOutboxCursor(): number {
|
|
53
|
-
const row = getDb().
|
|
53
|
+
const row = getDb().query("SELECT min(seq) AS cursor FROM bus_outbox").get() as { cursor: number | null };
|
|
54
54
|
return row.cursor ?? 0;
|
|
55
55
|
}
|
|
56
56
|
|
package/src/bus.ts
CHANGED
|
@@ -99,7 +99,7 @@ export function getBusConnectionCount(): number {
|
|
|
99
99
|
export function expireStaleBusAgents(graceMs = Number(process.env.AGENT_RELAY_STALE_GRACE_MS) || 120_000): { agentIds: string[]; orphanedTasks: Task[] } {
|
|
100
100
|
const cutoff = Date.now() - graceMs;
|
|
101
101
|
const rows = getDb()
|
|
102
|
-
.
|
|
102
|
+
.query("SELECT id FROM agents WHERE status = 'stale' AND last_seen < ? AND id NOT IN ('user', 'system')")
|
|
103
103
|
.all(cutoff) as Array<{ id: string }>;
|
|
104
104
|
const orphanedTasks: Task[] = [];
|
|
105
105
|
for (const row of rows) {
|
package/src/commands-db.ts
CHANGED
|
@@ -41,7 +41,7 @@ export function createCommand(input: CreateCommandInput): Command {
|
|
|
41
41
|
updatedAt: now,
|
|
42
42
|
expiresAt: input.ttlMs ? now + input.ttlMs : defaultExpiresAt(input.type, now),
|
|
43
43
|
};
|
|
44
|
-
getDb().
|
|
44
|
+
getDb().query(`
|
|
45
45
|
INSERT INTO commands (id, type, source, target, params, status, correlation_id, created_at, updated_at, expires_at)
|
|
46
46
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
47
47
|
`).run(
|
|
@@ -60,7 +60,7 @@ export function createCommand(input: CreateCommandInput): Command {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
export function getCommand(id: string): Command | null {
|
|
63
|
-
const row = getDb().
|
|
63
|
+
const row = getDb().query("SELECT * FROM commands WHERE id = ?").get(id) as CommandRow | undefined;
|
|
64
64
|
return row ? rowToCommand(row) : null;
|
|
65
65
|
}
|
|
66
66
|
|
|
@@ -87,7 +87,7 @@ export function listCommands(filters: CommandFilters = {}): Command[] {
|
|
|
87
87
|
const limit = Math.min(Math.max(filters.limit ?? 100, 1), 500);
|
|
88
88
|
const bindings = [...params, limit] as any[];
|
|
89
89
|
const rows = getDb()
|
|
90
|
-
.
|
|
90
|
+
.query(`SELECT * FROM commands ${where} ORDER BY created_at DESC LIMIT ?`)
|
|
91
91
|
.all(...bindings) as CommandRow[];
|
|
92
92
|
return rows.map(rowToCommand);
|
|
93
93
|
}
|
|
@@ -97,7 +97,7 @@ export function updateCommand(id: string, input: UpdateCommandInput): Command |
|
|
|
97
97
|
if (!existing) return null;
|
|
98
98
|
const nextStatus = input.status ?? existing.status;
|
|
99
99
|
const now = Date.now();
|
|
100
|
-
getDb().
|
|
100
|
+
getDb().query(`
|
|
101
101
|
UPDATE commands
|
|
102
102
|
SET status = ?, result = ?, error = ?, updated_at = ?
|
|
103
103
|
WHERE id = ?
|
|
@@ -121,7 +121,7 @@ export function deleteCommand(id: string): boolean {
|
|
|
121
121
|
|
|
122
122
|
export function expireCommands(now: number = Date.now()): Command[] {
|
|
123
123
|
const rows = getDb()
|
|
124
|
-
.
|
|
124
|
+
.query(`SELECT * FROM commands WHERE expires_at IS NOT NULL AND expires_at <= ? AND status IN (${ACTIVE_STATUSES.map(() => "?").join(",")})`)
|
|
125
125
|
.all(now, ...ACTIVE_STATUSES) as CommandRow[];
|
|
126
126
|
for (const row of rows) updateCommand(row.id, { status: "timed_out", error: "command timed out" });
|
|
127
127
|
return rows.map((row) => getCommand(row.id)).filter((command): command is Command => Boolean(command));
|
package/src/config-store.ts
CHANGED
|
@@ -483,13 +483,13 @@ function normalizeValue(namespace: string, key: string, value: unknown): unknown
|
|
|
483
483
|
}
|
|
484
484
|
|
|
485
485
|
export function getConfig<T = unknown>(namespace: string, key: string): ConfigEntry<T> | null {
|
|
486
|
-
const row = getDb().
|
|
486
|
+
const row = getDb().query("SELECT * FROM config WHERE namespace = ? AND key = ?").get(namespace, key) as ConfigRow | undefined;
|
|
487
487
|
return row ? rowToConfigEntry<T>(row) : null;
|
|
488
488
|
}
|
|
489
489
|
|
|
490
490
|
export function listConfig<T = unknown>(namespace: string): ConfigEntry<T>[] {
|
|
491
491
|
const rows = getDb()
|
|
492
|
-
.
|
|
492
|
+
.query("SELECT * FROM config WHERE namespace = ? ORDER BY key ASC")
|
|
493
493
|
.all(namespace) as ConfigRow[];
|
|
494
494
|
return rows.map(rowToConfigEntry<T>);
|
|
495
495
|
}
|
|
@@ -502,12 +502,12 @@ export function setConfig<T = unknown>(namespace: string, key: string, value: T,
|
|
|
502
502
|
|
|
503
503
|
getDb().transaction(() => {
|
|
504
504
|
if (existing) {
|
|
505
|
-
getDb().
|
|
505
|
+
getDb().query(`
|
|
506
506
|
INSERT INTO config_history (namespace, key, value, version, changed_at, changed_by)
|
|
507
507
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
508
508
|
`).run(existing.namespace, existing.key, JSON.stringify(existing.value), existing.version, now, updatedBy ?? null);
|
|
509
509
|
}
|
|
510
|
-
getDb().
|
|
510
|
+
getDb().query(`
|
|
511
511
|
INSERT INTO config (namespace, key, value, version, updated_at, updated_by)
|
|
512
512
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
513
513
|
ON CONFLICT(namespace, key) DO UPDATE SET
|
|
@@ -527,11 +527,11 @@ export function deleteConfig(namespace: string, key: string, updatedBy?: string)
|
|
|
527
527
|
if (!existing) return false;
|
|
528
528
|
const now = new Date().toISOString();
|
|
529
529
|
getDb().transaction(() => {
|
|
530
|
-
getDb().
|
|
530
|
+
getDb().query(`
|
|
531
531
|
INSERT INTO config_history (namespace, key, value, version, changed_at, changed_by)
|
|
532
532
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
533
533
|
`).run(existing.namespace, existing.key, JSON.stringify(existing.value), existing.version, now, updatedBy ?? null);
|
|
534
|
-
getDb().
|
|
534
|
+
getDb().query("DELETE FROM config WHERE namespace = ? AND key = ?").run(namespace, key);
|
|
535
535
|
pruneConfigHistory(namespace, key);
|
|
536
536
|
})();
|
|
537
537
|
return true;
|
|
@@ -540,13 +540,13 @@ export function deleteConfig(namespace: string, key: string, updatedBy?: string)
|
|
|
540
540
|
export function getConfigHistory<T = unknown>(namespace: string, key: string, limit = CONFIG_HISTORY_LIMIT): ConfigHistoryEntry<T>[] {
|
|
541
541
|
const safeLimit = Math.min(Math.max(limit, 1), 500);
|
|
542
542
|
const rows = getDb()
|
|
543
|
-
.
|
|
543
|
+
.query("SELECT * FROM config_history WHERE namespace = ? AND key = ? ORDER BY version DESC, id DESC LIMIT ?")
|
|
544
544
|
.all(namespace, key, safeLimit) as ConfigHistoryRow[];
|
|
545
545
|
return rows.map(rowToConfigHistoryEntry<T>);
|
|
546
546
|
}
|
|
547
547
|
|
|
548
548
|
function pruneConfigHistory(namespace: string, key: string): void {
|
|
549
|
-
getDb().
|
|
549
|
+
getDb().query(`
|
|
550
550
|
DELETE FROM config_history
|
|
551
551
|
WHERE namespace = ? AND key = ? AND id NOT IN (
|
|
552
552
|
SELECT id FROM config_history
|
|
@@ -649,13 +649,13 @@ export function deleteAgentProfile(name: string, updatedBy?: string): boolean {
|
|
|
649
649
|
}
|
|
650
650
|
|
|
651
651
|
export function getManagedAgentState(policyName: string): ManagedAgentState | null {
|
|
652
|
-
const row = getDb().
|
|
652
|
+
const row = getDb().query("SELECT * FROM managed_agent_state WHERE policy_name = ?").get(policyName) as ManagedAgentStateRow | undefined;
|
|
653
653
|
return row ? rowToManagedAgentState(row) : null;
|
|
654
654
|
}
|
|
655
655
|
|
|
656
656
|
function listManagedAgentStates(): ManagedAgentState[] {
|
|
657
657
|
const rows = getDb()
|
|
658
|
-
.
|
|
658
|
+
.query("SELECT * FROM managed_agent_state ORDER BY policy_name ASC")
|
|
659
659
|
.all() as ManagedAgentStateRow[];
|
|
660
660
|
return rows.map(rowToManagedAgentState);
|
|
661
661
|
}
|
|
@@ -664,7 +664,7 @@ export function upsertManagedAgentState(input: ManagedAgentStateInput): ManagedA
|
|
|
664
664
|
if (!VALID_MANAGED_STATUSES.includes(input.status)) throw new ValidationError("status must be a managed-agent status");
|
|
665
665
|
if (!VALID_PROVIDERS.includes(input.provider)) throw new ValidationError("provider must be claude or codex");
|
|
666
666
|
const now = input.updatedAt ?? Date.now();
|
|
667
|
-
getDb().
|
|
667
|
+
getDb().query(`
|
|
668
668
|
INSERT INTO managed_agent_state (
|
|
669
669
|
policy_name, status, agent_id, orchestrator_id, provider, tmux_session, spawn_request_id, workspace_id, workspace_path, workspace_branch,
|
|
670
670
|
last_spawn_at, last_stop_at, healthy_since, restart_count, consecutive_failures,
|
|
@@ -724,6 +724,6 @@ export function updateManagedAgentState(policyName: string, patch: ManagedAgentS
|
|
|
724
724
|
}
|
|
725
725
|
|
|
726
726
|
function deleteManagedAgentState(policyName: string): boolean {
|
|
727
|
-
const result = getDb().
|
|
727
|
+
const result = getDb().query("DELETE FROM managed_agent_state WHERE policy_name = ?").run(policyName);
|
|
728
728
|
return result.changes > 0;
|
|
729
729
|
}
|