hillclimb 0.9.4 → 0.9.6

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 +774 -255
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -13,8 +13,8 @@ import {
13
13
  } from "./chunk-UPXMA7RC.js";
14
14
 
15
15
  // src/main.ts
16
- import fs27 from "fs";
17
- import path26 from "path";
16
+ import fs28 from "fs";
17
+ import path27 from "path";
18
18
  import * as p6 from "@clack/prompts";
19
19
 
20
20
  // src/commands/init.ts
@@ -2714,9 +2714,9 @@ async function runStatus(args = []) {
2714
2714
  // src/commands/upload.ts
2715
2715
  import { spawn as spawn3 } from "child_process";
2716
2716
  import crypto8 from "crypto";
2717
- import fs20 from "fs";
2717
+ import fs21 from "fs";
2718
2718
  import os10 from "os";
2719
- import path19 from "path";
2719
+ import path20 from "path";
2720
2720
 
2721
2721
  // src/agent-pending.ts
2722
2722
  import crypto4 from "crypto";
@@ -15178,16 +15178,276 @@ async function recordDebugLogCompletion(args) {
15178
15178
  // src/git-traces/pending-captures.ts
15179
15179
  import { AsyncLocalStorage } from "async_hooks";
15180
15180
  import crypto7 from "crypto";
15181
- import fs19 from "fs";
15181
+ import fs20 from "fs";
15182
15182
  import os9 from "os";
15183
- import path18 from "path";
15183
+ import path19 from "path";
15184
15184
 
15185
15185
  // src/git-traces/git-ops.ts
15186
- import { execFileSync as execFileSync2, spawn as spawn2, spawnSync } from "child_process";
15187
- import fs17 from "fs";
15186
+ import { execFileSync as execFileSync2, spawn as spawn2, spawnSync as spawnSync2 } from "child_process";
15187
+ import fs18 from "fs";
15188
15188
  import os7 from "os";
15189
- import path16 from "path";
15189
+ import path17 from "path";
15190
15190
  import { createGzip } from "zlib";
15191
+
15192
+ // src/git-traces/object-store-probe.ts
15193
+ import { spawnSync } from "child_process";
15194
+ import fs17 from "fs";
15195
+ import path16 from "path";
15196
+ var probedRepos = /* @__PURE__ */ new Set();
15197
+ var SAMPLE_FANOUT_DIRS = 8;
15198
+ var SAMPLE_READS = 20;
15199
+ var SAMPLE_READ_BYTES = 4096;
15200
+ var MAX_NAMED_OBJECTS = 3;
15201
+ var MAX_LOCATE_BLOBS = 2e4;
15202
+ var MAX_STALLED_PATH_CHARS = 120;
15203
+ var OBJECT_SHA = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;
15204
+ var gitLimits = {
15205
+ versionMs: 2e3,
15206
+ listMs: 1e4,
15207
+ readMs: 15e3,
15208
+ headMs: 5e3
15209
+ };
15210
+ function probeObjectStore(repoRoot, trigger, stderr, args = []) {
15211
+ if (probedRepos.has(repoRoot)) return;
15212
+ probedRepos.add(repoRoot);
15213
+ const started = Date.now();
15214
+ const fields = [`repo=${repoRoot}`, `trigger=${trigger}`];
15215
+ try {
15216
+ const objectsDir = resolveObjectsDir(repoRoot);
15217
+ if (!objectsDir) {
15218
+ fields.push("objectsDir=unresolved");
15219
+ return;
15220
+ }
15221
+ fields.push(`objectsDir=${objectsDir}`);
15222
+ if (typeof fs17.statfsSync === "function") {
15223
+ const stats = fs17.statfsSync(objectsDir);
15224
+ fields.push(
15225
+ `fsType=${stats.type}`,
15226
+ `freeBytes=${stats.bavail * stats.bsize}`
15227
+ );
15228
+ }
15229
+ const { estimate, sample } = sampleLooseObjects(objectsDir);
15230
+ fields.push(`looseEstimate=${estimate}`);
15231
+ const packDir = path16.join(objectsDir, "pack");
15232
+ const packFiles = fs17.existsSync(packDir) ? fs17.readdirSync(packDir) : [];
15233
+ fields.push(
15234
+ `packs=${packFiles.filter((name) => name.endsWith(".pack")).length}`,
15235
+ `promisorPacks=${packFiles.filter((name) => name.endsWith(".promisor")).length}`,
15236
+ `alternates=${fs17.existsSync(path16.join(objectsDir, "info", "alternates")) ? "yes" : "no"}`
15237
+ );
15238
+ fields.push(...timeSampleReads(sample));
15239
+ const named = namedObjects(stderr);
15240
+ for (const sha of named) {
15241
+ const loose = path16.join(objectsDir, sha.slice(0, 2), sha.slice(2));
15242
+ const stats = fs17.statSync(loose, { throwIfNoEntry: false });
15243
+ fields.push(
15244
+ stats ? `named=${sha.slice(0, 8)}(size=${stats.size},blocks=${stats.blocks})` : `named=${sha.slice(0, 8)}(no loose file)`
15245
+ );
15246
+ }
15247
+ fields.push(...gitVersion(repoRoot));
15248
+ locateGitRead(fields, repoRoot, objectsDir, failedTreeish(args), [
15249
+ ...sample.map(sampleSha).filter((sha) => !!sha),
15250
+ ...named
15251
+ ]);
15252
+ } catch (err) {
15253
+ fields.push(
15254
+ `probeError=${err instanceof Error ? err.message : String(err)}`
15255
+ );
15256
+ } finally {
15257
+ fields.push(`probeMs=${Date.now() - started}`);
15258
+ appendLog("info", `git-traces: object store probe (${fields.join(", ")})`);
15259
+ }
15260
+ }
15261
+ function resolveObjectsDir(repoRoot) {
15262
+ let gitDir = path16.join(repoRoot, ".git");
15263
+ const stats = fs17.statSync(gitDir, { throwIfNoEntry: false });
15264
+ if (!stats) return null;
15265
+ if (stats.isFile()) {
15266
+ const match = /^gitdir:\s*(.+)$/m.exec(fs17.readFileSync(gitDir, "utf-8"));
15267
+ if (!match) return null;
15268
+ gitDir = path16.resolve(repoRoot, match[1].trim());
15269
+ }
15270
+ const commonDir = path16.join(gitDir, "commondir");
15271
+ if (fs17.existsSync(commonDir)) {
15272
+ gitDir = path16.resolve(gitDir, fs17.readFileSync(commonDir, "utf-8").trim());
15273
+ }
15274
+ return path16.join(gitDir, "objects");
15275
+ }
15276
+ function sampleLooseObjects(objectsDir) {
15277
+ const fanout = fs17.readdirSync(objectsDir).filter((name) => /^[0-9a-f]{2}$/.test(name)).sort().slice(0, SAMPLE_FANOUT_DIRS);
15278
+ let counted = 0;
15279
+ const sample = [];
15280
+ for (const dir of fanout) {
15281
+ const files = fs17.readdirSync(path16.join(objectsDir, dir));
15282
+ counted += files.length;
15283
+ for (const file of files) {
15284
+ if (sample.length >= SAMPLE_READS) break;
15285
+ sample.push(path16.join(objectsDir, dir, file));
15286
+ }
15287
+ }
15288
+ return {
15289
+ estimate: fanout.length ? Math.round(counted / fanout.length * 256) : 0,
15290
+ sample
15291
+ };
15292
+ }
15293
+ function timeSampleReads(sample) {
15294
+ let dataless = 0;
15295
+ let readErrors = 0;
15296
+ let firstReadError;
15297
+ let totalMs = 0;
15298
+ let maxMs = 0;
15299
+ const buffer = Buffer.alloc(SAMPLE_READ_BYTES);
15300
+ for (const file of sample) {
15301
+ const readStarted = performance.now();
15302
+ try {
15303
+ const stats = fs17.statSync(file);
15304
+ if (!stats.isFile()) continue;
15305
+ if (stats.size > 0 && stats.blocks === 0) dataless++;
15306
+ const fd = fs17.openSync(file, "r");
15307
+ try {
15308
+ fs17.readSync(fd, buffer, 0, SAMPLE_READ_BYTES, 0);
15309
+ } finally {
15310
+ fs17.closeSync(fd);
15311
+ }
15312
+ } catch (err) {
15313
+ readErrors++;
15314
+ firstReadError ??= err instanceof Error ? err.message : String(err);
15315
+ }
15316
+ const elapsed = performance.now() - readStarted;
15317
+ totalMs += elapsed;
15318
+ maxMs = Math.max(maxMs, elapsed);
15319
+ }
15320
+ const fields = [
15321
+ `sampled=${sample.length}`,
15322
+ `dataless=${dataless}`,
15323
+ `readMsMax=${maxMs.toFixed(1)}`,
15324
+ `readMsTotal=${totalMs.toFixed(1)}`,
15325
+ `readErrors=${readErrors}`
15326
+ ];
15327
+ if (firstReadError) fields.push(`firstReadError=${firstReadError}`);
15328
+ return fields;
15329
+ }
15330
+ function namedObjects(stderr) {
15331
+ const shas = new Set(stderr?.match(/\b[0-9a-f]{40}\b/g) ?? []);
15332
+ return [...shas].slice(0, MAX_NAMED_OBJECTS);
15333
+ }
15334
+ function sampleSha(file) {
15335
+ const sha = path16.basename(path16.dirname(file)) + path16.basename(file);
15336
+ return OBJECT_SHA.test(sha) ? sha : null;
15337
+ }
15338
+ function failedTreeish(args) {
15339
+ for (const arg of args.slice(1).reverse()) {
15340
+ if (OBJECT_SHA.test(arg) || arg.startsWith("refs/")) return arg;
15341
+ }
15342
+ return null;
15343
+ }
15344
+ function probeGit(repoRoot, args, timeout, input) {
15345
+ return spawnSync("git", args, {
15346
+ cwd: repoRoot,
15347
+ input,
15348
+ timeout,
15349
+ killSignal: "SIGKILL",
15350
+ maxBuffer: 64 * 1024 * 1024,
15351
+ windowsHide: true,
15352
+ // Never prompt or lazily fetch from inside a diagnostic.
15353
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_NO_LAZY_FETCH: "1" }
15354
+ });
15355
+ }
15356
+ function gitVersion(repoRoot) {
15357
+ const started = performance.now();
15358
+ const result = probeGit(repoRoot, ["--version"], gitLimits.versionMs);
15359
+ const version2 = result.stdout?.toString().trim().replace(/^git version /, "");
15360
+ return [
15361
+ `gitVersion=${result.status === 0 && version2 ? version2 : `unavailable(${result.error ? result.error.code : result.status})`}`,
15362
+ `gitSpawnMs=${(performance.now() - started).toFixed(0)}`
15363
+ ];
15364
+ }
15365
+ function locateGitRead(fields, repoRoot, objectsDir, treeish, fallback) {
15366
+ let targets = null;
15367
+ if (treeish) {
15368
+ const listed = probeGit(
15369
+ repoRoot,
15370
+ ["ls-tree", "-r", "-z", treeish],
15371
+ gitLimits.listMs
15372
+ );
15373
+ if (listed.status === 0 && listed.stdout) {
15374
+ targets = treeBlobs(listed.stdout.toString());
15375
+ fields.push(
15376
+ `gitReadTarget=tree:${OBJECT_SHA.test(treeish) ? treeish.slice(0, 8) : treeish}`,
15377
+ `treeBlobs=${targets.length}`
15378
+ );
15379
+ targets = targets.slice(0, MAX_LOCATE_BLOBS);
15380
+ } else {
15381
+ fields.push("gitReadTarget=sample", `lsTreeError=${failureText(listed)}`);
15382
+ }
15383
+ } else {
15384
+ fields.push("gitReadTarget=sample");
15385
+ }
15386
+ targets ??= [...new Set(fallback)].map((sha) => ({ sha }));
15387
+ if (targets.length === 0) return;
15388
+ const started = performance.now();
15389
+ const read = probeGit(
15390
+ repoRoot,
15391
+ ["cat-file", "--batch-check=%(objectname) %(objecttype) %(objectsize)"],
15392
+ gitLimits.readMs,
15393
+ `${targets.map((target) => target.sha).join("\n")}
15394
+ `
15395
+ );
15396
+ const answered = (read.stdout?.toString() ?? "").split("\n").slice(0, -1);
15397
+ fields.push(
15398
+ `gitRead=${answered.length}/${targets.length}`,
15399
+ `gitReadMs=${(performance.now() - started).toFixed(1)}`,
15400
+ `gitMissing=${answered.filter((line) => line.endsWith(" missing")).length}`
15401
+ );
15402
+ const stalled = targets[answered.length];
15403
+ if (!stalled) return;
15404
+ fields.push(
15405
+ read.error?.code === "ETIMEDOUT" ? `gitReadTimeout=${gitLimits.readMs}` : `gitReadError=${failureText(read)}`,
15406
+ `gitStalledOn=${stalled.sha.slice(0, 8)}`
15407
+ );
15408
+ if (stalled.path)
15409
+ fields.push(
15410
+ `stalledPath=${JSON.stringify(stalled.path.slice(0, MAX_STALLED_PATH_CHARS))}`
15411
+ );
15412
+ const loose = path16.join(
15413
+ objectsDir,
15414
+ stalled.sha.slice(0, 2),
15415
+ stalled.sha.slice(2)
15416
+ );
15417
+ const stats = fs17.statSync(loose, { throwIfNoEntry: false });
15418
+ fields.push(
15419
+ stats ? `stalledLoose=size=${stats.size},blocks=${stats.blocks}` : "stalledLoose=no"
15420
+ );
15421
+ if (stats && process.platform !== "win32") {
15422
+ const headStarted = performance.now();
15423
+ const head = spawnSync("head", ["-c", "4096", loose], {
15424
+ timeout: gitLimits.headMs,
15425
+ killSignal: "SIGKILL"
15426
+ });
15427
+ fields.push(
15428
+ head.error?.code === "ETIMEDOUT" ? `headTimeout=${gitLimits.headMs}` : `headMs=${(performance.now() - headStarted).toFixed(0)}`
15429
+ );
15430
+ }
15431
+ }
15432
+ function treeBlobs(listing) {
15433
+ const blobs = /* @__PURE__ */ new Map();
15434
+ for (const entry of listing.split("\0")) {
15435
+ const tab = entry.indexOf(" ");
15436
+ if (tab < 0) continue;
15437
+ const [, type, sha] = entry.slice(0, tab).split(" ");
15438
+ if (type === "blob" && sha && !blobs.has(sha))
15439
+ blobs.set(sha, entry.slice(tab + 1));
15440
+ }
15441
+ return [...blobs].map(([sha, blobPath]) => ({ sha, path: blobPath }));
15442
+ }
15443
+ function failureText(result) {
15444
+ const error = result.error;
15445
+ if (error) return error.code ?? error.message;
15446
+ const stderr = result.stderr?.toString().trim().split("\n")[0];
15447
+ return stderr || `exit=${result.status}`;
15448
+ }
15449
+
15450
+ // src/git-traces/git-ops.ts
15191
15451
  var DEFAULT_GIT_COMMAND_TIMEOUT_MS = 12e4;
15192
15452
  var DEFAULT_BUNDLE_TIMEOUT_MS = 3e5;
15193
15453
  var FULL_TREE_SCAN_TIMEOUT_MS = 3e5;
@@ -15378,15 +15638,15 @@ var EXCLUDED_SNAPSHOT_BASENAMES = /* @__PURE__ */ new Set([
15378
15638
  ]);
15379
15639
  var EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
15380
15640
  function isExcludedSnapshotPath(filePath) {
15381
- if (EXCLUDED_SNAPSHOT_BASENAMES.has(path16.basename(filePath))) return true;
15382
- return EXCLUDED_SNAPSHOT_EXTENSIONS.has(path16.extname(filePath).toLowerCase());
15641
+ if (EXCLUDED_SNAPSHOT_BASENAMES.has(path17.basename(filePath))) return true;
15642
+ return EXCLUDED_SNAPSHOT_EXTENSIONS.has(path17.extname(filePath).toLowerCase());
15383
15643
  }
15384
15644
  function isBinaryBuffer(buffer) {
15385
15645
  return buffer.includes(0);
15386
15646
  }
15387
15647
  function readTreeBlobHead(repoRoot, sha) {
15388
15648
  try {
15389
- const { stdout } = spawnSync("git", ["cat-file", "blob", sha], {
15649
+ const { stdout } = spawnSync2("git", ["cat-file", "blob", sha], {
15390
15650
  cwd: repoRoot,
15391
15651
  timeout: GIT_COMMAND_TIMEOUT_MS,
15392
15652
  maxBuffer: BINARY_SNIFF_BYTES,
@@ -15400,16 +15660,16 @@ function readTreeBlobHead(repoRoot, sha) {
15400
15660
  function readWorkingFileHead(absPath) {
15401
15661
  let fd = null;
15402
15662
  try {
15403
- fd = fs17.openSync(absPath, "r");
15663
+ fd = fs18.openSync(absPath, "r");
15404
15664
  const buffer = Buffer.alloc(BINARY_SNIFF_BYTES);
15405
- const bytesRead = fs17.readSync(fd, buffer, 0, BINARY_SNIFF_BYTES, 0);
15665
+ const bytesRead = fs18.readSync(fd, buffer, 0, BINARY_SNIFF_BYTES, 0);
15406
15666
  return buffer.subarray(0, bytesRead);
15407
15667
  } catch {
15408
15668
  return null;
15409
15669
  } finally {
15410
15670
  if (fd !== null) {
15411
15671
  try {
15412
- fs17.closeSync(fd);
15672
+ fs18.closeSync(fd);
15413
15673
  } catch {
15414
15674
  }
15415
15675
  }
@@ -15462,11 +15722,19 @@ function formatGitFailure(command, failure, err) {
15462
15722
  }
15463
15723
  return `${command} failed${details.length ? ` (${details.join("; ")})` : ""}`;
15464
15724
  }
15465
- function createGitError(args, err, timeoutMs = GIT_COMMAND_TIMEOUT_MS) {
15725
+ function formatGitTimeout(command, failure, timeoutMs, elapsedMs) {
15726
+ const details = [];
15727
+ if (elapsedMs !== void 0) details.push(`elapsed=${elapsedMs}ms`);
15728
+ if (failure.signal) details.push(`signal=${failure.signal}`);
15729
+ const stderr = outputToString(failure.stderr);
15730
+ if (stderr) details.push(`stderr=${truncateOutput(stderr)}`);
15731
+ return `${command} timed out after ${timeoutMs}ms${details.length ? ` (${details.join("; ")})` : ""}`;
15732
+ }
15733
+ function createGitError(args, err, timeoutMs = GIT_COMMAND_TIMEOUT_MS, elapsedMs) {
15466
15734
  const failure = err;
15467
15735
  const command = formatGitCommand(args);
15468
15736
  const isTimeout = failure.code === "ETIMEDOUT";
15469
- const message = isTimeout ? `${command} timed out after ${timeoutMs}ms` : failure.code === "ENOBUFS" ? `${command} output exceeded the ${formatSize(failure.maxBufferBytes ?? EXEC_OPTS.maxBuffer)} limit` : formatGitFailure(command, failure, err);
15737
+ const message = isTimeout ? formatGitTimeout(command, failure, timeoutMs, elapsedMs) : failure.code === "ENOBUFS" ? `${command} output exceeded the ${formatSize(failure.maxBufferBytes ?? EXEC_OPTS.maxBuffer)} limit` : formatGitFailure(command, failure, err);
15470
15738
  const wrapped = new Error(message);
15471
15739
  wrapped.isGitTimeout = isTimeout;
15472
15740
  wrapped.gitExitStatus = failure.code || failure.signal ? void 0 : failure.status;
@@ -15498,6 +15766,8 @@ function isGitTimeoutError(err) {
15498
15766
  }
15499
15767
  function gitBuffer(repoRoot, args, options = {}) {
15500
15768
  const timeout = options.timeoutMs ?? EXEC_OPTS.timeout;
15769
+ const detached = options.killProcessGroup && process.platform !== "win32";
15770
+ const started = Date.now();
15501
15771
  try {
15502
15772
  return execFileSync2("git", args, {
15503
15773
  cwd: repoRoot,
@@ -15505,10 +15775,28 @@ function gitBuffer(repoRoot, args, options = {}) {
15505
15775
  ...options.input !== void 0 ? { input: options.input } : {},
15506
15776
  stdio: ["pipe", "pipe", "pipe"],
15507
15777
  ...EXEC_OPTS,
15508
- timeout
15778
+ timeout,
15779
+ // Not in the sync typings, but spawnSync honors it (setsid).
15780
+ ...detached ? { detached: true } : {}
15509
15781
  });
15510
15782
  } catch (err) {
15511
- throw createGitError(args, err, timeout);
15783
+ const pid = err.pid;
15784
+ if (detached && pid) {
15785
+ try {
15786
+ process.kill(-pid, "SIGKILL");
15787
+ } catch {
15788
+ }
15789
+ }
15790
+ const wrapped = createGitError(args, err, timeout, Date.now() - started);
15791
+ if (wrapped.isGitTimeout || wrapped.isGitMissingObject && !options.expectMissingObjects) {
15792
+ probeObjectStore(
15793
+ repoRoot,
15794
+ wrapped.isGitTimeout ? "timeout" : "missing-object",
15795
+ wrapped.gitStderr,
15796
+ args
15797
+ );
15798
+ }
15799
+ throw wrapped;
15512
15800
  }
15513
15801
  }
15514
15802
  function git(repoRoot, args) {
@@ -15546,23 +15834,23 @@ function withPrivateIndex(repoRoot, run) {
15546
15834
  let tmpDir = null;
15547
15835
  let copy = null;
15548
15836
  try {
15549
- const indexPath = path16.resolve(
15837
+ const indexPath = path17.resolve(
15550
15838
  repoRoot,
15551
15839
  git(repoRoot, ["rev-parse", "--git-path", "index"])
15552
15840
  );
15553
- tmpDir = fs17.mkdtempSync(path16.join(os7.tmpdir(), "hillclimb-index-"));
15554
- copy = path16.join(tmpDir, "index");
15555
- const fd = fs17.openSync(indexPath, "r");
15841
+ tmpDir = fs18.mkdtempSync(path17.join(os7.tmpdir(), "hillclimb-index-"));
15842
+ copy = path17.join(tmpDir, "index");
15843
+ const fd = fs18.openSync(indexPath, "r");
15556
15844
  let stat;
15557
15845
  let data;
15558
15846
  try {
15559
- stat = fs17.fstatSync(fd);
15560
- data = fs17.readFileSync(fd);
15847
+ stat = fs18.fstatSync(fd);
15848
+ data = fs18.readFileSync(fd);
15561
15849
  } finally {
15562
- fs17.closeSync(fd);
15850
+ fs18.closeSync(fd);
15563
15851
  }
15564
- fs17.writeFileSync(copy, data);
15565
- fs17.utimesSync(copy, stat.atime, stat.mtime);
15852
+ fs18.writeFileSync(copy, data);
15853
+ fs18.utimesSync(copy, stat.atime, stat.mtime);
15566
15854
  } catch (err) {
15567
15855
  const missing = err.code === "ENOENT";
15568
15856
  appendLog(
@@ -15574,7 +15862,7 @@ function withPrivateIndex(repoRoot, run) {
15574
15862
  try {
15575
15863
  return run(copy ? privateIndexEnv(copy) : null);
15576
15864
  } finally {
15577
- if (tmpDir) fs17.rmSync(tmpDir, { recursive: true, force: true });
15865
+ if (tmpDir) fs18.rmSync(tmpDir, { recursive: true, force: true });
15578
15866
  }
15579
15867
  }
15580
15868
  function privateIndexEnv(indexPath) {
@@ -15638,7 +15926,7 @@ function captureWorkingCommitShaOnIndex(repoRoot, indexEnv) {
15638
15926
  function hasUnbornHead(repoRoot) {
15639
15927
  const ref = git(repoRoot, ["symbolic-ref", "--quiet", "HEAD"]);
15640
15928
  const args = ["show-ref", "--verify", "--quiet", ref];
15641
- const result = spawnSync("git", args, { cwd: repoRoot, ...EXEC_OPTS });
15929
+ const result = spawnSync2("git", args, { cwd: repoRoot, ...EXEC_OPTS });
15642
15930
  if (result.error) throw createGitError(args, result.error);
15643
15931
  return result.status === 1;
15644
15932
  }
@@ -15657,20 +15945,99 @@ function deleteRef(repoRoot, refName) {
15657
15945
  function captureSnapshotSha(repoRoot) {
15658
15946
  return captureWorkingCommitSha(repoRoot);
15659
15947
  }
15660
- function snapshotBlobInventory(repoRoot, treeish) {
15661
- const blobs = /* @__PURE__ */ new Map();
15948
+ var OBJECT_SHA2 = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;
15949
+ function gitExpectingMissing(repoRoot, args) {
15950
+ return gitBuffer(repoRoot, args, { expectMissingObjects: true }).toString("utf-8").trim();
15951
+ }
15952
+ function commitTreeSha(repoRoot, commit) {
15953
+ const tree = gitExpectingMissing(repoRoot, [
15954
+ "rev-list",
15955
+ "--no-walk",
15956
+ "--format=%T",
15957
+ commit,
15958
+ "--"
15959
+ ]).split("\n").at(-1);
15960
+ return tree && OBJECT_SHA2.test(tree) ? tree : null;
15961
+ }
15962
+ function listUnsharedObjects(repoRoot, treeSha, baseTrees) {
15963
+ const out = gitExpectingMissing(repoRoot, [
15964
+ "rev-list",
15965
+ "--objects",
15966
+ treeSha,
15967
+ ...baseTrees.map((base) => `^${base}`),
15968
+ "--"
15969
+ ]);
15970
+ const shas = /* @__PURE__ */ new Set();
15971
+ for (const line of out.split("\n")) {
15972
+ const sha = line.split(" ", 1)[0];
15973
+ if (OBJECT_SHA2.test(sha)) shas.add(sha);
15974
+ }
15975
+ return [...shas];
15976
+ }
15977
+ function walkBaseTree(repoRoot, tree) {
15978
+ gitExpectingMissing(repoRoot, ["rev-list", "--objects", `^${tree}`, "--"]);
15979
+ }
15980
+ function blobSizeTotals(repoRoot, shas) {
15981
+ const totals = { bytes: 0, blobs: 0, unreadable: 0 };
15982
+ for (const object of batchCheckObjects(repoRoot, shas)) {
15983
+ if (object.type === "missing") totals.unreadable++;
15984
+ else if (object.type === "blob" && object.size !== null) {
15985
+ totals.bytes += object.size;
15986
+ totals.blobs++;
15987
+ }
15988
+ }
15989
+ return totals;
15990
+ }
15991
+ var gitVersionCache;
15992
+ function gitVersion2(repoRoot) {
15993
+ if (gitVersionCache !== void 0) return gitVersionCache;
15994
+ try {
15995
+ const match = /git version (\d+)\.(\d+)(?:\.(\d+))?/.exec(
15996
+ git(repoRoot, ["--version"])
15997
+ );
15998
+ gitVersionCache = match ? [Number(match[1]), Number(match[2]), Number(match[3] ?? 0)] : null;
15999
+ } catch {
16000
+ gitVersionCache = null;
16001
+ }
16002
+ return gitVersionCache;
16003
+ }
16004
+ function gitExcludesNegativeTrees(repoRoot) {
16005
+ const version2 = gitVersion2(repoRoot);
16006
+ if (!version2) return true;
16007
+ const [major, minor, patch] = version2;
16008
+ return major > 1 || minor > 8 || minor === 8 && patch >= 5;
16009
+ }
16010
+ function treeBlobSizes(repoRoot, treeSha) {
16011
+ const sizes = /* @__PURE__ */ new Map();
16012
+ for (const entry of lsTreeEntries(repoRoot, ["-l"], treeSha)) {
16013
+ if (entry.type === "blob" && !sizes.has(entry.sha))
16014
+ sizes.set(
16015
+ entry.sha,
16016
+ /^\d+$/.test(entry.size) ? Number(entry.size) : null
16017
+ );
16018
+ }
16019
+ return sizes;
16020
+ }
16021
+ function treeBlobShas(repoRoot, treeSha) {
16022
+ const shas = /* @__PURE__ */ new Set();
16023
+ for (const entry of lsTreeEntries(repoRoot, [], treeSha)) {
16024
+ if (entry.type === "blob") shas.add(entry.sha);
16025
+ }
16026
+ return shas;
16027
+ }
16028
+ function lsTreeEntries(repoRoot, flags, treeSha) {
16029
+ const entries = [];
15662
16030
  for (const entry of gitBuffer(repoRoot, [
15663
16031
  "ls-tree",
15664
16032
  "-r",
15665
- "-l",
16033
+ ...flags,
15666
16034
  "-z",
15667
- treeish
16035
+ treeSha
15668
16036
  ]).toString("utf-8").split("\0")) {
15669
- const [, type, sha, size] = entry.slice(0, entry.indexOf(" ")).trim().split(/\s+/);
15670
- if (type === "blob" && sha && /^\d+$/.test(size ?? ""))
15671
- blobs.set(sha, Number(size));
16037
+ const [, type, sha, size = ""] = entry.slice(0, entry.indexOf(" ")).trim().split(/\s+/);
16038
+ if (type && sha) entries.push({ type, sha, size });
15672
16039
  }
15673
- return blobs;
16040
+ return entries;
15674
16041
  }
15675
16042
  function formatSize(bytes) {
15676
16043
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
@@ -15742,20 +16109,28 @@ function parseDiffTreeRawZ(output) {
15742
16109
  }
15743
16110
  function lookupBlobSizes(repoRoot, shas) {
15744
16111
  const sizes = /* @__PURE__ */ new Map();
15745
- if (shas.length === 0) return sizes;
16112
+ for (const object of batchCheckObjects(repoRoot, shas)) {
16113
+ if (object.type === "blob" && object.size !== null)
16114
+ sizes.set(object.sha, object.size);
16115
+ }
16116
+ return sizes;
16117
+ }
16118
+ function batchCheckObjects(repoRoot, shas) {
16119
+ if (shas.length === 0) return [];
15746
16120
  const out = gitBuffer(
15747
16121
  repoRoot,
15748
16122
  ["cat-file", "--batch-check=%(objectname) %(objecttype) %(objectsize)"],
15749
16123
  { input: `${shas.join("\n")}
15750
16124
  ` }
15751
16125
  ).toString("utf-8");
16126
+ const objects = [];
15752
16127
  for (const line of out.split("\n")) {
15753
16128
  const [sha, type, sizeRaw] = line.split(" ");
15754
- if (type !== "blob") continue;
16129
+ if (!sha || !type) continue;
15755
16130
  const size = Number.parseInt(sizeRaw, 10);
15756
- if (Number.isFinite(size)) sizes.set(sha, size);
16131
+ objects.push({ sha, type, size: Number.isFinite(size) ? size : null });
15757
16132
  }
15758
- return sizes;
16133
+ return objects;
15759
16134
  }
15760
16135
  function sortOmittedByPath(files) {
15761
16136
  return [...files].sort(
@@ -15904,7 +16279,7 @@ function filterOmittedFilesFromTree(repoRoot, treeSha, options = {}) {
15904
16279
  options.omittedFiles?.push(...omittedFiles);
15905
16280
  return timeCaptureStage(repoRoot, "tracked-tree-filter", () => {
15906
16281
  if (omittedFiles.length === 0) return treeSha;
15907
- const tmpIndex = path16.join(
16282
+ const tmpIndex = path17.join(
15908
16283
  os7.tmpdir(),
15909
16284
  `hillclimb-filter-${Date.now()}-${process.pid}`
15910
16285
  );
@@ -15919,7 +16294,7 @@ function filterOmittedFilesFromTree(repoRoot, treeSha, options = {}) {
15919
16294
  return gitWithEnv(repoRoot, ["write-tree"], env);
15920
16295
  } finally {
15921
16296
  try {
15922
- fs17.unlinkSync(tmpIndex);
16297
+ fs18.unlinkSync(tmpIndex);
15923
16298
  } catch {
15924
16299
  }
15925
16300
  }
@@ -15944,8 +16319,8 @@ function buildUntrackedTree(repoRoot, options = {}, attempt = 1) {
15944
16319
  if (!relPath) continue;
15945
16320
  candidates++;
15946
16321
  try {
15947
- const absPath = path16.join(repoRoot, relPath);
15948
- const stat = fs17.lstatSync(absPath);
16322
+ const absPath = path17.join(repoRoot, relPath);
16323
+ const stat = fs18.lstatSync(absPath);
15949
16324
  const reason = classifyOmission(
15950
16325
  relPath,
15951
16326
  stat.size,
@@ -15976,11 +16351,11 @@ function buildUntrackedTree(repoRoot, options = {}, attempt = 1) {
15976
16351
  `git-traces: untracked inventory (repo=${repoRoot}, candidates=${candidates}, kept=${kept.length}, keptBytes=${keptBytes}, omitted=${omitted}, omittedBytes=${omittedBytes}, skipped=${skipped})`
15977
16352
  );
15978
16353
  if (kept.length === 0) return null;
15979
- const tmpDir = fs17.mkdtempSync(path16.join(os7.tmpdir(), "hillclimb-untracked-"));
16354
+ const tmpDir = fs18.mkdtempSync(path17.join(os7.tmpdir(), "hillclimb-untracked-"));
15980
16355
  const env = {
15981
16356
  ...process.env,
15982
16357
  LC_ALL: "C",
15983
- GIT_INDEX_FILE: path16.join(tmpDir, "index")
16358
+ GIT_INDEX_FILE: path17.join(tmpDir, "index")
15984
16359
  };
15985
16360
  try {
15986
16361
  timeCaptureStage(
@@ -16008,7 +16383,7 @@ function buildUntrackedTree(repoRoot, options = {}, attempt = 1) {
16008
16383
  return buildUntrackedTree(repoRoot, options, attempt + 1);
16009
16384
  } finally {
16010
16385
  try {
16011
- fs17.rmSync(tmpDir, { recursive: true, force: true });
16386
+ fs18.rmSync(tmpDir, { recursive: true, force: true });
16012
16387
  } catch {
16013
16388
  }
16014
16389
  }
@@ -16054,10 +16429,10 @@ function buildSnapshotTree(repoRoot, stashSha, options = {}) {
16054
16429
  `git-traces: tree assembly reused (repo=${repoRoot})`
16055
16430
  );
16056
16431
  return previous.snapshotTreeSha;
16057
- } catch {
16432
+ } catch (err) {
16058
16433
  appendLog(
16059
16434
  "info",
16060
- `git-traces: tree assembly cache unavailable; rebuilding (repo=${repoRoot})`
16435
+ `git-traces: tree assembly cache unavailable; rebuilding (repo=${repoRoot}): ${err instanceof Error ? err.message : String(err)}`
16061
16436
  );
16062
16437
  }
16063
16438
  }
@@ -16079,7 +16454,7 @@ function mergeSnapshotTrees(repoRoot, filteredTrackedTree, untrackedTree) {
16079
16454
  if (!untrackedTree || untrackedTree === EMPTY_TREE_SHA)
16080
16455
  return filteredTrackedTree;
16081
16456
  if (filteredTrackedTree === EMPTY_TREE_SHA) return untrackedTree;
16082
- const tmpIndex = path16.join(
16457
+ const tmpIndex = path17.join(
16083
16458
  os7.tmpdir(),
16084
16459
  `hillclimb-index-${Date.now()}-${process.pid}`
16085
16460
  );
@@ -16108,7 +16483,7 @@ function mergeSnapshotTrees(repoRoot, filteredTrackedTree, untrackedTree) {
16108
16483
  );
16109
16484
  } finally {
16110
16485
  try {
16111
- fs17.unlinkSync(tmpIndex);
16486
+ fs18.unlinkSync(tmpIndex);
16112
16487
  } catch {
16113
16488
  }
16114
16489
  }
@@ -16123,18 +16498,19 @@ function createBundleFromTree(repoRoot, treeSha, sessionId, label) {
16123
16498
  pinRef(repoRoot, orphanRef, orphanCommit);
16124
16499
  let tmpDir;
16125
16500
  try {
16126
- tmpDir = fs17.mkdtempSync(
16127
- path16.join(os7.tmpdir(), `hillclimb-bundle-${process.pid}-`)
16501
+ tmpDir = fs18.mkdtempSync(
16502
+ path17.join(os7.tmpdir(), `hillclimb-bundle-${process.pid}-`)
16128
16503
  );
16129
- const tmpFile = path16.join(tmpDir, "baseline.bundle");
16504
+ const tmpFile = path17.join(tmpDir, "baseline.bundle");
16130
16505
  timeCaptureStage(
16131
16506
  repoRoot,
16132
16507
  "bundle-create",
16133
16508
  () => gitBuffer(repoRoot, ["bundle", "create", tmpFile, orphanRef], {
16134
- timeoutMs: resolveGitCommandTimeoutMs(DEFAULT_BUNDLE_TIMEOUT_MS)
16509
+ timeoutMs: resolveGitCommandTimeoutMs(DEFAULT_BUNDLE_TIMEOUT_MS),
16510
+ killProcessGroup: true
16135
16511
  })
16136
16512
  );
16137
- const bundle = fs17.readFileSync(tmpFile);
16513
+ const bundle = fs18.readFileSync(tmpFile);
16138
16514
  appendLog(
16139
16515
  "info",
16140
16516
  `git-traces: bundle inventory (repo=${repoRoot}, bytes=${bundle.length})`
@@ -16142,7 +16518,7 @@ function createBundleFromTree(repoRoot, treeSha, sessionId, label) {
16142
16518
  return bundle;
16143
16519
  } finally {
16144
16520
  try {
16145
- if (tmpDir) fs17.rmSync(tmpDir, { recursive: true, force: true });
16521
+ if (tmpDir) fs18.rmSync(tmpDir, { recursive: true, force: true });
16146
16522
  } catch {
16147
16523
  }
16148
16524
  deleteRef(repoRoot, orphanRef);
@@ -16151,6 +16527,7 @@ function createBundleFromTree(repoRoot, treeSha, sessionId, label) {
16151
16527
  var MAX_PATCH_GZ_BYTES = 500 * 1024 * 1024;
16152
16528
  function gitGzipStream(repoRoot, args, maxGzBytes) {
16153
16529
  return new Promise((resolve, reject) => {
16530
+ const started = Date.now();
16154
16531
  const child = spawn2("git", args, {
16155
16532
  cwd: repoRoot,
16156
16533
  stdio: ["ignore", "pipe", "pipe"],
@@ -16179,7 +16556,18 @@ function gitGzipStream(repoRoot, args, maxGzBytes) {
16179
16556
  settled = true;
16180
16557
  clearTimeout(timer);
16181
16558
  if (timedOut) {
16182
- reject(createGitError(args, { code: "ETIMEDOUT" }));
16559
+ const wrapped = createGitError(
16560
+ args,
16561
+ {
16562
+ code: "ETIMEDOUT",
16563
+ signal: exitSignal,
16564
+ stderr: Buffer.concat(stderrChunks)
16565
+ },
16566
+ GIT_COMMAND_TIMEOUT_MS,
16567
+ Date.now() - started
16568
+ );
16569
+ probeObjectStore(repoRoot, "timeout", wrapped.gitStderr, args);
16570
+ reject(wrapped);
16183
16571
  } else if (overflowed) {
16184
16572
  reject(
16185
16573
  createGitError(args, { code: "ENOBUFS", maxBufferBytes: maxGzBytes })
@@ -16441,9 +16829,9 @@ function parseCommitFiles(repoRoot, sha) {
16441
16829
  oldPath
16442
16830
  });
16443
16831
  } else {
16444
- const path27 = parts[parts.length - 1];
16445
- indexByPath.set(path27, files.length);
16446
- files.push({ path: path27, status, additions: 0, deletions: 0 });
16832
+ const path28 = parts[parts.length - 1];
16833
+ indexByPath.set(path28, files.length);
16834
+ files.push({ path: path28, status, additions: 0, deletions: 0 });
16447
16835
  }
16448
16836
  }
16449
16837
  for (const line of numstat.split("\n")) {
@@ -16527,11 +16915,11 @@ function countScopedTurnTreeRefs(repoRoot, sessionId, epochPrefix3) {
16527
16915
 
16528
16916
  // src/git-traces/session-state.ts
16529
16917
  import crypto6 from "crypto";
16530
- import fs18 from "fs";
16918
+ import fs19 from "fs";
16531
16919
  import os8 from "os";
16532
- import path17 from "path";
16920
+ import path18 from "path";
16533
16921
  var CURRENT_SCHEMA_VERSION3 = 3;
16534
- var DEFAULT_STATE_DIR2 = path17.join(os8.homedir(), ".hillclimb", "git-traces");
16922
+ var DEFAULT_STATE_DIR2 = path18.join(os8.homedir(), ".hillclimb", "git-traces");
16535
16923
  var LOCK_RETRIES2 = 120;
16536
16924
  var LOCK_RETRY_DELAY_MS2 = 500;
16537
16925
  var DEFAULT_LIVE_OWNER_MAX_WAIT_MS2 = 10 * 60 * 1e3;
@@ -16556,16 +16944,16 @@ function stateDir3() {
16556
16944
  }
16557
16945
  function stateFileForRepo(repoRoot, tool, sessionId) {
16558
16946
  const hash = crypto6.createHash("sha256").update(
16559
- sessionId ? `${path17.resolve(repoRoot)}\0${tool}\0${sessionId}` : `${path17.resolve(repoRoot)}\0${tool}`
16947
+ sessionId ? `${path18.resolve(repoRoot)}\0${tool}\0${sessionId}` : `${path18.resolve(repoRoot)}\0${tool}`
16560
16948
  ).digest("hex").slice(0, 16);
16561
- return path17.join(stateDir3(), `${hash}.json`);
16949
+ return path18.join(stateDir3(), `${hash}.json`);
16562
16950
  }
16563
16951
  function lockFileForRepo(repoRoot, tool, sessionId) {
16564
16952
  return `${stateFileForRepo(repoRoot, tool, sessionId)}.lock`;
16565
16953
  }
16566
16954
  async function readStateFile(file) {
16567
16955
  try {
16568
- const raw = await fs18.promises.readFile(file, "utf-8");
16956
+ const raw = await fs19.promises.readFile(file, "utf-8");
16569
16957
  const parsed = JSON.parse(raw);
16570
16958
  if (parsed.schemaVersion !== CURRENT_SCHEMA_VERSION3) {
16571
16959
  return null;
@@ -16579,7 +16967,7 @@ async function readStoredStateFile(file) {
16579
16967
  const state = await readStateFile(file);
16580
16968
  if (!state) return null;
16581
16969
  try {
16582
- return { state, mtimeMs: (await fs18.promises.stat(file)).mtimeMs };
16970
+ return { state, mtimeMs: (await fs19.promises.stat(file)).mtimeMs };
16583
16971
  } catch {
16584
16972
  return null;
16585
16973
  }
@@ -16587,26 +16975,26 @@ async function readStoredStateFile(file) {
16587
16975
  async function listScopedSessionStates(repoRoot, tool) {
16588
16976
  let entries;
16589
16977
  try {
16590
- entries = await fs18.promises.readdir(stateDir3(), { withFileTypes: true });
16978
+ entries = await fs19.promises.readdir(stateDir3(), { withFileTypes: true });
16591
16979
  } catch {
16592
16980
  return [];
16593
16981
  }
16594
16982
  const states = [];
16595
16983
  for (const entry of entries) {
16596
16984
  if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
16597
- const file = path17.join(stateDir3(), entry.name);
16985
+ const file = path18.join(stateDir3(), entry.name);
16598
16986
  const state = await readStateFile(file);
16599
16987
  if (!state) continue;
16600
16988
  if (typeof state.repoRoot !== "string" || typeof state.sessionId !== "string") {
16601
16989
  continue;
16602
16990
  }
16603
- if (path17.resolve(state.repoRoot) !== path17.resolve(repoRoot)) continue;
16604
- if (path17.resolve(file) !== path17.resolve(stateFileForRepo(repoRoot, tool, state.sessionId))) {
16991
+ if (path18.resolve(state.repoRoot) !== path18.resolve(repoRoot)) continue;
16992
+ if (path18.resolve(file) !== path18.resolve(stateFileForRepo(repoRoot, tool, state.sessionId))) {
16605
16993
  continue;
16606
16994
  }
16607
16995
  let mtimeMs = 0;
16608
16996
  try {
16609
- mtimeMs = (await fs18.promises.stat(file)).mtimeMs;
16997
+ mtimeMs = (await fs19.promises.stat(file)).mtimeMs;
16610
16998
  } catch {
16611
16999
  continue;
16612
17000
  }
@@ -16617,26 +17005,26 @@ async function listScopedSessionStates(repoRoot, tool) {
16617
17005
  async function listSessionStatesForSession(tool, sessionId) {
16618
17006
  let entries;
16619
17007
  try {
16620
- entries = await fs18.promises.readdir(stateDir3(), { withFileTypes: true });
17008
+ entries = await fs19.promises.readdir(stateDir3(), { withFileTypes: true });
16621
17009
  } catch {
16622
17010
  return [];
16623
17011
  }
16624
17012
  const states = [];
16625
17013
  for (const entry of entries) {
16626
17014
  if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
16627
- const file = path17.join(stateDir3(), entry.name);
17015
+ const file = path18.join(stateDir3(), entry.name);
16628
17016
  const state = await readStateFile(file);
16629
17017
  if (!state) continue;
16630
17018
  if (typeof state.repoRoot !== "string" || typeof state.sessionId !== "string") {
16631
17019
  continue;
16632
17020
  }
16633
17021
  if (state.sessionId !== sessionId) continue;
16634
- if (path17.resolve(file) !== path17.resolve(stateFileForRepo(state.repoRoot, tool, sessionId))) {
17022
+ if (path18.resolve(file) !== path18.resolve(stateFileForRepo(state.repoRoot, tool, sessionId))) {
16635
17023
  continue;
16636
17024
  }
16637
17025
  let mtimeMs = 0;
16638
17026
  try {
16639
- mtimeMs = (await fs18.promises.stat(file)).mtimeMs;
17027
+ mtimeMs = (await fs19.promises.stat(file)).mtimeMs;
16640
17028
  } catch {
16641
17029
  continue;
16642
17030
  }
@@ -16685,17 +17073,17 @@ async function writeLegacySessionState(state, tool) {
16685
17073
  );
16686
17074
  }
16687
17075
  async function writeStateFile(file, state) {
16688
- await fs18.promises.mkdir(stateDir3(), { recursive: true, mode: 448 });
17076
+ await fs19.promises.mkdir(stateDir3(), { recursive: true, mode: 448 });
16689
17077
  const tmp = `${file}.tmp`;
16690
- await fs18.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
17078
+ await fs19.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
16691
17079
  mode: 384
16692
17080
  });
16693
- await fs18.promises.rename(tmp, file);
17081
+ await fs19.promises.rename(tmp, file);
16694
17082
  }
16695
17083
  async function touchSessionState(repoRoot, tool, sessionId) {
16696
17084
  const now = /* @__PURE__ */ new Date();
16697
17085
  try {
16698
- await fs18.promises.utimes(
17086
+ await fs19.promises.utimes(
16699
17087
  stateFileForRepo(repoRoot, tool, sessionId),
16700
17088
  now,
16701
17089
  now
@@ -16705,7 +17093,7 @@ async function touchSessionState(repoRoot, tool, sessionId) {
16705
17093
  }
16706
17094
  async function statSessionStateMtime(repoRoot, tool, sessionId) {
16707
17095
  try {
16708
- const stat = await fs18.promises.stat(
17096
+ const stat = await fs19.promises.stat(
16709
17097
  stateFileForRepo(repoRoot, tool, sessionId)
16710
17098
  );
16711
17099
  return stat.mtimeMs;
@@ -16715,7 +17103,7 @@ async function statSessionStateMtime(repoRoot, tool, sessionId) {
16715
17103
  }
16716
17104
  async function deleteStateFile(file) {
16717
17105
  try {
16718
- await fs18.promises.unlink(file);
17106
+ await fs19.promises.unlink(file);
16719
17107
  } catch {
16720
17108
  }
16721
17109
  }
@@ -16754,7 +17142,7 @@ async function acquireLock3(repoRoot, tool, sessionId, retries, delayMs, options
16754
17142
  }
16755
17143
  async function acquireLockWithoutTiming(repoRoot, tool, sessionId, retries, delayMs, options = {}) {
16756
17144
  const lockPath = lockFileForRepo(repoRoot, tool, sessionId);
16757
- await fs18.promises.mkdir(stateDir3(), { recursive: true, mode: 448 });
17145
+ await fs19.promises.mkdir(stateDir3(), { recursive: true, mode: 448 });
16758
17146
  const bounded = retries !== void 0 || delayMs !== void 0;
16759
17147
  const attempts = retries ?? LOCK_RETRIES2;
16760
17148
  const delay = delayMs ?? LOCK_RETRY_DELAY_MS2;
@@ -16764,9 +17152,9 @@ async function acquireLockWithoutTiming(repoRoot, tool, sessionId, retries, dela
16764
17152
  let extensionLogged = false;
16765
17153
  for (let i = 0; ; i++) {
16766
17154
  try {
16767
- const fd = await fs18.promises.open(
17155
+ const fd = await fs19.promises.open(
16768
17156
  lockPath,
16769
- fs18.constants.O_CREAT | fs18.constants.O_EXCL | fs18.constants.O_WRONLY
17157
+ fs19.constants.O_CREAT | fs19.constants.O_EXCL | fs19.constants.O_WRONLY
16770
17158
  );
16771
17159
  await fd.write(String(process.pid));
16772
17160
  await fd.close();
@@ -16775,7 +17163,7 @@ async function acquireLockWithoutTiming(repoRoot, tool, sessionId, retries, dela
16775
17163
  if (err.code !== "EEXIST") throw err;
16776
17164
  let regularLockFile = false;
16777
17165
  try {
16778
- regularLockFile = (await fs18.promises.lstat(lockPath)).isFile();
17166
+ regularLockFile = (await fs19.promises.lstat(lockPath)).isFile();
16779
17167
  } catch (statErr) {
16780
17168
  if (statErr.code === "ENOENT") {
16781
17169
  i--;
@@ -16837,21 +17225,21 @@ async function acquireLockWithoutTiming(repoRoot, tool, sessionId, retries, dela
16837
17225
  }
16838
17226
  async function releaseLock3(repoRoot, tool, sessionId) {
16839
17227
  try {
16840
- await fs18.promises.unlink(lockFileForRepo(repoRoot, tool, sessionId));
17228
+ await fs19.promises.unlink(lockFileForRepo(repoRoot, tool, sessionId));
16841
17229
  } catch {
16842
17230
  }
16843
17231
  }
16844
17232
  async function sweepStaleLockFiles(ttlMs = STALE_LOCK_TTL_MS3, now = Date.now()) {
16845
17233
  let entries;
16846
17234
  try {
16847
- entries = await fs18.promises.readdir(stateDir3(), { withFileTypes: true });
17235
+ entries = await fs19.promises.readdir(stateDir3(), { withFileTypes: true });
16848
17236
  } catch {
16849
17237
  return 0;
16850
17238
  }
16851
17239
  let removed = 0;
16852
17240
  for (const entry of entries) {
16853
17241
  if (!entry.isFile() || !entry.name.endsWith(".lock")) continue;
16854
- const file = path17.join(stateDir3(), entry.name);
17242
+ const file = path18.join(stateDir3(), entry.name);
16855
17243
  if (await reapLockIfStale(file, {
16856
17244
  maxAgeMs: ttlMs,
16857
17245
  preserveLiveOwner: true,
@@ -16878,6 +17266,13 @@ function replayBackoffMs(attempts) {
16878
17266
  REPLAY_BACKOFF_MAX_MS
16879
17267
  );
16880
17268
  }
17269
+ function bundleBackoffRemainingMs(timeout, now) {
17270
+ if (!timeout) return 0;
17271
+ return Math.max(
17272
+ 0,
17273
+ timeout.lastAttemptAt + replayBackoffMs(timeout.attempts) - now
17274
+ );
17275
+ }
16881
17276
  function replayEligibility(oldestCapturedAt, replay, now) {
16882
17277
  if (now - oldestCapturedAt < STRANDED_MIN_AGE_MS)
16883
17278
  return { eligible: false, reason: "young" };
@@ -16906,12 +17301,12 @@ function withPendingCapture(record, work) {
16906
17301
  return activeCapture.run({ record, save: () => saveCapture(record) }, work);
16907
17302
  }
16908
17303
  function queueDirectory(repoRoot, tool, sessionId) {
16909
- const stateDir4 = process.env.HILLCLIMB_GIT_TRACES_STATE_DIR ?? path18.join(os9.homedir(), ".hillclimb", "git-traces");
16910
- const key = crypto7.createHash("sha256").update(`${path18.resolve(repoRoot)}\0${tool}\0${sessionId}`).digest("hex");
16911
- return path18.join(stateDir4, "pending-v1", key);
17304
+ const stateDir4 = process.env.HILLCLIMB_GIT_TRACES_STATE_DIR ?? path19.join(os9.homedir(), ".hillclimb", "git-traces");
17305
+ const key = crypto7.createHash("sha256").update(`${path19.resolve(repoRoot)}\0${tool}\0${sessionId}`).digest("hex");
17306
+ return path19.join(stateDir4, "pending-v1", key);
16912
17307
  }
16913
17308
  function captureDirectory(record) {
16914
- return path18.join(
17309
+ return path19.join(
16915
17310
  queueDirectory(record.repoRoot, record.tool, record.sessionId),
16916
17311
  record.id
16917
17312
  );
@@ -16919,20 +17314,20 @@ function captureDirectory(record) {
16919
17314
  function writeDurable(file, body) {
16920
17315
  const temporary = `${file}.${process.pid}.tmp`;
16921
17316
  try {
16922
- const descriptor = fs19.openSync(temporary, "w", 384);
17317
+ const descriptor = fs20.openSync(temporary, "w", 384);
16923
17318
  try {
16924
- fs19.writeFileSync(descriptor, body);
16925
- fs19.fsyncSync(descriptor);
17319
+ fs20.writeFileSync(descriptor, body);
17320
+ fs20.fsyncSync(descriptor);
16926
17321
  } finally {
16927
- fs19.closeSync(descriptor);
17322
+ fs20.closeSync(descriptor);
16928
17323
  }
16929
- fs19.renameSync(temporary, file);
17324
+ fs20.renameSync(temporary, file);
16930
17325
  try {
16931
- const directory = fs19.openSync(path18.dirname(file), "r");
17326
+ const directory = fs20.openSync(path19.dirname(file), "r");
16932
17327
  try {
16933
- fs19.fsyncSync(directory);
17328
+ fs20.fsyncSync(directory);
16934
17329
  } finally {
16935
- fs19.closeSync(directory);
17330
+ fs20.closeSync(directory);
16936
17331
  }
16937
17332
  } catch (err) {
16938
17333
  if (!["EINVAL", "ENOTSUP", "EBADF", "EPERM", "EISDIR"].includes(
@@ -16941,7 +17336,7 @@ function writeDurable(file, body) {
16941
17336
  throw err;
16942
17337
  }
16943
17338
  } finally {
16944
- if (fs19.existsSync(temporary)) fs19.unlinkSync(temporary);
17339
+ if (fs20.existsSync(temporary)) fs20.unlinkSync(temporary);
16945
17340
  }
16946
17341
  }
16947
17342
  function saveCapture(record) {
@@ -16952,7 +17347,7 @@ function saveCapture(record) {
16952
17347
  let status = "failed";
16953
17348
  try {
16954
17349
  writeDurable(
16955
- path18.join(captureDirectory(record), "capture.json"),
17350
+ path19.join(captureDirectory(record), "capture.json"),
16956
17351
  JSON.stringify(record)
16957
17352
  );
16958
17353
  status = "ok";
@@ -16964,7 +17359,7 @@ function listPendingCaptures2(repoRoot, tool, sessionId) {
16964
17359
  const directory = queueDirectory(repoRoot, tool, sessionId);
16965
17360
  let entries;
16966
17361
  try {
16967
- entries = fs19.readdirSync(directory, { withFileTypes: true });
17362
+ entries = fs20.readdirSync(directory, { withFileTypes: true });
16968
17363
  } catch (err) {
16969
17364
  if (err.code === "ENOENT") return [];
16970
17365
  throw err;
@@ -16972,10 +17367,10 @@ function listPendingCaptures2(repoRoot, tool, sessionId) {
16972
17367
  const records = [];
16973
17368
  for (const entry of entries) {
16974
17369
  if (!entry.isDirectory() || !/^[0-9a-f-]{36}$/.test(entry.name)) continue;
16975
- const file = path18.join(directory, entry.name, "capture.json");
16976
- if (!fs19.existsSync(file)) continue;
17370
+ const file = path19.join(directory, entry.name, "capture.json");
17371
+ if (!fs20.existsSync(file)) continue;
16977
17372
  const record = JSON.parse(
16978
- fs19.readFileSync(file, "utf-8")
17373
+ fs20.readFileSync(file, "utf-8")
16979
17374
  );
16980
17375
  if (record.version !== 1 || record.id !== entry.name || record.repoRoot !== repoRoot || record.tool !== tool || record.sessionId !== sessionId) {
16981
17376
  throw new Error(`Invalid pending Git capture: ${file}`);
@@ -16988,13 +17383,13 @@ function hasPendingCaptures(repoRoot, tool, sessionId) {
16988
17383
  return listPendingCaptures2(repoRoot, tool, sessionId).length > 0;
16989
17384
  }
16990
17385
  function listPendingQueues() {
16991
- const root = path18.join(
16992
- process.env.HILLCLIMB_GIT_TRACES_STATE_DIR ?? path18.join(os9.homedir(), ".hillclimb", "git-traces"),
17386
+ const root = path19.join(
17387
+ process.env.HILLCLIMB_GIT_TRACES_STATE_DIR ?? path19.join(os9.homedir(), ".hillclimb", "git-traces"),
16993
17388
  "pending-v1"
16994
17389
  );
16995
17390
  let queueDirs;
16996
17391
  try {
16997
- queueDirs = fs19.readdirSync(root, { withFileTypes: true });
17392
+ queueDirs = fs20.readdirSync(root, { withFileTypes: true });
16998
17393
  } catch (err) {
16999
17394
  if (err.code === "ENOENT") return [];
17000
17395
  throw err;
@@ -17002,15 +17397,15 @@ function listPendingQueues() {
17002
17397
  const queues = [];
17003
17398
  for (const queueDir of queueDirs) {
17004
17399
  if (!queueDir.isDirectory()) continue;
17005
- const directory = path18.join(root, queueDir.name);
17400
+ const directory = path19.join(root, queueDir.name);
17006
17401
  let identity = null;
17007
17402
  try {
17008
- for (const entry of fs19.readdirSync(directory, { withFileTypes: true })) {
17403
+ for (const entry of fs20.readdirSync(directory, { withFileTypes: true })) {
17009
17404
  if (!entry.isDirectory()) continue;
17010
- const file = path18.join(directory, entry.name, "capture.json");
17011
- if (!fs19.existsSync(file)) continue;
17405
+ const file = path19.join(directory, entry.name, "capture.json");
17406
+ if (!fs20.existsSync(file)) continue;
17012
17407
  identity = JSON.parse(
17013
- fs19.readFileSync(file, "utf-8")
17408
+ fs20.readFileSync(file, "utf-8")
17014
17409
  );
17015
17410
  break;
17016
17411
  }
@@ -17043,15 +17438,15 @@ function noteReplayFailure(record, error, now = Date.now()) {
17043
17438
  }
17044
17439
  function queueDiskBytes(directory) {
17045
17440
  let diskBytes = 0;
17046
- if (fs19.existsSync(directory)) {
17047
- for (const entry of fs19.readdirSync(directory, { withFileTypes: true })) {
17441
+ if (fs20.existsSync(directory)) {
17442
+ for (const entry of fs20.readdirSync(directory, { withFileTypes: true })) {
17048
17443
  if (!entry.isDirectory()) continue;
17049
- for (const file of fs19.readdirSync(path18.join(directory, entry.name), {
17444
+ for (const file of fs20.readdirSync(path19.join(directory, entry.name), {
17050
17445
  withFileTypes: true
17051
17446
  })) {
17052
17447
  if (file.isFile())
17053
- diskBytes += fs19.statSync(
17054
- path18.join(directory, entry.name, file.name)
17448
+ diskBytes += fs20.statSync(
17449
+ path19.join(directory, entry.name, file.name)
17055
17450
  ).size;
17056
17451
  }
17057
17452
  }
@@ -17059,7 +17454,7 @@ function queueDiskBytes(directory) {
17059
17454
  return diskBytes;
17060
17455
  }
17061
17456
  function chargedBytes(record) {
17062
- return record.retainedBytes ?? record.treeBytes;
17457
+ return record.retainedBytes ?? record.treeBytes ?? 0;
17063
17458
  }
17064
17459
  function pendingBytes(records) {
17065
17460
  const first = records[0];
@@ -17067,46 +17462,113 @@ function pendingBytes(records) {
17067
17462
  return queueDiskBytes(
17068
17463
  queueDirectory(first.repoRoot, first.tool, first.sessionId)
17069
17464
  ) + records.reduce(
17070
- (sum, record) => sum + chargedBytes(record) + (fs19.existsSync(path18.join(captureDirectory(record), "capture.json")) ? 0 : Buffer.byteLength(JSON.stringify(record))),
17465
+ (sum, record) => sum + chargedBytes(record) + (fs20.existsSync(path19.join(captureDirectory(record), "capture.json")) ? 0 : Buffer.byteLength(JSON.stringify(record))),
17071
17466
  0
17072
17467
  );
17073
17468
  }
17469
+ function warnMissingBase(repoRoot, base) {
17470
+ appendLog(
17471
+ "warn",
17472
+ `git-traces: capture base is missing for retention accounting; charging unshared content (repo=${repoRoot}, base=${base})`
17473
+ );
17474
+ }
17074
17475
  function measureTreeRetention(repoRoot, treeSha, bases, context) {
17075
17476
  const finish = startCaptureStage("tree-inventory", context);
17076
17477
  let status = "failed";
17077
17478
  let details;
17078
17479
  try {
17079
- const blobs = snapshotBlobInventory(repoRoot, treeSha);
17080
- const shared = /* @__PURE__ */ new Set();
17081
- let basesRead = 0;
17082
- for (const base of new Set(
17083
- bases.filter((base2) => !!base2)
17084
- )) {
17480
+ const trees = [
17481
+ ...new Set(bases.trees.filter((base) => !!base))
17482
+ ];
17483
+ if (!trees.includes(treeSha) && bases.headSha) {
17085
17484
  try {
17086
- const baseBlobs = base === treeSha ? blobs : snapshotBlobInventory(repoRoot, base);
17087
- for (const sha of baseBlobs.keys()) shared.add(sha);
17088
- basesRead++;
17485
+ const headTree = commitTreeSha(repoRoot, bases.headSha);
17486
+ if (headTree && !trees.includes(headTree)) trees.push(headTree);
17487
+ if (!headTree) warnMissingBase(repoRoot, bases.headSha);
17089
17488
  } catch (err) {
17090
17489
  if (!isMissingObjectError(err)) throw err;
17091
- appendLog(
17092
- "warn",
17093
- `git-traces: capture base is missing for retention accounting; charging unshared content (repo=${repoRoot}, base=${base})`
17094
- );
17490
+ warnMissingBase(repoRoot, bases.headSha);
17095
17491
  }
17096
17492
  }
17097
- let treeBytes = 0;
17098
- let retainedBytes = 0;
17099
- for (const [sha, size] of blobs) {
17100
- treeBytes += size;
17101
- if (!shared.has(sha)) retainedBytes += size;
17493
+ if (trees.includes(treeSha)) {
17494
+ status = "ok";
17495
+ details = "retainedBytes=0, sameAsBase=yes";
17496
+ return 0;
17497
+ }
17498
+ let totals;
17499
+ if (gitExcludesNegativeTrees(repoRoot)) {
17500
+ const { objects, usable } = unsharedObjects(repoRoot, treeSha, trees);
17501
+ totals = { ...blobSizeTotals(repoRoot, objects), used: usable };
17502
+ } else {
17503
+ totals = legacyRetention(repoRoot, treeSha, trees);
17102
17504
  }
17505
+ const { used } = totals;
17103
17506
  status = "ok";
17104
- details = `treeBytes=${treeBytes}, treeFiles=${blobs.size}, retainedBytes=${retainedBytes}, bases=${basesRead}`;
17105
- return { treeBytes, treeFiles: blobs.size, retainedBytes };
17507
+ details = `retainedBytes=${totals.bytes}, retainedBlobs=${totals.blobs}, bases=${used}`;
17508
+ if (totals.unreadable > 0)
17509
+ details += `, unreadableBlobs=${totals.unreadable}`;
17510
+ if (used < trees.length) details += `, droppedBases=${trees.length - used}`;
17511
+ return totals.bytes;
17106
17512
  } finally {
17107
17513
  finish(status, details);
17108
17514
  }
17109
17515
  }
17516
+ function legacyRetention(repoRoot, treeSha, bases) {
17517
+ appendLog(
17518
+ "info",
17519
+ `git-traces: git before 1.8.5 ignores negative trees; measuring retention from ls-tree (repo=${repoRoot})`
17520
+ );
17521
+ const shared = /* @__PURE__ */ new Set();
17522
+ let used = 0;
17523
+ for (const base of bases) {
17524
+ try {
17525
+ for (const sha of treeBlobShas(repoRoot, base)) shared.add(sha);
17526
+ used++;
17527
+ } catch (err) {
17528
+ if (!isMissingObjectError(err)) throw err;
17529
+ warnMissingBase(repoRoot, base);
17530
+ }
17531
+ }
17532
+ const totals = { bytes: 0, blobs: 0, unreadable: 0, used };
17533
+ for (const [sha, size] of treeBlobSizes(repoRoot, treeSha)) {
17534
+ if (shared.has(sha)) continue;
17535
+ if (size === null) totals.unreadable++;
17536
+ else {
17537
+ totals.bytes += size;
17538
+ totals.blobs++;
17539
+ }
17540
+ }
17541
+ return totals;
17542
+ }
17543
+ function unsharedObjects(repoRoot, treeSha, bases) {
17544
+ try {
17545
+ return {
17546
+ objects: listUnsharedObjects(repoRoot, treeSha, bases),
17547
+ usable: bases.length
17548
+ };
17549
+ } catch (err) {
17550
+ if (!isMissingObjectError(err) || bases.length === 0) throw err;
17551
+ const usable = bases.filter((base) => {
17552
+ try {
17553
+ walkBaseTree(repoRoot, base);
17554
+ return true;
17555
+ } catch (baseErr) {
17556
+ if (!isMissingObjectError(baseErr)) throw baseErr;
17557
+ warnMissingBase(repoRoot, base);
17558
+ return false;
17559
+ }
17560
+ });
17561
+ if (usable.length === bases.length)
17562
+ return {
17563
+ objects: listUnsharedObjects(repoRoot, treeSha, bases),
17564
+ usable: bases.length
17565
+ };
17566
+ return {
17567
+ objects: listUnsharedObjects(repoRoot, treeSha, usable),
17568
+ usable: usable.length
17569
+ };
17570
+ }
17571
+ }
17110
17572
  async function withCaptureQueueLock(repoRoot, tool, sessionId, work) {
17111
17573
  const lockTool = `${tool}:capture-queue-v1`;
17112
17574
  await acquireLock3(repoRoot, lockTool, sessionId);
@@ -17130,21 +17592,26 @@ function storePendingCapture(params) {
17130
17592
  params.tool,
17131
17593
  params.sessionId
17132
17594
  );
17133
- const retention = measureTreeRetention(
17595
+ const retainedBytes = measureTreeRetention(
17134
17596
  params.repoRoot,
17135
17597
  input.treeSha,
17136
- [
17137
- existing.at(-1)?.treeSha,
17138
- params.seed.lastSnapshotTreeSha,
17139
- params.seed.baselineTreeSha,
17140
- input.headSha
17141
- ],
17598
+ {
17599
+ trees: [
17600
+ existing.at(-1)?.treeSha,
17601
+ params.seed.lastSnapshotTreeSha,
17602
+ params.seed.baselineTreeSha
17603
+ ],
17604
+ headSha: input.headSha
17605
+ },
17142
17606
  `repo=${params.repoRoot}, tool=${params.tool}, session=${params.sessionId}, recordedAt=${params.recordedAt}`
17143
17607
  );
17144
17608
  const record = {
17145
17609
  ...identity,
17146
17610
  ...input,
17147
- ...retention,
17611
+ retainedBytes,
17612
+ // 0.8.12 sums treeBytes with no fallback; a missing value would
17613
+ // disable its byte limit for the whole queue.
17614
+ treeBytes: retainedBytes,
17148
17615
  version: 1,
17149
17616
  id: crypto7.randomUUID(),
17150
17617
  sequence: Math.max(Date.now(), (existing.at(-1)?.sequence ?? 0) + 1),
@@ -17153,14 +17620,14 @@ function storePendingCapture(params) {
17153
17620
  const pendingBefore = queueDiskBytes(
17154
17621
  queueDirectory(params.repoRoot, params.tool, params.sessionId)
17155
17622
  ) + existing.reduce((sum, item) => sum + chargedBytes(item), 0);
17156
- const incomingBytes = retention.retainedBytes + Buffer.byteLength(JSON.stringify(record));
17623
+ const incomingBytes = retainedBytes + Buffer.byteLength(JSON.stringify(record));
17157
17624
  if (existing.length >= MAX_CAPTURES || pendingBefore + incomingBytes > MAX_PENDING_BYTES2) {
17158
17625
  throw new PendingCaptureLimitError(
17159
- `Pending Git capture limit reached (count=${existing.length}, maxCount=${MAX_CAPTURES}, pendingBytes=${pendingBefore}, incomingBytes=${incomingBytes}, treeBytes=${retention.treeBytes}, maxBytes=${MAX_PENDING_BYTES2}); existing captures retained`
17626
+ `Pending Git capture limit reached (count=${existing.length}, maxCount=${MAX_CAPTURES}, pendingBytes=${pendingBefore}, incomingBytes=${incomingBytes}, retainedBytes=${retainedBytes}, maxBytes=${MAX_PENDING_BYTES2}); existing captures retained`
17160
17627
  );
17161
17628
  }
17162
17629
  const directory = captureDirectory(record);
17163
- fs19.mkdirSync(directory, { recursive: true, mode: 448 });
17630
+ fs20.mkdirSync(directory, { recursive: true, mode: 448 });
17164
17631
  const refRoot = `refs/hillclimb/pending-v1/${record.id}`;
17165
17632
  let published = false;
17166
17633
  try {
@@ -17198,12 +17665,12 @@ function storePendingCapture(params) {
17198
17665
  durableDetails = `capture=${record.id}, durableCapturedAt=${durableCapturedAt}`;
17199
17666
  appendLog(
17200
17667
  "info",
17201
- `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)}, treeBytes=${record.treeBytes}, treeFiles=${record.treeFiles}, retainedBytes=${record.retainedBytes}, pendingBytes=${pendingBefore + incomingBytes})`
17668
+ `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})`
17202
17669
  );
17203
17670
  return record;
17204
17671
  } finally {
17205
17672
  if (!published) {
17206
- fs19.rmSync(directory, { recursive: true, force: true });
17673
+ fs20.rmSync(directory, { recursive: true, force: true });
17207
17674
  for (const label of [
17208
17675
  "snapshot",
17209
17676
  "tree",
@@ -17234,7 +17701,7 @@ async function preparePendingUpload(context, contributionId, filename, buffer) {
17234
17701
  let status = "failed";
17235
17702
  try {
17236
17703
  if (saved) {
17237
- const bytes = fs19.readFileSync(path18.join(directory, saved.file));
17704
+ const bytes = fs20.readFileSync(path19.join(directory, saved.file));
17238
17705
  if (bytes.length !== saved.bytes || crypto7.createHash("sha256").update(bytes).digest("hex") !== saved.sha256) {
17239
17706
  throw new Error(
17240
17707
  `Pending Git artifact is corrupt: ${saved.file}; capture retained`
@@ -17254,7 +17721,7 @@ async function preparePendingUpload(context, contributionId, filename, buffer) {
17254
17721
  );
17255
17722
  }
17256
17723
  const file = `${crypto7.createHash("sha256").update(key).digest("hex")}.artifact`;
17257
- writeDurable(path18.join(directory, file), buffer);
17724
+ writeDurable(path19.join(directory, file), buffer);
17258
17725
  context.record.uploads[key] = {
17259
17726
  file,
17260
17727
  bytes: buffer.byteLength,
@@ -17264,7 +17731,7 @@ async function preparePendingUpload(context, contributionId, filename, buffer) {
17264
17731
  context.save();
17265
17732
  } catch (err) {
17266
17733
  delete context.record.uploads[key];
17267
- fs19.rmSync(path18.join(directory, file), { force: true });
17734
+ fs20.rmSync(path19.join(directory, file), { force: true });
17268
17735
  throw err;
17269
17736
  }
17270
17737
  status = "ok";
@@ -17296,8 +17763,8 @@ function readPreparedCaptureArtifact(context, filename, input) {
17296
17763
  throw new Error(
17297
17764
  `Prepared Git artifact input changed: ${filename}; capture retained`
17298
17765
  );
17299
- const bytes = fs19.readFileSync(
17300
- path18.join(captureDirectory(context.record), saved.file)
17766
+ const bytes = fs20.readFileSync(
17767
+ path19.join(captureDirectory(context.record), saved.file)
17301
17768
  );
17302
17769
  if (bytes.length !== saved.bytes || crypto7.createHash("sha256").update(bytes).digest("hex") !== saved.sha256)
17303
17770
  throw new Error(
@@ -17316,7 +17783,7 @@ function completePendingCapture(record) {
17316
17783
  function removeCompletedCapture(record) {
17317
17784
  if (!record.completed)
17318
17785
  throw new Error("Cannot remove an unacknowledged Git capture");
17319
- fs19.rmSync(captureDirectory(record), { recursive: true, force: true });
17786
+ fs20.rmSync(captureDirectory(record), { recursive: true, force: true });
17320
17787
  for (const label of ["snapshot", "tree", "head", "baseline", "previous"]) {
17321
17788
  deleteRef(
17322
17789
  record.repoRoot,
@@ -17389,7 +17856,7 @@ async function uploadArtifact(client, contributionId, filename, mimeType, buffer
17389
17856
  }
17390
17857
 
17391
17858
  // package.json
17392
- var version = "0.9.4";
17859
+ var version = "0.9.6";
17393
17860
 
17394
17861
  // src/version.ts
17395
17862
  var CLI_VERSION = version;
@@ -17413,7 +17880,7 @@ function lineHasAssistant(line) {
17413
17880
  return line.includes('"type":"assistant"') || line.includes('"role":"assistant"') || line.includes('"type":"assistant.');
17414
17881
  }
17415
17882
  async function hasAssistantMessage(transcriptPath) {
17416
- const stream = fs20.createReadStream(transcriptPath, { encoding: "utf-8" });
17883
+ const stream = fs21.createReadStream(transcriptPath, { encoding: "utf-8" });
17417
17884
  let buffer = "";
17418
17885
  try {
17419
17886
  for await (const chunk of stream) {
@@ -17500,7 +17967,7 @@ function resolveCursorTranscriptPath(payload) {
17500
17967
  const workspace = payload.workspace_roots?.[0];
17501
17968
  if (!id || !workspace) return void 0;
17502
17969
  const encoded = workspace.replace(/^\//, "").replace(/\//g, "-");
17503
- return path19.join(
17970
+ return path20.join(
17504
17971
  os10.homedir(),
17505
17972
  ".cursor",
17506
17973
  "projects",
@@ -17556,14 +18023,14 @@ async function sweepClaudeSubagentTranscripts(payload) {
17556
18023
  const cwd = resolveHookCwd(payload);
17557
18024
  if (!parentSessionId || !cwd || !payload.transcript_path) return;
17558
18025
  try {
17559
- const directory = path19.join(
17560
- path19.dirname(path19.resolve(payload.transcript_path)),
18026
+ const directory = path20.join(
18027
+ path20.dirname(path20.resolve(payload.transcript_path)),
17561
18028
  parentSessionId,
17562
18029
  "subagents"
17563
18030
  );
17564
18031
  let names;
17565
18032
  try {
17566
- names = await fs20.promises.readdir(directory);
18033
+ names = await fs21.promises.readdir(directory);
17567
18034
  } catch (err) {
17568
18035
  if (err.code === "ENOENT") return;
17569
18036
  throw err;
@@ -17572,12 +18039,12 @@ async function sweepClaudeSubagentTranscripts(payload) {
17572
18039
  for (const name of names) {
17573
18040
  const match = /^agent-([A-Za-z0-9_-]+)\.jsonl$/.exec(name);
17574
18041
  if (!match) continue;
17575
- const file = path19.join(directory, name);
18042
+ const file = path20.join(directory, name);
17576
18043
  try {
17577
18044
  files.push({
17578
18045
  agentId: match[1],
17579
18046
  file,
17580
- modifiedMs: (await fs20.promises.stat(file)).mtimeMs
18047
+ modifiedMs: (await fs21.promises.stat(file)).mtimeMs
17581
18048
  });
17582
18049
  } catch {
17583
18050
  }
@@ -17743,7 +18210,7 @@ async function sweepCodexForkTranscripts(payload) {
17743
18210
  const parent = await findProjectForCwd(cwd);
17744
18211
  if (!parent) return;
17745
18212
  const registeredRoots = Object.keys((await loadProjects()).projects).map(
17746
- (root) => path19.resolve(root)
18213
+ (root) => path20.resolve(root)
17747
18214
  );
17748
18215
  const discovery = await discoverCodexForkThreads({
17749
18216
  parentThreadId: parentSessionId,
@@ -17884,9 +18351,9 @@ async function runUploadForSession(payload, options) {
17884
18351
  );
17885
18352
  return "skipped";
17886
18353
  }
17887
- const transcriptResolved = path19.resolve(transcriptPath);
18354
+ const transcriptResolved = path20.resolve(transcriptPath);
17888
18355
  try {
17889
- const stat = await fs20.promises.stat(transcriptResolved);
18356
+ const stat = await fs21.promises.stat(transcriptResolved);
17890
18357
  if (!stat.isFile()) {
17891
18358
  appendLog(
17892
18359
  "warn",
@@ -17942,7 +18409,7 @@ function makeTranscriptGroup(repoRoot, sourceTool, transcriptPath, content) {
17942
18409
  };
17943
18410
  return {
17944
18411
  repoPath: repoRoot,
17945
- label: path19.basename(repoRoot),
18412
+ label: path20.basename(repoRoot),
17946
18413
  files: [sourceFile],
17947
18414
  sourceNames: [sourceTool],
17948
18415
  lastModified: /* @__PURE__ */ new Date()
@@ -17953,7 +18420,7 @@ async function buildRedactChain(repoRoot, capturedSecrets = []) {
17953
18420
  "secret-discovery",
17954
18421
  () => discoverEnvFiles(repoRoot)
17955
18422
  );
17956
- const envFilePaths = envFileNames.map((n) => path19.join(repoRoot, n));
18423
+ const envFilePaths = envFileNames.map((n) => path20.join(repoRoot, n));
17957
18424
  const secretResult = await measureAgentStage(
17958
18425
  "secret-collection",
17959
18426
  () => collectSecrets(repoRoot, envFilePaths, [])
@@ -18607,7 +19074,7 @@ async function uploadSession(args) {
18607
19074
  try {
18608
19075
  raw = await measureAgentStage(
18609
19076
  "transcript-read",
18610
- () => captured ? Promise.resolve(captured.bytes) : fs20.promises.readFile(transcriptPath)
19077
+ () => captured ? Promise.resolve(captured.bytes) : fs21.promises.readFile(transcriptPath)
18611
19078
  );
18612
19079
  } catch (err) {
18613
19080
  if (err.code === "ERR_FS_FILE_TOO_LARGE") {
@@ -18978,15 +19445,15 @@ import crypto9 from "crypto";
18978
19445
 
18979
19446
  // src/git-traces/handlers.ts
18980
19447
  import { execFileSync as execFileSync3 } from "child_process";
18981
- import fs21 from "fs";
18982
- import path20 from "path";
19448
+ import fs22 from "fs";
19449
+ import path21 from "path";
18983
19450
  var GIT_TRACES_SLUG = "git-traces";
18984
19451
  var MAX_FORK_SWEEP_THREADS2 = 8;
18985
19452
  var MAX_UPLOAD_BYTES = 500 * 1024 * 1024;
18986
19453
  var STALE_SCOPED_STATE_TTL_MS = 24 * 60 * 60 * 1e3;
18987
19454
  var REBASELINE_AFTER_DIFF_FAILURES = 2;
18988
19455
  async function withRepoCaptureLock(repoRoot, capture) {
18989
- const canonicalRoot = fs21.realpathSync(repoRoot);
19456
+ const canonicalRoot = fs22.realpathSync(repoRoot);
18990
19457
  await acquireLock3(canonicalRoot, "snapshot-index-v1", null);
18991
19458
  try {
18992
19459
  return capture();
@@ -19044,7 +19511,7 @@ var PendingQueueConflictError = class extends Error {
19044
19511
  async function loadConfiguredRepos() {
19045
19512
  const file = await loadProjects();
19046
19513
  return Object.entries(file.projects).map(([repoRoot, config]) => ({
19047
- repoRoot: path20.resolve(repoRoot),
19514
+ repoRoot: path21.resolve(repoRoot),
19048
19515
  config
19049
19516
  })).sort((a, b) => a.repoRoot.localeCompare(b.repoRoot));
19050
19517
  }
@@ -19062,12 +19529,12 @@ async function configuredReposFor(trigger) {
19062
19529
  return repos;
19063
19530
  }
19064
19531
  async function propagateCodexHooks(parentRoot, worktreeRoot) {
19065
- const source = path20.join(parentRoot, ".codex", "hooks.json");
19066
- const target = path20.join(worktreeRoot, ".codex", "hooks.json");
19067
- if (fs21.existsSync(target) || !fs21.existsSync(source)) return;
19532
+ const source = path21.join(parentRoot, ".codex", "hooks.json");
19533
+ const target = path21.join(worktreeRoot, ".codex", "hooks.json");
19534
+ if (fs22.existsSync(target) || !fs22.existsSync(source)) return;
19068
19535
  try {
19069
- fs21.mkdirSync(path20.dirname(target), { recursive: true });
19070
- fs21.copyFileSync(source, target, fs21.constants.COPYFILE_EXCL);
19536
+ fs22.mkdirSync(path21.dirname(target), { recursive: true });
19537
+ fs22.copyFileSync(source, target, fs22.constants.COPYFILE_EXCL);
19071
19538
  } catch (err) {
19072
19539
  appendLog(
19073
19540
  "warn",
@@ -19089,7 +19556,7 @@ async function propagateCodexHooks(parentRoot, worktreeRoot) {
19089
19556
  }
19090
19557
  }
19091
19558
  function repoLabel(repoRoot) {
19092
- return path20.basename(repoRoot) || repoRoot;
19559
+ return path21.basename(repoRoot) || repoRoot;
19093
19560
  }
19094
19561
  function resolveCwd2(payload) {
19095
19562
  return resolveHookCwd(payload);
@@ -19331,6 +19798,53 @@ function freezeEpochBaseline(params) {
19331
19798
  return null;
19332
19799
  }
19333
19800
  }
19801
+ function createBundleWithBackoff(pending, params) {
19802
+ const { repoRoot, baselineTreeSha } = params;
19803
+ const previous = pending?.record.bundleTimeout?.input === baselineTreeSha ? pending.record.bundleTimeout : void 0;
19804
+ if (pending && previous) {
19805
+ const retryInMs = bundleBackoffRemainingMs(previous, Date.now());
19806
+ if (retryInMs > 0) {
19807
+ appendLog(
19808
+ "warn",
19809
+ `git-traces: baseline bundle deferred after timeout (repo=${repoRoot}, session=${pending.record.sessionId}, capture=${pending.record.id}, attempts=${previous.attempts}, retryInMs=${retryInMs})`
19810
+ );
19811
+ return null;
19812
+ }
19813
+ }
19814
+ try {
19815
+ const bundle = createBundleFromTree(
19816
+ repoRoot,
19817
+ baselineTreeSha,
19818
+ params.bundleKey,
19819
+ params.label
19820
+ );
19821
+ if (pending?.record.bundleTimeout) {
19822
+ delete pending.record.bundleTimeout;
19823
+ saveBundleBackoff(pending);
19824
+ }
19825
+ return bundle;
19826
+ } catch (err) {
19827
+ if (pending && isGitTimeoutError(err)) {
19828
+ pending.record.bundleTimeout = {
19829
+ input: baselineTreeSha,
19830
+ attempts: (previous?.attempts ?? 0) + 1,
19831
+ lastAttemptAt: Date.now()
19832
+ };
19833
+ saveBundleBackoff(pending);
19834
+ }
19835
+ throw err;
19836
+ }
19837
+ }
19838
+ function saveBundleBackoff(pending) {
19839
+ try {
19840
+ pending.save();
19841
+ } catch (err) {
19842
+ appendLog(
19843
+ "warn",
19844
+ `git-traces: could not save baseline bundle backoff (capture=${pending.record.id}): ${formatError(err)}`
19845
+ );
19846
+ }
19847
+ }
19334
19848
  function buildFrozenEpochBaselineArtifacts(params) {
19335
19849
  const { repoRoot, sessionId, epoch, baselineTreeSha, baselineMetadata } = params;
19336
19850
  const prefix = epochPrefix2(epoch);
@@ -19340,12 +19854,13 @@ function buildFrozenEpochBaselineArtifacts(params) {
19340
19854
  pending,
19341
19855
  `${prefix}-baseline.bundle`,
19342
19856
  baselineTreeSha
19343
- )) ?? createBundleFromTree(
19857
+ )) ?? createBundleWithBackoff(pending, {
19344
19858
  repoRoot,
19345
19859
  baselineTreeSha,
19346
- `${sessionId}-${prefix}`,
19347
- `baseline ${prefix}`
19348
- );
19860
+ bundleKey: `${sessionId}-${prefix}`,
19861
+ label: `baseline ${prefix}`
19862
+ });
19863
+ if (!bundleBuffer) return null;
19349
19864
  const metadataBuffer = Buffer.from(
19350
19865
  JSON.stringify(baselineMetadata, null, 2)
19351
19866
  );
@@ -21250,6 +21765,10 @@ async function sweepStrandedCaptures(params) {
21250
21765
  tally[state.reason]++;
21251
21766
  continue;
21252
21767
  }
21768
+ if (bundleBackoffRemainingMs(head.bundleTimeout, now) > 0) {
21769
+ tally.backoff++;
21770
+ continue;
21771
+ }
21253
21772
  eligible.push({
21254
21773
  queue,
21255
21774
  oldestCapturedAt,
@@ -21306,7 +21825,7 @@ async function collectStopTargets(tool, sessionId, repos, repoByRoot) {
21306
21825
  let needsLineageCheck = false;
21307
21826
  const storedStates = await listSessionStatesForSession(tool, sessionId);
21308
21827
  for (const { state } of storedStates) {
21309
- const repo = repoByRoot.get(path20.resolve(state.repoRoot));
21828
+ const repo = repoByRoot.get(path21.resolve(state.repoRoot));
21310
21829
  if (!repo) {
21311
21830
  missingConfig++;
21312
21831
  appendLog(
@@ -21338,7 +21857,7 @@ async function lateInitSkipReason(payload, tool) {
21338
21857
  const transcriptPath = payload.transcript_path;
21339
21858
  if (!transcriptPath) return tool === "codex" ? null : "no-transcript-path";
21340
21859
  try {
21341
- const stat = await fs21.promises.stat(path20.resolve(transcriptPath));
21860
+ const stat = await fs22.promises.stat(path21.resolve(transcriptPath));
21342
21861
  return stat.isFile() ? null : "transcript-not-a-file";
21343
21862
  } catch {
21344
21863
  return "transcript-missing";
@@ -21547,9 +22066,9 @@ async function handleSessionEnd(payload, tool) {
21547
22066
  if (sessionId) {
21548
22067
  const states = await listSessionStatesForSession(tool, sessionId);
21549
22068
  for (const { state } of states) {
21550
- const repoRoot = path20.resolve(state.repoRoot);
22069
+ const repoRoot = path21.resolve(state.repoRoot);
21551
22070
  repoRoots.add(repoRoot);
21552
- const canProcess = repoByRoot.has(repoRoot) || project && path20.resolve(project.repoRoot) === repoRoot;
22071
+ const canProcess = repoByRoot.has(repoRoot) || project && path21.resolve(project.repoRoot) === repoRoot;
21553
22072
  if (canProcess && tool === "codex" && state.codexLineageChecked !== true) {
21554
22073
  needsLineageCheck = true;
21555
22074
  }
@@ -21586,7 +22105,7 @@ async function handleSessionEnd(payload, tool) {
21586
22105
  let rejected = 0;
21587
22106
  const finalCaptures = /* @__PURE__ */ new Map();
21588
22107
  for (const repoRoot of repoRoots) {
21589
- const repo = repoByRoot.get(path20.resolve(repoRoot)) ?? (project && path20.resolve(project.repoRoot) === path20.resolve(repoRoot) ? { repoRoot: project.repoRoot, config: project.config } : null);
22108
+ const repo = repoByRoot.get(path21.resolve(repoRoot)) ?? (project && path21.resolve(project.repoRoot) === path21.resolve(repoRoot) ? { repoRoot: project.repoRoot, config: project.config } : null);
21590
22109
  if (repo)
21591
22110
  finalCaptures.set(
21592
22111
  repoRoot,
@@ -21594,7 +22113,7 @@ async function handleSessionEnd(payload, tool) {
21594
22113
  );
21595
22114
  }
21596
22115
  for (const repoRoot of repoRoots) {
21597
- const repo = repoByRoot.get(path20.resolve(repoRoot)) ?? (project && path20.resolve(project.repoRoot) === path20.resolve(repoRoot) ? { repoRoot: project.repoRoot, config: project.config } : null);
22116
+ const repo = repoByRoot.get(path21.resolve(repoRoot)) ?? (project && path21.resolve(project.repoRoot) === path21.resolve(repoRoot) ? { repoRoot: project.repoRoot, config: project.config } : null);
21598
22117
  if (!repo) {
21599
22118
  skipped++;
21600
22119
  appendLog(
@@ -21973,15 +22492,15 @@ ${stack}` : ""}`
21973
22492
  }
21974
22493
 
21975
22494
  // src/outputs/zip.ts
21976
- import fs23 from "fs";
21977
- import path22 from "path";
22495
+ import fs24 from "fs";
22496
+ import path23 from "path";
21978
22497
  import archiver2 from "archiver";
21979
22498
 
21980
22499
  // src/outputs/downloads.ts
21981
22500
  import { execSync as execSync2 } from "child_process";
21982
- import fs22 from "fs";
22501
+ import fs23 from "fs";
21983
22502
  import os11 from "os";
21984
- import path21 from "path";
22503
+ import path22 from "path";
21985
22504
  function getDownloadsFolder() {
21986
22505
  const home = os11.homedir();
21987
22506
  if (process.platform === "linux") {
@@ -21990,12 +22509,12 @@ function getDownloadsFolder() {
21990
22509
  encoding: "utf-8",
21991
22510
  timeout: 3e3
21992
22511
  }).trim();
21993
- if (xdgDir && fs22.existsSync(xdgDir)) return xdgDir;
22512
+ if (xdgDir && fs23.existsSync(xdgDir)) return xdgDir;
21994
22513
  } catch {
21995
22514
  }
21996
22515
  }
21997
- const downloads = path21.join(home, "Downloads");
21998
- if (fs22.existsSync(downloads)) return downloads;
22516
+ const downloads = path22.join(home, "Downloads");
22517
+ if (fs23.existsSync(downloads)) return downloads;
21999
22518
  return home;
22000
22519
  }
22001
22520
 
@@ -22004,11 +22523,11 @@ function sanitizeFilename(name) {
22004
22523
  return name.replace(/[^a-zA-Z0-9._-]/g, "_");
22005
22524
  }
22006
22525
  function getUniqueFilename(dir, base, ext) {
22007
- let candidate = path22.join(dir, `${base}${ext}`);
22008
- if (!fs23.existsSync(candidate)) return candidate;
22526
+ let candidate = path23.join(dir, `${base}${ext}`);
22527
+ if (!fs24.existsSync(candidate)) return candidate;
22009
22528
  let i = 1;
22010
- while (fs23.existsSync(candidate)) {
22011
- candidate = path22.join(dir, `${base}-${i}${ext}`);
22529
+ while (fs24.existsSync(candidate)) {
22530
+ candidate = path23.join(dir, `${base}-${i}${ext}`);
22012
22531
  i++;
22013
22532
  }
22014
22533
  return candidate;
@@ -22018,13 +22537,13 @@ var ZipOutput = class {
22018
22537
  label = "Save as .zip to Downloads";
22019
22538
  async emit(group, options) {
22020
22539
  const downloadsDir = getDownloadsFolder();
22021
- const repoName = sanitizeFilename(path22.basename(group.repoPath));
22540
+ const repoName = sanitizeFilename(path23.basename(group.repoPath));
22022
22541
  const timeRange = options.timeRange;
22023
22542
  const rangePart = timeRange?.label ?? "all";
22024
22543
  const epochSeconds = Math.floor(Date.now() / 1e3);
22025
22544
  const baseName = `logs-${repoName}-${rangePart}-${epochSeconds}`;
22026
22545
  const outputPath = getUniqueFilename(downloadsDir, baseName, ".zip");
22027
- const output = fs23.createWriteStream(outputPath);
22546
+ const output = fs24.createWriteStream(outputPath);
22028
22547
  const archive = archiver2("zip", { zlib: { level: 6 } });
22029
22548
  const done = new Promise((resolve, reject) => {
22030
22549
  output.on("close", resolve);
@@ -22218,15 +22737,15 @@ async function confirmExport(group, output) {
22218
22737
  }
22219
22738
 
22220
22739
  // src/sources/claude.ts
22221
- import fs24 from "fs";
22740
+ import fs25 from "fs";
22222
22741
  import os12 from "os";
22223
- import path23 from "path";
22742
+ import path24 from "path";
22224
22743
  import readline3 from "readline";
22225
22744
  var SKIP_DIRS = /* @__PURE__ */ new Set(["tool-results"]);
22226
22745
  async function resolveRepoPath(projectDir) {
22227
- const indexPath = path23.join(projectDir, "sessions-index.json");
22746
+ const indexPath = path24.join(projectDir, "sessions-index.json");
22228
22747
  try {
22229
- const raw = await fs24.promises.readFile(indexPath, "utf-8");
22748
+ const raw = await fs25.promises.readFile(indexPath, "utf-8");
22230
22749
  const data = JSON.parse(raw);
22231
22750
  if (data.originalPath && typeof data.originalPath === "string") {
22232
22751
  return data.originalPath;
@@ -22234,12 +22753,12 @@ async function resolveRepoPath(projectDir) {
22234
22753
  } catch {
22235
22754
  }
22236
22755
  const cwdCounts = /* @__PURE__ */ new Map();
22237
- const entries = await fs24.promises.readdir(projectDir, {
22756
+ const entries = await fs25.promises.readdir(projectDir, {
22238
22757
  withFileTypes: true
22239
22758
  });
22240
22759
  for (const entry of entries) {
22241
22760
  if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
22242
- const cwd = await extractCwdFromJsonl(path23.join(projectDir, entry.name));
22761
+ const cwd = await extractCwdFromJsonl(path24.join(projectDir, entry.name));
22243
22762
  if (cwd) {
22244
22763
  cwdCounts.set(cwd, (cwdCounts.get(cwd) ?? 0) + 1);
22245
22764
  }
@@ -22258,7 +22777,7 @@ async function resolveRepoPath(projectDir) {
22258
22777
  return null;
22259
22778
  }
22260
22779
  async function extractCwdFromJsonl(filePath) {
22261
- const stream = fs24.createReadStream(filePath, { encoding: "utf-8" });
22780
+ const stream = fs25.createReadStream(filePath, { encoding: "utf-8" });
22262
22781
  const rl = readline3.createInterface({ input: stream, crlfDelay: Infinity });
22263
22782
  try {
22264
22783
  for await (const line of rl) {
@@ -22280,12 +22799,12 @@ async function extractCwdFromJsonl(filePath) {
22280
22799
  async function collectFiles(dir, baseDir, sourceName, repoPath, results) {
22281
22800
  let entries;
22282
22801
  try {
22283
- entries = await fs24.promises.readdir(dir, { withFileTypes: true });
22802
+ entries = await fs25.promises.readdir(dir, { withFileTypes: true });
22284
22803
  } catch {
22285
22804
  return;
22286
22805
  }
22287
22806
  for (const entry of entries) {
22288
- const fullPath = path23.join(dir, entry.name);
22807
+ const fullPath = path24.join(dir, entry.name);
22289
22808
  if (entry.isDirectory()) {
22290
22809
  if (SKIP_DIRS.has(entry.name)) continue;
22291
22810
  await collectFiles(fullPath, baseDir, sourceName, repoPath, results);
@@ -22307,19 +22826,19 @@ function fallbackDecode(encodedName) {
22307
22826
  var ClaudeSource = class {
22308
22827
  name = "claude";
22309
22828
  async scan() {
22310
- const baseDir = path23.join(os12.homedir(), ".claude", "projects");
22829
+ const baseDir = path24.join(os12.homedir(), ".claude", "projects");
22311
22830
  try {
22312
- await fs24.promises.access(baseDir);
22831
+ await fs25.promises.access(baseDir);
22313
22832
  } catch {
22314
22833
  return [];
22315
22834
  }
22316
- const projectDirs = await fs24.promises.readdir(baseDir, {
22835
+ const projectDirs = await fs25.promises.readdir(baseDir, {
22317
22836
  withFileTypes: true
22318
22837
  });
22319
22838
  const dirEntries = projectDirs.filter((d) => d.isDirectory());
22320
22839
  const resultArrays = await Promise.all(
22321
22840
  dirEntries.map(async (dir) => {
22322
- const projectPath = path23.join(baseDir, dir.name);
22841
+ const projectPath = path24.join(baseDir, dir.name);
22323
22842
  const repoPath = await resolveRepoPath(projectPath) ?? fallbackDecode(dir.name);
22324
22843
  const files = [];
22325
22844
  await collectFiles(
@@ -22337,12 +22856,12 @@ var ClaudeSource = class {
22337
22856
  };
22338
22857
 
22339
22858
  // src/sources/codex.ts
22340
- import fs25 from "fs";
22859
+ import fs26 from "fs";
22341
22860
  import os13 from "os";
22342
- import path24 from "path";
22861
+ import path25 from "path";
22343
22862
  import readline4 from "readline";
22344
22863
  async function parseSessionMeta2(filePath) {
22345
- const stream = fs25.createReadStream(filePath, { encoding: "utf-8" });
22864
+ const stream = fs26.createReadStream(filePath, { encoding: "utf-8" });
22346
22865
  const rl = readline4.createInterface({ input: stream, crlfDelay: Infinity });
22347
22866
  try {
22348
22867
  for await (const line of rl) {
@@ -22367,12 +22886,12 @@ async function findJsonlFiles(dir) {
22367
22886
  async function walk(d) {
22368
22887
  let entries;
22369
22888
  try {
22370
- entries = await fs25.promises.readdir(d, { withFileTypes: true });
22889
+ entries = await fs26.promises.readdir(d, { withFileTypes: true });
22371
22890
  } catch {
22372
22891
  return;
22373
22892
  }
22374
22893
  for (const entry of entries) {
22375
- const full = path24.join(d, entry.name);
22894
+ const full = path25.join(d, entry.name);
22376
22895
  if (entry.isDirectory()) {
22377
22896
  await walk(full);
22378
22897
  } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
@@ -22386,11 +22905,11 @@ async function findJsonlFiles(dir) {
22386
22905
  async function loadHistory(historyPath) {
22387
22906
  const map = /* @__PURE__ */ new Map();
22388
22907
  try {
22389
- await fs25.promises.access(historyPath);
22908
+ await fs26.promises.access(historyPath);
22390
22909
  } catch {
22391
22910
  return map;
22392
22911
  }
22393
- const stream = fs25.createReadStream(historyPath, { encoding: "utf-8" });
22912
+ const stream = fs26.createReadStream(historyPath, { encoding: "utf-8" });
22394
22913
  const rl = readline4.createInterface({ input: stream, crlfDelay: Infinity });
22395
22914
  try {
22396
22915
  for await (const line of rl) {
@@ -22417,14 +22936,14 @@ async function loadHistory(historyPath) {
22417
22936
  var CodexSource = class {
22418
22937
  name = "codex";
22419
22938
  async scan() {
22420
- const codexDir = path24.join(os13.homedir(), ".codex");
22421
- const sessionsDir2 = path24.join(codexDir, "sessions");
22939
+ const codexDir = path25.join(os13.homedir(), ".codex");
22940
+ const sessionsDir2 = path25.join(codexDir, "sessions");
22422
22941
  try {
22423
- await fs25.promises.access(sessionsDir2);
22942
+ await fs26.promises.access(sessionsDir2);
22424
22943
  } catch {
22425
22944
  return [];
22426
22945
  }
22427
- const historyPath = path24.join(codexDir, "history.jsonl");
22946
+ const historyPath = path25.join(codexDir, "history.jsonl");
22428
22947
  const [jsonlFiles, historyMap] = await Promise.all([
22429
22948
  findJsonlFiles(sessionsDir2),
22430
22949
  loadHistory(historyPath)
@@ -22447,8 +22966,8 @@ var CodexSource = class {
22447
22966
  });
22448
22967
  const historyLines = historyMap.get(meta.sessionId);
22449
22968
  if (historyLines) {
22450
- const sessionDir = path24.relative(sessionsDir2, path24.dirname(filePath));
22451
- const historyAbsPath = path24.join(
22969
+ const sessionDir = path25.relative(sessionsDir2, path25.dirname(filePath));
22970
+ const historyAbsPath = path25.join(
22452
22971
  sessionsDir2,
22453
22972
  sessionDir,
22454
22973
  `history-${meta.sessionId}.jsonl`
@@ -22468,18 +22987,18 @@ var CodexSource = class {
22468
22987
  };
22469
22988
 
22470
22989
  // src/sources/copilotChat.ts
22471
- import fs26 from "fs";
22990
+ import fs27 from "fs";
22472
22991
  import os14 from "os";
22473
- import path25 from "path";
22992
+ import path26 from "path";
22474
22993
  import { fileURLToPath as fileURLToPath2 } from "url";
22475
22994
  function vsCodeUserDirs() {
22476
22995
  const home = os14.homedir();
22477
22996
  const dirs = [
22478
- path25.join(home, "Library", "Application Support", "Code", "User"),
22479
- path25.join(home, ".config", "Code", "User")
22997
+ path26.join(home, "Library", "Application Support", "Code", "User"),
22998
+ path26.join(home, ".config", "Code", "User")
22480
22999
  ];
22481
23000
  if (process.env.APPDATA) {
22482
- dirs.push(path25.join(process.env.APPDATA, "Code", "User"));
23001
+ dirs.push(path26.join(process.env.APPDATA, "Code", "User"));
22483
23002
  }
22484
23003
  return dirs;
22485
23004
  }
@@ -22494,7 +23013,7 @@ function uriToFsPath(uri) {
22494
23013
  async function readWorkspaceFolder(workspaceJsonPath) {
22495
23014
  let raw;
22496
23015
  try {
22497
- raw = await fs26.promises.readFile(workspaceJsonPath, "utf-8");
23016
+ raw = await fs27.promises.readFile(workspaceJsonPath, "utf-8");
22498
23017
  } catch {
22499
23018
  return null;
22500
23019
  }
@@ -22516,10 +23035,10 @@ var CopilotChatSource = class {
22516
23035
  async scan() {
22517
23036
  const results = [];
22518
23037
  for (const userDir of vsCodeUserDirs()) {
22519
- const workspaceStorage = path25.join(userDir, "workspaceStorage");
23038
+ const workspaceStorage = path26.join(userDir, "workspaceStorage");
22520
23039
  let hashDirs;
22521
23040
  try {
22522
- hashDirs = await fs26.promises.readdir(workspaceStorage, {
23041
+ hashDirs = await fs27.promises.readdir(workspaceStorage, {
22523
23042
  withFileTypes: true
22524
23043
  });
22525
23044
  } catch {
@@ -22527,22 +23046,22 @@ var CopilotChatSource = class {
22527
23046
  }
22528
23047
  for (const hash of hashDirs) {
22529
23048
  if (!hash.isDirectory()) continue;
22530
- const wsRoot = path25.join(workspaceStorage, hash.name);
22531
- const transcriptsDir = path25.join(
23049
+ const wsRoot = path26.join(workspaceStorage, hash.name);
23050
+ const transcriptsDir = path26.join(
22532
23051
  wsRoot,
22533
23052
  "GitHub.copilot-chat",
22534
23053
  "transcripts"
22535
23054
  );
22536
23055
  let transcriptEntries;
22537
23056
  try {
22538
- transcriptEntries = await fs26.promises.readdir(transcriptsDir, {
23057
+ transcriptEntries = await fs27.promises.readdir(transcriptsDir, {
22539
23058
  withFileTypes: true
22540
23059
  });
22541
23060
  } catch {
22542
23061
  continue;
22543
23062
  }
22544
23063
  const repoPath = await readWorkspaceFolder(
22545
- path25.join(wsRoot, "workspace.json")
23064
+ path26.join(wsRoot, "workspace.json")
22546
23065
  );
22547
23066
  if (!repoPath) continue;
22548
23067
  for (const entry of transcriptEntries) {
@@ -22550,7 +23069,7 @@ var CopilotChatSource = class {
22550
23069
  const sessionId = entry.name.slice(0, -".jsonl".length);
22551
23070
  results.push({
22552
23071
  sourceName: this.name,
22553
- absolutePath: path25.join(transcriptsDir, entry.name),
23072
+ absolutePath: path26.join(transcriptsDir, entry.name),
22554
23073
  repoPath,
22555
23074
  metadata: { sessionId }
22556
23075
  });
@@ -22590,7 +23109,7 @@ function reportRedactionStats(noun, stats) {
22590
23109
  async function filterByTimeRange(group, range) {
22591
23110
  const results = await Promise.all(
22592
23111
  group.files.map(
22593
- (f) => fs27.promises.stat(f.absolutePath).then((s) => ({ file: f, stat: s })).catch(() => null)
23112
+ (f) => fs28.promises.stat(f.absolutePath).then((s) => ({ file: f, stat: s })).catch(() => null)
22594
23113
  )
22595
23114
  );
22596
23115
  const filtered = [];
@@ -22617,10 +23136,10 @@ async function runInteractive() {
22617
23136
  s.start(`Scanning ${source.name} logs...`);
22618
23137
  const allFiles = await source.scan();
22619
23138
  const allGroups = await mergeByRepo(allFiles);
22620
- const repoRoot = path26.resolve(repo.root);
23139
+ const repoRoot = path27.resolve(repo.root);
22621
23140
  const matching = allGroups.filter((g) => {
22622
- const resolved = path26.resolve(g.repoPath);
22623
- return resolved === repoRoot || resolved.startsWith(repoRoot + path26.sep);
23141
+ const resolved = path27.resolve(g.repoPath);
23142
+ return resolved === repoRoot || resolved.startsWith(repoRoot + path27.sep);
22624
23143
  });
22625
23144
  if (matching.length === 0) {
22626
23145
  s.stop(`No ${source.name} logs found for ${repo.name}.`);
@@ -22651,7 +23170,7 @@ async function runInteractive() {
22651
23170
  }
22652
23171
  }
22653
23172
  const envFileNames = await discoverEnvFiles(repoRoot);
22654
- const envFilePaths = envFileNames.map((n) => path26.join(repoRoot, n));
23173
+ const envFilePaths = envFileNames.map((n) => path27.join(repoRoot, n));
22655
23174
  const additionalFiles = await promptSecretFiles(envFileNames);
22656
23175
  const secretResult = await collectSecrets(
22657
23176
  repoRoot,