hillclimb 0.8.3 → 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 +321 -37
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -2349,7 +2349,11 @@ var VOLATILE_KEYS = /* @__PURE__ */ new Set([
2349
2349
  "wall_time_ms"
2350
2350
  ]);
2351
2351
  function sessionsDir() {
2352
- return process.env.HILLCLIMB_CODEX_SESSIONS_DIR ?? path9.join(os3.homedir(), ".codex", "sessions");
2352
+ if (process.env.HILLCLIMB_CODEX_SESSIONS_DIR) {
2353
+ return process.env.HILLCLIMB_CODEX_SESSIONS_DIR;
2354
+ }
2355
+ const codexHome = process.env.CODEX_HOME || path9.join(os3.homedir(), ".codex");
2356
+ return path9.join(codexHome, "sessions");
2353
2357
  }
2354
2358
  function asRecord(value) {
2355
2359
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
@@ -2519,6 +2523,10 @@ async function findCodexRolloutPath(sessionId) {
2519
2523
  }
2520
2524
  return void 0;
2521
2525
  }
2526
+ async function isCodexInternalThread(payload, sessionId) {
2527
+ if (!sessionId || payload.transcript_path) return false;
2528
+ return await findCodexRolloutPath(sessionId) === void 0;
2529
+ }
2522
2530
  function commonPrefixLength(a, b) {
2523
2531
  const limit = Math.min(a.length, b.length);
2524
2532
  let count = 0;
@@ -11762,14 +11770,20 @@ function getSourceBaseDir(sourceName) {
11762
11770
  }
11763
11771
  }
11764
11772
  function archivePathFor(file) {
11773
+ return path11.posix.join(file.sourceName, path11.basename(file.absolutePath));
11774
+ }
11775
+ function legacyArchivePathFor(file) {
11765
11776
  const baseDir = getSourceBaseDir(file.sourceName);
11766
11777
  const relativePath = file.absolutePath.startsWith(baseDir) ? path11.relative(baseDir, file.absolutePath) : path11.basename(file.absolutePath);
11767
11778
  return path11.join(file.sourceName, relativePath);
11768
11779
  }
11769
11780
  function addGroupToArchive(archive, group, selectedSources) {
11781
+ const used = /* @__PURE__ */ new Set();
11770
11782
  for (const file of group.files) {
11771
11783
  if (!selectedSources.has(file.sourceName)) continue;
11772
- const archivePath = archivePathFor(file);
11784
+ let archivePath = archivePathFor(file);
11785
+ if (used.has(archivePath)) archivePath = legacyArchivePathFor(file);
11786
+ used.add(archivePath);
11773
11787
  if (file.content) {
11774
11788
  archive.append(file.content, { name: archivePath });
11775
11789
  } else {
@@ -13065,7 +13079,7 @@ async function runUploadInner(payload) {
13065
13079
  "warn",
13066
13080
  `Skipping upload: missing required hook fields (session_id=${!!sessionId}, transcript_path=${!!payload.transcript_path}, cwd=${!!cwd})`
13067
13081
  );
13068
- return false;
13082
+ return "skipped";
13069
13083
  }
13070
13084
  const match = await findProjectForCwd(cwd);
13071
13085
  if (!match) {
@@ -13073,13 +13087,13 @@ async function runUploadInner(payload) {
13073
13087
  "warn",
13074
13088
  `Skipping session ${sessionId}: no hillclimb config for cwd ${cwd}. Run \`npx hillclimb\` in the repo.`
13075
13089
  );
13076
- return false;
13090
+ return "skipped";
13077
13091
  }
13078
13092
  const { repoRoot, config } = match;
13079
13093
  const sourceTool = await resolveSourceTool(payload, repoRoot);
13080
13094
  if (!sourceTool) {
13081
13095
  payload.tool = "unknown";
13082
- return false;
13096
+ return "skipped";
13083
13097
  }
13084
13098
  payload.tool = sourceTool;
13085
13099
  appendLog(
@@ -13093,11 +13107,18 @@ async function runUploadInner(payload) {
13093
13107
  sessionId
13094
13108
  );
13095
13109
  if (!transcriptPath) {
13110
+ if (sourceTool === "codex") {
13111
+ appendLog(
13112
+ "info",
13113
+ `Skipping session ${sessionId}: Codex internal thread (no transcript_path and no rollout under ~/.codex/sessions); not a user session`
13114
+ );
13115
+ return "internal-thread";
13116
+ }
13096
13117
  appendLog(
13097
13118
  "warn",
13098
13119
  `Skipping upload: missing required hook fields (session_id=true, transcript_path=false, cwd=true)`
13099
13120
  );
13100
- return false;
13121
+ return "skipped";
13101
13122
  }
13102
13123
  const transcriptResolved = path15.resolve(transcriptPath);
13103
13124
  try {
@@ -13107,23 +13128,32 @@ async function runUploadInner(payload) {
13107
13128
  "warn",
13108
13129
  `Skipping session ${sessionId}: transcript_path is not a file: ${transcriptResolved}`
13109
13130
  );
13110
- return false;
13131
+ return "skipped";
13111
13132
  }
13112
13133
  } catch (err) {
13134
+ const detail = err instanceof Error ? err.message : String(err);
13135
+ const missing = err?.code === "ENOENT";
13136
+ if (missing && eventKind2 === "sessionEnd") {
13137
+ appendLog(
13138
+ "info",
13139
+ `Skipping session ${sessionId}: session ended without any turns; transcript was never written (${transcriptResolved})`
13140
+ );
13141
+ return "skipped";
13142
+ }
13113
13143
  appendLog(
13114
13144
  "warn",
13115
- `Skipping session ${sessionId}: transcript_path not readable (${transcriptResolved}): ${err instanceof Error ? err.message : String(err)}`
13145
+ `Skipping session ${sessionId}: transcript_path not readable (${transcriptResolved}): ${detail}`
13116
13146
  );
13117
- return false;
13147
+ return "skipped";
13118
13148
  }
13119
13149
  if (!await hasAssistantMessage(transcriptResolved)) {
13120
13150
  appendLog(
13121
13151
  "info",
13122
13152
  `Skipping session ${sessionId}: transcript contains no assistant messages (nothing to upload).`
13123
13153
  );
13124
- return false;
13154
+ return "skipped";
13125
13155
  }
13126
- return await uploadSession({
13156
+ const uploaded = await uploadSession({
13127
13157
  sessionId,
13128
13158
  transcriptPath: transcriptResolved,
13129
13159
  repoRoot,
@@ -13132,6 +13162,7 @@ async function runUploadInner(payload) {
13132
13162
  eventKind: eventKind2,
13133
13163
  recordedAt
13134
13164
  });
13165
+ return uploaded ? "uploaded" : "skipped";
13135
13166
  }
13136
13167
  var AGENT_MAX_UPLOAD_BYTES = 1024 * 1024 * 1024;
13137
13168
  var CLI_VERSION = "0.8.0";
@@ -13351,6 +13382,10 @@ async function uploadSession(args) {
13351
13382
  sourceName: sourceTool,
13352
13383
  absolutePath: transcriptPath
13353
13384
  });
13385
+ const legacyTranscriptArchivePath = legacyArchivePathFor({
13386
+ sourceName: sourceTool,
13387
+ absolutePath: transcriptPath
13388
+ });
13354
13389
  const alreadySubmitted = prior?.submitted ?? false;
13355
13390
  const submitThisUpload = config.autoSubmit && !alreadySubmitted;
13356
13391
  if (mode === "meta-refresh" && prior?.contributionId && prior.epochMeta && cursor) {
@@ -13428,7 +13463,7 @@ async function uploadSession(args) {
13428
13463
  epochMeta2 = {
13429
13464
  transitionKind: "legacy-adopted",
13430
13465
  baseline: "legacy-latest-zip",
13431
- transcriptArchivePath,
13466
+ transcriptArchivePath: legacyTranscriptArchivePath,
13432
13467
  rawByteOffset: baseCursor.rawByteOffset,
13433
13468
  rawPrefixSha256: baseCursor.rawPrefixSha256
13434
13469
  };
@@ -13444,7 +13479,7 @@ async function uploadSession(args) {
13444
13479
  epoch: epoch2,
13445
13480
  transitionKind: "legacy-adopted",
13446
13481
  baseline: "legacy-latest-zip",
13447
- transcriptArchivePath,
13482
+ transcriptArchivePath: legacyTranscriptArchivePath,
13448
13483
  cursor: baseCursor,
13449
13484
  recordedAt,
13450
13485
  lineage: codexLineage
@@ -13885,16 +13920,16 @@ async function runUploadWorker() {
13885
13920
  const toolOverride = process.env[TOOL_ENV_FLAG];
13886
13921
  if (toolOverride) payload.tool = toolOverride;
13887
13922
  appendLog("info", `worker: payload summary: ${summarizePayload(payload)}`);
13888
- let completed = false;
13923
+ let outcome = "skipped";
13889
13924
  try {
13890
- completed = await runUploadInner(payload);
13925
+ outcome = await runUploadInner(payload);
13891
13926
  } catch (err) {
13892
13927
  appendLog(
13893
13928
  "error",
13894
13929
  `worker: unexpected error: ${err instanceof Error ? err.stack ?? err.message : String(err)}`
13895
13930
  );
13896
13931
  } finally {
13897
- if (completed) {
13932
+ if (outcome !== "internal-thread") {
13898
13933
  await recordDebugLogCompletion({
13899
13934
  kind: "agent",
13900
13935
  tool: fallbackSourceTool(payload),
@@ -13910,7 +13945,7 @@ async function runUploadWorker() {
13910
13945
  }
13911
13946
 
13912
13947
  // src/git-traces/index.ts
13913
- import { spawn as spawn3 } from "child_process";
13948
+ import { spawn as spawn4 } from "child_process";
13914
13949
  import crypto7 from "crypto";
13915
13950
 
13916
13951
  // src/git-traces/handlers.ts
@@ -13918,11 +13953,11 @@ import { execFileSync as execFileSync3 } from "child_process";
13918
13953
  import path18 from "path";
13919
13954
 
13920
13955
  // src/git-traces/git-ops.ts
13921
- import { execFileSync as execFileSync2, spawnSync } from "child_process";
13956
+ import { execFileSync as execFileSync2, spawn as spawn3, spawnSync } from "child_process";
13922
13957
  import fs16 from "fs";
13923
13958
  import os8 from "os";
13924
13959
  import path16 from "path";
13925
- import { gzipSync as gzipSync2 } from "zlib";
13960
+ import { createGzip } from "zlib";
13926
13961
  var GIT_COMMAND_TIMEOUT_MS = 12e4;
13927
13962
  var EXEC_OPTS = {
13928
13963
  timeout: GIT_COMMAND_TIMEOUT_MS,
@@ -14150,7 +14185,7 @@ function createGitError(args, err) {
14150
14185
  const failure = err;
14151
14186
  const command = formatGitCommand(args);
14152
14187
  const isTimeout = failure.code === "ETIMEDOUT";
14153
- 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);
14154
14189
  const wrapped = new Error(message);
14155
14190
  wrapped.isGitTimeout = isTimeout;
14156
14191
  return wrapped;
@@ -14442,7 +14477,111 @@ function createBundleFromTree(repoRoot, treeSha, sessionId, label) {
14442
14477
  deleteRef(repoRoot, orphanRef);
14443
14478
  }
14444
14479
  }
14445
- 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 = {}) {
14446
14585
  if (fromTreeSha === toTreeSha) return null;
14447
14586
  const filteredFromTreeSha = filterOmittedFilesFromTree(
14448
14587
  repoRoot,
@@ -14453,14 +14592,13 @@ function createTreeDiffPatchGz(repoRoot, fromTreeSha, toTreeSha, options = {}) {
14453
14592
  limits: options.limits
14454
14593
  });
14455
14594
  if (filteredFromTreeSha === filteredToTreeSha) return null;
14456
- const diff = gitBuffer(repoRoot, [
14457
- "diff",
14458
- "--binary",
14459
- filteredFromTreeSha,
14460
- filteredToTreeSha
14461
- ]);
14462
- if (diff.length === 0) return null;
14463
- 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;
14464
14602
  }
14465
14603
  function detectTransitionKind(repoRoot, prevHeadSha, nextHeadSha) {
14466
14604
  if (!prevHeadSha) return "initial";
@@ -14961,6 +15099,7 @@ var CLI_VERSION2 = "0.8.0";
14961
15099
  var GIT_TRACES_SLUG = "git-traces";
14962
15100
  var MAX_UPLOAD_BYTES = 500 * 1024 * 1024;
14963
15101
  var STALE_SCOPED_STATE_TTL_MS = 24 * 60 * 60 * 1e3;
15102
+ var REBASELINE_AFTER_DIFF_FAILURES = 2;
14964
15103
  function formatEpochSeconds3(date) {
14965
15104
  return String(Math.floor(date.getTime() / 1e3));
14966
15105
  }
@@ -15844,6 +15983,107 @@ async function refreshLineageBaseline(params) {
15844
15983
  return false;
15845
15984
  }
15846
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
+ }
15847
16087
  async function registerInitialContribution(params) {
15848
16088
  const { repo, state, tool, persistState, client, artifacts } = params;
15849
16089
  const contributionId = await createGitTracesContribution({
@@ -16076,6 +16316,7 @@ async function processStopRepo(repo, tool, sessionId, recordedAt, lineageCheck,
16076
16316
  omittedKeys: artifacts.omittedKeys,
16077
16317
  codexLineageStamped: state.codexLineage
16078
16318
  };
16319
+ delete next.diffFailureCount;
16079
16320
  await persistState(next);
16080
16321
  appendLog(
16081
16322
  "info",
@@ -16090,19 +16331,55 @@ async function processStopRepo(repo, tool, sessionId, recordedAt, lineageCheck,
16090
16331
  log: false,
16091
16332
  limits
16092
16333
  });
16334
+ const previousOmittedKeys = state.omittedKeys;
16093
16335
  const omissionsChanged = reportNewlyOmittedFiles(
16094
16336
  state,
16095
16337
  turnOmittedFiles,
16096
16338
  repoRoot
16097
16339
  );
16098
- const patchBuffer = createTreeDiffPatchGz(
16099
- repoRoot,
16100
- lastSnapshotTreeSha,
16101
- currentTreeSha,
16102
- { limits }
16103
- );
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;
16104
16381
  if (!patchBuffer) {
16105
- if (omissionsChanged) {
16382
+ if (omissionsChanged || diffFailuresCleared) {
16106
16383
  await persistState(state);
16107
16384
  } else {
16108
16385
  await touchSessionState(state.repoRoot, tool, sessionId);
@@ -16618,7 +16895,7 @@ async function runGitTraces() {
16618
16895
  };
16619
16896
  await applyDebugLogParentEnv(workerEnv, raw);
16620
16897
  try {
16621
- const child = spawn3(
16898
+ const child = spawn4(
16622
16899
  process.execPath,
16623
16900
  [entrypoint, "git-traces", `--tool=${tool}`],
16624
16901
  {
@@ -16693,6 +16970,13 @@ async function runGitTracesWorker() {
16693
16970
  );
16694
16971
  return;
16695
16972
  }
16973
+ if (tool === "codex" && await isCodexInternalThread(payload, resolveHookSessionId(payload))) {
16974
+ appendLog(
16975
+ "info",
16976
+ `git-traces: skipping Codex internal thread ${resolveHookSessionId(payload)} on ${event} (no transcript_path and no rollout on disk; not a user session)`
16977
+ );
16978
+ return;
16979
+ }
16696
16980
  try {
16697
16981
  await selfHealHook2(payload, tool);
16698
16982
  switch (eventKind2) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hillclimb",
3
- "version": "0.8.3",
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",