@triflux/remote 10.51.0 → 10.53.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/cto/brief.mjs +0 -8
- package/cto/collect.mjs +1 -127
- package/cto/current.schema.json +2 -19
- package/cto/hygiene.mjs +2 -61
- package/cto/lake-root.mjs +0 -56
- package/cto/status.mjs +5 -139
- package/hub/index.mjs +0 -9
- package/hub/team/agents-row-attach.py +120 -0
- package/hub/team/agents-row.mjs +200 -0
- package/hub/team/claude-daemon-control.mjs +1 -2
- package/hub/team/claude-session-projection.mjs +5 -73
- package/hub/team/claude-transcript.mjs +19 -0
- package/hub/team/cli/commands/debug.mjs +1 -2
- package/hub/team/cli/commands/start/index.mjs +2 -1
- package/hub/team/cli/commands/start/start-headless.mjs +2 -0
- package/hub/team/cli/commands/start/start-mux.mjs +2 -4
- package/hub/team/cli/services/state-store.mjs +5 -20
- package/hub/team/headless.mjs +132 -551
- package/hub/team/psmux.mjs +3 -33
- package/hub/team/results-index.mjs +92 -0
- package/hub/team/session-context.mjs +1 -1
- package/hub/team/tui-viewer.mjs +9 -56
- package/hub/team/tui.mjs +0 -37
- package/hub/team/uds-orchestrator.mjs +20 -599
- package/hub/workers/factory.mjs +6 -51
- package/hub/workers/interface.mjs +1 -7
- package/hub/workers/lib/jsonrpc-core.mjs +11 -0
- package/hub/workers/lib/jsonrpc-ws-uds.mjs +5 -6
- package/package.json +3 -8
- package/scripts/lib/agent-route-policy.mjs +0 -67
- package/scripts/lib/cli-claude.mjs +0 -12
- package/scripts/lib/env-probe.mjs +0 -70
- package/scripts/lib/legacy-mcp-cleanup.mjs +283 -0
- package/scripts/lib/mcp-guard-engine.mjs +42 -101
- package/hub/hub-lifecycle.mjs +0 -248
- package/hub/pipe.mjs +0 -825
- package/hub/promote-penalties.mjs +0 -138
- package/hub/public/dashboard.html +0 -416
- package/hub/schema.sql +0 -209
- package/hub/server.mjs +0 -3048
- package/hub/store-adapter.mjs +0 -182
- package/hub/store.mjs +0 -1736
- package/hub/team/cli/services/hub-client.mjs +0 -178
- package/hub/team/conductor-registry.mjs +0 -121
- package/hub/team/cto-auto-collect.mjs +0 -115
- package/hub/team/lead-control.mjs +0 -191
- package/hub/team/session-sync.mjs +0 -316
- package/hub/team/swarm-intent.mjs +0 -18
- package/hub/team/synapse-cli.mjs +0 -458
- package/hub/team/synapse-http.mjs +0 -311
- package/hub/team/synapse-registry.mjs +0 -618
- package/hub/team/tui-synapse.mjs +0 -223
- package/hub/team/worker-sandbox.mjs +0 -125
- package/hub/tools.mjs +0 -900
- package/hub/workers/codex-app-server-worker.mjs +0 -1067
- package/hub/workers/delegator-mcp.mjs +0 -1098
- package/hub/workers/lib/jsonrpc-stdio.mjs +0 -204
- package/scripts/lib/claudemd-scanner.mjs +0 -26
- package/scripts/lib/context.mjs +0 -51
- package/scripts/lib/handoff.mjs +0 -187
- package/scripts/lib/logger.mjs +0 -122
- 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
|
-
}
|