hillclimb 0.9.7 → 0.9.8

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 +138 -23
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -115,6 +115,7 @@ function formatError(err) {
115
115
  import fs2 from "fs";
116
116
  import http from "http";
117
117
  import https from "https";
118
+ import net from "net";
118
119
  var DEFAULT_SOCKET_IDLE_TIMEOUT_MS = 12e4;
119
120
  var DEFAULT_DEADLINE_MS = 60 * 6e4;
120
121
  var MAX_RESPONSE_BODY_BYTES = 64 * 1024;
@@ -129,18 +130,33 @@ async function putToPresignedUrl(url, headers, body, options) {
129
130
  let inputStream;
130
131
  let progressTimer;
131
132
  let phase = "connecting";
133
+ let requestFinished = false;
134
+ const seen = {};
135
+ const snapshot = () => ({
136
+ elapsedMs: Date.now() - start,
137
+ phase,
138
+ requestFinished,
139
+ totalBodyBytes: contentLength,
140
+ ...inputStream ? { bodyBytesRead: inputStream.bytesRead } : {},
141
+ ...seen,
142
+ // Bun fills the peer address in only after 'connect'.
143
+ remoteAddress: seen.remoteAddress ?? req.socket?.remoteAddress
144
+ });
132
145
  let settled = false;
133
146
  const settleResolve = (result) => {
134
147
  if (settled) return;
135
148
  settled = true;
149
+ const attempt = snapshot();
136
150
  clearTimeout(deadlineTimer);
137
151
  clearInterval(progressTimer);
138
152
  inputStream?.destroy();
139
- resolve(result);
153
+ resolve({ ...result, attempt });
140
154
  };
141
155
  const settleReject = (err) => {
142
156
  if (settled) return;
143
157
  settled = true;
158
+ err.message = redactLocalAddress(err.message);
159
+ err.putAttempt = snapshot();
144
160
  clearTimeout(deadlineTimer);
145
161
  clearInterval(progressTimer);
146
162
  inputStream?.destroy();
@@ -158,6 +174,13 @@ async function putToPresignedUrl(url, headers, body, options) {
158
174
  },
159
175
  (res) => {
160
176
  phase = "receiving-response";
177
+ seen.status = res.statusCode;
178
+ const header2 = (name) => {
179
+ const value = res.headers[name];
180
+ return Array.isArray(value) ? value[0] : value;
181
+ };
182
+ seen.requestId = header2("x-amz-request-id");
183
+ seen.hostId = header2("x-amz-id-2");
161
184
  const chunks = [];
162
185
  let buffered = 0;
163
186
  res.on("data", (chunk) => {
@@ -191,11 +214,32 @@ async function putToPresignedUrl(url, headers, body, options) {
191
214
  const connected = () => {
192
215
  if (phase === "connecting") phase = "sending";
193
216
  };
194
- if (parsed.protocol === "https:") socket.once("secureConnect", connected);
195
- else if (!socket.connecting) connected();
217
+ const tcpConnected = () => {
218
+ seen.connectMs = Date.now() - start;
219
+ seen.remoteAddress = socket.remoteAddress;
220
+ seen.localNet = addressClass(socket.localAddress);
221
+ };
222
+ if (!socket.connecting) tcpConnected();
223
+ else socket.once("connect", tcpConnected);
224
+ if (parsed.protocol === "https:") {
225
+ socket.once("secureConnect", () => {
226
+ seen.tlsMs = Date.now() - start;
227
+ const secure = socket;
228
+ try {
229
+ seen.tlsProtocol = secure.getProtocol?.() ?? void 0;
230
+ const issuer = secure.getPeerCertificate?.()?.issuer;
231
+ const name = [issuer?.O, issuer?.CN].filter(Boolean).join("/");
232
+ if (name) seen.tlsIssuer = name;
233
+ } catch {
234
+ }
235
+ connected();
236
+ });
237
+ } else if (!socket.connecting) connected();
196
238
  else socket.once("connect", connected);
197
239
  });
198
240
  req.on("finish", () => {
241
+ if (req.socket?.errored) return;
242
+ requestFinished = true;
199
243
  if (phase !== "receiving-response") phase = "awaiting-response";
200
244
  });
201
245
  if (options?.onProgress) {
@@ -204,7 +248,8 @@ async function putToPresignedUrl(url, headers, body, options) {
204
248
  elapsedMs: Date.now() - start,
205
249
  phase,
206
250
  socketBytesWritten: req.socket?.bytesWritten ?? 0,
207
- totalBodyBytes: contentLength
251
+ totalBodyBytes: contentLength,
252
+ requestFinished
208
253
  });
209
254
  }, options.progressIntervalMs ?? 3e4);
210
255
  progressTimer.unref();
@@ -229,10 +274,36 @@ async function putToPresignedUrl(url, headers, body, options) {
229
274
  }
230
275
  });
231
276
  }
277
+ function addressClass(address) {
278
+ if (!address) return void 0;
279
+ const ip = address.startsWith("::ffff:") ? address.slice(7) : address;
280
+ if (net.isIPv4(ip)) {
281
+ const [a, b] = ip.split(".").map(Number);
282
+ if (a === 10 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168)
283
+ return "private";
284
+ if (a === 100 && b >= 64 && b <= 127) return "cgnat";
285
+ if (a === 169 && b === 254) return "link-local";
286
+ if (a === 127) return "loopback";
287
+ return "public";
288
+ }
289
+ const lower = ip.toLowerCase();
290
+ if (lower === "::1") return "loopback";
291
+ if (/^fe[89ab]/.test(lower)) return "link-local";
292
+ if (/^f[cd]/.test(lower)) return "private";
293
+ return "public";
294
+ }
295
+ function redactLocalAddress(message) {
296
+ return message.replace(/ - Local \([^)]*\)/g, "");
297
+ }
232
298
 
233
299
  // src/platform/retry.ts
234
300
  var MAX_ATTEMPTS = 3;
235
- var TRANSIENT_DELAYS_MS = [1e3, 3e3];
301
+ var LARGE_PUT_BYTES = 8 * 1024 * 1024;
302
+ var LARGE_PUT_MAX_ATTEMPTS = 5;
303
+ function putMaxAttempts(bodyBytes) {
304
+ return bodyBytes >= LARGE_PUT_BYTES ? LARGE_PUT_MAX_ATTEMPTS : MAX_ATTEMPTS;
305
+ }
306
+ var TRANSIENT_DELAYS_MS = [1e3, 3e3, 1e4, 3e4];
236
307
  var RATE_LIMIT_DELAYS_MS = [5e3, 15e3];
237
308
  var MAX_RETRY_AFTER_MS = 3e4;
238
309
  var CONNECT_FAILURE_CODES = /* @__PURE__ */ new Set([
@@ -301,7 +372,7 @@ function apiRetryDelayMs(input) {
301
372
  return null;
302
373
  }
303
374
  function putRetryDelayMs(input) {
304
- if (input.attempt >= MAX_ATTEMPTS) return null;
375
+ if (input.attempt >= (input.maxAttempts ?? MAX_ATTEMPTS)) return null;
305
376
  const delayIndex = input.attempt - 1;
306
377
  if (input.status !== void 0) {
307
378
  if (input.status === 429 || input.status === 500 || input.status === 502 || input.status === 503 || input.status === 504) {
@@ -599,6 +670,7 @@ var PlatformClient = class {
599
670
  const context = {
600
671
  contributionId,
601
672
  uploadId: target2.upload.id,
673
+ filename: input.originalFilename,
602
674
  requireVerificationOnConflict: true
603
675
  };
604
676
  try {
@@ -790,6 +862,12 @@ var PlatformClient = class {
790
862
  }
791
863
  })();
792
864
  const sizeBytes = body.kind === "buffer" ? body.buffer.byteLength : body.sizeBytes;
865
+ const maxAttempts = putMaxAttempts(sizeBytes);
866
+ const label = [
867
+ refresh?.filename && `file=${refresh.filename}`,
868
+ refresh?.uploadId && `upload=${refresh.uploadId}`,
869
+ `size=${sizeBytes}`
870
+ ].filter(Boolean).join(" ");
793
871
  let hadNetworkError = false;
794
872
  let refreshed = false;
795
873
  for (let attempt = 1; ; attempt++) {
@@ -799,52 +877,56 @@ var PlatformClient = class {
799
877
  res = await putToPresignedUrl(presignedUrl, headers, body, {
800
878
  onProgress: (progress) => appendLog(
801
879
  "info",
802
- `[PUT ${host}] progress (attempt=${attempt}, phase=${progress.phase}, elapsedMs=${progress.elapsedMs}, socketBytesWritten=${progress.socketBytesWritten}, totalBodyBytes=${progress.totalBodyBytes})`
880
+ `[PUT ${host}] progress (attempt=${attempt}, phase=${progress.phase}, elapsedMs=${progress.elapsedMs}, requestFinished=${progress.requestFinished}, socketBytesWritten=${progress.socketBytesWritten}, totalBodyBytes=${progress.totalBodyBytes}) | ${label}`
803
881
  )
804
882
  });
805
883
  } catch (err) {
806
884
  hadNetworkError = true;
885
+ const diagnostics2 = `| ${label} ${formatPutAttempt(err.putAttempt)}`;
807
886
  const delayMs2 = putRetryDelayMs({
808
887
  attempt,
809
- networkErrorCode: networkErrorCode(err)
888
+ networkErrorCode: networkErrorCode(err),
889
+ maxAttempts
810
890
  });
811
891
  if (delayMs2 !== null) {
812
892
  appendLog(
813
893
  "warn",
814
- `[PUT ${host}] network error, retrying in ${delayMs2}ms (attempt ${attempt}/${MAX_ATTEMPTS}) ${formatError(err)}`
894
+ `[PUT ${host}] network error, retrying in ${delayMs2}ms (attempt ${attempt}/${maxAttempts}) ${formatError(err)} ${diagnostics2}`
815
895
  );
816
896
  await sleep(delayMs2);
817
897
  continue;
818
898
  }
819
899
  appendLog(
820
900
  "error",
821
- `[PUT ${host}] FAILED ${Date.now() - start}ms ${formatError(err)}`
901
+ `[PUT ${host}] FAILED ${Date.now() - start}ms ${formatError(err)} (attempt ${attempt}/${maxAttempts}) ${diagnostics2}`
822
902
  );
823
903
  throw err;
824
904
  }
825
905
  const elapsedMs = Date.now() - start;
906
+ const diagnostics = `| ${label} ${formatPutAttempt(res.attempt)}`;
826
907
  if (res.ok) {
827
908
  appendLog(
828
909
  "info",
829
- `[PUT ${host}] ${res.status} ${elapsedMs}ms (${sizeBytes} bytes)`
910
+ `[PUT ${host}] ${res.status} ${elapsedMs}ms (${sizeBytes} bytes) (attempt ${attempt}/${maxAttempts}) | ${label}${sizeBytes >= LARGE_PUT_BYTES ? ` ${formatPutAttempt(res.attempt)}` : ""}`
830
911
  );
831
912
  return;
832
913
  }
833
914
  if (res.status === 412 && hadNetworkError && !refresh?.requireVerificationOnConflict) {
834
915
  appendLog(
835
916
  "info",
836
- `[PUT ${host}] 412 after retry \u2014 object already stored (${sizeBytes} bytes)`
917
+ `[PUT ${host}] 412 after retry \u2014 object already stored (${sizeBytes} bytes) ${diagnostics}`
837
918
  );
838
919
  return;
839
920
  }
840
921
  const code = /<Code>\s*([A-Za-z][A-Za-z0-9]*)\s*<\/Code>/.exec(
841
922
  res.bodyText
842
923
  )?.[1];
843
- if ((res.status === 400 || res.status === 403) && code === "ExpiredToken" && refresh && attempt < MAX_ATTEMPTS && !refreshed) {
924
+ const urlExpired = res.status === 403 && code === "AccessDenied" && /<Message>\s*Request has expired\s*<\/Message>/.test(res.bodyText);
925
+ if ((res.status === 400 || res.status === 403) && (code === "ExpiredToken" || urlExpired) && refresh && attempt < maxAttempts && !refreshed) {
844
926
  refreshed = true;
845
927
  appendLog(
846
928
  "warn",
847
- `[PUT ${host}] ${res.status} ${elapsedMs}ms (code=ExpiredToken); refreshing AWS upload URL once for the existing object`
929
+ `[PUT ${host}] ${res.status} ${elapsedMs}ms (code=${code}${urlExpired ? ", request expired" : ""}); refreshing AWS upload URL once for the existing object ${diagnostics}`
848
930
  );
849
931
  const renewed = await this.request(
850
932
  "POST",
@@ -864,19 +946,23 @@ var PlatformClient = class {
864
946
  headers = renewed.headers;
865
947
  continue;
866
948
  }
867
- const delayMs = putRetryDelayMs({ attempt, status: res.status });
949
+ const delayMs = putRetryDelayMs({
950
+ attempt,
951
+ status: res.status,
952
+ maxAttempts
953
+ });
868
954
  if (delayMs !== null) {
869
955
  appendLog(
870
956
  "warn",
871
- `[PUT ${host}] ${res.status} ${elapsedMs}ms, retrying in ${delayMs}ms (attempt ${attempt}/${MAX_ATTEMPTS})`
957
+ `[PUT ${host}] ${res.status} ${elapsedMs}ms${code ? ` (code=${code})` : ""}, retrying in ${delayMs}ms (attempt ${attempt}/${maxAttempts}) ${diagnostics}`
872
958
  );
873
959
  await sleep(delayMs);
874
960
  continue;
875
961
  }
876
- const detail = code ? `(code=${code})` : res.bodyText;
962
+ const detail = code ? `(code=${code})` : res.bodyText ? `(${Buffer.byteLength(res.bodyText)}-byte body without an S3 error code)` : "";
877
963
  appendLog(
878
964
  "warn",
879
- `[PUT ${host}] ${res.status} ${elapsedMs}ms${code ? ` (code=${code})` : ""}`
965
+ `[PUT ${host}] ${res.status} ${elapsedMs}ms${code ? ` (code=${code})` : ""} (attempt ${attempt}/${maxAttempts}) ${diagnostics}`
880
966
  );
881
967
  throw new PlatformError(
882
968
  `PUT to presigned URL failed: HTTP ${res.status}${detail ? ` ${detail}` : ""}`,
@@ -906,6 +992,31 @@ var PlatformClient = class {
906
992
  );
907
993
  }
908
994
  };
995
+ function formatPutAttempt(attempt) {
996
+ const fields = [];
997
+ if (attempt) {
998
+ fields.push(
999
+ `elapsedMs=${attempt.elapsedMs}`,
1000
+ `phase=${attempt.phase}`,
1001
+ `requestFinished=${attempt.requestFinished}`
1002
+ );
1003
+ if (attempt.bodyBytesRead !== void 0)
1004
+ fields.push(`bodyBytesRead=${attempt.bodyBytesRead}`);
1005
+ if (attempt.connectMs !== void 0)
1006
+ fields.push(`connectMs=${attempt.connectMs}`);
1007
+ if (attempt.tlsMs !== void 0) fields.push(`tlsMs=${attempt.tlsMs}`);
1008
+ if (attempt.remoteAddress) fields.push(`remote=${attempt.remoteAddress}`);
1009
+ if (attempt.localNet) fields.push(`localNet=${attempt.localNet}`);
1010
+ if (attempt.tlsProtocol) fields.push(`tls=${attempt.tlsProtocol}`);
1011
+ if (attempt.tlsIssuer)
1012
+ fields.push(`issuer=${JSON.stringify(attempt.tlsIssuer)}`);
1013
+ if (attempt.status !== void 0) fields.push(`status=${attempt.status}`);
1014
+ if (attempt.requestId) fields.push(`requestId=${attempt.requestId}`);
1015
+ if (attempt.hostId) fields.push(`hostId=${attempt.hostId}`);
1016
+ }
1017
+ fields.push(`node=${process.version}`, `platform=${process.platform}`);
1018
+ return fields.join(" ");
1019
+ }
909
1020
 
910
1021
  // src/platform/hooks.ts
911
1022
  import { execFileSync } from "child_process";
@@ -3729,7 +3840,7 @@ var DEFAULT_STATE_DIR = path9.join(
3729
3840
  "agent-uploads"
3730
3841
  );
3731
3842
  var DEFAULT_LOCK_WAIT_MS = 5 * 60 * 1e3;
3732
- var DEFAULT_LIVE_OWNER_MAX_WAIT_MS = 4 * 60 * 60 * 1e3;
3843
+ var DEFAULT_LIVE_OWNER_MAX_WAIT_MS = 6 * 60 * 60 * 1e3;
3733
3844
  var DEFAULT_LOCK_RETRY_DELAY_MS = 500;
3734
3845
  var DEFAULT_STATE_TTL_MS = 24 * 60 * 60 * 1e3;
3735
3846
  var STALE_LOCK_TTL_MS = DEFAULT_LIVE_OWNER_MAX_WAIT_MS;
@@ -14356,7 +14467,11 @@ var PlatformUploadOutput = class {
14356
14467
  presigned.headers,
14357
14468
  filePath,
14358
14469
  sizeBytes,
14359
- { contributionId, uploadId: presigned.upload.id }
14470
+ {
14471
+ contributionId,
14472
+ uploadId: presigned.upload.id,
14473
+ filename: zipFilename
14474
+ }
14360
14475
  );
14361
14476
  appendLog("info", `PUT to presigned URL succeeded for ${zipFilename}`);
14362
14477
  }
@@ -17850,13 +17965,13 @@ async function uploadArtifact(client, contributionId, filename, mimeType, buffer
17850
17965
  presigned.presignedUrl,
17851
17966
  presigned.headers,
17852
17967
  buffer,
17853
- { contributionId, uploadId: presigned.upload.id }
17968
+ { contributionId, uploadId: presigned.upload.id, filename }
17854
17969
  );
17855
17970
  appendLog("info", `PUT to presigned URL succeeded for ${filename}`);
17856
17971
  }
17857
17972
 
17858
17973
  // package.json
17859
- var version = "0.9.7";
17974
+ var version = "0.9.8";
17860
17975
 
17861
17976
  // src/version.ts
17862
17977
  var CLI_VERSION = version;
@@ -19631,7 +19746,7 @@ async function uploadFile(client, contributionId, filename, mimeType, buffer) {
19631
19746
  presigned.presignedUrl,
19632
19747
  presigned.headers,
19633
19748
  buffer,
19634
- { contributionId, uploadId: presigned.upload.id }
19749
+ { contributionId, uploadId: presigned.upload.id, filename }
19635
19750
  );
19636
19751
  appendLog(
19637
19752
  "info",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hillclimb",
3
- "version": "0.9.7",
3
+ "version": "0.9.8",
4
4
  "description": "Extract and export AI coding tool logs grouped by repo",
5
5
  "license": "MIT",
6
6
  "type": "module",