nolo-cli 0.156.0 → 0.157.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/{_executeModel-3FMHTHSS.js → _executeModel-ZIQHOREX.js} +13 -13
  2. package/{agentDeleteCommand-VRHLJZKX.js → agentDeleteCommand-LQSOJEXY.js} +11 -11
  3. package/{agentEmailCommands-YGBQXOL3.js → agentEmailCommands-HTPKXONW.js} +13 -13
  4. package/{agentMachineCommands-FBAWZYHI.js → agentMachineCommands-T7DAZG77.js} +1 -1
  5. package/{agentRecordCommands-KAK2G56H.js → agentRecordCommands-HSW3RJ4H.js} +12 -12
  6. package/{agentRunCommand-62P3AGJA.js → agentRunCommand-TIRJ57DK.js} +13 -13
  7. package/{agentRunControl-XBSNHHZE.js → agentRunControl-QWFFIAQY.js} +3 -1
  8. package/{agentSlice-IZFU3WWH.js → agentSlice-JNM3HF3V.js} +1 -1
  9. package/{chunk-JIAZK434.js → chunk-4226O6F7.js} +2 -2
  10. package/{chunk-HRZW3SQN.js → chunk-5EDQTOUR.js} +1 -1
  11. package/{chunk-K3N2DWLT.js → chunk-6Y3ETA2Y.js} +2 -2
  12. package/{chunk-4FDDVVCE.js → chunk-B3ZIFOFI.js} +2 -2
  13. package/{chunk-U5U4WAFS.js → chunk-FHG46N5C.js} +36 -3
  14. package/{chunk-7YQQXP42.js → chunk-GGUTTH6C.js} +2 -2
  15. package/{chunk-FTHF2P2I.js → chunk-IKOKYBYJ.js} +1 -1
  16. package/{chunk-6QIBWOPT.js → chunk-K6Y5T4GE.js} +2 -2
  17. package/{chunk-CI6YDDL2.js → chunk-KESKU5BG.js} +2 -2
  18. package/{chunk-CNWKLQRL.js → chunk-KSTEXKL6.js} +70 -36
  19. package/{chunk-SNAWGDXT.js → chunk-LPO6AMQU.js} +526 -69
  20. package/{chunk-UUPXTEZA.js → chunk-MLEANT7D.js} +1 -1
  21. package/{chunk-7PDNSQZC.js → chunk-MMSQTZCJ.js} +2 -2
  22. package/{chunk-QFL42TMV.js → chunk-MR3ITQEV.js} +102 -20
  23. package/{chunk-PNACBSRF.js → chunk-NLFTBC6S.js} +194 -31
  24. package/{chunk-Q5LVXPGT.js → chunk-OLUKIUX6.js} +2 -2
  25. package/{chunk-EDJND3FK.js → chunk-R7GCGOGZ.js} +3 -3
  26. package/{chunk-V67W64OY.js → chunk-UVYSBQ2M.js} +7 -7
  27. package/{chunk-2QQ7D3P5.js → chunk-XZIW3Q5B.js} +8 -8
  28. package/{chunk-36IWXF25.js → chunk-YN5DT4JS.js} +5 -5
  29. package/{chunk-GXZZOTDM.js → chunk-ZKVS36H3.js} +4545 -3343
  30. package/{commandRegistry-2DDDET7G.js → commandRegistry-DR2RPWK6.js} +26 -26
  31. package/{contentThunks-5DNEHJ2Z.js → contentThunks-CSJDJDEJ.js} +3 -3
  32. package/{createAgentAutomationAction-4SUS2EFX.js → createAgentAutomationAction-ISEUEQSY.js} +3 -3
  33. package/{createDialogAction-JSFXX6JE.js → createDialogAction-HUCFOZY3.js} +4 -4
  34. package/{createPageAction-37HHOXZ2.js → createPageAction-DT5CYXCL.js} +4 -4
  35. package/{dialogCommands-ZJRMCP4S.js → dialogCommands-3W6AIUXT.js} +2 -2
  36. package/{dialogSlice-2Z66AWEW.js → dialogSlice-WJEEPG4W.js} +2 -2
  37. package/{docStore-XMTUF3GX.js → docStore-7JPYT25V.js} +1 -1
  38. package/{handleSendMessageAction-4VFVXTF2.js → handleSendMessageAction-BV5FMID4.js} +7 -7
  39. package/index.js +3 -3
  40. package/{localRuntimeAdapter-KKIVRYVN.js → localRuntimeAdapter-6FBEJJ4D.js} +11 -11
  41. package/{machineCommands-UHQVCOD3.js → machineCommands-WCAFDI5N.js} +12 -12
  42. package/{machineWsRunDispatch-TX4X7TOJ.js → machineWsRunDispatch-ZF6BBQ3X.js} +12 -12
  43. package/package.json +1 -1
  44. package/{readlineWorkspace-BFVB2ON5.js → readlineWorkspace-I5MHMEUE.js} +14 -14
  45. package/{runAgentClientLoop-HHLHNMBE.js → runAgentClientLoop-TSNSRQEP.js} +11 -11
  46. package/{streamAgentChatTurn-KNPOLET7.js → streamAgentChatTurn-RJLDV4WZ.js} +13 -13
  47. package/{tools-ULA7XJQT.js → tools-X6QNIYFJ.js} +8 -8
  48. package/{updateDialogTitleAction-VUIVBDAS.js → updateDialogTitleAction-6LURCTMG.js} +3 -3
@@ -320,8 +320,8 @@ function createShellApprovalChild(channel, timeoutMs = TTL) {
320
320
 
321
321
  // packages/cli/agentRunControl.ts
322
322
  import { homedir as nodeHomedir } from "node:os";
323
- import { dirname, isAbsolute as isAbsolute2, join, resolve } from "node:path";
324
- import * as nodeFs from "node:fs";
323
+ import { dirname, isAbsolute as isAbsolute2, join as join2, resolve } from "node:path";
324
+ import * as nodeFs2 from "node:fs";
325
325
  import { execFileSync as nodeExecFileSync, spawn as nodeSpawn } from "node:child_process";
326
326
 
327
327
  // packages/cli/client/hostMemory.ts
@@ -551,6 +551,404 @@ function formatHostMemoryLine(mem) {
551
551
  return isHostMemoryTight(mem) ? `${line} \xB7 ${HOST_MEMORY_PRESSURE_HINT}` : line;
552
552
  }
553
553
 
554
+ // packages/cli/client/hostMemoryReservation.ts
555
+ import { join } from "path";
556
+ import * as nodeFs from "fs";
557
+
558
+ // packages/cli/client/hostMemoryAdmission.ts
559
+ var LOCAL_AGENT_RUN_MIN_AVAILABLE_MB = 512;
560
+ var STARTUP_WINDOW_MS = 1e4;
561
+ var STARTUP_WINDOW_RESERVED_MB_PER_RUN = 384;
562
+ var HostMemoryAdmissionError = class extends Error {
563
+ code = "HOST_MEMORY_ADMISSION_DENIED";
564
+ details;
565
+ constructor(message, details) {
566
+ super(message);
567
+ this.name = "HostMemoryAdmissionError";
568
+ this.details = details;
569
+ }
570
+ };
571
+ function assertHostMemoryAdmission(deps = {}) {
572
+ const readMem = deps.readHostMemory ?? readHostMemory;
573
+ const snapshot = readMem(deps.hostMemoryDeps);
574
+ const minAvailableMb = deps.minAvailableMb ?? LOCAL_AGENT_RUN_MIN_AVAILABLE_MB;
575
+ const startupWindowMs = deps.startupWindowMs ?? STARTUP_WINDOW_MS;
576
+ const reservedMbPerRun = deps.reservedMbPerRun ?? STARTUP_WINDOW_RESERVED_MB_PER_RUN;
577
+ const now = deps.now ? deps.now().getTime() : Date.now();
578
+ for (const [name, value] of Object.entries({ minAvailableMb, startupWindowMs, reservedMbPerRun, now })) {
579
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) throw new Error(`Invalid ${name}`);
580
+ }
581
+ if (!snapshot) {
582
+ throw new HostMemoryAdmissionError(
583
+ `[host-memory-admission] \u65E0\u6CD5\u6D4B\u5B9A\u672C\u673A\u53EF\u7528\u5185\u5B58\uFF08\u5FEB\u7167\u4E3A null\uFF09\uFF0C\u62D2\u7EDD\u672C\u5730 fork\u3002\u9700\u786E\u4FDD /proc/meminfo \u6216 OS \u5185\u5B58\u6307\u6807\u53EF\u8BFB\u3002`,
584
+ {
585
+ effectiveAvailableMb: null,
586
+ requiredMinMb: minAvailableMb,
587
+ source: "null",
588
+ limitingSource: "unknown",
589
+ activeStartupRuns: 0,
590
+ reservedStartupMb: 0,
591
+ unreservedAvailableMb: null
592
+ }
593
+ );
594
+ }
595
+ if (!["cgroup", "proc-meminfo", "os"].includes(snapshot.source)) throw new Error("Unknown host memory source");
596
+ let effectiveAvailableMb = null;
597
+ let limitingSource = "host";
598
+ if (snapshot.source === "cgroup") {
599
+ const cgroupAvail = snapshot.availableMb;
600
+ const hostAvail = snapshot.hostAvailableMb;
601
+ if (cgroupAvail === null || typeof cgroupAvail !== "number" || Number.isNaN(cgroupAvail) || !Number.isFinite(cgroupAvail) || cgroupAvail < 0) {
602
+ throw new HostMemoryAdmissionError(
603
+ `[host-memory-admission] cgroup \u914D\u989D\u5185\u5B58\u5360\u7528\u672A\u77E5\u6216\u65E0\u6548\uFF0C\u62D2\u7EDD\u65E0\u58F0\u5F53\u5145\u8DB3\u3002`,
604
+ {
605
+ effectiveAvailableMb: null,
606
+ requiredMinMb: minAvailableMb,
607
+ source: snapshot.source,
608
+ limitingSource: "cgroup",
609
+ activeStartupRuns: 0,
610
+ reservedStartupMb: 0,
611
+ unreservedAvailableMb: null
612
+ }
613
+ );
614
+ }
615
+ if (typeof hostAvail !== "number" || !Number.isFinite(hostAvail) || hostAvail < 0) {
616
+ throw new HostMemoryAdmissionError("[host-memory-admission] \u5BBF\u4E3B\u53EF\u7528\u5185\u5B58\u672A\u77E5\u6216\u65E0\u6548\uFF0C\u62D2\u7EDD\u672C\u5730 fork\u3002", {
617
+ effectiveAvailableMb: null,
618
+ requiredMinMb: minAvailableMb,
619
+ source: snapshot.source,
620
+ limitingSource: "host",
621
+ activeStartupRuns: 0,
622
+ reservedStartupMb: 0,
623
+ unreservedAvailableMb: null
624
+ });
625
+ }
626
+ if (Number.isFinite(hostAvail)) {
627
+ if (cgroupAvail <= hostAvail) {
628
+ effectiveAvailableMb = cgroupAvail;
629
+ limitingSource = "cgroup";
630
+ } else {
631
+ effectiveAvailableMb = hostAvail;
632
+ limitingSource = "host";
633
+ }
634
+ } else {
635
+ effectiveAvailableMb = cgroupAvail;
636
+ limitingSource = "cgroup";
637
+ }
638
+ } else {
639
+ if (snapshot.availableMb === null || typeof snapshot.availableMb !== "number" || Number.isNaN(snapshot.availableMb) || !Number.isFinite(snapshot.availableMb) || snapshot.availableMb < 0) {
640
+ throw new HostMemoryAdmissionError(
641
+ `[host-memory-admission] \u5BBF\u4E3B\u53EF\u7528\u5185\u5B58\u672A\u77E5\u6216\u65E0\u6548\uFF0C\u62D2\u7EDD\u65E0\u58F0\u5F53\u5145\u8DB3\u3002`,
642
+ {
643
+ effectiveAvailableMb: null,
644
+ requiredMinMb: minAvailableMb,
645
+ source: snapshot.source,
646
+ limitingSource: "host",
647
+ activeStartupRuns: 0,
648
+ reservedStartupMb: 0,
649
+ unreservedAvailableMb: null
650
+ }
651
+ );
652
+ }
653
+ effectiveAvailableMb = snapshot.availableMb;
654
+ limitingSource = "host";
655
+ }
656
+ let activeStartupRuns = 0;
657
+ if (deps.listRecentRuns) {
658
+ const records = deps.listRecentRuns();
659
+ if (!Array.isArray(records)) {
660
+ throw new HostMemoryAdmissionError(
661
+ `[host-memory-admission] \u83B7\u53D6\u6700\u8FD1\u8FD0\u884C\u8BB0\u5F55\u5217\u8868\u5931\u8D25\uFF0C\u62D2\u7EDD\u672A\u77E5\u5F53\u5F52\u96F6\u3002`,
662
+ {
663
+ effectiveAvailableMb,
664
+ requiredMinMb: minAvailableMb,
665
+ source: snapshot.source,
666
+ limitingSource,
667
+ activeStartupRuns: 0,
668
+ reservedStartupMb: 0,
669
+ unreservedAvailableMb: effectiveAvailableMb
670
+ }
671
+ );
672
+ }
673
+ for (const rec of records) {
674
+ if (!rec || typeof rec !== "object") {
675
+ throw new HostMemoryAdmissionError(
676
+ `[host-memory-admission] \u8FD0\u884C\u8BB0\u5F55\u635F\u574F\uFF0C\u62D2\u7EDD\u672A\u77E5\u5F53\u5F52\u96F6\u3002`,
677
+ {
678
+ effectiveAvailableMb,
679
+ requiredMinMb: minAvailableMb,
680
+ source: snapshot.source,
681
+ limitingSource,
682
+ activeStartupRuns: 0,
683
+ reservedStartupMb: 0,
684
+ unreservedAvailableMb: effectiveAvailableMb
685
+ }
686
+ );
687
+ }
688
+ if (rec.status !== "running") continue;
689
+ const startedTime = new Date(rec.startedAt).getTime();
690
+ if (!Number.isFinite(startedTime) || Number.isNaN(startedTime)) {
691
+ throw new HostMemoryAdmissionError(
692
+ `[host-memory-admission] \u8FD0\u884C\u8BB0\u5F55 startedAt \u65E0\u6548\uFF0C\u62D2\u7EDD\u672A\u77E5\u5F53\u5F52\u96F6\u3002`,
693
+ {
694
+ effectiveAvailableMb,
695
+ requiredMinMb: minAvailableMb,
696
+ source: snapshot.source,
697
+ limitingSource,
698
+ activeStartupRuns: 0,
699
+ reservedStartupMb: 0,
700
+ unreservedAvailableMb: effectiveAvailableMb
701
+ }
702
+ );
703
+ }
704
+ if (now - startedTime >= 0 && now - startedTime < startupWindowMs) {
705
+ activeStartupRuns += 1;
706
+ }
707
+ }
708
+ }
709
+ const reservedStartupMb = activeStartupRuns * reservedMbPerRun;
710
+ const netAvailableMb = effectiveAvailableMb - reservedStartupMb;
711
+ if (netAvailableMb < minAvailableMb) {
712
+ const detailMsg = `\u5F53\u524D\u6709\u6548\u53EF\u7528 ${formatHostMemoryMb(effectiveAvailableMb)}\uFF08\u53D7\u9650\u6E90: ${limitingSource}\uFF09` + (activeStartupRuns > 0 ? `\uFF0C\u5E76\u53D1\u542F\u52A8\u7A97\u53E3\u5185\u6709 ${activeStartupRuns} \u4E2A\u8FDB\u884C\u4E2D\u4EFB\u52A1\uFF08\u9884\u7559 ${formatHostMemoryMb(reservedStartupMb)}\uFF0C\u6298\u7B97\u5269\u4F59 ${formatHostMemoryMb(netAvailableMb)}\uFF09` : "") + `\uFF0C\u4F4E\u4E8E\u5B89\u5168\u9608\u503C ${formatHostMemoryMb(minAvailableMb)}\u3002`;
713
+ throw new HostMemoryAdmissionError(
714
+ `[host-memory-admission] \u62D2\u7EDD\u672C\u5730\u6D3E\u53D1\uFF1A${detailMsg}\u4E3A\u907F\u514D\u89E6\u53D1\u5BBF\u4E3B/cgroup OOM-killer \u5D29\u6E83\uFF0C\u8BF7\u7B49\u5F85\u5176\u4ED6\u4EFB\u52A1\u5B8C\u6210\u6216\u91CA\u653E\u5185\u5B58\u540E\u518D\u8BD5\u3002`,
715
+ {
716
+ effectiveAvailableMb: netAvailableMb,
717
+ requiredMinMb: minAvailableMb,
718
+ source: snapshot.source,
719
+ limitingSource,
720
+ activeStartupRuns,
721
+ reservedStartupMb,
722
+ unreservedAvailableMb: effectiveAvailableMb
723
+ }
724
+ );
725
+ }
726
+ return {
727
+ allowed: true,
728
+ effectiveAvailableMb: netAvailableMb,
729
+ requiredMinMb: minAvailableMb,
730
+ activeStartupRuns,
731
+ snapshot
732
+ };
733
+ }
734
+
735
+ // packages/cli/client/hostMemoryReservation.ts
736
+ var MEMORY_RESERVATION_LOCK_ID = "__host_memory_reservation__";
737
+ function resolveReservationsDir(runsDir) {
738
+ return join(runsDir, "reservations");
739
+ }
740
+ function defaultIsProcessAlive(pid) {
741
+ if (!Number.isInteger(pid) || pid <= 0) throw new Error("Invalid PID");
742
+ try {
743
+ process.kill(pid, 0);
744
+ return true;
745
+ } catch (err) {
746
+ if (err && err.code === "ESRCH") {
747
+ return false;
748
+ }
749
+ return true;
750
+ }
751
+ }
752
+ function sleepSync(ms) {
753
+ if (ms <= 0) return;
754
+ try {
755
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
756
+ } catch {
757
+ const end = Date.now() + ms;
758
+ while (Date.now() < end) {
759
+ }
760
+ }
761
+ }
762
+ function updateReservationFile(filePath, fs, updater) {
763
+ const record = JSON.parse(fs.readFileSync(filePath, "utf8"));
764
+ updater(record);
765
+ const payload = JSON.stringify(record, null, 2);
766
+ if (fs.renameSync) {
767
+ const tmpPath = `${filePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
768
+ fs.writeFileSync(tmpPath, payload);
769
+ fs.renameSync(tmpPath, filePath);
770
+ } else {
771
+ fs.writeFileSync(filePath, payload);
772
+ }
773
+ return record;
774
+ }
775
+ function scanActiveReservations(reservationsDir, deps) {
776
+ const { fs, nowMs, startupWindowMs, isProcessAlive } = deps;
777
+ if (!fs.existsSync(reservationsDir)) {
778
+ return { activeStartupRuns: 0, reservedStartupMb: 0 };
779
+ }
780
+ let entries;
781
+ try {
782
+ entries = fs.readdirSync(reservationsDir);
783
+ } catch (err) {
784
+ throw new Error(
785
+ `[host-memory-reservation] \u8BFB\u53D6\u9884\u7559\u76EE\u5F55\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`
786
+ );
787
+ }
788
+ let activeStartupRuns = 0;
789
+ let reservedStartupMb = 0;
790
+ for (const entry of entries) {
791
+ if (!entry.endsWith(".json") || !entry.startsWith("reservation-")) continue;
792
+ const filePath = join(reservationsDir, entry);
793
+ let record;
794
+ try {
795
+ const raw = fs.readFileSync(filePath, "utf8");
796
+ record = JSON.parse(raw);
797
+ } catch (err) {
798
+ throw new Error(
799
+ `[host-memory-reservation] \u89E3\u6790\u9884\u7559\u8BB0\u5F55\u6587\u4EF6\u5931\u8D25 (${filePath}): ${err instanceof Error ? err.message : String(err)}\uFF0C\u62D2\u7EDD\u672A\u77E5\u5F53\u5F52\u96F6`
800
+ );
801
+ }
802
+ if (!record || typeof record !== "object") {
803
+ throw new Error(`[host-memory-reservation] \u9884\u7559\u8BB0\u5F55\u683C\u5F0F\u635F\u574F (${filePath})\uFF0C\u62D2\u7EDD\u672A\u77E5\u5F53\u5F52\u96F6`);
804
+ }
805
+ if (typeof record.reservedMb !== "number" || Number.isNaN(record.reservedMb) || !Number.isFinite(record.reservedMb) || record.reservedMb < 0) {
806
+ throw new Error(`[host-memory-reservation] \u9884\u7559\u8BB0\u5F55\u5305\u542B\u65E0\u6548 reservedMb (${filePath})`);
807
+ }
808
+ if (typeof record.createdAtMs !== "number" || Number.isNaN(record.createdAtMs) || !Number.isFinite(record.createdAtMs) || record.createdAtMs < 0) {
809
+ throw new Error(`[host-memory-reservation] \u9884\u7559\u8BB0\u5F55\u5305\u542B\u65E0\u6548 createdAtMs (${filePath})`);
810
+ }
811
+ if (record.status === "released" || record.status === "reclaimed") {
812
+ continue;
813
+ }
814
+ if (record.status !== "reserved" && record.status !== "active") {
815
+ throw new Error(`[host-memory-reservation] invalid status: ${filePath}`);
816
+ }
817
+ const ageMs = nowMs - record.createdAtMs;
818
+ const pid = record.childPid ?? record.holderPid;
819
+ if (!Number.isInteger(pid) || pid <= 0 || ageMs < 0) {
820
+ throw new Error(`[host-memory-reservation] invalid process/time: ${filePath}`);
821
+ }
822
+ const expectedStart = record.childPid ? record.childStartedAt : record.holderStartedAt;
823
+ let alive = true;
824
+ let actualStart;
825
+ try {
826
+ alive = isProcessAlive(pid);
827
+ if (alive && expectedStart) actualStart = deps.getProcessStartTime?.(pid);
828
+ } catch {
829
+ }
830
+ const reused = expectedStart && actualStart && Number.isFinite(actualStart.getTime()) && actualStart.toISOString() !== expectedStart;
831
+ if (!alive || reused || record.status === "active" && ageMs >= startupWindowMs) {
832
+ updateReservationFile(filePath, fs, (r) => {
833
+ r.status = "reclaimed";
834
+ r.reclaimedAt = new Date(nowMs).toISOString();
835
+ r.reason = !alive ? "stale_process_died" : reused ? "pid_reused" : "startup_window_elapsed_rss_accounted";
836
+ });
837
+ continue;
838
+ }
839
+ activeStartupRuns += 1;
840
+ reservedStartupMb += record.reservedMb;
841
+ if (!Number.isFinite(reservedStartupMb)) throw new Error("Invalid reservation total");
842
+ }
843
+ return { activeStartupRuns, reservedStartupMb };
844
+ }
845
+ function reserveHostMemory(deps) {
846
+ const fs = deps.fs ?? nodeFs;
847
+ const runsDir = deps.runsDir;
848
+ const reservationsDir = resolveReservationsDir(runsDir);
849
+ const now = deps.now ? deps.now() : /* @__PURE__ */ new Date();
850
+ let nowMs = now.getTime();
851
+ const minAvailableMb = deps.minAvailableMb ?? LOCAL_AGENT_RUN_MIN_AVAILABLE_MB;
852
+ const startupWindowMs = deps.startupWindowMs ?? STARTUP_WINDOW_MS;
853
+ const requestedMb = deps.requestedMb ?? STARTUP_WINDOW_RESERVED_MB_PER_RUN;
854
+ const isProcessAlive = deps.isProcessAlive ?? defaultIsProcessAlive;
855
+ const maxAttempts = deps.maxLockAttempts ?? 5;
856
+ const delayMs = deps.lockRetryDelayMs ?? 15;
857
+ for (const [name, value] of Object.entries({ requestedMb, minAvailableMb, startupWindowMs, maxAttempts, delayMs, nowMs })) {
858
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
859
+ throw new Error(`[host-memory-reservation] invalid ${name}: ${value}`);
860
+ }
861
+ }
862
+ if (!Number.isInteger(maxAttempts) || maxAttempts < 1) throw new Error("Invalid lock attempts");
863
+ if (!deps.withLock) throw new Error("[host-memory-reservation] exclusive lock required");
864
+ fs.mkdirSync(reservationsDir, { recursive: true });
865
+ const locked = (fn) => {
866
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
867
+ const result = deps.withLock(MEMORY_RESERVATION_LOCK_ID, () => ({ value: fn() }), { strict: true });
868
+ if (result !== void 0) return result.value;
869
+ if (attempt + 1 < maxAttempts) sleepSync(delayMs);
870
+ }
871
+ throw new Error("[host-memory-reservation] exclusive lock timeout");
872
+ };
873
+ const executeReservation = () => {
874
+ nowMs = (deps.now ? deps.now() : /* @__PURE__ */ new Date()).getTime();
875
+ if (!Number.isFinite(nowMs)) throw new Error("Invalid reservation time");
876
+ const { activeStartupRuns, reservedStartupMb } = scanActiveReservations(reservationsDir, {
877
+ fs,
878
+ nowMs,
879
+ startupWindowMs,
880
+ isProcessAlive,
881
+ getProcessStartTime: deps.getProcessStartTime
882
+ });
883
+ assertHostMemoryAdmission({
884
+ readHostMemory: deps.readHostMemory,
885
+ hostMemoryDeps: deps.hostMemoryDeps,
886
+ minAvailableMb: minAvailableMb + reservedStartupMb + requestedMb,
887
+ now: () => new Date(nowMs)
888
+ });
889
+ const id = `res-${nowMs.toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
890
+ const recordPath = join(reservationsDir, `reservation-${id}.json`);
891
+ const record = {
892
+ id,
893
+ runId: deps.runId,
894
+ status: "reserved",
895
+ reservedMb: requestedMb,
896
+ createdAt: new Date(nowMs).toISOString(),
897
+ createdAtMs: nowMs,
898
+ holderPid: process.pid,
899
+ holderStartedAt: deps.getProcessStartTime?.(process.pid)?.toISOString()
900
+ };
901
+ const payload = JSON.stringify(record, null, 2);
902
+ fs.writeFileSync(recordPath, payload, "utf8");
903
+ return {
904
+ id,
905
+ reservedMb: requestedMb,
906
+ recordPath,
907
+ associate(params) {
908
+ if (!params.runId || !Number.isInteger(params.pid) || params.pid <= 0) throw new Error("Invalid child association");
909
+ const validStart = !!params.processStartedAt && Number.isFinite(Date.parse(params.processStartedAt));
910
+ locked(() => updateReservationFile(recordPath, fs, (r) => {
911
+ if (r.status !== "reserved") throw new Error("Reservation is not reserved");
912
+ if (validStart) {
913
+ r.status = "active";
914
+ r.childStartedAt = params.processStartedAt;
915
+ }
916
+ if (params.runId) r.runId = params.runId;
917
+ if (typeof params.pid === "number") r.childPid = params.pid;
918
+ }));
919
+ if (!validStart) throw new Error("Child starttime unknown; reservation retained");
920
+ },
921
+ release(reason) {
922
+ locked(() => updateReservationFile(recordPath, fs, (r) => {
923
+ if (r.status === "reserved" || r.status === "active") {
924
+ r.status = "released";
925
+ r.releasedAt = (/* @__PURE__ */ new Date()).toISOString();
926
+ if (reason) r.reason = reason;
927
+ }
928
+ }));
929
+ }
930
+ };
931
+ };
932
+ if (deps.withLock) {
933
+ const withLock = deps.withLock;
934
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
935
+ const handle = withLock(MEMORY_RESERVATION_LOCK_ID, executeReservation, {
936
+ strict: true
937
+ });
938
+ if (handle !== void 0) {
939
+ return handle;
940
+ }
941
+ if (attempt < maxAttempts - 1) {
942
+ sleepSync(delayMs);
943
+ }
944
+ }
945
+ throw new Error(
946
+ `[host-memory-reservation] \u83B7\u53D6\u9884\u7559\u4E92\u65A5\u9501\u8D85\u65F6\uFF08\u5C1D\u8BD5 ${maxAttempts} \u6B21\uFF09\uFF0C\u62D2\u7EDD\u5E76\u53D1\u51FB\u7A7F`
947
+ );
948
+ }
949
+ throw new Error("[host-memory-reservation] exclusive lock required");
950
+ }
951
+
554
952
  // packages/cli/client/localRuntimeHelpers.ts
555
953
  var REMOTE_DIALOG_SYNC_TIMEOUT_MS = 1e4;
556
954
  var remoteDialogSyncTimeoutMsForTest;
@@ -695,19 +1093,19 @@ function isRunTerminalStatus(status) {
695
1093
  function resolveNoloHome(env, homedir = nodeHomedir) {
696
1094
  const fromEnv = env?.NOLO_HOME;
697
1095
  if (typeof fromEnv === "string" && fromEnv.length > 0) return fromEnv;
698
- return join(homedir(), ".nolo");
1096
+ return join2(homedir(), ".nolo");
699
1097
  }
700
1098
  function resolveRunsDir(env, homedir = nodeHomedir) {
701
- return join(resolveNoloHome(env, homedir), "runs");
1099
+ return join2(resolveNoloHome(env, homedir), "runs");
702
1100
  }
703
1101
  function resolveRunRecordPath(runId, env, homedir = nodeHomedir) {
704
- return join(resolveRunsDir(env, homedir), `${runId}.json`);
1102
+ return join2(resolveRunsDir(env, homedir), `${runId}.json`);
705
1103
  }
706
1104
  function resolveRunLogPath(runId, env, homedir = nodeHomedir) {
707
- return join(resolveRunsDir(env, homedir), `${runId}.log`);
1105
+ return join2(resolveRunsDir(env, homedir), `${runId}.log`);
708
1106
  }
709
1107
  function resolveRunQueuePath(runId, env, homedir = nodeHomedir) {
710
- return join(resolveRunsDir(env, homedir), `${runId}.queue.jsonl`);
1108
+ return join2(resolveRunsDir(env, homedir), `${runId}.queue.jsonl`);
711
1109
  }
712
1110
  function defaultGenerateQueueEntryId() {
713
1111
  const ts = Date.now().toString(36);
@@ -746,7 +1144,7 @@ function _serializeQueueEntries(entries) {
746
1144
  return entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
747
1145
  }
748
1146
  var serializeQueueEntries = _serializeQueueEntries;
749
- function _countQueueMessagesLocked(queuePath, fs = nodeFs) {
1147
+ function _countQueueMessagesLocked(queuePath, fs = nodeFs2) {
750
1148
  if (!fs.existsSync(queuePath)) return 0;
751
1149
  try {
752
1150
  const content = fs.readFileSync(queuePath, "utf8");
@@ -755,7 +1153,7 @@ function _countQueueMessagesLocked(queuePath, fs = nodeFs) {
755
1153
  return 0;
756
1154
  }
757
1155
  }
758
- function _readQueueMessagesLocked(queuePath, fs = nodeFs) {
1156
+ function _readQueueMessagesLocked(queuePath, fs = nodeFs2) {
759
1157
  if (!fs.existsSync(queuePath)) return [];
760
1158
  try {
761
1159
  const content = fs.readFileSync(queuePath, "utf8");
@@ -764,7 +1162,7 @@ function _readQueueMessagesLocked(queuePath, fs = nodeFs) {
764
1162
  return [];
765
1163
  }
766
1164
  }
767
- function _readQueueEntriesLocked(queuePath, fs = nodeFs) {
1165
+ function _readQueueEntriesLocked(queuePath, fs = nodeFs2) {
768
1166
  if (!fs.existsSync(queuePath)) return [];
769
1167
  try {
770
1168
  const content = fs.readFileSync(queuePath, "utf8");
@@ -831,13 +1229,13 @@ async function withQueueLock(queuePath, fnOrDeps, depsOrFnOrOpts, maybeOpts) {
831
1229
  fn = depsOrFnOrOpts;
832
1230
  }
833
1231
  const deps = normalizeQueueDeps(rawDeps);
834
- const fs = deps.fs ?? nodeFs;
1232
+ const fs = deps.fs ?? nodeFs2;
835
1233
  const now = deps.now ?? (() => /* @__PURE__ */ new Date());
836
1234
  const staleLockMs = opts.staleLockMs ?? QUEUE_LOCK_STALE_MS;
837
1235
  const retries = opts.retries ?? QUEUE_LOCK_RETRIES;
838
1236
  const retryIntervalMs = opts.retryIntervalMs ?? QUEUE_LOCK_RETRY_INTERVAL_MS;
839
1237
  const lockPath = `${queuePath}.lock`;
840
- const tokenPath = join(lockPath, "owner.json");
1238
+ const tokenPath = join2(lockPath, "owner.json");
841
1239
  try {
842
1240
  fs.mkdirSync(dirname(lockPath), { recursive: true });
843
1241
  } catch {
@@ -930,7 +1328,7 @@ async function withQueueLock(queuePath, fnOrDeps, depsOrFnOrOpts, maybeOpts) {
930
1328
  }
931
1329
  async function appendRunQueue(queuePath, input, depsOrFs = {}) {
932
1330
  const deps = normalizeQueueDeps(depsOrFs);
933
- const fs = deps.fs ?? nodeFs;
1331
+ const fs = deps.fs ?? nodeFs2;
934
1332
  const now = deps.now ?? (() => /* @__PURE__ */ new Date());
935
1333
  const text = typeof input === "string" ? input : input.text;
936
1334
  const entryId = typeof input === "object" && typeof input.id === "string" && input.id.trim() ? input.id.trim() : defaultGenerateQueueEntryId();
@@ -949,7 +1347,7 @@ async function appendRunQueue(queuePath, input, depsOrFs = {}) {
949
1347
  }
950
1348
  async function countQueueMessages(queuePath, depsOrFs = {}) {
951
1349
  const deps = normalizeQueueDeps(depsOrFs);
952
- const fs = deps.fs ?? nodeFs;
1350
+ const fs = deps.fs ?? nodeFs2;
953
1351
  if (!fs.existsSync(queuePath)) return 0;
954
1352
  return await withQueueLock(queuePath, deps, async () => {
955
1353
  return _countQueueMessagesLocked(queuePath, fs);
@@ -957,7 +1355,7 @@ async function countQueueMessages(queuePath, depsOrFs = {}) {
957
1355
  }
958
1356
  async function readQueueMessages(queuePath, depsOrFs = {}) {
959
1357
  const deps = normalizeQueueDeps(depsOrFs);
960
- const fs = deps.fs ?? nodeFs;
1358
+ const fs = deps.fs ?? nodeFs2;
961
1359
  if (!fs.existsSync(queuePath)) return [];
962
1360
  return await withQueueLock(queuePath, deps, async () => {
963
1361
  return _readQueueMessagesLocked(queuePath, fs);
@@ -965,7 +1363,7 @@ async function readQueueMessages(queuePath, depsOrFs = {}) {
965
1363
  }
966
1364
  async function readQueueEntries(queuePath, depsOrFs = {}) {
967
1365
  const deps = normalizeQueueDeps(depsOrFs);
968
- const fs = deps.fs ?? nodeFs;
1366
+ const fs = deps.fs ?? nodeFs2;
969
1367
  if (!fs.existsSync(queuePath)) return [];
970
1368
  return await withQueueLock(queuePath, deps, async () => {
971
1369
  return _readQueueEntriesLocked(queuePath, fs);
@@ -973,7 +1371,7 @@ async function readQueueEntries(queuePath, depsOrFs = {}) {
973
1371
  }
974
1372
  async function popAllQueueEntries(queuePath, depsOrFs = {}) {
975
1373
  const deps = normalizeQueueDeps(depsOrFs);
976
- const fs = deps.fs ?? nodeFs;
1374
+ const fs = deps.fs ?? nodeFs2;
977
1375
  return await withQueueLock(queuePath, deps, async () => {
978
1376
  if (!fs.existsSync(queuePath)) return [];
979
1377
  const content = fs.readFileSync(queuePath, "utf8");
@@ -984,7 +1382,7 @@ async function popAllQueueEntries(queuePath, depsOrFs = {}) {
984
1382
  }
985
1383
  async function popSingleQueueMessage(queuePath, depsOrFs = {}) {
986
1384
  const deps = normalizeQueueDeps(depsOrFs);
987
- const fs = deps.fs ?? nodeFs;
1385
+ const fs = deps.fs ?? nodeFs2;
988
1386
  return await withQueueLock(queuePath, deps, async () => {
989
1387
  if (!fs.existsSync(queuePath)) return null;
990
1388
  const content = fs.readFileSync(queuePath, "utf8");
@@ -1019,15 +1417,19 @@ function defaultGenerateBatchId() {
1019
1417
  var LOCK_STALE_MS = 5e3;
1020
1418
  var TMP_STALE_MS = 6e4;
1021
1419
  function withRunRecordLock(runId, deps, fn, opts = {}) {
1022
- const fs = deps.fs ?? nodeFs;
1420
+ const fs = deps.fs ?? nodeFs2;
1023
1421
  const now = deps.now ?? (() => /* @__PURE__ */ new Date());
1024
1422
  const lockPath = `${resolveRunRecordPath(runId, deps.env, deps.homedir)}.lock`;
1025
1423
  let held = false;
1026
1424
  try {
1425
+ fs.mkdirSync(dirname(lockPath), { recursive: true });
1027
1426
  try {
1028
1427
  fs.writeFileSync(lockPath, String(process.pid), { flag: "wx" });
1029
1428
  held = true;
1030
- } catch {
1429
+ } catch (err) {
1430
+ if (err?.code !== "EEXIST") {
1431
+ throw err;
1432
+ }
1031
1433
  try {
1032
1434
  const stat = fs.statSync?.(lockPath);
1033
1435
  const ageMs = stat ? now().getTime() - stat.mtimeMs : Number.POSITIVE_INFINITY;
@@ -1051,7 +1453,7 @@ function withRunRecordLock(runId, deps, fn, opts = {}) {
1051
1453
  }
1052
1454
  }
1053
1455
  function writeRunRecord(record, deps = {}) {
1054
- const fs = deps.fs ?? nodeFs;
1456
+ const fs = deps.fs ?? nodeFs2;
1055
1457
  const path = resolveRunRecordPath(record.runId, deps.env, deps.homedir);
1056
1458
  fs.mkdirSync(resolveRunsDir(deps.env, deps.homedir), { recursive: true });
1057
1459
  const payload = JSON.stringify(record, null, 2);
@@ -1073,7 +1475,7 @@ function writeRunRecord(record, deps = {}) {
1073
1475
  var FILE_EDIT_TOOL_NAMES = /* @__PURE__ */ new Set(["writeFile", "editFile"]);
1074
1476
  var DEFAULT_ACTIVITY_WRITE_INTERVAL_MS = 2e3;
1075
1477
  function createRunActivityTracker(runId, deps = {}, options = {}) {
1076
- const fs = deps.fs ?? nodeFs;
1478
+ const fs = deps.fs ?? nodeFs2;
1077
1479
  const now = deps.now ?? (() => /* @__PURE__ */ new Date());
1078
1480
  const minWriteIntervalMs = options.minWriteIntervalMs ?? DEFAULT_ACTIVITY_WRITE_INTERVAL_MS;
1079
1481
  const keepAlive = options.keepAlive ?? true;
@@ -1168,7 +1570,7 @@ function createRunActivityTracker(runId, deps = {}, options = {}) {
1168
1570
  return { onLoopEvent, getActivity, flush, dispose };
1169
1571
  }
1170
1572
  function readRunRecord(runId, deps = {}) {
1171
- const fs = deps.fs ?? nodeFs;
1573
+ const fs = deps.fs ?? nodeFs2;
1172
1574
  const path = resolveRunRecordPath(runId, deps.env, deps.homedir);
1173
1575
  for (let attempt = 0; attempt < 2; attempt += 1) {
1174
1576
  try {
@@ -1190,7 +1592,7 @@ function backfillRunRecordParentDialog(runId, parentDialogId, deps = {}) {
1190
1592
  return updated === true;
1191
1593
  }
1192
1594
  function listRunRecords(deps = {}) {
1193
- const fs = deps.fs ?? nodeFs;
1595
+ const fs = deps.fs ?? nodeFs2;
1194
1596
  const dir = resolveRunsDir(deps.env, deps.homedir);
1195
1597
  let entries = [];
1196
1598
  try {
@@ -1261,7 +1663,7 @@ function queryRunRecords(query, deps = {}) {
1261
1663
  }
1262
1664
  var DEFAULT_GC_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
1263
1665
  async function gcRunRecords(deps = {}, options = {}) {
1264
- const fs = deps.fs ?? nodeFs;
1666
+ const fs = deps.fs ?? nodeFs2;
1265
1667
  const now = (deps.now ?? (() => /* @__PURE__ */ new Date()))();
1266
1668
  const nowMs = now instanceof Date ? now.getTime() : Date.parse(String(now));
1267
1669
  const retentionMs = typeof options.retentionMs === "number" && options.retentionMs >= 0 ? options.retentionMs : DEFAULT_GC_RETENTION_MS;
@@ -1276,7 +1678,7 @@ async function gcRunRecords(deps = {}, options = {}) {
1276
1678
  const jsonPath = resolveRunRecordPath(record.runId, deps.env, deps.homedir);
1277
1679
  const logPath = resolveRunLogPath(record.runId, deps.env, deps.homedir);
1278
1680
  const queuePath = record.queuePath ?? resolveRunQueuePath(record.runId, deps.env, deps.homedir);
1279
- const msgPath = record.msgFile ?? join(resolveRunsDir(deps.env, deps.homedir), `${record.runId}.msg.md`);
1681
+ const msgPath = record.msgFile ?? join2(resolveRunsDir(deps.env, deps.homedir), `${record.runId}.msg.md`);
1280
1682
  const logOk = tryUnlinkFile(fs, logPath);
1281
1683
  const msgOk = tryUnlinkFile(fs, msgPath);
1282
1684
  let queueOk = true;
@@ -1290,9 +1692,9 @@ async function gcRunRecords(deps = {}, options = {}) {
1290
1692
  }
1291
1693
  }
1292
1694
  const runsDirForLegacy = resolveRunsDir(deps.env, deps.homedir);
1293
- tryUnlinkFile(fs, join(runsDirForLegacy, `${record.runId}.report.md`));
1294
- tryUnlinkFile(fs, join(runsDirForLegacy, `${record.runId}.report.json`));
1295
- tryUnlinkFile(fs, record.resultFile ?? join(runsDirForLegacy, `${record.runId}.result.md`));
1695
+ tryUnlinkFile(fs, join2(runsDirForLegacy, `${record.runId}.report.md`));
1696
+ tryUnlinkFile(fs, join2(runsDirForLegacy, `${record.runId}.report.json`));
1697
+ tryUnlinkFile(fs, record.resultFile ?? join2(runsDirForLegacy, `${record.runId}.result.md`));
1296
1698
  if (!logOk || !msgOk || !queueOk) {
1297
1699
  failedIds.push(record.runId);
1298
1700
  continue;
@@ -1309,7 +1711,7 @@ async function gcRunRecords(deps = {}, options = {}) {
1309
1711
  const statSync = fs.statSync;
1310
1712
  for (const entry of fs.readdirSync(dir)) {
1311
1713
  if (typeof entry !== "string" || !entry.endsWith(".tmp")) continue;
1312
- const tmpPath = join(dir, entry);
1714
+ const tmpPath = join2(dir, entry);
1313
1715
  try {
1314
1716
  const stat = statSync?.(tmpPath);
1315
1717
  if (stat && nowMs - stat.mtimeMs <= TMP_STALE_MS) continue;
@@ -1370,7 +1772,7 @@ function rewriteMsgFileArg(args, messagePath) {
1370
1772
  }
1371
1773
  return result;
1372
1774
  }
1373
- function resolveValidCliEntrypoint(cliEntrypointPath, fs = nodeFs) {
1775
+ function resolveValidCliEntrypoint(cliEntrypointPath, fs = nodeFs2) {
1374
1776
  if (cliEntrypointPath && fs.existsSync(cliEntrypointPath)) {
1375
1777
  return cliEntrypointPath;
1376
1778
  }
@@ -1379,11 +1781,11 @@ function resolveValidCliEntrypoint(cliEntrypointPath, fs = nodeFs) {
1379
1781
  return fallback;
1380
1782
  }
1381
1783
  const candidates = [
1382
- join(dirname(fallback), "cli", "index.ts"),
1383
- join(dirname(fallback), "cli", "index.js"),
1384
- join(dirname(fallback), "..", "packages", "cli", "index.ts"),
1385
- join(dirname(fallback), "..", "packages", "cli", "index.js"),
1386
- join(dirname(fallback), "..", "index.js")
1784
+ join2(dirname(fallback), "cli", "index.ts"),
1785
+ join2(dirname(fallback), "cli", "index.js"),
1786
+ join2(dirname(fallback), "..", "packages", "cli", "index.ts"),
1787
+ join2(dirname(fallback), "..", "packages", "cli", "index.js"),
1788
+ join2(dirname(fallback), "..", "index.js")
1387
1789
  ];
1388
1790
  for (const cand of candidates) {
1389
1791
  if (fs.existsSync(cand)) {
@@ -1393,7 +1795,7 @@ function resolveValidCliEntrypoint(cliEntrypointPath, fs = nodeFs) {
1393
1795
  return fallback;
1394
1796
  }
1395
1797
  function buildAgentRunChildCommand(options, deps = {}) {
1396
- const fs = deps.fs ?? nodeFs;
1798
+ const fs = deps.fs ?? nodeFs2;
1397
1799
  const execPath = process.execPath;
1398
1800
  const commandParts = options.commandPath ?? [];
1399
1801
  let strippedArgs = stripBackgroundFlag(options.rawArgs);
@@ -1410,13 +1812,6 @@ function buildAgentRunChildCommand(options, deps = {}) {
1410
1812
  }
1411
1813
  return { execPath, childArgs: [entrypoint, ...commandParts, ...strippedArgs] };
1412
1814
  }
1413
- function safeReadHostMemory(reader) {
1414
- try {
1415
- return reader();
1416
- } catch {
1417
- return null;
1418
- }
1419
- }
1420
1815
  function emitHostMemoryNotice(mem, output) {
1421
1816
  if (!mem || !output) return void 0;
1422
1817
  let notice;
@@ -1434,9 +1829,10 @@ function emitHostMemoryNotice(mem, output) {
1434
1829
  return notice;
1435
1830
  }
1436
1831
  async function spawnLocalBackgroundRun(input, deps = {}) {
1437
- const env = deps.env ?? process.env;
1438
1832
  const homedir = deps.homedir ?? nodeHomedir;
1439
- const fs = deps.fs ?? nodeFs;
1833
+ const env = deps.env ?? (deps.homedir ? { ...process.env, NOLO_HOME: join2(homedir(), ".nolo") } : process.env);
1834
+ const resolvedDeps = { ...deps, env, homedir };
1835
+ const fs = deps.fs ?? nodeFs2;
1440
1836
  const spawn = deps.spawn ?? nodeSpawn;
1441
1837
  const generateRunId = deps.generateRunId ?? defaultGenerateRunId;
1442
1838
  const now = deps.now ?? (() => /* @__PURE__ */ new Date());
@@ -1449,7 +1845,7 @@ async function spawnLocalBackgroundRun(input, deps = {}) {
1449
1845
  fs.mkdirSync(runsDir, { recursive: true });
1450
1846
  let messagePath;
1451
1847
  if (typeof input.message === "string") {
1452
- messagePath = join(runsDir, `${runId}.msg.md`);
1848
+ messagePath = join2(runsDir, `${runId}.msg.md`);
1453
1849
  fs.writeFileSync(messagePath, input.message);
1454
1850
  }
1455
1851
  const rawDodCommands = [];
@@ -1516,34 +1912,92 @@ async function spawnLocalBackgroundRun(input, deps = {}) {
1516
1912
  NOLO_AGENT_RUN_CHILD: "1",
1517
1913
  NOLO_AGENT_RUN_ID: runId
1518
1914
  };
1519
- const hostMemory = safeReadHostMemory(deps.readHostMemory ?? readHostMemory);
1915
+ let hostMemory = null;
1916
+ let reservation;
1520
1917
  const logFd = fs.openSync(logPath, "a");
1521
- const proc = spawn(execPath, childArgs, {
1522
- cwd: input.cwd,
1523
- env: childEnv,
1524
- detached: true,
1525
- stdio: deps.shellApprovalHost ? ["ignore", logFd, logFd, "ipc"] : ["ignore", logFd, logFd]
1918
+ const reservationFs = {
1919
+ existsSync: (path) => fs.existsSync(path),
1920
+ readFileSync: (path, encoding) => fs.readFileSync(path, encoding),
1921
+ writeFileSync: (path, data, options) => fs.writeFileSync(path, data, options),
1922
+ mkdirSync: (path, options) => fs.mkdirSync(path, options),
1923
+ readdirSync: (path) => fs.readdirSync(path),
1924
+ ...fs.renameSync ? { renameSync: (from, to) => fs.renameSync(from, to) } : {},
1925
+ ...fs.statSync ? { statSync: (path) => ({ mtimeMs: fs.statSync(path).mtimeMs }) } : {}
1926
+ };
1927
+ let proc;
1928
+ try {
1929
+ reservation = reserveHostMemory({
1930
+ runsDir,
1931
+ runId,
1932
+ fs: reservationFs,
1933
+ now: resolvedDeps.now,
1934
+ readHostMemory: () => hostMemory = (resolvedDeps.readHostMemory ?? readHostMemory)(),
1935
+ getProcessStartTime: resolvedDeps.getProcessStartTime ?? defaultGetProcessStartTime,
1936
+ withLock: (id, fn, opts) => withRunRecordLock(id, resolvedDeps, fn, opts)
1937
+ });
1938
+ proc = spawn(execPath, childArgs, {
1939
+ cwd: input.cwd,
1940
+ env: childEnv,
1941
+ detached: true,
1942
+ stdio: deps.shellApprovalHost ? ["ignore", logFd, logFd, "ipc"] : ["ignore", logFd, logFd]
1943
+ });
1944
+ } catch (error) {
1945
+ try {
1946
+ reservation?.release("spawn_sync_failed");
1947
+ } finally {
1948
+ finalizeRunRecord(runId, { status: "failed", exitCode: 1, note: String(error) }, resolvedDeps);
1949
+ }
1950
+ throw error;
1951
+ } finally {
1952
+ fs.closeSync?.(logFd);
1953
+ }
1954
+ proc.once?.("error", (error) => {
1955
+ try {
1956
+ reservation.release("child_error");
1957
+ } catch (releaseError) {
1958
+ finalizeRunRecord(runId, {
1959
+ status: "failed",
1960
+ exitCode: 1,
1961
+ note: `${error}; reservation release failed: ${releaseError}`
1962
+ }, resolvedDeps);
1963
+ return;
1964
+ }
1965
+ finalizeRunRecord(runId, { status: "failed", exitCode: 1, note: String(error) }, resolvedDeps);
1526
1966
  });
1967
+ const hostMemoryNotice = emitHostMemoryNotice(hostMemory, input.output);
1968
+ if (typeof proc.pid === "number") {
1969
+ let processStartedAt;
1970
+ try {
1971
+ processStartedAt = (resolvedDeps.getProcessStartTime ?? defaultGetProcessStartTime)(proc.pid);
1972
+ if (processStartedAt && !Number.isFinite(processStartedAt.getTime())) processStartedAt = null;
1973
+ } catch {
1974
+ processStartedAt = null;
1975
+ }
1976
+ withRunRecordLock(runId, resolvedDeps, () => {
1977
+ const latest = readRunRecord(runId, resolvedDeps) ?? record;
1978
+ latest.pid = proc.pid;
1979
+ if (processStartedAt) latest.processStartedAt = processStartedAt.toISOString();
1980
+ writeRunRecord(latest, resolvedDeps);
1981
+ return true;
1982
+ }, { strict: true });
1983
+ try {
1984
+ const persisted = readRunRecord(runId, resolvedDeps);
1985
+ reservation.associate({ runId, pid: proc.pid, processStartedAt: processStartedAt?.toISOString() });
1986
+ if (persisted?.pid !== proc.pid || !persisted.processStartedAt) throw new Error("Child identity persistence failed");
1987
+ } catch (error) {
1988
+ throw new Error(`[host-memory-reservation] child ${proc.pid}, run ${runId}: association failed: ${error}`);
1989
+ }
1990
+ }
1527
1991
  if (deps.shellApprovalHost) {
1528
1992
  attachShellApprovalHost(proc, { ...deps.shellApprovalHost, binding: {
1529
1993
  ...deps.shellApprovalHost.binding,
1530
1994
  runId,
1531
1995
  agentKey: input.agentKey,
1532
- workspaceRoot: nodeFs.realpathSync(input.cwd ?? process.cwd())
1996
+ workspaceRoot: nodeFs2.realpathSync(input.cwd ?? process.cwd())
1533
1997
  } });
1534
1998
  proc.channel?.unref?.();
1535
1999
  }
1536
2000
  proc.unref();
1537
- const hostMemoryNotice = emitHostMemoryNotice(hostMemory, input.output);
1538
- if (typeof proc.pid === "number") {
1539
- const processStartedAt = (deps.getProcessStartTime ?? defaultGetProcessStartTime)(proc.pid);
1540
- withRunRecordLock(runId, deps, () => {
1541
- const latest = readRunRecord(runId, deps) ?? record;
1542
- latest.pid = proc.pid;
1543
- if (processStartedAt) latest.processStartedAt = processStartedAt.toISOString();
1544
- writeRunRecord(latest, deps);
1545
- });
1546
- }
1547
2001
  return {
1548
2002
  runId,
1549
2003
  pid: proc.pid,
@@ -1661,8 +2115,8 @@ function transitionRunToTerminal(runId, update, deps = {}, options = {}) {
1661
2115
  record2.endedAt = now().toISOString();
1662
2116
  if (typeof update.lastAssistantText === "string" && update.lastAssistantText) {
1663
2117
  try {
1664
- const fs = deps.fs ?? nodeFs;
1665
- const resultFile = join(resolveRunsDir(deps.env, deps.homedir), `${runId}.result.md`);
2118
+ const fs = deps.fs ?? nodeFs2;
2119
+ const resultFile = join2(resolveRunsDir(deps.env, deps.homedir), `${runId}.result.md`);
1666
2120
  const startedMs = Date.parse(record2.startedAt);
1667
2121
  const endedMs = Date.parse(record2.endedAt);
1668
2122
  const content = buildResultFileContent(
@@ -1829,7 +2283,7 @@ function formatDuration(startedAt, endedAt) {
1829
2283
  return `${seconds}s`;
1830
2284
  }
1831
2285
  function readLastLogLines(logPath, count, deps) {
1832
- const fs = deps.fs ?? nodeFs;
2286
+ const fs = deps.fs ?? nodeFs2;
1833
2287
  try {
1834
2288
  const content = fs.readFileSync(logPath, "utf8");
1835
2289
  const lines = content.split("\n");
@@ -1840,7 +2294,7 @@ function readLastLogLines(logPath, count, deps) {
1840
2294
  }
1841
2295
  }
1842
2296
  function readLogContent(logPath, tailCount, deps) {
1843
- const fs = deps.fs ?? nodeFs;
2297
+ const fs = deps.fs ?? nodeFs2;
1844
2298
  try {
1845
2299
  const content = fs.readFileSync(logPath, "utf8");
1846
2300
  if (typeof tailCount === "number" && tailCount > 0) {
@@ -2127,7 +2581,7 @@ async function runAgentLogsCommand(args, deps) {
2127
2581
  `);
2128
2582
  return 1;
2129
2583
  }
2130
- const fs = deps.fs ?? nodeFs;
2584
+ const fs = deps.fs ?? nodeFs2;
2131
2585
  if (!fs.existsSync(record.logPath)) {
2132
2586
  deps.output.write(`Log not found: ${record.logPath}
2133
2587
  `);
@@ -2230,6 +2684,8 @@ async function runAgentKillCommand(args, deps) {
2230
2684
 
2231
2685
  export {
2232
2686
  createShellApprovalChild,
2687
+ readHostMemory,
2688
+ assertHostMemoryAdmission,
2233
2689
  setRemoteDialogSyncTimeoutForTest,
2234
2690
  remoteDialogSyncTimeout,
2235
2691
  resolveRuntimeServerUrl,
@@ -2267,6 +2723,7 @@ export {
2267
2723
  popQueueMessages,
2268
2724
  defaultGenerateRunId,
2269
2725
  defaultGenerateBatchId,
2726
+ withRunRecordLock,
2270
2727
  writeRunRecord,
2271
2728
  createRunActivityTracker,
2272
2729
  readRunRecord,