agent-relay-server 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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 (
@@ -193,6 +245,11 @@ export function initDb(path: string = "agent-relay.db"): Database {
193
245
  CREATE INDEX IF NOT EXISTS idx_msg_to ON messages(to_target);
194
246
  CREATE INDEX IF NOT EXISTS idx_msg_created ON messages(created_at);
195
247
  CREATE INDEX IF NOT EXISTS idx_msg_channel ON messages(channel);
248
+ -- (reply_to, from_agent) powers the reply-obligation NOT EXISTS subquery in
249
+ -- listPendingReplyObligations. Without it that subquery full-scans messages
250
+ -- per candidate row (O(n^2)): ~8.6s at 4k rows, which blew the 5s Stop-hook
251
+ -- timeout and wedged turns in "busy" (#199).
252
+ CREATE INDEX IF NOT EXISTS idx_msg_reply_to ON messages(reply_to, from_agent);
196
253
 
197
254
  CREATE TABLE IF NOT EXISTS message_reactions (
198
255
  message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
@@ -789,7 +846,7 @@ export function initDb(path: string = "agent-relay.db"): Database {
789
846
  normalizeExistingMessageReactions();
790
847
 
791
848
  // Migrations
792
- const cols = db.prepare("PRAGMA table_info(messages)").all() as any[];
849
+ const cols = db.query("PRAGMA table_info(messages)").all() as any[];
793
850
  const colNames = cols.map((c: any) => c.name);
794
851
  if (!colNames.includes("thread_id")) {
795
852
  db.run("ALTER TABLE messages ADD COLUMN thread_id INTEGER");
@@ -833,7 +890,7 @@ export function initDb(path: string = "agent-relay.db"): Database {
833
890
  if (!colNames.includes("resolved_to_agent")) {
834
891
  db.run("ALTER TABLE messages ADD COLUMN resolved_to_agent TEXT");
835
892
  }
836
- db.prepare(
893
+ db.query(
837
894
  "UPDATE messages SET claim_expires_at = coalesce(claimed_at, ?) + ? WHERE claimed_by IS NOT NULL AND claim_expires_at IS NULL",
838
895
  ).run(Date.now(), CLAIM_LEASE_MS);
839
896
  db.run("CREATE INDEX IF NOT EXISTS idx_msg_thread ON messages(thread_id)");
@@ -841,7 +898,7 @@ export function initDb(path: string = "agent-relay.db"): Database {
841
898
  db.run("CREATE INDEX IF NOT EXISTS idx_msg_delivery_status ON messages(delivery_status)");
842
899
  db.run("CREATE INDEX IF NOT EXISTS idx_msg_resolved_to_agent ON messages(resolved_to_agent)");
843
900
 
844
- const tokenCols = db.prepare("PRAGMA table_info(tokens)").all() as any[];
901
+ const tokenCols = db.query("PRAGMA table_info(tokens)").all() as any[];
845
902
  const tokenColNames = tokenCols.map((c: any) => c.name);
846
903
  if (!tokenColNames.includes("constraints")) {
847
904
  db.run("ALTER TABLE tokens ADD COLUMN constraints TEXT");
@@ -865,7 +922,7 @@ export function initDb(path: string = "agent-relay.db"): Database {
865
922
  `);
866
923
  db.run("CREATE INDEX IF NOT EXISTS idx_mda_message ON message_delivery_attempts(message_id, created_at DESC)");
867
924
 
868
- 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;
869
926
  if (channelBindingsSql?.sql?.includes("UNIQUE(channel_id, conversation_key)")) {
870
927
  db.transaction(() => {
871
928
  db.run("ALTER TABLE channel_bindings RENAME TO channel_bindings_old");
@@ -894,7 +951,7 @@ export function initDb(path: string = "agent-relay.db"): Database {
894
951
  db.run("CREATE INDEX IF NOT EXISTS idx_channel_bindings_channel ON channel_bindings(channel_id, priority)");
895
952
  db.run("CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_bindings_target ON channel_bindings(channel_id, conversation_key, target_type, target_id)");
896
953
 
897
- 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);
898
955
  if (!bindingColNames.includes("pool_selector")) {
899
956
  db.run("ALTER TABLE channel_bindings ADD COLUMN pool_selector TEXT");
900
957
  db.run("ALTER TABLE channel_bindings ADD COLUMN pool_agent_id TEXT");
@@ -913,23 +970,23 @@ export function initDb(path: string = "agent-relay.db"): Database {
913
970
  // Backfill thread_id for pre-migration rows (self-threaded).
914
971
  db.run("UPDATE messages SET thread_id = id WHERE thread_id IS NULL");
915
972
 
916
- const taskCols = db.prepare("PRAGMA table_info(tasks)").all() as any[];
973
+ const taskCols = db.query("PRAGMA table_info(tasks)").all() as any[];
917
974
  const taskColNames = taskCols.map((c: any) => c.name);
918
975
  if (!taskColNames.includes("claim_expires_at")) {
919
976
  db.run("ALTER TABLE tasks ADD COLUMN claim_expires_at INTEGER");
920
977
  }
921
- db.prepare(
978
+ db.query(
922
979
  "UPDATE tasks SET claim_expires_at = coalesce(claimed_at, ?) + ? WHERE claimed_by IS NOT NULL AND claim_expires_at IS NULL",
923
980
  ).run(Date.now(), CLAIM_LEASE_MS);
924
981
 
925
982
  // Migration: orchestrators.api_url
926
- const orchCols = db.prepare("PRAGMA table_info(orchestrators)").all() as any[];
983
+ const orchCols = db.query("PRAGMA table_info(orchestrators)").all() as any[];
927
984
  const orchColNames = orchCols.map((c: any) => c.name);
928
985
  if (!orchColNames.includes("api_url")) {
929
986
  db.run("ALTER TABLE orchestrators ADD COLUMN api_url TEXT");
930
987
  }
931
988
 
932
- 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[];
933
990
  const managedStateColNames = managedStateCols.map((c: any) => c.name);
934
991
  if (!managedStateColNames.includes("workspace_id")) {
935
992
  db.run("ALTER TABLE managed_agent_state ADD COLUMN workspace_id TEXT");
@@ -949,7 +1006,7 @@ export function initDb(path: string = "agent-relay.db"): Database {
949
1006
  `);
950
1007
 
951
1008
  // Migration: agents.label
952
- const agentCols = db.prepare("PRAGMA table_info(agents)").all() as any[];
1009
+ const agentCols = db.query("PRAGMA table_info(agents)").all() as any[];
953
1010
  const agentColNames = agentCols.map((c: any) => c.name);
954
1011
  if (!agentColNames.includes("label")) {
955
1012
  db.run("ALTER TABLE agents ADD COLUMN label TEXT");
@@ -1071,7 +1128,7 @@ export function initDb(path: string = "agent-relay.db"): Database {
1071
1128
  // pass the sendMessage validation. The reaper exempts these by checking
1072
1129
  // meta.builtin (or by id for "user").
1073
1130
  const now = Date.now();
1074
- const builtinStmt = db.prepare(`
1131
+ const builtinStmt = db.query(`
1075
1132
  INSERT INTO agents (id, name, kind, tags, machine, rig, capabilities, ready, status, meta, last_seen, created_at)
1076
1133
  VALUES (?, ?, ?, ?, NULL, NULL, '[]', 1, 'online', '{"builtin":true}', ?, ?)
1077
1134
  ON CONFLICT(id) DO UPDATE SET status = 'online', ready = 1, last_seen = excluded.last_seen
@@ -1090,6 +1147,15 @@ export function initDb(path: string = "agent-relay.db"): Database {
1090
1147
  `);
1091
1148
  }
1092
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
+
1093
1159
  return db;
1094
1160
  }
1095
1161
 
@@ -1682,7 +1748,7 @@ export function upsertAgent(input: RegisterAgentInput): AgentCard {
1682
1748
  const labelProvided = Object.prototype.hasOwnProperty.call(input, "label");
1683
1749
  const readyProvided = Object.prototype.hasOwnProperty.call(input, "ready");
1684
1750
  const instanceProvided = Boolean(input.instanceId);
1685
- const stmt = db.prepare(`
1751
+ const stmt = db.query(`
1686
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)
1687
1753
  VALUES ($id, $name, $kind, $label, $tags, $machine, $rig, $capabilities, $ready, $status, $instanceId, $initialEpoch, $providerCapabilities, $contextState, $meta, $now, $now)
1688
1754
  ON CONFLICT(id) DO UPDATE SET
@@ -1754,18 +1820,18 @@ export function validateAgentSession(id: string, guard?: AgentSessionGuard): { o
1754
1820
  export function setLabel(id: string, label: string | null): boolean {
1755
1821
  const normalized = label && label.trim() ? label.trim() : null;
1756
1822
  return (
1757
- 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
1758
1824
  );
1759
1825
  }
1760
1826
 
1761
1827
  export function setTags(id: string, tags: string[]): AgentCard | null {
1762
1828
  const normalized = [...new Set(tags.map((tag) => tag.trim()).filter(Boolean))];
1763
- 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);
1764
1830
  return result.changes > 0 ? getAgent(id) : null;
1765
1831
  }
1766
1832
 
1767
1833
  export function getAgent(id: string): AgentCard | null {
1768
- 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;
1769
1835
  return row ? rowToAgent(row) : null;
1770
1836
  }
1771
1837
 
@@ -1791,7 +1857,7 @@ export function listAgents(filter?: {
1791
1857
  }
1792
1858
 
1793
1859
  sql += " ORDER BY last_seen DESC";
1794
- return (db.prepare(sql).all(...params) as any[]).map(rowToAgent);
1860
+ return (db.query(sql).all(...params) as any[]).map(rowToAgent);
1795
1861
  }
1796
1862
 
1797
1863
  export function setStatus(id: string, status: AgentCard["status"], guard?: AgentSessionGuard): boolean {
@@ -1801,7 +1867,7 @@ export function setStatus(id: string, status: AgentCard["status"], guard?: Agent
1801
1867
  const sql = ready === 0
1802
1868
  ? "UPDATE agents SET status = ?, ready = 0, last_seen = ? WHERE id = ?"
1803
1869
  : "UPDATE agents SET status = ?, last_seen = ? WHERE id = ?";
1804
- const changed = db.prepare(sql).run(status, now, id).changes > 0;
1870
+ const changed = db.query(sql).run(status, now, id).changes > 0;
1805
1871
  if (changed && status === "offline") closeOpenPairsForAgent(id, now);
1806
1872
  if (changed && status === "offline") electWorkspaceStewards();
1807
1873
  return changed;
@@ -1812,7 +1878,7 @@ export function markReady(id: string, ready: boolean, guard?: AgentSessionGuard)
1812
1878
  const now = Date.now();
1813
1879
  return (
1814
1880
  db
1815
- .prepare("UPDATE agents SET ready = ?, last_seen = ? WHERE id = ?")
1881
+ .query("UPDATE agents SET ready = ?, last_seen = ? WHERE id = ?")
1816
1882
  .run(ready ? 1 : 0, now, id).changes > 0
1817
1883
  );
1818
1884
  }
@@ -1823,7 +1889,7 @@ export function mergeAgentMeta(id: string, meta: Record<string, unknown>, guard?
1823
1889
  if (!agent) return false;
1824
1890
  const merged = { ...(agent.meta ?? {}), ...meta };
1825
1891
  const result = db
1826
- .prepare("UPDATE agents SET meta = ?, last_seen = ? WHERE id = ?")
1892
+ .query("UPDATE agents SET meta = ?, last_seen = ? WHERE id = ?")
1827
1893
  .run(JSON.stringify(merged), Date.now(), id);
1828
1894
  return result.changes > 0;
1829
1895
  }
@@ -1839,7 +1905,7 @@ export function recordAgentExitDiagnostics(id: string, diagnostics: ManagedSessi
1839
1905
  lastExitAt: diagnostics.detectedAt || now,
1840
1906
  };
1841
1907
  const result = db
1842
- .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 = ?")
1843
1909
  .run(JSON.stringify(merged), now, id);
1844
1910
  if (result.changes <= 0) return null;
1845
1911
  closeOpenPairsForAgent(id, now);
@@ -1855,7 +1921,7 @@ export function heartbeat(
1855
1921
  const now = Date.now();
1856
1922
  if (runtime?.providerCapabilities || runtime?.context) {
1857
1923
  const result = db
1858
- .prepare(`
1924
+ .query(`
1859
1925
  UPDATE agents SET
1860
1926
  last_seen = ?,
1861
1927
  status = CASE WHEN status = 'offline' THEN 'idle' ELSE status END,
@@ -1873,7 +1939,7 @@ export function heartbeat(
1873
1939
  return result.changes > 0;
1874
1940
  }
1875
1941
  const result = db
1876
- .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 = ?")
1877
1943
  .run(now, id);
1878
1944
  return result.changes > 0;
1879
1945
  }
@@ -1888,23 +1954,23 @@ export function listContextSnapshots(agentId: string, options: { since?: number;
1888
1954
  }
1889
1955
  sql += " ORDER BY captured_at ASC LIMIT ?";
1890
1956
  params.push(limit);
1891
- return (db.prepare(sql).all(...params) as any[]).map(rowToContextSnapshot);
1957
+ return (db.query(sql).all(...params) as any[]).map(rowToContextSnapshot);
1892
1958
  }
1893
1959
 
1894
1960
  function pruneContextSnapshots(agentId?: string, olderThan = Date.now() - DAY_MS): number {
1895
1961
  const result = agentId
1896
- ? db.prepare("DELETE FROM context_snapshots WHERE agent_id = ? AND captured_at < ?").run(agentId, olderThan)
1897
- : 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);
1898
1964
  return result.changes;
1899
1965
  }
1900
1966
 
1901
1967
  function recordContextSnapshot(agentId: string, context: ContextState, now: number): void {
1902
1968
  const latest = db
1903
- .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")
1904
1970
  .get(agentId) as { captured_at: number } | undefined;
1905
1971
  if (latest && latest.captured_at > now - CONTEXT_SNAPSHOT_DEBOUNCE_MS) return;
1906
1972
 
1907
- db.prepare(`
1973
+ db.query(`
1908
1974
  INSERT INTO context_snapshots (
1909
1975
  agent_id,
1910
1976
  utilization,
@@ -1994,7 +2060,7 @@ function upsertChannelForAgent(agent: AgentCard): void {
1994
2060
  ? (agent.meta.topicChannels as unknown[]).filter((item): item is string => typeof item === "string" && item.trim().length > 0)
1995
2061
  : [];
1996
2062
 
1997
- db.prepare(`
2063
+ db.query(`
1998
2064
  INSERT INTO channels (id, provider, account_id, display_name, agent_id, transport, direction, topic_channels, capabilities, meta, created_at, updated_at)
1999
2065
  VALUES ($id, $provider, $accountId, $displayName, $agentId, $transport, $direction, $topicChannels, $capabilities, $meta, $now, $now)
2000
2066
  ON CONFLICT(provider, account_id) DO UPDATE SET
@@ -2034,7 +2100,7 @@ function upsertChannelForAgent(agent: AgentCard): void {
2034
2100
  }
2035
2101
 
2036
2102
  export function listChannels(): ChannelSummary[] {
2037
- const rows = db.prepare(`
2103
+ const rows = db.query(`
2038
2104
  SELECT
2039
2105
  c.*,
2040
2106
  c.capabilities AS channel_capabilities,
@@ -2069,8 +2135,8 @@ export function getChannel(channelId: string): ChannelSummary | null {
2069
2135
 
2070
2136
  export function listChannelBindings(channelId?: string): ChannelBinding[] {
2071
2137
  const rows = channelId
2072
- ? db.prepare("SELECT * FROM channel_bindings WHERE channel_id = ? ORDER BY priority DESC, updated_at DESC").all(channelId) as any[]
2073
- : 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[];
2074
2140
  return rows.map(rowToChannelBinding);
2075
2141
  }
2076
2142
 
@@ -2092,9 +2158,9 @@ export function upsertChannelBinding(input: {
2092
2158
  const now = Date.now();
2093
2159
  db.transaction(() => {
2094
2160
  if (mode === "exclusive") {
2095
- 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);
2096
2162
  }
2097
- db.prepare(`
2163
+ db.query(`
2098
2164
  INSERT INTO channel_bindings (id, channel_id, conversation_key, conversation_id, target_type, target_id, mode, priority, pool_selector, created_at, updated_at)
2099
2165
  VALUES ($id, $channelId, $conversationKey, $conversationId, $targetType, $targetId, $mode, $priority, $poolSelector, $now, $now)
2100
2166
  ON CONFLICT(channel_id, conversation_key, target_type, target_id) DO UPDATE SET
@@ -2117,7 +2183,7 @@ export function upsertChannelBinding(input: {
2117
2183
  })();
2118
2184
 
2119
2185
  evaluatePoolBindings(now);
2120
- 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;
2121
2187
  return rowToChannelBinding(row);
2122
2188
  }
2123
2189
 
@@ -2129,7 +2195,7 @@ interface PoolBindingChange {
2129
2195
  }
2130
2196
 
2131
2197
  export function evaluatePoolBindings(now: number = Date.now()): PoolBindingChange[] {
2132
- 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[];
2133
2199
  const changes: PoolBindingChange[] = [];
2134
2200
 
2135
2201
  for (const row of rows) {
@@ -2144,7 +2210,7 @@ export function evaluatePoolBindings(now: number = Date.now()): PoolBindingChang
2144
2210
  const holder = getAgent(currentAgentId);
2145
2211
  if (holder && holder.status !== "offline" && holder.ready && holder.lastSeen > now - STALE_TTL_MS && agentCanServeChannel(holder, channelId)) {
2146
2212
  if (currentEpoch === null || holder.epoch === currentEpoch) {
2147
- 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 = ?")
2148
2214
  .run(now + POOL_CLAIM_LEASE_MS, bindingId);
2149
2215
  holderValid = true;
2150
2216
  }
@@ -2152,7 +2218,7 @@ export function evaluatePoolBindings(now: number = Date.now()): PoolBindingChang
2152
2218
  }
2153
2219
 
2154
2220
  if (!holderValid && currentAgentId) {
2155
- 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 = ?")
2156
2222
  .run(bindingId);
2157
2223
  changes.push({ bindingId, channelId, previousAgentId: currentAgentId, newAgentId: null });
2158
2224
  }
@@ -2168,7 +2234,7 @@ export function evaluatePoolBindings(now: number = Date.now()): PoolBindingChang
2168
2234
 
2169
2235
  if (eligible.length > 0) {
2170
2236
  const picked = eligible[0]!;
2171
- 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 = ?")
2172
2238
  .run(picked.id, picked.epoch, now + POOL_CLAIM_LEASE_MS, bindingId);
2173
2239
  const lastChange = changes[changes.length - 1];
2174
2240
  if (lastChange && lastChange.bindingId === bindingId && lastChange.newAgentId === null) {
@@ -2185,12 +2251,12 @@ export function evaluatePoolBindings(now: number = Date.now()): PoolBindingChang
2185
2251
 
2186
2252
  export function resolveChannelRoutes(channelId: string, conversationId?: string): ChannelBinding[] {
2187
2253
  const rows = conversationId
2188
- ? db.prepare(`
2254
+ ? db.query(`
2189
2255
  SELECT * FROM channel_bindings
2190
2256
  WHERE channel_id = ? AND conversation_key IN (?, '')
2191
2257
  ORDER BY CASE WHEN conversation_key = ? THEN 1 ELSE 0 END DESC, priority DESC, updated_at DESC
2192
2258
  `).all(channelId, conversationId, conversationId) as any[]
2193
- : db.prepare(`
2259
+ : db.query(`
2194
2260
  SELECT * FROM channel_bindings
2195
2261
  WHERE channel_id = ? AND conversation_key = ''
2196
2262
  ORDER BY priority DESC, updated_at DESC
@@ -2203,9 +2269,9 @@ export function resolveChannelRoutes(channelId: string, conversationId?: string)
2203
2269
  export function reapStaleAgents(ttlMs: number = STALE_TTL_MS): string[] {
2204
2270
  const now = Date.now();
2205
2271
  const cutoff = now - ttlMs;
2206
- 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);
2207
2273
  const rows = db
2208
- .prepare(
2274
+ .query(
2209
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"
2210
2276
  )
2211
2277
  .all(cutoff) as any[];
@@ -2227,9 +2293,9 @@ export function reapStaleAgents(ttlMs: number = STALE_TTL_MS): string[] {
2227
2293
  // re-open (gated on status IN claimed/in_progress/blocked/orphaned) then skips.
2228
2294
  function settleSingleTargetOnDemandTasks(condition: string, params: any[], now: number, reason: string): void {
2229
2295
  const selectSql = `${TASK_SELECT} WHERE (${condition}) AND json_extract(metadata, '$.targetMode') = 'on_demand_agent' AND status IN ('claimed', 'in_progress', 'blocked', 'orphaned')`;
2230
- const rows = db.prepare(selectSql).all(...params) as any[];
2296
+ const rows = db.query(selectSql).all(...params) as any[];
2231
2297
  if (rows.length === 0) return;
2232
- db.prepare(
2298
+ db.query(
2233
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')`
2234
2300
  ).run(now, now, ...params);
2235
2301
  for (const row of rows) {
@@ -2248,7 +2314,7 @@ export function pruneOfflineAgents(maxOfflineMs: number = DAY_MS): string[] {
2248
2314
  const cutoff = Date.now() - maxOfflineMs;
2249
2315
  return db.transaction(() => {
2250
2316
  const rows = db
2251
- .prepare(
2317
+ .query(
2252
2318
  "SELECT id FROM agents WHERE status = 'offline' AND last_seen < ? AND id NOT IN ('user', 'system')"
2253
2319
  )
2254
2320
  .all(cutoff) as any[];
@@ -2257,7 +2323,7 @@ export function pruneOfflineAgents(maxOfflineMs: number = DAY_MS): string[] {
2257
2323
 
2258
2324
  // Release claims held by pruned agents so work becomes claimable again.
2259
2325
  db
2260
- .prepare(
2326
+ .query(
2261
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'))"
2262
2328
  )
2263
2329
  .run(cutoff);
@@ -2266,7 +2332,7 @@ export function pruneOfflineAgents(maxOfflineMs: number = DAY_MS): string[] {
2266
2332
  settleSingleTargetOnDemandTasks(offlineClaimCondition, [cutoff], now, "agent-pruned");
2267
2333
 
2268
2334
  db
2269
- .prepare(
2335
+ .query(
2270
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')`
2271
2337
  )
2272
2338
  .run(now, cutoff);
@@ -2277,7 +2343,7 @@ export function pruneOfflineAgents(maxOfflineMs: number = DAY_MS): string[] {
2277
2343
  }
2278
2344
 
2279
2345
  db
2280
- .prepare(
2346
+ .query(
2281
2347
  "DELETE FROM agents WHERE status = 'offline' AND last_seen < ? AND id NOT IN ('user', 'system')"
2282
2348
  )
2283
2349
  .run(cutoff);
@@ -2294,7 +2360,7 @@ export function findAgentsByCapability(capability: string, onlineOnly = true): A
2294
2360
  params.push(Date.now() - STALE_TTL_MS);
2295
2361
  }
2296
2362
  sql += " ORDER BY last_seen DESC";
2297
- return (db.prepare(sql).all(...params) as any[]).map(rowToAgent);
2363
+ return (db.query(sql).all(...params) as any[]).map(rowToAgent);
2298
2364
  }
2299
2365
 
2300
2366
  export function deleteAgent(id: string): { ok: boolean; error?: string } {
@@ -2305,23 +2371,23 @@ export function deleteAgent(id: string): { ok: boolean; error?: string } {
2305
2371
  // Release any claims held by this agent so the tasks become claimable again.
2306
2372
  // from_agent is left intact as historical record.
2307
2373
  const now = Date.now();
2308
- 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);
2309
2375
  settleSingleTargetOnDemandTasks("claimed_by = ?", [id], now, "agent-removed");
2310
- 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);
2311
2377
  revokeRuntimeTokensForAgent(id, now);
2312
2378
  closeOpenPairsForAgent(id, now);
2313
- 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;
2314
2380
  })();
2315
2381
  return deleted ? { ok: true } : { ok: false, error: "agent not found" };
2316
2382
  }
2317
2383
 
2318
2384
  export function revokeRuntimeTokensForAgent(agentId: string, now = Date.now()): string[] {
2319
- 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;
2320
2386
  const jtis = runtimeTokenJtisFromMeta(parseJson(row?.meta ?? "{}", {}));
2321
2387
  if (jtis.length === 0) return [];
2322
2388
  const revokedAt = Math.floor(now / 1000);
2323
2389
  const placeholders = jtis.map(() => "?").join(",");
2324
- const rows = db.prepare(`
2390
+ const rows = db.query(`
2325
2391
  UPDATE tokens
2326
2392
  SET revoked_at = ?
2327
2393
  WHERE revoked_at IS NULL
@@ -2355,13 +2421,13 @@ const TASK_SELECT = "SELECT * FROM tasks";
2355
2421
 
2356
2422
  function findOpenTaskByDedupe(source: string, dedupeKey: string): Task | null {
2357
2423
  const row = db
2358
- .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`)
2359
2425
  .get(source, dedupeKey) as any;
2360
2426
  return row ? rowToTask(row) : null;
2361
2427
  }
2362
2428
 
2363
2429
  function insertTaskEvent(taskId: number, event: Required<Omit<TaskEvent, "id" | "taskId" | "createdAt">>, now: number): TaskEvent {
2364
- const result = db.prepare(`
2430
+ const result = db.query(`
2365
2431
  INSERT INTO task_events (task_id, source, type, severity, title, body, metadata, created_at)
2366
2432
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)
2367
2433
  `).run(taskId, event.source, event.type, event.severity, event.title, event.body, JSON.stringify(event.metadata), now);
@@ -2369,7 +2435,7 @@ function insertTaskEvent(taskId: number, event: Required<Omit<TaskEvent, "id" |
2369
2435
  }
2370
2436
 
2371
2437
  function getTaskEvent(id: number): TaskEvent | null {
2372
- 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;
2373
2439
  return row ? rowToTaskEvent(row) : null;
2374
2440
  }
2375
2441
 
@@ -2401,7 +2467,7 @@ export function ingestIntegrationEvent(input: IntegrationEventInput, integration
2401
2467
  let created = false;
2402
2468
 
2403
2469
  if (existing) {
2404
- db.prepare(`
2470
+ db.query(`
2405
2471
  UPDATE tasks
2406
2472
  SET title = ?, body = ?, severity = ?, target = ?, channel = ?, external_url = ?,
2407
2473
  occurrence_count = occurrence_count + 1, metadata = ?, updated_at = ?, last_seen_at = ?,
@@ -2426,7 +2492,7 @@ export function ingestIntegrationEvent(input: IntegrationEventInput, integration
2426
2492
  );
2427
2493
  taskId = existing.id;
2428
2494
  } else {
2429
- const result = db.prepare(`
2495
+ const result = db.query(`
2430
2496
  INSERT INTO tasks (source, title, body, severity, status, target, channel, dedupe_key, external_url, metadata, created_at, updated_at, last_seen_at)
2431
2497
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2432
2498
  `).run(
@@ -2480,7 +2546,7 @@ export function ingestIntegrationEvent(input: IntegrationEventInput, integration
2480
2546
  ...(attachmentRefs.length ? { attachments: attachmentRefs } : {}),
2481
2547
  },
2482
2548
  });
2483
- 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);
2484
2550
  }
2485
2551
 
2486
2552
  return { task: getTask(taskId)!, event, created, message };
@@ -2488,7 +2554,7 @@ export function ingestIntegrationEvent(input: IntegrationEventInput, integration
2488
2554
  }
2489
2555
 
2490
2556
  export function getTask(id: number): Task | null {
2491
- 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;
2492
2558
  return row ? rowToTask(row) : null;
2493
2559
  }
2494
2560
 
@@ -2509,11 +2575,11 @@ export function listTasks(filter?: { status?: string; source?: string; target?:
2509
2575
  }
2510
2576
  const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
2511
2577
  params.push(filter?.limit ?? 100);
2512
- 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);
2513
2579
  }
2514
2580
 
2515
2581
  export function listIntegrationTaskStats(): IntegrationTaskStats[] {
2516
- const rows = db.prepare(`
2582
+ const rows = db.query(`
2517
2583
  SELECT
2518
2584
  source,
2519
2585
  COUNT(*) AS tasks,
@@ -2592,7 +2658,7 @@ export function upsertIntegrationRegistry(input: IntegrationRegistryInput): Retu
2592
2658
  const name = stringValue(input.name);
2593
2659
  if (!name) throw new ValidationError("integration name required");
2594
2660
  const now = Date.now();
2595
- db.prepare(`
2661
+ db.query(`
2596
2662
  INSERT INTO integration_registry (
2597
2663
  name, display_name, description, enabled, scopes, targets, channels, type, icon, accent_color,
2598
2664
  tags, homepage_url, repository_url, docs_url, manifest, source, created_at, updated_at
@@ -2639,17 +2705,17 @@ export function upsertIntegrationRegistry(input: IntegrationRegistryInput): Retu
2639
2705
  }
2640
2706
 
2641
2707
  function getIntegrationRegistry(name: string): ReturnType<typeof rowToIntegrationRegistry> | null {
2642
- 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;
2643
2709
  return row ? rowToIntegrationRegistry(row) : null;
2644
2710
  }
2645
2711
 
2646
2712
  export function listIntegrationRegistry(): ReturnType<typeof rowToIntegrationRegistry>[] {
2647
- 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);
2648
2714
  }
2649
2715
 
2650
2716
  function updateIntegrationObserved(name: string, eventAt: number): void {
2651
2717
  const now = Date.now();
2652
- db.prepare(`
2718
+ db.query(`
2653
2719
  INSERT INTO integration_registry (name, enabled, scopes, targets, channels, tags, manifest, source, created_at, updated_at, last_event_at, last_task_at)
2654
2720
  VALUES (?, 1, '[]', '[]', '[]', '[]', '{}', 'observed', ?, ?, ?, ?)
2655
2721
  ON CONFLICT(name) DO UPDATE SET
@@ -2665,7 +2731,7 @@ export function isIntegrationRegistryEnabled(name: string): boolean {
2665
2731
  }
2666
2732
 
2667
2733
  export function listTaskEvents(taskId: number): TaskEvent[] {
2668
- 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);
2669
2735
  }
2670
2736
 
2671
2737
  export function recordTaskEvent(taskId: number, input: {
@@ -2693,21 +2759,21 @@ export function releaseExpiredClaims(now: number = Date.now()): { messageIds: nu
2693
2759
  settleSingleTargetOnDemandTasks("claimed_by IS NOT NULL AND (claim_expires_at IS NULL OR claim_expires_at <= ?)", [now], now, "claim-lease-expired");
2694
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'))";
2695
2761
  const messageRows = db
2696
- .prepare(`SELECT id FROM messages WHERE ${releasableMessageClaim}`)
2762
+ .query(`SELECT id FROM messages WHERE ${releasableMessageClaim}`)
2697
2763
  .all(now) as any[];
2698
2764
  const taskRows = db
2699
- .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')`)
2700
2766
  .all(now) as any[];
2701
2767
 
2702
2768
  if (messageRows.length > 0) {
2703
2769
  db
2704
- .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}`)
2705
2771
  .run(now);
2706
2772
  }
2707
2773
 
2708
2774
  if (taskRows.length > 0) {
2709
2775
  db
2710
- .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')")
2711
2777
  .run(now, now);
2712
2778
 
2713
2779
  for (const row of taskRows) {
@@ -2732,11 +2798,11 @@ export function releaseExpiredClaims(now: number = Date.now()): { messageIds: nu
2732
2798
  export function orphanTasksForAgent(agentId: string, now: number = Date.now()): Task[] {
2733
2799
  return db.transaction(() => {
2734
2800
  const rows = db
2735
- .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')`)
2736
2802
  .all(agentId) as any[];
2737
2803
  if (rows.length === 0) return [];
2738
2804
 
2739
- db.prepare(`
2805
+ db.query(`
2740
2806
  UPDATE tasks
2741
2807
  SET status = 'orphaned', updated_at = ?, last_seen_at = ?
2742
2808
  WHERE claimed_by = ? AND status IN ('claimed', 'in_progress')
@@ -2762,11 +2828,11 @@ export function releaseOrphanedTasks(graceMs = 120_000, now: number = Date.now()
2762
2828
  const cutoff = now - graceMs;
2763
2829
  settleSingleTargetOnDemandTasks("status = 'orphaned' AND claimed_by IS NOT NULL AND updated_at <= ?", [cutoff], now, "orphan-grace-elapsed");
2764
2830
  const rows = db
2765
- .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 <= ?`)
2766
2832
  .all(cutoff) as any[];
2767
2833
  if (rows.length === 0) return [];
2768
2834
 
2769
- db.prepare(`
2835
+ db.query(`
2770
2836
  UPDATE tasks
2771
2837
  SET status = 'open', claimed_by = NULL, claimed_at = NULL, claim_expires_at = NULL, updated_at = ?
2772
2838
  WHERE status = 'orphaned' AND claimed_by IS NOT NULL AND updated_at <= ?
@@ -2802,7 +2868,7 @@ export function claimTask(taskId: number, agentId: string, guard?: AgentSessionG
2802
2868
  return db.transaction(() => {
2803
2869
  const now = Date.now();
2804
2870
  const expiresAt = now + CLAIM_LEASE_MS;
2805
- const result = db.prepare(`
2871
+ const result = db.query(`
2806
2872
  UPDATE tasks SET status = 'claimed', claimed_by = ?, claimed_at = ?, claim_expires_at = ?, updated_at = ?
2807
2873
  WHERE id = ? AND status IN ('open', 'blocked')
2808
2874
  `).run(agentId, now, expiresAt, now, taskId);
@@ -2841,9 +2907,9 @@ export function renewTaskClaim(taskId: number, agentId: string, guard?: AgentSes
2841
2907
 
2842
2908
  const now = Date.now();
2843
2909
  const expiresAt = now + CLAIM_LEASE_MS;
2844
- 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);
2845
2911
  if (task.messageId) {
2846
- 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);
2847
2913
  }
2848
2914
  return { ok: true, task: getTask(taskId)! };
2849
2915
  }
@@ -2857,7 +2923,7 @@ export function updateTaskStatus(taskId: number, input: TaskStatusInput): { ok:
2857
2923
  const session = validateAgentSession(agentId, input);
2858
2924
  if (!session.ok) return { ok: false, error: session.error };
2859
2925
  }
2860
- const result = db.prepare(`
2926
+ const result = db.query(`
2861
2927
  UPDATE tasks
2862
2928
  SET status = ?, result = COALESCE(?, result), claimed_by = COALESCE(?, claimed_by),
2863
2929
  claimed_at = CASE WHEN claimed_by IS NULL AND ? IS NOT NULL THEN ? ELSE claimed_at END,
@@ -2867,13 +2933,13 @@ export function updateTaskStatus(taskId: number, input: TaskStatusInput): { ok:
2867
2933
  `).run(input.status, input.result ?? null, agentId, agentId, now, input.status, now, now, taskId);
2868
2934
  if (result.changes === 0) return { ok: false, error: "task not found" };
2869
2935
  if (["done", "failed", "canceled"].includes(input.status) && task.messageId) {
2870
- 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);
2871
2937
  }
2872
2938
  if (agentId && ["claimed", "in_progress", "blocked"].includes(input.status)) {
2873
2939
  const expiresAt = now + CLAIM_LEASE_MS;
2874
- 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);
2875
2941
  if (task.messageId) {
2876
- 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);
2877
2943
  }
2878
2944
  }
2879
2945
  const event = insertTaskEvent(taskId, {
@@ -2889,7 +2955,7 @@ export function updateTaskStatus(taskId: number, input: TaskStatusInput): { ok:
2889
2955
 
2890
2956
  export function createCallbackDelivery(taskId: number, url: string, eventType: string, payload: unknown): number {
2891
2957
  const now = Date.now();
2892
- const result = db.prepare(`
2958
+ const result = db.query(`
2893
2959
  INSERT INTO task_callback_deliveries (task_id, url, event_type, payload, status, attempts, created_at, updated_at)
2894
2960
  VALUES (?, ?, ?, ?, 'pending', 0, ?, ?)
2895
2961
  `).run(taskId, url, eventType, JSON.stringify(payload), now, now);
@@ -2897,7 +2963,7 @@ export function createCallbackDelivery(taskId: number, url: string, eventType: s
2897
2963
  }
2898
2964
 
2899
2965
  export function finishCallbackDelivery(id: number, ok: boolean, error?: string): void {
2900
- db.prepare(`
2966
+ db.query(`
2901
2967
  UPDATE task_callback_deliveries
2902
2968
  SET status = ?, attempts = attempts + 1, last_error = ?, updated_at = ?
2903
2969
  WHERE id = ?
@@ -2913,7 +2979,7 @@ export function listCallbackDeliveries(taskId: number): Array<{
2913
2979
  attempts: number;
2914
2980
  lastError?: string;
2915
2981
  }> {
2916
- 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[])
2917
2983
  .map((row) => ({
2918
2984
  id: row.id,
2919
2985
  taskId: row.task_id,
@@ -2932,12 +2998,12 @@ const DEFAULT_PAIR_TTL_MS = 5 * 60_000;
2932
2998
  const MAX_PAIR_TTL_MS = DAY_MS;
2933
2999
 
2934
3000
  function expirePendingPairs(now: number = Date.now()): void {
2935
- 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 <= ?")
2936
3002
  .run(now, now, now);
2937
3003
  }
2938
3004
 
2939
3005
  function closeOpenPairsForAgent(agentId: string, now: number = Date.now()): void {
2940
- db.prepare(`
3006
+ db.query(`
2941
3007
  UPDATE pairs
2942
3008
  SET status = 'ended', ended_at = ?, ended_by = ?, updated_at = ?
2943
3009
  WHERE status IN ('pending', 'active') AND (requester_id = ? OR target_id = ?)
@@ -2946,7 +3012,7 @@ function closeOpenPairsForAgent(agentId: string, now: number = Date.now()): void
2946
3012
 
2947
3013
  function getOpenPairForAgent(agentId: string): PairSession | null {
2948
3014
  expirePendingPairs();
2949
- const row = db.prepare(`
3015
+ const row = db.query(`
2950
3016
  SELECT * FROM pairs
2951
3017
  WHERE status IN ('pending', 'active') AND (requester_id = ? OR target_id = ?)
2952
3018
  ORDER BY updated_at DESC
@@ -3033,7 +3099,7 @@ function pairSystemMessage(pair: PairSession, to: string, event: string, subject
3033
3099
 
3034
3100
  export function getPair(id: string): PairSession | null {
3035
3101
  expirePendingPairs();
3036
- 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;
3037
3103
  return row ? rowToPair(row) : null;
3038
3104
  }
3039
3105
 
@@ -3050,7 +3116,7 @@ export function listPairs(filter?: { agentId?: string; status?: PairStatus }): P
3050
3116
  params.push(filter.status);
3051
3117
  }
3052
3118
  const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
3053
- 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);
3054
3120
  }
3055
3121
 
3056
3122
  export function createPair(input: CreatePairInput): {
@@ -3077,7 +3143,7 @@ export function createPair(input: CreatePairInput): {
3077
3143
  const now = Date.now();
3078
3144
  const ttlMs = Math.min(Math.max(input.ttlMs ?? DEFAULT_PAIR_TTL_MS, 10_000), MAX_PAIR_TTL_MS);
3079
3145
  const id = randomUUID();
3080
- db.prepare(`
3146
+ db.query(`
3081
3147
  INSERT INTO pairs (id, requester_id, target_id, status, objective, meta, created_at, updated_at, expires_at)
3082
3148
  VALUES (?, ?, ?, 'pending', ?, ?, ?, ?, ?)
3083
3149
  `).run(id, input.from, resolved.agent.id, input.objective ?? null, JSON.stringify(input.meta ?? {}), now, now, now + ttlMs);
@@ -3117,7 +3183,7 @@ export function acceptPair(id: string, input: PairActionInput): { ok: true; pair
3117
3183
  }
3118
3184
 
3119
3185
  const now = Date.now();
3120
- 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'")
3121
3187
  .run(now, now, id);
3122
3188
  const active = getPair(id)!;
3123
3189
  const notices = [
@@ -3134,7 +3200,7 @@ export function rejectPair(id: string, input: PairActionInput): { ok: true; pair
3134
3200
  if (pair.status !== "pending") return { ok: false, code: "invalid", error: `pair is ${pair.status}` };
3135
3201
  if (pair.targetId !== input.agentId) return { ok: false, code: "forbidden", error: "only the target agent can reject this pair" };
3136
3202
  const now = Date.now();
3137
- 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 = ?")
3138
3204
  .run(now, input.agentId, now, id);
3139
3205
  const rejected = getPair(id)!;
3140
3206
  const reason = input.reason ? `\n\nReason:\n${input.reason}` : "";
@@ -3149,7 +3215,7 @@ export function endPair(id: string, input: PairActionInput): { ok: true; pair: P
3149
3215
  if (!pairParticipant(pair, input.agentId)) return { ok: false, code: "forbidden", error: "only pair participants can hang up" };
3150
3216
  if (!OPEN_PAIR_STATUSES.includes(pair.status as any)) return { ok: false, code: "invalid", error: `pair is ${pair.status}` };
3151
3217
  const now = Date.now();
3152
- 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 = ?")
3153
3219
  .run(now, input.agentId, now, id);
3154
3220
  const ended = getPair(id)!;
3155
3221
  const reason = input.reason ? `\n\nReason:\n${input.reason}` : "";
@@ -3179,7 +3245,7 @@ export function sendPairMessage(id: string, input: PairMessageInput): { ok: true
3179
3245
  targetId: pair.targetId,
3180
3246
  },
3181
3247
  });
3182
- 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);
3183
3249
  return { ok: true, pair: getPair(id)!, message };
3184
3250
  }
3185
3251
 
@@ -3219,7 +3285,7 @@ export function upsertArtifactBlob(input: {
3219
3285
  createdAt?: number;
3220
3286
  }): ArtifactBlob {
3221
3287
  const now = input.createdAt ?? Date.now();
3222
- db.prepare(`
3288
+ db.query(`
3223
3289
  INSERT INTO artifact_blobs (digest, storage_uri, media_type, size, created_at)
3224
3290
  VALUES (?, ?, ?, ?, ?)
3225
3291
  ON CONFLICT(digest) DO UPDATE SET
@@ -3231,12 +3297,12 @@ export function upsertArtifactBlob(input: {
3231
3297
  }
3232
3298
 
3233
3299
  export function getArtifactBlob(digest: string): ArtifactBlob | null {
3234
- 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;
3235
3301
  return row ? rowToArtifactBlob(row) : null;
3236
3302
  }
3237
3303
 
3238
3304
  export function deleteArtifactBlob(digest: string): boolean {
3239
- 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;
3240
3306
  }
3241
3307
 
3242
3308
  export function createArtifact(input: {
@@ -3255,7 +3321,7 @@ export function createArtifact(input: {
3255
3321
  if (!getArtifactBlob(input.blobDigest)) throw new ValidationError("artifact blob not found");
3256
3322
  const id = input.id ?? artifactId();
3257
3323
  const now = Date.now();
3258
- db.prepare(`
3324
+ db.query(`
3259
3325
  INSERT INTO artifacts (
3260
3326
  id, blob_digest, media_type, kind, filename, size, visibility, sensitivity,
3261
3327
  created_by, created_at, expires_at, metadata
@@ -3279,7 +3345,7 @@ export function createArtifact(input: {
3279
3345
  }
3280
3346
 
3281
3347
  export function getArtifact(id: string): Artifact | null {
3282
- 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;
3283
3349
  if (!row) return null;
3284
3350
  return rowToArtifact(row, listArtifactLinks(id));
3285
3351
  }
@@ -3306,22 +3372,22 @@ export function listArtifacts(query: {
3306
3372
  }
3307
3373
  const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
3308
3374
  const limit = Math.min(Math.max(query.limit ?? 100, 1), 500);
3309
- 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[];
3310
3376
  return rows.map((row) => rowToArtifact(row, listArtifactLinks(row.id)));
3311
3377
  }
3312
3378
 
3313
3379
  export function deleteArtifact(id: string): boolean {
3314
3380
  return db.transaction(() => {
3315
- 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;
3316
3382
  })();
3317
3383
  }
3318
3384
 
3319
3385
  function listArtifactLinks(artifactId: string): ArtifactLink[] {
3320
- 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);
3321
3387
  }
3322
3388
 
3323
3389
  export function listArtifactsForEntity(entityType: ArtifactLink["entityType"], entityId: string | number): Artifact[] {
3324
- const rows = db.prepare(`
3390
+ const rows = db.query(`
3325
3391
  SELECT a.*
3326
3392
  FROM artifacts a
3327
3393
  JOIN artifact_links l ON l.artifact_id = a.id
@@ -3344,7 +3410,7 @@ export function linkArtifact(input: {
3344
3410
  if (!getArtifact(input.artifactId)) throw new ValidationError(`artifact ${input.artifactId} not found`);
3345
3411
  const now = Date.now();
3346
3412
  const id = artifactLinkId();
3347
- db.prepare(`
3413
+ db.query(`
3348
3414
  INSERT OR IGNORE INTO artifact_links (id, artifact_id, entity_type, entity_id, role, title, created_by, created_at)
3349
3415
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)
3350
3416
  `).run(
@@ -3357,7 +3423,7 @@ export function linkArtifact(input: {
3357
3423
  input.createdBy,
3358
3424
  now,
3359
3425
  );
3360
- const row = db.prepare(`
3426
+ const row = db.query(`
3361
3427
  SELECT * FROM artifact_links
3362
3428
  WHERE artifact_id = ? AND entity_type = ? AND entity_id = ? AND ((role IS NULL AND ? IS NULL) OR role = ?)
3363
3429
  ORDER BY created_at DESC
@@ -3367,16 +3433,16 @@ export function linkArtifact(input: {
3367
3433
  }
3368
3434
 
3369
3435
  function deleteArtifactLinksForEntity(entityType: ArtifactLink["entityType"], entityId: string | number): number {
3370
- 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;
3371
3437
  }
3372
3438
 
3373
3439
  export function artifactBlobReferenceCount(digest: string): number {
3374
- 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 };
3375
3441
  return row.count ?? 0;
3376
3442
  }
3377
3443
 
3378
3444
  function unreferencedArtifactBlobs(): ArtifactBlob[] {
3379
- return (db.prepare(`
3445
+ return (db.query(`
3380
3446
  SELECT b.*
3381
3447
  FROM artifact_blobs b
3382
3448
  WHERE NOT EXISTS (SELECT 1 FROM artifacts a WHERE a.blob_digest = b.digest)
@@ -3387,12 +3453,12 @@ export function sweepArtifacts(input: { now?: number; unlinkedGraceMs?: number }
3387
3453
  const now = input.now ?? Date.now();
3388
3454
  const unlinkedCutoff = now - (input.unlinkedGraceMs ?? 60 * 60 * 1000);
3389
3455
  return db.transaction(() => {
3390
- const rows = db.prepare(`
3456
+ const rows = db.query(`
3391
3457
  SELECT id FROM artifacts a
3392
3458
  WHERE (expires_at IS NOT NULL AND expires_at <= ?)
3393
3459
  OR (created_at <= ? AND NOT EXISTS (SELECT 1 FROM artifact_links l WHERE l.artifact_id = a.id))
3394
3460
  `).all(now, unlinkedCutoff) as Array<{ id: string }>;
3395
- 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);
3396
3462
  const blobs = unreferencedArtifactBlobs();
3397
3463
  for (const blob of blobs) deleteArtifactBlob(blob.digest);
3398
3464
  return { artifactIds: rows.map((row) => row.id), blobs };
@@ -3446,7 +3512,7 @@ function linkAttachmentRefs(entityType: ArtifactLink["entityType"], entityId: st
3446
3512
 
3447
3513
  function findMessageByIdempotencyKey(from: string, key: string): Message | null {
3448
3514
  const row = db
3449
- .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`)
3450
3516
  .get(from, key) as any;
3451
3517
  return row ? rowToMessage(row) : null;
3452
3518
  }
@@ -3458,12 +3524,12 @@ function policyNameFromTarget(target: string): string | null {
3458
3524
  }
3459
3525
 
3460
3526
  function spawnPolicyExists(policyName: string): boolean {
3461
- 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);
3462
3528
  return Boolean(row);
3463
3529
  }
3464
3530
 
3465
3531
  function runningAgentForPolicy(policyName: string): string | null {
3466
- const row = db.prepare(`
3532
+ const row = db.query(`
3467
3533
  SELECT agent_id
3468
3534
  FROM managed_agent_state
3469
3535
  WHERE policy_name = ? AND status = 'running' AND agent_id IS NOT NULL
@@ -3475,7 +3541,7 @@ function runningAgentForPolicy(policyName: string): string | null {
3475
3541
  }
3476
3542
 
3477
3543
  function queueDepthLimit(target: string): number {
3478
- 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;
3479
3545
  const parsed = row?.value ? parseJson<Record<string, unknown>>(row.value, {}) : {};
3480
3546
  const perTarget = parsed?.maxDepthPerTarget;
3481
3547
  if (typeof perTarget === "number" && Number.isSafeInteger(perTarget) && perTarget > 0) return perTarget;
@@ -3489,7 +3555,7 @@ function queueDepthLimit(target: string): number {
3489
3555
 
3490
3556
  function enforceQueueLimit(target: string): void {
3491
3557
  const limit = queueDepthLimit(target);
3492
- const rows = db.prepare(`
3558
+ const rows = db.query(`
3493
3559
  SELECT id FROM messages
3494
3560
  WHERE to_target = ? AND delivery_status = 'queued'
3495
3561
  ORDER BY queued_at DESC, id DESC
@@ -3591,7 +3657,7 @@ const REPLY_DUPLICATE_WINDOW_MS = 2 * 60 * 1000;
3591
3657
 
3592
3658
  function findRecentDuplicateReply(input: SendMessageInput, threadId: number | null, now: number, hasAttachments: boolean): Message | null {
3593
3659
  if (!input.replyTo || threadId === null || hasAttachments) return null;
3594
- const row = db.prepare(`
3660
+ const row = db.query(`
3595
3661
  ${MSG_SELECT}
3596
3662
  WHERE m.from_agent = ?
3597
3663
  AND m.to_target = ?
@@ -3640,7 +3706,7 @@ export function sendMessageWithResult(input: SendMessageInput): { message: Messa
3640
3706
  const duplicateReply = findRecentDuplicateReply(input, threadId, now, attachmentRefs.length > 0);
3641
3707
  if (duplicateReply) return { message: duplicateReply, created: false };
3642
3708
 
3643
- const insert = db.prepare(`
3709
+ const insert = db.query(`
3644
3710
  INSERT INTO messages (
3645
3711
  from_agent, to_target, kind, channel, subject, body, thread_id, reply_to, claimable,
3646
3712
  idempotency_key, delivery_status, queued_at, max_age_seconds, resolved_to_agent,
@@ -3652,7 +3718,7 @@ export function sendMessageWithResult(input: SendMessageInput): { message: Messa
3652
3718
  $payload, $meta, $now
3653
3719
  )
3654
3720
  `);
3655
- const setSelfThread = db.prepare("UPDATE messages SET thread_id = ? WHERE id = ?");
3721
+ const setSelfThread = db.query("UPDATE messages SET thread_id = ? WHERE id = ?");
3656
3722
  const claimable = shouldStoreClaimable(input);
3657
3723
  const kind = inferMessageKind(input);
3658
3724
  const policyName = policyNameFromTarget(input.to);
@@ -3711,7 +3777,7 @@ export function getThread(messageId: number): Message[] {
3711
3777
  const threadId = msg.threadId ?? msg.id;
3712
3778
  return (
3713
3779
  db
3714
- .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`)
3715
3781
  .all(threadId) as any[]
3716
3782
  ).map(rowToMessage);
3717
3783
  }
@@ -3730,10 +3796,10 @@ export function setMessageReaction(input: {
3730
3796
 
3731
3797
  const now = Date.now();
3732
3798
  if (input.action === "remove") {
3733
- 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 = ?")
3734
3800
  .run(input.messageId, actorId, emoji);
3735
3801
  } else {
3736
- db.prepare(`
3802
+ db.query(`
3737
3803
  INSERT INTO message_reactions (message_id, actor_id, emoji, created_at, updated_at)
3738
3804
  VALUES (?, ?, ?, ?, ?)
3739
3805
  ON CONFLICT(message_id, actor_id, emoji) DO UPDATE SET updated_at = excluded.updated_at
@@ -3748,7 +3814,7 @@ export function findMessageByTelegramSource(input: {
3748
3814
  chatId: string;
3749
3815
  messageId: string;
3750
3816
  }): Message | null {
3751
- const rows = db.prepare(`
3817
+ const rows = db.query(`
3752
3818
  ${MSG_SELECT}
3753
3819
  WHERE json_extract(m.payload, '$.source.telegram.chatId') = ?
3754
3820
  AND json_extract(m.payload, '$.source.telegram.messageId') = ?
@@ -3762,7 +3828,7 @@ export function findMessageByTelegramSource(input: {
3762
3828
 
3763
3829
  function claimMessageRow(messageId: number, agentId: string, now: number): { ok: false; error: string } | { ok: true } {
3764
3830
  const expiresAt = now + CLAIM_LEASE_MS;
3765
- const result = db.prepare(
3831
+ const result = db.query(
3766
3832
  "UPDATE messages SET claimed_by = ?, claimed_at = ?, claim_expires_at = ? WHERE id = ? AND claimed_by IS NULL"
3767
3833
  ).run(agentId, now, expiresAt, messageId);
3768
3834
 
@@ -3802,7 +3868,7 @@ export function claimMessage(messageId: number, agentId: string, guard?: AgentSe
3802
3868
  throw new ClaimError(`linked task is ${task.status}`);
3803
3869
  }
3804
3870
 
3805
- const taskClaim = db.prepare(`
3871
+ const taskClaim = db.query(`
3806
3872
  UPDATE tasks SET status = 'claimed', claimed_by = ?, claimed_at = ?, claim_expires_at = ?, updated_at = ?
3807
3873
  WHERE id = ? AND message_id = ? AND status IN ('open', 'blocked')
3808
3874
  `).run(agentId, now, expiresAt, now, taskId, messageId);
@@ -3841,12 +3907,12 @@ export function renewMessageClaim(messageId: number, agentId: string, guard?: Ag
3841
3907
  const expiresAt = now + CLAIM_LEASE_MS;
3842
3908
  let task: Task | undefined;
3843
3909
  db.transaction(() => {
3844
- 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);
3845
3911
  const taskId = typeof msg.payload?.taskId === "number" && Number.isSafeInteger(msg.payload.taskId)
3846
3912
  ? msg.payload.taskId
3847
3913
  : null;
3848
3914
  if (taskId) {
3849
- 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);
3850
3916
  task = getTask(taskId) ?? undefined;
3851
3917
  }
3852
3918
  })();
@@ -3854,13 +3920,13 @@ export function renewMessageClaim(messageId: number, agentId: string, guard?: Ag
3854
3920
  }
3855
3921
 
3856
3922
  export function getMessage(id: number): Message | null {
3857
- 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;
3858
3924
  return row ? rowToMessage(row) : null;
3859
3925
  }
3860
3926
 
3861
3927
  export function getMessageDeliveryAttempts(messageId: number, limit = 50): MessageDeliveryAttempt[] {
3862
3928
  const safeLimit = Math.min(Math.max(limit, 1), 200);
3863
- return (db.prepare(`
3929
+ return (db.query(`
3864
3930
  SELECT * FROM message_delivery_attempts
3865
3931
  WHERE message_id = ?
3866
3932
  ORDER BY created_at DESC, id DESC
@@ -3880,7 +3946,7 @@ function insertMessageDeliveryAttempt(
3880
3946
  },
3881
3947
  now: number,
3882
3948
  ): void {
3883
- db.prepare(`
3949
+ db.query(`
3884
3950
  INSERT INTO message_delivery_attempts (message_id, agent_id, action, status, error, next_retry_at, poison_reason, created_at)
3885
3951
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)
3886
3952
  `).run(
@@ -3906,7 +3972,7 @@ function setMessageDeliveryState(
3906
3972
  },
3907
3973
  now: number,
3908
3974
  ): boolean {
3909
- return db.prepare(`
3975
+ return db.query(`
3910
3976
  UPDATE messages
3911
3977
  SET delivery_status = ?,
3912
3978
  delivery_attempts = delivery_attempts + ?,
@@ -4000,7 +4066,7 @@ export function getMessageDeliveryStatus(id: number): Pick<Message, "id" | "to"
4000
4066
 
4001
4067
  export function listQueuedMessages(target: string, limit = 100): Message[] {
4002
4068
  const safeLimit = Math.min(Math.max(limit, 1), 500);
4003
- return (db.prepare(`
4069
+ return (db.query(`
4004
4070
  ${MSG_SELECT}
4005
4071
  WHERE m.to_target = ? AND m.delivery_status = 'queued'
4006
4072
  ORDER BY m.queued_at ASC, m.id ASC
@@ -4010,7 +4076,7 @@ export function listQueuedMessages(target: string, limit = 100): Message[] {
4010
4076
 
4011
4077
  export function resolveQueuedPolicyMessages(policyName: string, agentId: string): Message[] {
4012
4078
  const target = `policy:${policyName}`;
4013
- const rows = db.prepare(`
4079
+ const rows = db.query(`
4014
4080
  SELECT m.id
4015
4081
  FROM messages m
4016
4082
  WHERE m.to_target = ?
@@ -4026,7 +4092,7 @@ export function resolveQueuedPolicyMessages(policyName: string, agentId: string)
4026
4092
  if (rows.length === 0) return [];
4027
4093
  const ids = rows.map((row) => row.id);
4028
4094
  const placeholders = ids.map(() => "?").join(",");
4029
- db.prepare(`
4095
+ db.query(`
4030
4096
  UPDATE messages
4031
4097
  SET delivery_status = 'pending',
4032
4098
  resolved_to_agent = ?,
@@ -4045,7 +4111,7 @@ export function resolveQueuedPolicyMessages(policyName: string, agentId: string)
4045
4111
  }
4046
4112
 
4047
4113
  export function expireQueuedMessages(now: number = Date.now()): Message[] {
4048
- const rows = db.prepare(`
4114
+ const rows = db.query(`
4049
4115
  SELECT id FROM messages
4050
4116
  WHERE delivery_status = 'queued'
4051
4117
  AND queued_at IS NOT NULL
@@ -4090,7 +4156,8 @@ export function listRecentMessages(limit: number = 100, since?: number, channel?
4090
4156
  const sql = `${MSG_SELECT} ${where} ORDER BY m.created_at DESC LIMIT ?`;
4091
4157
  params.push(limit);
4092
4158
 
4093
- 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();
4094
4161
  }
4095
4162
 
4096
4163
  export function pollMessages(query: PollQuery): Message[] {
@@ -4158,7 +4225,8 @@ export function pollMessages(query: PollQuery): Message[] {
4158
4225
  const sql = `${MSG_SELECT} WHERE ${conditions.join(" AND ")} ORDER BY m.created_at ASC LIMIT ?`;
4159
4226
  params.push(limit);
4160
4227
 
4161
- 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);
4162
4230
  }
4163
4231
 
4164
4232
  function messageRequiresReply(message: Message): boolean {
@@ -4186,7 +4254,7 @@ function isCoveredByLaterAgentResponse(message: Message, agentId: string): boole
4186
4254
  // Order by id, not created_at: ids are monotonic insertion order, so this is
4187
4255
  // robust when a reply lands in the same millisecond as the message it covers
4188
4256
  // (created_at > … strictly would miss it, leaving the message wrongly pending).
4189
- const replies = (db.prepare(`
4257
+ const replies = (db.query(`
4190
4258
  ${MSG_SELECT}
4191
4259
  WHERE m.from_agent = ?
4192
4260
  AND m.id > ?
@@ -4219,7 +4287,7 @@ function replyObligationFromMessage(message: Message, agentId: string): ReplyObl
4219
4287
 
4220
4288
  export function listPendingReplyObligations(agentId: string, limit = 20): ReplyObligation[] {
4221
4289
  const scanLimit = Math.max(limit * 5, 50);
4222
- const rows = db.prepare(`
4290
+ const rows = timedQuery("listPendingReplyObligations", () => db.query(`
4223
4291
  ${MSG_SELECT}
4224
4292
  WHERE EXISTS (
4225
4293
  SELECT 1 FROM message_reads mr
@@ -4233,7 +4301,7 @@ export function listPendingReplyObligations(agentId: string, limit = 20): ReplyO
4233
4301
  )
4234
4302
  ORDER BY m.created_at ASC
4235
4303
  LIMIT ?
4236
- `).all(agentId, agentId, agentId, scanLimit) as any[];
4304
+ `).all(agentId, agentId, agentId, scanLimit) as any[]);
4237
4305
  return rows
4238
4306
  .map(rowToMessage)
4239
4307
  .filter(messageRequiresReply)
@@ -4243,12 +4311,12 @@ export function listPendingReplyObligations(agentId: string, limit = 20): ReplyO
4243
4311
  }
4244
4312
 
4245
4313
  export function markRead(messageId: number, agentId: string): boolean {
4246
- 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);
4247
4315
  if (!exists) return false;
4248
- db.prepare(
4316
+ db.query(
4249
4317
  "INSERT OR IGNORE INTO message_reads (message_id, agent_id, read_at) VALUES (?, ?, ?)"
4250
4318
  ).run(messageId, agentId, Date.now());
4251
- db.prepare(`
4319
+ db.query(`
4252
4320
  UPDATE messages
4253
4321
  SET delivery_status = 'delivered',
4254
4322
  delivery_last_error = NULL,
@@ -4261,10 +4329,10 @@ export function markRead(messageId: number, agentId: string): boolean {
4261
4329
  }
4262
4330
 
4263
4331
  export function getInboxState(operatorId: string): InboxState {
4264
- const threads = (db.prepare(
4332
+ const threads = (db.query(
4265
4333
  "SELECT * FROM inbox_thread_state WHERE operator_id = ? ORDER BY updated_at DESC",
4266
4334
  ).all(operatorId) as any[]).map(rowToInboxThreadState);
4267
- const drafts = (db.prepare(
4335
+ const drafts = (db.query(
4268
4336
  "SELECT * FROM inbox_drafts WHERE operator_id = ? ORDER BY updated_at DESC",
4269
4337
  ).all(operatorId) as any[]).map(rowToInboxDraft);
4270
4338
  return { operatorId, threads, drafts };
@@ -4277,7 +4345,7 @@ export function setInboxThreadState(input: {
4277
4345
  archivedAtMessageId?: number | null;
4278
4346
  }): InboxThreadState {
4279
4347
  const now = Date.now();
4280
- const current = db.prepare(
4348
+ const current = db.query(
4281
4349
  "SELECT * FROM inbox_thread_state WHERE operator_id = ? AND peer_id = ?",
4282
4350
  ).get(input.operatorId, input.peerId) as any | undefined;
4283
4351
 
@@ -4288,7 +4356,7 @@ export function setInboxThreadState(input: {
4288
4356
  ? input.archivedAtMessageId ?? null
4289
4357
  : current?.archived_at_message_id ?? null;
4290
4358
 
4291
- db.prepare(`
4359
+ db.query(`
4292
4360
  INSERT INTO inbox_thread_state (operator_id, peer_id, read_cursor_message_id, archived_at_message_id, updated_at)
4293
4361
  VALUES (?, ?, ?, ?, ?)
4294
4362
  ON CONFLICT(operator_id, peer_id) DO UPDATE SET
@@ -4297,7 +4365,7 @@ export function setInboxThreadState(input: {
4297
4365
  updated_at = excluded.updated_at
4298
4366
  `).run(input.operatorId, input.peerId, readCursorMessageId, archivedAtMessageId, now);
4299
4367
 
4300
- return rowToInboxThreadState(db.prepare(
4368
+ return rowToInboxThreadState(db.query(
4301
4369
  "SELECT * FROM inbox_thread_state WHERE operator_id = ? AND peer_id = ?",
4302
4370
  ).get(input.operatorId, input.peerId));
4303
4371
  }
@@ -4310,7 +4378,7 @@ export function setInboxDraft(input: {
4310
4378
  channel?: string | null;
4311
4379
  }): InboxDraft {
4312
4380
  const now = Date.now();
4313
- db.prepare(`
4381
+ db.query(`
4314
4382
  INSERT INTO inbox_drafts (operator_id, peer_id, body, subject, channel, updated_at)
4315
4383
  VALUES (?, ?, ?, ?, ?, ?)
4316
4384
  ON CONFLICT(operator_id, peer_id) DO UPDATE SET
@@ -4320,13 +4388,13 @@ export function setInboxDraft(input: {
4320
4388
  updated_at = excluded.updated_at
4321
4389
  `).run(input.operatorId, input.peerId, input.body, input.subject ?? null, input.channel ?? null, now);
4322
4390
 
4323
- return rowToInboxDraft(db.prepare(
4391
+ return rowToInboxDraft(db.query(
4324
4392
  "SELECT * FROM inbox_drafts WHERE operator_id = ? AND peer_id = ?",
4325
4393
  ).get(input.operatorId, input.peerId));
4326
4394
  }
4327
4395
 
4328
4396
  export function deleteInboxDraft(operatorId: string, peerId: string): boolean {
4329
- 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;
4330
4398
  }
4331
4399
 
4332
4400
  export function createChatHistoryImport(input: {
@@ -4354,7 +4422,7 @@ export function createChatHistoryImport(input: {
4354
4422
  const id = randomUUID();
4355
4423
  const now = Date.now();
4356
4424
  db.transaction(() => {
4357
- db.prepare(`
4425
+ db.query(`
4358
4426
  INSERT INTO chat_history_imports (
4359
4427
  id, target_agent_id, target_spawn_request_id, source_peer_id, source_agent_id,
4360
4428
  source_thread_id, source_agent_label, imported_by, imported_at
@@ -4371,7 +4439,7 @@ export function createChatHistoryImport(input: {
4371
4439
  now,
4372
4440
  );
4373
4441
 
4374
- const insertEntry = db.prepare(`
4442
+ const insertEntry = db.query(`
4375
4443
  INSERT INTO chat_history_import_entries (
4376
4444
  import_id, position, original_message_id, original_from, original_to, original_created_at, message_snapshot
4377
4445
  ) VALUES (?, ?, ?, ?, ?, ?, ?)
@@ -4393,9 +4461,9 @@ export function createChatHistoryImport(input: {
4393
4461
  }
4394
4462
 
4395
4463
  function getChatHistoryImport(id: string): ChatHistoryImport | null {
4396
- 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;
4397
4465
  if (!row) return null;
4398
- const entries = (db.prepare(
4466
+ const entries = (db.query(
4399
4467
  "SELECT * FROM chat_history_import_entries WHERE import_id = ? ORDER BY position ASC",
4400
4468
  ).all(id) as any[]).map(rowToChatHistoryImportEntry);
4401
4469
  return rowToChatHistoryImport(row, entries);
@@ -4418,9 +4486,9 @@ export function listChatHistoryImports(input: {
4418
4486
  }
4419
4487
  const limit = Math.max(1, Math.min(input.limit ?? 100, 500));
4420
4488
  const where = conditions.length ? `WHERE ${conditions.join(" OR ")}` : "";
4421
- 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[];
4422
4490
  return rows.map((row) => {
4423
- const entries = (db.prepare(
4491
+ const entries = (db.query(
4424
4492
  "SELECT * FROM chat_history_import_entries WHERE import_id = ? ORDER BY position ASC",
4425
4493
  ).all(row.id) as any[]).map(rowToChatHistoryImportEntry);
4426
4494
  return rowToChatHistoryImport(row, entries);
@@ -4449,7 +4517,7 @@ export function listActivityEvents(input: {
4449
4517
  }
4450
4518
  const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
4451
4519
  params.push(input.limit ?? 200);
4452
- return (db.prepare(
4520
+ return (db.query(
4453
4521
  `SELECT * FROM activity_events ${where} ORDER BY created_at DESC, id DESC LIMIT ?`,
4454
4522
  ).all(...params) as any[]).map(rowToActivityEvent);
4455
4523
  }
@@ -4478,11 +4546,11 @@ export function getAgentTimeline(agentId: string, opts: { limit?: number; since?
4478
4546
 
4479
4547
  // Managed agents: include policy-scoped lifecycle transitions recorded before
4480
4548
  // the agent registered (agent_id null), correlated via metadata.policyName.
4481
- 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;
4482
4550
  if (managed?.policy_name) {
4483
4551
  const rows = (since !== undefined
4484
- ? 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)
4485
- : 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[];
4486
4554
  for (const ev of rows.map(rowToActivityEvent)) {
4487
4555
  entries.push({ ts: ev.createdAt, source: "activity", kind: ev.kind, title: ev.title, detail: ev.body, metadata: ev.metadata });
4488
4556
  }
@@ -4490,16 +4558,16 @@ export function getAgentTimeline(agentId: string, opts: { limit?: number; since?
4490
4558
 
4491
4559
  // Commands targeting this agent (spawn/shutdown/restart/compact) with outcome.
4492
4560
  const cmds = (since !== undefined
4493
- ? 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)
4494
- : 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[];
4495
4563
  for (const c of cmds) {
4496
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 } });
4497
4565
  }
4498
4566
 
4499
4567
  // Delivery attempts involving this agent (failures, retries, poison).
4500
4568
  const attempts = (since !== undefined
4501
- ? db.prepare("SELECT * FROM message_delivery_attempts WHERE agent_id = ? AND created_at >= ? ORDER BY created_at DESC LIMIT ?").all(agentId, since, limit)
4502
- : 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[];
4503
4571
  for (const row of attempts) {
4504
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 } });
4505
4573
  }
@@ -4510,11 +4578,11 @@ export function getAgentTimeline(agentId: string, opts: { limit?: number; since?
4510
4578
 
4511
4579
  export function createActivityEvent(input: ActivityEventInput): ActivityEvent {
4512
4580
  if (input.clientId) {
4513
- 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;
4514
4582
  if (existing) return rowToActivityEvent(existing);
4515
4583
  }
4516
4584
  const now = Date.now();
4517
- const result = db.prepare(`
4585
+ const result = db.query(`
4518
4586
  INSERT INTO activity_events (
4519
4587
  operator_id, client_id, kind, title, body, meta_text, icon, view, peer_id,
4520
4588
  message_id, pair_id, task_id, agent_id, metadata, created_at
@@ -4537,21 +4605,21 @@ export function createActivityEvent(input: ActivityEventInput): ActivityEvent {
4537
4605
  JSON.stringify(input.metadata ?? {}),
4538
4606
  now,
4539
4607
  );
4540
- 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)));
4541
4609
  }
4542
4610
 
4543
4611
  export function deleteMessage(id: number): boolean {
4544
4612
  return db.transaction(() => {
4545
4613
  // Break reply_to references from children so the FK doesn't block delete.
4546
4614
  // Children keep their thread_id — the thread shows up minus this message.
4547
- 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);
4548
4616
  deleteArtifactLinksForEntity("message", id);
4549
- 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;
4550
4618
  })();
4551
4619
  }
4552
4620
 
4553
4621
  export function getLatestMessageId(): number {
4554
- 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;
4555
4623
  return row?.id ?? 0;
4556
4624
  }
4557
4625
 
@@ -4559,10 +4627,10 @@ export function pruneOldMessages(maxAgeMs: number): number {
4559
4627
  const cutoff = Date.now() - maxAgeMs;
4560
4628
  return db.transaction(() => {
4561
4629
  db
4562
- .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 < ?)")
4563
4631
  .run(cutoff);
4564
4632
  return db
4565
- .prepare("DELETE FROM messages WHERE created_at < ?")
4633
+ .query("DELETE FROM messages WHERE created_at < ?")
4566
4634
  .run(cutoff).changes;
4567
4635
  })();
4568
4636
  }
@@ -4577,28 +4645,28 @@ export function getStats(): {
4577
4645
  openTasks: number;
4578
4646
  } {
4579
4647
  const agents = (
4580
- db.prepare("SELECT COUNT(*) as c FROM agents").get() as any
4648
+ db.query("SELECT COUNT(*) as c FROM agents").get() as any
4581
4649
  ).c;
4582
4650
  const online = (
4583
4651
  db
4584
- .prepare(
4652
+ .query(
4585
4653
  "SELECT COUNT(*) as c FROM agents WHERE status != 'offline' AND last_seen > ?"
4586
4654
  )
4587
4655
  .get(Date.now() - STALE_TTL_MS) as any
4588
4656
  ).c;
4589
4657
  const messages = (
4590
- db.prepare("SELECT COUNT(*) as c FROM messages").get() as any
4658
+ db.query("SELECT COUNT(*) as c FROM messages").get() as any
4591
4659
  ).c;
4592
4660
  const messagesLast24h = (
4593
4661
  db
4594
- .prepare("SELECT COUNT(*) as c FROM messages WHERE created_at > ?")
4662
+ .query("SELECT COUNT(*) as c FROM messages WHERE created_at > ?")
4595
4663
  .get(Date.now() - DAY_MS) as any
4596
4664
  ).c;
4597
4665
  const tasks = (
4598
- db.prepare("SELECT COUNT(*) as c FROM tasks").get() as any
4666
+ db.query("SELECT COUNT(*) as c FROM tasks").get() as any
4599
4667
  ).c;
4600
4668
  const openTasks = (
4601
- 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
4602
4670
  ).c;
4603
4671
 
4604
4672
  return { version: VERSION, agents, online, messages, messagesLast24h, tasks, openTasks };
@@ -4608,14 +4676,14 @@ export function getHealth(now: number = Date.now()): HealthReport {
4608
4676
  const checks: HealthCheck[] = [];
4609
4677
 
4610
4678
  try {
4611
- db.prepare("SELECT 1").get();
4679
+ db.query("SELECT 1").get();
4612
4680
  checks.push({ name: "database", status: "ok" });
4613
4681
  } catch (e) {
4614
4682
  checks.push({ name: "database", status: "error", detail: e instanceof Error ? e.message : "database check failed" });
4615
4683
  }
4616
4684
 
4617
4685
  const staleLiveAgents = (db
4618
- .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')")
4619
4687
  .get(now - STALE_TTL_MS) as any).c as number;
4620
4688
  checks.push({
4621
4689
  name: "stale-live-agents",
@@ -4625,7 +4693,7 @@ export function getHealth(now: number = Date.now()): HealthReport {
4625
4693
  });
4626
4694
 
4627
4695
  const expiredMessageClaims = (db
4628
- .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'))")
4629
4697
  .get(now) as any).c as number;
4630
4698
  checks.push({
4631
4699
  name: "expired-message-claims",
@@ -4635,7 +4703,7 @@ export function getHealth(now: number = Date.now()): HealthReport {
4635
4703
  });
4636
4704
 
4637
4705
  const expiredTaskClaims = (db
4638
- .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')")
4639
4707
  .get(now) as any).c as number;
4640
4708
  checks.push({
4641
4709
  name: "expired-task-claims",
@@ -4645,7 +4713,7 @@ export function getHealth(now: number = Date.now()): HealthReport {
4645
4713
  });
4646
4714
 
4647
4715
  const offlineClaimedTasks = (db
4648
- .prepare(`
4716
+ .query(`
4649
4717
  SELECT COUNT(*) as c
4650
4718
  FROM tasks t
4651
4719
  JOIN agents a ON a.id = t.claimed_by
@@ -4792,12 +4860,12 @@ function parseOrchestratorUpgrade(value: unknown): OrchestratorUpgradeState | un
4792
4860
  * meta blob (no schema change). Pass null to clear.
4793
4861
  */
4794
4862
  export function setOrchestratorUpgradeState(id: string, state: OrchestratorUpgradeState | null): Orchestrator | null {
4795
- 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;
4796
4864
  if (!row) return null;
4797
4865
  const meta = parseJson<Record<string, unknown>>(row.meta ?? "{}", {});
4798
4866
  if (state) meta.upgrade = state;
4799
4867
  else delete meta.upgrade;
4800
- 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);
4801
4869
  return getOrchestrator(id);
4802
4870
  }
4803
4871
 
@@ -4885,11 +4953,11 @@ export function upsertOrchestrator(input: RegisterOrchestratorInput): Orchestrat
4885
4953
  // Carry forward server-managed meta the orchestrator never reports (upgrade
4886
4954
  // state) — registration meta would otherwise drop it on the very re-register
4887
4955
  // that follows a self-upgrade restart, breaking version reconciliation.
4888
- 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;
4889
4957
  const existingMeta = existingRow ? parseJson<Record<string, unknown>>(existingRow.meta ?? "{}", {}) : {};
4890
4958
  const mergedMeta = mergeOrchestratorRuntimeMeta(input.meta ?? {}, input);
4891
4959
  if (existingMeta.upgrade !== undefined && mergedMeta.upgrade === undefined) mergedMeta.upgrade = existingMeta.upgrade;
4892
- const stmt = db.prepare(`
4960
+ const stmt = db.query(`
4893
4961
  INSERT INTO orchestrators (id, hostname, status, agent_id, providers, base_dir, api_url, env_keys, meta, last_seen, created_at)
4894
4962
  VALUES ($id, $hostname, 'online', $agentId, $providers, $baseDir, $apiUrl, $envKeys, $meta, $now, $now)
4895
4963
  ON CONFLICT(id) DO UPDATE SET
@@ -4939,24 +5007,24 @@ export function upsertOrchestrator(input: RegisterOrchestratorInput): Orchestrat
4939
5007
  }
4940
5008
 
4941
5009
  export function getOrchestrator(id: string): Orchestrator | null {
4942
- 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;
4943
5011
  return row ? rowToOrchestrator(row) : null;
4944
5012
  }
4945
5013
 
4946
5014
  export function listOrchestrators(): Orchestrator[] {
4947
- 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);
4948
5016
  }
4949
5017
 
4950
5018
  export function orchestratorHeartbeat(id: string, runtime: OrchestratorRuntimeInput = {}): Orchestrator | null {
4951
5019
  const now = Date.now();
4952
- 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;
4953
5021
  if (!row) return null;
4954
5022
  const meta = mergeOrchestratorRuntimeMeta(parseJson<Record<string, unknown>>(row.meta ?? "{}", {}), runtime);
4955
5023
  if (runtime.providers) {
4956
- 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 = ?")
4957
5025
  .run(now, JSON.stringify(runtime.providers), JSON.stringify(meta), id);
4958
5026
  } else {
4959
- 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);
4960
5028
  }
4961
5029
  // Also heartbeat the agent
4962
5030
  const orch = getOrchestrator(id);
@@ -4988,7 +5056,7 @@ export function updateManagedAgents(id: string, agents: ManagedAgent[]): Orchest
4988
5056
  },
4989
5057
  } : agent;
4990
5058
  });
4991
- db.prepare("UPDATE orchestrators SET managed_agents = ?, last_seen = ? WHERE id = ?")
5059
+ db.query("UPDATE orchestrators SET managed_agents = ?, last_seen = ? WHERE id = ?")
4992
5060
  .run(JSON.stringify(enriched), Date.now(), id);
4993
5061
  return getOrchestrator(id);
4994
5062
  }
@@ -5046,7 +5114,7 @@ function upsertWorkspaceFromManagedAgent(agent: ManagedAgent): WorkspaceRecord |
5046
5114
 
5047
5115
  export function upsertWorkspace(input: Omit<WorkspaceRecord, "createdAt" | "updatedAt"> & Partial<Pick<WorkspaceRecord, "createdAt" | "updatedAt">>): WorkspaceRecord {
5048
5116
  const now = Date.now();
5049
- db.prepare(`
5117
+ db.query(`
5050
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)
5051
5119
  VALUES ($id, $repoRoot, $sourceCwd, $worktreePath, $branch, $baseRef, $baseSha, $mode, $requestedMode, $status, $ownerAgentId, $ownerPolicyName, $ownerAutomationRunId, $stewardAgentId, $metadata, $createdAt, $updatedAt, $readyAt, $cleanedAt)
5052
5120
  ON CONFLICT(id) DO UPDATE SET
@@ -5093,7 +5161,7 @@ export function upsertWorkspace(input: Omit<WorkspaceRecord, "createdAt" | "upda
5093
5161
  }
5094
5162
 
5095
5163
  export function getWorkspace(id: string): WorkspaceRecord | null {
5096
- 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;
5097
5165
  return row ? rowToWorkspace(row) : null;
5098
5166
  }
5099
5167
 
@@ -5104,11 +5172,11 @@ export function listWorkspaces(filter: { repoRoot?: string; ownerAgentId?: strin
5104
5172
  if (filter.ownerAgentId) { where.push("owner_agent_id = ?"); params.push(filter.ownerAgentId); }
5105
5173
  if (filter.status) { where.push("status = ?"); params.push(filter.status); }
5106
5174
  const sql = `SELECT * FROM workspaces${where.length ? " WHERE " + where.join(" AND ") : ""} ORDER BY updated_at DESC`;
5107
- return (db.prepare(sql).all(...params) as any[]).map(rowToWorkspace);
5175
+ return (db.query(sql).all(...params) as any[]).map(rowToWorkspace);
5108
5176
  }
5109
5177
 
5110
5178
  export function deleteWorkspace(id: string): boolean {
5111
- 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;
5112
5180
  }
5113
5181
 
5114
5182
  // Shared-mode rows are pure occupancy markers (no worktree on disk) that only
@@ -5126,9 +5194,9 @@ export function pruneOrphanedSharedWorkspaces(): string[] {
5126
5194
  OR owner_agent_id NOT IN (SELECT id FROM agents)
5127
5195
  OR owner_agent_id IN (SELECT id FROM agents WHERE status = 'offline')
5128
5196
  )`;
5129
- 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 }>;
5130
5198
  if (!rows.length) return [];
5131
- db.prepare(`DELETE FROM workspaces WHERE ${orphanCondition}`).run();
5199
+ db.query(`DELETE FROM workspaces WHERE ${orphanCondition}`).run();
5132
5200
  return rows.map((r) => r.id);
5133
5201
  })();
5134
5202
  }
@@ -5138,7 +5206,7 @@ export function updateWorkspaceStatus(id: string, status: WorkspaceStatus, metad
5138
5206
  if (!existing) return null;
5139
5207
  const nextMeta = { ...existing.metadata, ...metadata };
5140
5208
  const now = Date.now();
5141
- db.prepare(`
5209
+ db.query(`
5142
5210
  UPDATE workspaces
5143
5211
  SET status = ?, metadata = ?, updated_at = ?, ready_at = coalesce(ready_at, ?), cleaned_at = coalesce(cleaned_at, ?)
5144
5212
  WHERE id = ?
@@ -5177,19 +5245,19 @@ function rowToRepoSteward(row: any): RepoStewardRecord {
5177
5245
  }
5178
5246
 
5179
5247
  export function getRepoSteward(repoRoot: string): RepoStewardRecord | null {
5180
- 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;
5181
5249
  return row ? rowToRepoSteward(row) : null;
5182
5250
  }
5183
5251
 
5184
5252
  export function listRepoStewards(): RepoStewardRecord[] {
5185
- 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);
5186
5254
  }
5187
5255
 
5188
5256
  // Persist the elected steward for a repo. The row is never deleted, so a repo's
5189
5257
  // stewardship survives a full all-agents-offline gap (steward goes NULL/dormant,
5190
5258
  // last_steward_agent_id keeps continuity) and resumes on the next agent join.
5191
5259
  function upsertRepoSteward(repoRoot: string, steward: string | null, now: number): void {
5192
- db.prepare(`
5260
+ db.query(`
5193
5261
  INSERT INTO repo_stewards (repo_root, steward_agent_id, last_steward_agent_id, elected_at, updated_at)
5194
5262
  VALUES ($repoRoot, $steward, $steward, $electedAt, $now)
5195
5263
  ON CONFLICT(repo_root) DO UPDATE SET
@@ -5205,7 +5273,7 @@ function upsertRepoSteward(repoRoot: string, steward: string | null, now: number
5205
5273
 
5206
5274
  function electWorkspaceStewards(repoRoot?: string): void {
5207
5275
  const params: string[] = repoRoot ? [repoRoot] : [];
5208
- const repoRows = db.prepare(`
5276
+ const repoRows = db.query(`
5209
5277
  SELECT DISTINCT repo_root FROM workspaces
5210
5278
  WHERE status IN (${STEWARD_LIVE_STATUSES})
5211
5279
  ${repoRoot ? "AND repo_root = ?" : ""}
@@ -5215,7 +5283,7 @@ function electWorkspaceStewards(repoRoot?: string): void {
5215
5283
  // Candidate pool: owners of live workspaces in this repo who are online,
5216
5284
  // oldest first. A steward must be an online agent actively in the repo — an
5217
5285
  // offline agent can't coordinate, so it is never elected (the old bug).
5218
- const pool = (db.prepare(`
5286
+ const pool = (db.query(`
5219
5287
  SELECT w.owner_agent_id AS id, MIN(w.created_at) AS created_at
5220
5288
  FROM workspaces w JOIN agents a ON a.id = w.owner_agent_id
5221
5289
  WHERE w.repo_root = ? AND w.owner_agent_id IS NOT NULL
@@ -5234,7 +5302,7 @@ function electWorkspaceStewards(repoRoot?: string): void {
5234
5302
  // re-elections don't churn updated_at and reset the auto-abandon clock for a
5235
5303
  // dormant repo (a stranded review_requested must still age out).
5236
5304
  if (steward !== current) {
5237
- 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})`)
5238
5306
  .run(steward, now, row.repo_root);
5239
5307
  }
5240
5308
  }
@@ -5258,7 +5326,7 @@ export function patchWorkspaceMetadata(id: string, patch: Record<string, unknown
5258
5326
  if (v === undefined) delete next[k];
5259
5327
  else next[k] = v;
5260
5328
  }
5261
- 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);
5262
5330
  return getWorkspace(id);
5263
5331
  }
5264
5332
 
@@ -5266,7 +5334,7 @@ export function patchWorkspaceMetadata(id: string, patch: Record<string, unknown
5266
5334
  // when an agent (re)registers so a dormant repo regains a steward on rejoin
5267
5335
  // without a full unscoped sweep.
5268
5336
  function electWorkspaceStewardsForAgent(agentId: string): void {
5269
- const repos = db.prepare(`
5337
+ const repos = db.query(`
5270
5338
  SELECT DISTINCT repo_root FROM workspaces
5271
5339
  WHERE owner_agent_id = ? AND status IN (${STEWARD_LIVE_STATUSES})
5272
5340
  `).all(agentId) as Array<{ repo_root: string }>;
@@ -5296,18 +5364,18 @@ function rowToMergeLease(row: any): MergeLeaseRecord {
5296
5364
  }
5297
5365
 
5298
5366
  export function getMergeLease(repoRoot: string): MergeLeaseRecord | null {
5299
- 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;
5300
5368
  return row ? rowToMergeLease(row) : null;
5301
5369
  }
5302
5370
 
5303
5371
  export function listMergeLeases(): MergeLeaseRecord[] {
5304
- 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);
5305
5373
  }
5306
5374
 
5307
5375
  export function releaseExpiredMergeLeases(now: number = Date.now()): string[] {
5308
- 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 }>;
5309
5377
  if (!expired.length) return [];
5310
- 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);
5311
5379
  return expired.map((r) => r.repo_root);
5312
5380
  }
5313
5381
 
@@ -5324,7 +5392,7 @@ export function acquireMergeLease(
5324
5392
  const existing = getMergeLease(repoRoot);
5325
5393
  if (existing && existing.expiresAt > now) return { ok: false as const, lease: existing };
5326
5394
  const expiresAt = now + WORKSPACE_MERGE_LEASE_MS;
5327
- db.prepare(`
5395
+ db.query(`
5328
5396
  INSERT INTO workspace_merge_leases (repo_root, workspace_id, command_id, holder, acquired_at, expires_at)
5329
5397
  VALUES (?, ?, NULL, ?, ?, ?)
5330
5398
  ON CONFLICT(repo_root) DO UPDATE SET
@@ -5338,7 +5406,7 @@ export function acquireMergeLease(
5338
5406
  // Attach the dispatched command id to a held lease so it can be released by
5339
5407
  // command id when the merge settles.
5340
5408
  export function setMergeLeaseCommand(repoRoot: string, commandId: string): void {
5341
- 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);
5342
5410
  }
5343
5411
 
5344
5412
  // Release a merge lease. Guard by commandId/workspaceId when known so a stale
@@ -5350,26 +5418,26 @@ export function releaseMergeLease(opts: { repoRoot?: string; commandId?: string;
5350
5418
  if (opts.commandId) { where.push("command_id = ?"); params.push(opts.commandId); }
5351
5419
  if (opts.workspaceId) { where.push("workspace_id = ?"); params.push(opts.workspaceId); }
5352
5420
  if (!where.length) return false;
5353
- 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;
5354
5422
  }
5355
5423
 
5356
5424
  export function deleteOrchestrator(id: string): boolean {
5357
5425
  const orch = getOrchestrator(id);
5358
5426
  if (!orch) return false;
5359
- db.prepare("DELETE FROM orchestrators WHERE id = ?").run(id);
5427
+ db.query("DELETE FROM orchestrators WHERE id = ?").run(id);
5360
5428
  deleteAgent(orch.agentId);
5361
5429
  return true;
5362
5430
  }
5363
5431
 
5364
5432
  export function reapStaleOrchestrators(): string[] {
5365
5433
  const cutoff = Date.now() - STALE_TTL_MS;
5366
- 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[];
5367
5435
  for (const row of stale) {
5368
- 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);
5369
5437
  // An orchestrator agent holds no workspaces, so use a direct status update
5370
5438
  // instead of setStatus() (which triggers an unscoped electWorkspaceStewards
5371
5439
  // sweep across all repos on every offline transition).
5372
- 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);
5373
5441
  }
5374
5442
  return stale.map((row: any) => row.id);
5375
5443
  }