@jphutchins/code-review 0.1.0-alpha.33 → 0.1.0-alpha.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -773,24 +773,35 @@ var DEFAULT_RESERVE = {
773
773
  frac: 0.15,
774
774
  growth: 0.25,
775
775
  flatUsd: 0.02,
776
- flatMs: 12e4
776
+ flatMs: 12e4,
777
+ flatMem: 2 * 1024 * 1024 * 1024
777
778
  };
778
779
  var SOFT_MULTIPLE = 2;
779
- var costAxis = (i) => i.spentUsd !== null && i.budgetUsd !== null && i.budgetUsd > 0 ? { used: i.spentUsd, limit: i.budgetUsd, flat: i.reserve.flatUsd } : null;
780
- var timeAxis = (i) => i.elapsedMs !== null && i.wallMs !== null && i.wallMs > 0 ? { used: i.elapsedMs, limit: i.wallMs, flat: i.reserve.flatMs } : null;
781
- var axisSeverity = (a, reserve) => {
782
- const usedFrac = Math.min(1, Math.max(0, a.used / a.limit));
783
- const effFrac = reserve.frac + reserve.growth * usedFrac;
784
- const hardReserve = Math.max(a.flat, effFrac * a.limit);
780
+ var growingReserve = (used, limit, flat, r) => {
781
+ const usedFrac = Math.min(1, Math.max(0, used / limit));
782
+ return Math.max(flat, (r.frac + r.growth * usedFrac) * limit);
783
+ };
784
+ var costAxis = (i) => i.spentUsd !== null && i.budgetUsd !== null && i.budgetUsd > 0 ? {
785
+ used: i.spentUsd,
786
+ limit: i.budgetUsd,
787
+ hardReserve: growingReserve(i.spentUsd, i.budgetUsd, i.reserve.flatUsd, i.reserve)
788
+ } : null;
789
+ var timeAxis = (i) => i.elapsedMs !== null && i.wallMs !== null && i.wallMs > 0 ? {
790
+ used: i.elapsedMs,
791
+ limit: i.wallMs,
792
+ hardReserve: growingReserve(i.elapsedMs, i.wallMs, i.reserve.flatMs, i.reserve)
793
+ } : null;
794
+ var axisSeverity = (a) => {
785
795
  const remaining = a.limit - a.used;
786
- if (remaining <= hardReserve) return 2;
787
- if (remaining <= SOFT_MULTIPLE * hardReserve) return 1;
796
+ if (remaining <= a.hardReserve) return 2;
797
+ if (remaining <= SOFT_MULTIPLE * a.hardReserve) return 1;
788
798
  return 0;
789
799
  };
790
800
  var decideBudget = (i) => {
791
- const worst = [costAxis(i), timeAxis(i)].filter((a) => a !== null).reduce((max, a) => Math.max(max, axisSeverity(a, i.reserve)), 0);
801
+ const worst = [costAxis(i), timeAxis(i)].filter((a) => a !== null).reduce((max, a) => Math.max(max, axisSeverity(a)), 0);
792
802
  return worst === 2 ? { kind: "hard" } : worst === 1 ? { kind: "soft" } : { kind: "ok" };
793
803
  };
804
+ var memoryCritical = (availMemBytes, totalMemBytes, floorBytes) => availMemBytes !== null && totalMemBytes !== null && totalMemBytes > 0 && availMemBytes <= floorBytes;
794
805
  var pct = (n) => `${String(Math.round(n * 100))}%`;
795
806
  var money = (n) => `$${n.toFixed(2)}`;
796
807
  var mins = (ms) => `${(ms / 6e4).toFixed(1)}m`;
@@ -845,6 +856,7 @@ var lastValidPath = (draftPath) => {
845
856
  };
846
857
  var mainHasWrittenDraft = (draftMtimeMs, seedMarkerMtimeMs) => draftMtimeMs !== null && (seedMarkerMtimeMs === null || draftMtimeMs > seedMarkerMtimeMs);
847
858
  var spawnFloorMessage = (draftPath) => `Write your own first-pass findings to ${draftPath} before spawning subagents \u2014 a review must never depend on subagents alone, and a pre-seeded draft does not count until you have revised it yourself this run. Write ${draftPath} from what you have read so far (preliminary findings are fine), run \`code-review validate ${draftPath} --explain\` until it passes, then fan out; your subagents run in the background, so keep refining the draft as their reports arrive.`;
859
+ var memoryPressureMessage = (draftPath) => `System memory is critically low right now, so another subagent can't be spawned \u2014 a fresh process is the fastest way to tip the runner into an out-of-memory kill that would lose the whole review. Don't wind down: keep reading code directly and keep folding the reports from subagents already running into ${draftPath}, then try spawning again in a moment \u2014 this clears as soon as running subagents finish and free their memory.`;
848
860
  var forceBackgroundSpawn = (toolInput) => ({
849
861
  hookSpecificOutput: {
850
862
  hookEventName: "PreToolUse",
@@ -890,6 +902,8 @@ var evaluateBudgetHook = (input, params) => {
890
902
  if (phase.kind === "hard" && blockedDuringConvergence(toolName, rec["tool_input"]))
891
903
  return denyPreTool(budgetMessage(inputs, phase, params.draftPath, isSubagent));
892
904
  if (SPAWN_TOOLS.has(toolName)) {
905
+ if (memoryCritical(params.availMemBytes, params.totalMemBytes, params.reserve.flatMem))
906
+ return denyPreTool(memoryPressureMessage(params.draftPath));
893
907
  if (!isSubagent && !params.mainDraftWritten)
894
908
  return denyPreTool(spawnFloorMessage(params.draftPath));
895
909
  return forceBackgroundSpawn(rec["tool_input"]);
@@ -936,6 +950,21 @@ var parseFraction = (raw, fallback) => {
936
950
  const n = Number.parseFloat(raw);
937
951
  return Number.isFinite(n) && n >= 0 && n <= 1 ? n : fallback;
938
952
  };
953
+ var BYTE_UNIT = {
954
+ "": 1,
955
+ k: 1024,
956
+ m: 1024 * 1024,
957
+ g: 1024 * 1024 * 1024,
958
+ t: 1024 * 1024 * 1024 * 1024
959
+ };
960
+ var parseByteSize = (raw) => {
961
+ const m = /^(\d+(?:\.\d+)?)\s*([kmgt])?(?:i?b)?$/i.exec(raw.trim());
962
+ if (m === null) return null;
963
+ const [, num = "", unit = ""] = m;
964
+ const n = Number.parseFloat(num);
965
+ const mult = BYTE_UNIT[unit.toLowerCase()];
966
+ return Number.isFinite(n) && mult !== void 0 ? n * mult : null;
967
+ };
939
968
  var budgetHookCommand = (draftPath, opts) => [
940
969
  "code-review budget-hook --draft",
941
970
  shellQuote(draftPath),
@@ -945,7 +974,8 @@ var budgetHookCommand = (draftPath, opts) => [
945
974
  ...opts.reserveFrac ? ["--reserve-frac", shellQuote(opts.reserveFrac)] : [],
946
975
  ...opts.reserveGrowth ? ["--reserve-growth", shellQuote(opts.reserveGrowth)] : [],
947
976
  ...opts.reserveUsd ? ["--reserve-usd", shellQuote(opts.reserveUsd)] : [],
948
- ...opts.reserveWall ? ["--reserve-wall", shellQuote(opts.reserveWall)] : []
977
+ ...opts.reserveWall ? ["--reserve-wall", shellQuote(opts.reserveWall)] : [],
978
+ ...opts.reserveMem ? ["--reserve-mem", shellQuote(opts.reserveMem)] : []
949
979
  ].join(" ");
950
980
 
951
981
  // src/format.ts
@@ -3002,6 +3032,18 @@ var snapshotIfValid = (draftPath) => {
3002
3032
  );
3003
3033
  }
3004
3034
  };
3035
+ var readMemInfo = () => {
3036
+ try {
3037
+ const text = readFileSync("/proc/meminfo", "utf8");
3038
+ const kb = (key2) => {
3039
+ const m = new RegExp(`^${key2}:\\s+(\\d+)\\s+kB`, "m").exec(text);
3040
+ return m?.[1] !== void 0 ? Number(m[1]) * 1024 : null;
3041
+ };
3042
+ return { availBytes: kb("MemAvailable"), totalBytes: kb("MemTotal") };
3043
+ } catch {
3044
+ return { availBytes: null, totalBytes: null };
3045
+ }
3046
+ };
3005
3047
  var budgetHookCmd = defineCommand({
3006
3048
  meta: {
3007
3049
  name: "budget-hook",
@@ -3040,12 +3082,17 @@ var budgetHookCmd = defineCommand({
3040
3082
  "reserve-wall": {
3041
3083
  type: "string",
3042
3084
  description: "Flat wall-clock wind-down floor (e.g. 2m, 120s), whichever is larger with --reserve-frac (default: 2m)"
3085
+ },
3086
+ "reserve-mem": {
3087
+ type: "string",
3088
+ description: "Free-RAM floor (e.g. 2g, 1536m) below which new subagent spawns are denied until memory recovers; not a convergence axis (default: 2g)"
3043
3089
  }
3044
3090
  },
3045
3091
  run: async ({ args }) => {
3046
3092
  try {
3047
3093
  const draftPath = resolve$1(args.draft);
3048
3094
  const input = readStdinJSON();
3095
+ const mem = readMemInfo();
3049
3096
  const transcriptPath = transcriptPathOf(input);
3050
3097
  const tree = transcriptPath ? readTranscriptTree(resolve$1(transcriptPath)) : void 0;
3051
3098
  const usage = tree ? sumTranscriptUsage(tree.entries) : void 0;
@@ -3062,11 +3109,14 @@ var budgetHookCmd = defineCommand({
3062
3109
  nowMs: Date.now()
3063
3110
  }),
3064
3111
  wallMs,
3112
+ availMemBytes: mem.availBytes,
3113
+ totalMemBytes: mem.totalBytes,
3065
3114
  reserve: {
3066
3115
  frac: parseFraction(args["reserve-frac"], DEFAULT_RESERVE.frac),
3067
3116
  growth: parseFraction(args["reserve-growth"], DEFAULT_RESERVE.growth),
3068
3117
  flatUsd: parseBudgetUsd(args["reserve-usd"]) ?? DEFAULT_RESERVE.flatUsd,
3069
- flatMs: args["reserve-wall"] ? parseWallMs(args["reserve-wall"]) ?? DEFAULT_RESERVE.flatMs : DEFAULT_RESERVE.flatMs
3118
+ flatMs: args["reserve-wall"] ? parseWallMs(args["reserve-wall"]) ?? DEFAULT_RESERVE.flatMs : DEFAULT_RESERVE.flatMs,
3119
+ flatMem: args["reserve-mem"] ? parseByteSize(args["reserve-mem"]) ?? DEFAULT_RESERVE.flatMem : DEFAULT_RESERVE.flatMem
3070
3120
  },
3071
3121
  draftPath,
3072
3122
  mainDraftWritten: mainHasWrittenDraft(
@@ -3143,6 +3193,10 @@ var printSettingsCmd = defineCommand({
3143
3193
  "reserve-wall": {
3144
3194
  type: "string",
3145
3195
  description: "Flat wall-clock wind-down floor (e.g. 2m), whichever is larger with --reserve-frac (default: 2m)"
3196
+ },
3197
+ "reserve-mem": {
3198
+ type: "string",
3199
+ description: "Free-RAM floor (e.g. 2g) below which new subagent spawns are denied until memory recovers (default: 2g)"
3146
3200
  }
3147
3201
  },
3148
3202
  run: async ({ args }) => {
@@ -3164,7 +3218,8 @@ var printSettingsCmd = defineCommand({
3164
3218
  reserveFrac: args["reserve-frac"],
3165
3219
  reserveGrowth: args["reserve-growth"],
3166
3220
  reserveUsd: args["reserve-usd"],
3167
- reserveWall: args["reserve-wall"]
3221
+ reserveWall: args["reserve-wall"],
3222
+ reserveMem: args["reserve-mem"]
3168
3223
  }
3169
3224
  });
3170
3225
  process.stdout.write(`${JSON.stringify(settings)}