triflux 10.43.0 → 10.44.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/README.md +2 -0
- package/adapters/codex/skills/tfx-harness/SKILL.md +30 -0
- package/adapters/codex/skills/tfx-harness/skill.json +5 -0
- package/bin/triflux.mjs +14 -0
- package/hooks/keyword-rules.json +139 -19
- package/hub/cli-adapter-base.mjs +11 -2
- package/hub/codex-adapter.mjs +3 -0
- package/hub/intent.mjs +24 -32
- package/hub/router.mjs +272 -0
- package/hub/schema.sql +61 -2
- package/hub/server.mjs +42 -0
- package/hub/store.mjs +577 -5
- package/hub/team/conductor.mjs +23 -2
- package/hub/team/execution-mode.mjs +12 -2
- package/hub/team/headless.mjs +16 -0
- package/hub/team/retry-state-machine.mjs +8 -3
- package/hub/team/swarm-hypervisor.mjs +1 -0
- package/hub/team/swarm-preflight.mjs +47 -0
- package/hub/team/synapse-http.mjs +149 -4
- package/hub/team/worktree-lifecycle.mjs +56 -59
- package/hub/tools.mjs +11 -0
- package/hub/workers/codex-mcp.mjs +20 -4
- package/hub/workers/delegator-mcp.mjs +3 -49
- package/package.json +2 -1
- package/scripts/__tests__/lint-skills.test.mjs +68 -0
- package/scripts/__tests__/tfx-route-node-entry.test.mjs +36 -0
- package/scripts/keyword-detector.mjs +55 -8
- package/scripts/lib/agent-route-policy.mjs +360 -0
- package/scripts/lib/cli-codex.mjs +22 -160
- package/scripts/lib/codex-profile-config.mjs +29 -3
- package/scripts/lib/keyword-rules.mjs +12 -0
- package/scripts/lint-skills.mjs +65 -0
- package/scripts/pack.mjs +13 -0
- package/scripts/release/check-packages-mirror.mjs +5 -0
- package/scripts/setup.mjs +94 -2
- package/scripts/tfx-route.mjs +14 -1
- package/scripts/tfx-route.sh +233 -70
- package/skills/tfx-harness/SKILL.md +19 -76
- package/skills/tfx-hub/SKILL.md +1 -1
- package/skills/tfx-interview/SKILL.md +3 -1
- package/skills/tfx-interview/skill.json +1 -13
- package/skills/tfx-profile/SKILL.md +25 -15
- package/skills/tfx-prune/SKILL.md +3 -1
- package/skills/tfx-prune/skill.json +1 -8
- package/skills/tfx-setup/SKILL.md +1 -1
- package/tui/codex-profile.mjs +11 -2
- package/tui/setup.mjs +2 -0
package/hub/store.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// hub/store.mjs — SQLite 감사
|
|
2
|
-
// 실시간
|
|
1
|
+
// hub/store.mjs — SQLite 감사 로그/메타데이터/역할 레지스트리 저장소
|
|
2
|
+
// 실시간 배달은 router/pipe가 담당하고, 역할 권한과 복구 상태는 SQLite가 보존한다.
|
|
3
3
|
|
|
4
4
|
import { mkdirSync, readFileSync } from "node:fs";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from "./lib/timeout-defaults.mjs";
|
|
13
13
|
import { uuidv7 } from "./lib/uuidv7.mjs";
|
|
14
14
|
import { recalcConfidence } from "./reflexion.mjs";
|
|
15
|
+
import { decodeRoleKey, ROLE_REACHABILITY_STATES } from "./role-contract.mjs";
|
|
15
16
|
|
|
16
17
|
export { uuidv7 };
|
|
17
18
|
|
|
@@ -76,6 +77,21 @@ function parseReflexionRow(row) {
|
|
|
76
77
|
};
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
function parseRoleCandidateRow(row) {
|
|
81
|
+
if (!row) return null;
|
|
82
|
+
const { transport_locators_json, ...rest } = row;
|
|
83
|
+
return {
|
|
84
|
+
...rest,
|
|
85
|
+
transport_locators: parseJson(transport_locators_json, []),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function hasTable(db, tableName) {
|
|
90
|
+
return !!db
|
|
91
|
+
.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?")
|
|
92
|
+
.get(tableName);
|
|
93
|
+
}
|
|
94
|
+
|
|
79
95
|
function hasColumn(db, tableName, columnName) {
|
|
80
96
|
const rows = db.prepare(`PRAGMA table_info(${tableName})`).all();
|
|
81
97
|
return rows.some((row) => row.name === columnName);
|
|
@@ -86,6 +102,23 @@ function ensureColumn(db, tableName, columnName, definition) {
|
|
|
86
102
|
db.exec(`ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${definition}`);
|
|
87
103
|
}
|
|
88
104
|
|
|
105
|
+
function ensureV6Columns(db) {
|
|
106
|
+
if (hasTable(db, "agents")) {
|
|
107
|
+
ensureColumn(db, "agents", "project_id", "TEXT");
|
|
108
|
+
ensureColumn(db, "agents", "session_id", "TEXT");
|
|
109
|
+
ensureColumn(db, "agents", "host_id", "TEXT");
|
|
110
|
+
ensureColumn(
|
|
111
|
+
db,
|
|
112
|
+
"agents",
|
|
113
|
+
"transport_locators_json",
|
|
114
|
+
"TEXT NOT NULL DEFAULT '[]'",
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
if (hasTable(db, "messages")) {
|
|
118
|
+
ensureColumn(db, "messages", "role_key_wire", "TEXT");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
89
122
|
/**
|
|
90
123
|
* 저장소 생성
|
|
91
124
|
* @param {string} dbPath
|
|
@@ -116,7 +149,7 @@ export function createStore(dbPath, options = {}) {
|
|
|
116
149
|
db.exec(
|
|
117
150
|
"CREATE TABLE IF NOT EXISTS _meta (key TEXT PRIMARY KEY, value TEXT)",
|
|
118
151
|
);
|
|
119
|
-
const SCHEMA_VERSION = "
|
|
152
|
+
const SCHEMA_VERSION = "6";
|
|
120
153
|
const curVer = (() => {
|
|
121
154
|
try {
|
|
122
155
|
return db
|
|
@@ -130,6 +163,9 @@ export function createStore(dbPath, options = {}) {
|
|
|
130
163
|
// 마이그레이션 전략: 스키마 버전이 다르면 schema.sql을 재실행한다.
|
|
131
164
|
// schema.sql은 CREATE TABLE IF NOT EXISTS 패턴을 사용하므로 멱등하게 적용된다.
|
|
132
165
|
// 비파괴적 컬럼 추가는 자동으로 처리되지만, 컬럼 제거/이름 변경은 수동 마이그레이션이 필요하다.
|
|
166
|
+
// v5 테이블에는 schema.sql의 v6 인덱스가 참조하는 컬럼이 없으므로
|
|
167
|
+
// CREATE IF NOT EXISTS 재실행 전에 멱등 컬럼 seam을 먼저 적용한다.
|
|
168
|
+
ensureV6Columns(db);
|
|
133
169
|
if (curVer !== SCHEMA_VERSION) {
|
|
134
170
|
if (curVer != null) {
|
|
135
171
|
// 이미 버전이 기록된 DB에서 버전 불일치가 발생한 경우 경고한다.
|
|
@@ -142,6 +178,8 @@ export function createStore(dbPath, options = {}) {
|
|
|
142
178
|
"INSERT OR REPLACE INTO _meta (key, value) VALUES ('schema_version', ?)",
|
|
143
179
|
).run(SCHEMA_VERSION);
|
|
144
180
|
}
|
|
181
|
+
// 매 부팅 실행해 부분 적용된 마이그레이션도 안전하게 복구한다.
|
|
182
|
+
ensureV6Columns(db);
|
|
145
183
|
ensureColumn(
|
|
146
184
|
db,
|
|
147
185
|
"reflexion_entries",
|
|
@@ -189,9 +227,160 @@ export function createStore(dbPath, options = {}) {
|
|
|
189
227
|
"UPDATE agents SET status='offline' WHERE status='stale' AND lease_expires_ms < ? - 300000",
|
|
190
228
|
),
|
|
191
229
|
|
|
230
|
+
getRole: db.prepare("SELECT * FROM role_registry WHERE role_key_wire = ?"),
|
|
231
|
+
insertRoleReservation: db.prepare(`
|
|
232
|
+
INSERT INTO role_registry (
|
|
233
|
+
role_key_wire, project_id, role_kind, scope_id, state,
|
|
234
|
+
holder_agent_id, previous_holder_agent_id, epoch, activation_seq,
|
|
235
|
+
activation_id, activation_deadline_ms, holder_lease_expires_ms,
|
|
236
|
+
charter_version, charter_acked_epoch, retry_count, next_probe_ms,
|
|
237
|
+
blocked_reason, last_transition_ms, last_reason, version
|
|
238
|
+
) VALUES (
|
|
239
|
+
@role_key_wire, @project_id, @role_kind, @scope_id, @state,
|
|
240
|
+
@holder_agent_id, @previous_holder_agent_id, @epoch, @activation_seq,
|
|
241
|
+
@activation_id, @activation_deadline_ms, @holder_lease_expires_ms,
|
|
242
|
+
@charter_version, @charter_acked_epoch, @retry_count, @next_probe_ms,
|
|
243
|
+
@blocked_reason, @last_transition_ms, @last_reason, @version
|
|
244
|
+
)`),
|
|
245
|
+
updateRoleReservation: db.prepare(`
|
|
246
|
+
UPDATE role_registry SET
|
|
247
|
+
state=@state,
|
|
248
|
+
holder_agent_id=@holder_agent_id,
|
|
249
|
+
previous_holder_agent_id=@previous_holder_agent_id,
|
|
250
|
+
epoch=@epoch,
|
|
251
|
+
activation_seq=@activation_seq,
|
|
252
|
+
activation_id=@activation_id,
|
|
253
|
+
activation_deadline_ms=@activation_deadline_ms,
|
|
254
|
+
holder_lease_expires_ms=@holder_lease_expires_ms,
|
|
255
|
+
charter_version=@charter_version,
|
|
256
|
+
charter_acked_epoch=@charter_acked_epoch,
|
|
257
|
+
retry_count=@retry_count,
|
|
258
|
+
next_probe_ms=@next_probe_ms,
|
|
259
|
+
blocked_reason=@blocked_reason,
|
|
260
|
+
last_transition_ms=@last_transition_ms,
|
|
261
|
+
last_reason=@last_reason,
|
|
262
|
+
version=version + 1
|
|
263
|
+
WHERE role_key_wire=@role_key_wire
|
|
264
|
+
AND version=@expected_version
|
|
265
|
+
AND epoch=@expected_epoch`),
|
|
266
|
+
compareAndSetRole: db.prepare(`
|
|
267
|
+
UPDATE role_registry SET
|
|
268
|
+
state=@state,
|
|
269
|
+
holder_agent_id=@holder_agent_id,
|
|
270
|
+
previous_holder_agent_id=@previous_holder_agent_id,
|
|
271
|
+
activation_id=@activation_id,
|
|
272
|
+
activation_deadline_ms=@activation_deadline_ms,
|
|
273
|
+
holder_lease_expires_ms=@holder_lease_expires_ms,
|
|
274
|
+
charter_version=@charter_version,
|
|
275
|
+
charter_acked_epoch=@charter_acked_epoch,
|
|
276
|
+
retry_count=@retry_count,
|
|
277
|
+
next_probe_ms=@next_probe_ms,
|
|
278
|
+
blocked_reason=@blocked_reason,
|
|
279
|
+
last_transition_ms=@last_transition_ms,
|
|
280
|
+
last_reason=@last_reason,
|
|
281
|
+
version=version + 1
|
|
282
|
+
WHERE role_key_wire=@role_key_wire
|
|
283
|
+
AND epoch=@expected_epoch
|
|
284
|
+
AND activation_seq=@expected_activation_seq
|
|
285
|
+
AND activation_id IS @expected_activation_id
|
|
286
|
+
AND version=@expected_version`),
|
|
287
|
+
listRoles: db.prepare("SELECT * FROM role_registry ORDER BY role_key_wire"),
|
|
288
|
+
expiredHolderRoles: db.prepare(`
|
|
289
|
+
SELECT * FROM role_registry
|
|
290
|
+
WHERE holder_agent_id IS NOT NULL
|
|
291
|
+
AND (holder_lease_expires_ms IS NULL OR holder_lease_expires_ms <= ?)
|
|
292
|
+
ORDER BY role_key_wire`),
|
|
293
|
+
listRoleCandidates: db.prepare(`
|
|
294
|
+
SELECT * FROM role_candidates
|
|
295
|
+
WHERE role_key_wire = ?
|
|
296
|
+
ORDER BY priority DESC, updated_at_ms DESC, agent_id`),
|
|
297
|
+
getRoleCandidate: db.prepare(`
|
|
298
|
+
SELECT * FROM role_candidates
|
|
299
|
+
WHERE role_key_wire = ? AND agent_id = ?`),
|
|
300
|
+
upsertRoleCandidate: db.prepare(`
|
|
301
|
+
INSERT INTO role_candidates (
|
|
302
|
+
role_key_wire, agent_id, source, priority, transport_locators_json,
|
|
303
|
+
consecutive_probe_failures, excluded_until_ms, last_probe_ms,
|
|
304
|
+
last_probe_code, updated_at_ms
|
|
305
|
+
) VALUES (
|
|
306
|
+
@role_key_wire, @agent_id, @source, @priority,
|
|
307
|
+
@transport_locators_json, @consecutive_probe_failures,
|
|
308
|
+
@excluded_until_ms, @last_probe_ms, @last_probe_code, @updated_at_ms
|
|
309
|
+
)
|
|
310
|
+
ON CONFLICT(role_key_wire, agent_id) DO UPDATE SET
|
|
311
|
+
source=excluded.source,
|
|
312
|
+
priority=excluded.priority,
|
|
313
|
+
transport_locators_json=excluded.transport_locators_json,
|
|
314
|
+
consecutive_probe_failures=excluded.consecutive_probe_failures,
|
|
315
|
+
excluded_until_ms=excluded.excluded_until_ms,
|
|
316
|
+
last_probe_ms=excluded.last_probe_ms,
|
|
317
|
+
last_probe_code=excluded.last_probe_code,
|
|
318
|
+
updated_at_ms=excluded.updated_at_ms`),
|
|
319
|
+
updateCandidateReachability: db.prepare(`
|
|
320
|
+
UPDATE role_candidates SET
|
|
321
|
+
consecutive_probe_failures=@consecutive_probe_failures,
|
|
322
|
+
excluded_until_ms=@excluded_until_ms,
|
|
323
|
+
last_probe_ms=@last_probe_ms,
|
|
324
|
+
last_probe_code=@last_probe_code,
|
|
325
|
+
updated_at_ms=@updated_at_ms
|
|
326
|
+
WHERE role_key_wire=@role_key_wire AND agent_id=@agent_id`),
|
|
327
|
+
pendingRoleMessages: db.prepare(`
|
|
328
|
+
SELECT * FROM messages
|
|
329
|
+
WHERE role_key_wire=?
|
|
330
|
+
AND status IN ('queued','delivered')
|
|
331
|
+
AND expires_at_ms > ?
|
|
332
|
+
ORDER BY priority DESC, created_at_ms ASC, id`),
|
|
333
|
+
recoverExpiredRole: db.prepare(`
|
|
334
|
+
UPDATE role_registry SET
|
|
335
|
+
state='electable',
|
|
336
|
+
previous_holder_agent_id=holder_agent_id,
|
|
337
|
+
holder_agent_id=NULL,
|
|
338
|
+
epoch=epoch + 1,
|
|
339
|
+
activation_seq=activation_seq + 1,
|
|
340
|
+
activation_id=NULL,
|
|
341
|
+
activation_deadline_ms=NULL,
|
|
342
|
+
holder_lease_expires_ms=NULL,
|
|
343
|
+
charter_acked_epoch=NULL,
|
|
344
|
+
blocked_reason=NULL,
|
|
345
|
+
last_transition_ms=@now,
|
|
346
|
+
last_reason='restart_holder_lease_expired',
|
|
347
|
+
version=version + 1
|
|
348
|
+
WHERE role_key_wire=@role_key_wire
|
|
349
|
+
AND version=@expected_version
|
|
350
|
+
AND epoch=@expected_epoch`),
|
|
351
|
+
recoverLiveRole: db.prepare(`
|
|
352
|
+
UPDATE role_registry SET
|
|
353
|
+
state='elected-probing',
|
|
354
|
+
epoch=epoch + 1,
|
|
355
|
+
activation_seq=activation_seq + 1,
|
|
356
|
+
activation_id=@activation_id,
|
|
357
|
+
activation_deadline_ms=NULL,
|
|
358
|
+
charter_acked_epoch=NULL,
|
|
359
|
+
blocked_reason=NULL,
|
|
360
|
+
last_transition_ms=@now,
|
|
361
|
+
last_reason='restart_reprobe_required',
|
|
362
|
+
version=version + 1
|
|
363
|
+
WHERE role_key_wire=@role_key_wire
|
|
364
|
+
AND version=@expected_version
|
|
365
|
+
AND epoch=@expected_epoch`),
|
|
366
|
+
recoverOrphanedActivation: db.prepare(`
|
|
367
|
+
UPDATE role_registry SET
|
|
368
|
+
state='electable',
|
|
369
|
+
epoch=epoch + 1,
|
|
370
|
+
activation_seq=activation_seq + 1,
|
|
371
|
+
activation_id=NULL,
|
|
372
|
+
activation_deadline_ms=NULL,
|
|
373
|
+
charter_acked_epoch=NULL,
|
|
374
|
+
last_transition_ms=@now,
|
|
375
|
+
last_reason='restart_activation_fenced',
|
|
376
|
+
version=version + 1
|
|
377
|
+
WHERE role_key_wire=@role_key_wire
|
|
378
|
+
AND version=@expected_version
|
|
379
|
+
AND epoch=@expected_epoch`),
|
|
380
|
+
|
|
192
381
|
insertAuditMessage: db.prepare(`
|
|
193
|
-
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)
|
|
194
|
-
VALUES (@id, @type, @from_agent, @to_agent, @topic, @priority, @ttl_ms, @created_at_ms, @expires_at_ms, @correlation_id, @trace_id, @payload_json, @status)`),
|
|
382
|
+
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)
|
|
383
|
+
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)`),
|
|
195
384
|
getMsg: db.prepare("SELECT * FROM messages WHERE id=?"),
|
|
196
385
|
getResponse: db.prepare(
|
|
197
386
|
"SELECT * FROM messages WHERE correlation_id=? AND type='response' ORDER BY created_at_ms DESC LIMIT 1",
|
|
@@ -363,6 +552,157 @@ export function createStore(dbPath, options = {}) {
|
|
|
363
552
|
return Math.max(1, Math.min(Math.trunc(num), 9));
|
|
364
553
|
}
|
|
365
554
|
|
|
555
|
+
function roleIdentity(input) {
|
|
556
|
+
const roleKeyWire = input?.role_key_wire ?? input?.roleKeyWire;
|
|
557
|
+
const decoded = decodeRoleKey(roleKeyWire);
|
|
558
|
+
const identity = {
|
|
559
|
+
role_key_wire: roleKeyWire,
|
|
560
|
+
project_id: decoded.project_id,
|
|
561
|
+
role_kind: decoded.role_kind,
|
|
562
|
+
scope_id: decoded.scope_id ?? "",
|
|
563
|
+
};
|
|
564
|
+
for (const field of ["project_id", "role_kind", "scope_id"]) {
|
|
565
|
+
if (Object.hasOwn(input, field) && input[field] !== identity[field]) {
|
|
566
|
+
throw new Error(`role key identity mismatch: ${field}`);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return identity;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function roleFailureReason(current, expected) {
|
|
573
|
+
if (!current) return "not_found";
|
|
574
|
+
if (current.version !== expected.version) return "stale_version";
|
|
575
|
+
if (current.epoch !== expected.epoch) return "stale_epoch";
|
|
576
|
+
if (current.activation_seq !== expected.activation_seq) {
|
|
577
|
+
return "stale_activation_seq";
|
|
578
|
+
}
|
|
579
|
+
if (current.activation_id !== expected.activation_id) {
|
|
580
|
+
return "stale_activation_id";
|
|
581
|
+
}
|
|
582
|
+
return "cas_conflict";
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const reserveRole = db.transaction((input) => {
|
|
586
|
+
const identity = roleIdentity(input);
|
|
587
|
+
const current = S.getRole.get(identity.role_key_wire) ?? null;
|
|
588
|
+
const expectedVersion =
|
|
589
|
+
input.expected_version ?? input.expectedVersion ?? current?.version;
|
|
590
|
+
const expectedEpoch =
|
|
591
|
+
input.expected_epoch ?? input.expectedEpoch ?? current?.epoch;
|
|
592
|
+
|
|
593
|
+
if (current && expectedVersion !== current.version) {
|
|
594
|
+
return { ok: false, reason: "stale_version", role: current };
|
|
595
|
+
}
|
|
596
|
+
if (current && expectedEpoch !== current.epoch) {
|
|
597
|
+
return { ok: false, reason: "stale_epoch", role: current };
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const nextEpoch =
|
|
601
|
+
input.epoch ?? input.next_epoch ?? (current ? current.epoch + 1 : 0);
|
|
602
|
+
if (current && nextEpoch !== current.epoch + 1) {
|
|
603
|
+
return { ok: false, reason: "stale_epoch", role: current };
|
|
604
|
+
}
|
|
605
|
+
const nextActivationSeq =
|
|
606
|
+
input.activation_seq ??
|
|
607
|
+
input.next_activation_seq ??
|
|
608
|
+
(current ? current.activation_seq + 1 : 0);
|
|
609
|
+
if (current && nextActivationSeq !== current.activation_seq + 1) {
|
|
610
|
+
return { ok: false, reason: "stale_activation_seq", role: current };
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const state =
|
|
614
|
+
input.state ?? (input.holder_agent_id ? "elected-probing" : "electable");
|
|
615
|
+
if (!ROLE_REACHABILITY_STATES.includes(state)) {
|
|
616
|
+
throw new Error(`invalid role reachability state: ${state}`);
|
|
617
|
+
}
|
|
618
|
+
const row = {
|
|
619
|
+
...identity,
|
|
620
|
+
state,
|
|
621
|
+
holder_agent_id: input.holder_agent_id ?? null,
|
|
622
|
+
previous_holder_agent_id:
|
|
623
|
+
input.previous_holder_agent_id ??
|
|
624
|
+
(current?.holder_agent_id !== input.holder_agent_id
|
|
625
|
+
? (current?.holder_agent_id ?? null)
|
|
626
|
+
: (current?.previous_holder_agent_id ?? null)),
|
|
627
|
+
epoch: nextEpoch,
|
|
628
|
+
activation_seq: nextActivationSeq,
|
|
629
|
+
activation_id:
|
|
630
|
+
input.activation_id ??
|
|
631
|
+
(input.holder_agent_id ? uuidv7() : (current?.activation_id ?? null)),
|
|
632
|
+
activation_deadline_ms:
|
|
633
|
+
input.activation_deadline_ms ?? current?.activation_deadline_ms ?? null,
|
|
634
|
+
holder_lease_expires_ms:
|
|
635
|
+
input.holder_lease_expires_ms ??
|
|
636
|
+
current?.holder_lease_expires_ms ??
|
|
637
|
+
null,
|
|
638
|
+
charter_version:
|
|
639
|
+
input.charter_version ?? current?.charter_version ?? null,
|
|
640
|
+
charter_acked_epoch:
|
|
641
|
+
input.charter_acked_epoch ?? current?.charter_acked_epoch ?? null,
|
|
642
|
+
retry_count: input.retry_count ?? current?.retry_count ?? 0,
|
|
643
|
+
next_probe_ms: input.next_probe_ms ?? current?.next_probe_ms ?? null,
|
|
644
|
+
blocked_reason:
|
|
645
|
+
input.blocked_reason === undefined
|
|
646
|
+
? (current?.blocked_reason ?? null)
|
|
647
|
+
: input.blocked_reason,
|
|
648
|
+
last_transition_ms: input.last_transition_ms ?? Date.now(),
|
|
649
|
+
last_reason: input.last_reason ?? "role_reserved",
|
|
650
|
+
version: 0,
|
|
651
|
+
expected_version: current?.version,
|
|
652
|
+
expected_epoch: current?.epoch,
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
if (!current) {
|
|
656
|
+
S.insertRoleReservation.run(row);
|
|
657
|
+
} else if (S.updateRoleReservation.run(row).changes !== 1) {
|
|
658
|
+
const latest = S.getRole.get(identity.role_key_wire) ?? null;
|
|
659
|
+
return {
|
|
660
|
+
ok: false,
|
|
661
|
+
reason: roleFailureReason(latest, {
|
|
662
|
+
version: current.version,
|
|
663
|
+
epoch: current.epoch,
|
|
664
|
+
activation_seq: current.activation_seq,
|
|
665
|
+
activation_id: current.activation_id,
|
|
666
|
+
}),
|
|
667
|
+
role: latest,
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
return { ok: true, role: S.getRole.get(identity.role_key_wire) };
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
const recoverRegistry = db.transaction((now) => {
|
|
674
|
+
for (const role of S.listRoles.all()) {
|
|
675
|
+
const guard = {
|
|
676
|
+
role_key_wire: role.role_key_wire,
|
|
677
|
+
expected_version: role.version,
|
|
678
|
+
expected_epoch: role.epoch,
|
|
679
|
+
now,
|
|
680
|
+
};
|
|
681
|
+
if (
|
|
682
|
+
role.holder_agent_id &&
|
|
683
|
+
(role.holder_lease_expires_ms == null ||
|
|
684
|
+
role.holder_lease_expires_ms <= now)
|
|
685
|
+
) {
|
|
686
|
+
S.recoverExpiredRole.run(guard);
|
|
687
|
+
} else if (role.holder_agent_id) {
|
|
688
|
+
S.recoverLiveRole.run({ ...guard, activation_id: uuidv7() });
|
|
689
|
+
} else if (role.activation_id || role.state === "elected-probing") {
|
|
690
|
+
S.recoverOrphanedActivation.run(guard);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const roles = S.listRoles.all();
|
|
695
|
+
return {
|
|
696
|
+
roles,
|
|
697
|
+
candidates: roles.flatMap((role) =>
|
|
698
|
+
S.listRoleCandidates.all(role.role_key_wire).map(parseRoleCandidateRow),
|
|
699
|
+
),
|
|
700
|
+
pending_messages: roles.flatMap((role) =>
|
|
701
|
+
S.pendingRoleMessages.all(role.role_key_wire, now).map(parseMessageRow),
|
|
702
|
+
),
|
|
703
|
+
};
|
|
704
|
+
});
|
|
705
|
+
|
|
366
706
|
const store = {
|
|
367
707
|
db,
|
|
368
708
|
uuidv7,
|
|
@@ -464,6 +804,236 @@ export function createStore(dbPath, options = {}) {
|
|
|
464
804
|
return S.setAgentStatus.run(status, agentId).changes > 0;
|
|
465
805
|
},
|
|
466
806
|
|
|
807
|
+
getRole(roleKeyWire) {
|
|
808
|
+
decodeRoleKey(roleKeyWire);
|
|
809
|
+
return S.getRole.get(roleKeyWire) ?? null;
|
|
810
|
+
},
|
|
811
|
+
|
|
812
|
+
upsertRoleReservation(input) {
|
|
813
|
+
return reserveRole.immediate(input);
|
|
814
|
+
},
|
|
815
|
+
|
|
816
|
+
compareAndSetRole(input) {
|
|
817
|
+
const identity = roleIdentity(input);
|
|
818
|
+
const current = S.getRole.get(identity.role_key_wire) ?? null;
|
|
819
|
+
if (!current) return { ok: false, reason: "not_found", role: null };
|
|
820
|
+
|
|
821
|
+
const hasExpectedActivationSeq =
|
|
822
|
+
Object.hasOwn(input, "expected_activation_seq") ||
|
|
823
|
+
Object.hasOwn(input, "expectedActivationSeq") ||
|
|
824
|
+
Object.hasOwn(input, "activation_seq");
|
|
825
|
+
const hasExpectedActivationId =
|
|
826
|
+
Object.hasOwn(input, "expected_activation_id") ||
|
|
827
|
+
Object.hasOwn(input, "expectedActivationId") ||
|
|
828
|
+
Object.hasOwn(input, "activation_id");
|
|
829
|
+
const expected = {
|
|
830
|
+
version:
|
|
831
|
+
input.expected_version ?? input.expectedVersion ?? input.version,
|
|
832
|
+
epoch: input.expected_epoch ?? input.expectedEpoch ?? input.epoch,
|
|
833
|
+
activation_seq:
|
|
834
|
+
input.expected_activation_seq ??
|
|
835
|
+
input.expectedActivationSeq ??
|
|
836
|
+
input.activation_seq,
|
|
837
|
+
activation_id: Object.hasOwn(input, "expected_activation_id")
|
|
838
|
+
? input.expected_activation_id
|
|
839
|
+
: Object.hasOwn(input, "expectedActivationId")
|
|
840
|
+
? input.expectedActivationId
|
|
841
|
+
: input.activation_id,
|
|
842
|
+
};
|
|
843
|
+
if (
|
|
844
|
+
expected.version == null ||
|
|
845
|
+
expected.epoch == null ||
|
|
846
|
+
!hasExpectedActivationSeq ||
|
|
847
|
+
!hasExpectedActivationId
|
|
848
|
+
) {
|
|
849
|
+
return { ok: false, reason: "invalid_cas", role: current };
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
const reason = roleFailureReason(current, expected);
|
|
853
|
+
if (reason !== "cas_conflict") {
|
|
854
|
+
return { ok: false, reason, role: current };
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
const patch = input.patch ?? input.changes ?? {};
|
|
858
|
+
const state = patch.state ?? current.state;
|
|
859
|
+
if (!ROLE_REACHABILITY_STATES.includes(state)) {
|
|
860
|
+
throw new Error(`invalid role reachability state: ${state}`);
|
|
861
|
+
}
|
|
862
|
+
const next = {
|
|
863
|
+
role_key_wire: identity.role_key_wire,
|
|
864
|
+
expected_version: expected.version,
|
|
865
|
+
expected_epoch: expected.epoch,
|
|
866
|
+
expected_activation_seq: expected.activation_seq,
|
|
867
|
+
expected_activation_id: expected.activation_id,
|
|
868
|
+
state,
|
|
869
|
+
holder_agent_id:
|
|
870
|
+
patch.holder_agent_id === undefined
|
|
871
|
+
? current.holder_agent_id
|
|
872
|
+
: patch.holder_agent_id,
|
|
873
|
+
previous_holder_agent_id:
|
|
874
|
+
patch.previous_holder_agent_id === undefined
|
|
875
|
+
? current.previous_holder_agent_id
|
|
876
|
+
: patch.previous_holder_agent_id,
|
|
877
|
+
activation_id:
|
|
878
|
+
patch.activation_id === undefined
|
|
879
|
+
? current.activation_id
|
|
880
|
+
: patch.activation_id,
|
|
881
|
+
activation_deadline_ms:
|
|
882
|
+
patch.activation_deadline_ms === undefined
|
|
883
|
+
? current.activation_deadline_ms
|
|
884
|
+
: patch.activation_deadline_ms,
|
|
885
|
+
holder_lease_expires_ms:
|
|
886
|
+
patch.holder_lease_expires_ms === undefined
|
|
887
|
+
? current.holder_lease_expires_ms
|
|
888
|
+
: patch.holder_lease_expires_ms,
|
|
889
|
+
charter_version:
|
|
890
|
+
patch.charter_version === undefined
|
|
891
|
+
? current.charter_version
|
|
892
|
+
: patch.charter_version,
|
|
893
|
+
charter_acked_epoch:
|
|
894
|
+
patch.charter_acked_epoch === undefined
|
|
895
|
+
? current.charter_acked_epoch
|
|
896
|
+
: patch.charter_acked_epoch,
|
|
897
|
+
retry_count: patch.retry_count ?? current.retry_count,
|
|
898
|
+
next_probe_ms:
|
|
899
|
+
patch.next_probe_ms === undefined
|
|
900
|
+
? current.next_probe_ms
|
|
901
|
+
: patch.next_probe_ms,
|
|
902
|
+
blocked_reason:
|
|
903
|
+
patch.blocked_reason === undefined
|
|
904
|
+
? current.blocked_reason
|
|
905
|
+
: patch.blocked_reason,
|
|
906
|
+
last_transition_ms: patch.last_transition_ms ?? Date.now(),
|
|
907
|
+
last_reason: patch.last_reason ?? current.last_reason,
|
|
908
|
+
};
|
|
909
|
+
if (S.compareAndSetRole.run(next).changes !== 1) {
|
|
910
|
+
const latest = S.getRole.get(identity.role_key_wire) ?? null;
|
|
911
|
+
return {
|
|
912
|
+
ok: false,
|
|
913
|
+
reason: roleFailureReason(latest, expected),
|
|
914
|
+
role: latest,
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
return { ok: true, role: S.getRole.get(identity.role_key_wire) };
|
|
918
|
+
},
|
|
919
|
+
|
|
920
|
+
listRoleKeys(filter = {}) {
|
|
921
|
+
const clauses = [];
|
|
922
|
+
const params = {};
|
|
923
|
+
for (const field of ["project_id", "role_kind", "scope_id", "state"]) {
|
|
924
|
+
if (filter[field] !== undefined) {
|
|
925
|
+
clauses.push(`${field}=@${field}`);
|
|
926
|
+
params[field] = filter[field];
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
if (Array.isArray(filter.states) && filter.states.length > 0) {
|
|
930
|
+
clauses.push(
|
|
931
|
+
`state IN (${filter.states.map((_, index) => `@state_${index}`).join(",")})`,
|
|
932
|
+
);
|
|
933
|
+
filter.states.forEach((state, index) => {
|
|
934
|
+
params[`state_${index}`] = state;
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
const where = clauses.length ? ` WHERE ${clauses.join(" AND ")}` : "";
|
|
938
|
+
return db
|
|
939
|
+
.prepare(
|
|
940
|
+
`SELECT role_key_wire FROM role_registry${where} ORDER BY role_key_wire`,
|
|
941
|
+
)
|
|
942
|
+
.pluck()
|
|
943
|
+
.all(params);
|
|
944
|
+
},
|
|
945
|
+
|
|
946
|
+
listRolesWithExpiredHolder(now) {
|
|
947
|
+
return S.expiredHolderRoles.all(now);
|
|
948
|
+
},
|
|
949
|
+
|
|
950
|
+
listRoleCandidates(roleKeyWire) {
|
|
951
|
+
decodeRoleKey(roleKeyWire);
|
|
952
|
+
return S.listRoleCandidates.all(roleKeyWire).map(parseRoleCandidateRow);
|
|
953
|
+
},
|
|
954
|
+
|
|
955
|
+
upsertRoleCandidate(input) {
|
|
956
|
+
const { role_key_wire } = roleIdentity(input);
|
|
957
|
+
const current = S.getRoleCandidate.get(role_key_wire, input.agent_id);
|
|
958
|
+
S.upsertRoleCandidate.run({
|
|
959
|
+
role_key_wire,
|
|
960
|
+
agent_id: input.agent_id,
|
|
961
|
+
source: input.source ?? current?.source ?? "operator",
|
|
962
|
+
priority: input.priority ?? current?.priority ?? 0,
|
|
963
|
+
transport_locators_json: JSON.stringify(
|
|
964
|
+
input.transport_locators ??
|
|
965
|
+
parseJson(current?.transport_locators_json, []),
|
|
966
|
+
),
|
|
967
|
+
consecutive_probe_failures:
|
|
968
|
+
input.consecutive_probe_failures ??
|
|
969
|
+
current?.consecutive_probe_failures ??
|
|
970
|
+
0,
|
|
971
|
+
excluded_until_ms:
|
|
972
|
+
input.excluded_until_ms === undefined
|
|
973
|
+
? (current?.excluded_until_ms ?? null)
|
|
974
|
+
: input.excluded_until_ms,
|
|
975
|
+
last_probe_ms:
|
|
976
|
+
input.last_probe_ms === undefined
|
|
977
|
+
? (current?.last_probe_ms ?? null)
|
|
978
|
+
: input.last_probe_ms,
|
|
979
|
+
last_probe_code:
|
|
980
|
+
input.last_probe_code === undefined
|
|
981
|
+
? (current?.last_probe_code ?? null)
|
|
982
|
+
: input.last_probe_code,
|
|
983
|
+
updated_at_ms: input.updated_at_ms ?? Date.now(),
|
|
984
|
+
});
|
|
985
|
+
return parseRoleCandidateRow(
|
|
986
|
+
S.getRoleCandidate.get(role_key_wire, input.agent_id),
|
|
987
|
+
);
|
|
988
|
+
},
|
|
989
|
+
|
|
990
|
+
updateCandidateReachability(input) {
|
|
991
|
+
const { role_key_wire } = roleIdentity(input);
|
|
992
|
+
const current = S.getRoleCandidate.get(role_key_wire, input.agent_id);
|
|
993
|
+
if (!current) return { ok: false, reason: "not_found" };
|
|
994
|
+
|
|
995
|
+
let failures =
|
|
996
|
+
input.consecutive_probe_failures ?? current.consecutive_probe_failures;
|
|
997
|
+
if (
|
|
998
|
+
input.probe_failed === true ||
|
|
999
|
+
input.increment_probe_failures === true
|
|
1000
|
+
) {
|
|
1001
|
+
failures = current.consecutive_probe_failures + 1;
|
|
1002
|
+
} else if (input.probe_succeeded === true) {
|
|
1003
|
+
failures = 0;
|
|
1004
|
+
}
|
|
1005
|
+
S.updateCandidateReachability.run({
|
|
1006
|
+
role_key_wire,
|
|
1007
|
+
agent_id: input.agent_id,
|
|
1008
|
+
consecutive_probe_failures: failures,
|
|
1009
|
+
excluded_until_ms:
|
|
1010
|
+
input.excluded_until_ms === undefined
|
|
1011
|
+
? current.excluded_until_ms
|
|
1012
|
+
: input.excluded_until_ms,
|
|
1013
|
+
last_probe_ms: input.last_probe_ms ?? Date.now(),
|
|
1014
|
+
last_probe_code:
|
|
1015
|
+
input.last_probe_code === undefined
|
|
1016
|
+
? current.last_probe_code
|
|
1017
|
+
: input.last_probe_code,
|
|
1018
|
+
updated_at_ms: input.updated_at_ms ?? Date.now(),
|
|
1019
|
+
});
|
|
1020
|
+
return {
|
|
1021
|
+
ok: true,
|
|
1022
|
+
candidate: parseRoleCandidateRow(
|
|
1023
|
+
S.getRoleCandidate.get(role_key_wire, input.agent_id),
|
|
1024
|
+
),
|
|
1025
|
+
};
|
|
1026
|
+
},
|
|
1027
|
+
|
|
1028
|
+
listPendingRoleMessages(roleKeyWire, now) {
|
|
1029
|
+
decodeRoleKey(roleKeyWire);
|
|
1030
|
+
return S.pendingRoleMessages.all(roleKeyWire, now).map(parseMessageRow);
|
|
1031
|
+
},
|
|
1032
|
+
|
|
1033
|
+
recoverRoleRegistry(now) {
|
|
1034
|
+
return recoverRegistry.immediate(now);
|
|
1035
|
+
},
|
|
1036
|
+
|
|
467
1037
|
auditLog({
|
|
468
1038
|
type,
|
|
469
1039
|
from,
|
|
@@ -475,6 +1045,7 @@ export function createStore(dbPath, options = {}) {
|
|
|
475
1045
|
trace_id,
|
|
476
1046
|
correlation_id,
|
|
477
1047
|
status = "queued",
|
|
1048
|
+
role_key_wire = null,
|
|
478
1049
|
}) {
|
|
479
1050
|
const now = Date.now();
|
|
480
1051
|
const row = {
|
|
@@ -491,6 +1062,7 @@ export function createStore(dbPath, options = {}) {
|
|
|
491
1062
|
trace_id: trace_id || uuidv7(),
|
|
492
1063
|
payload_json: JSON.stringify(payload),
|
|
493
1064
|
status,
|
|
1065
|
+
role_key_wire,
|
|
494
1066
|
};
|
|
495
1067
|
S.insertAuditMessage.run(row);
|
|
496
1068
|
return { ...row, payload };
|