@triflux/core 10.42.0 → 10.43.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/hooks/pipeline-stop.mjs +1 -1
- package/hub/bridge.mjs +80 -1
- package/hub/cli-adapter-base.mjs +100 -19
- package/hub/codex-adapter.mjs +5 -1
- package/hub/dynamic-routing-engine.mjs +1 -1
- package/hub/gemini-adapter.mjs +3 -1
- package/hub/intent.mjs +13 -9
- package/hub/lib/cto-env.mjs +46 -0
- package/hub/lib/memory-store.mjs +25 -13
- package/hub/lib/timeout-defaults.mjs +45 -0
- package/hub/lib/worker-lifecycle.mjs +101 -0
- package/hub/middleware/request-logger.mjs +4 -1
- package/hub/role-contract.mjs +314 -0
- package/hub/role-control-events.mjs +113 -0
- package/hub/router.mjs +111 -29
- package/hub/team/claude-daemon-control.mjs +28 -15
- package/hub/team/intervention.mjs +530 -0
- package/hub/team/retry-state-machine.mjs +6 -2
- package/hud/constants.mjs +21 -0
- package/hud/context-monitor.mjs +18 -21
- package/hud/hud-qos-status.mjs +1 -1
- package/hud/providers/codex-probe.mjs +216 -0
- package/hud/providers/codex.mjs +206 -29
- package/hud/renderers.mjs +6 -9
- package/package.json +1 -1
- package/scripts/lib/cli-agy.mjs +1 -0
- package/scripts/lib/cli-claude.mjs +1 -0
- package/scripts/lib/cli-codex.mjs +22 -21
- package/scripts/lib/codex-profile-config.mjs +2 -2
package/hub/router.mjs
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
// hub/router.mjs — 실시간 라우팅/수신함 상태 관리자
|
|
2
2
|
// SQLite는 감사 로그만 담당하고, 실제 배달 상태는 메모리에서 관리한다.
|
|
3
3
|
import { EventEmitter, once } from "node:events";
|
|
4
|
+
import {
|
|
5
|
+
clampDurationMs,
|
|
6
|
+
DEFAULT_ASSIGN_TIMEOUT_MS,
|
|
7
|
+
DEFAULT_ASSIGN_TTL_MS,
|
|
8
|
+
DEFAULT_HANDOFF_TTL_MS,
|
|
9
|
+
resolveHardCeilingMs,
|
|
10
|
+
} from "./lib/timeout-defaults.mjs";
|
|
4
11
|
import { uuidv7 } from "./lib/uuidv7.mjs";
|
|
5
12
|
|
|
6
13
|
const ASSIGN_PENDING_STATUSES = new Set(["queued", "running"]);
|
|
@@ -49,17 +56,6 @@ function rolePriority(metadata = {}, roleName = "") {
|
|
|
49
56
|
return 0;
|
|
50
57
|
}
|
|
51
58
|
|
|
52
|
-
function clampAssignDuration(
|
|
53
|
-
value,
|
|
54
|
-
fallback = 600000,
|
|
55
|
-
min = 1000,
|
|
56
|
-
max = 86400000,
|
|
57
|
-
) {
|
|
58
|
-
const num = Number(value);
|
|
59
|
-
if (!Number.isFinite(num)) return fallback;
|
|
60
|
-
return Math.max(min, Math.min(Math.trunc(num), max));
|
|
61
|
-
}
|
|
62
|
-
|
|
63
59
|
function normalizeAssignTerminalStatus(input, metadata = {}) {
|
|
64
60
|
const status = String(input || "")
|
|
65
61
|
.trim()
|
|
@@ -606,11 +602,31 @@ export function createRouter(store) {
|
|
|
606
602
|
record.message.status = "delivered";
|
|
607
603
|
store.updateMessageStatus(messageId, "delivered");
|
|
608
604
|
recordLatency(delivery.delivered_at_ms - record.message.created_at_ms);
|
|
605
|
+
rearmAssignDeadlineOnDispatch(record.message, delivery.delivered_at_ms);
|
|
609
606
|
return true;
|
|
610
607
|
}
|
|
611
608
|
return false;
|
|
612
609
|
}
|
|
613
610
|
|
|
611
|
+
function rearmAssignDeadlineOnDispatch(message, deliveredAtMs) {
|
|
612
|
+
if (message?.payload?.kind !== "assign.job") return;
|
|
613
|
+
const jobId = message.payload.assign_job_id;
|
|
614
|
+
if (!jobId) return;
|
|
615
|
+
const job = store.getAssign(jobId);
|
|
616
|
+
if (
|
|
617
|
+
!job ||
|
|
618
|
+
!ASSIGN_PENDING_STATUSES.has(job.status) ||
|
|
619
|
+
job.dispatched_at_ms
|
|
620
|
+
)
|
|
621
|
+
return;
|
|
622
|
+
if (Number(message.payload.attempt) !== job.attempt) return;
|
|
623
|
+
store.updateAssignStatus(job.job_id, job.status, {
|
|
624
|
+
dispatched_at_ms: deliveredAtMs,
|
|
625
|
+
deadline_ms:
|
|
626
|
+
deliveredAtMs + clampDurationMs(job.timeout_ms, job.timeout_ms),
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
|
|
614
630
|
function ackMessages(ids, agentId) {
|
|
615
631
|
const now = Date.now();
|
|
616
632
|
let count = 0;
|
|
@@ -706,7 +722,7 @@ export function createRouter(store) {
|
|
|
706
722
|
to: job.supervisor_agent,
|
|
707
723
|
topic: "assign.result",
|
|
708
724
|
priority: Math.max(5, job.priority || 5),
|
|
709
|
-
ttl_ms: job.ttl_ms || job.timeout_ms ||
|
|
725
|
+
ttl_ms: job.ttl_ms || job.timeout_ms || DEFAULT_ASSIGN_TTL_MS,
|
|
710
726
|
payload: {
|
|
711
727
|
event,
|
|
712
728
|
...buildAssignSnapshot(job),
|
|
@@ -725,7 +741,7 @@ export function createRouter(store) {
|
|
|
725
741
|
to: job.worker_agent,
|
|
726
742
|
topic: job.topic || "assign.job",
|
|
727
743
|
priority: job.priority || 5,
|
|
728
|
-
ttl_ms: job.ttl_ms || job.timeout_ms ||
|
|
744
|
+
ttl_ms: job.ttl_ms || job.timeout_ms || DEFAULT_ASSIGN_TTL_MS,
|
|
729
745
|
payload: {
|
|
730
746
|
kind: "assign.job",
|
|
731
747
|
reason,
|
|
@@ -797,6 +813,24 @@ export function createRouter(store) {
|
|
|
797
813
|
const timedOut = store.updateAssignStatus(job.job_id, "timed_out", {
|
|
798
814
|
error: job.error ?? { message: "assign job timed out" },
|
|
799
815
|
});
|
|
816
|
+
if (timedOut?.worker_agent) {
|
|
817
|
+
dispatchMessage({
|
|
818
|
+
type: "event",
|
|
819
|
+
from: "assign-router",
|
|
820
|
+
to: timedOut.worker_agent,
|
|
821
|
+
topic: "assign.cancel",
|
|
822
|
+
priority: 8,
|
|
823
|
+
ttl_ms: clampDurationMs(timedOut.ttl_ms, DEFAULT_ASSIGN_TTL_MS),
|
|
824
|
+
payload: {
|
|
825
|
+
kind: "assign.cancel",
|
|
826
|
+
assign_job_id: timedOut.job_id,
|
|
827
|
+
attempt: timedOut.attempt,
|
|
828
|
+
reason: "timed_out",
|
|
829
|
+
},
|
|
830
|
+
trace_id: timedOut.trace_id,
|
|
831
|
+
correlation_id: timedOut.correlation_id,
|
|
832
|
+
});
|
|
833
|
+
}
|
|
800
834
|
|
|
801
835
|
if (timedOut.retry_count < timedOut.max_retries) {
|
|
802
836
|
return scheduleAssignRetry(
|
|
@@ -1146,7 +1180,7 @@ export function createRouter(store) {
|
|
|
1146
1180
|
acceptance_criteria,
|
|
1147
1181
|
context_refs,
|
|
1148
1182
|
priority = 5,
|
|
1149
|
-
ttl_ms =
|
|
1183
|
+
ttl_ms = DEFAULT_HANDOFF_TTL_MS,
|
|
1150
1184
|
trace_id,
|
|
1151
1185
|
correlation_id,
|
|
1152
1186
|
}) {
|
|
@@ -1174,8 +1208,8 @@ export function createRouter(store) {
|
|
|
1174
1208
|
task = "",
|
|
1175
1209
|
payload = {},
|
|
1176
1210
|
priority = 5,
|
|
1177
|
-
ttl_ms =
|
|
1178
|
-
timeout_ms =
|
|
1211
|
+
ttl_ms = DEFAULT_ASSIGN_TTL_MS,
|
|
1212
|
+
timeout_ms = DEFAULT_ASSIGN_TIMEOUT_MS,
|
|
1179
1213
|
max_retries = 0,
|
|
1180
1214
|
trace_id,
|
|
1181
1215
|
correlation_id,
|
|
@@ -1232,16 +1266,6 @@ export function createRouter(store) {
|
|
|
1232
1266
|
},
|
|
1233
1267
|
};
|
|
1234
1268
|
}
|
|
1235
|
-
if (Number.isFinite(Number(attempt)) && Number(attempt) !== job.attempt) {
|
|
1236
|
-
return {
|
|
1237
|
-
ok: false,
|
|
1238
|
-
error: {
|
|
1239
|
-
code: "ASSIGN_ATTEMPT_MISMATCH",
|
|
1240
|
-
message: `stale assign result for attempt ${attempt} (current ${job.attempt})`,
|
|
1241
|
-
},
|
|
1242
|
-
};
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
1269
|
const mergedMetadata = {
|
|
1246
1270
|
...(payload?.metadata || {}),
|
|
1247
1271
|
...(metadata || {}),
|
|
@@ -1255,11 +1279,37 @@ export function createRouter(store) {
|
|
|
1255
1279
|
(Object.hasOwn(payload || {}, "result") ? payload.result : payload);
|
|
1256
1280
|
const nextError = error ?? payload?.error ?? null;
|
|
1257
1281
|
|
|
1282
|
+
if (Number.isFinite(Number(attempt)) && Number(attempt) !== job.attempt) {
|
|
1283
|
+
const preserved = notifyAssignSupervisor(job, "late_result", {
|
|
1284
|
+
stale_attempt: Number(attempt),
|
|
1285
|
+
late_status: normalizedStatus,
|
|
1286
|
+
late_result: nextResult,
|
|
1287
|
+
late_error: nextError,
|
|
1288
|
+
reported_by: worker_agent || job.worker_agent,
|
|
1289
|
+
});
|
|
1290
|
+
return {
|
|
1291
|
+
ok: false,
|
|
1292
|
+
error: {
|
|
1293
|
+
code: "ASSIGN_ATTEMPT_MISMATCH",
|
|
1294
|
+
message: `stale assign result for attempt ${attempt} (current ${job.attempt})`,
|
|
1295
|
+
details: {
|
|
1296
|
+
preserved: Boolean(preserved),
|
|
1297
|
+
preserved_message_id: preserved?.id ?? null,
|
|
1298
|
+
},
|
|
1299
|
+
},
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1258
1303
|
if (normalizedStatus === "running") {
|
|
1304
|
+
const nowMs = Date.now();
|
|
1305
|
+
const ceilingAtMs =
|
|
1306
|
+
(job.dispatched_at_ms || job.created_at_ms) + resolveHardCeilingMs();
|
|
1259
1307
|
const running = store.updateAssignStatus(job.job_id, "running", {
|
|
1260
|
-
started_at_ms: job.started_at_ms ||
|
|
1261
|
-
deadline_ms:
|
|
1262
|
-
|
|
1308
|
+
started_at_ms: job.started_at_ms || nowMs,
|
|
1309
|
+
deadline_ms: Math.min(
|
|
1310
|
+
nowMs + clampDurationMs(job.timeout_ms, job.timeout_ms),
|
|
1311
|
+
ceilingAtMs,
|
|
1312
|
+
),
|
|
1263
1313
|
result: nextResult,
|
|
1264
1314
|
error: nextError,
|
|
1265
1315
|
});
|
|
@@ -1328,12 +1378,44 @@ export function createRouter(store) {
|
|
|
1328
1378
|
sweepExpired() {
|
|
1329
1379
|
const now = Date.now();
|
|
1330
1380
|
let expired = 0;
|
|
1381
|
+
const deadLettered = [];
|
|
1331
1382
|
for (const [messageId, record] of Array.from(liveMessages.entries())) {
|
|
1332
1383
|
if (record.message.expires_at_ms > now) continue;
|
|
1333
1384
|
store.moveToDeadLetter(messageId, "ttl_expired", null);
|
|
1385
|
+
deadLettered.push(record.message);
|
|
1334
1386
|
removeMessage(messageId);
|
|
1335
1387
|
expired += 1;
|
|
1336
1388
|
}
|
|
1389
|
+
for (const message of deadLettered) {
|
|
1390
|
+
if (
|
|
1391
|
+
!["handoff", "request"].includes(message.type) ||
|
|
1392
|
+
message.payload?.kind === "assign.job"
|
|
1393
|
+
)
|
|
1394
|
+
continue;
|
|
1395
|
+
if (
|
|
1396
|
+
!message.from_agent ||
|
|
1397
|
+
String(message.from_agent).startsWith("topic:")
|
|
1398
|
+
)
|
|
1399
|
+
continue;
|
|
1400
|
+
dispatchMessage({
|
|
1401
|
+
type: "event",
|
|
1402
|
+
from: "hub-sweeper",
|
|
1403
|
+
to: message.from_agent,
|
|
1404
|
+
topic: "handoff.dead_letter",
|
|
1405
|
+
priority: 7,
|
|
1406
|
+
ttl_ms: 300000,
|
|
1407
|
+
payload: {
|
|
1408
|
+
kind: "handoff.dead_letter",
|
|
1409
|
+
message_id: message.id,
|
|
1410
|
+
original_topic: message.topic,
|
|
1411
|
+
original_to: message.to_agent,
|
|
1412
|
+
reason: "ttl_expired",
|
|
1413
|
+
task: message.payload?.task ?? null,
|
|
1414
|
+
},
|
|
1415
|
+
trace_id: message.trace_id,
|
|
1416
|
+
correlation_id: message.correlation_id,
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1337
1419
|
return { messages: expired };
|
|
1338
1420
|
},
|
|
1339
1421
|
|
|
@@ -464,28 +464,41 @@ export function sendClaudeControlRequest(
|
|
|
464
464
|
|
|
465
465
|
// claude daemon 은 control.sock 의 mutating op(dispatch 등)에 control-key
|
|
466
466
|
// 인증을 강제한다 (미제시 시 EAUTH "didn't present the daemon control key").
|
|
467
|
-
//
|
|
468
|
-
//
|
|
467
|
+
// OMC runtime config 는 .omc-launch-profile.json 의 sourceConfigDir 에서 key 를
|
|
468
|
+
// 발급한다. control.sock 은 runtime config hash 를 쓰더라도 auth 는 발급 config
|
|
469
|
+
// 에서 읽어야 한다. 일반 daemon 은 <configDir>/daemon/control.key 를 사용한다.
|
|
470
|
+
// 어느 경로에도 파일이 없으면 인증 미강제 daemon 으로 보고 auth 를 생략한다
|
|
471
|
+
// (fail-open — 구버전 호환).
|
|
469
472
|
export async function readDaemonControlKey(
|
|
470
473
|
configDir = resolveClaudeConfigDir(),
|
|
471
474
|
{ diagnostics } = {},
|
|
472
475
|
) {
|
|
473
476
|
if (!configDir) return undefined;
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
477
|
+
|
|
478
|
+
const runtimeConfigDir = path.resolve(configDir);
|
|
479
|
+
const omcProfile = await readOmcLaunchProfile(runtimeConfigDir);
|
|
480
|
+
const keyConfigDirs = [omcProfile?.sourceConfigDir, runtimeConfigDir].filter(
|
|
481
|
+
(candidate, index, entries) =>
|
|
482
|
+
candidate && entries.indexOf(candidate) === index,
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
for (const keyConfigDir of keyConfigDirs) {
|
|
486
|
+
const keyPath = path.join(keyConfigDir, "daemon", "control.key");
|
|
487
|
+
try {
|
|
488
|
+
const key = await fs.readFile(keyPath, "utf8");
|
|
489
|
+
if (key.trim()) return key.trim();
|
|
490
|
+
} catch (error) {
|
|
491
|
+
if (error?.code === "ENOENT") continue;
|
|
492
|
+
if (Array.isArray(diagnostics)) {
|
|
493
|
+
diagnostics.push({
|
|
494
|
+
code: error?.code || "UNKNOWN",
|
|
495
|
+
path: keyPath,
|
|
496
|
+
message: error?.message || String(error),
|
|
497
|
+
});
|
|
498
|
+
}
|
|
486
499
|
}
|
|
487
|
-
return undefined;
|
|
488
500
|
}
|
|
501
|
+
return undefined;
|
|
489
502
|
}
|
|
490
503
|
|
|
491
504
|
export async function buildDaemonControlAuth(configDir, opts = {}) {
|