@rulemetric/cli 0.6.8 → 0.7.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/dist/base-command.js +4 -3
- package/dist/{chunk-SZ7VDCD6.js → chunk-2HK7RGZB.js} +22 -3
- package/dist/chunk-2HK7RGZB.js.map +7 -0
- package/dist/{chunk-RUCSRN7F.js → chunk-2ORJWTNZ.js} +2 -2
- package/dist/{chunk-SLL3OBKN.js → chunk-37LN5O7M.js} +2 -2
- package/dist/{chunk-RCMXTLQF.js → chunk-3LKVS6W5.js} +5 -5
- package/dist/{chunk-XN23W5HL.js → chunk-4KRMZLQV.js} +91 -7
- package/dist/chunk-4KRMZLQV.js.map +7 -0
- package/dist/{chunk-DAJNO4OI.js → chunk-4LYCDS2N.js} +28 -16
- package/dist/chunk-4LYCDS2N.js.map +7 -0
- package/dist/{chunk-UPXIO7CG.js → chunk-4XAWK6S2.js} +8 -8
- package/dist/{chunk-N5A2IQAK.js → chunk-4YAP7RT7.js} +5 -5
- package/dist/{chunk-QD6X4L4L.js → chunk-5BAOGNUH.js} +4 -4
- package/dist/chunk-5EDMGK3W.js +39 -0
- package/dist/chunk-5EDMGK3W.js.map +7 -0
- package/dist/{chunk-5ADHO5QF.js → chunk-6NBS5XFS.js} +6 -6
- package/dist/chunk-7LVVKQMZ.js +98 -0
- package/dist/chunk-7LVVKQMZ.js.map +7 -0
- package/dist/{chunk-PXVQLD7S.js → chunk-7NNFYWKK.js} +48 -1
- package/dist/chunk-7NNFYWKK.js.map +7 -0
- package/dist/{chunk-BBLTN35F.js → chunk-7UDETV3P.js} +49 -20
- package/dist/chunk-7UDETV3P.js.map +7 -0
- package/dist/{chunk-DTFXCHHI.js → chunk-DNKSVWMD.js} +4 -4
- package/dist/{chunk-6EMZI4Y3.js → chunk-EI5BHWXA.js} +437 -401
- package/dist/chunk-EI5BHWXA.js.map +7 -0
- package/dist/{chunk-HNZLEEG2.js → chunk-EJ3YAFDP.js} +4 -4
- package/dist/{chunk-HNZLEEG2.js.map → chunk-EJ3YAFDP.js.map} +2 -2
- package/dist/{chunk-YSEQWCN4.js → chunk-EYPAAGN5.js} +2 -2
- package/dist/{chunk-JEJ3J5J6.js → chunk-GIUBARWS.js} +2 -2
- package/dist/chunk-GQNJO2P2.js +102 -0
- package/dist/chunk-GQNJO2P2.js.map +7 -0
- package/dist/{chunk-CNMWYYSV.js → chunk-HZK43UCF.js} +6 -6
- package/dist/chunk-ILYDELUE.js +46 -0
- package/dist/chunk-ILYDELUE.js.map +7 -0
- package/dist/chunk-J6QFW4LD.js +55 -0
- package/dist/chunk-J6QFW4LD.js.map +7 -0
- package/dist/{chunk-OTOWLUOJ.js → chunk-JULOLTZS.js} +2 -2
- package/dist/{chunk-DR5WLIOU.js → chunk-KJKBDQ4D.js} +2 -2
- package/dist/{chunk-AHDRN6NE.js → chunk-OO7JDFS4.js} +5 -5
- package/dist/{chunk-N2X35ITW.js → chunk-OQBBVTDG.js} +11 -1
- package/dist/{chunk-N2X35ITW.js.map → chunk-OQBBVTDG.js.map} +2 -2
- package/dist/{chunk-3QH73QOQ.js → chunk-PED7L5GX.js} +2 -2
- package/dist/{chunk-VBGUNFKY.js → chunk-PMI56ED5.js} +5 -5
- package/dist/{chunk-XNNOXGP6.js → chunk-POFTPB33.js} +38 -19
- package/dist/chunk-POFTPB33.js.map +7 -0
- package/dist/{chunk-W4MZXKZ2.js → chunk-UKMLICWS.js} +2 -2
- package/dist/{chunk-3POYC5NA.js → chunk-XK42ZBDE.js} +3 -3
- package/dist/{chunk-ZSGKRCKD.js → chunk-XUV6DDBF.js} +2 -2
- package/dist/{chunk-LAKZUE35.js → chunk-Y3WD5CSW.js} +2 -2
- package/dist/{chunk-3QVMIJUL.js → chunk-YOBA3NAM.js} +5 -5
- package/dist/{chunk-ZRLYHBPN.js → chunk-ZUJBM6HE.js} +20 -2
- package/dist/chunk-ZUJBM6HE.js.map +7 -0
- package/dist/commands/antigravity/backfill.js +5 -4
- package/dist/commands/antigravity/backfill.js.map +1 -1
- package/dist/commands/auth/create-key.js +4 -3
- package/dist/commands/auth/create-key.js.map +1 -1
- package/dist/commands/auth/login.js +13 -9
- package/dist/commands/auth/login.js.map +2 -2
- package/dist/commands/auth/logout.js +4 -3
- package/dist/commands/auth/logout.js.map +1 -1
- package/dist/commands/auth/revoke-key.js +4 -3
- package/dist/commands/auth/revoke-key.js.map +1 -1
- package/dist/commands/auth/signup.js +13 -9
- package/dist/commands/auth/signup.js.map +2 -2
- package/dist/commands/auth/status.js +4 -3
- package/dist/commands/auth/status.js.map +1 -1
- package/dist/commands/catalog/index.js +4 -3
- package/dist/commands/catalog/index.js.map +1 -1
- package/dist/commands/catalog/recommend.js +4 -3
- package/dist/commands/catalog/recommend.js.map +1 -1
- package/dist/commands/convert.js +4 -3
- package/dist/commands/convert.js.map +1 -1
- package/dist/commands/converters.js +4 -3
- package/dist/commands/converters.js.map +1 -1
- package/dist/commands/crons/list.js +4 -3
- package/dist/commands/crons/list.js.map +1 -1
- package/dist/commands/crons/run.js +4 -3
- package/dist/commands/crons/run.js.map +1 -1
- package/dist/commands/dashboard.js +4 -3
- package/dist/commands/dashboard.js.map +1 -1
- package/dist/commands/diagnostics.js +117 -0
- package/dist/commands/diagnostics.js.map +7 -0
- package/dist/commands/eval-targets/create.js +4 -3
- package/dist/commands/eval-targets/create.js.map +1 -1
- package/dist/commands/eval-targets/list.js +4 -3
- package/dist/commands/eval-targets/list.js.map +1 -1
- package/dist/commands/evals/agent.js +44 -23
- package/dist/commands/evals/agent.js.map +2 -2
- package/dist/commands/evals/benchmark.js +5 -4
- package/dist/commands/evals/benchmark.js.map +1 -1
- package/dist/commands/evals/compare.js +4 -3
- package/dist/commands/evals/compare.js.map +1 -1
- package/dist/commands/evals/create.js +4 -3
- package/dist/commands/evals/create.js.map +1 -1
- package/dist/commands/evals/optimize-description.js +4 -3
- package/dist/commands/evals/optimize-description.js.map +1 -1
- package/dist/commands/evals/optimize.js +4 -3
- package/dist/commands/evals/optimize.js.map +1 -1
- package/dist/commands/evals/run.js +4 -3
- package/dist/commands/evals/run.js.map +1 -1
- package/dist/commands/gateway/ensure.js +58 -5
- package/dist/commands/gateway/ensure.js.map +2 -2
- package/dist/commands/hooks/install.js +76 -23
- package/dist/commands/hooks/install.js.map +2 -2
- package/dist/commands/hooks/run.js +9 -5
- package/dist/commands/hooks/run.js.map +2 -2
- package/dist/commands/hooks/uninstall.js +31 -40
- package/dist/commands/hooks/uninstall.js.map +2 -2
- package/dist/commands/import.js +4 -3
- package/dist/commands/import.js.map +1 -1
- package/dist/commands/instructions/create.js +4 -3
- package/dist/commands/instructions/create.js.map +1 -1
- package/dist/commands/instructions/delete.js +4 -3
- package/dist/commands/instructions/delete.js.map +1 -1
- package/dist/commands/instructions/fork.js +4 -3
- package/dist/commands/instructions/fork.js.map +1 -1
- package/dist/commands/instructions/get.js +4 -3
- package/dist/commands/instructions/get.js.map +1 -1
- package/dist/commands/instructions/list.js +4 -3
- package/dist/commands/instructions/list.js.map +1 -1
- package/dist/commands/instructions/promote.js +4 -3
- package/dist/commands/instructions/promote.js.map +1 -1
- package/dist/commands/instructions/pull.js +4 -3
- package/dist/commands/instructions/pull.js.map +1 -1
- package/dist/commands/instructions/upstream.js +4 -3
- package/dist/commands/instructions/upstream.js.map +1 -1
- package/dist/commands/instructions/versions.js +4 -3
- package/dist/commands/instructions/versions.js.map +1 -1
- package/dist/commands/launcher.js +4 -3
- package/dist/commands/launcher.js.map +1 -1
- package/dist/commands/org/current.js +4 -3
- package/dist/commands/org/current.js.map +1 -1
- package/dist/commands/org/list.js +4 -3
- package/dist/commands/org/list.js.map +1 -1
- package/dist/commands/org/switch.js +4 -3
- package/dist/commands/org/switch.js.map +1 -1
- package/dist/commands/packs/add-instruction.js +4 -3
- package/dist/commands/packs/add-instruction.js.map +1 -1
- package/dist/commands/packs/apply.js +4 -3
- package/dist/commands/packs/apply.js.map +1 -1
- package/dist/commands/packs/create.js +4 -3
- package/dist/commands/packs/create.js.map +1 -1
- package/dist/commands/packs/fork.js +4 -3
- package/dist/commands/packs/fork.js.map +1 -1
- package/dist/commands/packs/get.js +4 -3
- package/dist/commands/packs/get.js.map +1 -1
- package/dist/commands/packs/list.js +4 -3
- package/dist/commands/packs/list.js.map +1 -1
- package/dist/commands/packs/remove-instruction.js +4 -3
- package/dist/commands/packs/remove-instruction.js.map +1 -1
- package/dist/commands/proxy/env.js +7 -4
- package/dist/commands/proxy/env.js.map +1 -1
- package/dist/commands/proxy/logs.js +4 -3
- package/dist/commands/proxy/logs.js.map +1 -1
- package/dist/commands/proxy/restart.js +4 -3
- package/dist/commands/proxy/restart.js.map +1 -1
- package/dist/commands/proxy/setup.js +20 -14
- package/dist/commands/proxy/setup.js.map +2 -2
- package/dist/commands/proxy/start.js +18 -14
- package/dist/commands/proxy/start.js.map +2 -2
- package/dist/commands/proxy/status.js +16 -13
- package/dist/commands/proxy/status.js.map +2 -2
- package/dist/commands/proxy/stop.js +21 -12
- package/dist/commands/proxy/stop.js.map +2 -2
- package/dist/commands/research/backfill.js +5 -4
- package/dist/commands/research/backfill.js.map +1 -1
- package/dist/commands/research/tail-transcript.js +5 -4
- package/dist/commands/research/tail-transcript.js.map +1 -1
- package/dist/commands/service/install.js +6 -6
- package/dist/commands/service/status.js +4 -3
- package/dist/commands/service/status.js.map +1 -1
- package/dist/commands/service/uninstall.js +4 -3
- package/dist/commands/service/uninstall.js.map +1 -1
- package/dist/commands/sessions/analyze.js +4 -3
- package/dist/commands/sessions/analyze.js.map +1 -1
- package/dist/commands/sessions/end.js +4 -3
- package/dist/commands/sessions/end.js.map +1 -1
- package/dist/commands/sessions/import.js +4 -3
- package/dist/commands/sessions/import.js.map +1 -1
- package/dist/commands/sessions/list.js +4 -3
- package/dist/commands/sessions/list.js.map +1 -1
- package/dist/commands/sessions/start.js +4 -3
- package/dist/commands/sessions/start.js.map +1 -1
- package/dist/commands/setup.js +10 -6
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/skills/export-approved.js +4 -3
- package/dist/commands/skills/export-approved.js.map +1 -1
- package/dist/commands/skills/install.js +4 -3
- package/dist/commands/skills/install.js.map +1 -1
- package/dist/commands/skills/list.js +4 -3
- package/dist/commands/skills/list.js.map +1 -1
- package/dist/commands/skills/publish.js +4 -3
- package/dist/commands/skills/publish.js.map +1 -1
- package/dist/commands/skills/search.js +4 -3
- package/dist/commands/skills/search.js.map +1 -1
- package/dist/commands/skills/update.js +4 -3
- package/dist/commands/skills/update.js.map +1 -1
- package/dist/commands/suggestions/accept.js +4 -3
- package/dist/commands/suggestions/accept.js.map +1 -1
- package/dist/commands/suggestions/dismiss.js +4 -3
- package/dist/commands/suggestions/dismiss.js.map +1 -1
- package/dist/commands/suggestions/list.js +4 -3
- package/dist/commands/suggestions/list.js.map +1 -1
- package/dist/commands/suggestions/refresh.js +4 -3
- package/dist/commands/suggestions/refresh.js.map +1 -1
- package/dist/dashboard/Dashboard.js +16 -16
- package/dist/dashboard/data.js +2 -2
- package/dist/dashboard/launcher/ActiveJobsList.js +2 -2
- package/dist/dashboard/launcher/LauncherOverlay.js +7 -7
- package/dist/dashboard/launcher/NewLaunchForm.js +2 -2
- package/dist/dashboard/launcher/api.js +2 -2
- package/dist/dashboard/launcher/useLaunchJobs.js +3 -3
- package/dist/lib/active-org-refresh.js +2 -2
- package/dist/lib/agent-loop.js +20 -18
- package/dist/lib/antigravity-transcript.js +2 -2
- package/dist/lib/api-client.js +1 -1
- package/dist/lib/diagnostics.js +12 -0
- package/dist/lib/diagnostics.js.map +7 -0
- package/dist/lib/ensure-api-key.js +3 -3
- package/dist/lib/eval-benchmark.js +2 -2
- package/dist/lib/eval-conversation-handler.js +2 -2
- package/dist/lib/gateway-entry.js +39 -1
- package/dist/lib/gateway-entry.js.map +2 -2
- package/dist/lib/gateway-identity.js +16 -0
- package/dist/lib/gateway-identity.js.map +7 -0
- package/dist/lib/gateway-lifecycle.js +9 -3
- package/dist/lib/gateway.js +3 -1
- package/dist/lib/handlers/cron-suggest-instructions.js +3 -3
- package/dist/lib/handlers/process-announcement.js +3 -3
- package/dist/lib/handlers/process-changelog.js +3 -3
- package/dist/lib/handlers/process-conversation.js +3 -3
- package/dist/lib/handlers/process-eval.js +3 -3
- package/dist/lib/handlers/process-insights.js +3 -3
- package/dist/lib/handlers/process-launch.js +2 -2
- package/dist/lib/handlers/process-run-cleanup.js +2 -2
- package/dist/lib/handlers/process-session-goal.js +2 -2
- package/dist/lib/hooks-config.js +14 -1
- package/dist/lib/manual-tasks.js +6 -6
- package/dist/lib/pid-identity.js +14 -0
- package/dist/lib/pid-identity.js.map +7 -0
- package/dist/lib/proxy-respawn.js +12 -0
- package/dist/lib/proxy-respawn.js.map +7 -0
- package/dist/lib/research-client.js +2 -2
- package/dist/lib/setup-steps.js +7 -4
- package/dist/lib/statusline-shim.js +3 -1
- package/dist/lib/telemetry.js +4 -2
- package/dist/lib/terminal-target-heartbeat.js +2 -2
- package/oclif.manifest.json +34 -1
- package/package.json +6 -6
- package/dist/chunk-6EMZI4Y3.js.map +0 -7
- package/dist/chunk-BBLTN35F.js.map +0 -7
- package/dist/chunk-DAJNO4OI.js.map +0 -7
- package/dist/chunk-PXVQLD7S.js.map +0 -7
- package/dist/chunk-SZ7VDCD6.js.map +0 -7
- package/dist/chunk-TX2HKJLA.js +0 -55
- package/dist/chunk-TX2HKJLA.js.map +0 -7
- package/dist/chunk-XN23W5HL.js.map +0 -7
- package/dist/chunk-XNNOXGP6.js.map +0 -7
- package/dist/chunk-ZRLYHBPN.js.map +0 -7
- /package/dist/{chunk-RUCSRN7F.js.map → chunk-2ORJWTNZ.js.map} +0 -0
- /package/dist/{chunk-SLL3OBKN.js.map → chunk-37LN5O7M.js.map} +0 -0
- /package/dist/{chunk-RCMXTLQF.js.map → chunk-3LKVS6W5.js.map} +0 -0
- /package/dist/{chunk-UPXIO7CG.js.map → chunk-4XAWK6S2.js.map} +0 -0
- /package/dist/{chunk-N5A2IQAK.js.map → chunk-4YAP7RT7.js.map} +0 -0
- /package/dist/{chunk-QD6X4L4L.js.map → chunk-5BAOGNUH.js.map} +0 -0
- /package/dist/{chunk-5ADHO5QF.js.map → chunk-6NBS5XFS.js.map} +0 -0
- /package/dist/{chunk-DTFXCHHI.js.map → chunk-DNKSVWMD.js.map} +0 -0
- /package/dist/{chunk-YSEQWCN4.js.map → chunk-EYPAAGN5.js.map} +0 -0
- /package/dist/{chunk-JEJ3J5J6.js.map → chunk-GIUBARWS.js.map} +0 -0
- /package/dist/{chunk-CNMWYYSV.js.map → chunk-HZK43UCF.js.map} +0 -0
- /package/dist/{chunk-OTOWLUOJ.js.map → chunk-JULOLTZS.js.map} +0 -0
- /package/dist/{chunk-DR5WLIOU.js.map → chunk-KJKBDQ4D.js.map} +0 -0
- /package/dist/{chunk-AHDRN6NE.js.map → chunk-OO7JDFS4.js.map} +0 -0
- /package/dist/{chunk-3QH73QOQ.js.map → chunk-PED7L5GX.js.map} +0 -0
- /package/dist/{chunk-VBGUNFKY.js.map → chunk-PMI56ED5.js.map} +0 -0
- /package/dist/{chunk-W4MZXKZ2.js.map → chunk-UKMLICWS.js.map} +0 -0
- /package/dist/{chunk-3POYC5NA.js.map → chunk-XK42ZBDE.js.map} +0 -0
- /package/dist/{chunk-ZSGKRCKD.js.map → chunk-XUV6DDBF.js.map} +0 -0
- /package/dist/{chunk-LAKZUE35.js.map → chunk-Y3WD5CSW.js.map} +0 -0
- /package/dist/{chunk-3QVMIJUL.js.map → chunk-YOBA3NAM.js.map} +0 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// src/lib/pid-identity.ts
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
var GATEWAY_ENTRY_COMMAND_PATTERN = /gateway-entry\.[cm]?js/;
|
|
4
|
+
var MITMPROXY_COMMAND_PATTERN = /mitm(dump|web)/;
|
|
5
|
+
function pidAlive(pid) {
|
|
6
|
+
try {
|
|
7
|
+
process.kill(pid, 0);
|
|
8
|
+
return true;
|
|
9
|
+
} catch (e) {
|
|
10
|
+
return e.code === "EPERM";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function readCommandViaPs(pid) {
|
|
14
|
+
try {
|
|
15
|
+
return execFileSync("ps", ["-p", String(pid), "-o", "command="], {
|
|
16
|
+
encoding: "utf-8",
|
|
17
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
18
|
+
});
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function isPidLiveAndMatching(pid, commandPattern, opts = {}) {
|
|
24
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
25
|
+
const isAlive = opts.isAlive ?? pidAlive;
|
|
26
|
+
if (!isAlive(pid)) return false;
|
|
27
|
+
const readCommand = opts.readCommand ?? readCommandViaPs;
|
|
28
|
+
const command = readCommand(pid);
|
|
29
|
+
if (command === null) return true;
|
|
30
|
+
return commandPattern.test(command);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export {
|
|
34
|
+
GATEWAY_ENTRY_COMMAND_PATTERN,
|
|
35
|
+
MITMPROXY_COMMAND_PATTERN,
|
|
36
|
+
pidAlive,
|
|
37
|
+
isPidLiveAndMatching
|
|
38
|
+
};
|
|
39
|
+
//# sourceMappingURL=chunk-5EDMGK3W.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/lib/pid-identity.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * PID-file identity validation \u2014 the shared defense against PID reuse.\n *\n * PID files survive reboots and SIGKILLs, and the OS may later hand the\n * recorded PID to an unrelated process. `kill -0` alone then reports \"alive\"\n * forever: self-heal is defeated and restarts are blocked (\"already running\").\n * Before trusting a PID file, confirm the process's command line actually\n * matches what the file claims to track (same pattern as the evals-agent\n * single-instance lock).\n */\nimport { execFileSync } from 'node:child_process';\n\n/** The gateway is always spawned as `node \u2026/gateway-entry.js`. */\nexport const GATEWAY_ENTRY_COMMAND_PATTERN = /gateway-entry\\.[cm]?js/;\n/** mitmproxy runs as mitmdump or mitmweb (pipx/brew both keep the name in argv). */\nexport const MITMPROXY_COMMAND_PATTERN = /mitm(dump|web)/;\n\nexport interface PidIdentityOptions {\n /** Override the liveness check (`process.kill(pid, 0)`). Used in tests. */\n isAlive?: (pid: number) => boolean;\n /**\n * Override `ps -p <pid> -o command=`. Return the command line, or null when\n * ps itself is unavailable/failed. Used in tests.\n */\n readCommand?: (pid: number) => string | null;\n}\n\n/** `kill -0` liveness. EPERM means the process exists but is owned by another user. */\nexport function pidAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (e) {\n return (e as NodeJS.ErrnoException).code === 'EPERM';\n }\n}\n\nfunction readCommandViaPs(pid: number): string | null {\n try {\n return execFileSync('ps', ['-p', String(pid), '-o', 'command='], {\n encoding: 'utf-8',\n stdio: ['ignore', 'pipe', 'ignore'],\n });\n } catch {\n // ps unavailable (e.g. Windows) or failed \u2014 identity is unverifiable.\n return null;\n }\n}\n\n/**\n * True iff `pid` is alive AND its command line matches `commandPattern`.\n * A live-but-mismatched PID is a reused PID: callers must treat the PID file\n * as stale (reclaim/overwrite), and must never signal that process.\n * When the command line can't be read at all, falls back to liveness only\n * (conservative: a live PID stays \"held\", matching the evals-agent lock).\n */\nexport function isPidLiveAndMatching(\n pid: number,\n commandPattern: RegExp,\n opts: PidIdentityOptions = {},\n): boolean {\n if (!Number.isInteger(pid) || pid <= 0) return false;\n const isAlive = opts.isAlive ?? pidAlive;\n if (!isAlive(pid)) return false;\n const readCommand = opts.readCommand ?? readCommandViaPs;\n const command = readCommand(pid);\n if (command === null) return true;\n return commandPattern.test(command);\n}\n"],
|
|
5
|
+
"mappings": ";AAUA,SAAS,oBAAoB;AAGtB,IAAM,gCAAgC;AAEtC,IAAM,4BAA4B;AAalC,SAAS,SAAS,KAAsB;AAC7C,MAAI;AACF,YAAQ,KAAK,KAAK,CAAC;AACnB,WAAO;AAAA,EACT,SAAS,GAAG;AACV,WAAQ,EAA4B,SAAS;AAAA,EAC/C;AACF;AAEA,SAAS,iBAAiB,KAA4B;AACpD,MAAI;AACF,WAAO,aAAa,MAAM,CAAC,MAAM,OAAO,GAAG,GAAG,MAAM,UAAU,GAAG;AAAA,MAC/D,UAAU;AAAA,MACV,OAAO,CAAC,UAAU,QAAQ,QAAQ;AAAA,IACpC,CAAC;AAAA,EACH,QAAQ;AAEN,WAAO;AAAA,EACT;AACF;AASO,SAAS,qBACd,KACA,gBACA,OAA2B,CAAC,GACnB;AACT,MAAI,CAAC,OAAO,UAAU,GAAG,KAAK,OAAO,EAAG,QAAO;AAC/C,QAAM,UAAU,KAAK,WAAW;AAChC,MAAI,CAAC,QAAQ,GAAG,EAAG,QAAO;AAC1B,QAAM,cAAc,KAAK,eAAe;AACxC,QAAM,UAAU,YAAY,GAAG;AAC/B,MAAI,YAAY,KAAM,QAAO;AAC7B,SAAO,eAAe,KAAK,OAAO;AACpC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
agentRuns,
|
|
3
|
+
getDb,
|
|
4
|
+
sessions
|
|
5
|
+
} from "./chunk-EI5BHWXA.js";
|
|
1
6
|
import {
|
|
2
7
|
removeWorktree
|
|
3
8
|
} from "./chunk-Y4BJXXYV.js";
|
|
4
9
|
import {
|
|
5
10
|
PermanentJobError
|
|
6
11
|
} from "./chunk-DGHWRQXL.js";
|
|
7
|
-
import {
|
|
8
|
-
agentRuns,
|
|
9
|
-
getDb,
|
|
10
|
-
sessions
|
|
11
|
-
} from "./chunk-6EMZI4Y3.js";
|
|
12
12
|
|
|
13
13
|
// src/lib/handlers/process-run-cleanup.ts
|
|
14
14
|
import { and, eq, isNotNull } from "drizzle-orm";
|
|
@@ -96,4 +96,4 @@ async function processRunCleanup(payload, deps = {}) {
|
|
|
96
96
|
export {
|
|
97
97
|
processRunCleanup
|
|
98
98
|
};
|
|
99
|
-
//# sourceMappingURL=chunk-
|
|
99
|
+
//# sourceMappingURL=chunk-6NBS5XFS.js.map
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// src/lib/gateway-identity.ts
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import * as http from "node:http";
|
|
4
|
+
var GATEWAY_IDENTITY_PATH = "/__rulemetric_gateway__";
|
|
5
|
+
var GATEWAY_IDENTITY_SERVICE = "rulemetric-gateway";
|
|
6
|
+
var MAX_IDENTITY_BODY_BYTES = 4096;
|
|
7
|
+
function probeGatewayIdentity(port, timeoutMs = 1500) {
|
|
8
|
+
return new Promise((resolve) => {
|
|
9
|
+
let settled = false;
|
|
10
|
+
const finish = (result) => {
|
|
11
|
+
if (settled) return;
|
|
12
|
+
settled = true;
|
|
13
|
+
resolve(result);
|
|
14
|
+
};
|
|
15
|
+
const req = http.get(
|
|
16
|
+
{ host: "127.0.0.1", port, path: GATEWAY_IDENTITY_PATH, timeout: timeoutMs },
|
|
17
|
+
(res) => {
|
|
18
|
+
let body = "";
|
|
19
|
+
res.setEncoding("utf-8");
|
|
20
|
+
res.on("data", (chunk) => {
|
|
21
|
+
body += chunk;
|
|
22
|
+
if (body.length > MAX_IDENTITY_BODY_BYTES) {
|
|
23
|
+
req.destroy();
|
|
24
|
+
finish({ status: "foreign", detail: "oversized identity response" });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
res.on("end", () => {
|
|
28
|
+
if (res.statusCode === 200) {
|
|
29
|
+
try {
|
|
30
|
+
const parsed = JSON.parse(body);
|
|
31
|
+
if (parsed?.service === GATEWAY_IDENTITY_SERVICE) {
|
|
32
|
+
finish({
|
|
33
|
+
status: "ours-live",
|
|
34
|
+
version: typeof parsed.version === "string" ? parsed.version : null,
|
|
35
|
+
pid: typeof parsed.pid === "number" ? parsed.pid : null
|
|
36
|
+
});
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
finish({ status: "foreign", detail: `HTTP ${res.statusCode ?? "?"} without identity marker` });
|
|
43
|
+
});
|
|
44
|
+
res.on("error", () => finish({ status: "foreign", detail: "response stream error" }));
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
req.on("timeout", () => {
|
|
48
|
+
req.destroy();
|
|
49
|
+
finish({ status: "foreign", detail: "accepted TCP but no identity response (timeout)" });
|
|
50
|
+
});
|
|
51
|
+
req.on("error", (err) => {
|
|
52
|
+
const code = err.code;
|
|
53
|
+
if (code === "ECONNREFUSED") {
|
|
54
|
+
finish({ status: "dead" });
|
|
55
|
+
} else {
|
|
56
|
+
finish({ status: "foreign", detail: `socket error ${code ?? err.message}` });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
async function waitForGatewayIdentity(port, { timeoutMs = 3e3, intervalMs = 150 } = {}) {
|
|
62
|
+
const deadline = Date.now() + timeoutMs;
|
|
63
|
+
for (; ; ) {
|
|
64
|
+
const identity = await probeGatewayIdentity(port, Math.min(1500, timeoutMs));
|
|
65
|
+
if (identity.status !== "dead") return identity;
|
|
66
|
+
if (Date.now() >= deadline) return identity;
|
|
67
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function runLsofDefault(port) {
|
|
71
|
+
try {
|
|
72
|
+
return execFileSync("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN"], {
|
|
73
|
+
encoding: "utf-8",
|
|
74
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
75
|
+
});
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function describePortOwner(port, opts = {}) {
|
|
81
|
+
const output = (opts.runLsof ?? runLsofDefault)(port);
|
|
82
|
+
if (!output) return null;
|
|
83
|
+
const rows = output.split("\n").filter((line) => line.trim().length > 0);
|
|
84
|
+
const dataRow = rows.find((line) => !/^COMMAND\s/.test(line));
|
|
85
|
+
if (!dataRow) return null;
|
|
86
|
+
const [command, pid] = dataRow.trim().split(/\s+/);
|
|
87
|
+
if (!command) return null;
|
|
88
|
+
return pid ? `${command} (PID ${pid})` : command;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export {
|
|
92
|
+
GATEWAY_IDENTITY_PATH,
|
|
93
|
+
GATEWAY_IDENTITY_SERVICE,
|
|
94
|
+
probeGatewayIdentity,
|
|
95
|
+
waitForGatewayIdentity,
|
|
96
|
+
describePortOwner
|
|
97
|
+
};
|
|
98
|
+
//# sourceMappingURL=chunk-7LVVKQMZ.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/lib/gateway-identity.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Gateway identity handshake \u2014 distinguishes OUR gateway from a foreign\n * process squatting on :8787.\n *\n * A bare TCP-accept probe cannot tell the difference, and pinning Claude\n * Code's HTTPS_PROXY at a port owned by some other app breaks ALL of its\n * HTTPS (the worst client-side failure we have). The gateway serves a JSON\n * marker at GATEWAY_IDENTITY_PATH; this module probes it and classifies the\n * port as ours-live / foreign / dead. HTTPS_PROXY may only be pinned on\n * ours-live.\n *\n * Kept free of oclif so gateway-entry (a standalone Node process) can import it.\n */\nimport { execFileSync } from 'node:child_process';\nimport * as http from 'node:http';\n\nexport const GATEWAY_IDENTITY_PATH = '/__rulemetric_gateway__';\nexport const GATEWAY_IDENTITY_SERVICE = 'rulemetric-gateway';\n// The probe reads at most this much of a response \u2014 a foreign server could\n// stream forever on an unknown path.\nconst MAX_IDENTITY_BODY_BYTES = 4096;\n\nexport type GatewayIdentity =\n /** The port answered with our JSON marker \u2014 safe to pin HTTPS_PROXY. */\n | { status: 'ours-live'; version: string | null; pid: number | null }\n /** Something accepts connections but is NOT our gateway \u2014 never pin. */\n | { status: 'foreign'; detail: string }\n /** Nothing is listening \u2014 safe to spawn a gateway, unsafe to pin. */\n | { status: 'dead' };\n\n/**\n * One-shot identity probe against 127.0.0.1:<port>. Connects directly\n * (node:http ignores HTTPS_PROXY env), requests the identity path, and\n * verifies the marker. Anything that accepts TCP but fails the handshake \u2014\n * wrong body, non-JSON, hang, mid-request reset \u2014 is classified foreign:\n * the conservative direction, since foreign means \"do not pin\".\n */\nexport function probeGatewayIdentity(port: number, timeoutMs = 1500): Promise<GatewayIdentity> {\n return new Promise((resolve) => {\n let settled = false;\n const finish = (result: GatewayIdentity) => {\n if (settled) return;\n settled = true;\n resolve(result);\n };\n\n const req = http.get(\n { host: '127.0.0.1', port, path: GATEWAY_IDENTITY_PATH, timeout: timeoutMs },\n (res) => {\n let body = '';\n res.setEncoding('utf-8');\n res.on('data', (chunk: string) => {\n body += chunk;\n if (body.length > MAX_IDENTITY_BODY_BYTES) {\n req.destroy();\n finish({ status: 'foreign', detail: 'oversized identity response' });\n }\n });\n res.on('end', () => {\n if (res.statusCode === 200) {\n try {\n const parsed = JSON.parse(body) as { service?: unknown; version?: unknown; pid?: unknown };\n if (parsed?.service === GATEWAY_IDENTITY_SERVICE) {\n finish({\n status: 'ours-live',\n version: typeof parsed.version === 'string' ? parsed.version : null,\n pid: typeof parsed.pid === 'number' ? parsed.pid : null,\n });\n return;\n }\n } catch {\n /* not our JSON \u2014 fall through to foreign */\n }\n }\n finish({ status: 'foreign', detail: `HTTP ${res.statusCode ?? '?'} without identity marker` });\n });\n res.on('error', () => finish({ status: 'foreign', detail: 'response stream error' }));\n },\n );\n\n req.on('timeout', () => {\n // The connection was accepted but nothing answered the handshake in\n // time \u2014 on loopback that is a listener that isn't our gateway.\n req.destroy();\n finish({ status: 'foreign', detail: 'accepted TCP but no identity response (timeout)' });\n });\n req.on('error', (err) => {\n const code = (err as NodeJS.ErrnoException).code;\n if (code === 'ECONNREFUSED') {\n finish({ status: 'dead' });\n } else {\n // Accepted-then-reset (e.g. a TLS-only listener) and anything else\n // that isn't a clean refusal means SOMETHING owns the port.\n finish({ status: 'foreign', detail: `socket error ${code ?? err.message}` });\n }\n });\n });\n}\n\n/**\n * Poll probeGatewayIdentity() until it resolves to a terminal state or the\n * window elapses. 'dead' is the only retryable state (a just-spawned gateway\n * needs a moment to bind); 'ours-live' and 'foreign' can't change by waiting.\n */\nexport async function waitForGatewayIdentity(\n port: number,\n { timeoutMs = 3000, intervalMs = 150 }: { timeoutMs?: number; intervalMs?: number } = {},\n): Promise<GatewayIdentity> {\n const deadline = Date.now() + timeoutMs;\n for (;;) {\n const identity = await probeGatewayIdentity(port, Math.min(1500, timeoutMs));\n if (identity.status !== 'dead') return identity;\n if (Date.now() >= deadline) return identity;\n await new Promise((resolve) => setTimeout(resolve, intervalMs));\n }\n}\n\nexport interface DescribePortOwnerOptions {\n /** Override the `lsof` exec. Return raw lsof output, or null on failure. Used in tests. */\n runLsof?: (port: number) => string | null;\n}\n\nfunction runLsofDefault(port: number): string | null {\n try {\n return execFileSync('lsof', ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN'], {\n encoding: 'utf-8',\n stdio: ['ignore', 'pipe', 'ignore'],\n });\n } catch {\n // lsof missing (some Linuxes) or no permission \u2014 the caller degrades to a\n // generic \"something else holds the port\" message.\n return null;\n }\n}\n\n/**\n * Best-effort \"what is holding this port\" description for user-facing\n * messages, e.g. `dev-server (PID 4242)`. Returns null when it can't tell.\n */\nexport function describePortOwner(port: number, opts: DescribePortOwnerOptions = {}): string | null {\n const output = (opts.runLsof ?? runLsofDefault)(port);\n if (!output) return null;\n // lsof output: header line, then `COMMAND PID USER ...` rows.\n const rows = output.split('\\n').filter((line) => line.trim().length > 0);\n const dataRow = rows.find((line) => !/^COMMAND\\s/.test(line));\n if (!dataRow) return null;\n const [command, pid] = dataRow.trim().split(/\\s+/);\n if (!command) return null;\n return pid ? `${command} (PID ${pid})` : command;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MitmproxyRespawnMonitor
|
|
3
|
+
} from "./chunk-ILYDELUE.js";
|
|
4
|
+
import {
|
|
5
|
+
GATEWAY_IDENTITY_PATH,
|
|
6
|
+
GATEWAY_IDENTITY_SERVICE
|
|
7
|
+
} from "./chunk-7LVVKQMZ.js";
|
|
8
|
+
|
|
1
9
|
// src/lib/gateway.ts
|
|
2
10
|
import { spawn } from "node:child_process";
|
|
3
11
|
import * as fs from "node:fs";
|
|
@@ -11,9 +19,12 @@ var HEALTH_INTERVAL = 3e3;
|
|
|
11
19
|
var HEALTH_TIMEOUT = 500;
|
|
12
20
|
var RESTART_COOLDOWN_MS = 6e4;
|
|
13
21
|
var VERSION_FILE = path.join(os.homedir(), ".config", "rulemetric", "proxy-addon.version");
|
|
22
|
+
var PROXY_PID_FILE = path.join(os.homedir(), ".config", "rulemetric", "proxy.pid");
|
|
14
23
|
var isProxyAlive = false;
|
|
15
24
|
var lastSeenAddonVersion = null;
|
|
16
25
|
var lastRestartAt = 0;
|
|
26
|
+
var identityVersion = "unknown";
|
|
27
|
+
var respawnMonitor = null;
|
|
17
28
|
function log(msg) {
|
|
18
29
|
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
19
30
|
console.log(`[${ts}] ${msg}`);
|
|
@@ -46,14 +57,31 @@ function maybeRestartOnDrift(gatewayCliVersion, cliEntry) {
|
|
|
46
57
|
`addon version drift detected (gateway=${gatewayCliVersion}, addon=${addonVersion}). Restarting proxy to pick up latest code.`
|
|
47
58
|
);
|
|
48
59
|
lastRestartAt = Date.now();
|
|
60
|
+
spawnProxyRestart(cliEntry);
|
|
61
|
+
}
|
|
62
|
+
function spawnProxyRestart(cliEntry) {
|
|
63
|
+
if (!cliEntry) {
|
|
64
|
+
log("cannot restart proxy: no CLI entry recorded (RULEMETRIC_CLI_ENTRY unset)");
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
49
67
|
try {
|
|
50
68
|
const child = spawn(process.execPath, [cliEntry, "proxy", "restart"], {
|
|
51
69
|
detached: true,
|
|
52
70
|
stdio: "ignore"
|
|
53
71
|
});
|
|
54
72
|
child.unref();
|
|
73
|
+
return true;
|
|
55
74
|
} catch (err) {
|
|
56
75
|
log(`restart spawn failed: ${err.message}`);
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function proxyCrashEvidence() {
|
|
80
|
+
try {
|
|
81
|
+
const content = fs.readFileSync(PROXY_PID_FILE, "utf-8").trim();
|
|
82
|
+
return /^\d+$/.test(content);
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
57
85
|
}
|
|
58
86
|
}
|
|
59
87
|
function checkProxyHealth(mitmproxyPort) {
|
|
@@ -64,12 +92,14 @@ function checkProxyHealth(mitmproxyPort) {
|
|
|
64
92
|
isProxyAlive = true;
|
|
65
93
|
log(`mitmproxy up on :${mitmproxyPort}, routing through proxy`);
|
|
66
94
|
}
|
|
95
|
+
respawnMonitor?.onHealthCheck(true);
|
|
67
96
|
});
|
|
68
97
|
sock.on("error", () => {
|
|
69
98
|
if (isProxyAlive) {
|
|
70
99
|
isProxyAlive = false;
|
|
71
100
|
log(`mitmproxy down on :${mitmproxyPort}, routing direct`);
|
|
72
101
|
}
|
|
102
|
+
respawnMonitor?.onHealthCheck(false);
|
|
73
103
|
});
|
|
74
104
|
sock.on("timeout", () => {
|
|
75
105
|
sock.destroy();
|
|
@@ -77,6 +107,7 @@ function checkProxyHealth(mitmproxyPort) {
|
|
|
77
107
|
isProxyAlive = false;
|
|
78
108
|
log(`mitmproxy timeout on :${mitmproxyPort}, routing direct`);
|
|
79
109
|
}
|
|
110
|
+
respawnMonitor?.onHealthCheck(false);
|
|
80
111
|
});
|
|
81
112
|
}
|
|
82
113
|
function handleConnect(req, clientSocket, head, mitmproxyPort) {
|
|
@@ -133,6 +164,16 @@ function connectDirect(req, clientSocket, head, targetHost, targetPort) {
|
|
|
133
164
|
});
|
|
134
165
|
}
|
|
135
166
|
function handleRequest(req, res, mitmproxyPort) {
|
|
167
|
+
if (req.url === GATEWAY_IDENTITY_PATH) {
|
|
168
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
169
|
+
res.end(JSON.stringify({
|
|
170
|
+
service: GATEWAY_IDENTITY_SERVICE,
|
|
171
|
+
version: identityVersion,
|
|
172
|
+
pid: process.pid,
|
|
173
|
+
mitmproxyAlive: isProxyAlive
|
|
174
|
+
}));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
136
177
|
const targetUrl = req.url ?? "";
|
|
137
178
|
let parsedUrl;
|
|
138
179
|
try {
|
|
@@ -163,6 +204,12 @@ function handleRequest(req, res, mitmproxyPort) {
|
|
|
163
204
|
function startGateway(port = GATEWAY_PORT, mitmproxyPort = MITMPROXY_PORT, options = {}) {
|
|
164
205
|
const cliVersion = options.cliVersion ?? process.env.RULEMETRIC_CLI_VERSION ?? "unknown";
|
|
165
206
|
const cliEntry = options.cliEntry ?? process.env.RULEMETRIC_CLI_ENTRY ?? null;
|
|
207
|
+
identityVersion = cliVersion;
|
|
208
|
+
respawnMonitor = new MitmproxyRespawnMonitor({
|
|
209
|
+
hasCrashEvidence: proxyCrashEvidence,
|
|
210
|
+
respawn: () => spawnProxyRestart(cliEntry),
|
|
211
|
+
log
|
|
212
|
+
});
|
|
166
213
|
const server = http.createServer((req, res) => {
|
|
167
214
|
handleRequest(req, res, mitmproxyPort);
|
|
168
215
|
});
|
|
@@ -193,4 +240,4 @@ export {
|
|
|
193
240
|
MITMPROXY_PORT,
|
|
194
241
|
startGateway
|
|
195
242
|
};
|
|
196
|
-
//# sourceMappingURL=chunk-
|
|
243
|
+
//# sourceMappingURL=chunk-7NNFYWKK.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/lib/gateway.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Resilient HTTP proxy gateway that always accepts connections on GATEWAY_PORT.\n * Routes through mitmproxy when it's alive, direct to upstream when it's not.\n */\nimport { spawn } from 'node:child_process';\nimport * as fs from 'node:fs';\nimport * as http from 'node:http';\nimport * as net from 'node:net';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\nimport { GATEWAY_IDENTITY_PATH, GATEWAY_IDENTITY_SERVICE } from './gateway-identity.js';\nimport { MitmproxyRespawnMonitor } from './proxy-respawn.js';\n\nexport const GATEWAY_PORT = 8787;\nexport const MITMPROXY_PORT = 8788;\nconst HEALTH_INTERVAL = 3000;\nconst HEALTH_TIMEOUT = 500;\n// Floor on auto-restarts so a misconfigured drift can't trigger a restart\n// loop. Plenty long for the user to fix any underlying problem manually.\nconst RESTART_COOLDOWN_MS = 60_000;\nconst VERSION_FILE = path.join(os.homedir(), '.config', 'rulemetric', 'proxy-addon.version');\n// Crash evidence for the mitmproxy auto-restart: `proxy stop` removes this\n// file, a crash leaves it behind.\nconst PROXY_PID_FILE = path.join(os.homedir(), '.config', 'rulemetric', 'proxy.pid');\n\nlet isProxyAlive = false;\nlet lastState: boolean | null = null;\n// Drift state \u2014 tracked across health-check ticks. lastSeenAddonVersion is\n// what the file said last time we read it; lastRestartAt prevents loops.\nlet lastSeenAddonVersion: string | null = null;\nlet lastRestartAt = 0;\n// Set by startGateway; served on the identity endpoint so callers can verify\n// the thing on :8787 is actually us (and which build).\nlet identityVersion = 'unknown';\n// Mid-session mitmproxy crash recovery \u2014 constructed in startGateway.\nlet respawnMonitor: MitmproxyRespawnMonitor | null = null;\n\nfunction log(msg: string): void {\n const ts = new Date().toISOString();\n console.log(`[${ts}] ${msg}`);\n}\n\nfunction readAddonVersion(): string | null {\n try {\n return fs.readFileSync(VERSION_FILE, 'utf-8').trim() || null;\n } catch {\n // File may not exist yet (proxy not started, or older addon that doesn't\n // write the file). Returning null disables drift checks for this tick.\n return null;\n }\n}\n\nfunction maybeRestartOnDrift(gatewayCliVersion: string, cliEntry: string | null): void {\n // No reference version means we can't compare \u2014 skip.\n if (gatewayCliVersion === 'unknown' || !cliEntry) return;\n const addonVersion = readAddonVersion();\n if (!addonVersion || addonVersion === 'unknown') return;\n // Same as last tick \u2014 nothing changed, no need to log or act.\n if (addonVersion === lastSeenAddonVersion) return;\n lastSeenAddonVersion = addonVersion;\n\n if (addonVersion === gatewayCliVersion) {\n // Drift cleared (e.g. proxy was just restarted). Quiet log, no action.\n log(`addon version matches gateway (${gatewayCliVersion})`);\n return;\n }\n\n const sinceLast = Date.now() - lastRestartAt;\n if (sinceLast < RESTART_COOLDOWN_MS) {\n log(\n `addon version drift (gateway=${gatewayCliVersion}, addon=${addonVersion}); ` +\n `restart cooldown active (${Math.round((RESTART_COOLDOWN_MS - sinceLast) / 1000)}s left)`,\n );\n return;\n }\n\n log(\n `addon version drift detected (gateway=${gatewayCliVersion}, addon=${addonVersion}). ` +\n `Restarting proxy to pick up latest code.`,\n );\n lastRestartAt = Date.now();\n spawnProxyRestart(cliEntry);\n}\n\n/**\n * Fire one detached `rulemetric proxy restart` via the CLI entry the spawning\n * process recorded. Shared by the version-drift restart and the mid-session\n * crash auto-restart. Output goes to /dev/null; the proxy.log captures the\n * restart events from mitmproxy itself.\n */\nfunction spawnProxyRestart(cliEntry: string | null): boolean {\n if (!cliEntry) {\n log('cannot restart proxy: no CLI entry recorded (RULEMETRIC_CLI_ENTRY unset)');\n return false;\n }\n try {\n const child = spawn(process.execPath, [cliEntry, 'proxy', 'restart'], {\n detached: true,\n stdio: 'ignore',\n });\n child.unref();\n return true;\n } catch (err) {\n log(`restart spawn failed: ${(err as Error).message}`);\n return false;\n }\n}\n\n/**\n * True when the local proxy.pid file says a local-mode mitmproxy SHOULD be\n * running (numeric PID). `proxy stop` deletes the file, so its absence means\n * a deliberate stop \u2014 never auto-restart over that. A `docker:` marker means\n * Docker owns the lifecycle.\n */\nfunction proxyCrashEvidence(): boolean {\n try {\n const content = fs.readFileSync(PROXY_PID_FILE, 'utf-8').trim();\n return /^\\d+$/.test(content);\n } catch {\n return false;\n }\n}\n\nfunction checkProxyHealth(mitmproxyPort: number): void {\n const sock = net.connect({ port: mitmproxyPort, timeout: HEALTH_TIMEOUT });\n sock.on('connect', () => {\n sock.destroy();\n if (!isProxyAlive) {\n isProxyAlive = true;\n log(`mitmproxy up on :${mitmproxyPort}, routing through proxy`);\n }\n respawnMonitor?.onHealthCheck(true);\n });\n sock.on('error', () => {\n if (isProxyAlive) {\n isProxyAlive = false;\n log(`mitmproxy down on :${mitmproxyPort}, routing direct`);\n }\n respawnMonitor?.onHealthCheck(false);\n });\n sock.on('timeout', () => {\n sock.destroy();\n if (isProxyAlive) {\n isProxyAlive = false;\n log(`mitmproxy timeout on :${mitmproxyPort}, routing direct`);\n }\n respawnMonitor?.onHealthCheck(false);\n });\n}\n\nfunction handleConnect(\n req: http.IncomingMessage,\n clientSocket: net.Socket,\n head: Buffer,\n mitmproxyPort: number,\n): void {\n const [targetHost, targetPortStr] = (req.url ?? '').split(':');\n const targetPort = parseInt(targetPortStr, 10) || 443;\n\n if (isProxyAlive) {\n // Forward CONNECT to mitmproxy\n const proxySocket = net.connect({ port: mitmproxyPort, host: '127.0.0.1' });\n\n proxySocket.on('connect', () => {\n // Send the CONNECT request to mitmproxy\n proxySocket.write(\n `CONNECT ${req.url} HTTP/1.1\\r\\nHost: ${req.url}\\r\\n\\r\\n`,\n );\n });\n\n // Wait for mitmproxy's 200 response before piping\n let responseBuffer = Buffer.alloc(0);\n const onData = (chunk: Buffer) => {\n responseBuffer = Buffer.concat([responseBuffer, chunk]);\n const responseStr = responseBuffer.toString();\n if (responseStr.includes('\\r\\n\\r\\n')) {\n proxySocket.removeListener('data', onData);\n // Forward the 200 to client\n clientSocket.write('HTTP/1.1 200 Connection Established\\r\\n\\r\\n');\n // Pipe remaining data and head\n if (head.length > 0) proxySocket.write(head);\n clientSocket.pipe(proxySocket);\n proxySocket.pipe(clientSocket);\n }\n };\n proxySocket.on('data', onData);\n\n proxySocket.on('error', (err) => {\n log(`proxy socket error for ${req.url}: ${err.message}, falling back to direct`);\n // Mitmproxy failed mid-connect, fall back to direct\n connectDirect(req, clientSocket, head, targetHost, targetPort);\n });\n\n clientSocket.on('error', () => {\n proxySocket.destroy();\n });\n } else {\n connectDirect(req, clientSocket, head, targetHost, targetPort);\n }\n}\n\nfunction connectDirect(\n req: http.IncomingMessage,\n clientSocket: net.Socket,\n head: Buffer,\n targetHost: string,\n targetPort: number,\n): void {\n const upstream = net.connect({ port: targetPort, host: targetHost });\n\n upstream.on('connect', () => {\n clientSocket.write('HTTP/1.1 200 Connection Established\\r\\n\\r\\n');\n if (head.length > 0) upstream.write(head);\n clientSocket.pipe(upstream);\n upstream.pipe(clientSocket);\n });\n\n upstream.on('error', (err) => {\n log(`upstream error for ${req.url}: ${err.message}`);\n clientSocket.end('HTTP/1.1 502 Bad Gateway\\r\\n\\r\\n');\n });\n\n clientSocket.on('error', () => {\n upstream.destroy();\n });\n}\n\nfunction handleRequest(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n mitmproxyPort: number,\n): void {\n // Identity handshake (origin-form GET, not a proxy request): lets callers\n // distinguish OUR gateway from a foreign process squatting on the port \u2014\n // the gate for pinning HTTPS_PROXY and for idempotent double-spawns.\n if (req.url === GATEWAY_IDENTITY_PATH) {\n res.writeHead(200, { 'content-type': 'application/json' });\n res.end(JSON.stringify({\n service: GATEWAY_IDENTITY_SERVICE,\n version: identityVersion,\n pid: process.pid,\n mitmproxyAlive: isProxyAlive,\n }));\n return;\n }\n\n // Plain HTTP proxy (non-CONNECT)\n const targetUrl = req.url ?? '';\n let parsedUrl: URL;\n try {\n parsedUrl = new URL(targetUrl);\n } catch {\n res.writeHead(400);\n res.end('Bad Request');\n return;\n }\n\n const options: http.RequestOptions = {\n hostname: isProxyAlive ? '127.0.0.1' : parsedUrl.hostname,\n port: isProxyAlive ? mitmproxyPort : (parseInt(parsedUrl.port, 10) || 80),\n path: isProxyAlive ? targetUrl : parsedUrl.pathname + parsedUrl.search,\n method: req.method,\n headers: req.headers,\n };\n\n const proxyReq = http.request(options, (proxyRes) => {\n res.writeHead(proxyRes.statusCode ?? 502, proxyRes.headers);\n proxyRes.pipe(res);\n });\n\n proxyReq.on('error', (err) => {\n log(`HTTP proxy error for ${targetUrl}: ${err.message}`);\n res.writeHead(502);\n res.end('Bad Gateway');\n });\n\n req.pipe(proxyReq);\n}\n\nexport function startGateway(\n port: number = GATEWAY_PORT,\n mitmproxyPort: number = MITMPROXY_PORT,\n options: { cliVersion?: string; cliEntry?: string } = {},\n): { server: http.Server; healthInterval: NodeJS.Timeout } {\n const cliVersion = options.cliVersion ?? process.env.RULEMETRIC_CLI_VERSION ?? 'unknown';\n const cliEntry = options.cliEntry ?? process.env.RULEMETRIC_CLI_ENTRY ?? null;\n identityVersion = cliVersion;\n // Mid-session crash recovery: when the health check sees mitmproxy stay\n // dead, attempt a bounded, crash-evidence-gated `proxy restart` (see\n // proxy-respawn.ts for the guard rails).\n respawnMonitor = new MitmproxyRespawnMonitor({\n hasCrashEvidence: proxyCrashEvidence,\n respawn: () => spawnProxyRestart(cliEntry),\n log,\n });\n const server = http.createServer((req, res) => {\n handleRequest(req, res, mitmproxyPort);\n });\n\n server.on('connect', (req, clientSocket, head) => {\n handleConnect(req, clientSocket as net.Socket, head as Buffer, mitmproxyPort);\n });\n\n // Don't crash on client socket errors\n server.on('clientError', (_err, socket) => {\n if (socket.writable) {\n socket.end('HTTP/1.1 400 Bad Request\\r\\n\\r\\n');\n }\n });\n\n const healthInterval = setInterval(() => {\n checkProxyHealth(mitmproxyPort);\n maybeRestartOnDrift(cliVersion, cliEntry);\n }, HEALTH_INTERVAL);\n\n // Do an immediate check\n checkProxyHealth(mitmproxyPort);\n maybeRestartOnDrift(cliVersion, cliEntry);\n\n server.listen(port, '127.0.0.1', () => {\n log(\n `gateway listening on 127.0.0.1:${port} (mitmproxy target: :${mitmproxyPort}, ` +\n `cli version: ${cliVersion})`,\n );\n });\n\n return { server, healthInterval };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,44 +1,44 @@
|
|
|
1
1
|
import {
|
|
2
2
|
startTerminalTargetHeartbeat
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-GIUBARWS.js";
|
|
4
|
+
import {
|
|
5
|
+
processSessionGoal
|
|
6
|
+
} from "./chunk-YOBA3NAM.js";
|
|
7
|
+
import {
|
|
8
|
+
processAnnouncement
|
|
9
|
+
} from "./chunk-OO7JDFS4.js";
|
|
10
|
+
import {
|
|
11
|
+
processConversation
|
|
12
|
+
} from "./chunk-XK42ZBDE.js";
|
|
4
13
|
import {
|
|
5
14
|
processEval
|
|
6
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-3LKVS6W5.js";
|
|
7
16
|
import {
|
|
8
17
|
processInsights
|
|
9
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-4YAP7RT7.js";
|
|
10
19
|
import {
|
|
11
20
|
processLaunch
|
|
12
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-JULOLTZS.js";
|
|
13
22
|
import {
|
|
14
23
|
processRunCleanup
|
|
15
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-6NBS5XFS.js";
|
|
16
25
|
import {
|
|
17
26
|
processSendMessage
|
|
18
27
|
} from "./chunk-W53GKIZQ.js";
|
|
19
28
|
import {
|
|
20
|
-
|
|
21
|
-
} from "./chunk-
|
|
29
|
+
isPermanentJobError
|
|
30
|
+
} from "./chunk-DGHWRQXL.js";
|
|
22
31
|
import {
|
|
23
32
|
cronRefreshSkills
|
|
24
33
|
} from "./chunk-RQ2TMLKG.js";
|
|
25
34
|
import {
|
|
26
35
|
cronSuggestInstructions
|
|
27
|
-
} from "./chunk-
|
|
28
|
-
import {
|
|
29
|
-
processAnnouncement
|
|
30
|
-
} from "./chunk-AHDRN6NE.js";
|
|
31
|
-
import {
|
|
32
|
-
isPermanentJobError
|
|
33
|
-
} from "./chunk-DGHWRQXL.js";
|
|
34
|
-
import {
|
|
35
|
-
processConversation
|
|
36
|
-
} from "./chunk-3POYC5NA.js";
|
|
36
|
+
} from "./chunk-PMI56ED5.js";
|
|
37
37
|
import {
|
|
38
38
|
ApiError,
|
|
39
39
|
apiGet,
|
|
40
40
|
apiPost
|
|
41
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-ZUJBM6HE.js";
|
|
42
42
|
|
|
43
43
|
// src/lib/agent-loop.ts
|
|
44
44
|
var HANDLERS = {
|
|
@@ -65,6 +65,15 @@ var HANDLERS = {
|
|
|
65
65
|
cron_refresh_skills: cronRefreshSkills
|
|
66
66
|
};
|
|
67
67
|
var CAPABILITIES = Object.keys(HANDLERS);
|
|
68
|
+
var DB_REQUIRING_KINDS = /* @__PURE__ */ new Set([
|
|
69
|
+
"process_session_goal",
|
|
70
|
+
"process_announcement",
|
|
71
|
+
"process_run_cleanup"
|
|
72
|
+
]);
|
|
73
|
+
function availableCapabilities(env = process.env) {
|
|
74
|
+
if (env.DATABASE_URL) return CAPABILITIES;
|
|
75
|
+
return CAPABILITIES.filter((kind) => !DB_REQUIRING_KINDS.has(kind));
|
|
76
|
+
}
|
|
68
77
|
var DEFAULT_POLL_WAIT_MS = 3e4;
|
|
69
78
|
var DEFAULT_HEARTBEAT_MS = 3e4;
|
|
70
79
|
var PRESENCE_HEARTBEAT_MS = 3e4;
|
|
@@ -217,7 +226,7 @@ async function runAgentLoop(opts) {
|
|
|
217
226
|
const log = opts.log ?? (() => void 0);
|
|
218
227
|
const pollWaitMs = opts.pollWaitMs ?? DEFAULT_POLL_WAIT_MS;
|
|
219
228
|
const heartbeatIntervalMs = opts.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_MS;
|
|
220
|
-
const capabilities = opts.capabilities ??
|
|
229
|
+
const capabilities = opts.capabilities ?? availableCapabilities();
|
|
221
230
|
let backoffMs = BACKOFF_INITIAL_MS;
|
|
222
231
|
structuredLog(log, {
|
|
223
232
|
event: "agent_start",
|
|
@@ -232,7 +241,26 @@ async function runAgentLoop(opts) {
|
|
|
232
241
|
error: err instanceof Error ? err.message : String(err)
|
|
233
242
|
});
|
|
234
243
|
});
|
|
244
|
+
const VERSION_CHECK_INTERVAL_MS = 5 * 6e4;
|
|
245
|
+
let lastVersionCheck = 0;
|
|
246
|
+
const versionDrifted = () => {
|
|
247
|
+
if (!opts.readInstalledVersion || !opts.version) return false;
|
|
248
|
+
const now = Date.now();
|
|
249
|
+
if (now - lastVersionCheck < VERSION_CHECK_INTERVAL_MS) return false;
|
|
250
|
+
lastVersionCheck = now;
|
|
251
|
+
const onDisk = opts.readInstalledVersion();
|
|
252
|
+
return Boolean(onDisk && onDisk !== opts.version);
|
|
253
|
+
};
|
|
235
254
|
while (!opts.signal.aborted) {
|
|
255
|
+
if (versionDrifted()) {
|
|
256
|
+
structuredLog(log, {
|
|
257
|
+
event: "agent_version_drift",
|
|
258
|
+
bootVersion: opts.version,
|
|
259
|
+
installedVersion: opts.readInstalledVersion?.() ?? null
|
|
260
|
+
});
|
|
261
|
+
opts.onVersionDrift?.();
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
236
264
|
let job = null;
|
|
237
265
|
try {
|
|
238
266
|
job = await pollForWork(capabilities, pollWaitMs);
|
|
@@ -264,7 +292,8 @@ async function runAgentLoop(opts) {
|
|
|
264
292
|
export {
|
|
265
293
|
HANDLERS,
|
|
266
294
|
CAPABILITIES,
|
|
295
|
+
availableCapabilities,
|
|
267
296
|
pollForWork,
|
|
268
297
|
runAgentLoop
|
|
269
298
|
};
|
|
270
|
-
//# sourceMappingURL=chunk-
|
|
299
|
+
//# sourceMappingURL=chunk-7UDETV3P.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/lib/agent-loop.ts"],
|
|
4
|
+
"sourcesContent": ["// Phase 4b thin-agent migration: the parallel-run worker loop that replaces\n// graphile-worker's LISTEN/NOTIFY pickup with HTTP long-polling against the\n// Phase 4a /api/work endpoints. See docs/plans/2026-05-25-thin-agent-migration.md.\n//\n// This file is invoked exclusively by `rulemetric evals agent` \u2014 the only\n// worker mode since Phase 4c deleted the graphile path.\n//\n// Lifecycle for one cycle:\n// 1. Long-poll GET /api/work/next?capabilities=...&waitMs=...\n// 2. If 204 (returned as \"\" by api-client because no JSON body), increment\n// a small backoff capped at 5s and re-poll.\n// 3. If 200, dispatch by `kind` to the matching handler from HANDLERS.\n// 4. Start a heartbeat timer that POSTs /api/work/:id/heartbeat at\n// heartbeatIntervalMs while the handler runs.\n// 5. On success: POST /api/work/:id/complete with { result, leaseToken }.\n// On PermanentJobError: POST /fail with terminal=true (server will not\n// requeue regardless of attempts). On any other error: POST /fail with\n// terminal=false (server requeues if attempts < max).\n//\n// Abort behavior: an AbortSignal short-circuits the next long-poll and any\n// backoff sleep. An in-flight handler can't be cancelled (it has already\n// spawned `claude` subprocesses), so we await it to natural completion and\n// then exit. This matches listen.ts's 10s shutdown grace pattern, except we\n// don't need an outer hard-kill timer here \u2014 the caller (the oclif command)\n// owns the process.exit.\n\nimport type { JobHelpers } from './job-helpers.js';\nimport { apiGet, apiPost, ApiError } from './api-client.js';\nimport { isPermanentJobError } from './permanent-job-error.js';\nimport { processEval } from './handlers/process-eval.js';\nimport { processInsights } from './handlers/process-insights.js';\nimport { processLaunch } from './handlers/process-launch.js';\nimport { processConversation } from './handlers/process-conversation.js';\nimport { processSendMessage } from './handlers/process-send-message.js';\nimport { processSessionGoal } from './handlers/process-session-goal.js';\nimport { processAnnouncement } from './handlers/process-announcement.js';\nimport { processRunCleanup } from './handlers/process-run-cleanup.js';\nimport { cronSuggestInstructions } from './handlers/cron-suggest-instructions.js';\nimport { cronRefreshSkills } from './handlers/cron-refresh-skills.js';\nimport { startTerminalTargetHeartbeat } from './terminal-target-heartbeat.js';\n\n// Handler dispatch table. Keys MUST match the API's CAPABILITY_TO_TABLE keys\n// in apps/api/src/routes/work.ts; mismatches would cause the API to silently\n// drop the capability and starve this kind of work.\ntype Handler = (\n payload: Record<string, unknown>,\n helpers: JobHelpers,\n) => Promise<Record<string, unknown> | undefined | void>;\n\nexport const HANDLERS: Record<string, Handler> = {\n process_eval: processEval as unknown as Handler,\n process_insights: processInsights as unknown as Handler,\n process_launch: processLaunch as unknown as Handler,\n process_send_message: processSendMessage as unknown as Handler,\n process_conversation: processConversation as unknown as Handler,\n // agent_jobs kinds (Phase 4c Stage 3b, migration 00083). processSessionGoal\n // and processAnnouncement take an optional deps object in the helpers\n // position \u2014 every key is optional, so JobHelpers passing through is\n // harmless and they fall back to their defaults.\n process_session_goal: processSessionGoal as unknown as Handler,\n process_announcement: processAnnouncement as unknown as Handler,\n // Multi-session orchestration (\"runs\") worktree cleanup (Task 5.2). agent_jobs\n // kind \u2014 no mirror table. Enqueued by POST /api/runs/:id/cleanup; prunes the\n // worktrees of a run's ended sessions, guarding against uncommitted work.\n process_run_cleanup: processRunCleanup as unknown as Handler,\n cron_suggest_instructions: cronSuggestInstructions as unknown as Handler,\n // Dropped by accident in the Phase 4c graphile\u2192agent_jobs migration: the\n // handler survived but lost its registration + schedule, so the skills\n // registry froze (last refresh 2026-05-21) and the suggest cron marked\n // every suggestion stale on each run. Re-scheduled by migration 00086.\n cron_refresh_skills: cronRefreshSkills as unknown as Handler,\n};\n\nexport const CAPABILITIES: readonly string[] = Object.keys(HANDLERS);\n\n// Kinds whose handlers open a DIRECT Postgres connection (getDb \u2192 requires\n// DATABASE_URL): process-session-goal, process-announcement, process-run-cleanup.\n// A worker-only npm install (the onboarding default) has API auth but no DB\n// creds \u2014 if it advertises these kinds it claims jobs it can only throw on, and\n// process_session_goal has MAX_ATTEMPTS=1, so a single claim PERMANENTLY fails\n// the job (goalSummary dead for every npm user). Advertise them only when a\n// direct DB connection is actually configured; otherwise the rows stay pending\n// for a repo-checkout worker instead of being burned.\nconst DB_REQUIRING_KINDS: ReadonlySet<string> = new Set([\n 'process_session_goal',\n 'process_announcement',\n 'process_run_cleanup',\n]);\n\n/** The capability list this process can actually serve (see DB_REQUIRING_KINDS). */\nexport function availableCapabilities(\n env: NodeJS.ProcessEnv = process.env,\n): readonly string[] {\n if (env.DATABASE_URL) return CAPABILITIES;\n return CAPABILITIES.filter((kind) => !DB_REQUIRING_KINDS.has(kind));\n}\n\ninterface NextWorkResponse {\n id: string;\n kind: string;\n payload: Record<string, unknown>;\n attempts: number;\n leaseToken: string;\n leaseDurationSeconds: number;\n table: string;\n}\n\nexport interface RunAgentLoopOptions {\n signal: AbortSignal;\n log?: (msg: string) => void;\n /** Long-poll wait passed to GET /api/work/next?waitMs= */\n pollWaitMs?: number;\n /** How often to POST /api/work/:id/heartbeat while a handler runs */\n heartbeatIntervalMs?: number;\n /** Capability list to advertise. Defaults to all known handlers. */\n capabilities?: readonly string[];\n /** Running CLI version, reported as `clientVersion` in the presence heartbeat\n * so the dashboard can detect an outdated CLI and nudge an upgrade. */\n version?: string;\n /** Reads the CURRENTLY INSTALLED CLI version from disk (vs `version`, which\n * is frozen at boot). When they diverge \u2014 `npm i -g` upgraded the CLI under\n * a running worker \u2014 the loop stops between jobs and calls `onVersionDrift`\n * so the supervisor can respawn onto the new code. */\n readInstalledVersion?: () => string | null;\n /** Invoked (idle, never mid-job) when version drift is detected. */\n onVersionDrift?: () => void;\n}\n\nconst DEFAULT_POLL_WAIT_MS = 30_000;\nconst DEFAULT_HEARTBEAT_MS = 30_000;\nconst PRESENCE_HEARTBEAT_MS = 30_000;\nconst BACKOFF_INITIAL_MS = 250;\nconst BACKOFF_MAX_MS = 5_000;\n\n// Stable per-process identifier so worker_connections groups the same agent\n// across heartbeats (the unique key is (userId, workerType, clientName)).\nconst PRESENCE_CLIENT_NAME = `thin-agent-${process.pid}`;\n\n/**\n * Background presence heartbeat. Posts to /api/workers/heartbeat every 30s so\n * worker_connections has a fresh row \u2014 that's what the dashboard's\n * useWorkerStatus reads to decide \"Worker online\" for live-send gating.\n * Distinct from the per-job heartbeat in runOne, which only extends a\n * specific job's lease. The graphile-worker runner had this since day one;\n * the thin agent was missing it, so the dashboard showed \"Worker offline\"\n * even when the agent was actively processing jobs.\n */\nfunction startPresenceHeartbeat(\n capabilities: readonly string[],\n signal: AbortSignal,\n log: (msg: string) => void,\n version: string | undefined,\n): void {\n const send = (status: 'alive' | 'shutting_down') => {\n void apiPost('/api/workers/heartbeat', {\n workerType: 'cli-worker',\n clientName: PRESENCE_CLIENT_NAME,\n clientVersion: version,\n workerVersion: '0.1.0',\n currentJobId: null,\n inFlightCount: 0,\n lastError: null,\n status,\n metadata: { capabilities, mode: 'thin-agent' },\n }).catch((err) => {\n structuredLog(log, {\n event: 'presence_heartbeat_error',\n error: err instanceof Error ? err.message : String(err),\n });\n });\n };\n send('alive');\n const timer = setInterval(() => send('alive'), PRESENCE_HEARTBEAT_MS);\n signal.addEventListener('abort', () => {\n clearInterval(timer);\n send('shutting_down');\n }, { once: true });\n}\n\nfunction structuredLog(\n log: (msg: string) => void,\n event: Record<string, unknown>,\n): void {\n try {\n log(JSON.stringify({ ts: new Date().toISOString(), ...event }));\n } catch {\n // JSON.stringify can throw on a circular structure; fall back to a\n // best-effort string so a log failure never takes down the loop.\n log(`[agent-loop] log_error event=${event['event']}`);\n }\n}\n\n/** Sleep that resolves early when the abort signal fires. Returns true if\n * the full duration elapsed, false if aborted. */\nfunction abortableSleep(ms: number, signal: AbortSignal): Promise<boolean> {\n if (signal.aborted) return Promise.resolve(false);\n if (ms <= 0) return Promise.resolve(true);\n return new Promise<boolean>((resolve) => {\n const timer = setTimeout(() => {\n signal.removeEventListener('abort', onAbort);\n resolve(true);\n }, ms);\n const onAbort = () => {\n clearTimeout(timer);\n signal.removeEventListener('abort', onAbort);\n resolve(false);\n };\n signal.addEventListener('abort', onAbort, { once: true });\n });\n}\n\n/** Poll once for work. Returns the parsed response or null on 204. api-client\n * returns \"\" for an empty response body (Content-Type is not JSON on 204), so\n * we use that as the 204 sentinel. */\nexport async function pollForWork(\n capabilities: readonly string[],\n waitMs: number,\n): Promise<NextWorkResponse | null> {\n const qs = `capabilities=${encodeURIComponent(capabilities.join(','))}&waitMs=${waitMs}`;\n const result = await apiGet<NextWorkResponse | string>(`/api/work/next?${qs}`);\n if (result == null || typeof result === 'string') return null;\n return result;\n}\n\n/** Construct a minimal JobHelpers shim. Handlers only touch\n * helpers.logger.{info,warn,error}(msg) and helpers.job.attempts \u2014 verified\n * by grep against apps/cli/src/lib/handlers/. We cast through `unknown` so\n * TS accepts the structurally smaller object as the full JobHelpers type;\n * pulling graphile-worker into this hot path would defeat the Phase 4c\n * cleanup that removes the dep entirely. */\nfunction makeHelpers(attempts: number, log: (msg: string) => void): JobHelpers {\n return {\n logger: {\n info: (msg: string) => log(msg),\n warn: (msg: string) => log(msg),\n error: (msg: string) => log(msg),\n },\n job: { attempts },\n };\n}\n\nasync function runOne(\n job: NextWorkResponse,\n heartbeatIntervalMs: number,\n log: (msg: string) => void,\n): Promise<void> {\n const handler = HANDLERS[job.kind];\n const startedAt = Date.now();\n structuredLog(log, {\n event: 'claim',\n jobId: job.id,\n kind: job.kind,\n attempts: job.attempts,\n });\n\n // Defensive: if the API returned a kind the agent doesn't know about\n // (capability filter mismatch, schema drift), fail terminally so the row\n // doesn't bounce forever. The /api/work/next filter should already prevent\n // this, but failing closed at the dispatcher means a single new kind on\n // the API side can't wedge an older agent.\n if (!handler) {\n const msg = `unknown kind: ${job.kind}`;\n structuredLog(log, {\n event: 'fail',\n jobId: job.id,\n kind: job.kind,\n terminal: true,\n error: msg,\n });\n await apiPost(`/api/work/${job.id}/fail`, {\n leaseToken: job.leaseToken,\n error: msg,\n terminal: true,\n }).catch(() => {\n // best-effort: the lease will expire and the reconciler reclaims it\n });\n return;\n }\n\n // Heartbeat timer. Starts after the first heartbeatIntervalMs elapses so\n // we don't spam an extra request for sub-second jobs. Errors are swallowed\n // \u2014 a single failed heartbeat doesn't abort the run; the server's\n // 5-minute lease gives us up to ten chances at 30s intervals before we\n // lose the claim.\n const heartbeatTimer = setInterval(() => {\n structuredLog(log, { event: 'heartbeat', jobId: job.id });\n apiPost(`/api/work/${job.id}/heartbeat`, {\n leaseToken: job.leaseToken,\n }).catch((err) => {\n structuredLog(log, {\n event: 'heartbeat_error',\n jobId: job.id,\n error: err instanceof Error ? err.message : String(err),\n });\n });\n }, heartbeatIntervalMs);\n\n try {\n const helpers = makeHelpers(job.attempts, log);\n const result = (await handler(job.payload, helpers)) ?? {};\n clearInterval(heartbeatTimer);\n structuredLog(log, {\n event: 'complete',\n jobId: job.id,\n kind: job.kind,\n durationMs: Date.now() - startedAt,\n });\n await apiPost(`/api/work/${job.id}/complete`, {\n leaseToken: job.leaseToken,\n result,\n });\n } catch (err) {\n clearInterval(heartbeatTimer);\n const terminal = isPermanentJobError(err);\n const message = err instanceof Error ? err.message : String(err);\n structuredLog(log, {\n event: 'fail',\n jobId: job.id,\n kind: job.kind,\n durationMs: Date.now() - startedAt,\n terminal,\n error: message,\n });\n await apiPost(`/api/work/${job.id}/fail`, {\n leaseToken: job.leaseToken,\n error: message,\n terminal,\n }).catch((postErr) => {\n structuredLog(log, {\n event: 'fail_post_error',\n jobId: job.id,\n error: postErr instanceof Error ? postErr.message : String(postErr),\n });\n });\n }\n}\n\nexport async function runAgentLoop(opts: RunAgentLoopOptions): Promise<void> {\n const log = opts.log ?? (() => undefined);\n const pollWaitMs = opts.pollWaitMs ?? DEFAULT_POLL_WAIT_MS;\n const heartbeatIntervalMs = opts.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_MS;\n const capabilities = opts.capabilities ?? availableCapabilities();\n\n let backoffMs = BACKOFF_INITIAL_MS;\n structuredLog(log, {\n event: 'agent_start',\n capabilities,\n pollWaitMs,\n heartbeatIntervalMs,\n });\n\n // Background presence heartbeat \u2014 writes to worker_connections so the\n // dashboard's \"Worker online\" indicator turns on. Fire-and-forget; failures\n // are logged but don't block the work poll.\n startPresenceHeartbeat(capabilities, opts.signal, log, opts.version);\n\n // Fire-and-forget the terminal-target liveness heartbeat. It runs on its\n // own interval (default 60s) and shuts down on the same abort signal.\n // Errors are caught internally \u2014 we don't await it so a slow tmux probe\n // can't delay the work poll.\n void startTerminalTargetHeartbeat({ signal: opts.signal, log }).catch((err) => {\n structuredLog(log, {\n event: 'tt_heartbeat_fatal',\n error: err instanceof Error ? err.message : String(err),\n });\n });\n\n // Version-drift self-restart: after `npm i -g` upgrades the CLI, this\n // long-lived process keeps executing the OLD code forever \u2014 launchd bounces\n // on dist changes only in the repo-checkout install, and the Linux systemd\n // unit has no WatchPaths equivalent at all. Between jobs (never mid-job),\n // compare the on-disk installed version against the one this process booted\n // with; on drift, hand control to the caller (which exits non-zero so BOTH\n // supervisors respawn onto the new code: systemd is Restart=on-failure,\n // launchd KeepAlive restarts regardless). Throttled \u2014 a disk read per idle\n // poll would be wasteful.\n const VERSION_CHECK_INTERVAL_MS = 5 * 60_000;\n let lastVersionCheck = 0;\n const versionDrifted = (): boolean => {\n if (!opts.readInstalledVersion || !opts.version) return false;\n const now = Date.now();\n if (now - lastVersionCheck < VERSION_CHECK_INTERVAL_MS) return false;\n lastVersionCheck = now;\n const onDisk = opts.readInstalledVersion();\n return Boolean(onDisk && onDisk !== opts.version);\n };\n\n while (!opts.signal.aborted) {\n if (versionDrifted()) {\n structuredLog(log, {\n event: 'agent_version_drift',\n bootVersion: opts.version,\n installedVersion: opts.readInstalledVersion?.() ?? null,\n });\n opts.onVersionDrift?.();\n break;\n }\n let job: NextWorkResponse | null = null;\n try {\n job = await pollForWork(capabilities, pollWaitMs);\n } catch (err) {\n // Network/API errors during poll: log + backoff. The API may be\n // restarting, the laptop may have just woken, etc. Don't crash the\n // loop \u2014 that's the whole point of this migration.\n const status = err instanceof ApiError ? err.statusCode : undefined;\n structuredLog(log, {\n event: 'poll_error',\n status,\n error: err instanceof Error ? err.message : String(err),\n });\n const completed = await abortableSleep(backoffMs, opts.signal);\n if (!completed) break;\n backoffMs = Math.min(backoffMs * 2, BACKOFF_MAX_MS);\n continue;\n }\n\n if (!job) {\n structuredLog(log, { event: 'poll_204' });\n const completed = await abortableSleep(backoffMs, opts.signal);\n if (!completed) break;\n backoffMs = Math.min(backoffMs * 2, BACKOFF_MAX_MS);\n continue;\n }\n\n // Got work \u2014 reset the backoff and run it. An in-flight handler is\n // intentionally not abort-cancellable; we await its natural completion\n // even if the signal fires mid-run.\n backoffMs = BACKOFF_INITIAL_MS;\n await runOne(job, heartbeatIntervalMs, log);\n }\n\n structuredLog(log, { event: 'agent_stop' });\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
toolLabel
|
|
3
|
-
} from "./chunk-RH7NHVSK.js";
|
|
4
1
|
import {
|
|
5
2
|
DIM,
|
|
6
3
|
ORANGE,
|
|
7
4
|
RED
|
|
8
5
|
} from "./chunk-QJQS6TUN.js";
|
|
6
|
+
import {
|
|
7
|
+
toolLabel
|
|
8
|
+
} from "./chunk-RH7NHVSK.js";
|
|
9
9
|
|
|
10
10
|
// src/dashboard/launcher/NewLaunchForm.tsx
|
|
11
11
|
import { useState } from "react";
|
|
@@ -105,4 +105,4 @@ function NewLaunchForm({
|
|
|
105
105
|
export {
|
|
106
106
|
NewLaunchForm
|
|
107
107
|
};
|
|
108
|
-
//# sourceMappingURL=chunk-
|
|
108
|
+
//# sourceMappingURL=chunk-DNKSVWMD.js.map
|