@triflux/remote 10.42.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 +9 -0
- package/hub/schema.sql +62 -2
- package/hub/server.mjs +131 -44
- package/hub/store.mjs +616 -19
- package/hub/team/claude-daemon-control.mjs +33 -12
- package/hub/team/cli/commands/start/parse-args.mjs +2 -2
- package/hub/team/conductor.mjs +23 -2
- package/hub/team/execution-mode.mjs +12 -7
- package/hub/team/headless.mjs +311 -24
- package/hub/team/intervention.mjs +530 -0
- package/hub/team/notify.mjs +31 -11
- package/hub/team/retry-state-machine.mjs +12 -3
- package/hub/team/swarm-hypervisor.mjs +21 -2
- package/hub/team/swarm-locks.mjs +17 -1
- package/hub/team/swarm-preflight.mjs +47 -0
- package/hub/team/uds-orchestrator.mjs +62 -9
- package/hub/team/worktree-lifecycle.mjs +56 -59
- package/hub/tools.mjs +16 -3
- package/hub/workers/codex-app-server-worker.mjs +54 -5
- package/hub/workers/codex-mcp.mjs +21 -5
- 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-agy.mjs +1 -0
- package/scripts/lib/cli-claude.mjs +1 -0
- package/scripts/lib/cli-codex.mjs +21 -160
- package/scripts/lib/codex-profile-config.mjs +31 -5
- 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,12 +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 {
|
|
9
|
+
clampDurationMs,
|
|
10
|
+
DEFAULT_ASSIGN_TIMEOUT_MS,
|
|
11
|
+
DEFAULT_ASSIGN_TTL_MS,
|
|
12
|
+
} from "@triflux/core/hub/lib/timeout-defaults.mjs";
|
|
8
13
|
import { uuidv7 } from "@triflux/core/hub/lib/uuidv7.mjs";
|
|
9
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";
|
|
10
19
|
|
|
11
20
|
export { uuidv7 };
|
|
12
21
|
|
|
@@ -71,6 +80,21 @@ function parseReflexionRow(row) {
|
|
|
71
80
|
};
|
|
72
81
|
}
|
|
73
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
|
+
|
|
74
98
|
function hasColumn(db, tableName, columnName) {
|
|
75
99
|
const rows = db.prepare(`PRAGMA table_info(${tableName})`).all();
|
|
76
100
|
return rows.some((row) => row.name === columnName);
|
|
@@ -81,6 +105,23 @@ function ensureColumn(db, tableName, columnName, definition) {
|
|
|
81
105
|
db.exec(`ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${definition}`);
|
|
82
106
|
}
|
|
83
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
|
+
|
|
84
125
|
/**
|
|
85
126
|
* 저장소 생성
|
|
86
127
|
* @param {string} dbPath
|
|
@@ -111,7 +152,7 @@ export function createStore(dbPath, options = {}) {
|
|
|
111
152
|
db.exec(
|
|
112
153
|
"CREATE TABLE IF NOT EXISTS _meta (key TEXT PRIMARY KEY, value TEXT)",
|
|
113
154
|
);
|
|
114
|
-
const SCHEMA_VERSION = "
|
|
155
|
+
const SCHEMA_VERSION = "6";
|
|
115
156
|
const curVer = (() => {
|
|
116
157
|
try {
|
|
117
158
|
return db
|
|
@@ -125,6 +166,9 @@ export function createStore(dbPath, options = {}) {
|
|
|
125
166
|
// 마이그레이션 전략: 스키마 버전이 다르면 schema.sql을 재실행한다.
|
|
126
167
|
// schema.sql은 CREATE TABLE IF NOT EXISTS 패턴을 사용하므로 멱등하게 적용된다.
|
|
127
168
|
// 비파괴적 컬럼 추가는 자동으로 처리되지만, 컬럼 제거/이름 변경은 수동 마이그레이션이 필요하다.
|
|
169
|
+
// v5 테이블에는 schema.sql의 v6 인덱스가 참조하는 컬럼이 없으므로
|
|
170
|
+
// CREATE IF NOT EXISTS 재실행 전에 멱등 컬럼 seam을 먼저 적용한다.
|
|
171
|
+
ensureV6Columns(db);
|
|
128
172
|
if (curVer !== SCHEMA_VERSION) {
|
|
129
173
|
if (curVer != null) {
|
|
130
174
|
// 이미 버전이 기록된 DB에서 버전 불일치가 발생한 경우 경고한다.
|
|
@@ -137,12 +181,15 @@ export function createStore(dbPath, options = {}) {
|
|
|
137
181
|
"INSERT OR REPLACE INTO _meta (key, value) VALUES ('schema_version', ?)",
|
|
138
182
|
).run(SCHEMA_VERSION);
|
|
139
183
|
}
|
|
184
|
+
// 매 부팅 실행해 부분 적용된 마이그레이션도 안전하게 복구한다.
|
|
185
|
+
ensureV6Columns(db);
|
|
140
186
|
ensureColumn(
|
|
141
187
|
db,
|
|
142
188
|
"reflexion_entries",
|
|
143
189
|
"type",
|
|
144
190
|
"TEXT NOT NULL DEFAULT 'reflexion'",
|
|
145
191
|
);
|
|
192
|
+
ensureColumn(db, "assign_jobs", "dispatched_at_ms", "INTEGER");
|
|
146
193
|
ensureColumn(
|
|
147
194
|
db,
|
|
148
195
|
"reflexion_entries",
|
|
@@ -183,9 +230,164 @@ export function createStore(dbPath, options = {}) {
|
|
|
183
230
|
"UPDATE agents SET status='offline' WHERE status='stale' AND lease_expires_ms < ? - 300000",
|
|
184
231
|
),
|
|
185
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
|
+
|
|
186
388
|
insertAuditMessage: db.prepare(`
|
|
187
|
-
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)
|
|
188
|
-
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)`),
|
|
189
391
|
getMsg: db.prepare("SELECT * FROM messages WHERE id=?"),
|
|
190
392
|
getResponse: db.prepare(
|
|
191
393
|
"SELECT * FROM messages WHERE correlation_id=? AND type='response' ORDER BY created_at_ms DESC LIMIT 1",
|
|
@@ -233,12 +435,12 @@ export function createStore(dbPath, options = {}) {
|
|
|
233
435
|
job_id, supervisor_agent, worker_agent, topic, task, payload_json,
|
|
234
436
|
status, attempt, retry_count, max_retries, priority, ttl_ms, timeout_ms, deadline_ms,
|
|
235
437
|
trace_id, correlation_id, last_message_id, result_json, error_json,
|
|
236
|
-
created_at_ms, updated_at_ms, started_at_ms, completed_at_ms, last_retry_at_ms
|
|
438
|
+
created_at_ms, updated_at_ms, started_at_ms, dispatched_at_ms, completed_at_ms, last_retry_at_ms
|
|
237
439
|
) VALUES (
|
|
238
440
|
@job_id, @supervisor_agent, @worker_agent, @topic, @task, @payload_json,
|
|
239
441
|
@status, @attempt, @retry_count, @max_retries, @priority, @ttl_ms, @timeout_ms, @deadline_ms,
|
|
240
442
|
@trace_id, @correlation_id, @last_message_id, @result_json, @error_json,
|
|
241
|
-
@created_at_ms, @updated_at_ms, @started_at_ms, @completed_at_ms, @last_retry_at_ms
|
|
443
|
+
@created_at_ms, @updated_at_ms, @started_at_ms, @dispatched_at_ms, @completed_at_ms, @last_retry_at_ms
|
|
242
444
|
)`),
|
|
243
445
|
getAssign: db.prepare("SELECT * FROM assign_jobs WHERE job_id = ?"),
|
|
244
446
|
updateAssign: db.prepare(`
|
|
@@ -263,6 +465,7 @@ export function createStore(dbPath, options = {}) {
|
|
|
263
465
|
error_json=@error_json,
|
|
264
466
|
updated_at_ms=@updated_at_ms,
|
|
265
467
|
started_at_ms=@started_at_ms,
|
|
468
|
+
dispatched_at_ms=@dispatched_at_ms,
|
|
266
469
|
completed_at_ms=@completed_at_ms,
|
|
267
470
|
last_retry_at_ms=@last_retry_at_ms
|
|
268
471
|
WHERE job_id=@job_id`),
|
|
@@ -356,12 +559,160 @@ export function createStore(dbPath, options = {}) {
|
|
|
356
559
|
return Math.max(1, Math.min(Math.trunc(num), 9));
|
|
357
560
|
}
|
|
358
561
|
|
|
359
|
-
function
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
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;
|
|
363
577
|
}
|
|
364
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
|
+
|
|
365
716
|
const store = {
|
|
366
717
|
db,
|
|
367
718
|
uuidv7,
|
|
@@ -419,11 +770,16 @@ export function createStore(dbPath, options = {}) {
|
|
|
419
770
|
|
|
420
771
|
refreshLease(agentId, ttlMs = 30000) {
|
|
421
772
|
const now = Date.now();
|
|
422
|
-
S.heartbeat.run(now, now + ttlMs, agentId);
|
|
773
|
+
const write = S.heartbeat.run(now, now + ttlMs, agentId);
|
|
423
774
|
return {
|
|
424
775
|
agent_id: agentId,
|
|
425
776
|
lease_expires_ms: now + ttlMs,
|
|
426
777
|
server_time_ms: now,
|
|
778
|
+
effective: {
|
|
779
|
+
updated: write.changes > 0,
|
|
780
|
+
store_type: "sqlite",
|
|
781
|
+
db_path: dbPath ?? null,
|
|
782
|
+
},
|
|
427
783
|
};
|
|
428
784
|
},
|
|
429
785
|
|
|
@@ -458,6 +814,233 @@ export function createStore(dbPath, options = {}) {
|
|
|
458
814
|
return S.setAgentStatus.run(status, agentId).changes > 0;
|
|
459
815
|
},
|
|
460
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
|
+
|
|
461
1044
|
auditLog({
|
|
462
1045
|
type,
|
|
463
1046
|
from,
|
|
@@ -469,6 +1052,7 @@ export function createStore(dbPath, options = {}) {
|
|
|
469
1052
|
trace_id,
|
|
470
1053
|
correlation_id,
|
|
471
1054
|
status = "queued",
|
|
1055
|
+
role_key_wire = null,
|
|
472
1056
|
}) {
|
|
473
1057
|
const now = Date.now();
|
|
474
1058
|
const row = {
|
|
@@ -485,6 +1069,7 @@ export function createStore(dbPath, options = {}) {
|
|
|
485
1069
|
trace_id: trace_id || uuidv7(),
|
|
486
1070
|
payload_json: JSON.stringify(payload),
|
|
487
1071
|
status,
|
|
1072
|
+
role_key_wire,
|
|
488
1073
|
};
|
|
489
1074
|
S.insertAuditMessage.run(row);
|
|
490
1075
|
return { ...row, payload };
|
|
@@ -629,9 +1214,10 @@ export function createStore(dbPath, options = {}) {
|
|
|
629
1214
|
retry_count = 0,
|
|
630
1215
|
max_retries = 0,
|
|
631
1216
|
priority = 5,
|
|
632
|
-
ttl_ms =
|
|
633
|
-
timeout_ms =
|
|
1217
|
+
ttl_ms = DEFAULT_ASSIGN_TTL_MS,
|
|
1218
|
+
timeout_ms = DEFAULT_ASSIGN_TIMEOUT_MS,
|
|
634
1219
|
deadline_ms,
|
|
1220
|
+
dispatched_at_ms = null,
|
|
635
1221
|
trace_id,
|
|
636
1222
|
correlation_id,
|
|
637
1223
|
last_message_id = null,
|
|
@@ -639,7 +1225,10 @@ export function createStore(dbPath, options = {}) {
|
|
|
639
1225
|
error = null,
|
|
640
1226
|
}) {
|
|
641
1227
|
const now = Date.now();
|
|
642
|
-
const normalizedTimeout =
|
|
1228
|
+
const normalizedTimeout = clampDurationMs(
|
|
1229
|
+
timeout_ms,
|
|
1230
|
+
DEFAULT_ASSIGN_TIMEOUT_MS,
|
|
1231
|
+
);
|
|
643
1232
|
const row = {
|
|
644
1233
|
job_id: job_id || uuidv7(),
|
|
645
1234
|
supervisor_agent,
|
|
@@ -652,7 +1241,7 @@ export function createStore(dbPath, options = {}) {
|
|
|
652
1241
|
retry_count: Math.max(0, Number(retry_count) || 0),
|
|
653
1242
|
max_retries: Math.max(0, Number(max_retries) || 0),
|
|
654
1243
|
priority: clampPriority(priority, 5),
|
|
655
|
-
ttl_ms:
|
|
1244
|
+
ttl_ms: clampDurationMs(ttl_ms, normalizedTimeout),
|
|
656
1245
|
timeout_ms: normalizedTimeout,
|
|
657
1246
|
deadline_ms: Number.isFinite(Number(deadline_ms))
|
|
658
1247
|
? Math.trunc(Number(deadline_ms))
|
|
@@ -665,6 +1254,10 @@ export function createStore(dbPath, options = {}) {
|
|
|
665
1254
|
created_at_ms: now,
|
|
666
1255
|
updated_at_ms: now,
|
|
667
1256
|
started_at_ms: status === "running" ? now : null,
|
|
1257
|
+
dispatched_at_ms:
|
|
1258
|
+
dispatched_at_ms == null || !Number.isFinite(Number(dispatched_at_ms))
|
|
1259
|
+
? null
|
|
1260
|
+
: Math.trunc(Number(dispatched_at_ms)),
|
|
668
1261
|
completed_at_ms: ["succeeded", "failed", "timed_out"].includes(status)
|
|
669
1262
|
? now
|
|
670
1263
|
: null,
|
|
@@ -689,7 +1282,7 @@ export function createStore(dbPath, options = {}) {
|
|
|
689
1282
|
const isTerminal = ["succeeded", "failed", "timed_out"].includes(
|
|
690
1283
|
nextStatus,
|
|
691
1284
|
);
|
|
692
|
-
const nextTimeout =
|
|
1285
|
+
const nextTimeout = clampDurationMs(
|
|
693
1286
|
patch.timeout_ms ?? current.timeout_ms,
|
|
694
1287
|
current.timeout_ms,
|
|
695
1288
|
);
|
|
@@ -717,7 +1310,7 @@ export function createStore(dbPath, options = {}) {
|
|
|
717
1310
|
patch.priority ?? current.priority,
|
|
718
1311
|
current.priority || 5,
|
|
719
1312
|
),
|
|
720
|
-
ttl_ms:
|
|
1313
|
+
ttl_ms: clampDurationMs(
|
|
721
1314
|
patch.ttl_ms ?? current.ttl_ms,
|
|
722
1315
|
current.ttl_ms || nextTimeout,
|
|
723
1316
|
),
|
|
@@ -758,6 +1351,9 @@ export function createStore(dbPath, options = {}) {
|
|
|
758
1351
|
: nextStatus === "running"
|
|
759
1352
|
? current.started_at_ms || now
|
|
760
1353
|
: current.started_at_ms,
|
|
1354
|
+
dispatched_at_ms: Object.hasOwn(patch, "dispatched_at_ms")
|
|
1355
|
+
? patch.dispatched_at_ms
|
|
1356
|
+
: current.dispatched_at_ms,
|
|
761
1357
|
completed_at_ms: Object.hasOwn(patch, "completed_at_ms")
|
|
762
1358
|
? patch.completed_at_ms
|
|
763
1359
|
: isTerminal
|
|
@@ -849,7 +1445,7 @@ export function createStore(dbPath, options = {}) {
|
|
|
849
1445
|
current.attempt + 1,
|
|
850
1446
|
Number(patch.attempt ?? current.attempt + 1) || 1,
|
|
851
1447
|
);
|
|
852
|
-
const nextTimeout =
|
|
1448
|
+
const nextTimeout = clampDurationMs(
|
|
853
1449
|
patch.timeout_ms ?? current.timeout_ms,
|
|
854
1450
|
current.timeout_ms,
|
|
855
1451
|
);
|
|
@@ -861,6 +1457,7 @@ export function createStore(dbPath, options = {}) {
|
|
|
861
1457
|
deadline_ms: Date.now() + nextTimeout,
|
|
862
1458
|
completed_at_ms: null,
|
|
863
1459
|
started_at_ms: null,
|
|
1460
|
+
dispatched_at_ms: null,
|
|
864
1461
|
last_retry_at_ms: Date.now(),
|
|
865
1462
|
result: patch.result ?? null,
|
|
866
1463
|
error: Object.hasOwn(patch, "error") ? patch.error : current.error,
|