triflux 3.2.0-dev.1 → 3.2.0-dev.10
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/README.ko.md +26 -18
- package/README.md +26 -18
- package/bin/triflux.mjs +1614 -1084
- package/hooks/hooks.json +12 -0
- package/hooks/keyword-rules.json +354 -0
- package/hub/bridge.mjs +371 -193
- package/hub/hitl.mjs +45 -31
- package/hub/pipe.mjs +457 -0
- package/hub/router.mjs +422 -161
- package/hub/server.mjs +429 -344
- package/hub/store.mjs +388 -314
- package/hub/team/cli-team-common.mjs +348 -0
- package/hub/team/cli-team-control.mjs +393 -0
- package/hub/team/cli-team-start.mjs +516 -0
- package/hub/team/cli-team-status.mjs +269 -0
- package/hub/team/cli.mjs +99 -368
- package/hub/team/dashboard.mjs +165 -64
- package/hub/team/native-supervisor.mjs +300 -0
- package/hub/team/native.mjs +62 -0
- package/hub/team/nativeProxy.mjs +534 -0
- package/hub/team/orchestrator.mjs +99 -35
- package/hub/team/pane.mjs +138 -101
- package/hub/team/psmux.mjs +297 -0
- package/hub/team/session.mjs +608 -186
- package/hub/team/shared.mjs +13 -0
- package/hub/team/staleState.mjs +299 -0
- package/hub/tools.mjs +140 -53
- package/hub/workers/claude-worker.mjs +446 -0
- package/hub/workers/codex-mcp.mjs +414 -0
- package/hub/workers/factory.mjs +18 -0
- package/hub/workers/gemini-worker.mjs +349 -0
- package/hub/workers/interface.mjs +41 -0
- package/hud/hud-qos-status.mjs +1789 -1732
- package/package.json +6 -2
- package/scripts/__tests__/keyword-detector.test.mjs +234 -0
- package/scripts/hub-ensure.mjs +83 -0
- package/scripts/keyword-detector.mjs +272 -0
- package/scripts/keyword-rules-expander.mjs +521 -0
- package/scripts/lib/keyword-rules.mjs +168 -0
- package/scripts/psmux-steering-prototype.sh +368 -0
- package/scripts/run.cjs +62 -0
- package/scripts/setup.mjs +189 -7
- package/scripts/test-tfx-route-no-claude-native.mjs +49 -0
- package/scripts/tfx-route-worker.mjs +161 -0
- package/scripts/tfx-route.sh +943 -508
- package/skills/tfx-auto/SKILL.md +90 -564
- package/skills/tfx-auto-codex/SKILL.md +77 -0
- package/skills/tfx-codex/SKILL.md +1 -4
- package/skills/tfx-doctor/SKILL.md +1 -0
- package/skills/tfx-gemini/SKILL.md +1 -4
- package/skills/tfx-multi/SKILL.md +296 -0
- package/skills/tfx-setup/SKILL.md +1 -4
- package/skills/tfx-team/SKILL.md +0 -172
package/hub/store.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
// hub/store.mjs — SQLite
|
|
2
|
-
//
|
|
3
|
-
import Database from 'better-sqlite3';
|
|
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
|
-
|
|
1
|
+
// hub/store.mjs — SQLite 감사 로그/메타데이터 저장소
|
|
2
|
+
// 실시간 배달 큐는 router/pipe가 담당하고, SQLite는 재생/감사 용도로만 유지한다.
|
|
3
|
+
import Database from 'better-sqlite3';
|
|
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
|
+
|
|
9
9
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
10
|
let _rndPool = Buffer.alloc(0), _rndOff = 0;
|
|
11
11
|
|
|
@@ -19,64 +19,82 @@ function pooledRandom(n) {
|
|
|
19
19
|
return out;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
/** UUIDv7 생성 (RFC 9562) */
|
|
22
|
+
/** UUIDv7 생성 (RFC 9562, 단조 증가 보장) */
|
|
23
|
+
let _lastMs = 0n;
|
|
24
|
+
let _seq = 0;
|
|
23
25
|
export function uuidv7() {
|
|
24
|
-
|
|
26
|
+
let now = BigInt(Date.now());
|
|
27
|
+
if (now <= _lastMs) {
|
|
28
|
+
_seq++;
|
|
29
|
+
if (_seq > 0xfff) {
|
|
30
|
+
now = _lastMs + 1n;
|
|
31
|
+
_seq = 0;
|
|
32
|
+
}
|
|
33
|
+
} else {
|
|
34
|
+
_seq = 0;
|
|
35
|
+
}
|
|
36
|
+
_lastMs = now;
|
|
25
37
|
const buf = pooledRandom(16);
|
|
26
|
-
buf[0] = Number((now >> 40n) & 0xffn);
|
|
27
|
-
buf[1] = Number((now >> 32n) & 0xffn);
|
|
28
|
-
buf[2] = Number((now >> 24n) & 0xffn);
|
|
29
|
-
buf[3] = Number((now >> 16n) & 0xffn);
|
|
30
|
-
buf[4] = Number((now >> 8n) & 0xffn);
|
|
31
|
-
buf[5] = Number(now & 0xffn);
|
|
32
|
-
buf[6] = (
|
|
33
|
-
buf[
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function parseHumanRequestRow(row) {
|
|
56
|
-
if (!row) return null;
|
|
57
|
-
const { schema_json, response_json, ...rest } = row;
|
|
58
|
-
return { ...rest, schema: parseJson(schema_json, {}), response: parseJson(response_json, null) };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* 상태 저장소 생성
|
|
63
|
-
* @param {string} dbPath — SQLite DB 파일 경로
|
|
64
|
-
*/
|
|
65
|
-
export function createStore(dbPath) {
|
|
66
|
-
mkdirSync(dirname(dbPath), { recursive: true });
|
|
67
|
-
const db = new Database(dbPath);
|
|
68
|
-
|
|
69
|
-
// PRAGMA
|
|
70
|
-
db.pragma('journal_mode = WAL');
|
|
71
|
-
db.pragma('synchronous = NORMAL');
|
|
72
|
-
db.pragma('foreign_keys = ON');
|
|
73
|
-
db.pragma('busy_timeout = 5000');
|
|
74
|
-
db.pragma('wal_autocheckpoint = 1000');
|
|
75
|
-
|
|
76
|
-
// 스키마 초기화 (schema.sql 전체 실행 — 주석 포함 안전 처리)
|
|
77
|
-
const schemaSQL = readFileSync(join(__dirname, 'schema.sql'), 'utf8');
|
|
38
|
+
buf[0] = Number((now >> 40n) & 0xffn);
|
|
39
|
+
buf[1] = Number((now >> 32n) & 0xffn);
|
|
40
|
+
buf[2] = Number((now >> 24n) & 0xffn);
|
|
41
|
+
buf[3] = Number((now >> 16n) & 0xffn);
|
|
42
|
+
buf[4] = Number((now >> 8n) & 0xffn);
|
|
43
|
+
buf[5] = Number(now & 0xffn);
|
|
44
|
+
buf[6] = ((_seq >> 8) & 0x0f) | 0x70;
|
|
45
|
+
buf[7] = _seq & 0xff;
|
|
46
|
+
buf[8] = (buf[8] & 0x3f) | 0x80;
|
|
47
|
+
const h = buf.toString('hex');
|
|
48
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseJson(str, fallback = null) {
|
|
52
|
+
if (str == null) return fallback;
|
|
53
|
+
try { return JSON.parse(str); } catch { return fallback; }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parseAgentRow(row) {
|
|
57
|
+
if (!row) return null;
|
|
58
|
+
const { capabilities_json, topics_json, metadata_json, ...rest } = row;
|
|
59
|
+
return {
|
|
60
|
+
...rest,
|
|
61
|
+
capabilities: parseJson(capabilities_json, []),
|
|
62
|
+
topics: parseJson(topics_json, []),
|
|
63
|
+
metadata: parseJson(metadata_json, {}),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
78
66
|
|
|
79
|
-
|
|
67
|
+
function parseMessageRow(row) {
|
|
68
|
+
if (!row) return null;
|
|
69
|
+
const { payload_json, ...rest } = row;
|
|
70
|
+
return { ...rest, payload: parseJson(payload_json, {}) };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseHumanRequestRow(row) {
|
|
74
|
+
if (!row) return null;
|
|
75
|
+
const { schema_json, response_json, ...rest } = row;
|
|
76
|
+
return {
|
|
77
|
+
...rest,
|
|
78
|
+
schema: parseJson(schema_json, {}),
|
|
79
|
+
response: parseJson(response_json, null),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 저장소 생성
|
|
85
|
+
* @param {string} dbPath
|
|
86
|
+
*/
|
|
87
|
+
export function createStore(dbPath) {
|
|
88
|
+
mkdirSync(dirname(dbPath), { recursive: true });
|
|
89
|
+
const db = new Database(dbPath);
|
|
90
|
+
|
|
91
|
+
db.pragma('journal_mode = WAL');
|
|
92
|
+
db.pragma('synchronous = NORMAL');
|
|
93
|
+
db.pragma('foreign_keys = ON');
|
|
94
|
+
db.pragma('busy_timeout = 5000');
|
|
95
|
+
db.pragma('wal_autocheckpoint = 1000');
|
|
96
|
+
|
|
97
|
+
const schemaSQL = readFileSync(join(__dirname, 'schema.sql'), 'utf8');
|
|
80
98
|
db.exec("CREATE TABLE IF NOT EXISTS _meta (key TEXT PRIMARY KEY, value TEXT)");
|
|
81
99
|
const SCHEMA_VERSION = '1';
|
|
82
100
|
const curVer = (() => {
|
|
@@ -87,254 +105,310 @@ export function createStore(dbPath) {
|
|
|
87
105
|
db.exec(schemaSQL);
|
|
88
106
|
db.prepare("INSERT OR REPLACE INTO _meta (key, value) VALUES ('schema_version', ?)").run(SCHEMA_VERSION);
|
|
89
107
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
lease_expires_ms=excluded.lease_expires_ms,
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
refreshLease(agentId, ttlMs = 30000) {
|
|
192
|
-
const now = Date.now();
|
|
193
|
-
S.heartbeat.run(now, now + ttlMs, agentId);
|
|
194
|
-
return { agent_id: agentId, lease_expires_ms: now + ttlMs };
|
|
195
|
-
},
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
return
|
|
204
|
-
},
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
},
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
return
|
|
262
|
-
},
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
return
|
|
332
|
-
},
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
return
|
|
336
|
-
},
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
108
|
+
|
|
109
|
+
const S = {
|
|
110
|
+
upsertAgent: db.prepare(`
|
|
111
|
+
INSERT INTO agents (agent_id, cli, pid, capabilities_json, topics_json, last_seen_ms, lease_expires_ms, status, metadata_json)
|
|
112
|
+
VALUES (@agent_id, @cli, @pid, @capabilities_json, @topics_json, @last_seen_ms, @lease_expires_ms, @status, @metadata_json)
|
|
113
|
+
ON CONFLICT(agent_id) DO UPDATE SET
|
|
114
|
+
cli=excluded.cli,
|
|
115
|
+
pid=excluded.pid,
|
|
116
|
+
capabilities_json=excluded.capabilities_json,
|
|
117
|
+
topics_json=excluded.topics_json,
|
|
118
|
+
last_seen_ms=excluded.last_seen_ms,
|
|
119
|
+
lease_expires_ms=excluded.lease_expires_ms,
|
|
120
|
+
status=excluded.status,
|
|
121
|
+
metadata_json=excluded.metadata_json`),
|
|
122
|
+
getAgent: db.prepare('SELECT * FROM agents WHERE agent_id = ?'),
|
|
123
|
+
setAgentTopics: db.prepare('UPDATE agents SET topics_json=?, last_seen_ms=? WHERE agent_id=?'),
|
|
124
|
+
heartbeat: db.prepare("UPDATE agents SET last_seen_ms=?, lease_expires_ms=?, status='online' WHERE agent_id=?"),
|
|
125
|
+
setAgentStatus: db.prepare('UPDATE agents SET status=? WHERE agent_id=?'),
|
|
126
|
+
onlineAgents: db.prepare("SELECT * FROM agents WHERE status != 'offline'"),
|
|
127
|
+
allAgents: db.prepare('SELECT * FROM agents'),
|
|
128
|
+
agentsByTopic: db.prepare("SELECT a.* FROM agents a, json_each(a.topics_json) t WHERE t.value=? AND a.status != 'offline'"),
|
|
129
|
+
markStale: db.prepare("UPDATE agents SET status='stale' WHERE status='online' AND lease_expires_ms < ?"),
|
|
130
|
+
markOffline: db.prepare("UPDATE agents SET status='offline' WHERE status='stale' AND lease_expires_ms < ? - 300000"),
|
|
131
|
+
|
|
132
|
+
insertAuditMessage: db.prepare(`
|
|
133
|
+
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)
|
|
134
|
+
VALUES (@id, @type, @from_agent, @to_agent, @topic, @priority, @ttl_ms, @created_at_ms, @expires_at_ms, @correlation_id, @trace_id, @payload_json, @status)`),
|
|
135
|
+
getMsg: db.prepare('SELECT * FROM messages WHERE id=?'),
|
|
136
|
+
getResponse: db.prepare("SELECT * FROM messages WHERE correlation_id=? AND type='response' ORDER BY created_at_ms DESC LIMIT 1"),
|
|
137
|
+
getMsgsByTrace: db.prepare('SELECT * FROM messages WHERE trace_id=? ORDER BY created_at_ms'),
|
|
138
|
+
setMsgStatus: db.prepare('UPDATE messages SET status=? WHERE id=?'),
|
|
139
|
+
recentAgentMessages: db.prepare(`
|
|
140
|
+
SELECT * FROM messages
|
|
141
|
+
WHERE to_agent=?
|
|
142
|
+
ORDER BY created_at_ms DESC
|
|
143
|
+
LIMIT ?`),
|
|
144
|
+
recentAgentMessagesWithTopics: db.prepare(`
|
|
145
|
+
SELECT * FROM messages
|
|
146
|
+
WHERE to_agent=?
|
|
147
|
+
OR (
|
|
148
|
+
substr(to_agent, 1, 6)='topic:'
|
|
149
|
+
AND topic IN (SELECT value FROM json_each(?))
|
|
150
|
+
)
|
|
151
|
+
ORDER BY created_at_ms DESC
|
|
152
|
+
LIMIT ?`),
|
|
153
|
+
|
|
154
|
+
insertHR: db.prepare(`
|
|
155
|
+
INSERT INTO human_requests (request_id, requester_agent, kind, prompt, schema_json, state, deadline_ms, default_action, correlation_id, trace_id, response_json)
|
|
156
|
+
VALUES (@request_id, @requester_agent, @kind, @prompt, @schema_json, @state, @deadline_ms, @default_action, @correlation_id, @trace_id, @response_json)`),
|
|
157
|
+
getHR: db.prepare('SELECT * FROM human_requests WHERE request_id=?'),
|
|
158
|
+
updateHR: db.prepare('UPDATE human_requests SET state=?, response_json=? WHERE request_id=?'),
|
|
159
|
+
pendingHR: db.prepare("SELECT * FROM human_requests WHERE state='pending'"),
|
|
160
|
+
expireHR: db.prepare("UPDATE human_requests SET state='timed_out' WHERE state='pending' AND deadline_ms < ?"),
|
|
161
|
+
|
|
162
|
+
insertDL: db.prepare('INSERT OR REPLACE INTO dead_letters (message_id, reason, failed_at_ms, last_error) VALUES (?,?,?,?)'),
|
|
163
|
+
getDL: db.prepare('SELECT * FROM dead_letters ORDER BY failed_at_ms DESC LIMIT ?'),
|
|
164
|
+
|
|
165
|
+
findExpired: db.prepare("SELECT id FROM messages WHERE status='queued' AND expires_at_ms < ?"),
|
|
166
|
+
urgentDepth: db.prepare("SELECT COUNT(*) as cnt FROM messages WHERE status='queued' AND priority >= 7"),
|
|
167
|
+
normalDepth: db.prepare("SELECT COUNT(*) as cnt FROM messages WHERE status='queued' AND priority < 7"),
|
|
168
|
+
onlineCount: db.prepare("SELECT COUNT(*) as cnt FROM agents WHERE status='online'"),
|
|
169
|
+
msgCount: db.prepare('SELECT COUNT(*) as cnt FROM messages'),
|
|
170
|
+
dlqDepth: db.prepare('SELECT COUNT(*) as cnt FROM dead_letters'),
|
|
171
|
+
ackedRecent: db.prepare("SELECT COUNT(*) as cnt FROM messages WHERE status='acked' AND created_at_ms > ? - 300000"),
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
function clampMaxMessages(value, fallback = 20) {
|
|
175
|
+
const num = Number(value);
|
|
176
|
+
if (!Number.isFinite(num)) return fallback;
|
|
177
|
+
return Math.max(1, Math.min(Math.trunc(num), 100));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const store = {
|
|
181
|
+
db,
|
|
182
|
+
uuidv7,
|
|
183
|
+
|
|
184
|
+
close() {
|
|
185
|
+
db.close();
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
registerAgent({ agent_id, cli, pid, capabilities = [], topics = [], heartbeat_ttl_ms = 30000, metadata = {} }) {
|
|
189
|
+
const now = Date.now();
|
|
190
|
+
const leaseExpires = now + heartbeat_ttl_ms;
|
|
191
|
+
S.upsertAgent.run({
|
|
192
|
+
agent_id,
|
|
193
|
+
cli,
|
|
194
|
+
pid: pid ?? null,
|
|
195
|
+
capabilities_json: JSON.stringify(capabilities),
|
|
196
|
+
topics_json: JSON.stringify(topics),
|
|
197
|
+
last_seen_ms: now,
|
|
198
|
+
lease_expires_ms: leaseExpires,
|
|
199
|
+
status: 'online',
|
|
200
|
+
metadata_json: JSON.stringify(metadata),
|
|
201
|
+
});
|
|
202
|
+
return { agent_id, lease_id: uuidv7(), lease_expires_ms: leaseExpires, server_time_ms: now };
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
getAgent(id) {
|
|
206
|
+
return parseAgentRow(S.getAgent.get(id));
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
refreshLease(agentId, ttlMs = 30000) {
|
|
210
|
+
const now = Date.now();
|
|
211
|
+
S.heartbeat.run(now, now + ttlMs, agentId);
|
|
212
|
+
return { agent_id: agentId, lease_expires_ms: now + ttlMs, server_time_ms: now };
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
updateAgentTopics(agentId, topics = []) {
|
|
216
|
+
const now = Date.now();
|
|
217
|
+
return S.setAgentTopics.run(JSON.stringify(topics), now, agentId).changes > 0;
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
listOnlineAgents() {
|
|
221
|
+
return S.onlineAgents.all().map(parseAgentRow);
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
listAllAgents() {
|
|
225
|
+
return S.allAgents.all().map(parseAgentRow);
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
getAgentsByTopic(topic) {
|
|
229
|
+
return S.agentsByTopic.all(topic).map(parseAgentRow);
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
sweepStaleAgents() {
|
|
233
|
+
const now = Date.now();
|
|
234
|
+
return {
|
|
235
|
+
stale: S.markStale.run(now).changes,
|
|
236
|
+
offline: S.markOffline.run(now).changes,
|
|
237
|
+
};
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
updateAgentStatus(agentId, status) {
|
|
241
|
+
return S.setAgentStatus.run(status, agentId).changes > 0;
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
auditLog({ type, from, to, topic, priority = 5, ttl_ms = 300000, payload = {}, trace_id, correlation_id, status = 'queued' }) {
|
|
245
|
+
const now = Date.now();
|
|
246
|
+
const row = {
|
|
247
|
+
id: uuidv7(),
|
|
248
|
+
type,
|
|
249
|
+
from_agent: from,
|
|
250
|
+
to_agent: to,
|
|
251
|
+
topic,
|
|
252
|
+
priority,
|
|
253
|
+
ttl_ms,
|
|
254
|
+
created_at_ms: now,
|
|
255
|
+
expires_at_ms: now + ttl_ms,
|
|
256
|
+
correlation_id: correlation_id || uuidv7(),
|
|
257
|
+
trace_id: trace_id || uuidv7(),
|
|
258
|
+
payload_json: JSON.stringify(payload),
|
|
259
|
+
status,
|
|
260
|
+
};
|
|
261
|
+
S.insertAuditMessage.run(row);
|
|
262
|
+
return { ...row, payload };
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
// 하위 호환: 기존 enqueueMessage 호출은 auditLog로 위임한다.
|
|
266
|
+
enqueueMessage(args) {
|
|
267
|
+
return store.auditLog(args);
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
getMessage(id) {
|
|
271
|
+
return parseMessageRow(S.getMsg.get(id));
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
getResponseByCorrelation(cid) {
|
|
275
|
+
return parseMessageRow(S.getResponse.get(cid));
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
getMessagesByTrace(tid) {
|
|
279
|
+
return S.getMsgsByTrace.all(tid).map(parseMessageRow);
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
updateMessageStatus(id, status) {
|
|
283
|
+
return S.setMsgStatus.run(status, id).changes > 0;
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
getAuditMessagesForAgent(agentId, { max_messages = 20, include_topics = null } = {}) {
|
|
287
|
+
const limit = clampMaxMessages(max_messages);
|
|
288
|
+
const topics = Array.isArray(include_topics) && include_topics.length
|
|
289
|
+
? include_topics
|
|
290
|
+
: (store.getAgent(agentId)?.topics || []);
|
|
291
|
+
|
|
292
|
+
const rows = topics.length
|
|
293
|
+
? S.recentAgentMessagesWithTopics.all(agentId, JSON.stringify(topics), limit)
|
|
294
|
+
: S.recentAgentMessages.all(agentId, limit);
|
|
295
|
+
|
|
296
|
+
return rows.map(parseMessageRow);
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
// 하위 호환: 실시간 수신함 대신 감사 로그 재생 결과를 반환한다.
|
|
300
|
+
deliverToAgent(messageId, agentId) {
|
|
301
|
+
return !!store.getMessage(messageId) && !!agentId;
|
|
302
|
+
},
|
|
303
|
+
|
|
304
|
+
deliverToTopic(messageId, topic) {
|
|
305
|
+
void messageId;
|
|
306
|
+
return store.getAgentsByTopic(topic).length;
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
pollForAgent(agentId, { max_messages = 20, include_topics = null } = {}) {
|
|
310
|
+
return store.getAuditMessagesForAgent(agentId, {
|
|
311
|
+
max_messages,
|
|
312
|
+
include_topics,
|
|
313
|
+
});
|
|
314
|
+
},
|
|
315
|
+
|
|
316
|
+
ackMessages() {
|
|
317
|
+
return 0;
|
|
318
|
+
},
|
|
319
|
+
|
|
320
|
+
insertHumanRequest({ requester_agent, kind, prompt, requested_schema = {}, deadline_ms, default_action, correlation_id, trace_id }) {
|
|
321
|
+
const requestId = uuidv7();
|
|
322
|
+
const now = Date.now();
|
|
323
|
+
const deadlineAt = now + deadline_ms;
|
|
324
|
+
S.insertHR.run({
|
|
325
|
+
request_id: requestId,
|
|
326
|
+
requester_agent,
|
|
327
|
+
kind,
|
|
328
|
+
prompt,
|
|
329
|
+
schema_json: JSON.stringify(requested_schema),
|
|
330
|
+
state: 'pending',
|
|
331
|
+
deadline_ms: deadlineAt,
|
|
332
|
+
default_action,
|
|
333
|
+
correlation_id: correlation_id || uuidv7(),
|
|
334
|
+
trace_id: trace_id || uuidv7(),
|
|
335
|
+
response_json: null,
|
|
336
|
+
});
|
|
337
|
+
return { request_id: requestId, state: 'pending', deadline_ms: deadlineAt };
|
|
338
|
+
},
|
|
339
|
+
|
|
340
|
+
getHumanRequest(id) {
|
|
341
|
+
return parseHumanRequestRow(S.getHR.get(id));
|
|
342
|
+
},
|
|
343
|
+
|
|
344
|
+
updateHumanRequest(id, state, resp = null) {
|
|
345
|
+
return S.updateHR.run(state, resp ? JSON.stringify(resp) : null, id).changes > 0;
|
|
346
|
+
},
|
|
347
|
+
|
|
348
|
+
getPendingHumanRequests() {
|
|
349
|
+
return S.pendingHR.all().map(parseHumanRequestRow);
|
|
350
|
+
},
|
|
351
|
+
|
|
352
|
+
expireHumanRequests() {
|
|
353
|
+
return S.expireHR.run(Date.now()).changes;
|
|
354
|
+
},
|
|
355
|
+
|
|
356
|
+
moveToDeadLetter(messageId, reason, lastError = null) {
|
|
357
|
+
db.transaction(() => {
|
|
358
|
+
S.setMsgStatus.run('dead_letter', messageId);
|
|
359
|
+
S.insertDL.run(messageId, reason, Date.now(), lastError);
|
|
360
|
+
})();
|
|
361
|
+
return true;
|
|
362
|
+
},
|
|
363
|
+
|
|
364
|
+
getDeadLetters(limit = 50) {
|
|
365
|
+
return S.getDL.all(limit);
|
|
366
|
+
},
|
|
367
|
+
|
|
368
|
+
sweepExpired() {
|
|
369
|
+
const now = Date.now();
|
|
370
|
+
return db.transaction(() => {
|
|
371
|
+
const expired = S.findExpired.all(now);
|
|
372
|
+
for (const { id } of expired) {
|
|
373
|
+
S.setMsgStatus.run('dead_letter', id);
|
|
374
|
+
S.insertDL.run(id, 'ttl_expired', now, null);
|
|
375
|
+
}
|
|
376
|
+
const humanRequests = S.expireHR.run(now).changes;
|
|
377
|
+
return { messages: expired.length, human_requests: humanRequests };
|
|
378
|
+
})();
|
|
379
|
+
},
|
|
380
|
+
|
|
381
|
+
getQueueDepths() {
|
|
382
|
+
return {
|
|
383
|
+
urgent: S.urgentDepth.get().cnt,
|
|
384
|
+
normal: S.normalDepth.get().cnt,
|
|
385
|
+
dlq: S.dlqDepth.get().cnt,
|
|
386
|
+
};
|
|
387
|
+
},
|
|
388
|
+
|
|
389
|
+
getDeliveryStats() {
|
|
390
|
+
return {
|
|
391
|
+
total_deliveries: S.ackedRecent.get(Date.now()).cnt,
|
|
392
|
+
avg_delivery_ms: 0,
|
|
393
|
+
};
|
|
394
|
+
},
|
|
395
|
+
|
|
396
|
+
getHubStats() {
|
|
397
|
+
return {
|
|
398
|
+
online_agents: S.onlineCount.get().cnt,
|
|
399
|
+
total_messages: S.msgCount.get().cnt,
|
|
400
|
+
...store.getQueueDepths(),
|
|
401
|
+
};
|
|
402
|
+
},
|
|
403
|
+
|
|
404
|
+
getAuditStats() {
|
|
405
|
+
return {
|
|
406
|
+
online_agents: S.onlineCount.get().cnt,
|
|
407
|
+
total_messages: S.msgCount.get().cnt,
|
|
408
|
+
dlq: S.dlqDepth.get().cnt,
|
|
409
|
+
};
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
return store;
|
|
414
|
+
}
|