agent-relay-server 0.15.1 → 0.17.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/docs/openapi.json +101 -1
- package/package.json +2 -2
- package/public/index.html +75 -3
- package/src/automations.ts +19 -18
- 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 +71 -12
- package/src/db.ts +311 -229
- package/src/insights-db.ts +6 -6
- package/src/lifecycle-manager.ts +3 -3
- package/src/maintenance.ts +25 -6
- package/src/managed-policy.ts +2 -1
- 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/routes.ts +44 -0
- package/src/security.ts +1 -1
- package/src/token-db.ts +10 -10
package/docs/openapi.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"openapi": "3.1.0",
|
|
3
3
|
"info": {
|
|
4
4
|
"title": "Agent Relay API",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.16.0",
|
|
6
6
|
"description": "Real-time message bus for inter-agent communication. Agent-first: this spec is designed for machine consumption — agents can self-discover the full API surface via GET /api/spec.",
|
|
7
7
|
"license": {
|
|
8
8
|
"name": "MIT",
|
|
@@ -5957,6 +5957,100 @@
|
|
|
5957
5957
|
]
|
|
5958
5958
|
}
|
|
5959
5959
|
},
|
|
5960
|
+
"/api/workspace-config": {
|
|
5961
|
+
"get": {
|
|
5962
|
+
"operationId": "getWorkspaceConfigRoute",
|
|
5963
|
+
"summary": "Get global workspace (isolated worktree) config",
|
|
5964
|
+
"tags": [
|
|
5965
|
+
"Other"
|
|
5966
|
+
],
|
|
5967
|
+
"description": "Returns the global workspace provisioning config `{ symlinkPaths: string[] }` — untracked files/filename patterns symlinked from the main checkout into each isolated worktree (the dashboard \"Workspace\" spawn option). Defaults seed `AGENTS.md` and `.claude-rig`; merged over defaults until set.",
|
|
5968
|
+
"responses": {
|
|
5969
|
+
"200": {
|
|
5970
|
+
"description": "Success",
|
|
5971
|
+
"content": {
|
|
5972
|
+
"application/json": {}
|
|
5973
|
+
}
|
|
5974
|
+
},
|
|
5975
|
+
"400": {
|
|
5976
|
+
"description": "Bad request",
|
|
5977
|
+
"content": {
|
|
5978
|
+
"application/json": {
|
|
5979
|
+
"schema": {
|
|
5980
|
+
"$ref": "#/components/schemas/Error"
|
|
5981
|
+
}
|
|
5982
|
+
}
|
|
5983
|
+
}
|
|
5984
|
+
}
|
|
5985
|
+
},
|
|
5986
|
+
"security": [
|
|
5987
|
+
{
|
|
5988
|
+
"bearerAuth": []
|
|
5989
|
+
},
|
|
5990
|
+
{
|
|
5991
|
+
"tokenHeader": []
|
|
5992
|
+
},
|
|
5993
|
+
{
|
|
5994
|
+
"tokenQuery": []
|
|
5995
|
+
}
|
|
5996
|
+
]
|
|
5997
|
+
},
|
|
5998
|
+
"put": {
|
|
5999
|
+
"operationId": "putWorkspaceConfigRoute",
|
|
6000
|
+
"summary": "Set global workspace (isolated worktree) config",
|
|
6001
|
+
"tags": [
|
|
6002
|
+
"Other"
|
|
6003
|
+
],
|
|
6004
|
+
"description": "Updates the global workspace config. Body: `{ value: { symlinkPaths: [...] }, updatedBy? }`. Each entry is a relative path or glob pattern; plain names match files and directories, entries with `*?[]{}` are expanded against main. A path is only linked if it exists in main — missing entries are ignored at spawn time. Rejects absolute paths and `..` traversal (400). Versioned with full config history.",
|
|
6005
|
+
"requestBody": {
|
|
6006
|
+
"required": true,
|
|
6007
|
+
"content": {
|
|
6008
|
+
"application/json": {
|
|
6009
|
+
"schema": {
|
|
6010
|
+
"type": "object",
|
|
6011
|
+
"properties": {
|
|
6012
|
+
"value": {
|
|
6013
|
+
"type": "string"
|
|
6014
|
+
},
|
|
6015
|
+
"updatedBy": {
|
|
6016
|
+
"type": "string"
|
|
6017
|
+
}
|
|
6018
|
+
}
|
|
6019
|
+
}
|
|
6020
|
+
}
|
|
6021
|
+
}
|
|
6022
|
+
},
|
|
6023
|
+
"responses": {
|
|
6024
|
+
"200": {
|
|
6025
|
+
"description": "Success",
|
|
6026
|
+
"content": {
|
|
6027
|
+
"application/json": {}
|
|
6028
|
+
}
|
|
6029
|
+
},
|
|
6030
|
+
"400": {
|
|
6031
|
+
"description": "Bad request",
|
|
6032
|
+
"content": {
|
|
6033
|
+
"application/json": {
|
|
6034
|
+
"schema": {
|
|
6035
|
+
"$ref": "#/components/schemas/Error"
|
|
6036
|
+
}
|
|
6037
|
+
}
|
|
6038
|
+
}
|
|
6039
|
+
}
|
|
6040
|
+
},
|
|
6041
|
+
"security": [
|
|
6042
|
+
{
|
|
6043
|
+
"bearerAuth": []
|
|
6044
|
+
},
|
|
6045
|
+
{
|
|
6046
|
+
"tokenHeader": []
|
|
6047
|
+
},
|
|
6048
|
+
{
|
|
6049
|
+
"tokenQuery": []
|
|
6050
|
+
}
|
|
6051
|
+
]
|
|
6052
|
+
}
|
|
6053
|
+
},
|
|
5960
6054
|
"/api/insights/config": {
|
|
5961
6055
|
"get": {
|
|
5962
6056
|
"operationId": "getInsightsConfigRoute",
|
|
@@ -6143,6 +6237,9 @@
|
|
|
6143
6237
|
},
|
|
6144
6238
|
"source": {
|
|
6145
6239
|
"type": "string"
|
|
6240
|
+
},
|
|
6241
|
+
"occurredAt": {
|
|
6242
|
+
"type": "string"
|
|
6146
6243
|
}
|
|
6147
6244
|
}
|
|
6148
6245
|
}
|
|
@@ -7217,6 +7314,9 @@
|
|
|
7217
7314
|
"maxAgeSeconds": {
|
|
7218
7315
|
"type": "string"
|
|
7219
7316
|
},
|
|
7317
|
+
"occurredAt": {
|
|
7318
|
+
"type": "string"
|
|
7319
|
+
},
|
|
7220
7320
|
"channel": {
|
|
7221
7321
|
"type": "string"
|
|
7222
7322
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Lightweight HTTP message relay for inter-agent communication across machines",
|
|
5
5
|
"module": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"CONTRIBUTING.md"
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"agent-relay-sdk": "0.2.
|
|
36
|
+
"agent-relay-sdk": "0.2.8"
|
|
37
37
|
},
|
|
38
38
|
"scripts": {
|
|
39
39
|
"prepack": "bun run build:dashboard:bundle >&2",
|
package/public/index.html
CHANGED
|
@@ -125641,6 +125641,9 @@ function chatTimestamp(iso) {
|
|
|
125641
125641
|
minute: "2-digit"
|
|
125642
125642
|
});
|
|
125643
125643
|
}
|
|
125644
|
+
function messageEventTime(msg) {
|
|
125645
|
+
return new Date(msg.occurredAt ?? msg.createdAt).getTime();
|
|
125646
|
+
}
|
|
125644
125647
|
function dateSeparatorLabel(dateStr) {
|
|
125645
125648
|
const today = /* @__PURE__ */ new Date();
|
|
125646
125649
|
const [y, m, d] = dateStr.split("-").map(Number);
|
|
@@ -126444,7 +126447,7 @@ var MessageBubble = (0, import_react.memo)(function MessageBubble({ msg, peer, o
|
|
|
126444
126447
|
const pointerStartRef = (0, import_react.useRef)(null);
|
|
126445
126448
|
const suppressBubbleClickRef = (0, import_react.useRef)(false);
|
|
126446
126449
|
const body = messageBody(msg);
|
|
126447
|
-
const time = chatTimestamp(msg.createdAt);
|
|
126450
|
+
const time = chatTimestamp(msg.occurredAt ?? msg.createdAt);
|
|
126448
126451
|
const attachments = messageAttachments(msg);
|
|
126449
126452
|
const reactions = groupedReactions(msg);
|
|
126450
126453
|
const receipt = isOutbound ? outboundReceipt(msg, peer) : null;
|
|
@@ -126580,7 +126583,7 @@ function sessionActivityStep(msg) {
|
|
|
126580
126583
|
kind: s.type,
|
|
126581
126584
|
label: typeof s.label === "string" ? s.label : void 0,
|
|
126582
126585
|
text: msg.body,
|
|
126583
|
-
ts:
|
|
126586
|
+
ts: messageEventTime(msg),
|
|
126584
126587
|
turnId: typeof s.turnId === "string" ? s.turnId : void 0
|
|
126585
126588
|
};
|
|
126586
126589
|
}
|
|
@@ -126674,7 +126677,7 @@ function buildTimeline(messages, statusEvents, createdAt, importedHistory = [])
|
|
|
126674
126677
|
continue;
|
|
126675
126678
|
}
|
|
126676
126679
|
raw.push({
|
|
126677
|
-
ts:
|
|
126680
|
+
ts: messageEventTime(msg),
|
|
126678
126681
|
entry: {
|
|
126679
126682
|
type: "message",
|
|
126680
126683
|
msg
|
|
@@ -153682,11 +153685,80 @@ function SettingsView() {
|
|
|
153682
153685
|
]
|
|
153683
153686
|
})]
|
|
153684
153687
|
}),
|
|
153688
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(WorkspaceSettings, {}),
|
|
153685
153689
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(StewardSettings, {}),
|
|
153686
153690
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(VoiceSettings, {})
|
|
153687
153691
|
]
|
|
153688
153692
|
});
|
|
153689
153693
|
}
|
|
153694
|
+
var DEFAULT_WORKSPACE = { symlinkPaths: [] };
|
|
153695
|
+
function WorkspaceSettings() {
|
|
153696
|
+
const [config, setConfig] = (0, import_react.useState)(DEFAULT_WORKSPACE);
|
|
153697
|
+
const [status, setStatus] = (0, import_react.useState)("");
|
|
153698
|
+
(0, import_react.useEffect)(() => {
|
|
153699
|
+
api("GET", "/workspace-config").then((entry) => setConfig({
|
|
153700
|
+
...DEFAULT_WORKSPACE,
|
|
153701
|
+
...entry.value
|
|
153702
|
+
})).catch((e) => setStatus(e.message));
|
|
153703
|
+
}, []);
|
|
153704
|
+
async function save() {
|
|
153705
|
+
try {
|
|
153706
|
+
const saved = await api("PUT", "/workspace-config", config);
|
|
153707
|
+
setConfig({
|
|
153708
|
+
...DEFAULT_WORKSPACE,
|
|
153709
|
+
...saved.value
|
|
153710
|
+
});
|
|
153711
|
+
setStatus("Workspace config saved");
|
|
153712
|
+
} catch (e) {
|
|
153713
|
+
setStatus(e.message);
|
|
153714
|
+
}
|
|
153715
|
+
}
|
|
153716
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", {
|
|
153717
|
+
className: "space-y-3 rounded-lg border p-4",
|
|
153718
|
+
children: [
|
|
153719
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
153720
|
+
className: "flex items-center gap-2",
|
|
153721
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(FolderSymlink, { className: "w-4 h-4" }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", {
|
|
153722
|
+
className: "text-sm font-semibold",
|
|
153723
|
+
children: "Isolated worktree symlinks"
|
|
153724
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", {
|
|
153725
|
+
className: "text-xs text-muted-foreground",
|
|
153726
|
+
children: [
|
|
153727
|
+
"Untracked files or filename patterns symlinked from main into each isolated worktree (the \"Workspace\" spawn option), one per line. Plain names like ",
|
|
153728
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: "AGENTS.md" }),
|
|
153729
|
+
" or ",
|
|
153730
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: ".claude-rig" }),
|
|
153731
|
+
" match files and directories; entries with ",
|
|
153732
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("code", { children: ["*?[]", "{}"] }),
|
|
153733
|
+
" are expanded as globs. A path is only linked if it exists in main."
|
|
153734
|
+
]
|
|
153735
|
+
})] })]
|
|
153736
|
+
}),
|
|
153737
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, {
|
|
153738
|
+
label: "Paths / patterns",
|
|
153739
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Textarea, {
|
|
153740
|
+
rows: 5,
|
|
153741
|
+
placeholder: "AGENTS.md\n.claude-rig",
|
|
153742
|
+
value: config.symlinkPaths.join("\n"),
|
|
153743
|
+
onChange: (e) => setConfig({
|
|
153744
|
+
...config,
|
|
153745
|
+
symlinkPaths: lines(e.target.value)
|
|
153746
|
+
})
|
|
153747
|
+
})
|
|
153748
|
+
}),
|
|
153749
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
153750
|
+
className: "flex items-center gap-2 pt-1",
|
|
153751
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button, {
|
|
153752
|
+
onClick: save,
|
|
153753
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Save, { className: "w-4 h-4" }), "Save"]
|
|
153754
|
+
}), status && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
|
|
153755
|
+
className: "text-sm text-muted-foreground",
|
|
153756
|
+
children: status
|
|
153757
|
+
})]
|
|
153758
|
+
})
|
|
153759
|
+
]
|
|
153760
|
+
});
|
|
153761
|
+
}
|
|
153690
153762
|
function VoiceSettings() {
|
|
153691
153763
|
const voiceInputMode = useRelayStore((s) => s.voiceInputMode);
|
|
153692
153764
|
const setVoiceInputMode = useRelayStore((s) => s.setVoiceInputMode);
|
package/src/automations.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
ValidationError,
|
|
13
13
|
} from "./db";
|
|
14
14
|
import { createCommand } from "./commands-db";
|
|
15
|
-
import { getAgentProfile, getSpawnPolicy } from "./config-store";
|
|
15
|
+
import { getAgentProfile, getSpawnPolicy, workspaceSpawnParams } from "./config-store";
|
|
16
16
|
import { resolveProviderSelection, type ProviderEffort } from "agent-relay-sdk/provider-catalog";
|
|
17
17
|
import { runnerRuntimeTokenEnv } from "./runtime-tokens";
|
|
18
18
|
import type {
|
|
@@ -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
|
|
@@ -559,6 +559,7 @@ function dispatchOnDemandAutomation(
|
|
|
559
559
|
effort: selection.effort,
|
|
560
560
|
profile: policy.profile,
|
|
561
561
|
agentProfile,
|
|
562
|
+
...workspaceSpawnParams(),
|
|
562
563
|
cwd: policy.cwd || orchestrator.baseDir,
|
|
563
564
|
workspaceMode: policy.workspaceMode ?? "inherit",
|
|
564
565
|
label,
|
|
@@ -629,7 +630,7 @@ function createRunTask(
|
|
|
629
630
|
|
|
630
631
|
function insertRun(automation: Automation, scheduledFor: number, now: number): AutomationRun {
|
|
631
632
|
const id = randomUUID();
|
|
632
|
-
db().
|
|
633
|
+
db().query(`
|
|
633
634
|
INSERT INTO automation_runs (id, automation_id, status, scheduled_for, orchestrator_id, meta, created_at, updated_at)
|
|
634
635
|
VALUES (?, ?, 'dispatching', ?, ?, '{}', ?, ?)
|
|
635
636
|
`).run(id, automation.id, scheduledFor, automation.orchestratorId, now, now);
|
|
@@ -637,11 +638,11 @@ function insertRun(automation: Automation, scheduledFor: number, now: number): A
|
|
|
637
638
|
}
|
|
638
639
|
|
|
639
640
|
function enqueueAutomationRun(automation: Automation, scheduledFor: number, now: number): AutomationRun {
|
|
640
|
-
const existing = db().
|
|
641
|
+
const existing = db().query("SELECT * FROM automation_runs WHERE automation_id = ? AND status = 'scheduled' AND scheduled_for = ?")
|
|
641
642
|
.get(automation.id, scheduledFor) as any;
|
|
642
643
|
if (existing) return rowToAutomationRun(existing);
|
|
643
644
|
const id = randomUUID();
|
|
644
|
-
db().
|
|
645
|
+
db().query(`
|
|
645
646
|
INSERT INTO automation_runs (id, automation_id, status, scheduled_for, orchestrator_id, meta, created_at, updated_at)
|
|
646
647
|
VALUES (?, ?, 'scheduled', ?, ?, '{}', ?, ?)
|
|
647
648
|
`).run(id, automation.id, scheduledFor, automation.orchestratorId, now, now);
|
|
@@ -649,7 +650,7 @@ function enqueueAutomationRun(automation: Automation, scheduledFor: number, now:
|
|
|
649
650
|
}
|
|
650
651
|
|
|
651
652
|
function dispatchQueuedAutomationRuns(now: number): AutomationDispatchResult[] {
|
|
652
|
-
const queued = (db().
|
|
653
|
+
const queued = (db().query(`
|
|
653
654
|
SELECT r.* FROM automation_runs r
|
|
654
655
|
JOIN automations a ON a.id = r.automation_id
|
|
655
656
|
WHERE r.status = 'scheduled' AND a.enabled = 1
|
|
@@ -680,7 +681,7 @@ function updateRun(id: string, input: {
|
|
|
680
681
|
}, now: number): void {
|
|
681
682
|
const current = getAutomationRun(id);
|
|
682
683
|
const meta = input.meta !== undefined ? input.meta : current?.meta;
|
|
683
|
-
db().
|
|
684
|
+
db().query(`
|
|
684
685
|
UPDATE automation_runs
|
|
685
686
|
SET status = COALESCE(?, status),
|
|
686
687
|
started_at = COALESCE(?, started_at),
|
|
@@ -715,20 +716,20 @@ function updateRun(id: string, input: {
|
|
|
715
716
|
|
|
716
717
|
function hasActiveRun(automationId: string): boolean {
|
|
717
718
|
const placeholders = [...BLOCKING_RUN_STATUSES].map(() => "?").join(",");
|
|
718
|
-
const row = db().
|
|
719
|
+
const row = db().query(`SELECT id FROM automation_runs WHERE automation_id = ? AND status IN (${placeholders}) LIMIT 1`)
|
|
719
720
|
.get(automationId, ...BLOCKING_RUN_STATUSES) as any;
|
|
720
721
|
return Boolean(row);
|
|
721
722
|
}
|
|
722
723
|
|
|
723
724
|
function cancelActiveRuns(automationId: string, now: number, reason: string): void {
|
|
724
725
|
const placeholders = [...OPEN_RUN_STATUSES].map(() => "?").join(",");
|
|
725
|
-
db().
|
|
726
|
+
db().query(`UPDATE automation_runs SET status = 'canceled', finished_at = ?, error = ?, updated_at = ? WHERE automation_id = ? AND status IN (${placeholders})`)
|
|
726
727
|
.run(now, reason, now, automationId, ...OPEN_RUN_STATUSES);
|
|
727
728
|
}
|
|
728
729
|
|
|
729
730
|
function rescheduleAutomation(automation: Automation, now: number): void {
|
|
730
731
|
const next = automation.enabled ? nextScheduledAt(automation.schedule, automation.timezone, now) : undefined;
|
|
731
|
-
db().
|
|
732
|
+
db().query("UPDATE automations SET next_run_at = ?, updated_at = ? WHERE id = ?").run(next ?? null, now, automation.id);
|
|
732
733
|
}
|
|
733
734
|
|
|
734
735
|
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));
|