triflux 10.44.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.
Files changed (193) hide show
  1. package/README.md +1 -1
  2. package/adapters/codex/skills/tfx-harness/SKILL.md +45 -9
  3. package/bin/tfx-live.mjs +752 -77
  4. package/bin/triflux.mjs +9 -6
  5. package/hooks/codex-session-hook.mjs +67 -1
  6. package/hooks/cto-north-star-brief.mjs +3 -16
  7. package/hooks/error-context.mjs +3 -6
  8. package/hooks/hook-manager.mjs +1 -1
  9. package/hooks/hook-registry.json +0 -11
  10. package/hooks/keyword-rules.json +8 -4
  11. package/hooks/permission-safe-allow.mjs +36 -2
  12. package/hooks/session-start-fast.mjs +50 -1
  13. package/hooks/session-start-lake.mjs +3 -16
  14. package/hub/bridge.mjs +111 -5
  15. package/hub/gemini-adapter.mjs +29 -11
  16. package/hub/lib/memory-store.mjs +66 -30
  17. package/hub/lib/tfx-route-args.mjs +41 -4
  18. package/hub/pipe.mjs +10 -15
  19. package/hub/reflexion.mjs +0 -57
  20. package/hub/router.mjs +202 -24
  21. package/hub/schema.sql +1 -0
  22. package/hub/server.mjs +172 -93
  23. package/hub/store.mjs +106 -33
  24. package/hub/team/backend.mjs +4 -2
  25. package/hub/team/claude-daemon-control.mjs +2 -0
  26. package/hub/team/cli/commands/start/start-headless.mjs +2 -0
  27. package/hub/team/cli/commands/start/start-mux.mjs +24 -3
  28. package/hub/team/cli/services/runtime-mode.mjs +23 -9
  29. package/hub/team/codex-review.mjs +2 -3
  30. package/hub/team/conductor.mjs +80 -24
  31. package/hub/team/cto-auto-collect.mjs +2 -6
  32. package/hub/team/cto-pull.mjs +4 -0
  33. package/hub/team/dashboard-open.mjs +1 -0
  34. package/hub/team/execution-mode.mjs +38 -2
  35. package/hub/team/headless.mjs +493 -82
  36. package/hub/team/interactive-tui-transport.mjs +15 -1
  37. package/hub/team/launcher-template.mjs +19 -2
  38. package/hub/team/orchestrator.mjs +36 -9
  39. package/hub/team/pane.mjs +176 -14
  40. package/hub/team/psmux.mjs +79 -70
  41. package/hub/team/retry-state-machine.mjs +39 -16
  42. package/hub/team/session.mjs +57 -0
  43. package/hub/team/terminal-opener.mjs +68 -13
  44. package/hub/team/tui-viewer.mjs +65 -14
  45. package/hub/tools.mjs +6 -0
  46. package/hub/workers/codex-mcp.mjs +34 -5
  47. package/hud/cli-policy.mjs +69 -0
  48. package/hud/constants.mjs +20 -17
  49. package/hud/context-monitor.mjs +31 -21
  50. package/hud/hud-qos-status.mjs +81 -91
  51. package/hud/providers/claude.mjs +150 -84
  52. package/hud/providers/codex.mjs +10 -1
  53. package/hud/renderers.mjs +21 -59
  54. package/hud/utils.mjs +1 -15
  55. package/package.json +1 -1
  56. package/scripts/__tests__/keyword-detector.test.mjs +89 -1
  57. package/scripts/__tests__/release-governance.test.mjs +94 -1
  58. package/scripts/__tests__/skill-surface.test.mjs +6 -4
  59. package/scripts/__tests__/tfx-route-bash-node-parity.test.mjs +6 -4
  60. package/scripts/__tests__/tfx-route-node-entry.test.mjs +23 -17
  61. package/scripts/claude-login-detect.mjs +107 -25
  62. package/scripts/ensure-agy-hooks.mjs +31 -1
  63. package/scripts/keyword-detector.mjs +11 -1
  64. package/scripts/lib/agent-route-policy.mjs +29 -22
  65. package/scripts/lib/cli-agy.mjs +24 -4
  66. package/scripts/lib/codex-profile-config.mjs +26 -8
  67. package/scripts/lib/codex-recovery.sh +19 -0
  68. package/scripts/lib/gemini-profiles.mjs +113 -29
  69. package/scripts/lib/keyword-rules.mjs +12 -0
  70. package/scripts/lib/logger.mjs +60 -43
  71. package/scripts/lib/machine-profile.mjs +293 -0
  72. package/scripts/lib/mcp-health.mjs +44 -18
  73. package/scripts/notion-read.mjs +2 -1
  74. package/scripts/release/check-packages-mirror.mjs +53 -1
  75. package/scripts/release/lib.mjs +1 -2
  76. package/scripts/release/prepare.mjs +24 -9
  77. package/scripts/remote-spawn.mjs +830 -32
  78. package/scripts/session-stale-cleanup.mjs +0 -1
  79. package/scripts/setup.mjs +474 -143
  80. package/scripts/sync-hub-mcp-settings.mjs +11 -3
  81. package/scripts/test-lock.mjs +15 -1
  82. package/scripts/tfx-gate-activate.mjs +4 -4
  83. package/scripts/tfx-route-post.mjs +90 -13
  84. package/scripts/tfx-route.sh +502 -126
  85. package/skills/merge-worktree/SKILL.md +3 -3
  86. package/skills/star-prompt/skill.json +1 -1
  87. package/skills/tfx-analysis/SKILL.md +8 -9
  88. package/skills/tfx-analysis/skill.json +1 -10
  89. package/skills/tfx-auto/SKILL.md +203 -46
  90. package/skills/tfx-auto/skill.json +2 -23
  91. package/skills/tfx-doctor/skill.json +0 -3
  92. package/skills/tfx-find/skill.json +0 -7
  93. package/skills/tfx-forge/SKILL.md +2 -2
  94. package/skills/tfx-forge/skill.json +0 -7
  95. package/skills/tfx-goal-clarify/skill.json +5 -0
  96. package/skills/tfx-harness/SKILL.md +26 -3
  97. package/skills/tfx-harness/skill.json +5 -0
  98. package/skills/tfx-hooks/SKILL.md +0 -2
  99. package/skills/tfx-hooks/skill.json +0 -3
  100. package/skills/tfx-hub/SKILL.md +1 -6
  101. package/skills/tfx-hub/skill.json +0 -3
  102. package/skills/tfx-index/skill.json +0 -6
  103. package/skills/tfx-interview/SKILL.md +0 -2
  104. package/skills/tfx-live/SKILL.md +22 -1
  105. package/skills/tfx-live/skill.json +0 -5
  106. package/skills/tfx-plan/SKILL.md +7 -8
  107. package/skills/tfx-plan/skill.json +1 -12
  108. package/skills/tfx-profile/SKILL.md +6 -6
  109. package/skills/tfx-profile/skill.json +0 -3
  110. package/skills/tfx-prune/SKILL.md +4 -6
  111. package/skills/tfx-qa/SKILL.md +7 -7
  112. package/skills/tfx-qa/skill.json +1 -11
  113. package/skills/tfx-ralph/SKILL.md +8 -11
  114. package/skills/tfx-remote/SKILL.md +2 -2
  115. package/skills/tfx-remote/skill.json +0 -3
  116. package/skills/tfx-research/SKILL.md +2 -2
  117. package/skills/tfx-research/skill.json +1 -16
  118. package/skills/tfx-review/SKILL.md +11 -11
  119. package/skills/tfx-review/skill.json +1 -11
  120. package/skills/tfx-setup/SKILL.md +5 -6
  121. package/skills/tfx-setup/skill.json +0 -3
  122. package/skills/tfx-ship/skill.json +1 -9
  123. package/skills/tfx-wt/skill.json +0 -7
  124. package/tui/codex-profile.mjs +2 -1
  125. package/tui/gemini-profile.mjs +11 -24
  126. package/tui/monitor.mjs +1 -0
  127. package/tui/setup.mjs +3 -3
  128. package/scripts/headless-guard-fast.sh +0 -29
  129. package/scripts/headless-guard.mjs +0 -523
  130. package/skills/tfx-autopilot/SKILL.md +0 -51
  131. package/skills/tfx-autopilot/skill.json +0 -12
  132. package/skills/tfx-codex-swarm-workspace/iteration-1/benchmark.json +0 -75
  133. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/eval_metadata.json +0 -42
  134. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/grading.json +0 -39
  135. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/outputs/analysis.md +0 -87
  136. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/outputs/classification.md +0 -35
  137. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/outputs/commands.sh +0 -275
  138. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/outputs/routing.md +0 -56
  139. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/timing.json +0 -5
  140. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/grading.json +0 -39
  141. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/outputs/analysis.md +0 -92
  142. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/outputs/classification.md +0 -71
  143. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/outputs/commands.sh +0 -264
  144. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/outputs/routing.md +0 -113
  145. package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/timing.json +0 -5
  146. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/eval_metadata.json +0 -32
  147. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/grading.json +0 -29
  148. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/outputs/analysis.md +0 -96
  149. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/outputs/classification.md +0 -38
  150. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/outputs/commands.sh +0 -151
  151. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/outputs/routing.md +0 -51
  152. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/timing.json +0 -5
  153. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/grading.json +0 -29
  154. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/outputs/analysis.md +0 -127
  155. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/outputs/classification.md +0 -57
  156. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/outputs/commands.sh +0 -129
  157. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/outputs/routing.md +0 -84
  158. package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/timing.json +0 -5
  159. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/eval_metadata.json +0 -27
  160. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/grading.json +0 -24
  161. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/outputs/analysis.md +0 -98
  162. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/outputs/classification.md +0 -65
  163. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/outputs/commands.sh +0 -123
  164. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/outputs/routing.md +0 -66
  165. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/timing.json +0 -5
  166. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/grading.json +0 -20
  167. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/outputs/analysis.md +0 -88
  168. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/outputs/classification.md +0 -40
  169. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/outputs/commands.sh +0 -130
  170. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/outputs/routing.md +0 -61
  171. package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/timing.json +0 -5
  172. package/skills/tfx-consensus/SKILL.md +0 -56
  173. package/skills/tfx-consensus/skill.json +0 -8
  174. package/skills/tfx-debate/SKILL.md +0 -60
  175. package/skills/tfx-debate/skill.json +0 -14
  176. package/skills/tfx-fullcycle/SKILL.md +0 -47
  177. package/skills/tfx-fullcycle/skill.json +0 -13
  178. package/skills/tfx-multi/SKILL.md +0 -45
  179. package/skills/tfx-multi/references/agent-wrapper-rules.md +0 -94
  180. package/skills/tfx-multi/references/thorough-pipeline.md +0 -105
  181. package/skills/tfx-multi/skill.json +0 -10
  182. package/skills/tfx-panel/SKILL.md +0 -60
  183. package/skills/tfx-panel/skill.json +0 -14
  184. package/skills/tfx-persist/SKILL.md +0 -63
  185. package/skills/tfx-persist/skill.json +0 -14
  186. package/skills/tfx-psmux-rules/SKILL.md +0 -41
  187. package/skills/tfx-psmux-rules/skill.json +0 -10
  188. package/skills/tfx-remote-setup/SKILL.md +0 -42
  189. package/skills/tfx-remote-setup/skill.json +0 -10
  190. package/skills/tfx-remote-spawn/SKILL.md +0 -52
  191. package/skills/tfx-remote-spawn/skill.json +0 -10
  192. package/skills/tfx-swarm/SKILL.md +0 -49
  193. package/skills/tfx-swarm/skill.json +0 -13
package/hub/router.mjs CHANGED
@@ -376,6 +376,9 @@ export function createRouter(store) {
376
376
  const queuesByAgent = new Map();
377
377
  const liveMessages = new Map();
378
378
  const roleStates = new Map();
379
+ const hubInstanceId = uuidv7();
380
+ const storeFingerprint =
381
+ store.fingerprint || `${store.type || "unknown"}:unknown`;
379
382
  const updateRegistrationIdentity = store.db?.prepare
380
383
  ? store.db.prepare(`
381
384
  UPDATE agents SET
@@ -392,6 +395,70 @@ export function createRouter(store) {
392
395
  let latencyIdx = 0;
393
396
  const deliveryLatencies = new Array(MAX_LATENCY_SAMPLES).fill(0);
394
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
+
395
462
  function ensureAgentQueue(agentId) {
396
463
  let queue = queuesByAgent.get(agentId);
397
464
  if (!queue) {
@@ -1120,6 +1187,18 @@ export function createRouter(store) {
1120
1187
  deliveryEmitter,
1121
1188
 
1122
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
+ }
1123
1202
  const identity = normalizeRegistrationIdentity(args);
1124
1203
  const result = store.registerAgent(args);
1125
1204
  if (!registerPresenceIsEffective(result)) {
@@ -1139,11 +1218,23 @@ export function createRouter(store) {
1139
1218
  transferBacklog: true,
1140
1219
  });
1141
1220
  }
1142
- return { ok: true, data: { ...result, hub_pid: process.pid } };
1221
+ return {
1222
+ ok: true,
1223
+ data: { ...result, hub_pid: process.pid, ...hubIdentity() },
1224
+ };
1143
1225
  },
1144
1226
 
1145
- refreshAgentLease(agentId, ttlMs = 30000) {
1146
- const result = store.refreshLease(agentId, ttlMs);
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);
1147
1238
  refreshRoleCandidateForAgent(agentId);
1148
1239
  for (const roleName of ROLE_TOPICS) {
1149
1240
  ensureRoleLeader(roleName, {
@@ -1151,7 +1242,7 @@ export function createRouter(store) {
1151
1242
  transferBacklog: true,
1152
1243
  });
1153
1244
  }
1154
- return result;
1245
+ return { ok: true, data: result };
1155
1246
  },
1156
1247
 
1157
1248
  subscribeAgent(agentId, topics, { replace = false } = {}) {
@@ -1170,11 +1261,15 @@ export function createRouter(store) {
1170
1261
  return listRuntimeTopics(agentId);
1171
1262
  },
1172
1263
 
1173
- updateAgentStatus(agentId, status) {
1264
+ updateAgentStatus(agentId, status, presenceGeneration) {
1174
1265
  if (status === "offline") {
1175
1266
  runtimeTopics.delete(agentId);
1176
1267
  }
1177
- const updated = store.updateAgentStatus(agentId, status);
1268
+ const updated = store.updateAgentStatus(
1269
+ agentId,
1270
+ status,
1271
+ presenceGeneration,
1272
+ );
1178
1273
  refreshRoleCandidateForAgent(agentId);
1179
1274
  for (const roleName of ROLE_TOPICS) {
1180
1275
  ensureRoleLeader(roleName, {
@@ -1185,6 +1280,39 @@ export function createRouter(store) {
1185
1280
  return updated;
1186
1281
  },
1187
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
+
1188
1316
  route(msg) {
1189
1317
  const recipients = uniqueStrings(resolveRecipients(msg));
1190
1318
  if (!recipients.length) {
@@ -1369,7 +1497,14 @@ export function createRouter(store) {
1369
1497
  agent_id,
1370
1498
  reason = "manual",
1371
1499
  requested_by = "manual",
1500
+ hub_instance_id,
1501
+ store_fingerprint,
1372
1502
  } = {}) {
1503
+ const identityFailure = validateCallerIdentity({
1504
+ hub_instance_id,
1505
+ store_fingerprint,
1506
+ });
1507
+ if (identityFailure) return identityFailure;
1373
1508
  const roleName = normalizeRoleName(role);
1374
1509
  if (!roleName) {
1375
1510
  return {
@@ -1440,6 +1575,7 @@ export function createRouter(store) {
1440
1575
  leader_epoch: snapshot.leader_epoch,
1441
1576
  ...transfer,
1442
1577
  status: snapshot,
1578
+ ...hubIdentity(),
1443
1579
  },
1444
1580
  };
1445
1581
  },
@@ -1647,18 +1783,15 @@ export function createRouter(store) {
1647
1783
  return scheduleAssignRetry(job, reason, job.error, requested_by);
1648
1784
  },
1649
1785
 
1650
- sweepExpired() {
1651
- const now = Date.now();
1652
- let expired = 0;
1653
- const deadLettered = [];
1786
+ sweepExpired({ now_ms = Date.now(), source = "manual" } = {}) {
1787
+ const result = store.sweepExpired({ now_ms });
1654
1788
  for (const [messageId, record] of Array.from(liveMessages.entries())) {
1655
- if (record.message.expires_at_ms > now) continue;
1656
- store.moveToDeadLetter(messageId, "ttl_expired", null);
1657
- deadLettered.push(record.message);
1789
+ if (record.message.expires_at_ms > now_ms) continue;
1658
1790
  removeMessage(messageId);
1659
- expired += 1;
1660
1791
  }
1661
- for (const message of deadLettered) {
1792
+ for (const transition of result.transitions) {
1793
+ if (!transition.dlq_inserted) continue;
1794
+ const { message } = transition;
1662
1795
  if (
1663
1796
  !["handoff", "request"].includes(message.type) ||
1664
1797
  message.payload?.kind === "assign.job"
@@ -1683,12 +1816,15 @@ export function createRouter(store) {
1683
1816
  original_to: message.to_agent,
1684
1817
  reason: "ttl_expired",
1685
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,
1686
1822
  },
1687
1823
  trace_id: message.trace_id,
1688
1824
  correlation_id: message.correlation_id,
1689
1825
  });
1690
1826
  }
1691
- return { messages: expired };
1827
+ return result;
1692
1828
  },
1693
1829
 
1694
1830
  sweepTimedOutAssigns() {
@@ -1709,19 +1845,49 @@ export function createRouter(store) {
1709
1845
  return { timed_out, retried };
1710
1846
  },
1711
1847
 
1712
- startSweeper() {
1848
+ startSweeper({
1849
+ run_immediately = false,
1850
+ interval_ms = 10000,
1851
+ onSweepError,
1852
+ } = {}) {
1713
1853
  if (sweepTimer) return;
1714
- sweepTimer = setInterval(() => {
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
+ }
1715
1869
  try {
1716
- router.sweepExpired();
1717
1870
  router.sweepTimedOutAssigns();
1718
- } catch {}
1719
- }, 10000);
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;
1720
1882
  staleTimer = setInterval(() => {
1721
1883
  try {
1722
1884
  store.sweepStaleAgents();
1723
1885
  router.reelectStaleRoles();
1724
- } catch {}
1886
+ staleFailures = 0;
1887
+ } catch (error) {
1888
+ staleFailures += 1;
1889
+ reportSweepError("stale_agents", error, staleFailures);
1890
+ }
1725
1891
  }, 120000);
1726
1892
  sweepTimer.unref();
1727
1893
  staleTimer.unref();
@@ -1777,8 +1943,19 @@ export function createRouter(store) {
1777
1943
 
1778
1944
  getStatus(
1779
1945
  scope = "hub",
1780
- { agent_id, trace_id, include_metrics = true } = {},
1946
+ {
1947
+ agent_id,
1948
+ trace_id,
1949
+ include_metrics = true,
1950
+ hub_instance_id,
1951
+ store_fingerprint,
1952
+ } = {},
1781
1953
  ) {
1954
+ const identityFailure = validateCallerIdentity({
1955
+ hub_instance_id,
1956
+ store_fingerprint,
1957
+ });
1958
+ if (identityFailure) return identityFailure;
1782
1959
  const data = {};
1783
1960
 
1784
1961
  if (scope === "hub" || scope === "queue") {
@@ -1787,6 +1964,7 @@ export function createRouter(store) {
1787
1964
  uptime_ms: (process.uptime() * 1000) | 0,
1788
1965
  realtime_transport: "named-pipe",
1789
1966
  audit_store: store.type || "sqlite",
1967
+ ...hubIdentity(),
1790
1968
  };
1791
1969
  data.roles = {
1792
1970
  cto: getRoleSnapshot("cto"),
@@ -1840,7 +2018,7 @@ export function createRouter(store) {
1840
2018
  data.trace = store.getMessagesByTrace(trace_id);
1841
2019
  }
1842
2020
 
1843
- return { ok: true, data };
2021
+ return { ok: true, data: { ...data, ...hubIdentity() } };
1844
2022
  },
1845
2023
  };
1846
2024
 
package/hub/schema.sql CHANGED
@@ -10,6 +10,7 @@ CREATE TABLE IF NOT EXISTS agents (
10
10
  topics_json TEXT NOT NULL DEFAULT '[]',
11
11
  last_seen_ms INTEGER NOT NULL,
12
12
  lease_expires_ms INTEGER NOT NULL,
13
+ presence_generation INTEGER NOT NULL DEFAULT 0,
13
14
  status TEXT NOT NULL CHECK (status IN ('online','stale','offline')),
14
15
  metadata_json TEXT NOT NULL DEFAULT '{}',
15
16
  project_id TEXT,