@triflux/remote 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/cto/hygiene.mjs +1 -1
- package/hub/lib/cto-env.mjs +1 -0
- package/hub/pipe.mjs +7 -1
- package/hub/schema.sql +61 -2
- package/hub/server.mjs +104 -39
- package/hub/store.mjs +601 -9
- package/hub/team/conductor.mjs +23 -2
- package/hub/team/execution-mode.mjs +12 -7
- package/hub/team/headless.mjs +51 -15
- package/hub/team/intervention.mjs +1 -1
- package/hub/team/notify.mjs +31 -11
- package/hub/team/retry-state-machine.mjs +8 -3
- package/hub/team/swarm-hypervisor.mjs +4 -2
- package/hub/team/swarm-preflight.mjs +47 -0
- package/hub/team/worktree-lifecycle.mjs +56 -59
- package/hub/tools.mjs +13 -1
- package/hub/workers/codex-mcp.mjs +20 -4
- package/hub/workers/delegator-mcp.mjs +3 -49
- package/package.json +1 -1
- package/scripts/lib/agent-route-policy.mjs +307 -0
- package/scripts/lib/cli-codex.mjs +20 -160
- package/scripts/lib/codex-profile-config.mjs +29 -3
- package/scripts/lib/keyword-rules.mjs +12 -0
- package/cto/steward.mjs +0 -362
- package/hub/log-retention.mjs +0 -726
package/hub/store.mjs
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
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";
|
|
6
6
|
import { dirname, join } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
clampDurationMs,
|
|
10
|
+
DEFAULT_ASSIGN_TIMEOUT_MS,
|
|
11
|
+
DEFAULT_ASSIGN_TTL_MS,
|
|
12
|
+
} from "@triflux/core/hub/lib/timeout-defaults.mjs";
|
|
9
13
|
import { uuidv7 } from "@triflux/core/hub/lib/uuidv7.mjs";
|
|
10
14
|
import { recalcConfidence } from "@triflux/core/hub/reflexion.mjs";
|
|
15
|
+
import {
|
|
16
|
+
decodeRoleKey,
|
|
17
|
+
ROLE_REACHABILITY_STATES,
|
|
18
|
+
} from "@triflux/core/hub/role-contract.mjs";
|
|
11
19
|
|
|
12
20
|
export { uuidv7 };
|
|
13
21
|
|
|
@@ -72,6 +80,21 @@ function parseReflexionRow(row) {
|
|
|
72
80
|
};
|
|
73
81
|
}
|
|
74
82
|
|
|
83
|
+
function parseRoleCandidateRow(row) {
|
|
84
|
+
if (!row) return null;
|
|
85
|
+
const { transport_locators_json, ...rest } = row;
|
|
86
|
+
return {
|
|
87
|
+
...rest,
|
|
88
|
+
transport_locators: parseJson(transport_locators_json, []),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function hasTable(db, tableName) {
|
|
93
|
+
return !!db
|
|
94
|
+
.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?")
|
|
95
|
+
.get(tableName);
|
|
96
|
+
}
|
|
97
|
+
|
|
75
98
|
function hasColumn(db, tableName, columnName) {
|
|
76
99
|
const rows = db.prepare(`PRAGMA table_info(${tableName})`).all();
|
|
77
100
|
return rows.some((row) => row.name === columnName);
|
|
@@ -82,6 +105,23 @@ function ensureColumn(db, tableName, columnName, definition) {
|
|
|
82
105
|
db.exec(`ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${definition}`);
|
|
83
106
|
}
|
|
84
107
|
|
|
108
|
+
function ensureV6Columns(db) {
|
|
109
|
+
if (hasTable(db, "agents")) {
|
|
110
|
+
ensureColumn(db, "agents", "project_id", "TEXT");
|
|
111
|
+
ensureColumn(db, "agents", "session_id", "TEXT");
|
|
112
|
+
ensureColumn(db, "agents", "host_id", "TEXT");
|
|
113
|
+
ensureColumn(
|
|
114
|
+
db,
|
|
115
|
+
"agents",
|
|
116
|
+
"transport_locators_json",
|
|
117
|
+
"TEXT NOT NULL DEFAULT '[]'",
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
if (hasTable(db, "messages")) {
|
|
121
|
+
ensureColumn(db, "messages", "role_key_wire", "TEXT");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
85
125
|
/**
|
|
86
126
|
* 저장소 생성
|
|
87
127
|
* @param {string} dbPath
|
|
@@ -112,7 +152,7 @@ export function createStore(dbPath, options = {}) {
|
|
|
112
152
|
db.exec(
|
|
113
153
|
"CREATE TABLE IF NOT EXISTS _meta (key TEXT PRIMARY KEY, value TEXT)",
|
|
114
154
|
);
|
|
115
|
-
const SCHEMA_VERSION = "
|
|
155
|
+
const SCHEMA_VERSION = "6";
|
|
116
156
|
const curVer = (() => {
|
|
117
157
|
try {
|
|
118
158
|
return db
|
|
@@ -126,6 +166,9 @@ export function createStore(dbPath, options = {}) {
|
|
|
126
166
|
// 마이그레이션 전략: 스키마 버전이 다르면 schema.sql을 재실행한다.
|
|
127
167
|
// schema.sql은 CREATE TABLE IF NOT EXISTS 패턴을 사용하므로 멱등하게 적용된다.
|
|
128
168
|
// 비파괴적 컬럼 추가는 자동으로 처리되지만, 컬럼 제거/이름 변경은 수동 마이그레이션이 필요하다.
|
|
169
|
+
// v5 테이블에는 schema.sql의 v6 인덱스가 참조하는 컬럼이 없으므로
|
|
170
|
+
// CREATE IF NOT EXISTS 재실행 전에 멱등 컬럼 seam을 먼저 적용한다.
|
|
171
|
+
ensureV6Columns(db);
|
|
129
172
|
if (curVer !== SCHEMA_VERSION) {
|
|
130
173
|
if (curVer != null) {
|
|
131
174
|
// 이미 버전이 기록된 DB에서 버전 불일치가 발생한 경우 경고한다.
|
|
@@ -138,6 +181,8 @@ export function createStore(dbPath, options = {}) {
|
|
|
138
181
|
"INSERT OR REPLACE INTO _meta (key, value) VALUES ('schema_version', ?)",
|
|
139
182
|
).run(SCHEMA_VERSION);
|
|
140
183
|
}
|
|
184
|
+
// 매 부팅 실행해 부분 적용된 마이그레이션도 안전하게 복구한다.
|
|
185
|
+
ensureV6Columns(db);
|
|
141
186
|
ensureColumn(
|
|
142
187
|
db,
|
|
143
188
|
"reflexion_entries",
|
|
@@ -185,9 +230,164 @@ export function createStore(dbPath, options = {}) {
|
|
|
185
230
|
"UPDATE agents SET status='offline' WHERE status='stale' AND lease_expires_ms < ? - 300000",
|
|
186
231
|
),
|
|
187
232
|
|
|
233
|
+
getRole: db.prepare(
|
|
234
|
+
"SELECT * FROM role_registry WHERE role_key_wire = ?",
|
|
235
|
+
),
|
|
236
|
+
insertRoleReservation: db.prepare(`
|
|
237
|
+
INSERT INTO role_registry (
|
|
238
|
+
role_key_wire, project_id, role_kind, scope_id, state,
|
|
239
|
+
holder_agent_id, previous_holder_agent_id, epoch, activation_seq,
|
|
240
|
+
activation_id, activation_deadline_ms, holder_lease_expires_ms,
|
|
241
|
+
charter_version, charter_acked_epoch, retry_count, next_probe_ms,
|
|
242
|
+
blocked_reason, last_transition_ms, last_reason, version
|
|
243
|
+
) VALUES (
|
|
244
|
+
@role_key_wire, @project_id, @role_kind, @scope_id, @state,
|
|
245
|
+
@holder_agent_id, @previous_holder_agent_id, @epoch, @activation_seq,
|
|
246
|
+
@activation_id, @activation_deadline_ms, @holder_lease_expires_ms,
|
|
247
|
+
@charter_version, @charter_acked_epoch, @retry_count, @next_probe_ms,
|
|
248
|
+
@blocked_reason, @last_transition_ms, @last_reason, @version
|
|
249
|
+
)`),
|
|
250
|
+
updateRoleReservation: db.prepare(`
|
|
251
|
+
UPDATE role_registry SET
|
|
252
|
+
state=@state,
|
|
253
|
+
holder_agent_id=@holder_agent_id,
|
|
254
|
+
previous_holder_agent_id=@previous_holder_agent_id,
|
|
255
|
+
epoch=@epoch,
|
|
256
|
+
activation_seq=@activation_seq,
|
|
257
|
+
activation_id=@activation_id,
|
|
258
|
+
activation_deadline_ms=@activation_deadline_ms,
|
|
259
|
+
holder_lease_expires_ms=@holder_lease_expires_ms,
|
|
260
|
+
charter_version=@charter_version,
|
|
261
|
+
charter_acked_epoch=@charter_acked_epoch,
|
|
262
|
+
retry_count=@retry_count,
|
|
263
|
+
next_probe_ms=@next_probe_ms,
|
|
264
|
+
blocked_reason=@blocked_reason,
|
|
265
|
+
last_transition_ms=@last_transition_ms,
|
|
266
|
+
last_reason=@last_reason,
|
|
267
|
+
version=version + 1
|
|
268
|
+
WHERE role_key_wire=@role_key_wire
|
|
269
|
+
AND version=@expected_version
|
|
270
|
+
AND epoch=@expected_epoch`),
|
|
271
|
+
compareAndSetRole: db.prepare(`
|
|
272
|
+
UPDATE role_registry SET
|
|
273
|
+
state=@state,
|
|
274
|
+
holder_agent_id=@holder_agent_id,
|
|
275
|
+
previous_holder_agent_id=@previous_holder_agent_id,
|
|
276
|
+
activation_id=@activation_id,
|
|
277
|
+
activation_deadline_ms=@activation_deadline_ms,
|
|
278
|
+
holder_lease_expires_ms=@holder_lease_expires_ms,
|
|
279
|
+
charter_version=@charter_version,
|
|
280
|
+
charter_acked_epoch=@charter_acked_epoch,
|
|
281
|
+
retry_count=@retry_count,
|
|
282
|
+
next_probe_ms=@next_probe_ms,
|
|
283
|
+
blocked_reason=@blocked_reason,
|
|
284
|
+
last_transition_ms=@last_transition_ms,
|
|
285
|
+
last_reason=@last_reason,
|
|
286
|
+
version=version + 1
|
|
287
|
+
WHERE role_key_wire=@role_key_wire
|
|
288
|
+
AND epoch=@expected_epoch
|
|
289
|
+
AND activation_seq=@expected_activation_seq
|
|
290
|
+
AND activation_id IS @expected_activation_id
|
|
291
|
+
AND version=@expected_version`),
|
|
292
|
+
listRoles: db.prepare(
|
|
293
|
+
"SELECT * FROM role_registry ORDER BY role_key_wire",
|
|
294
|
+
),
|
|
295
|
+
expiredHolderRoles: db.prepare(`
|
|
296
|
+
SELECT * FROM role_registry
|
|
297
|
+
WHERE holder_agent_id IS NOT NULL
|
|
298
|
+
AND (holder_lease_expires_ms IS NULL OR holder_lease_expires_ms <= ?)
|
|
299
|
+
ORDER BY role_key_wire`),
|
|
300
|
+
listRoleCandidates: db.prepare(`
|
|
301
|
+
SELECT * FROM role_candidates
|
|
302
|
+
WHERE role_key_wire = ?
|
|
303
|
+
ORDER BY priority DESC, updated_at_ms DESC, agent_id`),
|
|
304
|
+
getRoleCandidate: db.prepare(`
|
|
305
|
+
SELECT * FROM role_candidates
|
|
306
|
+
WHERE role_key_wire = ? AND agent_id = ?`),
|
|
307
|
+
upsertRoleCandidate: db.prepare(`
|
|
308
|
+
INSERT INTO role_candidates (
|
|
309
|
+
role_key_wire, agent_id, source, priority, transport_locators_json,
|
|
310
|
+
consecutive_probe_failures, excluded_until_ms, last_probe_ms,
|
|
311
|
+
last_probe_code, updated_at_ms
|
|
312
|
+
) VALUES (
|
|
313
|
+
@role_key_wire, @agent_id, @source, @priority,
|
|
314
|
+
@transport_locators_json, @consecutive_probe_failures,
|
|
315
|
+
@excluded_until_ms, @last_probe_ms, @last_probe_code, @updated_at_ms
|
|
316
|
+
)
|
|
317
|
+
ON CONFLICT(role_key_wire, agent_id) DO UPDATE SET
|
|
318
|
+
source=excluded.source,
|
|
319
|
+
priority=excluded.priority,
|
|
320
|
+
transport_locators_json=excluded.transport_locators_json,
|
|
321
|
+
consecutive_probe_failures=excluded.consecutive_probe_failures,
|
|
322
|
+
excluded_until_ms=excluded.excluded_until_ms,
|
|
323
|
+
last_probe_ms=excluded.last_probe_ms,
|
|
324
|
+
last_probe_code=excluded.last_probe_code,
|
|
325
|
+
updated_at_ms=excluded.updated_at_ms`),
|
|
326
|
+
updateCandidateReachability: db.prepare(`
|
|
327
|
+
UPDATE role_candidates SET
|
|
328
|
+
consecutive_probe_failures=@consecutive_probe_failures,
|
|
329
|
+
excluded_until_ms=@excluded_until_ms,
|
|
330
|
+
last_probe_ms=@last_probe_ms,
|
|
331
|
+
last_probe_code=@last_probe_code,
|
|
332
|
+
updated_at_ms=@updated_at_ms
|
|
333
|
+
WHERE role_key_wire=@role_key_wire AND agent_id=@agent_id`),
|
|
334
|
+
pendingRoleMessages: db.prepare(`
|
|
335
|
+
SELECT * FROM messages
|
|
336
|
+
WHERE role_key_wire=?
|
|
337
|
+
AND status IN ('queued','delivered')
|
|
338
|
+
AND expires_at_ms > ?
|
|
339
|
+
ORDER BY priority DESC, created_at_ms ASC, id`),
|
|
340
|
+
recoverExpiredRole: db.prepare(`
|
|
341
|
+
UPDATE role_registry SET
|
|
342
|
+
state='electable',
|
|
343
|
+
previous_holder_agent_id=holder_agent_id,
|
|
344
|
+
holder_agent_id=NULL,
|
|
345
|
+
epoch=epoch + 1,
|
|
346
|
+
activation_seq=activation_seq + 1,
|
|
347
|
+
activation_id=NULL,
|
|
348
|
+
activation_deadline_ms=NULL,
|
|
349
|
+
holder_lease_expires_ms=NULL,
|
|
350
|
+
charter_acked_epoch=NULL,
|
|
351
|
+
blocked_reason=NULL,
|
|
352
|
+
last_transition_ms=@now,
|
|
353
|
+
last_reason='restart_holder_lease_expired',
|
|
354
|
+
version=version + 1
|
|
355
|
+
WHERE role_key_wire=@role_key_wire
|
|
356
|
+
AND version=@expected_version
|
|
357
|
+
AND epoch=@expected_epoch`),
|
|
358
|
+
recoverLiveRole: db.prepare(`
|
|
359
|
+
UPDATE role_registry SET
|
|
360
|
+
state='elected-probing',
|
|
361
|
+
epoch=epoch + 1,
|
|
362
|
+
activation_seq=activation_seq + 1,
|
|
363
|
+
activation_id=@activation_id,
|
|
364
|
+
activation_deadline_ms=NULL,
|
|
365
|
+
charter_acked_epoch=NULL,
|
|
366
|
+
blocked_reason=NULL,
|
|
367
|
+
last_transition_ms=@now,
|
|
368
|
+
last_reason='restart_reprobe_required',
|
|
369
|
+
version=version + 1
|
|
370
|
+
WHERE role_key_wire=@role_key_wire
|
|
371
|
+
AND version=@expected_version
|
|
372
|
+
AND epoch=@expected_epoch`),
|
|
373
|
+
recoverOrphanedActivation: db.prepare(`
|
|
374
|
+
UPDATE role_registry SET
|
|
375
|
+
state='electable',
|
|
376
|
+
epoch=epoch + 1,
|
|
377
|
+
activation_seq=activation_seq + 1,
|
|
378
|
+
activation_id=NULL,
|
|
379
|
+
activation_deadline_ms=NULL,
|
|
380
|
+
charter_acked_epoch=NULL,
|
|
381
|
+
last_transition_ms=@now,
|
|
382
|
+
last_reason='restart_activation_fenced',
|
|
383
|
+
version=version + 1
|
|
384
|
+
WHERE role_key_wire=@role_key_wire
|
|
385
|
+
AND version=@expected_version
|
|
386
|
+
AND epoch=@expected_epoch`),
|
|
387
|
+
|
|
188
388
|
insertAuditMessage: db.prepare(`
|
|
189
|
-
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)
|
|
190
|
-
VALUES (@id, @type, @from_agent, @to_agent, @topic, @priority, @ttl_ms, @created_at_ms, @expires_at_ms, @correlation_id, @trace_id, @payload_json, @status)`),
|
|
389
|
+
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)
|
|
390
|
+
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)`),
|
|
191
391
|
getMsg: db.prepare("SELECT * FROM messages WHERE id=?"),
|
|
192
392
|
getResponse: db.prepare(
|
|
193
393
|
"SELECT * FROM messages WHERE correlation_id=? AND type='response' ORDER BY created_at_ms DESC LIMIT 1",
|
|
@@ -359,6 +559,160 @@ export function createStore(dbPath, options = {}) {
|
|
|
359
559
|
return Math.max(1, Math.min(Math.trunc(num), 9));
|
|
360
560
|
}
|
|
361
561
|
|
|
562
|
+
function roleIdentity(input) {
|
|
563
|
+
const roleKeyWire = input?.role_key_wire ?? input?.roleKeyWire;
|
|
564
|
+
const decoded = decodeRoleKey(roleKeyWire);
|
|
565
|
+
const identity = {
|
|
566
|
+
role_key_wire: roleKeyWire,
|
|
567
|
+
project_id: decoded.project_id,
|
|
568
|
+
role_kind: decoded.role_kind,
|
|
569
|
+
scope_id: decoded.scope_id ?? "",
|
|
570
|
+
};
|
|
571
|
+
for (const field of ["project_id", "role_kind", "scope_id"]) {
|
|
572
|
+
if (Object.hasOwn(input, field) && input[field] !== identity[field]) {
|
|
573
|
+
throw new Error(`role key identity mismatch: ${field}`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return identity;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function roleFailureReason(current, expected) {
|
|
580
|
+
if (!current) return "not_found";
|
|
581
|
+
if (current.version !== expected.version) return "stale_version";
|
|
582
|
+
if (current.epoch !== expected.epoch) return "stale_epoch";
|
|
583
|
+
if (current.activation_seq !== expected.activation_seq) {
|
|
584
|
+
return "stale_activation_seq";
|
|
585
|
+
}
|
|
586
|
+
if (current.activation_id !== expected.activation_id) {
|
|
587
|
+
return "stale_activation_id";
|
|
588
|
+
}
|
|
589
|
+
return "cas_conflict";
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const reserveRole = db.transaction((input) => {
|
|
593
|
+
const identity = roleIdentity(input);
|
|
594
|
+
const current = S.getRole.get(identity.role_key_wire) ?? null;
|
|
595
|
+
const expectedVersion =
|
|
596
|
+
input.expected_version ?? input.expectedVersion ?? current?.version;
|
|
597
|
+
const expectedEpoch =
|
|
598
|
+
input.expected_epoch ?? input.expectedEpoch ?? current?.epoch;
|
|
599
|
+
|
|
600
|
+
if (current && expectedVersion !== current.version) {
|
|
601
|
+
return { ok: false, reason: "stale_version", role: current };
|
|
602
|
+
}
|
|
603
|
+
if (current && expectedEpoch !== current.epoch) {
|
|
604
|
+
return { ok: false, reason: "stale_epoch", role: current };
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const nextEpoch =
|
|
608
|
+
input.epoch ?? input.next_epoch ?? (current ? current.epoch + 1 : 0);
|
|
609
|
+
if (current && nextEpoch !== current.epoch + 1) {
|
|
610
|
+
return { ok: false, reason: "stale_epoch", role: current };
|
|
611
|
+
}
|
|
612
|
+
const nextActivationSeq =
|
|
613
|
+
input.activation_seq ??
|
|
614
|
+
input.next_activation_seq ??
|
|
615
|
+
(current ? current.activation_seq + 1 : 0);
|
|
616
|
+
if (current && nextActivationSeq !== current.activation_seq + 1) {
|
|
617
|
+
return { ok: false, reason: "stale_activation_seq", role: current };
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const state =
|
|
621
|
+
input.state ?? (input.holder_agent_id ? "elected-probing" : "electable");
|
|
622
|
+
if (!ROLE_REACHABILITY_STATES.includes(state)) {
|
|
623
|
+
throw new Error(`invalid role reachability state: ${state}`);
|
|
624
|
+
}
|
|
625
|
+
const row = {
|
|
626
|
+
...identity,
|
|
627
|
+
state,
|
|
628
|
+
holder_agent_id: input.holder_agent_id ?? null,
|
|
629
|
+
previous_holder_agent_id:
|
|
630
|
+
input.previous_holder_agent_id ??
|
|
631
|
+
(current?.holder_agent_id !== input.holder_agent_id
|
|
632
|
+
? current?.holder_agent_id ?? null
|
|
633
|
+
: current?.previous_holder_agent_id ?? null),
|
|
634
|
+
epoch: nextEpoch,
|
|
635
|
+
activation_seq: nextActivationSeq,
|
|
636
|
+
activation_id:
|
|
637
|
+
input.activation_id ??
|
|
638
|
+
(input.holder_agent_id ? uuidv7() : current?.activation_id ?? null),
|
|
639
|
+
activation_deadline_ms:
|
|
640
|
+
input.activation_deadline_ms ?? current?.activation_deadline_ms ?? null,
|
|
641
|
+
holder_lease_expires_ms:
|
|
642
|
+
input.holder_lease_expires_ms ??
|
|
643
|
+
current?.holder_lease_expires_ms ??
|
|
644
|
+
null,
|
|
645
|
+
charter_version: input.charter_version ?? current?.charter_version ?? null,
|
|
646
|
+
charter_acked_epoch:
|
|
647
|
+
input.charter_acked_epoch ?? current?.charter_acked_epoch ?? null,
|
|
648
|
+
retry_count: input.retry_count ?? current?.retry_count ?? 0,
|
|
649
|
+
next_probe_ms: input.next_probe_ms ?? current?.next_probe_ms ?? null,
|
|
650
|
+
blocked_reason:
|
|
651
|
+
input.blocked_reason === undefined
|
|
652
|
+
? current?.blocked_reason ?? null
|
|
653
|
+
: input.blocked_reason,
|
|
654
|
+
last_transition_ms: input.last_transition_ms ?? Date.now(),
|
|
655
|
+
last_reason: input.last_reason ?? "role_reserved",
|
|
656
|
+
version: 0,
|
|
657
|
+
expected_version: current?.version,
|
|
658
|
+
expected_epoch: current?.epoch,
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
if (!current) {
|
|
662
|
+
S.insertRoleReservation.run(row);
|
|
663
|
+
} else if (S.updateRoleReservation.run(row).changes !== 1) {
|
|
664
|
+
const latest = S.getRole.get(identity.role_key_wire) ?? null;
|
|
665
|
+
return {
|
|
666
|
+
ok: false,
|
|
667
|
+
reason: roleFailureReason(latest, {
|
|
668
|
+
version: current.version,
|
|
669
|
+
epoch: current.epoch,
|
|
670
|
+
activation_seq: current.activation_seq,
|
|
671
|
+
activation_id: current.activation_id,
|
|
672
|
+
}),
|
|
673
|
+
role: latest,
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
return { ok: true, role: S.getRole.get(identity.role_key_wire) };
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
const recoverRegistry = db.transaction((now) => {
|
|
680
|
+
for (const role of S.listRoles.all()) {
|
|
681
|
+
const guard = {
|
|
682
|
+
role_key_wire: role.role_key_wire,
|
|
683
|
+
expected_version: role.version,
|
|
684
|
+
expected_epoch: role.epoch,
|
|
685
|
+
now,
|
|
686
|
+
};
|
|
687
|
+
if (
|
|
688
|
+
role.holder_agent_id &&
|
|
689
|
+
(role.holder_lease_expires_ms == null ||
|
|
690
|
+
role.holder_lease_expires_ms <= now)
|
|
691
|
+
) {
|
|
692
|
+
S.recoverExpiredRole.run(guard);
|
|
693
|
+
} else if (role.holder_agent_id) {
|
|
694
|
+
S.recoverLiveRole.run({ ...guard, activation_id: uuidv7() });
|
|
695
|
+
} else if (role.activation_id || role.state === "elected-probing") {
|
|
696
|
+
S.recoverOrphanedActivation.run(guard);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const roles = S.listRoles.all();
|
|
701
|
+
return {
|
|
702
|
+
roles,
|
|
703
|
+
candidates: roles.flatMap((role) =>
|
|
704
|
+
S.listRoleCandidates
|
|
705
|
+
.all(role.role_key_wire)
|
|
706
|
+
.map(parseRoleCandidateRow),
|
|
707
|
+
),
|
|
708
|
+
pending_messages: roles.flatMap((role) =>
|
|
709
|
+
S.pendingRoleMessages
|
|
710
|
+
.all(role.role_key_wire, now)
|
|
711
|
+
.map(parseMessageRow),
|
|
712
|
+
),
|
|
713
|
+
};
|
|
714
|
+
});
|
|
715
|
+
|
|
362
716
|
const store = {
|
|
363
717
|
db,
|
|
364
718
|
uuidv7,
|
|
@@ -421,7 +775,11 @@ export function createStore(dbPath, options = {}) {
|
|
|
421
775
|
agent_id: agentId,
|
|
422
776
|
lease_expires_ms: now + ttlMs,
|
|
423
777
|
server_time_ms: now,
|
|
424
|
-
effective: {
|
|
778
|
+
effective: {
|
|
779
|
+
updated: write.changes > 0,
|
|
780
|
+
store_type: "sqlite",
|
|
781
|
+
db_path: dbPath ?? null,
|
|
782
|
+
},
|
|
425
783
|
};
|
|
426
784
|
},
|
|
427
785
|
|
|
@@ -456,6 +814,233 @@ export function createStore(dbPath, options = {}) {
|
|
|
456
814
|
return S.setAgentStatus.run(status, agentId).changes > 0;
|
|
457
815
|
},
|
|
458
816
|
|
|
817
|
+
getRole(roleKeyWire) {
|
|
818
|
+
decodeRoleKey(roleKeyWire);
|
|
819
|
+
return S.getRole.get(roleKeyWire) ?? null;
|
|
820
|
+
},
|
|
821
|
+
|
|
822
|
+
upsertRoleReservation(input) {
|
|
823
|
+
return reserveRole.immediate(input);
|
|
824
|
+
},
|
|
825
|
+
|
|
826
|
+
compareAndSetRole(input) {
|
|
827
|
+
const identity = roleIdentity(input);
|
|
828
|
+
const current = S.getRole.get(identity.role_key_wire) ?? null;
|
|
829
|
+
if (!current) return { ok: false, reason: "not_found", role: null };
|
|
830
|
+
|
|
831
|
+
const hasExpectedActivationSeq =
|
|
832
|
+
Object.hasOwn(input, "expected_activation_seq") ||
|
|
833
|
+
Object.hasOwn(input, "expectedActivationSeq") ||
|
|
834
|
+
Object.hasOwn(input, "activation_seq");
|
|
835
|
+
const hasExpectedActivationId =
|
|
836
|
+
Object.hasOwn(input, "expected_activation_id") ||
|
|
837
|
+
Object.hasOwn(input, "expectedActivationId") ||
|
|
838
|
+
Object.hasOwn(input, "activation_id");
|
|
839
|
+
const expected = {
|
|
840
|
+
version: input.expected_version ?? input.expectedVersion ?? input.version,
|
|
841
|
+
epoch: input.expected_epoch ?? input.expectedEpoch ?? input.epoch,
|
|
842
|
+
activation_seq:
|
|
843
|
+
input.expected_activation_seq ??
|
|
844
|
+
input.expectedActivationSeq ??
|
|
845
|
+
input.activation_seq,
|
|
846
|
+
activation_id: Object.hasOwn(input, "expected_activation_id")
|
|
847
|
+
? input.expected_activation_id
|
|
848
|
+
: Object.hasOwn(input, "expectedActivationId")
|
|
849
|
+
? input.expectedActivationId
|
|
850
|
+
: input.activation_id,
|
|
851
|
+
};
|
|
852
|
+
if (
|
|
853
|
+
expected.version == null ||
|
|
854
|
+
expected.epoch == null ||
|
|
855
|
+
!hasExpectedActivationSeq ||
|
|
856
|
+
!hasExpectedActivationId
|
|
857
|
+
) {
|
|
858
|
+
return { ok: false, reason: "invalid_cas", role: current };
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
const reason = roleFailureReason(current, expected);
|
|
862
|
+
if (reason !== "cas_conflict") {
|
|
863
|
+
return { ok: false, reason, role: current };
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
const patch = input.patch ?? input.changes ?? {};
|
|
867
|
+
const state = patch.state ?? current.state;
|
|
868
|
+
if (!ROLE_REACHABILITY_STATES.includes(state)) {
|
|
869
|
+
throw new Error(`invalid role reachability state: ${state}`);
|
|
870
|
+
}
|
|
871
|
+
const next = {
|
|
872
|
+
role_key_wire: identity.role_key_wire,
|
|
873
|
+
expected_version: expected.version,
|
|
874
|
+
expected_epoch: expected.epoch,
|
|
875
|
+
expected_activation_seq: expected.activation_seq,
|
|
876
|
+
expected_activation_id: expected.activation_id,
|
|
877
|
+
state,
|
|
878
|
+
holder_agent_id:
|
|
879
|
+
patch.holder_agent_id === undefined
|
|
880
|
+
? current.holder_agent_id
|
|
881
|
+
: patch.holder_agent_id,
|
|
882
|
+
previous_holder_agent_id:
|
|
883
|
+
patch.previous_holder_agent_id === undefined
|
|
884
|
+
? current.previous_holder_agent_id
|
|
885
|
+
: patch.previous_holder_agent_id,
|
|
886
|
+
activation_id:
|
|
887
|
+
patch.activation_id === undefined
|
|
888
|
+
? current.activation_id
|
|
889
|
+
: patch.activation_id,
|
|
890
|
+
activation_deadline_ms:
|
|
891
|
+
patch.activation_deadline_ms === undefined
|
|
892
|
+
? current.activation_deadline_ms
|
|
893
|
+
: patch.activation_deadline_ms,
|
|
894
|
+
holder_lease_expires_ms:
|
|
895
|
+
patch.holder_lease_expires_ms === undefined
|
|
896
|
+
? current.holder_lease_expires_ms
|
|
897
|
+
: patch.holder_lease_expires_ms,
|
|
898
|
+
charter_version:
|
|
899
|
+
patch.charter_version === undefined
|
|
900
|
+
? current.charter_version
|
|
901
|
+
: patch.charter_version,
|
|
902
|
+
charter_acked_epoch:
|
|
903
|
+
patch.charter_acked_epoch === undefined
|
|
904
|
+
? current.charter_acked_epoch
|
|
905
|
+
: patch.charter_acked_epoch,
|
|
906
|
+
retry_count: patch.retry_count ?? current.retry_count,
|
|
907
|
+
next_probe_ms:
|
|
908
|
+
patch.next_probe_ms === undefined
|
|
909
|
+
? current.next_probe_ms
|
|
910
|
+
: patch.next_probe_ms,
|
|
911
|
+
blocked_reason:
|
|
912
|
+
patch.blocked_reason === undefined
|
|
913
|
+
? current.blocked_reason
|
|
914
|
+
: patch.blocked_reason,
|
|
915
|
+
last_transition_ms: patch.last_transition_ms ?? Date.now(),
|
|
916
|
+
last_reason: patch.last_reason ?? current.last_reason,
|
|
917
|
+
};
|
|
918
|
+
if (S.compareAndSetRole.run(next).changes !== 1) {
|
|
919
|
+
const latest = S.getRole.get(identity.role_key_wire) ?? null;
|
|
920
|
+
return {
|
|
921
|
+
ok: false,
|
|
922
|
+
reason: roleFailureReason(latest, expected),
|
|
923
|
+
role: latest,
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
return { ok: true, role: S.getRole.get(identity.role_key_wire) };
|
|
927
|
+
},
|
|
928
|
+
|
|
929
|
+
listRoleKeys(filter = {}) {
|
|
930
|
+
const clauses = [];
|
|
931
|
+
const params = {};
|
|
932
|
+
for (const field of ["project_id", "role_kind", "scope_id", "state"]) {
|
|
933
|
+
if (filter[field] !== undefined) {
|
|
934
|
+
clauses.push(`${field}=@${field}`);
|
|
935
|
+
params[field] = filter[field];
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
if (Array.isArray(filter.states) && filter.states.length > 0) {
|
|
939
|
+
clauses.push(
|
|
940
|
+
`state IN (${filter.states.map((_, index) => `@state_${index}`).join(",")})`,
|
|
941
|
+
);
|
|
942
|
+
filter.states.forEach((state, index) => {
|
|
943
|
+
params[`state_${index}`] = state;
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
const where = clauses.length ? ` WHERE ${clauses.join(" AND ")}` : "";
|
|
947
|
+
return db
|
|
948
|
+
.prepare(
|
|
949
|
+
`SELECT role_key_wire FROM role_registry${where} ORDER BY role_key_wire`,
|
|
950
|
+
)
|
|
951
|
+
.pluck()
|
|
952
|
+
.all(params);
|
|
953
|
+
},
|
|
954
|
+
|
|
955
|
+
listRolesWithExpiredHolder(now) {
|
|
956
|
+
return S.expiredHolderRoles.all(now);
|
|
957
|
+
},
|
|
958
|
+
|
|
959
|
+
listRoleCandidates(roleKeyWire) {
|
|
960
|
+
decodeRoleKey(roleKeyWire);
|
|
961
|
+
return S.listRoleCandidates.all(roleKeyWire).map(parseRoleCandidateRow);
|
|
962
|
+
},
|
|
963
|
+
|
|
964
|
+
upsertRoleCandidate(input) {
|
|
965
|
+
const { role_key_wire } = roleIdentity(input);
|
|
966
|
+
const current = S.getRoleCandidate.get(role_key_wire, input.agent_id);
|
|
967
|
+
S.upsertRoleCandidate.run({
|
|
968
|
+
role_key_wire,
|
|
969
|
+
agent_id: input.agent_id,
|
|
970
|
+
source: input.source ?? current?.source ?? "operator",
|
|
971
|
+
priority: input.priority ?? current?.priority ?? 0,
|
|
972
|
+
transport_locators_json: JSON.stringify(
|
|
973
|
+
input.transport_locators ??
|
|
974
|
+
parseJson(current?.transport_locators_json, []),
|
|
975
|
+
),
|
|
976
|
+
consecutive_probe_failures:
|
|
977
|
+
input.consecutive_probe_failures ??
|
|
978
|
+
current?.consecutive_probe_failures ??
|
|
979
|
+
0,
|
|
980
|
+
excluded_until_ms:
|
|
981
|
+
input.excluded_until_ms === undefined
|
|
982
|
+
? current?.excluded_until_ms ?? null
|
|
983
|
+
: input.excluded_until_ms,
|
|
984
|
+
last_probe_ms:
|
|
985
|
+
input.last_probe_ms === undefined
|
|
986
|
+
? current?.last_probe_ms ?? null
|
|
987
|
+
: input.last_probe_ms,
|
|
988
|
+
last_probe_code:
|
|
989
|
+
input.last_probe_code === undefined
|
|
990
|
+
? current?.last_probe_code ?? null
|
|
991
|
+
: input.last_probe_code,
|
|
992
|
+
updated_at_ms: input.updated_at_ms ?? Date.now(),
|
|
993
|
+
});
|
|
994
|
+
return parseRoleCandidateRow(
|
|
995
|
+
S.getRoleCandidate.get(role_key_wire, input.agent_id),
|
|
996
|
+
);
|
|
997
|
+
},
|
|
998
|
+
|
|
999
|
+
updateCandidateReachability(input) {
|
|
1000
|
+
const { role_key_wire } = roleIdentity(input);
|
|
1001
|
+
const current = S.getRoleCandidate.get(role_key_wire, input.agent_id);
|
|
1002
|
+
if (!current) return { ok: false, reason: "not_found" };
|
|
1003
|
+
|
|
1004
|
+
let failures =
|
|
1005
|
+
input.consecutive_probe_failures ??
|
|
1006
|
+
current.consecutive_probe_failures;
|
|
1007
|
+
if (input.probe_failed === true || input.increment_probe_failures === true) {
|
|
1008
|
+
failures = current.consecutive_probe_failures + 1;
|
|
1009
|
+
} else if (input.probe_succeeded === true) {
|
|
1010
|
+
failures = 0;
|
|
1011
|
+
}
|
|
1012
|
+
S.updateCandidateReachability.run({
|
|
1013
|
+
role_key_wire,
|
|
1014
|
+
agent_id: input.agent_id,
|
|
1015
|
+
consecutive_probe_failures: failures,
|
|
1016
|
+
excluded_until_ms:
|
|
1017
|
+
input.excluded_until_ms === undefined
|
|
1018
|
+
? current.excluded_until_ms
|
|
1019
|
+
: input.excluded_until_ms,
|
|
1020
|
+
last_probe_ms: input.last_probe_ms ?? Date.now(),
|
|
1021
|
+
last_probe_code:
|
|
1022
|
+
input.last_probe_code === undefined
|
|
1023
|
+
? current.last_probe_code
|
|
1024
|
+
: input.last_probe_code,
|
|
1025
|
+
updated_at_ms: input.updated_at_ms ?? Date.now(),
|
|
1026
|
+
});
|
|
1027
|
+
return {
|
|
1028
|
+
ok: true,
|
|
1029
|
+
candidate: parseRoleCandidateRow(
|
|
1030
|
+
S.getRoleCandidate.get(role_key_wire, input.agent_id),
|
|
1031
|
+
),
|
|
1032
|
+
};
|
|
1033
|
+
},
|
|
1034
|
+
|
|
1035
|
+
listPendingRoleMessages(roleKeyWire, now) {
|
|
1036
|
+
decodeRoleKey(roleKeyWire);
|
|
1037
|
+
return S.pendingRoleMessages.all(roleKeyWire, now).map(parseMessageRow);
|
|
1038
|
+
},
|
|
1039
|
+
|
|
1040
|
+
recoverRoleRegistry(now) {
|
|
1041
|
+
return recoverRegistry.immediate(now);
|
|
1042
|
+
},
|
|
1043
|
+
|
|
459
1044
|
auditLog({
|
|
460
1045
|
type,
|
|
461
1046
|
from,
|
|
@@ -467,6 +1052,7 @@ export function createStore(dbPath, options = {}) {
|
|
|
467
1052
|
trace_id,
|
|
468
1053
|
correlation_id,
|
|
469
1054
|
status = "queued",
|
|
1055
|
+
role_key_wire = null,
|
|
470
1056
|
}) {
|
|
471
1057
|
const now = Date.now();
|
|
472
1058
|
const row = {
|
|
@@ -483,6 +1069,7 @@ export function createStore(dbPath, options = {}) {
|
|
|
483
1069
|
trace_id: trace_id || uuidv7(),
|
|
484
1070
|
payload_json: JSON.stringify(payload),
|
|
485
1071
|
status,
|
|
1072
|
+
role_key_wire,
|
|
486
1073
|
};
|
|
487
1074
|
S.insertAuditMessage.run(row);
|
|
488
1075
|
return { ...row, payload };
|
|
@@ -638,7 +1225,10 @@ export function createStore(dbPath, options = {}) {
|
|
|
638
1225
|
error = null,
|
|
639
1226
|
}) {
|
|
640
1227
|
const now = Date.now();
|
|
641
|
-
const normalizedTimeout = clampDurationMs(
|
|
1228
|
+
const normalizedTimeout = clampDurationMs(
|
|
1229
|
+
timeout_ms,
|
|
1230
|
+
DEFAULT_ASSIGN_TIMEOUT_MS,
|
|
1231
|
+
);
|
|
642
1232
|
const row = {
|
|
643
1233
|
job_id: job_id || uuidv7(),
|
|
644
1234
|
supervisor_agent,
|
|
@@ -761,7 +1351,9 @@ export function createStore(dbPath, options = {}) {
|
|
|
761
1351
|
: nextStatus === "running"
|
|
762
1352
|
? current.started_at_ms || now
|
|
763
1353
|
: current.started_at_ms,
|
|
764
|
-
dispatched_at_ms: Object.hasOwn(patch, "dispatched_at_ms")
|
|
1354
|
+
dispatched_at_ms: Object.hasOwn(patch, "dispatched_at_ms")
|
|
1355
|
+
? patch.dispatched_at_ms
|
|
1356
|
+
: current.dispatched_at_ms,
|
|
765
1357
|
completed_at_ms: Object.hasOwn(patch, "completed_at_ms")
|
|
766
1358
|
? patch.completed_at_ms
|
|
767
1359
|
: isTerminal
|