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 CHANGED
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Agent Relay API",
5
- "version": "0.12.4",
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.15.1",
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.7"
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: new Date(msg.createdAt).getTime(),
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: new Date(msg.createdAt).getTime(),
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);
@@ -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().prepare(`
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().prepare(`
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().prepare("DELETE FROM automations WHERE id = ?").run(id).changes > 0;
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().prepare("SELECT * FROM automations WHERE id = ?").get(id) as any;
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().prepare("SELECT * FROM automations ORDER BY enabled DESC, next_run_at IS NULL, next_run_at ASC, name COLLATE NOCASE").all() as any[])
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().prepare(`SELECT * FROM automation_runs ${where} ORDER BY created_at DESC LIMIT ?`).all(...params) as any[])
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().prepare("SELECT * FROM automation_runs WHERE id = ?").get(id) as any;
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().prepare("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[])
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().prepare(`
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().prepare(`
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().prepare("SELECT * FROM automation_runs WHERE automation_id = ? AND status = 'scheduled' AND scheduled_for = ?")
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().prepare(`
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().prepare(`
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().prepare(`
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().prepare(`SELECT id FROM automation_runs WHERE automation_id = ? AND status IN (${placeholders}) LIMIT 1`)
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().prepare(`UPDATE automation_runs SET status = 'canceled', finished_at = ?, error = ?, updated_at = ? WHERE automation_id = ? AND status IN (${placeholders})`)
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().prepare("UPDATE automations SET next_run_at = ?, updated_at = ? WHERE id = ?").run(next ?? null, now, automation.id);
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().prepare(`
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().prepare(`
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().prepare("DELETE FROM bus_outbox WHERE timestamp < ?").run(threshold);
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().prepare("SELECT coalesce(max(seq), 0) AS cursor FROM bus_outbox").get() as { cursor: number };
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().prepare("SELECT min(seq) AS cursor FROM bus_outbox").get() as { cursor: number | null };
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
- .prepare("SELECT id FROM agents WHERE status = 'stale' AND last_seen < ? AND id NOT IN ('user', 'system')")
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) {
@@ -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().prepare(`
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().prepare("SELECT * FROM commands WHERE id = ?").get(id) as CommandRow | undefined;
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
- .prepare(`SELECT * FROM commands ${where} ORDER BY created_at DESC LIMIT ?`)
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().prepare(`
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
- .prepare(`SELECT * FROM commands WHERE expires_at IS NOT NULL AND expires_at <= ? AND status IN (${ACTIVE_STATUSES.map(() => "?").join(",")})`)
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));