@triflux/core 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/hooks/codex-session-hook.mjs +67 -1
- package/hooks/cto-north-star-brief.mjs +3 -16
- package/hooks/error-context.mjs +3 -6
- package/hooks/hook-manager.mjs +1 -1
- package/hooks/hook-registry.json +0 -11
- package/hooks/keyword-rules.json +147 -23
- package/hooks/permission-safe-allow.mjs +36 -2
- package/hooks/session-start-fast.mjs +50 -1
- package/hooks/session-start-lake.mjs +3 -16
- package/hub/bridge.mjs +111 -5
- package/hub/cli-adapter-base.mjs +11 -2
- package/hub/codex-adapter.mjs +3 -0
- package/hub/gemini-adapter.mjs +29 -11
- package/hub/intent.mjs +24 -32
- package/hub/lib/memory-store.mjs +66 -30
- package/hub/lib/tfx-route-args.mjs +41 -4
- package/hub/reflexion.mjs +0 -57
- package/hub/router.mjs +474 -24
- package/hub/team/claude-daemon-control.mjs +2 -0
- package/hub/team/intervention.mjs +1 -1
- package/hub/team/retry-state-machine.mjs +41 -13
- package/hub/team/synapse-http.mjs +149 -4
- package/hud/cli-policy.mjs +69 -0
- package/hud/constants.mjs +20 -17
- package/hud/context-monitor.mjs +31 -21
- package/hud/hud-qos-status.mjs +81 -91
- package/hud/providers/claude.mjs +150 -84
- package/hud/providers/codex.mjs +10 -1
- package/hud/renderers.mjs +21 -59
- package/hud/utils.mjs +1 -15
- 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/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/hub/log-retention.mjs +0 -726
package/hub/router.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
resolveHardCeilingMs,
|
|
10
10
|
} from "./lib/timeout-defaults.mjs";
|
|
11
11
|
import { uuidv7 } from "./lib/uuidv7.mjs";
|
|
12
|
+
import { normalizeRoleKey } from "./role-contract.mjs";
|
|
12
13
|
|
|
13
14
|
const ASSIGN_PENDING_STATUSES = new Set(["queued", "running"]);
|
|
14
15
|
const ROLE_TOPICS = new Set(["cto"]);
|
|
@@ -16,6 +17,258 @@ const ROLE_SOURCE_RANK = Object.freeze({
|
|
|
16
17
|
explicit: 2,
|
|
17
18
|
"topic-fallback": 1,
|
|
18
19
|
});
|
|
20
|
+
const TRANSPORT_LOCATOR_SCHEMA_VERSION = "tfx.transport-locator.v1";
|
|
21
|
+
const TRANSPORT_KINDS = new Set(["claude-uds", "tmux"]);
|
|
22
|
+
const TRANSPORT_CAPABILITIES = new Set([
|
|
23
|
+
"probe",
|
|
24
|
+
"wake",
|
|
25
|
+
"activate",
|
|
26
|
+
"interrupt",
|
|
27
|
+
]);
|
|
28
|
+
const HOST_ID_RE = /^hst_[A-Za-z0-9_-]+$/u;
|
|
29
|
+
|
|
30
|
+
function registrationError(code, message = code) {
|
|
31
|
+
const error = new Error(message);
|
|
32
|
+
error.code = code;
|
|
33
|
+
return error;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function registrationString(value, code, { maxLength = 256 } = {}) {
|
|
37
|
+
if (typeof value !== "string" || !value.trim() || value.length > maxLength) {
|
|
38
|
+
throw registrationError(code);
|
|
39
|
+
}
|
|
40
|
+
return value.trim();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function registrationLogicalRef(value, code, { maxLength = 128 } = {}) {
|
|
44
|
+
const normalized = registrationString(value, code, { maxLength });
|
|
45
|
+
if (!/^[A-Za-z0-9._:-]+$/u.test(normalized)) {
|
|
46
|
+
throw registrationError(code);
|
|
47
|
+
}
|
|
48
|
+
return normalized;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function assertExactFields(value, allowed, code) {
|
|
52
|
+
if (
|
|
53
|
+
value === null ||
|
|
54
|
+
typeof value !== "object" ||
|
|
55
|
+
Array.isArray(value) ||
|
|
56
|
+
Object.keys(value).some((field) => !allowed.has(field))
|
|
57
|
+
) {
|
|
58
|
+
throw registrationError(code);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeTransportLocator(
|
|
63
|
+
value,
|
|
64
|
+
{ cli, hostId, now, leaseExpiresMs },
|
|
65
|
+
) {
|
|
66
|
+
assertExactFields(
|
|
67
|
+
value,
|
|
68
|
+
new Set([
|
|
69
|
+
"schema_version",
|
|
70
|
+
"transport_id",
|
|
71
|
+
"kind",
|
|
72
|
+
"host_id",
|
|
73
|
+
"capabilities",
|
|
74
|
+
"priority",
|
|
75
|
+
"expires_at_ms",
|
|
76
|
+
"locator",
|
|
77
|
+
]),
|
|
78
|
+
"TRANSPORT_LOCATOR_INVALID",
|
|
79
|
+
);
|
|
80
|
+
if (value.schema_version !== TRANSPORT_LOCATOR_SCHEMA_VERSION) {
|
|
81
|
+
throw registrationError("TRANSPORT_LOCATOR_SCHEMA_UNSUPPORTED");
|
|
82
|
+
}
|
|
83
|
+
const transportId = registrationLogicalRef(
|
|
84
|
+
value.transport_id,
|
|
85
|
+
"TRANSPORT_ID_INVALID",
|
|
86
|
+
{ maxLength: 128 },
|
|
87
|
+
);
|
|
88
|
+
if (!TRANSPORT_KINDS.has(value.kind)) {
|
|
89
|
+
throw registrationError("TRANSPORT_KIND_UNSUPPORTED");
|
|
90
|
+
}
|
|
91
|
+
if (cli === "codex" && value.kind !== "tmux") {
|
|
92
|
+
throw registrationError("CODEX_TRANSPORT_UNSUPPORTED");
|
|
93
|
+
}
|
|
94
|
+
const locatorHostId = registrationString(value.host_id, "HOST_ID_INVALID", {
|
|
95
|
+
maxLength: 128,
|
|
96
|
+
});
|
|
97
|
+
if (!HOST_ID_RE.test(locatorHostId) || (hostId && locatorHostId !== hostId)) {
|
|
98
|
+
throw registrationError("HOST_ID_INVALID");
|
|
99
|
+
}
|
|
100
|
+
if (!Array.isArray(value.capabilities)) {
|
|
101
|
+
throw registrationError("TRANSPORT_CAPABILITIES_INVALID");
|
|
102
|
+
}
|
|
103
|
+
const capabilities = uniqueStrings(value.capabilities);
|
|
104
|
+
if (
|
|
105
|
+
capabilities.length !== value.capabilities.length ||
|
|
106
|
+
capabilities.some((capability) => !TRANSPORT_CAPABILITIES.has(capability))
|
|
107
|
+
) {
|
|
108
|
+
throw registrationError("TRANSPORT_CAPABILITIES_INVALID");
|
|
109
|
+
}
|
|
110
|
+
if (!Number.isSafeInteger(value.priority)) {
|
|
111
|
+
throw registrationError("TRANSPORT_PRIORITY_INVALID");
|
|
112
|
+
}
|
|
113
|
+
if (
|
|
114
|
+
!Number.isSafeInteger(value.expires_at_ms) ||
|
|
115
|
+
value.expires_at_ms <= now ||
|
|
116
|
+
value.expires_at_ms > leaseExpiresMs
|
|
117
|
+
) {
|
|
118
|
+
throw registrationError("TRANSPORT_EXPIRY_INVALID");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let locator;
|
|
122
|
+
if (value.kind === "claude-uds") {
|
|
123
|
+
assertExactFields(
|
|
124
|
+
value.locator,
|
|
125
|
+
new Set(["session_id", "short", "bridge_id"]),
|
|
126
|
+
"TRANSPORT_LOCATOR_INVALID",
|
|
127
|
+
);
|
|
128
|
+
const sessionId = value.locator.session_id;
|
|
129
|
+
const short = value.locator.short;
|
|
130
|
+
if (!sessionId && !short) {
|
|
131
|
+
throw registrationError("CLAUDE_UDS_SESSION_REQUIRED");
|
|
132
|
+
}
|
|
133
|
+
locator = {
|
|
134
|
+
...(sessionId
|
|
135
|
+
? {
|
|
136
|
+
session_id: registrationLogicalRef(
|
|
137
|
+
sessionId,
|
|
138
|
+
"CLAUDE_UDS_SESSION_INVALID",
|
|
139
|
+
),
|
|
140
|
+
}
|
|
141
|
+
: {}),
|
|
142
|
+
...(short
|
|
143
|
+
? {
|
|
144
|
+
short: registrationLogicalRef(short, "CLAUDE_UDS_SHORT_INVALID", {
|
|
145
|
+
maxLength: 128,
|
|
146
|
+
}),
|
|
147
|
+
}
|
|
148
|
+
: {}),
|
|
149
|
+
bridge_id: registrationLogicalRef(
|
|
150
|
+
value.locator.bridge_id,
|
|
151
|
+
"CLAUDE_UDS_BRIDGE_INVALID",
|
|
152
|
+
{ maxLength: 128 },
|
|
153
|
+
),
|
|
154
|
+
};
|
|
155
|
+
} else {
|
|
156
|
+
assertExactFields(
|
|
157
|
+
value.locator,
|
|
158
|
+
new Set(["session_name", "mux_server_id"]),
|
|
159
|
+
"TRANSPORT_LOCATOR_INVALID",
|
|
160
|
+
);
|
|
161
|
+
locator = {
|
|
162
|
+
session_name: registrationLogicalRef(
|
|
163
|
+
value.locator.session_name,
|
|
164
|
+
"TMUX_SESSION_INVALID",
|
|
165
|
+
{ maxLength: 128 },
|
|
166
|
+
),
|
|
167
|
+
mux_server_id: registrationLogicalRef(
|
|
168
|
+
value.locator.mux_server_id,
|
|
169
|
+
"TMUX_SERVER_INVALID",
|
|
170
|
+
{ maxLength: 128 },
|
|
171
|
+
),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
schema_version: TRANSPORT_LOCATOR_SCHEMA_VERSION,
|
|
177
|
+
transport_id: transportId,
|
|
178
|
+
kind: value.kind,
|
|
179
|
+
host_id: locatorHostId,
|
|
180
|
+
capabilities,
|
|
181
|
+
priority: value.priority,
|
|
182
|
+
expires_at_ms: value.expires_at_ms,
|
|
183
|
+
locator,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function normalizeRegistrationIdentity(args = {}, now = Date.now()) {
|
|
188
|
+
const hasProjectId = Object.hasOwn(args, "project_id");
|
|
189
|
+
const hasSessionId = Object.hasOwn(args, "session_id");
|
|
190
|
+
const hasHostId = Object.hasOwn(args, "host_id");
|
|
191
|
+
const hasLocatorJson = Object.hasOwn(args, "transport_locators_json");
|
|
192
|
+
const hasLocators = Object.hasOwn(args, "transport_locators");
|
|
193
|
+
if (
|
|
194
|
+
!hasProjectId &&
|
|
195
|
+
!hasSessionId &&
|
|
196
|
+
!hasHostId &&
|
|
197
|
+
!hasLocatorJson &&
|
|
198
|
+
!hasLocators
|
|
199
|
+
) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const projectId = hasProjectId
|
|
204
|
+
? normalizeRoleKey({ project_id: args.project_id, role_kind: "cto" })
|
|
205
|
+
.project_id
|
|
206
|
+
: null;
|
|
207
|
+
const sessionId = hasSessionId
|
|
208
|
+
? registrationString(args.session_id, "SESSION_ID_INVALID")
|
|
209
|
+
: null;
|
|
210
|
+
const hostId = hasHostId
|
|
211
|
+
? registrationString(args.host_id, "HOST_ID_INVALID", { maxLength: 128 })
|
|
212
|
+
: null;
|
|
213
|
+
if (hostId && !HOST_ID_RE.test(hostId)) {
|
|
214
|
+
throw registrationError("HOST_ID_INVALID");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let rawLocators = [];
|
|
218
|
+
if (hasLocatorJson) {
|
|
219
|
+
if (Array.isArray(args.transport_locators_json)) {
|
|
220
|
+
rawLocators = args.transport_locators_json;
|
|
221
|
+
} else if (typeof args.transport_locators_json === "string") {
|
|
222
|
+
try {
|
|
223
|
+
rawLocators = JSON.parse(args.transport_locators_json);
|
|
224
|
+
} catch {
|
|
225
|
+
throw registrationError("TRANSPORT_LOCATORS_JSON_INVALID");
|
|
226
|
+
}
|
|
227
|
+
} else {
|
|
228
|
+
throw registrationError("TRANSPORT_LOCATORS_JSON_INVALID");
|
|
229
|
+
}
|
|
230
|
+
} else if (hasLocators) {
|
|
231
|
+
rawLocators = args.transport_locators;
|
|
232
|
+
}
|
|
233
|
+
if (!Array.isArray(rawLocators)) {
|
|
234
|
+
throw registrationError("TRANSPORT_LOCATORS_INVALID");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const ttlMs = Number(args.heartbeat_ttl_ms ?? 30000);
|
|
238
|
+
if (rawLocators.length > 0 && (!Number.isFinite(ttlMs) || ttlMs <= 0)) {
|
|
239
|
+
throw registrationError("HEARTBEAT_TTL_INVALID");
|
|
240
|
+
}
|
|
241
|
+
const leaseExpiresMs = now + ttlMs;
|
|
242
|
+
const normalizedLocators = rawLocators
|
|
243
|
+
.map((locator) =>
|
|
244
|
+
normalizeTransportLocator(locator, {
|
|
245
|
+
cli: args.cli,
|
|
246
|
+
hostId,
|
|
247
|
+
now,
|
|
248
|
+
leaseExpiresMs,
|
|
249
|
+
}),
|
|
250
|
+
)
|
|
251
|
+
.sort(
|
|
252
|
+
(left, right) =>
|
|
253
|
+
right.priority - left.priority ||
|
|
254
|
+
left.transport_id.localeCompare(right.transport_id),
|
|
255
|
+
);
|
|
256
|
+
const resolvedHostId = hostId || normalizedLocators[0]?.host_id || null;
|
|
257
|
+
if (
|
|
258
|
+
resolvedHostId &&
|
|
259
|
+
normalizedLocators.some((locator) => locator.host_id !== resolvedHostId)
|
|
260
|
+
) {
|
|
261
|
+
throw registrationError("HOST_ID_INVALID");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
project_id: projectId,
|
|
266
|
+
session_id: sessionId,
|
|
267
|
+
host_id: resolvedHostId,
|
|
268
|
+
transport_locators_json:
|
|
269
|
+
hasLocatorJson || hasLocators ? JSON.stringify(normalizedLocators) : null,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
19
272
|
|
|
20
273
|
function uniqueStrings(values = []) {
|
|
21
274
|
return Array.from(
|
|
@@ -123,10 +376,89 @@ export function createRouter(store) {
|
|
|
123
376
|
const queuesByAgent = new Map();
|
|
124
377
|
const liveMessages = new Map();
|
|
125
378
|
const roleStates = new Map();
|
|
379
|
+
const hubInstanceId = uuidv7();
|
|
380
|
+
const storeFingerprint =
|
|
381
|
+
store.fingerprint || `${store.type || "unknown"}:unknown`;
|
|
382
|
+
const updateRegistrationIdentity = store.db?.prepare
|
|
383
|
+
? store.db.prepare(`
|
|
384
|
+
UPDATE agents SET
|
|
385
|
+
project_id=COALESCE(@project_id, project_id),
|
|
386
|
+
session_id=COALESCE(@session_id, session_id),
|
|
387
|
+
host_id=COALESCE(@host_id, host_id),
|
|
388
|
+
transport_locators_json=COALESCE(
|
|
389
|
+
@transport_locators_json,
|
|
390
|
+
transport_locators_json
|
|
391
|
+
)
|
|
392
|
+
WHERE agent_id=@agent_id`)
|
|
393
|
+
: null;
|
|
126
394
|
const MAX_LATENCY_SAMPLES = 100;
|
|
127
395
|
let latencyIdx = 0;
|
|
128
396
|
const deliveryLatencies = new Array(MAX_LATENCY_SAMPLES).fill(0);
|
|
129
397
|
|
|
398
|
+
function hubIdentity() {
|
|
399
|
+
return {
|
|
400
|
+
hub_instance_id: hubInstanceId,
|
|
401
|
+
store_fingerprint: storeFingerprint,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function validateCallerIdentity(input = {}) {
|
|
406
|
+
if (
|
|
407
|
+
input.hub_instance_id != null &&
|
|
408
|
+
input.hub_instance_id !== hubInstanceId
|
|
409
|
+
) {
|
|
410
|
+
return {
|
|
411
|
+
ok: false,
|
|
412
|
+
error: {
|
|
413
|
+
code: "STALE_HUB_INSTANCE",
|
|
414
|
+
message: "caller is bound to a previous hub instance",
|
|
415
|
+
},
|
|
416
|
+
data: hubIdentity(),
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
if (
|
|
420
|
+
input.store_fingerprint != null &&
|
|
421
|
+
input.store_fingerprint !== storeFingerprint
|
|
422
|
+
) {
|
|
423
|
+
return {
|
|
424
|
+
ok: false,
|
|
425
|
+
error: {
|
|
426
|
+
code: "STORE_FINGERPRINT_MISMATCH",
|
|
427
|
+
message: "caller store fingerprint does not match this hub",
|
|
428
|
+
},
|
|
429
|
+
data: hubIdentity(),
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function isControlPlaneRegistration(args = {}) {
|
|
436
|
+
const metadata = args.metadata || {};
|
|
437
|
+
const values = [
|
|
438
|
+
...(Array.isArray(args.capabilities) ? args.capabilities : []),
|
|
439
|
+
...(Array.isArray(args.topics) ? args.topics : []),
|
|
440
|
+
metadata.role,
|
|
441
|
+
...(Array.isArray(metadata.roles) ? metadata.roles : []),
|
|
442
|
+
]
|
|
443
|
+
.flat()
|
|
444
|
+
.map((value) => String(value || "").toLowerCase());
|
|
445
|
+
return (
|
|
446
|
+
metadata.control_plane === true ||
|
|
447
|
+
values.some((value) => value === "cto" || value === "role:cto")
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function staleOwnerFailure(agentId, result) {
|
|
452
|
+
return {
|
|
453
|
+
ok: false,
|
|
454
|
+
error: {
|
|
455
|
+
code: "STALE_PRESENCE_OWNER",
|
|
456
|
+
message: `stale presence owner: ${agentId}`,
|
|
457
|
+
},
|
|
458
|
+
data: { ...result, ...hubIdentity() },
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
130
462
|
function ensureAgentQueue(agentId) {
|
|
131
463
|
let queue = queuesByAgent.get(agentId);
|
|
132
464
|
if (!queue) {
|
|
@@ -855,10 +1187,29 @@ export function createRouter(store) {
|
|
|
855
1187
|
deliveryEmitter,
|
|
856
1188
|
|
|
857
1189
|
registerAgent(args) {
|
|
1190
|
+
const identityFailure = validateCallerIdentity(args);
|
|
1191
|
+
if (identityFailure) return identityFailure;
|
|
1192
|
+
if (isControlPlaneRegistration(args) && store.type !== "sqlite") {
|
|
1193
|
+
return {
|
|
1194
|
+
ok: false,
|
|
1195
|
+
error: {
|
|
1196
|
+
code: "CONTROL_PLANE_STORE_DEGRADED",
|
|
1197
|
+
message: "control-plane presence requires durable SQLite storage",
|
|
1198
|
+
},
|
|
1199
|
+
data: { ...hubIdentity(), store_type: store.type || "unknown" },
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
const identity = normalizeRegistrationIdentity(args);
|
|
858
1203
|
const result = store.registerAgent(args);
|
|
859
1204
|
if (!registerPresenceIsEffective(result)) {
|
|
860
1205
|
return registerPresenceFailure(args.agent_id, result);
|
|
861
1206
|
}
|
|
1207
|
+
if (identity && updateRegistrationIdentity) {
|
|
1208
|
+
updateRegistrationIdentity.run({
|
|
1209
|
+
agent_id: args.agent_id,
|
|
1210
|
+
...identity,
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
862
1213
|
upsertRuntimeTopics(args.agent_id, args.topics || [], { replace: true });
|
|
863
1214
|
refreshRoleCandidateForAgent(args.agent_id);
|
|
864
1215
|
for (const roleName of ROLE_TOPICS) {
|
|
@@ -867,11 +1218,23 @@ export function createRouter(store) {
|
|
|
867
1218
|
transferBacklog: true,
|
|
868
1219
|
});
|
|
869
1220
|
}
|
|
870
|
-
return {
|
|
1221
|
+
return {
|
|
1222
|
+
ok: true,
|
|
1223
|
+
data: { ...result, hub_pid: process.pid, ...hubIdentity() },
|
|
1224
|
+
};
|
|
871
1225
|
},
|
|
872
1226
|
|
|
873
|
-
refreshAgentLease(
|
|
874
|
-
|
|
1227
|
+
refreshAgentLease(
|
|
1228
|
+
agentId,
|
|
1229
|
+
ttlMs = 30000,
|
|
1230
|
+
presenceGeneration,
|
|
1231
|
+
callerIdentity = {},
|
|
1232
|
+
) {
|
|
1233
|
+
const identityFailure = validateCallerIdentity(callerIdentity);
|
|
1234
|
+
if (identityFailure) return identityFailure;
|
|
1235
|
+
const result = store.refreshLease(agentId, ttlMs, presenceGeneration);
|
|
1236
|
+
if (result?.effective?.updated === false)
|
|
1237
|
+
return staleOwnerFailure(agentId, result);
|
|
875
1238
|
refreshRoleCandidateForAgent(agentId);
|
|
876
1239
|
for (const roleName of ROLE_TOPICS) {
|
|
877
1240
|
ensureRoleLeader(roleName, {
|
|
@@ -879,7 +1242,7 @@ export function createRouter(store) {
|
|
|
879
1242
|
transferBacklog: true,
|
|
880
1243
|
});
|
|
881
1244
|
}
|
|
882
|
-
return result;
|
|
1245
|
+
return { ok: true, data: result };
|
|
883
1246
|
},
|
|
884
1247
|
|
|
885
1248
|
subscribeAgent(agentId, topics, { replace = false } = {}) {
|
|
@@ -898,11 +1261,15 @@ export function createRouter(store) {
|
|
|
898
1261
|
return listRuntimeTopics(agentId);
|
|
899
1262
|
},
|
|
900
1263
|
|
|
901
|
-
updateAgentStatus(agentId, status) {
|
|
1264
|
+
updateAgentStatus(agentId, status, presenceGeneration) {
|
|
902
1265
|
if (status === "offline") {
|
|
903
1266
|
runtimeTopics.delete(agentId);
|
|
904
1267
|
}
|
|
905
|
-
const updated = store.updateAgentStatus(
|
|
1268
|
+
const updated = store.updateAgentStatus(
|
|
1269
|
+
agentId,
|
|
1270
|
+
status,
|
|
1271
|
+
presenceGeneration,
|
|
1272
|
+
);
|
|
906
1273
|
refreshRoleCandidateForAgent(agentId);
|
|
907
1274
|
for (const roleName of ROLE_TOPICS) {
|
|
908
1275
|
ensureRoleLeader(roleName, {
|
|
@@ -913,6 +1280,39 @@ export function createRouter(store) {
|
|
|
913
1280
|
return updated;
|
|
914
1281
|
},
|
|
915
1282
|
|
|
1283
|
+
transitionAgentStatus(
|
|
1284
|
+
agentId,
|
|
1285
|
+
status,
|
|
1286
|
+
presenceGeneration,
|
|
1287
|
+
callerIdentity = {},
|
|
1288
|
+
) {
|
|
1289
|
+
const identityFailure = validateCallerIdentity(callerIdentity);
|
|
1290
|
+
if (identityFailure) return identityFailure;
|
|
1291
|
+
const updated = store.updateAgentStatus(
|
|
1292
|
+
agentId,
|
|
1293
|
+
status,
|
|
1294
|
+
presenceGeneration,
|
|
1295
|
+
);
|
|
1296
|
+
if (!updated) return staleOwnerFailure(agentId, { status });
|
|
1297
|
+
if (status === "offline") runtimeTopics.delete(agentId);
|
|
1298
|
+
refreshRoleCandidateForAgent(agentId);
|
|
1299
|
+
for (const roleName of ROLE_TOPICS) {
|
|
1300
|
+
ensureRoleLeader(roleName, {
|
|
1301
|
+
reason: `status:${status}`,
|
|
1302
|
+
transferBacklog: true,
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
return {
|
|
1306
|
+
ok: true,
|
|
1307
|
+
data: {
|
|
1308
|
+
agent_id: agentId,
|
|
1309
|
+
status,
|
|
1310
|
+
presence_generation: presenceGeneration,
|
|
1311
|
+
...hubIdentity(),
|
|
1312
|
+
},
|
|
1313
|
+
};
|
|
1314
|
+
},
|
|
1315
|
+
|
|
916
1316
|
route(msg) {
|
|
917
1317
|
const recipients = uniqueStrings(resolveRecipients(msg));
|
|
918
1318
|
if (!recipients.length) {
|
|
@@ -1097,7 +1497,14 @@ export function createRouter(store) {
|
|
|
1097
1497
|
agent_id,
|
|
1098
1498
|
reason = "manual",
|
|
1099
1499
|
requested_by = "manual",
|
|
1500
|
+
hub_instance_id,
|
|
1501
|
+
store_fingerprint,
|
|
1100
1502
|
} = {}) {
|
|
1503
|
+
const identityFailure = validateCallerIdentity({
|
|
1504
|
+
hub_instance_id,
|
|
1505
|
+
store_fingerprint,
|
|
1506
|
+
});
|
|
1507
|
+
if (identityFailure) return identityFailure;
|
|
1101
1508
|
const roleName = normalizeRoleName(role);
|
|
1102
1509
|
if (!roleName) {
|
|
1103
1510
|
return {
|
|
@@ -1168,6 +1575,7 @@ export function createRouter(store) {
|
|
|
1168
1575
|
leader_epoch: snapshot.leader_epoch,
|
|
1169
1576
|
...transfer,
|
|
1170
1577
|
status: snapshot,
|
|
1578
|
+
...hubIdentity(),
|
|
1171
1579
|
},
|
|
1172
1580
|
};
|
|
1173
1581
|
},
|
|
@@ -1375,18 +1783,15 @@ export function createRouter(store) {
|
|
|
1375
1783
|
return scheduleAssignRetry(job, reason, job.error, requested_by);
|
|
1376
1784
|
},
|
|
1377
1785
|
|
|
1378
|
-
sweepExpired() {
|
|
1379
|
-
const
|
|
1380
|
-
let expired = 0;
|
|
1381
|
-
const deadLettered = [];
|
|
1786
|
+
sweepExpired({ now_ms = Date.now(), source = "manual" } = {}) {
|
|
1787
|
+
const result = store.sweepExpired({ now_ms });
|
|
1382
1788
|
for (const [messageId, record] of Array.from(liveMessages.entries())) {
|
|
1383
|
-
if (record.message.expires_at_ms >
|
|
1384
|
-
store.moveToDeadLetter(messageId, "ttl_expired", null);
|
|
1385
|
-
deadLettered.push(record.message);
|
|
1789
|
+
if (record.message.expires_at_ms > now_ms) continue;
|
|
1386
1790
|
removeMessage(messageId);
|
|
1387
|
-
expired += 1;
|
|
1388
1791
|
}
|
|
1389
|
-
for (const
|
|
1792
|
+
for (const transition of result.transitions) {
|
|
1793
|
+
if (!transition.dlq_inserted) continue;
|
|
1794
|
+
const { message } = transition;
|
|
1390
1795
|
if (
|
|
1391
1796
|
!["handoff", "request"].includes(message.type) ||
|
|
1392
1797
|
message.payload?.kind === "assign.job"
|
|
@@ -1411,12 +1816,15 @@ export function createRouter(store) {
|
|
|
1411
1816
|
original_to: message.to_agent,
|
|
1412
1817
|
reason: "ttl_expired",
|
|
1413
1818
|
task: message.payload?.task ?? null,
|
|
1819
|
+
expired_at_ms: message.expires_at_ms,
|
|
1820
|
+
failed_at_ms: transition.failed_at_ms,
|
|
1821
|
+
recovery_source: source,
|
|
1414
1822
|
},
|
|
1415
1823
|
trace_id: message.trace_id,
|
|
1416
1824
|
correlation_id: message.correlation_id,
|
|
1417
1825
|
});
|
|
1418
1826
|
}
|
|
1419
|
-
return
|
|
1827
|
+
return result;
|
|
1420
1828
|
},
|
|
1421
1829
|
|
|
1422
1830
|
sweepTimedOutAssigns() {
|
|
@@ -1437,19 +1845,49 @@ export function createRouter(store) {
|
|
|
1437
1845
|
return { timed_out, retried };
|
|
1438
1846
|
},
|
|
1439
1847
|
|
|
1440
|
-
startSweeper(
|
|
1848
|
+
startSweeper({
|
|
1849
|
+
run_immediately = false,
|
|
1850
|
+
interval_ms = 10000,
|
|
1851
|
+
onSweepError,
|
|
1852
|
+
} = {}) {
|
|
1441
1853
|
if (sweepTimer) return;
|
|
1442
|
-
|
|
1854
|
+
const reportSweepError = (source, error, failures) => {
|
|
1855
|
+
if (typeof onSweepError === "function") {
|
|
1856
|
+
onSweepError({ source, error, consecutive_failures: failures });
|
|
1857
|
+
}
|
|
1858
|
+
};
|
|
1859
|
+
let sweepFailures = 0;
|
|
1860
|
+
let assignFailures = 0;
|
|
1861
|
+
const runSweep = () => {
|
|
1862
|
+
try {
|
|
1863
|
+
router.sweepExpired({ source: "periodic" });
|
|
1864
|
+
sweepFailures = 0;
|
|
1865
|
+
} catch (error) {
|
|
1866
|
+
sweepFailures += 1;
|
|
1867
|
+
reportSweepError("periodic", error, sweepFailures);
|
|
1868
|
+
}
|
|
1443
1869
|
try {
|
|
1444
|
-
router.sweepExpired();
|
|
1445
1870
|
router.sweepTimedOutAssigns();
|
|
1446
|
-
|
|
1447
|
-
|
|
1871
|
+
assignFailures = 0;
|
|
1872
|
+
} catch (error) {
|
|
1873
|
+
assignFailures += 1;
|
|
1874
|
+
reportSweepError("assign_timeout", error, assignFailures);
|
|
1875
|
+
}
|
|
1876
|
+
};
|
|
1877
|
+
if (run_immediately) runSweep();
|
|
1878
|
+
sweepTimer = setInterval(() => {
|
|
1879
|
+
runSweep();
|
|
1880
|
+
}, interval_ms);
|
|
1881
|
+
let staleFailures = 0;
|
|
1448
1882
|
staleTimer = setInterval(() => {
|
|
1449
1883
|
try {
|
|
1450
1884
|
store.sweepStaleAgents();
|
|
1451
1885
|
router.reelectStaleRoles();
|
|
1452
|
-
|
|
1886
|
+
staleFailures = 0;
|
|
1887
|
+
} catch (error) {
|
|
1888
|
+
staleFailures += 1;
|
|
1889
|
+
reportSweepError("stale_agents", error, staleFailures);
|
|
1890
|
+
}
|
|
1453
1891
|
}, 120000);
|
|
1454
1892
|
sweepTimer.unref();
|
|
1455
1893
|
staleTimer.unref();
|
|
@@ -1505,8 +1943,19 @@ export function createRouter(store) {
|
|
|
1505
1943
|
|
|
1506
1944
|
getStatus(
|
|
1507
1945
|
scope = "hub",
|
|
1508
|
-
{
|
|
1946
|
+
{
|
|
1947
|
+
agent_id,
|
|
1948
|
+
trace_id,
|
|
1949
|
+
include_metrics = true,
|
|
1950
|
+
hub_instance_id,
|
|
1951
|
+
store_fingerprint,
|
|
1952
|
+
} = {},
|
|
1509
1953
|
) {
|
|
1954
|
+
const identityFailure = validateCallerIdentity({
|
|
1955
|
+
hub_instance_id,
|
|
1956
|
+
store_fingerprint,
|
|
1957
|
+
});
|
|
1958
|
+
if (identityFailure) return identityFailure;
|
|
1510
1959
|
const data = {};
|
|
1511
1960
|
|
|
1512
1961
|
if (scope === "hub" || scope === "queue") {
|
|
@@ -1515,6 +1964,7 @@ export function createRouter(store) {
|
|
|
1515
1964
|
uptime_ms: (process.uptime() * 1000) | 0,
|
|
1516
1965
|
realtime_transport: "named-pipe",
|
|
1517
1966
|
audit_store: store.type || "sqlite",
|
|
1967
|
+
...hubIdentity(),
|
|
1518
1968
|
};
|
|
1519
1969
|
data.roles = {
|
|
1520
1970
|
cto: getRoleSnapshot("cto"),
|
|
@@ -1568,7 +2018,7 @@ export function createRouter(store) {
|
|
|
1568
2018
|
data.trace = store.getMessagesByTrace(trace_id);
|
|
1569
2019
|
}
|
|
1570
2020
|
|
|
1571
|
-
return { ok: true, data };
|
|
2021
|
+
return { ok: true, data: { ...data, ...hubIdentity() } };
|
|
1572
2022
|
},
|
|
1573
2023
|
};
|
|
1574
2024
|
|
|
@@ -321,10 +321,12 @@ export async function probeClaudeDaemonCandidates({
|
|
|
321
321
|
short,
|
|
322
322
|
sessionId,
|
|
323
323
|
timeoutMs = 6000,
|
|
324
|
+
tmpRoot = "/tmp",
|
|
324
325
|
} = {}) {
|
|
325
326
|
const candidates = await buildClaudeDaemonDiscoveryCandidates({
|
|
326
327
|
configDir,
|
|
327
328
|
env,
|
|
329
|
+
tmpRoot,
|
|
328
330
|
});
|
|
329
331
|
const callerProvenance = detectCallerProvenance(env);
|
|
330
332
|
const targetRequested = Boolean(short || sessionId);
|
|
@@ -5,11 +5,11 @@ import { statSync } from "node:fs";
|
|
|
5
5
|
import { readdir, readlink } from "node:fs/promises";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
import { isPidAlive } from "../lib/process-utils.mjs";
|
|
8
|
-
import { killProcess } from "../platform.mjs";
|
|
9
8
|
import {
|
|
10
9
|
resolveHardCeilingMs,
|
|
11
10
|
resolveStallInterventionMs as resolveStallThresholdMs,
|
|
12
11
|
} from "../lib/timeout-defaults.mjs";
|
|
12
|
+
import { killProcess } from "../platform.mjs";
|
|
13
13
|
|
|
14
14
|
export { resolveHardCeilingMs, resolveStallThresholdMs };
|
|
15
15
|
|