hillclimb 0.8.9 → 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 +647 -373
- 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
|
}
|
|
@@ -14711,9 +14934,11 @@ function createGitError(args, err, timeoutMs = GIT_COMMAND_TIMEOUT_MS) {
|
|
|
14711
14934
|
const message = isTimeout ? `${command} timed out after ${timeoutMs}ms` : failure.code === "ENOBUFS" ? `${command} output exceeded the ${formatSize(failure.maxBufferBytes ?? EXEC_OPTS.maxBuffer)} limit` : formatGitFailure(command, failure, err);
|
|
14712
14935
|
const wrapped = new Error(message);
|
|
14713
14936
|
wrapped.isGitTimeout = isTimeout;
|
|
14714
|
-
wrapped.gitExitStatus = failure.status;
|
|
14937
|
+
wrapped.gitExitStatus = failure.code || failure.signal ? void 0 : failure.status;
|
|
14715
14938
|
const stderr = outputToString(failure.stderr);
|
|
14716
14939
|
wrapped.gitStderr = stderr ? truncateOutput(stderr) : void 0;
|
|
14940
|
+
const stdout = outputToString(failure.stdout);
|
|
14941
|
+
wrapped.gitStdout = stdout ? truncateOutput(stdout) : void 0;
|
|
14717
14942
|
wrapped.isGitMissingObject = !isTimeout && failure.code !== "ENOBUFS" && matchesMissingObject(stderr);
|
|
14718
14943
|
return wrapped;
|
|
14719
14944
|
}
|
|
@@ -14752,6 +14977,19 @@ function isGitRepo(dir) {
|
|
|
14752
14977
|
return false;
|
|
14753
14978
|
}
|
|
14754
14979
|
}
|
|
14980
|
+
function stashRetryReason(failure) {
|
|
14981
|
+
const status = failure.gitExitStatus;
|
|
14982
|
+
if (status !== 1 && status !== 128) return null;
|
|
14983
|
+
if (/Unable to create [^\n]*index\.lock[^\n]*: File exists/.test(
|
|
14984
|
+
failure.gitStderr ?? ""
|
|
14985
|
+
)) {
|
|
14986
|
+
return "index-lock-contention";
|
|
14987
|
+
}
|
|
14988
|
+
if (status === 1 && !failure.gitStderr && !failure.gitStdout) {
|
|
14989
|
+
return "empty-stash-failure";
|
|
14990
|
+
}
|
|
14991
|
+
return null;
|
|
14992
|
+
}
|
|
14755
14993
|
function captureWorkingCommitSha(repoRoot) {
|
|
14756
14994
|
for (let attempt = 1; ; attempt++) {
|
|
14757
14995
|
let sha;
|
|
@@ -14778,11 +15016,11 @@ function captureWorkingCommitSha(repoRoot) {
|
|
|
14778
15016
|
const tree = git(repoRoot, ["write-tree"]);
|
|
14779
15017
|
return git(repoRoot, ["commit-tree", tree, "-m", "empty baseline"]);
|
|
14780
15018
|
}
|
|
14781
|
-
|
|
14782
|
-
|
|
15019
|
+
const retryReason = stashRetryReason(failure);
|
|
15020
|
+
if (!retryReason || attempt >= 5) throw err;
|
|
14783
15021
|
appendLog(
|
|
14784
15022
|
"warn",
|
|
14785
|
-
`git-traces: retrying stash create after index lock contention (repo=${repoRoot}, attempt=${attempt}, maxAttempts=5): ${failure.message}`
|
|
15023
|
+
`git-traces: retrying stash create after ${retryReason === "index-lock-contention" ? "index lock contention" : "empty Git failure (possible index contention)"} (repo=${repoRoot}, attempt=${attempt}, maxAttempts=5, reason=${retryReason}): ${failure.message}`
|
|
14786
15024
|
);
|
|
14787
15025
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 250);
|
|
14788
15026
|
continue;
|
|
@@ -15044,8 +15282,8 @@ function filterOmittedFilesFromTree(repoRoot, treeSha, options = {}) {
|
|
|
15044
15282
|
options.omittedFiles?.push(...omittedFiles);
|
|
15045
15283
|
return timeCaptureStage(repoRoot, "tracked-tree-filter", () => {
|
|
15046
15284
|
if (omittedFiles.length === 0) return treeSha;
|
|
15047
|
-
const tmpIndex =
|
|
15048
|
-
|
|
15285
|
+
const tmpIndex = path15.join(
|
|
15286
|
+
os8.tmpdir(),
|
|
15049
15287
|
`hillclimb-filter-${Date.now()}-${process.pid}`
|
|
15050
15288
|
);
|
|
15051
15289
|
const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
|
|
@@ -15059,7 +15297,7 @@ function filterOmittedFilesFromTree(repoRoot, treeSha, options = {}) {
|
|
|
15059
15297
|
return gitWithEnv(repoRoot, ["write-tree"], env);
|
|
15060
15298
|
} finally {
|
|
15061
15299
|
try {
|
|
15062
|
-
|
|
15300
|
+
fs15.unlinkSync(tmpIndex);
|
|
15063
15301
|
} catch {
|
|
15064
15302
|
}
|
|
15065
15303
|
}
|
|
@@ -15083,8 +15321,8 @@ function buildUntrackedTree(repoRoot, options = {}) {
|
|
|
15083
15321
|
if (!relPath) continue;
|
|
15084
15322
|
candidates++;
|
|
15085
15323
|
try {
|
|
15086
|
-
const absPath =
|
|
15087
|
-
const stat =
|
|
15324
|
+
const absPath = path15.join(repoRoot, relPath);
|
|
15325
|
+
const stat = fs15.lstatSync(absPath);
|
|
15088
15326
|
const reason = classifyOmission(
|
|
15089
15327
|
relPath,
|
|
15090
15328
|
stat.size,
|
|
@@ -15115,8 +15353,8 @@ function buildUntrackedTree(repoRoot, options = {}) {
|
|
|
15115
15353
|
`git-traces: untracked inventory (repo=${repoRoot}, candidates=${candidates}, kept=${kept.length}, keptBytes=${keptBytes}, omitted=${omitted}, omittedBytes=${omittedBytes}, skipped=${skipped})`
|
|
15116
15354
|
);
|
|
15117
15355
|
if (kept.length === 0) return null;
|
|
15118
|
-
const tmpIndex =
|
|
15119
|
-
|
|
15356
|
+
const tmpIndex = path15.join(
|
|
15357
|
+
os8.tmpdir(),
|
|
15120
15358
|
`hillclimb-untracked-${Date.now()}-${process.pid}`
|
|
15121
15359
|
);
|
|
15122
15360
|
const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
|
|
@@ -15136,7 +15374,7 @@ function buildUntrackedTree(repoRoot, options = {}) {
|
|
|
15136
15374
|
);
|
|
15137
15375
|
} finally {
|
|
15138
15376
|
try {
|
|
15139
|
-
|
|
15377
|
+
fs15.unlinkSync(tmpIndex);
|
|
15140
15378
|
} catch {
|
|
15141
15379
|
}
|
|
15142
15380
|
}
|
|
@@ -15166,14 +15404,38 @@ function buildSnapshotTree(repoRoot, stashSha, options = {}) {
|
|
|
15166
15404
|
() => buildUntrackedTree(repoRoot, { omittedFiles, limits })
|
|
15167
15405
|
);
|
|
15168
15406
|
if (options.log !== false) logOmittedSnapshotSummary(omittedFiles);
|
|
15169
|
-
const snapshotTree = timeCaptureStage(
|
|
15170
|
-
|
|
15171
|
-
|
|
15172
|
-
|
|
15173
|
-
|
|
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
|
+
});
|
|
15174
15432
|
options.scanOut?.push({
|
|
15175
15433
|
trackedTreeSha: trackedTree,
|
|
15176
15434
|
snapshotTreeSha: snapshotTree,
|
|
15435
|
+
assembly: {
|
|
15436
|
+
trackedTreeSha: filteredTrackedTree,
|
|
15437
|
+
untrackedTreeSha: untrackedTree
|
|
15438
|
+
},
|
|
15177
15439
|
limits,
|
|
15178
15440
|
omittedFiles: trackedOmitted
|
|
15179
15441
|
});
|
|
@@ -15182,8 +15444,9 @@ function buildSnapshotTree(repoRoot, stashSha, options = {}) {
|
|
|
15182
15444
|
function mergeSnapshotTrees(repoRoot, filteredTrackedTree, untrackedTree) {
|
|
15183
15445
|
if (!untrackedTree || untrackedTree === EMPTY_TREE_SHA)
|
|
15184
15446
|
return filteredTrackedTree;
|
|
15185
|
-
|
|
15186
|
-
|
|
15447
|
+
if (filteredTrackedTree === EMPTY_TREE_SHA) return untrackedTree;
|
|
15448
|
+
const tmpIndex = path15.join(
|
|
15449
|
+
os8.tmpdir(),
|
|
15187
15450
|
`hillclimb-index-${Date.now()}-${process.pid}`
|
|
15188
15451
|
);
|
|
15189
15452
|
const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
|
|
@@ -15232,7 +15495,7 @@ function mergeSnapshotTrees(repoRoot, filteredTrackedTree, untrackedTree) {
|
|
|
15232
15495
|
);
|
|
15233
15496
|
} finally {
|
|
15234
15497
|
try {
|
|
15235
|
-
|
|
15498
|
+
fs15.unlinkSync(tmpIndex);
|
|
15236
15499
|
} catch {
|
|
15237
15500
|
}
|
|
15238
15501
|
}
|
|
@@ -15246,19 +15509,28 @@ function createBundleFromTree(repoRoot, treeSha, sessionId, label) {
|
|
|
15246
15509
|
]);
|
|
15247
15510
|
const orphanRef = `refs/hillclimb/bundle/${sessionId}`;
|
|
15248
15511
|
pinRef(repoRoot, orphanRef, orphanCommit);
|
|
15249
|
-
|
|
15250
|
-
os7.tmpdir(),
|
|
15251
|
-
// Include the pid (like the other temp files in this module) so concurrent
|
|
15252
|
-
// git-traces workers — e.g. two sessions, or a parent + subagent — don't
|
|
15253
|
-
// collide on the same `git bundle create` path and its `.lock`.
|
|
15254
|
-
`hillclimb-bundle-${Date.now()}-${process.pid}.bundle`
|
|
15255
|
-
);
|
|
15512
|
+
let tmpDir;
|
|
15256
15513
|
try {
|
|
15257
|
-
|
|
15258
|
-
|
|
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;
|
|
15259
15531
|
} finally {
|
|
15260
15532
|
try {
|
|
15261
|
-
|
|
15533
|
+
if (tmpDir) fs15.rmSync(tmpDir, { recursive: true, force: true });
|
|
15262
15534
|
} catch {
|
|
15263
15535
|
}
|
|
15264
15536
|
deleteRef(repoRoot, orphanRef);
|
|
@@ -15456,7 +15728,7 @@ function buildBaselineMetadata(repoRoot, sessionId, tool, baselineSha, cliVersio
|
|
|
15456
15728
|
...omittedFiles.length > 0 ? { omittedFiles } : {}
|
|
15457
15729
|
},
|
|
15458
15730
|
author: { name: authorName, email: authorEmail },
|
|
15459
|
-
hostname:
|
|
15731
|
+
hostname: os8.hostname(),
|
|
15460
15732
|
cliVersion,
|
|
15461
15733
|
commits,
|
|
15462
15734
|
...lineage,
|
|
@@ -15548,9 +15820,9 @@ function parseCommitFiles(repoRoot, sha) {
|
|
|
15548
15820
|
oldPath
|
|
15549
15821
|
});
|
|
15550
15822
|
} else {
|
|
15551
|
-
const
|
|
15552
|
-
indexByPath.set(
|
|
15553
|
-
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 });
|
|
15554
15826
|
}
|
|
15555
15827
|
}
|
|
15556
15828
|
for (const line of numstat.split("\n")) {
|
|
@@ -15630,14 +15902,14 @@ function countScopedTurnTreeRefs(repoRoot, sessionId, epochPrefix3) {
|
|
|
15630
15902
|
|
|
15631
15903
|
// src/git-traces/session-state.ts
|
|
15632
15904
|
import crypto6 from "crypto";
|
|
15633
|
-
import
|
|
15634
|
-
import
|
|
15635
|
-
import
|
|
15905
|
+
import fs16 from "fs";
|
|
15906
|
+
import os9 from "os";
|
|
15907
|
+
import path16 from "path";
|
|
15636
15908
|
var CURRENT_SCHEMA_VERSION3 = 3;
|
|
15637
|
-
var DEFAULT_STATE_DIR2 =
|
|
15909
|
+
var DEFAULT_STATE_DIR2 = path16.join(os9.homedir(), ".hillclimb", "git-traces");
|
|
15638
15910
|
var LOCK_RETRIES2 = 120;
|
|
15639
15911
|
var LOCK_RETRY_DELAY_MS2 = 500;
|
|
15640
|
-
var
|
|
15912
|
+
var DEFAULT_LIVE_OWNER_MAX_WAIT_MS2 = 10 * 60 * 1e3;
|
|
15641
15913
|
var STALE_LOCK_TTL_MS3 = 60 * 60 * 1e3;
|
|
15642
15914
|
var LockContentionError = class extends Error {
|
|
15643
15915
|
constructor(lockPath, options) {
|
|
@@ -15650,25 +15922,25 @@ var LockContentionError = class extends Error {
|
|
|
15650
15922
|
};
|
|
15651
15923
|
function liveOwnerMaxWaitMs() {
|
|
15652
15924
|
const raw = process.env.HILLCLIMB_GIT_TRACES_LOCK_LIVE_OWNER_MAX_WAIT_MS;
|
|
15653
|
-
if (!raw) return
|
|
15925
|
+
if (!raw) return DEFAULT_LIVE_OWNER_MAX_WAIT_MS2;
|
|
15654
15926
|
const parsed = Number(raw);
|
|
15655
|
-
return Number.isFinite(parsed) && parsed >= 0 ? parsed :
|
|
15927
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_LIVE_OWNER_MAX_WAIT_MS2;
|
|
15656
15928
|
}
|
|
15657
15929
|
function stateDir3() {
|
|
15658
15930
|
return process.env.HILLCLIMB_GIT_TRACES_STATE_DIR ?? DEFAULT_STATE_DIR2;
|
|
15659
15931
|
}
|
|
15660
15932
|
function stateFileForRepo(repoRoot, tool, sessionId) {
|
|
15661
15933
|
const hash = crypto6.createHash("sha256").update(
|
|
15662
|
-
sessionId ? `${
|
|
15934
|
+
sessionId ? `${path16.resolve(repoRoot)}\0${tool}\0${sessionId}` : `${path16.resolve(repoRoot)}\0${tool}`
|
|
15663
15935
|
).digest("hex").slice(0, 16);
|
|
15664
|
-
return
|
|
15936
|
+
return path16.join(stateDir3(), `${hash}.json`);
|
|
15665
15937
|
}
|
|
15666
15938
|
function lockFileForRepo(repoRoot, tool, sessionId) {
|
|
15667
15939
|
return `${stateFileForRepo(repoRoot, tool, sessionId)}.lock`;
|
|
15668
15940
|
}
|
|
15669
15941
|
async function readStateFile(file) {
|
|
15670
15942
|
try {
|
|
15671
|
-
const raw = await
|
|
15943
|
+
const raw = await fs16.promises.readFile(file, "utf-8");
|
|
15672
15944
|
const parsed = JSON.parse(raw);
|
|
15673
15945
|
if (parsed.schemaVersion !== CURRENT_SCHEMA_VERSION3) {
|
|
15674
15946
|
return null;
|
|
@@ -15682,7 +15954,7 @@ async function readStoredStateFile(file) {
|
|
|
15682
15954
|
const state = await readStateFile(file);
|
|
15683
15955
|
if (!state) return null;
|
|
15684
15956
|
try {
|
|
15685
|
-
return { state, mtimeMs: (await
|
|
15957
|
+
return { state, mtimeMs: (await fs16.promises.stat(file)).mtimeMs };
|
|
15686
15958
|
} catch {
|
|
15687
15959
|
return null;
|
|
15688
15960
|
}
|
|
@@ -15690,26 +15962,26 @@ async function readStoredStateFile(file) {
|
|
|
15690
15962
|
async function listScopedSessionStates(repoRoot, tool) {
|
|
15691
15963
|
let entries;
|
|
15692
15964
|
try {
|
|
15693
|
-
entries = await
|
|
15965
|
+
entries = await fs16.promises.readdir(stateDir3(), { withFileTypes: true });
|
|
15694
15966
|
} catch {
|
|
15695
15967
|
return [];
|
|
15696
15968
|
}
|
|
15697
15969
|
const states = [];
|
|
15698
15970
|
for (const entry of entries) {
|
|
15699
15971
|
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
15700
|
-
const file =
|
|
15972
|
+
const file = path16.join(stateDir3(), entry.name);
|
|
15701
15973
|
const state = await readStateFile(file);
|
|
15702
15974
|
if (!state) continue;
|
|
15703
15975
|
if (typeof state.repoRoot !== "string" || typeof state.sessionId !== "string") {
|
|
15704
15976
|
continue;
|
|
15705
15977
|
}
|
|
15706
|
-
if (
|
|
15707
|
-
if (
|
|
15978
|
+
if (path16.resolve(state.repoRoot) !== path16.resolve(repoRoot)) continue;
|
|
15979
|
+
if (path16.resolve(file) !== path16.resolve(stateFileForRepo(repoRoot, tool, state.sessionId))) {
|
|
15708
15980
|
continue;
|
|
15709
15981
|
}
|
|
15710
15982
|
let mtimeMs = 0;
|
|
15711
15983
|
try {
|
|
15712
|
-
mtimeMs = (await
|
|
15984
|
+
mtimeMs = (await fs16.promises.stat(file)).mtimeMs;
|
|
15713
15985
|
} catch {
|
|
15714
15986
|
continue;
|
|
15715
15987
|
}
|
|
@@ -15720,26 +15992,26 @@ async function listScopedSessionStates(repoRoot, tool) {
|
|
|
15720
15992
|
async function listSessionStatesForSession(tool, sessionId) {
|
|
15721
15993
|
let entries;
|
|
15722
15994
|
try {
|
|
15723
|
-
entries = await
|
|
15995
|
+
entries = await fs16.promises.readdir(stateDir3(), { withFileTypes: true });
|
|
15724
15996
|
} catch {
|
|
15725
15997
|
return [];
|
|
15726
15998
|
}
|
|
15727
15999
|
const states = [];
|
|
15728
16000
|
for (const entry of entries) {
|
|
15729
16001
|
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
15730
|
-
const file =
|
|
16002
|
+
const file = path16.join(stateDir3(), entry.name);
|
|
15731
16003
|
const state = await readStateFile(file);
|
|
15732
16004
|
if (!state) continue;
|
|
15733
16005
|
if (typeof state.repoRoot !== "string" || typeof state.sessionId !== "string") {
|
|
15734
16006
|
continue;
|
|
15735
16007
|
}
|
|
15736
16008
|
if (state.sessionId !== sessionId) continue;
|
|
15737
|
-
if (
|
|
16009
|
+
if (path16.resolve(file) !== path16.resolve(stateFileForRepo(state.repoRoot, tool, sessionId))) {
|
|
15738
16010
|
continue;
|
|
15739
16011
|
}
|
|
15740
16012
|
let mtimeMs = 0;
|
|
15741
16013
|
try {
|
|
15742
|
-
mtimeMs = (await
|
|
16014
|
+
mtimeMs = (await fs16.promises.stat(file)).mtimeMs;
|
|
15743
16015
|
} catch {
|
|
15744
16016
|
continue;
|
|
15745
16017
|
}
|
|
@@ -15780,17 +16052,17 @@ async function writeLegacySessionState(state, tool) {
|
|
|
15780
16052
|
await writeStateFile(stateFileForRepo(state.repoRoot, tool), state);
|
|
15781
16053
|
}
|
|
15782
16054
|
async function writeStateFile(file, state) {
|
|
15783
|
-
await
|
|
16055
|
+
await fs16.promises.mkdir(stateDir3(), { recursive: true, mode: 448 });
|
|
15784
16056
|
const tmp = `${file}.tmp`;
|
|
15785
|
-
await
|
|
16057
|
+
await fs16.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
|
|
15786
16058
|
mode: 384
|
|
15787
16059
|
});
|
|
15788
|
-
await
|
|
16060
|
+
await fs16.promises.rename(tmp, file);
|
|
15789
16061
|
}
|
|
15790
16062
|
async function touchSessionState(repoRoot, tool, sessionId) {
|
|
15791
16063
|
const now = /* @__PURE__ */ new Date();
|
|
15792
16064
|
try {
|
|
15793
|
-
await
|
|
16065
|
+
await fs16.promises.utimes(
|
|
15794
16066
|
stateFileForRepo(repoRoot, tool, sessionId),
|
|
15795
16067
|
now,
|
|
15796
16068
|
now
|
|
@@ -15800,7 +16072,7 @@ async function touchSessionState(repoRoot, tool, sessionId) {
|
|
|
15800
16072
|
}
|
|
15801
16073
|
async function statSessionStateMtime(repoRoot, tool, sessionId) {
|
|
15802
16074
|
try {
|
|
15803
|
-
const stat = await
|
|
16075
|
+
const stat = await fs16.promises.stat(
|
|
15804
16076
|
stateFileForRepo(repoRoot, tool, sessionId)
|
|
15805
16077
|
);
|
|
15806
16078
|
return stat.mtimeMs;
|
|
@@ -15810,7 +16082,7 @@ async function statSessionStateMtime(repoRoot, tool, sessionId) {
|
|
|
15810
16082
|
}
|
|
15811
16083
|
async function deleteStateFile(file) {
|
|
15812
16084
|
try {
|
|
15813
|
-
await
|
|
16085
|
+
await fs16.promises.unlink(file);
|
|
15814
16086
|
} catch {
|
|
15815
16087
|
}
|
|
15816
16088
|
}
|
|
@@ -15849,7 +16121,7 @@ async function acquireLock3(repoRoot, tool, sessionId, retries, delayMs, options
|
|
|
15849
16121
|
}
|
|
15850
16122
|
async function acquireLockWithoutTiming(repoRoot, tool, sessionId, retries, delayMs, options = {}) {
|
|
15851
16123
|
const lockPath = lockFileForRepo(repoRoot, tool, sessionId);
|
|
15852
|
-
await
|
|
16124
|
+
await fs16.promises.mkdir(stateDir3(), { recursive: true, mode: 448 });
|
|
15853
16125
|
const bounded = retries !== void 0 || delayMs !== void 0;
|
|
15854
16126
|
const attempts = retries ?? LOCK_RETRIES2;
|
|
15855
16127
|
const delay = delayMs ?? LOCK_RETRY_DELAY_MS2;
|
|
@@ -15859,9 +16131,9 @@ async function acquireLockWithoutTiming(repoRoot, tool, sessionId, retries, dela
|
|
|
15859
16131
|
let extensionLogged = false;
|
|
15860
16132
|
for (let i = 0; ; i++) {
|
|
15861
16133
|
try {
|
|
15862
|
-
const fd = await
|
|
16134
|
+
const fd = await fs16.promises.open(
|
|
15863
16135
|
lockPath,
|
|
15864
|
-
|
|
16136
|
+
fs16.constants.O_CREAT | fs16.constants.O_EXCL | fs16.constants.O_WRONLY
|
|
15865
16137
|
);
|
|
15866
16138
|
await fd.write(String(process.pid));
|
|
15867
16139
|
await fd.close();
|
|
@@ -15870,7 +16142,7 @@ async function acquireLockWithoutTiming(repoRoot, tool, sessionId, retries, dela
|
|
|
15870
16142
|
if (err.code !== "EEXIST") throw err;
|
|
15871
16143
|
let regularLockFile = false;
|
|
15872
16144
|
try {
|
|
15873
|
-
regularLockFile = (await
|
|
16145
|
+
regularLockFile = (await fs16.promises.lstat(lockPath)).isFile();
|
|
15874
16146
|
} catch (statErr) {
|
|
15875
16147
|
if (statErr.code === "ENOENT") {
|
|
15876
16148
|
i--;
|
|
@@ -15921,21 +16193,21 @@ async function acquireLockWithoutTiming(repoRoot, tool, sessionId, retries, dela
|
|
|
15921
16193
|
}
|
|
15922
16194
|
async function releaseLock3(repoRoot, tool, sessionId) {
|
|
15923
16195
|
try {
|
|
15924
|
-
await
|
|
16196
|
+
await fs16.promises.unlink(lockFileForRepo(repoRoot, tool, sessionId));
|
|
15925
16197
|
} catch {
|
|
15926
16198
|
}
|
|
15927
16199
|
}
|
|
15928
16200
|
async function sweepStaleLockFiles(ttlMs = STALE_LOCK_TTL_MS3, now = Date.now()) {
|
|
15929
16201
|
let entries;
|
|
15930
16202
|
try {
|
|
15931
|
-
entries = await
|
|
16203
|
+
entries = await fs16.promises.readdir(stateDir3(), { withFileTypes: true });
|
|
15932
16204
|
} catch {
|
|
15933
16205
|
return 0;
|
|
15934
16206
|
}
|
|
15935
16207
|
let removed = 0;
|
|
15936
16208
|
for (const entry of entries) {
|
|
15937
16209
|
if (!entry.isFile() || !entry.name.endsWith(".lock")) continue;
|
|
15938
|
-
const file =
|
|
16210
|
+
const file = path16.join(stateDir3(), entry.name);
|
|
15939
16211
|
if (await reapLockIfStale(file, {
|
|
15940
16212
|
maxAgeMs: ttlMs,
|
|
15941
16213
|
now
|
|
@@ -15947,7 +16219,6 @@ async function sweepStaleLockFiles(ttlMs = STALE_LOCK_TTL_MS3, now = Date.now())
|
|
|
15947
16219
|
}
|
|
15948
16220
|
|
|
15949
16221
|
// src/git-traces/handlers.ts
|
|
15950
|
-
var CLI_VERSION2 = "0.8.0";
|
|
15951
16222
|
var GIT_TRACES_SLUG = "git-traces";
|
|
15952
16223
|
var MAX_UPLOAD_BYTES = 500 * 1024 * 1024;
|
|
15953
16224
|
var STALE_SCOPED_STATE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -15965,12 +16236,12 @@ var TOOL_LABELS = {
|
|
|
15965
16236
|
async function loadConfiguredRepos() {
|
|
15966
16237
|
const file = await loadProjects();
|
|
15967
16238
|
return Object.entries(file.projects).map(([repoRoot, config]) => ({
|
|
15968
|
-
repoRoot:
|
|
16239
|
+
repoRoot: path17.resolve(repoRoot),
|
|
15969
16240
|
config
|
|
15970
16241
|
})).sort((a, b) => a.repoRoot.localeCompare(b.repoRoot));
|
|
15971
16242
|
}
|
|
15972
16243
|
function repoLabel(repoRoot) {
|
|
15973
|
-
return
|
|
16244
|
+
return path17.basename(repoRoot) || repoRoot;
|
|
15974
16245
|
}
|
|
15975
16246
|
function resolveCwd2(payload) {
|
|
15976
16247
|
return resolveHookCwd(payload);
|
|
@@ -16023,7 +16294,8 @@ async function uploadFile(client, contributionId, filename, mimeType, buffer) {
|
|
|
16023
16294
|
await client.uploadToPresignedUrl(
|
|
16024
16295
|
presigned.presignedUrl,
|
|
16025
16296
|
presigned.headers,
|
|
16026
|
-
buffer
|
|
16297
|
+
buffer,
|
|
16298
|
+
{ contributionId, uploadId: presigned.upload.id }
|
|
16027
16299
|
);
|
|
16028
16300
|
appendLog(
|
|
16029
16301
|
"info",
|
|
@@ -16158,7 +16430,7 @@ function freezeEpochBaseline(params) {
|
|
|
16158
16430
|
sessionId,
|
|
16159
16431
|
tool,
|
|
16160
16432
|
baselineSha,
|
|
16161
|
-
|
|
16433
|
+
CLI_VERSION,
|
|
16162
16434
|
epoch,
|
|
16163
16435
|
prevHeadSha,
|
|
16164
16436
|
transitionKind,
|
|
@@ -16249,7 +16521,7 @@ function buildEpochBaselineArtifacts(params) {
|
|
|
16249
16521
|
sessionId,
|
|
16250
16522
|
tool,
|
|
16251
16523
|
baselineSha,
|
|
16252
|
-
|
|
16524
|
+
CLI_VERSION,
|
|
16253
16525
|
epoch,
|
|
16254
16526
|
prevHeadSha,
|
|
16255
16527
|
transitionKind,
|
|
@@ -16998,7 +17270,7 @@ async function recoverFromWedgedDiff(params) {
|
|
|
16998
17270
|
state.sessionId,
|
|
16999
17271
|
tool,
|
|
17000
17272
|
currentSha,
|
|
17001
|
-
|
|
17273
|
+
CLI_VERSION,
|
|
17002
17274
|
state.epoch,
|
|
17003
17275
|
null,
|
|
17004
17276
|
"initial",
|
|
@@ -17275,7 +17547,8 @@ async function processStopRepo(repo, tool, sessionId, recordedAt, lineageCheck,
|
|
|
17275
17547
|
});
|
|
17276
17548
|
return outcome === "uploaded" ? await finishSuccessfulStop("uploaded") : outcome;
|
|
17277
17549
|
}
|
|
17278
|
-
if (!artifacts2
|
|
17550
|
+
if (!artifacts2) return "failed";
|
|
17551
|
+
if (!canUploadEpochBaselineArtifacts(1, artifacts2)) {
|
|
17279
17552
|
return "skipped";
|
|
17280
17553
|
}
|
|
17281
17554
|
const registered = await registerInitialContribution({
|
|
@@ -17469,7 +17742,8 @@ async function processStopRepo(repo, tool, sessionId, recordedAt, lineageCheck,
|
|
|
17469
17742
|
missingObjectRecovery(state, "stop-diff")
|
|
17470
17743
|
);
|
|
17471
17744
|
}
|
|
17472
|
-
if (!artifacts
|
|
17745
|
+
if (!artifacts) return "failed";
|
|
17746
|
+
if (!canUploadEpochBaselineArtifacts(1, artifacts)) {
|
|
17473
17747
|
return "skipped";
|
|
17474
17748
|
}
|
|
17475
17749
|
const registered = await registerInitialContribution({
|
|
@@ -17586,7 +17860,7 @@ async function collectStopTargets(tool, sessionId, repos, repoByRoot) {
|
|
|
17586
17860
|
let needsLineageCheck = false;
|
|
17587
17861
|
const storedStates = await listSessionStatesForSession(tool, sessionId);
|
|
17588
17862
|
for (const { state } of storedStates) {
|
|
17589
|
-
const repo = repoByRoot.get(
|
|
17863
|
+
const repo = repoByRoot.get(path17.resolve(state.repoRoot));
|
|
17590
17864
|
if (!repo) {
|
|
17591
17865
|
missingConfig++;
|
|
17592
17866
|
appendLog(
|
|
@@ -17618,7 +17892,7 @@ async function lateInitSkipReason(payload, tool) {
|
|
|
17618
17892
|
const transcriptPath = payload.transcript_path;
|
|
17619
17893
|
if (!transcriptPath) return tool === "codex" ? null : "no-transcript-path";
|
|
17620
17894
|
try {
|
|
17621
|
-
const stat = await
|
|
17895
|
+
const stat = await fs17.promises.stat(path17.resolve(transcriptPath));
|
|
17622
17896
|
return stat.isFile() ? null : "transcript-not-a-file";
|
|
17623
17897
|
} catch {
|
|
17624
17898
|
return "transcript-missing";
|
|
@@ -17735,9 +18009,9 @@ async function handleSessionEnd(payload, tool) {
|
|
|
17735
18009
|
if (sessionId) {
|
|
17736
18010
|
const states = await listSessionStatesForSession(tool, sessionId);
|
|
17737
18011
|
for (const { state } of states) {
|
|
17738
|
-
const repoRoot =
|
|
18012
|
+
const repoRoot = path17.resolve(state.repoRoot);
|
|
17739
18013
|
repoRoots.add(repoRoot);
|
|
17740
|
-
const canProcess = repoByRoot.has(repoRoot) || project &&
|
|
18014
|
+
const canProcess = repoByRoot.has(repoRoot) || project && path17.resolve(project.repoRoot) === repoRoot;
|
|
17741
18015
|
if (canProcess && tool === "codex" && state.codexLineageChecked !== true) {
|
|
17742
18016
|
needsLineageCheck = true;
|
|
17743
18017
|
}
|
|
@@ -17771,7 +18045,7 @@ async function handleSessionEnd(payload, tool) {
|
|
|
17771
18045
|
let skipped = 0;
|
|
17772
18046
|
let failed = 0;
|
|
17773
18047
|
for (const repoRoot of repoRoots) {
|
|
17774
|
-
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);
|
|
17775
18049
|
if (!repo) {
|
|
17776
18050
|
skipped++;
|
|
17777
18051
|
appendLog(
|
|
@@ -18033,7 +18307,7 @@ async function runGitTracesWorker() {
|
|
|
18033
18307
|
const tool = process.env[TOOL_ENV_FLAG2] ?? parseToolArg2(process.argv.slice(2)) ?? null;
|
|
18034
18308
|
appendLog(
|
|
18035
18309
|
"info",
|
|
18036
|
-
`git-traces worker started (pid ${process.pid}, tool=${tool ?? "<none>"})`
|
|
18310
|
+
`git-traces worker started (pid ${process.pid}, tool=${tool ?? "<none>"}, cliVersion=${CLI_VERSION})`
|
|
18037
18311
|
);
|
|
18038
18312
|
if (!tool || !KNOWN_TOOLS.has(tool)) {
|
|
18039
18313
|
appendLog(
|
|
@@ -18118,29 +18392,29 @@ ${stack}` : ""}`
|
|
|
18118
18392
|
}
|
|
18119
18393
|
|
|
18120
18394
|
// src/outputs/zip.ts
|
|
18121
|
-
import
|
|
18122
|
-
import
|
|
18395
|
+
import fs19 from "fs";
|
|
18396
|
+
import path19 from "path";
|
|
18123
18397
|
import archiver2 from "archiver";
|
|
18124
18398
|
|
|
18125
18399
|
// src/outputs/downloads.ts
|
|
18126
18400
|
import { execSync as execSync2 } from "child_process";
|
|
18127
|
-
import
|
|
18128
|
-
import
|
|
18129
|
-
import
|
|
18401
|
+
import fs18 from "fs";
|
|
18402
|
+
import os10 from "os";
|
|
18403
|
+
import path18 from "path";
|
|
18130
18404
|
function getDownloadsFolder() {
|
|
18131
|
-
const home =
|
|
18405
|
+
const home = os10.homedir();
|
|
18132
18406
|
if (process.platform === "linux") {
|
|
18133
18407
|
try {
|
|
18134
18408
|
const xdgDir = execSync2("xdg-user-dir DOWNLOAD", {
|
|
18135
18409
|
encoding: "utf-8",
|
|
18136
18410
|
timeout: 3e3
|
|
18137
18411
|
}).trim();
|
|
18138
|
-
if (xdgDir &&
|
|
18412
|
+
if (xdgDir && fs18.existsSync(xdgDir)) return xdgDir;
|
|
18139
18413
|
} catch {
|
|
18140
18414
|
}
|
|
18141
18415
|
}
|
|
18142
|
-
const downloads =
|
|
18143
|
-
if (
|
|
18416
|
+
const downloads = path18.join(home, "Downloads");
|
|
18417
|
+
if (fs18.existsSync(downloads)) return downloads;
|
|
18144
18418
|
return home;
|
|
18145
18419
|
}
|
|
18146
18420
|
|
|
@@ -18149,11 +18423,11 @@ function sanitizeFilename(name) {
|
|
|
18149
18423
|
return name.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
18150
18424
|
}
|
|
18151
18425
|
function getUniqueFilename(dir, base, ext) {
|
|
18152
|
-
let candidate =
|
|
18153
|
-
if (!
|
|
18426
|
+
let candidate = path19.join(dir, `${base}${ext}`);
|
|
18427
|
+
if (!fs19.existsSync(candidate)) return candidate;
|
|
18154
18428
|
let i = 1;
|
|
18155
|
-
while (
|
|
18156
|
-
candidate =
|
|
18429
|
+
while (fs19.existsSync(candidate)) {
|
|
18430
|
+
candidate = path19.join(dir, `${base}-${i}${ext}`);
|
|
18157
18431
|
i++;
|
|
18158
18432
|
}
|
|
18159
18433
|
return candidate;
|
|
@@ -18163,13 +18437,13 @@ var ZipOutput = class {
|
|
|
18163
18437
|
label = "Save as .zip to Downloads";
|
|
18164
18438
|
async emit(group, options) {
|
|
18165
18439
|
const downloadsDir = getDownloadsFolder();
|
|
18166
|
-
const repoName = sanitizeFilename(
|
|
18440
|
+
const repoName = sanitizeFilename(path19.basename(group.repoPath));
|
|
18167
18441
|
const timeRange = options.timeRange;
|
|
18168
18442
|
const rangePart = timeRange?.label ?? "all";
|
|
18169
18443
|
const epochSeconds = Math.floor(Date.now() / 1e3);
|
|
18170
18444
|
const baseName = `logs-${repoName}-${rangePart}-${epochSeconds}`;
|
|
18171
18445
|
const outputPath = getUniqueFilename(downloadsDir, baseName, ".zip");
|
|
18172
|
-
const output =
|
|
18446
|
+
const output = fs19.createWriteStream(outputPath);
|
|
18173
18447
|
const archive = archiver2("zip", { zlib: { level: 6 } });
|
|
18174
18448
|
const done = new Promise((resolve, reject) => {
|
|
18175
18449
|
output.on("close", resolve);
|
|
@@ -18363,15 +18637,15 @@ async function confirmExport(group, output) {
|
|
|
18363
18637
|
}
|
|
18364
18638
|
|
|
18365
18639
|
// src/sources/claude.ts
|
|
18366
|
-
import
|
|
18367
|
-
import
|
|
18368
|
-
import
|
|
18640
|
+
import fs20 from "fs";
|
|
18641
|
+
import os11 from "os";
|
|
18642
|
+
import path20 from "path";
|
|
18369
18643
|
import readline2 from "readline";
|
|
18370
18644
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["tool-results"]);
|
|
18371
18645
|
async function resolveRepoPath(projectDir) {
|
|
18372
|
-
const indexPath =
|
|
18646
|
+
const indexPath = path20.join(projectDir, "sessions-index.json");
|
|
18373
18647
|
try {
|
|
18374
|
-
const raw = await
|
|
18648
|
+
const raw = await fs20.promises.readFile(indexPath, "utf-8");
|
|
18375
18649
|
const data = JSON.parse(raw);
|
|
18376
18650
|
if (data.originalPath && typeof data.originalPath === "string") {
|
|
18377
18651
|
return data.originalPath;
|
|
@@ -18379,12 +18653,12 @@ async function resolveRepoPath(projectDir) {
|
|
|
18379
18653
|
} catch {
|
|
18380
18654
|
}
|
|
18381
18655
|
const cwdCounts = /* @__PURE__ */ new Map();
|
|
18382
|
-
const entries = await
|
|
18656
|
+
const entries = await fs20.promises.readdir(projectDir, {
|
|
18383
18657
|
withFileTypes: true
|
|
18384
18658
|
});
|
|
18385
18659
|
for (const entry of entries) {
|
|
18386
18660
|
if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
|
|
18387
|
-
const cwd = await extractCwdFromJsonl(
|
|
18661
|
+
const cwd = await extractCwdFromJsonl(path20.join(projectDir, entry.name));
|
|
18388
18662
|
if (cwd) {
|
|
18389
18663
|
cwdCounts.set(cwd, (cwdCounts.get(cwd) ?? 0) + 1);
|
|
18390
18664
|
}
|
|
@@ -18403,7 +18677,7 @@ async function resolveRepoPath(projectDir) {
|
|
|
18403
18677
|
return null;
|
|
18404
18678
|
}
|
|
18405
18679
|
async function extractCwdFromJsonl(filePath) {
|
|
18406
|
-
const stream =
|
|
18680
|
+
const stream = fs20.createReadStream(filePath, { encoding: "utf-8" });
|
|
18407
18681
|
const rl = readline2.createInterface({ input: stream, crlfDelay: Infinity });
|
|
18408
18682
|
try {
|
|
18409
18683
|
for await (const line of rl) {
|
|
@@ -18425,12 +18699,12 @@ async function extractCwdFromJsonl(filePath) {
|
|
|
18425
18699
|
async function collectFiles(dir, baseDir, sourceName, repoPath, results) {
|
|
18426
18700
|
let entries;
|
|
18427
18701
|
try {
|
|
18428
|
-
entries = await
|
|
18702
|
+
entries = await fs20.promises.readdir(dir, { withFileTypes: true });
|
|
18429
18703
|
} catch {
|
|
18430
18704
|
return;
|
|
18431
18705
|
}
|
|
18432
18706
|
for (const entry of entries) {
|
|
18433
|
-
const fullPath =
|
|
18707
|
+
const fullPath = path20.join(dir, entry.name);
|
|
18434
18708
|
if (entry.isDirectory()) {
|
|
18435
18709
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
18436
18710
|
await collectFiles(fullPath, baseDir, sourceName, repoPath, results);
|
|
@@ -18452,19 +18726,19 @@ function fallbackDecode(encodedName) {
|
|
|
18452
18726
|
var ClaudeSource = class {
|
|
18453
18727
|
name = "claude";
|
|
18454
18728
|
async scan() {
|
|
18455
|
-
const baseDir =
|
|
18729
|
+
const baseDir = path20.join(os11.homedir(), ".claude", "projects");
|
|
18456
18730
|
try {
|
|
18457
|
-
await
|
|
18731
|
+
await fs20.promises.access(baseDir);
|
|
18458
18732
|
} catch {
|
|
18459
18733
|
return [];
|
|
18460
18734
|
}
|
|
18461
|
-
const projectDirs = await
|
|
18735
|
+
const projectDirs = await fs20.promises.readdir(baseDir, {
|
|
18462
18736
|
withFileTypes: true
|
|
18463
18737
|
});
|
|
18464
18738
|
const dirEntries = projectDirs.filter((d) => d.isDirectory());
|
|
18465
18739
|
const resultArrays = await Promise.all(
|
|
18466
18740
|
dirEntries.map(async (dir) => {
|
|
18467
|
-
const projectPath =
|
|
18741
|
+
const projectPath = path20.join(baseDir, dir.name);
|
|
18468
18742
|
const repoPath = await resolveRepoPath(projectPath) ?? fallbackDecode(dir.name);
|
|
18469
18743
|
const files = [];
|
|
18470
18744
|
await collectFiles(
|
|
@@ -18482,12 +18756,12 @@ var ClaudeSource = class {
|
|
|
18482
18756
|
};
|
|
18483
18757
|
|
|
18484
18758
|
// src/sources/codex.ts
|
|
18485
|
-
import
|
|
18486
|
-
import
|
|
18487
|
-
import
|
|
18759
|
+
import fs21 from "fs";
|
|
18760
|
+
import os12 from "os";
|
|
18761
|
+
import path21 from "path";
|
|
18488
18762
|
import readline3 from "readline";
|
|
18489
18763
|
async function parseSessionMeta2(filePath) {
|
|
18490
|
-
const stream =
|
|
18764
|
+
const stream = fs21.createReadStream(filePath, { encoding: "utf-8" });
|
|
18491
18765
|
const rl = readline3.createInterface({ input: stream, crlfDelay: Infinity });
|
|
18492
18766
|
try {
|
|
18493
18767
|
for await (const line of rl) {
|
|
@@ -18512,12 +18786,12 @@ async function findJsonlFiles(dir) {
|
|
|
18512
18786
|
async function walk(d) {
|
|
18513
18787
|
let entries;
|
|
18514
18788
|
try {
|
|
18515
|
-
entries = await
|
|
18789
|
+
entries = await fs21.promises.readdir(d, { withFileTypes: true });
|
|
18516
18790
|
} catch {
|
|
18517
18791
|
return;
|
|
18518
18792
|
}
|
|
18519
18793
|
for (const entry of entries) {
|
|
18520
|
-
const full =
|
|
18794
|
+
const full = path21.join(d, entry.name);
|
|
18521
18795
|
if (entry.isDirectory()) {
|
|
18522
18796
|
await walk(full);
|
|
18523
18797
|
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
@@ -18531,11 +18805,11 @@ async function findJsonlFiles(dir) {
|
|
|
18531
18805
|
async function loadHistory(historyPath) {
|
|
18532
18806
|
const map = /* @__PURE__ */ new Map();
|
|
18533
18807
|
try {
|
|
18534
|
-
await
|
|
18808
|
+
await fs21.promises.access(historyPath);
|
|
18535
18809
|
} catch {
|
|
18536
18810
|
return map;
|
|
18537
18811
|
}
|
|
18538
|
-
const stream =
|
|
18812
|
+
const stream = fs21.createReadStream(historyPath, { encoding: "utf-8" });
|
|
18539
18813
|
const rl = readline3.createInterface({ input: stream, crlfDelay: Infinity });
|
|
18540
18814
|
try {
|
|
18541
18815
|
for await (const line of rl) {
|
|
@@ -18562,14 +18836,14 @@ async function loadHistory(historyPath) {
|
|
|
18562
18836
|
var CodexSource = class {
|
|
18563
18837
|
name = "codex";
|
|
18564
18838
|
async scan() {
|
|
18565
|
-
const codexDir =
|
|
18566
|
-
const sessionsDir2 =
|
|
18839
|
+
const codexDir = path21.join(os12.homedir(), ".codex");
|
|
18840
|
+
const sessionsDir2 = path21.join(codexDir, "sessions");
|
|
18567
18841
|
try {
|
|
18568
|
-
await
|
|
18842
|
+
await fs21.promises.access(sessionsDir2);
|
|
18569
18843
|
} catch {
|
|
18570
18844
|
return [];
|
|
18571
18845
|
}
|
|
18572
|
-
const historyPath =
|
|
18846
|
+
const historyPath = path21.join(codexDir, "history.jsonl");
|
|
18573
18847
|
const [jsonlFiles, historyMap] = await Promise.all([
|
|
18574
18848
|
findJsonlFiles(sessionsDir2),
|
|
18575
18849
|
loadHistory(historyPath)
|
|
@@ -18592,8 +18866,8 @@ var CodexSource = class {
|
|
|
18592
18866
|
});
|
|
18593
18867
|
const historyLines = historyMap.get(meta.sessionId);
|
|
18594
18868
|
if (historyLines) {
|
|
18595
|
-
const sessionDir =
|
|
18596
|
-
const historyAbsPath =
|
|
18869
|
+
const sessionDir = path21.relative(sessionsDir2, path21.dirname(filePath));
|
|
18870
|
+
const historyAbsPath = path21.join(
|
|
18597
18871
|
sessionsDir2,
|
|
18598
18872
|
sessionDir,
|
|
18599
18873
|
`history-${meta.sessionId}.jsonl`
|
|
@@ -18613,18 +18887,18 @@ var CodexSource = class {
|
|
|
18613
18887
|
};
|
|
18614
18888
|
|
|
18615
18889
|
// src/sources/copilotChat.ts
|
|
18616
|
-
import
|
|
18617
|
-
import
|
|
18618
|
-
import
|
|
18890
|
+
import fs22 from "fs";
|
|
18891
|
+
import os13 from "os";
|
|
18892
|
+
import path22 from "path";
|
|
18619
18893
|
import { fileURLToPath } from "url";
|
|
18620
18894
|
function vsCodeUserDirs() {
|
|
18621
|
-
const home =
|
|
18895
|
+
const home = os13.homedir();
|
|
18622
18896
|
const dirs = [
|
|
18623
|
-
|
|
18624
|
-
|
|
18897
|
+
path22.join(home, "Library", "Application Support", "Code", "User"),
|
|
18898
|
+
path22.join(home, ".config", "Code", "User")
|
|
18625
18899
|
];
|
|
18626
18900
|
if (process.env.APPDATA) {
|
|
18627
|
-
dirs.push(
|
|
18901
|
+
dirs.push(path22.join(process.env.APPDATA, "Code", "User"));
|
|
18628
18902
|
}
|
|
18629
18903
|
return dirs;
|
|
18630
18904
|
}
|
|
@@ -18639,7 +18913,7 @@ function uriToFsPath(uri) {
|
|
|
18639
18913
|
async function readWorkspaceFolder(workspaceJsonPath) {
|
|
18640
18914
|
let raw;
|
|
18641
18915
|
try {
|
|
18642
|
-
raw = await
|
|
18916
|
+
raw = await fs22.promises.readFile(workspaceJsonPath, "utf-8");
|
|
18643
18917
|
} catch {
|
|
18644
18918
|
return null;
|
|
18645
18919
|
}
|
|
@@ -18661,10 +18935,10 @@ var CopilotChatSource = class {
|
|
|
18661
18935
|
async scan() {
|
|
18662
18936
|
const results = [];
|
|
18663
18937
|
for (const userDir of vsCodeUserDirs()) {
|
|
18664
|
-
const workspaceStorage =
|
|
18938
|
+
const workspaceStorage = path22.join(userDir, "workspaceStorage");
|
|
18665
18939
|
let hashDirs;
|
|
18666
18940
|
try {
|
|
18667
|
-
hashDirs = await
|
|
18941
|
+
hashDirs = await fs22.promises.readdir(workspaceStorage, {
|
|
18668
18942
|
withFileTypes: true
|
|
18669
18943
|
});
|
|
18670
18944
|
} catch {
|
|
@@ -18672,22 +18946,22 @@ var CopilotChatSource = class {
|
|
|
18672
18946
|
}
|
|
18673
18947
|
for (const hash of hashDirs) {
|
|
18674
18948
|
if (!hash.isDirectory()) continue;
|
|
18675
|
-
const wsRoot =
|
|
18676
|
-
const transcriptsDir =
|
|
18949
|
+
const wsRoot = path22.join(workspaceStorage, hash.name);
|
|
18950
|
+
const transcriptsDir = path22.join(
|
|
18677
18951
|
wsRoot,
|
|
18678
18952
|
"GitHub.copilot-chat",
|
|
18679
18953
|
"transcripts"
|
|
18680
18954
|
);
|
|
18681
18955
|
let transcriptEntries;
|
|
18682
18956
|
try {
|
|
18683
|
-
transcriptEntries = await
|
|
18957
|
+
transcriptEntries = await fs22.promises.readdir(transcriptsDir, {
|
|
18684
18958
|
withFileTypes: true
|
|
18685
18959
|
});
|
|
18686
18960
|
} catch {
|
|
18687
18961
|
continue;
|
|
18688
18962
|
}
|
|
18689
18963
|
const repoPath = await readWorkspaceFolder(
|
|
18690
|
-
|
|
18964
|
+
path22.join(wsRoot, "workspace.json")
|
|
18691
18965
|
);
|
|
18692
18966
|
if (!repoPath) continue;
|
|
18693
18967
|
for (const entry of transcriptEntries) {
|
|
@@ -18695,7 +18969,7 @@ var CopilotChatSource = class {
|
|
|
18695
18969
|
const sessionId = entry.name.slice(0, -".jsonl".length);
|
|
18696
18970
|
results.push({
|
|
18697
18971
|
sourceName: this.name,
|
|
18698
|
-
absolutePath:
|
|
18972
|
+
absolutePath: path22.join(transcriptsDir, entry.name),
|
|
18699
18973
|
repoPath,
|
|
18700
18974
|
metadata: { sessionId }
|
|
18701
18975
|
});
|
|
@@ -18735,7 +19009,7 @@ function reportRedactionStats(noun, stats) {
|
|
|
18735
19009
|
async function filterByTimeRange(group, range) {
|
|
18736
19010
|
const results = await Promise.all(
|
|
18737
19011
|
group.files.map(
|
|
18738
|
-
(f) =>
|
|
19012
|
+
(f) => fs23.promises.stat(f.absolutePath).then((s) => ({ file: f, stat: s })).catch(() => null)
|
|
18739
19013
|
)
|
|
18740
19014
|
);
|
|
18741
19015
|
const filtered = [];
|
|
@@ -18762,10 +19036,10 @@ async function runInteractive() {
|
|
|
18762
19036
|
s.start(`Scanning ${source.name} logs...`);
|
|
18763
19037
|
const allFiles = await source.scan();
|
|
18764
19038
|
const allGroups = await mergeByRepo(allFiles);
|
|
18765
|
-
const repoRoot =
|
|
19039
|
+
const repoRoot = path23.resolve(repo.root);
|
|
18766
19040
|
const matching = allGroups.filter((g) => {
|
|
18767
|
-
const resolved =
|
|
18768
|
-
return resolved === repoRoot || resolved.startsWith(repoRoot +
|
|
19041
|
+
const resolved = path23.resolve(g.repoPath);
|
|
19042
|
+
return resolved === repoRoot || resolved.startsWith(repoRoot + path23.sep);
|
|
18769
19043
|
});
|
|
18770
19044
|
if (matching.length === 0) {
|
|
18771
19045
|
s.stop(`No ${source.name} logs found for ${repo.name}.`);
|
|
@@ -18796,7 +19070,7 @@ async function runInteractive() {
|
|
|
18796
19070
|
}
|
|
18797
19071
|
}
|
|
18798
19072
|
const envFileNames = await discoverEnvFiles(repoRoot);
|
|
18799
|
-
const envFilePaths = envFileNames.map((n) =>
|
|
19073
|
+
const envFilePaths = envFileNames.map((n) => path23.join(repoRoot, n));
|
|
18800
19074
|
const additionalFiles = await promptSecretFiles(envFileNames);
|
|
18801
19075
|
const secretResult = await collectSecrets(
|
|
18802
19076
|
repoRoot,
|