@triflux/remote 10.0.0-alpha.1
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/hub/pipe.mjs +579 -0
- package/hub/public/dashboard.html +355 -0
- package/hub/public/tray-icon.ico +0 -0
- package/hub/public/tray-icon.png +0 -0
- package/hub/server.mjs +1124 -0
- package/hub/store-adapter.mjs +851 -0
- package/hub/store.mjs +897 -0
- package/hub/team/agent-map.json +11 -0
- package/hub/team/ansi.mjs +379 -0
- package/hub/team/backend.mjs +90 -0
- package/hub/team/cli/commands/attach.mjs +37 -0
- package/hub/team/cli/commands/control.mjs +43 -0
- package/hub/team/cli/commands/debug.mjs +74 -0
- package/hub/team/cli/commands/focus.mjs +53 -0
- package/hub/team/cli/commands/interrupt.mjs +36 -0
- package/hub/team/cli/commands/kill.mjs +37 -0
- package/hub/team/cli/commands/list.mjs +24 -0
- package/hub/team/cli/commands/send.mjs +37 -0
- package/hub/team/cli/commands/start/index.mjs +106 -0
- package/hub/team/cli/commands/start/parse-args.mjs +130 -0
- package/hub/team/cli/commands/start/start-headless.mjs +109 -0
- package/hub/team/cli/commands/start/start-in-process.mjs +40 -0
- package/hub/team/cli/commands/start/start-mux.mjs +73 -0
- package/hub/team/cli/commands/start/start-wt.mjs +69 -0
- package/hub/team/cli/commands/status.mjs +87 -0
- package/hub/team/cli/commands/stop.mjs +31 -0
- package/hub/team/cli/commands/task.mjs +30 -0
- package/hub/team/cli/commands/tasks.mjs +13 -0
- package/hub/team/cli/help.mjs +42 -0
- package/hub/team/cli/index.mjs +41 -0
- package/hub/team/cli/manifest.mjs +29 -0
- package/hub/team/cli/render.mjs +30 -0
- package/hub/team/cli/services/attach-fallback.mjs +54 -0
- package/hub/team/cli/services/hub-client.mjs +208 -0
- package/hub/team/cli/services/member-selector.mjs +30 -0
- package/hub/team/cli/services/native-control.mjs +117 -0
- package/hub/team/cli/services/runtime-mode.mjs +62 -0
- package/hub/team/cli/services/state-store.mjs +48 -0
- package/hub/team/cli/services/task-model.mjs +30 -0
- package/hub/team/dashboard-anchor.mjs +14 -0
- package/hub/team/dashboard-layout.mjs +33 -0
- package/hub/team/dashboard-open.mjs +153 -0
- package/hub/team/dashboard.mjs +274 -0
- package/hub/team/handoff.mjs +303 -0
- package/hub/team/headless.mjs +1149 -0
- package/hub/team/native-supervisor.mjs +392 -0
- package/hub/team/native.mjs +649 -0
- package/hub/team/nativeProxy.mjs +681 -0
- package/hub/team/orchestrator.mjs +161 -0
- package/hub/team/pane.mjs +153 -0
- package/hub/team/psmux.mjs +1354 -0
- package/hub/team/routing.mjs +223 -0
- package/hub/team/session.mjs +611 -0
- package/hub/team/shared.mjs +13 -0
- package/hub/team/staleState.mjs +361 -0
- package/hub/team/tui-lite.mjs +380 -0
- package/hub/team/tui-viewer.mjs +463 -0
- package/hub/team/tui.mjs +1245 -0
- package/hub/tools.mjs +554 -0
- package/hub/tray.mjs +376 -0
- package/hub/workers/claude-worker.mjs +475 -0
- package/hub/workers/codex-mcp.mjs +504 -0
- package/hub/workers/delegator-mcp.mjs +1076 -0
- package/hub/workers/factory.mjs +21 -0
- package/hub/workers/gemini-worker.mjs +373 -0
- package/hub/workers/interface.mjs +52 -0
- package/hub/workers/worker-utils.mjs +104 -0
- package/package.json +31 -0
package/hub/store.mjs
ADDED
|
@@ -0,0 +1,897 @@
|
|
|
1
|
+
// hub/store.mjs — SQLite 감사 로그/메타데이터 저장소
|
|
2
|
+
// 실시간 배달 큐는 router/pipe가 담당하고, SQLite는 재생/감사 용도로만 유지한다.
|
|
3
|
+
import { recalcConfidence } from './reflexion.mjs';
|
|
4
|
+
import { readFileSync, mkdirSync } from 'node:fs';
|
|
5
|
+
import { join, dirname } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { randomBytes } from 'node:crypto';
|
|
8
|
+
import { createRequire } from 'node:module';
|
|
9
|
+
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const require = createRequire(import.meta.url);
|
|
12
|
+
let _rndPool = Buffer.alloc(0), _rndOff = 0;
|
|
13
|
+
|
|
14
|
+
function pooledRandom(n) {
|
|
15
|
+
if (_rndOff + n > _rndPool.length) {
|
|
16
|
+
_rndPool = randomBytes(256);
|
|
17
|
+
_rndOff = 0;
|
|
18
|
+
}
|
|
19
|
+
const out = Buffer.from(_rndPool.subarray(_rndOff, _rndOff + n));
|
|
20
|
+
_rndOff += n;
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** UUIDv7 생성 (RFC 9562, 단조 증가 보장) */
|
|
25
|
+
let _lastMs = 0n;
|
|
26
|
+
let _seq = 0;
|
|
27
|
+
export function uuidv7() {
|
|
28
|
+
let now = BigInt(Date.now());
|
|
29
|
+
if (now <= _lastMs) {
|
|
30
|
+
_seq++;
|
|
31
|
+
// _seq > 0xfff (4095): 시퀀스 공간 소진 시 타임스탬프를 1ms 앞당겨 단조 증가를 보장.
|
|
32
|
+
// 고처리량 환경에서는 타임스탬프가 실제 벽시계보다 앞서 드리프트될 수 있음 (설계상 의도).
|
|
33
|
+
if (_seq > 0xfff) {
|
|
34
|
+
now = _lastMs + 1n;
|
|
35
|
+
_seq = 0;
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
_seq = 0;
|
|
39
|
+
}
|
|
40
|
+
_lastMs = now;
|
|
41
|
+
const buf = pooledRandom(16);
|
|
42
|
+
buf[0] = Number((now >> 40n) & 0xffn);
|
|
43
|
+
buf[1] = Number((now >> 32n) & 0xffn);
|
|
44
|
+
buf[2] = Number((now >> 24n) & 0xffn);
|
|
45
|
+
buf[3] = Number((now >> 16n) & 0xffn);
|
|
46
|
+
buf[4] = Number((now >> 8n) & 0xffn);
|
|
47
|
+
buf[5] = Number(now & 0xffn);
|
|
48
|
+
buf[6] = ((_seq >> 8) & 0x0f) | 0x70;
|
|
49
|
+
buf[7] = _seq & 0xff;
|
|
50
|
+
buf[8] = (buf[8] & 0x3f) | 0x80;
|
|
51
|
+
const h = buf.toString('hex');
|
|
52
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseJson(str, fallback = null) {
|
|
56
|
+
if (str == null) return fallback;
|
|
57
|
+
try { return JSON.parse(str); } catch { return fallback; }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parseAgentRow(row) {
|
|
61
|
+
if (!row) return null;
|
|
62
|
+
const { capabilities_json, topics_json, metadata_json, ...rest } = row;
|
|
63
|
+
return {
|
|
64
|
+
...rest,
|
|
65
|
+
capabilities: parseJson(capabilities_json, []),
|
|
66
|
+
topics: parseJson(topics_json, []),
|
|
67
|
+
metadata: parseJson(metadata_json, {}),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function parseMessageRow(row) {
|
|
72
|
+
if (!row) return null;
|
|
73
|
+
const { payload_json, ...rest } = row;
|
|
74
|
+
return { ...rest, payload: parseJson(payload_json, {}) };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function parseHumanRequestRow(row) {
|
|
78
|
+
if (!row) return null;
|
|
79
|
+
const { schema_json, response_json, ...rest } = row;
|
|
80
|
+
return {
|
|
81
|
+
...rest,
|
|
82
|
+
schema: parseJson(schema_json, {}),
|
|
83
|
+
response: parseJson(response_json, null),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parseAssignRow(row) {
|
|
88
|
+
if (!row) return null;
|
|
89
|
+
const { payload_json, result_json, error_json, ...rest } = row;
|
|
90
|
+
return {
|
|
91
|
+
...rest,
|
|
92
|
+
payload: parseJson(payload_json, {}),
|
|
93
|
+
result: parseJson(result_json, null),
|
|
94
|
+
error: parseJson(error_json, null),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function parseReflexionRow(row) {
|
|
99
|
+
if (!row) return null;
|
|
100
|
+
const { context_json, adaptive_state_json, ...rest } = row;
|
|
101
|
+
return {
|
|
102
|
+
...rest,
|
|
103
|
+
type: rest.type || 'reflexion',
|
|
104
|
+
context: parseJson(context_json, {}),
|
|
105
|
+
adaptive_state: parseJson(adaptive_state_json, {}),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function hasColumn(db, tableName, columnName) {
|
|
110
|
+
const rows = db.prepare(`PRAGMA table_info(${tableName})`).all();
|
|
111
|
+
return rows.some((row) => row.name === columnName);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function ensureColumn(db, tableName, columnName, definition) {
|
|
115
|
+
if (hasColumn(db, tableName, columnName)) return;
|
|
116
|
+
db.exec(`ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${definition}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 저장소 생성
|
|
121
|
+
* @param {string} dbPath
|
|
122
|
+
*/
|
|
123
|
+
export async function importBetterSqlite3() {
|
|
124
|
+
const mod = await import('better-sqlite3');
|
|
125
|
+
return mod.default ?? mod;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function resolveBetterSqlite3(options = {}) {
|
|
129
|
+
if (options.DatabaseCtor) return options.DatabaseCtor;
|
|
130
|
+
const mod = require('better-sqlite3');
|
|
131
|
+
return mod.default ?? mod;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function createStore(dbPath, options = {}) {
|
|
135
|
+
mkdirSync(dirname(dbPath), { recursive: true });
|
|
136
|
+
const Database = resolveBetterSqlite3(options);
|
|
137
|
+
const db = new Database(dbPath);
|
|
138
|
+
|
|
139
|
+
db.pragma('journal_mode = WAL');
|
|
140
|
+
db.pragma('synchronous = NORMAL');
|
|
141
|
+
db.pragma('foreign_keys = ON');
|
|
142
|
+
db.pragma('busy_timeout = 5000');
|
|
143
|
+
db.pragma('wal_autocheckpoint = 1000');
|
|
144
|
+
|
|
145
|
+
const schemaSQL = readFileSync(join(__dirname, 'schema.sql'), 'utf8');
|
|
146
|
+
db.exec("CREATE TABLE IF NOT EXISTS _meta (key TEXT PRIMARY KEY, value TEXT)");
|
|
147
|
+
const SCHEMA_VERSION = '4';
|
|
148
|
+
const curVer = (() => {
|
|
149
|
+
try { return db.prepare("SELECT value FROM _meta WHERE key='schema_version'").pluck().get(); }
|
|
150
|
+
catch { return null; }
|
|
151
|
+
})();
|
|
152
|
+
// 마이그레이션 전략: 스키마 버전이 다르면 schema.sql을 재실행한다.
|
|
153
|
+
// schema.sql은 CREATE TABLE IF NOT EXISTS 패턴을 사용하므로 멱등하게 적용된다.
|
|
154
|
+
// 비파괴적 컬럼 추가는 자동으로 처리되지만, 컬럼 제거/이름 변경은 수동 마이그레이션이 필요하다.
|
|
155
|
+
if (curVer !== SCHEMA_VERSION) {
|
|
156
|
+
if (curVer != null) {
|
|
157
|
+
// 이미 버전이 기록된 DB에서 버전 불일치가 발생한 경우 경고한다.
|
|
158
|
+
console.warn(`[store] schema version mismatch: found=${curVer} expected=${SCHEMA_VERSION}. Applying schema.sql (idempotent).`);
|
|
159
|
+
}
|
|
160
|
+
db.exec(schemaSQL);
|
|
161
|
+
db.prepare("INSERT OR REPLACE INTO _meta (key, value) VALUES ('schema_version', ?)").run(SCHEMA_VERSION);
|
|
162
|
+
}
|
|
163
|
+
ensureColumn(db, 'reflexion_entries', 'type', "TEXT NOT NULL DEFAULT 'reflexion'");
|
|
164
|
+
ensureColumn(db, 'reflexion_entries', 'adaptive_state_json', "TEXT NOT NULL DEFAULT '{}'");
|
|
165
|
+
|
|
166
|
+
const S = {
|
|
167
|
+
upsertAgent: db.prepare(`
|
|
168
|
+
INSERT INTO agents (agent_id, cli, pid, capabilities_json, topics_json, last_seen_ms, lease_expires_ms, status, metadata_json)
|
|
169
|
+
VALUES (@agent_id, @cli, @pid, @capabilities_json, @topics_json, @last_seen_ms, @lease_expires_ms, @status, @metadata_json)
|
|
170
|
+
ON CONFLICT(agent_id) DO UPDATE SET
|
|
171
|
+
cli=excluded.cli,
|
|
172
|
+
pid=excluded.pid,
|
|
173
|
+
capabilities_json=excluded.capabilities_json,
|
|
174
|
+
topics_json=excluded.topics_json,
|
|
175
|
+
last_seen_ms=excluded.last_seen_ms,
|
|
176
|
+
lease_expires_ms=excluded.lease_expires_ms,
|
|
177
|
+
status=excluded.status,
|
|
178
|
+
metadata_json=excluded.metadata_json`),
|
|
179
|
+
getAgent: db.prepare('SELECT * FROM agents WHERE agent_id = ?'),
|
|
180
|
+
setAgentTopics: db.prepare('UPDATE agents SET topics_json=?, last_seen_ms=? WHERE agent_id=?'),
|
|
181
|
+
heartbeat: db.prepare("UPDATE agents SET last_seen_ms=?, lease_expires_ms=?, status='online' WHERE agent_id=?"),
|
|
182
|
+
setAgentStatus: db.prepare('UPDATE agents SET status=? WHERE agent_id=?'),
|
|
183
|
+
onlineAgents: db.prepare("SELECT * FROM agents WHERE status != 'offline'"),
|
|
184
|
+
allAgents: db.prepare('SELECT * FROM agents'),
|
|
185
|
+
agentsByTopic: db.prepare("SELECT a.* FROM agents a, json_each(a.topics_json) t WHERE t.value=? AND a.status != 'offline'"),
|
|
186
|
+
markStale: db.prepare("UPDATE agents SET status='stale' WHERE status='online' AND lease_expires_ms < ?"),
|
|
187
|
+
markOffline: db.prepare("UPDATE agents SET status='offline' WHERE status='stale' AND lease_expires_ms < ? - 300000"),
|
|
188
|
+
|
|
189
|
+
insertAuditMessage: db.prepare(`
|
|
190
|
+
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)
|
|
191
|
+
VALUES (@id, @type, @from_agent, @to_agent, @topic, @priority, @ttl_ms, @created_at_ms, @expires_at_ms, @correlation_id, @trace_id, @payload_json, @status)`),
|
|
192
|
+
getMsg: db.prepare('SELECT * FROM messages WHERE id=?'),
|
|
193
|
+
getResponse: db.prepare("SELECT * FROM messages WHERE correlation_id=? AND type='response' ORDER BY created_at_ms DESC LIMIT 1"),
|
|
194
|
+
getMsgsByTrace: db.prepare('SELECT * FROM messages WHERE trace_id=? ORDER BY created_at_ms'),
|
|
195
|
+
setMsgStatus: db.prepare('UPDATE messages SET status=? WHERE id=?'),
|
|
196
|
+
recentAgentMessages: db.prepare(`
|
|
197
|
+
SELECT * FROM messages
|
|
198
|
+
WHERE to_agent=?
|
|
199
|
+
ORDER BY created_at_ms DESC
|
|
200
|
+
LIMIT ?`),
|
|
201
|
+
recentAgentMessagesWithTopics: db.prepare(`
|
|
202
|
+
SELECT * FROM messages
|
|
203
|
+
WHERE to_agent=?
|
|
204
|
+
OR (
|
|
205
|
+
substr(to_agent, 1, 6)='topic:'
|
|
206
|
+
AND topic IN (SELECT value FROM json_each(?))
|
|
207
|
+
)
|
|
208
|
+
ORDER BY created_at_ms DESC
|
|
209
|
+
LIMIT ?`),
|
|
210
|
+
|
|
211
|
+
insertHR: db.prepare(`
|
|
212
|
+
INSERT INTO human_requests (request_id, requester_agent, kind, prompt, schema_json, state, deadline_ms, default_action, correlation_id, trace_id, response_json)
|
|
213
|
+
VALUES (@request_id, @requester_agent, @kind, @prompt, @schema_json, @state, @deadline_ms, @default_action, @correlation_id, @trace_id, @response_json)`),
|
|
214
|
+
getHR: db.prepare('SELECT * FROM human_requests WHERE request_id=?'),
|
|
215
|
+
updateHR: db.prepare('UPDATE human_requests SET state=?, response_json=? WHERE request_id=?'),
|
|
216
|
+
pendingHR: db.prepare("SELECT * FROM human_requests WHERE state='pending'"),
|
|
217
|
+
expireHR: db.prepare("UPDATE human_requests SET state='timed_out' WHERE state='pending' AND deadline_ms < ?"),
|
|
218
|
+
|
|
219
|
+
insertDL: db.prepare('INSERT OR REPLACE INTO dead_letters (message_id, reason, failed_at_ms, last_error) VALUES (?,?,?,?)'),
|
|
220
|
+
getDL: db.prepare('SELECT * FROM dead_letters ORDER BY failed_at_ms DESC LIMIT ?'),
|
|
221
|
+
|
|
222
|
+
insertAssign: db.prepare(`
|
|
223
|
+
INSERT INTO assign_jobs (
|
|
224
|
+
job_id, supervisor_agent, worker_agent, topic, task, payload_json,
|
|
225
|
+
status, attempt, retry_count, max_retries, priority, ttl_ms, timeout_ms, deadline_ms,
|
|
226
|
+
trace_id, correlation_id, last_message_id, result_json, error_json,
|
|
227
|
+
created_at_ms, updated_at_ms, started_at_ms, completed_at_ms, last_retry_at_ms
|
|
228
|
+
) VALUES (
|
|
229
|
+
@job_id, @supervisor_agent, @worker_agent, @topic, @task, @payload_json,
|
|
230
|
+
@status, @attempt, @retry_count, @max_retries, @priority, @ttl_ms, @timeout_ms, @deadline_ms,
|
|
231
|
+
@trace_id, @correlation_id, @last_message_id, @result_json, @error_json,
|
|
232
|
+
@created_at_ms, @updated_at_ms, @started_at_ms, @completed_at_ms, @last_retry_at_ms
|
|
233
|
+
)`),
|
|
234
|
+
getAssign: db.prepare('SELECT * FROM assign_jobs WHERE job_id = ?'),
|
|
235
|
+
updateAssign: db.prepare(`
|
|
236
|
+
UPDATE assign_jobs SET
|
|
237
|
+
supervisor_agent=@supervisor_agent,
|
|
238
|
+
worker_agent=@worker_agent,
|
|
239
|
+
topic=@topic,
|
|
240
|
+
task=@task,
|
|
241
|
+
payload_json=@payload_json,
|
|
242
|
+
status=@status,
|
|
243
|
+
attempt=@attempt,
|
|
244
|
+
retry_count=@retry_count,
|
|
245
|
+
max_retries=@max_retries,
|
|
246
|
+
priority=@priority,
|
|
247
|
+
ttl_ms=@ttl_ms,
|
|
248
|
+
timeout_ms=@timeout_ms,
|
|
249
|
+
deadline_ms=@deadline_ms,
|
|
250
|
+
trace_id=@trace_id,
|
|
251
|
+
correlation_id=@correlation_id,
|
|
252
|
+
last_message_id=@last_message_id,
|
|
253
|
+
result_json=@result_json,
|
|
254
|
+
error_json=@error_json,
|
|
255
|
+
updated_at_ms=@updated_at_ms,
|
|
256
|
+
started_at_ms=@started_at_ms,
|
|
257
|
+
completed_at_ms=@completed_at_ms,
|
|
258
|
+
last_retry_at_ms=@last_retry_at_ms
|
|
259
|
+
WHERE job_id=@job_id`),
|
|
260
|
+
|
|
261
|
+
findExpired: db.prepare("SELECT id FROM messages WHERE status='queued' AND expires_at_ms < ?"),
|
|
262
|
+
urgentDepth: db.prepare("SELECT COUNT(*) as cnt FROM messages WHERE status='queued' AND priority >= 7"),
|
|
263
|
+
normalDepth: db.prepare("SELECT COUNT(*) as cnt FROM messages WHERE status='queued' AND priority < 7"),
|
|
264
|
+
onlineCount: db.prepare("SELECT COUNT(*) as cnt FROM agents WHERE status='online'"),
|
|
265
|
+
msgCount: db.prepare('SELECT COUNT(*) as cnt FROM messages'),
|
|
266
|
+
dlqDepth: db.prepare('SELECT COUNT(*) as cnt FROM dead_letters'),
|
|
267
|
+
ackedRecent: db.prepare("SELECT COUNT(*) as cnt FROM messages WHERE status='acked' AND created_at_ms > ? - 300000"),
|
|
268
|
+
assignCountByStatus: db.prepare('SELECT COUNT(*) as cnt FROM assign_jobs WHERE status = ?'),
|
|
269
|
+
activeAssignCount: db.prepare("SELECT COUNT(*) as cnt FROM assign_jobs WHERE status IN ('queued','running')"),
|
|
270
|
+
|
|
271
|
+
// reflexion
|
|
272
|
+
insertReflexion: db.prepare(`
|
|
273
|
+
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)
|
|
274
|
+
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)`),
|
|
275
|
+
getReflexionById: db.prepare('SELECT * FROM reflexion_entries WHERE id = ?'),
|
|
276
|
+
findReflexionExact: db.prepare('SELECT * FROM reflexion_entries WHERE error_pattern = ? ORDER BY confidence DESC'),
|
|
277
|
+
findReflexionLike: db.prepare("SELECT * FROM reflexion_entries WHERE error_pattern LIKE ? ESCAPE '\\' ORDER BY confidence DESC LIMIT 10"),
|
|
278
|
+
updateReflexionHitSuccess: db.prepare('UPDATE reflexion_entries SET hit_count = hit_count + 1, success_count = success_count + 1, last_hit_ms = ?, updated_at_ms = ? WHERE id = ?'),
|
|
279
|
+
updateReflexionHitOnly: db.prepare('UPDATE reflexion_entries SET hit_count = hit_count + 1, last_hit_ms = ?, updated_at_ms = ? WHERE id = ?'),
|
|
280
|
+
updateReflexionConfidence: db.prepare('UPDATE reflexion_entries SET confidence = ?, updated_at_ms = ? WHERE id = ?'),
|
|
281
|
+
pruneReflexionEntries: db.prepare('DELETE FROM reflexion_entries WHERE updated_at_ms < ? AND confidence < ?'),
|
|
282
|
+
listReflexionEntries: db.prepare('SELECT * FROM reflexion_entries ORDER BY confidence DESC, updated_at_ms DESC'),
|
|
283
|
+
deleteReflexionEntry: db.prepare('DELETE FROM reflexion_entries WHERE id = ?'),
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const assignStatusListeners = new Set();
|
|
287
|
+
|
|
288
|
+
function buildAssignCallbackEvent(row) {
|
|
289
|
+
return {
|
|
290
|
+
job_id: row.job_id,
|
|
291
|
+
status: row.status,
|
|
292
|
+
result: row.result ?? row.error ?? null,
|
|
293
|
+
timestamp: new Date(row.updated_at_ms || Date.now()).toISOString(),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function notifyAssignStatusListeners(row) {
|
|
298
|
+
const event = buildAssignCallbackEvent(row);
|
|
299
|
+
for (const listener of Array.from(assignStatusListeners)) {
|
|
300
|
+
try { listener(event, row); } catch {}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function clampMaxMessages(value, fallback = 20) {
|
|
305
|
+
const num = Number(value);
|
|
306
|
+
if (!Number.isFinite(num)) return fallback;
|
|
307
|
+
return Math.max(1, Math.min(Math.trunc(num), 100));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function clampPriority(value, fallback = 5) {
|
|
311
|
+
const num = Number(value);
|
|
312
|
+
if (!Number.isFinite(num)) return fallback;
|
|
313
|
+
return Math.max(1, Math.min(Math.trunc(num), 9));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function clampDuration(value, fallback = 600000, min = 1000, max = 86400000) {
|
|
317
|
+
const num = Number(value);
|
|
318
|
+
if (!Number.isFinite(num)) return fallback;
|
|
319
|
+
return Math.max(min, Math.min(Math.trunc(num), max));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const store = {
|
|
323
|
+
db,
|
|
324
|
+
uuidv7,
|
|
325
|
+
|
|
326
|
+
close() {
|
|
327
|
+
db.close();
|
|
328
|
+
},
|
|
329
|
+
|
|
330
|
+
registerAgent({ agent_id, cli, pid, capabilities = [], topics = [], heartbeat_ttl_ms = 30000, metadata = {} }) {
|
|
331
|
+
const now = Date.now();
|
|
332
|
+
const leaseExpires = now + heartbeat_ttl_ms;
|
|
333
|
+
S.upsertAgent.run({
|
|
334
|
+
agent_id,
|
|
335
|
+
cli,
|
|
336
|
+
pid: pid ?? null,
|
|
337
|
+
capabilities_json: JSON.stringify(capabilities),
|
|
338
|
+
topics_json: JSON.stringify(topics),
|
|
339
|
+
last_seen_ms: now,
|
|
340
|
+
lease_expires_ms: leaseExpires,
|
|
341
|
+
status: 'online',
|
|
342
|
+
metadata_json: JSON.stringify(metadata),
|
|
343
|
+
});
|
|
344
|
+
return { agent_id, lease_id: uuidv7(), lease_expires_ms: leaseExpires, server_time_ms: now };
|
|
345
|
+
},
|
|
346
|
+
|
|
347
|
+
getAgent(id) {
|
|
348
|
+
return parseAgentRow(S.getAgent.get(id));
|
|
349
|
+
},
|
|
350
|
+
|
|
351
|
+
refreshLease(agentId, ttlMs = 30000) {
|
|
352
|
+
const now = Date.now();
|
|
353
|
+
S.heartbeat.run(now, now + ttlMs, agentId);
|
|
354
|
+
return { agent_id: agentId, lease_expires_ms: now + ttlMs, server_time_ms: now };
|
|
355
|
+
},
|
|
356
|
+
|
|
357
|
+
updateAgentTopics(agentId, topics = []) {
|
|
358
|
+
const now = Date.now();
|
|
359
|
+
return S.setAgentTopics.run(JSON.stringify(topics), now, agentId).changes > 0;
|
|
360
|
+
},
|
|
361
|
+
|
|
362
|
+
listOnlineAgents() {
|
|
363
|
+
return S.onlineAgents.all().map(parseAgentRow);
|
|
364
|
+
},
|
|
365
|
+
|
|
366
|
+
listAllAgents() {
|
|
367
|
+
return S.allAgents.all().map(parseAgentRow);
|
|
368
|
+
},
|
|
369
|
+
|
|
370
|
+
getAgentsByTopic(topic) {
|
|
371
|
+
return S.agentsByTopic.all(topic).map(parseAgentRow);
|
|
372
|
+
},
|
|
373
|
+
|
|
374
|
+
sweepStaleAgents() {
|
|
375
|
+
const now = Date.now();
|
|
376
|
+
return {
|
|
377
|
+
stale: S.markStale.run(now).changes,
|
|
378
|
+
offline: S.markOffline.run(now).changes,
|
|
379
|
+
};
|
|
380
|
+
},
|
|
381
|
+
|
|
382
|
+
updateAgentStatus(agentId, status) {
|
|
383
|
+
return S.setAgentStatus.run(status, agentId).changes > 0;
|
|
384
|
+
},
|
|
385
|
+
|
|
386
|
+
auditLog({ type, from, to, topic, priority = 5, ttl_ms = 300000, payload = {}, trace_id, correlation_id, status = 'queued' }) {
|
|
387
|
+
const now = Date.now();
|
|
388
|
+
const row = {
|
|
389
|
+
id: uuidv7(),
|
|
390
|
+
type,
|
|
391
|
+
from_agent: from,
|
|
392
|
+
to_agent: to,
|
|
393
|
+
topic,
|
|
394
|
+
priority,
|
|
395
|
+
ttl_ms,
|
|
396
|
+
created_at_ms: now,
|
|
397
|
+
expires_at_ms: now + ttl_ms,
|
|
398
|
+
correlation_id: correlation_id || uuidv7(),
|
|
399
|
+
trace_id: trace_id || uuidv7(),
|
|
400
|
+
payload_json: JSON.stringify(payload),
|
|
401
|
+
status,
|
|
402
|
+
};
|
|
403
|
+
S.insertAuditMessage.run(row);
|
|
404
|
+
return { ...row, payload };
|
|
405
|
+
},
|
|
406
|
+
|
|
407
|
+
// 하위 호환: 기존 enqueueMessage 호출은 auditLog로 위임한다.
|
|
408
|
+
enqueueMessage(args) {
|
|
409
|
+
return store.auditLog(args);
|
|
410
|
+
},
|
|
411
|
+
|
|
412
|
+
getMessage(id) {
|
|
413
|
+
return parseMessageRow(S.getMsg.get(id));
|
|
414
|
+
},
|
|
415
|
+
|
|
416
|
+
getResponseByCorrelation(cid) {
|
|
417
|
+
return parseMessageRow(S.getResponse.get(cid));
|
|
418
|
+
},
|
|
419
|
+
|
|
420
|
+
getMessagesByTrace(tid) {
|
|
421
|
+
return S.getMsgsByTrace.all(tid).map(parseMessageRow);
|
|
422
|
+
},
|
|
423
|
+
|
|
424
|
+
updateMessageStatus(id, status) {
|
|
425
|
+
return S.setMsgStatus.run(status, id).changes > 0;
|
|
426
|
+
},
|
|
427
|
+
|
|
428
|
+
getAuditMessagesForAgent(agentId, { max_messages = 20, include_topics = null } = {}) {
|
|
429
|
+
const limit = clampMaxMessages(max_messages);
|
|
430
|
+
const topics = Array.isArray(include_topics) && include_topics.length
|
|
431
|
+
? include_topics
|
|
432
|
+
: (store.getAgent(agentId)?.topics || []);
|
|
433
|
+
|
|
434
|
+
const rows = topics.length
|
|
435
|
+
? S.recentAgentMessagesWithTopics.all(agentId, JSON.stringify(topics), limit)
|
|
436
|
+
: S.recentAgentMessages.all(agentId, limit);
|
|
437
|
+
|
|
438
|
+
return rows.map(parseMessageRow);
|
|
439
|
+
},
|
|
440
|
+
|
|
441
|
+
// 하위 호환: 실시간 수신함 대신 감사 로그 재생 결과를 반환한다.
|
|
442
|
+
deliverToAgent(messageId, agentId) {
|
|
443
|
+
return !!store.getMessage(messageId) && !!agentId;
|
|
444
|
+
},
|
|
445
|
+
|
|
446
|
+
deliverToTopic(messageId, topic) {
|
|
447
|
+
void messageId;
|
|
448
|
+
return store.getAgentsByTopic(topic).length;
|
|
449
|
+
},
|
|
450
|
+
|
|
451
|
+
pollForAgent(agentId, { max_messages = 20, include_topics = null } = {}) {
|
|
452
|
+
return store.getAuditMessagesForAgent(agentId, {
|
|
453
|
+
max_messages,
|
|
454
|
+
include_topics,
|
|
455
|
+
});
|
|
456
|
+
},
|
|
457
|
+
|
|
458
|
+
ackMessages() {
|
|
459
|
+
return 0;
|
|
460
|
+
},
|
|
461
|
+
|
|
462
|
+
insertHumanRequest({ requester_agent, kind, prompt, requested_schema = {}, deadline_ms, default_action, correlation_id, trace_id }) {
|
|
463
|
+
const requestId = uuidv7();
|
|
464
|
+
const now = Date.now();
|
|
465
|
+
const deadlineAt = now + deadline_ms;
|
|
466
|
+
S.insertHR.run({
|
|
467
|
+
request_id: requestId,
|
|
468
|
+
requester_agent,
|
|
469
|
+
kind,
|
|
470
|
+
prompt,
|
|
471
|
+
schema_json: JSON.stringify(requested_schema),
|
|
472
|
+
state: 'pending',
|
|
473
|
+
deadline_ms: deadlineAt,
|
|
474
|
+
default_action,
|
|
475
|
+
correlation_id: correlation_id || uuidv7(),
|
|
476
|
+
trace_id: trace_id || uuidv7(),
|
|
477
|
+
response_json: null,
|
|
478
|
+
});
|
|
479
|
+
return { request_id: requestId, state: 'pending', deadline_ms: deadlineAt };
|
|
480
|
+
},
|
|
481
|
+
|
|
482
|
+
getHumanRequest(id) {
|
|
483
|
+
return parseHumanRequestRow(S.getHR.get(id));
|
|
484
|
+
},
|
|
485
|
+
|
|
486
|
+
updateHumanRequest(id, state, resp = null) {
|
|
487
|
+
return S.updateHR.run(state, resp ? JSON.stringify(resp) : null, id).changes > 0;
|
|
488
|
+
},
|
|
489
|
+
|
|
490
|
+
getPendingHumanRequests() {
|
|
491
|
+
return S.pendingHR.all().map(parseHumanRequestRow);
|
|
492
|
+
},
|
|
493
|
+
|
|
494
|
+
expireHumanRequests() {
|
|
495
|
+
return S.expireHR.run(Date.now()).changes;
|
|
496
|
+
},
|
|
497
|
+
|
|
498
|
+
moveToDeadLetter(messageId, reason, lastError = null) {
|
|
499
|
+
db.transaction(() => {
|
|
500
|
+
S.setMsgStatus.run('dead_letter', messageId);
|
|
501
|
+
S.insertDL.run(messageId, reason, Date.now(), lastError);
|
|
502
|
+
})();
|
|
503
|
+
return true;
|
|
504
|
+
},
|
|
505
|
+
|
|
506
|
+
getDeadLetters(limit = 50) {
|
|
507
|
+
return S.getDL.all(limit);
|
|
508
|
+
},
|
|
509
|
+
|
|
510
|
+
createAssign({
|
|
511
|
+
job_id,
|
|
512
|
+
supervisor_agent,
|
|
513
|
+
worker_agent,
|
|
514
|
+
topic = 'assign.job',
|
|
515
|
+
task = '',
|
|
516
|
+
payload = {},
|
|
517
|
+
status = 'queued',
|
|
518
|
+
attempt = 1,
|
|
519
|
+
retry_count = 0,
|
|
520
|
+
max_retries = 0,
|
|
521
|
+
priority = 5,
|
|
522
|
+
ttl_ms = 600000,
|
|
523
|
+
timeout_ms = 600000,
|
|
524
|
+
deadline_ms,
|
|
525
|
+
trace_id,
|
|
526
|
+
correlation_id,
|
|
527
|
+
last_message_id = null,
|
|
528
|
+
result = null,
|
|
529
|
+
error = null,
|
|
530
|
+
}) {
|
|
531
|
+
const now = Date.now();
|
|
532
|
+
const normalizedTimeout = clampDuration(timeout_ms, 600000);
|
|
533
|
+
const row = {
|
|
534
|
+
job_id: job_id || uuidv7(),
|
|
535
|
+
supervisor_agent,
|
|
536
|
+
worker_agent,
|
|
537
|
+
topic: String(topic || 'assign.job'),
|
|
538
|
+
task: String(task || ''),
|
|
539
|
+
payload_json: JSON.stringify(payload || {}),
|
|
540
|
+
status,
|
|
541
|
+
attempt: Math.max(1, Number(attempt) || 1),
|
|
542
|
+
retry_count: Math.max(0, Number(retry_count) || 0),
|
|
543
|
+
max_retries: Math.max(0, Number(max_retries) || 0),
|
|
544
|
+
priority: clampPriority(priority, 5),
|
|
545
|
+
ttl_ms: clampDuration(ttl_ms, normalizedTimeout),
|
|
546
|
+
timeout_ms: normalizedTimeout,
|
|
547
|
+
deadline_ms: Number.isFinite(Number(deadline_ms))
|
|
548
|
+
? Math.trunc(Number(deadline_ms))
|
|
549
|
+
: now + normalizedTimeout,
|
|
550
|
+
trace_id: trace_id || uuidv7(),
|
|
551
|
+
correlation_id: correlation_id || uuidv7(),
|
|
552
|
+
last_message_id,
|
|
553
|
+
result_json: result == null ? null : JSON.stringify(result),
|
|
554
|
+
error_json: error == null ? null : JSON.stringify(error),
|
|
555
|
+
created_at_ms: now,
|
|
556
|
+
updated_at_ms: now,
|
|
557
|
+
started_at_ms: status === 'running' ? now : null,
|
|
558
|
+
completed_at_ms: ['succeeded', 'failed', 'timed_out'].includes(status) ? now : null,
|
|
559
|
+
last_retry_at_ms: retry_count > 0 ? now : null,
|
|
560
|
+
};
|
|
561
|
+
S.insertAssign.run(row);
|
|
562
|
+
const inserted = store.getAssign(row.job_id);
|
|
563
|
+
notifyAssignStatusListeners(inserted);
|
|
564
|
+
return inserted;
|
|
565
|
+
},
|
|
566
|
+
|
|
567
|
+
getAssign(jobId) {
|
|
568
|
+
return parseAssignRow(S.getAssign.get(jobId));
|
|
569
|
+
},
|
|
570
|
+
|
|
571
|
+
updateAssignStatus(jobId, status, patch = {}) {
|
|
572
|
+
const current = store.getAssign(jobId);
|
|
573
|
+
if (!current) return null;
|
|
574
|
+
|
|
575
|
+
const now = Date.now();
|
|
576
|
+
const nextStatus = status || current.status;
|
|
577
|
+
const isTerminal = ['succeeded', 'failed', 'timed_out'].includes(nextStatus);
|
|
578
|
+
const nextTimeout = clampDuration(patch.timeout_ms ?? current.timeout_ms, current.timeout_ms);
|
|
579
|
+
const nextRow = {
|
|
580
|
+
job_id: current.job_id,
|
|
581
|
+
supervisor_agent: patch.supervisor_agent ?? current.supervisor_agent,
|
|
582
|
+
worker_agent: patch.worker_agent ?? current.worker_agent,
|
|
583
|
+
topic: patch.topic ?? current.topic,
|
|
584
|
+
task: patch.task ?? current.task,
|
|
585
|
+
payload_json: JSON.stringify(patch.payload ?? current.payload ?? {}),
|
|
586
|
+
status: nextStatus,
|
|
587
|
+
attempt: Math.max(1, Number(patch.attempt ?? current.attempt) || current.attempt || 1),
|
|
588
|
+
retry_count: Math.max(0, Number(patch.retry_count ?? current.retry_count) || 0),
|
|
589
|
+
max_retries: Math.max(0, Number(patch.max_retries ?? current.max_retries) || 0),
|
|
590
|
+
priority: clampPriority(patch.priority ?? current.priority, current.priority || 5),
|
|
591
|
+
ttl_ms: clampDuration(patch.ttl_ms ?? current.ttl_ms, current.ttl_ms || nextTimeout),
|
|
592
|
+
timeout_ms: nextTimeout,
|
|
593
|
+
deadline_ms: (() => {
|
|
594
|
+
if (Object.prototype.hasOwnProperty.call(patch, 'deadline_ms')) {
|
|
595
|
+
return patch.deadline_ms == null ? null : Math.trunc(Number(patch.deadline_ms));
|
|
596
|
+
}
|
|
597
|
+
if (isTerminal) return null;
|
|
598
|
+
if (nextStatus === 'running' && !current.deadline_ms) return now + nextTimeout;
|
|
599
|
+
return current.deadline_ms;
|
|
600
|
+
})(),
|
|
601
|
+
trace_id: patch.trace_id ?? current.trace_id,
|
|
602
|
+
correlation_id: patch.correlation_id ?? current.correlation_id,
|
|
603
|
+
last_message_id: Object.prototype.hasOwnProperty.call(patch, 'last_message_id')
|
|
604
|
+
? patch.last_message_id
|
|
605
|
+
: current.last_message_id,
|
|
606
|
+
result_json: Object.prototype.hasOwnProperty.call(patch, 'result')
|
|
607
|
+
? (patch.result == null ? null : JSON.stringify(patch.result))
|
|
608
|
+
: (current.result == null ? null : JSON.stringify(current.result)),
|
|
609
|
+
error_json: Object.prototype.hasOwnProperty.call(patch, 'error')
|
|
610
|
+
? (patch.error == null ? null : JSON.stringify(patch.error))
|
|
611
|
+
: (current.error == null ? null : JSON.stringify(current.error)),
|
|
612
|
+
updated_at_ms: now,
|
|
613
|
+
started_at_ms: Object.prototype.hasOwnProperty.call(patch, 'started_at_ms')
|
|
614
|
+
? patch.started_at_ms
|
|
615
|
+
: (nextStatus === 'running' ? (current.started_at_ms || now) : current.started_at_ms),
|
|
616
|
+
completed_at_ms: Object.prototype.hasOwnProperty.call(patch, 'completed_at_ms')
|
|
617
|
+
? patch.completed_at_ms
|
|
618
|
+
: (isTerminal ? (current.completed_at_ms || now) : current.completed_at_ms),
|
|
619
|
+
last_retry_at_ms: Object.prototype.hasOwnProperty.call(patch, 'last_retry_at_ms')
|
|
620
|
+
? patch.last_retry_at_ms
|
|
621
|
+
: current.last_retry_at_ms,
|
|
622
|
+
};
|
|
623
|
+
S.updateAssign.run(nextRow);
|
|
624
|
+
const updated = store.getAssign(jobId);
|
|
625
|
+
if (updated && current.status !== updated.status) {
|
|
626
|
+
notifyAssignStatusListeners(updated);
|
|
627
|
+
}
|
|
628
|
+
return updated;
|
|
629
|
+
},
|
|
630
|
+
|
|
631
|
+
listAssigns({
|
|
632
|
+
supervisor_agent,
|
|
633
|
+
worker_agent,
|
|
634
|
+
status,
|
|
635
|
+
statuses,
|
|
636
|
+
trace_id,
|
|
637
|
+
correlation_id,
|
|
638
|
+
active_before_ms,
|
|
639
|
+
limit = 50,
|
|
640
|
+
} = {}) {
|
|
641
|
+
const clauses = [];
|
|
642
|
+
const values = [];
|
|
643
|
+
|
|
644
|
+
if (supervisor_agent) {
|
|
645
|
+
clauses.push('supervisor_agent = ?');
|
|
646
|
+
values.push(supervisor_agent);
|
|
647
|
+
}
|
|
648
|
+
if (worker_agent) {
|
|
649
|
+
clauses.push('worker_agent = ?');
|
|
650
|
+
values.push(worker_agent);
|
|
651
|
+
}
|
|
652
|
+
if (trace_id) {
|
|
653
|
+
clauses.push('trace_id = ?');
|
|
654
|
+
values.push(trace_id);
|
|
655
|
+
}
|
|
656
|
+
if (correlation_id) {
|
|
657
|
+
clauses.push('correlation_id = ?');
|
|
658
|
+
values.push(correlation_id);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const statusList = Array.isArray(statuses) && statuses.length
|
|
662
|
+
? statuses
|
|
663
|
+
: (status ? [status] : []);
|
|
664
|
+
if (statusList.length) {
|
|
665
|
+
clauses.push(`status IN (${statusList.map(() => '?').join(',')})`);
|
|
666
|
+
values.push(...statusList);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (Number.isFinite(Number(active_before_ms))) {
|
|
670
|
+
clauses.push('deadline_ms IS NOT NULL AND deadline_ms <= ?');
|
|
671
|
+
values.push(Math.trunc(Number(active_before_ms)));
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// WHERE 절은 호출마다 달라지므로 prepared statement를 미리 캐시할 수 없다.
|
|
675
|
+
// db.prepare()는 호출당 한 번 실행되며, better-sqlite3 내부에서 SQLite 구문 파싱을
|
|
676
|
+
// 수행한다. 필터 조합이 2^6 = 64가지이므로 정적 캐시 대신 동적 생성을 선택했다.
|
|
677
|
+
// 이 함수는 hot path(heartbeat/poll)가 아닌 관리/조회 경로에서만 호출되므로 허용한다.
|
|
678
|
+
const sql = `
|
|
679
|
+
SELECT * FROM assign_jobs
|
|
680
|
+
${clauses.length ? `WHERE ${clauses.join(' AND ')}` : ''}
|
|
681
|
+
ORDER BY updated_at_ms DESC
|
|
682
|
+
LIMIT ?`;
|
|
683
|
+
values.push(clampMaxMessages(limit, 50));
|
|
684
|
+
return db.prepare(sql).all(...values).map(parseAssignRow);
|
|
685
|
+
},
|
|
686
|
+
|
|
687
|
+
retryAssign(jobId, patch = {}) {
|
|
688
|
+
const current = store.getAssign(jobId);
|
|
689
|
+
if (!current) return null;
|
|
690
|
+
|
|
691
|
+
const nextRetryCount = Math.max(0, Number(patch.retry_count ?? current.retry_count + 1) || 0);
|
|
692
|
+
const nextAttempt = Math.max(current.attempt + 1, Number(patch.attempt ?? current.attempt + 1) || 1);
|
|
693
|
+
const nextTimeout = clampDuration(patch.timeout_ms ?? current.timeout_ms, current.timeout_ms);
|
|
694
|
+
return store.updateAssignStatus(jobId, 'queued', {
|
|
695
|
+
retry_count: nextRetryCount,
|
|
696
|
+
attempt: nextAttempt,
|
|
697
|
+
timeout_ms: nextTimeout,
|
|
698
|
+
ttl_ms: patch.ttl_ms ?? current.ttl_ms,
|
|
699
|
+
deadline_ms: Date.now() + nextTimeout,
|
|
700
|
+
completed_at_ms: null,
|
|
701
|
+
started_at_ms: null,
|
|
702
|
+
last_retry_at_ms: Date.now(),
|
|
703
|
+
result: patch.result ?? null,
|
|
704
|
+
error: Object.prototype.hasOwnProperty.call(patch, 'error') ? patch.error : current.error,
|
|
705
|
+
last_message_id: null,
|
|
706
|
+
});
|
|
707
|
+
},
|
|
708
|
+
|
|
709
|
+
sweepExpired() {
|
|
710
|
+
const now = Date.now();
|
|
711
|
+
return db.transaction(() => {
|
|
712
|
+
const expired = S.findExpired.all(now);
|
|
713
|
+
for (const { id } of expired) {
|
|
714
|
+
S.setMsgStatus.run('dead_letter', id);
|
|
715
|
+
S.insertDL.run(id, 'ttl_expired', now, null);
|
|
716
|
+
}
|
|
717
|
+
const humanRequests = S.expireHR.run(now).changes;
|
|
718
|
+
return { messages: expired.length, human_requests: humanRequests };
|
|
719
|
+
})();
|
|
720
|
+
},
|
|
721
|
+
|
|
722
|
+
getQueueDepths() {
|
|
723
|
+
return {
|
|
724
|
+
urgent: S.urgentDepth.get().cnt,
|
|
725
|
+
normal: S.normalDepth.get().cnt,
|
|
726
|
+
dlq: S.dlqDepth.get().cnt,
|
|
727
|
+
};
|
|
728
|
+
},
|
|
729
|
+
|
|
730
|
+
onAssignStatusChange(listener) {
|
|
731
|
+
if (typeof listener !== 'function') {
|
|
732
|
+
return () => {};
|
|
733
|
+
}
|
|
734
|
+
assignStatusListeners.add(listener);
|
|
735
|
+
return () => {
|
|
736
|
+
assignStatusListeners.delete(listener);
|
|
737
|
+
};
|
|
738
|
+
},
|
|
739
|
+
|
|
740
|
+
getDeliveryStats() {
|
|
741
|
+
return {
|
|
742
|
+
total_deliveries: S.ackedRecent.get(Date.now()).cnt,
|
|
743
|
+
avg_delivery_ms: 0,
|
|
744
|
+
};
|
|
745
|
+
},
|
|
746
|
+
|
|
747
|
+
getHubStats() {
|
|
748
|
+
return {
|
|
749
|
+
online_agents: S.onlineCount.get().cnt,
|
|
750
|
+
total_messages: S.msgCount.get().cnt,
|
|
751
|
+
active_assign_jobs: S.activeAssignCount.get().cnt,
|
|
752
|
+
...store.getQueueDepths(),
|
|
753
|
+
};
|
|
754
|
+
},
|
|
755
|
+
|
|
756
|
+
getAuditStats() {
|
|
757
|
+
return {
|
|
758
|
+
online_agents: S.onlineCount.get().cnt,
|
|
759
|
+
total_messages: S.msgCount.get().cnt,
|
|
760
|
+
dlq: S.dlqDepth.get().cnt,
|
|
761
|
+
assign_queued: S.assignCountByStatus.get('queued').cnt,
|
|
762
|
+
assign_running: S.assignCountByStatus.get('running').cnt,
|
|
763
|
+
assign_failed: S.assignCountByStatus.get('failed').cnt,
|
|
764
|
+
assign_timed_out: S.assignCountByStatus.get('timed_out').cnt,
|
|
765
|
+
};
|
|
766
|
+
},
|
|
767
|
+
|
|
768
|
+
// --- Reflexion CRUD ---
|
|
769
|
+
|
|
770
|
+
addReflexion({
|
|
771
|
+
type = 'reflexion',
|
|
772
|
+
error_pattern,
|
|
773
|
+
error_message,
|
|
774
|
+
context = {},
|
|
775
|
+
solution,
|
|
776
|
+
solution_code = null,
|
|
777
|
+
adaptive_state = {},
|
|
778
|
+
confidence = 0.5,
|
|
779
|
+
hit_count = 1,
|
|
780
|
+
success_count = 0,
|
|
781
|
+
last_hit_ms,
|
|
782
|
+
created_at_ms,
|
|
783
|
+
updated_at_ms,
|
|
784
|
+
}) {
|
|
785
|
+
const now = Date.now();
|
|
786
|
+
const id = uuidv7();
|
|
787
|
+
S.insertReflexion.run({
|
|
788
|
+
id,
|
|
789
|
+
type,
|
|
790
|
+
error_pattern,
|
|
791
|
+
error_message,
|
|
792
|
+
context_json: JSON.stringify(context),
|
|
793
|
+
solution,
|
|
794
|
+
solution_code,
|
|
795
|
+
adaptive_state_json: JSON.stringify(adaptive_state),
|
|
796
|
+
confidence,
|
|
797
|
+
hit_count,
|
|
798
|
+
success_count,
|
|
799
|
+
last_hit_ms: last_hit_ms ?? now,
|
|
800
|
+
created_at_ms: created_at_ms ?? now,
|
|
801
|
+
updated_at_ms: updated_at_ms ?? now,
|
|
802
|
+
});
|
|
803
|
+
return store.getReflexion(id);
|
|
804
|
+
},
|
|
805
|
+
|
|
806
|
+
getReflexion(id) {
|
|
807
|
+
return parseReflexionRow(S.getReflexionById.get(id));
|
|
808
|
+
},
|
|
809
|
+
|
|
810
|
+
findReflexion(errorPattern, context = {}) {
|
|
811
|
+
const ctxKeys = Object.keys(context).filter(k => context[k] != null);
|
|
812
|
+
const ctxWhere = ctxKeys.map(k => ` AND json_extract(context_json, '$.${k}') = ?`).join('');
|
|
813
|
+
const ctxVals = ctxKeys.map(k => context[k]);
|
|
814
|
+
|
|
815
|
+
if (ctxKeys.length === 0) {
|
|
816
|
+
let rows = S.findReflexionExact.all(errorPattern);
|
|
817
|
+
if (rows.length) return rows.map(parseReflexionRow);
|
|
818
|
+
const escaped = errorPattern.replace(/[%_\\]/g, '\\$&');
|
|
819
|
+
rows = S.findReflexionLike.all(`%${escaped.slice(0, 100)}%`);
|
|
820
|
+
return rows.map(parseReflexionRow);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
const exactSql = `SELECT * FROM reflexion_entries WHERE error_pattern = ?${ctxWhere} ORDER BY confidence DESC`;
|
|
824
|
+
let rows = db.prepare(exactSql).all(errorPattern, ...ctxVals);
|
|
825
|
+
if (rows.length) return rows.map(parseReflexionRow);
|
|
826
|
+
|
|
827
|
+
const escaped = errorPattern.replace(/[%_\\]/g, '\\$&');
|
|
828
|
+
const likeSql = `SELECT * FROM reflexion_entries WHERE error_pattern LIKE ? ESCAPE '\\'${ctxWhere} ORDER BY confidence DESC LIMIT 10`;
|
|
829
|
+
rows = db.prepare(likeSql).all(`%${escaped.slice(0, 100)}%`, ...ctxVals);
|
|
830
|
+
return rows.map(parseReflexionRow);
|
|
831
|
+
},
|
|
832
|
+
|
|
833
|
+
updateReflexionHit(id, success = false) {
|
|
834
|
+
const now = Date.now();
|
|
835
|
+
if (success) {
|
|
836
|
+
S.updateReflexionHitSuccess.run(now, now, id);
|
|
837
|
+
} else {
|
|
838
|
+
S.updateReflexionHitOnly.run(now, now, id);
|
|
839
|
+
}
|
|
840
|
+
const entry = store.getReflexion(id);
|
|
841
|
+
if (entry && entry.hit_count > 0) {
|
|
842
|
+
const conf = recalcConfidence(entry);
|
|
843
|
+
S.updateReflexionConfidence.run(Math.max(0, Math.min(1, conf)), now, id);
|
|
844
|
+
}
|
|
845
|
+
return store.getReflexion(id);
|
|
846
|
+
},
|
|
847
|
+
|
|
848
|
+
listReflexion(filters = {}) {
|
|
849
|
+
const { type, minConfidence = Number.NEGATIVE_INFINITY, projectSlug } = filters;
|
|
850
|
+
return S.listReflexionEntries.all()
|
|
851
|
+
.map(parseReflexionRow)
|
|
852
|
+
.filter((entry) => !type || entry.type === type)
|
|
853
|
+
.filter((entry) => entry.confidence >= minConfidence)
|
|
854
|
+
.filter((entry) => !projectSlug || entry.adaptive_state?.project_slug === projectSlug);
|
|
855
|
+
},
|
|
856
|
+
|
|
857
|
+
patchReflexion(id, patch = {}) {
|
|
858
|
+
const current = store.getReflexion(id);
|
|
859
|
+
if (!current) return null;
|
|
860
|
+
const next = {
|
|
861
|
+
...current,
|
|
862
|
+
...patch,
|
|
863
|
+
context: patch.context ?? current.context,
|
|
864
|
+
adaptive_state: patch.adaptive_state ?? current.adaptive_state,
|
|
865
|
+
updated_at_ms: patch.updated_at_ms ?? Date.now(),
|
|
866
|
+
};
|
|
867
|
+
const sets = [
|
|
868
|
+
['type', next.type],
|
|
869
|
+
['error_pattern', next.error_pattern],
|
|
870
|
+
['error_message', next.error_message],
|
|
871
|
+
['context_json', JSON.stringify(next.context ?? {})],
|
|
872
|
+
['solution', next.solution],
|
|
873
|
+
['solution_code', next.solution_code ?? null],
|
|
874
|
+
['adaptive_state_json', JSON.stringify(next.adaptive_state ?? {})],
|
|
875
|
+
['confidence', next.confidence],
|
|
876
|
+
['hit_count', next.hit_count],
|
|
877
|
+
['success_count', next.success_count],
|
|
878
|
+
['last_hit_ms', next.last_hit_ms],
|
|
879
|
+
['updated_at_ms', next.updated_at_ms],
|
|
880
|
+
];
|
|
881
|
+
const sql = `UPDATE reflexion_entries SET ${sets.map(([key]) => `${key} = ?`).join(', ')} WHERE id = ?`;
|
|
882
|
+
db.prepare(sql).run(...sets.map(([, value]) => value), id);
|
|
883
|
+
return store.getReflexion(id);
|
|
884
|
+
},
|
|
885
|
+
|
|
886
|
+
deleteReflexion(id) {
|
|
887
|
+
return S.deleteReflexionEntry.run(id).changes > 0;
|
|
888
|
+
},
|
|
889
|
+
|
|
890
|
+
pruneReflexion(maxAge_ms = 30 * 24 * 3600 * 1000, minConfidence = 0.2) {
|
|
891
|
+
const cutoff = Date.now() - maxAge_ms;
|
|
892
|
+
return S.pruneReflexionEntries.run(cutoff, minConfidence).changes;
|
|
893
|
+
},
|
|
894
|
+
};
|
|
895
|
+
|
|
896
|
+
return store;
|
|
897
|
+
}
|