@triflux/remote 10.51.0 → 10.52.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.
Files changed (59) hide show
  1. package/cto/brief.mjs +0 -8
  2. package/cto/collect.mjs +1 -127
  3. package/cto/current.schema.json +2 -19
  4. package/cto/hygiene.mjs +2 -61
  5. package/cto/lake-root.mjs +0 -56
  6. package/cto/status.mjs +5 -139
  7. package/hub/index.mjs +0 -9
  8. package/hub/team/agents-row-attach.py +120 -0
  9. package/hub/team/agents-row.mjs +200 -0
  10. package/hub/team/claude-daemon-control.mjs +1 -2
  11. package/hub/team/claude-session-projection.mjs +5 -73
  12. package/hub/team/cli/commands/debug.mjs +1 -2
  13. package/hub/team/cli/commands/start/index.mjs +2 -1
  14. package/hub/team/cli/commands/start/start-mux.mjs +2 -4
  15. package/hub/team/cli/services/state-store.mjs +5 -20
  16. package/hub/team/headless.mjs +96 -543
  17. package/hub/team/psmux.mjs +3 -33
  18. package/hub/team/session-context.mjs +1 -1
  19. package/hub/team/tui-viewer.mjs +9 -56
  20. package/hub/team/tui.mjs +0 -37
  21. package/hub/team/uds-orchestrator.mjs +2 -580
  22. package/hub/workers/factory.mjs +6 -51
  23. package/hub/workers/interface.mjs +1 -7
  24. package/hub/workers/lib/jsonrpc-core.mjs +11 -0
  25. package/hub/workers/lib/jsonrpc-ws-uds.mjs +5 -6
  26. package/package.json +3 -8
  27. package/scripts/lib/agent-route-policy.mjs +0 -67
  28. package/scripts/lib/cli-claude.mjs +0 -12
  29. package/scripts/lib/env-probe.mjs +0 -70
  30. package/scripts/lib/legacy-mcp-cleanup.mjs +283 -0
  31. package/scripts/lib/mcp-guard-engine.mjs +40 -98
  32. package/hub/hub-lifecycle.mjs +0 -248
  33. package/hub/pipe.mjs +0 -825
  34. package/hub/promote-penalties.mjs +0 -138
  35. package/hub/public/dashboard.html +0 -416
  36. package/hub/schema.sql +0 -209
  37. package/hub/server.mjs +0 -3048
  38. package/hub/store-adapter.mjs +0 -182
  39. package/hub/store.mjs +0 -1736
  40. package/hub/team/cli/services/hub-client.mjs +0 -178
  41. package/hub/team/conductor-registry.mjs +0 -121
  42. package/hub/team/cto-auto-collect.mjs +0 -115
  43. package/hub/team/lead-control.mjs +0 -191
  44. package/hub/team/session-sync.mjs +0 -316
  45. package/hub/team/swarm-intent.mjs +0 -18
  46. package/hub/team/synapse-cli.mjs +0 -458
  47. package/hub/team/synapse-http.mjs +0 -311
  48. package/hub/team/synapse-registry.mjs +0 -618
  49. package/hub/team/tui-synapse.mjs +0 -223
  50. package/hub/team/worker-sandbox.mjs +0 -125
  51. package/hub/tools.mjs +0 -900
  52. package/hub/workers/codex-app-server-worker.mjs +0 -1067
  53. package/hub/workers/delegator-mcp.mjs +0 -1098
  54. package/hub/workers/lib/jsonrpc-stdio.mjs +0 -204
  55. package/scripts/lib/claudemd-scanner.mjs +0 -26
  56. package/scripts/lib/context.mjs +0 -51
  57. package/scripts/lib/handoff.mjs +0 -187
  58. package/scripts/lib/logger.mjs +0 -122
  59. package/scripts/lib/stealth-fetch.mjs +0 -176
package/hub/store.mjs DELETED
@@ -1,1736 +0,0 @@
1
- // hub/store.mjs — SQLite 감사 로그/메타데이터/역할 레지스트리 저장소
2
- // 실시간 배달은 router/pipe가 담당하고, 역할 권한과 복구 상태는 SQLite가 보존한다.
3
-
4
- import { createHash } from "node:crypto";
5
- import { mkdirSync, readFileSync } from "node:fs";
6
- import { createRequire } from "node:module";
7
- import { dirname, join } from "node:path";
8
- import { fileURLToPath } from "node:url";
9
- import {
10
- clampDurationMs,
11
- DEFAULT_ASSIGN_TIMEOUT_MS,
12
- DEFAULT_ASSIGN_TTL_MS,
13
- } from "@triflux/core/hub/lib/timeout-defaults.mjs";
14
- import { uuidv7 } from "@triflux/core/hub/lib/uuidv7.mjs";
15
- import { recalcConfidence } from "@triflux/core/hub/reflexion.mjs";
16
- import { decodeRoleKey, ROLE_REACHABILITY_STATES } from "@triflux/core/hub/role-contract.mjs";
17
-
18
- export { uuidv7 };
19
-
20
- const __dirname = dirname(fileURLToPath(import.meta.url));
21
- const require = createRequire(import.meta.url);
22
-
23
- function parseJson(str, fallback = null) {
24
- if (str == null) return fallback;
25
- try {
26
- return JSON.parse(str);
27
- } catch {
28
- return fallback;
29
- }
30
- }
31
-
32
- function parseAgentRow(row) {
33
- if (!row) return null;
34
- const { capabilities_json, topics_json, metadata_json, ...rest } = row;
35
- return {
36
- ...rest,
37
- capabilities: parseJson(capabilities_json, []),
38
- topics: parseJson(topics_json, []),
39
- metadata: parseJson(metadata_json, {}),
40
- };
41
- }
42
-
43
- function parseMessageRow(row) {
44
- if (!row) return null;
45
- const { payload_json, ...rest } = row;
46
- return { ...rest, payload: parseJson(payload_json, {}) };
47
- }
48
-
49
- function parseHumanRequestRow(row) {
50
- if (!row) return null;
51
- const { schema_json, response_json, ...rest } = row;
52
- return {
53
- ...rest,
54
- schema: parseJson(schema_json, {}),
55
- response: parseJson(response_json, null),
56
- };
57
- }
58
-
59
- function parseAssignRow(row) {
60
- if (!row) return null;
61
- const { payload_json, result_json, error_json, ...rest } = row;
62
- return {
63
- ...rest,
64
- payload: parseJson(payload_json, {}),
65
- result: parseJson(result_json, null),
66
- error: parseJson(error_json, null),
67
- };
68
- }
69
-
70
- function parseReflexionRow(row) {
71
- if (!row) return null;
72
- const { context_json, adaptive_state_json, ...rest } = row;
73
- return {
74
- ...rest,
75
- type: rest.type || "reflexion",
76
- context: parseJson(context_json, {}),
77
- adaptive_state: parseJson(adaptive_state_json, {}),
78
- };
79
- }
80
-
81
- function parseRoleCandidateRow(row) {
82
- if (!row) return null;
83
- const { transport_locators_json, ...rest } = row;
84
- return {
85
- ...rest,
86
- transport_locators: parseJson(transport_locators_json, []),
87
- };
88
- }
89
-
90
- function hasTable(db, tableName) {
91
- return !!db
92
- .prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?")
93
- .get(tableName);
94
- }
95
-
96
- function hasColumn(db, tableName, columnName) {
97
- const rows = db.prepare(`PRAGMA table_info(${tableName})`).all();
98
- return rows.some((row) => row.name === columnName);
99
- }
100
-
101
- function ensureColumn(db, tableName, columnName, definition) {
102
- if (hasColumn(db, tableName, columnName)) return;
103
- db.exec(`ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${definition}`);
104
- }
105
-
106
- function ensureV6Columns(db) {
107
- if (hasTable(db, "agents")) {
108
- ensureColumn(db, "agents", "project_id", "TEXT");
109
- ensureColumn(db, "agents", "session_id", "TEXT");
110
- ensureColumn(db, "agents", "host_id", "TEXT");
111
- ensureColumn(
112
- db,
113
- "agents",
114
- "transport_locators_json",
115
- "TEXT NOT NULL DEFAULT '[]'",
116
- );
117
- }
118
- if (hasTable(db, "messages")) {
119
- ensureColumn(db, "messages", "role_key_wire", "TEXT");
120
- }
121
- }
122
-
123
- function ensureV7Columns(db) {
124
- if (hasTable(db, "agents")) {
125
- ensureColumn(
126
- db,
127
- "agents",
128
- "presence_generation",
129
- "INTEGER NOT NULL DEFAULT 0",
130
- );
131
- }
132
- }
133
-
134
- function storeFingerprint(dbPath) {
135
- return `sqlite:${createHash("sha256").update(String(dbPath)).digest("hex")}`;
136
- }
137
-
138
- /**
139
- * 저장소 생성
140
- * @param {string} dbPath
141
- */
142
- export async function importBetterSqlite3() {
143
- const mod = await import("better-sqlite3");
144
- return mod.default ?? mod;
145
- }
146
-
147
- function resolveBetterSqlite3(options = {}) {
148
- if (options.DatabaseCtor) return options.DatabaseCtor;
149
- const mod = require("better-sqlite3");
150
- return mod.default ?? mod;
151
- }
152
-
153
- export function createStore(dbPath, options = {}) {
154
- mkdirSync(dirname(dbPath), { recursive: true });
155
- const Database = resolveBetterSqlite3(options);
156
- const db = new Database(dbPath);
157
-
158
- db.pragma("journal_mode = WAL");
159
- db.pragma("synchronous = NORMAL");
160
- db.pragma("foreign_keys = ON");
161
- db.pragma("busy_timeout = 5000");
162
- db.pragma("wal_autocheckpoint = 1000");
163
-
164
- const schemaSQL = readFileSync(join(__dirname, "schema.sql"), "utf8");
165
- db.exec(
166
- "CREATE TABLE IF NOT EXISTS _meta (key TEXT PRIMARY KEY, value TEXT)",
167
- );
168
- const SCHEMA_VERSION = "7";
169
- const curVer = (() => {
170
- try {
171
- return db
172
- .prepare("SELECT value FROM _meta WHERE key='schema_version'")
173
- .pluck()
174
- .get();
175
- } catch {
176
- return null;
177
- }
178
- })();
179
- // 마이그레이션 전략: 스키마 버전이 다르면 schema.sql을 재실행한다.
180
- // schema.sql은 CREATE TABLE IF NOT EXISTS 패턴을 사용하므로 멱등하게 적용된다.
181
- // 비파괴적 컬럼 추가는 자동으로 처리되지만, 컬럼 제거/이름 변경은 수동 마이그레이션이 필요하다.
182
- // v5 테이블에는 schema.sql의 v6 인덱스가 참조하는 컬럼이 없으므로
183
- // CREATE IF NOT EXISTS 재실행 전에 멱등 컬럼 seam을 먼저 적용한다.
184
- ensureV6Columns(db);
185
- ensureV7Columns(db);
186
- if (curVer !== SCHEMA_VERSION) {
187
- if (curVer != null) {
188
- // 이미 버전이 기록된 DB에서 버전 불일치가 발생한 경우 경고한다.
189
- console.warn(
190
- `[store] schema version mismatch: found=${curVer} expected=${SCHEMA_VERSION}. Applying schema.sql (idempotent).`,
191
- );
192
- }
193
- db.exec(schemaSQL);
194
- db.prepare(
195
- "INSERT OR REPLACE INTO _meta (key, value) VALUES ('schema_version', ?)",
196
- ).run(SCHEMA_VERSION);
197
- }
198
- // 매 부팅 실행해 부분 적용된 마이그레이션도 안전하게 복구한다.
199
- ensureV6Columns(db);
200
- ensureV7Columns(db);
201
- ensureColumn(
202
- db,
203
- "reflexion_entries",
204
- "type",
205
- "TEXT NOT NULL DEFAULT 'reflexion'",
206
- );
207
- ensureColumn(db, "assign_jobs", "dispatched_at_ms", "INTEGER");
208
- ensureColumn(
209
- db,
210
- "reflexion_entries",
211
- "adaptive_state_json",
212
- "TEXT NOT NULL DEFAULT '{}'",
213
- );
214
-
215
- const S = {
216
- upsertAgent: db.prepare(`
217
- INSERT INTO agents (agent_id, cli, pid, capabilities_json, topics_json, last_seen_ms, lease_expires_ms, presence_generation, status, metadata_json)
218
- VALUES (@agent_id, @cli, @pid, @capabilities_json, @topics_json, @last_seen_ms, @lease_expires_ms, 1, @status, @metadata_json)
219
- ON CONFLICT(agent_id) DO UPDATE SET
220
- cli=excluded.cli,
221
- pid=excluded.pid,
222
- capabilities_json=excluded.capabilities_json,
223
- topics_json=excluded.topics_json,
224
- last_seen_ms=excluded.last_seen_ms,
225
- lease_expires_ms=excluded.lease_expires_ms,
226
- presence_generation=agents.presence_generation + 1,
227
- status=excluded.status,
228
- metadata_json=excluded.metadata_json`),
229
- getAgent: db.prepare("SELECT * FROM agents WHERE agent_id = ?"),
230
- setAgentTopics: db.prepare(
231
- "UPDATE agents SET topics_json=?, last_seen_ms=? WHERE agent_id=?",
232
- ),
233
- heartbeat: db.prepare(
234
- "UPDATE agents SET last_seen_ms=?, lease_expires_ms=?, status='online' WHERE agent_id=? AND presence_generation=?",
235
- ),
236
- heartbeatWithoutGeneration: db.prepare(
237
- "UPDATE agents SET last_seen_ms=?, lease_expires_ms=?, status='online' WHERE agent_id=?",
238
- ),
239
- setAgentStatus: db.prepare(
240
- "UPDATE agents SET status=? WHERE agent_id=? AND presence_generation=?",
241
- ),
242
- setAgentStatusWithoutGeneration: db.prepare(
243
- "UPDATE agents SET status=? WHERE agent_id=?",
244
- ),
245
- onlineAgents: db.prepare("SELECT * FROM agents WHERE status != 'offline'"),
246
- allAgents: db.prepare("SELECT * FROM agents"),
247
- agentsByTopic: db.prepare(
248
- "SELECT a.* FROM agents a, json_each(a.topics_json) t WHERE t.value=? AND a.status != 'offline'",
249
- ),
250
- markStale: db.prepare(
251
- "UPDATE agents SET status='stale' WHERE agent_id=? AND presence_generation=? AND status='online' AND lease_expires_ms < ?",
252
- ),
253
- markOffline: db.prepare(
254
- "UPDATE agents SET status='offline' WHERE agent_id=? AND presence_generation=? AND status='stale' AND lease_expires_ms < ? - 300000",
255
- ),
256
-
257
- getRole: db.prepare("SELECT * FROM role_registry WHERE role_key_wire = ?"),
258
- insertRoleReservation: db.prepare(`
259
- INSERT INTO role_registry (
260
- role_key_wire, project_id, role_kind, scope_id, state,
261
- holder_agent_id, previous_holder_agent_id, epoch, activation_seq,
262
- activation_id, activation_deadline_ms, holder_lease_expires_ms,
263
- charter_version, charter_acked_epoch, retry_count, next_probe_ms,
264
- blocked_reason, last_transition_ms, last_reason, version
265
- ) VALUES (
266
- @role_key_wire, @project_id, @role_kind, @scope_id, @state,
267
- @holder_agent_id, @previous_holder_agent_id, @epoch, @activation_seq,
268
- @activation_id, @activation_deadline_ms, @holder_lease_expires_ms,
269
- @charter_version, @charter_acked_epoch, @retry_count, @next_probe_ms,
270
- @blocked_reason, @last_transition_ms, @last_reason, @version
271
- )`),
272
- updateRoleReservation: db.prepare(`
273
- UPDATE role_registry SET
274
- state=@state,
275
- holder_agent_id=@holder_agent_id,
276
- previous_holder_agent_id=@previous_holder_agent_id,
277
- epoch=@epoch,
278
- activation_seq=@activation_seq,
279
- activation_id=@activation_id,
280
- activation_deadline_ms=@activation_deadline_ms,
281
- holder_lease_expires_ms=@holder_lease_expires_ms,
282
- charter_version=@charter_version,
283
- charter_acked_epoch=@charter_acked_epoch,
284
- retry_count=@retry_count,
285
- next_probe_ms=@next_probe_ms,
286
- blocked_reason=@blocked_reason,
287
- last_transition_ms=@last_transition_ms,
288
- last_reason=@last_reason,
289
- version=version + 1
290
- WHERE role_key_wire=@role_key_wire
291
- AND version=@expected_version
292
- AND epoch=@expected_epoch`),
293
- compareAndSetRole: db.prepare(`
294
- UPDATE role_registry SET
295
- state=@state,
296
- holder_agent_id=@holder_agent_id,
297
- previous_holder_agent_id=@previous_holder_agent_id,
298
- activation_id=@activation_id,
299
- activation_deadline_ms=@activation_deadline_ms,
300
- holder_lease_expires_ms=@holder_lease_expires_ms,
301
- charter_version=@charter_version,
302
- charter_acked_epoch=@charter_acked_epoch,
303
- retry_count=@retry_count,
304
- next_probe_ms=@next_probe_ms,
305
- blocked_reason=@blocked_reason,
306
- last_transition_ms=@last_transition_ms,
307
- last_reason=@last_reason,
308
- version=version + 1
309
- WHERE role_key_wire=@role_key_wire
310
- AND epoch=@expected_epoch
311
- AND activation_seq=@expected_activation_seq
312
- AND activation_id IS @expected_activation_id
313
- AND version=@expected_version`),
314
- listRoles: db.prepare("SELECT * FROM role_registry ORDER BY role_key_wire"),
315
- expiredHolderRoles: db.prepare(`
316
- SELECT * FROM role_registry
317
- WHERE holder_agent_id IS NOT NULL
318
- AND (holder_lease_expires_ms IS NULL OR holder_lease_expires_ms <= ?)
319
- ORDER BY role_key_wire`),
320
- listRoleCandidates: db.prepare(`
321
- SELECT * FROM role_candidates
322
- WHERE role_key_wire = ?
323
- ORDER BY priority DESC, updated_at_ms DESC, agent_id`),
324
- getRoleCandidate: db.prepare(`
325
- SELECT * FROM role_candidates
326
- WHERE role_key_wire = ? AND agent_id = ?`),
327
- upsertRoleCandidate: db.prepare(`
328
- INSERT INTO role_candidates (
329
- role_key_wire, agent_id, source, priority, transport_locators_json,
330
- consecutive_probe_failures, excluded_until_ms, last_probe_ms,
331
- last_probe_code, updated_at_ms
332
- ) VALUES (
333
- @role_key_wire, @agent_id, @source, @priority,
334
- @transport_locators_json, @consecutive_probe_failures,
335
- @excluded_until_ms, @last_probe_ms, @last_probe_code, @updated_at_ms
336
- )
337
- ON CONFLICT(role_key_wire, agent_id) DO UPDATE SET
338
- source=excluded.source,
339
- priority=excluded.priority,
340
- transport_locators_json=excluded.transport_locators_json,
341
- consecutive_probe_failures=excluded.consecutive_probe_failures,
342
- excluded_until_ms=excluded.excluded_until_ms,
343
- last_probe_ms=excluded.last_probe_ms,
344
- last_probe_code=excluded.last_probe_code,
345
- updated_at_ms=excluded.updated_at_ms`),
346
- updateCandidateReachability: db.prepare(`
347
- UPDATE role_candidates SET
348
- consecutive_probe_failures=@consecutive_probe_failures,
349
- excluded_until_ms=@excluded_until_ms,
350
- last_probe_ms=@last_probe_ms,
351
- last_probe_code=@last_probe_code,
352
- updated_at_ms=@updated_at_ms
353
- WHERE role_key_wire=@role_key_wire AND agent_id=@agent_id`),
354
- pendingRoleMessages: db.prepare(`
355
- SELECT * FROM messages
356
- WHERE role_key_wire=?
357
- AND status IN ('queued','delivered')
358
- AND expires_at_ms > ?
359
- ORDER BY priority DESC, created_at_ms ASC, id`),
360
- recoverExpiredRole: db.prepare(`
361
- UPDATE role_registry SET
362
- state='electable',
363
- previous_holder_agent_id=holder_agent_id,
364
- holder_agent_id=NULL,
365
- epoch=epoch + 1,
366
- activation_seq=activation_seq + 1,
367
- activation_id=NULL,
368
- activation_deadline_ms=NULL,
369
- holder_lease_expires_ms=NULL,
370
- charter_acked_epoch=NULL,
371
- blocked_reason=NULL,
372
- last_transition_ms=@now,
373
- last_reason='restart_holder_lease_expired',
374
- version=version + 1
375
- WHERE role_key_wire=@role_key_wire
376
- AND version=@expected_version
377
- AND epoch=@expected_epoch`),
378
- recoverLiveRole: db.prepare(`
379
- UPDATE role_registry SET
380
- state='elected-probing',
381
- epoch=epoch + 1,
382
- activation_seq=activation_seq + 1,
383
- activation_id=@activation_id,
384
- activation_deadline_ms=NULL,
385
- charter_acked_epoch=NULL,
386
- blocked_reason=NULL,
387
- last_transition_ms=@now,
388
- last_reason='restart_reprobe_required',
389
- version=version + 1
390
- WHERE role_key_wire=@role_key_wire
391
- AND version=@expected_version
392
- AND epoch=@expected_epoch`),
393
- recoverOrphanedActivation: db.prepare(`
394
- UPDATE role_registry SET
395
- state='electable',
396
- epoch=epoch + 1,
397
- activation_seq=activation_seq + 1,
398
- activation_id=NULL,
399
- activation_deadline_ms=NULL,
400
- charter_acked_epoch=NULL,
401
- last_transition_ms=@now,
402
- last_reason='restart_activation_fenced',
403
- version=version + 1
404
- WHERE role_key_wire=@role_key_wire
405
- AND version=@expected_version
406
- AND epoch=@expected_epoch`),
407
-
408
- insertAuditMessage: db.prepare(`
409
- INSERT INTO messages (id, type, from_agent, to_agent, topic, priority, ttl_ms, created_at_ms, expires_at_ms, correlation_id, trace_id, payload_json, status, role_key_wire)
410
- VALUES (@id, @type, @from_agent, @to_agent, @topic, @priority, @ttl_ms, @created_at_ms, @expires_at_ms, @correlation_id, @trace_id, @payload_json, @status, @role_key_wire)`),
411
- getMsg: db.prepare("SELECT * FROM messages WHERE id=?"),
412
- getResponse: db.prepare(
413
- "SELECT * FROM messages WHERE correlation_id=? AND type='response' ORDER BY created_at_ms DESC LIMIT 1",
414
- ),
415
- getMsgsByTrace: db.prepare(
416
- "SELECT * FROM messages WHERE trace_id=? ORDER BY created_at_ms",
417
- ),
418
- setMsgStatus: db.prepare("UPDATE messages SET status=? WHERE id=?"),
419
- recentAgentMessages: db.prepare(`
420
- SELECT * FROM messages
421
- WHERE to_agent=?
422
- ORDER BY created_at_ms DESC
423
- LIMIT ?`),
424
- recentAgentMessagesWithTopics: db.prepare(`
425
- SELECT * FROM messages
426
- WHERE to_agent=?
427
- OR (
428
- substr(to_agent, 1, 6)='topic:'
429
- AND topic IN (SELECT value FROM json_each(?))
430
- )
431
- ORDER BY created_at_ms DESC
432
- LIMIT ?`),
433
-
434
- insertHR: db.prepare(`
435
- INSERT INTO human_requests (request_id, requester_agent, kind, prompt, schema_json, state, deadline_ms, default_action, correlation_id, trace_id, response_json)
436
- VALUES (@request_id, @requester_agent, @kind, @prompt, @schema_json, @state, @deadline_ms, @default_action, @correlation_id, @trace_id, @response_json)`),
437
- getHR: db.prepare("SELECT * FROM human_requests WHERE request_id=?"),
438
- updateHR: db.prepare(
439
- "UPDATE human_requests SET state=?, response_json=? WHERE request_id=?",
440
- ),
441
- pendingHR: db.prepare("SELECT * FROM human_requests WHERE state='pending'"),
442
-
443
- insertDL: db.prepare(
444
- "INSERT INTO dead_letters (message_id, reason, failed_at_ms, last_error) VALUES (?,?,?,?) ON CONFLICT(message_id) DO NOTHING",
445
- ),
446
- getDL: db.prepare(
447
- "SELECT * FROM dead_letters ORDER BY failed_at_ms DESC LIMIT ?",
448
- ),
449
-
450
- insertAssign: db.prepare(`
451
- INSERT INTO assign_jobs (
452
- job_id, supervisor_agent, worker_agent, topic, task, payload_json,
453
- status, attempt, retry_count, max_retries, priority, ttl_ms, timeout_ms, deadline_ms,
454
- trace_id, correlation_id, last_message_id, result_json, error_json,
455
- created_at_ms, updated_at_ms, started_at_ms, dispatched_at_ms, completed_at_ms, last_retry_at_ms
456
- ) VALUES (
457
- @job_id, @supervisor_agent, @worker_agent, @topic, @task, @payload_json,
458
- @status, @attempt, @retry_count, @max_retries, @priority, @ttl_ms, @timeout_ms, @deadline_ms,
459
- @trace_id, @correlation_id, @last_message_id, @result_json, @error_json,
460
- @created_at_ms, @updated_at_ms, @started_at_ms, @dispatched_at_ms, @completed_at_ms, @last_retry_at_ms
461
- )`),
462
- getAssign: db.prepare("SELECT * FROM assign_jobs WHERE job_id = ?"),
463
- updateAssign: db.prepare(`
464
- UPDATE assign_jobs SET
465
- supervisor_agent=@supervisor_agent,
466
- worker_agent=@worker_agent,
467
- topic=@topic,
468
- task=@task,
469
- payload_json=@payload_json,
470
- status=@status,
471
- attempt=@attempt,
472
- retry_count=@retry_count,
473
- max_retries=@max_retries,
474
- priority=@priority,
475
- ttl_ms=@ttl_ms,
476
- timeout_ms=@timeout_ms,
477
- deadline_ms=@deadline_ms,
478
- trace_id=@trace_id,
479
- correlation_id=@correlation_id,
480
- last_message_id=@last_message_id,
481
- result_json=@result_json,
482
- error_json=@error_json,
483
- updated_at_ms=@updated_at_ms,
484
- started_at_ms=@started_at_ms,
485
- dispatched_at_ms=@dispatched_at_ms,
486
- completed_at_ms=@completed_at_ms,
487
- last_retry_at_ms=@last_retry_at_ms
488
- WHERE job_id=@job_id`),
489
-
490
- findExpired: db.prepare(
491
- "SELECT * FROM messages WHERE status IN ('queued', 'delivered') AND expires_at_ms <= ?",
492
- ),
493
- claimExpired: db.prepare(
494
- "UPDATE messages SET status='dead_letter' WHERE id=? AND status IN ('queued', 'delivered') AND expires_at_ms <= ?",
495
- ),
496
- claimDeadLetter: db.prepare(
497
- "UPDATE messages SET status='dead_letter' WHERE id=? AND status != 'dead_letter'",
498
- ),
499
- urgentDepth: db.prepare(
500
- "SELECT COUNT(*) as cnt FROM messages WHERE status='queued' AND priority >= 7",
501
- ),
502
- normalDepth: db.prepare(
503
- "SELECT COUNT(*) as cnt FROM messages WHERE status='queued' AND priority < 7",
504
- ),
505
- onlineCount: db.prepare(
506
- "SELECT COUNT(*) as cnt FROM agents WHERE status='online'",
507
- ),
508
- msgCount: db.prepare("SELECT COUNT(*) as cnt FROM messages"),
509
- dlqDepth: db.prepare("SELECT COUNT(*) as cnt FROM dead_letters"),
510
- ackedRecent: db.prepare(
511
- "SELECT COUNT(*) as cnt FROM messages WHERE status='acked' AND created_at_ms > ? - 300000",
512
- ),
513
- assignCountByStatus: db.prepare(
514
- "SELECT COUNT(*) as cnt FROM assign_jobs WHERE status = ?",
515
- ),
516
- activeAssignCount: db.prepare(
517
- "SELECT COUNT(*) as cnt FROM assign_jobs WHERE status IN ('queued','running')",
518
- ),
519
-
520
- // reflexion
521
- insertReflexion: db.prepare(`
522
- INSERT INTO reflexion_entries (id, type, error_pattern, error_message, context_json, solution, solution_code, adaptive_state_json, confidence, hit_count, success_count, last_hit_ms, created_at_ms, updated_at_ms)
523
- VALUES (@id, @type, @error_pattern, @error_message, @context_json, @solution, @solution_code, @adaptive_state_json, @confidence, @hit_count, @success_count, @last_hit_ms, @created_at_ms, @updated_at_ms)`),
524
- getReflexionById: db.prepare(
525
- "SELECT * FROM reflexion_entries WHERE id = ?",
526
- ),
527
- findReflexionExact: db.prepare(
528
- "SELECT * FROM reflexion_entries WHERE error_pattern = ? ORDER BY confidence DESC",
529
- ),
530
- findReflexionLike: db.prepare(
531
- "SELECT * FROM reflexion_entries WHERE error_pattern LIKE ? ESCAPE '\\' ORDER BY confidence DESC LIMIT 10",
532
- ),
533
- updateReflexionHitSuccess: db.prepare(
534
- "UPDATE reflexion_entries SET hit_count = hit_count + 1, success_count = success_count + 1, last_hit_ms = ?, updated_at_ms = ? WHERE id = ?",
535
- ),
536
- updateReflexionHitOnly: db.prepare(
537
- "UPDATE reflexion_entries SET hit_count = hit_count + 1, last_hit_ms = ?, updated_at_ms = ? WHERE id = ?",
538
- ),
539
- updateReflexionConfidence: db.prepare(
540
- "UPDATE reflexion_entries SET confidence = ?, updated_at_ms = ? WHERE id = ?",
541
- ),
542
- pruneReflexionEntries: db.prepare(
543
- "DELETE FROM reflexion_entries WHERE updated_at_ms < ? AND confidence < ?",
544
- ),
545
- listReflexionEntries: db.prepare(
546
- "SELECT * FROM reflexion_entries ORDER BY confidence DESC, updated_at_ms DESC",
547
- ),
548
- deleteReflexionEntry: db.prepare(
549
- "DELETE FROM reflexion_entries WHERE id = ?",
550
- ),
551
- };
552
-
553
- const assignStatusListeners = new Set();
554
-
555
- function buildAssignCallbackEvent(row) {
556
- return {
557
- job_id: row.job_id,
558
- status: row.status,
559
- result: row.result ?? row.error ?? null,
560
- timestamp: new Date(row.updated_at_ms || Date.now()).toISOString(),
561
- };
562
- }
563
-
564
- function notifyAssignStatusListeners(row) {
565
- const event = buildAssignCallbackEvent(row);
566
- for (const listener of Array.from(assignStatusListeners)) {
567
- try {
568
- listener(event, row);
569
- } catch {}
570
- }
571
- }
572
-
573
- function clampMaxMessages(value, fallback = 20) {
574
- const num = Number(value);
575
- if (!Number.isFinite(num)) return fallback;
576
- return Math.max(1, Math.min(Math.trunc(num), 100));
577
- }
578
-
579
- function clampPriority(value, fallback = 5) {
580
- const num = Number(value);
581
- if (!Number.isFinite(num)) return fallback;
582
- return Math.max(1, Math.min(Math.trunc(num), 9));
583
- }
584
-
585
- function roleIdentity(input) {
586
- const roleKeyWire = input?.role_key_wire ?? input?.roleKeyWire;
587
- const decoded = decodeRoleKey(roleKeyWire);
588
- const identity = {
589
- role_key_wire: roleKeyWire,
590
- project_id: decoded.project_id,
591
- role_kind: decoded.role_kind,
592
- scope_id: decoded.scope_id ?? "",
593
- };
594
- for (const field of ["project_id", "role_kind", "scope_id"]) {
595
- if (Object.hasOwn(input, field) && input[field] !== identity[field]) {
596
- throw new Error(`role key identity mismatch: ${field}`);
597
- }
598
- }
599
- return identity;
600
- }
601
-
602
- function roleFailureReason(current, expected) {
603
- if (!current) return "not_found";
604
- if (current.version !== expected.version) return "stale_version";
605
- if (current.epoch !== expected.epoch) return "stale_epoch";
606
- if (current.activation_seq !== expected.activation_seq) {
607
- return "stale_activation_seq";
608
- }
609
- if (current.activation_id !== expected.activation_id) {
610
- return "stale_activation_id";
611
- }
612
- return "cas_conflict";
613
- }
614
-
615
- const reserveRole = db.transaction((input) => {
616
- const identity = roleIdentity(input);
617
- const current = S.getRole.get(identity.role_key_wire) ?? null;
618
- const expectedVersion =
619
- input.expected_version ?? input.expectedVersion ?? current?.version;
620
- const expectedEpoch =
621
- input.expected_epoch ?? input.expectedEpoch ?? current?.epoch;
622
-
623
- if (current && expectedVersion !== current.version) {
624
- return { ok: false, reason: "stale_version", role: current };
625
- }
626
- if (current && expectedEpoch !== current.epoch) {
627
- return { ok: false, reason: "stale_epoch", role: current };
628
- }
629
-
630
- const nextEpoch =
631
- input.epoch ?? input.next_epoch ?? (current ? current.epoch + 1 : 0);
632
- if (current && nextEpoch !== current.epoch + 1) {
633
- return { ok: false, reason: "stale_epoch", role: current };
634
- }
635
- const nextActivationSeq =
636
- input.activation_seq ??
637
- input.next_activation_seq ??
638
- (current ? current.activation_seq + 1 : 0);
639
- if (current && nextActivationSeq !== current.activation_seq + 1) {
640
- return { ok: false, reason: "stale_activation_seq", role: current };
641
- }
642
-
643
- const state =
644
- input.state ?? (input.holder_agent_id ? "elected-probing" : "electable");
645
- if (!ROLE_REACHABILITY_STATES.includes(state)) {
646
- throw new Error(`invalid role reachability state: ${state}`);
647
- }
648
- const row = {
649
- ...identity,
650
- state,
651
- holder_agent_id: input.holder_agent_id ?? null,
652
- previous_holder_agent_id:
653
- input.previous_holder_agent_id ??
654
- (current?.holder_agent_id !== input.holder_agent_id
655
- ? (current?.holder_agent_id ?? null)
656
- : (current?.previous_holder_agent_id ?? null)),
657
- epoch: nextEpoch,
658
- activation_seq: nextActivationSeq,
659
- activation_id:
660
- input.activation_id ??
661
- (input.holder_agent_id ? uuidv7() : (current?.activation_id ?? null)),
662
- activation_deadline_ms:
663
- input.activation_deadline_ms ?? current?.activation_deadline_ms ?? null,
664
- holder_lease_expires_ms:
665
- input.holder_lease_expires_ms ??
666
- current?.holder_lease_expires_ms ??
667
- null,
668
- charter_version:
669
- input.charter_version ?? current?.charter_version ?? null,
670
- charter_acked_epoch:
671
- input.charter_acked_epoch ?? current?.charter_acked_epoch ?? null,
672
- retry_count: input.retry_count ?? current?.retry_count ?? 0,
673
- next_probe_ms: input.next_probe_ms ?? current?.next_probe_ms ?? null,
674
- blocked_reason:
675
- input.blocked_reason === undefined
676
- ? (current?.blocked_reason ?? null)
677
- : input.blocked_reason,
678
- last_transition_ms: input.last_transition_ms ?? Date.now(),
679
- last_reason: input.last_reason ?? "role_reserved",
680
- version: 0,
681
- expected_version: current?.version,
682
- expected_epoch: current?.epoch,
683
- };
684
-
685
- if (!current) {
686
- S.insertRoleReservation.run(row);
687
- } else if (S.updateRoleReservation.run(row).changes !== 1) {
688
- const latest = S.getRole.get(identity.role_key_wire) ?? null;
689
- return {
690
- ok: false,
691
- reason: roleFailureReason(latest, {
692
- version: current.version,
693
- epoch: current.epoch,
694
- activation_seq: current.activation_seq,
695
- activation_id: current.activation_id,
696
- }),
697
- role: latest,
698
- };
699
- }
700
- return { ok: true, role: S.getRole.get(identity.role_key_wire) };
701
- });
702
-
703
- const recoverRegistry = db.transaction((now) => {
704
- for (const role of S.listRoles.all()) {
705
- const guard = {
706
- role_key_wire: role.role_key_wire,
707
- expected_version: role.version,
708
- expected_epoch: role.epoch,
709
- now,
710
- };
711
- if (
712
- role.holder_agent_id &&
713
- (role.holder_lease_expires_ms == null ||
714
- role.holder_lease_expires_ms <= now)
715
- ) {
716
- S.recoverExpiredRole.run(guard);
717
- } else if (role.holder_agent_id) {
718
- S.recoverLiveRole.run({ ...guard, activation_id: uuidv7() });
719
- } else if (role.activation_id || role.state === "elected-probing") {
720
- S.recoverOrphanedActivation.run(guard);
721
- }
722
- }
723
-
724
- const roles = S.listRoles.all();
725
- return {
726
- roles,
727
- candidates: roles.flatMap((role) =>
728
- S.listRoleCandidates.all(role.role_key_wire).map(parseRoleCandidateRow),
729
- ),
730
- pending_messages: roles.flatMap((role) =>
731
- S.pendingRoleMessages.all(role.role_key_wire, now).map(parseMessageRow),
732
- ),
733
- };
734
- });
735
-
736
- const store = {
737
- db,
738
- type: "sqlite",
739
- fingerprint: storeFingerprint(dbPath),
740
- uuidv7,
741
-
742
- close() {
743
- db.close();
744
- },
745
-
746
- registerAgent({
747
- agent_id,
748
- cli,
749
- pid,
750
- capabilities = [],
751
- topics = [],
752
- heartbeat_ttl_ms = 30000,
753
- metadata = {},
754
- }) {
755
- const now = Date.now();
756
- const leaseExpires = now + heartbeat_ttl_ms;
757
- const leaseId = uuidv7();
758
- const write = S.upsertAgent.run({
759
- agent_id,
760
- cli,
761
- pid: pid ?? null,
762
- capabilities_json: JSON.stringify(capabilities),
763
- topics_json: JSON.stringify(topics),
764
- last_seen_ms: now,
765
- lease_expires_ms: leaseExpires,
766
- status: "online",
767
- metadata_json: JSON.stringify(metadata),
768
- });
769
- const persisted = parseAgentRow(S.getAgent.get(agent_id));
770
- return {
771
- agent_id,
772
- lease_id: leaseId,
773
- presence_generation: persisted?.presence_generation ?? null,
774
- lease_expires_ms: leaseExpires,
775
- server_time_ms: now,
776
- effective: {
777
- updated: write.changes > 0,
778
- online:
779
- persisted?.status === "online" &&
780
- Number(persisted.lease_expires_ms) === leaseExpires,
781
- status: persisted?.status ?? null,
782
- last_seen_ms: persisted?.last_seen_ms ?? null,
783
- lease_expires_ms: persisted?.lease_expires_ms ?? null,
784
- store_type: "sqlite",
785
- db_path: dbPath ?? null,
786
- store_fingerprint: storeFingerprint(dbPath),
787
- },
788
- };
789
- },
790
-
791
- getAgent(id) {
792
- return parseAgentRow(S.getAgent.get(id));
793
- },
794
-
795
- refreshLease(agentId, ttlMs = 30000, presenceGeneration) {
796
- const now = Date.now();
797
- const write =
798
- presenceGeneration == null
799
- ? S.heartbeatWithoutGeneration.run(now, now + ttlMs, agentId)
800
- : S.heartbeat.run(now, now + ttlMs, agentId, presenceGeneration);
801
- return {
802
- agent_id: agentId,
803
- lease_expires_ms: now + ttlMs,
804
- server_time_ms: now,
805
- effective: {
806
- updated: write.changes > 0,
807
- store_type: "sqlite",
808
- db_path: dbPath ?? null,
809
- store_fingerprint: storeFingerprint(dbPath),
810
- },
811
- };
812
- },
813
-
814
- updateAgentTopics(agentId, topics = []) {
815
- const now = Date.now();
816
- return (
817
- S.setAgentTopics.run(JSON.stringify(topics), now, agentId).changes > 0
818
- );
819
- },
820
-
821
- listOnlineAgents() {
822
- return S.onlineAgents.all().map(parseAgentRow);
823
- },
824
-
825
- listAllAgents() {
826
- return S.allAgents.all().map(parseAgentRow);
827
- },
828
-
829
- getAgentsByTopic(topic) {
830
- return S.agentsByTopic.all(topic).map(parseAgentRow);
831
- },
832
-
833
- sweepStaleAgents() {
834
- const now = Date.now();
835
- const candidates = S.allAgents.all();
836
- let stale = 0;
837
- let offline = 0;
838
- for (const candidate of candidates) {
839
- if (candidate.status === "online" && candidate.lease_expires_ms < now) {
840
- stale += S.markStale.run(
841
- candidate.agent_id,
842
- candidate.presence_generation,
843
- now,
844
- ).changes;
845
- } else if (
846
- candidate.status === "stale" &&
847
- candidate.lease_expires_ms < now - 300000
848
- ) {
849
- offline += S.markOffline.run(
850
- candidate.agent_id,
851
- candidate.presence_generation,
852
- now,
853
- ).changes;
854
- }
855
- }
856
- return {
857
- stale,
858
- offline,
859
- };
860
- },
861
-
862
- updateAgentStatus(agentId, status, presenceGeneration) {
863
- return (
864
- (presenceGeneration == null
865
- ? S.setAgentStatusWithoutGeneration.run(status, agentId)
866
- : S.setAgentStatus.run(status, agentId, presenceGeneration)
867
- ).changes > 0
868
- );
869
- },
870
-
871
- getRole(roleKeyWire) {
872
- decodeRoleKey(roleKeyWire);
873
- return S.getRole.get(roleKeyWire) ?? null;
874
- },
875
-
876
- upsertRoleReservation(input) {
877
- return reserveRole.immediate(input);
878
- },
879
-
880
- compareAndSetRole(input) {
881
- const identity = roleIdentity(input);
882
- const current = S.getRole.get(identity.role_key_wire) ?? null;
883
- if (!current) return { ok: false, reason: "not_found", role: null };
884
-
885
- const hasExpectedActivationSeq =
886
- Object.hasOwn(input, "expected_activation_seq") ||
887
- Object.hasOwn(input, "expectedActivationSeq") ||
888
- Object.hasOwn(input, "activation_seq");
889
- const hasExpectedActivationId =
890
- Object.hasOwn(input, "expected_activation_id") ||
891
- Object.hasOwn(input, "expectedActivationId") ||
892
- Object.hasOwn(input, "activation_id");
893
- const expected = {
894
- version:
895
- input.expected_version ?? input.expectedVersion ?? input.version,
896
- epoch: input.expected_epoch ?? input.expectedEpoch ?? input.epoch,
897
- activation_seq:
898
- input.expected_activation_seq ??
899
- input.expectedActivationSeq ??
900
- input.activation_seq,
901
- activation_id: Object.hasOwn(input, "expected_activation_id")
902
- ? input.expected_activation_id
903
- : Object.hasOwn(input, "expectedActivationId")
904
- ? input.expectedActivationId
905
- : input.activation_id,
906
- };
907
- if (
908
- expected.version == null ||
909
- expected.epoch == null ||
910
- !hasExpectedActivationSeq ||
911
- !hasExpectedActivationId
912
- ) {
913
- return { ok: false, reason: "invalid_cas", role: current };
914
- }
915
-
916
- const reason = roleFailureReason(current, expected);
917
- if (reason !== "cas_conflict") {
918
- return { ok: false, reason, role: current };
919
- }
920
-
921
- const patch = input.patch ?? input.changes ?? {};
922
- const state = patch.state ?? current.state;
923
- if (!ROLE_REACHABILITY_STATES.includes(state)) {
924
- throw new Error(`invalid role reachability state: ${state}`);
925
- }
926
- const next = {
927
- role_key_wire: identity.role_key_wire,
928
- expected_version: expected.version,
929
- expected_epoch: expected.epoch,
930
- expected_activation_seq: expected.activation_seq,
931
- expected_activation_id: expected.activation_id,
932
- state,
933
- holder_agent_id:
934
- patch.holder_agent_id === undefined
935
- ? current.holder_agent_id
936
- : patch.holder_agent_id,
937
- previous_holder_agent_id:
938
- patch.previous_holder_agent_id === undefined
939
- ? current.previous_holder_agent_id
940
- : patch.previous_holder_agent_id,
941
- activation_id:
942
- patch.activation_id === undefined
943
- ? current.activation_id
944
- : patch.activation_id,
945
- activation_deadline_ms:
946
- patch.activation_deadline_ms === undefined
947
- ? current.activation_deadline_ms
948
- : patch.activation_deadline_ms,
949
- holder_lease_expires_ms:
950
- patch.holder_lease_expires_ms === undefined
951
- ? current.holder_lease_expires_ms
952
- : patch.holder_lease_expires_ms,
953
- charter_version:
954
- patch.charter_version === undefined
955
- ? current.charter_version
956
- : patch.charter_version,
957
- charter_acked_epoch:
958
- patch.charter_acked_epoch === undefined
959
- ? current.charter_acked_epoch
960
- : patch.charter_acked_epoch,
961
- retry_count: patch.retry_count ?? current.retry_count,
962
- next_probe_ms:
963
- patch.next_probe_ms === undefined
964
- ? current.next_probe_ms
965
- : patch.next_probe_ms,
966
- blocked_reason:
967
- patch.blocked_reason === undefined
968
- ? current.blocked_reason
969
- : patch.blocked_reason,
970
- last_transition_ms: patch.last_transition_ms ?? Date.now(),
971
- last_reason: patch.last_reason ?? current.last_reason,
972
- };
973
- if (S.compareAndSetRole.run(next).changes !== 1) {
974
- const latest = S.getRole.get(identity.role_key_wire) ?? null;
975
- return {
976
- ok: false,
977
- reason: roleFailureReason(latest, expected),
978
- role: latest,
979
- };
980
- }
981
- return { ok: true, role: S.getRole.get(identity.role_key_wire) };
982
- },
983
-
984
- listRoleKeys(filter = {}) {
985
- const clauses = [];
986
- const params = {};
987
- for (const field of ["project_id", "role_kind", "scope_id", "state"]) {
988
- if (filter[field] !== undefined) {
989
- clauses.push(`${field}=@${field}`);
990
- params[field] = filter[field];
991
- }
992
- }
993
- if (Array.isArray(filter.states) && filter.states.length > 0) {
994
- clauses.push(
995
- `state IN (${filter.states.map((_, index) => `@state_${index}`).join(",")})`,
996
- );
997
- filter.states.forEach((state, index) => {
998
- params[`state_${index}`] = state;
999
- });
1000
- }
1001
- const where = clauses.length ? ` WHERE ${clauses.join(" AND ")}` : "";
1002
- return db
1003
- .prepare(
1004
- `SELECT role_key_wire FROM role_registry${where} ORDER BY role_key_wire`,
1005
- )
1006
- .pluck()
1007
- .all(params);
1008
- },
1009
-
1010
- listRolesWithExpiredHolder(now) {
1011
- return S.expiredHolderRoles.all(now);
1012
- },
1013
-
1014
- listRoleCandidates(roleKeyWire) {
1015
- decodeRoleKey(roleKeyWire);
1016
- return S.listRoleCandidates.all(roleKeyWire).map(parseRoleCandidateRow);
1017
- },
1018
-
1019
- upsertRoleCandidate(input) {
1020
- const { role_key_wire } = roleIdentity(input);
1021
- const current = S.getRoleCandidate.get(role_key_wire, input.agent_id);
1022
- S.upsertRoleCandidate.run({
1023
- role_key_wire,
1024
- agent_id: input.agent_id,
1025
- source: input.source ?? current?.source ?? "operator",
1026
- priority: input.priority ?? current?.priority ?? 0,
1027
- transport_locators_json: JSON.stringify(
1028
- input.transport_locators ??
1029
- parseJson(current?.transport_locators_json, []),
1030
- ),
1031
- consecutive_probe_failures:
1032
- input.consecutive_probe_failures ??
1033
- current?.consecutive_probe_failures ??
1034
- 0,
1035
- excluded_until_ms:
1036
- input.excluded_until_ms === undefined
1037
- ? (current?.excluded_until_ms ?? null)
1038
- : input.excluded_until_ms,
1039
- last_probe_ms:
1040
- input.last_probe_ms === undefined
1041
- ? (current?.last_probe_ms ?? null)
1042
- : input.last_probe_ms,
1043
- last_probe_code:
1044
- input.last_probe_code === undefined
1045
- ? (current?.last_probe_code ?? null)
1046
- : input.last_probe_code,
1047
- updated_at_ms: input.updated_at_ms ?? Date.now(),
1048
- });
1049
- return parseRoleCandidateRow(
1050
- S.getRoleCandidate.get(role_key_wire, input.agent_id),
1051
- );
1052
- },
1053
-
1054
- updateCandidateReachability(input) {
1055
- const { role_key_wire } = roleIdentity(input);
1056
- const current = S.getRoleCandidate.get(role_key_wire, input.agent_id);
1057
- if (!current) return { ok: false, reason: "not_found" };
1058
-
1059
- let failures =
1060
- input.consecutive_probe_failures ?? current.consecutive_probe_failures;
1061
- if (
1062
- input.probe_failed === true ||
1063
- input.increment_probe_failures === true
1064
- ) {
1065
- failures = current.consecutive_probe_failures + 1;
1066
- } else if (input.probe_succeeded === true) {
1067
- failures = 0;
1068
- }
1069
- S.updateCandidateReachability.run({
1070
- role_key_wire,
1071
- agent_id: input.agent_id,
1072
- consecutive_probe_failures: failures,
1073
- excluded_until_ms:
1074
- input.excluded_until_ms === undefined
1075
- ? current.excluded_until_ms
1076
- : input.excluded_until_ms,
1077
- last_probe_ms: input.last_probe_ms ?? Date.now(),
1078
- last_probe_code:
1079
- input.last_probe_code === undefined
1080
- ? current.last_probe_code
1081
- : input.last_probe_code,
1082
- updated_at_ms: input.updated_at_ms ?? Date.now(),
1083
- });
1084
- return {
1085
- ok: true,
1086
- candidate: parseRoleCandidateRow(
1087
- S.getRoleCandidate.get(role_key_wire, input.agent_id),
1088
- ),
1089
- };
1090
- },
1091
-
1092
- listPendingRoleMessages(roleKeyWire, now) {
1093
- decodeRoleKey(roleKeyWire);
1094
- return S.pendingRoleMessages.all(roleKeyWire, now).map(parseMessageRow);
1095
- },
1096
-
1097
- recoverRoleRegistry(now) {
1098
- return recoverRegistry.immediate(now);
1099
- },
1100
-
1101
- auditLog({
1102
- type,
1103
- from,
1104
- to,
1105
- topic,
1106
- priority = 5,
1107
- ttl_ms = 300000,
1108
- payload = {},
1109
- trace_id,
1110
- correlation_id,
1111
- status = "queued",
1112
- role_key_wire = null,
1113
- }) {
1114
- const now = Date.now();
1115
- const row = {
1116
- id: uuidv7(),
1117
- type,
1118
- from_agent: from,
1119
- to_agent: to,
1120
- topic,
1121
- priority,
1122
- ttl_ms,
1123
- created_at_ms: now,
1124
- expires_at_ms: now + ttl_ms,
1125
- correlation_id: correlation_id || uuidv7(),
1126
- trace_id: trace_id || uuidv7(),
1127
- payload_json: JSON.stringify(payload),
1128
- status,
1129
- role_key_wire,
1130
- };
1131
- S.insertAuditMessage.run(row);
1132
- return { ...row, payload };
1133
- },
1134
-
1135
- // 하위 호환: 기존 enqueueMessage 호출은 auditLog로 위임한다.
1136
- enqueueMessage(args) {
1137
- return store.auditLog(args);
1138
- },
1139
-
1140
- getMessage(id) {
1141
- return parseMessageRow(S.getMsg.get(id));
1142
- },
1143
-
1144
- getResponseByCorrelation(cid) {
1145
- return parseMessageRow(S.getResponse.get(cid));
1146
- },
1147
-
1148
- getMessagesByTrace(tid) {
1149
- return S.getMsgsByTrace.all(tid).map(parseMessageRow);
1150
- },
1151
-
1152
- updateMessageStatus(id, status) {
1153
- return S.setMsgStatus.run(status, id).changes > 0;
1154
- },
1155
-
1156
- getAuditMessagesForAgent(
1157
- agentId,
1158
- { max_messages = 20, include_topics = null } = {},
1159
- ) {
1160
- const limit = clampMaxMessages(max_messages);
1161
- const topics =
1162
- Array.isArray(include_topics) && include_topics.length
1163
- ? include_topics
1164
- : store.getAgent(agentId)?.topics || [];
1165
-
1166
- const rows = topics.length
1167
- ? S.recentAgentMessagesWithTopics.all(
1168
- agentId,
1169
- JSON.stringify(topics),
1170
- limit,
1171
- )
1172
- : S.recentAgentMessages.all(agentId, limit);
1173
-
1174
- return rows.map(parseMessageRow);
1175
- },
1176
-
1177
- // 하위 호환: 실시간 수신함 대신 감사 로그 재생 결과를 반환한다.
1178
- deliverToAgent(messageId, agentId) {
1179
- return !!store.getMessage(messageId) && !!agentId;
1180
- },
1181
-
1182
- deliverToTopic(messageId, topic) {
1183
- void messageId;
1184
- return store.getAgentsByTopic(topic).length;
1185
- },
1186
-
1187
- pollForAgent(agentId, { max_messages = 20, include_topics = null } = {}) {
1188
- return store.getAuditMessagesForAgent(agentId, {
1189
- max_messages,
1190
- include_topics,
1191
- });
1192
- },
1193
-
1194
- ackMessages() {
1195
- return 0;
1196
- },
1197
-
1198
- insertHumanRequest({
1199
- requester_agent,
1200
- kind,
1201
- prompt,
1202
- requested_schema = {},
1203
- deadline_ms,
1204
- default_action,
1205
- correlation_id,
1206
- trace_id,
1207
- }) {
1208
- const requestId = uuidv7();
1209
- const now = Date.now();
1210
- const deadlineAt = now + deadline_ms;
1211
- S.insertHR.run({
1212
- request_id: requestId,
1213
- requester_agent,
1214
- kind,
1215
- prompt,
1216
- schema_json: JSON.stringify(requested_schema),
1217
- state: "pending",
1218
- deadline_ms: deadlineAt,
1219
- default_action,
1220
- correlation_id: correlation_id || uuidv7(),
1221
- trace_id: trace_id || uuidv7(),
1222
- response_json: null,
1223
- });
1224
- return {
1225
- request_id: requestId,
1226
- state: "pending",
1227
- deadline_ms: deadlineAt,
1228
- };
1229
- },
1230
-
1231
- getHumanRequest(id) {
1232
- return parseHumanRequestRow(S.getHR.get(id));
1233
- },
1234
-
1235
- updateHumanRequest(id, state, resp = null) {
1236
- return (
1237
- S.updateHR.run(state, resp ? JSON.stringify(resp) : null, id).changes >
1238
- 0
1239
- );
1240
- },
1241
-
1242
- getPendingHumanRequests() {
1243
- return S.pendingHR.all().map(parseHumanRequestRow);
1244
- },
1245
-
1246
- moveToDeadLetter(messageId, reason, lastError = null) {
1247
- const failedAt = Date.now();
1248
- return db.transaction(() => {
1249
- const changed = S.claimDeadLetter.run(messageId).changes;
1250
- if (!changed) return false;
1251
- S.insertDL.run(messageId, reason, failedAt, lastError);
1252
- return true;
1253
- })();
1254
- },
1255
-
1256
- getDeadLetters(limit = 50) {
1257
- return S.getDL.all(limit);
1258
- },
1259
-
1260
- createAssign({
1261
- job_id,
1262
- supervisor_agent,
1263
- worker_agent,
1264
- topic = "assign.job",
1265
- task = "",
1266
- payload = {},
1267
- status = "queued",
1268
- attempt = 1,
1269
- retry_count = 0,
1270
- max_retries = 0,
1271
- priority = 5,
1272
- ttl_ms = DEFAULT_ASSIGN_TTL_MS,
1273
- timeout_ms = DEFAULT_ASSIGN_TIMEOUT_MS,
1274
- deadline_ms,
1275
- dispatched_at_ms = null,
1276
- trace_id,
1277
- correlation_id,
1278
- last_message_id = null,
1279
- result = null,
1280
- error = null,
1281
- }) {
1282
- const now = Date.now();
1283
- const normalizedTimeout = clampDurationMs(
1284
- timeout_ms,
1285
- DEFAULT_ASSIGN_TIMEOUT_MS,
1286
- );
1287
- const row = {
1288
- job_id: job_id || uuidv7(),
1289
- supervisor_agent,
1290
- worker_agent,
1291
- topic: String(topic || "assign.job"),
1292
- task: String(task || ""),
1293
- payload_json: JSON.stringify(payload || {}),
1294
- status,
1295
- attempt: Math.max(1, Number(attempt) || 1),
1296
- retry_count: Math.max(0, Number(retry_count) || 0),
1297
- max_retries: Math.max(0, Number(max_retries) || 0),
1298
- priority: clampPriority(priority, 5),
1299
- ttl_ms: clampDurationMs(ttl_ms, normalizedTimeout),
1300
- timeout_ms: normalizedTimeout,
1301
- deadline_ms: Number.isFinite(Number(deadline_ms))
1302
- ? Math.trunc(Number(deadline_ms))
1303
- : now + normalizedTimeout,
1304
- trace_id: trace_id || uuidv7(),
1305
- correlation_id: correlation_id || uuidv7(),
1306
- last_message_id,
1307
- result_json: result == null ? null : JSON.stringify(result),
1308
- error_json: error == null ? null : JSON.stringify(error),
1309
- created_at_ms: now,
1310
- updated_at_ms: now,
1311
- started_at_ms: status === "running" ? now : null,
1312
- dispatched_at_ms:
1313
- dispatched_at_ms == null || !Number.isFinite(Number(dispatched_at_ms))
1314
- ? null
1315
- : Math.trunc(Number(dispatched_at_ms)),
1316
- completed_at_ms: ["succeeded", "failed", "timed_out"].includes(status)
1317
- ? now
1318
- : null,
1319
- last_retry_at_ms: retry_count > 0 ? now : null,
1320
- };
1321
- S.insertAssign.run(row);
1322
- const inserted = store.getAssign(row.job_id);
1323
- notifyAssignStatusListeners(inserted);
1324
- return inserted;
1325
- },
1326
-
1327
- getAssign(jobId) {
1328
- return parseAssignRow(S.getAssign.get(jobId));
1329
- },
1330
-
1331
- updateAssignStatus(jobId, status, patch = {}) {
1332
- const current = store.getAssign(jobId);
1333
- if (!current) return null;
1334
-
1335
- const now = Date.now();
1336
- const nextStatus = status || current.status;
1337
- const isTerminal = ["succeeded", "failed", "timed_out"].includes(
1338
- nextStatus,
1339
- );
1340
- const nextTimeout = clampDurationMs(
1341
- patch.timeout_ms ?? current.timeout_ms,
1342
- current.timeout_ms,
1343
- );
1344
- const nextRow = {
1345
- job_id: current.job_id,
1346
- supervisor_agent: patch.supervisor_agent ?? current.supervisor_agent,
1347
- worker_agent: patch.worker_agent ?? current.worker_agent,
1348
- topic: patch.topic ?? current.topic,
1349
- task: patch.task ?? current.task,
1350
- payload_json: JSON.stringify(patch.payload ?? current.payload ?? {}),
1351
- status: nextStatus,
1352
- attempt: Math.max(
1353
- 1,
1354
- Number(patch.attempt ?? current.attempt) || current.attempt || 1,
1355
- ),
1356
- retry_count: Math.max(
1357
- 0,
1358
- Number(patch.retry_count ?? current.retry_count) || 0,
1359
- ),
1360
- max_retries: Math.max(
1361
- 0,
1362
- Number(patch.max_retries ?? current.max_retries) || 0,
1363
- ),
1364
- priority: clampPriority(
1365
- patch.priority ?? current.priority,
1366
- current.priority || 5,
1367
- ),
1368
- ttl_ms: clampDurationMs(
1369
- patch.ttl_ms ?? current.ttl_ms,
1370
- current.ttl_ms || nextTimeout,
1371
- ),
1372
- timeout_ms: nextTimeout,
1373
- deadline_ms: (() => {
1374
- if (Object.hasOwn(patch, "deadline_ms")) {
1375
- return patch.deadline_ms == null
1376
- ? null
1377
- : Math.trunc(Number(patch.deadline_ms));
1378
- }
1379
- if (isTerminal) return null;
1380
- if (nextStatus === "running" && !current.deadline_ms)
1381
- return now + nextTimeout;
1382
- return current.deadline_ms;
1383
- })(),
1384
- trace_id: patch.trace_id ?? current.trace_id,
1385
- correlation_id: patch.correlation_id ?? current.correlation_id,
1386
- last_message_id: Object.hasOwn(patch, "last_message_id")
1387
- ? patch.last_message_id
1388
- : current.last_message_id,
1389
- result_json: Object.hasOwn(patch, "result")
1390
- ? patch.result == null
1391
- ? null
1392
- : JSON.stringify(patch.result)
1393
- : current.result == null
1394
- ? null
1395
- : JSON.stringify(current.result),
1396
- error_json: Object.hasOwn(patch, "error")
1397
- ? patch.error == null
1398
- ? null
1399
- : JSON.stringify(patch.error)
1400
- : current.error == null
1401
- ? null
1402
- : JSON.stringify(current.error),
1403
- updated_at_ms: now,
1404
- started_at_ms: Object.hasOwn(patch, "started_at_ms")
1405
- ? patch.started_at_ms
1406
- : nextStatus === "running"
1407
- ? current.started_at_ms || now
1408
- : current.started_at_ms,
1409
- dispatched_at_ms: Object.hasOwn(patch, "dispatched_at_ms")
1410
- ? patch.dispatched_at_ms
1411
- : current.dispatched_at_ms,
1412
- completed_at_ms: Object.hasOwn(patch, "completed_at_ms")
1413
- ? patch.completed_at_ms
1414
- : isTerminal
1415
- ? current.completed_at_ms || now
1416
- : current.completed_at_ms,
1417
- last_retry_at_ms: Object.hasOwn(patch, "last_retry_at_ms")
1418
- ? patch.last_retry_at_ms
1419
- : current.last_retry_at_ms,
1420
- };
1421
- S.updateAssign.run(nextRow);
1422
- const updated = store.getAssign(jobId);
1423
- if (updated && current.status !== updated.status) {
1424
- notifyAssignStatusListeners(updated);
1425
- }
1426
- return updated;
1427
- },
1428
-
1429
- listAssigns({
1430
- supervisor_agent,
1431
- worker_agent,
1432
- status,
1433
- statuses,
1434
- trace_id,
1435
- correlation_id,
1436
- active_before_ms,
1437
- limit = 50,
1438
- } = {}) {
1439
- const clauses = [];
1440
- const values = [];
1441
-
1442
- if (supervisor_agent) {
1443
- clauses.push("supervisor_agent = ?");
1444
- values.push(supervisor_agent);
1445
- }
1446
- if (worker_agent) {
1447
- clauses.push("worker_agent = ?");
1448
- values.push(worker_agent);
1449
- }
1450
- if (trace_id) {
1451
- clauses.push("trace_id = ?");
1452
- values.push(trace_id);
1453
- }
1454
- if (correlation_id) {
1455
- clauses.push("correlation_id = ?");
1456
- values.push(correlation_id);
1457
- }
1458
-
1459
- const statusList =
1460
- Array.isArray(statuses) && statuses.length
1461
- ? statuses
1462
- : status
1463
- ? [status]
1464
- : [];
1465
- if (statusList.length) {
1466
- clauses.push(`status IN (${statusList.map(() => "?").join(",")})`);
1467
- values.push(...statusList);
1468
- }
1469
-
1470
- if (Number.isFinite(Number(active_before_ms))) {
1471
- clauses.push("deadline_ms IS NOT NULL AND deadline_ms <= ?");
1472
- values.push(Math.trunc(Number(active_before_ms)));
1473
- }
1474
-
1475
- // WHERE 절은 호출마다 달라지므로 prepared statement를 미리 캐시할 수 없다.
1476
- // db.prepare()는 호출당 한 번 실행되며, better-sqlite3 내부에서 SQLite 구문 파싱을
1477
- // 수행한다. 필터 조합이 2^6 = 64가지이므로 정적 캐시 대신 동적 생성을 선택했다.
1478
- // 이 함수는 hot path(heartbeat/poll)가 아닌 관리/조회 경로에서만 호출되므로 허용한다.
1479
- const sql = `
1480
- SELECT * FROM assign_jobs
1481
- ${clauses.length ? `WHERE ${clauses.join(" AND ")}` : ""}
1482
- ORDER BY updated_at_ms DESC
1483
- LIMIT ?`;
1484
- values.push(clampMaxMessages(limit, 50));
1485
- return db
1486
- .prepare(sql)
1487
- .all(...values)
1488
- .map(parseAssignRow);
1489
- },
1490
-
1491
- retryAssign(jobId, patch = {}) {
1492
- const current = store.getAssign(jobId);
1493
- if (!current) return null;
1494
-
1495
- const nextRetryCount = Math.max(
1496
- 0,
1497
- Number(patch.retry_count ?? current.retry_count + 1) || 0,
1498
- );
1499
- const nextAttempt = Math.max(
1500
- current.attempt + 1,
1501
- Number(patch.attempt ?? current.attempt + 1) || 1,
1502
- );
1503
- const nextTimeout = clampDurationMs(
1504
- patch.timeout_ms ?? current.timeout_ms,
1505
- current.timeout_ms,
1506
- );
1507
- return store.updateAssignStatus(jobId, "queued", {
1508
- retry_count: nextRetryCount,
1509
- attempt: nextAttempt,
1510
- timeout_ms: nextTimeout,
1511
- ttl_ms: patch.ttl_ms ?? current.ttl_ms,
1512
- deadline_ms: Date.now() + nextTimeout,
1513
- completed_at_ms: null,
1514
- started_at_ms: null,
1515
- dispatched_at_ms: null,
1516
- last_retry_at_ms: Date.now(),
1517
- result: patch.result ?? null,
1518
- error: Object.hasOwn(patch, "error") ? patch.error : current.error,
1519
- last_message_id: null,
1520
- });
1521
- },
1522
-
1523
- sweepExpired({ now_ms = Date.now() } = {}) {
1524
- return db.transaction(() => {
1525
- const transitions = [];
1526
- for (const row of S.findExpired.all(now_ms)) {
1527
- if (!S.claimExpired.run(row.id, now_ms).changes) continue;
1528
- options.beforeDeadLetterInsert?.({
1529
- message_id: row.id,
1530
- reason: "ttl_expired",
1531
- failed_at_ms: now_ms,
1532
- });
1533
- const dlqInserted =
1534
- S.insertDL.run(row.id, "ttl_expired", now_ms, null).changes === 1;
1535
- transitions.push({
1536
- message: parseMessageRow(row),
1537
- previous_status: row.status,
1538
- reason: "ttl_expired",
1539
- failed_at_ms: now_ms,
1540
- dlq_inserted: dlqInserted,
1541
- });
1542
- }
1543
- return { now_ms, messages: transitions.length, transitions };
1544
- })();
1545
- },
1546
-
1547
- getQueueDepths() {
1548
- return {
1549
- urgent: S.urgentDepth.get().cnt,
1550
- normal: S.normalDepth.get().cnt,
1551
- dlq: S.dlqDepth.get().cnt,
1552
- };
1553
- },
1554
-
1555
- onAssignStatusChange(listener) {
1556
- if (typeof listener !== "function") {
1557
- return () => {};
1558
- }
1559
- assignStatusListeners.add(listener);
1560
- return () => {
1561
- assignStatusListeners.delete(listener);
1562
- };
1563
- },
1564
-
1565
- getDeliveryStats() {
1566
- return {
1567
- total_deliveries: S.ackedRecent.get(Date.now()).cnt,
1568
- avg_delivery_ms: 0,
1569
- };
1570
- },
1571
-
1572
- getHubStats() {
1573
- return {
1574
- online_agents: S.onlineCount.get().cnt,
1575
- total_messages: S.msgCount.get().cnt,
1576
- active_assign_jobs: S.activeAssignCount.get().cnt,
1577
- ...store.getQueueDepths(),
1578
- };
1579
- },
1580
-
1581
- getAuditStats() {
1582
- return {
1583
- online_agents: S.onlineCount.get().cnt,
1584
- total_messages: S.msgCount.get().cnt,
1585
- dlq: S.dlqDepth.get().cnt,
1586
- assign_queued: S.assignCountByStatus.get("queued").cnt,
1587
- assign_running: S.assignCountByStatus.get("running").cnt,
1588
- assign_failed: S.assignCountByStatus.get("failed").cnt,
1589
- assign_timed_out: S.assignCountByStatus.get("timed_out").cnt,
1590
- };
1591
- },
1592
-
1593
- // --- Reflexion CRUD ---
1594
-
1595
- addReflexion({
1596
- type = "reflexion",
1597
- error_pattern,
1598
- error_message,
1599
- context = {},
1600
- solution,
1601
- solution_code = null,
1602
- adaptive_state = {},
1603
- confidence = 0.5,
1604
- hit_count = 1,
1605
- success_count = 0,
1606
- last_hit_ms,
1607
- created_at_ms,
1608
- updated_at_ms,
1609
- }) {
1610
- const now = Date.now();
1611
- const id = uuidv7();
1612
- S.insertReflexion.run({
1613
- id,
1614
- type,
1615
- error_pattern,
1616
- error_message,
1617
- context_json: JSON.stringify(context),
1618
- solution,
1619
- solution_code,
1620
- adaptive_state_json: JSON.stringify(adaptive_state),
1621
- confidence,
1622
- hit_count,
1623
- success_count,
1624
- last_hit_ms: last_hit_ms ?? now,
1625
- created_at_ms: created_at_ms ?? now,
1626
- updated_at_ms: updated_at_ms ?? now,
1627
- });
1628
- return store.getReflexion(id);
1629
- },
1630
-
1631
- getReflexion(id) {
1632
- return parseReflexionRow(S.getReflexionById.get(id));
1633
- },
1634
-
1635
- findReflexion(errorPattern, context = {}) {
1636
- const ctxKeys = Object.keys(context).filter((k) => context[k] != null);
1637
- const ctxWhere = ctxKeys
1638
- .map((k) => ` AND json_extract(context_json, '$.${k}') = ?`)
1639
- .join("");
1640
- const ctxVals = ctxKeys.map((k) => context[k]);
1641
-
1642
- if (ctxKeys.length === 0) {
1643
- let rows = S.findReflexionExact.all(errorPattern);
1644
- if (rows.length) return rows.map(parseReflexionRow);
1645
- const escaped = errorPattern.replace(/[%_\\]/g, "\\$&");
1646
- rows = S.findReflexionLike.all(`%${escaped.slice(0, 100)}%`);
1647
- return rows.map(parseReflexionRow);
1648
- }
1649
-
1650
- const exactSql = `SELECT * FROM reflexion_entries WHERE error_pattern = ?${ctxWhere} ORDER BY confidence DESC`;
1651
- let rows = db.prepare(exactSql).all(errorPattern, ...ctxVals);
1652
- if (rows.length) return rows.map(parseReflexionRow);
1653
-
1654
- const escaped = errorPattern.replace(/[%_\\]/g, "\\$&");
1655
- const likeSql = `SELECT * FROM reflexion_entries WHERE error_pattern LIKE ? ESCAPE '\\'${ctxWhere} ORDER BY confidence DESC LIMIT 10`;
1656
- rows = db.prepare(likeSql).all(`%${escaped.slice(0, 100)}%`, ...ctxVals);
1657
- return rows.map(parseReflexionRow);
1658
- },
1659
-
1660
- updateReflexionHit(id, success = false) {
1661
- const now = Date.now();
1662
- if (success) {
1663
- S.updateReflexionHitSuccess.run(now, now, id);
1664
- } else {
1665
- S.updateReflexionHitOnly.run(now, now, id);
1666
- }
1667
- const entry = store.getReflexion(id);
1668
- if (entry && entry.hit_count > 0) {
1669
- const conf = recalcConfidence(entry);
1670
- S.updateReflexionConfidence.run(
1671
- Math.max(0, Math.min(1, conf)),
1672
- now,
1673
- id,
1674
- );
1675
- }
1676
- return store.getReflexion(id);
1677
- },
1678
-
1679
- listReflexion(filters = {}) {
1680
- const {
1681
- type,
1682
- minConfidence = Number.NEGATIVE_INFINITY,
1683
- projectSlug,
1684
- } = filters;
1685
- return S.listReflexionEntries
1686
- .all()
1687
- .map(parseReflexionRow)
1688
- .filter((entry) => !type || entry.type === type)
1689
- .filter((entry) => entry.confidence >= minConfidence)
1690
- .filter(
1691
- (entry) =>
1692
- !projectSlug || entry.adaptive_state?.project_slug === projectSlug,
1693
- );
1694
- },
1695
-
1696
- patchReflexion(id, patch = {}) {
1697
- const current = store.getReflexion(id);
1698
- if (!current) return null;
1699
- const next = {
1700
- ...current,
1701
- ...patch,
1702
- context: patch.context ?? current.context,
1703
- adaptive_state: patch.adaptive_state ?? current.adaptive_state,
1704
- updated_at_ms: patch.updated_at_ms ?? Date.now(),
1705
- };
1706
- const sets = [
1707
- ["type", next.type],
1708
- ["error_pattern", next.error_pattern],
1709
- ["error_message", next.error_message],
1710
- ["context_json", JSON.stringify(next.context ?? {})],
1711
- ["solution", next.solution],
1712
- ["solution_code", next.solution_code ?? null],
1713
- ["adaptive_state_json", JSON.stringify(next.adaptive_state ?? {})],
1714
- ["confidence", next.confidence],
1715
- ["hit_count", next.hit_count],
1716
- ["success_count", next.success_count],
1717
- ["last_hit_ms", next.last_hit_ms],
1718
- ["updated_at_ms", next.updated_at_ms],
1719
- ];
1720
- const sql = `UPDATE reflexion_entries SET ${sets.map(([key]) => `${key} = ?`).join(", ")} WHERE id = ?`;
1721
- db.prepare(sql).run(...sets.map(([, value]) => value), id);
1722
- return store.getReflexion(id);
1723
- },
1724
-
1725
- deleteReflexion(id) {
1726
- return S.deleteReflexionEntry.run(id).changes > 0;
1727
- },
1728
-
1729
- pruneReflexion(maxAge_ms = 30 * 24 * 3600 * 1000, minConfidence = 0.2) {
1730
- const cutoff = Date.now() - maxAge_ms;
1731
- return S.pruneReflexionEntries.run(cutoff, minConfidence).changes;
1732
- },
1733
- };
1734
-
1735
- return store;
1736
- }