@triflux/core 10.41.1 → 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/keyword-rules.json +39 -5
- 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 +94 -0
- package/hub/lib/memory-store.mjs +34 -13
- package/hub/lib/timeout-defaults.mjs +45 -0
- package/hub/lib/worker-lifecycle.mjs +101 -0
- package/hub/log-retention.mjs +726 -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 +135 -30
- 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()
|
|
@@ -260,6 +256,26 @@ export function createRouter(store) {
|
|
|
260
256
|
);
|
|
261
257
|
}
|
|
262
258
|
|
|
259
|
+
function registerPresenceIsEffective(result) {
|
|
260
|
+
const effective = result?.effective;
|
|
261
|
+
return (
|
|
262
|
+
effective?.online === true &&
|
|
263
|
+
effective.status === "online" &&
|
|
264
|
+
Number(effective.lease_expires_ms) === Number(result?.lease_expires_ms)
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function registerPresenceFailure(agentId, result) {
|
|
269
|
+
return {
|
|
270
|
+
ok: false,
|
|
271
|
+
error: {
|
|
272
|
+
code: "REGISTER_PRESENCE_NOT_UPDATED",
|
|
273
|
+
message: `${agentId} register did not update effective presence`,
|
|
274
|
+
},
|
|
275
|
+
data: { ...result, hub_pid: process.pid },
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
263
279
|
function sortRoleCandidates(candidates = []) {
|
|
264
280
|
return [...candidates].sort((left, right) => {
|
|
265
281
|
const sourceDelta =
|
|
@@ -586,11 +602,31 @@ export function createRouter(store) {
|
|
|
586
602
|
record.message.status = "delivered";
|
|
587
603
|
store.updateMessageStatus(messageId, "delivered");
|
|
588
604
|
recordLatency(delivery.delivered_at_ms - record.message.created_at_ms);
|
|
605
|
+
rearmAssignDeadlineOnDispatch(record.message, delivery.delivered_at_ms);
|
|
589
606
|
return true;
|
|
590
607
|
}
|
|
591
608
|
return false;
|
|
592
609
|
}
|
|
593
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
|
+
|
|
594
630
|
function ackMessages(ids, agentId) {
|
|
595
631
|
const now = Date.now();
|
|
596
632
|
let count = 0;
|
|
@@ -686,7 +722,7 @@ export function createRouter(store) {
|
|
|
686
722
|
to: job.supervisor_agent,
|
|
687
723
|
topic: "assign.result",
|
|
688
724
|
priority: Math.max(5, job.priority || 5),
|
|
689
|
-
ttl_ms: job.ttl_ms || job.timeout_ms ||
|
|
725
|
+
ttl_ms: job.ttl_ms || job.timeout_ms || DEFAULT_ASSIGN_TTL_MS,
|
|
690
726
|
payload: {
|
|
691
727
|
event,
|
|
692
728
|
...buildAssignSnapshot(job),
|
|
@@ -705,7 +741,7 @@ export function createRouter(store) {
|
|
|
705
741
|
to: job.worker_agent,
|
|
706
742
|
topic: job.topic || "assign.job",
|
|
707
743
|
priority: job.priority || 5,
|
|
708
|
-
ttl_ms: job.ttl_ms || job.timeout_ms ||
|
|
744
|
+
ttl_ms: job.ttl_ms || job.timeout_ms || DEFAULT_ASSIGN_TTL_MS,
|
|
709
745
|
payload: {
|
|
710
746
|
kind: "assign.job",
|
|
711
747
|
reason,
|
|
@@ -777,6 +813,24 @@ export function createRouter(store) {
|
|
|
777
813
|
const timedOut = store.updateAssignStatus(job.job_id, "timed_out", {
|
|
778
814
|
error: job.error ?? { message: "assign job timed out" },
|
|
779
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
|
+
}
|
|
780
834
|
|
|
781
835
|
if (timedOut.retry_count < timedOut.max_retries) {
|
|
782
836
|
return scheduleAssignRetry(
|
|
@@ -802,6 +856,9 @@ export function createRouter(store) {
|
|
|
802
856
|
|
|
803
857
|
registerAgent(args) {
|
|
804
858
|
const result = store.registerAgent(args);
|
|
859
|
+
if (!registerPresenceIsEffective(result)) {
|
|
860
|
+
return registerPresenceFailure(args.agent_id, result);
|
|
861
|
+
}
|
|
805
862
|
upsertRuntimeTopics(args.agent_id, args.topics || [], { replace: true });
|
|
806
863
|
refreshRoleCandidateForAgent(args.agent_id);
|
|
807
864
|
for (const roleName of ROLE_TOPICS) {
|
|
@@ -810,7 +867,7 @@ export function createRouter(store) {
|
|
|
810
867
|
transferBacklog: true,
|
|
811
868
|
});
|
|
812
869
|
}
|
|
813
|
-
return result;
|
|
870
|
+
return { ok: true, data: { ...result, hub_pid: process.pid } };
|
|
814
871
|
},
|
|
815
872
|
|
|
816
873
|
refreshAgentLease(agentId, ttlMs = 30000) {
|
|
@@ -1123,7 +1180,7 @@ export function createRouter(store) {
|
|
|
1123
1180
|
acceptance_criteria,
|
|
1124
1181
|
context_refs,
|
|
1125
1182
|
priority = 5,
|
|
1126
|
-
ttl_ms =
|
|
1183
|
+
ttl_ms = DEFAULT_HANDOFF_TTL_MS,
|
|
1127
1184
|
trace_id,
|
|
1128
1185
|
correlation_id,
|
|
1129
1186
|
}) {
|
|
@@ -1151,8 +1208,8 @@ export function createRouter(store) {
|
|
|
1151
1208
|
task = "",
|
|
1152
1209
|
payload = {},
|
|
1153
1210
|
priority = 5,
|
|
1154
|
-
ttl_ms =
|
|
1155
|
-
timeout_ms =
|
|
1211
|
+
ttl_ms = DEFAULT_ASSIGN_TTL_MS,
|
|
1212
|
+
timeout_ms = DEFAULT_ASSIGN_TIMEOUT_MS,
|
|
1156
1213
|
max_retries = 0,
|
|
1157
1214
|
trace_id,
|
|
1158
1215
|
correlation_id,
|
|
@@ -1209,16 +1266,6 @@ export function createRouter(store) {
|
|
|
1209
1266
|
},
|
|
1210
1267
|
};
|
|
1211
1268
|
}
|
|
1212
|
-
if (Number.isFinite(Number(attempt)) && Number(attempt) !== job.attempt) {
|
|
1213
|
-
return {
|
|
1214
|
-
ok: false,
|
|
1215
|
-
error: {
|
|
1216
|
-
code: "ASSIGN_ATTEMPT_MISMATCH",
|
|
1217
|
-
message: `stale assign result for attempt ${attempt} (current ${job.attempt})`,
|
|
1218
|
-
},
|
|
1219
|
-
};
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
1269
|
const mergedMetadata = {
|
|
1223
1270
|
...(payload?.metadata || {}),
|
|
1224
1271
|
...(metadata || {}),
|
|
@@ -1232,11 +1279,37 @@ export function createRouter(store) {
|
|
|
1232
1279
|
(Object.hasOwn(payload || {}, "result") ? payload.result : payload);
|
|
1233
1280
|
const nextError = error ?? payload?.error ?? null;
|
|
1234
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
|
+
|
|
1235
1303
|
if (normalizedStatus === "running") {
|
|
1304
|
+
const nowMs = Date.now();
|
|
1305
|
+
const ceilingAtMs =
|
|
1306
|
+
(job.dispatched_at_ms || job.created_at_ms) + resolveHardCeilingMs();
|
|
1236
1307
|
const running = store.updateAssignStatus(job.job_id, "running", {
|
|
1237
|
-
started_at_ms: job.started_at_ms ||
|
|
1238
|
-
deadline_ms:
|
|
1239
|
-
|
|
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
|
+
),
|
|
1240
1313
|
result: nextResult,
|
|
1241
1314
|
error: nextError,
|
|
1242
1315
|
});
|
|
@@ -1305,12 +1378,44 @@ export function createRouter(store) {
|
|
|
1305
1378
|
sweepExpired() {
|
|
1306
1379
|
const now = Date.now();
|
|
1307
1380
|
let expired = 0;
|
|
1381
|
+
const deadLettered = [];
|
|
1308
1382
|
for (const [messageId, record] of Array.from(liveMessages.entries())) {
|
|
1309
1383
|
if (record.message.expires_at_ms > now) continue;
|
|
1310
1384
|
store.moveToDeadLetter(messageId, "ttl_expired", null);
|
|
1385
|
+
deadLettered.push(record.message);
|
|
1311
1386
|
removeMessage(messageId);
|
|
1312
1387
|
expired += 1;
|
|
1313
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
|
+
}
|
|
1314
1419
|
return { messages: expired };
|
|
1315
1420
|
},
|
|
1316
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 = {}) {
|