tide-commander 1.199.0 → 1.199.2
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/CHANGELOG.md +14 -0
- package/dist/assets/{AudioPlayer-CusBq3ON.js → AudioPlayer-XaWiU7f4.js} +1 -1
- package/dist/assets/{BossLogsModal-B1PLdne_.js → BossLogsModal-1tI-leZQ.js} +1 -1
- package/dist/assets/{BossSpawnModal-DQN_2lzh.js → BossSpawnModal-CRoChH0J.js} +1 -1
- package/dist/assets/{ControlsModal-D4JsU4VU.js → ControlsModal-Cu7kwhct.js} +1 -1
- package/dist/assets/{DockerLogsModal-BuCiJ02l.js → DockerLogsModal-DFAiOsWH.js} +1 -1
- package/dist/assets/{EmbeddedEditor-CBaRvdh1.js → EmbeddedEditor-hnSknX5L.js} +1 -1
- package/dist/assets/{FcStdViewer-CpJhmVIt.js → FcStdViewer-B_cbRz1w.js} +1 -1
- package/dist/assets/{GcodeViewer-B8BHZMmG.js → GcodeViewer-CFgwEZ6g.js} +1 -1
- package/dist/assets/{GlbViewer-DdVYw2Jh.js → GlbViewer-14WSSXX7.js} +1 -1
- package/dist/assets/{GmailOAuthSetup-Cj0r8z52.js → GmailOAuthSetup-waw2142C.js} +1 -1
- package/dist/assets/{GoogleOAuthSetup-BO0udRUZ.js → GoogleOAuthSetup-Bo59grtp.js} +1 -1
- package/dist/assets/{IframeModal-CecB4A27.js → IframeModal-B3M77grB.js} +1 -1
- package/dist/assets/{IntegrationsPanel-BVnzRrTb.js → IntegrationsPanel-BLHrC0GX.js} +2 -2
- package/dist/assets/{LogViewerModal-CNbfgDQK.js → LogViewerModal-CitlmDgQ.js} +1 -1
- package/dist/assets/{MonitoringModal-CUXXMqLv.js → MonitoringModal-BcKOCSRM.js} +1 -1
- package/dist/assets/{OrbitControls-DS7gPhsZ.js → OrbitControls-ANLmgNeR.js} +1 -1
- package/dist/assets/{PM2LogsModal-DFAuRDwm.js → PM2LogsModal-DrHpuMNf.js} +1 -1
- package/dist/assets/{RestoreArchivedAreaModal-DT5-U0A8.js → RestoreArchivedAreaModal-DcD3vdUJ.js} +1 -1
- package/dist/assets/{Scene2DCanvas-DqakJMdS.js → Scene2DCanvas-DKUcFYZW.js} +1 -1
- package/dist/assets/SceneManager-DPojKMM-.js +104 -0
- package/dist/assets/{SkillsPanel-1hYjREqo.js → SkillsPanel-CUW0prbh.js} +1 -1
- package/dist/assets/{SlackMultiInstanceSetup-C6EtUfP8.js → SlackMultiInstanceSetup-YFYeZ94t.js} +1 -1
- package/dist/assets/{SpawnModal-BmMZFOUj.js → SpawnModal-DPvq-R5b.js} +1 -1
- package/dist/assets/{StatisticsModal-mmqE6JaF.js → StatisticsModal-Dt-Hof51.js} +1 -1
- package/dist/assets/{StlViewer-D0XChi1U.js → StlViewer-GyyomEax.js} +1 -1
- package/dist/assets/{SubordinateAssignmentModal-CBISWK7c.js → SubordinateAssignmentModal-ahzGBjr4.js} +1 -1
- package/dist/assets/{ThreeAreaSelector-C_7zwy_Q.js → ThreeAreaSelector-Bj_mL6NG.js} +1 -1
- package/dist/assets/{ThreeViewShortcuts-DZZmjQAV.js → ThreeViewShortcuts-CKtE3yZV.js} +1 -1
- package/dist/assets/{TriggerManagerPanel-Bwd5-VGg.js → TriggerManagerPanel-DtdbOk9B.js} +1 -1
- package/dist/assets/{VideoPlayer-BuFPsPNu.js → VideoPlayer-Crq-MwPS.js} +1 -1
- package/dist/assets/{WorkflowEditorPanel-DxQfrs71.js → WorkflowEditorPanel-CitR9Em3.js} +1 -1
- package/dist/assets/{abnfDiagram-VRR7QNED-AhR0vtDs.js → abnfDiagram-VRR7QNED-QFcuJYPe.js} +1 -1
- package/dist/assets/{arc-CTR1mrNz.js → arc-CLuSzQ4E.js} +1 -1
- package/dist/assets/{architectureDiagram-ZJ3FMSHR-DrsB_-NW.js → architectureDiagram-ZJ3FMSHR-DlhVck93.js} +1 -1
- package/dist/assets/{area-logos-D_dodLqh.js → area-logos-DYARiYqI.js} +1 -1
- package/dist/assets/{blockDiagram-677ZJIJ3-Bf6BktPD.js → blockDiagram-677ZJIJ3-CJwOmsfj.js} +1 -1
- package/dist/assets/{c4Diagram-LMCZKHZV--UrsSHrg.js → c4Diagram-LMCZKHZV-OsgCNPWJ.js} +1 -1
- package/dist/assets/channel-cpl_NpAy.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-DajcrDU0.js → chunk-2Q5K7J3B-KG-L7vvm.js} +1 -1
- package/dist/assets/{chunk-32BRIVSS-BoJToPN6.js → chunk-32BRIVSS-BXGbqO9v.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-CWyIsEjs.js → chunk-5VM5RSS4-BWXkm8f0.js} +1 -1
- package/dist/assets/{chunk-EX3LRPZG-BxQt59EY.js → chunk-EX3LRPZG-7-8ulJ19.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-CcltxnxE.js → chunk-JWPE2WC7-mkwFN5xu.js} +1 -1
- package/dist/assets/{chunk-MOJQB5TN-CoFz0cis.js → chunk-MOJQB5TN-WEfWznKE.js} +1 -1
- package/dist/assets/{chunk-RYQCIY6F-CqD5SIAs.js → chunk-RYQCIY6F-ByP5odu4.js} +1 -1
- package/dist/assets/{chunk-V7JOEXUC-B38vYRK3.js → chunk-V7JOEXUC-yWHmp1L1.js} +1 -1
- package/dist/assets/{chunk-VR4S4FIN-CVwpBl0T.js → chunk-VR4S4FIN-DHbZaHzt.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-DznOU5hd.js → chunk-XXDRQBXY-SNa-Wh3U.js} +1 -1
- package/dist/assets/classDiagram-OUVF2IWQ-D_lsO4mm.js +1 -0
- package/dist/assets/classDiagram-v2-EOCWNBFH-D_lsO4mm.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-B1uTqWwQ.js → cose-bilkent-JH36ORCC-CyxHYUhP.js} +1 -1
- package/dist/assets/{cynefin-VYW2F7L2-DgpqdGRT.js → cynefin-VYW2F7L2-BvwjSDiQ.js} +1 -1
- package/dist/assets/{cynefinDiagram-TSTJHNR4-Ld74Dvow.js → cynefinDiagram-TSTJHNR4-C8doInhn.js} +1 -1
- package/dist/assets/{dagre-VKFMJZFB-DAp2Br8X.js → dagre-VKFMJZFB-BNDcupek.js} +1 -1
- package/dist/assets/{diagram-FQU43EPY-CaMUb0eE.js → diagram-FQU43EPY-Bst0bBnQ.js} +1 -1
- package/dist/assets/{diagram-G47NLZAW-kTOq861A.js → diagram-G47NLZAW-CfEv0aTY.js} +1 -1
- package/dist/assets/{diagram-NH7WQ7WH-LWZrE7zu.js → diagram-NH7WQ7WH-B7MYvWXL.js} +1 -1
- package/dist/assets/{diagram-OA4YK3LP-Csm-frID.js → diagram-OA4YK3LP-BIzII0d2.js} +1 -1
- package/dist/assets/{diagram-WEI45ONY-BconZEMM.js → diagram-WEI45ONY-D5yd1HWr.js} +1 -1
- package/dist/assets/{ebnfDiagram-CCIWWBDH-Cr5xwAgl.js → ebnfDiagram-CCIWWBDH-gyhVYUos.js} +1 -1
- package/dist/assets/{erDiagram-Q63AITRT-76P2E9Q6.js → erDiagram-Q63AITRT-Duz-DwfE.js} +1 -1
- package/dist/assets/{flowDiagram-23GEKE2U-CciJAjTA.js → flowDiagram-23GEKE2U-CrnARkmw.js} +1 -1
- package/dist/assets/{ganttDiagram-NO4QXBWP-D7VgBp68.js → ganttDiagram-NO4QXBWP-D6cBteIN.js} +1 -1
- package/dist/assets/{gitGraphDiagram-IHSO6WYX-CxICR8Bg.js → gitGraphDiagram-IHSO6WYX-CMHfr_lR.js} +1 -1
- package/dist/assets/{index-Cd53q1WZ.js → index-2427ATYD.js} +3 -3
- package/dist/assets/{index-lQifR7XC.js → index-B1vOCcI-.js} +1 -1
- package/dist/assets/index-B5wHDczA.js +2 -0
- package/dist/assets/{index-BNsHVfai.js → index-CbMtvQTH.js} +1 -1
- package/dist/assets/{index-DzzLdCr9.js → index-D5O_0aq9.js} +1 -1
- package/dist/assets/{index-Bfmfqbw7.js → index-D5pIRxFm.js} +1 -1
- package/dist/assets/index-DJKuwqJG.js +2 -0
- package/dist/assets/{index-Dc4yP2fA.js → index-DeqoOoo6.js} +3 -3
- package/dist/assets/{index-Z0bYekBH.js → index-fiC5OXgR.js} +1 -1
- package/dist/assets/{infoDiagram-FWYZ7A6U-h-R7gGrP.js → infoDiagram-FWYZ7A6U-Mvk2Mpp3.js} +1 -1
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-D1RbbiMz.js → ishikawaDiagram-FXEZZL3T-DXsaNDYg.js} +1 -1
- package/dist/assets/{journeyDiagram-5HDEW3XC-BnNUbZMI.js → journeyDiagram-5HDEW3XC-6QlK8xHN.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-kX3kneLI.js → kanban-definition-HUTT4EX6-D5EeRs07.js} +1 -1
- package/dist/assets/{linear-DC5X1tTJ.js → linear-D_aP9MD5.js} +1 -1
- package/dist/assets/main-B2ZLroL1.css +1 -0
- package/dist/assets/{main-Bb9H5jmt.js → main-Dk3qq80S.js} +156 -156
- package/dist/assets/{mermaid.core-BE33DHym.js → mermaid.core-CEculPxD.js} +4 -4
- package/dist/assets/{mindmap-definition-LN4V7U3C-CY3A4tSH.js → mindmap-definition-LN4V7U3C-Vjj9vgEN.js} +1 -1
- package/dist/assets/{pegDiagram-2B236MQR-B99ZSEym.js → pegDiagram-2B236MQR-B-AwbaoO.js} +1 -1
- package/dist/assets/{pieDiagram-ENE6RG2P-7PsmMteT.js → pieDiagram-ENE6RG2P-D5o1UYPK.js} +1 -1
- package/dist/assets/{quadrantDiagram-ABIIQ3AL-DjShiKUv.js → quadrantDiagram-ABIIQ3AL-Bl_Rbmz-.js} +1 -1
- package/dist/assets/{railroadDiagram-RFXS5EU6-B6KuCiHH.js → railroadDiagram-RFXS5EU6-CW-qy1dp.js} +1 -1
- package/dist/assets/{requirementDiagram-TGXJPOKE-k_7TCScw.js → requirementDiagram-TGXJPOKE-DGUQpy3K.js} +1 -1
- package/dist/assets/{sankeyDiagram-HTMAVEWB-D2YbTfpK.js → sankeyDiagram-HTMAVEWB-CUlWZH9G.js} +1 -1
- package/dist/assets/{sequenceDiagram-DBY2YBRQ-CuYh53OC.js → sequenceDiagram-DBY2YBRQ-CeDVJrbJ.js} +1 -1
- package/dist/assets/{sizeCapture-X5ZJPWSS-D4_qcFkr.js → sizeCapture-X5ZJPWSS-BGSL3ofq.js} +1 -1
- package/dist/assets/{stateDiagram-2N3HPSRC-DO9QVMBT.js → stateDiagram-2N3HPSRC-9A8tAPIu.js} +1 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-DzVFFq0g.js +1 -0
- package/dist/assets/{swimlanes-5IMT3BWC-gMS-z6zh.js → swimlanes-5IMT3BWC-CipRR1uk.js} +2 -2
- package/dist/assets/swimlanesDiagram-G3AALYLV-XDQw-C4e.js +8 -0
- package/dist/assets/{timeline-definition-FHXFAJF6-CUmQCwOG.js → timeline-definition-FHXFAJF6-IgrCsFe-.js} +1 -1
- package/dist/assets/{vendor-three-CBPuP1jE.js → vendor-three-DYBBrXTx.js} +1 -1
- package/dist/assets/{vennDiagram-L72KCM5P-CIP-5b8u.js → vennDiagram-L72KCM5P-okSGsDy3.js} +1 -1
- package/dist/assets/{wardleyDiagram-EHGQE667-DlQqKMjr.js → wardleyDiagram-EHGQE667-G0RLggUt.js} +1 -1
- package/dist/assets/{web-DmAaMs-2.js → web-B5BfOO5C.js} +1 -1
- package/dist/assets/{web-D42BXPx5.js → web-BPOqvnJL.js} +1 -1
- package/dist/assets/{web-DVkGXGfI.js → web-Q8DwQW0W.js} +1 -1
- package/dist/assets/{xychartDiagram-FW5EYKEG-CekYCp3d.js → xychartDiagram-FW5EYKEG-DtVJEFyI.js} +1 -1
- package/dist/build-info.json +1 -1
- package/dist/index.html +3 -3
- package/dist/src/packages/server/app.js +26 -1
- package/dist/src/packages/server/claude/backend.js +5 -4
- package/dist/src/packages/server/claude/runner/stdout-pipeline.js +29 -30
- package/dist/src/packages/server/claude/runner/watchdog.js +2 -1
- package/dist/src/packages/server/claude/session-loader.js +141 -81
- package/dist/src/packages/server/data/index.js +35 -2
- package/dist/src/packages/server/routes/perf.js +50 -39
- package/dist/src/packages/server/services/git-watch-service.js +14 -2
- package/dist/src/packages/server/services/runtime-service.js +85 -39
- package/dist/src/packages/server/websocket/handler.js +33 -8
- package/dist/src/packages/server/websocket/listeners/runtime-listeners.js +5 -2
- package/package.json +3 -1
- package/dist/assets/SceneManager-htGN7YfA.js +0 -104
- package/dist/assets/channel-BkokQZXi.js +0 -1
- package/dist/assets/classDiagram-OUVF2IWQ-DibSrtP1.js +0 -1
- package/dist/assets/classDiagram-v2-EOCWNBFH-DibSrtP1.js +0 -1
- package/dist/assets/index-C8zT43_7.js +0 -2
- package/dist/assets/index-WhxnTPGa.js +0 -2
- package/dist/assets/main-CSv15YYo.css +0 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-CekJ4qac.js +0 -1
- package/dist/assets/swimlanesDiagram-G3AALYLV-CfPVdXRk.js +0 -8
|
@@ -3845,68 +3845,154 @@ function providerDisplayName(provider) {
|
|
|
3845
3845
|
return 'Pi';
|
|
3846
3846
|
return 'Claude';
|
|
3847
3847
|
}
|
|
3848
|
-
|
|
3849
|
-
|
|
3848
|
+
const PROVIDER_PROCESS_REGEXES = Object.keys(PROVIDER_PROCESS_PATTERNS).map((provider) => ({
|
|
3849
|
+
provider,
|
|
3850
|
+
regex: new RegExp(PROVIDER_PROCESS_PATTERNS[provider]),
|
|
3851
|
+
}));
|
|
3852
|
+
/**
|
|
3853
|
+
* How long a process snapshot is reused by callers that don't ask for a fresh
|
|
3854
|
+
* one. The snapshot is what every per-agent lookup (orphan polling, perf
|
|
3855
|
+
* metrics, PID resolution) needs, and those lookups come in bursts of one per
|
|
3856
|
+
* agent — with hundreds of agents that used to mean hundreds of blocking
|
|
3857
|
+
* `ps aux | grep` + `readlink` spawns back-to-back. One second of staleness is
|
|
3858
|
+
* harmless for all of them (a process that just died fails `process.kill(pid, 0)`
|
|
3859
|
+
* downstream anyway); kill paths pass `fresh: true`.
|
|
3860
|
+
*/
|
|
3861
|
+
const PROCESS_SNAPSHOT_TTL_MS = 1000;
|
|
3862
|
+
let processSnapshotCache = null;
|
|
3863
|
+
let processSnapshotInFlight = null;
|
|
3864
|
+
function normalizeCwd(cwd) {
|
|
3865
|
+
return cwd.replace(/\/+$/, '');
|
|
3866
|
+
}
|
|
3867
|
+
function runCommand(file, args, timeoutMs) {
|
|
3868
|
+
return new Promise((resolve, reject) => {
|
|
3869
|
+
const child = spawn(file, args, { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
3870
|
+
const chunks = [];
|
|
3871
|
+
let settled = false;
|
|
3872
|
+
const timer = setTimeout(() => {
|
|
3873
|
+
if (settled)
|
|
3874
|
+
return;
|
|
3875
|
+
settled = true;
|
|
3876
|
+
child.kill('SIGKILL');
|
|
3877
|
+
reject(new Error(`${file} timed out after ${timeoutMs}ms`));
|
|
3878
|
+
}, timeoutMs);
|
|
3879
|
+
child.stdout.on('data', (chunk) => chunks.push(chunk));
|
|
3880
|
+
child.on('error', (err) => {
|
|
3881
|
+
if (settled)
|
|
3882
|
+
return;
|
|
3883
|
+
settled = true;
|
|
3884
|
+
clearTimeout(timer);
|
|
3885
|
+
reject(err);
|
|
3886
|
+
});
|
|
3887
|
+
child.on('close', (code) => {
|
|
3888
|
+
if (settled)
|
|
3889
|
+
return;
|
|
3890
|
+
settled = true;
|
|
3891
|
+
clearTimeout(timer);
|
|
3892
|
+
if (code !== 0) {
|
|
3893
|
+
reject(new Error(`${file} exited with code ${code}`));
|
|
3894
|
+
return;
|
|
3895
|
+
}
|
|
3896
|
+
resolve(Buffer.concat(chunks).toString('utf-8'));
|
|
3897
|
+
});
|
|
3898
|
+
});
|
|
3899
|
+
}
|
|
3900
|
+
/** Resolve a process's cwd without blocking: /proc readlink on Linux, lsof on macOS. */
|
|
3901
|
+
async function readProcessCwd(pid) {
|
|
3850
3902
|
try {
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
if (!psOutput) {
|
|
3856
|
-
return [];
|
|
3903
|
+
if (process.platform === 'darwin') {
|
|
3904
|
+
const lsofOutput = await runCommand('lsof', ['-a', '-d', 'cwd', '-p', String(pid), '-Fn'], 2000);
|
|
3905
|
+
const line = lsofOutput.split('\n').find((l) => l.startsWith('n'));
|
|
3906
|
+
return line ? line.substring(1).trim() : null;
|
|
3857
3907
|
}
|
|
3858
|
-
return
|
|
3908
|
+
return (await fs.promises.readlink(`/proc/${pid}/cwd`)).trim();
|
|
3859
3909
|
}
|
|
3860
3910
|
catch {
|
|
3861
|
-
return
|
|
3911
|
+
return null;
|
|
3862
3912
|
}
|
|
3863
3913
|
}
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3914
|
+
/**
|
|
3915
|
+
* List every running provider CLI process (claude/codex/opencode/grok/pi) with
|
|
3916
|
+
* its cwd, using ONE `ps aux` spawn plus one non-blocking cwd lookup per match.
|
|
3917
|
+
* Matching runs against the full `ps aux` line, exactly like the former
|
|
3918
|
+
* `ps aux | grep -E <pattern>` pipeline, so the provider patterns keep their
|
|
3919
|
+
* meaning. Snapshots are memoized for PROCESS_SNAPSHOT_TTL_MS and concurrent
|
|
3920
|
+
* callers share the in-flight promise; pass `fresh: true` to bypass both.
|
|
3921
|
+
*/
|
|
3922
|
+
export async function snapshotProviderProcesses(options = {}) {
|
|
3923
|
+
if (process.platform === 'win32') {
|
|
3924
|
+
return [];
|
|
3925
|
+
}
|
|
3926
|
+
const now = Date.now();
|
|
3927
|
+
if (!options.fresh) {
|
|
3928
|
+
if (processSnapshotCache && now - processSnapshotCache.takenAt < PROCESS_SNAPSHOT_TTL_MS) {
|
|
3929
|
+
return processSnapshotCache.processes;
|
|
3930
|
+
}
|
|
3931
|
+
if (processSnapshotInFlight) {
|
|
3932
|
+
return processSnapshotInFlight;
|
|
3933
|
+
}
|
|
3934
|
+
}
|
|
3935
|
+
const work = (async () => {
|
|
3936
|
+
let psOutput;
|
|
3937
|
+
try {
|
|
3938
|
+
psOutput = await runCommand('ps', ['aux'], 5000);
|
|
3939
|
+
}
|
|
3940
|
+
catch (err) {
|
|
3941
|
+
log.error(' Failed to list processes with ps:', err);
|
|
3942
|
+
return [];
|
|
3943
|
+
}
|
|
3944
|
+
const matches = [];
|
|
3945
|
+
for (const line of psOutput.split('\n')) {
|
|
3946
|
+
if (!line)
|
|
3947
|
+
continue;
|
|
3948
|
+
// A line may satisfy more than one provider pattern; keep it under each,
|
|
3949
|
+
// as the former one-grep-per-provider pipeline did.
|
|
3950
|
+
const providers = PROVIDER_PROCESS_REGEXES.filter(({ regex }) => regex.test(line));
|
|
3951
|
+
if (providers.length === 0)
|
|
3952
|
+
continue;
|
|
3953
|
+
// `ps aux` columns: USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
|
|
3954
|
+
const pid = Number.parseInt(line.trim().split(/\s+/)[1] ?? '', 10);
|
|
3955
|
+
if (!Number.isFinite(pid) || pid <= 0)
|
|
3956
|
+
continue;
|
|
3957
|
+
for (const { provider } of providers) {
|
|
3958
|
+
matches.push({ pid, provider });
|
|
3874
3959
|
}
|
|
3875
|
-
return lsofOutput.substring(1);
|
|
3876
3960
|
}
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3961
|
+
const processes = await Promise.all(matches.map(async ({ pid, provider }) => ({
|
|
3962
|
+
pid,
|
|
3963
|
+
provider,
|
|
3964
|
+
cwd: await readProcessCwd(pid),
|
|
3965
|
+
})));
|
|
3966
|
+
processSnapshotCache = { takenAt: Date.now(), processes };
|
|
3967
|
+
return processes;
|
|
3968
|
+
})();
|
|
3969
|
+
processSnapshotInFlight = work;
|
|
3970
|
+
try {
|
|
3971
|
+
return await work;
|
|
3881
3972
|
}
|
|
3882
|
-
|
|
3883
|
-
|
|
3973
|
+
finally {
|
|
3974
|
+
if (processSnapshotInFlight === work) {
|
|
3975
|
+
processSnapshotInFlight = null;
|
|
3976
|
+
}
|
|
3884
3977
|
}
|
|
3885
3978
|
}
|
|
3979
|
+
/** First snapshot entry of `provider` whose cwd matches `cwd` (trailing slashes ignored). */
|
|
3980
|
+
function findProviderProcessInSnapshot(processes, cwd, provider) {
|
|
3981
|
+
const normalizedCwd = normalizeCwd(cwd);
|
|
3982
|
+
return processes.find((p) => p.provider === provider && p.cwd !== null && normalizeCwd(p.cwd) === normalizedCwd);
|
|
3983
|
+
}
|
|
3886
3984
|
async function isProviderProcessRunningInCwd(cwd, provider) {
|
|
3887
3985
|
// Only works on Linux/Unix/macOS
|
|
3888
3986
|
if (process.platform === 'win32') {
|
|
3889
3987
|
return false;
|
|
3890
3988
|
}
|
|
3891
3989
|
try {
|
|
3892
|
-
const
|
|
3893
|
-
|
|
3894
|
-
if (pids.length === 0) {
|
|
3990
|
+
const match = findProviderProcessInSnapshot(await snapshotProviderProcesses(), cwd, provider);
|
|
3991
|
+
if (!match) {
|
|
3895
3992
|
return false;
|
|
3896
3993
|
}
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
const processCwd = getProcessCwd(pid, execSync);
|
|
3900
|
-
if (!processCwd) {
|
|
3901
|
-
continue;
|
|
3902
|
-
}
|
|
3903
|
-
const normalizedProcessCwd = processCwd.replace(/\/+$/, '');
|
|
3904
|
-
if (normalizedProcessCwd === normalizedCwd) {
|
|
3905
|
-
log.log(` Found ${providerDisplayName(provider)} process ${pid} running in ${cwd}`);
|
|
3906
|
-
return true;
|
|
3907
|
-
}
|
|
3908
|
-
}
|
|
3909
|
-
return false;
|
|
3994
|
+
log.log(` Found ${providerDisplayName(provider)} process ${match.pid} running in ${cwd}`);
|
|
3995
|
+
return true;
|
|
3910
3996
|
}
|
|
3911
3997
|
catch (err) {
|
|
3912
3998
|
log.error(` Error checking for ${providerDisplayName(provider)} processes:`, err);
|
|
@@ -3918,26 +4004,7 @@ async function findProviderProcessPidInCwd(cwd, provider) {
|
|
|
3918
4004
|
return undefined;
|
|
3919
4005
|
}
|
|
3920
4006
|
try {
|
|
3921
|
-
|
|
3922
|
-
const pids = getProviderProcessPids(provider, execSync);
|
|
3923
|
-
if (pids.length === 0) {
|
|
3924
|
-
return undefined;
|
|
3925
|
-
}
|
|
3926
|
-
const normalizedCwd = cwd.replace(/\/+$/, '');
|
|
3927
|
-
for (const pid of pids) {
|
|
3928
|
-
const processCwd = getProcessCwd(pid, execSync);
|
|
3929
|
-
if (!processCwd) {
|
|
3930
|
-
continue;
|
|
3931
|
-
}
|
|
3932
|
-
const normalizedProcessCwd = processCwd.replace(/\/+$/, '');
|
|
3933
|
-
if (normalizedProcessCwd === normalizedCwd) {
|
|
3934
|
-
const numericPid = Number.parseInt(pid, 10);
|
|
3935
|
-
if (Number.isFinite(numericPid) && numericPid > 0) {
|
|
3936
|
-
return numericPid;
|
|
3937
|
-
}
|
|
3938
|
-
}
|
|
3939
|
-
}
|
|
3940
|
-
return undefined;
|
|
4007
|
+
return findProviderProcessInSnapshot(await snapshotProviderProcesses(), cwd, provider)?.pid;
|
|
3941
4008
|
}
|
|
3942
4009
|
catch (err) {
|
|
3943
4010
|
log.error(` Error finding ${providerDisplayName(provider)} PID in ${cwd}:`, err);
|
|
@@ -3950,31 +4017,24 @@ async function killProviderProcessInCwd(cwd, provider) {
|
|
|
3950
4017
|
return false;
|
|
3951
4018
|
}
|
|
3952
4019
|
try {
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
const
|
|
3959
|
-
|
|
3960
|
-
const processCwd = getProcessCwd(pid, execSync);
|
|
3961
|
-
if (!processCwd) {
|
|
3962
|
-
continue;
|
|
3963
|
-
}
|
|
3964
|
-
const normalizedProcessCwd = processCwd.replace(/\/+$/, '');
|
|
3965
|
-
if (normalizedProcessCwd !== normalizedCwd) {
|
|
4020
|
+
// Always look at live state here: killing on a stale snapshot could target
|
|
4021
|
+
// a PID that was already reused.
|
|
4022
|
+
const processes = await snapshotProviderProcesses({ fresh: true });
|
|
4023
|
+
const normalizedCwd = normalizeCwd(cwd);
|
|
4024
|
+
const label = providerDisplayName(provider);
|
|
4025
|
+
for (const proc of processes) {
|
|
4026
|
+
if (proc.provider !== provider || proc.cwd === null || normalizeCwd(proc.cwd) !== normalizedCwd) {
|
|
3966
4027
|
continue;
|
|
3967
4028
|
}
|
|
3968
|
-
const
|
|
4029
|
+
const pid = proc.pid;
|
|
3969
4030
|
log.log(`🛑 Killing detached ${label} process ${pid} in ${cwd}`);
|
|
3970
4031
|
try {
|
|
3971
|
-
|
|
3972
|
-
process.kill(numericPid, 'SIGTERM');
|
|
4032
|
+
process.kill(pid, 'SIGTERM');
|
|
3973
4033
|
setTimeout(() => {
|
|
3974
4034
|
try {
|
|
3975
|
-
process.kill(
|
|
4035
|
+
process.kill(pid, 0);
|
|
3976
4036
|
log.log(`🛑 Force killing ${label} process ${pid}`);
|
|
3977
|
-
process.kill(
|
|
4037
|
+
process.kill(pid, 'SIGKILL');
|
|
3978
4038
|
}
|
|
3979
4039
|
catch {
|
|
3980
4040
|
// Process already dead, good
|
|
@@ -542,17 +542,50 @@ export function saveCustomAgentClasses(classes) {
|
|
|
542
542
|
log.error(' Failed to save custom agent classes:', err);
|
|
543
543
|
}
|
|
544
544
|
}
|
|
545
|
+
/**
|
|
546
|
+
* Parsed running-processes file, keyed on the file's (mtime, size) so repeat
|
|
547
|
+
* callers (per-agent PID lookups, the periodic per-provider persist) don't
|
|
548
|
+
* re-parse a ~150 KB JSON (it embeds `lastRequest` per process) on every call.
|
|
549
|
+
* The file is only rewritten by saveRunningProcesses/clearRunningProcesses, so
|
|
550
|
+
* a stat() is enough to detect staleness.
|
|
551
|
+
*/
|
|
552
|
+
let runningProcessesCache = null;
|
|
553
|
+
let runningProcessesLoggedOnce = false;
|
|
545
554
|
/**
|
|
546
555
|
* Load running processes info from disk
|
|
547
556
|
* Used on startup to detect orphaned processes from previous commander instance
|
|
548
557
|
*/
|
|
549
558
|
export function loadRunningProcesses() {
|
|
550
559
|
ensureDataDir();
|
|
560
|
+
let stat = null;
|
|
561
|
+
try {
|
|
562
|
+
stat = fs.statSync(RUNNING_PROCESSES_FILE);
|
|
563
|
+
}
|
|
564
|
+
catch {
|
|
565
|
+
stat = null;
|
|
566
|
+
}
|
|
567
|
+
if (stat && runningProcessesCache && runningProcessesCache.mtimeMs === stat.mtimeMs && runningProcessesCache.size === stat.size) {
|
|
568
|
+
return [...runningProcessesCache.processes];
|
|
569
|
+
}
|
|
551
570
|
const data = safeReadJsonSync(RUNNING_PROCESSES_FILE, 'Running processes');
|
|
552
571
|
if (data?.processes) {
|
|
553
|
-
|
|
554
|
-
|
|
572
|
+
// The first load is a meaningful startup message; every later reload is
|
|
573
|
+
// just the periodic persist refreshing the file (was ~180 lines per
|
|
574
|
+
// /api/perf poll before the callers were batched — see runtime-service).
|
|
575
|
+
const message = ` Loaded ${data.processes.length} running process records (from commander PID ${data.commanderPid})`;
|
|
576
|
+
if (!runningProcessesLoggedOnce) {
|
|
577
|
+
runningProcessesLoggedOnce = true;
|
|
578
|
+
log.log(message);
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
log.debug(message);
|
|
582
|
+
}
|
|
583
|
+
if (stat) {
|
|
584
|
+
runningProcessesCache = { mtimeMs: stat.mtimeMs, size: stat.size, processes: data.processes };
|
|
585
|
+
}
|
|
586
|
+
return [...data.processes];
|
|
555
587
|
}
|
|
588
|
+
runningProcessesCache = null;
|
|
556
589
|
return [];
|
|
557
590
|
}
|
|
558
591
|
/**
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
import { Router } from 'express';
|
|
6
6
|
import * as agentService from '../services/agent-service.js';
|
|
7
7
|
import * as runtimeService from '../services/runtime-service.js';
|
|
8
|
-
import {
|
|
8
|
+
import { execFile } from 'child_process';
|
|
9
|
+
import * as fs from 'fs';
|
|
9
10
|
import os from 'os';
|
|
10
11
|
const router = Router();
|
|
11
12
|
// Server start time for uptime calculation
|
|
@@ -34,27 +35,37 @@ export function recordRequestTiming(method, path, duration) {
|
|
|
34
35
|
recentRequests.shift();
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Resident memory of a PID in MB. Reads /proc directly on Linux (no subprocess);
|
|
40
|
+
* falls back to an async `ps` elsewhere. Never blocks the event loop — this runs
|
|
41
|
+
* once per agent process on every perf poll.
|
|
42
|
+
*/
|
|
43
|
+
async function getProcessMemoryMB(pid) {
|
|
38
44
|
try {
|
|
39
|
-
const status =
|
|
40
|
-
encoding: 'utf8',
|
|
41
|
-
timeout: 500,
|
|
42
|
-
});
|
|
45
|
+
const status = await fs.promises.readFile(`/proc/${pid}/status`, 'utf8');
|
|
43
46
|
const match = status.match(/VmRSS:\s+(\d+)\s+kB/);
|
|
44
47
|
if (match) {
|
|
45
48
|
return Math.round(parseInt(match[1], 10) / 1024);
|
|
46
49
|
}
|
|
47
50
|
}
|
|
48
51
|
catch {
|
|
49
|
-
try
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
// Not Linux, or the process is gone — try ps below.
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const psOutput = await new Promise((resolve, reject) => {
|
|
56
|
+
execFile('ps', ['-o', 'rss=', '-p', String(pid)], { encoding: 'utf8', timeout: 500 }, (err, stdout) => {
|
|
57
|
+
if (err)
|
|
58
|
+
reject(err);
|
|
59
|
+
else
|
|
60
|
+
resolve(stdout);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
const kB = parseInt(psOutput.trim(), 10);
|
|
64
|
+
if (!isNaN(kB))
|
|
65
|
+
return Math.round(kB / 1024);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Process may not exist
|
|
58
69
|
}
|
|
59
70
|
return undefined;
|
|
60
71
|
}
|
|
@@ -75,26 +86,25 @@ router.get('/', async (_req, res) => {
|
|
|
75
86
|
// Per-agent process info with memory
|
|
76
87
|
const agentProcesses = [];
|
|
77
88
|
const rawProcessInfo = [];
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
pid: info.pid,
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
catch {
|
|
90
|
-
rawProcessInfo.push({
|
|
91
|
-
id: agent.id,
|
|
92
|
-
name: agent.name,
|
|
93
|
-
status: agent.status,
|
|
94
|
-
source: 'none',
|
|
95
|
-
});
|
|
96
|
-
}
|
|
89
|
+
// One batched lookup: a single persisted-file read + a single process
|
|
90
|
+
// snapshot for all agents (the per-agent form spawned `ps` per agent).
|
|
91
|
+
let processInfos = [];
|
|
92
|
+
try {
|
|
93
|
+
processInfos = await runtimeService.getAgentRuntimeProcessInfoBatch(agents.map((agent) => agent.id));
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
processInfos = [];
|
|
97
97
|
}
|
|
98
|
+
agents.forEach((agent, index) => {
|
|
99
|
+
const info = processInfos[index];
|
|
100
|
+
rawProcessInfo.push({
|
|
101
|
+
id: agent.id,
|
|
102
|
+
name: agent.name,
|
|
103
|
+
status: agent.status,
|
|
104
|
+
source: info?.source ?? 'none',
|
|
105
|
+
pid: info?.pid,
|
|
106
|
+
});
|
|
107
|
+
});
|
|
98
108
|
// "discovered" PIDs are inferred by cwd and may map multiple agents to one process.
|
|
99
109
|
// Only show discovered PID/memory when that discovered PID is unique in this snapshot.
|
|
100
110
|
const discoveredPidCounts = new Map();
|
|
@@ -104,22 +114,23 @@ router.get('/', async (_req, res) => {
|
|
|
104
114
|
}
|
|
105
115
|
discoveredPidCounts.set(info.pid, (discoveredPidCounts.get(info.pid) ?? 0) + 1);
|
|
106
116
|
}
|
|
107
|
-
|
|
117
|
+
const memoryReads = rawProcessInfo.map(async (info) => {
|
|
108
118
|
const isReliableSource = info.source === 'active' || info.source === 'persisted';
|
|
109
119
|
const isUniqueDiscoveredPid = info.source === 'discovered' &&
|
|
110
120
|
info.pid !== undefined &&
|
|
111
121
|
(discoveredPidCounts.get(info.pid) ?? 0) === 1;
|
|
112
122
|
const shouldShowProcessMetrics = isReliableSource || isUniqueDiscoveredPid;
|
|
113
123
|
const pid = shouldShowProcessMetrics ? info.pid : undefined;
|
|
114
|
-
const memMB = pid ? getProcessMemoryMB(pid) : undefined;
|
|
115
|
-
|
|
124
|
+
const memMB = pid ? await getProcessMemoryMB(pid) : undefined;
|
|
125
|
+
return {
|
|
116
126
|
id: info.id,
|
|
117
127
|
name: info.name,
|
|
118
128
|
status: info.status,
|
|
119
129
|
memoryMB: memMB,
|
|
120
130
|
pid,
|
|
121
|
-
}
|
|
122
|
-
}
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
agentProcesses.push(...(await Promise.all(memoryReads)));
|
|
123
134
|
// Request latency stats
|
|
124
135
|
const last30s = recentRequests.filter(r => Date.now() - r.timestamp < 30000);
|
|
125
136
|
const avgLatency = last30s.length > 0
|
|
@@ -33,6 +33,10 @@ const MAX_WATCHED_PATHS = 200;
|
|
|
33
33
|
const MAX_STATUS_FILES = 2_000;
|
|
34
34
|
const subscriptions = new Map();
|
|
35
35
|
const cache = new Map();
|
|
36
|
+
// Last over-cap file count warned per dir, so the 10 s poll doesn't repeat the
|
|
37
|
+
// same "has N changed files" warning forever (it used to log ~12 lines/min per
|
|
38
|
+
// big repo); re-warn only when the count changes.
|
|
39
|
+
const overCapWarned = new Map();
|
|
36
40
|
let pollTimer = null;
|
|
37
41
|
let pollInFlight = false;
|
|
38
42
|
async function git(args, cwd) {
|
|
@@ -162,7 +166,13 @@ async function computeStatus(dirPath) {
|
|
|
162
166
|
log.error(`[GitWatch] git status failed for ${dirPath}:`, err);
|
|
163
167
|
}
|
|
164
168
|
if (totalFiles > MAX_STATUS_FILES) {
|
|
165
|
-
|
|
169
|
+
if (overCapWarned.get(dirPath) !== totalFiles) {
|
|
170
|
+
overCapWarned.set(dirPath, totalFiles);
|
|
171
|
+
log.warn(`[GitWatch] ${dirPath} has ${totalFiles} changed files — pushing the first ${MAX_STATUS_FILES}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
overCapWarned.delete(dirPath);
|
|
166
176
|
}
|
|
167
177
|
let branch = null;
|
|
168
178
|
try {
|
|
@@ -255,8 +265,10 @@ async function pollOnce() {
|
|
|
255
265
|
const union = watchedUnion();
|
|
256
266
|
// Drop cache entries nobody watches anymore.
|
|
257
267
|
for (const key of cache.keys()) {
|
|
258
|
-
if (!union.has(key))
|
|
268
|
+
if (!union.has(key)) {
|
|
259
269
|
cache.delete(key);
|
|
270
|
+
overCapWarned.delete(key);
|
|
271
|
+
}
|
|
260
272
|
}
|
|
261
273
|
// Sequential on purpose: keeps the git subprocess load flat no matter how
|
|
262
274
|
// many directories are watched. Local git status is fast (~tens of ms).
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Runtime Service
|
|
3
3
|
* Manages Claude/Codex runtime runners and command execution
|
|
4
4
|
*/
|
|
5
|
-
import { isClaudeProcessRunningInCwd, isCodexProcessRunningInCwd, isOpencodeProcessRunningInCwd, isGrokProcessRunningInCwd, isPiProcessRunningInCwd, killClaudeProcessInCwd, killCodexProcessInCwd, killOpencodeProcessInCwd, killGrokProcessInCwd, killPiProcessInCwd,
|
|
5
|
+
import { isClaudeProcessRunningInCwd, isCodexProcessRunningInCwd, isOpencodeProcessRunningInCwd, isGrokProcessRunningInCwd, isPiProcessRunningInCwd, killClaudeProcessInCwd, killCodexProcessInCwd, killOpencodeProcessInCwd, killGrokProcessInCwd, killPiProcessInCwd, snapshotProviderProcesses, } from '../claude/session-loader.js';
|
|
6
6
|
import * as agentService from './agent-service.js';
|
|
7
7
|
import { ModelFallbackTracker } from '../../shared/model-fallback.js';
|
|
8
8
|
import { toModelFallbackEvent } from '../claude/types.js';
|
|
@@ -97,7 +97,10 @@ function emit(event, ...args) {
|
|
|
97
97
|
const listenerCount = listeners?.size || 0;
|
|
98
98
|
if (event === 'event') {
|
|
99
99
|
const standardEvent = args[1];
|
|
100
|
-
|
|
100
|
+
// debug-level: fires once per streaming delta (text/thinking) — at log
|
|
101
|
+
// level it was ~65k lines per afternoon, and every console.log under pm2
|
|
102
|
+
// is a synchronous pipe write on Linux.
|
|
103
|
+
log.debug(`[EMIT] event: type=${standardEvent?.type} tool=${standardEvent?.toolName || 'n/a'} listeners=${listenerCount}`);
|
|
101
104
|
}
|
|
102
105
|
if (listeners) {
|
|
103
106
|
listeners.forEach((listener) => listener(...args));
|
|
@@ -325,53 +328,96 @@ export function isAgentRunning(agentId) {
|
|
|
325
328
|
* Prefers in-memory active runner state, then falls back to persisted crash-recovery state.
|
|
326
329
|
*/
|
|
327
330
|
export async function getAgentRuntimeProcessInfo(agentId) {
|
|
328
|
-
|
|
331
|
+
const [info] = await getAgentRuntimeProcessInfoBatch([agentId]);
|
|
332
|
+
return info ?? { isRunning: false, source: 'none' };
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Batch variant of getAgentRuntimeProcessInfo: resolves every agent against
|
|
336
|
+
* ONE read of the persisted crash-recovery file and ONE process snapshot
|
|
337
|
+
* (a single `ps` + cwd lookups), instead of re-reading the file and spawning
|
|
338
|
+
* `ps aux | grep` + `readlink` per agent. With hundreds of agents the per-agent
|
|
339
|
+
* form made a single /api/perf poll take ~30 s of mostly event-loop-blocking
|
|
340
|
+
* work; this one is a handful of milliseconds after the snapshot.
|
|
341
|
+
*
|
|
342
|
+
* Result order matches `agentIds`. Unknown agents resolve to `{ source: 'none' }`.
|
|
343
|
+
*/
|
|
344
|
+
export async function getAgentRuntimeProcessInfoBatch(agentIds) {
|
|
345
|
+
// 1. In-memory runner state, gathered once across all runners so provider
|
|
346
|
+
// drift/migration doesn't hide live processes. First runner wins per agent
|
|
347
|
+
// (same as the former sequential scan).
|
|
348
|
+
const activePids = new Map();
|
|
329
349
|
for (const runner of runners.values()) {
|
|
330
350
|
const runnerWithState = runner;
|
|
331
351
|
if (!runnerWithState.getActiveProcessesState) {
|
|
332
352
|
continue;
|
|
333
353
|
}
|
|
334
|
-
const state
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
isRunning: true,
|
|
339
|
-
source: 'active',
|
|
340
|
-
};
|
|
354
|
+
for (const state of runnerWithState.getActiveProcessesState()) {
|
|
355
|
+
if (state.pid && !activePids.has(state.agentId)) {
|
|
356
|
+
activePids.set(state.agentId, state.pid);
|
|
357
|
+
}
|
|
341
358
|
}
|
|
342
359
|
}
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
360
|
+
const results = [];
|
|
361
|
+
const pendingDiscovery = [];
|
|
362
|
+
// Persisted crash-recovery state: the file is mtime-cached by loadRunningProcesses,
|
|
363
|
+
// but read it once per batch regardless so all agents see the same view.
|
|
364
|
+
let persistedByAgent = null;
|
|
365
|
+
const getPersistedPid = (agentId) => {
|
|
366
|
+
if (!persistedByAgent) {
|
|
367
|
+
persistedByAgent = new Map();
|
|
368
|
+
for (const proc of loadRunningProcesses()) {
|
|
369
|
+
if (proc.pid && !persistedByAgent.has(proc.agentId)) {
|
|
370
|
+
persistedByAgent.set(proc.agentId, proc.pid);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return persistedByAgent.get(agentId);
|
|
375
|
+
};
|
|
376
|
+
for (const agentId of agentIds) {
|
|
377
|
+
const activePid = activePids.get(agentId);
|
|
378
|
+
if (activePid) {
|
|
379
|
+
results.push({ pid: activePid, isRunning: true, source: 'active' });
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
const persistedPid = getPersistedPid(agentId);
|
|
383
|
+
if (persistedPid && isProcessRunning(persistedPid)) {
|
|
384
|
+
results.push({ pid: persistedPid, isRunning: true, source: 'persisted' });
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
results.push({ isRunning: false, source: 'none' });
|
|
388
|
+
const agent = agentService.getAgent(agentId);
|
|
389
|
+
if (agent?.cwd) {
|
|
390
|
+
pendingDiscovery.push({ index: results.length - 1, cwd: agent.cwd, provider: agent.provider ?? 'claude' });
|
|
391
|
+
}
|
|
350
392
|
}
|
|
351
|
-
|
|
352
|
-
if (
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
393
|
+
// 2. OS-level discovery by cwd — one snapshot for every agent that needs it.
|
|
394
|
+
if (pendingDiscovery.length > 0) {
|
|
395
|
+
let processes = [];
|
|
396
|
+
try {
|
|
397
|
+
processes = await snapshotProviderProcesses();
|
|
398
|
+
}
|
|
399
|
+
catch (err) {
|
|
400
|
+
log.error('Failed to snapshot provider processes:', err);
|
|
401
|
+
}
|
|
402
|
+
const normalizeCwd = (cwd) => cwd.replace(/\/+$/, '');
|
|
403
|
+
const byProviderAndCwd = new Map();
|
|
404
|
+
for (const proc of processes) {
|
|
405
|
+
if (proc.cwd === null)
|
|
406
|
+
continue;
|
|
407
|
+
const key = `${proc.provider}\0${normalizeCwd(proc.cwd)}`;
|
|
408
|
+
// First match wins, like the former "first pid in ps order" lookup.
|
|
409
|
+
if (!byProviderAndCwd.has(key)) {
|
|
410
|
+
byProviderAndCwd.set(key, proc.pid);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
for (const { index, cwd, provider } of pendingDiscovery) {
|
|
414
|
+
const discoveredPid = byProviderAndCwd.get(`${provider}\0${normalizeCwd(cwd)}`);
|
|
415
|
+
if (discoveredPid && isProcessRunning(discoveredPid)) {
|
|
416
|
+
results[index] = { pid: discoveredPid, isRunning: true, source: 'discovered' };
|
|
417
|
+
}
|
|
369
418
|
}
|
|
370
419
|
}
|
|
371
|
-
return
|
|
372
|
-
isRunning: false,
|
|
373
|
-
source: 'none',
|
|
374
|
-
};
|
|
420
|
+
return results;
|
|
375
421
|
}
|
|
376
422
|
/**
|
|
377
423
|
* Check if there's an orphaned process for this agent
|