hillclimb 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/main.js +67 -22
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -568,7 +568,7 @@ var PlatformClient = class {
568
568
  const sizeBytes = body.kind === "buffer" ? body.buffer.length : body.sizeBytes;
569
569
  const prefix = `/api/v1/contributions/${encodeURIComponent(contributionId)}`;
570
570
  let digest;
571
- const verify = async (uploadId) => {
571
+ const verify = async (uploadId, options = {}) => {
572
572
  if (digest === void 0) {
573
573
  const hash = createHash("sha256");
574
574
  let localSize = 0;
@@ -590,7 +590,8 @@ var PlatformClient = class {
590
590
  return this.verifyReplayArtifact(
591
591
  `${prefix}/uploads/${encodeURIComponent(uploadId)}/download`,
592
592
  sizeBytes,
593
- digest
593
+ digest,
594
+ options
594
595
  );
595
596
  };
596
597
  const put = async (target2) => {
@@ -642,7 +643,7 @@ var PlatformClient = class {
642
643
  "UPLOAD_REPLAY_MISMATCH"
643
644
  );
644
645
  }
645
- const verified = await verify(existing.id);
646
+ const verified = await verify(existing.id, { forbiddenIsAbsent: true });
646
647
  if (verified.exists) return;
647
648
  target = await this.request(
648
649
  "POST",
@@ -665,7 +666,7 @@ var PlatformClient = class {
665
666
  }
666
667
  await put(target);
667
668
  }
668
- async verifyReplayArtifact(relative, sizeBytes, expectedDigest) {
669
+ async verifyReplayArtifact(relative, sizeBytes, expectedDigest, options = {}) {
669
670
  const redirect = await fetch(this.url(relative), {
670
671
  headers: this.headers(),
671
672
  redirect: "manual",
@@ -708,8 +709,13 @@ var PlatformClient = class {
708
709
  } catch {
709
710
  throw new PlatformError("Stored artifact verification request failed");
710
711
  }
711
- if (response.status === 404) {
712
+ if (response.status === 404 || response.status === 403 && options.forbiddenIsAbsent) {
712
713
  await response.body?.cancel();
714
+ if (response.status === 403)
715
+ appendLog(
716
+ "info",
717
+ "upload-replay: storage refused the pending artifact (403); treating it as never written and letting the guarded PUT decide"
718
+ );
713
719
  return { exists: false, url: location };
714
720
  }
715
721
  if (!response.ok) {
@@ -16831,7 +16837,7 @@ async function uploadArtifact(client, contributionId, filename, mimeType, buffer
16831
16837
  }
16832
16838
 
16833
16839
  // package.json
16834
- var version = "0.9.0";
16840
+ var version = "0.9.2";
16835
16841
 
16836
16842
  // src/version.ts
16837
16843
  var CLI_VERSION = version;
@@ -16973,13 +16979,14 @@ function isPhantomClaudeHook(payload, toolOverride) {
16973
16979
  var MAX_FORK_SWEEP_THREADS = 8;
16974
16980
  async function runUploadInner(payload, options = {}) {
16975
16981
  const outcome = await runUploadForSession(payload, options);
16976
- if (!options.sweptFrom && payload.tool === "codex" && (outcome === "uploaded" || outcome === "skipped")) {
16982
+ const settled = outcome === "uploaded" || outcome === "unchanged" || outcome === "skipped";
16983
+ if (!options.sweptFrom && payload.tool === "codex" && settled) {
16977
16984
  await sweepCodexForkTranscripts(payload);
16978
16985
  }
16979
- if (!options.sweptFrom && payload.tool === "claude" && (outcome === "uploaded" || outcome === "skipped")) {
16986
+ if (!options.sweptFrom && payload.tool === "claude" && settled) {
16980
16987
  await sweepClaudeSubagentTranscripts(payload);
16981
16988
  }
16982
- if (!options.sweptFrom && (outcome === "uploaded" || outcome === "skipped")) {
16989
+ if (!options.sweptFrom && settled) {
16983
16990
  await sweepStrandedAgentUploads(resolveHookSessionId(payload));
16984
16991
  }
16985
16992
  return outcome;
@@ -17020,7 +17027,13 @@ async function sweepClaudeSubagentTranscripts(payload) {
17020
17027
  const project = await findProjectForCwd(cwd);
17021
17028
  if (!project) return;
17022
17029
  files.sort((a, b) => b.modifiedMs - a.modifiedMs);
17023
- const tally = { uploaded: 0, pending: 0, skipped: 0, failed: 0 };
17030
+ const tally = {
17031
+ uploaded: 0,
17032
+ unchanged: 0,
17033
+ pending: 0,
17034
+ skipped: 0,
17035
+ failed: 0
17036
+ };
17024
17037
  for (const entry of files.slice(0, MAX_SUBAGENT_SWEEP)) {
17025
17038
  const subagentSessionId = `agent-${entry.agentId}`;
17026
17039
  try {
@@ -17039,6 +17052,7 @@ async function sweepClaudeSubagentTranscripts(payload) {
17039
17052
  }
17040
17053
  );
17041
17054
  if (outcome === "uploaded") tally.uploaded++;
17055
+ else if (outcome === "unchanged") tally.unchanged++;
17042
17056
  else if (outcome === "pending") tally.pending++;
17043
17057
  else tally.skipped++;
17044
17058
  appendLog(
@@ -17055,7 +17069,7 @@ async function sweepClaudeSubagentTranscripts(payload) {
17055
17069
  }
17056
17070
  appendLog(
17057
17071
  "info",
17058
- `[${parentSessionId}] subagent sweep (found=${files.length}, uploaded=${tally.uploaded}, pending=${tally.pending}, skipped=${tally.skipped}, failed=${tally.failed})`
17072
+ `[${parentSessionId}] subagent sweep (found=${files.length}, uploaded=${tally.uploaded}, unchanged=${tally.unchanged}, pending=${tally.pending}, skipped=${tally.skipped}, failed=${tally.failed})`
17059
17073
  );
17060
17074
  } catch (err) {
17061
17075
  appendLog(
@@ -17178,7 +17192,13 @@ async function sweepCodexForkTranscripts(payload) {
17178
17192
  registeredRoots
17179
17193
  });
17180
17194
  if (discovery.forks.length === 0) return;
17181
- const tally = { uploaded: 0, pending: 0, skipped: 0, failed: 0 };
17195
+ const tally = {
17196
+ uploaded: 0,
17197
+ unchanged: 0,
17198
+ pending: 0,
17199
+ skipped: 0,
17200
+ failed: 0
17201
+ };
17182
17202
  for (const fork of discovery.forks.slice(0, MAX_FORK_SWEEP_THREADS)) {
17183
17203
  const project = await findProjectForCwd(fork.cwd) ?? parent;
17184
17204
  const forkPayload = {
@@ -17194,6 +17214,7 @@ async function sweepCodexForkTranscripts(payload) {
17194
17214
  sweptFrom: parentSessionId
17195
17215
  });
17196
17216
  if (outcome === "uploaded") tally.uploaded++;
17217
+ else if (outcome === "unchanged") tally.unchanged++;
17197
17218
  else if (outcome === "pending") tally.pending++;
17198
17219
  else tally.skipped++;
17199
17220
  appendLog(
@@ -17210,7 +17231,7 @@ async function sweepCodexForkTranscripts(payload) {
17210
17231
  }
17211
17232
  appendLog(
17212
17233
  "info",
17213
- `[${parentSessionId}] fork sweep (scannedRollouts=${discovery.scannedRollouts}, forks=${discovery.forks.length}, uploaded=${tally.uploaded}, pending=${tally.pending}, skipped=${tally.skipped}, failed=${tally.failed})`
17234
+ `[${parentSessionId}] fork sweep (scannedRollouts=${discovery.scannedRollouts}, forks=${discovery.forks.length}, uploaded=${tally.uploaded}, unchanged=${tally.unchanged}, pending=${tally.pending}, skipped=${tally.skipped}, failed=${tally.failed})`
17214
17235
  );
17215
17236
  } catch (err) {
17216
17237
  appendLog(
@@ -17219,6 +17240,13 @@ async function sweepCodexForkTranscripts(payload) {
17219
17240
  );
17220
17241
  }
17221
17242
  }
17243
+ async function settledOutcome(before, drained, repoRoot, sourceTool, sessionId) {
17244
+ if (!drained) return "pending";
17245
+ const after = await readUploadState(repoRoot, sourceTool, sessionId);
17246
+ if (!after) return "unchanged";
17247
+ if (!before) return "uploaded";
17248
+ return after.uploadCount !== before.uploadCount || after.lastUploadedAt !== before.lastUploadedAt ? "uploaded" : "unchanged";
17249
+ }
17222
17250
  async function runUploadForSession(payload, options) {
17223
17251
  if (isPhantomClaudeHook(payload)) {
17224
17252
  appendLog("info", "upload: skipping phantom Claude hook fired by Cursor");
@@ -17265,7 +17293,8 @@ async function runUploadForSession(payload, options) {
17265
17293
  "info",
17266
17294
  `[${sessionId}] replaying ${pending.length} pending capture(s) without reading the live transcript`
17267
17295
  );
17268
- return await drainCapturedSessions({
17296
+ const before2 = await readUploadState(repoRoot, sourceTool, sessionId);
17297
+ const drained2 = await drainCapturedSessions({
17269
17298
  sessionId,
17270
17299
  repoRoot,
17271
17300
  config,
@@ -17273,7 +17302,8 @@ async function runUploadForSession(payload, options) {
17273
17302
  eventKind: eventKind2,
17274
17303
  recordedAt,
17275
17304
  transcriptPath: firstPending.transcriptPath
17276
- }) ? "uploaded" : "pending";
17305
+ });
17306
+ return settledOutcome(before2, drained2, repoRoot, sourceTool, sessionId);
17277
17307
  };
17278
17308
  const transcriptPath = await resolveTranscriptPath(
17279
17309
  payload,
@@ -17330,7 +17360,8 @@ async function runUploadForSession(payload, options) {
17330
17360
  );
17331
17361
  return "skipped";
17332
17362
  }
17333
- const uploaded = await captureAndUploadSession({
17363
+ const before = await readUploadState(repoRoot, sourceTool, sessionId);
17364
+ const drained = await captureAndUploadSession({
17334
17365
  sessionId,
17335
17366
  transcriptPath: transcriptResolved,
17336
17367
  repoRoot,
@@ -17340,7 +17371,7 @@ async function runUploadForSession(payload, options) {
17340
17371
  recordedAt,
17341
17372
  subagent: options.subagent
17342
17373
  });
17343
- return uploaded ? "uploaded" : "pending";
17374
+ return settledOutcome(before, drained, repoRoot, sourceTool, sessionId);
17344
17375
  }
17345
17376
  var AGENT_MAX_UPLOAD_BYTES = 1024 * 1024 * 1024;
17346
17377
  function makeTranscriptGroup(repoRoot, sourceTool, transcriptPath, content) {
@@ -20402,6 +20433,7 @@ async function sweepCodexForkCaptures(params) {
20402
20433
  worktree: 0,
20403
20434
  sameTree: 0,
20404
20435
  unrelated: 0,
20436
+ deferred: 0,
20405
20437
  initialized: 0,
20406
20438
  uploaded: 0,
20407
20439
  unchanged: 0,
@@ -20409,8 +20441,14 @@ async function sweepCodexForkCaptures(params) {
20409
20441
  failed: 0,
20410
20442
  skipped: 0
20411
20443
  };
20412
- for (const fork of discovery.forks.slice(0, MAX_FORK_SWEEP_THREADS2)) {
20413
- const target = await resolveForkTarget(fork.cwd, repos);
20444
+ const targets = /* @__PURE__ */ new Map();
20445
+ const hooked = /* @__PURE__ */ new Set();
20446
+ for (const fork of discovery.forks) {
20447
+ let target = targets.get(fork.cwd);
20448
+ if (target === void 0) {
20449
+ target = await resolveForkTarget(fork.cwd, repos);
20450
+ targets.set(fork.cwd, target);
20451
+ }
20414
20452
  if (!target) {
20415
20453
  tally.unrelated++;
20416
20454
  continue;
@@ -20419,12 +20457,19 @@ async function sweepCodexForkCaptures(params) {
20419
20457
  tally.sameTree++;
20420
20458
  continue;
20421
20459
  }
20422
- tally.worktree++;
20423
20460
  const repo = {
20424
20461
  repoRoot: target.repoRoot,
20425
20462
  config: target.config
20426
20463
  };
20427
- await propagateCodexHooks(project.repoRoot, repo.repoRoot);
20464
+ if (!hooked.has(repo.repoRoot)) {
20465
+ hooked.add(repo.repoRoot);
20466
+ await propagateCodexHooks(project.repoRoot, repo.repoRoot);
20467
+ }
20468
+ if (tally.worktree >= MAX_FORK_SWEEP_THREADS2) {
20469
+ tally.deferred++;
20470
+ continue;
20471
+ }
20472
+ tally.worktree++;
20428
20473
  const forkPayload = {
20429
20474
  session_id: fork.threadId,
20430
20475
  cwd: fork.cwd,
@@ -20508,7 +20553,7 @@ async function sweepCodexForkCaptures(params) {
20508
20553
  }
20509
20554
  appendLog(
20510
20555
  "info",
20511
- `git-traces: fork sweep (parent=${parentSessionId}, event=${event}, triggerRepo=${project.repoRoot}, scannedRollouts=${discovery.scannedRollouts}, forks=${discovery.forks.length}, worktree=${tally.worktree}, sameTree=${tally.sameTree}, unrelated=${tally.unrelated}, initialized=${tally.initialized}, uploaded=${tally.uploaded}, unchanged=${tally.unchanged}, pending=${tally.pending}, failed=${tally.failed}, skipped=${tally.skipped})`
20556
+ `git-traces: fork sweep (parent=${parentSessionId}, event=${event}, triggerRepo=${project.repoRoot}, scannedRollouts=${discovery.scannedRollouts}, forks=${discovery.forks.length}, worktree=${tally.worktree}, sameTree=${tally.sameTree}, unrelated=${tally.unrelated}, initialized=${tally.initialized}, uploaded=${tally.uploaded}, unchanged=${tally.unchanged}, pending=${tally.pending}, failed=${tally.failed}, skipped=${tally.skipped}, deferred=${tally.deferred})`
20512
20557
  );
20513
20558
  }
20514
20559
  async function sweepCodexForksSafely(params) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hillclimb",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "Extract and export AI coding tool logs grouped by repo",
5
5
  "license": "MIT",
6
6
  "type": "module",