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.
Files changed (126) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/assets/{AudioPlayer-CusBq3ON.js → AudioPlayer-XaWiU7f4.js} +1 -1
  3. package/dist/assets/{BossLogsModal-B1PLdne_.js → BossLogsModal-1tI-leZQ.js} +1 -1
  4. package/dist/assets/{BossSpawnModal-DQN_2lzh.js → BossSpawnModal-CRoChH0J.js} +1 -1
  5. package/dist/assets/{ControlsModal-D4JsU4VU.js → ControlsModal-Cu7kwhct.js} +1 -1
  6. package/dist/assets/{DockerLogsModal-BuCiJ02l.js → DockerLogsModal-DFAiOsWH.js} +1 -1
  7. package/dist/assets/{EmbeddedEditor-CBaRvdh1.js → EmbeddedEditor-hnSknX5L.js} +1 -1
  8. package/dist/assets/{FcStdViewer-CpJhmVIt.js → FcStdViewer-B_cbRz1w.js} +1 -1
  9. package/dist/assets/{GcodeViewer-B8BHZMmG.js → GcodeViewer-CFgwEZ6g.js} +1 -1
  10. package/dist/assets/{GlbViewer-DdVYw2Jh.js → GlbViewer-14WSSXX7.js} +1 -1
  11. package/dist/assets/{GmailOAuthSetup-Cj0r8z52.js → GmailOAuthSetup-waw2142C.js} +1 -1
  12. package/dist/assets/{GoogleOAuthSetup-BO0udRUZ.js → GoogleOAuthSetup-Bo59grtp.js} +1 -1
  13. package/dist/assets/{IframeModal-CecB4A27.js → IframeModal-B3M77grB.js} +1 -1
  14. package/dist/assets/{IntegrationsPanel-BVnzRrTb.js → IntegrationsPanel-BLHrC0GX.js} +2 -2
  15. package/dist/assets/{LogViewerModal-CNbfgDQK.js → LogViewerModal-CitlmDgQ.js} +1 -1
  16. package/dist/assets/{MonitoringModal-CUXXMqLv.js → MonitoringModal-BcKOCSRM.js} +1 -1
  17. package/dist/assets/{OrbitControls-DS7gPhsZ.js → OrbitControls-ANLmgNeR.js} +1 -1
  18. package/dist/assets/{PM2LogsModal-DFAuRDwm.js → PM2LogsModal-DrHpuMNf.js} +1 -1
  19. package/dist/assets/{RestoreArchivedAreaModal-DT5-U0A8.js → RestoreArchivedAreaModal-DcD3vdUJ.js} +1 -1
  20. package/dist/assets/{Scene2DCanvas-DqakJMdS.js → Scene2DCanvas-DKUcFYZW.js} +1 -1
  21. package/dist/assets/SceneManager-DPojKMM-.js +104 -0
  22. package/dist/assets/{SkillsPanel-1hYjREqo.js → SkillsPanel-CUW0prbh.js} +1 -1
  23. package/dist/assets/{SlackMultiInstanceSetup-C6EtUfP8.js → SlackMultiInstanceSetup-YFYeZ94t.js} +1 -1
  24. package/dist/assets/{SpawnModal-BmMZFOUj.js → SpawnModal-DPvq-R5b.js} +1 -1
  25. package/dist/assets/{StatisticsModal-mmqE6JaF.js → StatisticsModal-Dt-Hof51.js} +1 -1
  26. package/dist/assets/{StlViewer-D0XChi1U.js → StlViewer-GyyomEax.js} +1 -1
  27. package/dist/assets/{SubordinateAssignmentModal-CBISWK7c.js → SubordinateAssignmentModal-ahzGBjr4.js} +1 -1
  28. package/dist/assets/{ThreeAreaSelector-C_7zwy_Q.js → ThreeAreaSelector-Bj_mL6NG.js} +1 -1
  29. package/dist/assets/{ThreeViewShortcuts-DZZmjQAV.js → ThreeViewShortcuts-CKtE3yZV.js} +1 -1
  30. package/dist/assets/{TriggerManagerPanel-Bwd5-VGg.js → TriggerManagerPanel-DtdbOk9B.js} +1 -1
  31. package/dist/assets/{VideoPlayer-BuFPsPNu.js → VideoPlayer-Crq-MwPS.js} +1 -1
  32. package/dist/assets/{WorkflowEditorPanel-DxQfrs71.js → WorkflowEditorPanel-CitR9Em3.js} +1 -1
  33. package/dist/assets/{abnfDiagram-VRR7QNED-AhR0vtDs.js → abnfDiagram-VRR7QNED-QFcuJYPe.js} +1 -1
  34. package/dist/assets/{arc-CTR1mrNz.js → arc-CLuSzQ4E.js} +1 -1
  35. package/dist/assets/{architectureDiagram-ZJ3FMSHR-DrsB_-NW.js → architectureDiagram-ZJ3FMSHR-DlhVck93.js} +1 -1
  36. package/dist/assets/{area-logos-D_dodLqh.js → area-logos-DYARiYqI.js} +1 -1
  37. package/dist/assets/{blockDiagram-677ZJIJ3-Bf6BktPD.js → blockDiagram-677ZJIJ3-CJwOmsfj.js} +1 -1
  38. package/dist/assets/{c4Diagram-LMCZKHZV--UrsSHrg.js → c4Diagram-LMCZKHZV-OsgCNPWJ.js} +1 -1
  39. package/dist/assets/channel-cpl_NpAy.js +1 -0
  40. package/dist/assets/{chunk-2Q5K7J3B-DajcrDU0.js → chunk-2Q5K7J3B-KG-L7vvm.js} +1 -1
  41. package/dist/assets/{chunk-32BRIVSS-BoJToPN6.js → chunk-32BRIVSS-BXGbqO9v.js} +1 -1
  42. package/dist/assets/{chunk-5VM5RSS4-CWyIsEjs.js → chunk-5VM5RSS4-BWXkm8f0.js} +1 -1
  43. package/dist/assets/{chunk-EX3LRPZG-BxQt59EY.js → chunk-EX3LRPZG-7-8ulJ19.js} +1 -1
  44. package/dist/assets/{chunk-JWPE2WC7-CcltxnxE.js → chunk-JWPE2WC7-mkwFN5xu.js} +1 -1
  45. package/dist/assets/{chunk-MOJQB5TN-CoFz0cis.js → chunk-MOJQB5TN-WEfWznKE.js} +1 -1
  46. package/dist/assets/{chunk-RYQCIY6F-CqD5SIAs.js → chunk-RYQCIY6F-ByP5odu4.js} +1 -1
  47. package/dist/assets/{chunk-V7JOEXUC-B38vYRK3.js → chunk-V7JOEXUC-yWHmp1L1.js} +1 -1
  48. package/dist/assets/{chunk-VR4S4FIN-CVwpBl0T.js → chunk-VR4S4FIN-DHbZaHzt.js} +1 -1
  49. package/dist/assets/{chunk-XXDRQBXY-DznOU5hd.js → chunk-XXDRQBXY-SNa-Wh3U.js} +1 -1
  50. package/dist/assets/classDiagram-OUVF2IWQ-D_lsO4mm.js +1 -0
  51. package/dist/assets/classDiagram-v2-EOCWNBFH-D_lsO4mm.js +1 -0
  52. package/dist/assets/{cose-bilkent-JH36ORCC-B1uTqWwQ.js → cose-bilkent-JH36ORCC-CyxHYUhP.js} +1 -1
  53. package/dist/assets/{cynefin-VYW2F7L2-DgpqdGRT.js → cynefin-VYW2F7L2-BvwjSDiQ.js} +1 -1
  54. package/dist/assets/{cynefinDiagram-TSTJHNR4-Ld74Dvow.js → cynefinDiagram-TSTJHNR4-C8doInhn.js} +1 -1
  55. package/dist/assets/{dagre-VKFMJZFB-DAp2Br8X.js → dagre-VKFMJZFB-BNDcupek.js} +1 -1
  56. package/dist/assets/{diagram-FQU43EPY-CaMUb0eE.js → diagram-FQU43EPY-Bst0bBnQ.js} +1 -1
  57. package/dist/assets/{diagram-G47NLZAW-kTOq861A.js → diagram-G47NLZAW-CfEv0aTY.js} +1 -1
  58. package/dist/assets/{diagram-NH7WQ7WH-LWZrE7zu.js → diagram-NH7WQ7WH-B7MYvWXL.js} +1 -1
  59. package/dist/assets/{diagram-OA4YK3LP-Csm-frID.js → diagram-OA4YK3LP-BIzII0d2.js} +1 -1
  60. package/dist/assets/{diagram-WEI45ONY-BconZEMM.js → diagram-WEI45ONY-D5yd1HWr.js} +1 -1
  61. package/dist/assets/{ebnfDiagram-CCIWWBDH-Cr5xwAgl.js → ebnfDiagram-CCIWWBDH-gyhVYUos.js} +1 -1
  62. package/dist/assets/{erDiagram-Q63AITRT-76P2E9Q6.js → erDiagram-Q63AITRT-Duz-DwfE.js} +1 -1
  63. package/dist/assets/{flowDiagram-23GEKE2U-CciJAjTA.js → flowDiagram-23GEKE2U-CrnARkmw.js} +1 -1
  64. package/dist/assets/{ganttDiagram-NO4QXBWP-D7VgBp68.js → ganttDiagram-NO4QXBWP-D6cBteIN.js} +1 -1
  65. package/dist/assets/{gitGraphDiagram-IHSO6WYX-CxICR8Bg.js → gitGraphDiagram-IHSO6WYX-CMHfr_lR.js} +1 -1
  66. package/dist/assets/{index-Cd53q1WZ.js → index-2427ATYD.js} +3 -3
  67. package/dist/assets/{index-lQifR7XC.js → index-B1vOCcI-.js} +1 -1
  68. package/dist/assets/index-B5wHDczA.js +2 -0
  69. package/dist/assets/{index-BNsHVfai.js → index-CbMtvQTH.js} +1 -1
  70. package/dist/assets/{index-DzzLdCr9.js → index-D5O_0aq9.js} +1 -1
  71. package/dist/assets/{index-Bfmfqbw7.js → index-D5pIRxFm.js} +1 -1
  72. package/dist/assets/index-DJKuwqJG.js +2 -0
  73. package/dist/assets/{index-Dc4yP2fA.js → index-DeqoOoo6.js} +3 -3
  74. package/dist/assets/{index-Z0bYekBH.js → index-fiC5OXgR.js} +1 -1
  75. package/dist/assets/{infoDiagram-FWYZ7A6U-h-R7gGrP.js → infoDiagram-FWYZ7A6U-Mvk2Mpp3.js} +1 -1
  76. package/dist/assets/{ishikawaDiagram-FXEZZL3T-D1RbbiMz.js → ishikawaDiagram-FXEZZL3T-DXsaNDYg.js} +1 -1
  77. package/dist/assets/{journeyDiagram-5HDEW3XC-BnNUbZMI.js → journeyDiagram-5HDEW3XC-6QlK8xHN.js} +1 -1
  78. package/dist/assets/{kanban-definition-HUTT4EX6-kX3kneLI.js → kanban-definition-HUTT4EX6-D5EeRs07.js} +1 -1
  79. package/dist/assets/{linear-DC5X1tTJ.js → linear-D_aP9MD5.js} +1 -1
  80. package/dist/assets/main-B2ZLroL1.css +1 -0
  81. package/dist/assets/{main-Bb9H5jmt.js → main-Dk3qq80S.js} +156 -156
  82. package/dist/assets/{mermaid.core-BE33DHym.js → mermaid.core-CEculPxD.js} +4 -4
  83. package/dist/assets/{mindmap-definition-LN4V7U3C-CY3A4tSH.js → mindmap-definition-LN4V7U3C-Vjj9vgEN.js} +1 -1
  84. package/dist/assets/{pegDiagram-2B236MQR-B99ZSEym.js → pegDiagram-2B236MQR-B-AwbaoO.js} +1 -1
  85. package/dist/assets/{pieDiagram-ENE6RG2P-7PsmMteT.js → pieDiagram-ENE6RG2P-D5o1UYPK.js} +1 -1
  86. package/dist/assets/{quadrantDiagram-ABIIQ3AL-DjShiKUv.js → quadrantDiagram-ABIIQ3AL-Bl_Rbmz-.js} +1 -1
  87. package/dist/assets/{railroadDiagram-RFXS5EU6-B6KuCiHH.js → railroadDiagram-RFXS5EU6-CW-qy1dp.js} +1 -1
  88. package/dist/assets/{requirementDiagram-TGXJPOKE-k_7TCScw.js → requirementDiagram-TGXJPOKE-DGUQpy3K.js} +1 -1
  89. package/dist/assets/{sankeyDiagram-HTMAVEWB-D2YbTfpK.js → sankeyDiagram-HTMAVEWB-CUlWZH9G.js} +1 -1
  90. package/dist/assets/{sequenceDiagram-DBY2YBRQ-CuYh53OC.js → sequenceDiagram-DBY2YBRQ-CeDVJrbJ.js} +1 -1
  91. package/dist/assets/{sizeCapture-X5ZJPWSS-D4_qcFkr.js → sizeCapture-X5ZJPWSS-BGSL3ofq.js} +1 -1
  92. package/dist/assets/{stateDiagram-2N3HPSRC-DO9QVMBT.js → stateDiagram-2N3HPSRC-9A8tAPIu.js} +1 -1
  93. package/dist/assets/stateDiagram-v2-6OUMAXLB-DzVFFq0g.js +1 -0
  94. package/dist/assets/{swimlanes-5IMT3BWC-gMS-z6zh.js → swimlanes-5IMT3BWC-CipRR1uk.js} +2 -2
  95. package/dist/assets/swimlanesDiagram-G3AALYLV-XDQw-C4e.js +8 -0
  96. package/dist/assets/{timeline-definition-FHXFAJF6-CUmQCwOG.js → timeline-definition-FHXFAJF6-IgrCsFe-.js} +1 -1
  97. package/dist/assets/{vendor-three-CBPuP1jE.js → vendor-three-DYBBrXTx.js} +1 -1
  98. package/dist/assets/{vennDiagram-L72KCM5P-CIP-5b8u.js → vennDiagram-L72KCM5P-okSGsDy3.js} +1 -1
  99. package/dist/assets/{wardleyDiagram-EHGQE667-DlQqKMjr.js → wardleyDiagram-EHGQE667-G0RLggUt.js} +1 -1
  100. package/dist/assets/{web-DmAaMs-2.js → web-B5BfOO5C.js} +1 -1
  101. package/dist/assets/{web-D42BXPx5.js → web-BPOqvnJL.js} +1 -1
  102. package/dist/assets/{web-DVkGXGfI.js → web-Q8DwQW0W.js} +1 -1
  103. package/dist/assets/{xychartDiagram-FW5EYKEG-CekYCp3d.js → xychartDiagram-FW5EYKEG-DtVJEFyI.js} +1 -1
  104. package/dist/build-info.json +1 -1
  105. package/dist/index.html +3 -3
  106. package/dist/src/packages/server/app.js +26 -1
  107. package/dist/src/packages/server/claude/backend.js +5 -4
  108. package/dist/src/packages/server/claude/runner/stdout-pipeline.js +29 -30
  109. package/dist/src/packages/server/claude/runner/watchdog.js +2 -1
  110. package/dist/src/packages/server/claude/session-loader.js +141 -81
  111. package/dist/src/packages/server/data/index.js +35 -2
  112. package/dist/src/packages/server/routes/perf.js +50 -39
  113. package/dist/src/packages/server/services/git-watch-service.js +14 -2
  114. package/dist/src/packages/server/services/runtime-service.js +85 -39
  115. package/dist/src/packages/server/websocket/handler.js +33 -8
  116. package/dist/src/packages/server/websocket/listeners/runtime-listeners.js +5 -2
  117. package/package.json +3 -1
  118. package/dist/assets/SceneManager-htGN7YfA.js +0 -104
  119. package/dist/assets/channel-BkokQZXi.js +0 -1
  120. package/dist/assets/classDiagram-OUVF2IWQ-DibSrtP1.js +0 -1
  121. package/dist/assets/classDiagram-v2-EOCWNBFH-DibSrtP1.js +0 -1
  122. package/dist/assets/index-C8zT43_7.js +0 -2
  123. package/dist/assets/index-WhxnTPGa.js +0 -2
  124. package/dist/assets/main-CSv15YYo.css +0 -1
  125. package/dist/assets/stateDiagram-v2-6OUMAXLB-CekJ4qac.js +0 -1
  126. 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
- function getProviderProcessPids(provider, execSync) {
3849
- const pattern = PROVIDER_PROCESS_PATTERNS[provider];
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
- const psOutput = execSync(`ps aux | grep -E "${pattern}" | grep -v grep | awk '{print $2}'`, {
3852
- encoding: 'utf-8',
3853
- timeout: 5000,
3854
- }).trim();
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 psOutput.split('\n').filter(p => p.trim());
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
- function getProcessCwd(pid, execSync) {
3865
- try {
3866
- if (process.platform === 'darwin') {
3867
- const lsofOutput = execSync(`lsof -a -d cwd -p ${pid} -Fn 2>/dev/null | grep '^n'`, {
3868
- encoding: 'utf-8',
3869
- timeout: 2000,
3870
- shell: '/bin/bash',
3871
- }).trim();
3872
- if (!lsofOutput.startsWith('n')) {
3873
- return null;
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
- return execSync(`readlink /proc/${pid}/cwd`, {
3878
- encoding: 'utf-8',
3879
- timeout: 1000,
3880
- }).trim();
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
- catch {
3883
- return null;
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 { execSync } = await import('child_process');
3893
- const pids = getProviderProcessPids(provider, execSync);
3894
- if (pids.length === 0) {
3990
+ const match = findProviderProcessInSnapshot(await snapshotProviderProcesses(), cwd, provider);
3991
+ if (!match) {
3895
3992
  return false;
3896
3993
  }
3897
- const normalizedCwd = cwd.replace(/\/+$/, '');
3898
- for (const pid of pids) {
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
- const { execSync } = await import('child_process');
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
- const { execSync } = await import('child_process');
3954
- const pids = getProviderProcessPids(provider, execSync);
3955
- if (pids.length === 0) {
3956
- return false;
3957
- }
3958
- const normalizedCwd = cwd.replace(/\/+$/, '');
3959
- for (const pid of pids) {
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 label = providerDisplayName(provider);
4029
+ const pid = proc.pid;
3969
4030
  log.log(`🛑 Killing detached ${label} process ${pid} in ${cwd}`);
3970
4031
  try {
3971
- const numericPid = parseInt(pid, 10);
3972
- process.kill(numericPid, 'SIGTERM');
4032
+ process.kill(pid, 'SIGTERM');
3973
4033
  setTimeout(() => {
3974
4034
  try {
3975
- process.kill(numericPid, 0);
4035
+ process.kill(pid, 0);
3976
4036
  log.log(`🛑 Force killing ${label} process ${pid}`);
3977
- process.kill(numericPid, 'SIGKILL');
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
- log.log(` Loaded ${data.processes.length} running process records (from commander PID ${data.commanderPid})`);
554
- return data.processes;
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 { execSync } from 'child_process';
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
- function getProcessMemoryMB(pid) {
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 = execSync(`cat /proc/${pid}/status 2>/dev/null | grep VmRSS`, {
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
- const psOutput = execSync(`ps -o rss= -p ${pid}`, { encoding: 'utf8', timeout: 500 });
51
- const kB = parseInt(psOutput.trim(), 10);
52
- if (!isNaN(kB))
53
- return Math.round(kB / 1024);
54
- }
55
- catch {
56
- // Process may not exist
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
- for (const agent of agents) {
79
- try {
80
- const info = await runtimeService.getAgentRuntimeProcessInfo(agent.id);
81
- rawProcessInfo.push({
82
- id: agent.id,
83
- name: agent.name,
84
- status: agent.status,
85
- source: info.source,
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
- for (const info of rawProcessInfo) {
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
- agentProcesses.push({
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
- log.warn(`[GitWatch] ${dirPath} has ${totalFiles} changed files — pushing the first ${MAX_STATUS_FILES}`);
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, findClaudeProcessPidInCwd, findCodexProcessPidInCwd, findOpencodeProcessPidInCwd, findGrokProcessPidInCwd, findPiProcessPidInCwd, } from '../claude/session-loader.js';
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
- log.log(`[EMIT] event: type=${standardEvent?.type} tool=${standardEvent?.toolName || 'n/a'} listeners=${listenerCount}`);
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
- // Check all in-memory runners first so provider drift/migration doesn't hide live processes.
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 = runnerWithState.getActiveProcessesState().find((proc) => proc.agentId === agentId);
335
- if (state?.pid) {
336
- return {
337
- pid: state.pid,
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 persistedProcess = loadRunningProcesses().find((proc) => proc.agentId === agentId);
344
- if (persistedProcess?.pid && isProcessRunning(persistedProcess.pid)) {
345
- return {
346
- pid: persistedProcess.pid,
347
- isRunning: true,
348
- source: 'persisted',
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
- const agent = agentService.getAgent(agentId);
352
- if (agent?.cwd) {
353
- const provider = agent.provider ?? 'claude';
354
- const discoveredPid = provider === 'codex'
355
- ? await findCodexProcessPidInCwd(agent.cwd)
356
- : provider === 'opencode'
357
- ? await findOpencodeProcessPidInCwd(agent.cwd)
358
- : provider === 'grok'
359
- ? await findGrokProcessPidInCwd(agent.cwd)
360
- : provider === 'pi'
361
- ? await findPiProcessPidInCwd(agent.cwd)
362
- : await findClaudeProcessPidInCwd(agent.cwd);
363
- if (discoveredPid && isProcessRunning(discoveredPid)) {
364
- return {
365
- pid: discoveredPid,
366
- isRunning: true,
367
- source: 'discovered',
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