agent-relay-server 0.15.1 → 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/src/db.ts CHANGED
@@ -76,6 +76,59 @@ import { STALE_TTL_MS, DAY_MS, CLAIM_LEASE_MS, POOL_CLAIM_LEASE_MS, WORKSPACE_ME
76
76
  let db: Database;
77
77
  const CONTEXT_SNAPSHOT_DEBOUNCE_MS = 60_000;
78
78
 
79
+ // Connection-scoped pragmas. journal_mode and auto_vacuum persist in the file;
80
+ // the rest reset per connection, so they must be re-applied on every open.
81
+ function applyConnectionPragmas(conn: Database): void {
82
+ conn.run("PRAGMA journal_mode = WAL");
83
+ // Wait up to 5s for a held lock instead of failing instantly with SQLITE_BUSY.
84
+ // Matters with many agents polling/writing concurrently.
85
+ conn.run("PRAGMA busy_timeout = 5000");
86
+ // NORMAL is the recommended durability level under WAL: as safe as FULL except
87
+ // for a power loss mid-checkpoint, and meaningfully faster on writes.
88
+ conn.run("PRAGMA synchronous = NORMAL");
89
+ conn.run("PRAGMA foreign_keys = ON");
90
+ // ~8MB page cache (negative = KiB) and 256MB mmap window for the read-heavy
91
+ // poll workload. Both are best-effort; SQLite ignores them if unsupported.
92
+ conn.run("PRAGMA cache_size = -8000");
93
+ conn.run("PRAGMA mmap_size = 268435456");
94
+ }
95
+
96
+ // Slow-query log. Off unless AGENT_RELAY_DB_SLOW_MS is set (>0). Wraps the
97
+ // execution of the heavy dynamic queries so the next slow query surfaces before
98
+ // it wedges something (see issue #197 — the 8.6s reply-obligation query).
99
+ const SLOW_QUERY_MS = Number(process.env.AGENT_RELAY_DB_SLOW_MS) || 0;
100
+ function timedQuery<T>(label: string, run: () => T): T {
101
+ if (SLOW_QUERY_MS <= 0) return run();
102
+ const start = performance.now();
103
+ try {
104
+ return run();
105
+ } finally {
106
+ const ms = performance.now() - start;
107
+ if (ms >= SLOW_QUERY_MS) {
108
+ console.warn(`[db.slow] ${label} took ${ms.toFixed(1)}ms (threshold ${SLOW_QUERY_MS}ms)`);
109
+ }
110
+ }
111
+ }
112
+
113
+ // Periodic DB upkeep, invoked by the maintenance scheduler. ANALYZE refreshes
114
+ // planner statistics, PRAGMA optimize applies them, wal_checkpoint(TRUNCATE)
115
+ // bounds WAL growth, and VACUUM (opt-in) reclaims space since auto_vacuum is off.
116
+ export function runDbMaintenance(options: { vacuum?: boolean } = {}): {
117
+ analyzed: boolean;
118
+ checkpointed: boolean;
119
+ vacuumed: boolean;
120
+ } {
121
+ db.run("ANALYZE");
122
+ db.run("PRAGMA optimize");
123
+ db.run("PRAGMA wal_checkpoint(TRUNCATE)");
124
+ let vacuumed = false;
125
+ if (options.vacuum) {
126
+ db.run("VACUUM");
127
+ vacuumed = true;
128
+ }
129
+ return { analyzed: true, checkpointed: true, vacuumed };
130
+ }
131
+
79
132
  const REACTION_EMOJI_ALIASES: Record<string, string> = {
80
133
  "+1": "👍",
81
134
  ":+1:": "👍",
@@ -110,7 +163,7 @@ export function normalizeReactionEmoji(value: string): string {
110
163
  }
111
164
 
112
165
  function normalizeExistingMessageReactions(): void {
113
- const rows = db.prepare("SELECT message_id, actor_id, emoji, created_at, updated_at FROM message_reactions").all() as Array<{
166
+ const rows = db.query("SELECT message_id, actor_id, emoji, created_at, updated_at FROM message_reactions").all() as Array<{
114
167
  message_id: number;
115
168
  actor_id: string;
116
169
  emoji: string;
@@ -118,14 +171,14 @@ function normalizeExistingMessageReactions(): void {
118
171
  updated_at: number;
119
172
  }>;
120
173
  const migrate = db.transaction((items: typeof rows) => {
121
- const upsert = db.prepare(`
174
+ const upsert = db.query(`
122
175
  INSERT INTO message_reactions (message_id, actor_id, emoji, created_at, updated_at)
123
176
  VALUES (?, ?, ?, ?, ?)
124
177
  ON CONFLICT(message_id, actor_id, emoji) DO UPDATE SET
125
178
  created_at = min(message_reactions.created_at, excluded.created_at),
126
179
  updated_at = max(message_reactions.updated_at, excluded.updated_at)
127
180
  `);
128
- const remove = db.prepare("DELETE FROM message_reactions WHERE message_id = ? AND actor_id = ? AND emoji = ?");
181
+ const remove = db.query("DELETE FROM message_reactions WHERE message_id = ? AND actor_id = ? AND emoji = ?");
129
182
  for (const row of items) {
130
183
  const normalized = normalizeReactionEmoji(row.emoji);
131
184
  if (normalized === row.emoji) continue;
@@ -138,8 +191,7 @@ function normalizeExistingMessageReactions(): void {
138
191
 
139
192
  export function initDb(path: string = "agent-relay.db"): Database {
140
193
  db = new Database(path, { create: true });
141
- db.run("PRAGMA journal_mode = WAL");
142
- db.run("PRAGMA foreign_keys = ON");
194
+ applyConnectionPragmas(db);
143
195
 
144
196
  db.run(`
145
197
  CREATE TABLE IF NOT EXISTS agents (
@@ -794,7 +846,7 @@ export function initDb(path: string = "agent-relay.db"): Database {
794
846
  normalizeExistingMessageReactions();
795
847
 
796
848
  // Migrations
797
- const cols = db.prepare("PRAGMA table_info(messages)").all() as any[];
849
+ const cols = db.query("PRAGMA table_info(messages)").all() as any[];
798
850
  const colNames = cols.map((c: any) => c.name);
799
851
  if (!colNames.includes("thread_id")) {
800
852
  db.run("ALTER TABLE messages ADD COLUMN thread_id INTEGER");
@@ -838,7 +890,7 @@ export function initDb(path: string = "agent-relay.db"): Database {
838
890
  if (!colNames.includes("resolved_to_agent")) {
839
891
  db.run("ALTER TABLE messages ADD COLUMN resolved_to_agent TEXT");
840
892
  }
841
- db.prepare(
893
+ db.query(
842
894
  "UPDATE messages SET claim_expires_at = coalesce(claimed_at, ?) + ? WHERE claimed_by IS NOT NULL AND claim_expires_at IS NULL",
843
895
  ).run(Date.now(), CLAIM_LEASE_MS);
844
896
  db.run("CREATE INDEX IF NOT EXISTS idx_msg_thread ON messages(thread_id)");
@@ -846,7 +898,7 @@ export function initDb(path: string = "agent-relay.db"): Database {
846
898
  db.run("CREATE INDEX IF NOT EXISTS idx_msg_delivery_status ON messages(delivery_status)");
847
899
  db.run("CREATE INDEX IF NOT EXISTS idx_msg_resolved_to_agent ON messages(resolved_to_agent)");
848
900
 
849
- const tokenCols = db.prepare("PRAGMA table_info(tokens)").all() as any[];
901
+ const tokenCols = db.query("PRAGMA table_info(tokens)").all() as any[];
850
902
  const tokenColNames = tokenCols.map((c: any) => c.name);
851
903
  if (!tokenColNames.includes("constraints")) {
852
904
  db.run("ALTER TABLE tokens ADD COLUMN constraints TEXT");
@@ -870,7 +922,7 @@ export function initDb(path: string = "agent-relay.db"): Database {
870
922
  `);
871
923
  db.run("CREATE INDEX IF NOT EXISTS idx_mda_message ON message_delivery_attempts(message_id, created_at DESC)");
872
924
 
873
- const channelBindingsSql = db.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'channel_bindings'").get() as { sql?: string } | undefined;
925
+ const channelBindingsSql = db.query("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'channel_bindings'").get() as { sql?: string } | undefined;
874
926
  if (channelBindingsSql?.sql?.includes("UNIQUE(channel_id, conversation_key)")) {
875
927
  db.transaction(() => {
876
928
  db.run("ALTER TABLE channel_bindings RENAME TO channel_bindings_old");
@@ -899,7 +951,7 @@ export function initDb(path: string = "agent-relay.db"): Database {
899
951
  db.run("CREATE INDEX IF NOT EXISTS idx_channel_bindings_channel ON channel_bindings(channel_id, priority)");
900
952
  db.run("CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_bindings_target ON channel_bindings(channel_id, conversation_key, target_type, target_id)");
901
953
 
902
- const bindingColNames = (db.prepare("PRAGMA table_info(channel_bindings)").all() as any[]).map((c: any) => c.name);
954
+ const bindingColNames = (db.query("PRAGMA table_info(channel_bindings)").all() as any[]).map((c: any) => c.name);
903
955
  if (!bindingColNames.includes("pool_selector")) {
904
956
  db.run("ALTER TABLE channel_bindings ADD COLUMN pool_selector TEXT");
905
957
  db.run("ALTER TABLE channel_bindings ADD COLUMN pool_agent_id TEXT");
@@ -918,23 +970,23 @@ export function initDb(path: string = "agent-relay.db"): Database {
918
970
  // Backfill thread_id for pre-migration rows (self-threaded).
919
971
  db.run("UPDATE messages SET thread_id = id WHERE thread_id IS NULL");
920
972
 
921
- const taskCols = db.prepare("PRAGMA table_info(tasks)").all() as any[];
973
+ const taskCols = db.query("PRAGMA table_info(tasks)").all() as any[];
922
974
  const taskColNames = taskCols.map((c: any) => c.name);
923
975
  if (!taskColNames.includes("claim_expires_at")) {
924
976
  db.run("ALTER TABLE tasks ADD COLUMN claim_expires_at INTEGER");
925
977
  }
926
- db.prepare(
978
+ db.query(
927
979
  "UPDATE tasks SET claim_expires_at = coalesce(claimed_at, ?) + ? WHERE claimed_by IS NOT NULL AND claim_expires_at IS NULL",
928
980
  ).run(Date.now(), CLAIM_LEASE_MS);
929
981
 
930
982
  // Migration: orchestrators.api_url
931
- const orchCols = db.prepare("PRAGMA table_info(orchestrators)").all() as any[];
983
+ const orchCols = db.query("PRAGMA table_info(orchestrators)").all() as any[];
932
984
  const orchColNames = orchCols.map((c: any) => c.name);
933
985
  if (!orchColNames.includes("api_url")) {
934
986
  db.run("ALTER TABLE orchestrators ADD COLUMN api_url TEXT");
935
987
  }
936
988
 
937
- const managedStateCols = db.prepare("PRAGMA table_info(managed_agent_state)").all() as any[];
989
+ const managedStateCols = db.query("PRAGMA table_info(managed_agent_state)").all() as any[];
938
990
  const managedStateColNames = managedStateCols.map((c: any) => c.name);
939
991
  if (!managedStateColNames.includes("workspace_id")) {
940
992
  db.run("ALTER TABLE managed_agent_state ADD COLUMN workspace_id TEXT");
@@ -954,7 +1006,7 @@ export function initDb(path: string = "agent-relay.db"): Database {
954
1006
  `);
955
1007
 
956
1008
  // Migration: agents.label
957
- const agentCols = db.prepare("PRAGMA table_info(agents)").all() as any[];
1009
+ const agentCols = db.query("PRAGMA table_info(agents)").all() as any[];
958
1010
  const agentColNames = agentCols.map((c: any) => c.name);
959
1011
  if (!agentColNames.includes("label")) {
960
1012
  db.run("ALTER TABLE agents ADD COLUMN label TEXT");
@@ -1076,7 +1128,7 @@ export function initDb(path: string = "agent-relay.db"): Database {
1076
1128
  // pass the sendMessage validation. The reaper exempts these by checking
1077
1129
  // meta.builtin (or by id for "user").
1078
1130
  const now = Date.now();
1079
- const builtinStmt = db.prepare(`
1131
+ const builtinStmt = db.query(`
1080
1132
  INSERT INTO agents (id, name, kind, tags, machine, rig, capabilities, ready, status, meta, last_seen, created_at)
1081
1133
  VALUES (?, ?, ?, ?, NULL, NULL, '[]', 1, 'online', '{"builtin":true}', ?, ?)
1082
1134
  ON CONFLICT(id) DO UPDATE SET status = 'online', ready = 1, last_seen = excluded.last_seen
@@ -1095,6 +1147,15 @@ export function initDb(path: string = "agent-relay.db"): Database {
1095
1147
  `);
1096
1148
  }
1097
1149
 
1150
+ // Bootstrap planner statistics on first run (sqlite_stat1 absent means ANALYZE
1151
+ // has never run — the planner would otherwise rely on heuristics only), then
1152
+ // let PRAGMA optimize apply/refresh them cheaply on every startup.
1153
+ const hasStats = db
1154
+ .query("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'sqlite_stat1'")
1155
+ .get();
1156
+ if (!hasStats) db.run("ANALYZE");
1157
+ db.run("PRAGMA optimize");
1158
+
1098
1159
  return db;
1099
1160
  }
1100
1161
 
@@ -1687,7 +1748,7 @@ export function upsertAgent(input: RegisterAgentInput): AgentCard {
1687
1748
  const labelProvided = Object.prototype.hasOwnProperty.call(input, "label");
1688
1749
  const readyProvided = Object.prototype.hasOwnProperty.call(input, "ready");
1689
1750
  const instanceProvided = Boolean(input.instanceId);
1690
- const stmt = db.prepare(`
1751
+ const stmt = db.query(`
1691
1752
  INSERT INTO agents (id, name, kind, label, tags, machine, rig, capabilities, ready, status, instance_id, epoch, provider_capabilities, context_state, meta, last_seen, created_at)
1692
1753
  VALUES ($id, $name, $kind, $label, $tags, $machine, $rig, $capabilities, $ready, $status, $instanceId, $initialEpoch, $providerCapabilities, $contextState, $meta, $now, $now)
1693
1754
  ON CONFLICT(id) DO UPDATE SET
@@ -1759,18 +1820,18 @@ export function validateAgentSession(id: string, guard?: AgentSessionGuard): { o
1759
1820
  export function setLabel(id: string, label: string | null): boolean {
1760
1821
  const normalized = label && label.trim() ? label.trim() : null;
1761
1822
  return (
1762
- db.prepare("UPDATE agents SET label = ? WHERE id = ?").run(normalized, id).changes > 0
1823
+ db.query("UPDATE agents SET label = ? WHERE id = ?").run(normalized, id).changes > 0
1763
1824
  );
1764
1825
  }
1765
1826
 
1766
1827
  export function setTags(id: string, tags: string[]): AgentCard | null {
1767
1828
  const normalized = [...new Set(tags.map((tag) => tag.trim()).filter(Boolean))];
1768
- const result = db.prepare("UPDATE agents SET tags = ?, last_seen = ? WHERE id = ?").run(JSON.stringify(normalized), Date.now(), id);
1829
+ const result = db.query("UPDATE agents SET tags = ?, last_seen = ? WHERE id = ?").run(JSON.stringify(normalized), Date.now(), id);
1769
1830
  return result.changes > 0 ? getAgent(id) : null;
1770
1831
  }
1771
1832
 
1772
1833
  export function getAgent(id: string): AgentCard | null {
1773
- const row = db.prepare("SELECT * FROM agents WHERE id = ?").get(id) as any;
1834
+ const row = db.query("SELECT * FROM agents WHERE id = ?").get(id) as any;
1774
1835
  return row ? rowToAgent(row) : null;
1775
1836
  }
1776
1837
 
@@ -1796,7 +1857,7 @@ export function listAgents(filter?: {
1796
1857
  }
1797
1858
 
1798
1859
  sql += " ORDER BY last_seen DESC";
1799
- return (db.prepare(sql).all(...params) as any[]).map(rowToAgent);
1860
+ return (db.query(sql).all(...params) as any[]).map(rowToAgent);
1800
1861
  }
1801
1862
 
1802
1863
  export function setStatus(id: string, status: AgentCard["status"], guard?: AgentSessionGuard): boolean {
@@ -1806,7 +1867,7 @@ export function setStatus(id: string, status: AgentCard["status"], guard?: Agent
1806
1867
  const sql = ready === 0
1807
1868
  ? "UPDATE agents SET status = ?, ready = 0, last_seen = ? WHERE id = ?"
1808
1869
  : "UPDATE agents SET status = ?, last_seen = ? WHERE id = ?";
1809
- const changed = db.prepare(sql).run(status, now, id).changes > 0;
1870
+ const changed = db.query(sql).run(status, now, id).changes > 0;
1810
1871
  if (changed && status === "offline") closeOpenPairsForAgent(id, now);
1811
1872
  if (changed && status === "offline") electWorkspaceStewards();
1812
1873
  return changed;
@@ -1817,7 +1878,7 @@ export function markReady(id: string, ready: boolean, guard?: AgentSessionGuard)
1817
1878
  const now = Date.now();
1818
1879
  return (
1819
1880
  db
1820
- .prepare("UPDATE agents SET ready = ?, last_seen = ? WHERE id = ?")
1881
+ .query("UPDATE agents SET ready = ?, last_seen = ? WHERE id = ?")
1821
1882
  .run(ready ? 1 : 0, now, id).changes > 0
1822
1883
  );
1823
1884
  }
@@ -1828,7 +1889,7 @@ export function mergeAgentMeta(id: string, meta: Record<string, unknown>, guard?
1828
1889
  if (!agent) return false;
1829
1890
  const merged = { ...(agent.meta ?? {}), ...meta };
1830
1891
  const result = db
1831
- .prepare("UPDATE agents SET meta = ?, last_seen = ? WHERE id = ?")
1892
+ .query("UPDATE agents SET meta = ?, last_seen = ? WHERE id = ?")
1832
1893
  .run(JSON.stringify(merged), Date.now(), id);
1833
1894
  return result.changes > 0;
1834
1895
  }
@@ -1844,7 +1905,7 @@ export function recordAgentExitDiagnostics(id: string, diagnostics: ManagedSessi
1844
1905
  lastExitAt: diagnostics.detectedAt || now,
1845
1906
  };
1846
1907
  const result = db
1847
- .prepare("UPDATE agents SET status = 'offline', ready = 0, meta = ?, last_seen = ? WHERE id = ?")
1908
+ .query("UPDATE agents SET status = 'offline', ready = 0, meta = ?, last_seen = ? WHERE id = ?")
1848
1909
  .run(JSON.stringify(merged), now, id);
1849
1910
  if (result.changes <= 0) return null;
1850
1911
  closeOpenPairsForAgent(id, now);
@@ -1860,7 +1921,7 @@ export function heartbeat(
1860
1921
  const now = Date.now();
1861
1922
  if (runtime?.providerCapabilities || runtime?.context) {
1862
1923
  const result = db
1863
- .prepare(`
1924
+ .query(`
1864
1925
  UPDATE agents SET
1865
1926
  last_seen = ?,
1866
1927
  status = CASE WHEN status = 'offline' THEN 'idle' ELSE status END,
@@ -1878,7 +1939,7 @@ export function heartbeat(
1878
1939
  return result.changes > 0;
1879
1940
  }
1880
1941
  const result = db
1881
- .prepare("UPDATE agents SET last_seen = ?, status = CASE WHEN status = 'offline' THEN 'idle' ELSE status END WHERE id = ?")
1942
+ .query("UPDATE agents SET last_seen = ?, status = CASE WHEN status = 'offline' THEN 'idle' ELSE status END WHERE id = ?")
1882
1943
  .run(now, id);
1883
1944
  return result.changes > 0;
1884
1945
  }
@@ -1893,23 +1954,23 @@ export function listContextSnapshots(agentId: string, options: { since?: number;
1893
1954
  }
1894
1955
  sql += " ORDER BY captured_at ASC LIMIT ?";
1895
1956
  params.push(limit);
1896
- return (db.prepare(sql).all(...params) as any[]).map(rowToContextSnapshot);
1957
+ return (db.query(sql).all(...params) as any[]).map(rowToContextSnapshot);
1897
1958
  }
1898
1959
 
1899
1960
  function pruneContextSnapshots(agentId?: string, olderThan = Date.now() - DAY_MS): number {
1900
1961
  const result = agentId
1901
- ? db.prepare("DELETE FROM context_snapshots WHERE agent_id = ? AND captured_at < ?").run(agentId, olderThan)
1902
- : db.prepare("DELETE FROM context_snapshots WHERE captured_at < ?").run(olderThan);
1962
+ ? db.query("DELETE FROM context_snapshots WHERE agent_id = ? AND captured_at < ?").run(agentId, olderThan)
1963
+ : db.query("DELETE FROM context_snapshots WHERE captured_at < ?").run(olderThan);
1903
1964
  return result.changes;
1904
1965
  }
1905
1966
 
1906
1967
  function recordContextSnapshot(agentId: string, context: ContextState, now: number): void {
1907
1968
  const latest = db
1908
- .prepare("SELECT captured_at FROM context_snapshots WHERE agent_id = ? ORDER BY captured_at DESC LIMIT 1")
1969
+ .query("SELECT captured_at FROM context_snapshots WHERE agent_id = ? ORDER BY captured_at DESC LIMIT 1")
1909
1970
  .get(agentId) as { captured_at: number } | undefined;
1910
1971
  if (latest && latest.captured_at > now - CONTEXT_SNAPSHOT_DEBOUNCE_MS) return;
1911
1972
 
1912
- db.prepare(`
1973
+ db.query(`
1913
1974
  INSERT INTO context_snapshots (
1914
1975
  agent_id,
1915
1976
  utilization,
@@ -1999,7 +2060,7 @@ function upsertChannelForAgent(agent: AgentCard): void {
1999
2060
  ? (agent.meta.topicChannels as unknown[]).filter((item): item is string => typeof item === "string" && item.trim().length > 0)
2000
2061
  : [];
2001
2062
 
2002
- db.prepare(`
2063
+ db.query(`
2003
2064
  INSERT INTO channels (id, provider, account_id, display_name, agent_id, transport, direction, topic_channels, capabilities, meta, created_at, updated_at)
2004
2065
  VALUES ($id, $provider, $accountId, $displayName, $agentId, $transport, $direction, $topicChannels, $capabilities, $meta, $now, $now)
2005
2066
  ON CONFLICT(provider, account_id) DO UPDATE SET
@@ -2039,7 +2100,7 @@ function upsertChannelForAgent(agent: AgentCard): void {
2039
2100
  }
2040
2101
 
2041
2102
  export function listChannels(): ChannelSummary[] {
2042
- const rows = db.prepare(`
2103
+ const rows = db.query(`
2043
2104
  SELECT
2044
2105
  c.*,
2045
2106
  c.capabilities AS channel_capabilities,
@@ -2074,8 +2135,8 @@ export function getChannel(channelId: string): ChannelSummary | null {
2074
2135
 
2075
2136
  export function listChannelBindings(channelId?: string): ChannelBinding[] {
2076
2137
  const rows = channelId
2077
- ? db.prepare("SELECT * FROM channel_bindings WHERE channel_id = ? ORDER BY priority DESC, updated_at DESC").all(channelId) as any[]
2078
- : db.prepare("SELECT * FROM channel_bindings ORDER BY channel_id, priority DESC, updated_at DESC").all() as any[];
2138
+ ? db.query("SELECT * FROM channel_bindings WHERE channel_id = ? ORDER BY priority DESC, updated_at DESC").all(channelId) as any[]
2139
+ : db.query("SELECT * FROM channel_bindings ORDER BY channel_id, priority DESC, updated_at DESC").all() as any[];
2079
2140
  return rows.map(rowToChannelBinding);
2080
2141
  }
2081
2142
 
@@ -2097,9 +2158,9 @@ export function upsertChannelBinding(input: {
2097
2158
  const now = Date.now();
2098
2159
  db.transaction(() => {
2099
2160
  if (mode === "exclusive") {
2100
- db.prepare("DELETE FROM channel_bindings WHERE channel_id = ? AND conversation_key = ?").run(input.channelId, conversationKey);
2161
+ db.query("DELETE FROM channel_bindings WHERE channel_id = ? AND conversation_key = ?").run(input.channelId, conversationKey);
2101
2162
  }
2102
- db.prepare(`
2163
+ db.query(`
2103
2164
  INSERT INTO channel_bindings (id, channel_id, conversation_key, conversation_id, target_type, target_id, mode, priority, pool_selector, created_at, updated_at)
2104
2165
  VALUES ($id, $channelId, $conversationKey, $conversationId, $targetType, $targetId, $mode, $priority, $poolSelector, $now, $now)
2105
2166
  ON CONFLICT(channel_id, conversation_key, target_type, target_id) DO UPDATE SET
@@ -2122,7 +2183,7 @@ export function upsertChannelBinding(input: {
2122
2183
  })();
2123
2184
 
2124
2185
  evaluatePoolBindings(now);
2125
- const row = db.prepare("SELECT * FROM channel_bindings WHERE id = ?").get(id) as any;
2186
+ const row = db.query("SELECT * FROM channel_bindings WHERE id = ?").get(id) as any;
2126
2187
  return rowToChannelBinding(row);
2127
2188
  }
2128
2189
 
@@ -2134,7 +2195,7 @@ interface PoolBindingChange {
2134
2195
  }
2135
2196
 
2136
2197
  export function evaluatePoolBindings(now: number = Date.now()): PoolBindingChange[] {
2137
- const rows = db.prepare("SELECT * FROM channel_bindings WHERE target_type = 'pool'").all() as any[];
2198
+ const rows = db.query("SELECT * FROM channel_bindings WHERE target_type = 'pool'").all() as any[];
2138
2199
  const changes: PoolBindingChange[] = [];
2139
2200
 
2140
2201
  for (const row of rows) {
@@ -2149,7 +2210,7 @@ export function evaluatePoolBindings(now: number = Date.now()): PoolBindingChang
2149
2210
  const holder = getAgent(currentAgentId);
2150
2211
  if (holder && holder.status !== "offline" && holder.ready && holder.lastSeen > now - STALE_TTL_MS && agentCanServeChannel(holder, channelId)) {
2151
2212
  if (currentEpoch === null || holder.epoch === currentEpoch) {
2152
- db.prepare("UPDATE channel_bindings SET pool_claim_expires_at = ? WHERE id = ?")
2213
+ db.query("UPDATE channel_bindings SET pool_claim_expires_at = ? WHERE id = ?")
2153
2214
  .run(now + POOL_CLAIM_LEASE_MS, bindingId);
2154
2215
  holderValid = true;
2155
2216
  }
@@ -2157,7 +2218,7 @@ export function evaluatePoolBindings(now: number = Date.now()): PoolBindingChang
2157
2218
  }
2158
2219
 
2159
2220
  if (!holderValid && currentAgentId) {
2160
- db.prepare("UPDATE channel_bindings SET pool_agent_id = NULL, pool_agent_epoch = NULL, pool_claim_expires_at = NULL WHERE id = ?")
2221
+ db.query("UPDATE channel_bindings SET pool_agent_id = NULL, pool_agent_epoch = NULL, pool_claim_expires_at = NULL WHERE id = ?")
2161
2222
  .run(bindingId);
2162
2223
  changes.push({ bindingId, channelId, previousAgentId: currentAgentId, newAgentId: null });
2163
2224
  }
@@ -2173,7 +2234,7 @@ export function evaluatePoolBindings(now: number = Date.now()): PoolBindingChang
2173
2234
 
2174
2235
  if (eligible.length > 0) {
2175
2236
  const picked = eligible[0]!;
2176
- db.prepare("UPDATE channel_bindings SET pool_agent_id = ?, pool_agent_epoch = ?, pool_claim_expires_at = ? WHERE id = ?")
2237
+ db.query("UPDATE channel_bindings SET pool_agent_id = ?, pool_agent_epoch = ?, pool_claim_expires_at = ? WHERE id = ?")
2177
2238
  .run(picked.id, picked.epoch, now + POOL_CLAIM_LEASE_MS, bindingId);
2178
2239
  const lastChange = changes[changes.length - 1];
2179
2240
  if (lastChange && lastChange.bindingId === bindingId && lastChange.newAgentId === null) {
@@ -2190,12 +2251,12 @@ export function evaluatePoolBindings(now: number = Date.now()): PoolBindingChang
2190
2251
 
2191
2252
  export function resolveChannelRoutes(channelId: string, conversationId?: string): ChannelBinding[] {
2192
2253
  const rows = conversationId
2193
- ? db.prepare(`
2254
+ ? db.query(`
2194
2255
  SELECT * FROM channel_bindings
2195
2256
  WHERE channel_id = ? AND conversation_key IN (?, '')
2196
2257
  ORDER BY CASE WHEN conversation_key = ? THEN 1 ELSE 0 END DESC, priority DESC, updated_at DESC
2197
2258
  `).all(channelId, conversationId, conversationId) as any[]
2198
- : db.prepare(`
2259
+ : db.query(`
2199
2260
  SELECT * FROM channel_bindings
2200
2261
  WHERE channel_id = ? AND conversation_key = ''
2201
2262
  ORDER BY priority DESC, updated_at DESC
@@ -2208,9 +2269,9 @@ export function resolveChannelRoutes(channelId: string, conversationId?: string)
2208
2269
  export function reapStaleAgents(ttlMs: number = STALE_TTL_MS): string[] {
2209
2270
  const now = Date.now();
2210
2271
  const cutoff = now - ttlMs;
2211
- db.prepare("UPDATE agents SET last_seen = ? WHERE id IN ('user', 'system')").run(now);
2272
+ db.query("UPDATE agents SET last_seen = ? WHERE id IN ('user', 'system')").run(now);
2212
2273
  const rows = db
2213
- .prepare(
2274
+ .query(
2214
2275
  "UPDATE agents SET status = 'offline', ready = 0 WHERE status NOT IN ('offline', 'stale') AND last_seen < ? AND id NOT IN ('user', 'system') RETURNING id"
2215
2276
  )
2216
2277
  .all(cutoff) as any[];
@@ -2232,9 +2293,9 @@ export function reapStaleAgents(ttlMs: number = STALE_TTL_MS): string[] {
2232
2293
  // re-open (gated on status IN claimed/in_progress/blocked/orphaned) then skips.
2233
2294
  function settleSingleTargetOnDemandTasks(condition: string, params: any[], now: number, reason: string): void {
2234
2295
  const selectSql = `${TASK_SELECT} WHERE (${condition}) AND json_extract(metadata, '$.targetMode') = 'on_demand_agent' AND status IN ('claimed', 'in_progress', 'blocked', 'orphaned')`;
2235
- const rows = db.prepare(selectSql).all(...params) as any[];
2296
+ const rows = db.query(selectSql).all(...params) as any[];
2236
2297
  if (rows.length === 0) return;
2237
- db.prepare(
2298
+ db.query(
2238
2299
  `UPDATE tasks SET status = 'done', claim_expires_at = NULL, updated_at = ?, last_seen_at = ? WHERE (${condition}) AND json_extract(metadata, '$.targetMode') = 'on_demand_agent' AND status IN ('claimed', 'in_progress', 'blocked', 'orphaned')`
2239
2300
  ).run(now, now, ...params);
2240
2301
  for (const row of rows) {
@@ -2253,7 +2314,7 @@ export function pruneOfflineAgents(maxOfflineMs: number = DAY_MS): string[] {
2253
2314
  const cutoff = Date.now() - maxOfflineMs;
2254
2315
  return db.transaction(() => {
2255
2316
  const rows = db
2256
- .prepare(
2317
+ .query(
2257
2318
  "SELECT id FROM agents WHERE status = 'offline' AND last_seen < ? AND id NOT IN ('user', 'system')"
2258
2319
  )
2259
2320
  .all(cutoff) as any[];
@@ -2262,7 +2323,7 @@ export function pruneOfflineAgents(maxOfflineMs: number = DAY_MS): string[] {
2262
2323
 
2263
2324
  // Release claims held by pruned agents so work becomes claimable again.
2264
2325
  db
2265
- .prepare(
2326
+ .query(
2266
2327
  "UPDATE messages SET claimed_by = NULL, claimed_at = NULL, claim_expires_at = NULL WHERE claimed_by IN (SELECT id FROM agents WHERE status = 'offline' AND last_seen < ? AND id NOT IN ('user', 'system'))"
2267
2328
  )
2268
2329
  .run(cutoff);
@@ -2271,7 +2332,7 @@ export function pruneOfflineAgents(maxOfflineMs: number = DAY_MS): string[] {
2271
2332
  settleSingleTargetOnDemandTasks(offlineClaimCondition, [cutoff], now, "agent-pruned");
2272
2333
 
2273
2334
  db
2274
- .prepare(
2335
+ .query(
2275
2336
  `UPDATE tasks SET status = 'open', claimed_by = NULL, claimed_at = NULL, claim_expires_at = NULL, updated_at = ? WHERE ${offlineClaimCondition} AND status IN ('claimed', 'in_progress', 'blocked')`
2276
2337
  )
2277
2338
  .run(now, cutoff);
@@ -2282,7 +2343,7 @@ export function pruneOfflineAgents(maxOfflineMs: number = DAY_MS): string[] {
2282
2343
  }
2283
2344
 
2284
2345
  db
2285
- .prepare(
2346
+ .query(
2286
2347
  "DELETE FROM agents WHERE status = 'offline' AND last_seen < ? AND id NOT IN ('user', 'system')"
2287
2348
  )
2288
2349
  .run(cutoff);
@@ -2299,7 +2360,7 @@ export function findAgentsByCapability(capability: string, onlineOnly = true): A
2299
2360
  params.push(Date.now() - STALE_TTL_MS);
2300
2361
  }
2301
2362
  sql += " ORDER BY last_seen DESC";
2302
- return (db.prepare(sql).all(...params) as any[]).map(rowToAgent);
2363
+ return (db.query(sql).all(...params) as any[]).map(rowToAgent);
2303
2364
  }
2304
2365
 
2305
2366
  export function deleteAgent(id: string): { ok: boolean; error?: string } {
@@ -2310,23 +2371,23 @@ export function deleteAgent(id: string): { ok: boolean; error?: string } {
2310
2371
  // Release any claims held by this agent so the tasks become claimable again.
2311
2372
  // from_agent is left intact as historical record.
2312
2373
  const now = Date.now();
2313
- db.prepare("UPDATE messages SET claimed_by = NULL, claimed_at = NULL, claim_expires_at = NULL WHERE claimed_by = ?").run(id);
2374
+ db.query("UPDATE messages SET claimed_by = NULL, claimed_at = NULL, claim_expires_at = NULL WHERE claimed_by = ?").run(id);
2314
2375
  settleSingleTargetOnDemandTasks("claimed_by = ?", [id], now, "agent-removed");
2315
- db.prepare("UPDATE tasks SET status = 'open', claimed_by = NULL, claimed_at = NULL, claim_expires_at = NULL, updated_at = ? WHERE claimed_by = ? AND status IN ('claimed', 'in_progress', 'blocked')").run(now, id);
2376
+ db.query("UPDATE tasks SET status = 'open', claimed_by = NULL, claimed_at = NULL, claim_expires_at = NULL, updated_at = ? WHERE claimed_by = ? AND status IN ('claimed', 'in_progress', 'blocked')").run(now, id);
2316
2377
  revokeRuntimeTokensForAgent(id, now);
2317
2378
  closeOpenPairsForAgent(id, now);
2318
- return db.prepare("DELETE FROM agents WHERE id = ?").run(id).changes > 0;
2379
+ return db.query("DELETE FROM agents WHERE id = ?").run(id).changes > 0;
2319
2380
  })();
2320
2381
  return deleted ? { ok: true } : { ok: false, error: "agent not found" };
2321
2382
  }
2322
2383
 
2323
2384
  export function revokeRuntimeTokensForAgent(agentId: string, now = Date.now()): string[] {
2324
- const row = db.prepare("SELECT meta FROM agents WHERE id = ?").get(agentId) as { meta?: string } | undefined;
2385
+ const row = db.query("SELECT meta FROM agents WHERE id = ?").get(agentId) as { meta?: string } | undefined;
2325
2386
  const jtis = runtimeTokenJtisFromMeta(parseJson(row?.meta ?? "{}", {}));
2326
2387
  if (jtis.length === 0) return [];
2327
2388
  const revokedAt = Math.floor(now / 1000);
2328
2389
  const placeholders = jtis.map(() => "?").join(",");
2329
- const rows = db.prepare(`
2390
+ const rows = db.query(`
2330
2391
  UPDATE tokens
2331
2392
  SET revoked_at = ?
2332
2393
  WHERE revoked_at IS NULL
@@ -2360,13 +2421,13 @@ const TASK_SELECT = "SELECT * FROM tasks";
2360
2421
 
2361
2422
  function findOpenTaskByDedupe(source: string, dedupeKey: string): Task | null {
2362
2423
  const row = db
2363
- .prepare(`${TASK_SELECT} WHERE source = ? AND dedupe_key = ? AND status NOT IN ('done', 'failed', 'canceled') ORDER BY id DESC LIMIT 1`)
2424
+ .query(`${TASK_SELECT} WHERE source = ? AND dedupe_key = ? AND status NOT IN ('done', 'failed', 'canceled') ORDER BY id DESC LIMIT 1`)
2364
2425
  .get(source, dedupeKey) as any;
2365
2426
  return row ? rowToTask(row) : null;
2366
2427
  }
2367
2428
 
2368
2429
  function insertTaskEvent(taskId: number, event: Required<Omit<TaskEvent, "id" | "taskId" | "createdAt">>, now: number): TaskEvent {
2369
- const result = db.prepare(`
2430
+ const result = db.query(`
2370
2431
  INSERT INTO task_events (task_id, source, type, severity, title, body, metadata, created_at)
2371
2432
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)
2372
2433
  `).run(taskId, event.source, event.type, event.severity, event.title, event.body, JSON.stringify(event.metadata), now);
@@ -2374,7 +2435,7 @@ function insertTaskEvent(taskId: number, event: Required<Omit<TaskEvent, "id" |
2374
2435
  }
2375
2436
 
2376
2437
  function getTaskEvent(id: number): TaskEvent | null {
2377
- const row = db.prepare("SELECT * FROM task_events WHERE id = ?").get(id) as any;
2438
+ const row = db.query("SELECT * FROM task_events WHERE id = ?").get(id) as any;
2378
2439
  return row ? rowToTaskEvent(row) : null;
2379
2440
  }
2380
2441
 
@@ -2406,7 +2467,7 @@ export function ingestIntegrationEvent(input: IntegrationEventInput, integration
2406
2467
  let created = false;
2407
2468
 
2408
2469
  if (existing) {
2409
- db.prepare(`
2470
+ db.query(`
2410
2471
  UPDATE tasks
2411
2472
  SET title = ?, body = ?, severity = ?, target = ?, channel = ?, external_url = ?,
2412
2473
  occurrence_count = occurrence_count + 1, metadata = ?, updated_at = ?, last_seen_at = ?,
@@ -2431,7 +2492,7 @@ export function ingestIntegrationEvent(input: IntegrationEventInput, integration
2431
2492
  );
2432
2493
  taskId = existing.id;
2433
2494
  } else {
2434
- const result = db.prepare(`
2495
+ const result = db.query(`
2435
2496
  INSERT INTO tasks (source, title, body, severity, status, target, channel, dedupe_key, external_url, metadata, created_at, updated_at, last_seen_at)
2436
2497
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2437
2498
  `).run(
@@ -2485,7 +2546,7 @@ export function ingestIntegrationEvent(input: IntegrationEventInput, integration
2485
2546
  ...(attachmentRefs.length ? { attachments: attachmentRefs } : {}),
2486
2547
  },
2487
2548
  });
2488
- db.prepare("UPDATE tasks SET message_id = ?, updated_at = ? WHERE id = ?").run(message.id, now, task.id);
2549
+ db.query("UPDATE tasks SET message_id = ?, updated_at = ? WHERE id = ?").run(message.id, now, task.id);
2489
2550
  }
2490
2551
 
2491
2552
  return { task: getTask(taskId)!, event, created, message };
@@ -2493,7 +2554,7 @@ export function ingestIntegrationEvent(input: IntegrationEventInput, integration
2493
2554
  }
2494
2555
 
2495
2556
  export function getTask(id: number): Task | null {
2496
- const row = db.prepare(`${TASK_SELECT} WHERE id = ?`).get(id) as any;
2557
+ const row = db.query(`${TASK_SELECT} WHERE id = ?`).get(id) as any;
2497
2558
  return row ? rowToTask(row) : null;
2498
2559
  }
2499
2560
 
@@ -2514,11 +2575,11 @@ export function listTasks(filter?: { status?: string; source?: string; target?:
2514
2575
  }
2515
2576
  const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
2516
2577
  params.push(filter?.limit ?? 100);
2517
- return (db.prepare(`${TASK_SELECT} ${where} ORDER BY updated_at DESC LIMIT ?`).all(...params) as any[]).map(rowToTask);
2578
+ return (db.query(`${TASK_SELECT} ${where} ORDER BY updated_at DESC LIMIT ?`).all(...params) as any[]).map(rowToTask);
2518
2579
  }
2519
2580
 
2520
2581
  export function listIntegrationTaskStats(): IntegrationTaskStats[] {
2521
- const rows = db.prepare(`
2582
+ const rows = db.query(`
2522
2583
  SELECT
2523
2584
  source,
2524
2585
  COUNT(*) AS tasks,
@@ -2597,7 +2658,7 @@ export function upsertIntegrationRegistry(input: IntegrationRegistryInput): Retu
2597
2658
  const name = stringValue(input.name);
2598
2659
  if (!name) throw new ValidationError("integration name required");
2599
2660
  const now = Date.now();
2600
- db.prepare(`
2661
+ db.query(`
2601
2662
  INSERT INTO integration_registry (
2602
2663
  name, display_name, description, enabled, scopes, targets, channels, type, icon, accent_color,
2603
2664
  tags, homepage_url, repository_url, docs_url, manifest, source, created_at, updated_at
@@ -2644,17 +2705,17 @@ export function upsertIntegrationRegistry(input: IntegrationRegistryInput): Retu
2644
2705
  }
2645
2706
 
2646
2707
  function getIntegrationRegistry(name: string): ReturnType<typeof rowToIntegrationRegistry> | null {
2647
- const row = db.prepare("SELECT * FROM integration_registry WHERE name = ?").get(name) as any;
2708
+ const row = db.query("SELECT * FROM integration_registry WHERE name = ?").get(name) as any;
2648
2709
  return row ? rowToIntegrationRegistry(row) : null;
2649
2710
  }
2650
2711
 
2651
2712
  export function listIntegrationRegistry(): ReturnType<typeof rowToIntegrationRegistry>[] {
2652
- return (db.prepare("SELECT * FROM integration_registry ORDER BY display_name COLLATE NOCASE, name COLLATE NOCASE").all() as any[]).map(rowToIntegrationRegistry);
2713
+ return (db.query("SELECT * FROM integration_registry ORDER BY display_name COLLATE NOCASE, name COLLATE NOCASE").all() as any[]).map(rowToIntegrationRegistry);
2653
2714
  }
2654
2715
 
2655
2716
  function updateIntegrationObserved(name: string, eventAt: number): void {
2656
2717
  const now = Date.now();
2657
- db.prepare(`
2718
+ db.query(`
2658
2719
  INSERT INTO integration_registry (name, enabled, scopes, targets, channels, tags, manifest, source, created_at, updated_at, last_event_at, last_task_at)
2659
2720
  VALUES (?, 1, '[]', '[]', '[]', '[]', '{}', 'observed', ?, ?, ?, ?)
2660
2721
  ON CONFLICT(name) DO UPDATE SET
@@ -2670,7 +2731,7 @@ export function isIntegrationRegistryEnabled(name: string): boolean {
2670
2731
  }
2671
2732
 
2672
2733
  export function listTaskEvents(taskId: number): TaskEvent[] {
2673
- return (db.prepare("SELECT * FROM task_events WHERE task_id = ? ORDER BY id ASC").all(taskId) as any[]).map(rowToTaskEvent);
2734
+ return (db.query("SELECT * FROM task_events WHERE task_id = ? ORDER BY id ASC").all(taskId) as any[]).map(rowToTaskEvent);
2674
2735
  }
2675
2736
 
2676
2737
  export function recordTaskEvent(taskId: number, input: {
@@ -2698,21 +2759,21 @@ export function releaseExpiredClaims(now: number = Date.now()): { messageIds: nu
2698
2759
  settleSingleTargetOnDemandTasks("claimed_by IS NOT NULL AND (claim_expires_at IS NULL OR claim_expires_at <= ?)", [now], now, "claim-lease-expired");
2699
2760
  const releasableMessageClaim = "claimed_by IS NOT NULL AND (claim_expires_at IS NULL OR claim_expires_at <= ?) AND NOT EXISTS (SELECT 1 FROM tasks t WHERE t.message_id = messages.id AND t.status IN ('done', 'failed', 'canceled'))";
2700
2761
  const messageRows = db
2701
- .prepare(`SELECT id FROM messages WHERE ${releasableMessageClaim}`)
2762
+ .query(`SELECT id FROM messages WHERE ${releasableMessageClaim}`)
2702
2763
  .all(now) as any[];
2703
2764
  const taskRows = db
2704
- .prepare(`${TASK_SELECT} WHERE claimed_by IS NOT NULL AND (claim_expires_at IS NULL OR claim_expires_at <= ?) AND status IN ('claimed', 'in_progress', 'blocked')`)
2765
+ .query(`${TASK_SELECT} WHERE claimed_by IS NOT NULL AND (claim_expires_at IS NULL OR claim_expires_at <= ?) AND status IN ('claimed', 'in_progress', 'blocked')`)
2705
2766
  .all(now) as any[];
2706
2767
 
2707
2768
  if (messageRows.length > 0) {
2708
2769
  db
2709
- .prepare(`UPDATE messages SET claimed_by = NULL, claimed_at = NULL, claim_expires_at = NULL WHERE ${releasableMessageClaim}`)
2770
+ .query(`UPDATE messages SET claimed_by = NULL, claimed_at = NULL, claim_expires_at = NULL WHERE ${releasableMessageClaim}`)
2710
2771
  .run(now);
2711
2772
  }
2712
2773
 
2713
2774
  if (taskRows.length > 0) {
2714
2775
  db
2715
- .prepare("UPDATE tasks SET status = 'open', claimed_by = NULL, claimed_at = NULL, claim_expires_at = NULL, updated_at = ? WHERE claimed_by IS NOT NULL AND (claim_expires_at IS NULL OR claim_expires_at <= ?) AND status IN ('claimed', 'in_progress', 'blocked')")
2776
+ .query("UPDATE tasks SET status = 'open', claimed_by = NULL, claimed_at = NULL, claim_expires_at = NULL, updated_at = ? WHERE claimed_by IS NOT NULL AND (claim_expires_at IS NULL OR claim_expires_at <= ?) AND status IN ('claimed', 'in_progress', 'blocked')")
2716
2777
  .run(now, now);
2717
2778
 
2718
2779
  for (const row of taskRows) {
@@ -2737,11 +2798,11 @@ export function releaseExpiredClaims(now: number = Date.now()): { messageIds: nu
2737
2798
  export function orphanTasksForAgent(agentId: string, now: number = Date.now()): Task[] {
2738
2799
  return db.transaction(() => {
2739
2800
  const rows = db
2740
- .prepare(`${TASK_SELECT} WHERE claimed_by = ? AND status IN ('claimed', 'in_progress')`)
2801
+ .query(`${TASK_SELECT} WHERE claimed_by = ? AND status IN ('claimed', 'in_progress')`)
2741
2802
  .all(agentId) as any[];
2742
2803
  if (rows.length === 0) return [];
2743
2804
 
2744
- db.prepare(`
2805
+ db.query(`
2745
2806
  UPDATE tasks
2746
2807
  SET status = 'orphaned', updated_at = ?, last_seen_at = ?
2747
2808
  WHERE claimed_by = ? AND status IN ('claimed', 'in_progress')
@@ -2767,11 +2828,11 @@ export function releaseOrphanedTasks(graceMs = 120_000, now: number = Date.now()
2767
2828
  const cutoff = now - graceMs;
2768
2829
  settleSingleTargetOnDemandTasks("status = 'orphaned' AND claimed_by IS NOT NULL AND updated_at <= ?", [cutoff], now, "orphan-grace-elapsed");
2769
2830
  const rows = db
2770
- .prepare(`${TASK_SELECT} WHERE status = 'orphaned' AND claimed_by IS NOT NULL AND updated_at <= ?`)
2831
+ .query(`${TASK_SELECT} WHERE status = 'orphaned' AND claimed_by IS NOT NULL AND updated_at <= ?`)
2771
2832
  .all(cutoff) as any[];
2772
2833
  if (rows.length === 0) return [];
2773
2834
 
2774
- db.prepare(`
2835
+ db.query(`
2775
2836
  UPDATE tasks
2776
2837
  SET status = 'open', claimed_by = NULL, claimed_at = NULL, claim_expires_at = NULL, updated_at = ?
2777
2838
  WHERE status = 'orphaned' AND claimed_by IS NOT NULL AND updated_at <= ?
@@ -2807,7 +2868,7 @@ export function claimTask(taskId: number, agentId: string, guard?: AgentSessionG
2807
2868
  return db.transaction(() => {
2808
2869
  const now = Date.now();
2809
2870
  const expiresAt = now + CLAIM_LEASE_MS;
2810
- const result = db.prepare(`
2871
+ const result = db.query(`
2811
2872
  UPDATE tasks SET status = 'claimed', claimed_by = ?, claimed_at = ?, claim_expires_at = ?, updated_at = ?
2812
2873
  WHERE id = ? AND status IN ('open', 'blocked')
2813
2874
  `).run(agentId, now, expiresAt, now, taskId);
@@ -2846,9 +2907,9 @@ export function renewTaskClaim(taskId: number, agentId: string, guard?: AgentSes
2846
2907
 
2847
2908
  const now = Date.now();
2848
2909
  const expiresAt = now + CLAIM_LEASE_MS;
2849
- db.prepare("UPDATE tasks SET claim_expires_at = ?, updated_at = ? WHERE id = ? AND claimed_by = ?").run(expiresAt, now, taskId, agentId);
2910
+ db.query("UPDATE tasks SET claim_expires_at = ?, updated_at = ? WHERE id = ? AND claimed_by = ?").run(expiresAt, now, taskId, agentId);
2850
2911
  if (task.messageId) {
2851
- db.prepare("UPDATE messages SET claim_expires_at = ? WHERE id = ? AND claimed_by = ?").run(expiresAt, task.messageId, agentId);
2912
+ db.query("UPDATE messages SET claim_expires_at = ? WHERE id = ? AND claimed_by = ?").run(expiresAt, task.messageId, agentId);
2852
2913
  }
2853
2914
  return { ok: true, task: getTask(taskId)! };
2854
2915
  }
@@ -2862,7 +2923,7 @@ export function updateTaskStatus(taskId: number, input: TaskStatusInput): { ok:
2862
2923
  const session = validateAgentSession(agentId, input);
2863
2924
  if (!session.ok) return { ok: false, error: session.error };
2864
2925
  }
2865
- const result = db.prepare(`
2926
+ const result = db.query(`
2866
2927
  UPDATE tasks
2867
2928
  SET status = ?, result = COALESCE(?, result), claimed_by = COALESCE(?, claimed_by),
2868
2929
  claimed_at = CASE WHEN claimed_by IS NULL AND ? IS NOT NULL THEN ? ELSE claimed_at END,
@@ -2872,13 +2933,13 @@ export function updateTaskStatus(taskId: number, input: TaskStatusInput): { ok:
2872
2933
  `).run(input.status, input.result ?? null, agentId, agentId, now, input.status, now, now, taskId);
2873
2934
  if (result.changes === 0) return { ok: false, error: "task not found" };
2874
2935
  if (["done", "failed", "canceled"].includes(input.status) && task.messageId) {
2875
- db.prepare("UPDATE messages SET claim_expires_at = NULL WHERE id = ?").run(task.messageId);
2936
+ db.query("UPDATE messages SET claim_expires_at = NULL WHERE id = ?").run(task.messageId);
2876
2937
  }
2877
2938
  if (agentId && ["claimed", "in_progress", "blocked"].includes(input.status)) {
2878
2939
  const expiresAt = now + CLAIM_LEASE_MS;
2879
- db.prepare("UPDATE tasks SET claim_expires_at = ? WHERE id = ? AND claimed_by = ?").run(expiresAt, taskId, agentId);
2940
+ db.query("UPDATE tasks SET claim_expires_at = ? WHERE id = ? AND claimed_by = ?").run(expiresAt, taskId, agentId);
2880
2941
  if (task.messageId) {
2881
- db.prepare("UPDATE messages SET claim_expires_at = ? WHERE id = ? AND claimed_by = ?").run(expiresAt, task.messageId, agentId);
2942
+ db.query("UPDATE messages SET claim_expires_at = ? WHERE id = ? AND claimed_by = ?").run(expiresAt, task.messageId, agentId);
2882
2943
  }
2883
2944
  }
2884
2945
  const event = insertTaskEvent(taskId, {
@@ -2894,7 +2955,7 @@ export function updateTaskStatus(taskId: number, input: TaskStatusInput): { ok:
2894
2955
 
2895
2956
  export function createCallbackDelivery(taskId: number, url: string, eventType: string, payload: unknown): number {
2896
2957
  const now = Date.now();
2897
- const result = db.prepare(`
2958
+ const result = db.query(`
2898
2959
  INSERT INTO task_callback_deliveries (task_id, url, event_type, payload, status, attempts, created_at, updated_at)
2899
2960
  VALUES (?, ?, ?, ?, 'pending', 0, ?, ?)
2900
2961
  `).run(taskId, url, eventType, JSON.stringify(payload), now, now);
@@ -2902,7 +2963,7 @@ export function createCallbackDelivery(taskId: number, url: string, eventType: s
2902
2963
  }
2903
2964
 
2904
2965
  export function finishCallbackDelivery(id: number, ok: boolean, error?: string): void {
2905
- db.prepare(`
2966
+ db.query(`
2906
2967
  UPDATE task_callback_deliveries
2907
2968
  SET status = ?, attempts = attempts + 1, last_error = ?, updated_at = ?
2908
2969
  WHERE id = ?
@@ -2918,7 +2979,7 @@ export function listCallbackDeliveries(taskId: number): Array<{
2918
2979
  attempts: number;
2919
2980
  lastError?: string;
2920
2981
  }> {
2921
- return (db.prepare("SELECT * FROM task_callback_deliveries WHERE task_id = ? ORDER BY id ASC").all(taskId) as any[])
2982
+ return (db.query("SELECT * FROM task_callback_deliveries WHERE task_id = ? ORDER BY id ASC").all(taskId) as any[])
2922
2983
  .map((row) => ({
2923
2984
  id: row.id,
2924
2985
  taskId: row.task_id,
@@ -2937,12 +2998,12 @@ const DEFAULT_PAIR_TTL_MS = 5 * 60_000;
2937
2998
  const MAX_PAIR_TTL_MS = DAY_MS;
2938
2999
 
2939
3000
  function expirePendingPairs(now: number = Date.now()): void {
2940
- db.prepare("UPDATE pairs SET status = 'expired', updated_at = ?, ended_at = ? WHERE status = 'pending' AND expires_at <= ?")
3001
+ db.query("UPDATE pairs SET status = 'expired', updated_at = ?, ended_at = ? WHERE status = 'pending' AND expires_at <= ?")
2941
3002
  .run(now, now, now);
2942
3003
  }
2943
3004
 
2944
3005
  function closeOpenPairsForAgent(agentId: string, now: number = Date.now()): void {
2945
- db.prepare(`
3006
+ db.query(`
2946
3007
  UPDATE pairs
2947
3008
  SET status = 'ended', ended_at = ?, ended_by = ?, updated_at = ?
2948
3009
  WHERE status IN ('pending', 'active') AND (requester_id = ? OR target_id = ?)
@@ -2951,7 +3012,7 @@ function closeOpenPairsForAgent(agentId: string, now: number = Date.now()): void
2951
3012
 
2952
3013
  function getOpenPairForAgent(agentId: string): PairSession | null {
2953
3014
  expirePendingPairs();
2954
- const row = db.prepare(`
3015
+ const row = db.query(`
2955
3016
  SELECT * FROM pairs
2956
3017
  WHERE status IN ('pending', 'active') AND (requester_id = ? OR target_id = ?)
2957
3018
  ORDER BY updated_at DESC
@@ -3038,7 +3099,7 @@ function pairSystemMessage(pair: PairSession, to: string, event: string, subject
3038
3099
 
3039
3100
  export function getPair(id: string): PairSession | null {
3040
3101
  expirePendingPairs();
3041
- const row = db.prepare("SELECT * FROM pairs WHERE id = ?").get(id) as any;
3102
+ const row = db.query("SELECT * FROM pairs WHERE id = ?").get(id) as any;
3042
3103
  return row ? rowToPair(row) : null;
3043
3104
  }
3044
3105
 
@@ -3055,7 +3116,7 @@ export function listPairs(filter?: { agentId?: string; status?: PairStatus }): P
3055
3116
  params.push(filter.status);
3056
3117
  }
3057
3118
  const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
3058
- return (db.prepare(`SELECT * FROM pairs ${where} ORDER BY updated_at DESC LIMIT 100`).all(...params) as any[]).map(rowToPair);
3119
+ return (db.query(`SELECT * FROM pairs ${where} ORDER BY updated_at DESC LIMIT 100`).all(...params) as any[]).map(rowToPair);
3059
3120
  }
3060
3121
 
3061
3122
  export function createPair(input: CreatePairInput): {
@@ -3082,7 +3143,7 @@ export function createPair(input: CreatePairInput): {
3082
3143
  const now = Date.now();
3083
3144
  const ttlMs = Math.min(Math.max(input.ttlMs ?? DEFAULT_PAIR_TTL_MS, 10_000), MAX_PAIR_TTL_MS);
3084
3145
  const id = randomUUID();
3085
- db.prepare(`
3146
+ db.query(`
3086
3147
  INSERT INTO pairs (id, requester_id, target_id, status, objective, meta, created_at, updated_at, expires_at)
3087
3148
  VALUES (?, ?, ?, 'pending', ?, ?, ?, ?, ?)
3088
3149
  `).run(id, input.from, resolved.agent.id, input.objective ?? null, JSON.stringify(input.meta ?? {}), now, now, now + ttlMs);
@@ -3122,7 +3183,7 @@ export function acceptPair(id: string, input: PairActionInput): { ok: true; pair
3122
3183
  }
3123
3184
 
3124
3185
  const now = Date.now();
3125
- db.prepare("UPDATE pairs SET status = 'active', accepted_at = ?, updated_at = ? WHERE id = ? AND status = 'pending'")
3186
+ db.query("UPDATE pairs SET status = 'active', accepted_at = ?, updated_at = ? WHERE id = ? AND status = 'pending'")
3126
3187
  .run(now, now, id);
3127
3188
  const active = getPair(id)!;
3128
3189
  const notices = [
@@ -3139,7 +3200,7 @@ export function rejectPair(id: string, input: PairActionInput): { ok: true; pair
3139
3200
  if (pair.status !== "pending") return { ok: false, code: "invalid", error: `pair is ${pair.status}` };
3140
3201
  if (pair.targetId !== input.agentId) return { ok: false, code: "forbidden", error: "only the target agent can reject this pair" };
3141
3202
  const now = Date.now();
3142
- db.prepare("UPDATE pairs SET status = 'rejected', ended_at = ?, ended_by = ?, updated_at = ? WHERE id = ?")
3203
+ db.query("UPDATE pairs SET status = 'rejected', ended_at = ?, ended_by = ?, updated_at = ? WHERE id = ?")
3143
3204
  .run(now, input.agentId, now, id);
3144
3205
  const rejected = getPair(id)!;
3145
3206
  const reason = input.reason ? `\n\nReason:\n${input.reason}` : "";
@@ -3154,7 +3215,7 @@ export function endPair(id: string, input: PairActionInput): { ok: true; pair: P
3154
3215
  if (!pairParticipant(pair, input.agentId)) return { ok: false, code: "forbidden", error: "only pair participants can hang up" };
3155
3216
  if (!OPEN_PAIR_STATUSES.includes(pair.status as any)) return { ok: false, code: "invalid", error: `pair is ${pair.status}` };
3156
3217
  const now = Date.now();
3157
- db.prepare("UPDATE pairs SET status = 'ended', ended_at = ?, ended_by = ?, updated_at = ? WHERE id = ?")
3218
+ db.query("UPDATE pairs SET status = 'ended', ended_at = ?, ended_by = ?, updated_at = ? WHERE id = ?")
3158
3219
  .run(now, input.agentId, now, id);
3159
3220
  const ended = getPair(id)!;
3160
3221
  const reason = input.reason ? `\n\nReason:\n${input.reason}` : "";
@@ -3184,7 +3245,7 @@ export function sendPairMessage(id: string, input: PairMessageInput): { ok: true
3184
3245
  targetId: pair.targetId,
3185
3246
  },
3186
3247
  });
3187
- db.prepare("UPDATE pairs SET last_message_at = ?, updated_at = ? WHERE id = ?").run(now, now, id);
3248
+ db.query("UPDATE pairs SET last_message_at = ?, updated_at = ? WHERE id = ?").run(now, now, id);
3188
3249
  return { ok: true, pair: getPair(id)!, message };
3189
3250
  }
3190
3251
 
@@ -3224,7 +3285,7 @@ export function upsertArtifactBlob(input: {
3224
3285
  createdAt?: number;
3225
3286
  }): ArtifactBlob {
3226
3287
  const now = input.createdAt ?? Date.now();
3227
- db.prepare(`
3288
+ db.query(`
3228
3289
  INSERT INTO artifact_blobs (digest, storage_uri, media_type, size, created_at)
3229
3290
  VALUES (?, ?, ?, ?, ?)
3230
3291
  ON CONFLICT(digest) DO UPDATE SET
@@ -3236,12 +3297,12 @@ export function upsertArtifactBlob(input: {
3236
3297
  }
3237
3298
 
3238
3299
  export function getArtifactBlob(digest: string): ArtifactBlob | null {
3239
- const row = db.prepare("SELECT * FROM artifact_blobs WHERE digest = ?").get(digest) as any;
3300
+ const row = db.query("SELECT * FROM artifact_blobs WHERE digest = ?").get(digest) as any;
3240
3301
  return row ? rowToArtifactBlob(row) : null;
3241
3302
  }
3242
3303
 
3243
3304
  export function deleteArtifactBlob(digest: string): boolean {
3244
- return db.prepare("DELETE FROM artifact_blobs WHERE digest = ?").run(digest).changes > 0;
3305
+ return db.query("DELETE FROM artifact_blobs WHERE digest = ?").run(digest).changes > 0;
3245
3306
  }
3246
3307
 
3247
3308
  export function createArtifact(input: {
@@ -3260,7 +3321,7 @@ export function createArtifact(input: {
3260
3321
  if (!getArtifactBlob(input.blobDigest)) throw new ValidationError("artifact blob not found");
3261
3322
  const id = input.id ?? artifactId();
3262
3323
  const now = Date.now();
3263
- db.prepare(`
3324
+ db.query(`
3264
3325
  INSERT INTO artifacts (
3265
3326
  id, blob_digest, media_type, kind, filename, size, visibility, sensitivity,
3266
3327
  created_by, created_at, expires_at, metadata
@@ -3284,7 +3345,7 @@ export function createArtifact(input: {
3284
3345
  }
3285
3346
 
3286
3347
  export function getArtifact(id: string): Artifact | null {
3287
- const row = db.prepare("SELECT * FROM artifacts WHERE id = ?").get(id) as any;
3348
+ const row = db.query("SELECT * FROM artifacts WHERE id = ?").get(id) as any;
3288
3349
  if (!row) return null;
3289
3350
  return rowToArtifact(row, listArtifactLinks(id));
3290
3351
  }
@@ -3311,22 +3372,22 @@ export function listArtifacts(query: {
3311
3372
  }
3312
3373
  const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
3313
3374
  const limit = Math.min(Math.max(query.limit ?? 100, 1), 500);
3314
- const rows = db.prepare(`SELECT a.* FROM artifacts a ${where} ORDER BY a.created_at DESC LIMIT ?`).all(...params, limit) as any[];
3375
+ const rows = db.query(`SELECT a.* FROM artifacts a ${where} ORDER BY a.created_at DESC LIMIT ?`).all(...params, limit) as any[];
3315
3376
  return rows.map((row) => rowToArtifact(row, listArtifactLinks(row.id)));
3316
3377
  }
3317
3378
 
3318
3379
  export function deleteArtifact(id: string): boolean {
3319
3380
  return db.transaction(() => {
3320
- return db.prepare("DELETE FROM artifacts WHERE id = ?").run(id).changes > 0;
3381
+ return db.query("DELETE FROM artifacts WHERE id = ?").run(id).changes > 0;
3321
3382
  })();
3322
3383
  }
3323
3384
 
3324
3385
  function listArtifactLinks(artifactId: string): ArtifactLink[] {
3325
- return (db.prepare("SELECT * FROM artifact_links WHERE artifact_id = ? ORDER BY created_at ASC").all(artifactId) as any[]).map(rowToArtifactLink);
3386
+ return (db.query("SELECT * FROM artifact_links WHERE artifact_id = ? ORDER BY created_at ASC").all(artifactId) as any[]).map(rowToArtifactLink);
3326
3387
  }
3327
3388
 
3328
3389
  export function listArtifactsForEntity(entityType: ArtifactLink["entityType"], entityId: string | number): Artifact[] {
3329
- const rows = db.prepare(`
3390
+ const rows = db.query(`
3330
3391
  SELECT a.*
3331
3392
  FROM artifacts a
3332
3393
  JOIN artifact_links l ON l.artifact_id = a.id
@@ -3349,7 +3410,7 @@ export function linkArtifact(input: {
3349
3410
  if (!getArtifact(input.artifactId)) throw new ValidationError(`artifact ${input.artifactId} not found`);
3350
3411
  const now = Date.now();
3351
3412
  const id = artifactLinkId();
3352
- db.prepare(`
3413
+ db.query(`
3353
3414
  INSERT OR IGNORE INTO artifact_links (id, artifact_id, entity_type, entity_id, role, title, created_by, created_at)
3354
3415
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)
3355
3416
  `).run(
@@ -3362,7 +3423,7 @@ export function linkArtifact(input: {
3362
3423
  input.createdBy,
3363
3424
  now,
3364
3425
  );
3365
- const row = db.prepare(`
3426
+ const row = db.query(`
3366
3427
  SELECT * FROM artifact_links
3367
3428
  WHERE artifact_id = ? AND entity_type = ? AND entity_id = ? AND ((role IS NULL AND ? IS NULL) OR role = ?)
3368
3429
  ORDER BY created_at DESC
@@ -3372,16 +3433,16 @@ export function linkArtifact(input: {
3372
3433
  }
3373
3434
 
3374
3435
  function deleteArtifactLinksForEntity(entityType: ArtifactLink["entityType"], entityId: string | number): number {
3375
- return db.prepare("DELETE FROM artifact_links WHERE entity_type = ? AND entity_id = ?").run(entityType, String(entityId)).changes;
3436
+ return db.query("DELETE FROM artifact_links WHERE entity_type = ? AND entity_id = ?").run(entityType, String(entityId)).changes;
3376
3437
  }
3377
3438
 
3378
3439
  export function artifactBlobReferenceCount(digest: string): number {
3379
- const row = db.prepare("SELECT COUNT(*) AS count FROM artifacts WHERE blob_digest = ?").get(digest) as { count?: number };
3440
+ const row = db.query("SELECT COUNT(*) AS count FROM artifacts WHERE blob_digest = ?").get(digest) as { count?: number };
3380
3441
  return row.count ?? 0;
3381
3442
  }
3382
3443
 
3383
3444
  function unreferencedArtifactBlobs(): ArtifactBlob[] {
3384
- return (db.prepare(`
3445
+ return (db.query(`
3385
3446
  SELECT b.*
3386
3447
  FROM artifact_blobs b
3387
3448
  WHERE NOT EXISTS (SELECT 1 FROM artifacts a WHERE a.blob_digest = b.digest)
@@ -3392,12 +3453,12 @@ export function sweepArtifacts(input: { now?: number; unlinkedGraceMs?: number }
3392
3453
  const now = input.now ?? Date.now();
3393
3454
  const unlinkedCutoff = now - (input.unlinkedGraceMs ?? 60 * 60 * 1000);
3394
3455
  return db.transaction(() => {
3395
- const rows = db.prepare(`
3456
+ const rows = db.query(`
3396
3457
  SELECT id FROM artifacts a
3397
3458
  WHERE (expires_at IS NOT NULL AND expires_at <= ?)
3398
3459
  OR (created_at <= ? AND NOT EXISTS (SELECT 1 FROM artifact_links l WHERE l.artifact_id = a.id))
3399
3460
  `).all(now, unlinkedCutoff) as Array<{ id: string }>;
3400
- for (const row of rows) db.prepare("DELETE FROM artifacts WHERE id = ?").run(row.id);
3461
+ for (const row of rows) db.query("DELETE FROM artifacts WHERE id = ?").run(row.id);
3401
3462
  const blobs = unreferencedArtifactBlobs();
3402
3463
  for (const blob of blobs) deleteArtifactBlob(blob.digest);
3403
3464
  return { artifactIds: rows.map((row) => row.id), blobs };
@@ -3451,7 +3512,7 @@ function linkAttachmentRefs(entityType: ArtifactLink["entityType"], entityId: st
3451
3512
 
3452
3513
  function findMessageByIdempotencyKey(from: string, key: string): Message | null {
3453
3514
  const row = db
3454
- .prepare(`${MSG_SELECT} WHERE m.from_agent = ? AND m.idempotency_key = ? LIMIT 1`)
3515
+ .query(`${MSG_SELECT} WHERE m.from_agent = ? AND m.idempotency_key = ? LIMIT 1`)
3455
3516
  .get(from, key) as any;
3456
3517
  return row ? rowToMessage(row) : null;
3457
3518
  }
@@ -3463,12 +3524,12 @@ function policyNameFromTarget(target: string): string | null {
3463
3524
  }
3464
3525
 
3465
3526
  function spawnPolicyExists(policyName: string): boolean {
3466
- const row = db.prepare("SELECT 1 FROM config WHERE namespace = 'spawn-policy' AND key = ?").get(policyName);
3527
+ const row = db.query("SELECT 1 FROM config WHERE namespace = 'spawn-policy' AND key = ?").get(policyName);
3467
3528
  return Boolean(row);
3468
3529
  }
3469
3530
 
3470
3531
  function runningAgentForPolicy(policyName: string): string | null {
3471
- const row = db.prepare(`
3532
+ const row = db.query(`
3472
3533
  SELECT agent_id
3473
3534
  FROM managed_agent_state
3474
3535
  WHERE policy_name = ? AND status = 'running' AND agent_id IS NOT NULL
@@ -3480,7 +3541,7 @@ function runningAgentForPolicy(policyName: string): string | null {
3480
3541
  }
3481
3542
 
3482
3543
  function queueDepthLimit(target: string): number {
3483
- const row = db.prepare("SELECT value FROM config WHERE namespace = 'system' AND key = 'message-queue'").get() as { value?: string } | undefined;
3544
+ const row = db.query("SELECT value FROM config WHERE namespace = 'system' AND key = 'message-queue'").get() as { value?: string } | undefined;
3484
3545
  const parsed = row?.value ? parseJson<Record<string, unknown>>(row.value, {}) : {};
3485
3546
  const perTarget = parsed?.maxDepthPerTarget;
3486
3547
  if (typeof perTarget === "number" && Number.isSafeInteger(perTarget) && perTarget > 0) return perTarget;
@@ -3494,7 +3555,7 @@ function queueDepthLimit(target: string): number {
3494
3555
 
3495
3556
  function enforceQueueLimit(target: string): void {
3496
3557
  const limit = queueDepthLimit(target);
3497
- const rows = db.prepare(`
3558
+ const rows = db.query(`
3498
3559
  SELECT id FROM messages
3499
3560
  WHERE to_target = ? AND delivery_status = 'queued'
3500
3561
  ORDER BY queued_at DESC, id DESC
@@ -3596,7 +3657,7 @@ const REPLY_DUPLICATE_WINDOW_MS = 2 * 60 * 1000;
3596
3657
 
3597
3658
  function findRecentDuplicateReply(input: SendMessageInput, threadId: number | null, now: number, hasAttachments: boolean): Message | null {
3598
3659
  if (!input.replyTo || threadId === null || hasAttachments) return null;
3599
- const row = db.prepare(`
3660
+ const row = db.query(`
3600
3661
  ${MSG_SELECT}
3601
3662
  WHERE m.from_agent = ?
3602
3663
  AND m.to_target = ?
@@ -3645,7 +3706,7 @@ export function sendMessageWithResult(input: SendMessageInput): { message: Messa
3645
3706
  const duplicateReply = findRecentDuplicateReply(input, threadId, now, attachmentRefs.length > 0);
3646
3707
  if (duplicateReply) return { message: duplicateReply, created: false };
3647
3708
 
3648
- const insert = db.prepare(`
3709
+ const insert = db.query(`
3649
3710
  INSERT INTO messages (
3650
3711
  from_agent, to_target, kind, channel, subject, body, thread_id, reply_to, claimable,
3651
3712
  idempotency_key, delivery_status, queued_at, max_age_seconds, resolved_to_agent,
@@ -3657,7 +3718,7 @@ export function sendMessageWithResult(input: SendMessageInput): { message: Messa
3657
3718
  $payload, $meta, $now
3658
3719
  )
3659
3720
  `);
3660
- const setSelfThread = db.prepare("UPDATE messages SET thread_id = ? WHERE id = ?");
3721
+ const setSelfThread = db.query("UPDATE messages SET thread_id = ? WHERE id = ?");
3661
3722
  const claimable = shouldStoreClaimable(input);
3662
3723
  const kind = inferMessageKind(input);
3663
3724
  const policyName = policyNameFromTarget(input.to);
@@ -3716,7 +3777,7 @@ export function getThread(messageId: number): Message[] {
3716
3777
  const threadId = msg.threadId ?? msg.id;
3717
3778
  return (
3718
3779
  db
3719
- .prepare(`${MSG_SELECT} WHERE m.thread_id = ? ORDER BY m.created_at ASC`)
3780
+ .query(`${MSG_SELECT} WHERE m.thread_id = ? ORDER BY m.created_at ASC`)
3720
3781
  .all(threadId) as any[]
3721
3782
  ).map(rowToMessage);
3722
3783
  }
@@ -3735,10 +3796,10 @@ export function setMessageReaction(input: {
3735
3796
 
3736
3797
  const now = Date.now();
3737
3798
  if (input.action === "remove") {
3738
- db.prepare("DELETE FROM message_reactions WHERE message_id = ? AND actor_id = ? AND emoji = ?")
3799
+ db.query("DELETE FROM message_reactions WHERE message_id = ? AND actor_id = ? AND emoji = ?")
3739
3800
  .run(input.messageId, actorId, emoji);
3740
3801
  } else {
3741
- db.prepare(`
3802
+ db.query(`
3742
3803
  INSERT INTO message_reactions (message_id, actor_id, emoji, created_at, updated_at)
3743
3804
  VALUES (?, ?, ?, ?, ?)
3744
3805
  ON CONFLICT(message_id, actor_id, emoji) DO UPDATE SET updated_at = excluded.updated_at
@@ -3753,7 +3814,7 @@ export function findMessageByTelegramSource(input: {
3753
3814
  chatId: string;
3754
3815
  messageId: string;
3755
3816
  }): Message | null {
3756
- const rows = db.prepare(`
3817
+ const rows = db.query(`
3757
3818
  ${MSG_SELECT}
3758
3819
  WHERE json_extract(m.payload, '$.source.telegram.chatId') = ?
3759
3820
  AND json_extract(m.payload, '$.source.telegram.messageId') = ?
@@ -3767,7 +3828,7 @@ export function findMessageByTelegramSource(input: {
3767
3828
 
3768
3829
  function claimMessageRow(messageId: number, agentId: string, now: number): { ok: false; error: string } | { ok: true } {
3769
3830
  const expiresAt = now + CLAIM_LEASE_MS;
3770
- const result = db.prepare(
3831
+ const result = db.query(
3771
3832
  "UPDATE messages SET claimed_by = ?, claimed_at = ?, claim_expires_at = ? WHERE id = ? AND claimed_by IS NULL"
3772
3833
  ).run(agentId, now, expiresAt, messageId);
3773
3834
 
@@ -3807,7 +3868,7 @@ export function claimMessage(messageId: number, agentId: string, guard?: AgentSe
3807
3868
  throw new ClaimError(`linked task is ${task.status}`);
3808
3869
  }
3809
3870
 
3810
- const taskClaim = db.prepare(`
3871
+ const taskClaim = db.query(`
3811
3872
  UPDATE tasks SET status = 'claimed', claimed_by = ?, claimed_at = ?, claim_expires_at = ?, updated_at = ?
3812
3873
  WHERE id = ? AND message_id = ? AND status IN ('open', 'blocked')
3813
3874
  `).run(agentId, now, expiresAt, now, taskId, messageId);
@@ -3846,12 +3907,12 @@ export function renewMessageClaim(messageId: number, agentId: string, guard?: Ag
3846
3907
  const expiresAt = now + CLAIM_LEASE_MS;
3847
3908
  let task: Task | undefined;
3848
3909
  db.transaction(() => {
3849
- db.prepare("UPDATE messages SET claim_expires_at = ? WHERE id = ? AND claimed_by = ?").run(expiresAt, messageId, agentId);
3910
+ db.query("UPDATE messages SET claim_expires_at = ? WHERE id = ? AND claimed_by = ?").run(expiresAt, messageId, agentId);
3850
3911
  const taskId = typeof msg.payload?.taskId === "number" && Number.isSafeInteger(msg.payload.taskId)
3851
3912
  ? msg.payload.taskId
3852
3913
  : null;
3853
3914
  if (taskId) {
3854
- db.prepare("UPDATE tasks SET claim_expires_at = ?, updated_at = ? WHERE id = ? AND claimed_by = ?").run(expiresAt, now, taskId, agentId);
3915
+ db.query("UPDATE tasks SET claim_expires_at = ?, updated_at = ? WHERE id = ? AND claimed_by = ?").run(expiresAt, now, taskId, agentId);
3855
3916
  task = getTask(taskId) ?? undefined;
3856
3917
  }
3857
3918
  })();
@@ -3859,13 +3920,13 @@ export function renewMessageClaim(messageId: number, agentId: string, guard?: Ag
3859
3920
  }
3860
3921
 
3861
3922
  export function getMessage(id: number): Message | null {
3862
- const row = db.prepare(`${MSG_SELECT} WHERE m.id = ?`).get(id) as any;
3923
+ const row = db.query(`${MSG_SELECT} WHERE m.id = ?`).get(id) as any;
3863
3924
  return row ? rowToMessage(row) : null;
3864
3925
  }
3865
3926
 
3866
3927
  export function getMessageDeliveryAttempts(messageId: number, limit = 50): MessageDeliveryAttempt[] {
3867
3928
  const safeLimit = Math.min(Math.max(limit, 1), 200);
3868
- return (db.prepare(`
3929
+ return (db.query(`
3869
3930
  SELECT * FROM message_delivery_attempts
3870
3931
  WHERE message_id = ?
3871
3932
  ORDER BY created_at DESC, id DESC
@@ -3885,7 +3946,7 @@ function insertMessageDeliveryAttempt(
3885
3946
  },
3886
3947
  now: number,
3887
3948
  ): void {
3888
- db.prepare(`
3949
+ db.query(`
3889
3950
  INSERT INTO message_delivery_attempts (message_id, agent_id, action, status, error, next_retry_at, poison_reason, created_at)
3890
3951
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)
3891
3952
  `).run(
@@ -3911,7 +3972,7 @@ function setMessageDeliveryState(
3911
3972
  },
3912
3973
  now: number,
3913
3974
  ): boolean {
3914
- return db.prepare(`
3975
+ return db.query(`
3915
3976
  UPDATE messages
3916
3977
  SET delivery_status = ?,
3917
3978
  delivery_attempts = delivery_attempts + ?,
@@ -4005,7 +4066,7 @@ export function getMessageDeliveryStatus(id: number): Pick<Message, "id" | "to"
4005
4066
 
4006
4067
  export function listQueuedMessages(target: string, limit = 100): Message[] {
4007
4068
  const safeLimit = Math.min(Math.max(limit, 1), 500);
4008
- return (db.prepare(`
4069
+ return (db.query(`
4009
4070
  ${MSG_SELECT}
4010
4071
  WHERE m.to_target = ? AND m.delivery_status = 'queued'
4011
4072
  ORDER BY m.queued_at ASC, m.id ASC
@@ -4015,7 +4076,7 @@ export function listQueuedMessages(target: string, limit = 100): Message[] {
4015
4076
 
4016
4077
  export function resolveQueuedPolicyMessages(policyName: string, agentId: string): Message[] {
4017
4078
  const target = `policy:${policyName}`;
4018
- const rows = db.prepare(`
4079
+ const rows = db.query(`
4019
4080
  SELECT m.id
4020
4081
  FROM messages m
4021
4082
  WHERE m.to_target = ?
@@ -4031,7 +4092,7 @@ export function resolveQueuedPolicyMessages(policyName: string, agentId: string)
4031
4092
  if (rows.length === 0) return [];
4032
4093
  const ids = rows.map((row) => row.id);
4033
4094
  const placeholders = ids.map(() => "?").join(",");
4034
- db.prepare(`
4095
+ db.query(`
4035
4096
  UPDATE messages
4036
4097
  SET delivery_status = 'pending',
4037
4098
  resolved_to_agent = ?,
@@ -4050,7 +4111,7 @@ export function resolveQueuedPolicyMessages(policyName: string, agentId: string)
4050
4111
  }
4051
4112
 
4052
4113
  export function expireQueuedMessages(now: number = Date.now()): Message[] {
4053
- const rows = db.prepare(`
4114
+ const rows = db.query(`
4054
4115
  SELECT id FROM messages
4055
4116
  WHERE delivery_status = 'queued'
4056
4117
  AND queued_at IS NOT NULL
@@ -4095,7 +4156,8 @@ export function listRecentMessages(limit: number = 100, since?: number, channel?
4095
4156
  const sql = `${MSG_SELECT} ${where} ORDER BY m.created_at DESC LIMIT ?`;
4096
4157
  params.push(limit);
4097
4158
 
4098
- return (db.prepare(sql).all(...params) as any[]).map(rowToMessage).reverse();
4159
+ const rows = timedQuery("listMessages", () => db.query(sql).all(...params) as any[]);
4160
+ return rows.map(rowToMessage).reverse();
4099
4161
  }
4100
4162
 
4101
4163
  export function pollMessages(query: PollQuery): Message[] {
@@ -4163,7 +4225,8 @@ export function pollMessages(query: PollQuery): Message[] {
4163
4225
  const sql = `${MSG_SELECT} WHERE ${conditions.join(" AND ")} ORDER BY m.created_at ASC LIMIT ?`;
4164
4226
  params.push(limit);
4165
4227
 
4166
- return (db.prepare(sql).all(...params) as any[]).map(rowToMessage);
4228
+ const rows = timedQuery("pollMessages", () => db.query(sql).all(...params) as any[]);
4229
+ return rows.map(rowToMessage);
4167
4230
  }
4168
4231
 
4169
4232
  function messageRequiresReply(message: Message): boolean {
@@ -4191,7 +4254,7 @@ function isCoveredByLaterAgentResponse(message: Message, agentId: string): boole
4191
4254
  // Order by id, not created_at: ids are monotonic insertion order, so this is
4192
4255
  // robust when a reply lands in the same millisecond as the message it covers
4193
4256
  // (created_at > … strictly would miss it, leaving the message wrongly pending).
4194
- const replies = (db.prepare(`
4257
+ const replies = (db.query(`
4195
4258
  ${MSG_SELECT}
4196
4259
  WHERE m.from_agent = ?
4197
4260
  AND m.id > ?
@@ -4224,7 +4287,7 @@ function replyObligationFromMessage(message: Message, agentId: string): ReplyObl
4224
4287
 
4225
4288
  export function listPendingReplyObligations(agentId: string, limit = 20): ReplyObligation[] {
4226
4289
  const scanLimit = Math.max(limit * 5, 50);
4227
- const rows = db.prepare(`
4290
+ const rows = timedQuery("listPendingReplyObligations", () => db.query(`
4228
4291
  ${MSG_SELECT}
4229
4292
  WHERE EXISTS (
4230
4293
  SELECT 1 FROM message_reads mr
@@ -4238,7 +4301,7 @@ export function listPendingReplyObligations(agentId: string, limit = 20): ReplyO
4238
4301
  )
4239
4302
  ORDER BY m.created_at ASC
4240
4303
  LIMIT ?
4241
- `).all(agentId, agentId, agentId, scanLimit) as any[];
4304
+ `).all(agentId, agentId, agentId, scanLimit) as any[]);
4242
4305
  return rows
4243
4306
  .map(rowToMessage)
4244
4307
  .filter(messageRequiresReply)
@@ -4248,12 +4311,12 @@ export function listPendingReplyObligations(agentId: string, limit = 20): ReplyO
4248
4311
  }
4249
4312
 
4250
4313
  export function markRead(messageId: number, agentId: string): boolean {
4251
- const exists = db.prepare("SELECT 1 FROM messages WHERE id = ?").get(messageId);
4314
+ const exists = db.query("SELECT 1 FROM messages WHERE id = ?").get(messageId);
4252
4315
  if (!exists) return false;
4253
- db.prepare(
4316
+ db.query(
4254
4317
  "INSERT OR IGNORE INTO message_reads (message_id, agent_id, read_at) VALUES (?, ?, ?)"
4255
4318
  ).run(messageId, agentId, Date.now());
4256
- db.prepare(`
4319
+ db.query(`
4257
4320
  UPDATE messages
4258
4321
  SET delivery_status = 'delivered',
4259
4322
  delivery_last_error = NULL,
@@ -4266,10 +4329,10 @@ export function markRead(messageId: number, agentId: string): boolean {
4266
4329
  }
4267
4330
 
4268
4331
  export function getInboxState(operatorId: string): InboxState {
4269
- const threads = (db.prepare(
4332
+ const threads = (db.query(
4270
4333
  "SELECT * FROM inbox_thread_state WHERE operator_id = ? ORDER BY updated_at DESC",
4271
4334
  ).all(operatorId) as any[]).map(rowToInboxThreadState);
4272
- const drafts = (db.prepare(
4335
+ const drafts = (db.query(
4273
4336
  "SELECT * FROM inbox_drafts WHERE operator_id = ? ORDER BY updated_at DESC",
4274
4337
  ).all(operatorId) as any[]).map(rowToInboxDraft);
4275
4338
  return { operatorId, threads, drafts };
@@ -4282,7 +4345,7 @@ export function setInboxThreadState(input: {
4282
4345
  archivedAtMessageId?: number | null;
4283
4346
  }): InboxThreadState {
4284
4347
  const now = Date.now();
4285
- const current = db.prepare(
4348
+ const current = db.query(
4286
4349
  "SELECT * FROM inbox_thread_state WHERE operator_id = ? AND peer_id = ?",
4287
4350
  ).get(input.operatorId, input.peerId) as any | undefined;
4288
4351
 
@@ -4293,7 +4356,7 @@ export function setInboxThreadState(input: {
4293
4356
  ? input.archivedAtMessageId ?? null
4294
4357
  : current?.archived_at_message_id ?? null;
4295
4358
 
4296
- db.prepare(`
4359
+ db.query(`
4297
4360
  INSERT INTO inbox_thread_state (operator_id, peer_id, read_cursor_message_id, archived_at_message_id, updated_at)
4298
4361
  VALUES (?, ?, ?, ?, ?)
4299
4362
  ON CONFLICT(operator_id, peer_id) DO UPDATE SET
@@ -4302,7 +4365,7 @@ export function setInboxThreadState(input: {
4302
4365
  updated_at = excluded.updated_at
4303
4366
  `).run(input.operatorId, input.peerId, readCursorMessageId, archivedAtMessageId, now);
4304
4367
 
4305
- return rowToInboxThreadState(db.prepare(
4368
+ return rowToInboxThreadState(db.query(
4306
4369
  "SELECT * FROM inbox_thread_state WHERE operator_id = ? AND peer_id = ?",
4307
4370
  ).get(input.operatorId, input.peerId));
4308
4371
  }
@@ -4315,7 +4378,7 @@ export function setInboxDraft(input: {
4315
4378
  channel?: string | null;
4316
4379
  }): InboxDraft {
4317
4380
  const now = Date.now();
4318
- db.prepare(`
4381
+ db.query(`
4319
4382
  INSERT INTO inbox_drafts (operator_id, peer_id, body, subject, channel, updated_at)
4320
4383
  VALUES (?, ?, ?, ?, ?, ?)
4321
4384
  ON CONFLICT(operator_id, peer_id) DO UPDATE SET
@@ -4325,13 +4388,13 @@ export function setInboxDraft(input: {
4325
4388
  updated_at = excluded.updated_at
4326
4389
  `).run(input.operatorId, input.peerId, input.body, input.subject ?? null, input.channel ?? null, now);
4327
4390
 
4328
- return rowToInboxDraft(db.prepare(
4391
+ return rowToInboxDraft(db.query(
4329
4392
  "SELECT * FROM inbox_drafts WHERE operator_id = ? AND peer_id = ?",
4330
4393
  ).get(input.operatorId, input.peerId));
4331
4394
  }
4332
4395
 
4333
4396
  export function deleteInboxDraft(operatorId: string, peerId: string): boolean {
4334
- return db.prepare("DELETE FROM inbox_drafts WHERE operator_id = ? AND peer_id = ?").run(operatorId, peerId).changes > 0;
4397
+ return db.query("DELETE FROM inbox_drafts WHERE operator_id = ? AND peer_id = ?").run(operatorId, peerId).changes > 0;
4335
4398
  }
4336
4399
 
4337
4400
  export function createChatHistoryImport(input: {
@@ -4359,7 +4422,7 @@ export function createChatHistoryImport(input: {
4359
4422
  const id = randomUUID();
4360
4423
  const now = Date.now();
4361
4424
  db.transaction(() => {
4362
- db.prepare(`
4425
+ db.query(`
4363
4426
  INSERT INTO chat_history_imports (
4364
4427
  id, target_agent_id, target_spawn_request_id, source_peer_id, source_agent_id,
4365
4428
  source_thread_id, source_agent_label, imported_by, imported_at
@@ -4376,7 +4439,7 @@ export function createChatHistoryImport(input: {
4376
4439
  now,
4377
4440
  );
4378
4441
 
4379
- const insertEntry = db.prepare(`
4442
+ const insertEntry = db.query(`
4380
4443
  INSERT INTO chat_history_import_entries (
4381
4444
  import_id, position, original_message_id, original_from, original_to, original_created_at, message_snapshot
4382
4445
  ) VALUES (?, ?, ?, ?, ?, ?, ?)
@@ -4398,9 +4461,9 @@ export function createChatHistoryImport(input: {
4398
4461
  }
4399
4462
 
4400
4463
  function getChatHistoryImport(id: string): ChatHistoryImport | null {
4401
- const row = db.prepare("SELECT * FROM chat_history_imports WHERE id = ?").get(id) as any | undefined;
4464
+ const row = db.query("SELECT * FROM chat_history_imports WHERE id = ?").get(id) as any | undefined;
4402
4465
  if (!row) return null;
4403
- const entries = (db.prepare(
4466
+ const entries = (db.query(
4404
4467
  "SELECT * FROM chat_history_import_entries WHERE import_id = ? ORDER BY position ASC",
4405
4468
  ).all(id) as any[]).map(rowToChatHistoryImportEntry);
4406
4469
  return rowToChatHistoryImport(row, entries);
@@ -4423,9 +4486,9 @@ export function listChatHistoryImports(input: {
4423
4486
  }
4424
4487
  const limit = Math.max(1, Math.min(input.limit ?? 100, 500));
4425
4488
  const where = conditions.length ? `WHERE ${conditions.join(" OR ")}` : "";
4426
- const rows = db.prepare(`SELECT * FROM chat_history_imports ${where} ORDER BY imported_at ASC LIMIT ?`).all(...params, limit) as any[];
4489
+ const rows = db.query(`SELECT * FROM chat_history_imports ${where} ORDER BY imported_at ASC LIMIT ?`).all(...params, limit) as any[];
4427
4490
  return rows.map((row) => {
4428
- const entries = (db.prepare(
4491
+ const entries = (db.query(
4429
4492
  "SELECT * FROM chat_history_import_entries WHERE import_id = ? ORDER BY position ASC",
4430
4493
  ).all(row.id) as any[]).map(rowToChatHistoryImportEntry);
4431
4494
  return rowToChatHistoryImport(row, entries);
@@ -4454,7 +4517,7 @@ export function listActivityEvents(input: {
4454
4517
  }
4455
4518
  const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
4456
4519
  params.push(input.limit ?? 200);
4457
- return (db.prepare(
4520
+ return (db.query(
4458
4521
  `SELECT * FROM activity_events ${where} ORDER BY created_at DESC, id DESC LIMIT ?`,
4459
4522
  ).all(...params) as any[]).map(rowToActivityEvent);
4460
4523
  }
@@ -4483,11 +4546,11 @@ export function getAgentTimeline(agentId: string, opts: { limit?: number; since?
4483
4546
 
4484
4547
  // Managed agents: include policy-scoped lifecycle transitions recorded before
4485
4548
  // the agent registered (agent_id null), correlated via metadata.policyName.
4486
- const managed = db.prepare("SELECT policy_name FROM managed_agent_state WHERE agent_id = ? LIMIT 1").get(agentId) as { policy_name?: string } | undefined;
4549
+ const managed = db.query("SELECT policy_name FROM managed_agent_state WHERE agent_id = ? LIMIT 1").get(agentId) as { policy_name?: string } | undefined;
4487
4550
  if (managed?.policy_name) {
4488
4551
  const rows = (since !== undefined
4489
- ? db.prepare("SELECT * FROM activity_events WHERE agent_id IS NULL AND json_extract(metadata, '$.policyName') = ? AND created_at >= ? ORDER BY created_at DESC LIMIT ?").all(managed.policy_name, since, limit)
4490
- : db.prepare("SELECT * FROM activity_events WHERE agent_id IS NULL AND json_extract(metadata, '$.policyName') = ? ORDER BY created_at DESC LIMIT ?").all(managed.policy_name, limit)) as any[];
4552
+ ? db.query("SELECT * FROM activity_events WHERE agent_id IS NULL AND json_extract(metadata, '$.policyName') = ? AND created_at >= ? ORDER BY created_at DESC LIMIT ?").all(managed.policy_name, since, limit)
4553
+ : db.query("SELECT * FROM activity_events WHERE agent_id IS NULL AND json_extract(metadata, '$.policyName') = ? ORDER BY created_at DESC LIMIT ?").all(managed.policy_name, limit)) as any[];
4491
4554
  for (const ev of rows.map(rowToActivityEvent)) {
4492
4555
  entries.push({ ts: ev.createdAt, source: "activity", kind: ev.kind, title: ev.title, detail: ev.body, metadata: ev.metadata });
4493
4556
  }
@@ -4495,16 +4558,16 @@ export function getAgentTimeline(agentId: string, opts: { limit?: number; since?
4495
4558
 
4496
4559
  // Commands targeting this agent (spawn/shutdown/restart/compact) with outcome.
4497
4560
  const cmds = (since !== undefined
4498
- ? db.prepare("SELECT id, type, status, result, correlation_id, created_at, updated_at FROM commands WHERE target = ? AND updated_at >= ? ORDER BY updated_at DESC LIMIT ?").all(agentId, since, limit)
4499
- : db.prepare("SELECT id, type, status, result, correlation_id, created_at, updated_at FROM commands WHERE target = ? ORDER BY updated_at DESC LIMIT ?").all(agentId, limit)) as any[];
4561
+ ? db.query("SELECT id, type, status, result, correlation_id, created_at, updated_at FROM commands WHERE target = ? AND updated_at >= ? ORDER BY updated_at DESC LIMIT ?").all(agentId, since, limit)
4562
+ : db.query("SELECT id, type, status, result, correlation_id, created_at, updated_at FROM commands WHERE target = ? ORDER BY updated_at DESC LIMIT ?").all(agentId, limit)) as any[];
4500
4563
  for (const c of cmds) {
4501
4564
  entries.push({ ts: c.updated_at ?? c.created_at, source: "command", kind: c.type, title: `${c.type} · ${c.status}`, detail: c.result ?? undefined, metadata: { id: c.id, status: c.status, correlationId: c.correlation_id ?? undefined } });
4502
4565
  }
4503
4566
 
4504
4567
  // Delivery attempts involving this agent (failures, retries, poison).
4505
4568
  const attempts = (since !== undefined
4506
- ? db.prepare("SELECT * FROM message_delivery_attempts WHERE agent_id = ? AND created_at >= ? ORDER BY created_at DESC LIMIT ?").all(agentId, since, limit)
4507
- : db.prepare("SELECT * FROM message_delivery_attempts WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?").all(agentId, limit)) as any[];
4569
+ ? db.query("SELECT * FROM message_delivery_attempts WHERE agent_id = ? AND created_at >= ? ORDER BY created_at DESC LIMIT ?").all(agentId, since, limit)
4570
+ : db.query("SELECT * FROM message_delivery_attempts WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?").all(agentId, limit)) as any[];
4508
4571
  for (const row of attempts) {
4509
4572
  entries.push({ ts: row.created_at, source: "delivery", kind: `delivery.${row.action}`, title: `delivery ${row.status}`, detail: row.error ?? undefined, metadata: { messageId: row.message_id, status: row.status, poisonReason: row.poison_reason ?? undefined } });
4510
4573
  }
@@ -4515,11 +4578,11 @@ export function getAgentTimeline(agentId: string, opts: { limit?: number; since?
4515
4578
 
4516
4579
  export function createActivityEvent(input: ActivityEventInput): ActivityEvent {
4517
4580
  if (input.clientId) {
4518
- const existing = db.prepare("SELECT * FROM activity_events WHERE client_id = ?").get(input.clientId) as any | undefined;
4581
+ const existing = db.query("SELECT * FROM activity_events WHERE client_id = ?").get(input.clientId) as any | undefined;
4519
4582
  if (existing) return rowToActivityEvent(existing);
4520
4583
  }
4521
4584
  const now = Date.now();
4522
- const result = db.prepare(`
4585
+ const result = db.query(`
4523
4586
  INSERT INTO activity_events (
4524
4587
  operator_id, client_id, kind, title, body, meta_text, icon, view, peer_id,
4525
4588
  message_id, pair_id, task_id, agent_id, metadata, created_at
@@ -4542,21 +4605,21 @@ export function createActivityEvent(input: ActivityEventInput): ActivityEvent {
4542
4605
  JSON.stringify(input.metadata ?? {}),
4543
4606
  now,
4544
4607
  );
4545
- return rowToActivityEvent(db.prepare("SELECT * FROM activity_events WHERE id = ?").get(Number(result.lastInsertRowid)));
4608
+ return rowToActivityEvent(db.query("SELECT * FROM activity_events WHERE id = ?").get(Number(result.lastInsertRowid)));
4546
4609
  }
4547
4610
 
4548
4611
  export function deleteMessage(id: number): boolean {
4549
4612
  return db.transaction(() => {
4550
4613
  // Break reply_to references from children so the FK doesn't block delete.
4551
4614
  // Children keep their thread_id — the thread shows up minus this message.
4552
- db.prepare("UPDATE messages SET reply_to = NULL WHERE reply_to = ?").run(id);
4615
+ db.query("UPDATE messages SET reply_to = NULL WHERE reply_to = ?").run(id);
4553
4616
  deleteArtifactLinksForEntity("message", id);
4554
- return db.prepare("DELETE FROM messages WHERE id = ?").run(id).changes > 0;
4617
+ return db.query("DELETE FROM messages WHERE id = ?").run(id).changes > 0;
4555
4618
  })();
4556
4619
  }
4557
4620
 
4558
4621
  export function getLatestMessageId(): number {
4559
- const row = db.prepare("SELECT MAX(id) as id FROM messages").get() as any;
4622
+ const row = db.query("SELECT MAX(id) as id FROM messages").get() as any;
4560
4623
  return row?.id ?? 0;
4561
4624
  }
4562
4625
 
@@ -4564,10 +4627,10 @@ export function pruneOldMessages(maxAgeMs: number): number {
4564
4627
  const cutoff = Date.now() - maxAgeMs;
4565
4628
  return db.transaction(() => {
4566
4629
  db
4567
- .prepare("UPDATE messages SET reply_to = NULL WHERE reply_to IN (SELECT id FROM messages WHERE created_at < ?)")
4630
+ .query("UPDATE messages SET reply_to = NULL WHERE reply_to IN (SELECT id FROM messages WHERE created_at < ?)")
4568
4631
  .run(cutoff);
4569
4632
  return db
4570
- .prepare("DELETE FROM messages WHERE created_at < ?")
4633
+ .query("DELETE FROM messages WHERE created_at < ?")
4571
4634
  .run(cutoff).changes;
4572
4635
  })();
4573
4636
  }
@@ -4582,28 +4645,28 @@ export function getStats(): {
4582
4645
  openTasks: number;
4583
4646
  } {
4584
4647
  const agents = (
4585
- db.prepare("SELECT COUNT(*) as c FROM agents").get() as any
4648
+ db.query("SELECT COUNT(*) as c FROM agents").get() as any
4586
4649
  ).c;
4587
4650
  const online = (
4588
4651
  db
4589
- .prepare(
4652
+ .query(
4590
4653
  "SELECT COUNT(*) as c FROM agents WHERE status != 'offline' AND last_seen > ?"
4591
4654
  )
4592
4655
  .get(Date.now() - STALE_TTL_MS) as any
4593
4656
  ).c;
4594
4657
  const messages = (
4595
- db.prepare("SELECT COUNT(*) as c FROM messages").get() as any
4658
+ db.query("SELECT COUNT(*) as c FROM messages").get() as any
4596
4659
  ).c;
4597
4660
  const messagesLast24h = (
4598
4661
  db
4599
- .prepare("SELECT COUNT(*) as c FROM messages WHERE created_at > ?")
4662
+ .query("SELECT COUNT(*) as c FROM messages WHERE created_at > ?")
4600
4663
  .get(Date.now() - DAY_MS) as any
4601
4664
  ).c;
4602
4665
  const tasks = (
4603
- db.prepare("SELECT COUNT(*) as c FROM tasks").get() as any
4666
+ db.query("SELECT COUNT(*) as c FROM tasks").get() as any
4604
4667
  ).c;
4605
4668
  const openTasks = (
4606
- db.prepare("SELECT COUNT(*) as c FROM tasks WHERE status NOT IN ('done', 'failed', 'canceled')").get() as any
4669
+ db.query("SELECT COUNT(*) as c FROM tasks WHERE status NOT IN ('done', 'failed', 'canceled')").get() as any
4607
4670
  ).c;
4608
4671
 
4609
4672
  return { version: VERSION, agents, online, messages, messagesLast24h, tasks, openTasks };
@@ -4613,14 +4676,14 @@ export function getHealth(now: number = Date.now()): HealthReport {
4613
4676
  const checks: HealthCheck[] = [];
4614
4677
 
4615
4678
  try {
4616
- db.prepare("SELECT 1").get();
4679
+ db.query("SELECT 1").get();
4617
4680
  checks.push({ name: "database", status: "ok" });
4618
4681
  } catch (e) {
4619
4682
  checks.push({ name: "database", status: "error", detail: e instanceof Error ? e.message : "database check failed" });
4620
4683
  }
4621
4684
 
4622
4685
  const staleLiveAgents = (db
4623
- .prepare("SELECT COUNT(*) as c FROM agents WHERE status != 'offline' AND last_seen <= ? AND id NOT IN ('user', 'system')")
4686
+ .query("SELECT COUNT(*) as c FROM agents WHERE status != 'offline' AND last_seen <= ? AND id NOT IN ('user', 'system')")
4624
4687
  .get(now - STALE_TTL_MS) as any).c as number;
4625
4688
  checks.push({
4626
4689
  name: "stale-live-agents",
@@ -4630,7 +4693,7 @@ export function getHealth(now: number = Date.now()): HealthReport {
4630
4693
  });
4631
4694
 
4632
4695
  const expiredMessageClaims = (db
4633
- .prepare("SELECT COUNT(*) as c FROM messages WHERE claimed_by IS NOT NULL AND (claim_expires_at IS NULL OR claim_expires_at <= ?) AND NOT EXISTS (SELECT 1 FROM tasks t WHERE t.message_id = messages.id AND t.status IN ('done', 'failed', 'canceled'))")
4696
+ .query("SELECT COUNT(*) as c FROM messages WHERE claimed_by IS NOT NULL AND (claim_expires_at IS NULL OR claim_expires_at <= ?) AND NOT EXISTS (SELECT 1 FROM tasks t WHERE t.message_id = messages.id AND t.status IN ('done', 'failed', 'canceled'))")
4634
4697
  .get(now) as any).c as number;
4635
4698
  checks.push({
4636
4699
  name: "expired-message-claims",
@@ -4640,7 +4703,7 @@ export function getHealth(now: number = Date.now()): HealthReport {
4640
4703
  });
4641
4704
 
4642
4705
  const expiredTaskClaims = (db
4643
- .prepare("SELECT COUNT(*) as c FROM tasks WHERE claimed_by IS NOT NULL AND (claim_expires_at IS NULL OR claim_expires_at <= ?) AND status IN ('claimed', 'in_progress', 'blocked')")
4706
+ .query("SELECT COUNT(*) as c FROM tasks WHERE claimed_by IS NOT NULL AND (claim_expires_at IS NULL OR claim_expires_at <= ?) AND status IN ('claimed', 'in_progress', 'blocked')")
4644
4707
  .get(now) as any).c as number;
4645
4708
  checks.push({
4646
4709
  name: "expired-task-claims",
@@ -4650,7 +4713,7 @@ export function getHealth(now: number = Date.now()): HealthReport {
4650
4713
  });
4651
4714
 
4652
4715
  const offlineClaimedTasks = (db
4653
- .prepare(`
4716
+ .query(`
4654
4717
  SELECT COUNT(*) as c
4655
4718
  FROM tasks t
4656
4719
  JOIN agents a ON a.id = t.claimed_by
@@ -4797,12 +4860,12 @@ function parseOrchestratorUpgrade(value: unknown): OrchestratorUpgradeState | un
4797
4860
  * meta blob (no schema change). Pass null to clear.
4798
4861
  */
4799
4862
  export function setOrchestratorUpgradeState(id: string, state: OrchestratorUpgradeState | null): Orchestrator | null {
4800
- const row = db.prepare("SELECT meta FROM orchestrators WHERE id = ?").get(id) as { meta?: string } | undefined;
4863
+ const row = db.query("SELECT meta FROM orchestrators WHERE id = ?").get(id) as { meta?: string } | undefined;
4801
4864
  if (!row) return null;
4802
4865
  const meta = parseJson<Record<string, unknown>>(row.meta ?? "{}", {});
4803
4866
  if (state) meta.upgrade = state;
4804
4867
  else delete meta.upgrade;
4805
- db.prepare("UPDATE orchestrators SET meta = ? WHERE id = ?").run(JSON.stringify(meta), id);
4868
+ db.query("UPDATE orchestrators SET meta = ? WHERE id = ?").run(JSON.stringify(meta), id);
4806
4869
  return getOrchestrator(id);
4807
4870
  }
4808
4871
 
@@ -4890,11 +4953,11 @@ export function upsertOrchestrator(input: RegisterOrchestratorInput): Orchestrat
4890
4953
  // Carry forward server-managed meta the orchestrator never reports (upgrade
4891
4954
  // state) — registration meta would otherwise drop it on the very re-register
4892
4955
  // that follows a self-upgrade restart, breaking version reconciliation.
4893
- const existingRow = db.prepare("SELECT meta FROM orchestrators WHERE id = ?").get(input.id) as { meta?: string } | undefined;
4956
+ const existingRow = db.query("SELECT meta FROM orchestrators WHERE id = ?").get(input.id) as { meta?: string } | undefined;
4894
4957
  const existingMeta = existingRow ? parseJson<Record<string, unknown>>(existingRow.meta ?? "{}", {}) : {};
4895
4958
  const mergedMeta = mergeOrchestratorRuntimeMeta(input.meta ?? {}, input);
4896
4959
  if (existingMeta.upgrade !== undefined && mergedMeta.upgrade === undefined) mergedMeta.upgrade = existingMeta.upgrade;
4897
- const stmt = db.prepare(`
4960
+ const stmt = db.query(`
4898
4961
  INSERT INTO orchestrators (id, hostname, status, agent_id, providers, base_dir, api_url, env_keys, meta, last_seen, created_at)
4899
4962
  VALUES ($id, $hostname, 'online', $agentId, $providers, $baseDir, $apiUrl, $envKeys, $meta, $now, $now)
4900
4963
  ON CONFLICT(id) DO UPDATE SET
@@ -4944,24 +5007,24 @@ export function upsertOrchestrator(input: RegisterOrchestratorInput): Orchestrat
4944
5007
  }
4945
5008
 
4946
5009
  export function getOrchestrator(id: string): Orchestrator | null {
4947
- const row = db.prepare("SELECT * FROM orchestrators WHERE id = ?").get(id) as any;
5010
+ const row = db.query("SELECT * FROM orchestrators WHERE id = ?").get(id) as any;
4948
5011
  return row ? rowToOrchestrator(row) : null;
4949
5012
  }
4950
5013
 
4951
5014
  export function listOrchestrators(): Orchestrator[] {
4952
- return (db.prepare("SELECT * FROM orchestrators ORDER BY hostname").all() as any[]).map(rowToOrchestrator);
5015
+ return (db.query("SELECT * FROM orchestrators ORDER BY hostname").all() as any[]).map(rowToOrchestrator);
4953
5016
  }
4954
5017
 
4955
5018
  export function orchestratorHeartbeat(id: string, runtime: OrchestratorRuntimeInput = {}): Orchestrator | null {
4956
5019
  const now = Date.now();
4957
- const row = db.prepare("SELECT meta FROM orchestrators WHERE id = ?").get(id) as { meta?: string } | undefined;
5020
+ const row = db.query("SELECT meta FROM orchestrators WHERE id = ?").get(id) as { meta?: string } | undefined;
4958
5021
  if (!row) return null;
4959
5022
  const meta = mergeOrchestratorRuntimeMeta(parseJson<Record<string, unknown>>(row.meta ?? "{}", {}), runtime);
4960
5023
  if (runtime.providers) {
4961
- db.prepare("UPDATE orchestrators SET last_seen = ?, status = 'online', providers = ?, meta = ? WHERE id = ?")
5024
+ db.query("UPDATE orchestrators SET last_seen = ?, status = 'online', providers = ?, meta = ? WHERE id = ?")
4962
5025
  .run(now, JSON.stringify(runtime.providers), JSON.stringify(meta), id);
4963
5026
  } else {
4964
- db.prepare("UPDATE orchestrators SET last_seen = ?, status = 'online', meta = ? WHERE id = ?").run(now, JSON.stringify(meta), id);
5027
+ db.query("UPDATE orchestrators SET last_seen = ?, status = 'online', meta = ? WHERE id = ?").run(now, JSON.stringify(meta), id);
4965
5028
  }
4966
5029
  // Also heartbeat the agent
4967
5030
  const orch = getOrchestrator(id);
@@ -4993,7 +5056,7 @@ export function updateManagedAgents(id: string, agents: ManagedAgent[]): Orchest
4993
5056
  },
4994
5057
  } : agent;
4995
5058
  });
4996
- db.prepare("UPDATE orchestrators SET managed_agents = ?, last_seen = ? WHERE id = ?")
5059
+ db.query("UPDATE orchestrators SET managed_agents = ?, last_seen = ? WHERE id = ?")
4997
5060
  .run(JSON.stringify(enriched), Date.now(), id);
4998
5061
  return getOrchestrator(id);
4999
5062
  }
@@ -5051,7 +5114,7 @@ function upsertWorkspaceFromManagedAgent(agent: ManagedAgent): WorkspaceRecord |
5051
5114
 
5052
5115
  export function upsertWorkspace(input: Omit<WorkspaceRecord, "createdAt" | "updatedAt"> & Partial<Pick<WorkspaceRecord, "createdAt" | "updatedAt">>): WorkspaceRecord {
5053
5116
  const now = Date.now();
5054
- db.prepare(`
5117
+ db.query(`
5055
5118
  INSERT INTO workspaces (id, repo_root, source_cwd, worktree_path, branch, base_ref, base_sha, mode, requested_mode, status, owner_agent_id, owner_policy_name, owner_automation_run_id, steward_agent_id, metadata, created_at, updated_at, ready_at, cleaned_at)
5056
5119
  VALUES ($id, $repoRoot, $sourceCwd, $worktreePath, $branch, $baseRef, $baseSha, $mode, $requestedMode, $status, $ownerAgentId, $ownerPolicyName, $ownerAutomationRunId, $stewardAgentId, $metadata, $createdAt, $updatedAt, $readyAt, $cleanedAt)
5057
5120
  ON CONFLICT(id) DO UPDATE SET
@@ -5098,7 +5161,7 @@ export function upsertWorkspace(input: Omit<WorkspaceRecord, "createdAt" | "upda
5098
5161
  }
5099
5162
 
5100
5163
  export function getWorkspace(id: string): WorkspaceRecord | null {
5101
- const row = db.prepare("SELECT * FROM workspaces WHERE id = ?").get(id) as any;
5164
+ const row = db.query("SELECT * FROM workspaces WHERE id = ?").get(id) as any;
5102
5165
  return row ? rowToWorkspace(row) : null;
5103
5166
  }
5104
5167
 
@@ -5109,11 +5172,11 @@ export function listWorkspaces(filter: { repoRoot?: string; ownerAgentId?: strin
5109
5172
  if (filter.ownerAgentId) { where.push("owner_agent_id = ?"); params.push(filter.ownerAgentId); }
5110
5173
  if (filter.status) { where.push("status = ?"); params.push(filter.status); }
5111
5174
  const sql = `SELECT * FROM workspaces${where.length ? " WHERE " + where.join(" AND ") : ""} ORDER BY updated_at DESC`;
5112
- return (db.prepare(sql).all(...params) as any[]).map(rowToWorkspace);
5175
+ return (db.query(sql).all(...params) as any[]).map(rowToWorkspace);
5113
5176
  }
5114
5177
 
5115
5178
  export function deleteWorkspace(id: string): boolean {
5116
- return db.prepare("DELETE FROM workspaces WHERE id = ?").run(id).changes > 0;
5179
+ return db.query("DELETE FROM workspaces WHERE id = ?").run(id).changes > 0;
5117
5180
  }
5118
5181
 
5119
5182
  // Shared-mode rows are pure occupancy markers (no worktree on disk) that only
@@ -5131,9 +5194,9 @@ export function pruneOrphanedSharedWorkspaces(): string[] {
5131
5194
  OR owner_agent_id NOT IN (SELECT id FROM agents)
5132
5195
  OR owner_agent_id IN (SELECT id FROM agents WHERE status = 'offline')
5133
5196
  )`;
5134
- const rows = db.prepare(`SELECT id FROM workspaces WHERE ${orphanCondition}`).all() as Array<{ id: string }>;
5197
+ const rows = db.query(`SELECT id FROM workspaces WHERE ${orphanCondition}`).all() as Array<{ id: string }>;
5135
5198
  if (!rows.length) return [];
5136
- db.prepare(`DELETE FROM workspaces WHERE ${orphanCondition}`).run();
5199
+ db.query(`DELETE FROM workspaces WHERE ${orphanCondition}`).run();
5137
5200
  return rows.map((r) => r.id);
5138
5201
  })();
5139
5202
  }
@@ -5143,7 +5206,7 @@ export function updateWorkspaceStatus(id: string, status: WorkspaceStatus, metad
5143
5206
  if (!existing) return null;
5144
5207
  const nextMeta = { ...existing.metadata, ...metadata };
5145
5208
  const now = Date.now();
5146
- db.prepare(`
5209
+ db.query(`
5147
5210
  UPDATE workspaces
5148
5211
  SET status = ?, metadata = ?, updated_at = ?, ready_at = coalesce(ready_at, ?), cleaned_at = coalesce(cleaned_at, ?)
5149
5212
  WHERE id = ?
@@ -5182,19 +5245,19 @@ function rowToRepoSteward(row: any): RepoStewardRecord {
5182
5245
  }
5183
5246
 
5184
5247
  export function getRepoSteward(repoRoot: string): RepoStewardRecord | null {
5185
- const row = db.prepare("SELECT * FROM repo_stewards WHERE repo_root = ?").get(repoRoot) as any;
5248
+ const row = db.query("SELECT * FROM repo_stewards WHERE repo_root = ?").get(repoRoot) as any;
5186
5249
  return row ? rowToRepoSteward(row) : null;
5187
5250
  }
5188
5251
 
5189
5252
  export function listRepoStewards(): RepoStewardRecord[] {
5190
- return (db.prepare("SELECT * FROM repo_stewards ORDER BY updated_at DESC").all() as any[]).map(rowToRepoSteward);
5253
+ return (db.query("SELECT * FROM repo_stewards ORDER BY updated_at DESC").all() as any[]).map(rowToRepoSteward);
5191
5254
  }
5192
5255
 
5193
5256
  // Persist the elected steward for a repo. The row is never deleted, so a repo's
5194
5257
  // stewardship survives a full all-agents-offline gap (steward goes NULL/dormant,
5195
5258
  // last_steward_agent_id keeps continuity) and resumes on the next agent join.
5196
5259
  function upsertRepoSteward(repoRoot: string, steward: string | null, now: number): void {
5197
- db.prepare(`
5260
+ db.query(`
5198
5261
  INSERT INTO repo_stewards (repo_root, steward_agent_id, last_steward_agent_id, elected_at, updated_at)
5199
5262
  VALUES ($repoRoot, $steward, $steward, $electedAt, $now)
5200
5263
  ON CONFLICT(repo_root) DO UPDATE SET
@@ -5210,7 +5273,7 @@ function upsertRepoSteward(repoRoot: string, steward: string | null, now: number
5210
5273
 
5211
5274
  function electWorkspaceStewards(repoRoot?: string): void {
5212
5275
  const params: string[] = repoRoot ? [repoRoot] : [];
5213
- const repoRows = db.prepare(`
5276
+ const repoRows = db.query(`
5214
5277
  SELECT DISTINCT repo_root FROM workspaces
5215
5278
  WHERE status IN (${STEWARD_LIVE_STATUSES})
5216
5279
  ${repoRoot ? "AND repo_root = ?" : ""}
@@ -5220,7 +5283,7 @@ function electWorkspaceStewards(repoRoot?: string): void {
5220
5283
  // Candidate pool: owners of live workspaces in this repo who are online,
5221
5284
  // oldest first. A steward must be an online agent actively in the repo — an
5222
5285
  // offline agent can't coordinate, so it is never elected (the old bug).
5223
- const pool = (db.prepare(`
5286
+ const pool = (db.query(`
5224
5287
  SELECT w.owner_agent_id AS id, MIN(w.created_at) AS created_at
5225
5288
  FROM workspaces w JOIN agents a ON a.id = w.owner_agent_id
5226
5289
  WHERE w.repo_root = ? AND w.owner_agent_id IS NOT NULL
@@ -5239,7 +5302,7 @@ function electWorkspaceStewards(repoRoot?: string): void {
5239
5302
  // re-elections don't churn updated_at and reset the auto-abandon clock for a
5240
5303
  // dormant repo (a stranded review_requested must still age out).
5241
5304
  if (steward !== current) {
5242
- db.prepare(`UPDATE workspaces SET steward_agent_id = ?, updated_at = ? WHERE repo_root = ? AND status IN (${STEWARD_LIVE_STATUSES})`)
5305
+ db.query(`UPDATE workspaces SET steward_agent_id = ?, updated_at = ? WHERE repo_root = ? AND status IN (${STEWARD_LIVE_STATUSES})`)
5243
5306
  .run(steward, now, row.repo_root);
5244
5307
  }
5245
5308
  }
@@ -5263,7 +5326,7 @@ export function patchWorkspaceMetadata(id: string, patch: Record<string, unknown
5263
5326
  if (v === undefined) delete next[k];
5264
5327
  else next[k] = v;
5265
5328
  }
5266
- db.prepare("UPDATE workspaces SET metadata = ? WHERE id = ?").run(JSON.stringify(next), id);
5329
+ db.query("UPDATE workspaces SET metadata = ? WHERE id = ?").run(JSON.stringify(next), id);
5267
5330
  return getWorkspace(id);
5268
5331
  }
5269
5332
 
@@ -5271,7 +5334,7 @@ export function patchWorkspaceMetadata(id: string, patch: Record<string, unknown
5271
5334
  // when an agent (re)registers so a dormant repo regains a steward on rejoin
5272
5335
  // without a full unscoped sweep.
5273
5336
  function electWorkspaceStewardsForAgent(agentId: string): void {
5274
- const repos = db.prepare(`
5337
+ const repos = db.query(`
5275
5338
  SELECT DISTINCT repo_root FROM workspaces
5276
5339
  WHERE owner_agent_id = ? AND status IN (${STEWARD_LIVE_STATUSES})
5277
5340
  `).all(agentId) as Array<{ repo_root: string }>;
@@ -5301,18 +5364,18 @@ function rowToMergeLease(row: any): MergeLeaseRecord {
5301
5364
  }
5302
5365
 
5303
5366
  export function getMergeLease(repoRoot: string): MergeLeaseRecord | null {
5304
- const row = db.prepare("SELECT * FROM workspace_merge_leases WHERE repo_root = ?").get(repoRoot) as any;
5367
+ const row = db.query("SELECT * FROM workspace_merge_leases WHERE repo_root = ?").get(repoRoot) as any;
5305
5368
  return row ? rowToMergeLease(row) : null;
5306
5369
  }
5307
5370
 
5308
5371
  export function listMergeLeases(): MergeLeaseRecord[] {
5309
- return (db.prepare("SELECT * FROM workspace_merge_leases ORDER BY acquired_at DESC").all() as any[]).map(rowToMergeLease);
5372
+ return (db.query("SELECT * FROM workspace_merge_leases ORDER BY acquired_at DESC").all() as any[]).map(rowToMergeLease);
5310
5373
  }
5311
5374
 
5312
5375
  export function releaseExpiredMergeLeases(now: number = Date.now()): string[] {
5313
- const expired = db.prepare("SELECT repo_root FROM workspace_merge_leases WHERE expires_at <= ?").all(now) as Array<{ repo_root: string }>;
5376
+ const expired = db.query("SELECT repo_root FROM workspace_merge_leases WHERE expires_at <= ?").all(now) as Array<{ repo_root: string }>;
5314
5377
  if (!expired.length) return [];
5315
- db.prepare("DELETE FROM workspace_merge_leases WHERE expires_at <= ?").run(now);
5378
+ db.query("DELETE FROM workspace_merge_leases WHERE expires_at <= ?").run(now);
5316
5379
  return expired.map((r) => r.repo_root);
5317
5380
  }
5318
5381
 
@@ -5329,7 +5392,7 @@ export function acquireMergeLease(
5329
5392
  const existing = getMergeLease(repoRoot);
5330
5393
  if (existing && existing.expiresAt > now) return { ok: false as const, lease: existing };
5331
5394
  const expiresAt = now + WORKSPACE_MERGE_LEASE_MS;
5332
- db.prepare(`
5395
+ db.query(`
5333
5396
  INSERT INTO workspace_merge_leases (repo_root, workspace_id, command_id, holder, acquired_at, expires_at)
5334
5397
  VALUES (?, ?, NULL, ?, ?, ?)
5335
5398
  ON CONFLICT(repo_root) DO UPDATE SET
@@ -5343,7 +5406,7 @@ export function acquireMergeLease(
5343
5406
  // Attach the dispatched command id to a held lease so it can be released by
5344
5407
  // command id when the merge settles.
5345
5408
  export function setMergeLeaseCommand(repoRoot: string, commandId: string): void {
5346
- db.prepare("UPDATE workspace_merge_leases SET command_id = ? WHERE repo_root = ?").run(commandId, repoRoot);
5409
+ db.query("UPDATE workspace_merge_leases SET command_id = ? WHERE repo_root = ?").run(commandId, repoRoot);
5347
5410
  }
5348
5411
 
5349
5412
  // Release a merge lease. Guard by commandId/workspaceId when known so a stale
@@ -5355,26 +5418,26 @@ export function releaseMergeLease(opts: { repoRoot?: string; commandId?: string;
5355
5418
  if (opts.commandId) { where.push("command_id = ?"); params.push(opts.commandId); }
5356
5419
  if (opts.workspaceId) { where.push("workspace_id = ?"); params.push(opts.workspaceId); }
5357
5420
  if (!where.length) return false;
5358
- return db.prepare(`DELETE FROM workspace_merge_leases WHERE ${where.join(" AND ")}`).run(...params).changes > 0;
5421
+ return db.query(`DELETE FROM workspace_merge_leases WHERE ${where.join(" AND ")}`).run(...params).changes > 0;
5359
5422
  }
5360
5423
 
5361
5424
  export function deleteOrchestrator(id: string): boolean {
5362
5425
  const orch = getOrchestrator(id);
5363
5426
  if (!orch) return false;
5364
- db.prepare("DELETE FROM orchestrators WHERE id = ?").run(id);
5427
+ db.query("DELETE FROM orchestrators WHERE id = ?").run(id);
5365
5428
  deleteAgent(orch.agentId);
5366
5429
  return true;
5367
5430
  }
5368
5431
 
5369
5432
  export function reapStaleOrchestrators(): string[] {
5370
5433
  const cutoff = Date.now() - STALE_TTL_MS;
5371
- const stale = db.prepare("SELECT id, agent_id FROM orchestrators WHERE last_seen < ? AND status = 'online'").all(cutoff) as any[];
5434
+ const stale = db.query("SELECT id, agent_id FROM orchestrators WHERE last_seen < ? AND status = 'online'").all(cutoff) as any[];
5372
5435
  for (const row of stale) {
5373
- db.prepare("UPDATE orchestrators SET status = 'offline' WHERE id = ?").run(row.id);
5436
+ db.query("UPDATE orchestrators SET status = 'offline' WHERE id = ?").run(row.id);
5374
5437
  // An orchestrator agent holds no workspaces, so use a direct status update
5375
5438
  // instead of setStatus() (which triggers an unscoped electWorkspaceStewards
5376
5439
  // sweep across all repos on every offline transition).
5377
- db.prepare("UPDATE agents SET status = 'offline', ready = 0 WHERE id = ?").run(row.agent_id);
5440
+ db.query("UPDATE agents SET status = 'offline', ready = 0 WHERE id = ?").run(row.agent_id);
5378
5441
  }
5379
5442
  return stale.map((row: any) => row.id);
5380
5443
  }