hillclimb 0.8.10 → 0.8.11

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 +628 -369
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -12,8 +12,8 @@ import {
12
12
  } from "./chunk-IFGDO3FI.js";
13
13
 
14
14
  // src/main.ts
15
- import fs22 from "fs";
16
- import path22 from "path";
15
+ import fs23 from "fs";
16
+ import path23 from "path";
17
17
  import * as p6 from "@clack/prompts";
18
18
 
19
19
  // src/commands/init.ts
@@ -121,17 +121,24 @@ async function putToPresignedUrl(url, headers, body, options) {
121
121
  const contentLength = body.kind === "buffer" ? body.buffer.byteLength : body.sizeBytes;
122
122
  const start = Date.now();
123
123
  return new Promise((resolve, reject) => {
124
+ let inputStream;
125
+ let progressTimer;
126
+ let phase = "connecting";
124
127
  let settled = false;
125
128
  const settleResolve = (result) => {
126
129
  if (settled) return;
127
130
  settled = true;
128
131
  clearTimeout(deadlineTimer);
132
+ clearInterval(progressTimer);
133
+ inputStream?.destroy();
129
134
  resolve(result);
130
135
  };
131
136
  const settleReject = (err) => {
132
137
  if (settled) return;
133
138
  settled = true;
134
139
  clearTimeout(deadlineTimer);
140
+ clearInterval(progressTimer);
141
+ inputStream?.destroy();
135
142
  reject(err);
136
143
  };
137
144
  const req = transport.request(
@@ -145,6 +152,7 @@ async function putToPresignedUrl(url, headers, body, options) {
145
152
  headers: { ...headers, "content-length": String(contentLength) }
146
153
  },
147
154
  (res) => {
155
+ phase = "receiving-response";
148
156
  const chunks = [];
149
157
  let buffered = 0;
150
158
  res.on("data", (chunk) => {
@@ -174,6 +182,28 @@ async function putToPresignedUrl(url, headers, body, options) {
174
182
  req.destroy(err);
175
183
  }, deadlineMs);
176
184
  deadlineTimer.unref();
185
+ req.on("socket", (socket) => {
186
+ const connected = () => {
187
+ if (phase === "connecting") phase = "sending";
188
+ };
189
+ if (parsed.protocol === "https:") socket.once("secureConnect", connected);
190
+ else if (!socket.connecting) connected();
191
+ else socket.once("connect", connected);
192
+ });
193
+ req.on("finish", () => {
194
+ if (phase !== "receiving-response") phase = "awaiting-response";
195
+ });
196
+ if (options?.onProgress) {
197
+ progressTimer = setInterval(() => {
198
+ options.onProgress?.({
199
+ elapsedMs: Date.now() - start,
200
+ phase,
201
+ socketBytesWritten: req.socket?.bytesWritten ?? 0,
202
+ totalBodyBytes: contentLength
203
+ });
204
+ }, options.progressIntervalMs ?? 3e4);
205
+ progressTimer.unref();
206
+ }
177
207
  req.setTimeout(socketIdleTimeoutMs, () => {
178
208
  const err = new Error(
179
209
  `PUT to presigned URL timed out: socket idle for ${socketIdleTimeoutMs}ms (elapsed ${Date.now() - start}ms)`
@@ -185,12 +215,12 @@ async function putToPresignedUrl(url, headers, body, options) {
185
215
  if (body.kind === "buffer") {
186
216
  req.end(body.buffer);
187
217
  } else {
188
- const stream = fs2.createReadStream(body.path);
189
- stream.on("error", (err) => {
218
+ inputStream = fs2.createReadStream(body.path);
219
+ inputStream.on("error", (err) => {
190
220
  settleReject(err);
191
221
  req.destroy(err);
192
222
  });
193
- stream.pipe(req);
223
+ inputStream.pipe(req);
194
224
  }
195
225
  });
196
226
  }
@@ -524,22 +554,25 @@ var PlatformClient = class {
524
554
  input
525
555
  );
526
556
  }
527
- async uploadToPresignedUrl(presignedUrl, headers, body) {
528
- await this.putToPresignedUrlLogged(presignedUrl, headers, {
529
- kind: "buffer",
530
- buffer: body
531
- });
557
+ async uploadToPresignedUrl(presignedUrl, headers, body, refresh) {
558
+ await this.putToPresignedUrlLogged(
559
+ presignedUrl,
560
+ headers,
561
+ { kind: "buffer", buffer: body },
562
+ refresh
563
+ );
532
564
  }
533
565
  // Same logging and error shape as uploadToPresignedUrl, but streams the
534
566
  // file from disk so large snapshots never have to fit in memory.
535
- async uploadFileToPresignedUrl(presignedUrl, headers, filePath, sizeBytes) {
536
- await this.putToPresignedUrlLogged(presignedUrl, headers, {
537
- kind: "file",
538
- path: filePath,
539
- sizeBytes
540
- });
567
+ async uploadFileToPresignedUrl(presignedUrl, headers, filePath, sizeBytes, refresh) {
568
+ await this.putToPresignedUrlLogged(
569
+ presignedUrl,
570
+ headers,
571
+ { kind: "file", path: filePath, sizeBytes },
572
+ refresh
573
+ );
541
574
  }
542
- async putToPresignedUrlLogged(presignedUrl, headers, body) {
575
+ async putToPresignedUrlLogged(presignedUrl, headers, body, refresh) {
543
576
  const host = (() => {
544
577
  try {
545
578
  return new URL(presignedUrl).host;
@@ -549,11 +582,17 @@ var PlatformClient = class {
549
582
  })();
550
583
  const sizeBytes = body.kind === "buffer" ? body.buffer.byteLength : body.sizeBytes;
551
584
  let hadNetworkError = false;
585
+ let refreshed = false;
552
586
  for (let attempt = 1; ; attempt++) {
553
587
  const start = Date.now();
554
588
  let res;
555
589
  try {
556
- res = await putToPresignedUrl(presignedUrl, headers, body);
590
+ res = await putToPresignedUrl(presignedUrl, headers, body, {
591
+ onProgress: (progress) => appendLog(
592
+ "info",
593
+ `[PUT ${host}] progress (attempt=${attempt}, phase=${progress.phase}, elapsedMs=${progress.elapsedMs}, socketBytesWritten=${progress.socketBytesWritten}, totalBodyBytes=${progress.totalBodyBytes})`
594
+ )
595
+ });
557
596
  } catch (err) {
558
597
  hadNetworkError = true;
559
598
  const delayMs2 = putRetryDelayMs({
@@ -589,6 +628,33 @@ var PlatformClient = class {
589
628
  );
590
629
  return;
591
630
  }
631
+ const code = /<Code>\s*([A-Za-z][A-Za-z0-9]*)\s*<\/Code>/.exec(
632
+ res.bodyText
633
+ )?.[1];
634
+ if ((res.status === 400 || res.status === 403) && code === "ExpiredToken" && refresh && attempt < MAX_ATTEMPTS && !refreshed) {
635
+ refreshed = true;
636
+ appendLog(
637
+ "warn",
638
+ `[PUT ${host}] ${res.status} ${elapsedMs}ms (code=ExpiredToken); refreshing AWS upload URL once for the existing object`
639
+ );
640
+ const renewed = await this.request(
641
+ "POST",
642
+ `/api/v1/contributions/${encodeURIComponent(refresh.contributionId)}/uploads/${encodeURIComponent(refresh.uploadId)}/refresh`
643
+ );
644
+ const previousUrl = new URL(presignedUrl);
645
+ const renewedUrl = new URL(renewed.presignedUrl);
646
+ const conditionalHeader = Object.entries(renewed.headers).find(
647
+ ([name]) => name.toLowerCase() === "if-none-match"
648
+ )?.[1];
649
+ if (renewed.upload.id !== refresh.uploadId || renewed.upload.declaredSizeBytes !== sizeBytes || renewedUrl.origin !== previousUrl.origin || renewedUrl.pathname !== previousUrl.pathname || conditionalHeader !== "*") {
650
+ throw new PlatformError(
651
+ "Refreshed upload must preserve the upload ID, object, and write-once precondition"
652
+ );
653
+ }
654
+ presignedUrl = renewed.presignedUrl;
655
+ headers = renewed.headers;
656
+ continue;
657
+ }
592
658
  const delayMs = putRetryDelayMs({ attempt, status: res.status });
593
659
  if (delayMs !== null) {
594
660
  appendLog(
@@ -598,11 +664,15 @@ var PlatformClient = class {
598
664
  await sleep(delayMs);
599
665
  continue;
600
666
  }
601
- appendLog("warn", `[PUT ${host}] ${res.status} ${elapsedMs}ms`);
602
- const detail = res.bodyText;
667
+ const detail = code ? `(code=${code})` : res.bodyText;
668
+ appendLog(
669
+ "warn",
670
+ `[PUT ${host}] ${res.status} ${elapsedMs}ms${code ? ` (code=${code})` : ""}`
671
+ );
603
672
  throw new PlatformError(
604
673
  `PUT to presigned URL failed: HTTP ${res.status}${detail ? ` ${detail}` : ""}`,
605
- res.status
674
+ res.status,
675
+ code
606
676
  );
607
677
  }
608
678
  }
@@ -2345,9 +2415,41 @@ async function runStatus(args = []) {
2345
2415
  // src/commands/upload.ts
2346
2416
  import { spawn as spawn2 } from "child_process";
2347
2417
  import crypto5 from "crypto";
2348
- import fs13 from "fs";
2349
- import os6 from "os";
2350
- import path13 from "path";
2418
+ import fs14 from "fs";
2419
+ import os7 from "os";
2420
+ import path14 from "path";
2421
+
2422
+ // src/agent-timing.ts
2423
+ async function measureAgentStage(stage, action) {
2424
+ appendLog("info", `agent-upload: timing (stage=${stage}, status=started)`);
2425
+ const started = performance.now();
2426
+ let status = "failed";
2427
+ try {
2428
+ const result = await action();
2429
+ status = "ok";
2430
+ return result;
2431
+ } finally {
2432
+ appendLog(
2433
+ "info",
2434
+ `agent-upload: timing (stage=${stage}, status=${status}, elapsedMs=${Math.round(performance.now() - started)})`
2435
+ );
2436
+ }
2437
+ }
2438
+ function measureAgentSyncStage(stage, action) {
2439
+ appendLog("info", `agent-upload: timing (stage=${stage}, status=started)`);
2440
+ const started = performance.now();
2441
+ let status = "failed";
2442
+ try {
2443
+ const result = action();
2444
+ status = "ok";
2445
+ return result;
2446
+ } finally {
2447
+ appendLog(
2448
+ "info",
2449
+ `agent-upload: timing (stage=${stage}, status=${status}, elapsedMs=${Math.round(performance.now() - started)})`
2450
+ );
2451
+ }
2452
+ }
2351
2453
 
2352
2454
  // src/codex-lineage.ts
2353
2455
  import crypto from "crypto";
@@ -2788,8 +2890,8 @@ async function detectCodexLineage(childPath) {
2788
2890
 
2789
2891
  // src/debug-logs.ts
2790
2892
  import crypto2 from "crypto";
2791
- import fs10 from "fs";
2792
- import path11 from "path";
2893
+ import fs11 from "fs";
2894
+ import path12 from "path";
2793
2895
 
2794
2896
  // src/hook-events.ts
2795
2897
  function classifyHookEvent(event) {
@@ -2877,7 +2979,7 @@ async function reapLockIfStaleImpl(lockPath, opts, beforeOwnerRecheck) {
2877
2979
  if (age < (opts.graceMs ?? DEFAULT_LOCK_GRACE_MS)) return false;
2878
2980
  const ownerPid = await readLockPid(lockPath);
2879
2981
  const alive = opts.isProcessAlive ?? isProcessAlive;
2880
- const shouldReap = ownerPid === null ? age > opts.maxAgeMs : !alive(ownerPid) || age > opts.maxAgeMs;
2982
+ const shouldReap = ownerPid === null ? age > opts.maxAgeMs : !alive(ownerPid) || !opts.preserveLiveOwner && age > opts.maxAgeMs;
2881
2983
  if (!shouldReap) return false;
2882
2984
  try {
2883
2985
  await beforeOwnerRecheck?.(lockPath);
@@ -12099,7 +12201,11 @@ async function collectSecrets(repoRoot, envFiles, additionalFiles) {
12099
12201
  }
12100
12202
 
12101
12203
  // src/outputs/platform.ts
12102
- import { PassThrough } from "stream";
12204
+ import fs9 from "fs";
12205
+ import os5 from "os";
12206
+ import path10 from "path";
12207
+ import { Transform } from "stream";
12208
+ import { pipeline } from "stream/promises";
12103
12209
  import archiver from "archiver";
12104
12210
 
12105
12211
  // src/outputs/archive.ts
@@ -12153,21 +12259,34 @@ var UploadTooLargeError = class extends Error {
12153
12259
  this.name = "UploadTooLargeError";
12154
12260
  }
12155
12261
  };
12156
- async function buildZipBuffer(group, selectedSources) {
12262
+ async function buildZipFile(group, selectedSources, filePath, zipFilename, maxUploadBytes) {
12157
12263
  const archive = archiver("zip", { zlib: { level: 6 } });
12158
- const stream = new PassThrough();
12159
- archive.pipe(stream);
12160
- const chunks = [];
12161
- const done = new Promise((resolve, reject) => {
12162
- stream.on("data", (chunk) => chunks.push(chunk));
12163
- stream.on("end", resolve);
12164
- stream.on("error", reject);
12165
- archive.on("error", reject);
12264
+ let sizeBytes = 0;
12265
+ const sizeGuard = new Transform({
12266
+ transform(chunk, _encoding, callback) {
12267
+ sizeBytes += chunk.length;
12268
+ if (maxUploadBytes !== void 0 && sizeBytes > maxUploadBytes) {
12269
+ callback(
12270
+ new UploadTooLargeError(zipFilename, sizeBytes, maxUploadBytes)
12271
+ );
12272
+ } else {
12273
+ callback(null, chunk);
12274
+ }
12275
+ }
12166
12276
  });
12167
- addGroupToArchive(archive, group, selectedSources);
12168
- await archive.finalize();
12169
- await done;
12170
- return Buffer.concat(chunks);
12277
+ const output = fs9.createWriteStream(filePath, { flags: "wx", mode: 384 });
12278
+ archive.on("warning", (err) => archive.destroy(err));
12279
+ const done = pipeline(archive, sizeGuard, output);
12280
+ try {
12281
+ addGroupToArchive(archive, group, selectedSources);
12282
+ await Promise.all([done, archive.finalize()]);
12283
+ return sizeBytes;
12284
+ } finally {
12285
+ archive.destroy();
12286
+ output.destroy();
12287
+ await done.catch(() => {
12288
+ });
12289
+ }
12171
12290
  }
12172
12291
  var PlatformUploadOutput = class {
12173
12292
  constructor(opts) {
@@ -12177,7 +12296,6 @@ var PlatformUploadOutput = class {
12177
12296
  label = "Upload to hillclimb platform";
12178
12297
  async emit(group, options) {
12179
12298
  const selectedSources = new Set(options.selectedSources);
12180
- const buffer = await buildZipBuffer(group, selectedSources);
12181
12299
  const {
12182
12300
  client,
12183
12301
  projectId,
@@ -12190,76 +12308,97 @@ var PlatformUploadOutput = class {
12190
12308
  onContributionCreated,
12191
12309
  maxUploadBytes
12192
12310
  } = this.opts;
12193
- if (maxUploadBytes !== void 0 && buffer.byteLength > maxUploadBytes) {
12194
- throw new UploadTooLargeError(
12195
- zipFilename,
12196
- buffer.byteLength,
12197
- maxUploadBytes
12311
+ const tempDir = await fs9.promises.mkdtemp(
12312
+ path10.join(os5.tmpdir(), "hillclimb-agent-zip-")
12313
+ );
12314
+ const filePath = path10.join(tempDir, "snapshot.zip");
12315
+ try {
12316
+ const sizeBytes = await measureAgentStage(
12317
+ "zip-create",
12318
+ () => buildZipFile(
12319
+ group,
12320
+ selectedSources,
12321
+ filePath,
12322
+ zipFilename,
12323
+ maxUploadBytes
12324
+ )
12198
12325
  );
12199
- }
12200
- let contributionId;
12201
- if (existingContributionId) {
12202
- contributionId = existingContributionId;
12203
- } else {
12204
- const contribution = await client.createContribution(projectId, {
12205
- contributionTypeSlug,
12206
- title: contributionTitle,
12207
- body: contributionBody
12326
+ appendLog(
12327
+ "info",
12328
+ `agent-upload: prepared archive (sizeBytes=${sizeBytes})`
12329
+ );
12330
+ let contributionId;
12331
+ if (existingContributionId) {
12332
+ contributionId = existingContributionId;
12333
+ } else {
12334
+ const contribution = await client.createContribution(projectId, {
12335
+ contributionTypeSlug,
12336
+ title: contributionTitle,
12337
+ body: contributionBody
12338
+ });
12339
+ contributionId = contribution.id;
12340
+ if (onContributionCreated) await onContributionCreated(contributionId);
12341
+ }
12342
+ const presigned = await client.createUpload(contributionId, {
12343
+ originalFilename: zipFilename,
12344
+ mimeType: "application/zip",
12345
+ sizeBytes
12208
12346
  });
12209
- contributionId = contribution.id;
12210
- if (onContributionCreated) await onContributionCreated(contributionId);
12211
- }
12212
- const presigned = await client.createUpload(contributionId, {
12213
- originalFilename: zipFilename,
12214
- mimeType: "application/zip",
12215
- sizeBytes: buffer.byteLength
12216
- });
12217
- appendLog(
12218
- "info",
12219
- `uploading ${zipFilename} (${buffer.byteLength} bytes) to presigned URL`
12220
- );
12221
- await client.uploadToPresignedUrl(
12222
- presigned.presignedUrl,
12223
- presigned.headers,
12224
- buffer
12225
- );
12226
- appendLog("info", `PUT to presigned URL succeeded for ${zipFilename}`);
12227
- if (autoSubmit) {
12228
- appendLog("info", `submitting contribution ${contributionId}`);
12229
- try {
12230
- await client.submitContribution(contributionId);
12231
- appendLog("info", `contribution ${contributionId} submitted`);
12232
- } catch (err) {
12233
- if (err instanceof PlatformError && err.status === 409 && err.code === "CONTRIBUTION_WRONG_STATE") {
12234
- appendLog(
12235
- "info",
12236
- `contribution ${contributionId} was already submitted (409); continuing`
12237
- );
12238
- } else {
12239
- throw err;
12347
+ appendLog(
12348
+ "info",
12349
+ `uploading ${zipFilename} (${sizeBytes} bytes) to presigned URL`
12350
+ );
12351
+ await client.uploadFileToPresignedUrl(
12352
+ presigned.presignedUrl,
12353
+ presigned.headers,
12354
+ filePath,
12355
+ sizeBytes,
12356
+ { contributionId, uploadId: presigned.upload.id }
12357
+ );
12358
+ appendLog("info", `PUT to presigned URL succeeded for ${zipFilename}`);
12359
+ if (autoSubmit) {
12360
+ appendLog("info", `submitting contribution ${contributionId}`);
12361
+ try {
12362
+ await client.submitContribution(contributionId);
12363
+ appendLog("info", `contribution ${contributionId} submitted`);
12364
+ } catch (err) {
12365
+ if (err instanceof PlatformError && err.status === 409 && err.code === "CONTRIBUTION_WRONG_STATE") {
12366
+ appendLog(
12367
+ "info",
12368
+ `contribution ${contributionId} was already submitted (409); continuing`
12369
+ );
12370
+ } else {
12371
+ throw err;
12372
+ }
12240
12373
  }
12241
12374
  }
12375
+ return contributionId;
12376
+ } finally {
12377
+ try {
12378
+ await fs9.promises.rm(tempDir, { recursive: true, force: true });
12379
+ } catch {
12380
+ appendLog("warn", "agent-upload: temporary ZIP cleanup failed");
12381
+ }
12242
12382
  }
12243
- return contributionId;
12244
12383
  }
12245
12384
  };
12246
12385
 
12247
12386
  // src/pipeline.ts
12248
- import fs9 from "fs";
12249
- import path10 from "path";
12387
+ import fs10 from "fs";
12388
+ import path11 from "path";
12250
12389
  function canonicalizePath(p7) {
12251
- let resolved = path10.resolve(p7);
12252
- if (resolved.endsWith(path10.sep) && resolved !== path10.sep) {
12390
+ let resolved = path11.resolve(p7);
12391
+ if (resolved.endsWith(path11.sep) && resolved !== path11.sep) {
12253
12392
  resolved = resolved.slice(0, -1);
12254
12393
  }
12255
12394
  return resolved;
12256
12395
  }
12257
12396
  function computeLabel(repoPath, allPaths) {
12258
- const segments = repoPath.split(path10.sep).filter(Boolean);
12397
+ const segments = repoPath.split(path11.sep).filter(Boolean);
12259
12398
  for (let depth = 1; depth <= segments.length; depth++) {
12260
12399
  const label = segments.slice(-depth).join("/");
12261
12400
  const matches = allPaths.filter((p7) => {
12262
- const s = p7.split(path10.sep).filter(Boolean);
12401
+ const s = p7.split(path11.sep).filter(Boolean);
12263
12402
  return s.slice(-depth).join("/") === label;
12264
12403
  });
12265
12404
  if (matches.length === 1) return label;
@@ -12281,7 +12420,7 @@ async function mergeByRepo(files) {
12281
12420
  const groups = [];
12282
12421
  for (const [repoPath, groupFiles] of grouped) {
12283
12422
  const stats = await Promise.all(
12284
- groupFiles.map((f) => fs9.promises.stat(f.absolutePath).catch(() => null))
12423
+ groupFiles.map((f) => fs10.promises.stat(f.absolutePath).catch(() => null))
12285
12424
  );
12286
12425
  let lastModified = /* @__PURE__ */ new Date(0);
12287
12426
  for (const stat of stats) {
@@ -12304,7 +12443,7 @@ async function preloadFiles(group) {
12304
12443
  group.files.map(async (file) => {
12305
12444
  if (file.content) return file;
12306
12445
  try {
12307
- const buf = await fs9.promises.readFile(file.absolutePath);
12446
+ const buf = await fs10.promises.readFile(file.absolutePath);
12308
12447
  const checkLen = Math.min(buf.length, 8192);
12309
12448
  for (let i = 0; i < checkLen; i++) {
12310
12449
  if (buf[i] === 0) {
@@ -12320,11 +12459,12 @@ async function preloadFiles(group) {
12320
12459
  return { ...group, files };
12321
12460
  }
12322
12461
  async function runPipeline(group, middleware2, output, options, onProgress) {
12323
- const preloaded = await preloadFiles(group);
12462
+ const measure = output.name === "platform";
12463
+ const preloaded = measure ? await measureAgentStage("preload", () => preloadFiles(group)) : await preloadFiles(group);
12324
12464
  let processed = preloaded;
12325
12465
  for (const mw of middleware2) {
12326
12466
  onProgress?.(`${mw.name} (${processed.files.length} files)`);
12327
- processed = await mw.process(processed);
12467
+ processed = measure ? await measureAgentStage(mw.name, () => mw.process(processed)) : await mw.process(processed);
12328
12468
  }
12329
12469
  onProgress?.(`Compressing ${processed.files.length} files`);
12330
12470
  return output.emit(processed, options);
@@ -12342,7 +12482,7 @@ var STALE_LOCK_TTL_MS = 60 * 60 * 1e3;
12342
12482
  var EVENT_STATE_TTL_MS = 24 * 60 * 60 * 1e3;
12343
12483
  var SESSION_STATE_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
12344
12484
  function stateDir() {
12345
- return process.env.HILLCLIMB_DEBUG_LOG_STATE_DIR ?? path11.join(configDir(), "debug-log-uploads");
12485
+ return process.env.HILLCLIMB_DEBUG_LOG_STATE_DIR ?? path12.join(configDir(), "debug-log-uploads");
12346
12486
  }
12347
12487
  function waitMs() {
12348
12488
  const raw = process.env.HILLCLIMB_DEBUG_LOG_WAIT_MS;
@@ -12365,7 +12505,7 @@ async function spawnKey(payload, env = process.env) {
12365
12505
  if (!sessionId && !turnId && !transcriptPath) return null;
12366
12506
  let pin = {};
12367
12507
  if (eventKind2 === "stop" && !turnId && transcriptPath) {
12368
- const resolved = path11.resolve(transcriptPath);
12508
+ const resolved = path12.resolve(transcriptPath);
12369
12509
  const raw = env[TRANSCRIPT_STAT_ENV];
12370
12510
  try {
12371
12511
  const pinned = raw ? JSON.parse(raw) : null;
@@ -12388,7 +12528,7 @@ async function spawnKey(payload, env = process.env) {
12388
12528
  ).digest("hex").slice(0, 32);
12389
12529
  }
12390
12530
  function spawnMarkerFile(kind, key) {
12391
- return path11.join(stateDir(), `spawn-${kind}-${key}`);
12531
+ return path12.join(stateDir(), `spawn-${kind}-${key}`);
12392
12532
  }
12393
12533
  async function recordDebugLogSpawn(args) {
12394
12534
  if (process.env.HILLCLIMB_DEBUG_LOG_UPLOAD === "0") return;
@@ -12397,8 +12537,8 @@ async function recordDebugLogSpawn(args) {
12397
12537
  if (typeof payload !== "object" || payload === null) return;
12398
12538
  const key = await spawnKey(payload, args.env);
12399
12539
  if (!key) return;
12400
- await fs10.promises.mkdir(stateDir(), { recursive: true, mode: 448 });
12401
- await fs10.promises.writeFile(spawnMarkerFile(args.kind, key), "", {
12540
+ await fs11.promises.mkdir(stateDir(), { recursive: true, mode: 448 });
12541
+ await fs11.promises.writeFile(spawnMarkerFile(args.kind, key), "", {
12402
12542
  mode: 384
12403
12543
  });
12404
12544
  } catch (err) {
@@ -12410,14 +12550,14 @@ async function recordDebugLogSpawn(args) {
12410
12550
  }
12411
12551
  async function fileExists(file) {
12412
12552
  try {
12413
- await fs10.promises.access(file);
12553
+ await fs11.promises.access(file);
12414
12554
  return true;
12415
12555
  } catch {
12416
12556
  return false;
12417
12557
  }
12418
12558
  }
12419
12559
  function stateFile(eventId) {
12420
- return path11.join(stateDir(), `${eventId}.json`);
12560
+ return path12.join(stateDir(), `${eventId}.json`);
12421
12561
  }
12422
12562
  function lockFile(eventId) {
12423
12563
  return `${stateFile(eventId)}.lock`;
@@ -12480,7 +12620,7 @@ function expectedKinds(tool, eventKind2, payload) {
12480
12620
  var TRANSCRIPT_STAT_ENV = "HILLCLIMB_DEBUG_LOG_TRANSCRIPT_STAT";
12481
12621
  async function statTranscriptFingerprint(resolved) {
12482
12622
  try {
12483
- const stat = await fs10.promises.stat(resolved);
12623
+ const stat = await fs11.promises.stat(resolved);
12484
12624
  return {
12485
12625
  transcriptPath: resolved,
12486
12626
  transcriptMtimeMs: stat.mtimeMs,
@@ -12503,7 +12643,7 @@ function pinnedTranscriptFingerprint(resolved) {
12503
12643
  async function transcriptFingerprint(payload) {
12504
12644
  const transcriptPath = stringOrNull(payload.transcript_path);
12505
12645
  if (!transcriptPath) return {};
12506
- const resolved = path11.resolve(transcriptPath);
12646
+ const resolved = path12.resolve(transcriptPath);
12507
12647
  return pinnedTranscriptFingerprint(resolved) ?? await statTranscriptFingerprint(resolved);
12508
12648
  }
12509
12649
  async function captureDebugLogParentEnv(rawPayload) {
@@ -12520,7 +12660,7 @@ async function captureDebugLogParentEnv(rawPayload) {
12520
12660
  }
12521
12661
  return {
12522
12662
  [TRANSCRIPT_STAT_ENV]: JSON.stringify(
12523
- await statTranscriptFingerprint(path11.resolve(transcriptPath))
12663
+ await statTranscriptFingerprint(path12.resolve(transcriptPath))
12524
12664
  )
12525
12665
  };
12526
12666
  }
@@ -12579,7 +12719,7 @@ async function eventContext(tool, payload) {
12579
12719
  }
12580
12720
  async function readState(eventId) {
12581
12721
  try {
12582
- const raw = await fs10.promises.readFile(stateFile(eventId), "utf-8");
12722
+ const raw = await fs11.promises.readFile(stateFile(eventId), "utf-8");
12583
12723
  const parsed = JSON.parse(raw);
12584
12724
  if (parsed.schemaVersion !== CURRENT_SCHEMA_VERSION) return null;
12585
12725
  return parsed;
@@ -12588,17 +12728,17 @@ async function readState(eventId) {
12588
12728
  }
12589
12729
  }
12590
12730
  async function writeState(state) {
12591
- await fs10.promises.mkdir(stateDir(), { recursive: true, mode: 448 });
12731
+ await fs11.promises.mkdir(stateDir(), { recursive: true, mode: 448 });
12592
12732
  const file = stateFile(state.eventId);
12593
12733
  const tmp = `${file}.tmp`;
12594
- await fs10.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
12734
+ await fs11.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
12595
12735
  mode: 384
12596
12736
  });
12597
- await fs10.promises.rename(tmp, file);
12737
+ await fs11.promises.rename(tmp, file);
12598
12738
  }
12599
12739
  async function readSessionState(key) {
12600
12740
  try {
12601
- const raw = await fs10.promises.readFile(
12741
+ const raw = await fs11.promises.readFile(
12602
12742
  stateFile(sessionStateId(key)),
12603
12743
  "utf-8"
12604
12744
  );
@@ -12612,26 +12752,26 @@ async function readSessionState(key) {
12612
12752
  }
12613
12753
  }
12614
12754
  async function writeSessionState(state) {
12615
- await fs10.promises.mkdir(stateDir(), { recursive: true, mode: 448 });
12755
+ await fs11.promises.mkdir(stateDir(), { recursive: true, mode: 448 });
12616
12756
  const file = stateFile(sessionStateId(state.sessionKey));
12617
12757
  const tmp = `${file}.tmp`;
12618
- await fs10.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
12758
+ await fs11.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
12619
12759
  mode: 384
12620
12760
  });
12621
- await fs10.promises.rename(tmp, file);
12761
+ await fs11.promises.rename(tmp, file);
12622
12762
  }
12623
12763
  async function deleteSessionState(key) {
12624
12764
  try {
12625
- await fs10.promises.unlink(stateFile(sessionStateId(key)));
12765
+ await fs11.promises.unlink(stateFile(sessionStateId(key)));
12626
12766
  } catch (err) {
12627
12767
  if (err.code !== "ENOENT") throw err;
12628
12768
  }
12629
12769
  }
12630
12770
  async function tryCreateLock(lockId) {
12631
12771
  try {
12632
- const fd = await fs10.promises.open(
12772
+ const fd = await fs11.promises.open(
12633
12773
  lockFile(lockId),
12634
- fs10.constants.O_CREAT | fs10.constants.O_EXCL | fs10.constants.O_WRONLY
12774
+ fs11.constants.O_CREAT | fs11.constants.O_EXCL | fs11.constants.O_WRONLY
12635
12775
  );
12636
12776
  try {
12637
12777
  await fd.write(String(process.pid));
@@ -12645,7 +12785,7 @@ async function tryCreateLock(lockId) {
12645
12785
  }
12646
12786
  }
12647
12787
  async function acquireLock(eventId) {
12648
- await fs10.promises.mkdir(stateDir(), { recursive: true, mode: 448 });
12788
+ await fs11.promises.mkdir(stateDir(), { recursive: true, mode: 448 });
12649
12789
  for (let i = 0; i < LOCK_RETRIES; i++) {
12650
12790
  if (await tryCreateLock(eventId)) return;
12651
12791
  if (await reapLockIfStale(lockFile(eventId), {
@@ -12660,7 +12800,7 @@ async function acquireLock(eventId) {
12660
12800
  );
12661
12801
  }
12662
12802
  async function tryAcquireLock(eventId, now) {
12663
- await fs10.promises.mkdir(stateDir(), { recursive: true, mode: 448 });
12803
+ await fs11.promises.mkdir(stateDir(), { recursive: true, mode: 448 });
12664
12804
  if (await tryCreateLock(eventId)) return true;
12665
12805
  if (await reapLockIfStale(lockFile(eventId), {
12666
12806
  maxAgeMs: STALE_LOCK_TTL_MS,
@@ -12673,13 +12813,13 @@ async function tryAcquireLock(eventId, now) {
12673
12813
  async function sweepStaleDebugLogState(now = Date.now()) {
12674
12814
  let entries;
12675
12815
  try {
12676
- entries = await fs10.promises.readdir(stateDir(), { withFileTypes: true });
12816
+ entries = await fs11.promises.readdir(stateDir(), { withFileTypes: true });
12677
12817
  } catch {
12678
12818
  return;
12679
12819
  }
12680
12820
  for (const entry of entries) {
12681
12821
  if (!entry.isFile()) continue;
12682
- const file = path11.join(stateDir(), entry.name);
12822
+ const file = path12.join(stateDir(), entry.name);
12683
12823
  try {
12684
12824
  if (entry.name.endsWith(".lock")) {
12685
12825
  await reapLockIfStale(file, {
@@ -12689,23 +12829,23 @@ async function sweepStaleDebugLogState(now = Date.now()) {
12689
12829
  continue;
12690
12830
  }
12691
12831
  if (entry.name.startsWith("session-") && entry.name.endsWith(".json")) {
12692
- const stat2 = await fs10.promises.stat(file);
12832
+ const stat2 = await fs11.promises.stat(file);
12693
12833
  if (now - stat2.mtimeMs <= SESSION_STATE_TTL_MS) continue;
12694
12834
  const sessionLockId = entry.name.slice(0, -".json".length);
12695
12835
  if (!await tryAcquireLock(sessionLockId, now)) continue;
12696
12836
  try {
12697
- const lockedStat = await fs10.promises.stat(file);
12837
+ const lockedStat = await fs11.promises.stat(file);
12698
12838
  if (now - lockedStat.mtimeMs > SESSION_STATE_TTL_MS) {
12699
- await fs10.promises.unlink(file);
12839
+ await fs11.promises.unlink(file);
12700
12840
  }
12701
12841
  } finally {
12702
12842
  await releaseLock(sessionLockId);
12703
12843
  }
12704
12844
  continue;
12705
12845
  }
12706
- const stat = await fs10.promises.stat(file);
12846
+ const stat = await fs11.promises.stat(file);
12707
12847
  if (now - stat.mtimeMs > EVENT_STATE_TTL_MS) {
12708
- await fs10.promises.unlink(file);
12848
+ await fs11.promises.unlink(file);
12709
12849
  }
12710
12850
  } catch {
12711
12851
  }
@@ -12713,7 +12853,7 @@ async function sweepStaleDebugLogState(now = Date.now()) {
12713
12853
  }
12714
12854
  async function releaseLock(eventId) {
12715
12855
  try {
12716
- await fs10.promises.unlink(lockFile(eventId));
12856
+ await fs11.promises.unlink(lockFile(eventId));
12717
12857
  } catch {
12718
12858
  }
12719
12859
  }
@@ -12729,7 +12869,7 @@ function initialState(ctx, now) {
12729
12869
  eventKind: ctx.eventKind,
12730
12870
  hookEventName: ctx.hookEventName,
12731
12871
  sessionId: ctx.sessionId,
12732
- logDate: path11.basename(todayLogPath(), ".log"),
12872
+ logDate: path12.basename(todayLogPath(), ".log"),
12733
12873
  firstSeenAt: now.toISOString()
12734
12874
  };
12735
12875
  }
@@ -12816,7 +12956,7 @@ async function waitForExpectedKinds(ctx, state, ownKind) {
12816
12956
  }
12817
12957
  async function buildMiddleware(repoRoot) {
12818
12958
  const envFileNames = await discoverEnvFiles(repoRoot);
12819
- const envFilePaths = envFileNames.map((n) => path11.join(repoRoot, n));
12959
+ const envFilePaths = envFileNames.map((n) => path12.join(repoRoot, n));
12820
12960
  const secretResult = await collectSecrets(repoRoot, envFilePaths, []);
12821
12961
  const middleware2 = [];
12822
12962
  if (secretResult.values.size > 0) {
@@ -12829,7 +12969,7 @@ async function uploadDebugLog(ctx, state, sessionState, onContributionCreated) {
12829
12969
  const logPath = todayLogPath();
12830
12970
  let content;
12831
12971
  try {
12832
- content = await fs10.promises.readFile(logPath);
12972
+ content = await fs11.promises.readFile(logPath);
12833
12973
  } catch (err) {
12834
12974
  appendLog(
12835
12975
  "warn",
@@ -12862,7 +13002,7 @@ async function uploadDebugLog(ctx, state, sessionState, onContributionCreated) {
12862
13002
  };
12863
13003
  const group = {
12864
13004
  repoPath: ctx.repoRoot,
12865
- label: path11.basename(ctx.repoRoot),
13005
+ label: path12.basename(ctx.repoRoot),
12866
13006
  files: [sourceFile],
12867
13007
  sourceNames: [DEBUG_LOGS_SLUG],
12868
13008
  lastModified: now
@@ -12885,7 +13025,7 @@ async function uploadDebugLog(ctx, state, sessionState, onContributionCreated) {
12885
13025
  `Tool: ${label}`,
12886
13026
  `Event: ${ctx.hookEventName ?? ctx.eventKind}`,
12887
13027
  `Repo: ${ctx.repoRoot}`,
12888
- `Log: ${path11.basename(logPath)}`,
13028
+ `Log: ${path12.basename(logPath)}`,
12889
13029
  `Agent done: ${state.agentDoneAt ?? "<not observed>"}`,
12890
13030
  `Git done: ${state.gitDoneAt ?? "<not observed>"}`,
12891
13031
  `Uploaded: ${now.toISOString()}`
@@ -12918,7 +13058,7 @@ async function uploadDebugLog(ctx, state, sessionState, onContributionCreated) {
12918
13058
  );
12919
13059
  appendLog(
12920
13060
  "info",
12921
- `debug-logs: uploaded ${path11.basename(logPath)} to project ${ctx.config.projectSlug} (${ctx.config.projectId}) as contribution ${contributionId}`
13061
+ `debug-logs: uploaded ${path12.basename(logPath)} to project ${ctx.config.projectSlug} (${ctx.config.projectId}) as contribution ${contributionId}`
12922
13062
  );
12923
13063
  return { kind: "uploaded", contributionId };
12924
13064
  } catch (err) {
@@ -13079,14 +13219,15 @@ async function uploadArtifact(client, contributionId, filename, mimeType, buffer
13079
13219
  await client.uploadToPresignedUrl(
13080
13220
  presigned.presignedUrl,
13081
13221
  presigned.headers,
13082
- buffer
13222
+ buffer,
13223
+ { contributionId, uploadId: presigned.upload.id }
13083
13224
  );
13084
13225
  appendLog("info", `PUT to presigned URL succeeded for ${filename}`);
13085
13226
  }
13086
13227
 
13087
13228
  // src/transcript-cursor.ts
13088
13229
  import crypto3 from "crypto";
13089
- import fs11 from "fs";
13230
+ import fs12 from "fs";
13090
13231
  function sha256OfBuffer(buffer) {
13091
13232
  return crypto3.createHash("sha256").update(buffer).digest("hex");
13092
13233
  }
@@ -13094,7 +13235,7 @@ async function hashPrefix(filePath, byteLength) {
13094
13235
  const hash = crypto3.createHash("sha256");
13095
13236
  if (byteLength === 0) return hash;
13096
13237
  await new Promise((resolve, reject) => {
13097
- const stream = fs11.createReadStream(filePath, {
13238
+ const stream = fs12.createReadStream(filePath, {
13098
13239
  start: 0,
13099
13240
  end: byteLength - 1
13100
13241
  });
@@ -13105,7 +13246,7 @@ async function hashPrefix(filePath, byteLength) {
13105
13246
  return hash;
13106
13247
  }
13107
13248
  async function readRange(filePath, start, end) {
13108
- const fd = await fs11.promises.open(filePath, "r");
13249
+ const fd = await fs12.promises.open(filePath, "r");
13109
13250
  try {
13110
13251
  const buffer = Buffer.alloc(end - start);
13111
13252
  let filled = 0;
@@ -13125,15 +13266,21 @@ async function readRange(filePath, start, end) {
13125
13266
  }
13126
13267
  }
13127
13268
  async function evaluateTranscript(filePath, cursor) {
13128
- const stat = await fs11.promises.stat(filePath);
13269
+ const stat = await fs12.promises.stat(filePath);
13129
13270
  if (stat.size < cursor.rawByteOffset) return { kind: "truncate" };
13130
- const prefixHash = await hashPrefix(filePath, cursor.rawByteOffset);
13271
+ const prefixHash = await measureAgentStage(
13272
+ "cursor-prefix-hash",
13273
+ () => hashPrefix(filePath, cursor.rawByteOffset)
13274
+ );
13131
13275
  const continuation = prefixHash.copy();
13132
13276
  if (prefixHash.digest("hex") !== cursor.rawPrefixSha256) {
13133
13277
  return { kind: "rewrite" };
13134
13278
  }
13135
13279
  if (stat.size === cursor.rawByteOffset) return { kind: "unchanged" };
13136
- const grown = await readRange(filePath, cursor.rawByteOffset, stat.size);
13280
+ const grown = await measureAgentStage(
13281
+ "transcript-tail-read",
13282
+ () => readRange(filePath, cursor.rawByteOffset, stat.size)
13283
+ );
13137
13284
  const lastNewline = grown.lastIndexOf(10);
13138
13285
  if (lastNewline === -1) return { kind: "unchanged" };
13139
13286
  const tail = grown.subarray(0, lastNewline + 1);
@@ -13160,12 +13307,15 @@ function truncateAtLastNewline(buffer) {
13160
13307
  }
13161
13308
  async function cursorMatchesFile(filePath, cursor) {
13162
13309
  try {
13163
- const stat = await fs11.promises.stat(filePath);
13310
+ const stat = await fs12.promises.stat(filePath);
13164
13311
  if (stat.size < cursor.rawByteOffset) return false;
13165
- const prefixHash = await hashPrefix(filePath, cursor.rawByteOffset);
13312
+ const prefixHash = await measureAgentStage(
13313
+ "legacy-prefix-hash",
13314
+ () => hashPrefix(filePath, cursor.rawByteOffset)
13315
+ );
13166
13316
  if (prefixHash.digest("hex") !== cursor.rawPrefixSha256) return false;
13167
13317
  if (cursor.rawByteOffset === 0) return true;
13168
- const fd = await fs11.promises.open(filePath, "r");
13318
+ const fd = await fs12.promises.open(filePath, "r");
13169
13319
  try {
13170
13320
  const byte = Buffer.alloc(1);
13171
13321
  const { bytesRead } = await fd.read(byte, 0, 1, cursor.rawByteOffset - 1);
@@ -13180,19 +13330,20 @@ async function cursorMatchesFile(filePath, cursor) {
13180
13330
 
13181
13331
  // src/upload-state.ts
13182
13332
  import crypto4 from "crypto";
13183
- import fs12 from "fs";
13184
- import os5 from "os";
13185
- import path12 from "path";
13333
+ import fs13 from "fs";
13334
+ import os6 from "os";
13335
+ import path13 from "path";
13186
13336
  var CURRENT_SCHEMA_VERSION2 = 1;
13187
- var DEFAULT_STATE_DIR = path12.join(
13188
- os5.homedir(),
13337
+ var DEFAULT_STATE_DIR = path13.join(
13338
+ os6.homedir(),
13189
13339
  ".hillclimb",
13190
13340
  "agent-uploads"
13191
13341
  );
13192
13342
  var DEFAULT_LOCK_WAIT_MS2 = 5 * 60 * 1e3;
13343
+ var DEFAULT_LIVE_OWNER_MAX_WAIT_MS = 4 * 60 * 60 * 1e3;
13193
13344
  var DEFAULT_LOCK_RETRY_DELAY_MS = 500;
13194
13345
  var DEFAULT_STATE_TTL_MS = 24 * 60 * 60 * 1e3;
13195
- var STALE_LOCK_TTL_MS2 = 60 * 60 * 1e3;
13346
+ var STALE_LOCK_TTL_MS2 = DEFAULT_LIVE_OWNER_MAX_WAIT_MS;
13196
13347
  function stateDir2() {
13197
13348
  return process.env.HILLCLIMB_UPLOAD_STATE_DIR ?? DEFAULT_STATE_DIR;
13198
13349
  }
@@ -13208,6 +13359,13 @@ function stateTtlMs() {
13208
13359
  DEFAULT_STATE_TTL_MS
13209
13360
  );
13210
13361
  }
13362
+ function staleLockTtlMs() {
13363
+ return Math.max(
13364
+ STALE_LOCK_TTL_MS2,
13365
+ readPositiveEnvMs("HILLCLIMB_UPLOAD_LOCK_LIVE_OWNER_MAX_WAIT_MS", 0),
13366
+ readPositiveEnvMs("HILLCLIMB_UPLOAD_LOCK_WAIT_MS", 0)
13367
+ );
13368
+ }
13211
13369
  function lockRetryDelayMs() {
13212
13370
  return Math.max(
13213
13371
  1,
@@ -13226,15 +13384,15 @@ function lockRetries() {
13226
13384
  );
13227
13385
  }
13228
13386
  function stateFileFor(repoRoot, tool, sessionId) {
13229
- const hash = crypto4.createHash("sha256").update(`${path12.resolve(repoRoot)}\0${tool}\0${sessionId}`).digest("hex").slice(0, 16);
13230
- return path12.join(stateDir2(), `${hash}.json`);
13387
+ const hash = crypto4.createHash("sha256").update(`${path13.resolve(repoRoot)}\0${tool}\0${sessionId}`).digest("hex").slice(0, 16);
13388
+ return path13.join(stateDir2(), `${hash}.json`);
13231
13389
  }
13232
13390
  function lockFileFor(repoRoot, tool, sessionId) {
13233
13391
  return `${stateFileFor(repoRoot, tool, sessionId)}.lock`;
13234
13392
  }
13235
13393
  async function readUploadState(repoRoot, tool, sessionId) {
13236
13394
  try {
13237
- const raw = await fs12.promises.readFile(
13395
+ const raw = await fs13.promises.readFile(
13238
13396
  stateFileFor(repoRoot, tool, sessionId),
13239
13397
  "utf-8"
13240
13398
  );
@@ -13246,21 +13404,23 @@ async function readUploadState(repoRoot, tool, sessionId) {
13246
13404
  }
13247
13405
  }
13248
13406
  async function writeUploadState(state) {
13249
- const file = stateFileFor(state.repoRoot, state.tool, state.sessionId);
13250
- await fs12.promises.mkdir(stateDir2(), { recursive: true, mode: 448 });
13251
- const tmp = `${file}.tmp`;
13252
- await fs12.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
13253
- mode: 384
13407
+ await measureAgentStage("state-persist", async () => {
13408
+ const file = stateFileFor(state.repoRoot, state.tool, state.sessionId);
13409
+ await fs13.promises.mkdir(stateDir2(), { recursive: true, mode: 448 });
13410
+ const tmp = `${file}.tmp`;
13411
+ await fs13.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
13412
+ mode: 384
13413
+ });
13414
+ await fs13.promises.rename(tmp, file);
13254
13415
  });
13255
- await fs12.promises.rename(tmp, file);
13256
13416
  }
13257
13417
  async function deleteUploadState(repoRoot, tool, sessionId) {
13258
13418
  try {
13259
- await fs12.promises.unlink(stateFileFor(repoRoot, tool, sessionId));
13419
+ await fs13.promises.unlink(stateFileFor(repoRoot, tool, sessionId));
13260
13420
  } catch {
13261
13421
  }
13262
13422
  try {
13263
- await fs12.promises.unlink(cursorFileFor(repoRoot, tool, sessionId));
13423
+ await fs13.promises.unlink(cursorFileFor(repoRoot, tool, sessionId));
13264
13424
  } catch {
13265
13425
  }
13266
13426
  }
@@ -13269,7 +13429,7 @@ function cursorFileFor(repoRoot, tool, sessionId) {
13269
13429
  }
13270
13430
  async function readCursorState(repoRoot, tool, sessionId) {
13271
13431
  try {
13272
- const raw = await fs12.promises.readFile(
13432
+ const raw = await fs13.promises.readFile(
13273
13433
  cursorFileFor(repoRoot, tool, sessionId),
13274
13434
  "utf-8"
13275
13435
  );
@@ -13283,13 +13443,15 @@ async function readCursorState(repoRoot, tool, sessionId) {
13283
13443
  }
13284
13444
  }
13285
13445
  async function writeCursorState(repoRoot, tool, sessionId, cursor) {
13286
- const file = cursorFileFor(repoRoot, tool, sessionId);
13287
- await fs12.promises.mkdir(stateDir2(), { recursive: true, mode: 448 });
13288
- const tmp = `${file}.tmp`;
13289
- await fs12.promises.writeFile(tmp, JSON.stringify(cursor, null, 2), {
13290
- mode: 384
13446
+ await measureAgentStage("cursor-persist", async () => {
13447
+ const file = cursorFileFor(repoRoot, tool, sessionId);
13448
+ await fs13.promises.mkdir(stateDir2(), { recursive: true, mode: 448 });
13449
+ const tmp = `${file}.tmp`;
13450
+ await fs13.promises.writeFile(tmp, JSON.stringify(cursor, null, 2), {
13451
+ mode: 384
13452
+ });
13453
+ await fs13.promises.rename(tmp, file);
13291
13454
  });
13292
- await fs12.promises.rename(tmp, file);
13293
13455
  }
13294
13456
  async function setUploadSessionEndedAt(repoRoot, tool, sessionId, sessionEndedAt) {
13295
13457
  const state = await readUploadState(repoRoot, tool, sessionId);
@@ -13297,7 +13459,7 @@ async function setUploadSessionEndedAt(repoRoot, tool, sessionId, sessionEndedAt
13297
13459
  await writeUploadState({ ...state, sessionEndedAt });
13298
13460
  const now = /* @__PURE__ */ new Date();
13299
13461
  try {
13300
- await fs12.promises.utimes(
13462
+ await fs13.promises.utimes(
13301
13463
  cursorFileFor(repoRoot, tool, sessionId),
13302
13464
  now,
13303
13465
  now
@@ -13306,14 +13468,23 @@ async function setUploadSessionEndedAt(repoRoot, tool, sessionId, sessionEndedAt
13306
13468
  }
13307
13469
  return true;
13308
13470
  }
13309
- async function acquireLock2(repoRoot, tool, sessionId, retries = lockRetries(), delayMs = lockRetryDelayMs()) {
13471
+ async function acquireLock2(repoRoot, tool, sessionId, retries, delayMs = lockRetryDelayMs(), options = {}) {
13310
13472
  const lockPath = lockFileFor(repoRoot, tool, sessionId);
13311
- await fs12.promises.mkdir(stateDir2(), { recursive: true, mode: 448 });
13312
- for (let i = 0; i < retries; i++) {
13473
+ const maxRetries = retries ?? lockRetries();
13474
+ const maxWaitMs = options.liveOwnerMaxWaitMs ?? (retries !== void 0 || process.env.HILLCLIMB_UPLOAD_LOCK_WAIT_MS !== void 0 ? 0 : readPositiveEnvMs(
13475
+ "HILLCLIMB_UPLOAD_LOCK_LIVE_OWNER_MAX_WAIT_MS",
13476
+ DEFAULT_LIVE_OWNER_MAX_WAIT_MS
13477
+ ));
13478
+ const started = Date.now();
13479
+ let extended = false;
13480
+ let attempts = 0;
13481
+ await fs13.promises.mkdir(stateDir2(), { recursive: true, mode: 448 });
13482
+ for (; ; ) {
13483
+ attempts++;
13313
13484
  try {
13314
- const fd = await fs12.promises.open(
13485
+ const fd = await fs13.promises.open(
13315
13486
  lockPath,
13316
- fs12.constants.O_CREAT | fs12.constants.O_EXCL | fs12.constants.O_WRONLY
13487
+ fs13.constants.O_CREAT | fs13.constants.O_EXCL | fs13.constants.O_WRONLY
13317
13488
  );
13318
13489
  try {
13319
13490
  await fd.write(String(process.pid));
@@ -13322,31 +13493,43 @@ async function acquireLock2(repoRoot, tool, sessionId, retries = lockRetries(),
13322
13493
  }
13323
13494
  return;
13324
13495
  } catch (err) {
13325
- if (err.code === "EEXIST" && i < retries - 1) {
13326
- if (await reapLockIfStale(lockPath, {
13327
- maxAgeMs: STALE_LOCK_TTL_MS2
13328
- })) {
13329
- continue;
13330
- }
13331
- await new Promise((r) => setTimeout(r, delayMs));
13496
+ if (err.code !== "EEXIST") throw err;
13497
+ if (await reapLockIfStale(lockPath, {
13498
+ maxAgeMs: Math.max(staleLockTtlMs(), maxWaitMs),
13499
+ preserveLiveOwner: true
13500
+ }))
13332
13501
  continue;
13502
+ if (attempts >= maxRetries) {
13503
+ if (Date.now() - started >= maxWaitMs) break;
13504
+ const owner = await probeLockOwner(lockPath);
13505
+ if (!owner) continue;
13506
+ if (!owner.alive) break;
13507
+ if (!extended) {
13508
+ appendLog(
13509
+ "info",
13510
+ `agent-upload: extending lock wait for live owner (pid=${owner.pid ?? "unwritten"}, maxWaitMs=${maxWaitMs})`
13511
+ );
13512
+ extended = true;
13513
+ }
13333
13514
  }
13334
- if (err.code === "EEXIST") break;
13335
- throw err;
13515
+ await new Promise((r) => setTimeout(r, delayMs));
13336
13516
  }
13337
13517
  }
13338
13518
  throw new Error(
13339
- `Failed to acquire upload lock for ${tool} session ${sessionId} after ${retries} retries (${delayMs}ms delay, lock=${lockPath})`
13519
+ `Failed to acquire upload lock for ${tool} session ${sessionId} after ${attempts} retries (${Date.now() - started}ms elapsed, ${delayMs}ms delay, lock=${lockPath})`
13340
13520
  );
13341
13521
  }
13342
13522
  async function releaseLock2(repoRoot, tool, sessionId) {
13343
13523
  try {
13344
- await fs12.promises.unlink(lockFileFor(repoRoot, tool, sessionId));
13524
+ await fs13.promises.unlink(lockFileFor(repoRoot, tool, sessionId));
13345
13525
  } catch {
13346
13526
  }
13347
13527
  }
13348
13528
  async function withUploadLock(repoRoot, tool, sessionId, fn) {
13349
- await acquireLock2(repoRoot, tool, sessionId);
13529
+ await measureAgentStage(
13530
+ "lock-wait",
13531
+ () => acquireLock2(repoRoot, tool, sessionId)
13532
+ );
13350
13533
  try {
13351
13534
  return await fn();
13352
13535
  } finally {
@@ -13356,30 +13539,37 @@ async function withUploadLock(repoRoot, tool, sessionId, fn) {
13356
13539
  async function sweepStaleUploadStates(ttlMs = stateTtlMs(), now = Date.now()) {
13357
13540
  let entries;
13358
13541
  try {
13359
- entries = await fs12.promises.readdir(stateDir2(), { withFileTypes: true });
13542
+ entries = await fs13.promises.readdir(stateDir2(), { withFileTypes: true });
13360
13543
  } catch {
13361
13544
  return;
13362
13545
  }
13363
13546
  for (const entry of entries) {
13364
13547
  if (!entry.isFile()) continue;
13365
- const file = path12.join(stateDir2(), entry.name);
13548
+ const file = path13.join(stateDir2(), entry.name);
13366
13549
  try {
13367
13550
  if (entry.name.endsWith(".lock")) {
13368
13551
  await reapLockIfStale(file, {
13369
- maxAgeMs: STALE_LOCK_TTL_MS2,
13552
+ maxAgeMs: staleLockTtlMs(),
13553
+ preserveLiveOwner: true,
13370
13554
  now
13371
13555
  });
13372
13556
  continue;
13373
13557
  }
13374
- const st = await fs12.promises.stat(file);
13558
+ const st = await fs13.promises.stat(file);
13375
13559
  if (now - st.mtimeMs > ttlMs) {
13376
- await fs12.promises.unlink(file);
13560
+ await fs13.promises.unlink(file);
13377
13561
  }
13378
13562
  } catch {
13379
13563
  }
13380
13564
  }
13381
13565
  }
13382
13566
 
13567
+ // package.json
13568
+ var version = "0.8.11";
13569
+
13570
+ // src/version.ts
13571
+ var CLI_VERSION = version;
13572
+
13383
13573
  // src/commands/upload.ts
13384
13574
  async function readStdin() {
13385
13575
  if (process.stdin.isTTY) return "";
@@ -13399,7 +13589,7 @@ function lineHasAssistant(line) {
13399
13589
  return line.includes('"type":"assistant"') || line.includes('"role":"assistant"') || line.includes('"type":"assistant.');
13400
13590
  }
13401
13591
  async function hasAssistantMessage(transcriptPath) {
13402
- const stream = fs13.createReadStream(transcriptPath, { encoding: "utf-8" });
13592
+ const stream = fs14.createReadStream(transcriptPath, { encoding: "utf-8" });
13403
13593
  let buffer = "";
13404
13594
  try {
13405
13595
  for await (const chunk of stream) {
@@ -13486,8 +13676,8 @@ function resolveCursorTranscriptPath(payload) {
13486
13676
  const workspace = payload.workspace_roots?.[0];
13487
13677
  if (!id || !workspace) return void 0;
13488
13678
  const encoded = workspace.replace(/^\//, "").replace(/\//g, "-");
13489
- return path13.join(
13490
- os6.homedir(),
13679
+ return path14.join(
13680
+ os7.homedir(),
13491
13681
  ".cursor",
13492
13682
  "projects",
13493
13683
  encoded,
@@ -13562,9 +13752,9 @@ async function runUploadInner(payload) {
13562
13752
  );
13563
13753
  return "skipped";
13564
13754
  }
13565
- const transcriptResolved = path13.resolve(transcriptPath);
13755
+ const transcriptResolved = path14.resolve(transcriptPath);
13566
13756
  try {
13567
- const stat = await fs13.promises.stat(transcriptResolved);
13757
+ const stat = await fs14.promises.stat(transcriptResolved);
13568
13758
  if (!stat.isFile()) {
13569
13759
  appendLog(
13570
13760
  "warn",
@@ -13607,7 +13797,6 @@ async function runUploadInner(payload) {
13607
13797
  return uploaded ? "uploaded" : "skipped";
13608
13798
  }
13609
13799
  var AGENT_MAX_UPLOAD_BYTES = 1024 * 1024 * 1024;
13610
- var CLI_VERSION = "0.8.0";
13611
13800
  function makeTranscriptGroup(repoRoot, sourceTool, transcriptPath, content) {
13612
13801
  const sourceFile = {
13613
13802
  sourceName: sourceTool,
@@ -13617,16 +13806,22 @@ function makeTranscriptGroup(repoRoot, sourceTool, transcriptPath, content) {
13617
13806
  };
13618
13807
  return {
13619
13808
  repoPath: repoRoot,
13620
- label: path13.basename(repoRoot),
13809
+ label: path14.basename(repoRoot),
13621
13810
  files: [sourceFile],
13622
13811
  sourceNames: [sourceTool],
13623
13812
  lastModified: /* @__PURE__ */ new Date()
13624
13813
  };
13625
13814
  }
13626
13815
  async function buildRedactChain(repoRoot) {
13627
- const envFileNames = await discoverEnvFiles(repoRoot);
13628
- const envFilePaths = envFileNames.map((n) => path13.join(repoRoot, n));
13629
- const secretResult = await collectSecrets(repoRoot, envFilePaths, []);
13816
+ const envFileNames = await measureAgentStage(
13817
+ "secret-discovery",
13818
+ () => discoverEnvFiles(repoRoot)
13819
+ );
13820
+ const envFilePaths = envFileNames.map((n) => path14.join(repoRoot, n));
13821
+ const secretResult = await measureAgentStage(
13822
+ "secret-collection",
13823
+ () => collectSecrets(repoRoot, envFilePaths, [])
13824
+ );
13630
13825
  const chain = [];
13631
13826
  if (secretResult.values.size > 0) {
13632
13827
  chain.push(new RedactMiddleware(secretResult.values));
@@ -13637,7 +13832,7 @@ async function buildRedactChain(repoRoot) {
13637
13832
  async function redactTail(tail, repoRoot, sourceTool, transcriptPath, redactChain) {
13638
13833
  let group = makeTranscriptGroup(repoRoot, sourceTool, transcriptPath, tail);
13639
13834
  for (const mw of redactChain) {
13640
- group = await mw.process(group);
13835
+ group = await measureAgentStage(mw.name, () => mw.process(group));
13641
13836
  }
13642
13837
  const file = group.files.find((f) => f.absolutePath === transcriptPath);
13643
13838
  if (!file?.content) {
@@ -13702,8 +13897,14 @@ async function uploadSession(args) {
13702
13897
  const isSessionEnd = eventKind2 === "sessionEnd";
13703
13898
  const eventLabel = isSessionEnd ? "SessionEnd" : "Stop";
13704
13899
  return await withUploadLock(repoRoot, sourceTool, sessionId, async () => {
13705
- const prior = await readUploadState(repoRoot, sourceTool, sessionId);
13706
- const lineageDetection = sourceTool === "codex" && prior?.codexLineageChecked !== true ? await detectCodexLineage(transcriptPath) : void 0;
13900
+ const prior = await measureAgentStage(
13901
+ "state-read",
13902
+ () => readUploadState(repoRoot, sourceTool, sessionId)
13903
+ );
13904
+ const lineageDetection = sourceTool === "codex" && prior?.codexLineageChecked !== true ? await measureAgentStage(
13905
+ "lineage-detection",
13906
+ () => detectCodexLineage(transcriptPath)
13907
+ ) : void 0;
13707
13908
  const codexLineage = sourceTool === "codex" ? mergeCodexLineage(prior?.codexLineage, lineageDetection?.lineage) : void 0;
13708
13909
  const codexLineageChecked = sourceTool === "codex" ? prior?.codexLineageChecked === true || lineageDetection?.conclusive === true : void 0;
13709
13910
  const lineageNeedsStamp = codexLineage !== void 0 && JSON.stringify(codexLineage) !== JSON.stringify(prior?.codexLineageStamped);
@@ -13715,6 +13916,7 @@ async function uploadSession(args) {
13715
13916
  });
13716
13917
  }
13717
13918
  let cursor = null;
13919
+ let snapshotReason = !prior ? "first-upload" : !prior.contributionId ? "missing-contribution" : prior.snapshotUploaded === false || prior.uploadCount === 0 ? "incomplete-baseline" : "missing-cursor-fields";
13718
13920
  let adoptedLegacy = false;
13719
13921
  let restored = null;
13720
13922
  if (prior?.contributionId && prior.snapshotUploaded && prior.rawByteOffset !== void 0 && prior.rawPrefixSha256 !== void 0) {
@@ -13725,6 +13927,7 @@ async function uploadSession(args) {
13725
13927
  } else if (prior?.contributionId && prior.rawByteOffset === void 0) {
13726
13928
  const sidecar = await readCursorState(repoRoot, sourceTool, sessionId);
13727
13929
  if (sidecar && sidecar.contributionId !== prior.contributionId) {
13930
+ snapshotReason = "sidecar-contribution-mismatch";
13728
13931
  appendLog(
13729
13932
  "info",
13730
13933
  `[${sessionId}] ignoring sidecar cursor for stale contribution ${sidecar.contributionId} (state has ${prior.contributionId})`
@@ -13745,7 +13948,10 @@ async function uploadSession(args) {
13745
13948
  rawByteOffset: prior.lastTranscriptSize,
13746
13949
  rawPrefixSha256: prior.lastTranscriptSha256
13747
13950
  };
13748
- if (await cursorMatchesFile(transcriptPath, candidate)) {
13951
+ if (await measureAgentStage(
13952
+ "legacy-cursor-validation",
13953
+ () => cursorMatchesFile(transcriptPath, candidate)
13954
+ )) {
13749
13955
  cursor = candidate;
13750
13956
  adoptedLegacy = true;
13751
13957
  appendLog(
@@ -13753,6 +13959,7 @@ async function uploadSession(args) {
13753
13959
  `[${sessionId}] adopting last uploaded full zip as epoch-001 baseline (legacy state bootstrap, offset=${candidate.rawByteOffset})`
13754
13960
  );
13755
13961
  } else {
13962
+ snapshotReason = "legacy-prefix-mismatch-or-incomplete-line";
13756
13963
  appendLog(
13757
13964
  "info",
13758
13965
  `[${sessionId}] legacy state present but prefix mismatch or mid-line offset (offset=${candidate.rawByteOffset}) \u2014 will re-baseline with a fresh snapshot`
@@ -13765,13 +13972,18 @@ async function uploadSession(args) {
13765
13972
  let tail;
13766
13973
  let nextCursor;
13767
13974
  if (cursor) {
13768
- const evaluation = await evaluateTranscript(transcriptPath, cursor);
13975
+ const currentCursor = cursor;
13976
+ const evaluation = await measureAgentStage(
13977
+ "cursor-validation-and-tail-read",
13978
+ () => evaluateTranscript(transcriptPath, currentCursor)
13979
+ );
13769
13980
  if (evaluation.kind === "unchanged") {
13770
13981
  if (lineageNeedsStamp && prior?.contributionId && prior.epochMeta) {
13771
13982
  mode = "meta-refresh";
13772
13983
  } else if (lineageNeedsStamp) {
13773
13984
  mode = "snapshot";
13774
13985
  transition = "state-lost";
13986
+ snapshotReason = "lineage-metadata-unavailable";
13775
13987
  } else {
13776
13988
  appendLog(
13777
13989
  "info",
@@ -13801,6 +14013,7 @@ async function uploadSession(args) {
13801
14013
  } else {
13802
14014
  mode = "snapshot";
13803
14015
  transition = evaluation.kind;
14016
+ snapshotReason = evaluation.kind;
13804
14017
  }
13805
14018
  } else {
13806
14019
  mode = "snapshot";
@@ -13935,7 +14148,7 @@ async function uploadSession(args) {
13935
14148
  contributionId2,
13936
14149
  patchFilename(epoch2, turn, recordedAt),
13937
14150
  "application/gzip",
13938
- gzipPatch(redactedTail),
14151
+ measureAgentSyncStage("patch-gzip", () => gzipPatch(redactedTail)),
13939
14152
  AGENT_MAX_UPLOAD_BYTES
13940
14153
  );
13941
14154
  } catch (err) {
@@ -14063,7 +14276,10 @@ async function uploadSession(args) {
14063
14276
  }
14064
14277
  let raw;
14065
14278
  try {
14066
- raw = await fs13.promises.readFile(transcriptPath);
14279
+ raw = await measureAgentStage(
14280
+ "transcript-read",
14281
+ () => fs14.promises.readFile(transcriptPath)
14282
+ );
14067
14283
  } catch (err) {
14068
14284
  if (err.code === "ERR_FS_FILE_TOO_LARGE") {
14069
14285
  appendLog(
@@ -14078,7 +14294,10 @@ async function uploadSession(args) {
14078
14294
  }
14079
14295
  return false;
14080
14296
  }
14081
- const truncated = truncateAtLastNewline(raw);
14297
+ const truncated = measureAgentSyncStage(
14298
+ "snapshot-cursor-hash",
14299
+ () => truncateAtLastNewline(raw)
14300
+ );
14082
14301
  if (!truncated) {
14083
14302
  appendLog(
14084
14303
  "info",
@@ -14168,7 +14387,7 @@ Uploaded: ${now.toISOString()}`;
14168
14387
  const reuseDesc = prior?.contributionId ? `reusing contribution ${prior.contributionId}` : prior ? "new contribution (prior state had no contribution \u2014 earlier create may have failed)" : "new contribution (first upload for session)";
14169
14388
  appendLog(
14170
14389
  "info",
14171
- `[${sessionId}] ${sourceTool} ${eventLabel} upload \u2192 snapshot epoch=${epoch} transition=${transition} (${truncated.covered.length} raw bytes, ${reuseDesc})`
14390
+ `[${sessionId}] ${sourceTool} ${eventLabel} upload \u2192 snapshot epoch=${epoch} transition=${transition} reason=${snapshotReason} (${truncated.covered.length} raw bytes, ${reuseDesc})`
14172
14391
  );
14173
14392
  let contributionId;
14174
14393
  try {
@@ -14335,7 +14554,10 @@ async function runUpload() {
14335
14554
  }
14336
14555
  async function runUploadWorker() {
14337
14556
  setLogPrefix(`[${process.env[FLOW_ID_ENV] ?? newFlowId()}]`);
14338
- appendLog("info", `upload worker started (pid ${process.pid})`);
14557
+ appendLog(
14558
+ "info",
14559
+ `upload worker started (pid ${process.pid}, cliVersion=${CLI_VERSION})`
14560
+ );
14339
14561
  let raw;
14340
14562
  try {
14341
14563
  raw = await readStdin();
@@ -14393,28 +14615,28 @@ import crypto7 from "crypto";
14393
14615
 
14394
14616
  // src/git-traces/handlers.ts
14395
14617
  import { execFileSync as execFileSync3 } from "child_process";
14396
- import fs16 from "fs";
14397
- import path16 from "path";
14618
+ import fs17 from "fs";
14619
+ import path17 from "path";
14398
14620
 
14399
14621
  // src/git-traces/git-ops.ts
14400
14622
  import { execFileSync as execFileSync2, spawn as spawn3, spawnSync } from "child_process";
14401
- import fs14 from "fs";
14402
- import os7 from "os";
14403
- import path14 from "path";
14623
+ import fs15 from "fs";
14624
+ import os8 from "os";
14625
+ import path15 from "path";
14404
14626
  import { createGzip } from "zlib";
14405
14627
 
14406
14628
  // src/git-traces/timing.ts
14407
- import { performance } from "perf_hooks";
14629
+ import { performance as performance2 } from "perf_hooks";
14408
14630
  function startCaptureStage(stage, context) {
14409
14631
  appendLog(
14410
14632
  "info",
14411
14633
  `git-traces: capture timing (${context}, stage=${stage}, status=started)`
14412
14634
  );
14413
- const started = performance.now();
14635
+ const started = performance2.now();
14414
14636
  return (status) => {
14415
14637
  appendLog(
14416
14638
  "info",
14417
- `git-traces: capture timing (${context}, stage=${stage}, status=${status}, elapsedMs=${Math.round(performance.now() - started)})`
14639
+ `git-traces: capture timing (${context}, stage=${stage}, status=${status}, elapsedMs=${Math.round(performance2.now() - started)})`
14418
14640
  );
14419
14641
  };
14420
14642
  }
@@ -14432,12 +14654,13 @@ function timeCaptureStage(repoRoot, stage, run) {
14432
14654
 
14433
14655
  // src/git-traces/git-ops.ts
14434
14656
  var DEFAULT_GIT_COMMAND_TIMEOUT_MS = 12e4;
14657
+ var DEFAULT_BUNDLE_TIMEOUT_MS = 3e5;
14435
14658
  var FULL_TREE_SCAN_TIMEOUT_MS = 3e5;
14436
- function resolveGitCommandTimeoutMs() {
14659
+ function resolveGitCommandTimeoutMs(fallback = DEFAULT_GIT_COMMAND_TIMEOUT_MS) {
14437
14660
  const raw = process.env.HILLCLIMB_GIT_COMMAND_TIMEOUT_MS;
14438
- if (!raw) return DEFAULT_GIT_COMMAND_TIMEOUT_MS;
14661
+ if (!raw) return fallback;
14439
14662
  const parsed = Number(raw);
14440
- return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_GIT_COMMAND_TIMEOUT_MS;
14663
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
14441
14664
  }
14442
14665
  var GIT_COMMAND_TIMEOUT_MS = resolveGitCommandTimeoutMs();
14443
14666
  var EXEC_OPTS = {
@@ -14620,8 +14843,8 @@ var EXCLUDED_SNAPSHOT_BASENAMES = /* @__PURE__ */ new Set([
14620
14843
  ]);
14621
14844
  var EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
14622
14845
  function isExcludedSnapshotPath(filePath) {
14623
- if (EXCLUDED_SNAPSHOT_BASENAMES.has(path14.basename(filePath))) return true;
14624
- return EXCLUDED_SNAPSHOT_EXTENSIONS.has(path14.extname(filePath).toLowerCase());
14846
+ if (EXCLUDED_SNAPSHOT_BASENAMES.has(path15.basename(filePath))) return true;
14847
+ return EXCLUDED_SNAPSHOT_EXTENSIONS.has(path15.extname(filePath).toLowerCase());
14625
14848
  }
14626
14849
  function isBinaryBuffer(buffer) {
14627
14850
  return buffer.includes(0);
@@ -14642,16 +14865,16 @@ function readTreeBlobHead(repoRoot, sha) {
14642
14865
  function readWorkingFileHead(absPath) {
14643
14866
  let fd = null;
14644
14867
  try {
14645
- fd = fs14.openSync(absPath, "r");
14868
+ fd = fs15.openSync(absPath, "r");
14646
14869
  const buffer = Buffer.alloc(BINARY_SNIFF_BYTES);
14647
- const bytesRead = fs14.readSync(fd, buffer, 0, BINARY_SNIFF_BYTES, 0);
14870
+ const bytesRead = fs15.readSync(fd, buffer, 0, BINARY_SNIFF_BYTES, 0);
14648
14871
  return buffer.subarray(0, bytesRead);
14649
14872
  } catch {
14650
14873
  return null;
14651
14874
  } finally {
14652
14875
  if (fd !== null) {
14653
14876
  try {
14654
- fs14.closeSync(fd);
14877
+ fs15.closeSync(fd);
14655
14878
  } catch {
14656
14879
  }
14657
14880
  }
@@ -15059,8 +15282,8 @@ function filterOmittedFilesFromTree(repoRoot, treeSha, options = {}) {
15059
15282
  options.omittedFiles?.push(...omittedFiles);
15060
15283
  return timeCaptureStage(repoRoot, "tracked-tree-filter", () => {
15061
15284
  if (omittedFiles.length === 0) return treeSha;
15062
- const tmpIndex = path14.join(
15063
- os7.tmpdir(),
15285
+ const tmpIndex = path15.join(
15286
+ os8.tmpdir(),
15064
15287
  `hillclimb-filter-${Date.now()}-${process.pid}`
15065
15288
  );
15066
15289
  const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
@@ -15074,7 +15297,7 @@ function filterOmittedFilesFromTree(repoRoot, treeSha, options = {}) {
15074
15297
  return gitWithEnv(repoRoot, ["write-tree"], env);
15075
15298
  } finally {
15076
15299
  try {
15077
- fs14.unlinkSync(tmpIndex);
15300
+ fs15.unlinkSync(tmpIndex);
15078
15301
  } catch {
15079
15302
  }
15080
15303
  }
@@ -15098,8 +15321,8 @@ function buildUntrackedTree(repoRoot, options = {}) {
15098
15321
  if (!relPath) continue;
15099
15322
  candidates++;
15100
15323
  try {
15101
- const absPath = path14.join(repoRoot, relPath);
15102
- const stat = fs14.lstatSync(absPath);
15324
+ const absPath = path15.join(repoRoot, relPath);
15325
+ const stat = fs15.lstatSync(absPath);
15103
15326
  const reason = classifyOmission(
15104
15327
  relPath,
15105
15328
  stat.size,
@@ -15130,8 +15353,8 @@ function buildUntrackedTree(repoRoot, options = {}) {
15130
15353
  `git-traces: untracked inventory (repo=${repoRoot}, candidates=${candidates}, kept=${kept.length}, keptBytes=${keptBytes}, omitted=${omitted}, omittedBytes=${omittedBytes}, skipped=${skipped})`
15131
15354
  );
15132
15355
  if (kept.length === 0) return null;
15133
- const tmpIndex = path14.join(
15134
- os7.tmpdir(),
15356
+ const tmpIndex = path15.join(
15357
+ os8.tmpdir(),
15135
15358
  `hillclimb-untracked-${Date.now()}-${process.pid}`
15136
15359
  );
15137
15360
  const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
@@ -15151,7 +15374,7 @@ function buildUntrackedTree(repoRoot, options = {}) {
15151
15374
  );
15152
15375
  } finally {
15153
15376
  try {
15154
- fs14.unlinkSync(tmpIndex);
15377
+ fs15.unlinkSync(tmpIndex);
15155
15378
  } catch {
15156
15379
  }
15157
15380
  }
@@ -15181,14 +15404,38 @@ function buildSnapshotTree(repoRoot, stashSha, options = {}) {
15181
15404
  () => buildUntrackedTree(repoRoot, { omittedFiles, limits })
15182
15405
  );
15183
15406
  if (options.log !== false) logOmittedSnapshotSummary(omittedFiles);
15184
- const snapshotTree = timeCaptureStage(
15185
- repoRoot,
15186
- "tree-assembly",
15187
- () => mergeSnapshotTrees(repoRoot, filteredTrackedTree, untrackedTree)
15188
- );
15407
+ const snapshotTree = timeCaptureStage(repoRoot, "tree-assembly", () => {
15408
+ const previous = usableOmissionScan(options.previousScan, limits);
15409
+ if (untrackedTree && untrackedTree !== EMPTY_TREE_SHA && filteredTrackedTree !== EMPTY_TREE_SHA && previous?.assembly?.trackedTreeSha === filteredTrackedTree && previous.assembly.untrackedTreeSha === untrackedTree) {
15410
+ try {
15411
+ gitBuffer(repoRoot, [
15412
+ "rev-list",
15413
+ "--objects",
15414
+ "--missing=error",
15415
+ previous.snapshotTreeSha,
15416
+ "--"
15417
+ ]);
15418
+ appendLog(
15419
+ "info",
15420
+ `git-traces: tree assembly reused (repo=${repoRoot})`
15421
+ );
15422
+ return previous.snapshotTreeSha;
15423
+ } catch {
15424
+ appendLog(
15425
+ "info",
15426
+ `git-traces: tree assembly cache unavailable; rebuilding (repo=${repoRoot})`
15427
+ );
15428
+ }
15429
+ }
15430
+ return mergeSnapshotTrees(repoRoot, filteredTrackedTree, untrackedTree);
15431
+ });
15189
15432
  options.scanOut?.push({
15190
15433
  trackedTreeSha: trackedTree,
15191
15434
  snapshotTreeSha: snapshotTree,
15435
+ assembly: {
15436
+ trackedTreeSha: filteredTrackedTree,
15437
+ untrackedTreeSha: untrackedTree
15438
+ },
15192
15439
  limits,
15193
15440
  omittedFiles: trackedOmitted
15194
15441
  });
@@ -15197,8 +15444,9 @@ function buildSnapshotTree(repoRoot, stashSha, options = {}) {
15197
15444
  function mergeSnapshotTrees(repoRoot, filteredTrackedTree, untrackedTree) {
15198
15445
  if (!untrackedTree || untrackedTree === EMPTY_TREE_SHA)
15199
15446
  return filteredTrackedTree;
15200
- const tmpIndex = path14.join(
15201
- os7.tmpdir(),
15447
+ if (filteredTrackedTree === EMPTY_TREE_SHA) return untrackedTree;
15448
+ const tmpIndex = path15.join(
15449
+ os8.tmpdir(),
15202
15450
  `hillclimb-index-${Date.now()}-${process.pid}`
15203
15451
  );
15204
15452
  const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
@@ -15247,7 +15495,7 @@ function mergeSnapshotTrees(repoRoot, filteredTrackedTree, untrackedTree) {
15247
15495
  );
15248
15496
  } finally {
15249
15497
  try {
15250
- fs14.unlinkSync(tmpIndex);
15498
+ fs15.unlinkSync(tmpIndex);
15251
15499
  } catch {
15252
15500
  }
15253
15501
  }
@@ -15261,19 +15509,28 @@ function createBundleFromTree(repoRoot, treeSha, sessionId, label) {
15261
15509
  ]);
15262
15510
  const orphanRef = `refs/hillclimb/bundle/${sessionId}`;
15263
15511
  pinRef(repoRoot, orphanRef, orphanCommit);
15264
- const tmpFile = path14.join(
15265
- os7.tmpdir(),
15266
- // Include the pid (like the other temp files in this module) so concurrent
15267
- // git-traces workers — e.g. two sessions, or a parent + subagent — don't
15268
- // collide on the same `git bundle create` path and its `.lock`.
15269
- `hillclimb-bundle-${Date.now()}-${process.pid}.bundle`
15270
- );
15512
+ let tmpDir;
15271
15513
  try {
15272
- git(repoRoot, ["bundle", "create", tmpFile, orphanRef]);
15273
- return fs14.readFileSync(tmpFile);
15514
+ tmpDir = fs15.mkdtempSync(
15515
+ path15.join(os8.tmpdir(), `hillclimb-bundle-${process.pid}-`)
15516
+ );
15517
+ const tmpFile = path15.join(tmpDir, "baseline.bundle");
15518
+ timeCaptureStage(
15519
+ repoRoot,
15520
+ "bundle-create",
15521
+ () => gitBuffer(repoRoot, ["bundle", "create", tmpFile, orphanRef], {
15522
+ timeoutMs: resolveGitCommandTimeoutMs(DEFAULT_BUNDLE_TIMEOUT_MS)
15523
+ })
15524
+ );
15525
+ const bundle = fs15.readFileSync(tmpFile);
15526
+ appendLog(
15527
+ "info",
15528
+ `git-traces: bundle inventory (repo=${repoRoot}, bytes=${bundle.length})`
15529
+ );
15530
+ return bundle;
15274
15531
  } finally {
15275
15532
  try {
15276
- fs14.unlinkSync(tmpFile);
15533
+ if (tmpDir) fs15.rmSync(tmpDir, { recursive: true, force: true });
15277
15534
  } catch {
15278
15535
  }
15279
15536
  deleteRef(repoRoot, orphanRef);
@@ -15471,7 +15728,7 @@ function buildBaselineMetadata(repoRoot, sessionId, tool, baselineSha, cliVersio
15471
15728
  ...omittedFiles.length > 0 ? { omittedFiles } : {}
15472
15729
  },
15473
15730
  author: { name: authorName, email: authorEmail },
15474
- hostname: os7.hostname(),
15731
+ hostname: os8.hostname(),
15475
15732
  cliVersion,
15476
15733
  commits,
15477
15734
  ...lineage,
@@ -15563,9 +15820,9 @@ function parseCommitFiles(repoRoot, sha) {
15563
15820
  oldPath
15564
15821
  });
15565
15822
  } else {
15566
- const path23 = parts[parts.length - 1];
15567
- indexByPath.set(path23, files.length);
15568
- files.push({ path: path23, status, additions: 0, deletions: 0 });
15823
+ const path24 = parts[parts.length - 1];
15824
+ indexByPath.set(path24, files.length);
15825
+ files.push({ path: path24, status, additions: 0, deletions: 0 });
15569
15826
  }
15570
15827
  }
15571
15828
  for (const line of numstat.split("\n")) {
@@ -15645,14 +15902,14 @@ function countScopedTurnTreeRefs(repoRoot, sessionId, epochPrefix3) {
15645
15902
 
15646
15903
  // src/git-traces/session-state.ts
15647
15904
  import crypto6 from "crypto";
15648
- import fs15 from "fs";
15649
- import os8 from "os";
15650
- import path15 from "path";
15905
+ import fs16 from "fs";
15906
+ import os9 from "os";
15907
+ import path16 from "path";
15651
15908
  var CURRENT_SCHEMA_VERSION3 = 3;
15652
- var DEFAULT_STATE_DIR2 = path15.join(os8.homedir(), ".hillclimb", "git-traces");
15909
+ var DEFAULT_STATE_DIR2 = path16.join(os9.homedir(), ".hillclimb", "git-traces");
15653
15910
  var LOCK_RETRIES2 = 120;
15654
15911
  var LOCK_RETRY_DELAY_MS2 = 500;
15655
- var DEFAULT_LIVE_OWNER_MAX_WAIT_MS = 10 * 60 * 1e3;
15912
+ var DEFAULT_LIVE_OWNER_MAX_WAIT_MS2 = 10 * 60 * 1e3;
15656
15913
  var STALE_LOCK_TTL_MS3 = 60 * 60 * 1e3;
15657
15914
  var LockContentionError = class extends Error {
15658
15915
  constructor(lockPath, options) {
@@ -15665,25 +15922,25 @@ var LockContentionError = class extends Error {
15665
15922
  };
15666
15923
  function liveOwnerMaxWaitMs() {
15667
15924
  const raw = process.env.HILLCLIMB_GIT_TRACES_LOCK_LIVE_OWNER_MAX_WAIT_MS;
15668
- if (!raw) return DEFAULT_LIVE_OWNER_MAX_WAIT_MS;
15925
+ if (!raw) return DEFAULT_LIVE_OWNER_MAX_WAIT_MS2;
15669
15926
  const parsed = Number(raw);
15670
- return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_LIVE_OWNER_MAX_WAIT_MS;
15927
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_LIVE_OWNER_MAX_WAIT_MS2;
15671
15928
  }
15672
15929
  function stateDir3() {
15673
15930
  return process.env.HILLCLIMB_GIT_TRACES_STATE_DIR ?? DEFAULT_STATE_DIR2;
15674
15931
  }
15675
15932
  function stateFileForRepo(repoRoot, tool, sessionId) {
15676
15933
  const hash = crypto6.createHash("sha256").update(
15677
- sessionId ? `${path15.resolve(repoRoot)}\0${tool}\0${sessionId}` : `${path15.resolve(repoRoot)}\0${tool}`
15934
+ sessionId ? `${path16.resolve(repoRoot)}\0${tool}\0${sessionId}` : `${path16.resolve(repoRoot)}\0${tool}`
15678
15935
  ).digest("hex").slice(0, 16);
15679
- return path15.join(stateDir3(), `${hash}.json`);
15936
+ return path16.join(stateDir3(), `${hash}.json`);
15680
15937
  }
15681
15938
  function lockFileForRepo(repoRoot, tool, sessionId) {
15682
15939
  return `${stateFileForRepo(repoRoot, tool, sessionId)}.lock`;
15683
15940
  }
15684
15941
  async function readStateFile(file) {
15685
15942
  try {
15686
- const raw = await fs15.promises.readFile(file, "utf-8");
15943
+ const raw = await fs16.promises.readFile(file, "utf-8");
15687
15944
  const parsed = JSON.parse(raw);
15688
15945
  if (parsed.schemaVersion !== CURRENT_SCHEMA_VERSION3) {
15689
15946
  return null;
@@ -15697,7 +15954,7 @@ async function readStoredStateFile(file) {
15697
15954
  const state = await readStateFile(file);
15698
15955
  if (!state) return null;
15699
15956
  try {
15700
- return { state, mtimeMs: (await fs15.promises.stat(file)).mtimeMs };
15957
+ return { state, mtimeMs: (await fs16.promises.stat(file)).mtimeMs };
15701
15958
  } catch {
15702
15959
  return null;
15703
15960
  }
@@ -15705,26 +15962,26 @@ async function readStoredStateFile(file) {
15705
15962
  async function listScopedSessionStates(repoRoot, tool) {
15706
15963
  let entries;
15707
15964
  try {
15708
- entries = await fs15.promises.readdir(stateDir3(), { withFileTypes: true });
15965
+ entries = await fs16.promises.readdir(stateDir3(), { withFileTypes: true });
15709
15966
  } catch {
15710
15967
  return [];
15711
15968
  }
15712
15969
  const states = [];
15713
15970
  for (const entry of entries) {
15714
15971
  if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
15715
- const file = path15.join(stateDir3(), entry.name);
15972
+ const file = path16.join(stateDir3(), entry.name);
15716
15973
  const state = await readStateFile(file);
15717
15974
  if (!state) continue;
15718
15975
  if (typeof state.repoRoot !== "string" || typeof state.sessionId !== "string") {
15719
15976
  continue;
15720
15977
  }
15721
- if (path15.resolve(state.repoRoot) !== path15.resolve(repoRoot)) continue;
15722
- if (path15.resolve(file) !== path15.resolve(stateFileForRepo(repoRoot, tool, state.sessionId))) {
15978
+ if (path16.resolve(state.repoRoot) !== path16.resolve(repoRoot)) continue;
15979
+ if (path16.resolve(file) !== path16.resolve(stateFileForRepo(repoRoot, tool, state.sessionId))) {
15723
15980
  continue;
15724
15981
  }
15725
15982
  let mtimeMs = 0;
15726
15983
  try {
15727
- mtimeMs = (await fs15.promises.stat(file)).mtimeMs;
15984
+ mtimeMs = (await fs16.promises.stat(file)).mtimeMs;
15728
15985
  } catch {
15729
15986
  continue;
15730
15987
  }
@@ -15735,26 +15992,26 @@ async function listScopedSessionStates(repoRoot, tool) {
15735
15992
  async function listSessionStatesForSession(tool, sessionId) {
15736
15993
  let entries;
15737
15994
  try {
15738
- entries = await fs15.promises.readdir(stateDir3(), { withFileTypes: true });
15995
+ entries = await fs16.promises.readdir(stateDir3(), { withFileTypes: true });
15739
15996
  } catch {
15740
15997
  return [];
15741
15998
  }
15742
15999
  const states = [];
15743
16000
  for (const entry of entries) {
15744
16001
  if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
15745
- const file = path15.join(stateDir3(), entry.name);
16002
+ const file = path16.join(stateDir3(), entry.name);
15746
16003
  const state = await readStateFile(file);
15747
16004
  if (!state) continue;
15748
16005
  if (typeof state.repoRoot !== "string" || typeof state.sessionId !== "string") {
15749
16006
  continue;
15750
16007
  }
15751
16008
  if (state.sessionId !== sessionId) continue;
15752
- if (path15.resolve(file) !== path15.resolve(stateFileForRepo(state.repoRoot, tool, sessionId))) {
16009
+ if (path16.resolve(file) !== path16.resolve(stateFileForRepo(state.repoRoot, tool, sessionId))) {
15753
16010
  continue;
15754
16011
  }
15755
16012
  let mtimeMs = 0;
15756
16013
  try {
15757
- mtimeMs = (await fs15.promises.stat(file)).mtimeMs;
16014
+ mtimeMs = (await fs16.promises.stat(file)).mtimeMs;
15758
16015
  } catch {
15759
16016
  continue;
15760
16017
  }
@@ -15795,17 +16052,17 @@ async function writeLegacySessionState(state, tool) {
15795
16052
  await writeStateFile(stateFileForRepo(state.repoRoot, tool), state);
15796
16053
  }
15797
16054
  async function writeStateFile(file, state) {
15798
- await fs15.promises.mkdir(stateDir3(), { recursive: true, mode: 448 });
16055
+ await fs16.promises.mkdir(stateDir3(), { recursive: true, mode: 448 });
15799
16056
  const tmp = `${file}.tmp`;
15800
- await fs15.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
16057
+ await fs16.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
15801
16058
  mode: 384
15802
16059
  });
15803
- await fs15.promises.rename(tmp, file);
16060
+ await fs16.promises.rename(tmp, file);
15804
16061
  }
15805
16062
  async function touchSessionState(repoRoot, tool, sessionId) {
15806
16063
  const now = /* @__PURE__ */ new Date();
15807
16064
  try {
15808
- await fs15.promises.utimes(
16065
+ await fs16.promises.utimes(
15809
16066
  stateFileForRepo(repoRoot, tool, sessionId),
15810
16067
  now,
15811
16068
  now
@@ -15815,7 +16072,7 @@ async function touchSessionState(repoRoot, tool, sessionId) {
15815
16072
  }
15816
16073
  async function statSessionStateMtime(repoRoot, tool, sessionId) {
15817
16074
  try {
15818
- const stat = await fs15.promises.stat(
16075
+ const stat = await fs16.promises.stat(
15819
16076
  stateFileForRepo(repoRoot, tool, sessionId)
15820
16077
  );
15821
16078
  return stat.mtimeMs;
@@ -15825,7 +16082,7 @@ async function statSessionStateMtime(repoRoot, tool, sessionId) {
15825
16082
  }
15826
16083
  async function deleteStateFile(file) {
15827
16084
  try {
15828
- await fs15.promises.unlink(file);
16085
+ await fs16.promises.unlink(file);
15829
16086
  } catch {
15830
16087
  }
15831
16088
  }
@@ -15864,7 +16121,7 @@ async function acquireLock3(repoRoot, tool, sessionId, retries, delayMs, options
15864
16121
  }
15865
16122
  async function acquireLockWithoutTiming(repoRoot, tool, sessionId, retries, delayMs, options = {}) {
15866
16123
  const lockPath = lockFileForRepo(repoRoot, tool, sessionId);
15867
- await fs15.promises.mkdir(stateDir3(), { recursive: true, mode: 448 });
16124
+ await fs16.promises.mkdir(stateDir3(), { recursive: true, mode: 448 });
15868
16125
  const bounded = retries !== void 0 || delayMs !== void 0;
15869
16126
  const attempts = retries ?? LOCK_RETRIES2;
15870
16127
  const delay = delayMs ?? LOCK_RETRY_DELAY_MS2;
@@ -15874,9 +16131,9 @@ async function acquireLockWithoutTiming(repoRoot, tool, sessionId, retries, dela
15874
16131
  let extensionLogged = false;
15875
16132
  for (let i = 0; ; i++) {
15876
16133
  try {
15877
- const fd = await fs15.promises.open(
16134
+ const fd = await fs16.promises.open(
15878
16135
  lockPath,
15879
- fs15.constants.O_CREAT | fs15.constants.O_EXCL | fs15.constants.O_WRONLY
16136
+ fs16.constants.O_CREAT | fs16.constants.O_EXCL | fs16.constants.O_WRONLY
15880
16137
  );
15881
16138
  await fd.write(String(process.pid));
15882
16139
  await fd.close();
@@ -15885,7 +16142,7 @@ async function acquireLockWithoutTiming(repoRoot, tool, sessionId, retries, dela
15885
16142
  if (err.code !== "EEXIST") throw err;
15886
16143
  let regularLockFile = false;
15887
16144
  try {
15888
- regularLockFile = (await fs15.promises.lstat(lockPath)).isFile();
16145
+ regularLockFile = (await fs16.promises.lstat(lockPath)).isFile();
15889
16146
  } catch (statErr) {
15890
16147
  if (statErr.code === "ENOENT") {
15891
16148
  i--;
@@ -15936,21 +16193,21 @@ async function acquireLockWithoutTiming(repoRoot, tool, sessionId, retries, dela
15936
16193
  }
15937
16194
  async function releaseLock3(repoRoot, tool, sessionId) {
15938
16195
  try {
15939
- await fs15.promises.unlink(lockFileForRepo(repoRoot, tool, sessionId));
16196
+ await fs16.promises.unlink(lockFileForRepo(repoRoot, tool, sessionId));
15940
16197
  } catch {
15941
16198
  }
15942
16199
  }
15943
16200
  async function sweepStaleLockFiles(ttlMs = STALE_LOCK_TTL_MS3, now = Date.now()) {
15944
16201
  let entries;
15945
16202
  try {
15946
- entries = await fs15.promises.readdir(stateDir3(), { withFileTypes: true });
16203
+ entries = await fs16.promises.readdir(stateDir3(), { withFileTypes: true });
15947
16204
  } catch {
15948
16205
  return 0;
15949
16206
  }
15950
16207
  let removed = 0;
15951
16208
  for (const entry of entries) {
15952
16209
  if (!entry.isFile() || !entry.name.endsWith(".lock")) continue;
15953
- const file = path15.join(stateDir3(), entry.name);
16210
+ const file = path16.join(stateDir3(), entry.name);
15954
16211
  if (await reapLockIfStale(file, {
15955
16212
  maxAgeMs: ttlMs,
15956
16213
  now
@@ -15962,7 +16219,6 @@ async function sweepStaleLockFiles(ttlMs = STALE_LOCK_TTL_MS3, now = Date.now())
15962
16219
  }
15963
16220
 
15964
16221
  // src/git-traces/handlers.ts
15965
- var CLI_VERSION2 = "0.8.0";
15966
16222
  var GIT_TRACES_SLUG = "git-traces";
15967
16223
  var MAX_UPLOAD_BYTES = 500 * 1024 * 1024;
15968
16224
  var STALE_SCOPED_STATE_TTL_MS = 24 * 60 * 60 * 1e3;
@@ -15980,12 +16236,12 @@ var TOOL_LABELS = {
15980
16236
  async function loadConfiguredRepos() {
15981
16237
  const file = await loadProjects();
15982
16238
  return Object.entries(file.projects).map(([repoRoot, config]) => ({
15983
- repoRoot: path16.resolve(repoRoot),
16239
+ repoRoot: path17.resolve(repoRoot),
15984
16240
  config
15985
16241
  })).sort((a, b) => a.repoRoot.localeCompare(b.repoRoot));
15986
16242
  }
15987
16243
  function repoLabel(repoRoot) {
15988
- return path16.basename(repoRoot) || repoRoot;
16244
+ return path17.basename(repoRoot) || repoRoot;
15989
16245
  }
15990
16246
  function resolveCwd2(payload) {
15991
16247
  return resolveHookCwd(payload);
@@ -16038,7 +16294,8 @@ async function uploadFile(client, contributionId, filename, mimeType, buffer) {
16038
16294
  await client.uploadToPresignedUrl(
16039
16295
  presigned.presignedUrl,
16040
16296
  presigned.headers,
16041
- buffer
16297
+ buffer,
16298
+ { contributionId, uploadId: presigned.upload.id }
16042
16299
  );
16043
16300
  appendLog(
16044
16301
  "info",
@@ -16173,7 +16430,7 @@ function freezeEpochBaseline(params) {
16173
16430
  sessionId,
16174
16431
  tool,
16175
16432
  baselineSha,
16176
- CLI_VERSION2,
16433
+ CLI_VERSION,
16177
16434
  epoch,
16178
16435
  prevHeadSha,
16179
16436
  transitionKind,
@@ -16264,7 +16521,7 @@ function buildEpochBaselineArtifacts(params) {
16264
16521
  sessionId,
16265
16522
  tool,
16266
16523
  baselineSha,
16267
- CLI_VERSION2,
16524
+ CLI_VERSION,
16268
16525
  epoch,
16269
16526
  prevHeadSha,
16270
16527
  transitionKind,
@@ -17013,7 +17270,7 @@ async function recoverFromWedgedDiff(params) {
17013
17270
  state.sessionId,
17014
17271
  tool,
17015
17272
  currentSha,
17016
- CLI_VERSION2,
17273
+ CLI_VERSION,
17017
17274
  state.epoch,
17018
17275
  null,
17019
17276
  "initial",
@@ -17290,7 +17547,8 @@ async function processStopRepo(repo, tool, sessionId, recordedAt, lineageCheck,
17290
17547
  });
17291
17548
  return outcome === "uploaded" ? await finishSuccessfulStop("uploaded") : outcome;
17292
17549
  }
17293
- if (!artifacts2 || !canUploadEpochBaselineArtifacts(1, artifacts2)) {
17550
+ if (!artifacts2) return "failed";
17551
+ if (!canUploadEpochBaselineArtifacts(1, artifacts2)) {
17294
17552
  return "skipped";
17295
17553
  }
17296
17554
  const registered = await registerInitialContribution({
@@ -17484,7 +17742,8 @@ async function processStopRepo(repo, tool, sessionId, recordedAt, lineageCheck,
17484
17742
  missingObjectRecovery(state, "stop-diff")
17485
17743
  );
17486
17744
  }
17487
- if (!artifacts || !canUploadEpochBaselineArtifacts(1, artifacts)) {
17745
+ if (!artifacts) return "failed";
17746
+ if (!canUploadEpochBaselineArtifacts(1, artifacts)) {
17488
17747
  return "skipped";
17489
17748
  }
17490
17749
  const registered = await registerInitialContribution({
@@ -17601,7 +17860,7 @@ async function collectStopTargets(tool, sessionId, repos, repoByRoot) {
17601
17860
  let needsLineageCheck = false;
17602
17861
  const storedStates = await listSessionStatesForSession(tool, sessionId);
17603
17862
  for (const { state } of storedStates) {
17604
- const repo = repoByRoot.get(path16.resolve(state.repoRoot));
17863
+ const repo = repoByRoot.get(path17.resolve(state.repoRoot));
17605
17864
  if (!repo) {
17606
17865
  missingConfig++;
17607
17866
  appendLog(
@@ -17633,7 +17892,7 @@ async function lateInitSkipReason(payload, tool) {
17633
17892
  const transcriptPath = payload.transcript_path;
17634
17893
  if (!transcriptPath) return tool === "codex" ? null : "no-transcript-path";
17635
17894
  try {
17636
- const stat = await fs16.promises.stat(path16.resolve(transcriptPath));
17895
+ const stat = await fs17.promises.stat(path17.resolve(transcriptPath));
17637
17896
  return stat.isFile() ? null : "transcript-not-a-file";
17638
17897
  } catch {
17639
17898
  return "transcript-missing";
@@ -17750,9 +18009,9 @@ async function handleSessionEnd(payload, tool) {
17750
18009
  if (sessionId) {
17751
18010
  const states = await listSessionStatesForSession(tool, sessionId);
17752
18011
  for (const { state } of states) {
17753
- const repoRoot = path16.resolve(state.repoRoot);
18012
+ const repoRoot = path17.resolve(state.repoRoot);
17754
18013
  repoRoots.add(repoRoot);
17755
- const canProcess = repoByRoot.has(repoRoot) || project && path16.resolve(project.repoRoot) === repoRoot;
18014
+ const canProcess = repoByRoot.has(repoRoot) || project && path17.resolve(project.repoRoot) === repoRoot;
17756
18015
  if (canProcess && tool === "codex" && state.codexLineageChecked !== true) {
17757
18016
  needsLineageCheck = true;
17758
18017
  }
@@ -17786,7 +18045,7 @@ async function handleSessionEnd(payload, tool) {
17786
18045
  let skipped = 0;
17787
18046
  let failed = 0;
17788
18047
  for (const repoRoot of repoRoots) {
17789
- const repo = repoByRoot.get(path16.resolve(repoRoot)) ?? (project && path16.resolve(project.repoRoot) === path16.resolve(repoRoot) ? { repoRoot: project.repoRoot, config: project.config } : null);
18048
+ const repo = repoByRoot.get(path17.resolve(repoRoot)) ?? (project && path17.resolve(project.repoRoot) === path17.resolve(repoRoot) ? { repoRoot: project.repoRoot, config: project.config } : null);
17790
18049
  if (!repo) {
17791
18050
  skipped++;
17792
18051
  appendLog(
@@ -18048,7 +18307,7 @@ async function runGitTracesWorker() {
18048
18307
  const tool = process.env[TOOL_ENV_FLAG2] ?? parseToolArg2(process.argv.slice(2)) ?? null;
18049
18308
  appendLog(
18050
18309
  "info",
18051
- `git-traces worker started (pid ${process.pid}, tool=${tool ?? "<none>"})`
18310
+ `git-traces worker started (pid ${process.pid}, tool=${tool ?? "<none>"}, cliVersion=${CLI_VERSION})`
18052
18311
  );
18053
18312
  if (!tool || !KNOWN_TOOLS.has(tool)) {
18054
18313
  appendLog(
@@ -18133,29 +18392,29 @@ ${stack}` : ""}`
18133
18392
  }
18134
18393
 
18135
18394
  // src/outputs/zip.ts
18136
- import fs18 from "fs";
18137
- import path18 from "path";
18395
+ import fs19 from "fs";
18396
+ import path19 from "path";
18138
18397
  import archiver2 from "archiver";
18139
18398
 
18140
18399
  // src/outputs/downloads.ts
18141
18400
  import { execSync as execSync2 } from "child_process";
18142
- import fs17 from "fs";
18143
- import os9 from "os";
18144
- import path17 from "path";
18401
+ import fs18 from "fs";
18402
+ import os10 from "os";
18403
+ import path18 from "path";
18145
18404
  function getDownloadsFolder() {
18146
- const home = os9.homedir();
18405
+ const home = os10.homedir();
18147
18406
  if (process.platform === "linux") {
18148
18407
  try {
18149
18408
  const xdgDir = execSync2("xdg-user-dir DOWNLOAD", {
18150
18409
  encoding: "utf-8",
18151
18410
  timeout: 3e3
18152
18411
  }).trim();
18153
- if (xdgDir && fs17.existsSync(xdgDir)) return xdgDir;
18412
+ if (xdgDir && fs18.existsSync(xdgDir)) return xdgDir;
18154
18413
  } catch {
18155
18414
  }
18156
18415
  }
18157
- const downloads = path17.join(home, "Downloads");
18158
- if (fs17.existsSync(downloads)) return downloads;
18416
+ const downloads = path18.join(home, "Downloads");
18417
+ if (fs18.existsSync(downloads)) return downloads;
18159
18418
  return home;
18160
18419
  }
18161
18420
 
@@ -18164,11 +18423,11 @@ function sanitizeFilename(name) {
18164
18423
  return name.replace(/[^a-zA-Z0-9._-]/g, "_");
18165
18424
  }
18166
18425
  function getUniqueFilename(dir, base, ext) {
18167
- let candidate = path18.join(dir, `${base}${ext}`);
18168
- if (!fs18.existsSync(candidate)) return candidate;
18426
+ let candidate = path19.join(dir, `${base}${ext}`);
18427
+ if (!fs19.existsSync(candidate)) return candidate;
18169
18428
  let i = 1;
18170
- while (fs18.existsSync(candidate)) {
18171
- candidate = path18.join(dir, `${base}-${i}${ext}`);
18429
+ while (fs19.existsSync(candidate)) {
18430
+ candidate = path19.join(dir, `${base}-${i}${ext}`);
18172
18431
  i++;
18173
18432
  }
18174
18433
  return candidate;
@@ -18178,13 +18437,13 @@ var ZipOutput = class {
18178
18437
  label = "Save as .zip to Downloads";
18179
18438
  async emit(group, options) {
18180
18439
  const downloadsDir = getDownloadsFolder();
18181
- const repoName = sanitizeFilename(path18.basename(group.repoPath));
18440
+ const repoName = sanitizeFilename(path19.basename(group.repoPath));
18182
18441
  const timeRange = options.timeRange;
18183
18442
  const rangePart = timeRange?.label ?? "all";
18184
18443
  const epochSeconds = Math.floor(Date.now() / 1e3);
18185
18444
  const baseName = `logs-${repoName}-${rangePart}-${epochSeconds}`;
18186
18445
  const outputPath = getUniqueFilename(downloadsDir, baseName, ".zip");
18187
- const output = fs18.createWriteStream(outputPath);
18446
+ const output = fs19.createWriteStream(outputPath);
18188
18447
  const archive = archiver2("zip", { zlib: { level: 6 } });
18189
18448
  const done = new Promise((resolve, reject) => {
18190
18449
  output.on("close", resolve);
@@ -18378,15 +18637,15 @@ async function confirmExport(group, output) {
18378
18637
  }
18379
18638
 
18380
18639
  // src/sources/claude.ts
18381
- import fs19 from "fs";
18382
- import os10 from "os";
18383
- import path19 from "path";
18640
+ import fs20 from "fs";
18641
+ import os11 from "os";
18642
+ import path20 from "path";
18384
18643
  import readline2 from "readline";
18385
18644
  var SKIP_DIRS = /* @__PURE__ */ new Set(["tool-results"]);
18386
18645
  async function resolveRepoPath(projectDir) {
18387
- const indexPath = path19.join(projectDir, "sessions-index.json");
18646
+ const indexPath = path20.join(projectDir, "sessions-index.json");
18388
18647
  try {
18389
- const raw = await fs19.promises.readFile(indexPath, "utf-8");
18648
+ const raw = await fs20.promises.readFile(indexPath, "utf-8");
18390
18649
  const data = JSON.parse(raw);
18391
18650
  if (data.originalPath && typeof data.originalPath === "string") {
18392
18651
  return data.originalPath;
@@ -18394,12 +18653,12 @@ async function resolveRepoPath(projectDir) {
18394
18653
  } catch {
18395
18654
  }
18396
18655
  const cwdCounts = /* @__PURE__ */ new Map();
18397
- const entries = await fs19.promises.readdir(projectDir, {
18656
+ const entries = await fs20.promises.readdir(projectDir, {
18398
18657
  withFileTypes: true
18399
18658
  });
18400
18659
  for (const entry of entries) {
18401
18660
  if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
18402
- const cwd = await extractCwdFromJsonl(path19.join(projectDir, entry.name));
18661
+ const cwd = await extractCwdFromJsonl(path20.join(projectDir, entry.name));
18403
18662
  if (cwd) {
18404
18663
  cwdCounts.set(cwd, (cwdCounts.get(cwd) ?? 0) + 1);
18405
18664
  }
@@ -18418,7 +18677,7 @@ async function resolveRepoPath(projectDir) {
18418
18677
  return null;
18419
18678
  }
18420
18679
  async function extractCwdFromJsonl(filePath) {
18421
- const stream = fs19.createReadStream(filePath, { encoding: "utf-8" });
18680
+ const stream = fs20.createReadStream(filePath, { encoding: "utf-8" });
18422
18681
  const rl = readline2.createInterface({ input: stream, crlfDelay: Infinity });
18423
18682
  try {
18424
18683
  for await (const line of rl) {
@@ -18440,12 +18699,12 @@ async function extractCwdFromJsonl(filePath) {
18440
18699
  async function collectFiles(dir, baseDir, sourceName, repoPath, results) {
18441
18700
  let entries;
18442
18701
  try {
18443
- entries = await fs19.promises.readdir(dir, { withFileTypes: true });
18702
+ entries = await fs20.promises.readdir(dir, { withFileTypes: true });
18444
18703
  } catch {
18445
18704
  return;
18446
18705
  }
18447
18706
  for (const entry of entries) {
18448
- const fullPath = path19.join(dir, entry.name);
18707
+ const fullPath = path20.join(dir, entry.name);
18449
18708
  if (entry.isDirectory()) {
18450
18709
  if (SKIP_DIRS.has(entry.name)) continue;
18451
18710
  await collectFiles(fullPath, baseDir, sourceName, repoPath, results);
@@ -18467,19 +18726,19 @@ function fallbackDecode(encodedName) {
18467
18726
  var ClaudeSource = class {
18468
18727
  name = "claude";
18469
18728
  async scan() {
18470
- const baseDir = path19.join(os10.homedir(), ".claude", "projects");
18729
+ const baseDir = path20.join(os11.homedir(), ".claude", "projects");
18471
18730
  try {
18472
- await fs19.promises.access(baseDir);
18731
+ await fs20.promises.access(baseDir);
18473
18732
  } catch {
18474
18733
  return [];
18475
18734
  }
18476
- const projectDirs = await fs19.promises.readdir(baseDir, {
18735
+ const projectDirs = await fs20.promises.readdir(baseDir, {
18477
18736
  withFileTypes: true
18478
18737
  });
18479
18738
  const dirEntries = projectDirs.filter((d) => d.isDirectory());
18480
18739
  const resultArrays = await Promise.all(
18481
18740
  dirEntries.map(async (dir) => {
18482
- const projectPath = path19.join(baseDir, dir.name);
18741
+ const projectPath = path20.join(baseDir, dir.name);
18483
18742
  const repoPath = await resolveRepoPath(projectPath) ?? fallbackDecode(dir.name);
18484
18743
  const files = [];
18485
18744
  await collectFiles(
@@ -18497,12 +18756,12 @@ var ClaudeSource = class {
18497
18756
  };
18498
18757
 
18499
18758
  // src/sources/codex.ts
18500
- import fs20 from "fs";
18501
- import os11 from "os";
18502
- import path20 from "path";
18759
+ import fs21 from "fs";
18760
+ import os12 from "os";
18761
+ import path21 from "path";
18503
18762
  import readline3 from "readline";
18504
18763
  async function parseSessionMeta2(filePath) {
18505
- const stream = fs20.createReadStream(filePath, { encoding: "utf-8" });
18764
+ const stream = fs21.createReadStream(filePath, { encoding: "utf-8" });
18506
18765
  const rl = readline3.createInterface({ input: stream, crlfDelay: Infinity });
18507
18766
  try {
18508
18767
  for await (const line of rl) {
@@ -18527,12 +18786,12 @@ async function findJsonlFiles(dir) {
18527
18786
  async function walk(d) {
18528
18787
  let entries;
18529
18788
  try {
18530
- entries = await fs20.promises.readdir(d, { withFileTypes: true });
18789
+ entries = await fs21.promises.readdir(d, { withFileTypes: true });
18531
18790
  } catch {
18532
18791
  return;
18533
18792
  }
18534
18793
  for (const entry of entries) {
18535
- const full = path20.join(d, entry.name);
18794
+ const full = path21.join(d, entry.name);
18536
18795
  if (entry.isDirectory()) {
18537
18796
  await walk(full);
18538
18797
  } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
@@ -18546,11 +18805,11 @@ async function findJsonlFiles(dir) {
18546
18805
  async function loadHistory(historyPath) {
18547
18806
  const map = /* @__PURE__ */ new Map();
18548
18807
  try {
18549
- await fs20.promises.access(historyPath);
18808
+ await fs21.promises.access(historyPath);
18550
18809
  } catch {
18551
18810
  return map;
18552
18811
  }
18553
- const stream = fs20.createReadStream(historyPath, { encoding: "utf-8" });
18812
+ const stream = fs21.createReadStream(historyPath, { encoding: "utf-8" });
18554
18813
  const rl = readline3.createInterface({ input: stream, crlfDelay: Infinity });
18555
18814
  try {
18556
18815
  for await (const line of rl) {
@@ -18577,14 +18836,14 @@ async function loadHistory(historyPath) {
18577
18836
  var CodexSource = class {
18578
18837
  name = "codex";
18579
18838
  async scan() {
18580
- const codexDir = path20.join(os11.homedir(), ".codex");
18581
- const sessionsDir2 = path20.join(codexDir, "sessions");
18839
+ const codexDir = path21.join(os12.homedir(), ".codex");
18840
+ const sessionsDir2 = path21.join(codexDir, "sessions");
18582
18841
  try {
18583
- await fs20.promises.access(sessionsDir2);
18842
+ await fs21.promises.access(sessionsDir2);
18584
18843
  } catch {
18585
18844
  return [];
18586
18845
  }
18587
- const historyPath = path20.join(codexDir, "history.jsonl");
18846
+ const historyPath = path21.join(codexDir, "history.jsonl");
18588
18847
  const [jsonlFiles, historyMap] = await Promise.all([
18589
18848
  findJsonlFiles(sessionsDir2),
18590
18849
  loadHistory(historyPath)
@@ -18607,8 +18866,8 @@ var CodexSource = class {
18607
18866
  });
18608
18867
  const historyLines = historyMap.get(meta.sessionId);
18609
18868
  if (historyLines) {
18610
- const sessionDir = path20.relative(sessionsDir2, path20.dirname(filePath));
18611
- const historyAbsPath = path20.join(
18869
+ const sessionDir = path21.relative(sessionsDir2, path21.dirname(filePath));
18870
+ const historyAbsPath = path21.join(
18612
18871
  sessionsDir2,
18613
18872
  sessionDir,
18614
18873
  `history-${meta.sessionId}.jsonl`
@@ -18628,18 +18887,18 @@ var CodexSource = class {
18628
18887
  };
18629
18888
 
18630
18889
  // src/sources/copilotChat.ts
18631
- import fs21 from "fs";
18632
- import os12 from "os";
18633
- import path21 from "path";
18890
+ import fs22 from "fs";
18891
+ import os13 from "os";
18892
+ import path22 from "path";
18634
18893
  import { fileURLToPath } from "url";
18635
18894
  function vsCodeUserDirs() {
18636
- const home = os12.homedir();
18895
+ const home = os13.homedir();
18637
18896
  const dirs = [
18638
- path21.join(home, "Library", "Application Support", "Code", "User"),
18639
- path21.join(home, ".config", "Code", "User")
18897
+ path22.join(home, "Library", "Application Support", "Code", "User"),
18898
+ path22.join(home, ".config", "Code", "User")
18640
18899
  ];
18641
18900
  if (process.env.APPDATA) {
18642
- dirs.push(path21.join(process.env.APPDATA, "Code", "User"));
18901
+ dirs.push(path22.join(process.env.APPDATA, "Code", "User"));
18643
18902
  }
18644
18903
  return dirs;
18645
18904
  }
@@ -18654,7 +18913,7 @@ function uriToFsPath(uri) {
18654
18913
  async function readWorkspaceFolder(workspaceJsonPath) {
18655
18914
  let raw;
18656
18915
  try {
18657
- raw = await fs21.promises.readFile(workspaceJsonPath, "utf-8");
18916
+ raw = await fs22.promises.readFile(workspaceJsonPath, "utf-8");
18658
18917
  } catch {
18659
18918
  return null;
18660
18919
  }
@@ -18676,10 +18935,10 @@ var CopilotChatSource = class {
18676
18935
  async scan() {
18677
18936
  const results = [];
18678
18937
  for (const userDir of vsCodeUserDirs()) {
18679
- const workspaceStorage = path21.join(userDir, "workspaceStorage");
18938
+ const workspaceStorage = path22.join(userDir, "workspaceStorage");
18680
18939
  let hashDirs;
18681
18940
  try {
18682
- hashDirs = await fs21.promises.readdir(workspaceStorage, {
18941
+ hashDirs = await fs22.promises.readdir(workspaceStorage, {
18683
18942
  withFileTypes: true
18684
18943
  });
18685
18944
  } catch {
@@ -18687,22 +18946,22 @@ var CopilotChatSource = class {
18687
18946
  }
18688
18947
  for (const hash of hashDirs) {
18689
18948
  if (!hash.isDirectory()) continue;
18690
- const wsRoot = path21.join(workspaceStorage, hash.name);
18691
- const transcriptsDir = path21.join(
18949
+ const wsRoot = path22.join(workspaceStorage, hash.name);
18950
+ const transcriptsDir = path22.join(
18692
18951
  wsRoot,
18693
18952
  "GitHub.copilot-chat",
18694
18953
  "transcripts"
18695
18954
  );
18696
18955
  let transcriptEntries;
18697
18956
  try {
18698
- transcriptEntries = await fs21.promises.readdir(transcriptsDir, {
18957
+ transcriptEntries = await fs22.promises.readdir(transcriptsDir, {
18699
18958
  withFileTypes: true
18700
18959
  });
18701
18960
  } catch {
18702
18961
  continue;
18703
18962
  }
18704
18963
  const repoPath = await readWorkspaceFolder(
18705
- path21.join(wsRoot, "workspace.json")
18964
+ path22.join(wsRoot, "workspace.json")
18706
18965
  );
18707
18966
  if (!repoPath) continue;
18708
18967
  for (const entry of transcriptEntries) {
@@ -18710,7 +18969,7 @@ var CopilotChatSource = class {
18710
18969
  const sessionId = entry.name.slice(0, -".jsonl".length);
18711
18970
  results.push({
18712
18971
  sourceName: this.name,
18713
- absolutePath: path21.join(transcriptsDir, entry.name),
18972
+ absolutePath: path22.join(transcriptsDir, entry.name),
18714
18973
  repoPath,
18715
18974
  metadata: { sessionId }
18716
18975
  });
@@ -18750,7 +19009,7 @@ function reportRedactionStats(noun, stats) {
18750
19009
  async function filterByTimeRange(group, range) {
18751
19010
  const results = await Promise.all(
18752
19011
  group.files.map(
18753
- (f) => fs22.promises.stat(f.absolutePath).then((s) => ({ file: f, stat: s })).catch(() => null)
19012
+ (f) => fs23.promises.stat(f.absolutePath).then((s) => ({ file: f, stat: s })).catch(() => null)
18754
19013
  )
18755
19014
  );
18756
19015
  const filtered = [];
@@ -18777,10 +19036,10 @@ async function runInteractive() {
18777
19036
  s.start(`Scanning ${source.name} logs...`);
18778
19037
  const allFiles = await source.scan();
18779
19038
  const allGroups = await mergeByRepo(allFiles);
18780
- const repoRoot = path22.resolve(repo.root);
19039
+ const repoRoot = path23.resolve(repo.root);
18781
19040
  const matching = allGroups.filter((g) => {
18782
- const resolved = path22.resolve(g.repoPath);
18783
- return resolved === repoRoot || resolved.startsWith(repoRoot + path22.sep);
19041
+ const resolved = path23.resolve(g.repoPath);
19042
+ return resolved === repoRoot || resolved.startsWith(repoRoot + path23.sep);
18784
19043
  });
18785
19044
  if (matching.length === 0) {
18786
19045
  s.stop(`No ${source.name} logs found for ${repo.name}.`);
@@ -18811,7 +19070,7 @@ async function runInteractive() {
18811
19070
  }
18812
19071
  }
18813
19072
  const envFileNames = await discoverEnvFiles(repoRoot);
18814
- const envFilePaths = envFileNames.map((n) => path22.join(repoRoot, n));
19073
+ const envFilePaths = envFileNames.map((n) => path23.join(repoRoot, n));
18815
19074
  const additionalFiles = await promptSecretFiles(envFileNames);
18816
19075
  const secretResult = await collectSecrets(
18817
19076
  repoRoot,