hillclimb 0.6.2 → 0.7.0

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 (3) hide show
  1. package/README.md +2 -2
  2. package/dist/cli.js +1085 -2111
  3. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import fs19 from "fs";
5
- import path24 from "path";
4
+ import fs21 from "fs";
5
+ import path23 from "path";
6
6
  import * as p6 from "@clack/prompts";
7
7
 
8
8
  // src/commands/init.ts
@@ -252,6 +252,95 @@ function appendLog(level, message) {
252
252
  }
253
253
  }
254
254
 
255
+ // src/platform/put.ts
256
+ import fs4 from "fs";
257
+ import http from "http";
258
+ import https from "https";
259
+ var DEFAULT_SOCKET_IDLE_TIMEOUT_MS = 12e4;
260
+ var DEFAULT_DEADLINE_MS = 60 * 6e4;
261
+ var MAX_RESPONSE_BODY_BYTES = 64 * 1024;
262
+ async function putToPresignedUrl(url, headers, body, options) {
263
+ const socketIdleTimeoutMs = options?.socketIdleTimeoutMs ?? DEFAULT_SOCKET_IDLE_TIMEOUT_MS;
264
+ const deadlineMs = options?.deadlineMs ?? DEFAULT_DEADLINE_MS;
265
+ const parsed = new URL(url);
266
+ const transport = parsed.protocol === "http:" ? http : https;
267
+ const contentLength = body.kind === "buffer" ? body.buffer.byteLength : body.sizeBytes;
268
+ const start = Date.now();
269
+ return new Promise((resolve, reject) => {
270
+ let settled = false;
271
+ const settleResolve = (result) => {
272
+ if (settled) return;
273
+ settled = true;
274
+ clearTimeout(deadlineTimer);
275
+ resolve(result);
276
+ };
277
+ const settleReject = (err) => {
278
+ if (settled) return;
279
+ settled = true;
280
+ clearTimeout(deadlineTimer);
281
+ reject(err);
282
+ };
283
+ const req = transport.request(
284
+ parsed,
285
+ {
286
+ method: "PUT",
287
+ // One-shot upload; skip the keep-alive agent pool so the idle
288
+ // timeout below dies with this socket instead of leaking onto a
289
+ // reused one.
290
+ agent: false,
291
+ headers: { ...headers, "content-length": String(contentLength) }
292
+ },
293
+ (res) => {
294
+ const chunks = [];
295
+ let buffered = 0;
296
+ res.on("data", (chunk) => {
297
+ if (buffered >= MAX_RESPONSE_BODY_BYTES) return;
298
+ const remaining = MAX_RESPONSE_BODY_BYTES - buffered;
299
+ chunks.push(
300
+ chunk.length > remaining ? chunk.subarray(0, remaining) : chunk
301
+ );
302
+ buffered += Math.min(chunk.length, remaining);
303
+ });
304
+ res.on("end", () => {
305
+ const status = res.statusCode ?? 0;
306
+ settleResolve({
307
+ status,
308
+ ok: status >= 200 && status < 300,
309
+ bodyText: Buffer.concat(chunks).toString("utf8")
310
+ });
311
+ });
312
+ res.on("error", settleReject);
313
+ }
314
+ );
315
+ const deadlineTimer = setTimeout(() => {
316
+ const err = new Error(
317
+ `PUT to presigned URL exceeded ${deadlineMs}ms deadline (elapsed ${Date.now() - start}ms)`
318
+ );
319
+ settleReject(err);
320
+ req.destroy(err);
321
+ }, deadlineMs);
322
+ deadlineTimer.unref();
323
+ req.setTimeout(socketIdleTimeoutMs, () => {
324
+ const err = new Error(
325
+ `PUT to presigned URL timed out: socket idle for ${socketIdleTimeoutMs}ms (elapsed ${Date.now() - start}ms)`
326
+ );
327
+ settleReject(err);
328
+ req.destroy(err);
329
+ });
330
+ req.on("error", settleReject);
331
+ if (body.kind === "buffer") {
332
+ req.end(body.buffer);
333
+ } else {
334
+ const stream = fs4.createReadStream(body.path);
335
+ stream.on("error", (err) => {
336
+ settleReject(err);
337
+ req.destroy(err);
338
+ });
339
+ stream.pipe(req);
340
+ }
341
+ });
342
+ }
343
+
255
344
  // src/platform/client.ts
256
345
  var PlatformError = class extends Error {
257
346
  constructor(message, status, code) {
@@ -457,8 +546,21 @@ var PlatformClient = class {
457
546
  );
458
547
  }
459
548
  async uploadToPresignedUrl(presignedUrl, headers, body) {
460
- const ab = new ArrayBuffer(body.byteLength);
461
- new Uint8Array(ab).set(body);
549
+ await this.putToPresignedUrlLogged(presignedUrl, headers, {
550
+ kind: "buffer",
551
+ buffer: body
552
+ });
553
+ }
554
+ // Same logging and error shape as uploadToPresignedUrl, but streams the
555
+ // file from disk so large snapshots never have to fit in memory.
556
+ async uploadFileToPresignedUrl(presignedUrl, headers, filePath, sizeBytes) {
557
+ await this.putToPresignedUrlLogged(presignedUrl, headers, {
558
+ kind: "file",
559
+ path: filePath,
560
+ sizeBytes
561
+ });
562
+ }
563
+ async putToPresignedUrlLogged(presignedUrl, headers, body) {
462
564
  const host = (() => {
463
565
  try {
464
566
  return new URL(presignedUrl).host;
@@ -466,14 +568,11 @@ var PlatformClient = class {
466
568
  return "presigned-url";
467
569
  }
468
570
  })();
571
+ const sizeBytes = body.kind === "buffer" ? body.buffer.byteLength : body.sizeBytes;
469
572
  const start = Date.now();
470
573
  let res;
471
574
  try {
472
- res = await fetch(presignedUrl, {
473
- method: "PUT",
474
- headers,
475
- body: new Blob([ab])
476
- });
575
+ res = await putToPresignedUrl(presignedUrl, headers, body);
477
576
  } catch (err) {
478
577
  appendLog(
479
578
  "error",
@@ -484,11 +583,7 @@ var PlatformClient = class {
484
583
  const elapsedMs = Date.now() - start;
485
584
  if (!res.ok) {
486
585
  appendLog("warn", `[PUT ${host}] ${res.status} ${elapsedMs}ms`);
487
- let detail = "";
488
- try {
489
- detail = await res.text();
490
- } catch {
491
- }
586
+ const detail = res.bodyText;
492
587
  throw new PlatformError(
493
588
  `PUT to presigned URL failed: HTTP ${res.status}${detail ? ` ${detail}` : ""}`,
494
589
  res.status
@@ -496,7 +591,7 @@ var PlatformClient = class {
496
591
  }
497
592
  appendLog(
498
593
  "info",
499
- `[PUT ${host}] ${res.status} ${elapsedMs}ms (${body.byteLength} bytes)`
594
+ `[PUT ${host}] ${res.status} ${elapsedMs}ms (${sizeBytes} bytes)`
500
595
  );
501
596
  }
502
597
  async submitContribution(contributionId) {
@@ -523,12 +618,12 @@ var PlatformClient = class {
523
618
 
524
619
  // src/platform/hooks.ts
525
620
  import { execFileSync } from "child_process";
526
- import fs5 from "fs";
621
+ import fs6 from "fs";
527
622
  import os2 from "os";
528
623
  import path6 from "path";
529
624
 
530
625
  // src/gitignore.ts
531
- import fs4 from "fs";
626
+ import fs5 from "fs";
532
627
  import path5 from "path";
533
628
  function toGitignorePattern(repoRoot, file) {
534
629
  const rel = path5.relative(repoRoot, file);
@@ -548,7 +643,7 @@ async function ensureGitignored(repoRoot, files) {
548
643
  let content = "";
549
644
  let created = false;
550
645
  try {
551
- content = await fs4.promises.readFile(gitignorePath, "utf-8");
646
+ content = await fs5.promises.readFile(gitignorePath, "utf-8");
552
647
  } catch (err) {
553
648
  if (err.code !== "ENOENT") throw err;
554
649
  created = true;
@@ -567,7 +662,7 @@ async function ensureGitignored(repoRoot, files) {
567
662
  if (next) next += "\n";
568
663
  next += ["# Hillclimb hook files", ...added].join("\n");
569
664
  next += "\n";
570
- await fs4.promises.writeFile(gitignorePath, next);
665
+ await fs5.promises.writeFile(gitignorePath, next);
571
666
  return {
572
667
  path: gitignorePath,
573
668
  added,
@@ -684,7 +779,7 @@ var TOOLS = [
684
779
  ];
685
780
  function isDir(p7) {
686
781
  try {
687
- return fs5.statSync(p7).isDirectory();
782
+ return fs6.statSync(p7).isDirectory();
688
783
  } catch {
689
784
  return false;
690
785
  }
@@ -710,7 +805,7 @@ function settingsPath(repoRoot, def) {
710
805
  }
711
806
  async function readJson(file, options = {}) {
712
807
  try {
713
- const raw = await fs5.promises.readFile(file, "utf-8");
808
+ const raw = await fs6.promises.readFile(file, "utf-8");
714
809
  if (!raw.trim()) return {};
715
810
  const parsed = JSON.parse(raw);
716
811
  if (parsed && typeof parsed === "object") return parsed;
@@ -720,8 +815,8 @@ async function readJson(file, options = {}) {
720
815
  if (options.repairMalformed && err instanceof SyntaxError) {
721
816
  const backup = `${file}.malformed-${Date.now()}`;
722
817
  try {
723
- const raw = await fs5.promises.readFile(file, "utf-8");
724
- await fs5.promises.writeFile(backup, raw);
818
+ const raw = await fs6.promises.readFile(file, "utf-8");
819
+ await fs6.promises.writeFile(backup, raw);
725
820
  appendLog(
726
821
  "warn",
727
822
  `hooks: backed up malformed JSON hook file ${file} to ${backup}`
@@ -739,8 +834,8 @@ async function readJson(file, options = {}) {
739
834
  }
740
835
  }
741
836
  async function writeJson(file, obj) {
742
- await fs5.promises.mkdir(path6.dirname(file), { recursive: true });
743
- await fs5.promises.writeFile(file, `${JSON.stringify(obj, null, 2)}
837
+ await fs6.promises.mkdir(path6.dirname(file), { recursive: true });
838
+ await fs6.promises.writeFile(file, `${JSON.stringify(obj, null, 2)}
744
839
  `);
745
840
  }
746
841
  function claudeHookPresent(matchers, command, options = {}) {
@@ -1084,19 +1179,19 @@ export default HillclimbPlugin;
1084
1179
  `;
1085
1180
  async function opencodeInstall(file, content = OPENCODE_PLUGIN_CONTENT) {
1086
1181
  try {
1087
- const existing = await fs5.promises.readFile(file, "utf-8");
1182
+ const existing = await fs6.promises.readFile(file, "utf-8");
1088
1183
  if (existing === content) {
1089
1184
  return { installed: 0, alreadyPresent: 1 };
1090
1185
  }
1091
1186
  } catch {
1092
1187
  }
1093
- await fs5.promises.mkdir(path6.dirname(file), { recursive: true });
1094
- await fs5.promises.writeFile(file, content);
1188
+ await fs6.promises.mkdir(path6.dirname(file), { recursive: true });
1189
+ await fs6.promises.writeFile(file, content);
1095
1190
  return { installed: 1, alreadyPresent: 0 };
1096
1191
  }
1097
1192
  async function opencodeCheck(file) {
1098
1193
  try {
1099
- const content = await fs5.promises.readFile(file, "utf-8");
1194
+ const content = await fs6.promises.readFile(file, "utf-8");
1100
1195
  return content.includes(OPENCODE_PLUGIN_MARKER);
1101
1196
  } catch {
1102
1197
  return false;
@@ -1395,7 +1490,7 @@ function codexHooksEnabledInConfig(content) {
1395
1490
  }
1396
1491
  async function areCodexHooksEnabled() {
1397
1492
  try {
1398
- const content = await fs5.promises.readFile(codexConfigPath(), "utf-8");
1493
+ const content = await fs6.promises.readFile(codexConfigPath(), "utf-8");
1399
1494
  return codexHooksEnabledInConfig(content);
1400
1495
  } catch {
1401
1496
  return false;
@@ -1405,13 +1500,13 @@ async function ensureCodexHooksEnabled() {
1405
1500
  let content;
1406
1501
  const configPath2 = codexConfigPath();
1407
1502
  try {
1408
- content = await fs5.promises.readFile(configPath2, "utf-8");
1503
+ content = await fs6.promises.readFile(configPath2, "utf-8");
1409
1504
  } catch (err) {
1410
1505
  if (err.code === "ENOENT") {
1411
- await fs5.promises.mkdir(path6.dirname(configPath2), {
1506
+ await fs6.promises.mkdir(path6.dirname(configPath2), {
1412
1507
  recursive: true
1413
1508
  });
1414
- await fs5.promises.writeFile(configPath2, "[features]\nhooks = true\n");
1509
+ await fs6.promises.writeFile(configPath2, "[features]\nhooks = true\n");
1415
1510
  return true;
1416
1511
  }
1417
1512
  throw err;
@@ -1449,7 +1544,7 @@ hooks = true
1449
1544
  `;
1450
1545
  }
1451
1546
  if (content === original) return false;
1452
- await fs5.promises.writeFile(configPath2, content);
1547
+ await fs6.promises.writeFile(configPath2, content);
1453
1548
  return true;
1454
1549
  }
1455
1550
  var CLAUDE_DEF = TOOLS[0];
@@ -2187,15 +2282,15 @@ async function runStatus(args = []) {
2187
2282
 
2188
2283
  // src/commands/upload.ts
2189
2284
  import { spawn as spawn2 } from "child_process";
2190
- import crypto3 from "crypto";
2191
- import fs11 from "fs";
2285
+ import crypto4 from "crypto";
2286
+ import fs13 from "fs";
2192
2287
  import os6 from "os";
2193
- import path15 from "path";
2288
+ import path14 from "path";
2194
2289
  import readline from "readline";
2195
2290
 
2196
2291
  // src/debug-logs.ts
2197
2292
  import crypto from "crypto";
2198
- import fs9 from "fs";
2293
+ import fs10 from "fs";
2199
2294
  import path12 from "path";
2200
2295
 
2201
2296
  // src/hook-events.ts
@@ -2255,11 +2350,11 @@ import os3 from "os";
2255
2350
  import { Worker } from "worker_threads";
2256
2351
 
2257
2352
  // src/middleware/file-utils.ts
2258
- import fs6 from "fs";
2353
+ import fs7 from "fs";
2259
2354
  async function checkBinary(filePath) {
2260
2355
  let handle = null;
2261
2356
  try {
2262
- handle = await fs6.promises.open(filePath, "r");
2357
+ handle = await fs7.promises.open(filePath, "r");
2263
2358
  const buf = Buffer.alloc(8192);
2264
2359
  const { bytesRead } = await handle.read(buf, 0, 8192, 0);
2265
2360
  for (let i = 0; i < bytesRead; i++) {
@@ -2284,7 +2379,7 @@ async function readFileContent(file) {
2284
2379
  return { kind: "binary" };
2285
2380
  }
2286
2381
  try {
2287
- const content = await fs6.promises.readFile(file.absolutePath, "utf-8");
2382
+ const content = await fs7.promises.readFile(file.absolutePath, "utf-8");
2288
2383
  return { kind: "text", content };
2289
2384
  } catch {
2290
2385
  return { kind: "error" };
@@ -10890,7 +10985,7 @@ var RedactMiddleware = class {
10890
10985
  var middleware = [];
10891
10986
 
10892
10987
  // src/middleware/secrets.ts
10893
- import fs7 from "fs";
10988
+ import fs8 from "fs";
10894
10989
  import path9 from "path";
10895
10990
  var KNOWN_NON_SECRETS = /* @__PURE__ */ new Set([
10896
10991
  "true",
@@ -10971,7 +11066,7 @@ async function parseEnvFile(filePath) {
10971
11066
  const values = [];
10972
11067
  let content;
10973
11068
  try {
10974
- content = await fs7.promises.readFile(filePath, "utf-8");
11069
+ content = await fs8.promises.readFile(filePath, "utf-8");
10975
11070
  } catch {
10976
11071
  return values;
10977
11072
  }
@@ -11039,7 +11134,7 @@ function addWithVariants(set, value) {
11039
11134
  async function discoverEnvFiles(repoRoot) {
11040
11135
  let entries;
11041
11136
  try {
11042
- entries = await fs7.promises.readdir(repoRoot);
11137
+ entries = await fs8.promises.readdir(repoRoot);
11043
11138
  } catch {
11044
11139
  return [];
11045
11140
  }
@@ -11048,7 +11143,7 @@ async function discoverEnvFiles(repoRoot) {
11048
11143
  if (!name.startsWith(".env")) continue;
11049
11144
  const filePath = path9.join(repoRoot, name);
11050
11145
  try {
11051
- const stat = await fs7.promises.stat(filePath);
11146
+ const stat = await fs8.promises.stat(filePath);
11052
11147
  if (stat.isFile()) envFiles.push(name);
11053
11148
  } catch {
11054
11149
  }
@@ -11115,12 +11210,15 @@ function getSourceBaseDir(sourceName) {
11115
11210
  return home;
11116
11211
  }
11117
11212
  }
11213
+ function archivePathFor(file) {
11214
+ const baseDir = getSourceBaseDir(file.sourceName);
11215
+ const relativePath = file.absolutePath.startsWith(baseDir) ? path10.relative(baseDir, file.absolutePath) : path10.basename(file.absolutePath);
11216
+ return path10.join(file.sourceName, relativePath);
11217
+ }
11118
11218
  function addGroupToArchive(archive, group, selectedSources) {
11119
11219
  for (const file of group.files) {
11120
11220
  if (!selectedSources.has(file.sourceName)) continue;
11121
- const baseDir = getSourceBaseDir(file.sourceName);
11122
- const relativePath = file.absolutePath.startsWith(baseDir) ? path10.relative(baseDir, file.absolutePath) : path10.basename(file.absolutePath);
11123
- const archivePath = path10.join(file.sourceName, relativePath);
11221
+ const archivePath = archivePathFor(file);
11124
11222
  if (file.content) {
11125
11223
  archive.append(file.content, { name: archivePath });
11126
11224
  } else {
@@ -11130,6 +11228,17 @@ function addGroupToArchive(archive, group, selectedSources) {
11130
11228
  }
11131
11229
 
11132
11230
  // src/outputs/platform.ts
11231
+ var UploadTooLargeError = class extends Error {
11232
+ constructor(filename, sizeBytes, maxBytes) {
11233
+ super(
11234
+ `upload ${filename} is ${sizeBytes} bytes, over the ${maxBytes}-byte limit`
11235
+ );
11236
+ this.filename = filename;
11237
+ this.sizeBytes = sizeBytes;
11238
+ this.maxBytes = maxBytes;
11239
+ this.name = "UploadTooLargeError";
11240
+ }
11241
+ };
11133
11242
  async function buildZipBuffer(group, selectedSources) {
11134
11243
  const archive = archiver("zip", { zlib: { level: 6 } });
11135
11244
  const stream = new PassThrough();
@@ -11164,8 +11273,16 @@ var PlatformUploadOutput = class {
11164
11273
  zipFilename,
11165
11274
  autoSubmit,
11166
11275
  existingContributionId,
11167
- onContributionCreated
11276
+ onContributionCreated,
11277
+ maxUploadBytes
11168
11278
  } = this.opts;
11279
+ if (maxUploadBytes !== void 0 && buffer.byteLength > maxUploadBytes) {
11280
+ throw new UploadTooLargeError(
11281
+ zipFilename,
11282
+ buffer.byteLength,
11283
+ maxUploadBytes
11284
+ );
11285
+ }
11169
11286
  let contributionId;
11170
11287
  if (existingContributionId) {
11171
11288
  contributionId = existingContributionId;
@@ -11195,15 +11312,26 @@ var PlatformUploadOutput = class {
11195
11312
  appendLog("info", `PUT to presigned URL succeeded for ${zipFilename}`);
11196
11313
  if (autoSubmit) {
11197
11314
  appendLog("info", `submitting contribution ${contributionId}`);
11198
- await client.submitContribution(contributionId);
11199
- appendLog("info", `contribution ${contributionId} submitted`);
11315
+ try {
11316
+ await client.submitContribution(contributionId);
11317
+ appendLog("info", `contribution ${contributionId} submitted`);
11318
+ } catch (err) {
11319
+ if (err instanceof PlatformError && err.status === 409 && err.code === "CONTRIBUTION_WRONG_STATE") {
11320
+ appendLog(
11321
+ "info",
11322
+ `contribution ${contributionId} was already submitted (409); continuing`
11323
+ );
11324
+ } else {
11325
+ throw err;
11326
+ }
11327
+ }
11200
11328
  }
11201
11329
  return contributionId;
11202
11330
  }
11203
11331
  };
11204
11332
 
11205
11333
  // src/pipeline.ts
11206
- import fs8 from "fs";
11334
+ import fs9 from "fs";
11207
11335
  import path11 from "path";
11208
11336
  function canonicalizePath(p7) {
11209
11337
  let resolved = path11.resolve(p7);
@@ -11239,7 +11367,7 @@ async function mergeByRepo(files) {
11239
11367
  const groups = [];
11240
11368
  for (const [repoPath, groupFiles] of grouped) {
11241
11369
  const stats = await Promise.all(
11242
- groupFiles.map((f) => fs8.promises.stat(f.absolutePath).catch(() => null))
11370
+ groupFiles.map((f) => fs9.promises.stat(f.absolutePath).catch(() => null))
11243
11371
  );
11244
11372
  let lastModified = /* @__PURE__ */ new Date(0);
11245
11373
  for (const stat of stats) {
@@ -11262,7 +11390,7 @@ async function preloadFiles(group) {
11262
11390
  group.files.map(async (file) => {
11263
11391
  if (file.content) return file;
11264
11392
  try {
11265
- const buf = await fs8.promises.readFile(file.absolutePath);
11393
+ const buf = await fs9.promises.readFile(file.absolutePath);
11266
11394
  const checkLen = Math.min(buf.length, 8192);
11267
11395
  for (let i = 0; i < checkLen; i++) {
11268
11396
  if (buf[i] === 0) {
@@ -11353,7 +11481,7 @@ async function transcriptFingerprint(payload) {
11353
11481
  if (!transcriptPath) return {};
11354
11482
  const resolved = path12.resolve(transcriptPath);
11355
11483
  try {
11356
- const stat = await fs9.promises.stat(resolved);
11484
+ const stat = await fs10.promises.stat(resolved);
11357
11485
  return {
11358
11486
  transcriptPath: resolved,
11359
11487
  transcriptMtimeMs: stat.mtimeMs,
@@ -11409,7 +11537,7 @@ async function eventContext(tool, payload) {
11409
11537
  }
11410
11538
  async function readState(eventId) {
11411
11539
  try {
11412
- const raw = await fs9.promises.readFile(stateFile(eventId), "utf-8");
11540
+ const raw = await fs10.promises.readFile(stateFile(eventId), "utf-8");
11413
11541
  const parsed = JSON.parse(raw);
11414
11542
  if (parsed.schemaVersion !== CURRENT_SCHEMA_VERSION) return null;
11415
11543
  return parsed;
@@ -11418,21 +11546,21 @@ async function readState(eventId) {
11418
11546
  }
11419
11547
  }
11420
11548
  async function writeState(state) {
11421
- await fs9.promises.mkdir(stateDir(), { recursive: true, mode: 448 });
11549
+ await fs10.promises.mkdir(stateDir(), { recursive: true, mode: 448 });
11422
11550
  const file = stateFile(state.eventId);
11423
11551
  const tmp = `${file}.tmp`;
11424
- await fs9.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
11552
+ await fs10.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
11425
11553
  mode: 384
11426
11554
  });
11427
- await fs9.promises.rename(tmp, file);
11555
+ await fs10.promises.rename(tmp, file);
11428
11556
  }
11429
11557
  async function acquireLock(eventId) {
11430
- await fs9.promises.mkdir(stateDir(), { recursive: true, mode: 448 });
11558
+ await fs10.promises.mkdir(stateDir(), { recursive: true, mode: 448 });
11431
11559
  for (let i = 0; i < LOCK_RETRIES; i++) {
11432
11560
  try {
11433
- const fd = await fs9.promises.open(
11561
+ const fd = await fs10.promises.open(
11434
11562
  lockFile(eventId),
11435
- fs9.constants.O_CREAT | fs9.constants.O_EXCL | fs9.constants.O_WRONLY
11563
+ fs10.constants.O_CREAT | fs10.constants.O_EXCL | fs10.constants.O_WRONLY
11436
11564
  );
11437
11565
  await fd.write(String(process.pid));
11438
11566
  await fd.close();
@@ -11448,7 +11576,7 @@ async function acquireLock(eventId) {
11448
11576
  }
11449
11577
  async function releaseLock(eventId) {
11450
11578
  try {
11451
- await fs9.promises.unlink(lockFile(eventId));
11579
+ await fs10.promises.unlink(lockFile(eventId));
11452
11580
  } catch {
11453
11581
  }
11454
11582
  }
@@ -11539,7 +11667,7 @@ async function uploadDebugLog(ctx, state) {
11539
11667
  const logPath = todayLogPath();
11540
11668
  let content;
11541
11669
  try {
11542
- content = await fs9.promises.readFile(logPath);
11670
+ content = await fs10.promises.readFile(logPath);
11543
11671
  } catch (err) {
11544
11672
  appendLog(
11545
11673
  "warn",
@@ -11663,1832 +11791,281 @@ async function recordDebugLogCompletion(args) {
11663
11791
  }
11664
11792
  }
11665
11793
 
11666
- // src/normalizer/index.ts
11667
- import path13 from "path";
11668
-
11669
- // src/normalizer/claude.ts
11670
- function stringify(value) {
11671
- if (typeof value === "string") return value;
11672
- try {
11673
- return JSON.stringify(value);
11674
- } catch {
11675
- return String(value);
11676
- }
11794
+ // src/transcript-artifacts.ts
11795
+ import { gzipSync } from "zlib";
11796
+ var TRANSCRIPT_PATCH_FORMAT = "jsonl-append-v1";
11797
+ function epochPrefix(epoch) {
11798
+ return `epoch-${String(epoch).padStart(3, "0")}`;
11677
11799
  }
11678
- function extractTextReasoningToolUses(content) {
11679
- if (typeof content === "string") {
11680
- return [content.trim(), void 0, []];
11681
- }
11682
- const textParts = [];
11683
- const reasoningParts = [];
11684
- const toolBlocks = [];
11685
- if (Array.isArray(content)) {
11686
- for (const block of content) {
11687
- if (typeof block !== "object" || block === null) {
11688
- textParts.push(stringify(block));
11689
- continue;
11690
- }
11691
- const b = block;
11692
- const blockType = b.type;
11693
- if (blockType === "tool_use") {
11694
- toolBlocks.push(b);
11695
- continue;
11696
- }
11697
- if (blockType === "thinking" || blockType === "reasoning" || blockType === "analysis") {
11698
- const textValue2 = b.text !== void 0 && b.text !== null ? b.text : b.thinking;
11699
- if (typeof textValue2 === "string") {
11700
- reasoningParts.push(textValue2.trim());
11701
- } else {
11702
- reasoningParts.push(stringify(textValue2));
11703
- }
11704
- continue;
11705
- }
11706
- if (blockType === "code" && typeof b.code === "string") {
11707
- textParts.push(b.code);
11708
- continue;
11709
- }
11710
- const textValue = b.text;
11711
- if (typeof textValue === "string") {
11712
- textParts.push(textValue);
11713
- } else {
11714
- textParts.push(stringify(b));
11715
- }
11716
- }
11717
- } else if (content !== void 0 && content !== null) {
11718
- textParts.push(stringify(content));
11719
- }
11720
- const text2 = textParts.filter((p7) => p7 && p7.trim()).map((p7) => p7.trim()).join("\n\n");
11721
- const reasoning = reasoningParts.filter((p7) => p7 && p7.trim()).map((p7) => p7.trim()).join("\n\n");
11722
- return [text2, reasoning || void 0, toolBlocks];
11723
- }
11724
- function buildMetrics(usage) {
11725
- if (typeof usage !== "object" || usage === null) return void 0;
11726
- const u = usage;
11727
- const cachedTokens = u.cache_read_input_tokens || 0;
11728
- const creation = u.cache_creation_input_tokens || 0;
11729
- const inputTokens = u.input_tokens || 0;
11730
- const promptTokens = inputTokens + cachedTokens + creation;
11731
- const completionTokens = u.output_tokens || 0;
11732
- const extra = {};
11733
- for (const [key, value] of Object.entries(u)) {
11734
- if (key === "input_tokens" || key === "output_tokens") continue;
11735
- extra[key] = value;
11736
- }
11737
- return {
11738
- prompt_tokens: promptTokens,
11739
- completion_tokens: completionTokens,
11740
- cached_tokens: cachedTokens,
11741
- extra: Object.keys(extra).length > 0 ? extra : void 0
11800
+ function turnSuffix(turn) {
11801
+ return `turn-${String(turn).padStart(3, "0")}`;
11802
+ }
11803
+ function snapshotFilename(epoch, recordedAt) {
11804
+ return `${epochPrefix(epoch)}-snapshot-${recordedAt}.zip`;
11805
+ }
11806
+ function metaFilename(epoch, recordedAt) {
11807
+ return `${epochPrefix(epoch)}-meta-${recordedAt}.json`;
11808
+ }
11809
+ function patchFilename(epoch, turn, recordedAt) {
11810
+ return `${epochPrefix(epoch)}-${turnSuffix(turn)}-${recordedAt}.patch.gz`;
11811
+ }
11812
+ function buildEpochMeta(params) {
11813
+ const meta = {
11814
+ formatVersion: 1,
11815
+ patchFormat: TRANSCRIPT_PATCH_FORMAT,
11816
+ sessionId: params.sessionId,
11817
+ tool: params.tool,
11818
+ cliVersion: params.cliVersion,
11819
+ epoch: params.epoch,
11820
+ transitionKind: params.transitionKind,
11821
+ baseline: params.baseline,
11822
+ snapshotFilename: params.snapshotFilename,
11823
+ transcriptArchivePath: params.transcriptArchivePath,
11824
+ rawByteOffset: params.cursor.rawByteOffset,
11825
+ rawPrefixSha256: params.cursor.rawPrefixSha256,
11826
+ recordedAt: new Date(params.recordedAt).toISOString()
11742
11827
  };
11828
+ return Buffer.from(JSON.stringify(meta, null, 2));
11743
11829
  }
11744
- function formatToolResult(block, toolUseResult) {
11745
- const parts = [];
11746
- const content = block.content;
11747
- if (typeof content === "string") {
11748
- if (content.trim()) parts.push(content.trim());
11749
- } else if (Array.isArray(content)) {
11750
- for (const item of content) {
11751
- const text2 = stringify(item);
11752
- if (text2.trim()) parts.push(text2.trim());
11753
- }
11754
- } else if (content !== void 0 && content !== null && content !== "") {
11755
- parts.push(stringify(content));
11756
- }
11757
- let metadata;
11758
- if (toolUseResult && typeof toolUseResult === "object") {
11759
- metadata = { tool_use_result: toolUseResult };
11760
- const stdout = toolUseResult.stdout;
11761
- const stderr = toolUseResult.stderr;
11762
- const exitCode = toolUseResult.exitCode ?? toolUseResult.exit_code;
11763
- const interrupted = toolUseResult.interrupted;
11764
- const isImage = toolUseResult.isImage;
11765
- const formatted = [];
11766
- if (stdout) formatted.push(`[stdout]
11767
- ${stdout}`.trimEnd());
11768
- if (stderr) formatted.push(`[stderr]
11769
- ${stderr}`.trimEnd());
11770
- if (exitCode !== void 0 && exitCode !== null && exitCode !== 0)
11771
- formatted.push(`[exit_code] ${exitCode}`);
11772
- if (interrupted) formatted.push(`[interrupted] ${interrupted}`);
11773
- if (isImage) formatted.push(`[is_image] ${isImage}`);
11774
- const skipKeys = /* @__PURE__ */ new Set([
11775
- "stdout",
11776
- "stderr",
11777
- "exitCode",
11778
- "exit_code",
11779
- "interrupted",
11780
- "isImage"
11781
- ]);
11782
- const remainingMeta = {};
11783
- for (const [k, v] of Object.entries(toolUseResult)) {
11784
- if (!skipKeys.has(k)) remainingMeta[k] = v;
11785
- }
11786
- if (Object.keys(remainingMeta).length > 0) {
11787
- formatted.push(`[metadata] ${JSON.stringify(remainingMeta)}`);
11788
- }
11789
- if (formatted.length > 0) {
11790
- parts.push(formatted.filter(Boolean).join("\n"));
11791
- }
11792
- }
11793
- if (block.is_error === true) {
11794
- parts.push("[error] tool reported failure");
11795
- metadata = metadata || {};
11796
- metadata.is_error = true;
11797
- }
11798
- if (metadata !== void 0) {
11799
- if (!("raw_tool_result" in metadata)) {
11800
- metadata.raw_tool_result = block;
11801
- }
11802
- }
11803
- const resultText = parts.filter(Boolean).join("\n\n").trim();
11804
- return [resultText || void 0, metadata];
11830
+ function gzipPatch(redactedTail) {
11831
+ return gzipSync(redactedTail);
11805
11832
  }
11806
- function convertClaudeToTrajectory(jsonlContent, sessionId) {
11807
- const rawEvents = [];
11808
- for (const line of jsonlContent.split("\n")) {
11809
- const trimmed = line.trim();
11810
- if (!trimmed) continue;
11811
- try {
11812
- rawEvents.push(JSON.parse(trimmed));
11813
- } catch {
11814
- }
11833
+ async function uploadArtifact(client, contributionId, filename, mimeType, buffer, maxBytes) {
11834
+ if (maxBytes !== void 0 && buffer.byteLength > maxBytes) {
11835
+ throw new UploadTooLargeError(filename, buffer.byteLength, maxBytes);
11815
11836
  }
11816
- if (rawEvents.length === 0) return null;
11817
- const events = rawEvents.sort(
11818
- (a, b) => (a.timestamp ?? "").localeCompare(b.timestamp ?? "")
11837
+ const presigned = await client.createUpload(contributionId, {
11838
+ originalFilename: filename,
11839
+ mimeType,
11840
+ sizeBytes: buffer.byteLength
11841
+ });
11842
+ appendLog(
11843
+ "info",
11844
+ `uploading ${filename} (${buffer.byteLength} bytes) to presigned URL`
11819
11845
  );
11820
- if (events.length === 0) return null;
11821
- let sid = sessionId ?? "";
11822
- for (const event of events) {
11823
- if (typeof event.sessionId === "string") {
11824
- sid = event.sessionId;
11825
- break;
11826
- }
11827
- }
11828
- let agentVersion = "unknown";
11829
- for (const event of events) {
11830
- if (typeof event.version === "string" && event.version) {
11831
- agentVersion = event.version;
11832
- break;
11833
- }
11834
- }
11835
- const cwds = /* @__PURE__ */ new Set();
11836
- const gitBranches = /* @__PURE__ */ new Set();
11837
- const agentIds = /* @__PURE__ */ new Set();
11838
- for (const event of events) {
11839
- if (typeof event.cwd === "string" && event.cwd) cwds.add(event.cwd);
11840
- if (typeof event.gitBranch === "string" && event.gitBranch)
11841
- gitBranches.add(event.gitBranch);
11842
- if (typeof event.agentId === "string" && event.agentId)
11843
- agentIds.add(event.agentId);
11844
- }
11845
- const agentExtra = {};
11846
- if (cwds.size > 0) agentExtra.cwds = [...cwds];
11847
- if (gitBranches.size > 0) agentExtra.git_branches = [...gitBranches];
11848
- if (agentIds.size > 0) agentExtra.agent_ids = [...agentIds];
11849
- let defaultModelName;
11850
- for (const event of events) {
11851
- const msg = event.message;
11852
- if (typeof msg !== "object" || msg === null) continue;
11853
- const model = msg.model;
11854
- if (typeof model === "string" && model) {
11855
- defaultModelName = model;
11856
- break;
11857
- }
11858
- }
11859
- const lastUsageByMsgId = /* @__PURE__ */ new Map();
11860
- for (const ev of events) {
11861
- if (ev.type !== "assistant") continue;
11862
- const msg = ev.message;
11863
- if (typeof msg !== "object" || msg === null) continue;
11864
- const mid = msg.id;
11865
- const usage = msg.usage;
11866
- if (mid && usage !== void 0) {
11867
- lastUsageByMsgId.set(mid, usage);
11868
- }
11869
- }
11870
- const normalizedEvents = [];
11871
- const pendingCalls = /* @__PURE__ */ new Map();
11872
- const seenMessageIds = /* @__PURE__ */ new Set();
11873
- for (const event of events) {
11874
- const message = event.message;
11875
- if (typeof message !== "object" || message === null) continue;
11876
- const msg = message;
11877
- const eventType = event.type;
11878
- const timestamp = event.timestamp;
11879
- if (eventType === "assistant") {
11880
- const [text2, reasoning, toolBlocks] = extractTextReasoningToolUses(
11881
- msg.content
11846
+ await client.uploadToPresignedUrl(
11847
+ presigned.presignedUrl,
11848
+ presigned.headers,
11849
+ buffer
11850
+ );
11851
+ appendLog("info", `PUT to presigned URL succeeded for ${filename}`);
11852
+ }
11853
+
11854
+ // src/transcript-cursor.ts
11855
+ import crypto2 from "crypto";
11856
+ import fs11 from "fs";
11857
+ function sha256OfBuffer(buffer) {
11858
+ return crypto2.createHash("sha256").update(buffer).digest("hex");
11859
+ }
11860
+ async function hashPrefix(filePath, byteLength) {
11861
+ const hash = crypto2.createHash("sha256");
11862
+ if (byteLength === 0) return hash;
11863
+ await new Promise((resolve, reject) => {
11864
+ const stream = fs11.createReadStream(filePath, {
11865
+ start: 0,
11866
+ end: byteLength - 1
11867
+ });
11868
+ stream.on("data", (chunk) => hash.update(chunk));
11869
+ stream.on("error", reject);
11870
+ stream.on("end", () => resolve());
11871
+ });
11872
+ return hash;
11873
+ }
11874
+ async function readRange(filePath, start, end) {
11875
+ const fd = await fs11.promises.open(filePath, "r");
11876
+ try {
11877
+ const buffer = Buffer.alloc(end - start);
11878
+ let filled = 0;
11879
+ while (filled < buffer.length) {
11880
+ const { bytesRead } = await fd.read(
11881
+ buffer,
11882
+ filled,
11883
+ buffer.length - filled,
11884
+ start + filled
11882
11885
  );
11883
- const msgId = msg.id;
11884
- let metrics;
11885
- if (msgId && seenMessageIds.has(msgId)) {
11886
- metrics = void 0;
11887
- } else {
11888
- const usage = msgId ? lastUsageByMsgId.get(msgId) ?? msg.usage : msg.usage;
11889
- metrics = buildMetrics(usage);
11890
- if (msgId) seenMessageIds.add(msgId);
11891
- }
11892
- const extra = {};
11893
- for (const key of ["stop_reason", "stop_sequence", "requestId"]) {
11894
- const value = msg[key];
11895
- if (value !== void 0 && value !== null) extra[key] = value;
11896
- }
11897
- if (event.id) extra.id = event.id;
11898
- if (event.agentId) extra.agent_id = event.agentId;
11899
- if (event.cwd) extra.cwd = extra.cwd ?? event.cwd;
11900
- if (event.userType && event.userType !== "external")
11901
- extra.user_type = event.userType;
11902
- extra.is_sidechain = event.isSidechain ?? false;
11903
- const modelName = msg.model || defaultModelName;
11904
- if (text2 || reasoning || toolBlocks.length === 0) {
11905
- normalizedEvents.push({
11906
- kind: "message",
11907
- timestamp,
11908
- role: msg.role ?? "assistant",
11909
- text: text2 || "",
11910
- reasoning: msg.role === "assistant" ? reasoning : void 0,
11911
- metrics,
11912
- extra: Object.keys(extra).length > 0 ? extra : void 0,
11913
- model_name: modelName
11914
- });
11915
- metrics = void 0;
11916
- }
11917
- for (let idx = 0; idx < toolBlocks.length; idx++) {
11918
- const toolBlock = toolBlocks[idx];
11919
- const callId = toolBlock.id ?? toolBlock.tool_use_id;
11920
- if (!callId) continue;
11921
- const rawArguments = toolBlock.input;
11922
- const args = typeof rawArguments === "object" && rawArguments !== null && !Array.isArray(rawArguments) ? rawArguments : { input: rawArguments };
11923
- const callExtra = { ...extra };
11924
- if (toolBlock.is_error !== void 0)
11925
- callExtra.tool_use_is_error = toolBlock.is_error;
11926
- if (toolBlock.name)
11927
- callExtra.tool_use_name = callExtra.tool_use_name ?? toolBlock.name;
11928
- pendingCalls.set(callId, {
11929
- kind: "tool_call",
11930
- timestamp,
11931
- call_id: callId,
11932
- tool_name: toolBlock.name ?? "",
11933
- arguments: args,
11934
- raw_arguments: rawArguments,
11935
- reasoning,
11936
- status: toolBlock.status,
11937
- message: void 0,
11938
- extra: Object.keys(callExtra).length > 0 ? callExtra : void 0,
11939
- metrics: idx === 0 && metrics !== void 0 ? metrics : void 0,
11940
- model_name: modelName
11941
- });
11942
- if (idx === 0 && metrics !== void 0) metrics = void 0;
11943
- }
11944
- continue;
11945
- }
11946
- if (eventType === "user") {
11947
- const content = msg.content;
11948
- if (typeof content === "string") {
11949
- const text2 = content.trim();
11950
- if (text2) {
11951
- normalizedEvents.push({
11952
- kind: "message",
11953
- timestamp,
11954
- role: "user",
11955
- text: text2,
11956
- extra: { is_sidechain: event.isSidechain ?? false }
11957
- });
11958
- }
11959
- continue;
11960
- }
11961
- if (Array.isArray(content)) {
11962
- const textParts = [];
11963
- for (const block of content) {
11964
- if (typeof block === "object" && block !== null && block.type === "tool_result") {
11965
- const b = block;
11966
- const callId = b.tool_use_id;
11967
- const [formattedOutput, resultMetadata] = formatToolResult(
11968
- b,
11969
- event.toolUseResult
11970
- );
11971
- let callInfo = callId ? pendingCalls.get(callId) : void 0;
11972
- if (callId) pendingCalls.delete(callId);
11973
- if (!callInfo) {
11974
- callInfo = {
11975
- kind: "tool_call",
11976
- timestamp,
11977
- call_id: callId ?? "",
11978
- tool_name: b.name ?? b.tool_name ?? "",
11979
- arguments: {},
11980
- raw_arguments: void 0,
11981
- reasoning: void 0,
11982
- status: void 0,
11983
- message: void 0,
11984
- extra: void 0,
11985
- metrics: void 0,
11986
- model_name: defaultModelName
11987
- };
11988
- }
11989
- const extraVal = callInfo.extra;
11990
- const callExtra = typeof extraVal === "object" && extraVal !== null ? { ...extraVal } : {};
11991
- if (resultMetadata)
11992
- callExtra.tool_result_metadata = callExtra.tool_result_metadata ?? resultMetadata;
11993
- if (b.is_error !== void 0)
11994
- callExtra.tool_result_is_error = callExtra.tool_result_is_error ?? b.is_error;
11995
- callInfo.extra = Object.keys(callExtra).length > 0 ? callExtra : void 0;
11996
- callInfo.output = formattedOutput;
11997
- callInfo.metadata = resultMetadata;
11998
- callInfo.timestamp = callInfo.timestamp ?? timestamp;
11999
- callInfo.model_name = callInfo.model_name ?? defaultModelName;
12000
- normalizedEvents.push(callInfo);
12001
- continue;
12002
- }
12003
- textParts.push(stringify(block));
12004
- }
12005
- const textMessage = textParts.filter((p7) => p7 && p7.trim()).map((p7) => p7.trim()).join("\n\n");
12006
- if (textMessage) {
12007
- normalizedEvents.push({
12008
- kind: "message",
12009
- timestamp,
12010
- role: "user",
12011
- text: textMessage
12012
- });
12013
- }
12014
- continue;
12015
- }
12016
- if (content !== void 0 && content !== null && content !== "") {
12017
- const text2 = stringify(content).trim();
12018
- if (text2) {
12019
- normalizedEvents.push({
12020
- kind: "message",
12021
- timestamp,
12022
- role: "user",
12023
- text: text2
12024
- });
12025
- }
12026
- }
12027
- }
12028
- }
12029
- for (const leftover of pendingCalls.values()) {
12030
- normalizedEvents.push(leftover);
12031
- }
12032
- const steps = [];
12033
- let stepId = 1;
12034
- for (const normEvent of normalizedEvents) {
12035
- const step = convertEventToStep(normEvent, stepId, defaultModelName);
12036
- if (!step) continue;
12037
- if (step.source === "agent" && !step.model_name && defaultModelName) {
12038
- step.model_name = defaultModelName;
12039
- }
12040
- steps.push(step);
12041
- stepId++;
12042
- }
12043
- if (steps.length === 0) return null;
12044
- const promptValues = steps.filter((s) => s.metrics?.prompt_tokens !== void 0).map((s) => s.metrics.prompt_tokens);
12045
- const completionValues = steps.filter((s) => s.metrics?.completion_tokens !== void 0).map((s) => s.metrics.completion_tokens);
12046
- const cachedValues = steps.filter((s) => s.metrics?.cached_tokens !== void 0).map((s) => s.metrics.cached_tokens);
12047
- const serviceTiers = /* @__PURE__ */ new Set();
12048
- let cacheCreationTotal = 0;
12049
- let cacheReadTotal = 0;
12050
- let cacheCreationSeen = false;
12051
- let cacheReadSeen = false;
12052
- for (const step of steps) {
12053
- if (!step.metrics?.extra) continue;
12054
- const tier = step.metrics.extra.service_tier;
12055
- if (typeof tier === "string") serviceTiers.add(tier);
12056
- const cacheCreation = step.metrics.extra.cache_creation_input_tokens;
12057
- if (typeof cacheCreation === "number") {
12058
- cacheCreationTotal += cacheCreation;
12059
- cacheCreationSeen = true;
11886
+ if (bytesRead === 0) break;
11887
+ filled += bytesRead;
12060
11888
  }
12061
- const cacheRead = step.metrics.extra.cache_read_input_tokens;
12062
- if (typeof cacheRead === "number") {
12063
- cacheReadTotal += cacheRead;
12064
- cacheReadSeen = true;
11889
+ return buffer.subarray(0, filled);
11890
+ } finally {
11891
+ await fd.close();
11892
+ }
11893
+ }
11894
+ async function evaluateTranscript(filePath, cursor) {
11895
+ const stat = await fs11.promises.stat(filePath);
11896
+ if (stat.size < cursor.rawByteOffset) return { kind: "truncate" };
11897
+ const prefixHash = await hashPrefix(filePath, cursor.rawByteOffset);
11898
+ const continuation = prefixHash.copy();
11899
+ if (prefixHash.digest("hex") !== cursor.rawPrefixSha256) {
11900
+ return { kind: "rewrite" };
11901
+ }
11902
+ if (stat.size === cursor.rawByteOffset) return { kind: "unchanged" };
11903
+ const grown = await readRange(filePath, cursor.rawByteOffset, stat.size);
11904
+ const lastNewline = grown.lastIndexOf(10);
11905
+ if (lastNewline === -1) return { kind: "unchanged" };
11906
+ const tail = grown.subarray(0, lastNewline + 1);
11907
+ return {
11908
+ kind: "append",
11909
+ tail,
11910
+ nextCursor: {
11911
+ rawByteOffset: cursor.rawByteOffset + tail.length,
11912
+ rawPrefixSha256: continuation.update(tail).digest("hex")
12065
11913
  }
12066
- }
12067
- const finalExtra = {};
12068
- if (serviceTiers.size > 0)
12069
- finalExtra.service_tiers = [...serviceTiers].sort();
12070
- if (cacheCreationSeen)
12071
- finalExtra.total_cache_creation_input_tokens = cacheCreationTotal;
12072
- if (cacheReadSeen) finalExtra.total_cache_read_input_tokens = cacheReadTotal;
12073
- const finalMetrics = {
12074
- total_prompt_tokens: promptValues.length > 0 ? promptValues.reduce((a, b) => a + b, 0) : void 0,
12075
- total_completion_tokens: completionValues.length > 0 ? completionValues.reduce((a, b) => a + b, 0) : void 0,
12076
- total_cached_tokens: cachedValues.length > 0 ? cachedValues.reduce((a, b) => a + b, 0) : void 0,
12077
- total_steps: steps.length,
12078
- extra: Object.keys(finalExtra).length > 0 ? finalExtra : void 0
12079
11914
  };
11915
+ }
11916
+ function truncateAtLastNewline(buffer) {
11917
+ const lastNewline = buffer.lastIndexOf(10);
11918
+ if (lastNewline === -1) return null;
11919
+ const covered = buffer.subarray(0, lastNewline + 1);
12080
11920
  return {
12081
- schema_version: "ATIF-v1.2",
12082
- session_id: sid,
12083
- agent: {
12084
- name: "claude-code",
12085
- version: agentVersion,
12086
- model_name: defaultModelName,
12087
- extra: Object.keys(agentExtra).length > 0 ? agentExtra : void 0
12088
- },
12089
- steps,
12090
- final_metrics: finalMetrics
11921
+ covered,
11922
+ cursor: {
11923
+ rawByteOffset: covered.length,
11924
+ rawPrefixSha256: sha256OfBuffer(covered)
11925
+ }
12091
11926
  };
12092
11927
  }
12093
- function convertEventToStep(event, stepId, defaultModelName) {
12094
- if (event.kind === "message") {
12095
- const role = event.role ?? "user";
12096
- let source;
12097
- if (role === "assistant") source = "agent";
12098
- else if (role === "user") source = "user";
12099
- else source = "system";
12100
- const step = {
12101
- step_id: stepId,
12102
- timestamp: event.timestamp,
12103
- source,
12104
- message: event.text ?? ""
12105
- };
12106
- if (source === "agent") {
12107
- if (event.reasoning) step.reasoning_content = event.reasoning;
12108
- if (event.model_name) step.model_name = event.model_name;
12109
- else if (defaultModelName) step.model_name = defaultModelName;
11928
+ async function cursorMatchesFile(filePath, cursor) {
11929
+ try {
11930
+ const stat = await fs11.promises.stat(filePath);
11931
+ if (stat.size < cursor.rawByteOffset) return false;
11932
+ const prefixHash = await hashPrefix(filePath, cursor.rawByteOffset);
11933
+ if (prefixHash.digest("hex") !== cursor.rawPrefixSha256) return false;
11934
+ if (cursor.rawByteOffset === 0) return true;
11935
+ const fd = await fs11.promises.open(filePath, "r");
11936
+ try {
11937
+ const byte = Buffer.alloc(1);
11938
+ const { bytesRead } = await fd.read(byte, 0, 1, cursor.rawByteOffset - 1);
11939
+ return bytesRead === 1 && byte[0] === 10;
11940
+ } finally {
11941
+ await fd.close();
12110
11942
  }
12111
- if (event.metrics) step.metrics = event.metrics;
12112
- if (event.extra) step.extra = event.extra;
12113
- return step;
12114
- }
12115
- if (event.kind === "tool_call") {
12116
- const callId = event.call_id;
12117
- const toolName = event.tool_name;
12118
- if (!callId || !toolName) return null;
12119
- const toolCall = {
12120
- tool_call_id: callId,
12121
- function_name: toolName,
12122
- arguments: event.arguments ?? {}
12123
- };
12124
- const observationResult = {
12125
- source_call_id: callId,
12126
- content: event.output
12127
- };
12128
- const observation = event.output !== void 0 ? { results: [observationResult] } : void 0;
12129
- const extra = { ...event.extra ?? {} };
12130
- if (event.metadata !== void 0)
12131
- extra.metadata = extra.metadata ?? event.metadata;
12132
- if (event.raw_arguments !== void 0)
12133
- extra.raw_arguments = extra.raw_arguments ?? event.raw_arguments;
12134
- if (event.status !== void 0) extra.status = extra.status ?? event.status;
12135
- const summaryParts = [toolName, callId].filter(Boolean);
12136
- const message = event.message || `Executed ${summaryParts.length > 0 ? summaryParts.join(" ") : "Tool call"}`;
12137
- const step = {
12138
- step_id: stepId,
12139
- timestamp: event.timestamp,
12140
- source: "agent",
12141
- message,
12142
- tool_calls: [toolCall],
12143
- observation
12144
- };
12145
- if (event.model_name) step.model_name = event.model_name;
12146
- if (event.reasoning) step.reasoning_content = event.reasoning;
12147
- if (event.metrics) step.metrics = event.metrics;
12148
- if (Object.keys(extra).length > 0) step.extra = extra;
12149
- return step;
11943
+ } catch {
11944
+ return false;
12150
11945
  }
12151
- return null;
12152
11946
  }
12153
11947
 
12154
- // src/normalizer/codex.ts
12155
- function extractMessageText(content) {
12156
- const parts = [];
12157
- for (const block of content) {
12158
- if (typeof block === "object" && block !== null) {
12159
- const text2 = block.text;
12160
- if (typeof text2 === "string") parts.push(text2);
12161
- }
12162
- }
12163
- return parts.join("");
11948
+ // src/upload-state.ts
11949
+ import crypto3 from "crypto";
11950
+ import fs12 from "fs";
11951
+ import os5 from "os";
11952
+ import path13 from "path";
11953
+ var CURRENT_SCHEMA_VERSION2 = 1;
11954
+ var DEFAULT_STATE_DIR = path13.join(
11955
+ os5.homedir(),
11956
+ ".hillclimb",
11957
+ "agent-uploads"
11958
+ );
11959
+ var DEFAULT_LOCK_WAIT_MS = 5 * 60 * 1e3;
11960
+ var DEFAULT_LOCK_RETRY_DELAY_MS = 500;
11961
+ var DEFAULT_STATE_TTL_MS = 24 * 60 * 60 * 1e3;
11962
+ var STALE_LOCK_TTL_MS = 60 * 60 * 1e3;
11963
+ function stateDir2() {
11964
+ return process.env.HILLCLIMB_UPLOAD_STATE_DIR ?? DEFAULT_STATE_DIR;
12164
11965
  }
12165
- function parseOutputBlob(raw) {
12166
- if (raw === void 0 || raw === null) return [void 0, void 0];
12167
- let parsed = raw;
12168
- if (typeof raw === "string") {
12169
- try {
12170
- parsed = JSON.parse(raw);
12171
- } catch {
12172
- return [raw, void 0];
12173
- }
12174
- }
12175
- if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
12176
- const obj = parsed;
12177
- let output = obj.output;
12178
- if (output === void 0 && Object.keys(obj).length > 0) {
12179
- output = JSON.stringify(obj);
12180
- }
12181
- const metadata = obj.metadata;
12182
- return [
12183
- output ?? void 0,
12184
- typeof metadata === "object" && metadata !== null ? metadata : void 0
12185
- ];
12186
- }
12187
- return [String(parsed), void 0];
11966
+ function readPositiveEnvMs(name, fallback) {
11967
+ const raw = process.env[name];
11968
+ if (raw === void 0) return fallback;
11969
+ const n = Number(raw);
11970
+ return Number.isFinite(n) && n >= 0 ? n : fallback;
11971
+ }
11972
+ function stateTtlMs() {
11973
+ return readPositiveEnvMs(
11974
+ "HILLCLIMB_UPLOAD_STATE_TTL_MS",
11975
+ DEFAULT_STATE_TTL_MS
11976
+ );
12188
11977
  }
12189
- function asObject(value) {
12190
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
11978
+ function lockRetryDelayMs() {
11979
+ return Math.max(
11980
+ 1,
11981
+ readPositiveEnvMs(
11982
+ "HILLCLIMB_UPLOAD_LOCK_RETRY_DELAY_MS",
11983
+ DEFAULT_LOCK_RETRY_DELAY_MS
11984
+ )
11985
+ );
12191
11986
  }
12192
- function compactExtra(extra) {
12193
- const compacted = {};
12194
- for (const [key, value] of Object.entries(extra)) {
12195
- if (value !== void 0 && value !== null) compacted[key] = value;
12196
- }
12197
- return Object.keys(compacted).length > 0 ? compacted : void 0;
11987
+ function lockRetries() {
11988
+ return Math.max(
11989
+ 1,
11990
+ Math.ceil(
11991
+ readPositiveEnvMs("HILLCLIMB_UPLOAD_LOCK_WAIT_MS", DEFAULT_LOCK_WAIT_MS) / lockRetryDelayMs()
11992
+ )
11993
+ );
11994
+ }
11995
+ function stateFileFor(repoRoot, tool, sessionId) {
11996
+ const hash = crypto3.createHash("sha256").update(`${path13.resolve(repoRoot)}\0${tool}\0${sessionId}`).digest("hex").slice(0, 16);
11997
+ return path13.join(stateDir2(), `${hash}.json`);
11998
+ }
11999
+ function lockFileFor(repoRoot, tool, sessionId) {
12000
+ return `${stateFileFor(repoRoot, tool, sessionId)}.lock`;
12198
12001
  }
12199
- function parseJsonObject(raw) {
12200
- const obj = asObject(raw);
12201
- if (obj) return obj;
12202
- if (typeof raw !== "string") return void 0;
12002
+ async function readUploadState(repoRoot, tool, sessionId) {
12203
12003
  try {
12204
- return asObject(JSON.parse(raw));
12004
+ const raw = await fs12.promises.readFile(
12005
+ stateFileFor(repoRoot, tool, sessionId),
12006
+ "utf-8"
12007
+ );
12008
+ const parsed = JSON.parse(raw);
12009
+ if (parsed.schemaVersion !== CURRENT_SCHEMA_VERSION2) return null;
12010
+ return parsed;
12205
12011
  } catch {
12206
- return void 0;
12012
+ return null;
12207
12013
  }
12208
12014
  }
12209
- function subagentRefFromSpawnOutput(args, rawOutput) {
12210
- const output = parseJsonObject(rawOutput);
12211
- const sessionId = typeof output?.agent_id === "string" && output.agent_id || typeof output?.agent_path === "string" && output.agent_path || void 0;
12212
- if (!sessionId) return void 0;
12213
- const extra = compactExtra({
12214
- agent_role: typeof args?.agent_type === "string" && args.agent_type || typeof output?.agent_role === "string" && output.agent_role || void 0,
12215
- nickname: typeof output?.nickname === "string" && output.nickname || typeof output?.agent_nickname === "string" && output.agent_nickname || void 0
12216
- });
12217
- return {
12218
- session_id: sessionId,
12219
- extra
12220
- };
12221
- }
12222
- function codexTrajectoryExtra(metaPayload) {
12223
- const source = asObject(metaPayload.source);
12224
- const subagent = asObject(source?.subagent);
12225
- const threadSpawn = asObject(subagent?.thread_spawn);
12226
- const parentThreadId = typeof threadSpawn?.parent_thread_id === "string" ? threadSpawn.parent_thread_id : void 0;
12227
- const subagentExtra = threadSpawn ? compactExtra({
12228
- depth: threadSpawn.depth,
12229
- agent_path: threadSpawn.agent_path,
12230
- agent_nickname: threadSpawn.agent_nickname,
12231
- agent_role: threadSpawn.agent_role
12232
- }) : void 0;
12233
- return compactExtra({
12234
- thread_source: metaPayload.thread_source,
12235
- agent_nickname: metaPayload.agent_nickname,
12236
- agent_role: metaPayload.agent_role,
12237
- parent_session_id: parentThreadId,
12238
- parent_thread_id: parentThreadId,
12239
- subagent: subagentExtra
12015
+ async function writeUploadState(state) {
12016
+ const file = stateFileFor(state.repoRoot, state.tool, state.sessionId);
12017
+ await fs12.promises.mkdir(stateDir2(), { recursive: true, mode: 448 });
12018
+ const tmp = `${file}.tmp`;
12019
+ await fs12.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
12020
+ mode: 384
12240
12021
  });
12022
+ await fs12.promises.rename(tmp, file);
12241
12023
  }
12242
- function convertCodexToTrajectory(jsonlContent, sessionId) {
12243
- const rawEvents = [];
12244
- for (const line of jsonlContent.split("\n")) {
12245
- const trimmed = line.trim();
12246
- if (!trimmed) continue;
12247
- try {
12248
- rawEvents.push(JSON.parse(trimmed));
12249
- } catch {
12250
- }
12251
- }
12252
- if (rawEvents.length === 0) return null;
12253
- const sessionMeta = rawEvents.find((e) => e.type === "session_meta");
12254
- const metaPayload = sessionMeta?.payload ?? {};
12255
- const sid = sessionId ?? (typeof metaPayload.id === "string" ? metaPayload.id : "");
12256
- const agentVersion = metaPayload.cli_version ?? "unknown";
12257
- const agentExtra = {};
12258
- for (const key of ["originator", "cwd", "git", "instructions"]) {
12259
- const value = metaPayload[key];
12260
- if (value !== void 0 && value !== null) agentExtra[key] = value;
12261
- }
12262
- let defaultModelName;
12263
- for (const event of rawEvents) {
12264
- if (event.type === "turn_context") {
12265
- const model = event.payload?.model;
12266
- if (typeof model === "string") {
12267
- defaultModelName = model;
12268
- break;
12269
- }
12270
- }
12271
- }
12272
- const normalizedEvents = [];
12273
- const pendingCalls = /* @__PURE__ */ new Map();
12274
- let pendingReasoning;
12275
- for (const event of rawEvents) {
12276
- const etype = event.type;
12277
- const payload = event.payload ?? {};
12278
- const timestamp = event.timestamp;
12279
- if (etype !== "response_item") continue;
12280
- const payloadType = payload.type;
12281
- if (payloadType === "reasoning") {
12282
- const summary = payload.summary;
12283
- if (Array.isArray(summary) && summary.length > 0) {
12284
- pendingReasoning = summary.filter((item) => typeof item === "string").join("\n");
12285
- } else {
12286
- pendingReasoning = void 0;
12287
- }
12288
- continue;
12289
- }
12290
- if (payloadType === "message") {
12291
- const content = payload.content;
12292
- const text2 = Array.isArray(content) ? extractMessageText(content) : "";
12293
- normalizedEvents.push({
12294
- kind: "message",
12295
- timestamp,
12296
- role: payload.role ?? "user",
12297
- text: text2,
12298
- reasoning: payload.role === "assistant" ? pendingReasoning : void 0
12299
- });
12300
- pendingReasoning = void 0;
12301
- continue;
12302
- }
12303
- if (payloadType === "web_search_call") {
12304
- const action = payload.action ?? {};
12305
- const actionType = action.type ?? "";
12306
- const args = { action_type: actionType };
12307
- if ("query" in action) args.query = action.query;
12308
- if ("queries" in action) args.queries = action.queries;
12309
- if ("url" in action) args.url = action.url;
12310
- normalizedEvents.push({
12311
- kind: "tool_call",
12312
- timestamp,
12313
- call_id: "",
12314
- tool_name: "web_search_call",
12315
- arguments: args,
12316
- reasoning: pendingReasoning,
12317
- status: payload.status
12318
- });
12319
- pendingReasoning = void 0;
12320
- continue;
12321
- }
12322
- if (payloadType === "function_call" || payloadType === "custom_tool_call") {
12323
- const callId = payload.call_id;
12324
- if (!callId) continue;
12325
- const rawArgsKey = payloadType === "function_call" ? "arguments" : "input";
12326
- const rawArguments = payload[rawArgsKey];
12327
- let parsedArgs;
12328
- if (typeof rawArguments === "string") {
12329
- try {
12330
- parsedArgs = JSON.parse(rawArguments);
12331
- } catch {
12332
- parsedArgs = { input: rawArguments };
12333
- }
12334
- } else if (rawArguments === void 0 || rawArguments === null) {
12335
- parsedArgs = {};
12336
- } else if (typeof rawArguments === "object" && !Array.isArray(rawArguments)) {
12337
- parsedArgs = rawArguments;
12338
- } else {
12339
- parsedArgs = { value: rawArguments };
12340
- }
12341
- pendingCalls.set(callId, {
12342
- kind: "tool_call",
12343
- timestamp,
12344
- call_id: callId,
12345
- tool_name: payload.name ?? "",
12346
- arguments: parsedArgs,
12347
- raw_arguments: rawArguments,
12348
- reasoning: pendingReasoning,
12349
- status: payload.status
12350
- });
12351
- pendingReasoning = void 0;
12352
- continue;
12353
- }
12354
- if (payloadType === "function_call_output" || payloadType === "custom_tool_call_output") {
12355
- const callId = payload.call_id;
12356
- const [outputText, metadata] = parseOutputBlob(payload.output);
12357
- let callInfo = callId ? pendingCalls.get(callId) : void 0;
12358
- if (callId) pendingCalls.delete(callId);
12359
- if (!callInfo) {
12360
- callInfo = {
12361
- kind: "tool_call",
12362
- timestamp,
12363
- call_id: callId ?? "",
12364
- tool_name: payload.name ?? "",
12365
- arguments: {},
12366
- reasoning: pendingReasoning
12367
- };
12368
- }
12369
- callInfo.output = outputText;
12370
- callInfo.metadata = metadata;
12371
- callInfo.timestamp = callInfo.timestamp ?? timestamp;
12372
- if (callInfo.tool_name === "spawn_agent") {
12373
- const subagentRef = subagentRefFromSpawnOutput(
12374
- callInfo.arguments,
12375
- payload.output
12376
- );
12377
- if (subagentRef) callInfo.subagentRefs = [subagentRef];
12378
- }
12379
- normalizedEvents.push(callInfo);
12380
- pendingReasoning = void 0;
12381
- }
12382
- }
12383
- const steps = [];
12384
- let stepId = 1;
12385
- for (const normEvent of normalizedEvents) {
12386
- const step = convertEventToStep2(normEvent, stepId, defaultModelName);
12387
- if (!step) continue;
12388
- if (step.source === "agent" && !step.model_name && defaultModelName) {
12389
- step.model_name = defaultModelName;
12390
- }
12391
- steps.push(step);
12392
- stepId++;
12393
- }
12394
- if (steps.length === 0) return null;
12395
- let finalMetrics;
12396
- for (let i = rawEvents.length - 1; i >= 0; i--) {
12397
- const event = rawEvents[i];
12398
- if (event.type !== "event_msg") continue;
12399
- const payload = event.payload;
12400
- if (!payload || payload.type !== "token_count") continue;
12401
- const info = payload.info;
12402
- if (!info || typeof info !== "object") continue;
12403
- const totalUsage = info.total_token_usage;
12404
- if (!totalUsage || typeof totalUsage !== "object") continue;
12405
- const promptTokens = totalUsage.input_tokens;
12406
- const completionTokens = totalUsage.output_tokens;
12407
- const reasoningTokens = totalUsage.reasoning_output_tokens;
12408
- const cachedTokens = totalUsage.cached_input_tokens;
12409
- const overallTokens = totalUsage.total_tokens;
12410
- finalMetrics = {
12411
- total_prompt_tokens: promptTokens || void 0,
12412
- total_completion_tokens: completionTokens || void 0,
12413
- total_cached_tokens: cachedTokens || void 0,
12414
- total_cost_usd: info.total_cost ?? info.cost_usd ?? void 0,
12415
- total_steps: steps.length,
12416
- extra: {
12417
- reasoning_output_tokens: reasoningTokens,
12418
- total_tokens: overallTokens,
12419
- last_token_usage: info.last_token_usage
12420
- }
12421
- };
12422
- break;
12423
- }
12424
- return {
12425
- schema_version: "ATIF-v1.5",
12426
- session_id: sid,
12427
- agent: {
12428
- name: "codex",
12429
- version: agentVersion,
12430
- model_name: defaultModelName,
12431
- extra: Object.keys(agentExtra).length > 0 ? agentExtra : void 0
12432
- },
12433
- steps,
12434
- final_metrics: finalMetrics,
12435
- extra: codexTrajectoryExtra(metaPayload)
12436
- };
12437
- }
12438
- function convertEventToStep2(event, stepId, defaultModelName) {
12439
- if (event.kind === "message") {
12440
- const role = event.role ?? "user";
12441
- let source;
12442
- if (role === "assistant") source = "agent";
12443
- else if (role === "user") source = "user";
12444
- else source = "system";
12445
- const step = {
12446
- step_id: stepId,
12447
- timestamp: event.timestamp,
12448
- source,
12449
- message: event.text ?? ""
12450
- };
12451
- if (source === "agent") {
12452
- if (event.reasoning) step.reasoning_content = event.reasoning;
12453
- if (defaultModelName) step.model_name = defaultModelName;
12454
- }
12455
- if (event.extra) step.extra = event.extra;
12456
- return step;
12457
- }
12458
- if (event.kind === "tool_call") {
12459
- const callId = event.call_id ?? "";
12460
- const toolName = event.tool_name ?? "";
12461
- const toolCall = {
12462
- tool_call_id: callId,
12463
- function_name: toolName,
12464
- arguments: typeof event.arguments === "object" && event.arguments !== null ? event.arguments : { value: event.arguments }
12465
- };
12466
- let observation;
12467
- if (event.output !== void 0) {
12468
- const result = {
12469
- source_call_id: callId || void 0,
12470
- content: event.output,
12471
- subagent_trajectory_ref: event.subagentRefs
12472
- };
12473
- observation = { results: [result] };
12474
- }
12475
- const extra = {};
12476
- if (event.metadata) extra.tool_metadata = event.metadata;
12477
- if (event.raw_arguments !== void 0)
12478
- extra.raw_arguments = event.raw_arguments;
12479
- if (event.status) extra.status = event.status;
12480
- const summaryParts = [toolName, callId].filter(Boolean);
12481
- const message = event.message || `Executed ${summaryParts.length > 0 ? summaryParts.join(" ") : "Tool call"}`;
12482
- const step = {
12483
- step_id: stepId,
12484
- timestamp: event.timestamp,
12485
- source: "agent",
12486
- message,
12487
- tool_calls: [toolCall],
12488
- observation
12489
- };
12490
- if (defaultModelName) step.model_name = defaultModelName;
12491
- if (event.reasoning) step.reasoning_content = event.reasoning;
12492
- if (event.metrics) step.metrics = event.metrics;
12493
- if (Object.keys(extra).length > 0) step.extra = extra;
12494
- return step;
12495
- }
12496
- return null;
12497
- }
12498
-
12499
- // src/normalizer/types.ts
12500
- var ATIF_VERSION = "ATIF-v1.6";
12501
- function excludeNone(obj) {
12502
- const result = {};
12503
- for (const [key, value] of Object.entries(obj)) {
12504
- if (value === void 0 || value === null) continue;
12505
- if (Array.isArray(value)) {
12506
- result[key] = value.map(
12507
- (item) => typeof item === "object" && item !== null && !Array.isArray(item) ? excludeNone(item) : item
12508
- );
12509
- } else if (typeof value === "object" && !Array.isArray(value)) {
12510
- result[key] = excludeNone(value);
12511
- } else {
12512
- result[key] = value;
12513
- }
12514
- }
12515
- return result;
12516
- }
12517
-
12518
- // src/normalizer/copilotChat.ts
12519
- function asObject2(value) {
12520
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
12521
- }
12522
- function asNumber(value) {
12523
- return typeof value === "number" && Number.isFinite(value) ? value : void 0;
12524
- }
12525
- function compactExtra2(extra) {
12526
- const result = {};
12527
- for (const [key, value] of Object.entries(extra)) {
12528
- if (value !== void 0 && value !== null) result[key] = value;
12529
- }
12530
- return Object.keys(result).length > 0 ? result : void 0;
12531
- }
12532
- function parseJsonLines(content) {
12533
- const entries = [];
12534
- for (const line of content.split("\n")) {
12535
- const trimmed = line.trim();
12536
- if (!trimmed) continue;
12537
- try {
12538
- const parsed = asObject2(JSON.parse(trimmed));
12539
- if (!parsed) continue;
12540
- entries.push({
12541
- type: typeof parsed.type === "string" ? parsed.type : void 0,
12542
- data: asObject2(parsed.data),
12543
- id: typeof parsed.id === "string" ? parsed.id : void 0,
12544
- timestamp: typeof parsed.timestamp === "string" ? parsed.timestamp : void 0
12545
- });
12546
- } catch {
12547
- }
12548
- }
12549
- return entries;
12550
- }
12551
- function parseArguments(value) {
12552
- const obj = asObject2(value);
12553
- if (obj) return obj;
12554
- if (typeof value === "string") {
12555
- try {
12556
- const parsed = asObject2(JSON.parse(value));
12557
- if (parsed) return parsed;
12558
- } catch {
12559
- return value ? { input: value } : {};
12560
- }
12561
- }
12562
- return value === void 0 || value === null ? {} : { value };
12563
- }
12564
- function buildMetrics2(data) {
12565
- const inputTokens = asNumber(data.inputTokens) ?? 0;
12566
- const outputTokens = asNumber(data.outputTokens) ?? 0;
12567
- const cacheReadTokens = asNumber(data.cacheReadTokens) ?? 0;
12568
- const cacheWriteTokens = asNumber(data.cacheWriteTokens) ?? 0;
12569
- const cost = asNumber(data.cost);
12570
- if (!inputTokens && !outputTokens && !cacheReadTokens && !cacheWriteTokens && !cost) {
12571
- return void 0;
12572
- }
12573
- return {
12574
- prompt_tokens: inputTokens + cacheReadTokens || void 0,
12575
- completion_tokens: outputTokens || void 0,
12576
- cached_tokens: cacheReadTokens || void 0,
12577
- cost_usd: cost || void 0,
12578
- extra: compactExtra2({
12579
- cache_write_tokens: cacheWriteTokens || void 0,
12580
- duration_ms: data.duration,
12581
- initiator: data.initiator,
12582
- api_call_id: data.apiCallId,
12583
- provider_call_id: data.providerCallId,
12584
- parent_tool_call_id: data.parentToolCallId,
12585
- quota_snapshots: data.quotaSnapshots,
12586
- copilot_usage: data.copilotUsage
12587
- })
12588
- };
12589
- }
12590
- function finalMetricsFromSteps(steps) {
12591
- let prompt = 0;
12592
- let completion = 0;
12593
- let cached = 0;
12594
- let cost = 0;
12595
- for (const step of steps) {
12596
- prompt += step.metrics?.prompt_tokens ?? 0;
12597
- completion += step.metrics?.completion_tokens ?? 0;
12598
- cached += step.metrics?.cached_tokens ?? 0;
12599
- cost += step.metrics?.cost_usd ?? 0;
12600
- }
12601
- return {
12602
- total_prompt_tokens: prompt || void 0,
12603
- total_completion_tokens: completion || void 0,
12604
- total_cached_tokens: cached || void 0,
12605
- total_cost_usd: cost || void 0,
12606
- total_steps: steps.length
12607
- };
12608
- }
12609
- function makeToolCall(request) {
12610
- const req = asObject2(request);
12611
- if (!req) return null;
12612
- const callId = typeof req.toolCallId === "string" && req.toolCallId || typeof req.id === "string" && req.id || "";
12613
- const name = typeof req.name === "string" && req.name || typeof req.toolName === "string" && req.toolName || "tool";
12614
- return {
12615
- tool_call_id: callId,
12616
- function_name: name,
12617
- arguments: parseArguments(req.arguments)
12618
- };
12619
- }
12620
- function toolCallFromExecutionStart(data) {
12621
- const callId = typeof data.toolCallId === "string" && data.toolCallId || typeof data.id === "string" && data.id || "";
12622
- const name = typeof data.toolName === "string" && data.toolName || typeof data.name === "string" && data.name || "tool";
12623
- return {
12624
- tool_call_id: callId,
12625
- function_name: name,
12626
- arguments: parseArguments(data.arguments)
12627
- };
12628
- }
12629
- function contentFromToolResult(data) {
12630
- const result = asObject2(data.result);
12631
- if (typeof result?.content === "string") return result.content;
12632
- if (typeof data.content === "string") return data.content;
12633
- return void 0;
12634
- }
12635
- function convertCopilotChatToTrajectory(jsonlContent, sessionId) {
12636
- const entries = parseJsonLines(jsonlContent);
12637
- if (entries.length === 0) return null;
12638
- let sid = sessionId ?? "unknown";
12639
- let copilotVersion = "unknown";
12640
- let vscodeVersion;
12641
- let cwd;
12642
- let defaultModelName;
12643
- const steps = [];
12644
- const pendingReasoning = [];
12645
- const pendingToolSteps = /* @__PURE__ */ new Map();
12646
- let lastAgentStep;
12647
- for (const entry of entries) {
12648
- const data = entry.data ?? {};
12649
- if (entry.type === "session.start") {
12650
- if (!sessionId && typeof data.sessionId === "string")
12651
- sid = data.sessionId;
12652
- if (typeof data.copilotVersion === "string")
12653
- copilotVersion = data.copilotVersion;
12654
- if (typeof data.vscodeVersion === "string")
12655
- vscodeVersion = data.vscodeVersion;
12656
- const context = asObject2(data.context);
12657
- if (typeof context?.cwd === "string") cwd = context.cwd;
12658
- continue;
12659
- }
12660
- if (entry.type === "user.message" || entry.type === "system.message") {
12661
- const content = typeof data.content === "string" && data.content || typeof data.transformedContent === "string" && data.transformedContent || "";
12662
- if (!content.trim()) continue;
12663
- const source = entry.type === "system.message" ? "system" : "user";
12664
- const step = {
12665
- step_id: steps.length + 1,
12666
- timestamp: entry.timestamp,
12667
- source,
12668
- message: content,
12669
- extra: compactExtra2({
12670
- attachments: data.attachments,
12671
- source: data.source,
12672
- agent_mode: data.agentMode,
12673
- interaction_id: data.interactionId
12674
- })
12675
- };
12676
- steps.push(step);
12677
- continue;
12678
- }
12679
- if (entry.type === "assistant.reasoning") {
12680
- if (typeof data.content === "string" && data.content.trim()) {
12681
- pendingReasoning.push(data.content.trim());
12682
- }
12683
- continue;
12684
- }
12685
- if (entry.type === "assistant.message") {
12686
- const toolCalls = (Array.isArray(data.toolRequests) ? data.toolRequests : []).flatMap((request) => {
12687
- const call = makeToolCall(request);
12688
- return call ? [call] : [];
12689
- });
12690
- const content = typeof data.content === "string" && data.content.trim() ? data.content.trim() : "";
12691
- const reasoning = typeof data.reasoningText === "string" && data.reasoningText.trim() || pendingReasoning.join("\n\n") || void 0;
12692
- pendingReasoning.length = 0;
12693
- const step = {
12694
- step_id: steps.length + 1,
12695
- timestamp: entry.timestamp,
12696
- source: "agent",
12697
- message: content || (toolCalls.length > 0 ? "(tool use)" : ""),
12698
- model_name: defaultModelName,
12699
- extra: compactExtra2({
12700
- message_id: data.messageId,
12701
- phase: data.phase,
12702
- output_tokens: data.outputTokens
12703
- })
12704
- };
12705
- if (reasoning) step.reasoning_content = reasoning;
12706
- if (toolCalls.length > 0) step.tool_calls = toolCalls;
12707
- if (step.message || step.tool_calls?.length || step.reasoning_content) {
12708
- steps.push(step);
12709
- lastAgentStep = step;
12710
- for (const call of toolCalls) {
12711
- if (call.tool_call_id) pendingToolSteps.set(call.tool_call_id, step);
12712
- }
12713
- }
12714
- continue;
12715
- }
12716
- if (entry.type === "tool.execution_start") {
12717
- const toolCall = toolCallFromExecutionStart(data);
12718
- const step = {
12719
- step_id: steps.length + 1,
12720
- timestamp: entry.timestamp,
12721
- source: "agent",
12722
- message: `Executed ${toolCall.function_name} ${toolCall.tool_call_id}`.trim(),
12723
- model_name: defaultModelName,
12724
- tool_calls: [toolCall]
12725
- };
12726
- steps.push(step);
12727
- lastAgentStep = step;
12728
- if (toolCall.tool_call_id)
12729
- pendingToolSteps.set(toolCall.tool_call_id, step);
12730
- continue;
12731
- }
12732
- if (entry.type === "tool.execution_complete") {
12733
- const callId = typeof data.toolCallId === "string" && data.toolCallId || typeof data.id === "string" && data.id || "";
12734
- const target = pendingToolSteps.get(callId);
12735
- const content = contentFromToolResult(data);
12736
- if (target && content !== void 0) {
12737
- const observation = target.observation ?? { results: [] };
12738
- observation.results.push({
12739
- source_call_id: callId || void 0,
12740
- content
12741
- });
12742
- target.observation = observation;
12743
- const extra = { ...target.extra ?? {} };
12744
- extra.tool_success = data.success;
12745
- target.extra = compactExtra2(extra);
12746
- }
12747
- if (callId) pendingToolSteps.delete(callId);
12748
- continue;
12749
- }
12750
- if (entry.type === "assistant.usage") {
12751
- if (typeof data.model === "string" && !defaultModelName) {
12752
- defaultModelName = data.model;
12753
- if (lastAgentStep && !lastAgentStep.model_name) {
12754
- lastAgentStep.model_name = defaultModelName;
12755
- }
12756
- }
12757
- const metrics = buildMetrics2(data);
12758
- if (metrics && lastAgentStep && !lastAgentStep.metrics) {
12759
- lastAgentStep.metrics = metrics;
12760
- }
12761
- }
12762
- }
12763
- if (steps.length === 0) return null;
12764
- return {
12765
- schema_version: ATIF_VERSION,
12766
- session_id: sid,
12767
- agent: {
12768
- name: "github-copilot-chat",
12769
- version: copilotVersion,
12770
- model_name: defaultModelName,
12771
- extra: compactExtra2({
12772
- vscode_version: vscodeVersion,
12773
- cwd
12774
- })
12775
- },
12776
- steps,
12777
- final_metrics: finalMetricsFromSteps(steps)
12778
- };
12779
- }
12780
-
12781
- // src/normalizer/cursor.ts
12782
- function convertCursorToTrajectory(jsonlContent, sessionId) {
12783
- const lines = [];
12784
- for (const line of jsonlContent.split("\n")) {
12785
- const trimmed = line.trim();
12786
- if (!trimmed) continue;
12787
- try {
12788
- lines.push(JSON.parse(trimmed));
12789
- } catch {
12790
- }
12791
- }
12792
- if (lines.length === 0) return null;
12793
- const steps = [];
12794
- let stepId = 1;
12795
- for (const entry of lines) {
12796
- const role = entry.role;
12797
- if (!role) continue;
12798
- let source;
12799
- if (role === "assistant") source = "agent";
12800
- else if (role === "user") source = "user";
12801
- else source = "system";
12802
- const contentParts = entry.message?.content;
12803
- let text2 = "";
12804
- if (Array.isArray(contentParts)) {
12805
- const textParts = [];
12806
- for (const part of contentParts) {
12807
- if (typeof part === "object" && part !== null && typeof part.text === "string") {
12808
- textParts.push(part.text);
12809
- }
12810
- }
12811
- text2 = textParts.join("\n\n").trim();
12812
- }
12813
- if (!text2) continue;
12814
- const step = {
12815
- step_id: stepId,
12816
- source,
12817
- message: text2
12818
- };
12819
- steps.push(step);
12820
- stepId++;
12821
- }
12822
- if (steps.length === 0) return null;
12823
- const finalMetrics = {
12824
- total_steps: steps.length
12825
- };
12826
- return {
12827
- schema_version: "ATIF-v1.6",
12828
- session_id: sessionId ?? "unknown",
12829
- agent: {
12830
- name: "cursor",
12831
- version: "unknown"
12832
- },
12833
- steps,
12834
- final_metrics: finalMetrics
12835
- };
12836
- }
12837
-
12838
- // src/normalizer/opencode.ts
12839
- function asObject3(value) {
12840
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
12841
- }
12842
- function asArray(value) {
12843
- return Array.isArray(value) ? value : [];
12844
- }
12845
- function asNumber2(value) {
12846
- return typeof value === "number" && Number.isFinite(value) ? value : void 0;
12847
- }
12848
- function compactExtra3(extra) {
12849
- const result = {};
12850
- for (const [key, value] of Object.entries(extra)) {
12851
- if (value !== void 0 && value !== null) result[key] = value;
12852
- }
12853
- return Object.keys(result).length > 0 ? result : void 0;
12854
- }
12855
- function parseJsonLines2(content) {
12856
- const events = [];
12857
- for (const line of content.split("\n")) {
12858
- const trimmed = line.trim();
12859
- if (!trimmed) continue;
12860
- try {
12861
- const parsed = JSON.parse(trimmed);
12862
- const obj = asObject3(parsed);
12863
- if (obj) events.push(obj);
12864
- } catch {
12865
- }
12866
- }
12867
- return events;
12868
- }
12869
- function timestampToIso(value) {
12870
- const numeric = asNumber2(value);
12871
- if (numeric === void 0) return void 0;
12872
- const millis = numeric < 1e12 ? numeric * 1e3 : numeric;
12873
- const date = new Date(millis);
12874
- return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
12875
- }
12876
- function timeFromObject(value) {
12877
- const obj = asObject3(value);
12878
- if (!obj) return void 0;
12879
- return timestampToIso(obj.start ?? obj.created ?? obj.completed ?? obj.end);
12880
- }
12881
- function stringify2(value) {
12882
- if (typeof value === "string") return value;
12024
+ async function deleteUploadState(repoRoot, tool, sessionId) {
12883
12025
  try {
12884
- return JSON.stringify(value);
12026
+ await fs12.promises.unlink(stateFileFor(repoRoot, tool, sessionId));
12885
12027
  } catch {
12886
- return String(value);
12887
- }
12888
- }
12889
- function argsFromUnknown(value) {
12890
- const obj = asObject3(value);
12891
- if (obj) return obj;
12892
- if (typeof value === "string") {
12893
- try {
12894
- const parsed = JSON.parse(value);
12895
- const parsedObj = asObject3(parsed);
12896
- if (parsedObj) return parsedObj;
12897
- } catch {
12898
- return value ? { input: value } : {};
12899
- }
12900
- }
12901
- return value === void 0 || value === null ? {} : { value };
12902
- }
12903
- function modelNameFromInfo(info) {
12904
- const model = asObject3(info.model);
12905
- const modelID = typeof info.modelID === "string" && info.modelID || typeof model?.modelID === "string" && model.modelID || void 0;
12906
- const providerID = typeof info.providerID === "string" && info.providerID || typeof model?.providerID === "string" && model.providerID || void 0;
12907
- if (providerID && modelID) return `${providerID}/${modelID}`;
12908
- return modelID;
12909
- }
12910
- function metricsFromTokens(tokens, cost) {
12911
- const t = asObject3(tokens);
12912
- if (!t) return void 0;
12913
- const cache = asObject3(t.cache);
12914
- const input = asNumber2(t.input) ?? 0;
12915
- const output = asNumber2(t.output) ?? 0;
12916
- const reasoning = asNumber2(t.reasoning) ?? 0;
12917
- const cacheRead = asNumber2(cache?.read) ?? 0;
12918
- const cacheWrite = asNumber2(cache?.write) ?? 0;
12919
- const costUsd = asNumber2(cost);
12920
- if (!input && !output && !cacheRead && !cacheWrite && !costUsd) {
12921
- return void 0;
12922
- }
12923
- const extra = compactExtra3({
12924
- reasoning_tokens: reasoning || void 0,
12925
- cache_write_tokens: cacheWrite || void 0
12926
- });
12927
- return {
12928
- prompt_tokens: input + cacheRead || void 0,
12929
- completion_tokens: output || void 0,
12930
- cached_tokens: cacheRead || void 0,
12931
- cost_usd: costUsd || void 0,
12932
- extra
12933
- };
12934
- }
12935
- function metricsFromInfo(info) {
12936
- return metricsFromTokens(info.tokens, info.cost);
12937
- }
12938
- function addFinalMetricTotals(totals, metrics) {
12939
- if (!metrics) return;
12940
- totals.total_prompt_tokens += metrics.prompt_tokens ?? 0;
12941
- totals.total_completion_tokens += metrics.completion_tokens ?? 0;
12942
- totals.total_cached_tokens += metrics.cached_tokens ?? 0;
12943
- totals.total_cost_usd += metrics.cost_usd ?? 0;
12944
- }
12945
- function finalMetricsFromSteps2(steps) {
12946
- const totals = {
12947
- total_prompt_tokens: 0,
12948
- total_completion_tokens: 0,
12949
- total_cached_tokens: 0,
12950
- total_cost_usd: 0,
12951
- total_steps: steps.length,
12952
- extra: {}
12953
- };
12954
- for (const step of steps) addFinalMetricTotals(totals, step.metrics);
12955
- return {
12956
- total_prompt_tokens: totals.total_prompt_tokens || void 0,
12957
- total_completion_tokens: totals.total_completion_tokens || void 0,
12958
- total_cached_tokens: totals.total_cached_tokens || void 0,
12959
- total_cost_usd: totals.total_cost_usd || void 0,
12960
- total_steps: steps.length
12961
- };
12962
- }
12963
- function entryFromLine(line) {
12964
- const info = asObject3(line.info);
12965
- if (info) {
12966
- return {
12967
- info,
12968
- parts: asArray(line.parts).flatMap((part) => {
12969
- const obj = asObject3(part);
12970
- return obj ? [obj] : [];
12971
- })
12972
- };
12973
- }
12974
- if (typeof line.role === "string" || typeof line.sessionID === "string") {
12975
- return {
12976
- info: line,
12977
- parts: asArray(line.parts).flatMap((part) => {
12978
- const obj = asObject3(part);
12979
- return obj ? [obj] : [];
12980
- })
12981
- };
12982
- }
12983
- return null;
12984
- }
12985
- function entriesFromEventWrappers(lines) {
12986
- const byMessage = /* @__PURE__ */ new Map();
12987
- function getEntry(messageID, sessionID) {
12988
- let entry = byMessage.get(messageID);
12989
- if (!entry) {
12990
- entry = { info: { id: messageID, sessionID }, parts: [] };
12991
- byMessage.set(messageID, entry);
12992
- }
12993
- return entry;
12994
- }
12995
- for (const line of lines) {
12996
- const type = line.type;
12997
- const props = asObject3(line.properties) ?? line;
12998
- if (type === "message.updated") {
12999
- const info = asObject3(props.info);
13000
- const id = typeof info?.id === "string" ? info.id : void 0;
13001
- if (!info || !id) continue;
13002
- const entry = getEntry(id, info.sessionID);
13003
- entry.info = info;
13004
- continue;
13005
- }
13006
- if (type === "message.part.updated") {
13007
- const part = asObject3(props.part);
13008
- const messageID = typeof part?.messageID === "string" && part.messageID || void 0;
13009
- if (!part || !messageID) continue;
13010
- const entry = getEntry(messageID, part.sessionID);
13011
- const partID = typeof part.id === "string" ? part.id : void 0;
13012
- const existingIndex = partID ? entry.parts.findIndex((p7) => p7.id === partID) : -1;
13013
- if (existingIndex >= 0) entry.parts[existingIndex] = part;
13014
- else entry.parts.push(part);
13015
- }
13016
- }
13017
- return [...byMessage.values()];
13018
- }
13019
- function getSessionId(sessionId, exportInfo, entries) {
13020
- if (sessionId) return sessionId;
13021
- if (typeof exportInfo?.id === "string") return exportInfo.id;
13022
- for (const entry of entries) {
13023
- if (typeof entry.info.sessionID === "string") return entry.info.sessionID;
13024
- }
13025
- return "unknown";
13026
- }
13027
- function getAgentVersion(exportInfo) {
13028
- return typeof exportInfo?.version === "string" && exportInfo.version || "unknown";
13029
- }
13030
- function sortEntries(entries) {
13031
- return [...entries].sort((a, b) => {
13032
- const at = asNumber2(asObject3(a.info.time)?.created) ?? 0;
13033
- const bt = asNumber2(asObject3(b.info.time)?.created) ?? 0;
13034
- return at - bt;
13035
- });
13036
- }
13037
- function splitAssistantParts(parts) {
13038
- let sawBoundary = false;
13039
- const groups = [];
13040
- let current = [];
13041
- for (const part of parts) {
13042
- const type = part.type;
13043
- if (type === "step-start") {
13044
- sawBoundary = true;
13045
- if (current.length > 0) groups.push(current);
13046
- current = [part];
13047
- continue;
13048
- }
13049
- current.push(part);
13050
- if (type === "step-finish") {
13051
- sawBoundary = true;
13052
- groups.push(current);
13053
- current = [];
13054
- }
13055
- }
13056
- if (current.length > 0) groups.push(current);
13057
- return sawBoundary ? groups : [parts];
13058
- }
13059
- function textFromFilePart(part) {
13060
- const filename = typeof part.filename === "string" ? part.filename : void 0;
13061
- const url = typeof part.url === "string" ? part.url : void 0;
13062
- const mime = typeof part.mime === "string" ? part.mime : void 0;
13063
- const label = filename ?? url;
13064
- if (!label) return void 0;
13065
- return mime ? `[file:${mime}] ${label}` : `[file] ${label}`;
13066
- }
13067
- function buildUserStep(entry, stepId, defaultModelName) {
13068
- const textParts = [];
13069
- const extra = {};
13070
- for (const part of entry.parts) {
13071
- if (part.type === "text" && typeof part.text === "string") {
13072
- if (part.text.trim()) textParts.push(part.text.trim());
13073
- continue;
13074
- }
13075
- if (part.type === "file") {
13076
- const fileText = textFromFilePart(part);
13077
- if (fileText) textParts.push(fileText);
13078
- }
13079
- }
13080
- const message = textParts.join("\n\n").trim();
13081
- if (!message) return null;
13082
- if (entry.info.agent) extra.agent = entry.info.agent;
13083
- if (entry.info.tools) extra.tools = entry.info.tools;
13084
- return {
13085
- step_id: stepId,
13086
- timestamp: timeFromObject(entry.info.time),
13087
- source: "user",
13088
- message,
13089
- model_name: defaultModelName,
13090
- extra: compactExtra3(extra)
13091
- };
13092
- }
13093
- function buildAgentStep(parts, info, stepId, defaultModelName, fallbackMetrics) {
13094
- const textParts = [];
13095
- const reasoningParts = [];
13096
- const toolCalls = [];
13097
- const observationResults = [];
13098
- const extra = {};
13099
- let metrics;
13100
- let timestamp = timeFromObject(info.time);
13101
- for (const part of parts) {
13102
- if (!timestamp) timestamp = timeFromObject(part.time);
13103
- switch (part.type) {
13104
- case "text":
13105
- if (typeof part.text === "string" && part.text.trim()) {
13106
- textParts.push(part.text.trim());
13107
- }
13108
- break;
13109
- case "reasoning":
13110
- if (typeof part.text === "string" && part.text.trim()) {
13111
- reasoningParts.push(part.text.trim());
13112
- }
13113
- break;
13114
- case "file": {
13115
- const fileText = textFromFilePart(part);
13116
- if (fileText) textParts.push(fileText);
13117
- break;
13118
- }
13119
- case "tool": {
13120
- const state = asObject3(part.state) ?? {};
13121
- const callID = typeof part.callID === "string" && part.callID || typeof part.id === "string" && part.id || "";
13122
- const toolName = typeof part.tool === "string" && part.tool || "tool";
13123
- const input = argsFromUnknown(state.input);
13124
- toolCalls.push({
13125
- tool_call_id: callID,
13126
- function_name: toolName,
13127
- arguments: input
13128
- });
13129
- const output = state.output ?? state.error;
13130
- if (output !== void 0 && output !== null) {
13131
- observationResults.push({
13132
- source_call_id: callID || void 0,
13133
- content: stringify2(output)
13134
- });
13135
- }
13136
- if (state.status) extra.status = state.status;
13137
- if (state.metadata) extra.tool_metadata = state.metadata;
13138
- if (part.metadata) extra.part_metadata = part.metadata;
13139
- break;
13140
- }
13141
- case "step-finish":
13142
- metrics = metricsFromTokens(part.tokens, part.cost) ?? metrics;
13143
- if (part.reason) extra.finish_reason = part.reason;
13144
- if (part.snapshot) extra.snapshot = part.snapshot;
13145
- break;
13146
- case "patch":
13147
- extra.patches = [...asArray(extra.patches), part];
13148
- break;
13149
- case "agent":
13150
- if (part.name) extra.agent = part.name;
13151
- break;
13152
- case "retry":
13153
- extra.retry = part;
13154
- break;
13155
- }
13156
- }
13157
- metrics = metrics ?? fallbackMetrics;
13158
- if (textParts.length === 0 && reasoningParts.length === 0 && toolCalls.length === 0 && !metrics) {
13159
- return null;
13160
- }
13161
- const observation = observationResults.length > 0 ? { results: observationResults } : void 0;
13162
- const step = {
13163
- step_id: stepId,
13164
- timestamp,
13165
- source: "agent",
13166
- message: textParts.length > 0 ? textParts.join("\n\n") : "(tool use)",
13167
- model_name: modelNameFromInfo(info) ?? defaultModelName
13168
- };
13169
- if (reasoningParts.length > 0)
13170
- step.reasoning_content = reasoningParts.join("\n\n");
13171
- if (toolCalls.length > 0) step.tool_calls = toolCalls;
13172
- if (observation) step.observation = observation;
13173
- if (metrics) step.metrics = metrics;
13174
- const compactedExtra = compactExtra3(extra);
13175
- if (compactedExtra) step.extra = compactedExtra;
13176
- return step;
13177
- }
13178
- function buildUnavailableAgentStep(info, stepId, defaultModelName) {
13179
- const metrics = metricsFromInfo(info);
13180
- if (!metrics && !modelNameFromInfo(info) && !info.error && !info.finish) {
13181
- return null;
13182
- }
13183
- return {
13184
- step_id: stepId,
13185
- timestamp: timeFromObject(info.time),
13186
- source: "agent",
13187
- message: "(message unavailable)",
13188
- model_name: modelNameFromInfo(info) ?? defaultModelName,
13189
- metrics,
13190
- extra: compactExtra3({
13191
- content_unavailable: true,
13192
- finish_reason: info.finish,
13193
- error: info.error
13194
- })
13195
- };
13196
- }
13197
- function convertMessageEntriesToTrajectory(entries, sessionId, exportInfo) {
13198
- if (entries.length === 0) return null;
13199
- const orderedEntries = sortEntries(entries);
13200
- const defaultModelName = orderedEntries.map((entry) => modelNameFromInfo(entry.info)).find((model) => typeof model === "string");
13201
- const steps = [];
13202
- for (const entry of orderedEntries) {
13203
- const role = entry.info.role;
13204
- if (role === "user") {
13205
- const step = buildUserStep(entry, steps.length + 1, defaultModelName);
13206
- if (step) steps.push(step);
13207
- continue;
13208
- }
13209
- if (role !== "assistant") continue;
13210
- if (entry.parts.length === 0) {
13211
- const step = buildUnavailableAgentStep(
13212
- entry.info,
13213
- steps.length + 1,
13214
- defaultModelName
13215
- );
13216
- if (step) steps.push(step);
13217
- continue;
13218
- }
13219
- const groups = splitAssistantParts(entry.parts);
13220
- const hasPartMetrics = groups.some(
13221
- (group) => group.some((part) => part.type === "step-finish")
13222
- );
13223
- const fallbackMetrics = hasPartMetrics ? void 0 : metricsFromInfo(entry.info);
13224
- for (let i = 0; i < groups.length; i++) {
13225
- const step = buildAgentStep(
13226
- groups[i],
13227
- entry.info,
13228
- steps.length + 1,
13229
- defaultModelName,
13230
- i === 0 ? fallbackMetrics : void 0
13231
- );
13232
- if (step) steps.push(step);
13233
- }
13234
12028
  }
13235
- if (steps.length === 0) return null;
13236
- return {
13237
- schema_version: ATIF_VERSION,
13238
- session_id: getSessionId(sessionId, exportInfo, orderedEntries),
13239
- agent: {
13240
- name: "opencode",
13241
- version: getAgentVersion(exportInfo),
13242
- model_name: defaultModelName
13243
- },
13244
- steps,
13245
- final_metrics: finalMetricsFromSteps2(steps)
13246
- };
13247
- }
13248
- function convertRunEventsToTrajectory(events, sessionId) {
13249
- const session = sessionId ?? (events.map((event) => event.sessionID).find((sid) => typeof sid === "string") || "unknown");
13250
- const turns = [];
13251
- let current = null;
13252
- for (const event of events) {
13253
- const type = event.type;
13254
- if (type === "step_start") {
13255
- current = { parts: [], timestamp: event.timestamp };
13256
- continue;
13257
- }
13258
- if (type === "step_finish") {
13259
- if (current) {
13260
- current.finish = asObject3(event.part) ?? {};
13261
- turns.push(current);
13262
- current = null;
13263
- }
13264
- continue;
13265
- }
13266
- if (current && (type === "text" || type === "reasoning" || type === "tool_use")) {
13267
- const part = asObject3(event.part);
13268
- if (part) current.parts.push(part);
13269
- }
13270
- }
13271
- const steps = [];
13272
- for (const turn of turns) {
13273
- const parts = [...turn.parts];
13274
- if (turn.finish) {
13275
- parts.push({ ...turn.finish, type: "step-finish" });
13276
- }
13277
- const step = buildAgentStep(
13278
- parts,
13279
- { time: { created: turn.timestamp } },
13280
- steps.length + 1,
13281
- void 0
13282
- );
13283
- if (step) steps.push(step);
13284
- }
13285
- if (steps.length === 0) return null;
13286
- return {
13287
- schema_version: ATIF_VERSION,
13288
- session_id: session,
13289
- agent: {
13290
- name: "opencode",
13291
- version: "unknown"
13292
- },
13293
- steps,
13294
- final_metrics: finalMetricsFromSteps2(steps)
13295
- };
13296
- }
13297
- function isRunEvent(lines) {
13298
- return lines.some(
13299
- (line) => ["step_start", "step_finish", "text", "reasoning", "tool_use"].includes(
13300
- String(line.type ?? "")
13301
- )
13302
- );
13303
- }
13304
- function convertOpenCodeToTrajectory(content, sessionId) {
13305
- const trimmed = content.trim();
13306
- if (!trimmed) return null;
13307
12029
  try {
13308
- const parsed = JSON.parse(trimmed);
13309
- const parsedObj = asObject3(parsed);
13310
- const messages = asArray(parsedObj?.messages);
13311
- if (parsedObj && messages.length > 0) {
13312
- const entries2 = messages.flatMap((message) => {
13313
- const entry = entryFromLine(asObject3(message) ?? {});
13314
- return entry ? [entry] : [];
13315
- });
13316
- return convertMessageEntriesToTrajectory(
13317
- entries2,
13318
- sessionId,
13319
- asObject3(parsedObj.info)
13320
- );
13321
- }
12030
+ await fs12.promises.unlink(cursorFileFor(repoRoot, tool, sessionId));
13322
12031
  } catch {
13323
12032
  }
13324
- const lines = parseJsonLines2(content);
13325
- if (lines.length === 0) return null;
13326
- if (isRunEvent(lines)) return convertRunEventsToTrajectory(lines, sessionId);
13327
- const wrappedEntries = entriesFromEventWrappers(lines);
13328
- const entries = wrappedEntries.length > 0 ? wrappedEntries : lines.flatMap((line) => {
13329
- const entry = entryFromLine(line);
13330
- return entry ? [entry] : [];
13331
- });
13332
- return convertMessageEntriesToTrajectory(entries, sessionId);
13333
- }
13334
-
13335
- // src/normalizer/index.ts
13336
- function normalizeContent(sourceName, content, sessionId) {
13337
- switch (sourceName) {
13338
- case "claude":
13339
- return convertClaudeToTrajectory(content, sessionId);
13340
- case "codex":
13341
- return convertCodexToTrajectory(content, sessionId);
13342
- case "cursor":
13343
- return convertCursorToTrajectory(content, sessionId);
13344
- case "opencode":
13345
- return convertOpenCodeToTrajectory(content, sessionId);
13346
- case "copilot-chat":
13347
- return convertCopilotChatToTrajectory(content, sessionId);
13348
- default:
13349
- return null;
13350
- }
13351
12033
  }
13352
- var NormalizeMiddleware = class {
13353
- name = "normalize";
13354
- async process(group) {
13355
- const newFiles = [];
13356
- for (const file of group.files) {
13357
- newFiles.push(file);
13358
- if (!file.absolutePath.endsWith(".jsonl")) continue;
13359
- if (!["claude", "codex", "cursor", "opencode", "copilot-chat"].includes(
13360
- file.sourceName
13361
- ))
13362
- continue;
13363
- const sessionId = file.metadata?.sessionId ?? (file.sourceName === "codex" ? void 0 : path13.basename(file.absolutePath, ".jsonl"));
13364
- try {
13365
- const content = file.content ? file.content.toString("utf-8") : null;
13366
- if (!content) continue;
13367
- const trajectory = normalizeContent(
13368
- file.sourceName,
13369
- content,
13370
- sessionId
13371
- );
13372
- if (!trajectory) {
13373
- appendLog(
13374
- "warn",
13375
- `normalize: produced no ATIF for source=${file.sourceName} session=${sessionId ?? "<unknown>"} file=${file.absolutePath}`
13376
- );
13377
- continue;
13378
- }
13379
- const json = JSON.stringify(
13380
- excludeNone(trajectory),
13381
- null,
13382
- 2
13383
- );
13384
- const atifPath = file.absolutePath.replace(/\.jsonl$/, ".atif.json");
13385
- newFiles.push({
13386
- sourceName: file.sourceName,
13387
- absolutePath: atifPath,
13388
- repoPath: file.repoPath,
13389
- metadata: { ...file.metadata, isAtif: true },
13390
- content: Buffer.from(json, "utf-8")
13391
- });
13392
- } catch (err) {
13393
- appendLog(
13394
- "warn",
13395
- `normalize: skipped ATIF for source=${file.sourceName} session=${sessionId ?? "<unknown>"} file=${file.absolutePath}: ${err instanceof Error ? err.message : String(err)}`
13396
- );
13397
- }
13398
- }
13399
- return { ...group, files: newFiles };
13400
- }
13401
- };
13402
-
13403
- // src/upload-state.ts
13404
- import crypto2 from "crypto";
13405
- import fs10 from "fs";
13406
- import os5 from "os";
13407
- import path14 from "path";
13408
- var CURRENT_SCHEMA_VERSION2 = 1;
13409
- var DEFAULT_STATE_DIR = path14.join(
13410
- os5.homedir(),
13411
- ".hillclimb",
13412
- "agent-uploads"
13413
- );
13414
- var DEFAULT_LOCK_WAIT_MS = 5 * 60 * 1e3;
13415
- var DEFAULT_LOCK_RETRY_DELAY_MS = 500;
13416
- var DEFAULT_STATE_TTL_MS = 24 * 60 * 60 * 1e3;
13417
- function stateDir2() {
13418
- return process.env.HILLCLIMB_UPLOAD_STATE_DIR ?? DEFAULT_STATE_DIR;
13419
- }
13420
- function readPositiveEnvMs(name, fallback) {
13421
- const raw = process.env[name];
13422
- if (raw === void 0) return fallback;
13423
- const n = Number(raw);
13424
- return Number.isFinite(n) && n >= 0 ? n : fallback;
13425
- }
13426
- function stateTtlMs() {
13427
- return readPositiveEnvMs(
13428
- "HILLCLIMB_UPLOAD_STATE_TTL_MS",
13429
- DEFAULT_STATE_TTL_MS
13430
- );
13431
- }
13432
- function lockRetryDelayMs() {
13433
- return Math.max(
13434
- 1,
13435
- readPositiveEnvMs(
13436
- "HILLCLIMB_UPLOAD_LOCK_RETRY_DELAY_MS",
13437
- DEFAULT_LOCK_RETRY_DELAY_MS
13438
- )
13439
- );
12034
+ function cursorFileFor(repoRoot, tool, sessionId) {
12035
+ return `${stateFileFor(repoRoot, tool, sessionId)}.cursor.json`;
13440
12036
  }
13441
- function lockRetries() {
13442
- return Math.max(
13443
- 1,
13444
- Math.ceil(
13445
- readPositiveEnvMs("HILLCLIMB_UPLOAD_LOCK_WAIT_MS", DEFAULT_LOCK_WAIT_MS) / lockRetryDelayMs()
13446
- )
13447
- );
13448
- }
13449
- function stateFileFor(repoRoot, tool, sessionId) {
13450
- const hash = crypto2.createHash("sha256").update(`${path14.resolve(repoRoot)}\0${tool}\0${sessionId}`).digest("hex").slice(0, 16);
13451
- return path14.join(stateDir2(), `${hash}.json`);
13452
- }
13453
- function lockFileFor(repoRoot, tool, sessionId) {
13454
- return `${stateFileFor(repoRoot, tool, sessionId)}.lock`;
13455
- }
13456
- async function readUploadState(repoRoot, tool, sessionId) {
12037
+ async function readCursorState(repoRoot, tool, sessionId) {
13457
12038
  try {
13458
- const raw = await fs10.promises.readFile(
13459
- stateFileFor(repoRoot, tool, sessionId),
12039
+ const raw = await fs12.promises.readFile(
12040
+ cursorFileFor(repoRoot, tool, sessionId),
13460
12041
  "utf-8"
13461
12042
  );
13462
12043
  const parsed = JSON.parse(raw);
13463
- if (parsed.schemaVersion !== CURRENT_SCHEMA_VERSION2) return null;
12044
+ if (typeof parsed.contributionId !== "string" || typeof parsed.epoch !== "number" || typeof parsed.turnCount !== "number" || typeof parsed.rawByteOffset !== "number" || typeof parsed.rawPrefixSha256 !== "string" || typeof parsed.snapshotUploaded !== "boolean") {
12045
+ return null;
12046
+ }
13464
12047
  return parsed;
13465
12048
  } catch {
13466
12049
  return null;
13467
12050
  }
13468
12051
  }
13469
- async function writeUploadState(state) {
13470
- const file = stateFileFor(state.repoRoot, state.tool, state.sessionId);
13471
- await fs10.promises.mkdir(stateDir2(), { recursive: true, mode: 448 });
12052
+ async function writeCursorState(repoRoot, tool, sessionId, cursor) {
12053
+ const file = cursorFileFor(repoRoot, tool, sessionId);
12054
+ await fs12.promises.mkdir(stateDir2(), { recursive: true, mode: 448 });
13472
12055
  const tmp = `${file}.tmp`;
13473
- await fs10.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
12056
+ await fs12.promises.writeFile(tmp, JSON.stringify(cursor, null, 2), {
13474
12057
  mode: 384
13475
12058
  });
13476
- await fs10.promises.rename(tmp, file);
13477
- }
13478
- async function deleteUploadState(repoRoot, tool, sessionId) {
13479
- try {
13480
- await fs10.promises.unlink(stateFileFor(repoRoot, tool, sessionId));
13481
- } catch {
13482
- }
12059
+ await fs12.promises.rename(tmp, file);
13483
12060
  }
13484
12061
  async function acquireLock2(repoRoot, tool, sessionId, retries = lockRetries(), delayMs = lockRetryDelayMs()) {
13485
12062
  const lockPath = lockFileFor(repoRoot, tool, sessionId);
13486
- await fs10.promises.mkdir(stateDir2(), { recursive: true, mode: 448 });
12063
+ await fs12.promises.mkdir(stateDir2(), { recursive: true, mode: 448 });
13487
12064
  for (let i = 0; i < retries; i++) {
13488
12065
  try {
13489
- const fd = await fs10.promises.open(
12066
+ const fd = await fs12.promises.open(
13490
12067
  lockPath,
13491
- fs10.constants.O_CREAT | fs10.constants.O_EXCL | fs10.constants.O_WRONLY
12068
+ fs12.constants.O_CREAT | fs12.constants.O_EXCL | fs12.constants.O_WRONLY
13492
12069
  );
13493
12070
  try {
13494
12071
  await fd.write(String(process.pid));
@@ -13511,7 +12088,7 @@ async function acquireLock2(repoRoot, tool, sessionId, retries = lockRetries(),
13511
12088
  }
13512
12089
  async function releaseLock2(repoRoot, tool, sessionId) {
13513
12090
  try {
13514
- await fs10.promises.unlink(lockFileFor(repoRoot, tool, sessionId));
12091
+ await fs12.promises.unlink(lockFileFor(repoRoot, tool, sessionId));
13515
12092
  } catch {
13516
12093
  }
13517
12094
  }
@@ -13523,20 +12100,43 @@ async function withUploadLock(repoRoot, tool, sessionId, fn) {
13523
12100
  await releaseLock2(repoRoot, tool, sessionId);
13524
12101
  }
13525
12102
  }
12103
+ function isProcessAlive(pid) {
12104
+ try {
12105
+ process.kill(pid, 0);
12106
+ return true;
12107
+ } catch (err) {
12108
+ return err.code === "EPERM";
12109
+ }
12110
+ }
13526
12111
  async function sweepStaleUploadStates(ttlMs = stateTtlMs(), now = Date.now()) {
13527
12112
  let entries;
13528
12113
  try {
13529
- entries = await fs10.promises.readdir(stateDir2(), { withFileTypes: true });
12114
+ entries = await fs12.promises.readdir(stateDir2(), { withFileTypes: true });
13530
12115
  } catch {
13531
12116
  return;
13532
12117
  }
13533
12118
  for (const entry of entries) {
13534
12119
  if (!entry.isFile()) continue;
13535
- const file = path14.join(stateDir2(), entry.name);
12120
+ const file = path13.join(stateDir2(), entry.name);
13536
12121
  try {
13537
- const st = await fs10.promises.stat(file);
12122
+ if (entry.name.endsWith(".lock")) {
12123
+ const raw = await fs12.promises.readFile(file, "utf-8").catch(() => "");
12124
+ const pid = Number.parseInt(raw.trim(), 10);
12125
+ if (Number.isInteger(pid) && pid > 0) {
12126
+ if (!isProcessAlive(pid)) {
12127
+ await fs12.promises.unlink(file);
12128
+ }
12129
+ } else {
12130
+ const st2 = await fs12.promises.stat(file);
12131
+ if (now - st2.mtimeMs > STALE_LOCK_TTL_MS) {
12132
+ await fs12.promises.unlink(file);
12133
+ }
12134
+ }
12135
+ continue;
12136
+ }
12137
+ const st = await fs12.promises.stat(file);
13538
12138
  if (now - st.mtimeMs > ttlMs) {
13539
- await fs10.promises.unlink(file);
12139
+ await fs12.promises.unlink(file);
13540
12140
  }
13541
12141
  } catch {
13542
12142
  }
@@ -13552,20 +12152,17 @@ async function readStdin() {
13552
12152
  }
13553
12153
  return Buffer.concat(chunks).toString("utf-8");
13554
12154
  }
13555
- function sanitize2(value) {
13556
- return value.replace(/[^a-zA-Z0-9._-]/g, "_");
13557
- }
13558
12155
  function formatEpochSeconds2(date) {
13559
12156
  return String(Math.floor(date.getTime() / 1e3));
13560
12157
  }
13561
12158
  function newFlowId() {
13562
- return crypto3.randomBytes(3).toString("hex");
12159
+ return crypto4.randomBytes(3).toString("hex");
13563
12160
  }
13564
12161
  function lineHasAssistant(line) {
13565
12162
  return line.includes('"type":"assistant"') || line.includes('"role":"assistant"') || line.includes('"type":"assistant.');
13566
12163
  }
13567
12164
  async function hasAssistantMessage(transcriptPath) {
13568
- const stream = fs11.createReadStream(transcriptPath, { encoding: "utf-8" });
12165
+ const stream = fs13.createReadStream(transcriptPath, { encoding: "utf-8" });
13569
12166
  let buffer = "";
13570
12167
  try {
13571
12168
  for await (const chunk of stream) {
@@ -13587,15 +12184,6 @@ async function hasAssistantMessage(transcriptPath) {
13587
12184
  }
13588
12185
  return false;
13589
12186
  }
13590
- function hashFileSha256(file) {
13591
- return new Promise((resolve, reject) => {
13592
- const hash = crypto3.createHash("sha256");
13593
- const stream = fs11.createReadStream(file);
13594
- stream.on("data", (chunk) => hash.update(chunk));
13595
- stream.on("error", reject);
13596
- stream.on("end", () => resolve(hash.digest("hex")));
13597
- });
13598
- }
13599
12187
  async function resolveSourceTool(payload, repoRoot) {
13600
12188
  const inferred = inferSourceToolFromPayload(payload);
13601
12189
  if (inferred) return inferred;
@@ -13636,7 +12224,7 @@ function summarizePayload(payload) {
13636
12224
  });
13637
12225
  }
13638
12226
  async function codexSessionIdFromFile(filePath) {
13639
- const stream = fs11.createReadStream(filePath, { encoding: "utf-8" });
12227
+ const stream = fs13.createReadStream(filePath, { encoding: "utf-8" });
13640
12228
  const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
13641
12229
  try {
13642
12230
  for await (const line of rl) {
@@ -13656,17 +12244,17 @@ async function codexSessionIdFromFile(filePath) {
13656
12244
  return null;
13657
12245
  }
13658
12246
  async function findCodexTranscriptPath(sessionId) {
13659
- const sessionsDir = process.env.HILLCLIMB_CODEX_SESSIONS_DIR ?? path15.join(os6.homedir(), ".codex", "sessions");
12247
+ const sessionsDir = process.env.HILLCLIMB_CODEX_SESSIONS_DIR ?? path14.join(os6.homedir(), ".codex", "sessions");
13660
12248
  const candidates = [];
13661
12249
  async function walk(dir) {
13662
12250
  let entries;
13663
12251
  try {
13664
- entries = await fs11.promises.readdir(dir, { withFileTypes: true });
12252
+ entries = await fs13.promises.readdir(dir, { withFileTypes: true });
13665
12253
  } catch {
13666
12254
  return;
13667
12255
  }
13668
12256
  for (const entry of entries) {
13669
- const full = path15.join(dir, entry.name);
12257
+ const full = path14.join(dir, entry.name);
13670
12258
  if (entry.isDirectory()) {
13671
12259
  await walk(full);
13672
12260
  } else if (entry.isFile() && entry.name.endsWith(".jsonl") && entry.name.includes(sessionId)) {
@@ -13706,7 +12294,7 @@ function resolveCursorTranscriptPath(payload) {
13706
12294
  const workspace = payload.workspace_roots?.[0];
13707
12295
  if (!id || !workspace) return void 0;
13708
12296
  const encoded = workspace.replace(/^\//, "").replace(/\//g, "-");
13709
- return path15.join(
12297
+ return path14.join(
13710
12298
  os6.homedir(),
13711
12299
  ".cursor",
13712
12300
  "projects",
@@ -13735,6 +12323,7 @@ async function runUploadInner(payload) {
13735
12323
  const sessionId = resolveHookSessionId(payload);
13736
12324
  const cwd = resolveHookCwd(payload);
13737
12325
  const eventKind = classifyHookEvent(payload.hook_event_name);
12326
+ const recordedAt = Date.now();
13738
12327
  if (!sessionId || !cwd) {
13739
12328
  appendLog(
13740
12329
  "warn",
@@ -13756,101 +12345,221 @@ async function runUploadInner(payload) {
13756
12345
  payload.tool = "unknown";
13757
12346
  return false;
13758
12347
  }
13759
- payload.tool = sourceTool;
13760
- appendLog(
13761
- "info",
13762
- `[${sessionId}] payload parsed (tool=${sourceTool}, cwd=${cwd}, event=${eventKind ?? "?"})`
13763
- );
13764
- await selfHealHook(repoRoot, sourceTool);
13765
- const transcriptPath = await resolveTranscriptPath(
13766
- payload,
13767
- sourceTool,
13768
- sessionId
13769
- );
13770
- if (!transcriptPath) {
12348
+ payload.tool = sourceTool;
12349
+ appendLog(
12350
+ "info",
12351
+ `[${sessionId}] payload parsed (tool=${sourceTool}, cwd=${cwd}, event=${eventKind ?? "?"})`
12352
+ );
12353
+ await selfHealHook(repoRoot, sourceTool);
12354
+ const transcriptPath = await resolveTranscriptPath(
12355
+ payload,
12356
+ sourceTool,
12357
+ sessionId
12358
+ );
12359
+ if (!transcriptPath) {
12360
+ appendLog(
12361
+ "warn",
12362
+ `Skipping upload: missing required hook fields (session_id=true, transcript_path=false, cwd=true)`
12363
+ );
12364
+ return false;
12365
+ }
12366
+ const transcriptResolved = path14.resolve(transcriptPath);
12367
+ try {
12368
+ const stat = await fs13.promises.stat(transcriptResolved);
12369
+ if (!stat.isFile()) {
12370
+ appendLog(
12371
+ "warn",
12372
+ `Skipping session ${sessionId}: transcript_path is not a file: ${transcriptResolved}`
12373
+ );
12374
+ return false;
12375
+ }
12376
+ } catch (err) {
12377
+ appendLog(
12378
+ "warn",
12379
+ `Skipping session ${sessionId}: transcript_path not readable (${transcriptResolved}): ${err instanceof Error ? err.message : String(err)}`
12380
+ );
12381
+ return false;
12382
+ }
12383
+ if (!await hasAssistantMessage(transcriptResolved)) {
12384
+ appendLog(
12385
+ "info",
12386
+ `Skipping session ${sessionId}: transcript contains no assistant messages (nothing to upload).`
12387
+ );
12388
+ return false;
12389
+ }
12390
+ return await uploadSession({
12391
+ sessionId,
12392
+ transcriptPath: transcriptResolved,
12393
+ repoRoot,
12394
+ config,
12395
+ sourceTool,
12396
+ eventKind,
12397
+ recordedAt
12398
+ });
12399
+ }
12400
+ var AGENT_MAX_UPLOAD_BYTES = 1024 * 1024 * 1024;
12401
+ var CLI_VERSION = "0.7.0";
12402
+ function makeTranscriptGroup(repoRoot, sourceTool, transcriptPath, content) {
12403
+ const sourceFile = {
12404
+ sourceName: sourceTool,
12405
+ absolutePath: transcriptPath,
12406
+ repoPath: repoRoot,
12407
+ content
12408
+ };
12409
+ return {
12410
+ repoPath: repoRoot,
12411
+ label: path14.basename(repoRoot),
12412
+ files: [sourceFile],
12413
+ sourceNames: [sourceTool],
12414
+ lastModified: /* @__PURE__ */ new Date()
12415
+ };
12416
+ }
12417
+ async function buildRedactChain(repoRoot) {
12418
+ const envFileNames = await discoverEnvFiles(repoRoot);
12419
+ const envFilePaths = envFileNames.map((n) => path14.join(repoRoot, n));
12420
+ const secretResult = await collectSecrets(repoRoot, envFilePaths, []);
12421
+ const chain = [];
12422
+ if (secretResult.values.size > 0) {
12423
+ chain.push(new RedactMiddleware(secretResult.values));
12424
+ }
12425
+ chain.push(new PatternRedactMiddleware());
12426
+ return chain;
12427
+ }
12428
+ async function redactTail(tail, repoRoot, sourceTool, transcriptPath, redactChain) {
12429
+ let group = makeTranscriptGroup(repoRoot, sourceTool, transcriptPath, tail);
12430
+ for (const mw of redactChain) {
12431
+ group = await mw.process(group);
12432
+ }
12433
+ const file = group.files.find((f) => f.absolutePath === transcriptPath);
12434
+ if (!file?.content) {
12435
+ throw new Error("redacted tail missing from middleware output");
12436
+ }
12437
+ return file.content;
12438
+ }
12439
+ async function handleUploadFailure(err, sessionId, repoRoot, sourceTool) {
12440
+ if (err instanceof UploadTooLargeError) {
12441
+ appendLog(
12442
+ "error",
12443
+ `Session ${sessionId} upload skipped: ${err.message}. The transcript snapshot exceeds the platform limit; later turns will keep failing until the session is split or the limit raised.`
12444
+ );
12445
+ return false;
12446
+ }
12447
+ if (err instanceof PlatformError && err.status === 401) {
13771
12448
  appendLog(
13772
- "warn",
13773
- `Skipping upload: missing required hook fields (session_id=true, transcript_path=false, cwd=true)`
12449
+ "error",
12450
+ `Session ${sessionId} upload failed: authentication expired. Re-run \`npx hillclimb\` in ${repoRoot}.`
13774
12451
  );
13775
12452
  return false;
13776
12453
  }
13777
- const transcriptResolved = path15.resolve(transcriptPath);
13778
- try {
13779
- const stat = await fs11.promises.stat(transcriptResolved);
13780
- if (!stat.isFile()) {
13781
- appendLog(
13782
- "warn",
13783
- `Skipping session ${sessionId}: transcript_path is not a file: ${transcriptResolved}`
13784
- );
13785
- return false;
13786
- }
13787
- } catch (err) {
12454
+ if (err instanceof PlatformError && err.status === 400 && /too big|sizebytes/i.test(err.message)) {
13788
12455
  appendLog(
13789
- "warn",
13790
- `Skipping session ${sessionId}: transcript_path not readable (${transcriptResolved}): ${err instanceof Error ? err.message : String(err)}`
12456
+ "error",
12457
+ `Session ${sessionId} upload skipped: the platform rejected the declared size (${err.message}). The server's upload limit is below this CLI's; the upload will keep failing until the platform limit is raised.`
13791
12458
  );
13792
12459
  return false;
13793
12460
  }
13794
- if (!await hasAssistantMessage(transcriptResolved)) {
12461
+ if (err instanceof PlatformError && err.status === 404 && err.code === "CONTRIBUTION_NOT_FOUND") {
12462
+ await deleteUploadState(repoRoot, sourceTool, sessionId);
13795
12463
  appendLog(
13796
- "info",
13797
- `Skipping session ${sessionId}: transcript contains no assistant messages (nothing to upload).`
12464
+ "warn",
12465
+ `Session ${sessionId} upload failed: contribution deleted server-side; local state cleared so the next turn re-creates it.`
13798
12466
  );
13799
12467
  return false;
13800
12468
  }
13801
- return await uploadSession({
12469
+ appendLog(
12470
+ "error",
12471
+ `Session ${sessionId} upload failed: ${err instanceof Error ? err.message : String(err)}`
12472
+ );
12473
+ return false;
12474
+ }
12475
+ async function uploadSession(args) {
12476
+ const {
13802
12477
  sessionId,
13803
- transcriptPath: transcriptResolved,
12478
+ transcriptPath,
13804
12479
  repoRoot,
13805
12480
  config,
13806
12481
  sourceTool,
13807
- eventKind
13808
- });
13809
- }
13810
- async function uploadSession(args) {
13811
- const { sessionId, transcriptPath, repoRoot, config, sourceTool, eventKind } = args;
12482
+ eventKind,
12483
+ recordedAt
12484
+ } = args;
13812
12485
  const isSessionEnd = eventKind === "sessionEnd";
12486
+ const eventLabel = isSessionEnd ? "SessionEnd" : "Stop";
13813
12487
  return await withUploadLock(repoRoot, sourceTool, sessionId, async () => {
13814
12488
  const prior = await readUploadState(repoRoot, sourceTool, sessionId);
13815
- let transcriptSize;
13816
- let transcriptSha256;
13817
- try {
13818
- transcriptSize = (await fs11.promises.stat(transcriptPath)).size;
13819
- transcriptSha256 = await hashFileSha256(transcriptPath);
13820
- } catch {
13821
- }
13822
- if (prior?.contributionId && transcriptSize !== void 0 && transcriptSha256 !== void 0 && transcriptSize === prior.lastTranscriptSize && transcriptSha256 === prior.lastTranscriptSha256) {
13823
- appendLog(
13824
- "info",
13825
- `[${sessionId}] skipping ${sourceTool} ${isSessionEnd ? "SessionEnd" : "Stop"} upload (transcript unchanged at ${transcriptSize} bytes${isSessionEnd ? "; local state cleared" : ""})`
13826
- );
13827
- if (isSessionEnd) {
13828
- await deleteUploadState(repoRoot, sourceTool, sessionId);
12489
+ let cursor = null;
12490
+ let adoptedLegacy = false;
12491
+ let restored = null;
12492
+ if (prior?.contributionId && prior.snapshotUploaded && prior.rawByteOffset !== void 0 && prior.rawPrefixSha256 !== void 0) {
12493
+ cursor = {
12494
+ rawByteOffset: prior.rawByteOffset,
12495
+ rawPrefixSha256: prior.rawPrefixSha256
12496
+ };
12497
+ } else if (prior?.contributionId && prior.rawByteOffset === void 0) {
12498
+ const sidecar = await readCursorState(repoRoot, sourceTool, sessionId);
12499
+ if (sidecar && sidecar.contributionId !== prior.contributionId) {
12500
+ appendLog(
12501
+ "info",
12502
+ `[${sessionId}] ignoring sidecar cursor for stale contribution ${sidecar.contributionId} (state has ${prior.contributionId})`
12503
+ );
12504
+ }
12505
+ if (sidecar && sidecar.contributionId === prior.contributionId && sidecar.snapshotUploaded) {
12506
+ cursor = {
12507
+ rawByteOffset: sidecar.rawByteOffset,
12508
+ rawPrefixSha256: sidecar.rawPrefixSha256
12509
+ };
12510
+ restored = sidecar;
12511
+ appendLog(
12512
+ "info",
12513
+ `[${sessionId}] restored epoch cursor from sidecar after old-CLI state rewrite (epoch=${sidecar.epoch}, turn=${sidecar.turnCount}, offset=${sidecar.rawByteOffset})`
12514
+ );
12515
+ } else if (prior.lastTranscriptSize !== void 0 && prior.lastTranscriptSha256 !== void 0) {
12516
+ const candidate = {
12517
+ rawByteOffset: prior.lastTranscriptSize,
12518
+ rawPrefixSha256: prior.lastTranscriptSha256
12519
+ };
12520
+ if (await cursorMatchesFile(transcriptPath, candidate)) {
12521
+ cursor = candidate;
12522
+ adoptedLegacy = true;
12523
+ appendLog(
12524
+ "info",
12525
+ `[${sessionId}] adopting last uploaded full zip as epoch-001 baseline (legacy state bootstrap, offset=${candidate.rawByteOffset})`
12526
+ );
12527
+ } else {
12528
+ appendLog(
12529
+ "info",
12530
+ `[${sessionId}] legacy state present but prefix mismatch or mid-line offset (offset=${candidate.rawByteOffset}) \u2014 will re-baseline with a fresh snapshot`
12531
+ );
12532
+ }
13829
12533
  }
13830
- return true;
13831
12534
  }
13832
- const now = /* @__PURE__ */ new Date();
13833
- const sourceFile = {
13834
- sourceName: sourceTool,
13835
- absolutePath: transcriptPath,
13836
- repoPath: repoRoot
13837
- };
13838
- const group = {
13839
- repoPath: repoRoot,
13840
- label: path15.basename(repoRoot),
13841
- files: [sourceFile],
13842
- sourceNames: [sourceTool],
13843
- lastModified: now
13844
- };
13845
- const envFileNames = await discoverEnvFiles(repoRoot);
13846
- const envFilePaths = envFileNames.map((n) => path15.join(repoRoot, n));
13847
- const secretResult = await collectSecrets(repoRoot, envFilePaths, []);
13848
- const mwChain = [];
13849
- if (secretResult.values.size > 0) {
13850
- mwChain.push(new RedactMiddleware(secretResult.values));
12535
+ let mode;
12536
+ let transition = "initial";
12537
+ let tail;
12538
+ let nextCursor;
12539
+ if (cursor) {
12540
+ const evaluation = await evaluateTranscript(transcriptPath, cursor);
12541
+ if (evaluation.kind === "unchanged") {
12542
+ appendLog(
12543
+ "info",
12544
+ `[${sessionId}] skipping ${sourceTool} ${eventLabel} upload (no new complete transcript lines past offset ${cursor.rawByteOffset}${isSessionEnd ? "; local state cleared" : ""})`
12545
+ );
12546
+ if (isSessionEnd) {
12547
+ await deleteUploadState(repoRoot, sourceTool, sessionId);
12548
+ }
12549
+ return true;
12550
+ }
12551
+ if (evaluation.kind === "append") {
12552
+ mode = "patch";
12553
+ tail = evaluation.tail;
12554
+ nextCursor = evaluation.nextCursor;
12555
+ } else {
12556
+ mode = "snapshot";
12557
+ transition = evaluation.kind;
12558
+ }
12559
+ } else {
12560
+ mode = "snapshot";
12561
+ transition = prior ? "state-lost" : "initial";
13851
12562
  }
13852
- mwChain.push(new PatternRedactMiddleware());
13853
- mwChain.push(new NormalizeMiddleware());
13854
12563
  const identity = await loadIdentity(config.apiBaseUrl);
13855
12564
  if (!identity) {
13856
12565
  appendLog(
@@ -13863,6 +12572,159 @@ async function uploadSession(args) {
13863
12572
  config.apiBaseUrl,
13864
12573
  identity.sessionCookie
13865
12574
  );
12575
+ const now = /* @__PURE__ */ new Date();
12576
+ const redactChain = await buildRedactChain(repoRoot);
12577
+ const transcriptArchivePath = archivePathFor({
12578
+ sourceName: sourceTool,
12579
+ absolutePath: transcriptPath
12580
+ });
12581
+ const alreadySubmitted = prior?.submitted ?? false;
12582
+ const submitThisUpload = config.autoSubmit && !alreadySubmitted;
12583
+ if (mode === "patch" && cursor && tail && nextCursor && prior?.contributionId) {
12584
+ const baseCursor = cursor;
12585
+ const contributionId2 = prior.contributionId;
12586
+ const epoch2 = prior.epoch ?? restored?.epoch ?? 1;
12587
+ const turn = (prior.turnCount ?? restored?.turnCount ?? 0) + 1;
12588
+ appendLog(
12589
+ "info",
12590
+ `[${sessionId}] ${sourceTool} ${eventLabel} upload \u2192 patch epoch=${epoch2} turn=${turn} (${tail.length} raw tail bytes, contribution ${contributionId2})`
12591
+ );
12592
+ try {
12593
+ const redactedTail = await redactTail(
12594
+ tail,
12595
+ repoRoot,
12596
+ sourceTool,
12597
+ transcriptPath,
12598
+ redactChain
12599
+ );
12600
+ if (adoptedLegacy) {
12601
+ await uploadArtifact(
12602
+ client,
12603
+ contributionId2,
12604
+ metaFilename(epoch2, recordedAt),
12605
+ "application/json",
12606
+ buildEpochMeta({
12607
+ sessionId,
12608
+ tool: sourceTool,
12609
+ cliVersion: CLI_VERSION,
12610
+ epoch: epoch2,
12611
+ transitionKind: "legacy-adopted",
12612
+ baseline: "legacy-latest-zip",
12613
+ transcriptArchivePath,
12614
+ cursor: baseCursor,
12615
+ recordedAt
12616
+ })
12617
+ );
12618
+ }
12619
+ await uploadArtifact(
12620
+ client,
12621
+ contributionId2,
12622
+ patchFilename(epoch2, turn, recordedAt),
12623
+ "application/gzip",
12624
+ gzipPatch(redactedTail),
12625
+ AGENT_MAX_UPLOAD_BYTES
12626
+ );
12627
+ } catch (err) {
12628
+ return handleUploadFailure(err, sessionId, repoRoot, sourceTool);
12629
+ }
12630
+ const persistState = async (submitted2) => {
12631
+ await writeUploadState({
12632
+ schemaVersion: CURRENT_SCHEMA_VERSION2,
12633
+ sessionId,
12634
+ tool: sourceTool,
12635
+ repoRoot,
12636
+ projectId: config.projectId,
12637
+ contributionId: contributionId2,
12638
+ submitted: submitted2,
12639
+ uploadCount: (prior.uploadCount ?? 0) + (adoptedLegacy ? 2 : 1),
12640
+ firstUploadedAt: prior.firstUploadedAt ?? now.toISOString(),
12641
+ lastUploadedAt: now.toISOString(),
12642
+ // Mirrors the cursor from here on: describes the raw bytes covered
12643
+ // by uploads, not the whole file (which may end in a torn line). An
12644
+ // old CLI reading these sees a mismatch against the grown file and
12645
+ // safely falls back to a full-zip upload.
12646
+ lastTranscriptSize: nextCursor.rawByteOffset,
12647
+ lastTranscriptSha256: nextCursor.rawPrefixSha256,
12648
+ epoch: epoch2,
12649
+ turnCount: turn,
12650
+ rawByteOffset: nextCursor.rawByteOffset,
12651
+ rawPrefixSha256: nextCursor.rawPrefixSha256,
12652
+ snapshotUploaded: true
12653
+ });
12654
+ await writeCursorState(repoRoot, sourceTool, sessionId, {
12655
+ contributionId: contributionId2,
12656
+ epoch: epoch2,
12657
+ turnCount: turn,
12658
+ rawByteOffset: nextCursor.rawByteOffset,
12659
+ rawPrefixSha256: nextCursor.rawPrefixSha256,
12660
+ snapshotUploaded: true
12661
+ });
12662
+ };
12663
+ await persistState(alreadySubmitted);
12664
+ let submitted = alreadySubmitted;
12665
+ if (submitThisUpload) {
12666
+ try {
12667
+ appendLog("info", `submitting contribution ${contributionId2}`);
12668
+ await client.submitContribution(contributionId2);
12669
+ submitted = true;
12670
+ appendLog("info", `contribution ${contributionId2} submitted`);
12671
+ } catch (err) {
12672
+ if (err instanceof PlatformError && err.status === 409 && err.code === "CONTRIBUTION_WRONG_STATE") {
12673
+ submitted = true;
12674
+ appendLog(
12675
+ "info",
12676
+ `contribution ${contributionId2} was already submitted (409); recording locally`
12677
+ );
12678
+ } else {
12679
+ appendLog(
12680
+ "warn",
12681
+ `[${sessionId}] submit failed (patch is durable; will retry next turn): ${err instanceof Error ? err.message : String(err)}`
12682
+ );
12683
+ }
12684
+ }
12685
+ if (submitted) await persistState(true);
12686
+ }
12687
+ appendLog(
12688
+ "info",
12689
+ `Uploaded session ${sessionId} patch epoch=${epoch2} turn=${turn} to contribution ${contributionId2} (offset ${baseCursor.rawByteOffset} \u2192 ${nextCursor.rawByteOffset})`
12690
+ );
12691
+ if (isSessionEnd) {
12692
+ await deleteUploadState(repoRoot, sourceTool, sessionId);
12693
+ appendLog(
12694
+ "info",
12695
+ `[${sessionId}] session complete \u2014 contribution ${contributionId2}, epoch ${epoch2}, ${turn} patch(es); local state cleared`
12696
+ );
12697
+ }
12698
+ return true;
12699
+ }
12700
+ let raw;
12701
+ try {
12702
+ raw = await fs13.promises.readFile(transcriptPath);
12703
+ } catch (err) {
12704
+ if (err.code === "ERR_FS_FILE_TOO_LARGE") {
12705
+ appendLog(
12706
+ "error",
12707
+ `Session ${sessionId} snapshot impossible: transcript is over Node's 2 GiB read limit (${transcriptPath}). Established patch chains keep working, but a (re-)baseline of this session cannot upload until snapshots are built streaming.`
12708
+ );
12709
+ } else {
12710
+ appendLog(
12711
+ "warn",
12712
+ `Skipping session ${sessionId}: transcript unreadable (${transcriptPath}): ${err instanceof Error ? err.message : String(err)}`
12713
+ );
12714
+ }
12715
+ return false;
12716
+ }
12717
+ const truncated = truncateAtLastNewline(raw);
12718
+ if (!truncated) {
12719
+ appendLog(
12720
+ "info",
12721
+ `[${sessionId}] skipping ${sourceTool} ${eventLabel} upload (no complete transcript line yet)`
12722
+ );
12723
+ if (isSessionEnd)
12724
+ await deleteUploadState(repoRoot, sourceTool, sessionId);
12725
+ return true;
12726
+ }
12727
+ const epoch = (prior?.epoch ?? restored?.epoch ?? 0) + 1;
13866
12728
  const shortId = sessionId.slice(0, 12);
13867
12729
  const toolLabels = {
13868
12730
  cursor: "Cursor",
@@ -13872,16 +12734,19 @@ async function uploadSession(args) {
13872
12734
  opencode: "opencode"
13873
12735
  };
13874
12736
  const toolLabel2 = toolLabels[sourceTool] ?? "Claude";
13875
- const epochSeconds = formatEpochSeconds2(now);
13876
- const seq = (prior?.uploadCount ?? 0) + 1;
13877
- const title = `${toolLabel2} session ${shortId} \u2014 ${epochSeconds}`;
12737
+ const title = `${toolLabel2} session ${shortId} \u2014 ${formatEpochSeconds2(now)}`;
13878
12738
  const body = `Session ID: ${sessionId}
13879
12739
  Tool: ${toolLabel2}
13880
12740
  Repo: ${repoRoot}
13881
12741
  Uploaded: ${now.toISOString()}`;
13882
- const zipFilename = `${sourceTool}-${sanitize2(shortId)}-${epochSeconds}-${String(seq).padStart(3, "0")}.zip`;
13883
- const alreadySubmitted = prior?.submitted ?? false;
13884
- const submitThisUpload = config.autoSubmit && !alreadySubmitted;
12742
+ const zipFilename = snapshotFilename(epoch, recordedAt);
12743
+ const group = makeTranscriptGroup(
12744
+ repoRoot,
12745
+ sourceTool,
12746
+ transcriptPath,
12747
+ truncated.covered
12748
+ );
12749
+ const mwChain = redactChain;
13885
12750
  const output = new PlatformUploadOutput({
13886
12751
  client,
13887
12752
  projectId: config.projectId,
@@ -13891,6 +12756,7 @@ Uploaded: ${now.toISOString()}`;
13891
12756
  zipFilename,
13892
12757
  autoSubmit: submitThisUpload,
13893
12758
  existingContributionId: prior?.contributionId ?? void 0,
12759
+ maxUploadBytes: AGENT_MAX_UPLOAD_BYTES,
13894
12760
  // Persist the new contribution id before the file PUT so a failed
13895
12761
  // upload can't make the next Stop create a second contribution.
13896
12762
  onContributionCreated: (id) => writeUploadState({
@@ -13908,10 +12774,10 @@ Uploaded: ${now.toISOString()}`;
13908
12774
  lastTranscriptSha256: prior?.lastTranscriptSha256
13909
12775
  })
13910
12776
  });
13911
- const reuseDesc = prior?.contributionId ? `reusing contribution ${prior.contributionId} (file #${seq})` : prior ? "new contribution (file #1; prior state had no contribution \u2014 earlier create may have failed)" : "new contribution (file #1; first upload for session)";
12777
+ 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)";
13912
12778
  appendLog(
13913
12779
  "info",
13914
- `[${sessionId}] ${sourceTool} ${isSessionEnd ? "SessionEnd" : "Stop"} upload \u2192 ${reuseDesc}`
12780
+ `[${sessionId}] ${sourceTool} ${eventLabel} upload \u2192 snapshot epoch=${epoch} transition=${transition} (${truncated.covered.length} raw bytes, ${reuseDesc})`
13915
12781
  );
13916
12782
  let contributionId;
13917
12783
  try {
@@ -13919,20 +12785,36 @@ Uploaded: ${now.toISOString()}`;
13919
12785
  selectedSources: [sourceTool]
13920
12786
  });
13921
12787
  } catch (err) {
13922
- if (err instanceof PlatformError && err.status === 401) {
13923
- appendLog(
13924
- "error",
13925
- `Session ${sessionId} upload failed: authentication expired. Re-run \`npx hillclimb\` in ${repoRoot}.`
13926
- );
13927
- return false;
13928
- }
12788
+ return handleUploadFailure(err, sessionId, repoRoot, sourceTool);
12789
+ }
12790
+ let metaUploaded = 0;
12791
+ try {
12792
+ await uploadArtifact(
12793
+ client,
12794
+ contributionId,
12795
+ metaFilename(epoch, recordedAt),
12796
+ "application/json",
12797
+ buildEpochMeta({
12798
+ sessionId,
12799
+ tool: sourceTool,
12800
+ cliVersion: CLI_VERSION,
12801
+ epoch,
12802
+ transitionKind: transition,
12803
+ baseline: "snapshot",
12804
+ snapshotFilename: zipFilename,
12805
+ transcriptArchivePath,
12806
+ cursor: truncated.cursor,
12807
+ recordedAt
12808
+ })
12809
+ );
12810
+ metaUploaded = 1;
12811
+ } catch (err) {
13929
12812
  appendLog(
13930
- "error",
13931
- `Session ${sessionId} upload failed: ${err instanceof Error ? err.message : String(err)}`
12813
+ "warn",
12814
+ `[${sessionId}] epoch meta upload failed (snapshot is durable; continuing): ${err instanceof Error ? err.message : String(err)}`
13932
12815
  );
13933
- return false;
13934
12816
  }
13935
- const next = {
12817
+ await writeUploadState({
13936
12818
  schemaVersion: CURRENT_SCHEMA_VERSION2,
13937
12819
  sessionId,
13938
12820
  tool: sourceTool,
@@ -13940,22 +12822,35 @@ Uploaded: ${now.toISOString()}`;
13940
12822
  projectId: config.projectId,
13941
12823
  contributionId,
13942
12824
  submitted: alreadySubmitted || submitThisUpload,
13943
- uploadCount: seq,
12825
+ uploadCount: (prior?.uploadCount ?? 0) + 1 + metaUploaded,
13944
12826
  firstUploadedAt: prior?.firstUploadedAt ?? now.toISOString(),
13945
12827
  lastUploadedAt: now.toISOString(),
13946
- lastTranscriptSize: transcriptSize,
13947
- lastTranscriptSha256: transcriptSha256
13948
- };
13949
- await writeUploadState(next);
12828
+ // Mirrors the cursor (see patch path).
12829
+ lastTranscriptSize: truncated.cursor.rawByteOffset,
12830
+ lastTranscriptSha256: truncated.cursor.rawPrefixSha256,
12831
+ epoch,
12832
+ turnCount: 0,
12833
+ rawByteOffset: truncated.cursor.rawByteOffset,
12834
+ rawPrefixSha256: truncated.cursor.rawPrefixSha256,
12835
+ snapshotUploaded: true
12836
+ });
12837
+ await writeCursorState(repoRoot, sourceTool, sessionId, {
12838
+ contributionId,
12839
+ epoch,
12840
+ turnCount: 0,
12841
+ rawByteOffset: truncated.cursor.rawByteOffset,
12842
+ rawPrefixSha256: truncated.cursor.rawPrefixSha256,
12843
+ snapshotUploaded: true
12844
+ });
13950
12845
  appendLog(
13951
12846
  "info",
13952
- `Uploaded session ${sessionId} to project ${config.projectSlug} (${config.projectId}) as contribution ${contributionId} (seq=${seq})`
12847
+ `Uploaded session ${sessionId} snapshot epoch=${epoch} to project ${config.projectSlug} (${config.projectId}) as contribution ${contributionId}`
13953
12848
  );
13954
12849
  if (isSessionEnd) {
13955
12850
  await deleteUploadState(repoRoot, sourceTool, sessionId);
13956
12851
  appendLog(
13957
12852
  "info",
13958
- `[${sessionId}] session complete \u2014 contribution ${contributionId}, ${seq} file(s); local state cleared`
12853
+ `[${sessionId}] session complete \u2014 contribution ${contributionId}, epoch ${epoch} snapshot; local state cleared`
13959
12854
  );
13960
12855
  }
13961
12856
  return true;
@@ -14087,18 +12982,18 @@ async function runUploadWorker() {
14087
12982
 
14088
12983
  // src/git-traces/index.ts
14089
12984
  import { spawn as spawn3 } from "child_process";
14090
- import crypto5 from "crypto";
12985
+ import crypto6 from "crypto";
14091
12986
 
14092
12987
  // src/git-traces/handlers.ts
14093
12988
  import { execFileSync as execFileSync3 } from "child_process";
14094
- import path18 from "path";
12989
+ import path17 from "path";
14095
12990
 
14096
12991
  // src/git-traces/git-ops.ts
14097
12992
  import { execFileSync as execFileSync2, spawnSync } from "child_process";
14098
- import fs12 from "fs";
12993
+ import fs14 from "fs";
14099
12994
  import os7 from "os";
14100
- import path16 from "path";
14101
- import { gzipSync } from "zlib";
12995
+ import path15 from "path";
12996
+ import { gzipSync as gzipSync2 } from "zlib";
14102
12997
  var GIT_COMMAND_TIMEOUT_MS = 12e4;
14103
12998
  var EXEC_OPTS = {
14104
12999
  timeout: GIT_COMMAND_TIMEOUT_MS,
@@ -14203,8 +13098,8 @@ var EXCLUDED_SNAPSHOT_BASENAMES = /* @__PURE__ */ new Set([
14203
13098
  ]);
14204
13099
  var EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
14205
13100
  function isExcludedSnapshotPath(filePath) {
14206
- if (EXCLUDED_SNAPSHOT_BASENAMES.has(path16.basename(filePath))) return true;
14207
- return EXCLUDED_SNAPSHOT_EXTENSIONS.has(path16.extname(filePath).toLowerCase());
13101
+ if (EXCLUDED_SNAPSHOT_BASENAMES.has(path15.basename(filePath))) return true;
13102
+ return EXCLUDED_SNAPSHOT_EXTENSIONS.has(path15.extname(filePath).toLowerCase());
14208
13103
  }
14209
13104
  function isBinaryBuffer(buffer) {
14210
13105
  return buffer.includes(0);
@@ -14224,16 +13119,16 @@ function readTreeBlobHead(repoRoot, sha) {
14224
13119
  function readWorkingFileHead(absPath) {
14225
13120
  let fd = null;
14226
13121
  try {
14227
- fd = fs12.openSync(absPath, "r");
13122
+ fd = fs14.openSync(absPath, "r");
14228
13123
  const buffer = Buffer.alloc(BINARY_SNIFF_BYTES);
14229
- const bytesRead = fs12.readSync(fd, buffer, 0, BINARY_SNIFF_BYTES, 0);
13124
+ const bytesRead = fs14.readSync(fd, buffer, 0, BINARY_SNIFF_BYTES, 0);
14230
13125
  return buffer.subarray(0, bytesRead);
14231
13126
  } catch {
14232
13127
  return null;
14233
13128
  } finally {
14234
13129
  if (fd !== null) {
14235
13130
  try {
14236
- fs12.closeSync(fd);
13131
+ fs14.closeSync(fd);
14237
13132
  } catch {
14238
13133
  }
14239
13134
  }
@@ -14411,7 +13306,7 @@ function removePathsFromIndex(repoRoot, env, paths) {
14411
13306
  function filterOmittedFilesFromTree(repoRoot, treeSha, options = {}) {
14412
13307
  const omittedFiles = listOmittedTreeFiles(repoRoot, treeSha, options);
14413
13308
  if (omittedFiles.length === 0) return treeSha;
14414
- const tmpIndex = path16.join(
13309
+ const tmpIndex = path15.join(
14415
13310
  os7.tmpdir(),
14416
13311
  `hillclimb-filter-${Date.now()}-${process.pid}`
14417
13312
  );
@@ -14426,7 +13321,7 @@ function filterOmittedFilesFromTree(repoRoot, treeSha, options = {}) {
14426
13321
  return gitWithEnv(repoRoot, ["write-tree"], env);
14427
13322
  } finally {
14428
13323
  try {
14429
- fs12.unlinkSync(tmpIndex);
13324
+ fs14.unlinkSync(tmpIndex);
14430
13325
  } catch {
14431
13326
  }
14432
13327
  }
@@ -14443,8 +13338,8 @@ function buildUntrackedTree(repoRoot, omittedFiles) {
14443
13338
  for (const relPath of list.split("\0")) {
14444
13339
  if (!relPath) continue;
14445
13340
  try {
14446
- const absPath = path16.join(repoRoot, relPath);
14447
- const stat = fs12.lstatSync(absPath);
13341
+ const absPath = path15.join(repoRoot, relPath);
13342
+ const stat = fs14.lstatSync(absPath);
14448
13343
  const reason = classifyOmission(
14449
13344
  relPath,
14450
13345
  stat.size,
@@ -14464,7 +13359,7 @@ function buildUntrackedTree(repoRoot, omittedFiles) {
14464
13359
  }
14465
13360
  }
14466
13361
  if (kept.length === 0) return null;
14467
- const tmpIndex = path16.join(
13362
+ const tmpIndex = path15.join(
14468
13363
  os7.tmpdir(),
14469
13364
  `hillclimb-untracked-${Date.now()}-${process.pid}`
14470
13365
  );
@@ -14477,7 +13372,7 @@ function buildUntrackedTree(repoRoot, omittedFiles) {
14477
13372
  return gitWithEnv(repoRoot, ["write-tree"], env);
14478
13373
  } finally {
14479
13374
  try {
14480
- fs12.unlinkSync(tmpIndex);
13375
+ fs14.unlinkSync(tmpIndex);
14481
13376
  } catch {
14482
13377
  }
14483
13378
  }
@@ -14494,7 +13389,7 @@ function buildSnapshotTree(repoRoot, stashSha, options = {}) {
14494
13389
  const untrackedTree = buildUntrackedTree(repoRoot, options.omittedFiles);
14495
13390
  if (!untrackedTree || untrackedTree === EMPTY_TREE_SHA)
14496
13391
  return filteredTrackedTree;
14497
- const tmpIndex = path16.join(
13392
+ const tmpIndex = path15.join(
14498
13393
  os7.tmpdir(),
14499
13394
  `hillclimb-index-${Date.now()}-${process.pid}`
14500
13395
  );
@@ -14522,7 +13417,7 @@ function buildSnapshotTree(repoRoot, stashSha, options = {}) {
14522
13417
  return gitWithEnv(repoRoot, ["write-tree"], env);
14523
13418
  } finally {
14524
13419
  try {
14525
- fs12.unlinkSync(tmpIndex);
13420
+ fs14.unlinkSync(tmpIndex);
14526
13421
  } catch {
14527
13422
  }
14528
13423
  }
@@ -14536,7 +13431,7 @@ function createBundleFromTree(repoRoot, treeSha, sessionId, label) {
14536
13431
  ]);
14537
13432
  const orphanRef = `refs/hillclimb/bundle/${sessionId}`;
14538
13433
  pinRef(repoRoot, orphanRef, orphanCommit);
14539
- const tmpFile = path16.join(
13434
+ const tmpFile = path15.join(
14540
13435
  os7.tmpdir(),
14541
13436
  // Include the pid (like the other temp files in this module) so concurrent
14542
13437
  // git-traces workers — e.g. two sessions, or a parent + subagent — don't
@@ -14545,10 +13440,10 @@ function createBundleFromTree(repoRoot, treeSha, sessionId, label) {
14545
13440
  );
14546
13441
  try {
14547
13442
  git(repoRoot, ["bundle", "create", tmpFile, orphanRef]);
14548
- return fs12.readFileSync(tmpFile);
13443
+ return fs14.readFileSync(tmpFile);
14549
13444
  } finally {
14550
13445
  try {
14551
- fs12.unlinkSync(tmpFile);
13446
+ fs14.unlinkSync(tmpFile);
14552
13447
  } catch {
14553
13448
  }
14554
13449
  deleteRef(repoRoot, orphanRef);
@@ -14574,7 +13469,7 @@ function createTreeDiffPatchGz(repoRoot, fromTreeSha, toTreeSha) {
14574
13469
  filteredToTreeSha
14575
13470
  ]);
14576
13471
  if (diff.length === 0) return null;
14577
- return Buffer.from(gzipSync(diff));
13472
+ return Buffer.from(gzipSync2(diff));
14578
13473
  }
14579
13474
  function detectTransitionKind(repoRoot, prevHeadSha, nextHeadSha) {
14580
13475
  if (!prevHeadSha) return "initial";
@@ -14735,9 +13630,9 @@ function parseCommitFiles(repoRoot, sha) {
14735
13630
  oldPath
14736
13631
  });
14737
13632
  } else {
14738
- const path25 = parts[parts.length - 1];
14739
- indexByPath.set(path25, files.length);
14740
- files.push({ path: path25, status, additions: 0, deletions: 0 });
13633
+ const path24 = parts[parts.length - 1];
13634
+ indexByPath.set(path24, files.length);
13635
+ files.push({ path: path24, status, additions: 0, deletions: 0 });
14741
13636
  }
14742
13637
  }
14743
13638
  for (const line of numstat.split("\n")) {
@@ -14803,29 +13698,30 @@ function cleanupSessionRefs(repoRoot, sessionId) {
14803
13698
  }
14804
13699
 
14805
13700
  // src/git-traces/session-state.ts
14806
- import crypto4 from "crypto";
14807
- import fs13 from "fs";
13701
+ import crypto5 from "crypto";
13702
+ import fs15 from "fs";
14808
13703
  import os8 from "os";
14809
- import path17 from "path";
13704
+ import path16 from "path";
14810
13705
  var CURRENT_SCHEMA_VERSION3 = 3;
14811
- var DEFAULT_STATE_DIR2 = path17.join(os8.homedir(), ".hillclimb", "git-traces");
13706
+ var DEFAULT_STATE_DIR2 = path16.join(os8.homedir(), ".hillclimb", "git-traces");
14812
13707
  var LOCK_RETRIES2 = 120;
14813
13708
  var LOCK_RETRY_DELAY_MS2 = 500;
13709
+ var STALE_LOCK_TTL_MS2 = 60 * 60 * 1e3;
14814
13710
  function stateDir3() {
14815
13711
  return process.env.HILLCLIMB_GIT_TRACES_STATE_DIR ?? DEFAULT_STATE_DIR2;
14816
13712
  }
14817
13713
  function stateFileForRepo(repoRoot, tool, sessionId) {
14818
- const hash = crypto4.createHash("sha256").update(
14819
- sessionId ? `${path17.resolve(repoRoot)}\0${tool}\0${sessionId}` : `${path17.resolve(repoRoot)}\0${tool}`
13714
+ const hash = crypto5.createHash("sha256").update(
13715
+ sessionId ? `${path16.resolve(repoRoot)}\0${tool}\0${sessionId}` : `${path16.resolve(repoRoot)}\0${tool}`
14820
13716
  ).digest("hex").slice(0, 16);
14821
- return path17.join(stateDir3(), `${hash}.json`);
13717
+ return path16.join(stateDir3(), `${hash}.json`);
14822
13718
  }
14823
- function lockFileForRepo(repoRoot, tool) {
14824
- return `${stateFileForRepo(repoRoot, tool)}.lock`;
13719
+ function lockFileForRepo(repoRoot, tool, sessionId) {
13720
+ return `${stateFileForRepo(repoRoot, tool, sessionId)}.lock`;
14825
13721
  }
14826
13722
  async function readStateFile(file) {
14827
13723
  try {
14828
- const raw = await fs13.promises.readFile(file, "utf-8");
13724
+ const raw = await fs15.promises.readFile(file, "utf-8");
14829
13725
  const parsed = JSON.parse(raw);
14830
13726
  if (parsed.schemaVersion !== CURRENT_SCHEMA_VERSION3) {
14831
13727
  return null;
@@ -14838,26 +13734,26 @@ async function readStateFile(file) {
14838
13734
  async function listScopedSessionStates(repoRoot, tool) {
14839
13735
  let entries;
14840
13736
  try {
14841
- entries = await fs13.promises.readdir(stateDir3(), { withFileTypes: true });
13737
+ entries = await fs15.promises.readdir(stateDir3(), { withFileTypes: true });
14842
13738
  } catch {
14843
13739
  return [];
14844
13740
  }
14845
13741
  const states = [];
14846
13742
  for (const entry of entries) {
14847
13743
  if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
14848
- const file = path17.join(stateDir3(), entry.name);
13744
+ const file = path16.join(stateDir3(), entry.name);
14849
13745
  const state = await readStateFile(file);
14850
13746
  if (!state) continue;
14851
13747
  if (typeof state.repoRoot !== "string" || typeof state.sessionId !== "string") {
14852
13748
  continue;
14853
13749
  }
14854
- if (path17.resolve(state.repoRoot) !== path17.resolve(repoRoot)) continue;
14855
- if (path17.resolve(file) !== path17.resolve(stateFileForRepo(repoRoot, tool, state.sessionId))) {
13750
+ if (path16.resolve(state.repoRoot) !== path16.resolve(repoRoot)) continue;
13751
+ if (path16.resolve(file) !== path16.resolve(stateFileForRepo(repoRoot, tool, state.sessionId))) {
14856
13752
  continue;
14857
13753
  }
14858
13754
  let mtimeMs = 0;
14859
13755
  try {
14860
- mtimeMs = (await fs13.promises.stat(file)).mtimeMs;
13756
+ mtimeMs = (await fs15.promises.stat(file)).mtimeMs;
14861
13757
  } catch {
14862
13758
  continue;
14863
13759
  }
@@ -14868,26 +13764,26 @@ async function listScopedSessionStates(repoRoot, tool) {
14868
13764
  async function listSessionStatesForSession(tool, sessionId) {
14869
13765
  let entries;
14870
13766
  try {
14871
- entries = await fs13.promises.readdir(stateDir3(), { withFileTypes: true });
13767
+ entries = await fs15.promises.readdir(stateDir3(), { withFileTypes: true });
14872
13768
  } catch {
14873
13769
  return [];
14874
13770
  }
14875
13771
  const states = [];
14876
13772
  for (const entry of entries) {
14877
13773
  if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
14878
- const file = path17.join(stateDir3(), entry.name);
13774
+ const file = path16.join(stateDir3(), entry.name);
14879
13775
  const state = await readStateFile(file);
14880
13776
  if (!state) continue;
14881
13777
  if (typeof state.repoRoot !== "string" || typeof state.sessionId !== "string") {
14882
13778
  continue;
14883
13779
  }
14884
13780
  if (state.sessionId !== sessionId) continue;
14885
- if (path17.resolve(file) !== path17.resolve(stateFileForRepo(state.repoRoot, tool, sessionId))) {
13781
+ if (path16.resolve(file) !== path16.resolve(stateFileForRepo(state.repoRoot, tool, sessionId))) {
14886
13782
  continue;
14887
13783
  }
14888
13784
  let mtimeMs = 0;
14889
13785
  try {
14890
- mtimeMs = (await fs13.promises.stat(file)).mtimeMs;
13786
+ mtimeMs = (await fs15.promises.stat(file)).mtimeMs;
14891
13787
  } catch {
14892
13788
  continue;
14893
13789
  }
@@ -14908,12 +13804,12 @@ async function readSessionState(repoRoot, tool, sessionId) {
14908
13804
  }
14909
13805
  async function writeSessionState(state, tool) {
14910
13806
  const file = stateFileForRepo(state.repoRoot, tool, state.sessionId);
14911
- await fs13.promises.mkdir(stateDir3(), { recursive: true, mode: 448 });
13807
+ await fs15.promises.mkdir(stateDir3(), { recursive: true, mode: 448 });
14912
13808
  const tmp = `${file}.tmp`;
14913
- await fs13.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
13809
+ await fs15.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
14914
13810
  mode: 384
14915
13811
  });
14916
- await fs13.promises.rename(tmp, file);
13812
+ await fs15.promises.rename(tmp, file);
14917
13813
  const legacyFile = stateFileForRepo(state.repoRoot, tool);
14918
13814
  const legacy = await readStateFile(legacyFile);
14919
13815
  if (legacy?.sessionId === state.sessionId) {
@@ -14922,7 +13818,7 @@ async function writeSessionState(state, tool) {
14922
13818
  }
14923
13819
  async function deleteStateFile(file) {
14924
13820
  try {
14925
- await fs13.promises.unlink(file);
13821
+ await fs15.promises.unlink(file);
14926
13822
  } catch {
14927
13823
  }
14928
13824
  }
@@ -14938,14 +13834,14 @@ async function deleteSessionState(repoRoot, tool, sessionId) {
14938
13834
  }
14939
13835
  await deleteStateFile(stateFileForRepo(repoRoot, tool));
14940
13836
  }
14941
- async function acquireLock3(repoRoot, tool, retries = LOCK_RETRIES2, delayMs = LOCK_RETRY_DELAY_MS2) {
14942
- const lockPath = lockFileForRepo(repoRoot, tool);
14943
- await fs13.promises.mkdir(stateDir3(), { recursive: true, mode: 448 });
13837
+ async function acquireLock3(repoRoot, tool, sessionId, retries = LOCK_RETRIES2, delayMs = LOCK_RETRY_DELAY_MS2) {
13838
+ const lockPath = lockFileForRepo(repoRoot, tool, sessionId);
13839
+ await fs15.promises.mkdir(stateDir3(), { recursive: true, mode: 448 });
14944
13840
  for (let i = 0; i < retries; i++) {
14945
13841
  try {
14946
- const fd = await fs13.promises.open(
13842
+ const fd = await fs15.promises.open(
14947
13843
  lockPath,
14948
- fs13.constants.O_CREAT | fs13.constants.O_EXCL | fs13.constants.O_WRONLY
13844
+ fs15.constants.O_CREAT | fs15.constants.O_EXCL | fs15.constants.O_WRONLY
14949
13845
  );
14950
13846
  await fd.write(String(process.pid));
14951
13847
  await fd.close();
@@ -14960,15 +13856,54 @@ async function acquireLock3(repoRoot, tool, retries = LOCK_RETRIES2, delayMs = L
14960
13856
  }
14961
13857
  throw new Error(`Failed to acquire lock after ${retries} retries`);
14962
13858
  }
14963
- async function releaseLock3(repoRoot, tool) {
13859
+ async function releaseLock3(repoRoot, tool, sessionId) {
13860
+ try {
13861
+ await fs15.promises.unlink(lockFileForRepo(repoRoot, tool, sessionId));
13862
+ } catch {
13863
+ }
13864
+ }
13865
+ function isProcessAlive2(pid) {
13866
+ try {
13867
+ process.kill(pid, 0);
13868
+ return true;
13869
+ } catch (err) {
13870
+ return err.code === "EPERM";
13871
+ }
13872
+ }
13873
+ async function sweepStaleLockFiles(ttlMs = STALE_LOCK_TTL_MS2, now = Date.now()) {
13874
+ let entries;
14964
13875
  try {
14965
- await fs13.promises.unlink(lockFileForRepo(repoRoot, tool));
13876
+ entries = await fs15.promises.readdir(stateDir3(), { withFileTypes: true });
14966
13877
  } catch {
13878
+ return 0;
14967
13879
  }
13880
+ let removed = 0;
13881
+ for (const entry of entries) {
13882
+ if (!entry.isFile() || !entry.name.endsWith(".lock")) continue;
13883
+ const file = path16.join(stateDir3(), entry.name);
13884
+ try {
13885
+ const raw = await fs15.promises.readFile(file, "utf-8").catch(() => "");
13886
+ const pid = Number.parseInt(raw.trim(), 10);
13887
+ const havePid = Number.isInteger(pid) && pid > 0;
13888
+ let reap;
13889
+ if (havePid) {
13890
+ reap = !isProcessAlive2(pid);
13891
+ } else {
13892
+ const st = await fs15.promises.stat(file);
13893
+ reap = now - st.mtimeMs > ttlMs;
13894
+ }
13895
+ if (reap) {
13896
+ await fs15.promises.unlink(file);
13897
+ removed++;
13898
+ }
13899
+ } catch {
13900
+ }
13901
+ }
13902
+ return removed;
14968
13903
  }
14969
13904
 
14970
13905
  // src/git-traces/handlers.ts
14971
- var CLI_VERSION = "0.2.0";
13906
+ var CLI_VERSION2 = "0.7.0";
14972
13907
  var GIT_TRACES_SLUG = "git-traces";
14973
13908
  var MAX_UPLOAD_BYTES = 500 * 1024 * 1024;
14974
13909
  var STALE_SCOPED_STATE_TTL_MS = 24 * 60 * 60 * 1e3;
@@ -14985,12 +13920,12 @@ var TOOL_LABELS = {
14985
13920
  async function loadConfiguredRepos() {
14986
13921
  const file = await loadProjects();
14987
13922
  return Object.entries(file.projects).map(([repoRoot, config]) => ({
14988
- repoRoot: path18.resolve(repoRoot),
13923
+ repoRoot: path17.resolve(repoRoot),
14989
13924
  config
14990
13925
  })).sort((a, b) => a.repoRoot.localeCompare(b.repoRoot));
14991
13926
  }
14992
13927
  function repoLabel(repoRoot) {
14993
- return path18.basename(repoRoot) || repoRoot;
13928
+ return path17.basename(repoRoot) || repoRoot;
14994
13929
  }
14995
13930
  function resolveCwd2(payload) {
14996
13931
  return resolveHookCwd(payload);
@@ -14998,10 +13933,10 @@ function resolveCwd2(payload) {
14998
13933
  function resolveSessionId2(payload) {
14999
13934
  return resolveHookSessionId(payload);
15000
13935
  }
15001
- function epochPrefix(epoch) {
13936
+ function epochPrefix2(epoch) {
15002
13937
  return `epoch-${String(epoch).padStart(3, "0")}`;
15003
13938
  }
15004
- function turnSuffix(turn) {
13939
+ function turnSuffix2(turn) {
15005
13940
  return `turn-${String(turn).padStart(3, "0")}`;
15006
13941
  }
15007
13942
  function captureHeadSha(cwd) {
@@ -15046,18 +13981,18 @@ async function uploadFile(client, contributionId, filename, mimeType, buffer) {
15046
13981
  return true;
15047
13982
  }
15048
13983
  function canUploadEpochBaselineArtifacts(epoch, artifacts) {
15049
- const prefix = epochPrefix(epoch);
13984
+ const prefix = epochPrefix2(epoch);
15050
13985
  return canUploadFile(`${prefix}-baseline.bundle`, artifacts.bundleBuffer) && canUploadFile(`${prefix}-baseline.json`, artifacts.metadataBuffer);
15051
13986
  }
15052
13987
  function pinEpochBaseline(repoRoot, sessionId, epoch) {
15053
13988
  const baselineSha = captureBaselineSha(repoRoot);
15054
- const baselineRefPrefix = `refs/hillclimb/baseline/${sessionId}/${epochPrefix(epoch)}`;
13989
+ const baselineRefPrefix = `refs/hillclimb/baseline/${sessionId}/${epochPrefix2(epoch)}`;
15055
13990
  deleteRef(repoRoot, baselineRefPrefix);
15056
13991
  pinRef(repoRoot, `${baselineRefPrefix}/tracked`, baselineSha);
15057
13992
  return { baselineSha, headSha: captureHeadSha(repoRoot) };
15058
13993
  }
15059
13994
  function pinFrozenBaselineTree(repoRoot, sessionId, epoch, baselineTreeSha) {
15060
- const prefix = epochPrefix(epoch);
13995
+ const prefix = epochPrefix2(epoch);
15061
13996
  const commit = execGit(repoRoot, [
15062
13997
  "commit-tree",
15063
13998
  baselineTreeSha,
@@ -15080,7 +14015,7 @@ function freezeEpochBaseline(params) {
15080
14015
  transitionKind,
15081
14016
  startedAt
15082
14017
  } = params;
15083
- const prefix = epochPrefix(epoch);
14018
+ const prefix = epochPrefix2(epoch);
15084
14019
  try {
15085
14020
  const { baselineSha, headSha } = pinEpochBaseline(
15086
14021
  repoRoot,
@@ -15096,7 +14031,7 @@ function freezeEpochBaseline(params) {
15096
14031
  sessionId,
15097
14032
  tool,
15098
14033
  baselineSha,
15099
- CLI_VERSION,
14034
+ CLI_VERSION2,
15100
14035
  epoch,
15101
14036
  prevHeadSha,
15102
14037
  transitionKind,
@@ -15115,7 +14050,7 @@ function freezeEpochBaseline(params) {
15115
14050
  }
15116
14051
  function buildFrozenEpochBaselineArtifacts(params) {
15117
14052
  const { repoRoot, sessionId, epoch, baselineTreeSha, baselineMetadata } = params;
15118
- const prefix = epochPrefix(epoch);
14053
+ const prefix = epochPrefix2(epoch);
15119
14054
  try {
15120
14055
  const bundleBuffer = createBundleFromTree(
15121
14056
  repoRoot,
@@ -15146,7 +14081,7 @@ function buildEpochBaselineArtifacts(params) {
15146
14081
  transitionKind,
15147
14082
  startedAt
15148
14083
  } = params;
15149
- const prefix = epochPrefix(epoch);
14084
+ const prefix = epochPrefix2(epoch);
15150
14085
  try {
15151
14086
  const omittedFiles = [];
15152
14087
  const baselineTreeSha = buildSnapshotTree(repoRoot, baselineSha, {
@@ -15157,7 +14092,7 @@ function buildEpochBaselineArtifacts(params) {
15157
14092
  sessionId,
15158
14093
  tool,
15159
14094
  baselineSha,
15160
- CLI_VERSION,
14095
+ CLI_VERSION2,
15161
14096
  epoch,
15162
14097
  prevHeadSha,
15163
14098
  transitionKind,
@@ -15181,7 +14116,7 @@ function buildEpochBaselineArtifacts(params) {
15181
14116
  }
15182
14117
  async function uploadEpochBaselineArtifacts(params) {
15183
14118
  const { client, contributionId, epoch, artifacts } = params;
15184
- const prefix = epochPrefix(epoch);
14119
+ const prefix = epochPrefix2(epoch);
15185
14120
  if (!canUploadEpochBaselineArtifacts(epoch, artifacts)) return false;
15186
14121
  const bundleUploaded = await uploadFile(
15187
14122
  client,
@@ -15302,13 +14237,26 @@ async function cleanupStaleScopedSessionStates(repoRoot, tool, currentSessionId,
15302
14237
  const startedAtMs = Date.parse(state.startedAt);
15303
14238
  const ageBaseMs = Number.isNaN(startedAtMs) ? mtimeMs : startedAtMs;
15304
14239
  if (nowMs - ageBaseMs <= ttlMs) continue;
15305
- appendLog(
15306
- "info",
15307
- `git-traces: cleaning up stale scoped session ${state.sessionId}`
15308
- );
15309
- cleanupSessionRefs(repoRoot, state.sessionId);
15310
- await deleteSessionState(repoRoot, tool, state.sessionId);
15311
- removed++;
14240
+ try {
14241
+ await acquireLock3(repoRoot, tool, state.sessionId, 1, 0);
14242
+ } catch {
14243
+ appendLog(
14244
+ "info",
14245
+ `git-traces: leaving session ${state.sessionId} (lock held \u2014 still active)`
14246
+ );
14247
+ continue;
14248
+ }
14249
+ try {
14250
+ appendLog(
14251
+ "info",
14252
+ `git-traces: cleaning up stale scoped session ${state.sessionId}`
14253
+ );
14254
+ cleanupSessionRefs(repoRoot, state.sessionId);
14255
+ await deleteSessionState(repoRoot, tool, state.sessionId);
14256
+ removed++;
14257
+ } finally {
14258
+ await releaseLock3(repoRoot, tool, state.sessionId);
14259
+ }
15312
14260
  }
15313
14261
  return removed;
15314
14262
  }
@@ -15321,16 +14269,34 @@ async function processSessionStartRepo(repo, tool, sessionId) {
15321
14269
  );
15322
14270
  return "skipped";
15323
14271
  }
15324
- await acquireLock3(repoRoot, tool);
14272
+ let acquired = false;
15325
14273
  try {
14274
+ await acquireLock3(repoRoot, tool, sessionId);
14275
+ acquired = true;
15326
14276
  const staleLegacy = await readSessionState(repoRoot, tool);
15327
14277
  if (staleLegacy && staleLegacy.sessionId !== sessionId) {
15328
- appendLog(
15329
- "info",
15330
- `git-traces: cleaning up stale session ${staleLegacy.sessionId} (repo=${repoRoot})`
15331
- );
15332
- cleanupSessionRefs(repoRoot, staleLegacy.sessionId);
15333
- await deleteSessionState(repoRoot, tool);
14278
+ let acquiredLegacy = false;
14279
+ try {
14280
+ await acquireLock3(repoRoot, tool, staleLegacy.sessionId, 1, 0);
14281
+ acquiredLegacy = true;
14282
+ } catch {
14283
+ appendLog(
14284
+ "info",
14285
+ `git-traces: leaving legacy session ${staleLegacy.sessionId} (lock held \u2014 still active)`
14286
+ );
14287
+ }
14288
+ if (acquiredLegacy) {
14289
+ try {
14290
+ appendLog(
14291
+ "info",
14292
+ `git-traces: cleaning up stale session ${staleLegacy.sessionId} (repo=${repoRoot})`
14293
+ );
14294
+ cleanupSessionRefs(repoRoot, staleLegacy.sessionId);
14295
+ await deleteSessionState(repoRoot, tool);
14296
+ } finally {
14297
+ await releaseLock3(repoRoot, tool, staleLegacy.sessionId);
14298
+ }
14299
+ }
15334
14300
  }
15335
14301
  const staleCount = await cleanupStaleScopedSessionStates(
15336
14302
  repoRoot,
@@ -15352,7 +14318,7 @@ async function processSessionStartRepo(repo, tool, sessionId) {
15352
14318
  );
15353
14319
  return "failed";
15354
14320
  } finally {
15355
- await releaseLock3(repoRoot, tool);
14321
+ if (acquired) await releaseLock3(repoRoot, tool, sessionId);
15356
14322
  }
15357
14323
  }
15358
14324
  async function handleSessionStart(payload, tool) {
@@ -15374,6 +14340,10 @@ async function handleSessionStart(payload, tool) {
15374
14340
  appendLog("warn", "git-traces: no session_id in payload, skipping");
15375
14341
  return;
15376
14342
  }
14343
+ const reaped = await sweepStaleLockFiles();
14344
+ if (reaped > 0) {
14345
+ appendLog("info", `git-traces: reaped ${reaped} stale lock file(s)`);
14346
+ }
15377
14347
  const repos = await loadConfiguredRepos();
15378
14348
  let initialized = 0;
15379
14349
  let skipped = 0;
@@ -15453,9 +14423,11 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
15453
14423
  );
15454
14424
  return "skipped";
15455
14425
  }
15456
- await acquireLock3(repoRoot, tool);
15457
14426
  let state = null;
14427
+ let acquired = false;
15458
14428
  try {
14429
+ await acquireLock3(repoRoot, tool, sessionId);
14430
+ acquired = true;
15459
14431
  state = await readSessionState(repoRoot, tool, sessionId);
15460
14432
  if (!state) {
15461
14433
  appendLog(
@@ -15555,9 +14527,9 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
15555
14527
  );
15556
14528
  return "unchanged";
15557
14529
  }
15558
- const prefix = epochPrefix(state.epoch);
14530
+ const prefix = epochPrefix2(state.epoch);
15559
14531
  const nextTurnCount = state.turnCount + 1;
15560
- const turnLabel = turnSuffix(nextTurnCount);
14532
+ const turnLabel = turnSuffix2(nextTurnCount);
15561
14533
  const filename = `${prefix}-${turnLabel}-${recordedAt}.patch.gz`;
15562
14534
  if ((state.contributionId === null || !state.baselineUploaded) && !canUploadFile(filename, patchBuffer)) {
15563
14535
  appendLog(
@@ -15655,7 +14627,7 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
15655
14627
  }
15656
14628
  return "failed";
15657
14629
  } finally {
15658
- await releaseLock3(repoRoot, tool);
14630
+ if (acquired) await releaseLock3(repoRoot, tool, sessionId);
15659
14631
  }
15660
14632
  }
15661
14633
  async function handleStop(payload, tool) {
@@ -15672,7 +14644,7 @@ async function handleStop(payload, tool) {
15672
14644
  if (sessionId) {
15673
14645
  const storedStates = await listSessionStatesForSession(tool, sessionId);
15674
14646
  for (const { state } of storedStates) {
15675
- const repo = repoByRoot.get(path18.resolve(state.repoRoot));
14647
+ const repo = repoByRoot.get(path17.resolve(state.repoRoot));
15676
14648
  if (!repo) {
15677
14649
  missingConfig++;
15678
14650
  appendLog(
@@ -15708,8 +14680,10 @@ async function handleStop(payload, tool) {
15708
14680
  );
15709
14681
  }
15710
14682
  async function cleanupSessionStateForRepo(repoRoot, tool, sessionId) {
15711
- await acquireLock3(repoRoot, tool);
14683
+ let acquired = false;
15712
14684
  try {
14685
+ await acquireLock3(repoRoot, tool, sessionId);
14686
+ acquired = true;
15713
14687
  const state = await readSessionState(repoRoot, tool, sessionId);
15714
14688
  if (!state) return "no-state";
15715
14689
  cleanupSessionRefs(repoRoot, state.sessionId);
@@ -15726,7 +14700,7 @@ async function cleanupSessionStateForRepo(repoRoot, tool, sessionId) {
15726
14700
  );
15727
14701
  return "failed";
15728
14702
  } finally {
15729
- await releaseLock3(repoRoot, tool);
14703
+ if (acquired) await releaseLock3(repoRoot, tool, sessionId);
15730
14704
  }
15731
14705
  }
15732
14706
  async function handleSessionEnd(payload, tool) {
@@ -15739,7 +14713,7 @@ async function handleSessionEnd(payload, tool) {
15739
14713
  if (sessionId) {
15740
14714
  const states = await listSessionStatesForSession(tool, sessionId);
15741
14715
  for (const { state } of states) {
15742
- repoRoots.push(path18.resolve(state.repoRoot));
14716
+ repoRoots.push(path17.resolve(state.repoRoot));
15743
14717
  }
15744
14718
  }
15745
14719
  if (repoRoots.length === 0 && cwd) {
@@ -15755,7 +14729,7 @@ async function handleSessionEnd(payload, tool) {
15755
14729
  let skipped = 0;
15756
14730
  let failed = 0;
15757
14731
  for (const repoRoot of repoRoots) {
15758
- const repo = repoByRoot.get(path18.resolve(repoRoot)) ?? (project && path18.resolve(project.repoRoot) === path18.resolve(repoRoot) ? { repoRoot: project.repoRoot, config: project.config } : null);
14732
+ const repo = repoByRoot.get(path17.resolve(repoRoot)) ?? (project && path17.resolve(project.repoRoot) === path17.resolve(repoRoot) ? { repoRoot: project.repoRoot, config: project.config } : null);
15759
14733
  if (!repo) {
15760
14734
  skipped++;
15761
14735
  appendLog(
@@ -15806,7 +14780,7 @@ var WORKER_ENV_FLAG2 = "HILLCLIMB_GIT_TRACES_WORKER";
15806
14780
  var TOOL_ENV_FLAG2 = "HILLCLIMB_GIT_TRACES_TOOL";
15807
14781
  var FLOW_ID_ENV2 = "HILLCLIMB_GIT_TRACES_FLOW";
15808
14782
  function newFlowId2() {
15809
- return crypto5.randomBytes(3).toString("hex");
14783
+ return crypto6.randomBytes(3).toString("hex");
15810
14784
  }
15811
14785
  var KNOWN_TOOLS = /* @__PURE__ */ new Set([
15812
14786
  "claude",
@@ -16092,15 +15066,15 @@ ${stack}` : ""}`
16092
15066
  }
16093
15067
 
16094
15068
  // src/outputs/zip.ts
16095
- import fs15 from "fs";
16096
- import path20 from "path";
15069
+ import fs17 from "fs";
15070
+ import path19 from "path";
16097
15071
  import archiver2 from "archiver";
16098
15072
 
16099
15073
  // src/outputs/downloads.ts
16100
15074
  import { execSync as execSync2 } from "child_process";
16101
- import fs14 from "fs";
15075
+ import fs16 from "fs";
16102
15076
  import os9 from "os";
16103
- import path19 from "path";
15077
+ import path18 from "path";
16104
15078
  function getDownloadsFolder() {
16105
15079
  const home = os9.homedir();
16106
15080
  if (process.platform === "linux") {
@@ -16109,12 +15083,12 @@ function getDownloadsFolder() {
16109
15083
  encoding: "utf-8",
16110
15084
  timeout: 3e3
16111
15085
  }).trim();
16112
- if (xdgDir && fs14.existsSync(xdgDir)) return xdgDir;
15086
+ if (xdgDir && fs16.existsSync(xdgDir)) return xdgDir;
16113
15087
  } catch {
16114
15088
  }
16115
15089
  }
16116
- const downloads = path19.join(home, "Downloads");
16117
- if (fs14.existsSync(downloads)) return downloads;
15090
+ const downloads = path18.join(home, "Downloads");
15091
+ if (fs16.existsSync(downloads)) return downloads;
16118
15092
  return home;
16119
15093
  }
16120
15094
 
@@ -16123,11 +15097,11 @@ function sanitizeFilename(name) {
16123
15097
  return name.replace(/[^a-zA-Z0-9._-]/g, "_");
16124
15098
  }
16125
15099
  function getUniqueFilename(dir, base, ext) {
16126
- let candidate = path20.join(dir, `${base}${ext}`);
16127
- if (!fs15.existsSync(candidate)) return candidate;
15100
+ let candidate = path19.join(dir, `${base}${ext}`);
15101
+ if (!fs17.existsSync(candidate)) return candidate;
16128
15102
  let i = 1;
16129
- while (fs15.existsSync(candidate)) {
16130
- candidate = path20.join(dir, `${base}-${i}${ext}`);
15103
+ while (fs17.existsSync(candidate)) {
15104
+ candidate = path19.join(dir, `${base}-${i}${ext}`);
16131
15105
  i++;
16132
15106
  }
16133
15107
  return candidate;
@@ -16137,13 +15111,13 @@ var ZipOutput = class {
16137
15111
  label = "Save as .zip to Downloads";
16138
15112
  async emit(group, options) {
16139
15113
  const downloadsDir = getDownloadsFolder();
16140
- const repoName = sanitizeFilename(path20.basename(group.repoPath));
15114
+ const repoName = sanitizeFilename(path19.basename(group.repoPath));
16141
15115
  const timeRange = options.timeRange;
16142
15116
  const rangePart = timeRange?.label ?? "all";
16143
15117
  const epochSeconds = Math.floor(Date.now() / 1e3);
16144
15118
  const baseName = `logs-${repoName}-${rangePart}-${epochSeconds}`;
16145
15119
  const outputPath = getUniqueFilename(downloadsDir, baseName, ".zip");
16146
- const output = fs15.createWriteStream(outputPath);
15120
+ const output = fs17.createWriteStream(outputPath);
16147
15121
  const archive = archiver2("zip", { zlib: { level: 6 } });
16148
15122
  const done = new Promise((resolve, reject) => {
16149
15123
  output.on("close", resolve);
@@ -16337,15 +15311,15 @@ async function confirmExport(group, output) {
16337
15311
  }
16338
15312
 
16339
15313
  // src/sources/claude.ts
16340
- import fs16 from "fs";
15314
+ import fs18 from "fs";
16341
15315
  import os10 from "os";
16342
- import path21 from "path";
15316
+ import path20 from "path";
16343
15317
  import readline2 from "readline";
16344
15318
  var SKIP_DIRS = /* @__PURE__ */ new Set(["tool-results"]);
16345
15319
  async function resolveRepoPath(projectDir) {
16346
- const indexPath = path21.join(projectDir, "sessions-index.json");
15320
+ const indexPath = path20.join(projectDir, "sessions-index.json");
16347
15321
  try {
16348
- const raw = await fs16.promises.readFile(indexPath, "utf-8");
15322
+ const raw = await fs18.promises.readFile(indexPath, "utf-8");
16349
15323
  const data = JSON.parse(raw);
16350
15324
  if (data.originalPath && typeof data.originalPath === "string") {
16351
15325
  return data.originalPath;
@@ -16353,12 +15327,12 @@ async function resolveRepoPath(projectDir) {
16353
15327
  } catch {
16354
15328
  }
16355
15329
  const cwdCounts = /* @__PURE__ */ new Map();
16356
- const entries = await fs16.promises.readdir(projectDir, {
15330
+ const entries = await fs18.promises.readdir(projectDir, {
16357
15331
  withFileTypes: true
16358
15332
  });
16359
15333
  for (const entry of entries) {
16360
15334
  if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
16361
- const cwd = await extractCwdFromJsonl(path21.join(projectDir, entry.name));
15335
+ const cwd = await extractCwdFromJsonl(path20.join(projectDir, entry.name));
16362
15336
  if (cwd) {
16363
15337
  cwdCounts.set(cwd, (cwdCounts.get(cwd) ?? 0) + 1);
16364
15338
  }
@@ -16377,7 +15351,7 @@ async function resolveRepoPath(projectDir) {
16377
15351
  return null;
16378
15352
  }
16379
15353
  async function extractCwdFromJsonl(filePath) {
16380
- const stream = fs16.createReadStream(filePath, { encoding: "utf-8" });
15354
+ const stream = fs18.createReadStream(filePath, { encoding: "utf-8" });
16381
15355
  const rl = readline2.createInterface({ input: stream, crlfDelay: Infinity });
16382
15356
  try {
16383
15357
  for await (const line of rl) {
@@ -16399,12 +15373,12 @@ async function extractCwdFromJsonl(filePath) {
16399
15373
  async function collectFiles(dir, baseDir, sourceName, repoPath, results) {
16400
15374
  let entries;
16401
15375
  try {
16402
- entries = await fs16.promises.readdir(dir, { withFileTypes: true });
15376
+ entries = await fs18.promises.readdir(dir, { withFileTypes: true });
16403
15377
  } catch {
16404
15378
  return;
16405
15379
  }
16406
15380
  for (const entry of entries) {
16407
- const fullPath = path21.join(dir, entry.name);
15381
+ const fullPath = path20.join(dir, entry.name);
16408
15382
  if (entry.isDirectory()) {
16409
15383
  if (SKIP_DIRS.has(entry.name)) continue;
16410
15384
  await collectFiles(fullPath, baseDir, sourceName, repoPath, results);
@@ -16426,19 +15400,19 @@ function fallbackDecode(encodedName) {
16426
15400
  var ClaudeSource = class {
16427
15401
  name = "claude";
16428
15402
  async scan() {
16429
- const baseDir = path21.join(os10.homedir(), ".claude", "projects");
15403
+ const baseDir = path20.join(os10.homedir(), ".claude", "projects");
16430
15404
  try {
16431
- await fs16.promises.access(baseDir);
15405
+ await fs18.promises.access(baseDir);
16432
15406
  } catch {
16433
15407
  return [];
16434
15408
  }
16435
- const projectDirs = await fs16.promises.readdir(baseDir, {
15409
+ const projectDirs = await fs18.promises.readdir(baseDir, {
16436
15410
  withFileTypes: true
16437
15411
  });
16438
15412
  const dirEntries = projectDirs.filter((d) => d.isDirectory());
16439
15413
  const resultArrays = await Promise.all(
16440
15414
  dirEntries.map(async (dir) => {
16441
- const projectPath = path21.join(baseDir, dir.name);
15415
+ const projectPath = path20.join(baseDir, dir.name);
16442
15416
  const repoPath = await resolveRepoPath(projectPath) ?? fallbackDecode(dir.name);
16443
15417
  const files = [];
16444
15418
  await collectFiles(
@@ -16456,12 +15430,12 @@ var ClaudeSource = class {
16456
15430
  };
16457
15431
 
16458
15432
  // src/sources/codex.ts
16459
- import fs17 from "fs";
15433
+ import fs19 from "fs";
16460
15434
  import os11 from "os";
16461
- import path22 from "path";
15435
+ import path21 from "path";
16462
15436
  import readline3 from "readline";
16463
15437
  async function parseSessionMeta(filePath) {
16464
- const stream = fs17.createReadStream(filePath, { encoding: "utf-8" });
15438
+ const stream = fs19.createReadStream(filePath, { encoding: "utf-8" });
16465
15439
  const rl = readline3.createInterface({ input: stream, crlfDelay: Infinity });
16466
15440
  try {
16467
15441
  for await (const line of rl) {
@@ -16486,12 +15460,12 @@ async function findJsonlFiles(dir) {
16486
15460
  async function walk(d) {
16487
15461
  let entries;
16488
15462
  try {
16489
- entries = await fs17.promises.readdir(d, { withFileTypes: true });
15463
+ entries = await fs19.promises.readdir(d, { withFileTypes: true });
16490
15464
  } catch {
16491
15465
  return;
16492
15466
  }
16493
15467
  for (const entry of entries) {
16494
- const full = path22.join(d, entry.name);
15468
+ const full = path21.join(d, entry.name);
16495
15469
  if (entry.isDirectory()) {
16496
15470
  await walk(full);
16497
15471
  } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
@@ -16505,11 +15479,11 @@ async function findJsonlFiles(dir) {
16505
15479
  async function loadHistory(historyPath) {
16506
15480
  const map = /* @__PURE__ */ new Map();
16507
15481
  try {
16508
- await fs17.promises.access(historyPath);
15482
+ await fs19.promises.access(historyPath);
16509
15483
  } catch {
16510
15484
  return map;
16511
15485
  }
16512
- const stream = fs17.createReadStream(historyPath, { encoding: "utf-8" });
15486
+ const stream = fs19.createReadStream(historyPath, { encoding: "utf-8" });
16513
15487
  const rl = readline3.createInterface({ input: stream, crlfDelay: Infinity });
16514
15488
  try {
16515
15489
  for await (const line of rl) {
@@ -16536,14 +15510,14 @@ async function loadHistory(historyPath) {
16536
15510
  var CodexSource = class {
16537
15511
  name = "codex";
16538
15512
  async scan() {
16539
- const codexDir = path22.join(os11.homedir(), ".codex");
16540
- const sessionsDir = path22.join(codexDir, "sessions");
15513
+ const codexDir = path21.join(os11.homedir(), ".codex");
15514
+ const sessionsDir = path21.join(codexDir, "sessions");
16541
15515
  try {
16542
- await fs17.promises.access(sessionsDir);
15516
+ await fs19.promises.access(sessionsDir);
16543
15517
  } catch {
16544
15518
  return [];
16545
15519
  }
16546
- const historyPath = path22.join(codexDir, "history.jsonl");
15520
+ const historyPath = path21.join(codexDir, "history.jsonl");
16547
15521
  const [jsonlFiles, historyMap] = await Promise.all([
16548
15522
  findJsonlFiles(sessionsDir),
16549
15523
  loadHistory(historyPath)
@@ -16566,8 +15540,8 @@ var CodexSource = class {
16566
15540
  });
16567
15541
  const historyLines = historyMap.get(meta.sessionId);
16568
15542
  if (historyLines) {
16569
- const sessionDir = path22.relative(sessionsDir, path22.dirname(filePath));
16570
- const historyAbsPath = path22.join(
15543
+ const sessionDir = path21.relative(sessionsDir, path21.dirname(filePath));
15544
+ const historyAbsPath = path21.join(
16571
15545
  sessionsDir,
16572
15546
  sessionDir,
16573
15547
  `history-${meta.sessionId}.jsonl`
@@ -16587,18 +15561,18 @@ var CodexSource = class {
16587
15561
  };
16588
15562
 
16589
15563
  // src/sources/copilotChat.ts
16590
- import fs18 from "fs";
15564
+ import fs20 from "fs";
16591
15565
  import os12 from "os";
16592
- import path23 from "path";
15566
+ import path22 from "path";
16593
15567
  import { fileURLToPath } from "url";
16594
15568
  function vsCodeUserDirs() {
16595
15569
  const home = os12.homedir();
16596
15570
  const dirs = [
16597
- path23.join(home, "Library", "Application Support", "Code", "User"),
16598
- path23.join(home, ".config", "Code", "User")
15571
+ path22.join(home, "Library", "Application Support", "Code", "User"),
15572
+ path22.join(home, ".config", "Code", "User")
16599
15573
  ];
16600
15574
  if (process.env.APPDATA) {
16601
- dirs.push(path23.join(process.env.APPDATA, "Code", "User"));
15575
+ dirs.push(path22.join(process.env.APPDATA, "Code", "User"));
16602
15576
  }
16603
15577
  return dirs;
16604
15578
  }
@@ -16613,7 +15587,7 @@ function uriToFsPath(uri) {
16613
15587
  async function readWorkspaceFolder(workspaceJsonPath) {
16614
15588
  let raw;
16615
15589
  try {
16616
- raw = await fs18.promises.readFile(workspaceJsonPath, "utf-8");
15590
+ raw = await fs20.promises.readFile(workspaceJsonPath, "utf-8");
16617
15591
  } catch {
16618
15592
  return null;
16619
15593
  }
@@ -16635,10 +15609,10 @@ var CopilotChatSource = class {
16635
15609
  async scan() {
16636
15610
  const results = [];
16637
15611
  for (const userDir of vsCodeUserDirs()) {
16638
- const workspaceStorage = path23.join(userDir, "workspaceStorage");
15612
+ const workspaceStorage = path22.join(userDir, "workspaceStorage");
16639
15613
  let hashDirs;
16640
15614
  try {
16641
- hashDirs = await fs18.promises.readdir(workspaceStorage, {
15615
+ hashDirs = await fs20.promises.readdir(workspaceStorage, {
16642
15616
  withFileTypes: true
16643
15617
  });
16644
15618
  } catch {
@@ -16646,22 +15620,22 @@ var CopilotChatSource = class {
16646
15620
  }
16647
15621
  for (const hash of hashDirs) {
16648
15622
  if (!hash.isDirectory()) continue;
16649
- const wsRoot = path23.join(workspaceStorage, hash.name);
16650
- const transcriptsDir = path23.join(
15623
+ const wsRoot = path22.join(workspaceStorage, hash.name);
15624
+ const transcriptsDir = path22.join(
16651
15625
  wsRoot,
16652
15626
  "GitHub.copilot-chat",
16653
15627
  "transcripts"
16654
15628
  );
16655
15629
  let transcriptEntries;
16656
15630
  try {
16657
- transcriptEntries = await fs18.promises.readdir(transcriptsDir, {
15631
+ transcriptEntries = await fs20.promises.readdir(transcriptsDir, {
16658
15632
  withFileTypes: true
16659
15633
  });
16660
15634
  } catch {
16661
15635
  continue;
16662
15636
  }
16663
15637
  const repoPath = await readWorkspaceFolder(
16664
- path23.join(wsRoot, "workspace.json")
15638
+ path22.join(wsRoot, "workspace.json")
16665
15639
  );
16666
15640
  if (!repoPath) continue;
16667
15641
  for (const entry of transcriptEntries) {
@@ -16669,7 +15643,7 @@ var CopilotChatSource = class {
16669
15643
  const sessionId = entry.name.slice(0, -".jsonl".length);
16670
15644
  results.push({
16671
15645
  sourceName: this.name,
16672
- absolutePath: path23.join(transcriptsDir, entry.name),
15646
+ absolutePath: path22.join(transcriptsDir, entry.name),
16673
15647
  repoPath,
16674
15648
  metadata: { sessionId }
16675
15649
  });
@@ -16709,7 +15683,7 @@ function reportRedactionStats(noun, stats) {
16709
15683
  async function filterByTimeRange(group, range) {
16710
15684
  const results = await Promise.all(
16711
15685
  group.files.map(
16712
- (f) => fs19.promises.stat(f.absolutePath).then((s) => ({ file: f, stat: s })).catch(() => null)
15686
+ (f) => fs21.promises.stat(f.absolutePath).then((s) => ({ file: f, stat: s })).catch(() => null)
16713
15687
  )
16714
15688
  );
16715
15689
  const filtered = [];
@@ -16736,10 +15710,10 @@ async function runInteractive() {
16736
15710
  s.start(`Scanning ${source.name} logs...`);
16737
15711
  const allFiles = await source.scan();
16738
15712
  const allGroups = await mergeByRepo(allFiles);
16739
- const repoRoot = path24.resolve(repo.root);
15713
+ const repoRoot = path23.resolve(repo.root);
16740
15714
  const matching = allGroups.filter((g) => {
16741
- const resolved = path24.resolve(g.repoPath);
16742
- return resolved === repoRoot || resolved.startsWith(repoRoot + path24.sep);
15715
+ const resolved = path23.resolve(g.repoPath);
15716
+ return resolved === repoRoot || resolved.startsWith(repoRoot + path23.sep);
16743
15717
  });
16744
15718
  if (matching.length === 0) {
16745
15719
  s.stop(`No ${source.name} logs found for ${repo.name}.`);
@@ -16770,7 +15744,7 @@ async function runInteractive() {
16770
15744
  }
16771
15745
  }
16772
15746
  const envFileNames = await discoverEnvFiles(repoRoot);
16773
- const envFilePaths = envFileNames.map((n) => path24.join(repoRoot, n));
15747
+ const envFilePaths = envFileNames.map((n) => path23.join(repoRoot, n));
16774
15748
  const additionalFiles = await promptSecretFiles(envFileNames);
16775
15749
  const secretResult = await collectSecrets(
16776
15750
  repoRoot,