agent-relay-server 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/automations.ts +17 -17
- package/src/bus-outbox.ts +5 -5
- package/src/bus.ts +1 -1
- package/src/commands-db.ts +5 -5
- package/src/config-store.ts +12 -12
- package/src/db.ts +294 -226
- package/src/insights-db.ts +6 -6
- package/src/lifecycle-manager.ts +3 -3
- package/src/maintenance.ts +25 -6
- package/src/memory-sqlite-broker.ts +12 -12
- package/src/provider-catalog-store.ts +2 -2
- package/src/recipe-db.ts +9 -9
- package/src/security.ts +1 -1
- package/src/token-db.ts +10 -10
package/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.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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.
|
|
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.
|
|
1897
|
-
: db.
|
|
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
|
-
.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
2073
|
-
: db.
|
|
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.
|
|
2161
|
+
db.query("DELETE FROM channel_bindings WHERE channel_id = ? AND conversation_key = ?").run(input.channelId, conversationKey);
|
|
2096
2162
|
}
|
|
2097
|
-
db.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
2272
|
+
db.query("UPDATE agents SET last_seen = ? WHERE id IN ('user', 'system')").run(now);
|
|
2207
2273
|
const rows = db
|
|
2208
|
-
.
|
|
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.
|
|
2296
|
+
const rows = db.query(selectSql).all(...params) as any[];
|
|
2231
2297
|
if (rows.length === 0) return;
|
|
2232
|
-
db.
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
.
|
|
2762
|
+
.query(`SELECT id FROM messages WHERE ${releasableMessageClaim}`)
|
|
2697
2763
|
.all(now) as any[];
|
|
2698
2764
|
const taskRows = db
|
|
2699
|
-
.
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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.
|
|
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
|
-
.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
4314
|
+
const exists = db.query("SELECT 1 FROM messages WHERE id = ?").get(messageId);
|
|
4247
4315
|
if (!exists) return false;
|
|
4248
|
-
db.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
4485
|
-
: db.
|
|
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.
|
|
4494
|
-
: db.
|
|
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.
|
|
4502
|
-
: db.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
4615
|
+
db.query("UPDATE messages SET reply_to = NULL WHERE reply_to = ?").run(id);
|
|
4548
4616
|
deleteArtifactLinksForEntity("message", id);
|
|
4549
|
-
return db.
|
|
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.
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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.
|
|
4648
|
+
db.query("SELECT COUNT(*) as c FROM agents").get() as any
|
|
4581
4649
|
).c;
|
|
4582
4650
|
const online = (
|
|
4583
4651
|
db
|
|
4584
|
-
.
|
|
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.
|
|
4658
|
+
db.query("SELECT COUNT(*) as c FROM messages").get() as any
|
|
4591
4659
|
).c;
|
|
4592
4660
|
const messagesLast24h = (
|
|
4593
4661
|
db
|
|
4594
|
-
.
|
|
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.
|
|
4666
|
+
db.query("SELECT COUNT(*) as c FROM tasks").get() as any
|
|
4599
4667
|
).c;
|
|
4600
4668
|
const openTasks = (
|
|
4601
|
-
db.
|
|
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.
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
5197
|
+
const rows = db.query(`SELECT id FROM workspaces WHERE ${orphanCondition}`).all() as Array<{ id: string }>;
|
|
5130
5198
|
if (!rows.length) return [];
|
|
5131
|
-
db.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
}
|