nolo-cli 0.163.0-alpha.1 → 0.164.0-alpha.1

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 (47) hide show
  1. package/{_executeModel-3FMHTHSS.js → _executeModel-LFPW52TC.js} +12 -12
  2. package/{agentDeleteCommand-VRHLJZKX.js → agentDeleteCommand-E5RWD5AM.js} +10 -10
  3. package/{agentEmailCommands-YGBQXOL3.js → agentEmailCommands-VTUCQASB.js} +12 -12
  4. package/{agentMachineCommands-FBAWZYHI.js → agentMachineCommands-CJ53NRUB.js} +1 -1
  5. package/{agentRecordCommands-KAK2G56H.js → agentRecordCommands-OL73UQE5.js} +11 -11
  6. package/{agentRunCommand-62P3AGJA.js → agentRunCommand-O6RR6VM2.js} +12 -12
  7. package/{agentRunControl-XBSNHHZE.js → agentRunControl-NQR3GB6H.js} +1 -1
  8. package/{agentSlice-IZFU3WWH.js → agentSlice-YDRAYF3Z.js} +1 -1
  9. package/{chunk-2QQ7D3P5.js → chunk-3WTD7IIQ.js} +8 -8
  10. package/{chunk-7YQQXP42.js → chunk-4BLKVQBS.js} +2 -2
  11. package/{chunk-EDJND3FK.js → chunk-AOVELMYY.js} +3 -3
  12. package/{chunk-Q5LVXPGT.js → chunk-CAWIORRR.js} +2 -2
  13. package/{chunk-K3N2DWLT.js → chunk-FKBWA3NN.js} +2 -2
  14. package/{chunk-6QIBWOPT.js → chunk-HXFIFEBA.js} +2 -2
  15. package/{chunk-CI6YDDL2.js → chunk-IG3DNKWC.js} +2 -2
  16. package/{chunk-V67W64OY.js → chunk-JGFVMK3S.js} +7 -7
  17. package/{chunk-PNACBSRF.js → chunk-KTFQFGMZ.js} +194 -31
  18. package/{chunk-FTHF2P2I.js → chunk-KVDB4IIT.js} +1 -1
  19. package/{chunk-UUPXTEZA.js → chunk-NOFLIR3Z.js} +1 -1
  20. package/{chunk-HRZW3SQN.js → chunk-Q2RZKBUU.js} +1 -1
  21. package/{chunk-7PDNSQZC.js → chunk-QKBEFLMA.js} +2 -2
  22. package/{chunk-U5U4WAFS.js → chunk-S53PIA3G.js} +36 -3
  23. package/{chunk-36IWXF25.js → chunk-VNPUBOKQ.js} +5 -5
  24. package/{chunk-JIAZK434.js → chunk-VVRYD7UL.js} +2 -2
  25. package/{chunk-4FDDVVCE.js → chunk-WBRBIRDG.js} +2 -2
  26. package/{chunk-GXZZOTDM.js → chunk-WBTXES5L.js} +4547 -3343
  27. package/{chunk-SNAWGDXT.js → chunk-WJJL7RMW.js} +518 -67
  28. package/{chunk-QFL42TMV.js → chunk-ZEOGFBR3.js} +90 -15
  29. package/{commandRegistry-2DDDET7G.js → commandRegistry-CZLHZ3XM.js} +26 -26
  30. package/{contentThunks-5DNEHJ2Z.js → contentThunks-FXRH3TBV.js} +3 -3
  31. package/{createAgentAutomationAction-4SUS2EFX.js → createAgentAutomationAction-6M64V55C.js} +3 -3
  32. package/{createDialogAction-JSFXX6JE.js → createDialogAction-LTK2YJ7I.js} +4 -4
  33. package/{createPageAction-37HHOXZ2.js → createPageAction-IKFGJRKB.js} +3 -3
  34. package/{dialogCommands-ZJRMCP4S.js → dialogCommands-7FZEZ5DV.js} +2 -2
  35. package/{dialogSlice-2Z66AWEW.js → dialogSlice-TTJ74TZ2.js} +2 -2
  36. package/{docStore-XMTUF3GX.js → docStore-GZW6PV6J.js} +1 -1
  37. package/{handleSendMessageAction-4VFVXTF2.js → handleSendMessageAction-YICSRGEU.js} +7 -7
  38. package/index.js +3 -3
  39. package/{localRuntimeAdapter-KKIVRYVN.js → localRuntimeAdapter-3ZJ3BHOU.js} +10 -10
  40. package/{machineCommands-UHQVCOD3.js → machineCommands-4RVMGZDN.js} +11 -11
  41. package/{machineWsRunDispatch-TX4X7TOJ.js → machineWsRunDispatch-CPKXENCS.js} +11 -11
  42. package/package.json +1 -1
  43. package/{readlineWorkspace-BFVB2ON5.js → readlineWorkspace-DPO4JJQ2.js} +13 -13
  44. package/{runAgentClientLoop-HHLHNMBE.js → runAgentClientLoop-DROGF2SZ.js} +10 -10
  45. package/{streamAgentChatTurn-KNPOLET7.js → streamAgentChatTurn-PJ2L3P25.js} +12 -12
  46. package/{tools-ULA7XJQT.js → tools-2MFN4Z5Y.js} +7 -7
  47. package/{updateDialogTitleAction-VUIVBDAS.js → updateDialogTitleAction-VQFUSGRH.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,7 +1417,7 @@ 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;
@@ -1051,7 +1449,7 @@ function withRunRecordLock(runId, deps, fn, opts = {}) {
1051
1449
  }
1052
1450
  }
1053
1451
  function writeRunRecord(record, deps = {}) {
1054
- const fs = deps.fs ?? nodeFs;
1452
+ const fs = deps.fs ?? nodeFs2;
1055
1453
  const path = resolveRunRecordPath(record.runId, deps.env, deps.homedir);
1056
1454
  fs.mkdirSync(resolveRunsDir(deps.env, deps.homedir), { recursive: true });
1057
1455
  const payload = JSON.stringify(record, null, 2);
@@ -1073,7 +1471,7 @@ function writeRunRecord(record, deps = {}) {
1073
1471
  var FILE_EDIT_TOOL_NAMES = /* @__PURE__ */ new Set(["writeFile", "editFile"]);
1074
1472
  var DEFAULT_ACTIVITY_WRITE_INTERVAL_MS = 2e3;
1075
1473
  function createRunActivityTracker(runId, deps = {}, options = {}) {
1076
- const fs = deps.fs ?? nodeFs;
1474
+ const fs = deps.fs ?? nodeFs2;
1077
1475
  const now = deps.now ?? (() => /* @__PURE__ */ new Date());
1078
1476
  const minWriteIntervalMs = options.minWriteIntervalMs ?? DEFAULT_ACTIVITY_WRITE_INTERVAL_MS;
1079
1477
  const keepAlive = options.keepAlive ?? true;
@@ -1168,7 +1566,7 @@ function createRunActivityTracker(runId, deps = {}, options = {}) {
1168
1566
  return { onLoopEvent, getActivity, flush, dispose };
1169
1567
  }
1170
1568
  function readRunRecord(runId, deps = {}) {
1171
- const fs = deps.fs ?? nodeFs;
1569
+ const fs = deps.fs ?? nodeFs2;
1172
1570
  const path = resolveRunRecordPath(runId, deps.env, deps.homedir);
1173
1571
  for (let attempt = 0; attempt < 2; attempt += 1) {
1174
1572
  try {
@@ -1190,7 +1588,7 @@ function backfillRunRecordParentDialog(runId, parentDialogId, deps = {}) {
1190
1588
  return updated === true;
1191
1589
  }
1192
1590
  function listRunRecords(deps = {}) {
1193
- const fs = deps.fs ?? nodeFs;
1591
+ const fs = deps.fs ?? nodeFs2;
1194
1592
  const dir = resolveRunsDir(deps.env, deps.homedir);
1195
1593
  let entries = [];
1196
1594
  try {
@@ -1261,7 +1659,7 @@ function queryRunRecords(query, deps = {}) {
1261
1659
  }
1262
1660
  var DEFAULT_GC_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
1263
1661
  async function gcRunRecords(deps = {}, options = {}) {
1264
- const fs = deps.fs ?? nodeFs;
1662
+ const fs = deps.fs ?? nodeFs2;
1265
1663
  const now = (deps.now ?? (() => /* @__PURE__ */ new Date()))();
1266
1664
  const nowMs = now instanceof Date ? now.getTime() : Date.parse(String(now));
1267
1665
  const retentionMs = typeof options.retentionMs === "number" && options.retentionMs >= 0 ? options.retentionMs : DEFAULT_GC_RETENTION_MS;
@@ -1276,7 +1674,7 @@ async function gcRunRecords(deps = {}, options = {}) {
1276
1674
  const jsonPath = resolveRunRecordPath(record.runId, deps.env, deps.homedir);
1277
1675
  const logPath = resolveRunLogPath(record.runId, deps.env, deps.homedir);
1278
1676
  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`);
1677
+ const msgPath = record.msgFile ?? join2(resolveRunsDir(deps.env, deps.homedir), `${record.runId}.msg.md`);
1280
1678
  const logOk = tryUnlinkFile(fs, logPath);
1281
1679
  const msgOk = tryUnlinkFile(fs, msgPath);
1282
1680
  let queueOk = true;
@@ -1290,9 +1688,9 @@ async function gcRunRecords(deps = {}, options = {}) {
1290
1688
  }
1291
1689
  }
1292
1690
  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`));
1691
+ tryUnlinkFile(fs, join2(runsDirForLegacy, `${record.runId}.report.md`));
1692
+ tryUnlinkFile(fs, join2(runsDirForLegacy, `${record.runId}.report.json`));
1693
+ tryUnlinkFile(fs, record.resultFile ?? join2(runsDirForLegacy, `${record.runId}.result.md`));
1296
1694
  if (!logOk || !msgOk || !queueOk) {
1297
1695
  failedIds.push(record.runId);
1298
1696
  continue;
@@ -1309,7 +1707,7 @@ async function gcRunRecords(deps = {}, options = {}) {
1309
1707
  const statSync = fs.statSync;
1310
1708
  for (const entry of fs.readdirSync(dir)) {
1311
1709
  if (typeof entry !== "string" || !entry.endsWith(".tmp")) continue;
1312
- const tmpPath = join(dir, entry);
1710
+ const tmpPath = join2(dir, entry);
1313
1711
  try {
1314
1712
  const stat = statSync?.(tmpPath);
1315
1713
  if (stat && nowMs - stat.mtimeMs <= TMP_STALE_MS) continue;
@@ -1370,7 +1768,7 @@ function rewriteMsgFileArg(args, messagePath) {
1370
1768
  }
1371
1769
  return result;
1372
1770
  }
1373
- function resolveValidCliEntrypoint(cliEntrypointPath, fs = nodeFs) {
1771
+ function resolveValidCliEntrypoint(cliEntrypointPath, fs = nodeFs2) {
1374
1772
  if (cliEntrypointPath && fs.existsSync(cliEntrypointPath)) {
1375
1773
  return cliEntrypointPath;
1376
1774
  }
@@ -1379,11 +1777,11 @@ function resolveValidCliEntrypoint(cliEntrypointPath, fs = nodeFs) {
1379
1777
  return fallback;
1380
1778
  }
1381
1779
  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")
1780
+ join2(dirname(fallback), "cli", "index.ts"),
1781
+ join2(dirname(fallback), "cli", "index.js"),
1782
+ join2(dirname(fallback), "..", "packages", "cli", "index.ts"),
1783
+ join2(dirname(fallback), "..", "packages", "cli", "index.js"),
1784
+ join2(dirname(fallback), "..", "index.js")
1387
1785
  ];
1388
1786
  for (const cand of candidates) {
1389
1787
  if (fs.existsSync(cand)) {
@@ -1393,7 +1791,7 @@ function resolveValidCliEntrypoint(cliEntrypointPath, fs = nodeFs) {
1393
1791
  return fallback;
1394
1792
  }
1395
1793
  function buildAgentRunChildCommand(options, deps = {}) {
1396
- const fs = deps.fs ?? nodeFs;
1794
+ const fs = deps.fs ?? nodeFs2;
1397
1795
  const execPath = process.execPath;
1398
1796
  const commandParts = options.commandPath ?? [];
1399
1797
  let strippedArgs = stripBackgroundFlag(options.rawArgs);
@@ -1410,13 +1808,6 @@ function buildAgentRunChildCommand(options, deps = {}) {
1410
1808
  }
1411
1809
  return { execPath, childArgs: [entrypoint, ...commandParts, ...strippedArgs] };
1412
1810
  }
1413
- function safeReadHostMemory(reader) {
1414
- try {
1415
- return reader();
1416
- } catch {
1417
- return null;
1418
- }
1419
- }
1420
1811
  function emitHostMemoryNotice(mem, output) {
1421
1812
  if (!mem || !output) return void 0;
1422
1813
  let notice;
@@ -1436,7 +1827,7 @@ function emitHostMemoryNotice(mem, output) {
1436
1827
  async function spawnLocalBackgroundRun(input, deps = {}) {
1437
1828
  const env = deps.env ?? process.env;
1438
1829
  const homedir = deps.homedir ?? nodeHomedir;
1439
- const fs = deps.fs ?? nodeFs;
1830
+ const fs = deps.fs ?? nodeFs2;
1440
1831
  const spawn = deps.spawn ?? nodeSpawn;
1441
1832
  const generateRunId = deps.generateRunId ?? defaultGenerateRunId;
1442
1833
  const now = deps.now ?? (() => /* @__PURE__ */ new Date());
@@ -1449,7 +1840,7 @@ async function spawnLocalBackgroundRun(input, deps = {}) {
1449
1840
  fs.mkdirSync(runsDir, { recursive: true });
1450
1841
  let messagePath;
1451
1842
  if (typeof input.message === "string") {
1452
- messagePath = join(runsDir, `${runId}.msg.md`);
1843
+ messagePath = join2(runsDir, `${runId}.msg.md`);
1453
1844
  fs.writeFileSync(messagePath, input.message);
1454
1845
  }
1455
1846
  const rawDodCommands = [];
@@ -1516,34 +1907,92 @@ async function spawnLocalBackgroundRun(input, deps = {}) {
1516
1907
  NOLO_AGENT_RUN_CHILD: "1",
1517
1908
  NOLO_AGENT_RUN_ID: runId
1518
1909
  };
1519
- const hostMemory = safeReadHostMemory(deps.readHostMemory ?? readHostMemory);
1910
+ let hostMemory = null;
1911
+ let reservation;
1520
1912
  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]
1526
- });
1527
- if (deps.shellApprovalHost) {
1528
- attachShellApprovalHost(proc, { ...deps.shellApprovalHost, binding: {
1529
- ...deps.shellApprovalHost.binding,
1913
+ const reservationFs = {
1914
+ existsSync: (path) => fs.existsSync(path),
1915
+ readFileSync: (path, encoding) => fs.readFileSync(path, encoding),
1916
+ writeFileSync: (path, data, options) => fs.writeFileSync(path, data, options),
1917
+ mkdirSync: (path, options) => fs.mkdirSync(path, options),
1918
+ readdirSync: (path) => fs.readdirSync(path),
1919
+ ...fs.renameSync ? { renameSync: (from, to) => fs.renameSync(from, to) } : {},
1920
+ ...fs.statSync ? { statSync: (path) => ({ mtimeMs: fs.statSync(path).mtimeMs }) } : {}
1921
+ };
1922
+ let proc;
1923
+ try {
1924
+ reservation = reserveHostMemory({
1925
+ runsDir,
1530
1926
  runId,
1531
- agentKey: input.agentKey,
1532
- workspaceRoot: nodeFs.realpathSync(input.cwd ?? process.cwd())
1533
- } });
1534
- proc.channel?.unref?.();
1927
+ fs: reservationFs,
1928
+ now: deps.now,
1929
+ readHostMemory: () => hostMemory = (deps.readHostMemory ?? readHostMemory)(),
1930
+ getProcessStartTime: deps.getProcessStartTime ?? defaultGetProcessStartTime,
1931
+ withLock: (id, fn, opts) => withRunRecordLock(id, deps, fn, opts)
1932
+ });
1933
+ proc = spawn(execPath, childArgs, {
1934
+ cwd: input.cwd,
1935
+ env: childEnv,
1936
+ detached: true,
1937
+ stdio: deps.shellApprovalHost ? ["ignore", logFd, logFd, "ipc"] : ["ignore", logFd, logFd]
1938
+ });
1939
+ } catch (error) {
1940
+ try {
1941
+ reservation?.release("spawn_sync_failed");
1942
+ } finally {
1943
+ finalizeRunRecord(runId, { status: "failed", exitCode: 1, note: String(error) }, deps);
1944
+ }
1945
+ throw error;
1946
+ } finally {
1947
+ fs.closeSync?.(logFd);
1535
1948
  }
1536
- proc.unref();
1949
+ proc.once?.("error", (error) => {
1950
+ try {
1951
+ reservation.release("child_error");
1952
+ } catch (releaseError) {
1953
+ finalizeRunRecord(runId, {
1954
+ status: "failed",
1955
+ exitCode: 1,
1956
+ note: `${error}; reservation release failed: ${releaseError}`
1957
+ }, deps);
1958
+ return;
1959
+ }
1960
+ finalizeRunRecord(runId, { status: "failed", exitCode: 1, note: String(error) }, deps);
1961
+ });
1537
1962
  const hostMemoryNotice = emitHostMemoryNotice(hostMemory, input.output);
1538
1963
  if (typeof proc.pid === "number") {
1539
- const processStartedAt = (deps.getProcessStartTime ?? defaultGetProcessStartTime)(proc.pid);
1964
+ let processStartedAt;
1965
+ try {
1966
+ processStartedAt = (deps.getProcessStartTime ?? defaultGetProcessStartTime)(proc.pid);
1967
+ if (processStartedAt && !Number.isFinite(processStartedAt.getTime())) processStartedAt = null;
1968
+ } catch {
1969
+ processStartedAt = null;
1970
+ }
1540
1971
  withRunRecordLock(runId, deps, () => {
1541
1972
  const latest = readRunRecord(runId, deps) ?? record;
1542
1973
  latest.pid = proc.pid;
1543
1974
  if (processStartedAt) latest.processStartedAt = processStartedAt.toISOString();
1544
1975
  writeRunRecord(latest, deps);
1545
- });
1976
+ return true;
1977
+ }, { strict: true });
1978
+ try {
1979
+ const persisted = readRunRecord(runId, deps);
1980
+ reservation.associate({ runId, pid: proc.pid, processStartedAt: processStartedAt?.toISOString() });
1981
+ if (persisted?.pid !== proc.pid || !persisted.processStartedAt) throw new Error("Child identity persistence failed");
1982
+ } catch (error) {
1983
+ throw new Error(`[host-memory-reservation] child ${proc.pid}, run ${runId}: association failed: ${error}`);
1984
+ }
1985
+ }
1986
+ if (deps.shellApprovalHost) {
1987
+ attachShellApprovalHost(proc, { ...deps.shellApprovalHost, binding: {
1988
+ ...deps.shellApprovalHost.binding,
1989
+ runId,
1990
+ agentKey: input.agentKey,
1991
+ workspaceRoot: nodeFs2.realpathSync(input.cwd ?? process.cwd())
1992
+ } });
1993
+ proc.channel?.unref?.();
1546
1994
  }
1995
+ proc.unref();
1547
1996
  return {
1548
1997
  runId,
1549
1998
  pid: proc.pid,
@@ -1661,8 +2110,8 @@ function transitionRunToTerminal(runId, update, deps = {}, options = {}) {
1661
2110
  record2.endedAt = now().toISOString();
1662
2111
  if (typeof update.lastAssistantText === "string" && update.lastAssistantText) {
1663
2112
  try {
1664
- const fs = deps.fs ?? nodeFs;
1665
- const resultFile = join(resolveRunsDir(deps.env, deps.homedir), `${runId}.result.md`);
2113
+ const fs = deps.fs ?? nodeFs2;
2114
+ const resultFile = join2(resolveRunsDir(deps.env, deps.homedir), `${runId}.result.md`);
1666
2115
  const startedMs = Date.parse(record2.startedAt);
1667
2116
  const endedMs = Date.parse(record2.endedAt);
1668
2117
  const content = buildResultFileContent(
@@ -1829,7 +2278,7 @@ function formatDuration(startedAt, endedAt) {
1829
2278
  return `${seconds}s`;
1830
2279
  }
1831
2280
  function readLastLogLines(logPath, count, deps) {
1832
- const fs = deps.fs ?? nodeFs;
2281
+ const fs = deps.fs ?? nodeFs2;
1833
2282
  try {
1834
2283
  const content = fs.readFileSync(logPath, "utf8");
1835
2284
  const lines = content.split("\n");
@@ -1840,7 +2289,7 @@ function readLastLogLines(logPath, count, deps) {
1840
2289
  }
1841
2290
  }
1842
2291
  function readLogContent(logPath, tailCount, deps) {
1843
- const fs = deps.fs ?? nodeFs;
2292
+ const fs = deps.fs ?? nodeFs2;
1844
2293
  try {
1845
2294
  const content = fs.readFileSync(logPath, "utf8");
1846
2295
  if (typeof tailCount === "number" && tailCount > 0) {
@@ -2127,7 +2576,7 @@ async function runAgentLogsCommand(args, deps) {
2127
2576
  `);
2128
2577
  return 1;
2129
2578
  }
2130
- const fs = deps.fs ?? nodeFs;
2579
+ const fs = deps.fs ?? nodeFs2;
2131
2580
  if (!fs.existsSync(record.logPath)) {
2132
2581
  deps.output.write(`Log not found: ${record.logPath}
2133
2582
  `);
@@ -2230,6 +2679,8 @@ async function runAgentKillCommand(args, deps) {
2230
2679
 
2231
2680
  export {
2232
2681
  createShellApprovalChild,
2682
+ readHostMemory,
2683
+ assertHostMemoryAdmission,
2233
2684
  setRemoteDialogSyncTimeoutForTest,
2234
2685
  remoteDialogSyncTimeout,
2235
2686
  resolveRuntimeServerUrl,