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.
- package/README.md +1 -1
- package/adapters/codex/skills/tfx-harness/SKILL.md +45 -9
- package/bin/tfx-live.mjs +752 -77
- package/bin/triflux.mjs +9 -6
- 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 +8 -4
- 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/gemini-adapter.mjs +29 -11
- package/hub/lib/memory-store.mjs +66 -30
- package/hub/lib/tfx-route-args.mjs +41 -4
- package/hub/pipe.mjs +10 -15
- package/hub/reflexion.mjs +0 -57
- package/hub/router.mjs +202 -24
- package/hub/schema.sql +1 -0
- package/hub/server.mjs +172 -93
- package/hub/store.mjs +106 -33
- package/hub/team/backend.mjs +4 -2
- package/hub/team/claude-daemon-control.mjs +2 -0
- package/hub/team/cli/commands/start/start-headless.mjs +2 -0
- package/hub/team/cli/commands/start/start-mux.mjs +24 -3
- package/hub/team/cli/services/runtime-mode.mjs +23 -9
- package/hub/team/codex-review.mjs +2 -3
- package/hub/team/conductor.mjs +80 -24
- package/hub/team/cto-auto-collect.mjs +2 -6
- package/hub/team/cto-pull.mjs +4 -0
- package/hub/team/dashboard-open.mjs +1 -0
- package/hub/team/execution-mode.mjs +38 -2
- package/hub/team/headless.mjs +493 -82
- package/hub/team/interactive-tui-transport.mjs +15 -1
- package/hub/team/launcher-template.mjs +19 -2
- package/hub/team/orchestrator.mjs +36 -9
- package/hub/team/pane.mjs +176 -14
- package/hub/team/psmux.mjs +79 -70
- package/hub/team/retry-state-machine.mjs +39 -16
- package/hub/team/session.mjs +57 -0
- package/hub/team/terminal-opener.mjs +68 -13
- package/hub/team/tui-viewer.mjs +65 -14
- package/hub/tools.mjs +6 -0
- package/hub/workers/codex-mcp.mjs +34 -5
- 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/__tests__/keyword-detector.test.mjs +89 -1
- package/scripts/__tests__/release-governance.test.mjs +94 -1
- package/scripts/__tests__/skill-surface.test.mjs +6 -4
- package/scripts/__tests__/tfx-route-bash-node-parity.test.mjs +6 -4
- package/scripts/__tests__/tfx-route-node-entry.test.mjs +23 -17
- package/scripts/claude-login-detect.mjs +107 -25
- package/scripts/ensure-agy-hooks.mjs +31 -1
- package/scripts/keyword-detector.mjs +11 -1
- package/scripts/lib/agent-route-policy.mjs +29 -22
- package/scripts/lib/cli-agy.mjs +24 -4
- package/scripts/lib/codex-profile-config.mjs +26 -8
- package/scripts/lib/codex-recovery.sh +19 -0
- package/scripts/lib/gemini-profiles.mjs +113 -29
- package/scripts/lib/keyword-rules.mjs +12 -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/scripts/notion-read.mjs +2 -1
- package/scripts/release/check-packages-mirror.mjs +53 -1
- package/scripts/release/lib.mjs +1 -2
- package/scripts/release/prepare.mjs +24 -9
- package/scripts/remote-spawn.mjs +830 -32
- package/scripts/session-stale-cleanup.mjs +0 -1
- package/scripts/setup.mjs +474 -143
- package/scripts/sync-hub-mcp-settings.mjs +11 -3
- package/scripts/test-lock.mjs +15 -1
- package/scripts/tfx-gate-activate.mjs +4 -4
- package/scripts/tfx-route-post.mjs +90 -13
- package/scripts/tfx-route.sh +502 -126
- package/skills/merge-worktree/SKILL.md +3 -3
- package/skills/star-prompt/skill.json +1 -1
- package/skills/tfx-analysis/SKILL.md +8 -9
- package/skills/tfx-analysis/skill.json +1 -10
- package/skills/tfx-auto/SKILL.md +203 -46
- package/skills/tfx-auto/skill.json +2 -23
- package/skills/tfx-doctor/skill.json +0 -3
- package/skills/tfx-find/skill.json +0 -7
- package/skills/tfx-forge/SKILL.md +2 -2
- package/skills/tfx-forge/skill.json +0 -7
- package/skills/tfx-goal-clarify/skill.json +5 -0
- package/skills/tfx-harness/SKILL.md +26 -3
- package/skills/tfx-harness/skill.json +5 -0
- package/skills/tfx-hooks/SKILL.md +0 -2
- package/skills/tfx-hooks/skill.json +0 -3
- package/skills/tfx-hub/SKILL.md +1 -6
- package/skills/tfx-hub/skill.json +0 -3
- package/skills/tfx-index/skill.json +0 -6
- package/skills/tfx-interview/SKILL.md +0 -2
- package/skills/tfx-live/SKILL.md +22 -1
- package/skills/tfx-live/skill.json +0 -5
- package/skills/tfx-plan/SKILL.md +7 -8
- package/skills/tfx-plan/skill.json +1 -12
- package/skills/tfx-profile/SKILL.md +6 -6
- package/skills/tfx-profile/skill.json +0 -3
- package/skills/tfx-prune/SKILL.md +4 -6
- package/skills/tfx-qa/SKILL.md +7 -7
- package/skills/tfx-qa/skill.json +1 -11
- package/skills/tfx-ralph/SKILL.md +8 -11
- package/skills/tfx-remote/SKILL.md +2 -2
- package/skills/tfx-remote/skill.json +0 -3
- package/skills/tfx-research/SKILL.md +2 -2
- package/skills/tfx-research/skill.json +1 -16
- package/skills/tfx-review/SKILL.md +11 -11
- package/skills/tfx-review/skill.json +1 -11
- package/skills/tfx-setup/SKILL.md +5 -6
- package/skills/tfx-setup/skill.json +0 -3
- package/skills/tfx-ship/skill.json +1 -9
- package/skills/tfx-wt/skill.json +0 -7
- package/tui/codex-profile.mjs +2 -1
- package/tui/gemini-profile.mjs +11 -24
- package/tui/monitor.mjs +1 -0
- package/tui/setup.mjs +3 -3
- package/scripts/headless-guard-fast.sh +0 -29
- package/scripts/headless-guard.mjs +0 -523
- package/skills/tfx-autopilot/SKILL.md +0 -51
- package/skills/tfx-autopilot/skill.json +0 -12
- package/skills/tfx-codex-swarm-workspace/iteration-1/benchmark.json +0 -75
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/eval_metadata.json +0 -42
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/grading.json +0 -39
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/outputs/analysis.md +0 -87
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/outputs/classification.md +0 -35
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/outputs/commands.sh +0 -275
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/outputs/routing.md +0 -56
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/with_skill/timing.json +0 -5
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/grading.json +0 -39
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/outputs/analysis.md +0 -92
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/outputs/classification.md +0 -71
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/outputs/commands.sh +0 -264
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/outputs/routing.md +0 -113
- package/skills/tfx-codex-swarm-workspace/iteration-1/full-swarm-all-prds/without_skill/timing.json +0 -5
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/eval_metadata.json +0 -32
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/grading.json +0 -29
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/outputs/analysis.md +0 -96
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/outputs/classification.md +0 -38
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/outputs/commands.sh +0 -151
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/outputs/routing.md +0 -51
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/with_skill/timing.json +0 -5
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/grading.json +0 -29
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/outputs/analysis.md +0 -127
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/outputs/classification.md +0 -57
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/outputs/commands.sh +0 -129
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/outputs/routing.md +0 -84
- package/skills/tfx-codex-swarm-workspace/iteration-1/implicit-swarm-no-keywords/without_skill/timing.json +0 -5
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/eval_metadata.json +0 -27
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/grading.json +0 -24
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/outputs/analysis.md +0 -98
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/outputs/classification.md +0 -65
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/outputs/commands.sh +0 -123
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/outputs/routing.md +0 -66
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/with_skill/timing.json +0 -5
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/grading.json +0 -20
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/outputs/analysis.md +0 -88
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/outputs/classification.md +0 -40
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/outputs/commands.sh +0 -130
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/outputs/routing.md +0 -61
- package/skills/tfx-codex-swarm-workspace/iteration-1/selective-spawn-with-override/without_skill/timing.json +0 -5
- package/skills/tfx-consensus/SKILL.md +0 -56
- package/skills/tfx-consensus/skill.json +0 -8
- package/skills/tfx-debate/SKILL.md +0 -60
- package/skills/tfx-debate/skill.json +0 -14
- package/skills/tfx-fullcycle/SKILL.md +0 -47
- package/skills/tfx-fullcycle/skill.json +0 -13
- package/skills/tfx-multi/SKILL.md +0 -45
- package/skills/tfx-multi/references/agent-wrapper-rules.md +0 -94
- package/skills/tfx-multi/references/thorough-pipeline.md +0 -105
- package/skills/tfx-multi/skill.json +0 -10
- package/skills/tfx-panel/SKILL.md +0 -60
- package/skills/tfx-panel/skill.json +0 -14
- package/skills/tfx-persist/SKILL.md +0 -63
- package/skills/tfx-persist/skill.json +0 -14
- package/skills/tfx-psmux-rules/SKILL.md +0 -41
- package/skills/tfx-psmux-rules/skill.json +0 -10
- package/skills/tfx-remote-setup/SKILL.md +0 -42
- package/skills/tfx-remote-setup/skill.json +0 -10
- package/skills/tfx-remote-spawn/SKILL.md +0 -52
- package/skills/tfx-remote-spawn/skill.json +0 -10
- package/skills/tfx-swarm/SKILL.md +0 -49
- package/skills/tfx-swarm/skill.json +0 -13
package/hub/server.mjs
CHANGED
|
@@ -961,6 +961,7 @@ function servePublicFile(res, path) {
|
|
|
961
961
|
* @param {string} [opts.host]
|
|
962
962
|
* @param {string|number} [opts.sessionId]
|
|
963
963
|
* @param {(options: { cwd: string }) => object} [opts.createDelegatorWorker]
|
|
964
|
+
* @param {(dbPath: string) => object|Promise<object>} [opts.createStoreAdapterFn] test seam
|
|
964
965
|
*/
|
|
965
966
|
export async function startHub({
|
|
966
967
|
port: portOpt,
|
|
@@ -968,6 +969,7 @@ export async function startHub({
|
|
|
968
969
|
host = "127.0.0.1",
|
|
969
970
|
sessionId = process.pid,
|
|
970
971
|
createDelegatorWorker = createDelegatorMcpWorker,
|
|
972
|
+
createStoreAdapterFn = createStoreAdapter,
|
|
971
973
|
} = {}) {
|
|
972
974
|
const resolvedPort = parseHubPort(portOpt);
|
|
973
975
|
const portSpecified = Number.isFinite(resolvedPort);
|
|
@@ -1062,7 +1064,7 @@ export async function startHub({
|
|
|
1062
1064
|
cleanupOwnedTokenFile({ token: HUB_TOKEN, port });
|
|
1063
1065
|
}
|
|
1064
1066
|
|
|
1065
|
-
const store = await
|
|
1067
|
+
const store = await createStoreAdapterFn(dbPath);
|
|
1066
1068
|
const router = createRouter(store);
|
|
1067
1069
|
const fingerprintService = createAdaptiveFingerprintService({ store });
|
|
1068
1070
|
|
|
@@ -1720,6 +1722,8 @@ export async function startHub({
|
|
|
1720
1722
|
agent_id,
|
|
1721
1723
|
reason = "manual",
|
|
1722
1724
|
requested_by = "http",
|
|
1725
|
+
hub_instance_id,
|
|
1726
|
+
store_fingerprint,
|
|
1723
1727
|
} = body;
|
|
1724
1728
|
if (!role || !agent_id) {
|
|
1725
1729
|
return writeJson(res, 400, {
|
|
@@ -1732,6 +1736,8 @@ export async function startHub({
|
|
|
1732
1736
|
agent_id,
|
|
1733
1737
|
reason,
|
|
1734
1738
|
requested_by,
|
|
1739
|
+
hub_instance_id,
|
|
1740
|
+
store_fingerprint,
|
|
1735
1741
|
});
|
|
1736
1742
|
return writeJson(res, result.ok ? 200 : 409, result);
|
|
1737
1743
|
}
|
|
@@ -1748,6 +1754,8 @@ export async function startHub({
|
|
|
1748
1754
|
session_id,
|
|
1749
1755
|
host_id,
|
|
1750
1756
|
transport_locators_json,
|
|
1757
|
+
hub_instance_id,
|
|
1758
|
+
store_fingerprint,
|
|
1751
1759
|
} = body;
|
|
1752
1760
|
if (!agent_id || !cli) {
|
|
1753
1761
|
return writeJson(res, 400, {
|
|
@@ -1782,6 +1790,8 @@ export async function startHub({
|
|
|
1782
1790
|
...(transport_locators_json === undefined
|
|
1783
1791
|
? {}
|
|
1784
1792
|
: { transport_locators_json }),
|
|
1793
|
+
...(hub_instance_id === undefined ? {} : { hub_instance_id }),
|
|
1794
|
+
...(store_fingerprint === undefined ? {} : { store_fingerprint }),
|
|
1785
1795
|
});
|
|
1786
1796
|
if (result.ok) {
|
|
1787
1797
|
workerSpans.set(agent_id, {
|
|
@@ -1816,7 +1826,13 @@ export async function startHub({
|
|
|
1816
1826
|
}
|
|
1817
1827
|
|
|
1818
1828
|
if (path === "/bridge/heartbeat" && req.method === "POST") {
|
|
1819
|
-
const {
|
|
1829
|
+
const {
|
|
1830
|
+
agent_id,
|
|
1831
|
+
ttl_ms,
|
|
1832
|
+
presence_generation,
|
|
1833
|
+
hub_instance_id,
|
|
1834
|
+
store_fingerprint,
|
|
1835
|
+
} = body;
|
|
1820
1836
|
if (!agent_id)
|
|
1821
1837
|
return writeJson(res, 400, { ok: false, error: "agent_id 필수" });
|
|
1822
1838
|
const result = await pipe.executeCommand("heartbeat", {
|
|
@@ -1825,6 +1841,9 @@ export async function startHub({
|
|
|
1825
1841
|
ttl_ms,
|
|
1826
1842
|
resolveWorkerLeaseTtlMs(),
|
|
1827
1843
|
),
|
|
1844
|
+
presence_generation,
|
|
1845
|
+
hub_instance_id,
|
|
1846
|
+
store_fingerprint,
|
|
1828
1847
|
});
|
|
1829
1848
|
return writeJson(res, result.ok ? 200 : 404, result);
|
|
1830
1849
|
}
|
|
@@ -1905,6 +1924,9 @@ export async function startHub({
|
|
|
1905
1924
|
status,
|
|
1906
1925
|
include_metrics = true,
|
|
1907
1926
|
trace_id,
|
|
1927
|
+
presence_generation,
|
|
1928
|
+
hub_instance_id,
|
|
1929
|
+
store_fingerprint,
|
|
1908
1930
|
} = body;
|
|
1909
1931
|
|
|
1910
1932
|
if (agent_id && status) {
|
|
@@ -1925,7 +1947,15 @@ export async function startHub({
|
|
|
1925
1947
|
]).has(normalizedStatus)
|
|
1926
1948
|
? normalizedStatus
|
|
1927
1949
|
: "online";
|
|
1928
|
-
router.
|
|
1950
|
+
const transition = router.transitionAgentStatus(
|
|
1951
|
+
normalizedAgentId,
|
|
1952
|
+
statusForStore,
|
|
1953
|
+
presence_generation,
|
|
1954
|
+
{ hub_instance_id, store_fingerprint },
|
|
1955
|
+
);
|
|
1956
|
+
if (!transition.ok) {
|
|
1957
|
+
return writeJson(res, 409, transition);
|
|
1958
|
+
}
|
|
1929
1959
|
const snapshot = await pipe.executeQuery("status", {
|
|
1930
1960
|
scope: "agent",
|
|
1931
1961
|
agent_id: normalizedAgentId,
|
|
@@ -1939,6 +1969,8 @@ export async function startHub({
|
|
|
1939
1969
|
reported_status: normalizedStatus,
|
|
1940
1970
|
reported_at_ms: Date.now(),
|
|
1941
1971
|
snapshot: snapshot?.data?.agent || null,
|
|
1972
|
+
hub_instance_id: transition.data.hub_instance_id,
|
|
1973
|
+
store_fingerprint: transition.data.store_fingerprint,
|
|
1942
1974
|
},
|
|
1943
1975
|
});
|
|
1944
1976
|
}
|
|
@@ -1948,6 +1980,8 @@ export async function startHub({
|
|
|
1948
1980
|
agent_id,
|
|
1949
1981
|
include_metrics,
|
|
1950
1982
|
trace_id,
|
|
1983
|
+
hub_instance_id,
|
|
1984
|
+
store_fingerprint,
|
|
1951
1985
|
});
|
|
1952
1986
|
return writeJson(res, 200, result);
|
|
1953
1987
|
}
|
|
@@ -2167,12 +2201,20 @@ export async function startHub({
|
|
|
2167
2201
|
}
|
|
2168
2202
|
|
|
2169
2203
|
if (path === "/bridge/deregister" && req.method === "POST") {
|
|
2170
|
-
const {
|
|
2204
|
+
const {
|
|
2205
|
+
agent_id,
|
|
2206
|
+
presence_generation,
|
|
2207
|
+
hub_instance_id,
|
|
2208
|
+
store_fingerprint,
|
|
2209
|
+
} = body;
|
|
2171
2210
|
if (!agent_id) {
|
|
2172
2211
|
return writeJson(res, 400, { ok: false, error: "agent_id 필수" });
|
|
2173
2212
|
}
|
|
2174
2213
|
const result = await pipe.executeCommand("deregister", {
|
|
2175
2214
|
agent_id,
|
|
2215
|
+
presence_generation,
|
|
2216
|
+
hub_instance_id,
|
|
2217
|
+
store_fingerprint,
|
|
2176
2218
|
});
|
|
2177
2219
|
if (result.ok) {
|
|
2178
2220
|
finishWorkerTrace(agent_id, "deregister");
|
|
@@ -2316,102 +2358,107 @@ export async function startHub({
|
|
|
2316
2358
|
httpServer.requestTimeout = 30000;
|
|
2317
2359
|
httpServer.headersTimeout = 10000;
|
|
2318
2360
|
|
|
2319
|
-
|
|
2361
|
+
let hitlTimer = null;
|
|
2362
|
+
let sessionTimer = null;
|
|
2363
|
+
let synapsePruneTimer = null;
|
|
2364
|
+
let orphanCleanupTimer = null;
|
|
2365
|
+
let rateLimitTimer = null;
|
|
2320
2366
|
|
|
2321
|
-
const
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2367
|
+
const startBackgroundTimers = () => {
|
|
2368
|
+
hitlTimer = setInterval(() => {
|
|
2369
|
+
try {
|
|
2370
|
+
hitl.checkTimeouts();
|
|
2371
|
+
} catch (err) {
|
|
2372
|
+
hubLog.warn({ err }, "hitl.timeout_check_failed");
|
|
2373
|
+
}
|
|
2374
|
+
}, 10000);
|
|
2375
|
+
hitlTimer.unref();
|
|
2376
|
+
|
|
2377
|
+
// MCP session TTL: sessions idle for SESSION_TTL_MS are closed automatically.
|
|
2378
|
+
// Configurable via SESSION_TTL_MS (default 30 minutes). The sweep runs every 60 s.
|
|
2379
|
+
const SESSION_TTL_MS =
|
|
2380
|
+
parseInt(process.env.TFX_SESSION_TTL_MS || "", 10) || 30 * 60 * 1000;
|
|
2381
|
+
sessionTimer = setInterval(() => {
|
|
2382
|
+
const now = Date.now();
|
|
2383
|
+
for (const [sid, session] of transports) {
|
|
2384
|
+
if (now - (session.transport._lastActivity || 0) <= SESSION_TTL_MS)
|
|
2385
|
+
continue;
|
|
2386
|
+
void closeMcpTransportSession(sid, session, "session_ttl");
|
|
2387
|
+
}
|
|
2388
|
+
}, 60000);
|
|
2389
|
+
sessionTimer.unref();
|
|
2343
2390
|
|
|
2344
|
-
|
|
2345
|
-
try {
|
|
2346
|
-
const { count } = synapseRegistry.pruneExpired();
|
|
2347
|
-
if (count > 0) hubLog.info({ count }, "synapse.prune");
|
|
2348
|
-
} catch (err) {
|
|
2349
|
-
hubLog.warn({ err }, "synapse.prune_failed");
|
|
2350
|
-
}
|
|
2351
|
-
}, 60000);
|
|
2352
|
-
synapsePruneTimer.unref();
|
|
2353
|
-
|
|
2354
|
-
// 고아 node.exe 프로세스 + stale spawn 세션 주기적 정리 (5분마다)
|
|
2355
|
-
// TFX_DISABLE_ORPHAN_CLEANUP=1 로 비활성 (active SSH/swarm 세션 보호 회피용 hotfix gate)
|
|
2356
|
-
const orphanCleanupTimer = setInterval(
|
|
2357
|
-
() => {
|
|
2358
|
-
if (process.env.TFX_DISABLE_ORPHAN_CLEANUP === "1") return;
|
|
2391
|
+
synapsePruneTimer = setInterval(() => {
|
|
2359
2392
|
try {
|
|
2360
|
-
const {
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
);
|
|
2375
|
-
|
|
2393
|
+
const { count } = synapseRegistry.pruneExpired();
|
|
2394
|
+
if (count > 0) hubLog.info({ count }, "synapse.prune");
|
|
2395
|
+
} catch (err) {
|
|
2396
|
+
hubLog.warn({ err }, "synapse.prune_failed");
|
|
2397
|
+
}
|
|
2398
|
+
}, 60000);
|
|
2399
|
+
synapsePruneTimer.unref();
|
|
2400
|
+
|
|
2401
|
+
// 고아 node.exe 프로세스 + stale spawn 세션 주기적 정리 (5분마다)
|
|
2402
|
+
// TFX_DISABLE_ORPHAN_CLEANUP=1 로 비활성 (active SSH/swarm 세션 보호 회피용 hotfix gate)
|
|
2403
|
+
orphanCleanupTimer = setInterval(
|
|
2404
|
+
() => {
|
|
2405
|
+
if (process.env.TFX_DISABLE_ORPHAN_CLEANUP === "1") return;
|
|
2406
|
+
try {
|
|
2407
|
+
const { killed, killedProcesses = [] } = cleanupOrphanNodeProcesses();
|
|
2408
|
+
const {
|
|
2409
|
+
killed: runtimeKilled,
|
|
2410
|
+
killedProcesses: runtimeKilledProcesses = [],
|
|
2411
|
+
} = cleanupOrphanRuntimeProcesses();
|
|
2412
|
+
const totalKilled = killed + runtimeKilled;
|
|
2413
|
+
if (totalKilled > 0) {
|
|
2414
|
+
hubLog.info(
|
|
2415
|
+
{
|
|
2416
|
+
killed: totalKilled,
|
|
2417
|
+
processes: [...killedProcesses, ...runtimeKilledProcesses],
|
|
2418
|
+
caller: "timer",
|
|
2419
|
+
},
|
|
2420
|
+
"hub.orphan_cleanup",
|
|
2421
|
+
);
|
|
2422
|
+
}
|
|
2376
2423
|
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
}
|
|
2388
|
-
} catch {}
|
|
2424
|
+
const { killed: fsmonitorKilled, stale } =
|
|
2425
|
+
cleanupStaleFsmonitorDaemons({
|
|
2426
|
+
minAgeMs: 24 * 60 * 60 * 1000,
|
|
2427
|
+
});
|
|
2428
|
+
if (fsmonitorKilled > 0) {
|
|
2429
|
+
hubLog.info(
|
|
2430
|
+
{ killed: fsmonitorKilled, stale: stale.length },
|
|
2431
|
+
"hub.fsmonitor_cleanup",
|
|
2432
|
+
);
|
|
2433
|
+
}
|
|
2434
|
+
} catch {}
|
|
2389
2435
|
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2436
|
+
// stale tfx-spawn-* psmux 세션 정리 (30분 이상 idle)
|
|
2437
|
+
try {
|
|
2438
|
+
const staleKilled = cleanupStaleSpawnSessions(hubLog);
|
|
2439
|
+
if (staleKilled > 0) {
|
|
2440
|
+
hubLog.info({ killed: staleKilled }, "hub.stale_spawn_cleanup");
|
|
2441
|
+
}
|
|
2442
|
+
} catch {}
|
|
2443
|
+
},
|
|
2444
|
+
5 * 60 * 1000,
|
|
2445
|
+
);
|
|
2446
|
+
orphanCleanupTimer.unref();
|
|
2447
|
+
|
|
2448
|
+
// Evict stale rate-limit buckets once per minute to bound memory usage.
|
|
2449
|
+
rateLimitTimer = setInterval(() => {
|
|
2450
|
+
const cutoff = Date.now() - RATE_LIMIT_WINDOW_MS;
|
|
2451
|
+
for (const [ip, timestamps] of rateLimitMap) {
|
|
2452
|
+
const fresh = timestamps.filter((t) => t >= cutoff);
|
|
2453
|
+
if (fresh.length === 0) {
|
|
2454
|
+
rateLimitMap.delete(ip);
|
|
2455
|
+
} else {
|
|
2456
|
+
rateLimitMap.set(ip, fresh);
|
|
2395
2457
|
}
|
|
2396
|
-
} catch {}
|
|
2397
|
-
},
|
|
2398
|
-
5 * 60 * 1000,
|
|
2399
|
-
);
|
|
2400
|
-
orphanCleanupTimer.unref();
|
|
2401
|
-
|
|
2402
|
-
// Evict stale rate-limit buckets once per minute to bound memory usage.
|
|
2403
|
-
const rateLimitTimer = setInterval(() => {
|
|
2404
|
-
const cutoff = Date.now() - RATE_LIMIT_WINDOW_MS;
|
|
2405
|
-
for (const [ip, timestamps] of rateLimitMap) {
|
|
2406
|
-
const fresh = timestamps.filter((t) => t >= cutoff);
|
|
2407
|
-
if (fresh.length === 0) {
|
|
2408
|
-
rateLimitMap.delete(ip);
|
|
2409
|
-
} else {
|
|
2410
|
-
rateLimitMap.set(ip, fresh);
|
|
2411
2458
|
}
|
|
2412
|
-
}
|
|
2413
|
-
|
|
2414
|
-
|
|
2459
|
+
}, RATE_LIMIT_WINDOW_MS);
|
|
2460
|
+
rateLimitTimer.unref();
|
|
2461
|
+
};
|
|
2415
2462
|
|
|
2416
2463
|
mkdirSync(PID_DIR, { recursive: true });
|
|
2417
2464
|
|
|
@@ -2419,6 +2466,19 @@ export async function startHub({
|
|
|
2419
2466
|
try {
|
|
2420
2467
|
router.stopSweeper();
|
|
2421
2468
|
} catch {}
|
|
2469
|
+
for (const timer of [
|
|
2470
|
+
hitlTimer,
|
|
2471
|
+
sessionTimer,
|
|
2472
|
+
synapsePruneTimer,
|
|
2473
|
+
orphanCleanupTimer,
|
|
2474
|
+
rateLimitTimer,
|
|
2475
|
+
]) {
|
|
2476
|
+
if (timer) clearInterval(timer);
|
|
2477
|
+
}
|
|
2478
|
+
for (const [sid, session] of Array.from(transports)) {
|
|
2479
|
+
await closeMcpTransportSession(sid, session, "hub.startup_failure");
|
|
2480
|
+
}
|
|
2481
|
+
transports.clear();
|
|
2422
2482
|
try {
|
|
2423
2483
|
await pipe.stop();
|
|
2424
2484
|
} catch {}
|
|
@@ -2428,6 +2488,9 @@ export async function startHub({
|
|
|
2428
2488
|
try {
|
|
2429
2489
|
await delegatorWorker.stop();
|
|
2430
2490
|
} catch {}
|
|
2491
|
+
try {
|
|
2492
|
+
synapseRegistry.destroy();
|
|
2493
|
+
} catch {}
|
|
2431
2494
|
try {
|
|
2432
2495
|
store.close();
|
|
2433
2496
|
} catch {}
|
|
@@ -2438,6 +2501,22 @@ export async function startHub({
|
|
|
2438
2501
|
};
|
|
2439
2502
|
|
|
2440
2503
|
try {
|
|
2504
|
+
router.sweepExpired({ now_ms: Date.now(), source: "startup" });
|
|
2505
|
+
router.startSweeper({
|
|
2506
|
+
run_immediately: false,
|
|
2507
|
+
interval_ms: 10000,
|
|
2508
|
+
onSweepError: ({ source, error, consecutive_failures }) => {
|
|
2509
|
+
hubLog.warn(
|
|
2510
|
+
{
|
|
2511
|
+
source,
|
|
2512
|
+
code: error?.code || "UNKNOWN",
|
|
2513
|
+
consecutive_failures,
|
|
2514
|
+
},
|
|
2515
|
+
"hub.sweep_failed",
|
|
2516
|
+
);
|
|
2517
|
+
},
|
|
2518
|
+
});
|
|
2519
|
+
startBackgroundTimers();
|
|
2441
2520
|
await pipe.start();
|
|
2442
2521
|
await assignCallbacks.start();
|
|
2443
2522
|
} catch (error) {
|
package/hub/store.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// hub/store.mjs — SQLite 감사 로그/메타데이터/역할 레지스트리 저장소
|
|
2
2
|
// 실시간 배달은 router/pipe가 담당하고, 역할 권한과 복구 상태는 SQLite가 보존한다.
|
|
3
3
|
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
4
5
|
import { mkdirSync, readFileSync } from "node:fs";
|
|
5
6
|
import { createRequire } from "node:module";
|
|
6
7
|
import { dirname, join } from "node:path";
|
|
@@ -119,6 +120,21 @@ function ensureV6Columns(db) {
|
|
|
119
120
|
}
|
|
120
121
|
}
|
|
121
122
|
|
|
123
|
+
function ensureV7Columns(db) {
|
|
124
|
+
if (hasTable(db, "agents")) {
|
|
125
|
+
ensureColumn(
|
|
126
|
+
db,
|
|
127
|
+
"agents",
|
|
128
|
+
"presence_generation",
|
|
129
|
+
"INTEGER NOT NULL DEFAULT 0",
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function storeFingerprint(dbPath) {
|
|
135
|
+
return `sqlite:${createHash("sha256").update(String(dbPath)).digest("hex")}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
122
138
|
/**
|
|
123
139
|
* 저장소 생성
|
|
124
140
|
* @param {string} dbPath
|
|
@@ -149,7 +165,7 @@ export function createStore(dbPath, options = {}) {
|
|
|
149
165
|
db.exec(
|
|
150
166
|
"CREATE TABLE IF NOT EXISTS _meta (key TEXT PRIMARY KEY, value TEXT)",
|
|
151
167
|
);
|
|
152
|
-
const SCHEMA_VERSION = "
|
|
168
|
+
const SCHEMA_VERSION = "7";
|
|
153
169
|
const curVer = (() => {
|
|
154
170
|
try {
|
|
155
171
|
return db
|
|
@@ -166,6 +182,7 @@ export function createStore(dbPath, options = {}) {
|
|
|
166
182
|
// v5 테이블에는 schema.sql의 v6 인덱스가 참조하는 컬럼이 없으므로
|
|
167
183
|
// CREATE IF NOT EXISTS 재실행 전에 멱등 컬럼 seam을 먼저 적용한다.
|
|
168
184
|
ensureV6Columns(db);
|
|
185
|
+
ensureV7Columns(db);
|
|
169
186
|
if (curVer !== SCHEMA_VERSION) {
|
|
170
187
|
if (curVer != null) {
|
|
171
188
|
// 이미 버전이 기록된 DB에서 버전 불일치가 발생한 경우 경고한다.
|
|
@@ -180,6 +197,7 @@ export function createStore(dbPath, options = {}) {
|
|
|
180
197
|
}
|
|
181
198
|
// 매 부팅 실행해 부분 적용된 마이그레이션도 안전하게 복구한다.
|
|
182
199
|
ensureV6Columns(db);
|
|
200
|
+
ensureV7Columns(db);
|
|
183
201
|
ensureColumn(
|
|
184
202
|
db,
|
|
185
203
|
"reflexion_entries",
|
|
@@ -196,8 +214,8 @@ export function createStore(dbPath, options = {}) {
|
|
|
196
214
|
|
|
197
215
|
const S = {
|
|
198
216
|
upsertAgent: db.prepare(`
|
|
199
|
-
INSERT INTO agents (agent_id, cli, pid, capabilities_json, topics_json, last_seen_ms, lease_expires_ms, status, metadata_json)
|
|
200
|
-
VALUES (@agent_id, @cli, @pid, @capabilities_json, @topics_json, @last_seen_ms, @lease_expires_ms, @status, @metadata_json)
|
|
217
|
+
INSERT INTO agents (agent_id, cli, pid, capabilities_json, topics_json, last_seen_ms, lease_expires_ms, presence_generation, status, metadata_json)
|
|
218
|
+
VALUES (@agent_id, @cli, @pid, @capabilities_json, @topics_json, @last_seen_ms, @lease_expires_ms, 1, @status, @metadata_json)
|
|
201
219
|
ON CONFLICT(agent_id) DO UPDATE SET
|
|
202
220
|
cli=excluded.cli,
|
|
203
221
|
pid=excluded.pid,
|
|
@@ -205,6 +223,7 @@ export function createStore(dbPath, options = {}) {
|
|
|
205
223
|
topics_json=excluded.topics_json,
|
|
206
224
|
last_seen_ms=excluded.last_seen_ms,
|
|
207
225
|
lease_expires_ms=excluded.lease_expires_ms,
|
|
226
|
+
presence_generation=agents.presence_generation + 1,
|
|
208
227
|
status=excluded.status,
|
|
209
228
|
metadata_json=excluded.metadata_json`),
|
|
210
229
|
getAgent: db.prepare("SELECT * FROM agents WHERE agent_id = ?"),
|
|
@@ -212,19 +231,27 @@ export function createStore(dbPath, options = {}) {
|
|
|
212
231
|
"UPDATE agents SET topics_json=?, last_seen_ms=? WHERE agent_id=?",
|
|
213
232
|
),
|
|
214
233
|
heartbeat: db.prepare(
|
|
234
|
+
"UPDATE agents SET last_seen_ms=?, lease_expires_ms=?, status='online' WHERE agent_id=? AND presence_generation=?",
|
|
235
|
+
),
|
|
236
|
+
heartbeatWithoutGeneration: db.prepare(
|
|
215
237
|
"UPDATE agents SET last_seen_ms=?, lease_expires_ms=?, status='online' WHERE agent_id=?",
|
|
216
238
|
),
|
|
217
|
-
setAgentStatus: db.prepare(
|
|
239
|
+
setAgentStatus: db.prepare(
|
|
240
|
+
"UPDATE agents SET status=? WHERE agent_id=? AND presence_generation=?",
|
|
241
|
+
),
|
|
242
|
+
setAgentStatusWithoutGeneration: db.prepare(
|
|
243
|
+
"UPDATE agents SET status=? WHERE agent_id=?",
|
|
244
|
+
),
|
|
218
245
|
onlineAgents: db.prepare("SELECT * FROM agents WHERE status != 'offline'"),
|
|
219
246
|
allAgents: db.prepare("SELECT * FROM agents"),
|
|
220
247
|
agentsByTopic: db.prepare(
|
|
221
248
|
"SELECT a.* FROM agents a, json_each(a.topics_json) t WHERE t.value=? AND a.status != 'offline'",
|
|
222
249
|
),
|
|
223
250
|
markStale: db.prepare(
|
|
224
|
-
"UPDATE agents SET status='stale' WHERE status='online' AND lease_expires_ms < ?",
|
|
251
|
+
"UPDATE agents SET status='stale' WHERE agent_id=? AND presence_generation=? AND status='online' AND lease_expires_ms < ?",
|
|
225
252
|
),
|
|
226
253
|
markOffline: db.prepare(
|
|
227
|
-
"UPDATE agents SET status='offline' WHERE status='stale' AND lease_expires_ms < ? - 300000",
|
|
254
|
+
"UPDATE agents SET status='offline' WHERE agent_id=? AND presence_generation=? AND status='stale' AND lease_expires_ms < ? - 300000",
|
|
228
255
|
),
|
|
229
256
|
|
|
230
257
|
getRole: db.prepare("SELECT * FROM role_registry WHERE role_key_wire = ?"),
|
|
@@ -412,12 +439,9 @@ export function createStore(dbPath, options = {}) {
|
|
|
412
439
|
"UPDATE human_requests SET state=?, response_json=? WHERE request_id=?",
|
|
413
440
|
),
|
|
414
441
|
pendingHR: db.prepare("SELECT * FROM human_requests WHERE state='pending'"),
|
|
415
|
-
expireHR: db.prepare(
|
|
416
|
-
"UPDATE human_requests SET state='timed_out' WHERE state='pending' AND deadline_ms < ?",
|
|
417
|
-
),
|
|
418
442
|
|
|
419
443
|
insertDL: db.prepare(
|
|
420
|
-
"INSERT
|
|
444
|
+
"INSERT INTO dead_letters (message_id, reason, failed_at_ms, last_error) VALUES (?,?,?,?) ON CONFLICT(message_id) DO NOTHING",
|
|
421
445
|
),
|
|
422
446
|
getDL: db.prepare(
|
|
423
447
|
"SELECT * FROM dead_letters ORDER BY failed_at_ms DESC LIMIT ?",
|
|
@@ -464,7 +488,13 @@ export function createStore(dbPath, options = {}) {
|
|
|
464
488
|
WHERE job_id=@job_id`),
|
|
465
489
|
|
|
466
490
|
findExpired: db.prepare(
|
|
467
|
-
"SELECT
|
|
491
|
+
"SELECT * FROM messages WHERE status IN ('queued', 'delivered') AND expires_at_ms <= ?",
|
|
492
|
+
),
|
|
493
|
+
claimExpired: db.prepare(
|
|
494
|
+
"UPDATE messages SET status='dead_letter' WHERE id=? AND status IN ('queued', 'delivered') AND expires_at_ms <= ?",
|
|
495
|
+
),
|
|
496
|
+
claimDeadLetter: db.prepare(
|
|
497
|
+
"UPDATE messages SET status='dead_letter' WHERE id=? AND status != 'dead_letter'",
|
|
468
498
|
),
|
|
469
499
|
urgentDepth: db.prepare(
|
|
470
500
|
"SELECT COUNT(*) as cnt FROM messages WHERE status='queued' AND priority >= 7",
|
|
@@ -705,6 +735,8 @@ export function createStore(dbPath, options = {}) {
|
|
|
705
735
|
|
|
706
736
|
const store = {
|
|
707
737
|
db,
|
|
738
|
+
type: "sqlite",
|
|
739
|
+
fingerprint: storeFingerprint(dbPath),
|
|
708
740
|
uuidv7,
|
|
709
741
|
|
|
710
742
|
close() {
|
|
@@ -738,6 +770,7 @@ export function createStore(dbPath, options = {}) {
|
|
|
738
770
|
return {
|
|
739
771
|
agent_id,
|
|
740
772
|
lease_id: leaseId,
|
|
773
|
+
presence_generation: persisted?.presence_generation ?? null,
|
|
741
774
|
lease_expires_ms: leaseExpires,
|
|
742
775
|
server_time_ms: now,
|
|
743
776
|
effective: {
|
|
@@ -750,6 +783,7 @@ export function createStore(dbPath, options = {}) {
|
|
|
750
783
|
lease_expires_ms: persisted?.lease_expires_ms ?? null,
|
|
751
784
|
store_type: "sqlite",
|
|
752
785
|
db_path: dbPath ?? null,
|
|
786
|
+
store_fingerprint: storeFingerprint(dbPath),
|
|
753
787
|
},
|
|
754
788
|
};
|
|
755
789
|
},
|
|
@@ -758,9 +792,12 @@ export function createStore(dbPath, options = {}) {
|
|
|
758
792
|
return parseAgentRow(S.getAgent.get(id));
|
|
759
793
|
},
|
|
760
794
|
|
|
761
|
-
refreshLease(agentId, ttlMs = 30000) {
|
|
795
|
+
refreshLease(agentId, ttlMs = 30000, presenceGeneration) {
|
|
762
796
|
const now = Date.now();
|
|
763
|
-
const write =
|
|
797
|
+
const write =
|
|
798
|
+
presenceGeneration == null
|
|
799
|
+
? S.heartbeatWithoutGeneration.run(now, now + ttlMs, agentId)
|
|
800
|
+
: S.heartbeat.run(now, now + ttlMs, agentId, presenceGeneration);
|
|
764
801
|
return {
|
|
765
802
|
agent_id: agentId,
|
|
766
803
|
lease_expires_ms: now + ttlMs,
|
|
@@ -769,6 +806,7 @@ export function createStore(dbPath, options = {}) {
|
|
|
769
806
|
updated: write.changes > 0,
|
|
770
807
|
store_type: "sqlite",
|
|
771
808
|
db_path: dbPath ?? null,
|
|
809
|
+
store_fingerprint: storeFingerprint(dbPath),
|
|
772
810
|
},
|
|
773
811
|
};
|
|
774
812
|
},
|
|
@@ -794,14 +832,40 @@ export function createStore(dbPath, options = {}) {
|
|
|
794
832
|
|
|
795
833
|
sweepStaleAgents() {
|
|
796
834
|
const now = Date.now();
|
|
835
|
+
const candidates = S.allAgents.all();
|
|
836
|
+
let stale = 0;
|
|
837
|
+
let offline = 0;
|
|
838
|
+
for (const candidate of candidates) {
|
|
839
|
+
if (candidate.status === "online" && candidate.lease_expires_ms < now) {
|
|
840
|
+
stale += S.markStale.run(
|
|
841
|
+
candidate.agent_id,
|
|
842
|
+
candidate.presence_generation,
|
|
843
|
+
now,
|
|
844
|
+
).changes;
|
|
845
|
+
} else if (
|
|
846
|
+
candidate.status === "stale" &&
|
|
847
|
+
candidate.lease_expires_ms < now - 300000
|
|
848
|
+
) {
|
|
849
|
+
offline += S.markOffline.run(
|
|
850
|
+
candidate.agent_id,
|
|
851
|
+
candidate.presence_generation,
|
|
852
|
+
now,
|
|
853
|
+
).changes;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
797
856
|
return {
|
|
798
|
-
stale
|
|
799
|
-
offline
|
|
857
|
+
stale,
|
|
858
|
+
offline,
|
|
800
859
|
};
|
|
801
860
|
},
|
|
802
861
|
|
|
803
|
-
updateAgentStatus(agentId, status) {
|
|
804
|
-
return
|
|
862
|
+
updateAgentStatus(agentId, status, presenceGeneration) {
|
|
863
|
+
return (
|
|
864
|
+
(presenceGeneration == null
|
|
865
|
+
? S.setAgentStatusWithoutGeneration.run(status, agentId)
|
|
866
|
+
: S.setAgentStatus.run(status, agentId, presenceGeneration)
|
|
867
|
+
).changes > 0
|
|
868
|
+
);
|
|
805
869
|
},
|
|
806
870
|
|
|
807
871
|
getRole(roleKeyWire) {
|
|
@@ -1179,16 +1243,14 @@ export function createStore(dbPath, options = {}) {
|
|
|
1179
1243
|
return S.pendingHR.all().map(parseHumanRequestRow);
|
|
1180
1244
|
},
|
|
1181
1245
|
|
|
1182
|
-
expireHumanRequests() {
|
|
1183
|
-
return S.expireHR.run(Date.now()).changes;
|
|
1184
|
-
},
|
|
1185
|
-
|
|
1186
1246
|
moveToDeadLetter(messageId, reason, lastError = null) {
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
S.
|
|
1247
|
+
const failedAt = Date.now();
|
|
1248
|
+
return db.transaction(() => {
|
|
1249
|
+
const changed = S.claimDeadLetter.run(messageId).changes;
|
|
1250
|
+
if (!changed) return false;
|
|
1251
|
+
S.insertDL.run(messageId, reason, failedAt, lastError);
|
|
1252
|
+
return true;
|
|
1190
1253
|
})();
|
|
1191
|
-
return true;
|
|
1192
1254
|
},
|
|
1193
1255
|
|
|
1194
1256
|
getDeadLetters(limit = 50) {
|
|
@@ -1458,16 +1520,27 @@ export function createStore(dbPath, options = {}) {
|
|
|
1458
1520
|
});
|
|
1459
1521
|
},
|
|
1460
1522
|
|
|
1461
|
-
sweepExpired() {
|
|
1462
|
-
const now = Date.now();
|
|
1523
|
+
sweepExpired({ now_ms = Date.now() } = {}) {
|
|
1463
1524
|
return db.transaction(() => {
|
|
1464
|
-
const
|
|
1465
|
-
for (const
|
|
1466
|
-
S.
|
|
1467
|
-
|
|
1525
|
+
const transitions = [];
|
|
1526
|
+
for (const row of S.findExpired.all(now_ms)) {
|
|
1527
|
+
if (!S.claimExpired.run(row.id, now_ms).changes) continue;
|
|
1528
|
+
options.beforeDeadLetterInsert?.({
|
|
1529
|
+
message_id: row.id,
|
|
1530
|
+
reason: "ttl_expired",
|
|
1531
|
+
failed_at_ms: now_ms,
|
|
1532
|
+
});
|
|
1533
|
+
const dlqInserted =
|
|
1534
|
+
S.insertDL.run(row.id, "ttl_expired", now_ms, null).changes === 1;
|
|
1535
|
+
transitions.push({
|
|
1536
|
+
message: parseMessageRow(row),
|
|
1537
|
+
previous_status: row.status,
|
|
1538
|
+
reason: "ttl_expired",
|
|
1539
|
+
failed_at_ms: now_ms,
|
|
1540
|
+
dlq_inserted: dlqInserted,
|
|
1541
|
+
});
|
|
1468
1542
|
}
|
|
1469
|
-
|
|
1470
|
-
return { messages: expired.length, human_requests: humanRequests };
|
|
1543
|
+
return { now_ms, messages: transitions.length, transitions };
|
|
1471
1544
|
})();
|
|
1472
1545
|
},
|
|
1473
1546
|
|