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