@triflux/remote 10.43.0 → 10.45.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 +10 -9
- package/hub/schema.sql +62 -2
- package/hub/server.mjs +276 -132
- package/hub/store.mjs +699 -41
- package/hub/team/backend.mjs +4 -2
- package/hub/team/claude-daemon-control.mjs +2 -0
- package/hub/team/cli/commands/start/start-headless.mjs +2 -0
- package/hub/team/cli/commands/start/start-mux.mjs +24 -3
- package/hub/team/cli/services/runtime-mode.mjs +23 -9
- package/hub/team/conductor.mjs +101 -24
- package/hub/team/cto-auto-collect.mjs +2 -6
- package/hub/team/cto-pull.mjs +4 -0
- package/hub/team/dashboard-open.mjs +1 -0
- package/hub/team/execution-mode.mjs +50 -9
- package/hub/team/headless.mjs +539 -92
- package/hub/team/interactive-tui-transport.mjs +15 -1
- package/hub/team/intervention.mjs +1 -1
- package/hub/team/launcher-template.mjs +17 -1
- package/hub/team/notify.mjs +31 -11
- package/hub/team/orchestrator.mjs +36 -9
- package/hub/team/pane.mjs +176 -14
- package/hub/team/psmux.mjs +79 -70
- package/hub/team/retry-state-machine.mjs +41 -13
- package/hub/team/session.mjs +57 -0
- package/hub/team/swarm-hypervisor.mjs +4 -2
- package/hub/team/swarm-preflight.mjs +47 -0
- package/hub/team/synapse-http.mjs +149 -4
- package/hub/team/terminal-opener.mjs +68 -13
- package/hub/team/tui-viewer.mjs +65 -14
- package/hub/team/worktree-lifecycle.mjs +56 -59
- package/hub/tools.mjs +22 -1
- package/hub/workers/codex-mcp.mjs +26 -5
- package/hub/workers/delegator-mcp.mjs +3 -49
- package/package.json +1 -1
- package/scripts/lib/agent-route-policy.mjs +367 -0
- package/scripts/lib/cli-agy.mjs +24 -4
- package/scripts/lib/cli-codex.mjs +22 -160
- package/scripts/lib/codex-profile-config.mjs +48 -4
- package/scripts/lib/codex-recovery.sh +19 -0
- package/scripts/lib/gemini-profiles.mjs +113 -29
- package/scripts/lib/keyword-rules.mjs +24 -0
- package/scripts/lib/logger.mjs +60 -43
- package/scripts/lib/machine-profile.mjs +293 -0
- package/scripts/lib/mcp-health.mjs +44 -18
- package/cto/steward.mjs +0 -362
- package/hub/log-retention.mjs +0 -726
package/hub/store.mjs
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
// hub/store.mjs — SQLite 감사
|
|
2
|
-
// 실시간
|
|
1
|
+
// hub/store.mjs — SQLite 감사 로그/메타데이터/역할 레지스트리 저장소
|
|
2
|
+
// 실시간 배달은 router/pipe가 담당하고, 역할 권한과 복구 상태는 SQLite가 보존한다.
|
|
3
3
|
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
4
5
|
import { mkdirSync, readFileSync } from "node:fs";
|
|
5
6
|
import { createRequire } from "node:module";
|
|
6
7
|
import { dirname, join } from "node:path";
|
|
7
8
|
import { fileURLToPath } from "node:url";
|
|
8
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
clampDurationMs,
|
|
11
|
+
DEFAULT_ASSIGN_TIMEOUT_MS,
|
|
12
|
+
DEFAULT_ASSIGN_TTL_MS,
|
|
13
|
+
} from "@triflux/core/hub/lib/timeout-defaults.mjs";
|
|
9
14
|
import { uuidv7 } from "@triflux/core/hub/lib/uuidv7.mjs";
|
|
10
15
|
import { recalcConfidence } from "@triflux/core/hub/reflexion.mjs";
|
|
16
|
+
import { decodeRoleKey, ROLE_REACHABILITY_STATES } from "@triflux/core/hub/role-contract.mjs";
|
|
11
17
|
|
|
12
18
|
export { uuidv7 };
|
|
13
19
|
|
|
@@ -72,6 +78,21 @@ function parseReflexionRow(row) {
|
|
|
72
78
|
};
|
|
73
79
|
}
|
|
74
80
|
|
|
81
|
+
function parseRoleCandidateRow(row) {
|
|
82
|
+
if (!row) return null;
|
|
83
|
+
const { transport_locators_json, ...rest } = row;
|
|
84
|
+
return {
|
|
85
|
+
...rest,
|
|
86
|
+
transport_locators: parseJson(transport_locators_json, []),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function hasTable(db, tableName) {
|
|
91
|
+
return !!db
|
|
92
|
+
.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?")
|
|
93
|
+
.get(tableName);
|
|
94
|
+
}
|
|
95
|
+
|
|
75
96
|
function hasColumn(db, tableName, columnName) {
|
|
76
97
|
const rows = db.prepare(`PRAGMA table_info(${tableName})`).all();
|
|
77
98
|
return rows.some((row) => row.name === columnName);
|
|
@@ -82,6 +103,38 @@ function ensureColumn(db, tableName, columnName, definition) {
|
|
|
82
103
|
db.exec(`ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${definition}`);
|
|
83
104
|
}
|
|
84
105
|
|
|
106
|
+
function ensureV6Columns(db) {
|
|
107
|
+
if (hasTable(db, "agents")) {
|
|
108
|
+
ensureColumn(db, "agents", "project_id", "TEXT");
|
|
109
|
+
ensureColumn(db, "agents", "session_id", "TEXT");
|
|
110
|
+
ensureColumn(db, "agents", "host_id", "TEXT");
|
|
111
|
+
ensureColumn(
|
|
112
|
+
db,
|
|
113
|
+
"agents",
|
|
114
|
+
"transport_locators_json",
|
|
115
|
+
"TEXT NOT NULL DEFAULT '[]'",
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
if (hasTable(db, "messages")) {
|
|
119
|
+
ensureColumn(db, "messages", "role_key_wire", "TEXT");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function ensureV7Columns(db) {
|
|
124
|
+
if (hasTable(db, "agents")) {
|
|
125
|
+
ensureColumn(
|
|
126
|
+
db,
|
|
127
|
+
"agents",
|
|
128
|
+
"presence_generation",
|
|
129
|
+
"INTEGER NOT NULL DEFAULT 0",
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function storeFingerprint(dbPath) {
|
|
135
|
+
return `sqlite:${createHash("sha256").update(String(dbPath)).digest("hex")}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
85
138
|
/**
|
|
86
139
|
* 저장소 생성
|
|
87
140
|
* @param {string} dbPath
|
|
@@ -112,7 +165,7 @@ export function createStore(dbPath, options = {}) {
|
|
|
112
165
|
db.exec(
|
|
113
166
|
"CREATE TABLE IF NOT EXISTS _meta (key TEXT PRIMARY KEY, value TEXT)",
|
|
114
167
|
);
|
|
115
|
-
const SCHEMA_VERSION = "
|
|
168
|
+
const SCHEMA_VERSION = "7";
|
|
116
169
|
const curVer = (() => {
|
|
117
170
|
try {
|
|
118
171
|
return db
|
|
@@ -126,6 +179,10 @@ export function createStore(dbPath, options = {}) {
|
|
|
126
179
|
// 마이그레이션 전략: 스키마 버전이 다르면 schema.sql을 재실행한다.
|
|
127
180
|
// schema.sql은 CREATE TABLE IF NOT EXISTS 패턴을 사용하므로 멱등하게 적용된다.
|
|
128
181
|
// 비파괴적 컬럼 추가는 자동으로 처리되지만, 컬럼 제거/이름 변경은 수동 마이그레이션이 필요하다.
|
|
182
|
+
// v5 테이블에는 schema.sql의 v6 인덱스가 참조하는 컬럼이 없으므로
|
|
183
|
+
// CREATE IF NOT EXISTS 재실행 전에 멱등 컬럼 seam을 먼저 적용한다.
|
|
184
|
+
ensureV6Columns(db);
|
|
185
|
+
ensureV7Columns(db);
|
|
129
186
|
if (curVer !== SCHEMA_VERSION) {
|
|
130
187
|
if (curVer != null) {
|
|
131
188
|
// 이미 버전이 기록된 DB에서 버전 불일치가 발생한 경우 경고한다.
|
|
@@ -138,6 +195,9 @@ export function createStore(dbPath, options = {}) {
|
|
|
138
195
|
"INSERT OR REPLACE INTO _meta (key, value) VALUES ('schema_version', ?)",
|
|
139
196
|
).run(SCHEMA_VERSION);
|
|
140
197
|
}
|
|
198
|
+
// 매 부팅 실행해 부분 적용된 마이그레이션도 안전하게 복구한다.
|
|
199
|
+
ensureV6Columns(db);
|
|
200
|
+
ensureV7Columns(db);
|
|
141
201
|
ensureColumn(
|
|
142
202
|
db,
|
|
143
203
|
"reflexion_entries",
|
|
@@ -154,8 +214,8 @@ export function createStore(dbPath, options = {}) {
|
|
|
154
214
|
|
|
155
215
|
const S = {
|
|
156
216
|
upsertAgent: db.prepare(`
|
|
157
|
-
INSERT INTO agents (agent_id, cli, pid, capabilities_json, topics_json, last_seen_ms, lease_expires_ms, status, metadata_json)
|
|
158
|
-
VALUES (@agent_id, @cli, @pid, @capabilities_json, @topics_json, @last_seen_ms, @lease_expires_ms, @status, @metadata_json)
|
|
217
|
+
INSERT INTO agents (agent_id, cli, pid, capabilities_json, topics_json, last_seen_ms, lease_expires_ms, presence_generation, status, metadata_json)
|
|
218
|
+
VALUES (@agent_id, @cli, @pid, @capabilities_json, @topics_json, @last_seen_ms, @lease_expires_ms, 1, @status, @metadata_json)
|
|
159
219
|
ON CONFLICT(agent_id) DO UPDATE SET
|
|
160
220
|
cli=excluded.cli,
|
|
161
221
|
pid=excluded.pid,
|
|
@@ -163,6 +223,7 @@ export function createStore(dbPath, options = {}) {
|
|
|
163
223
|
topics_json=excluded.topics_json,
|
|
164
224
|
last_seen_ms=excluded.last_seen_ms,
|
|
165
225
|
lease_expires_ms=excluded.lease_expires_ms,
|
|
226
|
+
presence_generation=agents.presence_generation + 1,
|
|
166
227
|
status=excluded.status,
|
|
167
228
|
metadata_json=excluded.metadata_json`),
|
|
168
229
|
getAgent: db.prepare("SELECT * FROM agents WHERE agent_id = ?"),
|
|
@@ -170,24 +231,183 @@ export function createStore(dbPath, options = {}) {
|
|
|
170
231
|
"UPDATE agents SET topics_json=?, last_seen_ms=? WHERE agent_id=?",
|
|
171
232
|
),
|
|
172
233
|
heartbeat: db.prepare(
|
|
234
|
+
"UPDATE agents SET last_seen_ms=?, lease_expires_ms=?, status='online' WHERE agent_id=? AND presence_generation=?",
|
|
235
|
+
),
|
|
236
|
+
heartbeatWithoutGeneration: db.prepare(
|
|
173
237
|
"UPDATE agents SET last_seen_ms=?, lease_expires_ms=?, status='online' WHERE agent_id=?",
|
|
174
238
|
),
|
|
175
|
-
setAgentStatus: db.prepare(
|
|
239
|
+
setAgentStatus: db.prepare(
|
|
240
|
+
"UPDATE agents SET status=? WHERE agent_id=? AND presence_generation=?",
|
|
241
|
+
),
|
|
242
|
+
setAgentStatusWithoutGeneration: db.prepare(
|
|
243
|
+
"UPDATE agents SET status=? WHERE agent_id=?",
|
|
244
|
+
),
|
|
176
245
|
onlineAgents: db.prepare("SELECT * FROM agents WHERE status != 'offline'"),
|
|
177
246
|
allAgents: db.prepare("SELECT * FROM agents"),
|
|
178
247
|
agentsByTopic: db.prepare(
|
|
179
248
|
"SELECT a.* FROM agents a, json_each(a.topics_json) t WHERE t.value=? AND a.status != 'offline'",
|
|
180
249
|
),
|
|
181
250
|
markStale: db.prepare(
|
|
182
|
-
"UPDATE agents SET status='stale' WHERE status='online' AND lease_expires_ms < ?",
|
|
251
|
+
"UPDATE agents SET status='stale' WHERE agent_id=? AND presence_generation=? AND status='online' AND lease_expires_ms < ?",
|
|
183
252
|
),
|
|
184
253
|
markOffline: db.prepare(
|
|
185
|
-
"UPDATE agents SET status='offline' WHERE status='stale' AND lease_expires_ms < ? - 300000",
|
|
254
|
+
"UPDATE agents SET status='offline' WHERE agent_id=? AND presence_generation=? AND status='stale' AND lease_expires_ms < ? - 300000",
|
|
186
255
|
),
|
|
187
256
|
|
|
257
|
+
getRole: db.prepare("SELECT * FROM role_registry WHERE role_key_wire = ?"),
|
|
258
|
+
insertRoleReservation: db.prepare(`
|
|
259
|
+
INSERT INTO role_registry (
|
|
260
|
+
role_key_wire, project_id, role_kind, scope_id, state,
|
|
261
|
+
holder_agent_id, previous_holder_agent_id, epoch, activation_seq,
|
|
262
|
+
activation_id, activation_deadline_ms, holder_lease_expires_ms,
|
|
263
|
+
charter_version, charter_acked_epoch, retry_count, next_probe_ms,
|
|
264
|
+
blocked_reason, last_transition_ms, last_reason, version
|
|
265
|
+
) VALUES (
|
|
266
|
+
@role_key_wire, @project_id, @role_kind, @scope_id, @state,
|
|
267
|
+
@holder_agent_id, @previous_holder_agent_id, @epoch, @activation_seq,
|
|
268
|
+
@activation_id, @activation_deadline_ms, @holder_lease_expires_ms,
|
|
269
|
+
@charter_version, @charter_acked_epoch, @retry_count, @next_probe_ms,
|
|
270
|
+
@blocked_reason, @last_transition_ms, @last_reason, @version
|
|
271
|
+
)`),
|
|
272
|
+
updateRoleReservation: db.prepare(`
|
|
273
|
+
UPDATE role_registry SET
|
|
274
|
+
state=@state,
|
|
275
|
+
holder_agent_id=@holder_agent_id,
|
|
276
|
+
previous_holder_agent_id=@previous_holder_agent_id,
|
|
277
|
+
epoch=@epoch,
|
|
278
|
+
activation_seq=@activation_seq,
|
|
279
|
+
activation_id=@activation_id,
|
|
280
|
+
activation_deadline_ms=@activation_deadline_ms,
|
|
281
|
+
holder_lease_expires_ms=@holder_lease_expires_ms,
|
|
282
|
+
charter_version=@charter_version,
|
|
283
|
+
charter_acked_epoch=@charter_acked_epoch,
|
|
284
|
+
retry_count=@retry_count,
|
|
285
|
+
next_probe_ms=@next_probe_ms,
|
|
286
|
+
blocked_reason=@blocked_reason,
|
|
287
|
+
last_transition_ms=@last_transition_ms,
|
|
288
|
+
last_reason=@last_reason,
|
|
289
|
+
version=version + 1
|
|
290
|
+
WHERE role_key_wire=@role_key_wire
|
|
291
|
+
AND version=@expected_version
|
|
292
|
+
AND epoch=@expected_epoch`),
|
|
293
|
+
compareAndSetRole: db.prepare(`
|
|
294
|
+
UPDATE role_registry SET
|
|
295
|
+
state=@state,
|
|
296
|
+
holder_agent_id=@holder_agent_id,
|
|
297
|
+
previous_holder_agent_id=@previous_holder_agent_id,
|
|
298
|
+
activation_id=@activation_id,
|
|
299
|
+
activation_deadline_ms=@activation_deadline_ms,
|
|
300
|
+
holder_lease_expires_ms=@holder_lease_expires_ms,
|
|
301
|
+
charter_version=@charter_version,
|
|
302
|
+
charter_acked_epoch=@charter_acked_epoch,
|
|
303
|
+
retry_count=@retry_count,
|
|
304
|
+
next_probe_ms=@next_probe_ms,
|
|
305
|
+
blocked_reason=@blocked_reason,
|
|
306
|
+
last_transition_ms=@last_transition_ms,
|
|
307
|
+
last_reason=@last_reason,
|
|
308
|
+
version=version + 1
|
|
309
|
+
WHERE role_key_wire=@role_key_wire
|
|
310
|
+
AND epoch=@expected_epoch
|
|
311
|
+
AND activation_seq=@expected_activation_seq
|
|
312
|
+
AND activation_id IS @expected_activation_id
|
|
313
|
+
AND version=@expected_version`),
|
|
314
|
+
listRoles: db.prepare("SELECT * FROM role_registry ORDER BY role_key_wire"),
|
|
315
|
+
expiredHolderRoles: db.prepare(`
|
|
316
|
+
SELECT * FROM role_registry
|
|
317
|
+
WHERE holder_agent_id IS NOT NULL
|
|
318
|
+
AND (holder_lease_expires_ms IS NULL OR holder_lease_expires_ms <= ?)
|
|
319
|
+
ORDER BY role_key_wire`),
|
|
320
|
+
listRoleCandidates: db.prepare(`
|
|
321
|
+
SELECT * FROM role_candidates
|
|
322
|
+
WHERE role_key_wire = ?
|
|
323
|
+
ORDER BY priority DESC, updated_at_ms DESC, agent_id`),
|
|
324
|
+
getRoleCandidate: db.prepare(`
|
|
325
|
+
SELECT * FROM role_candidates
|
|
326
|
+
WHERE role_key_wire = ? AND agent_id = ?`),
|
|
327
|
+
upsertRoleCandidate: db.prepare(`
|
|
328
|
+
INSERT INTO role_candidates (
|
|
329
|
+
role_key_wire, agent_id, source, priority, transport_locators_json,
|
|
330
|
+
consecutive_probe_failures, excluded_until_ms, last_probe_ms,
|
|
331
|
+
last_probe_code, updated_at_ms
|
|
332
|
+
) VALUES (
|
|
333
|
+
@role_key_wire, @agent_id, @source, @priority,
|
|
334
|
+
@transport_locators_json, @consecutive_probe_failures,
|
|
335
|
+
@excluded_until_ms, @last_probe_ms, @last_probe_code, @updated_at_ms
|
|
336
|
+
)
|
|
337
|
+
ON CONFLICT(role_key_wire, agent_id) DO UPDATE SET
|
|
338
|
+
source=excluded.source,
|
|
339
|
+
priority=excluded.priority,
|
|
340
|
+
transport_locators_json=excluded.transport_locators_json,
|
|
341
|
+
consecutive_probe_failures=excluded.consecutive_probe_failures,
|
|
342
|
+
excluded_until_ms=excluded.excluded_until_ms,
|
|
343
|
+
last_probe_ms=excluded.last_probe_ms,
|
|
344
|
+
last_probe_code=excluded.last_probe_code,
|
|
345
|
+
updated_at_ms=excluded.updated_at_ms`),
|
|
346
|
+
updateCandidateReachability: db.prepare(`
|
|
347
|
+
UPDATE role_candidates SET
|
|
348
|
+
consecutive_probe_failures=@consecutive_probe_failures,
|
|
349
|
+
excluded_until_ms=@excluded_until_ms,
|
|
350
|
+
last_probe_ms=@last_probe_ms,
|
|
351
|
+
last_probe_code=@last_probe_code,
|
|
352
|
+
updated_at_ms=@updated_at_ms
|
|
353
|
+
WHERE role_key_wire=@role_key_wire AND agent_id=@agent_id`),
|
|
354
|
+
pendingRoleMessages: db.prepare(`
|
|
355
|
+
SELECT * FROM messages
|
|
356
|
+
WHERE role_key_wire=?
|
|
357
|
+
AND status IN ('queued','delivered')
|
|
358
|
+
AND expires_at_ms > ?
|
|
359
|
+
ORDER BY priority DESC, created_at_ms ASC, id`),
|
|
360
|
+
recoverExpiredRole: db.prepare(`
|
|
361
|
+
UPDATE role_registry SET
|
|
362
|
+
state='electable',
|
|
363
|
+
previous_holder_agent_id=holder_agent_id,
|
|
364
|
+
holder_agent_id=NULL,
|
|
365
|
+
epoch=epoch + 1,
|
|
366
|
+
activation_seq=activation_seq + 1,
|
|
367
|
+
activation_id=NULL,
|
|
368
|
+
activation_deadline_ms=NULL,
|
|
369
|
+
holder_lease_expires_ms=NULL,
|
|
370
|
+
charter_acked_epoch=NULL,
|
|
371
|
+
blocked_reason=NULL,
|
|
372
|
+
last_transition_ms=@now,
|
|
373
|
+
last_reason='restart_holder_lease_expired',
|
|
374
|
+
version=version + 1
|
|
375
|
+
WHERE role_key_wire=@role_key_wire
|
|
376
|
+
AND version=@expected_version
|
|
377
|
+
AND epoch=@expected_epoch`),
|
|
378
|
+
recoverLiveRole: db.prepare(`
|
|
379
|
+
UPDATE role_registry SET
|
|
380
|
+
state='elected-probing',
|
|
381
|
+
epoch=epoch + 1,
|
|
382
|
+
activation_seq=activation_seq + 1,
|
|
383
|
+
activation_id=@activation_id,
|
|
384
|
+
activation_deadline_ms=NULL,
|
|
385
|
+
charter_acked_epoch=NULL,
|
|
386
|
+
blocked_reason=NULL,
|
|
387
|
+
last_transition_ms=@now,
|
|
388
|
+
last_reason='restart_reprobe_required',
|
|
389
|
+
version=version + 1
|
|
390
|
+
WHERE role_key_wire=@role_key_wire
|
|
391
|
+
AND version=@expected_version
|
|
392
|
+
AND epoch=@expected_epoch`),
|
|
393
|
+
recoverOrphanedActivation: db.prepare(`
|
|
394
|
+
UPDATE role_registry SET
|
|
395
|
+
state='electable',
|
|
396
|
+
epoch=epoch + 1,
|
|
397
|
+
activation_seq=activation_seq + 1,
|
|
398
|
+
activation_id=NULL,
|
|
399
|
+
activation_deadline_ms=NULL,
|
|
400
|
+
charter_acked_epoch=NULL,
|
|
401
|
+
last_transition_ms=@now,
|
|
402
|
+
last_reason='restart_activation_fenced',
|
|
403
|
+
version=version + 1
|
|
404
|
+
WHERE role_key_wire=@role_key_wire
|
|
405
|
+
AND version=@expected_version
|
|
406
|
+
AND epoch=@expected_epoch`),
|
|
407
|
+
|
|
188
408
|
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)`),
|
|
409
|
+
INSERT INTO messages (id, type, from_agent, to_agent, topic, priority, ttl_ms, created_at_ms, expires_at_ms, correlation_id, trace_id, payload_json, status, role_key_wire)
|
|
410
|
+
VALUES (@id, @type, @from_agent, @to_agent, @topic, @priority, @ttl_ms, @created_at_ms, @expires_at_ms, @correlation_id, @trace_id, @payload_json, @status, @role_key_wire)`),
|
|
191
411
|
getMsg: db.prepare("SELECT * FROM messages WHERE id=?"),
|
|
192
412
|
getResponse: db.prepare(
|
|
193
413
|
"SELECT * FROM messages WHERE correlation_id=? AND type='response' ORDER BY created_at_ms DESC LIMIT 1",
|
|
@@ -219,12 +439,9 @@ export function createStore(dbPath, options = {}) {
|
|
|
219
439
|
"UPDATE human_requests SET state=?, response_json=? WHERE request_id=?",
|
|
220
440
|
),
|
|
221
441
|
pendingHR: db.prepare("SELECT * FROM human_requests WHERE state='pending'"),
|
|
222
|
-
expireHR: db.prepare(
|
|
223
|
-
"UPDATE human_requests SET state='timed_out' WHERE state='pending' AND deadline_ms < ?",
|
|
224
|
-
),
|
|
225
442
|
|
|
226
443
|
insertDL: db.prepare(
|
|
227
|
-
"INSERT
|
|
444
|
+
"INSERT INTO dead_letters (message_id, reason, failed_at_ms, last_error) VALUES (?,?,?,?) ON CONFLICT(message_id) DO NOTHING",
|
|
228
445
|
),
|
|
229
446
|
getDL: db.prepare(
|
|
230
447
|
"SELECT * FROM dead_letters ORDER BY failed_at_ms DESC LIMIT ?",
|
|
@@ -271,7 +488,13 @@ export function createStore(dbPath, options = {}) {
|
|
|
271
488
|
WHERE job_id=@job_id`),
|
|
272
489
|
|
|
273
490
|
findExpired: db.prepare(
|
|
274
|
-
"SELECT
|
|
491
|
+
"SELECT * FROM messages WHERE status IN ('queued', 'delivered') AND expires_at_ms <= ?",
|
|
492
|
+
),
|
|
493
|
+
claimExpired: db.prepare(
|
|
494
|
+
"UPDATE messages SET status='dead_letter' WHERE id=? AND status IN ('queued', 'delivered') AND expires_at_ms <= ?",
|
|
495
|
+
),
|
|
496
|
+
claimDeadLetter: db.prepare(
|
|
497
|
+
"UPDATE messages SET status='dead_letter' WHERE id=? AND status != 'dead_letter'",
|
|
275
498
|
),
|
|
276
499
|
urgentDepth: db.prepare(
|
|
277
500
|
"SELECT COUNT(*) as cnt FROM messages WHERE status='queued' AND priority >= 7",
|
|
@@ -359,8 +582,161 @@ export function createStore(dbPath, options = {}) {
|
|
|
359
582
|
return Math.max(1, Math.min(Math.trunc(num), 9));
|
|
360
583
|
}
|
|
361
584
|
|
|
585
|
+
function roleIdentity(input) {
|
|
586
|
+
const roleKeyWire = input?.role_key_wire ?? input?.roleKeyWire;
|
|
587
|
+
const decoded = decodeRoleKey(roleKeyWire);
|
|
588
|
+
const identity = {
|
|
589
|
+
role_key_wire: roleKeyWire,
|
|
590
|
+
project_id: decoded.project_id,
|
|
591
|
+
role_kind: decoded.role_kind,
|
|
592
|
+
scope_id: decoded.scope_id ?? "",
|
|
593
|
+
};
|
|
594
|
+
for (const field of ["project_id", "role_kind", "scope_id"]) {
|
|
595
|
+
if (Object.hasOwn(input, field) && input[field] !== identity[field]) {
|
|
596
|
+
throw new Error(`role key identity mismatch: ${field}`);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return identity;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function roleFailureReason(current, expected) {
|
|
603
|
+
if (!current) return "not_found";
|
|
604
|
+
if (current.version !== expected.version) return "stale_version";
|
|
605
|
+
if (current.epoch !== expected.epoch) return "stale_epoch";
|
|
606
|
+
if (current.activation_seq !== expected.activation_seq) {
|
|
607
|
+
return "stale_activation_seq";
|
|
608
|
+
}
|
|
609
|
+
if (current.activation_id !== expected.activation_id) {
|
|
610
|
+
return "stale_activation_id";
|
|
611
|
+
}
|
|
612
|
+
return "cas_conflict";
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const reserveRole = db.transaction((input) => {
|
|
616
|
+
const identity = roleIdentity(input);
|
|
617
|
+
const current = S.getRole.get(identity.role_key_wire) ?? null;
|
|
618
|
+
const expectedVersion =
|
|
619
|
+
input.expected_version ?? input.expectedVersion ?? current?.version;
|
|
620
|
+
const expectedEpoch =
|
|
621
|
+
input.expected_epoch ?? input.expectedEpoch ?? current?.epoch;
|
|
622
|
+
|
|
623
|
+
if (current && expectedVersion !== current.version) {
|
|
624
|
+
return { ok: false, reason: "stale_version", role: current };
|
|
625
|
+
}
|
|
626
|
+
if (current && expectedEpoch !== current.epoch) {
|
|
627
|
+
return { ok: false, reason: "stale_epoch", role: current };
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const nextEpoch =
|
|
631
|
+
input.epoch ?? input.next_epoch ?? (current ? current.epoch + 1 : 0);
|
|
632
|
+
if (current && nextEpoch !== current.epoch + 1) {
|
|
633
|
+
return { ok: false, reason: "stale_epoch", role: current };
|
|
634
|
+
}
|
|
635
|
+
const nextActivationSeq =
|
|
636
|
+
input.activation_seq ??
|
|
637
|
+
input.next_activation_seq ??
|
|
638
|
+
(current ? current.activation_seq + 1 : 0);
|
|
639
|
+
if (current && nextActivationSeq !== current.activation_seq + 1) {
|
|
640
|
+
return { ok: false, reason: "stale_activation_seq", role: current };
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const state =
|
|
644
|
+
input.state ?? (input.holder_agent_id ? "elected-probing" : "electable");
|
|
645
|
+
if (!ROLE_REACHABILITY_STATES.includes(state)) {
|
|
646
|
+
throw new Error(`invalid role reachability state: ${state}`);
|
|
647
|
+
}
|
|
648
|
+
const row = {
|
|
649
|
+
...identity,
|
|
650
|
+
state,
|
|
651
|
+
holder_agent_id: input.holder_agent_id ?? null,
|
|
652
|
+
previous_holder_agent_id:
|
|
653
|
+
input.previous_holder_agent_id ??
|
|
654
|
+
(current?.holder_agent_id !== input.holder_agent_id
|
|
655
|
+
? (current?.holder_agent_id ?? null)
|
|
656
|
+
: (current?.previous_holder_agent_id ?? null)),
|
|
657
|
+
epoch: nextEpoch,
|
|
658
|
+
activation_seq: nextActivationSeq,
|
|
659
|
+
activation_id:
|
|
660
|
+
input.activation_id ??
|
|
661
|
+
(input.holder_agent_id ? uuidv7() : (current?.activation_id ?? null)),
|
|
662
|
+
activation_deadline_ms:
|
|
663
|
+
input.activation_deadline_ms ?? current?.activation_deadline_ms ?? null,
|
|
664
|
+
holder_lease_expires_ms:
|
|
665
|
+
input.holder_lease_expires_ms ??
|
|
666
|
+
current?.holder_lease_expires_ms ??
|
|
667
|
+
null,
|
|
668
|
+
charter_version:
|
|
669
|
+
input.charter_version ?? current?.charter_version ?? null,
|
|
670
|
+
charter_acked_epoch:
|
|
671
|
+
input.charter_acked_epoch ?? current?.charter_acked_epoch ?? null,
|
|
672
|
+
retry_count: input.retry_count ?? current?.retry_count ?? 0,
|
|
673
|
+
next_probe_ms: input.next_probe_ms ?? current?.next_probe_ms ?? null,
|
|
674
|
+
blocked_reason:
|
|
675
|
+
input.blocked_reason === undefined
|
|
676
|
+
? (current?.blocked_reason ?? null)
|
|
677
|
+
: input.blocked_reason,
|
|
678
|
+
last_transition_ms: input.last_transition_ms ?? Date.now(),
|
|
679
|
+
last_reason: input.last_reason ?? "role_reserved",
|
|
680
|
+
version: 0,
|
|
681
|
+
expected_version: current?.version,
|
|
682
|
+
expected_epoch: current?.epoch,
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
if (!current) {
|
|
686
|
+
S.insertRoleReservation.run(row);
|
|
687
|
+
} else if (S.updateRoleReservation.run(row).changes !== 1) {
|
|
688
|
+
const latest = S.getRole.get(identity.role_key_wire) ?? null;
|
|
689
|
+
return {
|
|
690
|
+
ok: false,
|
|
691
|
+
reason: roleFailureReason(latest, {
|
|
692
|
+
version: current.version,
|
|
693
|
+
epoch: current.epoch,
|
|
694
|
+
activation_seq: current.activation_seq,
|
|
695
|
+
activation_id: current.activation_id,
|
|
696
|
+
}),
|
|
697
|
+
role: latest,
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
return { ok: true, role: S.getRole.get(identity.role_key_wire) };
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
const recoverRegistry = db.transaction((now) => {
|
|
704
|
+
for (const role of S.listRoles.all()) {
|
|
705
|
+
const guard = {
|
|
706
|
+
role_key_wire: role.role_key_wire,
|
|
707
|
+
expected_version: role.version,
|
|
708
|
+
expected_epoch: role.epoch,
|
|
709
|
+
now,
|
|
710
|
+
};
|
|
711
|
+
if (
|
|
712
|
+
role.holder_agent_id &&
|
|
713
|
+
(role.holder_lease_expires_ms == null ||
|
|
714
|
+
role.holder_lease_expires_ms <= now)
|
|
715
|
+
) {
|
|
716
|
+
S.recoverExpiredRole.run(guard);
|
|
717
|
+
} else if (role.holder_agent_id) {
|
|
718
|
+
S.recoverLiveRole.run({ ...guard, activation_id: uuidv7() });
|
|
719
|
+
} else if (role.activation_id || role.state === "elected-probing") {
|
|
720
|
+
S.recoverOrphanedActivation.run(guard);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const roles = S.listRoles.all();
|
|
725
|
+
return {
|
|
726
|
+
roles,
|
|
727
|
+
candidates: roles.flatMap((role) =>
|
|
728
|
+
S.listRoleCandidates.all(role.role_key_wire).map(parseRoleCandidateRow),
|
|
729
|
+
),
|
|
730
|
+
pending_messages: roles.flatMap((role) =>
|
|
731
|
+
S.pendingRoleMessages.all(role.role_key_wire, now).map(parseMessageRow),
|
|
732
|
+
),
|
|
733
|
+
};
|
|
734
|
+
});
|
|
735
|
+
|
|
362
736
|
const store = {
|
|
363
737
|
db,
|
|
738
|
+
type: "sqlite",
|
|
739
|
+
fingerprint: storeFingerprint(dbPath),
|
|
364
740
|
uuidv7,
|
|
365
741
|
|
|
366
742
|
close() {
|
|
@@ -394,6 +770,7 @@ export function createStore(dbPath, options = {}) {
|
|
|
394
770
|
return {
|
|
395
771
|
agent_id,
|
|
396
772
|
lease_id: leaseId,
|
|
773
|
+
presence_generation: persisted?.presence_generation ?? null,
|
|
397
774
|
lease_expires_ms: leaseExpires,
|
|
398
775
|
server_time_ms: now,
|
|
399
776
|
effective: {
|
|
@@ -406,6 +783,7 @@ export function createStore(dbPath, options = {}) {
|
|
|
406
783
|
lease_expires_ms: persisted?.lease_expires_ms ?? null,
|
|
407
784
|
store_type: "sqlite",
|
|
408
785
|
db_path: dbPath ?? null,
|
|
786
|
+
store_fingerprint: storeFingerprint(dbPath),
|
|
409
787
|
},
|
|
410
788
|
};
|
|
411
789
|
},
|
|
@@ -414,14 +792,22 @@ export function createStore(dbPath, options = {}) {
|
|
|
414
792
|
return parseAgentRow(S.getAgent.get(id));
|
|
415
793
|
},
|
|
416
794
|
|
|
417
|
-
refreshLease(agentId, ttlMs = 30000) {
|
|
795
|
+
refreshLease(agentId, ttlMs = 30000, presenceGeneration) {
|
|
418
796
|
const now = Date.now();
|
|
419
|
-
const write =
|
|
797
|
+
const write =
|
|
798
|
+
presenceGeneration == null
|
|
799
|
+
? S.heartbeatWithoutGeneration.run(now, now + ttlMs, agentId)
|
|
800
|
+
: S.heartbeat.run(now, now + ttlMs, agentId, presenceGeneration);
|
|
420
801
|
return {
|
|
421
802
|
agent_id: agentId,
|
|
422
803
|
lease_expires_ms: now + ttlMs,
|
|
423
804
|
server_time_ms: now,
|
|
424
|
-
effective: {
|
|
805
|
+
effective: {
|
|
806
|
+
updated: write.changes > 0,
|
|
807
|
+
store_type: "sqlite",
|
|
808
|
+
db_path: dbPath ?? null,
|
|
809
|
+
store_fingerprint: storeFingerprint(dbPath),
|
|
810
|
+
},
|
|
425
811
|
};
|
|
426
812
|
},
|
|
427
813
|
|
|
@@ -446,14 +832,270 @@ export function createStore(dbPath, options = {}) {
|
|
|
446
832
|
|
|
447
833
|
sweepStaleAgents() {
|
|
448
834
|
const now = Date.now();
|
|
835
|
+
const candidates = S.allAgents.all();
|
|
836
|
+
let stale = 0;
|
|
837
|
+
let offline = 0;
|
|
838
|
+
for (const candidate of candidates) {
|
|
839
|
+
if (candidate.status === "online" && candidate.lease_expires_ms < now) {
|
|
840
|
+
stale += S.markStale.run(
|
|
841
|
+
candidate.agent_id,
|
|
842
|
+
candidate.presence_generation,
|
|
843
|
+
now,
|
|
844
|
+
).changes;
|
|
845
|
+
} else if (
|
|
846
|
+
candidate.status === "stale" &&
|
|
847
|
+
candidate.lease_expires_ms < now - 300000
|
|
848
|
+
) {
|
|
849
|
+
offline += S.markOffline.run(
|
|
850
|
+
candidate.agent_id,
|
|
851
|
+
candidate.presence_generation,
|
|
852
|
+
now,
|
|
853
|
+
).changes;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
449
856
|
return {
|
|
450
|
-
stale
|
|
451
|
-
offline
|
|
857
|
+
stale,
|
|
858
|
+
offline,
|
|
452
859
|
};
|
|
453
860
|
},
|
|
454
861
|
|
|
455
|
-
updateAgentStatus(agentId, status) {
|
|
456
|
-
return
|
|
862
|
+
updateAgentStatus(agentId, status, presenceGeneration) {
|
|
863
|
+
return (
|
|
864
|
+
(presenceGeneration == null
|
|
865
|
+
? S.setAgentStatusWithoutGeneration.run(status, agentId)
|
|
866
|
+
: S.setAgentStatus.run(status, agentId, presenceGeneration)
|
|
867
|
+
).changes > 0
|
|
868
|
+
);
|
|
869
|
+
},
|
|
870
|
+
|
|
871
|
+
getRole(roleKeyWire) {
|
|
872
|
+
decodeRoleKey(roleKeyWire);
|
|
873
|
+
return S.getRole.get(roleKeyWire) ?? null;
|
|
874
|
+
},
|
|
875
|
+
|
|
876
|
+
upsertRoleReservation(input) {
|
|
877
|
+
return reserveRole.immediate(input);
|
|
878
|
+
},
|
|
879
|
+
|
|
880
|
+
compareAndSetRole(input) {
|
|
881
|
+
const identity = roleIdentity(input);
|
|
882
|
+
const current = S.getRole.get(identity.role_key_wire) ?? null;
|
|
883
|
+
if (!current) return { ok: false, reason: "not_found", role: null };
|
|
884
|
+
|
|
885
|
+
const hasExpectedActivationSeq =
|
|
886
|
+
Object.hasOwn(input, "expected_activation_seq") ||
|
|
887
|
+
Object.hasOwn(input, "expectedActivationSeq") ||
|
|
888
|
+
Object.hasOwn(input, "activation_seq");
|
|
889
|
+
const hasExpectedActivationId =
|
|
890
|
+
Object.hasOwn(input, "expected_activation_id") ||
|
|
891
|
+
Object.hasOwn(input, "expectedActivationId") ||
|
|
892
|
+
Object.hasOwn(input, "activation_id");
|
|
893
|
+
const expected = {
|
|
894
|
+
version:
|
|
895
|
+
input.expected_version ?? input.expectedVersion ?? input.version,
|
|
896
|
+
epoch: input.expected_epoch ?? input.expectedEpoch ?? input.epoch,
|
|
897
|
+
activation_seq:
|
|
898
|
+
input.expected_activation_seq ??
|
|
899
|
+
input.expectedActivationSeq ??
|
|
900
|
+
input.activation_seq,
|
|
901
|
+
activation_id: Object.hasOwn(input, "expected_activation_id")
|
|
902
|
+
? input.expected_activation_id
|
|
903
|
+
: Object.hasOwn(input, "expectedActivationId")
|
|
904
|
+
? input.expectedActivationId
|
|
905
|
+
: input.activation_id,
|
|
906
|
+
};
|
|
907
|
+
if (
|
|
908
|
+
expected.version == null ||
|
|
909
|
+
expected.epoch == null ||
|
|
910
|
+
!hasExpectedActivationSeq ||
|
|
911
|
+
!hasExpectedActivationId
|
|
912
|
+
) {
|
|
913
|
+
return { ok: false, reason: "invalid_cas", role: current };
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const reason = roleFailureReason(current, expected);
|
|
917
|
+
if (reason !== "cas_conflict") {
|
|
918
|
+
return { ok: false, reason, role: current };
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
const patch = input.patch ?? input.changes ?? {};
|
|
922
|
+
const state = patch.state ?? current.state;
|
|
923
|
+
if (!ROLE_REACHABILITY_STATES.includes(state)) {
|
|
924
|
+
throw new Error(`invalid role reachability state: ${state}`);
|
|
925
|
+
}
|
|
926
|
+
const next = {
|
|
927
|
+
role_key_wire: identity.role_key_wire,
|
|
928
|
+
expected_version: expected.version,
|
|
929
|
+
expected_epoch: expected.epoch,
|
|
930
|
+
expected_activation_seq: expected.activation_seq,
|
|
931
|
+
expected_activation_id: expected.activation_id,
|
|
932
|
+
state,
|
|
933
|
+
holder_agent_id:
|
|
934
|
+
patch.holder_agent_id === undefined
|
|
935
|
+
? current.holder_agent_id
|
|
936
|
+
: patch.holder_agent_id,
|
|
937
|
+
previous_holder_agent_id:
|
|
938
|
+
patch.previous_holder_agent_id === undefined
|
|
939
|
+
? current.previous_holder_agent_id
|
|
940
|
+
: patch.previous_holder_agent_id,
|
|
941
|
+
activation_id:
|
|
942
|
+
patch.activation_id === undefined
|
|
943
|
+
? current.activation_id
|
|
944
|
+
: patch.activation_id,
|
|
945
|
+
activation_deadline_ms:
|
|
946
|
+
patch.activation_deadline_ms === undefined
|
|
947
|
+
? current.activation_deadline_ms
|
|
948
|
+
: patch.activation_deadline_ms,
|
|
949
|
+
holder_lease_expires_ms:
|
|
950
|
+
patch.holder_lease_expires_ms === undefined
|
|
951
|
+
? current.holder_lease_expires_ms
|
|
952
|
+
: patch.holder_lease_expires_ms,
|
|
953
|
+
charter_version:
|
|
954
|
+
patch.charter_version === undefined
|
|
955
|
+
? current.charter_version
|
|
956
|
+
: patch.charter_version,
|
|
957
|
+
charter_acked_epoch:
|
|
958
|
+
patch.charter_acked_epoch === undefined
|
|
959
|
+
? current.charter_acked_epoch
|
|
960
|
+
: patch.charter_acked_epoch,
|
|
961
|
+
retry_count: patch.retry_count ?? current.retry_count,
|
|
962
|
+
next_probe_ms:
|
|
963
|
+
patch.next_probe_ms === undefined
|
|
964
|
+
? current.next_probe_ms
|
|
965
|
+
: patch.next_probe_ms,
|
|
966
|
+
blocked_reason:
|
|
967
|
+
patch.blocked_reason === undefined
|
|
968
|
+
? current.blocked_reason
|
|
969
|
+
: patch.blocked_reason,
|
|
970
|
+
last_transition_ms: patch.last_transition_ms ?? Date.now(),
|
|
971
|
+
last_reason: patch.last_reason ?? current.last_reason,
|
|
972
|
+
};
|
|
973
|
+
if (S.compareAndSetRole.run(next).changes !== 1) {
|
|
974
|
+
const latest = S.getRole.get(identity.role_key_wire) ?? null;
|
|
975
|
+
return {
|
|
976
|
+
ok: false,
|
|
977
|
+
reason: roleFailureReason(latest, expected),
|
|
978
|
+
role: latest,
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
return { ok: true, role: S.getRole.get(identity.role_key_wire) };
|
|
982
|
+
},
|
|
983
|
+
|
|
984
|
+
listRoleKeys(filter = {}) {
|
|
985
|
+
const clauses = [];
|
|
986
|
+
const params = {};
|
|
987
|
+
for (const field of ["project_id", "role_kind", "scope_id", "state"]) {
|
|
988
|
+
if (filter[field] !== undefined) {
|
|
989
|
+
clauses.push(`${field}=@${field}`);
|
|
990
|
+
params[field] = filter[field];
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
if (Array.isArray(filter.states) && filter.states.length > 0) {
|
|
994
|
+
clauses.push(
|
|
995
|
+
`state IN (${filter.states.map((_, index) => `@state_${index}`).join(",")})`,
|
|
996
|
+
);
|
|
997
|
+
filter.states.forEach((state, index) => {
|
|
998
|
+
params[`state_${index}`] = state;
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
const where = clauses.length ? ` WHERE ${clauses.join(" AND ")}` : "";
|
|
1002
|
+
return db
|
|
1003
|
+
.prepare(
|
|
1004
|
+
`SELECT role_key_wire FROM role_registry${where} ORDER BY role_key_wire`,
|
|
1005
|
+
)
|
|
1006
|
+
.pluck()
|
|
1007
|
+
.all(params);
|
|
1008
|
+
},
|
|
1009
|
+
|
|
1010
|
+
listRolesWithExpiredHolder(now) {
|
|
1011
|
+
return S.expiredHolderRoles.all(now);
|
|
1012
|
+
},
|
|
1013
|
+
|
|
1014
|
+
listRoleCandidates(roleKeyWire) {
|
|
1015
|
+
decodeRoleKey(roleKeyWire);
|
|
1016
|
+
return S.listRoleCandidates.all(roleKeyWire).map(parseRoleCandidateRow);
|
|
1017
|
+
},
|
|
1018
|
+
|
|
1019
|
+
upsertRoleCandidate(input) {
|
|
1020
|
+
const { role_key_wire } = roleIdentity(input);
|
|
1021
|
+
const current = S.getRoleCandidate.get(role_key_wire, input.agent_id);
|
|
1022
|
+
S.upsertRoleCandidate.run({
|
|
1023
|
+
role_key_wire,
|
|
1024
|
+
agent_id: input.agent_id,
|
|
1025
|
+
source: input.source ?? current?.source ?? "operator",
|
|
1026
|
+
priority: input.priority ?? current?.priority ?? 0,
|
|
1027
|
+
transport_locators_json: JSON.stringify(
|
|
1028
|
+
input.transport_locators ??
|
|
1029
|
+
parseJson(current?.transport_locators_json, []),
|
|
1030
|
+
),
|
|
1031
|
+
consecutive_probe_failures:
|
|
1032
|
+
input.consecutive_probe_failures ??
|
|
1033
|
+
current?.consecutive_probe_failures ??
|
|
1034
|
+
0,
|
|
1035
|
+
excluded_until_ms:
|
|
1036
|
+
input.excluded_until_ms === undefined
|
|
1037
|
+
? (current?.excluded_until_ms ?? null)
|
|
1038
|
+
: input.excluded_until_ms,
|
|
1039
|
+
last_probe_ms:
|
|
1040
|
+
input.last_probe_ms === undefined
|
|
1041
|
+
? (current?.last_probe_ms ?? null)
|
|
1042
|
+
: input.last_probe_ms,
|
|
1043
|
+
last_probe_code:
|
|
1044
|
+
input.last_probe_code === undefined
|
|
1045
|
+
? (current?.last_probe_code ?? null)
|
|
1046
|
+
: input.last_probe_code,
|
|
1047
|
+
updated_at_ms: input.updated_at_ms ?? Date.now(),
|
|
1048
|
+
});
|
|
1049
|
+
return parseRoleCandidateRow(
|
|
1050
|
+
S.getRoleCandidate.get(role_key_wire, input.agent_id),
|
|
1051
|
+
);
|
|
1052
|
+
},
|
|
1053
|
+
|
|
1054
|
+
updateCandidateReachability(input) {
|
|
1055
|
+
const { role_key_wire } = roleIdentity(input);
|
|
1056
|
+
const current = S.getRoleCandidate.get(role_key_wire, input.agent_id);
|
|
1057
|
+
if (!current) return { ok: false, reason: "not_found" };
|
|
1058
|
+
|
|
1059
|
+
let failures =
|
|
1060
|
+
input.consecutive_probe_failures ?? current.consecutive_probe_failures;
|
|
1061
|
+
if (
|
|
1062
|
+
input.probe_failed === true ||
|
|
1063
|
+
input.increment_probe_failures === true
|
|
1064
|
+
) {
|
|
1065
|
+
failures = current.consecutive_probe_failures + 1;
|
|
1066
|
+
} else if (input.probe_succeeded === true) {
|
|
1067
|
+
failures = 0;
|
|
1068
|
+
}
|
|
1069
|
+
S.updateCandidateReachability.run({
|
|
1070
|
+
role_key_wire,
|
|
1071
|
+
agent_id: input.agent_id,
|
|
1072
|
+
consecutive_probe_failures: failures,
|
|
1073
|
+
excluded_until_ms:
|
|
1074
|
+
input.excluded_until_ms === undefined
|
|
1075
|
+
? current.excluded_until_ms
|
|
1076
|
+
: input.excluded_until_ms,
|
|
1077
|
+
last_probe_ms: input.last_probe_ms ?? Date.now(),
|
|
1078
|
+
last_probe_code:
|
|
1079
|
+
input.last_probe_code === undefined
|
|
1080
|
+
? current.last_probe_code
|
|
1081
|
+
: input.last_probe_code,
|
|
1082
|
+
updated_at_ms: input.updated_at_ms ?? Date.now(),
|
|
1083
|
+
});
|
|
1084
|
+
return {
|
|
1085
|
+
ok: true,
|
|
1086
|
+
candidate: parseRoleCandidateRow(
|
|
1087
|
+
S.getRoleCandidate.get(role_key_wire, input.agent_id),
|
|
1088
|
+
),
|
|
1089
|
+
};
|
|
1090
|
+
},
|
|
1091
|
+
|
|
1092
|
+
listPendingRoleMessages(roleKeyWire, now) {
|
|
1093
|
+
decodeRoleKey(roleKeyWire);
|
|
1094
|
+
return S.pendingRoleMessages.all(roleKeyWire, now).map(parseMessageRow);
|
|
1095
|
+
},
|
|
1096
|
+
|
|
1097
|
+
recoverRoleRegistry(now) {
|
|
1098
|
+
return recoverRegistry.immediate(now);
|
|
457
1099
|
},
|
|
458
1100
|
|
|
459
1101
|
auditLog({
|
|
@@ -467,6 +1109,7 @@ export function createStore(dbPath, options = {}) {
|
|
|
467
1109
|
trace_id,
|
|
468
1110
|
correlation_id,
|
|
469
1111
|
status = "queued",
|
|
1112
|
+
role_key_wire = null,
|
|
470
1113
|
}) {
|
|
471
1114
|
const now = Date.now();
|
|
472
1115
|
const row = {
|
|
@@ -483,6 +1126,7 @@ export function createStore(dbPath, options = {}) {
|
|
|
483
1126
|
trace_id: trace_id || uuidv7(),
|
|
484
1127
|
payload_json: JSON.stringify(payload),
|
|
485
1128
|
status,
|
|
1129
|
+
role_key_wire,
|
|
486
1130
|
};
|
|
487
1131
|
S.insertAuditMessage.run(row);
|
|
488
1132
|
return { ...row, payload };
|
|
@@ -599,16 +1243,14 @@ export function createStore(dbPath, options = {}) {
|
|
|
599
1243
|
return S.pendingHR.all().map(parseHumanRequestRow);
|
|
600
1244
|
},
|
|
601
1245
|
|
|
602
|
-
expireHumanRequests() {
|
|
603
|
-
return S.expireHR.run(Date.now()).changes;
|
|
604
|
-
},
|
|
605
|
-
|
|
606
1246
|
moveToDeadLetter(messageId, reason, lastError = null) {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
S.
|
|
1247
|
+
const failedAt = Date.now();
|
|
1248
|
+
return db.transaction(() => {
|
|
1249
|
+
const changed = S.claimDeadLetter.run(messageId).changes;
|
|
1250
|
+
if (!changed) return false;
|
|
1251
|
+
S.insertDL.run(messageId, reason, failedAt, lastError);
|
|
1252
|
+
return true;
|
|
610
1253
|
})();
|
|
611
|
-
return true;
|
|
612
1254
|
},
|
|
613
1255
|
|
|
614
1256
|
getDeadLetters(limit = 50) {
|
|
@@ -638,7 +1280,10 @@ export function createStore(dbPath, options = {}) {
|
|
|
638
1280
|
error = null,
|
|
639
1281
|
}) {
|
|
640
1282
|
const now = Date.now();
|
|
641
|
-
const normalizedTimeout = clampDurationMs(
|
|
1283
|
+
const normalizedTimeout = clampDurationMs(
|
|
1284
|
+
timeout_ms,
|
|
1285
|
+
DEFAULT_ASSIGN_TIMEOUT_MS,
|
|
1286
|
+
);
|
|
642
1287
|
const row = {
|
|
643
1288
|
job_id: job_id || uuidv7(),
|
|
644
1289
|
supervisor_agent,
|
|
@@ -761,7 +1406,9 @@ export function createStore(dbPath, options = {}) {
|
|
|
761
1406
|
: nextStatus === "running"
|
|
762
1407
|
? current.started_at_ms || now
|
|
763
1408
|
: current.started_at_ms,
|
|
764
|
-
dispatched_at_ms: Object.hasOwn(patch, "dispatched_at_ms")
|
|
1409
|
+
dispatched_at_ms: Object.hasOwn(patch, "dispatched_at_ms")
|
|
1410
|
+
? patch.dispatched_at_ms
|
|
1411
|
+
: current.dispatched_at_ms,
|
|
765
1412
|
completed_at_ms: Object.hasOwn(patch, "completed_at_ms")
|
|
766
1413
|
? patch.completed_at_ms
|
|
767
1414
|
: isTerminal
|
|
@@ -873,16 +1520,27 @@ export function createStore(dbPath, options = {}) {
|
|
|
873
1520
|
});
|
|
874
1521
|
},
|
|
875
1522
|
|
|
876
|
-
sweepExpired() {
|
|
877
|
-
const now = Date.now();
|
|
1523
|
+
sweepExpired({ now_ms = Date.now() } = {}) {
|
|
878
1524
|
return db.transaction(() => {
|
|
879
|
-
const
|
|
880
|
-
for (const
|
|
881
|
-
S.
|
|
882
|
-
|
|
1525
|
+
const transitions = [];
|
|
1526
|
+
for (const row of S.findExpired.all(now_ms)) {
|
|
1527
|
+
if (!S.claimExpired.run(row.id, now_ms).changes) continue;
|
|
1528
|
+
options.beforeDeadLetterInsert?.({
|
|
1529
|
+
message_id: row.id,
|
|
1530
|
+
reason: "ttl_expired",
|
|
1531
|
+
failed_at_ms: now_ms,
|
|
1532
|
+
});
|
|
1533
|
+
const dlqInserted =
|
|
1534
|
+
S.insertDL.run(row.id, "ttl_expired", now_ms, null).changes === 1;
|
|
1535
|
+
transitions.push({
|
|
1536
|
+
message: parseMessageRow(row),
|
|
1537
|
+
previous_status: row.status,
|
|
1538
|
+
reason: "ttl_expired",
|
|
1539
|
+
failed_at_ms: now_ms,
|
|
1540
|
+
dlq_inserted: dlqInserted,
|
|
1541
|
+
});
|
|
883
1542
|
}
|
|
884
|
-
|
|
885
|
-
return { messages: expired.length, human_requests: humanRequests };
|
|
1543
|
+
return { now_ms, messages: transitions.length, transitions };
|
|
886
1544
|
})();
|
|
887
1545
|
},
|
|
888
1546
|
|