hillclimb 0.9.14 → 0.9.15

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 (2) hide show
  1. package/dist/main.js +171 -28
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -16608,6 +16608,8 @@ function failureText(result) {
16608
16608
  // src/git-traces/git-ops.ts
16609
16609
  var DEFAULT_GIT_COMMAND_TIMEOUT_MS = 12e4;
16610
16610
  var DEFAULT_BUNDLE_TIMEOUT_MS = 3e5;
16611
+ var MAX_BASELINE_BUNDLE_BYTES = 1024 * 1024 * 1024;
16612
+ var DEFAULT_UNTRACKED_INDEX_TIMEOUT_MS = 3e5;
16611
16613
  var FULL_TREE_SCAN_TIMEOUT_MS = 3e5;
16612
16614
  function resolveGitCommandTimeoutMs(fallback = DEFAULT_GIT_COMMAND_TIMEOUT_MS) {
16613
16615
  const raw = process.env.HILLCLIMB_GIT_COMMAND_TIMEOUT_MS;
@@ -16901,6 +16903,7 @@ function createGitError(args, err, timeoutMs = GIT_COMMAND_TIMEOUT_MS, elapsedMs
16901
16903
  const stdout = outputToString(failure.stdout);
16902
16904
  wrapped.gitStdout = stdout ? truncateOutput(stdout) : void 0;
16903
16905
  wrapped.isGitMissingObject = !isTimeout && failure.code !== "ENOBUFS" && matchesMissingObject(stderr);
16906
+ wrapped.isGitMissingWorktreeFile = args[0] === "update-index" && failure.status === 128 && !failure.signal && (!failure.code || failure.code === "EPIPE") && !!stderr?.includes("does not exist and --remove not passed");
16904
16907
  return wrapped;
16905
16908
  }
16906
16909
  function commitTree(repoRoot, treeSha, message) {
@@ -17560,6 +17563,9 @@ function buildUntrackedTree(repoRoot, options = {}, attempt = 1) {
17560
17563
  () => gitBuffer(repoRoot, ["update-index", "--add", "-z", "--stdin"], {
17561
17564
  input: `${kept.join("\0")}\0`,
17562
17565
  env,
17566
+ timeoutMs: resolveGitCommandTimeoutMs(
17567
+ DEFAULT_UNTRACKED_INDEX_TIMEOUT_MS
17568
+ ),
17563
17569
  diagnostics: { stage: "untracked-hash-and-index" }
17564
17570
  })
17565
17571
  );
@@ -17570,8 +17576,7 @@ function buildUntrackedTree(repoRoot, options = {}, attempt = 1) {
17570
17576
  );
17571
17577
  } catch (err) {
17572
17578
  const failure = err;
17573
- if (attempt !== 1 || failure.gitExitStatus !== 128 || !failure.gitStderr?.includes("does not exist and --remove not passed"))
17574
- throw err;
17579
+ if (attempt !== 1 || !failure.isGitMissingWorktreeFile) throw err;
17575
17580
  appendLog(
17576
17581
  "warn",
17577
17582
  `git-traces: retrying untracked capture after files disappeared (repo=${repoRoot}, attempt=${attempt}, maxAttempts=2)`
@@ -17708,6 +17713,12 @@ function createBundleFromTree(repoRoot, treeSha, sessionId, label) {
17708
17713
  diagnostics: { stage: "bundle-create", bundleFile: tmpFile }
17709
17714
  })
17710
17715
  );
17716
+ const bytes = fs19.statSync(tmpFile).size;
17717
+ if (bytes > MAX_BASELINE_BUNDLE_BYTES) {
17718
+ throw new Error(
17719
+ `Git baseline bundle exceeds the upload limit (bytes=${bytes}, maxBytes=${MAX_BASELINE_BUNDLE_BYTES}); capture retained`
17720
+ );
17721
+ }
17711
17722
  const bundle = fs19.readFileSync(tmpFile);
17712
17723
  appendLog(
17713
17724
  "info",
@@ -17723,7 +17734,21 @@ function createBundleFromTree(repoRoot, treeSha, sessionId, label) {
17723
17734
  }
17724
17735
  }
17725
17736
  var MAX_PATCH_GZ_BYTES = 500 * 1024 * 1024;
17726
- function gitGzipStream(repoRoot, args, maxGzBytes) {
17737
+ var MAX_PATCH_RAW_BYTES = 1023 * 1024 * 1024 - 1;
17738
+ var GitPatchTooLargeError = class extends Error {
17739
+ constructor(rawBytes, maxRawBytes) {
17740
+ super(
17741
+ `git diff raw output exceeded the ${maxRawBytes}-byte replay limit (observedBytes=${rawBytes}); a new baseline is required`
17742
+ );
17743
+ this.rawBytes = rawBytes;
17744
+ this.maxRawBytes = maxRawBytes;
17745
+ this.name = "GitPatchTooLargeError";
17746
+ }
17747
+ };
17748
+ function resolveMaxPatchRawBytes(value) {
17749
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? Math.min(value, MAX_PATCH_RAW_BYTES) : MAX_PATCH_RAW_BYTES;
17750
+ }
17751
+ function gitGzipStream(repoRoot, args, maxGzBytes, maxRawBytes) {
17727
17752
  return new Promise((resolve, reject) => {
17728
17753
  const started = Date.now();
17729
17754
  const child = spawn2("git", args, {
@@ -17740,6 +17765,7 @@ function gitGzipStream(repoRoot, args, maxGzBytes) {
17740
17765
  let settled = false;
17741
17766
  let timedOut = false;
17742
17767
  let overflowed = false;
17768
+ let rawOverflowed = false;
17743
17769
  let streamError = null;
17744
17770
  let exitCode = null;
17745
17771
  let exitSignal = null;
@@ -17766,6 +17792,8 @@ function gitGzipStream(repoRoot, args, maxGzBytes) {
17766
17792
  );
17767
17793
  probeObjectStore(repoRoot, "timeout", wrapped.gitStderr, args);
17768
17794
  reject(wrapped);
17795
+ } else if (rawOverflowed) {
17796
+ reject(new GitPatchTooLargeError(rawBytes, maxRawBytes));
17769
17797
  } else if (overflowed) {
17770
17798
  reject(
17771
17799
  createGitError(args, { code: "ENOBUFS", maxBufferBytes: maxGzBytes })
@@ -17784,8 +17812,22 @@ function gitGzipStream(repoRoot, args, maxGzBytes) {
17784
17812
  resolve({ gz: Buffer.concat(gzChunks), rawBytes });
17785
17813
  }
17786
17814
  };
17815
+ const stopForOverflow = () => {
17816
+ child.stdout.unpipe(gzip);
17817
+ child.stdout.resume();
17818
+ child.kill("SIGTERM");
17819
+ gzip.destroy();
17820
+ gzChunks.length = 0;
17821
+ gzipDone = true;
17822
+ finish();
17823
+ };
17787
17824
  child.stdout.on("data", (chunk) => {
17825
+ if (rawOverflowed || overflowed) return;
17788
17826
  rawBytes += chunk.length;
17827
+ if (rawBytes > maxRawBytes) {
17828
+ rawOverflowed = true;
17829
+ stopForOverflow();
17830
+ }
17789
17831
  });
17790
17832
  child.stdout.on("error", (err) => {
17791
17833
  streamError ??= err;
@@ -17794,17 +17836,11 @@ function gitGzipStream(repoRoot, args, maxGzBytes) {
17794
17836
  });
17795
17837
  child.stdout.pipe(gzip);
17796
17838
  gzip.on("data", (chunk) => {
17839
+ if (rawOverflowed || overflowed) return;
17797
17840
  gzBytes += chunk.length;
17798
17841
  if (gzBytes > maxGzBytes) {
17799
- if (!overflowed) {
17800
- overflowed = true;
17801
- child.stdout.unpipe(gzip);
17802
- child.stdout.resume();
17803
- child.kill("SIGTERM");
17804
- gzip.destroy();
17805
- gzipDone = true;
17806
- finish();
17807
- }
17842
+ overflowed = true;
17843
+ stopForOverflow();
17808
17844
  return;
17809
17845
  }
17810
17846
  gzChunks.push(chunk);
@@ -17814,7 +17850,7 @@ function gitGzipStream(repoRoot, args, maxGzBytes) {
17814
17850
  finish();
17815
17851
  });
17816
17852
  gzip.on("error", (err) => {
17817
- if (!overflowed) streamError ??= err;
17853
+ if (!overflowed && !rawOverflowed) streamError ??= err;
17818
17854
  gzipDone = true;
17819
17855
  finish();
17820
17856
  });
@@ -17855,7 +17891,8 @@ async function createTreeDiffPatchGz(repoRoot, fromTreeSha, toTreeSha, options =
17855
17891
  const { gz, rawBytes } = await gitGzipStream(
17856
17892
  repoRoot,
17857
17893
  ["diff", "--binary", filteredFromTreeSha, filteredToTreeSha],
17858
- options.maxPatchGzBytes ?? MAX_PATCH_GZ_BYTES
17894
+ options.maxPatchGzBytes ?? MAX_PATCH_GZ_BYTES,
17895
+ resolveMaxPatchRawBytes(options.maxPatchRawBytes)
17859
17896
  );
17860
17897
  if (rawBytes === 0) return null;
17861
17898
  return gz;
@@ -18452,6 +18489,8 @@ async function sweepStaleLockFiles(ttlMs = STALE_LOCK_TTL_MS3, now = Date.now())
18452
18489
  // src/git-traces/pending-captures.ts
18453
18490
  var MAX_CAPTURES = 128;
18454
18491
  var MAX_PENDING_BYTES2 = 2 * 1024 * 1024 * 1024;
18492
+ var MAX_OVERSIZED_CAPTURE_BYTES = 4 * 1024 * 1024 * 1024;
18493
+ var MAX_OVERSIZED_ARTIFACT_BYTES = 4 * 1024 * 1024 * 1024;
18455
18494
  var PendingCaptureLimitError = class extends Error {
18456
18495
  };
18457
18496
  var STRANDED_MIN_AGE_MS = 10 * 60 * 1e3;
@@ -18664,6 +18703,11 @@ function pendingBytes(records) {
18664
18703
  0
18665
18704
  );
18666
18705
  }
18706
+ function artifactByteLimit(records, captureId) {
18707
+ const record = records.length === 1 ? records[0] : void 0;
18708
+ const retained = record ? chargedBytes(record) : 0;
18709
+ return record?.id === captureId && record.oversized === true && retained >= 0 && retained <= MAX_OVERSIZED_CAPTURE_BYTES ? retained + MAX_OVERSIZED_ARTIFACT_BYTES : MAX_PENDING_BYTES2;
18710
+ }
18667
18711
  function warnMissingBase(repoRoot, base) {
18668
18712
  appendLog(
18669
18713
  "warn",
@@ -18818,10 +18862,15 @@ function storePendingCapture(params) {
18818
18862
  const pendingBefore = queueDiskBytes(
18819
18863
  queueDirectory(params.repoRoot, params.tool, params.sessionId)
18820
18864
  ) + existing.reduce((sum, item) => sum + chargedBytes(item), 0);
18821
- const incomingBytes = retainedBytes + Buffer.byteLength(JSON.stringify(record));
18822
- if (existing.length >= MAX_CAPTURES || pendingBefore + incomingBytes > MAX_PENDING_BYTES2) {
18865
+ let incomingBytes = retainedBytes + Buffer.byteLength(JSON.stringify(record));
18866
+ if (existing.length === 0 && pendingBefore === 0 && incomingBytes > MAX_PENDING_BYTES2) {
18867
+ record.oversized = true;
18868
+ incomingBytes = retainedBytes + Buffer.byteLength(JSON.stringify(record));
18869
+ }
18870
+ const maxBytes = record.oversized ? MAX_OVERSIZED_CAPTURE_BYTES : MAX_PENDING_BYTES2;
18871
+ if (existing.some((item) => item.oversized === true) || existing.length >= MAX_CAPTURES || pendingBefore + incomingBytes > maxBytes) {
18823
18872
  throw new PendingCaptureLimitError(
18824
- `Pending Git capture limit reached (count=${existing.length}, maxCount=${MAX_CAPTURES}, pendingBytes=${pendingBefore}, incomingBytes=${incomingBytes}, retainedBytes=${retainedBytes}, maxBytes=${MAX_PENDING_BYTES2}); existing captures retained`
18873
+ `Pending Git capture limit reached (count=${existing.length}, maxCount=${MAX_CAPTURES}, pendingBytes=${pendingBefore}, incomingBytes=${incomingBytes}, retainedBytes=${retainedBytes}, maxBytes=${maxBytes}); existing captures retained`
18825
18874
  );
18826
18875
  }
18827
18876
  const directory = captureDirectory(record);
@@ -18863,7 +18912,7 @@ function storePendingCapture(params) {
18863
18912
  durableDetails = `capture=${record.id}, durableCapturedAt=${durableCapturedAt}`;
18864
18913
  appendLog(
18865
18914
  "info",
18866
- `git-traces: capture queued (repo=${record.repoRoot}, session=${record.sessionId}, capture=${record.id}, recordedAt=${record.recordedAt}, captureStartedAt=${record.captureStartedAt}, capturedAt=${record.capturedAt}, sourceCapturedAt=${record.capturedAt}, durableCapturedAt=${durableCapturedAt}, pending=${existing.length + 1}, oldestPendingAgeMs=${Date.now() - (existing[0]?.capturedAt ?? record.capturedAt)}, retainedBytes=${record.retainedBytes}, pendingBytes=${pendingBefore + incomingBytes})`
18915
+ `git-traces: capture queued (repo=${record.repoRoot}, session=${record.sessionId}, capture=${record.id}, recordedAt=${record.recordedAt}, captureStartedAt=${record.captureStartedAt}, capturedAt=${record.capturedAt}, sourceCapturedAt=${record.capturedAt}, durableCapturedAt=${durableCapturedAt}, pending=${existing.length + 1}, oldestPendingAgeMs=${Date.now() - (existing[0]?.capturedAt ?? record.capturedAt)}, retainedBytes=${record.retainedBytes}, pendingBytes=${pendingBefore + incomingBytes}, oversized=${record.oversized === true})`
18867
18916
  );
18868
18917
  return record;
18869
18918
  } finally {
@@ -18913,18 +18962,30 @@ async function preparePendingUpload(context, contributionId, filename, buffer) {
18913
18962
  context.record.tool,
18914
18963
  context.record.sessionId
18915
18964
  );
18916
- if (pendingBytes(records) + buffer.byteLength > MAX_PENDING_BYTES2) {
18917
- throw new Error(
18918
- `Pending Git artifact byte limit reached (${MAX_PENDING_BYTES2}); capture retained`
18919
- );
18920
- }
18921
18965
  const file = `${crypto7.createHash("sha256").update(key).digest("hex")}.artifact`;
18922
- writeDurable(path20.join(directory, file), buffer);
18923
- context.record.uploads[key] = {
18966
+ const upload = {
18924
18967
  file,
18925
18968
  bytes: buffer.byteLength,
18926
18969
  sha256: crypto7.createHash("sha256").update(buffer).digest("hex")
18927
18970
  };
18971
+ const nextRecordBytes = Buffer.byteLength(
18972
+ JSON.stringify({
18973
+ ...context.record,
18974
+ uploads: { ...context.record.uploads, [key]: upload }
18975
+ })
18976
+ );
18977
+ const recordGrowth = Math.max(
18978
+ 0,
18979
+ nextRecordBytes - fs21.statSync(path20.join(directory, "capture.json")).size
18980
+ );
18981
+ const maxBytes = artifactByteLimit(records, context.record.id);
18982
+ if (pendingBytes(records) + buffer.byteLength + recordGrowth > maxBytes) {
18983
+ throw new Error(
18984
+ `Pending Git artifact byte limit reached (${maxBytes}); capture retained`
18985
+ );
18986
+ }
18987
+ writeDurable(path20.join(directory, file), buffer);
18988
+ context.record.uploads[key] = upload;
18928
18989
  try {
18929
18990
  context.save();
18930
18991
  } catch (err) {
@@ -19054,7 +19115,7 @@ async function uploadArtifact(client, contributionId, filename, mimeType, buffer
19054
19115
  }
19055
19116
 
19056
19117
  // package.json
19057
- var version = "0.9.14";
19118
+ var version = "0.9.15";
19058
19119
 
19059
19120
  // src/version.ts
19060
19121
  var CLI_VERSION = version;
@@ -20643,7 +20704,6 @@ import path22 from "path";
20643
20704
  var GIT_TRACES_SLUG = "git-traces";
20644
20705
  var MAX_FORK_SWEEP_THREADS2 = 8;
20645
20706
  var MAX_UPLOAD_BYTES = 500 * 1024 * 1024;
20646
- var MAX_BASELINE_BUNDLE_UPLOAD_BYTES = 1024 * 1024 * 1024;
20647
20707
  var STALE_SCOPED_STATE_TTL_MS = 24 * 60 * 60 * 1e3;
20648
20708
  var REBASELINE_AFTER_DIFF_FAILURES = 2;
20649
20709
  async function withRepoCaptureLock(repoRoot, capture) {
@@ -20788,7 +20848,7 @@ function execGit(cwd, args) {
20788
20848
  }).toString("utf-8").trim();
20789
20849
  }
20790
20850
  function canUploadFile(filename, buffer) {
20791
- const limit = filename.endsWith("-baseline.bundle") ? MAX_BASELINE_BUNDLE_UPLOAD_BYTES : MAX_UPLOAD_BYTES;
20851
+ const limit = filename.endsWith("-baseline.bundle") ? MAX_BASELINE_BUNDLE_BYTES : MAX_UPLOAD_BYTES;
20792
20852
  if (buffer.byteLength <= limit) return true;
20793
20853
  appendLog(
20794
20854
  "warn",
@@ -21871,6 +21931,75 @@ async function refreshLineageBaseline(params) {
21871
21931
  return false;
21872
21932
  }
21873
21933
  }
21934
+ async function uploadLargeCaptureBaseline(params) {
21935
+ const { repo, state, tool, persistState, limits } = params;
21936
+ const pending = currentPendingCapture(repo.repoRoot);
21937
+ if (!pending) return "failed";
21938
+ const client = await loadRepoClient(repo);
21939
+ if (!client) return "skipped";
21940
+ if (state.contributionId === null || !state.baselineUploaded) {
21941
+ const initial = buildInitialBaselineArtifactsForState(
21942
+ repo.repoRoot,
21943
+ tool,
21944
+ state,
21945
+ limits
21946
+ );
21947
+ if (!initial || !canUploadEpochBaselineArtifacts(state.epoch, initial))
21948
+ return "failed";
21949
+ if (!await registerInitialContribution({
21950
+ ...params,
21951
+ client,
21952
+ artifacts: initial
21953
+ }))
21954
+ return "failed";
21955
+ }
21956
+ if (state.contributionId === null) return "failed";
21957
+ const nextEpoch = await reserveEpoch(state, persistState);
21958
+ const artifacts = await openEpoch({
21959
+ repoRoot: repo.repoRoot,
21960
+ client,
21961
+ contributionId: state.contributionId,
21962
+ sessionId: state.sessionId,
21963
+ tool,
21964
+ epoch: nextEpoch,
21965
+ prevHeadSha: state.headSha || null,
21966
+ transitionKind: "initial",
21967
+ startedAt: state.startedAt,
21968
+ limits,
21969
+ lineage: state.codexLineage,
21970
+ previousScan: pending.record.scan,
21971
+ provenance: {
21972
+ recovery: {
21973
+ reason: "patch-too-large",
21974
+ trigger: "stop-diff",
21975
+ detectedAt: new Date(pending.record.capturedAt).toISOString(),
21976
+ abandonedEpoch: state.epoch,
21977
+ abandonedBaselineSha: state.baselineSha,
21978
+ abandonedUploadedTurns: state.turnCount
21979
+ }
21980
+ }
21981
+ });
21982
+ if (!artifacts) return "failed";
21983
+ Object.assign(state, {
21984
+ baselineSha: artifacts.baselineSha,
21985
+ baselineTreeSha: artifacts.baselineTreeSha,
21986
+ lastSnapshotSha: artifacts.baselineSha,
21987
+ lastSnapshotTreeSha: artifacts.baselineTreeSha,
21988
+ epoch: nextEpoch,
21989
+ turnCount: 0,
21990
+ headSha: artifacts.headSha,
21991
+ omittedKeys: artifacts.omittedKeys,
21992
+ omissionScan: artifacts.scan,
21993
+ codexLineageStamped: artifacts.uploadedLineage
21994
+ });
21995
+ delete state.diffFailureCount;
21996
+ await persistState(state);
21997
+ appendLog(
21998
+ "info",
21999
+ `git-traces: large patch captured as baseline (repo=${repo.repoRoot}, session=${state.sessionId}, capture=${pending.record.id}, epoch=${nextEpoch}, tree=${artifacts.baselineTreeSha})`
22000
+ );
22001
+ return "uploaded";
22002
+ }
21874
22003
  async function recoverFromWedgedDiff(params) {
21875
22004
  const {
21876
22005
  repo,
@@ -22362,6 +22491,20 @@ async function processStopRepo(repo, tool, sessionId, recordedAt, lineageCheck,
22362
22491
  }
22363
22492
  );
22364
22493
  } catch (err) {
22494
+ if (err instanceof GitPatchTooLargeError) {
22495
+ appendLog(
22496
+ "warn",
22497
+ `git-traces: turn patch exceeds Git apply limit; attempting frozen baseline fallback (repo=${repoRoot}, session=${state.sessionId}, rawBytes=${err.rawBytes}, maxRawBytes=${err.maxRawBytes})`
22498
+ );
22499
+ const outcome = await uploadLargeCaptureBaseline({
22500
+ repo,
22501
+ state,
22502
+ tool,
22503
+ persistState,
22504
+ limits
22505
+ });
22506
+ return outcome === "uploaded" ? await finishSuccessfulStop("uploaded") : outcome;
22507
+ }
22365
22508
  if (isMissingObjectError(err)) {
22366
22509
  logMissingBaselineObjects(repoRoot, state, "stop-diff", err);
22367
22510
  state.diffFailureCount = REBASELINE_AFTER_DIFF_FAILURES;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hillclimb",
3
- "version": "0.9.14",
3
+ "version": "0.9.15",
4
4
  "description": "Extract and export AI coding tool logs grouped by repo",
5
5
  "license": "MIT",
6
6
  "type": "module",