hillclimb 0.8.4 → 0.8.5

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 +263 -21
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -13945,7 +13945,7 @@ async function runUploadWorker() {
13945
13945
  }
13946
13946
 
13947
13947
  // src/git-traces/index.ts
13948
- import { spawn as spawn3 } from "child_process";
13948
+ import { spawn as spawn4 } from "child_process";
13949
13949
  import crypto7 from "crypto";
13950
13950
 
13951
13951
  // src/git-traces/handlers.ts
@@ -13953,11 +13953,11 @@ import { execFileSync as execFileSync3 } from "child_process";
13953
13953
  import path18 from "path";
13954
13954
 
13955
13955
  // src/git-traces/git-ops.ts
13956
- import { execFileSync as execFileSync2, spawnSync } from "child_process";
13956
+ import { execFileSync as execFileSync2, spawn as spawn3, spawnSync } from "child_process";
13957
13957
  import fs16 from "fs";
13958
13958
  import os8 from "os";
13959
13959
  import path16 from "path";
13960
- import { gzipSync as gzipSync2 } from "zlib";
13960
+ import { createGzip } from "zlib";
13961
13961
  var GIT_COMMAND_TIMEOUT_MS = 12e4;
13962
13962
  var EXEC_OPTS = {
13963
13963
  timeout: GIT_COMMAND_TIMEOUT_MS,
@@ -14185,7 +14185,7 @@ function createGitError(args, err) {
14185
14185
  const failure = err;
14186
14186
  const command = formatGitCommand(args);
14187
14187
  const isTimeout = failure.code === "ETIMEDOUT";
14188
- const message = isTimeout ? `${command} timed out after ${GIT_COMMAND_TIMEOUT_MS}ms` : formatGitFailure(command, failure, err);
14188
+ const message = isTimeout ? `${command} timed out after ${GIT_COMMAND_TIMEOUT_MS}ms` : failure.code === "ENOBUFS" ? `${command} output exceeded the ${formatSize(failure.maxBufferBytes ?? EXEC_OPTS.maxBuffer)} limit` : formatGitFailure(command, failure, err);
14189
14189
  const wrapped = new Error(message);
14190
14190
  wrapped.isGitTimeout = isTimeout;
14191
14191
  return wrapped;
@@ -14477,7 +14477,111 @@ function createBundleFromTree(repoRoot, treeSha, sessionId, label) {
14477
14477
  deleteRef(repoRoot, orphanRef);
14478
14478
  }
14479
14479
  }
14480
- function createTreeDiffPatchGz(repoRoot, fromTreeSha, toTreeSha, options = {}) {
14480
+ var MAX_PATCH_GZ_BYTES = 500 * 1024 * 1024;
14481
+ function gitGzipStream(repoRoot, args, maxGzBytes) {
14482
+ return new Promise((resolve, reject) => {
14483
+ const child = spawn3("git", args, {
14484
+ cwd: repoRoot,
14485
+ stdio: ["ignore", "pipe", "pipe"],
14486
+ windowsHide: true
14487
+ });
14488
+ const gzip = createGzip();
14489
+ const gzChunks = [];
14490
+ const stderrChunks = [];
14491
+ let gzBytes = 0;
14492
+ let rawBytes = 0;
14493
+ let stderrBytes = 0;
14494
+ let settled = false;
14495
+ let timedOut = false;
14496
+ let overflowed = false;
14497
+ let streamError = null;
14498
+ let exitCode = null;
14499
+ let exitSignal = null;
14500
+ let childClosed = false;
14501
+ let gzipDone = false;
14502
+ const timer = setTimeout(() => {
14503
+ timedOut = true;
14504
+ child.kill("SIGTERM");
14505
+ }, GIT_COMMAND_TIMEOUT_MS);
14506
+ const finish = () => {
14507
+ if (settled || !childClosed || !gzipDone) return;
14508
+ settled = true;
14509
+ clearTimeout(timer);
14510
+ if (timedOut) {
14511
+ reject(createGitError(args, { code: "ETIMEDOUT" }));
14512
+ } else if (overflowed) {
14513
+ reject(
14514
+ createGitError(args, { code: "ENOBUFS", maxBufferBytes: maxGzBytes })
14515
+ );
14516
+ } else if (streamError) {
14517
+ reject(createGitError(args, streamError));
14518
+ } else if (exitCode !== 0 || exitSignal) {
14519
+ reject(
14520
+ createGitError(args, {
14521
+ status: exitCode,
14522
+ signal: exitSignal,
14523
+ stderr: Buffer.concat(stderrChunks)
14524
+ })
14525
+ );
14526
+ } else {
14527
+ resolve({ gz: Buffer.concat(gzChunks), rawBytes });
14528
+ }
14529
+ };
14530
+ child.stdout.on("data", (chunk) => {
14531
+ rawBytes += chunk.length;
14532
+ });
14533
+ child.stdout.on("error", (err) => {
14534
+ streamError ??= err;
14535
+ gzipDone = true;
14536
+ finish();
14537
+ });
14538
+ child.stdout.pipe(gzip);
14539
+ gzip.on("data", (chunk) => {
14540
+ gzBytes += chunk.length;
14541
+ if (gzBytes > maxGzBytes) {
14542
+ if (!overflowed) {
14543
+ overflowed = true;
14544
+ child.stdout.unpipe(gzip);
14545
+ child.stdout.resume();
14546
+ child.kill("SIGTERM");
14547
+ gzip.destroy();
14548
+ gzipDone = true;
14549
+ finish();
14550
+ }
14551
+ return;
14552
+ }
14553
+ gzChunks.push(chunk);
14554
+ });
14555
+ gzip.on("end", () => {
14556
+ gzipDone = true;
14557
+ finish();
14558
+ });
14559
+ gzip.on("error", (err) => {
14560
+ if (!overflowed) streamError ??= err;
14561
+ gzipDone = true;
14562
+ finish();
14563
+ });
14564
+ child.stderr.on("data", (chunk) => {
14565
+ if (stderrBytes < 16 * 1024) {
14566
+ stderrChunks.push(chunk);
14567
+ stderrBytes += chunk.length;
14568
+ }
14569
+ });
14570
+ child.on("error", (err) => {
14571
+ streamError ??= err;
14572
+ childClosed = true;
14573
+ gzipDone = true;
14574
+ finish();
14575
+ });
14576
+ child.on("close", (code, signal) => {
14577
+ exitCode = code;
14578
+ exitSignal = signal;
14579
+ childClosed = true;
14580
+ finish();
14581
+ });
14582
+ });
14583
+ }
14584
+ async function createTreeDiffPatchGz(repoRoot, fromTreeSha, toTreeSha, options = {}) {
14481
14585
  if (fromTreeSha === toTreeSha) return null;
14482
14586
  const filteredFromTreeSha = filterOmittedFilesFromTree(
14483
14587
  repoRoot,
@@ -14488,14 +14592,13 @@ function createTreeDiffPatchGz(repoRoot, fromTreeSha, toTreeSha, options = {}) {
14488
14592
  limits: options.limits
14489
14593
  });
14490
14594
  if (filteredFromTreeSha === filteredToTreeSha) return null;
14491
- const diff = gitBuffer(repoRoot, [
14492
- "diff",
14493
- "--binary",
14494
- filteredFromTreeSha,
14495
- filteredToTreeSha
14496
- ]);
14497
- if (diff.length === 0) return null;
14498
- return Buffer.from(gzipSync2(diff));
14595
+ const { gz, rawBytes } = await gitGzipStream(
14596
+ repoRoot,
14597
+ ["diff", "--binary", filteredFromTreeSha, filteredToTreeSha],
14598
+ options.maxPatchGzBytes ?? MAX_PATCH_GZ_BYTES
14599
+ );
14600
+ if (rawBytes === 0) return null;
14601
+ return gz;
14499
14602
  }
14500
14603
  function detectTransitionKind(repoRoot, prevHeadSha, nextHeadSha) {
14501
14604
  if (!prevHeadSha) return "initial";
@@ -14996,6 +15099,7 @@ var CLI_VERSION2 = "0.8.0";
14996
15099
  var GIT_TRACES_SLUG = "git-traces";
14997
15100
  var MAX_UPLOAD_BYTES = 500 * 1024 * 1024;
14998
15101
  var STALE_SCOPED_STATE_TTL_MS = 24 * 60 * 60 * 1e3;
15102
+ var REBASELINE_AFTER_DIFF_FAILURES = 2;
14999
15103
  function formatEpochSeconds3(date) {
15000
15104
  return String(Math.floor(date.getTime() / 1e3));
15001
15105
  }
@@ -15879,6 +15983,107 @@ async function refreshLineageBaseline(params) {
15879
15983
  return false;
15880
15984
  }
15881
15985
  }
15986
+ async function recoverFromWedgedDiff(params) {
15987
+ const {
15988
+ repo,
15989
+ state,
15990
+ tool,
15991
+ persistState,
15992
+ limits,
15993
+ currentSha,
15994
+ currentTreeSha,
15995
+ turnOmittedFiles
15996
+ } = params;
15997
+ const { repoRoot, config } = repo;
15998
+ if (state.contributionId === null || !state.baselineUploaded) {
15999
+ const prefix = epochPrefix2(state.epoch);
16000
+ pinRef(
16001
+ repoRoot,
16002
+ `refs/hillclimb/baseline/${state.sessionId}/${prefix}/tracked`,
16003
+ currentSha
16004
+ );
16005
+ pinRef(
16006
+ repoRoot,
16007
+ `refs/hillclimb/scoped/${state.sessionId}/baseline/${prefix}/tracked`,
16008
+ currentSha
16009
+ );
16010
+ pinFrozenBaselineTree(
16011
+ repoRoot,
16012
+ state.sessionId,
16013
+ state.epoch,
16014
+ currentTreeSha
16015
+ );
16016
+ const metadata = buildBaselineMetadata(
16017
+ repoRoot,
16018
+ state.sessionId,
16019
+ tool,
16020
+ currentSha,
16021
+ CLI_VERSION2,
16022
+ state.epoch,
16023
+ null,
16024
+ "initial",
16025
+ state.startedAt,
16026
+ turnOmittedFiles,
16027
+ state.codexLineage
16028
+ );
16029
+ Object.assign(state, {
16030
+ baselineSha: currentSha,
16031
+ baselineTreeSha: currentTreeSha,
16032
+ baselineMetadata: metadata,
16033
+ lastSnapshotSha: currentSha,
16034
+ lastSnapshotTreeSha: currentTreeSha,
16035
+ headSha: captureHeadSha(repoRoot),
16036
+ turnCount: 0,
16037
+ omittedKeys: omissionKeys(turnOmittedFiles)
16038
+ });
16039
+ delete state.diffFailureCount;
16040
+ await persistState(state);
16041
+ appendLog(
16042
+ "warn",
16043
+ `git-traces: wedge-recovery replaced the unuploaded ${prefix} baseline at the current state; the wedged interval is not recoverable (repo=${repoRoot}, project=${config.projectId}, session=${state.sessionId})`
16044
+ );
16045
+ return "skipped";
16046
+ }
16047
+ const client = await loadRepoClient(repo);
16048
+ if (!client) return "skipped";
16049
+ const nextEpoch = await reserveEpoch(state, persistState);
16050
+ appendLog(
16051
+ "warn",
16052
+ `git-traces: wedge-recovery abandoning the incremental chain after ${state.diffFailureCount} consecutive diff failures (repo=${repoRoot}, project=${config.projectId}, contribution=${state.contributionId}, abandonedEpoch=${state.epoch}, lastUploadedTurn=${state.turnCount}, openingEpoch=${nextEpoch})`
16053
+ );
16054
+ const artifacts = await openEpoch({
16055
+ repoRoot,
16056
+ client,
16057
+ contributionId: state.contributionId,
16058
+ sessionId: state.sessionId,
16059
+ tool,
16060
+ epoch: nextEpoch,
16061
+ prevHeadSha: state.headSha || null,
16062
+ transitionKind: "initial",
16063
+ startedAt: state.startedAt,
16064
+ limits,
16065
+ lineage: state.codexLineage
16066
+ });
16067
+ if (!artifacts) return "failed";
16068
+ Object.assign(state, {
16069
+ baselineSha: artifacts.baselineSha,
16070
+ baselineTreeSha: artifacts.baselineTreeSha,
16071
+ lastSnapshotSha: artifacts.baselineSha,
16072
+ lastSnapshotTreeSha: artifacts.baselineTreeSha,
16073
+ epoch: nextEpoch,
16074
+ turnCount: 0,
16075
+ headSha: artifacts.headSha,
16076
+ omittedKeys: artifacts.omittedKeys,
16077
+ codexLineageStamped: state.codexLineage
16078
+ });
16079
+ delete state.diffFailureCount;
16080
+ await persistState(state);
16081
+ appendLog(
16082
+ "info",
16083
+ `git-traces: wedge-recovery baseline uploaded (repo=${repoRoot}, project=${config.projectId}, contribution=${state.contributionId}, epoch=${nextEpoch}, baseline=${artifacts.baselineSha.slice(0, 8)})`
16084
+ );
16085
+ return "uploaded";
16086
+ }
15882
16087
  async function registerInitialContribution(params) {
15883
16088
  const { repo, state, tool, persistState, client, artifacts } = params;
15884
16089
  const contributionId = await createGitTracesContribution({
@@ -16111,6 +16316,7 @@ async function processStopRepo(repo, tool, sessionId, recordedAt, lineageCheck,
16111
16316
  omittedKeys: artifacts.omittedKeys,
16112
16317
  codexLineageStamped: state.codexLineage
16113
16318
  };
16319
+ delete next.diffFailureCount;
16114
16320
  await persistState(next);
16115
16321
  appendLog(
16116
16322
  "info",
@@ -16125,19 +16331,55 @@ async function processStopRepo(repo, tool, sessionId, recordedAt, lineageCheck,
16125
16331
  log: false,
16126
16332
  limits
16127
16333
  });
16334
+ const previousOmittedKeys = state.omittedKeys;
16128
16335
  const omissionsChanged = reportNewlyOmittedFiles(
16129
16336
  state,
16130
16337
  turnOmittedFiles,
16131
16338
  repoRoot
16132
16339
  );
16133
- const patchBuffer = createTreeDiffPatchGz(
16134
- repoRoot,
16135
- lastSnapshotTreeSha,
16136
- currentTreeSha,
16137
- { limits }
16138
- );
16340
+ const attemptWedgeRecovery = async () => {
16341
+ const outcome = await recoverFromWedgedDiff({
16342
+ repo,
16343
+ state: cleanupState,
16344
+ tool,
16345
+ persistState,
16346
+ limits,
16347
+ currentSha,
16348
+ currentTreeSha,
16349
+ turnOmittedFiles
16350
+ });
16351
+ return outcome === "uploaded" ? await finishSuccessfulStop("uploaded") : outcome;
16352
+ };
16353
+ if ((state.diffFailureCount ?? 0) >= REBASELINE_AFTER_DIFF_FAILURES) {
16354
+ appendLog(
16355
+ "warn",
16356
+ `git-traces: skipping turn diff after ${state.diffFailureCount} consecutive failures; retrying wedge recovery (repo=${repoRoot}, project=${config.projectId})`
16357
+ );
16358
+ return await attemptWedgeRecovery();
16359
+ }
16360
+ let patchBuffer;
16361
+ try {
16362
+ patchBuffer = await createTreeDiffPatchGz(
16363
+ repoRoot,
16364
+ lastSnapshotTreeSha,
16365
+ currentTreeSha,
16366
+ { limits }
16367
+ );
16368
+ } catch (err) {
16369
+ const failures = (state.diffFailureCount ?? 0) + 1;
16370
+ state.diffFailureCount = failures;
16371
+ await persistState({ ...state, omittedKeys: previousOmittedKeys });
16372
+ appendLog(
16373
+ "error",
16374
+ `git-traces: turn diff failed (repo=${repoRoot}, project=${config.projectId}, consecutiveFailures=${failures}): ${formatError(err)}`
16375
+ );
16376
+ if (failures < REBASELINE_AFTER_DIFF_FAILURES) return "failed";
16377
+ return await attemptWedgeRecovery();
16378
+ }
16379
+ const diffFailuresCleared = state.diffFailureCount !== void 0;
16380
+ delete state.diffFailureCount;
16139
16381
  if (!patchBuffer) {
16140
- if (omissionsChanged) {
16382
+ if (omissionsChanged || diffFailuresCleared) {
16141
16383
  await persistState(state);
16142
16384
  } else {
16143
16385
  await touchSessionState(state.repoRoot, tool, sessionId);
@@ -16653,7 +16895,7 @@ async function runGitTraces() {
16653
16895
  };
16654
16896
  await applyDebugLogParentEnv(workerEnv, raw);
16655
16897
  try {
16656
- const child = spawn3(
16898
+ const child = spawn4(
16657
16899
  process.execPath,
16658
16900
  [entrypoint, "git-traces", `--tool=${tool}`],
16659
16901
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hillclimb",
3
- "version": "0.8.4",
3
+ "version": "0.8.5",
4
4
  "description": "Extract and export AI coding tool logs grouped by repo",
5
5
  "license": "MIT",
6
6
  "type": "module",