hillclimb 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +2075 -587
- package/dist/pattern-worker.js +40 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import
|
|
5
|
-
import
|
|
4
|
+
import fs23 from "fs";
|
|
5
|
+
import path24 from "path";
|
|
6
6
|
import * as p6 from "@clack/prompts";
|
|
7
7
|
|
|
8
8
|
// src/commands/init.ts
|
|
@@ -13,6 +13,7 @@ import * as p3 from "@clack/prompts";
|
|
|
13
13
|
import fs from "fs";
|
|
14
14
|
import os from "os";
|
|
15
15
|
import path from "path";
|
|
16
|
+
var MAX_SNAPSHOT_LIMIT_OVERRIDE_BYTES = 100 * 1024 * 1024;
|
|
16
17
|
var DEFAULT_CONFIG_DIR = path.join(os.homedir(), ".hillclimb");
|
|
17
18
|
function normalizeProjectConfig(raw) {
|
|
18
19
|
if (!raw || typeof raw !== "object") return null;
|
|
@@ -34,9 +35,24 @@ function normalizeProjectConfig(raw) {
|
|
|
34
35
|
workspaceName: config.workspaceName ?? projectName,
|
|
35
36
|
contributionTypeSlug: config.contributionTypeSlug,
|
|
36
37
|
contributionTypeName: config.contributionTypeName,
|
|
37
|
-
autoSubmit: config.autoSubmit
|
|
38
|
+
autoSubmit: config.autoSubmit,
|
|
39
|
+
snapshotLimits: normalizeSnapshotLimits(config.snapshotLimits)
|
|
38
40
|
};
|
|
39
41
|
}
|
|
42
|
+
function normalizeSnapshotLimits(raw) {
|
|
43
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
44
|
+
const limits = { ...raw };
|
|
45
|
+
if (!isValidSnapshotLimit(limits.maxTrackedFileBytes))
|
|
46
|
+
delete limits.maxTrackedFileBytes;
|
|
47
|
+
if (!isValidSnapshotLimit(limits.maxUntrackedFileBytes))
|
|
48
|
+
delete limits.maxUntrackedFileBytes;
|
|
49
|
+
if (!isValidSnapshotLimit(limits.maxBinaryFileBytes))
|
|
50
|
+
delete limits.maxBinaryFileBytes;
|
|
51
|
+
return Object.keys(limits).length > 0 ? limits : void 0;
|
|
52
|
+
}
|
|
53
|
+
function isValidSnapshotLimit(value) {
|
|
54
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 && value <= MAX_SNAPSHOT_LIMIT_OVERRIDE_BYTES;
|
|
55
|
+
}
|
|
40
56
|
function configDir() {
|
|
41
57
|
return process.env.HILLCLIMB_CONFIG_DIR ?? DEFAULT_CONFIG_DIR;
|
|
42
58
|
}
|
|
@@ -72,7 +88,12 @@ async function upsertProject(repoRoot, config) {
|
|
|
72
88
|
const file = await loadProjects();
|
|
73
89
|
const normalized = normalizeProjectConfig(config);
|
|
74
90
|
if (!normalized) throw new Error("Invalid project config.");
|
|
75
|
-
|
|
91
|
+
const resolvedRepoRoot = path.resolve(repoRoot);
|
|
92
|
+
const existing = file.projects[resolvedRepoRoot];
|
|
93
|
+
if (!normalized.snapshotLimits && existing?.snapshotLimits) {
|
|
94
|
+
normalized.snapshotLimits = existing.snapshotLimits;
|
|
95
|
+
}
|
|
96
|
+
file.projects[resolvedRepoRoot] = normalized;
|
|
76
97
|
await saveProjects(file);
|
|
77
98
|
}
|
|
78
99
|
async function findProjectForCwd(cwd) {
|
|
@@ -350,6 +371,9 @@ var PlatformError = class extends Error {
|
|
|
350
371
|
this.name = "PlatformError";
|
|
351
372
|
}
|
|
352
373
|
};
|
|
374
|
+
function isContributionUploadNotUploaded(err) {
|
|
375
|
+
return err instanceof PlatformError && err.status === 409 && err.code === "CONTRIBUTION_UPLOAD_NOT_UPLOADED";
|
|
376
|
+
}
|
|
353
377
|
var PlatformClient = class {
|
|
354
378
|
apiBaseUrl;
|
|
355
379
|
sessionCookie;
|
|
@@ -2282,16 +2306,444 @@ async function runStatus(args = []) {
|
|
|
2282
2306
|
|
|
2283
2307
|
// src/commands/upload.ts
|
|
2284
2308
|
import { spawn as spawn2 } from "child_process";
|
|
2285
|
-
import
|
|
2286
|
-
import
|
|
2287
|
-
import
|
|
2288
|
-
import
|
|
2309
|
+
import crypto5 from "crypto";
|
|
2310
|
+
import fs15 from "fs";
|
|
2311
|
+
import os7 from "os";
|
|
2312
|
+
import path15 from "path";
|
|
2313
|
+
|
|
2314
|
+
// src/codex-lineage.ts
|
|
2315
|
+
import crypto from "crypto";
|
|
2316
|
+
import fs7 from "fs";
|
|
2317
|
+
import os3 from "os";
|
|
2318
|
+
import path9 from "path";
|
|
2289
2319
|
import readline from "readline";
|
|
2320
|
+
var MAX_PREFIX_CANDIDATES = 32;
|
|
2321
|
+
var MAX_CANDIDATE_BYTES = 64 * 1024 * 1024;
|
|
2322
|
+
var VOLATILE_KEYS = /* @__PURE__ */ new Set([
|
|
2323
|
+
"timestamp",
|
|
2324
|
+
"started_at",
|
|
2325
|
+
"completed_at",
|
|
2326
|
+
"turn_id",
|
|
2327
|
+
"session_id",
|
|
2328
|
+
"forked_from_id",
|
|
2329
|
+
"parent_thread_id",
|
|
2330
|
+
"usage",
|
|
2331
|
+
"rate_limits",
|
|
2332
|
+
"input_tokens",
|
|
2333
|
+
"output_tokens",
|
|
2334
|
+
"cached_input_tokens",
|
|
2335
|
+
"total_tokens",
|
|
2336
|
+
"model_context_window",
|
|
2337
|
+
"context_window",
|
|
2338
|
+
"sequence_number",
|
|
2339
|
+
"duration_ms",
|
|
2340
|
+
"elapsed_ms",
|
|
2341
|
+
"latency_ms",
|
|
2342
|
+
"wall_time_ms"
|
|
2343
|
+
]);
|
|
2344
|
+
function sessionsDir() {
|
|
2345
|
+
return process.env.HILLCLIMB_CODEX_SESSIONS_DIR ?? path9.join(os3.homedir(), ".codex", "sessions");
|
|
2346
|
+
}
|
|
2347
|
+
function asRecord(value) {
|
|
2348
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
2349
|
+
}
|
|
2350
|
+
function parseSessionMeta(payloadValue) {
|
|
2351
|
+
const payload = asRecord(payloadValue) ?? {};
|
|
2352
|
+
const source = asRecord(payload.source);
|
|
2353
|
+
return {
|
|
2354
|
+
id: typeof payload.id === "string" ? payload.id : null,
|
|
2355
|
+
forkedFromThreadId: typeof payload.forked_from_id === "string" ? payload.forked_from_id : null,
|
|
2356
|
+
isSubagent: payload.thread_source === "subagent" || source !== null && Object.hasOwn(source, "subagent")
|
|
2357
|
+
};
|
|
2358
|
+
}
|
|
2359
|
+
function normalizedValue(value, depth = 0) {
|
|
2360
|
+
if (typeof value === "string") return value.replace(/\r\n/g, "\n");
|
|
2361
|
+
if (Array.isArray(value)) {
|
|
2362
|
+
return value.map((item) => normalizedValue(item, depth + 1));
|
|
2363
|
+
}
|
|
2364
|
+
const record = asRecord(value);
|
|
2365
|
+
if (!record) return value;
|
|
2366
|
+
const normalized = {};
|
|
2367
|
+
for (const key of Object.keys(record).sort()) {
|
|
2368
|
+
if (depth === 0 && (key === "id" || key === "item_id")) continue;
|
|
2369
|
+
if (VOLATILE_KEYS.has(key)) continue;
|
|
2370
|
+
normalized[key] = normalizedValue(record[key], depth + 1);
|
|
2371
|
+
}
|
|
2372
|
+
return normalized;
|
|
2373
|
+
}
|
|
2374
|
+
function canonicalRecord(record) {
|
|
2375
|
+
if (record.type !== "response_item") return null;
|
|
2376
|
+
return JSON.stringify({
|
|
2377
|
+
type: "response_item",
|
|
2378
|
+
payload: normalizedValue(record.payload)
|
|
2379
|
+
});
|
|
2380
|
+
}
|
|
2381
|
+
function eventKind(record) {
|
|
2382
|
+
if (record.type !== "event_msg") return null;
|
|
2383
|
+
const payload = asRecord(record.payload);
|
|
2384
|
+
return typeof payload?.type === "string" ? payload.type : null;
|
|
2385
|
+
}
|
|
2386
|
+
function recordTurnId(record) {
|
|
2387
|
+
const payload = asRecord(record.payload);
|
|
2388
|
+
return typeof payload?.turn_id === "string" ? payload.turn_id : null;
|
|
2389
|
+
}
|
|
2390
|
+
function startTurn(turnId) {
|
|
2391
|
+
return {
|
|
2392
|
+
turnId,
|
|
2393
|
+
hash: crypto.createHash("sha256"),
|
|
2394
|
+
responseItemCount: 0
|
|
2395
|
+
};
|
|
2396
|
+
}
|
|
2397
|
+
async function parseRollout(filePath, maxTurns = Number.POSITIVE_INFINITY) {
|
|
2398
|
+
const stream = fs7.createReadStream(filePath, { encoding: "utf-8" });
|
|
2399
|
+
const lines = readline.createInterface({
|
|
2400
|
+
input: stream,
|
|
2401
|
+
crlfDelay: Infinity
|
|
2402
|
+
});
|
|
2403
|
+
let meta = null;
|
|
2404
|
+
let active = null;
|
|
2405
|
+
let malformed = false;
|
|
2406
|
+
const turns = [];
|
|
2407
|
+
try {
|
|
2408
|
+
for await (const line of lines) {
|
|
2409
|
+
if (!line.trim()) continue;
|
|
2410
|
+
let record;
|
|
2411
|
+
try {
|
|
2412
|
+
const parsed = JSON.parse(line);
|
|
2413
|
+
const parsedRecord = asRecord(parsed);
|
|
2414
|
+
if (!parsedRecord) {
|
|
2415
|
+
malformed = true;
|
|
2416
|
+
continue;
|
|
2417
|
+
}
|
|
2418
|
+
record = parsedRecord;
|
|
2419
|
+
} catch {
|
|
2420
|
+
malformed = true;
|
|
2421
|
+
continue;
|
|
2422
|
+
}
|
|
2423
|
+
if (record.type === "session_meta") {
|
|
2424
|
+
if (meta === null) meta = parseSessionMeta(record.payload);
|
|
2425
|
+
continue;
|
|
2426
|
+
}
|
|
2427
|
+
const kind = eventKind(record);
|
|
2428
|
+
const turnId = recordTurnId(record);
|
|
2429
|
+
if (kind === "task_started") {
|
|
2430
|
+
active = startTurn(turnId);
|
|
2431
|
+
} else if (record.type === "turn_context") {
|
|
2432
|
+
if (active === null || active.turnId !== null && turnId !== null && active.turnId !== turnId) {
|
|
2433
|
+
active = startTurn(turnId);
|
|
2434
|
+
} else if (active.turnId === null) {
|
|
2435
|
+
active.turnId = turnId;
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
if (active !== null) {
|
|
2439
|
+
const canonical = canonicalRecord(record);
|
|
2440
|
+
if (canonical !== null) {
|
|
2441
|
+
active.hash.update(canonical).update("\n");
|
|
2442
|
+
active.responseItemCount++;
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
if (kind === "task_complete" || kind === "turn_aborted") {
|
|
2446
|
+
if (active !== null && (active.turnId === null || turnId === null || active.turnId === turnId) && active.responseItemCount > 0) {
|
|
2447
|
+
turns.push({
|
|
2448
|
+
turnId: active.turnId ?? turnId,
|
|
2449
|
+
sha256: active.hash.digest("hex")
|
|
2450
|
+
});
|
|
2451
|
+
active = null;
|
|
2452
|
+
if (turns.length >= maxTurns) break;
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
} catch {
|
|
2457
|
+
malformed = true;
|
|
2458
|
+
} finally {
|
|
2459
|
+
lines.close();
|
|
2460
|
+
stream.destroy();
|
|
2461
|
+
}
|
|
2462
|
+
return { meta, turns, malformed, incomplete: active !== null };
|
|
2463
|
+
}
|
|
2464
|
+
async function codexSessionIdFromFile(filePath) {
|
|
2465
|
+
const stream = fs7.createReadStream(filePath, { encoding: "utf-8" });
|
|
2466
|
+
const lines = readline.createInterface({
|
|
2467
|
+
input: stream,
|
|
2468
|
+
crlfDelay: Infinity
|
|
2469
|
+
});
|
|
2470
|
+
try {
|
|
2471
|
+
for await (const line of lines) {
|
|
2472
|
+
if (!line.trim()) continue;
|
|
2473
|
+
try {
|
|
2474
|
+
const record = asRecord(JSON.parse(line));
|
|
2475
|
+
if (record?.type !== "session_meta") return null;
|
|
2476
|
+
return parseSessionMeta(record.payload).id;
|
|
2477
|
+
} catch {
|
|
2478
|
+
return null;
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
} catch {
|
|
2482
|
+
return null;
|
|
2483
|
+
} finally {
|
|
2484
|
+
lines.close();
|
|
2485
|
+
stream.destroy();
|
|
2486
|
+
}
|
|
2487
|
+
return null;
|
|
2488
|
+
}
|
|
2489
|
+
async function findCodexRolloutPath(sessionId) {
|
|
2490
|
+
const candidates = [];
|
|
2491
|
+
async function walk(dir) {
|
|
2492
|
+
let entries;
|
|
2493
|
+
try {
|
|
2494
|
+
entries = await fs7.promises.readdir(dir, { withFileTypes: true });
|
|
2495
|
+
} catch {
|
|
2496
|
+
return;
|
|
2497
|
+
}
|
|
2498
|
+
for (const entry of entries) {
|
|
2499
|
+
const full = path9.join(dir, entry.name);
|
|
2500
|
+
if (entry.isDirectory()) {
|
|
2501
|
+
await walk(full);
|
|
2502
|
+
} else if (entry.isFile() && entry.name.endsWith(".jsonl") && entry.name.includes(sessionId)) {
|
|
2503
|
+
candidates.push(full);
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
await walk(sessionsDir());
|
|
2508
|
+
candidates.sort();
|
|
2509
|
+
for (const candidate of candidates) {
|
|
2510
|
+
if (await codexSessionIdFromFile(candidate) === sessionId)
|
|
2511
|
+
return candidate;
|
|
2512
|
+
}
|
|
2513
|
+
return void 0;
|
|
2514
|
+
}
|
|
2515
|
+
function commonPrefixLength(a, b) {
|
|
2516
|
+
const limit = Math.min(a.length, b.length);
|
|
2517
|
+
let count = 0;
|
|
2518
|
+
while (count < limit && a[count]?.sha256 === b[count]?.sha256) count++;
|
|
2519
|
+
return count;
|
|
2520
|
+
}
|
|
2521
|
+
function boundary(turns, inheritedTurnCount, detectedBy) {
|
|
2522
|
+
if (inheritedTurnCount <= 0 || inheritedTurnCount > turns.length) {
|
|
2523
|
+
return void 0;
|
|
2524
|
+
}
|
|
2525
|
+
const replayPrefixSha256 = crypto.createHash("sha256").update("codex-turn-prefix-v1\0").update(String(inheritedTurnCount)).update("\0").update(
|
|
2526
|
+
turns.slice(0, inheritedTurnCount).map((turn) => turn.sha256).join("\0")
|
|
2527
|
+
).digest("hex");
|
|
2528
|
+
return {
|
|
2529
|
+
format: "codex-turn-prefix-v1",
|
|
2530
|
+
inheritedTurnCount,
|
|
2531
|
+
lastInheritedTurnId: turns[inheritedTurnCount - 1]?.turnId ?? null,
|
|
2532
|
+
replayPrefixSha256,
|
|
2533
|
+
detectedBy
|
|
2534
|
+
};
|
|
2535
|
+
}
|
|
2536
|
+
function uuidV7Timestamp(id) {
|
|
2537
|
+
if (!id || !/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
|
|
2538
|
+
id
|
|
2539
|
+
)) {
|
|
2540
|
+
return null;
|
|
2541
|
+
}
|
|
2542
|
+
return `${id.slice(0, 8)}${id.slice(9, 13)}`.toLowerCase();
|
|
2543
|
+
}
|
|
2544
|
+
function uuidV7ReplayBoundary(meta, turns) {
|
|
2545
|
+
const childTimestamp = uuidV7Timestamp(meta.id);
|
|
2546
|
+
if (!childTimestamp || turns.length === 0) return void 0;
|
|
2547
|
+
const turnTimestamps = [];
|
|
2548
|
+
for (const turn of turns) {
|
|
2549
|
+
const timestamp = uuidV7Timestamp(turn.turnId);
|
|
2550
|
+
if (!timestamp) return void 0;
|
|
2551
|
+
turnTimestamps.push(timestamp);
|
|
2552
|
+
}
|
|
2553
|
+
let inheritedTurnCount = 0;
|
|
2554
|
+
while (inheritedTurnCount < turnTimestamps.length && (turnTimestamps[inheritedTurnCount] ?? childTimestamp) < childTimestamp) {
|
|
2555
|
+
inheritedTurnCount++;
|
|
2556
|
+
}
|
|
2557
|
+
if (inheritedTurnCount === 0) return void 0;
|
|
2558
|
+
if (turnTimestamps.slice(inheritedTurnCount).some((timestamp) => timestamp < childTimestamp)) {
|
|
2559
|
+
return void 0;
|
|
2560
|
+
}
|
|
2561
|
+
return boundary(turns, inheritedTurnCount, "signature-only");
|
|
2562
|
+
}
|
|
2563
|
+
async function earlierCandidates(childPath, childId) {
|
|
2564
|
+
const childCreatedAt = uuidV7Timestamp(childId);
|
|
2565
|
+
const candidates = [];
|
|
2566
|
+
async function walk(dir) {
|
|
2567
|
+
let entries;
|
|
2568
|
+
try {
|
|
2569
|
+
entries = await fs7.promises.readdir(dir, { withFileTypes: true });
|
|
2570
|
+
} catch {
|
|
2571
|
+
return;
|
|
2572
|
+
}
|
|
2573
|
+
for (const entry of entries) {
|
|
2574
|
+
const full = path9.join(dir, entry.name);
|
|
2575
|
+
if (entry.isDirectory()) {
|
|
2576
|
+
await walk(full);
|
|
2577
|
+
continue;
|
|
2578
|
+
}
|
|
2579
|
+
if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
|
|
2580
|
+
if (path9.resolve(full) === path9.resolve(childPath)) continue;
|
|
2581
|
+
try {
|
|
2582
|
+
const stat = await fs7.promises.stat(full);
|
|
2583
|
+
if (stat.size > MAX_CANDIDATE_BYTES) continue;
|
|
2584
|
+
const filenameId = entry.name.match(
|
|
2585
|
+
/[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/i
|
|
2586
|
+
)?.[0];
|
|
2587
|
+
const createdAt = uuidV7Timestamp(filenameId ?? null);
|
|
2588
|
+
if (childCreatedAt !== null && (createdAt === null || createdAt >= childCreatedAt)) {
|
|
2589
|
+
continue;
|
|
2590
|
+
}
|
|
2591
|
+
candidates.push({ filePath: full, createdAt });
|
|
2592
|
+
} catch {
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
await walk(sessionsDir());
|
|
2597
|
+
return candidates.sort(
|
|
2598
|
+
(a, b) => (b.createdAt ?? "").localeCompare(a.createdAt ?? "") || b.filePath.localeCompare(a.filePath)
|
|
2599
|
+
).slice(0, MAX_PREFIX_CANDIDATES).map((candidate) => candidate.filePath);
|
|
2600
|
+
}
|
|
2601
|
+
function withFields(lineage) {
|
|
2602
|
+
return lineage.forkedFromThreadId || lineage.replayBoundary ? lineage : void 0;
|
|
2603
|
+
}
|
|
2604
|
+
function mergeCodexLineage(existing, detected) {
|
|
2605
|
+
if (!existing) return detected;
|
|
2606
|
+
if (!detected) return existing;
|
|
2607
|
+
if (existing.forkedFromThreadId && detected.forkedFromThreadId && existing.forkedFromThreadId !== detected.forkedFromThreadId) {
|
|
2608
|
+
const existingIsDirect = !existing.replayBoundary || existing.replayBoundary.detectedBy !== "local-prefix";
|
|
2609
|
+
const detectedIsDirect = !detected.replayBoundary || detected.replayBoundary.detectedBy !== "local-prefix";
|
|
2610
|
+
return detectedIsDirect && !existingIsDirect ? detected : existing;
|
|
2611
|
+
}
|
|
2612
|
+
const confidence = {
|
|
2613
|
+
"signature-only": 0,
|
|
2614
|
+
"local-prefix": 1,
|
|
2615
|
+
"codex-metadata": 2
|
|
2616
|
+
};
|
|
2617
|
+
let replayBoundary = existing.replayBoundary ?? detected.replayBoundary;
|
|
2618
|
+
if (existing.replayBoundary && detected.replayBoundary) {
|
|
2619
|
+
const existingConfidence = confidence[existing.replayBoundary.detectedBy];
|
|
2620
|
+
const detectedConfidence = confidence[detected.replayBoundary.detectedBy];
|
|
2621
|
+
if (detectedConfidence > existingConfidence || detectedConfidence === existingConfidence && detected.replayBoundary.inheritedTurnCount > existing.replayBoundary.inheritedTurnCount) {
|
|
2622
|
+
replayBoundary = detected.replayBoundary;
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
return withFields({
|
|
2626
|
+
forkedFromThreadId: existing.forkedFromThreadId ?? detected.forkedFromThreadId,
|
|
2627
|
+
replayBoundary
|
|
2628
|
+
});
|
|
2629
|
+
}
|
|
2630
|
+
async function detectCodexLineage(childPath) {
|
|
2631
|
+
try {
|
|
2632
|
+
const child = await parseRollout(childPath);
|
|
2633
|
+
if (!child.meta?.id) return { conclusive: false };
|
|
2634
|
+
if (child.meta.isSubagent) return { conclusive: true };
|
|
2635
|
+
const conclusive = !child.malformed && !child.incomplete;
|
|
2636
|
+
const metadataParent = child.meta.forkedFromThreadId;
|
|
2637
|
+
if (metadataParent && metadataParent !== child.meta.id) {
|
|
2638
|
+
if (child.malformed) {
|
|
2639
|
+
return {
|
|
2640
|
+
conclusive: false,
|
|
2641
|
+
lineage: { forkedFromThreadId: metadataParent }
|
|
2642
|
+
};
|
|
2643
|
+
}
|
|
2644
|
+
const parentPath = await findCodexRolloutPath(metadataParent);
|
|
2645
|
+
if (parentPath) {
|
|
2646
|
+
const parent = await parseRollout(parentPath, child.turns.length);
|
|
2647
|
+
if (!parent.malformed && !parent.incomplete) {
|
|
2648
|
+
const inheritedTurnCount = commonPrefixLength(
|
|
2649
|
+
parent.turns,
|
|
2650
|
+
child.turns
|
|
2651
|
+
);
|
|
2652
|
+
const replayBoundary = boundary(
|
|
2653
|
+
child.turns,
|
|
2654
|
+
inheritedTurnCount,
|
|
2655
|
+
"codex-metadata"
|
|
2656
|
+
);
|
|
2657
|
+
return {
|
|
2658
|
+
conclusive,
|
|
2659
|
+
lineage: withFields({
|
|
2660
|
+
forkedFromThreadId: metadataParent,
|
|
2661
|
+
replayBoundary: replayBoundary ?? uuidV7ReplayBoundary(child.meta, child.turns)
|
|
2662
|
+
})
|
|
2663
|
+
};
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
return {
|
|
2667
|
+
conclusive: false,
|
|
2668
|
+
lineage: withFields({
|
|
2669
|
+
forkedFromThreadId: metadataParent,
|
|
2670
|
+
replayBoundary: uuidV7ReplayBoundary(child.meta, child.turns)
|
|
2671
|
+
})
|
|
2672
|
+
};
|
|
2673
|
+
}
|
|
2674
|
+
if (child.malformed) return { conclusive: false };
|
|
2675
|
+
const signatureOnly = uuidV7ReplayBoundary(child.meta, child.turns);
|
|
2676
|
+
if (child.turns.length < 2) {
|
|
2677
|
+
return {
|
|
2678
|
+
conclusive,
|
|
2679
|
+
lineage: withFields({ replayBoundary: signatureOnly })
|
|
2680
|
+
};
|
|
2681
|
+
}
|
|
2682
|
+
const matches = [];
|
|
2683
|
+
const childCreatedAt = uuidV7Timestamp(child.meta.id);
|
|
2684
|
+
if (childCreatedAt === null) {
|
|
2685
|
+
return {
|
|
2686
|
+
conclusive,
|
|
2687
|
+
lineage: withFields({ replayBoundary: signatureOnly })
|
|
2688
|
+
};
|
|
2689
|
+
}
|
|
2690
|
+
for (const candidatePath of await earlierCandidates(
|
|
2691
|
+
childPath,
|
|
2692
|
+
child.meta.id
|
|
2693
|
+
)) {
|
|
2694
|
+
const candidate = await parseRollout(candidatePath, child.turns.length);
|
|
2695
|
+
if (candidate.malformed || !candidate.meta?.id || candidate.meta.isSubagent || candidate.meta.id === child.meta.id) {
|
|
2696
|
+
continue;
|
|
2697
|
+
}
|
|
2698
|
+
const candidateCreatedAt = uuidV7Timestamp(candidate.meta.id);
|
|
2699
|
+
if (candidateCreatedAt === null || candidateCreatedAt >= childCreatedAt) {
|
|
2700
|
+
continue;
|
|
2701
|
+
}
|
|
2702
|
+
const inheritedTurnCount = commonPrefixLength(
|
|
2703
|
+
candidate.turns,
|
|
2704
|
+
child.turns
|
|
2705
|
+
);
|
|
2706
|
+
if (inheritedTurnCount > 0 && inheritedTurnCount < child.turns.length) {
|
|
2707
|
+
matches.push({
|
|
2708
|
+
parentId: candidate.meta.id,
|
|
2709
|
+
inheritedTurnCount
|
|
2710
|
+
});
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
if (matches.length === 0) {
|
|
2714
|
+
return {
|
|
2715
|
+
conclusive,
|
|
2716
|
+
lineage: withFields({ replayBoundary: signatureOnly })
|
|
2717
|
+
};
|
|
2718
|
+
}
|
|
2719
|
+
const bestCount = Math.max(
|
|
2720
|
+
...matches.map((match) => match.inheritedTurnCount)
|
|
2721
|
+
);
|
|
2722
|
+
const best = matches.filter(
|
|
2723
|
+
(match) => match.inheritedTurnCount === bestCount
|
|
2724
|
+
);
|
|
2725
|
+
if (best.length === 1 && bestCount >= 2) {
|
|
2726
|
+
return {
|
|
2727
|
+
conclusive,
|
|
2728
|
+
lineage: {
|
|
2729
|
+
forkedFromThreadId: best[0]?.parentId,
|
|
2730
|
+
replayBoundary: boundary(child.turns, bestCount, "local-prefix")
|
|
2731
|
+
}
|
|
2732
|
+
};
|
|
2733
|
+
}
|
|
2734
|
+
return {
|
|
2735
|
+
conclusive,
|
|
2736
|
+
lineage: withFields({ replayBoundary: signatureOnly })
|
|
2737
|
+
};
|
|
2738
|
+
} catch {
|
|
2739
|
+
return { conclusive: false };
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2290
2742
|
|
|
2291
2743
|
// src/debug-logs.ts
|
|
2292
|
-
import
|
|
2293
|
-
import
|
|
2294
|
-
import
|
|
2744
|
+
import crypto2 from "crypto";
|
|
2745
|
+
import fs12 from "fs";
|
|
2746
|
+
import path13 from "path";
|
|
2295
2747
|
|
|
2296
2748
|
// src/hook-events.ts
|
|
2297
2749
|
function classifyHookEvent(event) {
|
|
@@ -2340,21 +2792,73 @@ function inferSourceToolFromPayload(payload) {
|
|
|
2340
2792
|
function fallbackSourceTool(payload) {
|
|
2341
2793
|
const inferred = inferSourceToolFromPayload(payload);
|
|
2342
2794
|
if (inferred) return inferred;
|
|
2343
|
-
const
|
|
2344
|
-
if (
|
|
2795
|
+
const eventKind2 = classifyHookEvent(payload.hook_event_name);
|
|
2796
|
+
if (eventKind2 === "stop") return "codex";
|
|
2345
2797
|
return "claude";
|
|
2346
2798
|
}
|
|
2347
2799
|
|
|
2800
|
+
// src/lock-reap.ts
|
|
2801
|
+
import fs8 from "fs";
|
|
2802
|
+
function isProcessAlive(pid) {
|
|
2803
|
+
try {
|
|
2804
|
+
process.kill(pid, 0);
|
|
2805
|
+
return true;
|
|
2806
|
+
} catch (err) {
|
|
2807
|
+
return err.code === "EPERM";
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
async function readLockPid(lockPath) {
|
|
2811
|
+
let contents;
|
|
2812
|
+
try {
|
|
2813
|
+
contents = await fs8.promises.readFile(lockPath, "utf-8");
|
|
2814
|
+
} catch {
|
|
2815
|
+
return null;
|
|
2816
|
+
}
|
|
2817
|
+
const value = contents.trim();
|
|
2818
|
+
if (!/^[1-9]\d*$/.test(value)) return null;
|
|
2819
|
+
const pid = Number(value);
|
|
2820
|
+
return Number.isSafeInteger(pid) ? pid : null;
|
|
2821
|
+
}
|
|
2822
|
+
async function reapLockIfStaleImpl(lockPath, opts, beforeOwnerRecheck) {
|
|
2823
|
+
let mtimeMs;
|
|
2824
|
+
try {
|
|
2825
|
+
mtimeMs = (await fs8.promises.stat(lockPath)).mtimeMs;
|
|
2826
|
+
} catch {
|
|
2827
|
+
return false;
|
|
2828
|
+
}
|
|
2829
|
+
const age = (opts.now ?? Date.now()) - mtimeMs;
|
|
2830
|
+
if (age < (opts.graceMs ?? 15e3)) return false;
|
|
2831
|
+
const ownerPid = await readLockPid(lockPath);
|
|
2832
|
+
const shouldReap = ownerPid === null ? age > opts.maxAgeMs : !isProcessAlive(ownerPid) || age > opts.maxAgeMs;
|
|
2833
|
+
if (!shouldReap) return false;
|
|
2834
|
+
try {
|
|
2835
|
+
await beforeOwnerRecheck?.(lockPath);
|
|
2836
|
+
} catch {
|
|
2837
|
+
return false;
|
|
2838
|
+
}
|
|
2839
|
+
const currentOwnerPid = await readLockPid(lockPath);
|
|
2840
|
+
if (currentOwnerPid !== ownerPid) return false;
|
|
2841
|
+
try {
|
|
2842
|
+
await fs8.promises.unlink(lockPath);
|
|
2843
|
+
} catch (err) {
|
|
2844
|
+
if (err.code !== "ENOENT") return false;
|
|
2845
|
+
}
|
|
2846
|
+
return true;
|
|
2847
|
+
}
|
|
2848
|
+
async function reapLockIfStale(lockPath, opts) {
|
|
2849
|
+
return reapLockIfStaleImpl(lockPath, opts);
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2348
2852
|
// src/middleware/pattern-redact.ts
|
|
2349
|
-
import
|
|
2853
|
+
import os4 from "os";
|
|
2350
2854
|
import { Worker } from "worker_threads";
|
|
2351
2855
|
|
|
2352
2856
|
// src/middleware/file-utils.ts
|
|
2353
|
-
import
|
|
2857
|
+
import fs9 from "fs";
|
|
2354
2858
|
async function checkBinary(filePath) {
|
|
2355
2859
|
let handle = null;
|
|
2356
2860
|
try {
|
|
2357
|
-
handle = await
|
|
2861
|
+
handle = await fs9.promises.open(filePath, "r");
|
|
2358
2862
|
const buf = Buffer.alloc(8192);
|
|
2359
2863
|
const { bytesRead } = await handle.read(buf, 0, 8192, 0);
|
|
2360
2864
|
for (let i = 0; i < bytesRead; i++) {
|
|
@@ -2379,7 +2883,7 @@ async function readFileContent(file) {
|
|
|
2379
2883
|
return { kind: "binary" };
|
|
2380
2884
|
}
|
|
2381
2885
|
try {
|
|
2382
|
-
const content = await
|
|
2886
|
+
const content = await fs9.promises.readFile(file.absolutePath, "utf-8");
|
|
2383
2887
|
return { kind: "text", content };
|
|
2384
2888
|
} catch {
|
|
2385
2889
|
return { kind: "error" };
|
|
@@ -10496,6 +11000,46 @@ var patterns_local_default = {
|
|
|
10496
11000
|
name: "Hugging Face Organization API Token",
|
|
10497
11001
|
regex: "(?:(?<![A-Za-z0-9])|(?<=\\\\[nrt]))api_org_[A-Za-z0-9]{34,40}",
|
|
10498
11002
|
confidence: "high"
|
|
11003
|
+
},
|
|
11004
|
+
{
|
|
11005
|
+
name: "OpenRouter API Key",
|
|
11006
|
+
regex: "(?:(?<![A-Za-z0-9])|(?<=\\\\[nrt]))sk-or-v1-[a-fA-F0-9]{64}",
|
|
11007
|
+
confidence: "high"
|
|
11008
|
+
},
|
|
11009
|
+
{
|
|
11010
|
+
name: "OpenRouter API Key (generic)",
|
|
11011
|
+
regex: "(?:(?<![A-Za-z0-9])|(?<=\\\\[nrt]))sk-or-[A-Za-z0-9-]{32,128}",
|
|
11012
|
+
confidence: "high"
|
|
11013
|
+
},
|
|
11014
|
+
{
|
|
11015
|
+
name: "Anthropic API Key",
|
|
11016
|
+
regex: "(?:(?<![A-Za-z0-9])|(?<=\\\\[nrt]))sk-ant-[A-Za-z0-9_-]{32,256}",
|
|
11017
|
+
confidence: "high"
|
|
11018
|
+
},
|
|
11019
|
+
{
|
|
11020
|
+
name: "OpenAI Project API Key",
|
|
11021
|
+
regex: "(?:(?<![A-Za-z0-9])|(?<=\\\\[nrt]))sk-proj-[A-Za-z0-9_-]{32,256}",
|
|
11022
|
+
confidence: "high"
|
|
11023
|
+
},
|
|
11024
|
+
{
|
|
11025
|
+
name: "OpenAI API Key (legacy)",
|
|
11026
|
+
regex: "(?:(?<![A-Za-z0-9])|(?<=\\\\[nrt]))sk-[A-Za-z0-9]{20}T3BlbkFJ[A-Za-z0-9]{20}",
|
|
11027
|
+
confidence: "high"
|
|
11028
|
+
},
|
|
11029
|
+
{
|
|
11030
|
+
name: "sshpass Command-Line Password",
|
|
11031
|
+
regex: `sshpass\\s+-p\\s*(?:'[^']{1,128}'|"[^"]{1,128}"|[^\\s'"-][^\\s'"]{0,127})`,
|
|
11032
|
+
confidence: "high"
|
|
11033
|
+
},
|
|
11034
|
+
{
|
|
11035
|
+
name: "URL Userinfo Credentials",
|
|
11036
|
+
regex: ":\\/\\/([^\\s:@\\/]{1,64}:[^\\s@\\/]{1,128})@",
|
|
11037
|
+
confidence: "high"
|
|
11038
|
+
},
|
|
11039
|
+
{
|
|
11040
|
+
name: "SSH Root Host (rented GPU node)",
|
|
11041
|
+
regex: "(?:(?<![A-Za-z0-9@._-])|(?<=\\\\[nrt]))root@(?:\\d{1,3}\\.){3}\\d{1,3}",
|
|
11042
|
+
confidence: "high"
|
|
10499
11043
|
}
|
|
10500
11044
|
]
|
|
10501
11045
|
};
|
|
@@ -10681,7 +11225,7 @@ function redactPatternLine(line, patterns, memo, isJsonl) {
|
|
|
10681
11225
|
return redactString(line, patterns);
|
|
10682
11226
|
}
|
|
10683
11227
|
var WORKER_COUNT = Math.min(
|
|
10684
|
-
|
|
11228
|
+
os4.availableParallelism?.() ?? os4.cpus().length,
|
|
10685
11229
|
4
|
|
10686
11230
|
);
|
|
10687
11231
|
var WORKER_THRESHOLD = 4;
|
|
@@ -10985,8 +11529,8 @@ var RedactMiddleware = class {
|
|
|
10985
11529
|
var middleware = [];
|
|
10986
11530
|
|
|
10987
11531
|
// src/middleware/secrets.ts
|
|
10988
|
-
import
|
|
10989
|
-
import
|
|
11532
|
+
import fs10 from "fs";
|
|
11533
|
+
import path10 from "path";
|
|
10990
11534
|
var KNOWN_NON_SECRETS = /* @__PURE__ */ new Set([
|
|
10991
11535
|
"true",
|
|
10992
11536
|
"false",
|
|
@@ -11066,7 +11610,7 @@ async function parseEnvFile(filePath) {
|
|
|
11066
11610
|
const values = [];
|
|
11067
11611
|
let content;
|
|
11068
11612
|
try {
|
|
11069
|
-
content = await
|
|
11613
|
+
content = await fs10.promises.readFile(filePath, "utf-8");
|
|
11070
11614
|
} catch {
|
|
11071
11615
|
return values;
|
|
11072
11616
|
}
|
|
@@ -11134,16 +11678,16 @@ function addWithVariants(set, value) {
|
|
|
11134
11678
|
async function discoverEnvFiles(repoRoot) {
|
|
11135
11679
|
let entries;
|
|
11136
11680
|
try {
|
|
11137
|
-
entries = await
|
|
11681
|
+
entries = await fs10.promises.readdir(repoRoot);
|
|
11138
11682
|
} catch {
|
|
11139
11683
|
return [];
|
|
11140
11684
|
}
|
|
11141
11685
|
const envFiles = [];
|
|
11142
11686
|
for (const name of entries) {
|
|
11143
11687
|
if (!name.startsWith(".env")) continue;
|
|
11144
|
-
const filePath =
|
|
11688
|
+
const filePath = path10.join(repoRoot, name);
|
|
11145
11689
|
try {
|
|
11146
|
-
const stat = await
|
|
11690
|
+
const stat = await fs10.promises.stat(filePath);
|
|
11147
11691
|
if (stat.isFile()) envFiles.push(name);
|
|
11148
11692
|
} catch {
|
|
11149
11693
|
}
|
|
@@ -11166,7 +11710,7 @@ async function collectSecrets(repoRoot, envFiles, additionalFiles) {
|
|
|
11166
11710
|
}
|
|
11167
11711
|
}
|
|
11168
11712
|
for (const filePath of additionalFiles) {
|
|
11169
|
-
const resolved =
|
|
11713
|
+
const resolved = path10.resolve(repoRoot, filePath);
|
|
11170
11714
|
sourceFiles.push(resolved);
|
|
11171
11715
|
for (const value of await parseEnvFile(resolved)) {
|
|
11172
11716
|
if (isUsableValue(value)) {
|
|
@@ -11195,25 +11739,25 @@ import { PassThrough } from "stream";
|
|
|
11195
11739
|
import archiver from "archiver";
|
|
11196
11740
|
|
|
11197
11741
|
// src/outputs/archive.ts
|
|
11198
|
-
import
|
|
11199
|
-
import
|
|
11742
|
+
import os5 from "os";
|
|
11743
|
+
import path11 from "path";
|
|
11200
11744
|
function getSourceBaseDir(sourceName) {
|
|
11201
|
-
const home =
|
|
11745
|
+
const home = os5.homedir();
|
|
11202
11746
|
switch (sourceName) {
|
|
11203
11747
|
case "claude":
|
|
11204
|
-
return
|
|
11748
|
+
return path11.join(home, ".claude", "projects");
|
|
11205
11749
|
case "codex":
|
|
11206
|
-
return
|
|
11750
|
+
return path11.join(home, ".codex", "sessions");
|
|
11207
11751
|
case "debug-logs":
|
|
11208
|
-
return
|
|
11752
|
+
return path11.join(configDir(), "logs");
|
|
11209
11753
|
default:
|
|
11210
11754
|
return home;
|
|
11211
11755
|
}
|
|
11212
11756
|
}
|
|
11213
11757
|
function archivePathFor(file) {
|
|
11214
11758
|
const baseDir = getSourceBaseDir(file.sourceName);
|
|
11215
|
-
const relativePath = file.absolutePath.startsWith(baseDir) ?
|
|
11216
|
-
return
|
|
11759
|
+
const relativePath = file.absolutePath.startsWith(baseDir) ? path11.relative(baseDir, file.absolutePath) : path11.basename(file.absolutePath);
|
|
11760
|
+
return path11.join(file.sourceName, relativePath);
|
|
11217
11761
|
}
|
|
11218
11762
|
function addGroupToArchive(archive, group, selectedSources) {
|
|
11219
11763
|
for (const file of group.files) {
|
|
@@ -11331,21 +11875,21 @@ var PlatformUploadOutput = class {
|
|
|
11331
11875
|
};
|
|
11332
11876
|
|
|
11333
11877
|
// src/pipeline.ts
|
|
11334
|
-
import
|
|
11335
|
-
import
|
|
11878
|
+
import fs11 from "fs";
|
|
11879
|
+
import path12 from "path";
|
|
11336
11880
|
function canonicalizePath(p7) {
|
|
11337
|
-
let resolved =
|
|
11338
|
-
if (resolved.endsWith(
|
|
11881
|
+
let resolved = path12.resolve(p7);
|
|
11882
|
+
if (resolved.endsWith(path12.sep) && resolved !== path12.sep) {
|
|
11339
11883
|
resolved = resolved.slice(0, -1);
|
|
11340
11884
|
}
|
|
11341
11885
|
return resolved;
|
|
11342
11886
|
}
|
|
11343
11887
|
function computeLabel(repoPath, allPaths) {
|
|
11344
|
-
const segments = repoPath.split(
|
|
11888
|
+
const segments = repoPath.split(path12.sep).filter(Boolean);
|
|
11345
11889
|
for (let depth = 1; depth <= segments.length; depth++) {
|
|
11346
11890
|
const label = segments.slice(-depth).join("/");
|
|
11347
11891
|
const matches = allPaths.filter((p7) => {
|
|
11348
|
-
const s = p7.split(
|
|
11892
|
+
const s = p7.split(path12.sep).filter(Boolean);
|
|
11349
11893
|
return s.slice(-depth).join("/") === label;
|
|
11350
11894
|
});
|
|
11351
11895
|
if (matches.length === 1) return label;
|
|
@@ -11367,7 +11911,7 @@ async function mergeByRepo(files) {
|
|
|
11367
11911
|
const groups = [];
|
|
11368
11912
|
for (const [repoPath, groupFiles] of grouped) {
|
|
11369
11913
|
const stats = await Promise.all(
|
|
11370
|
-
groupFiles.map((f) =>
|
|
11914
|
+
groupFiles.map((f) => fs11.promises.stat(f.absolutePath).catch(() => null))
|
|
11371
11915
|
);
|
|
11372
11916
|
let lastModified = /* @__PURE__ */ new Date(0);
|
|
11373
11917
|
for (const stat of stats) {
|
|
@@ -11390,7 +11934,7 @@ async function preloadFiles(group) {
|
|
|
11390
11934
|
group.files.map(async (file) => {
|
|
11391
11935
|
if (file.content) return file;
|
|
11392
11936
|
try {
|
|
11393
|
-
const buf = await
|
|
11937
|
+
const buf = await fs11.promises.readFile(file.absolutePath);
|
|
11394
11938
|
const checkLen = Math.min(buf.length, 8192);
|
|
11395
11939
|
for (let i = 0; i < checkLen; i++) {
|
|
11396
11940
|
if (buf[i] === 0) {
|
|
@@ -11420,10 +11964,14 @@ async function runPipeline(group, middleware2, output, options, onProgress) {
|
|
|
11420
11964
|
var DEBUG_LOGS_SLUG = "debug-logs";
|
|
11421
11965
|
var CURRENT_SCHEMA_VERSION = 1;
|
|
11422
11966
|
var DEFAULT_WAIT_MS = 6e4;
|
|
11423
|
-
var
|
|
11967
|
+
var DEFAULT_LOCK_WAIT_MS = 5 * 60 * 1e3;
|
|
11424
11968
|
var LOCK_RETRY_DELAY_MS = 100;
|
|
11969
|
+
var LOCK_RETRIES = Math.ceil(DEFAULT_LOCK_WAIT_MS / LOCK_RETRY_DELAY_MS);
|
|
11970
|
+
var STALE_LOCK_TTL_MS = 60 * 60 * 1e3;
|
|
11971
|
+
var EVENT_STATE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
11972
|
+
var SESSION_STATE_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
11425
11973
|
function stateDir() {
|
|
11426
|
-
return process.env.HILLCLIMB_DEBUG_LOG_STATE_DIR ??
|
|
11974
|
+
return process.env.HILLCLIMB_DEBUG_LOG_STATE_DIR ?? path13.join(configDir(), "debug-log-uploads");
|
|
11427
11975
|
}
|
|
11428
11976
|
function waitMs() {
|
|
11429
11977
|
const raw = process.env.HILLCLIMB_DEBUG_LOG_WAIT_MS;
|
|
@@ -11432,11 +11980,27 @@ function waitMs() {
|
|
|
11432
11980
|
return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_WAIT_MS;
|
|
11433
11981
|
}
|
|
11434
11982
|
function stateFile(eventId) {
|
|
11435
|
-
return
|
|
11983
|
+
return path13.join(stateDir(), `${eventId}.json`);
|
|
11436
11984
|
}
|
|
11437
11985
|
function lockFile(eventId) {
|
|
11438
11986
|
return `${stateFile(eventId)}.lock`;
|
|
11439
11987
|
}
|
|
11988
|
+
function sessionKey(ctx) {
|
|
11989
|
+
if (!ctx.sessionId) return null;
|
|
11990
|
+
return crypto2.createHash("sha256").update(
|
|
11991
|
+
JSON.stringify({
|
|
11992
|
+
schema: "debug-log-session-v1",
|
|
11993
|
+
apiBaseUrl: ctx.config.apiBaseUrl,
|
|
11994
|
+
projectId: ctx.config.projectId,
|
|
11995
|
+
repoRoot: ctx.repoRoot,
|
|
11996
|
+
tool: ctx.tool,
|
|
11997
|
+
sessionId: ctx.sessionId
|
|
11998
|
+
})
|
|
11999
|
+
).digest("hex");
|
|
12000
|
+
}
|
|
12001
|
+
function sessionStateId(key) {
|
|
12002
|
+
return `session-${key}`;
|
|
12003
|
+
}
|
|
11440
12004
|
function sleep2(ms) {
|
|
11441
12005
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
11442
12006
|
}
|
|
@@ -11465,8 +12029,8 @@ function resolveSessionId(payload) {
|
|
|
11465
12029
|
function stringOrNull(value) {
|
|
11466
12030
|
return typeof value === "string" && value.length > 0 ? value : null;
|
|
11467
12031
|
}
|
|
11468
|
-
function expectedKinds(tool,
|
|
11469
|
-
if (
|
|
12032
|
+
function expectedKinds(tool, eventKind2, payload) {
|
|
12033
|
+
if (eventKind2 === "stop") {
|
|
11470
12034
|
return new Set(
|
|
11471
12035
|
tool === "codex" || tool === "copilot-chat" || tool === "claude" || tool === "cursor" || tool === "opencode" ? ["agent", "git"] : ["git"]
|
|
11472
12036
|
);
|
|
@@ -11479,9 +12043,9 @@ function expectedKinds(tool, eventKind, payload) {
|
|
|
11479
12043
|
async function transcriptFingerprint(payload) {
|
|
11480
12044
|
const transcriptPath = stringOrNull(payload.transcript_path);
|
|
11481
12045
|
if (!transcriptPath) return {};
|
|
11482
|
-
const resolved =
|
|
12046
|
+
const resolved = path13.resolve(transcriptPath);
|
|
11483
12047
|
try {
|
|
11484
|
-
const stat = await
|
|
12048
|
+
const stat = await fs12.promises.stat(resolved);
|
|
11485
12049
|
return {
|
|
11486
12050
|
transcriptPath: resolved,
|
|
11487
12051
|
transcriptMtimeMs: stat.mtimeMs,
|
|
@@ -11492,8 +12056,8 @@ async function transcriptFingerprint(payload) {
|
|
|
11492
12056
|
}
|
|
11493
12057
|
}
|
|
11494
12058
|
async function eventContext(tool, payload) {
|
|
11495
|
-
const
|
|
11496
|
-
if (!
|
|
12059
|
+
const eventKind2 = classifyHookEvent(payload.hook_event_name);
|
|
12060
|
+
if (!eventKind2) return null;
|
|
11497
12061
|
const cwd = resolveCwd(payload);
|
|
11498
12062
|
if (!cwd) return null;
|
|
11499
12063
|
const project = await findProjectForCwd(cwd);
|
|
@@ -11504,15 +12068,15 @@ async function eventContext(tool, payload) {
|
|
|
11504
12068
|
if (!sessionId && !turnId && !transcriptPath) {
|
|
11505
12069
|
return null;
|
|
11506
12070
|
}
|
|
11507
|
-
const expected = expectedKinds(tool,
|
|
11508
|
-
const eventNonce =
|
|
12071
|
+
const expected = expectedKinds(tool, eventKind2, payload);
|
|
12072
|
+
const eventNonce = eventKind2 === "stop" && expected.size === 1 && expected.has("git") && !turnId && !transcriptPath ? crypto2.randomBytes(8).toString("hex") : null;
|
|
11509
12073
|
const fingerprint = {
|
|
11510
12074
|
schema: "debug-log-event-v1",
|
|
11511
12075
|
apiBaseUrl: project.config.apiBaseUrl,
|
|
11512
12076
|
projectId: project.config.projectId,
|
|
11513
12077
|
repoRoot: project.repoRoot,
|
|
11514
12078
|
tool,
|
|
11515
|
-
eventKind,
|
|
12079
|
+
eventKind: eventKind2,
|
|
11516
12080
|
hookEventName: payload.hook_event_name ?? null,
|
|
11517
12081
|
sessionId,
|
|
11518
12082
|
conversationId: stringOrNull(payload.conversation_id),
|
|
@@ -11521,15 +12085,15 @@ async function eventContext(tool, payload) {
|
|
|
11521
12085
|
// Only stop events without stable turn IDs need the transcript to
|
|
11522
12086
|
// disambiguate. When a turn_id exists, including mutable transcript
|
|
11523
12087
|
// mtime/size can split agent+git completions for the same logical event.
|
|
11524
|
-
...
|
|
12088
|
+
...eventKind2 === "stop" && !turnId ? await transcriptFingerprint(payload) : {}
|
|
11525
12089
|
};
|
|
11526
|
-
const eventId =
|
|
12090
|
+
const eventId = crypto2.createHash("sha256").update(JSON.stringify(fingerprint)).digest("hex").slice(0, 32);
|
|
11527
12091
|
return {
|
|
11528
12092
|
eventId,
|
|
11529
12093
|
repoRoot: project.repoRoot,
|
|
11530
12094
|
config: project.config,
|
|
11531
12095
|
tool,
|
|
11532
|
-
eventKind,
|
|
12096
|
+
eventKind: eventKind2,
|
|
11533
12097
|
hookEventName: payload.hook_event_name ?? null,
|
|
11534
12098
|
sessionId,
|
|
11535
12099
|
expectedKinds: expected
|
|
@@ -11537,7 +12101,7 @@ async function eventContext(tool, payload) {
|
|
|
11537
12101
|
}
|
|
11538
12102
|
async function readState(eventId) {
|
|
11539
12103
|
try {
|
|
11540
|
-
const raw = await
|
|
12104
|
+
const raw = await fs12.promises.readFile(stateFile(eventId), "utf-8");
|
|
11541
12105
|
const parsed = JSON.parse(raw);
|
|
11542
12106
|
if (parsed.schemaVersion !== CURRENT_SCHEMA_VERSION) return null;
|
|
11543
12107
|
return parsed;
|
|
@@ -11546,37 +12110,132 @@ async function readState(eventId) {
|
|
|
11546
12110
|
}
|
|
11547
12111
|
}
|
|
11548
12112
|
async function writeState(state) {
|
|
11549
|
-
await
|
|
12113
|
+
await fs12.promises.mkdir(stateDir(), { recursive: true, mode: 448 });
|
|
11550
12114
|
const file = stateFile(state.eventId);
|
|
11551
12115
|
const tmp = `${file}.tmp`;
|
|
11552
|
-
await
|
|
12116
|
+
await fs12.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
|
|
11553
12117
|
mode: 384
|
|
11554
12118
|
});
|
|
11555
|
-
await
|
|
12119
|
+
await fs12.promises.rename(tmp, file);
|
|
11556
12120
|
}
|
|
11557
|
-
async function
|
|
11558
|
-
|
|
11559
|
-
|
|
12121
|
+
async function readSessionState(key) {
|
|
12122
|
+
try {
|
|
12123
|
+
const raw = await fs12.promises.readFile(
|
|
12124
|
+
stateFile(sessionStateId(key)),
|
|
12125
|
+
"utf-8"
|
|
12126
|
+
);
|
|
12127
|
+
const parsed = JSON.parse(raw);
|
|
12128
|
+
if (parsed.schemaVersion !== CURRENT_SCHEMA_VERSION || parsed.sessionKey !== key || typeof parsed.contributionId !== "string") {
|
|
12129
|
+
return null;
|
|
12130
|
+
}
|
|
12131
|
+
return parsed;
|
|
12132
|
+
} catch {
|
|
12133
|
+
return null;
|
|
12134
|
+
}
|
|
12135
|
+
}
|
|
12136
|
+
async function writeSessionState(state) {
|
|
12137
|
+
await fs12.promises.mkdir(stateDir(), { recursive: true, mode: 448 });
|
|
12138
|
+
const file = stateFile(sessionStateId(state.sessionKey));
|
|
12139
|
+
const tmp = `${file}.tmp`;
|
|
12140
|
+
await fs12.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
|
|
12141
|
+
mode: 384
|
|
12142
|
+
});
|
|
12143
|
+
await fs12.promises.rename(tmp, file);
|
|
12144
|
+
}
|
|
12145
|
+
async function deleteSessionState(key) {
|
|
12146
|
+
try {
|
|
12147
|
+
await fs12.promises.unlink(stateFile(sessionStateId(key)));
|
|
12148
|
+
} catch (err) {
|
|
12149
|
+
if (err.code !== "ENOENT") throw err;
|
|
12150
|
+
}
|
|
12151
|
+
}
|
|
12152
|
+
async function tryCreateLock(lockId) {
|
|
12153
|
+
try {
|
|
12154
|
+
const fd = await fs12.promises.open(
|
|
12155
|
+
lockFile(lockId),
|
|
12156
|
+
fs12.constants.O_CREAT | fs12.constants.O_EXCL | fs12.constants.O_WRONLY
|
|
12157
|
+
);
|
|
11560
12158
|
try {
|
|
11561
|
-
const fd = await fs10.promises.open(
|
|
11562
|
-
lockFile(eventId),
|
|
11563
|
-
fs10.constants.O_CREAT | fs10.constants.O_EXCL | fs10.constants.O_WRONLY
|
|
11564
|
-
);
|
|
11565
12159
|
await fd.write(String(process.pid));
|
|
12160
|
+
} finally {
|
|
11566
12161
|
await fd.close();
|
|
12162
|
+
}
|
|
12163
|
+
return true;
|
|
12164
|
+
} catch (err) {
|
|
12165
|
+
if (err.code === "EEXIST") return false;
|
|
12166
|
+
throw err;
|
|
12167
|
+
}
|
|
12168
|
+
}
|
|
12169
|
+
async function acquireLock(eventId) {
|
|
12170
|
+
await fs12.promises.mkdir(stateDir(), { recursive: true, mode: 448 });
|
|
12171
|
+
for (let i = 0; i < LOCK_RETRIES; i++) {
|
|
12172
|
+
if (await tryCreateLock(eventId)) return;
|
|
12173
|
+
if (await reapLockIfStale(lockFile(eventId), {
|
|
12174
|
+
maxAgeMs: STALE_LOCK_TTL_MS
|
|
12175
|
+
}) && await tryCreateLock(eventId)) {
|
|
11567
12176
|
return;
|
|
11568
|
-
}
|
|
11569
|
-
|
|
11570
|
-
|
|
12177
|
+
}
|
|
12178
|
+
if (i < LOCK_RETRIES - 1) await sleep2(LOCK_RETRY_DELAY_MS);
|
|
12179
|
+
}
|
|
12180
|
+
throw new Error(
|
|
12181
|
+
`Failed to acquire debug-log lock after ${DEFAULT_LOCK_WAIT_MS}ms (lock=${lockFile(eventId)})`
|
|
12182
|
+
);
|
|
12183
|
+
}
|
|
12184
|
+
async function tryAcquireLock(eventId, now) {
|
|
12185
|
+
await fs12.promises.mkdir(stateDir(), { recursive: true, mode: 448 });
|
|
12186
|
+
if (await tryCreateLock(eventId)) return true;
|
|
12187
|
+
if (await reapLockIfStale(lockFile(eventId), {
|
|
12188
|
+
maxAgeMs: STALE_LOCK_TTL_MS,
|
|
12189
|
+
now
|
|
12190
|
+
})) {
|
|
12191
|
+
return tryCreateLock(eventId);
|
|
12192
|
+
}
|
|
12193
|
+
return false;
|
|
12194
|
+
}
|
|
12195
|
+
async function sweepStaleDebugLogState(now = Date.now()) {
|
|
12196
|
+
let entries;
|
|
12197
|
+
try {
|
|
12198
|
+
entries = await fs12.promises.readdir(stateDir(), { withFileTypes: true });
|
|
12199
|
+
} catch {
|
|
12200
|
+
return;
|
|
12201
|
+
}
|
|
12202
|
+
for (const entry of entries) {
|
|
12203
|
+
if (!entry.isFile()) continue;
|
|
12204
|
+
const file = path13.join(stateDir(), entry.name);
|
|
12205
|
+
try {
|
|
12206
|
+
if (entry.name.endsWith(".lock")) {
|
|
12207
|
+
await reapLockIfStale(file, {
|
|
12208
|
+
maxAgeMs: STALE_LOCK_TTL_MS,
|
|
12209
|
+
now
|
|
12210
|
+
});
|
|
11571
12211
|
continue;
|
|
11572
12212
|
}
|
|
11573
|
-
|
|
12213
|
+
if (entry.name.startsWith("session-") && entry.name.endsWith(".json")) {
|
|
12214
|
+
const stat2 = await fs12.promises.stat(file);
|
|
12215
|
+
if (now - stat2.mtimeMs <= SESSION_STATE_TTL_MS) continue;
|
|
12216
|
+
const sessionLockId = entry.name.slice(0, -".json".length);
|
|
12217
|
+
if (!await tryAcquireLock(sessionLockId, now)) continue;
|
|
12218
|
+
try {
|
|
12219
|
+
const lockedStat = await fs12.promises.stat(file);
|
|
12220
|
+
if (now - lockedStat.mtimeMs > SESSION_STATE_TTL_MS) {
|
|
12221
|
+
await fs12.promises.unlink(file);
|
|
12222
|
+
}
|
|
12223
|
+
} finally {
|
|
12224
|
+
await releaseLock(sessionLockId);
|
|
12225
|
+
}
|
|
12226
|
+
continue;
|
|
12227
|
+
}
|
|
12228
|
+
const stat = await fs12.promises.stat(file);
|
|
12229
|
+
if (now - stat.mtimeMs > EVENT_STATE_TTL_MS) {
|
|
12230
|
+
await fs12.promises.unlink(file);
|
|
12231
|
+
}
|
|
12232
|
+
} catch {
|
|
11574
12233
|
}
|
|
11575
12234
|
}
|
|
11576
12235
|
}
|
|
11577
12236
|
async function releaseLock(eventId) {
|
|
11578
12237
|
try {
|
|
11579
|
-
await
|
|
12238
|
+
await fs12.promises.unlink(lockFile(eventId));
|
|
11580
12239
|
} catch {
|
|
11581
12240
|
}
|
|
11582
12241
|
}
|
|
@@ -11592,7 +12251,7 @@ function initialState(ctx, now) {
|
|
|
11592
12251
|
eventKind: ctx.eventKind,
|
|
11593
12252
|
hookEventName: ctx.hookEventName,
|
|
11594
12253
|
sessionId: ctx.sessionId,
|
|
11595
|
-
logDate:
|
|
12254
|
+
logDate: path13.basename(todayLogPath(), ".log"),
|
|
11596
12255
|
firstSeenAt: now.toISOString()
|
|
11597
12256
|
};
|
|
11598
12257
|
}
|
|
@@ -11654,7 +12313,7 @@ async function waitForExpectedKinds(ctx, state) {
|
|
|
11654
12313
|
}
|
|
11655
12314
|
async function buildMiddleware(repoRoot) {
|
|
11656
12315
|
const envFileNames = await discoverEnvFiles(repoRoot);
|
|
11657
|
-
const envFilePaths = envFileNames.map((n) =>
|
|
12316
|
+
const envFilePaths = envFileNames.map((n) => path13.join(repoRoot, n));
|
|
11658
12317
|
const secretResult = await collectSecrets(repoRoot, envFilePaths, []);
|
|
11659
12318
|
const middleware2 = [];
|
|
11660
12319
|
if (secretResult.values.size > 0) {
|
|
@@ -11663,21 +12322,21 @@ async function buildMiddleware(repoRoot) {
|
|
|
11663
12322
|
middleware2.push(new PatternRedactMiddleware());
|
|
11664
12323
|
return middleware2;
|
|
11665
12324
|
}
|
|
11666
|
-
async function uploadDebugLog(ctx, state) {
|
|
12325
|
+
async function uploadDebugLog(ctx, state, sessionState, onContributionCreated) {
|
|
11667
12326
|
const logPath = todayLogPath();
|
|
11668
12327
|
let content;
|
|
11669
12328
|
try {
|
|
11670
|
-
content = await
|
|
12329
|
+
content = await fs12.promises.readFile(logPath);
|
|
11671
12330
|
} catch (err) {
|
|
11672
12331
|
appendLog(
|
|
11673
12332
|
"warn",
|
|
11674
12333
|
`debug-logs: skipped upload; log file not readable (${logPath}): ${err instanceof Error ? err.message : String(err)}`
|
|
11675
12334
|
);
|
|
11676
|
-
return
|
|
12335
|
+
return { kind: "skipped" };
|
|
11677
12336
|
}
|
|
11678
12337
|
if (content.byteLength === 0) {
|
|
11679
12338
|
appendLog("info", "debug-logs: skipped upload; log file is empty");
|
|
11680
|
-
return
|
|
12339
|
+
return { kind: "skipped" };
|
|
11681
12340
|
}
|
|
11682
12341
|
const identity = await loadIdentity(ctx.config.apiBaseUrl);
|
|
11683
12342
|
if (!identity) {
|
|
@@ -11685,7 +12344,7 @@ async function uploadDebugLog(ctx, state) {
|
|
|
11685
12344
|
"warn",
|
|
11686
12345
|
`debug-logs: skipped upload; no saved login for ${ctx.config.apiBaseUrl}`
|
|
11687
12346
|
);
|
|
11688
|
-
return
|
|
12347
|
+
return { kind: "skipped" };
|
|
11689
12348
|
}
|
|
11690
12349
|
const now = /* @__PURE__ */ new Date();
|
|
11691
12350
|
appendLog(
|
|
@@ -11700,7 +12359,7 @@ async function uploadDebugLog(ctx, state) {
|
|
|
11700
12359
|
};
|
|
11701
12360
|
const group = {
|
|
11702
12361
|
repoPath: ctx.repoRoot,
|
|
11703
|
-
label:
|
|
12362
|
+
label: path13.basename(ctx.repoRoot),
|
|
11704
12363
|
files: [sourceFile],
|
|
11705
12364
|
sourceNames: [DEBUG_LOGS_SLUG],
|
|
11706
12365
|
lastModified: now
|
|
@@ -11708,27 +12367,44 @@ async function uploadDebugLog(ctx, state) {
|
|
|
11708
12367
|
const shortSession = (ctx.sessionId ?? ctx.eventId).slice(0, 12);
|
|
11709
12368
|
const epochSeconds = formatEpochSeconds(now);
|
|
11710
12369
|
const label = toolLabel(ctx.tool);
|
|
11711
|
-
const
|
|
11712
|
-
|
|
11713
|
-
|
|
11714
|
-
|
|
11715
|
-
|
|
11716
|
-
|
|
11717
|
-
|
|
11718
|
-
|
|
12370
|
+
const sessionDescription = ctx.sessionId ? {
|
|
12371
|
+
contributionTitle: `${label} debug logs ${shortSession}`,
|
|
12372
|
+
contributionBody: [
|
|
12373
|
+
`Session ID: ${ctx.sessionId}`,
|
|
12374
|
+
`Tool: ${label}`,
|
|
12375
|
+
`Repo: ${ctx.repoRoot}`,
|
|
12376
|
+
"Log snapshots: complete Hillclimb daily log snapshots captured at hook events"
|
|
12377
|
+
].join("\n")
|
|
12378
|
+
} : {
|
|
11719
12379
|
contributionTitle: `${label} debug log ${shortSession} - ${epochSeconds}`,
|
|
11720
12380
|
contributionBody: [
|
|
11721
|
-
|
|
12381
|
+
"Session ID: <none>",
|
|
11722
12382
|
`Tool: ${label}`,
|
|
11723
12383
|
`Event: ${ctx.hookEventName ?? ctx.eventKind}`,
|
|
11724
12384
|
`Repo: ${ctx.repoRoot}`,
|
|
11725
|
-
`Log: ${
|
|
12385
|
+
`Log: ${path13.basename(logPath)}`,
|
|
11726
12386
|
`Agent done: ${state.agentDoneAt ?? "<not observed>"}`,
|
|
11727
12387
|
`Git done: ${state.gitDoneAt ?? "<not observed>"}`,
|
|
11728
12388
|
`Uploaded: ${now.toISOString()}`
|
|
11729
|
-
].join("\n")
|
|
11730
|
-
|
|
11731
|
-
|
|
12389
|
+
].join("\n")
|
|
12390
|
+
};
|
|
12391
|
+
const client = new PlatformClient(
|
|
12392
|
+
ctx.config.apiBaseUrl,
|
|
12393
|
+
identity.sessionCookie
|
|
12394
|
+
);
|
|
12395
|
+
let attemptContributionId = sessionState?.contributionId;
|
|
12396
|
+
const output = new PlatformUploadOutput({
|
|
12397
|
+
client,
|
|
12398
|
+
projectId: ctx.config.projectId,
|
|
12399
|
+
contributionTypeSlug: DEBUG_LOGS_SLUG,
|
|
12400
|
+
...sessionDescription,
|
|
12401
|
+
zipFilename: `hillclimb-debug-log-${sanitize(ctx.tool)}-${sanitize(shortSession)}-${now.getTime()}-${ctx.eventId.slice(0, 8)}-${crypto2.randomBytes(6).toString("hex")}.zip`,
|
|
12402
|
+
autoSubmit: !sessionState?.firstUploadSubmittedAt,
|
|
12403
|
+
existingContributionId: sessionState?.contributionId,
|
|
12404
|
+
onContributionCreated: onContributionCreated ? async (contributionId) => {
|
|
12405
|
+
attemptContributionId = contributionId;
|
|
12406
|
+
await onContributionCreated(contributionId);
|
|
12407
|
+
} : void 0
|
|
11732
12408
|
});
|
|
11733
12409
|
try {
|
|
11734
12410
|
const contributionId = await runPipeline(
|
|
@@ -11739,37 +12415,90 @@ async function uploadDebugLog(ctx, state) {
|
|
|
11739
12415
|
);
|
|
11740
12416
|
appendLog(
|
|
11741
12417
|
"info",
|
|
11742
|
-
`debug-logs: uploaded ${
|
|
12418
|
+
`debug-logs: uploaded ${path13.basename(logPath)} to project ${ctx.config.projectSlug} (${ctx.config.projectId}) as contribution ${contributionId}`
|
|
11743
12419
|
);
|
|
11744
|
-
return contributionId;
|
|
12420
|
+
return { kind: "uploaded", contributionId };
|
|
11745
12421
|
} catch (err) {
|
|
12422
|
+
if (attemptContributionId && err instanceof PlatformError && (err.status === 404 && err.code === "CONTRIBUTION_NOT_FOUND" || err.status === 409 && err.code === "CONTRIBUTION_UPLOAD_NOT_UPLOADED")) {
|
|
12423
|
+
appendLog(
|
|
12424
|
+
"warn",
|
|
12425
|
+
`debug-logs: contribution ${attemptContributionId} abandoned after ${err.code}; the next hook event will create a fresh contribution`
|
|
12426
|
+
);
|
|
12427
|
+
return { kind: "abandon-session" };
|
|
12428
|
+
}
|
|
11746
12429
|
if (err instanceof PlatformError && err.status === 404) {
|
|
11747
12430
|
appendLog(
|
|
11748
12431
|
"warn",
|
|
11749
12432
|
"debug-logs: upload skipped; platform does not have the debug-logs contribution type yet"
|
|
11750
12433
|
);
|
|
11751
|
-
return
|
|
12434
|
+
return { kind: "skipped" };
|
|
11752
12435
|
}
|
|
11753
12436
|
appendLog(
|
|
11754
12437
|
"error",
|
|
11755
12438
|
`debug-logs: upload failed: ${err instanceof Error ? err.message : String(err)}`
|
|
11756
12439
|
);
|
|
11757
|
-
return
|
|
12440
|
+
return { kind: "skipped" };
|
|
11758
12441
|
}
|
|
11759
12442
|
}
|
|
11760
12443
|
async function uploadOnce(ctx, state) {
|
|
11761
12444
|
await acquireLock(ctx.eventId);
|
|
11762
12445
|
try {
|
|
11763
12446
|
const latest = await readState(ctx.eventId) ?? state;
|
|
11764
|
-
if (latest.uploadedAt) return;
|
|
11765
|
-
const
|
|
11766
|
-
if (!
|
|
11767
|
-
|
|
11768
|
-
|
|
11769
|
-
|
|
11770
|
-
|
|
11771
|
-
|
|
11772
|
-
|
|
12447
|
+
if (latest.uploadedAt || latest.sessionMappingAbandonedAt) return;
|
|
12448
|
+
const key = sessionKey(ctx);
|
|
12449
|
+
if (!key) {
|
|
12450
|
+
const result = await uploadDebugLog(ctx, latest, null);
|
|
12451
|
+
if (result.kind !== "uploaded") return;
|
|
12452
|
+
await writeState({
|
|
12453
|
+
...latest,
|
|
12454
|
+
uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12455
|
+
uploadContributionId: result.contributionId
|
|
12456
|
+
});
|
|
12457
|
+
return;
|
|
12458
|
+
}
|
|
12459
|
+
const sessionLockId = sessionStateId(key);
|
|
12460
|
+
await acquireLock(sessionLockId);
|
|
12461
|
+
try {
|
|
12462
|
+
const currentSession = await readSessionState(key);
|
|
12463
|
+
const result = await uploadDebugLog(
|
|
12464
|
+
ctx,
|
|
12465
|
+
latest,
|
|
12466
|
+
currentSession,
|
|
12467
|
+
async (contributionId) => {
|
|
12468
|
+
await writeSessionState({
|
|
12469
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
12470
|
+
sessionKey: key,
|
|
12471
|
+
contributionId,
|
|
12472
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
12473
|
+
});
|
|
12474
|
+
}
|
|
12475
|
+
);
|
|
12476
|
+
if (result.kind === "abandon-session") {
|
|
12477
|
+
await deleteSessionState(key);
|
|
12478
|
+
await writeState({
|
|
12479
|
+
...latest,
|
|
12480
|
+
sessionMappingAbandonedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
12481
|
+
});
|
|
12482
|
+
return;
|
|
12483
|
+
}
|
|
12484
|
+
if (result.kind !== "uploaded") return;
|
|
12485
|
+
const uploadedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
12486
|
+
const persisted = await readSessionState(key);
|
|
12487
|
+
if (persisted?.contributionId === result.contributionId) {
|
|
12488
|
+
await writeSessionState({
|
|
12489
|
+
...persisted,
|
|
12490
|
+
firstUploadSubmittedAt: persisted.firstUploadSubmittedAt ?? uploadedAt,
|
|
12491
|
+
lastUploadAt: uploadedAt
|
|
12492
|
+
});
|
|
12493
|
+
}
|
|
12494
|
+
await writeState({
|
|
12495
|
+
...latest,
|
|
12496
|
+
uploadedAt,
|
|
12497
|
+
uploadContributionId: result.contributionId
|
|
12498
|
+
});
|
|
12499
|
+
} finally {
|
|
12500
|
+
await releaseLock(sessionLockId);
|
|
12501
|
+
}
|
|
11773
12502
|
} finally {
|
|
11774
12503
|
await releaseLock(ctx.eventId);
|
|
11775
12504
|
}
|
|
@@ -11823,7 +12552,8 @@ function buildEpochMeta(params) {
|
|
|
11823
12552
|
transcriptArchivePath: params.transcriptArchivePath,
|
|
11824
12553
|
rawByteOffset: params.cursor.rawByteOffset,
|
|
11825
12554
|
rawPrefixSha256: params.cursor.rawPrefixSha256,
|
|
11826
|
-
recordedAt: new Date(params.recordedAt).toISOString()
|
|
12555
|
+
recordedAt: new Date(params.recordedAt).toISOString(),
|
|
12556
|
+
...params.lineage
|
|
11827
12557
|
};
|
|
11828
12558
|
return Buffer.from(JSON.stringify(meta, null, 2));
|
|
11829
12559
|
}
|
|
@@ -11852,16 +12582,16 @@ async function uploadArtifact(client, contributionId, filename, mimeType, buffer
|
|
|
11852
12582
|
}
|
|
11853
12583
|
|
|
11854
12584
|
// src/transcript-cursor.ts
|
|
11855
|
-
import
|
|
11856
|
-
import
|
|
12585
|
+
import crypto3 from "crypto";
|
|
12586
|
+
import fs13 from "fs";
|
|
11857
12587
|
function sha256OfBuffer(buffer) {
|
|
11858
|
-
return
|
|
12588
|
+
return crypto3.createHash("sha256").update(buffer).digest("hex");
|
|
11859
12589
|
}
|
|
11860
12590
|
async function hashPrefix(filePath, byteLength) {
|
|
11861
|
-
const hash =
|
|
12591
|
+
const hash = crypto3.createHash("sha256");
|
|
11862
12592
|
if (byteLength === 0) return hash;
|
|
11863
12593
|
await new Promise((resolve, reject) => {
|
|
11864
|
-
const stream =
|
|
12594
|
+
const stream = fs13.createReadStream(filePath, {
|
|
11865
12595
|
start: 0,
|
|
11866
12596
|
end: byteLength - 1
|
|
11867
12597
|
});
|
|
@@ -11872,7 +12602,7 @@ async function hashPrefix(filePath, byteLength) {
|
|
|
11872
12602
|
return hash;
|
|
11873
12603
|
}
|
|
11874
12604
|
async function readRange(filePath, start, end) {
|
|
11875
|
-
const fd = await
|
|
12605
|
+
const fd = await fs13.promises.open(filePath, "r");
|
|
11876
12606
|
try {
|
|
11877
12607
|
const buffer = Buffer.alloc(end - start);
|
|
11878
12608
|
let filled = 0;
|
|
@@ -11892,7 +12622,7 @@ async function readRange(filePath, start, end) {
|
|
|
11892
12622
|
}
|
|
11893
12623
|
}
|
|
11894
12624
|
async function evaluateTranscript(filePath, cursor) {
|
|
11895
|
-
const stat = await
|
|
12625
|
+
const stat = await fs13.promises.stat(filePath);
|
|
11896
12626
|
if (stat.size < cursor.rawByteOffset) return { kind: "truncate" };
|
|
11897
12627
|
const prefixHash = await hashPrefix(filePath, cursor.rawByteOffset);
|
|
11898
12628
|
const continuation = prefixHash.copy();
|
|
@@ -11927,12 +12657,12 @@ function truncateAtLastNewline(buffer) {
|
|
|
11927
12657
|
}
|
|
11928
12658
|
async function cursorMatchesFile(filePath, cursor) {
|
|
11929
12659
|
try {
|
|
11930
|
-
const stat = await
|
|
12660
|
+
const stat = await fs13.promises.stat(filePath);
|
|
11931
12661
|
if (stat.size < cursor.rawByteOffset) return false;
|
|
11932
12662
|
const prefixHash = await hashPrefix(filePath, cursor.rawByteOffset);
|
|
11933
12663
|
if (prefixHash.digest("hex") !== cursor.rawPrefixSha256) return false;
|
|
11934
12664
|
if (cursor.rawByteOffset === 0) return true;
|
|
11935
|
-
const fd = await
|
|
12665
|
+
const fd = await fs13.promises.open(filePath, "r");
|
|
11936
12666
|
try {
|
|
11937
12667
|
const byte = Buffer.alloc(1);
|
|
11938
12668
|
const { bytesRead } = await fd.read(byte, 0, 1, cursor.rawByteOffset - 1);
|
|
@@ -11946,20 +12676,20 @@ async function cursorMatchesFile(filePath, cursor) {
|
|
|
11946
12676
|
}
|
|
11947
12677
|
|
|
11948
12678
|
// src/upload-state.ts
|
|
11949
|
-
import
|
|
11950
|
-
import
|
|
11951
|
-
import
|
|
11952
|
-
import
|
|
12679
|
+
import crypto4 from "crypto";
|
|
12680
|
+
import fs14 from "fs";
|
|
12681
|
+
import os6 from "os";
|
|
12682
|
+
import path14 from "path";
|
|
11953
12683
|
var CURRENT_SCHEMA_VERSION2 = 1;
|
|
11954
|
-
var DEFAULT_STATE_DIR =
|
|
11955
|
-
|
|
12684
|
+
var DEFAULT_STATE_DIR = path14.join(
|
|
12685
|
+
os6.homedir(),
|
|
11956
12686
|
".hillclimb",
|
|
11957
12687
|
"agent-uploads"
|
|
11958
12688
|
);
|
|
11959
|
-
var
|
|
12689
|
+
var DEFAULT_LOCK_WAIT_MS2 = 5 * 60 * 1e3;
|
|
11960
12690
|
var DEFAULT_LOCK_RETRY_DELAY_MS = 500;
|
|
11961
12691
|
var DEFAULT_STATE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
11962
|
-
var
|
|
12692
|
+
var STALE_LOCK_TTL_MS2 = 60 * 60 * 1e3;
|
|
11963
12693
|
function stateDir2() {
|
|
11964
12694
|
return process.env.HILLCLIMB_UPLOAD_STATE_DIR ?? DEFAULT_STATE_DIR;
|
|
11965
12695
|
}
|
|
@@ -11988,20 +12718,20 @@ function lockRetries() {
|
|
|
11988
12718
|
return Math.max(
|
|
11989
12719
|
1,
|
|
11990
12720
|
Math.ceil(
|
|
11991
|
-
readPositiveEnvMs("HILLCLIMB_UPLOAD_LOCK_WAIT_MS",
|
|
12721
|
+
readPositiveEnvMs("HILLCLIMB_UPLOAD_LOCK_WAIT_MS", DEFAULT_LOCK_WAIT_MS2) / lockRetryDelayMs()
|
|
11992
12722
|
)
|
|
11993
12723
|
);
|
|
11994
12724
|
}
|
|
11995
12725
|
function stateFileFor(repoRoot, tool, sessionId) {
|
|
11996
|
-
const hash =
|
|
11997
|
-
return
|
|
12726
|
+
const hash = crypto4.createHash("sha256").update(`${path14.resolve(repoRoot)}\0${tool}\0${sessionId}`).digest("hex").slice(0, 16);
|
|
12727
|
+
return path14.join(stateDir2(), `${hash}.json`);
|
|
11998
12728
|
}
|
|
11999
12729
|
function lockFileFor(repoRoot, tool, sessionId) {
|
|
12000
12730
|
return `${stateFileFor(repoRoot, tool, sessionId)}.lock`;
|
|
12001
12731
|
}
|
|
12002
12732
|
async function readUploadState(repoRoot, tool, sessionId) {
|
|
12003
12733
|
try {
|
|
12004
|
-
const raw = await
|
|
12734
|
+
const raw = await fs14.promises.readFile(
|
|
12005
12735
|
stateFileFor(repoRoot, tool, sessionId),
|
|
12006
12736
|
"utf-8"
|
|
12007
12737
|
);
|
|
@@ -12014,20 +12744,20 @@ async function readUploadState(repoRoot, tool, sessionId) {
|
|
|
12014
12744
|
}
|
|
12015
12745
|
async function writeUploadState(state) {
|
|
12016
12746
|
const file = stateFileFor(state.repoRoot, state.tool, state.sessionId);
|
|
12017
|
-
await
|
|
12747
|
+
await fs14.promises.mkdir(stateDir2(), { recursive: true, mode: 448 });
|
|
12018
12748
|
const tmp = `${file}.tmp`;
|
|
12019
|
-
await
|
|
12749
|
+
await fs14.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
|
|
12020
12750
|
mode: 384
|
|
12021
12751
|
});
|
|
12022
|
-
await
|
|
12752
|
+
await fs14.promises.rename(tmp, file);
|
|
12023
12753
|
}
|
|
12024
12754
|
async function deleteUploadState(repoRoot, tool, sessionId) {
|
|
12025
12755
|
try {
|
|
12026
|
-
await
|
|
12756
|
+
await fs14.promises.unlink(stateFileFor(repoRoot, tool, sessionId));
|
|
12027
12757
|
} catch {
|
|
12028
12758
|
}
|
|
12029
12759
|
try {
|
|
12030
|
-
await
|
|
12760
|
+
await fs14.promises.unlink(cursorFileFor(repoRoot, tool, sessionId));
|
|
12031
12761
|
} catch {
|
|
12032
12762
|
}
|
|
12033
12763
|
}
|
|
@@ -12036,7 +12766,7 @@ function cursorFileFor(repoRoot, tool, sessionId) {
|
|
|
12036
12766
|
}
|
|
12037
12767
|
async function readCursorState(repoRoot, tool, sessionId) {
|
|
12038
12768
|
try {
|
|
12039
|
-
const raw = await
|
|
12769
|
+
const raw = await fs14.promises.readFile(
|
|
12040
12770
|
cursorFileFor(repoRoot, tool, sessionId),
|
|
12041
12771
|
"utf-8"
|
|
12042
12772
|
);
|
|
@@ -12051,21 +12781,21 @@ async function readCursorState(repoRoot, tool, sessionId) {
|
|
|
12051
12781
|
}
|
|
12052
12782
|
async function writeCursorState(repoRoot, tool, sessionId, cursor) {
|
|
12053
12783
|
const file = cursorFileFor(repoRoot, tool, sessionId);
|
|
12054
|
-
await
|
|
12784
|
+
await fs14.promises.mkdir(stateDir2(), { recursive: true, mode: 448 });
|
|
12055
12785
|
const tmp = `${file}.tmp`;
|
|
12056
|
-
await
|
|
12786
|
+
await fs14.promises.writeFile(tmp, JSON.stringify(cursor, null, 2), {
|
|
12057
12787
|
mode: 384
|
|
12058
12788
|
});
|
|
12059
|
-
await
|
|
12789
|
+
await fs14.promises.rename(tmp, file);
|
|
12060
12790
|
}
|
|
12061
12791
|
async function acquireLock2(repoRoot, tool, sessionId, retries = lockRetries(), delayMs = lockRetryDelayMs()) {
|
|
12062
12792
|
const lockPath = lockFileFor(repoRoot, tool, sessionId);
|
|
12063
|
-
await
|
|
12793
|
+
await fs14.promises.mkdir(stateDir2(), { recursive: true, mode: 448 });
|
|
12064
12794
|
for (let i = 0; i < retries; i++) {
|
|
12065
12795
|
try {
|
|
12066
|
-
const fd = await
|
|
12796
|
+
const fd = await fs14.promises.open(
|
|
12067
12797
|
lockPath,
|
|
12068
|
-
|
|
12798
|
+
fs14.constants.O_CREAT | fs14.constants.O_EXCL | fs14.constants.O_WRONLY
|
|
12069
12799
|
);
|
|
12070
12800
|
try {
|
|
12071
12801
|
await fd.write(String(process.pid));
|
|
@@ -12075,6 +12805,11 @@ async function acquireLock2(repoRoot, tool, sessionId, retries = lockRetries(),
|
|
|
12075
12805
|
return;
|
|
12076
12806
|
} catch (err) {
|
|
12077
12807
|
if (err.code === "EEXIST" && i < retries - 1) {
|
|
12808
|
+
if (await reapLockIfStale(lockPath, {
|
|
12809
|
+
maxAgeMs: STALE_LOCK_TTL_MS2
|
|
12810
|
+
})) {
|
|
12811
|
+
continue;
|
|
12812
|
+
}
|
|
12078
12813
|
await new Promise((r) => setTimeout(r, delayMs));
|
|
12079
12814
|
continue;
|
|
12080
12815
|
}
|
|
@@ -12088,7 +12823,7 @@ async function acquireLock2(repoRoot, tool, sessionId, retries = lockRetries(),
|
|
|
12088
12823
|
}
|
|
12089
12824
|
async function releaseLock2(repoRoot, tool, sessionId) {
|
|
12090
12825
|
try {
|
|
12091
|
-
await
|
|
12826
|
+
await fs14.promises.unlink(lockFileFor(repoRoot, tool, sessionId));
|
|
12092
12827
|
} catch {
|
|
12093
12828
|
}
|
|
12094
12829
|
}
|
|
@@ -12100,43 +12835,27 @@ async function withUploadLock(repoRoot, tool, sessionId, fn) {
|
|
|
12100
12835
|
await releaseLock2(repoRoot, tool, sessionId);
|
|
12101
12836
|
}
|
|
12102
12837
|
}
|
|
12103
|
-
function isProcessAlive(pid) {
|
|
12104
|
-
try {
|
|
12105
|
-
process.kill(pid, 0);
|
|
12106
|
-
return true;
|
|
12107
|
-
} catch (err) {
|
|
12108
|
-
return err.code === "EPERM";
|
|
12109
|
-
}
|
|
12110
|
-
}
|
|
12111
12838
|
async function sweepStaleUploadStates(ttlMs = stateTtlMs(), now = Date.now()) {
|
|
12112
12839
|
let entries;
|
|
12113
12840
|
try {
|
|
12114
|
-
entries = await
|
|
12841
|
+
entries = await fs14.promises.readdir(stateDir2(), { withFileTypes: true });
|
|
12115
12842
|
} catch {
|
|
12116
12843
|
return;
|
|
12117
12844
|
}
|
|
12118
12845
|
for (const entry of entries) {
|
|
12119
12846
|
if (!entry.isFile()) continue;
|
|
12120
|
-
const file =
|
|
12847
|
+
const file = path14.join(stateDir2(), entry.name);
|
|
12121
12848
|
try {
|
|
12122
12849
|
if (entry.name.endsWith(".lock")) {
|
|
12123
|
-
|
|
12124
|
-
|
|
12125
|
-
|
|
12126
|
-
|
|
12127
|
-
await fs12.promises.unlink(file);
|
|
12128
|
-
}
|
|
12129
|
-
} else {
|
|
12130
|
-
const st2 = await fs12.promises.stat(file);
|
|
12131
|
-
if (now - st2.mtimeMs > STALE_LOCK_TTL_MS) {
|
|
12132
|
-
await fs12.promises.unlink(file);
|
|
12133
|
-
}
|
|
12134
|
-
}
|
|
12850
|
+
await reapLockIfStale(file, {
|
|
12851
|
+
maxAgeMs: STALE_LOCK_TTL_MS2,
|
|
12852
|
+
now
|
|
12853
|
+
});
|
|
12135
12854
|
continue;
|
|
12136
12855
|
}
|
|
12137
|
-
const st = await
|
|
12856
|
+
const st = await fs14.promises.stat(file);
|
|
12138
12857
|
if (now - st.mtimeMs > ttlMs) {
|
|
12139
|
-
await
|
|
12858
|
+
await fs14.promises.unlink(file);
|
|
12140
12859
|
}
|
|
12141
12860
|
} catch {
|
|
12142
12861
|
}
|
|
@@ -12156,13 +12875,13 @@ function formatEpochSeconds2(date) {
|
|
|
12156
12875
|
return String(Math.floor(date.getTime() / 1e3));
|
|
12157
12876
|
}
|
|
12158
12877
|
function newFlowId() {
|
|
12159
|
-
return
|
|
12878
|
+
return crypto5.randomBytes(3).toString("hex");
|
|
12160
12879
|
}
|
|
12161
12880
|
function lineHasAssistant(line) {
|
|
12162
12881
|
return line.includes('"type":"assistant"') || line.includes('"role":"assistant"') || line.includes('"type":"assistant.');
|
|
12163
12882
|
}
|
|
12164
12883
|
async function hasAssistantMessage(transcriptPath) {
|
|
12165
|
-
const stream =
|
|
12884
|
+
const stream = fs15.createReadStream(transcriptPath, { encoding: "utf-8" });
|
|
12166
12885
|
let buffer = "";
|
|
12167
12886
|
try {
|
|
12168
12887
|
for await (const chunk of stream) {
|
|
@@ -12223,53 +12942,8 @@ function summarizePayload(payload) {
|
|
|
12223
12942
|
cursor_version_present: !!payload.cursor_version
|
|
12224
12943
|
});
|
|
12225
12944
|
}
|
|
12226
|
-
async function codexSessionIdFromFile(filePath) {
|
|
12227
|
-
const stream = fs13.createReadStream(filePath, { encoding: "utf-8" });
|
|
12228
|
-
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
12229
|
-
try {
|
|
12230
|
-
for await (const line of rl) {
|
|
12231
|
-
if (!line.trim()) continue;
|
|
12232
|
-
try {
|
|
12233
|
-
const obj = JSON.parse(line);
|
|
12234
|
-
if (obj.type === "session_meta") return obj.payload?.id ?? null;
|
|
12235
|
-
} catch {
|
|
12236
|
-
return null;
|
|
12237
|
-
}
|
|
12238
|
-
return null;
|
|
12239
|
-
}
|
|
12240
|
-
} finally {
|
|
12241
|
-
rl.close();
|
|
12242
|
-
stream.destroy();
|
|
12243
|
-
}
|
|
12244
|
-
return null;
|
|
12245
|
-
}
|
|
12246
12945
|
async function findCodexTranscriptPath(sessionId) {
|
|
12247
|
-
|
|
12248
|
-
const candidates = [];
|
|
12249
|
-
async function walk(dir) {
|
|
12250
|
-
let entries;
|
|
12251
|
-
try {
|
|
12252
|
-
entries = await fs13.promises.readdir(dir, { withFileTypes: true });
|
|
12253
|
-
} catch {
|
|
12254
|
-
return;
|
|
12255
|
-
}
|
|
12256
|
-
for (const entry of entries) {
|
|
12257
|
-
const full = path14.join(dir, entry.name);
|
|
12258
|
-
if (entry.isDirectory()) {
|
|
12259
|
-
await walk(full);
|
|
12260
|
-
} else if (entry.isFile() && entry.name.endsWith(".jsonl") && entry.name.includes(sessionId)) {
|
|
12261
|
-
candidates.push(full);
|
|
12262
|
-
}
|
|
12263
|
-
}
|
|
12264
|
-
}
|
|
12265
|
-
await walk(sessionsDir);
|
|
12266
|
-
candidates.sort();
|
|
12267
|
-
for (const filePath of candidates) {
|
|
12268
|
-
if (await codexSessionIdFromFile(filePath) === sessionId) {
|
|
12269
|
-
return filePath;
|
|
12270
|
-
}
|
|
12271
|
-
}
|
|
12272
|
-
return void 0;
|
|
12946
|
+
return findCodexRolloutPath(sessionId);
|
|
12273
12947
|
}
|
|
12274
12948
|
async function selfHealHook(repoRoot, tool) {
|
|
12275
12949
|
if (process.env.HILLCLIMB_SKIP_HOOK_SELF_HEAL === "1") return;
|
|
@@ -12294,8 +12968,8 @@ function resolveCursorTranscriptPath(payload) {
|
|
|
12294
12968
|
const workspace = payload.workspace_roots?.[0];
|
|
12295
12969
|
if (!id || !workspace) return void 0;
|
|
12296
12970
|
const encoded = workspace.replace(/^\//, "").replace(/\//g, "-");
|
|
12297
|
-
return
|
|
12298
|
-
|
|
12971
|
+
return path15.join(
|
|
12972
|
+
os7.homedir(),
|
|
12299
12973
|
".cursor",
|
|
12300
12974
|
"projects",
|
|
12301
12975
|
encoded,
|
|
@@ -12322,7 +12996,7 @@ async function resolveTranscriptPath(payload, sourceTool, sessionId) {
|
|
|
12322
12996
|
async function runUploadInner(payload) {
|
|
12323
12997
|
const sessionId = resolveHookSessionId(payload);
|
|
12324
12998
|
const cwd = resolveHookCwd(payload);
|
|
12325
|
-
const
|
|
12999
|
+
const eventKind2 = classifyHookEvent(payload.hook_event_name);
|
|
12326
13000
|
const recordedAt = Date.now();
|
|
12327
13001
|
if (!sessionId || !cwd) {
|
|
12328
13002
|
appendLog(
|
|
@@ -12348,7 +13022,7 @@ async function runUploadInner(payload) {
|
|
|
12348
13022
|
payload.tool = sourceTool;
|
|
12349
13023
|
appendLog(
|
|
12350
13024
|
"info",
|
|
12351
|
-
`[${sessionId}] payload parsed (tool=${sourceTool}, cwd=${cwd}, event=${
|
|
13025
|
+
`[${sessionId}] payload parsed (tool=${sourceTool}, cwd=${cwd}, event=${eventKind2 ?? "?"})`
|
|
12352
13026
|
);
|
|
12353
13027
|
await selfHealHook(repoRoot, sourceTool);
|
|
12354
13028
|
const transcriptPath = await resolveTranscriptPath(
|
|
@@ -12363,9 +13037,9 @@ async function runUploadInner(payload) {
|
|
|
12363
13037
|
);
|
|
12364
13038
|
return false;
|
|
12365
13039
|
}
|
|
12366
|
-
const transcriptResolved =
|
|
13040
|
+
const transcriptResolved = path15.resolve(transcriptPath);
|
|
12367
13041
|
try {
|
|
12368
|
-
const stat = await
|
|
13042
|
+
const stat = await fs15.promises.stat(transcriptResolved);
|
|
12369
13043
|
if (!stat.isFile()) {
|
|
12370
13044
|
appendLog(
|
|
12371
13045
|
"warn",
|
|
@@ -12393,12 +13067,12 @@ async function runUploadInner(payload) {
|
|
|
12393
13067
|
repoRoot,
|
|
12394
13068
|
config,
|
|
12395
13069
|
sourceTool,
|
|
12396
|
-
eventKind,
|
|
13070
|
+
eventKind: eventKind2,
|
|
12397
13071
|
recordedAt
|
|
12398
13072
|
});
|
|
12399
13073
|
}
|
|
12400
13074
|
var AGENT_MAX_UPLOAD_BYTES = 1024 * 1024 * 1024;
|
|
12401
|
-
var CLI_VERSION = "0.
|
|
13075
|
+
var CLI_VERSION = "0.8.0";
|
|
12402
13076
|
function makeTranscriptGroup(repoRoot, sourceTool, transcriptPath, content) {
|
|
12403
13077
|
const sourceFile = {
|
|
12404
13078
|
sourceName: sourceTool,
|
|
@@ -12408,7 +13082,7 @@ function makeTranscriptGroup(repoRoot, sourceTool, transcriptPath, content) {
|
|
|
12408
13082
|
};
|
|
12409
13083
|
return {
|
|
12410
13084
|
repoPath: repoRoot,
|
|
12411
|
-
label:
|
|
13085
|
+
label: path15.basename(repoRoot),
|
|
12412
13086
|
files: [sourceFile],
|
|
12413
13087
|
sourceNames: [sourceTool],
|
|
12414
13088
|
lastModified: /* @__PURE__ */ new Date()
|
|
@@ -12416,7 +13090,7 @@ function makeTranscriptGroup(repoRoot, sourceTool, transcriptPath, content) {
|
|
|
12416
13090
|
}
|
|
12417
13091
|
async function buildRedactChain(repoRoot) {
|
|
12418
13092
|
const envFileNames = await discoverEnvFiles(repoRoot);
|
|
12419
|
-
const envFilePaths = envFileNames.map((n) =>
|
|
13093
|
+
const envFilePaths = envFileNames.map((n) => path15.join(repoRoot, n));
|
|
12420
13094
|
const secretResult = await collectSecrets(repoRoot, envFilePaths, []);
|
|
12421
13095
|
const chain = [];
|
|
12422
13096
|
if (secretResult.values.size > 0) {
|
|
@@ -12466,6 +13140,14 @@ async function handleUploadFailure(err, sessionId, repoRoot, sourceTool) {
|
|
|
12466
13140
|
);
|
|
12467
13141
|
return false;
|
|
12468
13142
|
}
|
|
13143
|
+
if (isContributionUploadNotUploaded(err)) {
|
|
13144
|
+
await deleteUploadState(repoRoot, sourceTool, sessionId);
|
|
13145
|
+
appendLog(
|
|
13146
|
+
"warn",
|
|
13147
|
+
`Session ${sessionId} upload failed: storage validation returned ${err.code}; contribution abandoned and local state cleared so the next hook uploads a complete snapshot to a fresh contribution.`
|
|
13148
|
+
);
|
|
13149
|
+
return false;
|
|
13150
|
+
}
|
|
12469
13151
|
appendLog(
|
|
12470
13152
|
"error",
|
|
12471
13153
|
`Session ${sessionId} upload failed: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -12479,13 +13161,24 @@ async function uploadSession(args) {
|
|
|
12479
13161
|
repoRoot,
|
|
12480
13162
|
config,
|
|
12481
13163
|
sourceTool,
|
|
12482
|
-
eventKind,
|
|
13164
|
+
eventKind: eventKind2,
|
|
12483
13165
|
recordedAt
|
|
12484
13166
|
} = args;
|
|
12485
|
-
const isSessionEnd =
|
|
13167
|
+
const isSessionEnd = eventKind2 === "sessionEnd";
|
|
12486
13168
|
const eventLabel = isSessionEnd ? "SessionEnd" : "Stop";
|
|
12487
13169
|
return await withUploadLock(repoRoot, sourceTool, sessionId, async () => {
|
|
12488
13170
|
const prior = await readUploadState(repoRoot, sourceTool, sessionId);
|
|
13171
|
+
const lineageDetection = sourceTool === "codex" && prior?.codexLineageChecked !== true ? await detectCodexLineage(transcriptPath) : void 0;
|
|
13172
|
+
const codexLineage = sourceTool === "codex" ? mergeCodexLineage(prior?.codexLineage, lineageDetection?.lineage) : void 0;
|
|
13173
|
+
const codexLineageChecked = sourceTool === "codex" ? prior?.codexLineageChecked === true || lineageDetection?.conclusive === true : void 0;
|
|
13174
|
+
const lineageNeedsStamp = codexLineage !== void 0 && JSON.stringify(codexLineage) !== JSON.stringify(prior?.codexLineageStamped);
|
|
13175
|
+
if (prior && sourceTool === "codex" && (JSON.stringify(codexLineage) !== JSON.stringify(prior.codexLineage) || codexLineageChecked !== prior.codexLineageChecked)) {
|
|
13176
|
+
await writeUploadState({
|
|
13177
|
+
...prior,
|
|
13178
|
+
codexLineage,
|
|
13179
|
+
codexLineageChecked
|
|
13180
|
+
});
|
|
13181
|
+
}
|
|
12489
13182
|
let cursor = null;
|
|
12490
13183
|
let adoptedLegacy = false;
|
|
12491
13184
|
let restored = null;
|
|
@@ -12539,16 +13232,22 @@ async function uploadSession(args) {
|
|
|
12539
13232
|
if (cursor) {
|
|
12540
13233
|
const evaluation = await evaluateTranscript(transcriptPath, cursor);
|
|
12541
13234
|
if (evaluation.kind === "unchanged") {
|
|
12542
|
-
|
|
12543
|
-
"
|
|
12544
|
-
|
|
12545
|
-
|
|
12546
|
-
|
|
12547
|
-
|
|
13235
|
+
if (lineageNeedsStamp && prior?.contributionId && prior.epochMeta) {
|
|
13236
|
+
mode = "meta-refresh";
|
|
13237
|
+
} else if (lineageNeedsStamp) {
|
|
13238
|
+
mode = "snapshot";
|
|
13239
|
+
transition = "state-lost";
|
|
13240
|
+
} else {
|
|
13241
|
+
appendLog(
|
|
13242
|
+
"info",
|
|
13243
|
+
`[${sessionId}] skipping ${sourceTool} ${eventLabel} upload (no new complete transcript lines past offset ${cursor.rawByteOffset}${isSessionEnd ? "; local state cleared" : ""})`
|
|
13244
|
+
);
|
|
13245
|
+
if (isSessionEnd) {
|
|
13246
|
+
await deleteUploadState(repoRoot, sourceTool, sessionId);
|
|
13247
|
+
}
|
|
13248
|
+
return true;
|
|
12548
13249
|
}
|
|
12549
|
-
|
|
12550
|
-
}
|
|
12551
|
-
if (evaluation.kind === "append") {
|
|
13250
|
+
} else if (evaluation.kind === "append") {
|
|
12552
13251
|
mode = "patch";
|
|
12553
13252
|
tail = evaluation.tail;
|
|
12554
13253
|
nextCursor = evaluation.nextCursor;
|
|
@@ -12580,6 +13279,51 @@ async function uploadSession(args) {
|
|
|
12580
13279
|
});
|
|
12581
13280
|
const alreadySubmitted = prior?.submitted ?? false;
|
|
12582
13281
|
const submitThisUpload = config.autoSubmit && !alreadySubmitted;
|
|
13282
|
+
if (mode === "meta-refresh" && prior?.contributionId && prior.epochMeta && cursor) {
|
|
13283
|
+
const epoch2 = prior.epoch ?? restored?.epoch ?? 1;
|
|
13284
|
+
try {
|
|
13285
|
+
await uploadArtifact(
|
|
13286
|
+
client,
|
|
13287
|
+
prior.contributionId,
|
|
13288
|
+
metaFilename(epoch2, recordedAt),
|
|
13289
|
+
"application/json",
|
|
13290
|
+
buildEpochMeta({
|
|
13291
|
+
sessionId,
|
|
13292
|
+
tool: sourceTool,
|
|
13293
|
+
cliVersion: CLI_VERSION,
|
|
13294
|
+
epoch: epoch2,
|
|
13295
|
+
transitionKind: prior.epochMeta.transitionKind,
|
|
13296
|
+
baseline: prior.epochMeta.baseline,
|
|
13297
|
+
snapshotFilename: prior.epochMeta.snapshotFilename,
|
|
13298
|
+
transcriptArchivePath: prior.epochMeta.transcriptArchivePath,
|
|
13299
|
+
cursor: {
|
|
13300
|
+
rawByteOffset: prior.epochMeta.rawByteOffset,
|
|
13301
|
+
rawPrefixSha256: prior.epochMeta.rawPrefixSha256
|
|
13302
|
+
},
|
|
13303
|
+
recordedAt,
|
|
13304
|
+
lineage: codexLineage
|
|
13305
|
+
})
|
|
13306
|
+
);
|
|
13307
|
+
} catch (err) {
|
|
13308
|
+
return handleUploadFailure(err, sessionId, repoRoot, sourceTool);
|
|
13309
|
+
}
|
|
13310
|
+
await writeUploadState({
|
|
13311
|
+
...prior,
|
|
13312
|
+
lastUploadedAt: now.toISOString(),
|
|
13313
|
+
uploadCount: prior.uploadCount + 1,
|
|
13314
|
+
codexLineage,
|
|
13315
|
+
codexLineageChecked,
|
|
13316
|
+
codexLineageStamped: codexLineage
|
|
13317
|
+
});
|
|
13318
|
+
appendLog(
|
|
13319
|
+
"info",
|
|
13320
|
+
`[${sessionId}] refreshed ${sourceTool} lineage meta for unchanged epoch=${epoch2}`
|
|
13321
|
+
);
|
|
13322
|
+
if (isSessionEnd) {
|
|
13323
|
+
await deleteUploadState(repoRoot, sourceTool, sessionId);
|
|
13324
|
+
}
|
|
13325
|
+
return true;
|
|
13326
|
+
}
|
|
12583
13327
|
if (mode === "patch" && cursor && tail && nextCursor && prior?.contributionId) {
|
|
12584
13328
|
const baseCursor = cursor;
|
|
12585
13329
|
const contributionId2 = prior.contributionId;
|
|
@@ -12589,6 +13333,9 @@ async function uploadSession(args) {
|
|
|
12589
13333
|
"info",
|
|
12590
13334
|
`[${sessionId}] ${sourceTool} ${eventLabel} upload \u2192 patch epoch=${epoch2} turn=${turn} (${tail.length} raw tail bytes, contribution ${contributionId2})`
|
|
12591
13335
|
);
|
|
13336
|
+
let metaUploaded2 = 0;
|
|
13337
|
+
let codexLineageStamped2 = prior.codexLineageStamped;
|
|
13338
|
+
let epochMeta2 = prior.epochMeta;
|
|
12592
13339
|
try {
|
|
12593
13340
|
const redactedTail = await redactTail(
|
|
12594
13341
|
tail,
|
|
@@ -12598,6 +13345,13 @@ async function uploadSession(args) {
|
|
|
12598
13345
|
redactChain
|
|
12599
13346
|
);
|
|
12600
13347
|
if (adoptedLegacy) {
|
|
13348
|
+
epochMeta2 = {
|
|
13349
|
+
transitionKind: "legacy-adopted",
|
|
13350
|
+
baseline: "legacy-latest-zip",
|
|
13351
|
+
transcriptArchivePath,
|
|
13352
|
+
rawByteOffset: baseCursor.rawByteOffset,
|
|
13353
|
+
rawPrefixSha256: baseCursor.rawPrefixSha256
|
|
13354
|
+
};
|
|
12601
13355
|
await uploadArtifact(
|
|
12602
13356
|
client,
|
|
12603
13357
|
contributionId2,
|
|
@@ -12612,9 +13366,12 @@ async function uploadSession(args) {
|
|
|
12612
13366
|
baseline: "legacy-latest-zip",
|
|
12613
13367
|
transcriptArchivePath,
|
|
12614
13368
|
cursor: baseCursor,
|
|
12615
|
-
recordedAt
|
|
13369
|
+
recordedAt,
|
|
13370
|
+
lineage: codexLineage
|
|
12616
13371
|
})
|
|
12617
13372
|
);
|
|
13373
|
+
metaUploaded2 = 1;
|
|
13374
|
+
codexLineageStamped2 = codexLineage;
|
|
12618
13375
|
}
|
|
12619
13376
|
await uploadArtifact(
|
|
12620
13377
|
client,
|
|
@@ -12636,7 +13393,7 @@ async function uploadSession(args) {
|
|
|
12636
13393
|
projectId: config.projectId,
|
|
12637
13394
|
contributionId: contributionId2,
|
|
12638
13395
|
submitted: submitted2,
|
|
12639
|
-
uploadCount: (prior.uploadCount ?? 0) +
|
|
13396
|
+
uploadCount: (prior.uploadCount ?? 0) + 1 + metaUploaded2,
|
|
12640
13397
|
firstUploadedAt: prior.firstUploadedAt ?? now.toISOString(),
|
|
12641
13398
|
lastUploadedAt: now.toISOString(),
|
|
12642
13399
|
// Mirrors the cursor from here on: describes the raw bytes covered
|
|
@@ -12649,7 +13406,11 @@ async function uploadSession(args) {
|
|
|
12649
13406
|
turnCount: turn,
|
|
12650
13407
|
rawByteOffset: nextCursor.rawByteOffset,
|
|
12651
13408
|
rawPrefixSha256: nextCursor.rawPrefixSha256,
|
|
12652
|
-
snapshotUploaded: true
|
|
13409
|
+
snapshotUploaded: true,
|
|
13410
|
+
codexLineage,
|
|
13411
|
+
codexLineageChecked,
|
|
13412
|
+
codexLineageStamped: codexLineageStamped2,
|
|
13413
|
+
epochMeta: epochMeta2
|
|
12653
13414
|
});
|
|
12654
13415
|
await writeCursorState(repoRoot, sourceTool, sessionId, {
|
|
12655
13416
|
contributionId: contributionId2,
|
|
@@ -12661,6 +13422,45 @@ async function uploadSession(args) {
|
|
|
12661
13422
|
});
|
|
12662
13423
|
};
|
|
12663
13424
|
await persistState(alreadySubmitted);
|
|
13425
|
+
if (!adoptedLegacy && codexLineage && epochMeta2 && JSON.stringify(codexLineage) !== JSON.stringify(prior.codexLineageStamped)) {
|
|
13426
|
+
try {
|
|
13427
|
+
await uploadArtifact(
|
|
13428
|
+
client,
|
|
13429
|
+
contributionId2,
|
|
13430
|
+
metaFilename(epoch2, recordedAt),
|
|
13431
|
+
"application/json",
|
|
13432
|
+
buildEpochMeta({
|
|
13433
|
+
sessionId,
|
|
13434
|
+
tool: sourceTool,
|
|
13435
|
+
cliVersion: CLI_VERSION,
|
|
13436
|
+
epoch: epoch2,
|
|
13437
|
+
transitionKind: epochMeta2.transitionKind,
|
|
13438
|
+
baseline: epochMeta2.baseline,
|
|
13439
|
+
snapshotFilename: epochMeta2.snapshotFilename,
|
|
13440
|
+
transcriptArchivePath: epochMeta2.transcriptArchivePath,
|
|
13441
|
+
cursor: {
|
|
13442
|
+
rawByteOffset: epochMeta2.rawByteOffset,
|
|
13443
|
+
rawPrefixSha256: epochMeta2.rawPrefixSha256
|
|
13444
|
+
},
|
|
13445
|
+
recordedAt,
|
|
13446
|
+
lineage: codexLineage
|
|
13447
|
+
})
|
|
13448
|
+
);
|
|
13449
|
+
metaUploaded2 = 1;
|
|
13450
|
+
codexLineageStamped2 = codexLineage;
|
|
13451
|
+
await persistState(alreadySubmitted);
|
|
13452
|
+
} catch (err) {
|
|
13453
|
+
appendLog(
|
|
13454
|
+
"warn",
|
|
13455
|
+
`[${sessionId}] lineage meta refresh failed after durable patch capture; lineage remains pending: ${err instanceof Error ? err.message : String(err)}`
|
|
13456
|
+
);
|
|
13457
|
+
}
|
|
13458
|
+
} else if (lineageNeedsStamp && !epochMeta2) {
|
|
13459
|
+
appendLog(
|
|
13460
|
+
"info",
|
|
13461
|
+
`[${sessionId}] lineage publication deferred after durable patch capture (pre-feature state has no reconstructable epoch metadata)`
|
|
13462
|
+
);
|
|
13463
|
+
}
|
|
12664
13464
|
let submitted = alreadySubmitted;
|
|
12665
13465
|
if (submitThisUpload) {
|
|
12666
13466
|
try {
|
|
@@ -12675,6 +13475,8 @@ async function uploadSession(args) {
|
|
|
12675
13475
|
"info",
|
|
12676
13476
|
`contribution ${contributionId2} was already submitted (409); recording locally`
|
|
12677
13477
|
);
|
|
13478
|
+
} else if (isContributionUploadNotUploaded(err)) {
|
|
13479
|
+
return handleUploadFailure(err, sessionId, repoRoot, sourceTool);
|
|
12678
13480
|
} else {
|
|
12679
13481
|
appendLog(
|
|
12680
13482
|
"warn",
|
|
@@ -12699,7 +13501,7 @@ async function uploadSession(args) {
|
|
|
12699
13501
|
}
|
|
12700
13502
|
let raw;
|
|
12701
13503
|
try {
|
|
12702
|
-
raw = await
|
|
13504
|
+
raw = await fs15.promises.readFile(transcriptPath);
|
|
12703
13505
|
} catch (err) {
|
|
12704
13506
|
if (err.code === "ERR_FS_FILE_TOO_LARGE") {
|
|
12705
13507
|
appendLog(
|
|
@@ -12740,6 +13542,17 @@ Tool: ${toolLabel2}
|
|
|
12740
13542
|
Repo: ${repoRoot}
|
|
12741
13543
|
Uploaded: ${now.toISOString()}`;
|
|
12742
13544
|
const zipFilename = snapshotFilename(epoch, recordedAt);
|
|
13545
|
+
const epochMeta = {
|
|
13546
|
+
transitionKind: transition,
|
|
13547
|
+
baseline: "snapshot",
|
|
13548
|
+
snapshotFilename: zipFilename,
|
|
13549
|
+
transcriptArchivePath: archivePathFor({
|
|
13550
|
+
sourceName: sourceTool,
|
|
13551
|
+
absolutePath: transcriptPath
|
|
13552
|
+
}),
|
|
13553
|
+
rawByteOffset: truncated.cursor.rawByteOffset,
|
|
13554
|
+
rawPrefixSha256: truncated.cursor.rawPrefixSha256
|
|
13555
|
+
};
|
|
12743
13556
|
const group = makeTranscriptGroup(
|
|
12744
13557
|
repoRoot,
|
|
12745
13558
|
sourceTool,
|
|
@@ -12771,7 +13584,10 @@ Uploaded: ${now.toISOString()}`;
|
|
|
12771
13584
|
firstUploadedAt: prior?.firstUploadedAt ?? now.toISOString(),
|
|
12772
13585
|
lastUploadedAt: prior?.lastUploadedAt ?? now.toISOString(),
|
|
12773
13586
|
lastTranscriptSize: prior?.lastTranscriptSize,
|
|
12774
|
-
lastTranscriptSha256: prior?.lastTranscriptSha256
|
|
13587
|
+
lastTranscriptSha256: prior?.lastTranscriptSha256,
|
|
13588
|
+
codexLineage,
|
|
13589
|
+
codexLineageChecked,
|
|
13590
|
+
epochMeta
|
|
12775
13591
|
})
|
|
12776
13592
|
});
|
|
12777
13593
|
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)";
|
|
@@ -12788,6 +13604,7 @@ Uploaded: ${now.toISOString()}`;
|
|
|
12788
13604
|
return handleUploadFailure(err, sessionId, repoRoot, sourceTool);
|
|
12789
13605
|
}
|
|
12790
13606
|
let metaUploaded = 0;
|
|
13607
|
+
let codexLineageStamped;
|
|
12791
13608
|
try {
|
|
12792
13609
|
await uploadArtifact(
|
|
12793
13610
|
client,
|
|
@@ -12804,10 +13621,12 @@ Uploaded: ${now.toISOString()}`;
|
|
|
12804
13621
|
snapshotFilename: zipFilename,
|
|
12805
13622
|
transcriptArchivePath,
|
|
12806
13623
|
cursor: truncated.cursor,
|
|
12807
|
-
recordedAt
|
|
13624
|
+
recordedAt,
|
|
13625
|
+
lineage: codexLineage
|
|
12808
13626
|
})
|
|
12809
13627
|
);
|
|
12810
13628
|
metaUploaded = 1;
|
|
13629
|
+
codexLineageStamped = codexLineage;
|
|
12811
13630
|
} catch (err) {
|
|
12812
13631
|
appendLog(
|
|
12813
13632
|
"warn",
|
|
@@ -12832,7 +13651,11 @@ Uploaded: ${now.toISOString()}`;
|
|
|
12832
13651
|
turnCount: 0,
|
|
12833
13652
|
rawByteOffset: truncated.cursor.rawByteOffset,
|
|
12834
13653
|
rawPrefixSha256: truncated.cursor.rawPrefixSha256,
|
|
12835
|
-
snapshotUploaded: true
|
|
13654
|
+
snapshotUploaded: true,
|
|
13655
|
+
codexLineage,
|
|
13656
|
+
codexLineageChecked,
|
|
13657
|
+
codexLineageStamped,
|
|
13658
|
+
epochMeta
|
|
12836
13659
|
});
|
|
12837
13660
|
await writeCursorState(repoRoot, sourceTool, sessionId, {
|
|
12838
13661
|
contributionId,
|
|
@@ -12975,6 +13798,7 @@ async function runUploadWorker() {
|
|
|
12975
13798
|
}
|
|
12976
13799
|
try {
|
|
12977
13800
|
await sweepStaleUploadStates();
|
|
13801
|
+
await sweepStaleDebugLogState();
|
|
12978
13802
|
} catch {
|
|
12979
13803
|
}
|
|
12980
13804
|
}
|
|
@@ -12982,26 +13806,58 @@ async function runUploadWorker() {
|
|
|
12982
13806
|
|
|
12983
13807
|
// src/git-traces/index.ts
|
|
12984
13808
|
import { spawn as spawn3 } from "child_process";
|
|
12985
|
-
import
|
|
13809
|
+
import crypto7 from "crypto";
|
|
12986
13810
|
|
|
12987
13811
|
// src/git-traces/handlers.ts
|
|
12988
13812
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
12989
|
-
import
|
|
13813
|
+
import path18 from "path";
|
|
12990
13814
|
|
|
12991
13815
|
// src/git-traces/git-ops.ts
|
|
12992
13816
|
import { execFileSync as execFileSync2, spawnSync } from "child_process";
|
|
12993
|
-
import
|
|
12994
|
-
import
|
|
12995
|
-
import
|
|
13817
|
+
import fs16 from "fs";
|
|
13818
|
+
import os8 from "os";
|
|
13819
|
+
import path16 from "path";
|
|
12996
13820
|
import { gzipSync as gzipSync2 } from "zlib";
|
|
12997
13821
|
var GIT_COMMAND_TIMEOUT_MS = 12e4;
|
|
12998
13822
|
var EXEC_OPTS = {
|
|
12999
13823
|
timeout: GIT_COMMAND_TIMEOUT_MS,
|
|
13000
|
-
|
|
13824
|
+
// A rewrite diff for a text file capped at ≤100 MB must fit. This transient
|
|
13825
|
+
// allocation only grows as large as the actual command output.
|
|
13826
|
+
maxBuffer: 256 * 1024 * 1024
|
|
13001
13827
|
};
|
|
13002
13828
|
var MAX_ERROR_OUTPUT_CHARS = 2e3;
|
|
13003
|
-
var
|
|
13004
|
-
|
|
13829
|
+
var DEFAULT_SNAPSHOT_LIMITS = {
|
|
13830
|
+
maxTrackedFileBytes: 25 * 1024 * 1024,
|
|
13831
|
+
// 25 MB
|
|
13832
|
+
maxUntrackedFileBytes: 10 * 1024 * 1024,
|
|
13833
|
+
// 10 MB
|
|
13834
|
+
maxBinaryFileBytes: 1024 * 1024
|
|
13835
|
+
// 1 MB
|
|
13836
|
+
};
|
|
13837
|
+
var LEGACY_SNAPSHOT_LIMITS = {
|
|
13838
|
+
maxTrackedFileBytes: 10 * 1024 * 1024,
|
|
13839
|
+
maxUntrackedFileBytes: 10 * 1024 * 1024,
|
|
13840
|
+
maxBinaryFileBytes: 1024 * 1024
|
|
13841
|
+
};
|
|
13842
|
+
function resolveSnapshotLimits(overrides) {
|
|
13843
|
+
return {
|
|
13844
|
+
maxTrackedFileBytes: positiveIntOrDefault(
|
|
13845
|
+
overrides?.maxTrackedFileBytes,
|
|
13846
|
+
DEFAULT_SNAPSHOT_LIMITS.maxTrackedFileBytes
|
|
13847
|
+
),
|
|
13848
|
+
maxUntrackedFileBytes: positiveIntOrDefault(
|
|
13849
|
+
overrides?.maxUntrackedFileBytes,
|
|
13850
|
+
DEFAULT_SNAPSHOT_LIMITS.maxUntrackedFileBytes
|
|
13851
|
+
),
|
|
13852
|
+
maxBinaryFileBytes: positiveIntOrDefault(
|
|
13853
|
+
overrides?.maxBinaryFileBytes,
|
|
13854
|
+
DEFAULT_SNAPSHOT_LIMITS.maxBinaryFileBytes
|
|
13855
|
+
)
|
|
13856
|
+
};
|
|
13857
|
+
}
|
|
13858
|
+
function positiveIntOrDefault(value, fallback) {
|
|
13859
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 && value <= MAX_SNAPSHOT_LIMIT_OVERRIDE_BYTES ? value : fallback;
|
|
13860
|
+
}
|
|
13005
13861
|
var BINARY_SNIFF_BYTES = 8e3;
|
|
13006
13862
|
var EXCLUDED_SNAPSHOT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
13007
13863
|
// Documents
|
|
@@ -13098,8 +13954,8 @@ var EXCLUDED_SNAPSHOT_BASENAMES = /* @__PURE__ */ new Set([
|
|
|
13098
13954
|
]);
|
|
13099
13955
|
var EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
13100
13956
|
function isExcludedSnapshotPath(filePath) {
|
|
13101
|
-
if (EXCLUDED_SNAPSHOT_BASENAMES.has(
|
|
13102
|
-
return EXCLUDED_SNAPSHOT_EXTENSIONS.has(
|
|
13957
|
+
if (EXCLUDED_SNAPSHOT_BASENAMES.has(path16.basename(filePath))) return true;
|
|
13958
|
+
return EXCLUDED_SNAPSHOT_EXTENSIONS.has(path16.extname(filePath).toLowerCase());
|
|
13103
13959
|
}
|
|
13104
13960
|
function isBinaryBuffer(buffer) {
|
|
13105
13961
|
return buffer.includes(0);
|
|
@@ -13119,25 +13975,26 @@ function readTreeBlobHead(repoRoot, sha) {
|
|
|
13119
13975
|
function readWorkingFileHead(absPath) {
|
|
13120
13976
|
let fd = null;
|
|
13121
13977
|
try {
|
|
13122
|
-
fd =
|
|
13978
|
+
fd = fs16.openSync(absPath, "r");
|
|
13123
13979
|
const buffer = Buffer.alloc(BINARY_SNIFF_BYTES);
|
|
13124
|
-
const bytesRead =
|
|
13980
|
+
const bytesRead = fs16.readSync(fd, buffer, 0, BINARY_SNIFF_BYTES, 0);
|
|
13125
13981
|
return buffer.subarray(0, bytesRead);
|
|
13126
13982
|
} catch {
|
|
13127
13983
|
return null;
|
|
13128
13984
|
} finally {
|
|
13129
13985
|
if (fd !== null) {
|
|
13130
13986
|
try {
|
|
13131
|
-
|
|
13987
|
+
fs16.closeSync(fd);
|
|
13132
13988
|
} catch {
|
|
13133
13989
|
}
|
|
13134
13990
|
}
|
|
13135
13991
|
}
|
|
13136
13992
|
}
|
|
13137
|
-
function classifyOmission(filePath, sizeBytes, readHead) {
|
|
13993
|
+
function classifyOmission(filePath, sizeBytes, readHead, limits, tracked) {
|
|
13994
|
+
const maxFileBytes = tracked ? limits.maxTrackedFileBytes : limits.maxUntrackedFileBytes;
|
|
13138
13995
|
if (isExcludedSnapshotPath(filePath)) return "excluded-extension";
|
|
13139
|
-
if (sizeBytes >
|
|
13140
|
-
if (sizeBytes >
|
|
13996
|
+
if (sizeBytes > maxFileBytes) return "file-over-limit";
|
|
13997
|
+
if (sizeBytes > limits.maxBinaryFileBytes) {
|
|
13141
13998
|
const head = readHead();
|
|
13142
13999
|
if (!head) {
|
|
13143
14000
|
appendLog(
|
|
@@ -13249,12 +14106,32 @@ function deleteRef(repoRoot, refName) {
|
|
|
13249
14106
|
function captureSnapshotSha(repoRoot) {
|
|
13250
14107
|
return captureWorkingCommitSha(repoRoot);
|
|
13251
14108
|
}
|
|
13252
|
-
function
|
|
13253
|
-
|
|
13254
|
-
|
|
13255
|
-
|
|
13256
|
-
|
|
13257
|
-
|
|
14109
|
+
function formatSize(bytes) {
|
|
14110
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
14111
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
14112
|
+
}
|
|
14113
|
+
function summarizeOmittedFiles(files) {
|
|
14114
|
+
const totalBytes = files.reduce((sum, file) => sum + file.sizeBytes, 0);
|
|
14115
|
+
const top = [...files].sort((a, b) => b.sizeBytes - a.sizeBytes).slice(0, 3).map((file) => `${file.path} (${formatSize(file.sizeBytes)})`).join(", ");
|
|
14116
|
+
const more = files.length > 3 ? ` +${files.length - 3} more` : "";
|
|
14117
|
+
const plural = files.length === 1 ? "" : "s";
|
|
14118
|
+
return `${files.length} file${plural} (${formatSize(totalBytes)}): ${top}${more}`;
|
|
14119
|
+
}
|
|
14120
|
+
function logOmittedSnapshotSummary(omittedFiles) {
|
|
14121
|
+
const tracked = omittedFiles.filter((file) => file.tracked);
|
|
14122
|
+
const untracked = omittedFiles.filter((file) => !file.tracked);
|
|
14123
|
+
if (tracked.length > 0) {
|
|
14124
|
+
appendLog(
|
|
14125
|
+
"warn",
|
|
14126
|
+
`git-traces: snapshot omitted tracked ${summarizeOmittedFiles(tracked)} \u2014 content excluded from the baseline bundle and all diffs; full list in baseline.json git.omittedFiles`
|
|
14127
|
+
);
|
|
14128
|
+
}
|
|
14129
|
+
if (untracked.length > 0) {
|
|
14130
|
+
appendLog(
|
|
14131
|
+
"info",
|
|
14132
|
+
`git-traces: snapshot skipped untracked ${summarizeOmittedFiles(untracked)}; full list in baseline.json git.omittedFiles`
|
|
14133
|
+
);
|
|
14134
|
+
}
|
|
13258
14135
|
}
|
|
13259
14136
|
function parseLsTreeLongZ(output) {
|
|
13260
14137
|
const entries = [];
|
|
@@ -13272,26 +14149,24 @@ function parseLsTreeLongZ(output) {
|
|
|
13272
14149
|
}
|
|
13273
14150
|
return entries;
|
|
13274
14151
|
}
|
|
13275
|
-
function listOmittedTreeFiles(repoRoot, treeSha,
|
|
14152
|
+
function listOmittedTreeFiles(repoRoot, treeSha, limits) {
|
|
13276
14153
|
const output = gitBuffer(repoRoot, ["ls-tree", "-r", "-l", "-z", treeSha]);
|
|
13277
14154
|
const omitted = [];
|
|
13278
14155
|
for (const entry of parseLsTreeLongZ(output)) {
|
|
13279
14156
|
const reason = classifyOmission(
|
|
13280
14157
|
entry.path,
|
|
13281
14158
|
entry.sizeBytes,
|
|
13282
|
-
() => readTreeBlobHead(repoRoot, entry.sha)
|
|
14159
|
+
() => readTreeBlobHead(repoRoot, entry.sha),
|
|
14160
|
+
limits,
|
|
14161
|
+
true
|
|
13283
14162
|
);
|
|
13284
14163
|
if (!reason) continue;
|
|
13285
|
-
|
|
14164
|
+
omitted.push({
|
|
13286
14165
|
path: entry.path,
|
|
13287
14166
|
sizeBytes: entry.sizeBytes,
|
|
13288
14167
|
tracked: true,
|
|
13289
14168
|
reason,
|
|
13290
14169
|
gitObjectSha: entry.sha
|
|
13291
|
-
};
|
|
13292
|
-
omitted.push(file);
|
|
13293
|
-
recordOmittedSnapshotFile(options.omittedFiles, file, {
|
|
13294
|
-
log: options.log
|
|
13295
14170
|
});
|
|
13296
14171
|
}
|
|
13297
14172
|
return omitted;
|
|
@@ -13304,10 +14179,15 @@ function removePathsFromIndex(repoRoot, env, paths) {
|
|
|
13304
14179
|
});
|
|
13305
14180
|
}
|
|
13306
14181
|
function filterOmittedFilesFromTree(repoRoot, treeSha, options = {}) {
|
|
13307
|
-
const omittedFiles = listOmittedTreeFiles(
|
|
14182
|
+
const omittedFiles = listOmittedTreeFiles(
|
|
14183
|
+
repoRoot,
|
|
14184
|
+
treeSha,
|
|
14185
|
+
options.limits ?? DEFAULT_SNAPSHOT_LIMITS
|
|
14186
|
+
);
|
|
14187
|
+
options.omittedFiles?.push(...omittedFiles);
|
|
13308
14188
|
if (omittedFiles.length === 0) return treeSha;
|
|
13309
|
-
const tmpIndex =
|
|
13310
|
-
|
|
14189
|
+
const tmpIndex = path16.join(
|
|
14190
|
+
os8.tmpdir(),
|
|
13311
14191
|
`hillclimb-filter-${Date.now()}-${process.pid}`
|
|
13312
14192
|
);
|
|
13313
14193
|
const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
|
|
@@ -13321,12 +14201,13 @@ function filterOmittedFilesFromTree(repoRoot, treeSha, options = {}) {
|
|
|
13321
14201
|
return gitWithEnv(repoRoot, ["write-tree"], env);
|
|
13322
14202
|
} finally {
|
|
13323
14203
|
try {
|
|
13324
|
-
|
|
14204
|
+
fs16.unlinkSync(tmpIndex);
|
|
13325
14205
|
} catch {
|
|
13326
14206
|
}
|
|
13327
14207
|
}
|
|
13328
14208
|
}
|
|
13329
|
-
function buildUntrackedTree(repoRoot,
|
|
14209
|
+
function buildUntrackedTree(repoRoot, options = {}) {
|
|
14210
|
+
const limits = options.limits ?? DEFAULT_SNAPSHOT_LIMITS;
|
|
13330
14211
|
const list = git(repoRoot, [
|
|
13331
14212
|
"ls-files",
|
|
13332
14213
|
"--others",
|
|
@@ -13338,15 +14219,17 @@ function buildUntrackedTree(repoRoot, omittedFiles) {
|
|
|
13338
14219
|
for (const relPath of list.split("\0")) {
|
|
13339
14220
|
if (!relPath) continue;
|
|
13340
14221
|
try {
|
|
13341
|
-
const absPath =
|
|
13342
|
-
const stat =
|
|
14222
|
+
const absPath = path16.join(repoRoot, relPath);
|
|
14223
|
+
const stat = fs16.lstatSync(absPath);
|
|
13343
14224
|
const reason = classifyOmission(
|
|
13344
14225
|
relPath,
|
|
13345
14226
|
stat.size,
|
|
13346
|
-
() => readWorkingFileHead(absPath)
|
|
14227
|
+
() => readWorkingFileHead(absPath),
|
|
14228
|
+
limits,
|
|
14229
|
+
false
|
|
13347
14230
|
);
|
|
13348
14231
|
if (reason) {
|
|
13349
|
-
|
|
14232
|
+
options.omittedFiles?.push({
|
|
13350
14233
|
path: relPath,
|
|
13351
14234
|
sizeBytes: stat.size,
|
|
13352
14235
|
tracked: false,
|
|
@@ -13359,8 +14242,8 @@ function buildUntrackedTree(repoRoot, omittedFiles) {
|
|
|
13359
14242
|
}
|
|
13360
14243
|
}
|
|
13361
14244
|
if (kept.length === 0) return null;
|
|
13362
|
-
const tmpIndex =
|
|
13363
|
-
|
|
14245
|
+
const tmpIndex = path16.join(
|
|
14246
|
+
os8.tmpdir(),
|
|
13364
14247
|
`hillclimb-untracked-${Date.now()}-${process.pid}`
|
|
13365
14248
|
);
|
|
13366
14249
|
const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
|
|
@@ -13372,25 +14255,26 @@ function buildUntrackedTree(repoRoot, omittedFiles) {
|
|
|
13372
14255
|
return gitWithEnv(repoRoot, ["write-tree"], env);
|
|
13373
14256
|
} finally {
|
|
13374
14257
|
try {
|
|
13375
|
-
|
|
14258
|
+
fs16.unlinkSync(tmpIndex);
|
|
13376
14259
|
} catch {
|
|
13377
14260
|
}
|
|
13378
14261
|
}
|
|
13379
14262
|
}
|
|
13380
14263
|
function buildSnapshotTree(repoRoot, stashSha, options = {}) {
|
|
14264
|
+
const omittedFiles = options.omittedFiles ?? [];
|
|
14265
|
+
const limits = options.limits ?? DEFAULT_SNAPSHOT_LIMITS;
|
|
13381
14266
|
const trackedTree = git(repoRoot, ["rev-parse", `${stashSha}^{tree}`]);
|
|
13382
14267
|
const filteredTrackedTree = filterOmittedFilesFromTree(
|
|
13383
14268
|
repoRoot,
|
|
13384
14269
|
trackedTree,
|
|
13385
|
-
{
|
|
13386
|
-
omittedFiles: options.omittedFiles
|
|
13387
|
-
}
|
|
14270
|
+
{ omittedFiles, limits }
|
|
13388
14271
|
);
|
|
13389
|
-
const untrackedTree = buildUntrackedTree(repoRoot,
|
|
14272
|
+
const untrackedTree = buildUntrackedTree(repoRoot, { omittedFiles, limits });
|
|
14273
|
+
if (options.log !== false) logOmittedSnapshotSummary(omittedFiles);
|
|
13390
14274
|
if (!untrackedTree || untrackedTree === EMPTY_TREE_SHA)
|
|
13391
14275
|
return filteredTrackedTree;
|
|
13392
|
-
const tmpIndex =
|
|
13393
|
-
|
|
14276
|
+
const tmpIndex = path16.join(
|
|
14277
|
+
os8.tmpdir(),
|
|
13394
14278
|
`hillclimb-index-${Date.now()}-${process.pid}`
|
|
13395
14279
|
);
|
|
13396
14280
|
const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
|
|
@@ -13417,7 +14301,7 @@ function buildSnapshotTree(repoRoot, stashSha, options = {}) {
|
|
|
13417
14301
|
return gitWithEnv(repoRoot, ["write-tree"], env);
|
|
13418
14302
|
} finally {
|
|
13419
14303
|
try {
|
|
13420
|
-
|
|
14304
|
+
fs16.unlinkSync(tmpIndex);
|
|
13421
14305
|
} catch {
|
|
13422
14306
|
}
|
|
13423
14307
|
}
|
|
@@ -13431,8 +14315,8 @@ function createBundleFromTree(repoRoot, treeSha, sessionId, label) {
|
|
|
13431
14315
|
]);
|
|
13432
14316
|
const orphanRef = `refs/hillclimb/bundle/${sessionId}`;
|
|
13433
14317
|
pinRef(repoRoot, orphanRef, orphanCommit);
|
|
13434
|
-
const tmpFile =
|
|
13435
|
-
|
|
14318
|
+
const tmpFile = path16.join(
|
|
14319
|
+
os8.tmpdir(),
|
|
13436
14320
|
// Include the pid (like the other temp files in this module) so concurrent
|
|
13437
14321
|
// git-traces workers — e.g. two sessions, or a parent + subagent — don't
|
|
13438
14322
|
// collide on the same `git bundle create` path and its `.lock`.
|
|
@@ -13440,26 +14324,24 @@ function createBundleFromTree(repoRoot, treeSha, sessionId, label) {
|
|
|
13440
14324
|
);
|
|
13441
14325
|
try {
|
|
13442
14326
|
git(repoRoot, ["bundle", "create", tmpFile, orphanRef]);
|
|
13443
|
-
return
|
|
14327
|
+
return fs16.readFileSync(tmpFile);
|
|
13444
14328
|
} finally {
|
|
13445
14329
|
try {
|
|
13446
|
-
|
|
14330
|
+
fs16.unlinkSync(tmpFile);
|
|
13447
14331
|
} catch {
|
|
13448
14332
|
}
|
|
13449
14333
|
deleteRef(repoRoot, orphanRef);
|
|
13450
14334
|
}
|
|
13451
14335
|
}
|
|
13452
|
-
function createTreeDiffPatchGz(repoRoot, fromTreeSha, toTreeSha) {
|
|
14336
|
+
function createTreeDiffPatchGz(repoRoot, fromTreeSha, toTreeSha, options = {}) {
|
|
13453
14337
|
if (fromTreeSha === toTreeSha) return null;
|
|
13454
14338
|
const filteredFromTreeSha = filterOmittedFilesFromTree(
|
|
13455
14339
|
repoRoot,
|
|
13456
14340
|
fromTreeSha,
|
|
13457
|
-
{
|
|
13458
|
-
log: false
|
|
13459
|
-
}
|
|
14341
|
+
{ limits: options.limits }
|
|
13460
14342
|
);
|
|
13461
14343
|
const filteredToTreeSha = filterOmittedFilesFromTree(repoRoot, toTreeSha, {
|
|
13462
|
-
|
|
14344
|
+
limits: options.limits
|
|
13463
14345
|
});
|
|
13464
14346
|
if (filteredFromTreeSha === filteredToTreeSha) return null;
|
|
13465
14347
|
const diff = gitBuffer(repoRoot, [
|
|
@@ -13513,7 +14395,7 @@ function parseDirtyFilesFromStatus(status) {
|
|
|
13513
14395
|
return pathPart;
|
|
13514
14396
|
}).filter(Boolean);
|
|
13515
14397
|
}
|
|
13516
|
-
function buildBaselineMetadata(repoRoot, sessionId, tool, baselineSha, cliVersion, epoch, prevHeadSha, transitionKind, startedAt, omittedFiles = []) {
|
|
14398
|
+
function buildBaselineMetadata(repoRoot, sessionId, tool, baselineSha, cliVersion, epoch, prevHeadSha, transitionKind, startedAt, omittedFiles = [], lineage) {
|
|
13517
14399
|
const headSha = safeGit(repoRoot, ["rev-parse", "HEAD"]) ?? "unknown";
|
|
13518
14400
|
const branch = safeGit(repoRoot, ["rev-parse", "--abbrev-ref", "HEAD"]) ?? null;
|
|
13519
14401
|
const remoteUrl = safeGit(repoRoot, ["config", "--get", "remote.origin.url"]) ?? null;
|
|
@@ -13541,9 +14423,10 @@ function buildBaselineMetadata(repoRoot, sessionId, tool, baselineSha, cliVersio
|
|
|
13541
14423
|
...omittedFiles.length > 0 ? { omittedFiles } : {}
|
|
13542
14424
|
},
|
|
13543
14425
|
author: { name: authorName, email: authorEmail },
|
|
13544
|
-
hostname:
|
|
14426
|
+
hostname: os8.hostname(),
|
|
13545
14427
|
cliVersion,
|
|
13546
|
-
commits
|
|
14428
|
+
commits,
|
|
14429
|
+
...lineage
|
|
13547
14430
|
};
|
|
13548
14431
|
}
|
|
13549
14432
|
function enumerateCommits(repoRoot, fromSha, toSha) {
|
|
@@ -13630,9 +14513,9 @@ function parseCommitFiles(repoRoot, sha) {
|
|
|
13630
14513
|
oldPath
|
|
13631
14514
|
});
|
|
13632
14515
|
} else {
|
|
13633
|
-
const
|
|
13634
|
-
indexByPath.set(
|
|
13635
|
-
files.push({ path:
|
|
14516
|
+
const path25 = parts[parts.length - 1];
|
|
14517
|
+
indexByPath.set(path25, files.length);
|
|
14518
|
+
files.push({ path: path25, status, additions: 0, deletions: 0 });
|
|
13636
14519
|
}
|
|
13637
14520
|
}
|
|
13638
14521
|
for (const line of numstat.split("\n")) {
|
|
@@ -13680,7 +14563,8 @@ function cleanupSessionRefs(repoRoot, sessionId) {
|
|
|
13680
14563
|
for (const prefix of [
|
|
13681
14564
|
`refs/hillclimb/baseline/${sessionId}`,
|
|
13682
14565
|
`refs/hillclimb/turns/${sessionId}`,
|
|
13683
|
-
`refs/hillclimb/bundle/${sessionId}
|
|
14566
|
+
`refs/hillclimb/bundle/${sessionId}`,
|
|
14567
|
+
`refs/hillclimb/scoped/${sessionId}`
|
|
13684
14568
|
]) {
|
|
13685
14569
|
deleteRef(repoRoot, prefix);
|
|
13686
14570
|
try {
|
|
@@ -13698,30 +14582,37 @@ function cleanupSessionRefs(repoRoot, sessionId) {
|
|
|
13698
14582
|
}
|
|
13699
14583
|
|
|
13700
14584
|
// src/git-traces/session-state.ts
|
|
13701
|
-
import
|
|
13702
|
-
import
|
|
13703
|
-
import
|
|
13704
|
-
import
|
|
14585
|
+
import crypto6 from "crypto";
|
|
14586
|
+
import fs17 from "fs";
|
|
14587
|
+
import os9 from "os";
|
|
14588
|
+
import path17 from "path";
|
|
13705
14589
|
var CURRENT_SCHEMA_VERSION3 = 3;
|
|
13706
|
-
var DEFAULT_STATE_DIR2 =
|
|
14590
|
+
var DEFAULT_STATE_DIR2 = path17.join(os9.homedir(), ".hillclimb", "git-traces");
|
|
13707
14591
|
var LOCK_RETRIES2 = 120;
|
|
13708
14592
|
var LOCK_RETRY_DELAY_MS2 = 500;
|
|
13709
|
-
var
|
|
14593
|
+
var STALE_LOCK_TTL_MS3 = 60 * 60 * 1e3;
|
|
14594
|
+
var LockContentionError = class extends Error {
|
|
14595
|
+
constructor(lockPath, options) {
|
|
14596
|
+
super(`Lock remains held: ${lockPath}`, options);
|
|
14597
|
+
this.lockPath = lockPath;
|
|
14598
|
+
this.name = "LockContentionError";
|
|
14599
|
+
}
|
|
14600
|
+
};
|
|
13710
14601
|
function stateDir3() {
|
|
13711
14602
|
return process.env.HILLCLIMB_GIT_TRACES_STATE_DIR ?? DEFAULT_STATE_DIR2;
|
|
13712
14603
|
}
|
|
13713
14604
|
function stateFileForRepo(repoRoot, tool, sessionId) {
|
|
13714
|
-
const hash =
|
|
13715
|
-
sessionId ? `${
|
|
14605
|
+
const hash = crypto6.createHash("sha256").update(
|
|
14606
|
+
sessionId ? `${path17.resolve(repoRoot)}\0${tool}\0${sessionId}` : `${path17.resolve(repoRoot)}\0${tool}`
|
|
13716
14607
|
).digest("hex").slice(0, 16);
|
|
13717
|
-
return
|
|
14608
|
+
return path17.join(stateDir3(), `${hash}.json`);
|
|
13718
14609
|
}
|
|
13719
14610
|
function lockFileForRepo(repoRoot, tool, sessionId) {
|
|
13720
14611
|
return `${stateFileForRepo(repoRoot, tool, sessionId)}.lock`;
|
|
13721
14612
|
}
|
|
13722
14613
|
async function readStateFile(file) {
|
|
13723
14614
|
try {
|
|
13724
|
-
const raw = await
|
|
14615
|
+
const raw = await fs17.promises.readFile(file, "utf-8");
|
|
13725
14616
|
const parsed = JSON.parse(raw);
|
|
13726
14617
|
if (parsed.schemaVersion !== CURRENT_SCHEMA_VERSION3) {
|
|
13727
14618
|
return null;
|
|
@@ -13731,29 +14622,38 @@ async function readStateFile(file) {
|
|
|
13731
14622
|
return null;
|
|
13732
14623
|
}
|
|
13733
14624
|
}
|
|
14625
|
+
async function readStoredStateFile(file) {
|
|
14626
|
+
const state = await readStateFile(file);
|
|
14627
|
+
if (!state) return null;
|
|
14628
|
+
try {
|
|
14629
|
+
return { state, mtimeMs: (await fs17.promises.stat(file)).mtimeMs };
|
|
14630
|
+
} catch {
|
|
14631
|
+
return null;
|
|
14632
|
+
}
|
|
14633
|
+
}
|
|
13734
14634
|
async function listScopedSessionStates(repoRoot, tool) {
|
|
13735
14635
|
let entries;
|
|
13736
14636
|
try {
|
|
13737
|
-
entries = await
|
|
14637
|
+
entries = await fs17.promises.readdir(stateDir3(), { withFileTypes: true });
|
|
13738
14638
|
} catch {
|
|
13739
14639
|
return [];
|
|
13740
14640
|
}
|
|
13741
14641
|
const states = [];
|
|
13742
14642
|
for (const entry of entries) {
|
|
13743
14643
|
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
13744
|
-
const file =
|
|
14644
|
+
const file = path17.join(stateDir3(), entry.name);
|
|
13745
14645
|
const state = await readStateFile(file);
|
|
13746
14646
|
if (!state) continue;
|
|
13747
14647
|
if (typeof state.repoRoot !== "string" || typeof state.sessionId !== "string") {
|
|
13748
14648
|
continue;
|
|
13749
14649
|
}
|
|
13750
|
-
if (
|
|
13751
|
-
if (
|
|
14650
|
+
if (path17.resolve(state.repoRoot) !== path17.resolve(repoRoot)) continue;
|
|
14651
|
+
if (path17.resolve(file) !== path17.resolve(stateFileForRepo(repoRoot, tool, state.sessionId))) {
|
|
13752
14652
|
continue;
|
|
13753
14653
|
}
|
|
13754
14654
|
let mtimeMs = 0;
|
|
13755
14655
|
try {
|
|
13756
|
-
mtimeMs = (await
|
|
14656
|
+
mtimeMs = (await fs17.promises.stat(file)).mtimeMs;
|
|
13757
14657
|
} catch {
|
|
13758
14658
|
continue;
|
|
13759
14659
|
}
|
|
@@ -13764,26 +14664,26 @@ async function listScopedSessionStates(repoRoot, tool) {
|
|
|
13764
14664
|
async function listSessionStatesForSession(tool, sessionId) {
|
|
13765
14665
|
let entries;
|
|
13766
14666
|
try {
|
|
13767
|
-
entries = await
|
|
14667
|
+
entries = await fs17.promises.readdir(stateDir3(), { withFileTypes: true });
|
|
13768
14668
|
} catch {
|
|
13769
14669
|
return [];
|
|
13770
14670
|
}
|
|
13771
14671
|
const states = [];
|
|
13772
14672
|
for (const entry of entries) {
|
|
13773
14673
|
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
13774
|
-
const file =
|
|
14674
|
+
const file = path17.join(stateDir3(), entry.name);
|
|
13775
14675
|
const state = await readStateFile(file);
|
|
13776
14676
|
if (!state) continue;
|
|
13777
14677
|
if (typeof state.repoRoot !== "string" || typeof state.sessionId !== "string") {
|
|
13778
14678
|
continue;
|
|
13779
14679
|
}
|
|
13780
14680
|
if (state.sessionId !== sessionId) continue;
|
|
13781
|
-
if (
|
|
14681
|
+
if (path17.resolve(file) !== path17.resolve(stateFileForRepo(state.repoRoot, tool, sessionId))) {
|
|
13782
14682
|
continue;
|
|
13783
14683
|
}
|
|
13784
14684
|
let mtimeMs = 0;
|
|
13785
14685
|
try {
|
|
13786
|
-
mtimeMs = (await
|
|
14686
|
+
mtimeMs = (await fs17.promises.stat(file)).mtimeMs;
|
|
13787
14687
|
} catch {
|
|
13788
14688
|
continue;
|
|
13789
14689
|
}
|
|
@@ -13791,7 +14691,7 @@ async function listSessionStatesForSession(tool, sessionId) {
|
|
|
13791
14691
|
}
|
|
13792
14692
|
return states;
|
|
13793
14693
|
}
|
|
13794
|
-
async function
|
|
14694
|
+
async function readSessionState2(repoRoot, tool, sessionId) {
|
|
13795
14695
|
if (sessionId) {
|
|
13796
14696
|
const scoped = await readStateFile(
|
|
13797
14697
|
stateFileForRepo(repoRoot, tool, sessionId)
|
|
@@ -13802,108 +14702,120 @@ async function readSessionState(repoRoot, tool, sessionId) {
|
|
|
13802
14702
|
}
|
|
13803
14703
|
return readStateFile(stateFileForRepo(repoRoot, tool));
|
|
13804
14704
|
}
|
|
13805
|
-
async function
|
|
14705
|
+
async function readLegacySessionState(repoRoot, tool, sessionId) {
|
|
14706
|
+
const legacy = await readStateFile(stateFileForRepo(repoRoot, tool));
|
|
14707
|
+
return legacy?.sessionId === sessionId ? legacy : null;
|
|
14708
|
+
}
|
|
14709
|
+
async function readSessionStateCandidates(repoRoot, tool, sessionId) {
|
|
14710
|
+
const [scoped, legacyCandidate] = await Promise.all([
|
|
14711
|
+
readStoredStateFile(stateFileForRepo(repoRoot, tool, sessionId)),
|
|
14712
|
+
readStoredStateFile(stateFileForRepo(repoRoot, tool))
|
|
14713
|
+
]);
|
|
14714
|
+
return {
|
|
14715
|
+
scoped,
|
|
14716
|
+
legacy: legacyCandidate?.state.sessionId === sessionId ? legacyCandidate : null
|
|
14717
|
+
};
|
|
14718
|
+
}
|
|
14719
|
+
async function writeSessionState2(state, tool) {
|
|
13806
14720
|
const file = stateFileForRepo(state.repoRoot, tool, state.sessionId);
|
|
13807
|
-
await
|
|
14721
|
+
await writeStateFile(file, state);
|
|
14722
|
+
}
|
|
14723
|
+
async function writeLegacySessionState(state, tool) {
|
|
14724
|
+
await writeStateFile(stateFileForRepo(state.repoRoot, tool), state);
|
|
14725
|
+
}
|
|
14726
|
+
async function writeStateFile(file, state) {
|
|
14727
|
+
await fs17.promises.mkdir(stateDir3(), { recursive: true, mode: 448 });
|
|
13808
14728
|
const tmp = `${file}.tmp`;
|
|
13809
|
-
await
|
|
14729
|
+
await fs17.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
|
|
13810
14730
|
mode: 384
|
|
13811
14731
|
});
|
|
13812
|
-
await
|
|
13813
|
-
const legacyFile = stateFileForRepo(state.repoRoot, tool);
|
|
13814
|
-
const legacy = await readStateFile(legacyFile);
|
|
13815
|
-
if (legacy?.sessionId === state.sessionId) {
|
|
13816
|
-
await deleteStateFile(legacyFile);
|
|
13817
|
-
}
|
|
14732
|
+
await fs17.promises.rename(tmp, file);
|
|
13818
14733
|
}
|
|
13819
14734
|
async function deleteStateFile(file) {
|
|
13820
14735
|
try {
|
|
13821
|
-
await
|
|
14736
|
+
await fs17.promises.unlink(file);
|
|
13822
14737
|
} catch {
|
|
13823
14738
|
}
|
|
13824
14739
|
}
|
|
13825
|
-
async function
|
|
14740
|
+
async function deleteSessionState2(repoRoot, tool, sessionId) {
|
|
13826
14741
|
if (sessionId) {
|
|
13827
14742
|
await deleteStateFile(stateFileForRepo(repoRoot, tool, sessionId));
|
|
13828
|
-
const legacyFile = stateFileForRepo(repoRoot, tool);
|
|
13829
|
-
const legacy = await readStateFile(legacyFile);
|
|
13830
|
-
if (legacy?.sessionId === sessionId) {
|
|
13831
|
-
await deleteStateFile(legacyFile);
|
|
13832
|
-
}
|
|
13833
14743
|
return;
|
|
13834
14744
|
}
|
|
13835
14745
|
await deleteStateFile(stateFileForRepo(repoRoot, tool));
|
|
13836
14746
|
}
|
|
14747
|
+
async function deleteLegacySessionState(repoRoot, tool, sessionId) {
|
|
14748
|
+
const legacyFile = stateFileForRepo(repoRoot, tool);
|
|
14749
|
+
const legacy = await readStateFile(legacyFile);
|
|
14750
|
+
if (legacy?.sessionId === sessionId) await deleteStateFile(legacyFile);
|
|
14751
|
+
}
|
|
13837
14752
|
async function acquireLock3(repoRoot, tool, sessionId, retries = LOCK_RETRIES2, delayMs = LOCK_RETRY_DELAY_MS2) {
|
|
13838
14753
|
const lockPath = lockFileForRepo(repoRoot, tool, sessionId);
|
|
13839
|
-
await
|
|
14754
|
+
await fs17.promises.mkdir(stateDir3(), { recursive: true, mode: 448 });
|
|
13840
14755
|
for (let i = 0; i < retries; i++) {
|
|
13841
14756
|
try {
|
|
13842
|
-
const fd = await
|
|
14757
|
+
const fd = await fs17.promises.open(
|
|
13843
14758
|
lockPath,
|
|
13844
|
-
|
|
14759
|
+
fs17.constants.O_CREAT | fs17.constants.O_EXCL | fs17.constants.O_WRONLY
|
|
13845
14760
|
);
|
|
13846
14761
|
await fd.write(String(process.pid));
|
|
13847
14762
|
await fd.close();
|
|
13848
14763
|
return fd.fd;
|
|
13849
14764
|
} catch (err) {
|
|
13850
|
-
if (err.code
|
|
14765
|
+
if (err.code !== "EEXIST") throw err;
|
|
14766
|
+
let regularLockFile = false;
|
|
14767
|
+
try {
|
|
14768
|
+
regularLockFile = (await fs17.promises.lstat(lockPath)).isFile();
|
|
14769
|
+
} catch (statErr) {
|
|
14770
|
+
if (statErr.code === "ENOENT") {
|
|
14771
|
+
i--;
|
|
14772
|
+
continue;
|
|
14773
|
+
}
|
|
14774
|
+
}
|
|
14775
|
+
if (!regularLockFile) throw err;
|
|
14776
|
+
if (i < retries - 1) {
|
|
14777
|
+
if (await reapLockIfStale(lockPath, {
|
|
14778
|
+
maxAgeMs: STALE_LOCK_TTL_MS3
|
|
14779
|
+
})) {
|
|
14780
|
+
continue;
|
|
14781
|
+
}
|
|
13851
14782
|
await new Promise((r) => setTimeout(r, delayMs));
|
|
13852
14783
|
continue;
|
|
13853
14784
|
}
|
|
13854
|
-
throw err;
|
|
14785
|
+
throw new LockContentionError(lockPath, { cause: err });
|
|
13855
14786
|
}
|
|
13856
14787
|
}
|
|
13857
14788
|
throw new Error(`Failed to acquire lock after ${retries} retries`);
|
|
13858
14789
|
}
|
|
13859
14790
|
async function releaseLock3(repoRoot, tool, sessionId) {
|
|
13860
14791
|
try {
|
|
13861
|
-
await
|
|
14792
|
+
await fs17.promises.unlink(lockFileForRepo(repoRoot, tool, sessionId));
|
|
13862
14793
|
} catch {
|
|
13863
14794
|
}
|
|
13864
14795
|
}
|
|
13865
|
-
function
|
|
13866
|
-
try {
|
|
13867
|
-
process.kill(pid, 0);
|
|
13868
|
-
return true;
|
|
13869
|
-
} catch (err) {
|
|
13870
|
-
return err.code === "EPERM";
|
|
13871
|
-
}
|
|
13872
|
-
}
|
|
13873
|
-
async function sweepStaleLockFiles(ttlMs = STALE_LOCK_TTL_MS2, now = Date.now()) {
|
|
14796
|
+
async function sweepStaleLockFiles(ttlMs = STALE_LOCK_TTL_MS3, now = Date.now()) {
|
|
13874
14797
|
let entries;
|
|
13875
14798
|
try {
|
|
13876
|
-
entries = await
|
|
14799
|
+
entries = await fs17.promises.readdir(stateDir3(), { withFileTypes: true });
|
|
13877
14800
|
} catch {
|
|
13878
14801
|
return 0;
|
|
13879
14802
|
}
|
|
13880
14803
|
let removed = 0;
|
|
13881
14804
|
for (const entry of entries) {
|
|
13882
14805
|
if (!entry.isFile() || !entry.name.endsWith(".lock")) continue;
|
|
13883
|
-
const file =
|
|
13884
|
-
|
|
13885
|
-
|
|
13886
|
-
|
|
13887
|
-
|
|
13888
|
-
|
|
13889
|
-
if (havePid) {
|
|
13890
|
-
reap = !isProcessAlive2(pid);
|
|
13891
|
-
} else {
|
|
13892
|
-
const st = await fs15.promises.stat(file);
|
|
13893
|
-
reap = now - st.mtimeMs > ttlMs;
|
|
13894
|
-
}
|
|
13895
|
-
if (reap) {
|
|
13896
|
-
await fs15.promises.unlink(file);
|
|
13897
|
-
removed++;
|
|
13898
|
-
}
|
|
13899
|
-
} catch {
|
|
14806
|
+
const file = path17.join(stateDir3(), entry.name);
|
|
14807
|
+
if (await reapLockIfStale(file, {
|
|
14808
|
+
maxAgeMs: ttlMs,
|
|
14809
|
+
now
|
|
14810
|
+
})) {
|
|
14811
|
+
removed++;
|
|
13900
14812
|
}
|
|
13901
14813
|
}
|
|
13902
14814
|
return removed;
|
|
13903
14815
|
}
|
|
13904
14816
|
|
|
13905
14817
|
// src/git-traces/handlers.ts
|
|
13906
|
-
var CLI_VERSION2 = "0.
|
|
14818
|
+
var CLI_VERSION2 = "0.8.0";
|
|
13907
14819
|
var GIT_TRACES_SLUG = "git-traces";
|
|
13908
14820
|
var MAX_UPLOAD_BYTES = 500 * 1024 * 1024;
|
|
13909
14821
|
var STALE_SCOPED_STATE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -13920,12 +14832,12 @@ var TOOL_LABELS = {
|
|
|
13920
14832
|
async function loadConfiguredRepos() {
|
|
13921
14833
|
const file = await loadProjects();
|
|
13922
14834
|
return Object.entries(file.projects).map(([repoRoot, config]) => ({
|
|
13923
|
-
repoRoot:
|
|
14835
|
+
repoRoot: path18.resolve(repoRoot),
|
|
13924
14836
|
config
|
|
13925
14837
|
})).sort((a, b) => a.repoRoot.localeCompare(b.repoRoot));
|
|
13926
14838
|
}
|
|
13927
14839
|
function repoLabel(repoRoot) {
|
|
13928
|
-
return
|
|
14840
|
+
return path18.basename(repoRoot) || repoRoot;
|
|
13929
14841
|
}
|
|
13930
14842
|
function resolveCwd2(payload) {
|
|
13931
14843
|
return resolveHookCwd(payload);
|
|
@@ -13933,6 +14845,11 @@ function resolveCwd2(payload) {
|
|
|
13933
14845
|
function resolveSessionId2(payload) {
|
|
13934
14846
|
return resolveHookSessionId(payload);
|
|
13935
14847
|
}
|
|
14848
|
+
async function detectCodexLineageForHook(payload, tool, sessionId) {
|
|
14849
|
+
if (tool !== "codex" || !sessionId) return { conclusive: false };
|
|
14850
|
+
const transcriptPath = payload.transcript_path ?? await findCodexRolloutPath(sessionId);
|
|
14851
|
+
return transcriptPath ? detectCodexLineage(transcriptPath) : { conclusive: false };
|
|
14852
|
+
}
|
|
13936
14853
|
function epochPrefix2(epoch) {
|
|
13937
14854
|
return `epoch-${String(epoch).padStart(3, "0")}`;
|
|
13938
14855
|
}
|
|
@@ -13980,18 +14897,48 @@ async function uploadFile(client, contributionId, filename, mimeType, buffer) {
|
|
|
13980
14897
|
);
|
|
13981
14898
|
return true;
|
|
13982
14899
|
}
|
|
14900
|
+
function omissionKey(file) {
|
|
14901
|
+
return `${file.tracked ? "tracked" : "untracked"}:${file.path}`;
|
|
14902
|
+
}
|
|
14903
|
+
function omissionKeys(files) {
|
|
14904
|
+
return [...new Set(files.map(omissionKey))].sort();
|
|
14905
|
+
}
|
|
14906
|
+
function omissionKeysFromMetadata(metadata) {
|
|
14907
|
+
const git2 = metadata.git;
|
|
14908
|
+
if (!git2 || typeof git2 !== "object") return [];
|
|
14909
|
+
const omitted = git2.omittedFiles;
|
|
14910
|
+
if (!Array.isArray(omitted)) return [];
|
|
14911
|
+
const keys = omitted.map(
|
|
14912
|
+
(file) => file && typeof file === "object" ? metadataOmissionKey(file) : void 0
|
|
14913
|
+
).filter((key) => key !== void 0);
|
|
14914
|
+
return [...new Set(keys)].sort();
|
|
14915
|
+
}
|
|
14916
|
+
function metadataOmissionKey(file) {
|
|
14917
|
+
const { path: filePath, tracked } = file;
|
|
14918
|
+
if (typeof filePath !== "string") return void 0;
|
|
14919
|
+
return omissionKey({ path: filePath, tracked: tracked !== false });
|
|
14920
|
+
}
|
|
13983
14921
|
function canUploadEpochBaselineArtifacts(epoch, artifacts) {
|
|
13984
14922
|
const prefix = epochPrefix2(epoch);
|
|
13985
14923
|
return canUploadFile(`${prefix}-baseline.bundle`, artifacts.bundleBuffer) && canUploadFile(`${prefix}-baseline.json`, artifacts.metadataBuffer);
|
|
13986
14924
|
}
|
|
13987
|
-
function pinEpochBaseline(repoRoot, sessionId, epoch) {
|
|
14925
|
+
function pinEpochBaseline(repoRoot, sessionId, epoch, pinCanonicalRefs = true) {
|
|
13988
14926
|
const baselineSha = captureBaselineSha(repoRoot);
|
|
13989
14927
|
const baselineRefPrefix = `refs/hillclimb/baseline/${sessionId}/${epochPrefix2(epoch)}`;
|
|
13990
|
-
|
|
13991
|
-
|
|
14928
|
+
const scopedRefPrefix = `refs/hillclimb/scoped/${sessionId}/baseline/${epochPrefix2(epoch)}`;
|
|
14929
|
+
if (pinCanonicalRefs) {
|
|
14930
|
+
deleteRef(repoRoot, baselineRefPrefix);
|
|
14931
|
+
pinRef(repoRoot, `${baselineRefPrefix}/tracked`, baselineSha);
|
|
14932
|
+
}
|
|
14933
|
+
pinRef(repoRoot, `${scopedRefPrefix}/tracked`, baselineSha);
|
|
13992
14934
|
return { baselineSha, headSha: captureHeadSha(repoRoot) };
|
|
13993
14935
|
}
|
|
13994
|
-
function
|
|
14936
|
+
function pinScopedTreeRef(repoRoot, refName, treeSha, message) {
|
|
14937
|
+
const commit = execGit(repoRoot, ["commit-tree", treeSha, "-m", message]);
|
|
14938
|
+
pinRef(repoRoot, refName, commit);
|
|
14939
|
+
return commit;
|
|
14940
|
+
}
|
|
14941
|
+
function pinFrozenBaselineTree(repoRoot, sessionId, epoch, baselineTreeSha, pinCanonicalRefs = true) {
|
|
13995
14942
|
const prefix = epochPrefix2(epoch);
|
|
13996
14943
|
const commit = execGit(repoRoot, [
|
|
13997
14944
|
"commit-tree",
|
|
@@ -13999,12 +14946,31 @@ function pinFrozenBaselineTree(repoRoot, sessionId, epoch, baselineTreeSha) {
|
|
|
13999
14946
|
"-m",
|
|
14000
14947
|
`frozen baseline ${prefix} for session ${sessionId}`
|
|
14001
14948
|
]);
|
|
14949
|
+
if (pinCanonicalRefs) {
|
|
14950
|
+
pinRef(
|
|
14951
|
+
repoRoot,
|
|
14952
|
+
`refs/hillclimb/baseline/${sessionId}/${prefix}/tree`,
|
|
14953
|
+
commit
|
|
14954
|
+
);
|
|
14955
|
+
}
|
|
14002
14956
|
pinRef(
|
|
14003
14957
|
repoRoot,
|
|
14004
|
-
`refs/hillclimb/
|
|
14958
|
+
`refs/hillclimb/scoped/${sessionId}/baseline/${prefix}/tree`,
|
|
14005
14959
|
commit
|
|
14006
14960
|
);
|
|
14007
14961
|
}
|
|
14962
|
+
function pinScopedTurnSnapshot(repoRoot, sessionId, epoch, turnCount, snapshotSha, treeSha) {
|
|
14963
|
+
const prefix = epochPrefix2(epoch);
|
|
14964
|
+
const turnLabel = turnSuffix2(turnCount);
|
|
14965
|
+
const refPrefix = `refs/hillclimb/scoped/${sessionId}/turns/${prefix}/${turnLabel}`;
|
|
14966
|
+
pinRef(repoRoot, `${refPrefix}/tracked`, snapshotSha);
|
|
14967
|
+
pinScopedTreeRef(
|
|
14968
|
+
repoRoot,
|
|
14969
|
+
`${refPrefix}/tree`,
|
|
14970
|
+
treeSha,
|
|
14971
|
+
`protected ${turnLabel} ${prefix} for session ${sessionId}`
|
|
14972
|
+
);
|
|
14973
|
+
}
|
|
14008
14974
|
function freezeEpochBaseline(params) {
|
|
14009
14975
|
const {
|
|
14010
14976
|
repoRoot,
|
|
@@ -14013,18 +14979,23 @@ function freezeEpochBaseline(params) {
|
|
|
14013
14979
|
epoch,
|
|
14014
14980
|
prevHeadSha,
|
|
14015
14981
|
transitionKind,
|
|
14016
|
-
startedAt
|
|
14982
|
+
startedAt,
|
|
14983
|
+
limits,
|
|
14984
|
+
lineage,
|
|
14985
|
+
pinCanonicalRefs = true
|
|
14017
14986
|
} = params;
|
|
14018
14987
|
const prefix = epochPrefix2(epoch);
|
|
14019
14988
|
try {
|
|
14020
14989
|
const { baselineSha, headSha } = pinEpochBaseline(
|
|
14021
14990
|
repoRoot,
|
|
14022
14991
|
sessionId,
|
|
14023
|
-
epoch
|
|
14992
|
+
epoch,
|
|
14993
|
+
pinCanonicalRefs
|
|
14024
14994
|
);
|
|
14025
14995
|
const omittedFiles = [];
|
|
14026
14996
|
const baselineTreeSha = buildSnapshotTree(repoRoot, baselineSha, {
|
|
14027
|
-
omittedFiles
|
|
14997
|
+
omittedFiles,
|
|
14998
|
+
limits
|
|
14028
14999
|
});
|
|
14029
15000
|
const baselineMetadata = buildBaselineMetadata(
|
|
14030
15001
|
repoRoot,
|
|
@@ -14036,10 +15007,23 @@ function freezeEpochBaseline(params) {
|
|
|
14036
15007
|
prevHeadSha,
|
|
14037
15008
|
transitionKind,
|
|
14038
15009
|
startedAt,
|
|
14039
|
-
omittedFiles
|
|
15010
|
+
omittedFiles,
|
|
15011
|
+
lineage
|
|
15012
|
+
);
|
|
15013
|
+
pinFrozenBaselineTree(
|
|
15014
|
+
repoRoot,
|
|
15015
|
+
sessionId,
|
|
15016
|
+
epoch,
|
|
15017
|
+
baselineTreeSha,
|
|
15018
|
+
pinCanonicalRefs
|
|
14040
15019
|
);
|
|
14041
|
-
|
|
14042
|
-
|
|
15020
|
+
return {
|
|
15021
|
+
baselineSha,
|
|
15022
|
+
baselineTreeSha,
|
|
15023
|
+
baselineMetadata,
|
|
15024
|
+
headSha,
|
|
15025
|
+
omittedFiles
|
|
15026
|
+
};
|
|
14043
15027
|
} catch (err) {
|
|
14044
15028
|
appendLog(
|
|
14045
15029
|
"error",
|
|
@@ -14061,7 +15045,12 @@ function buildFrozenEpochBaselineArtifacts(params) {
|
|
|
14061
15045
|
const metadataBuffer = Buffer.from(
|
|
14062
15046
|
JSON.stringify(baselineMetadata, null, 2)
|
|
14063
15047
|
);
|
|
14064
|
-
return {
|
|
15048
|
+
return {
|
|
15049
|
+
baselineTreeSha,
|
|
15050
|
+
metadataBuffer,
|
|
15051
|
+
bundleBuffer,
|
|
15052
|
+
omittedKeys: omissionKeysFromMetadata(baselineMetadata)
|
|
15053
|
+
};
|
|
14065
15054
|
} catch (err) {
|
|
14066
15055
|
appendLog(
|
|
14067
15056
|
"error",
|
|
@@ -14079,13 +15068,16 @@ function buildEpochBaselineArtifacts(params) {
|
|
|
14079
15068
|
baselineSha,
|
|
14080
15069
|
prevHeadSha,
|
|
14081
15070
|
transitionKind,
|
|
14082
|
-
startedAt
|
|
15071
|
+
startedAt,
|
|
15072
|
+
limits,
|
|
15073
|
+
lineage
|
|
14083
15074
|
} = params;
|
|
14084
15075
|
const prefix = epochPrefix2(epoch);
|
|
14085
15076
|
try {
|
|
14086
15077
|
const omittedFiles = [];
|
|
14087
15078
|
const baselineTreeSha = buildSnapshotTree(repoRoot, baselineSha, {
|
|
14088
|
-
omittedFiles
|
|
15079
|
+
omittedFiles,
|
|
15080
|
+
limits
|
|
14089
15081
|
});
|
|
14090
15082
|
const metadata = buildBaselineMetadata(
|
|
14091
15083
|
repoRoot,
|
|
@@ -14097,7 +15089,8 @@ function buildEpochBaselineArtifacts(params) {
|
|
|
14097
15089
|
prevHeadSha,
|
|
14098
15090
|
transitionKind,
|
|
14099
15091
|
startedAt,
|
|
14100
|
-
omittedFiles
|
|
15092
|
+
omittedFiles,
|
|
15093
|
+
lineage
|
|
14101
15094
|
);
|
|
14102
15095
|
return buildFrozenEpochBaselineArtifacts({
|
|
14103
15096
|
repoRoot,
|
|
@@ -14135,7 +15128,16 @@ async function uploadEpochBaselineArtifacts(params) {
|
|
|
14135
15128
|
);
|
|
14136
15129
|
}
|
|
14137
15130
|
async function createGitTracesContribution(params) {
|
|
14138
|
-
const {
|
|
15131
|
+
const {
|
|
15132
|
+
client,
|
|
15133
|
+
config,
|
|
15134
|
+
repoRoot,
|
|
15135
|
+
state,
|
|
15136
|
+
tool,
|
|
15137
|
+
persistState,
|
|
15138
|
+
now,
|
|
15139
|
+
artifacts
|
|
15140
|
+
} = params;
|
|
14139
15141
|
const toolLabel2 = TOOL_LABELS[tool] ?? "Claude";
|
|
14140
15142
|
const epochSeconds = formatEpochSeconds3(now);
|
|
14141
15143
|
const shortId = state.sessionId.slice(0, 12);
|
|
@@ -14153,7 +15155,7 @@ Uploaded: ${now.toISOString()}`
|
|
|
14153
15155
|
contributionId = contribution.id;
|
|
14154
15156
|
state.contributionId = contributionId;
|
|
14155
15157
|
state.baselineUploaded = false;
|
|
14156
|
-
await
|
|
15158
|
+
await persistState(state);
|
|
14157
15159
|
}
|
|
14158
15160
|
const uploaded = await uploadEpochBaselineArtifacts({
|
|
14159
15161
|
client,
|
|
@@ -14172,6 +15174,12 @@ Uploaded: ${now.toISOString()}`
|
|
|
14172
15174
|
async function uploadEpochBaseline(params) {
|
|
14173
15175
|
const artifacts = buildEpochBaselineArtifacts(params);
|
|
14174
15176
|
if (!artifacts) return null;
|
|
15177
|
+
pinScopedTreeRef(
|
|
15178
|
+
params.repoRoot,
|
|
15179
|
+
`refs/hillclimb/scoped/${params.sessionId}/baseline/${epochPrefix2(params.epoch)}/tree`,
|
|
15180
|
+
artifacts.baselineTreeSha,
|
|
15181
|
+
`protected baseline ${epochPrefix2(params.epoch)} for session ${params.sessionId}`
|
|
15182
|
+
);
|
|
14175
15183
|
if (!canUploadEpochBaselineArtifacts(params.epoch, artifacts)) return null;
|
|
14176
15184
|
const uploaded = await uploadEpochBaselineArtifacts({
|
|
14177
15185
|
client: params.client,
|
|
@@ -14180,7 +15188,10 @@ async function uploadEpochBaseline(params) {
|
|
|
14180
15188
|
artifacts
|
|
14181
15189
|
});
|
|
14182
15190
|
if (!uploaded) return null;
|
|
14183
|
-
return {
|
|
15191
|
+
return {
|
|
15192
|
+
baselineTreeSha: artifacts.baselineTreeSha,
|
|
15193
|
+
omittedKeys: artifacts.omittedKeys
|
|
15194
|
+
};
|
|
14184
15195
|
}
|
|
14185
15196
|
async function openEpoch(params) {
|
|
14186
15197
|
const { baselineSha, headSha } = pinEpochBaseline(
|
|
@@ -14190,10 +15201,28 @@ async function openEpoch(params) {
|
|
|
14190
15201
|
);
|
|
14191
15202
|
const uploaded = await uploadEpochBaseline({ ...params, baselineSha });
|
|
14192
15203
|
if (!uploaded) return null;
|
|
14193
|
-
return {
|
|
15204
|
+
return {
|
|
15205
|
+
baselineSha,
|
|
15206
|
+
baselineTreeSha: uploaded.baselineTreeSha,
|
|
15207
|
+
headSha,
|
|
15208
|
+
omittedKeys: uploaded.omittedKeys
|
|
15209
|
+
};
|
|
15210
|
+
}
|
|
15211
|
+
function isPositiveInteger(value) {
|
|
15212
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
14194
15213
|
}
|
|
14195
|
-
|
|
15214
|
+
function sessionSnapshotLimits(state) {
|
|
15215
|
+
const limits = state.snapshotLimits;
|
|
15216
|
+
if (!limits || typeof limits !== "object") return LEGACY_SNAPSHOT_LIMITS;
|
|
15217
|
+
const candidate = limits;
|
|
15218
|
+
if (!isPositiveInteger(candidate.maxTrackedFileBytes) || !isPositiveInteger(candidate.maxUntrackedFileBytes) || !isPositiveInteger(candidate.maxBinaryFileBytes)) {
|
|
15219
|
+
return LEGACY_SNAPSHOT_LIMITS;
|
|
15220
|
+
}
|
|
15221
|
+
return limits;
|
|
15222
|
+
}
|
|
15223
|
+
async function initializeSession(repoRoot, tool, sessionId, limits, lineage, legacyBoundaryPending = false) {
|
|
14196
15224
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
15225
|
+
const effectiveLimits = limits ?? resolveSnapshotLimits(void 0);
|
|
14197
15226
|
const frozen = freezeEpochBaseline({
|
|
14198
15227
|
repoRoot,
|
|
14199
15228
|
sessionId,
|
|
@@ -14201,7 +15230,10 @@ async function initializeSession(repoRoot, tool, sessionId) {
|
|
|
14201
15230
|
epoch: 1,
|
|
14202
15231
|
prevHeadSha: null,
|
|
14203
15232
|
transitionKind: "initial",
|
|
14204
|
-
startedAt
|
|
15233
|
+
startedAt,
|
|
15234
|
+
limits: effectiveLimits,
|
|
15235
|
+
lineage,
|
|
15236
|
+
pinCanonicalRefs: !legacyBoundaryPending
|
|
14205
15237
|
});
|
|
14206
15238
|
if (!frozen) return null;
|
|
14207
15239
|
const state = {
|
|
@@ -14216,17 +15248,67 @@ async function initializeSession(repoRoot, tool, sessionId) {
|
|
|
14216
15248
|
lastSnapshotTreeSha: frozen.baselineTreeSha,
|
|
14217
15249
|
startedAt,
|
|
14218
15250
|
epoch: 1,
|
|
15251
|
+
nextEpoch: 2,
|
|
14219
15252
|
turnCount: 0,
|
|
14220
15253
|
headSha: frozen.headSha,
|
|
14221
|
-
repoRoot
|
|
15254
|
+
repoRoot,
|
|
15255
|
+
snapshotLimits: effectiveLimits,
|
|
15256
|
+
omittedKeys: omissionKeys(frozen.omittedFiles),
|
|
15257
|
+
codexLineage: lineage,
|
|
15258
|
+
codexLineageChecked: tool === "codex" ? false : void 0,
|
|
15259
|
+
legacyBoundaryPending: legacyBoundaryPending || void 0
|
|
14222
15260
|
};
|
|
14223
|
-
await
|
|
15261
|
+
await writeSessionState2(state, tool);
|
|
14224
15262
|
appendLog(
|
|
14225
15263
|
"info",
|
|
14226
15264
|
`git-traces: session pending (repo=${repoRoot}, baseline=${frozen.baselineSha.slice(0, 8)}, awaiting first turn)`
|
|
14227
15265
|
);
|
|
14228
15266
|
return state;
|
|
14229
15267
|
}
|
|
15268
|
+
function pinScopedStateRefs(repoRoot, state) {
|
|
15269
|
+
const prefix = epochPrefix2(state.epoch);
|
|
15270
|
+
const baselinePrefix = `refs/hillclimb/scoped/${state.sessionId}/baseline/${prefix}`;
|
|
15271
|
+
pinRef(repoRoot, `${baselinePrefix}/tracked`, state.baselineSha);
|
|
15272
|
+
if (state.baselineTreeSha) {
|
|
15273
|
+
pinScopedTreeRef(
|
|
15274
|
+
repoRoot,
|
|
15275
|
+
`${baselinePrefix}/tree`,
|
|
15276
|
+
state.baselineTreeSha,
|
|
15277
|
+
`protected baseline ${prefix} for session ${state.sessionId}`
|
|
15278
|
+
);
|
|
15279
|
+
}
|
|
15280
|
+
if (state.turnCount > 0 && state.lastSnapshotTreeSha) {
|
|
15281
|
+
pinScopedTurnSnapshot(
|
|
15282
|
+
repoRoot,
|
|
15283
|
+
state.sessionId,
|
|
15284
|
+
state.epoch,
|
|
15285
|
+
state.turnCount,
|
|
15286
|
+
state.lastSnapshotSha,
|
|
15287
|
+
state.lastSnapshotTreeSha
|
|
15288
|
+
);
|
|
15289
|
+
}
|
|
15290
|
+
}
|
|
15291
|
+
function pinCanonicalStateRefs(repoRoot, state) {
|
|
15292
|
+
pinScopedStateRefs(repoRoot, state);
|
|
15293
|
+
const prefix = epochPrefix2(state.epoch);
|
|
15294
|
+
const baselinePrefix = `refs/hillclimb/baseline/${state.sessionId}/${prefix}`;
|
|
15295
|
+
pinRef(repoRoot, `${baselinePrefix}/tracked`, state.baselineSha);
|
|
15296
|
+
if (state.baselineTreeSha) {
|
|
15297
|
+
pinScopedTreeRef(
|
|
15298
|
+
repoRoot,
|
|
15299
|
+
`${baselinePrefix}/tree`,
|
|
15300
|
+
state.baselineTreeSha,
|
|
15301
|
+
`canonical baseline ${prefix} for session ${state.sessionId}`
|
|
15302
|
+
);
|
|
15303
|
+
}
|
|
15304
|
+
if (state.turnCount > 0) {
|
|
15305
|
+
pinRef(
|
|
15306
|
+
repoRoot,
|
|
15307
|
+
`refs/hillclimb/turns/${state.sessionId}/${prefix}/${turnSuffix2(state.turnCount)}`,
|
|
15308
|
+
state.lastSnapshotSha
|
|
15309
|
+
);
|
|
15310
|
+
}
|
|
15311
|
+
}
|
|
14230
15312
|
async function cleanupStaleScopedSessionStates(repoRoot, tool, currentSessionId, options = {}) {
|
|
14231
15313
|
const nowMs = options.nowMs ?? Date.now();
|
|
14232
15314
|
const ttlMs = options.ttlMs ?? STALE_SCOPED_STATE_TTL_MS;
|
|
@@ -14237,6 +15319,13 @@ async function cleanupStaleScopedSessionStates(repoRoot, tool, currentSessionId,
|
|
|
14237
15319
|
const startedAtMs = Date.parse(state.startedAt);
|
|
14238
15320
|
const ageBaseMs = Number.isNaN(startedAtMs) ? mtimeMs : startedAtMs;
|
|
14239
15321
|
if (nowMs - ageBaseMs <= ttlMs) continue;
|
|
15322
|
+
if (await readLegacySessionState(repoRoot, tool, state.sessionId)) {
|
|
15323
|
+
appendLog(
|
|
15324
|
+
"info",
|
|
15325
|
+
`git-traces: preserving stale scoped session ${state.sessionId} while matching legacy state exists`
|
|
15326
|
+
);
|
|
15327
|
+
continue;
|
|
15328
|
+
}
|
|
14240
15329
|
try {
|
|
14241
15330
|
await acquireLock3(repoRoot, tool, state.sessionId, 1, 0);
|
|
14242
15331
|
} catch {
|
|
@@ -14252,7 +15341,7 @@ async function cleanupStaleScopedSessionStates(repoRoot, tool, currentSessionId,
|
|
|
14252
15341
|
`git-traces: cleaning up stale scoped session ${state.sessionId}`
|
|
14253
15342
|
);
|
|
14254
15343
|
cleanupSessionRefs(repoRoot, state.sessionId);
|
|
14255
|
-
await
|
|
15344
|
+
await deleteSessionState2(repoRoot, tool, state.sessionId);
|
|
14256
15345
|
removed++;
|
|
14257
15346
|
} finally {
|
|
14258
15347
|
await releaseLock3(repoRoot, tool, state.sessionId);
|
|
@@ -14260,7 +15349,26 @@ async function cleanupStaleScopedSessionStates(repoRoot, tool, currentSessionId,
|
|
|
14260
15349
|
}
|
|
14261
15350
|
return removed;
|
|
14262
15351
|
}
|
|
14263
|
-
async function
|
|
15352
|
+
async function acquireBoundaryLegacyLock(repoRoot, tool, timing) {
|
|
15353
|
+
for (; ; ) {
|
|
15354
|
+
try {
|
|
15355
|
+
if (timing) {
|
|
15356
|
+
await acquireLock3(repoRoot, tool, null, timing.retries, timing.delayMs);
|
|
15357
|
+
} else {
|
|
15358
|
+
await acquireLock3(repoRoot, tool, null);
|
|
15359
|
+
}
|
|
15360
|
+
return;
|
|
15361
|
+
} catch (err) {
|
|
15362
|
+
if (!(err instanceof LockContentionError)) throw err;
|
|
15363
|
+
appendLog(
|
|
15364
|
+
"info",
|
|
15365
|
+
`git-traces: detached lifecycle worker still waiting for legacy boundary lock (repo=${repoRoot}, tool=${tool})`
|
|
15366
|
+
);
|
|
15367
|
+
timing?.onChunkExhausted?.();
|
|
15368
|
+
}
|
|
15369
|
+
}
|
|
15370
|
+
}
|
|
15371
|
+
async function prepareSessionStartRepo(repo, tool, sessionId, lineage) {
|
|
14264
15372
|
const { repoRoot } = repo;
|
|
14265
15373
|
if (!isGitRepo(repoRoot)) {
|
|
14266
15374
|
appendLog(
|
|
@@ -14269,34 +15377,71 @@ async function processSessionStartRepo(repo, tool, sessionId) {
|
|
|
14269
15377
|
);
|
|
14270
15378
|
return "skipped";
|
|
14271
15379
|
}
|
|
14272
|
-
let
|
|
15380
|
+
let scopedLockAcquired = false;
|
|
14273
15381
|
try {
|
|
14274
15382
|
await acquireLock3(repoRoot, tool, sessionId);
|
|
14275
|
-
|
|
14276
|
-
const
|
|
14277
|
-
if (
|
|
14278
|
-
|
|
14279
|
-
|
|
14280
|
-
|
|
14281
|
-
|
|
14282
|
-
|
|
14283
|
-
|
|
14284
|
-
|
|
14285
|
-
|
|
14286
|
-
|
|
14287
|
-
|
|
14288
|
-
|
|
14289
|
-
|
|
14290
|
-
|
|
14291
|
-
|
|
14292
|
-
|
|
14293
|
-
|
|
14294
|
-
|
|
14295
|
-
|
|
14296
|
-
|
|
14297
|
-
|
|
14298
|
-
|
|
14299
|
-
|
|
15383
|
+
scopedLockAcquired = true;
|
|
15384
|
+
const scoped = (await readSessionStateCandidates(repoRoot, tool, sessionId)).scoped?.state;
|
|
15385
|
+
if (scoped) {
|
|
15386
|
+
pinScopedStateRefs(repoRoot, scoped);
|
|
15387
|
+
return "initialized";
|
|
15388
|
+
}
|
|
15389
|
+
const state = await initializeSession(
|
|
15390
|
+
repoRoot,
|
|
15391
|
+
tool,
|
|
15392
|
+
sessionId,
|
|
15393
|
+
resolveSnapshotLimits(repo.config.snapshotLimits),
|
|
15394
|
+
lineage,
|
|
15395
|
+
true
|
|
15396
|
+
);
|
|
15397
|
+
return state ? "initialized" : "failed";
|
|
15398
|
+
} catch (err) {
|
|
15399
|
+
appendLog(
|
|
15400
|
+
"error",
|
|
15401
|
+
`git-traces: SessionStart failed for repo ${repoRoot}: ${formatError(err)}`
|
|
15402
|
+
);
|
|
15403
|
+
return "failed";
|
|
15404
|
+
} finally {
|
|
15405
|
+
if (scopedLockAcquired) await releaseLock3(repoRoot, tool, sessionId);
|
|
15406
|
+
}
|
|
15407
|
+
}
|
|
15408
|
+
function hasPreparedSessionCaptureProgress(state) {
|
|
15409
|
+
return state.contributionId !== null || state.baselineUploaded === true || state.epoch > 1 || state.turnCount > 0;
|
|
15410
|
+
}
|
|
15411
|
+
async function completeSessionStartRepo(repo, tool, sessionId, lineage, timing) {
|
|
15412
|
+
const { repoRoot } = repo;
|
|
15413
|
+
let legacyLockAcquired = false;
|
|
15414
|
+
let scopedLockAcquired = false;
|
|
15415
|
+
try {
|
|
15416
|
+
await acquireBoundaryLegacyLock(repoRoot, tool, timing);
|
|
15417
|
+
legacyLockAcquired = true;
|
|
15418
|
+
await acquireLock3(repoRoot, tool, sessionId);
|
|
15419
|
+
scopedLockAcquired = true;
|
|
15420
|
+
const candidates = await readSessionStateCandidates(
|
|
15421
|
+
repoRoot,
|
|
15422
|
+
tool,
|
|
15423
|
+
sessionId
|
|
15424
|
+
);
|
|
15425
|
+
if (!candidates.scoped) {
|
|
15426
|
+
appendLog(
|
|
15427
|
+
"info",
|
|
15428
|
+
`git-traces: SessionStart state already cleaned before legacy reconciliation (repo=${repoRoot}, session=${sessionId})`
|
|
15429
|
+
);
|
|
15430
|
+
return "skipped";
|
|
15431
|
+
}
|
|
15432
|
+
const reconciled = reconcileLockedSessionState(candidates);
|
|
15433
|
+
if (!reconciled.state) return "skipped";
|
|
15434
|
+
const lineageChanged = mergeLineageIntoState(reconciled.state, lineage);
|
|
15435
|
+
pinCanonicalStateRefs(repoRoot, reconciled.state);
|
|
15436
|
+
if (reconciled.needsWrite || lineageChanged) {
|
|
15437
|
+
await writeSessionState2(reconciled.state, tool);
|
|
15438
|
+
}
|
|
15439
|
+
if (candidates.legacy) {
|
|
15440
|
+
await deleteLegacySessionState(repoRoot, tool, sessionId);
|
|
15441
|
+
appendLog(
|
|
15442
|
+
"info",
|
|
15443
|
+
`git-traces: migrated legacy session on SessionStart (repo=${repoRoot}, session=${sessionId})`
|
|
15444
|
+
);
|
|
14300
15445
|
}
|
|
14301
15446
|
const staleCount = await cleanupStaleScopedSessionStates(
|
|
14302
15447
|
repoRoot,
|
|
@@ -14309,16 +15454,16 @@ async function processSessionStartRepo(repo, tool, sessionId) {
|
|
|
14309
15454
|
`git-traces: cleaned stale scoped sessions (repo=${repoRoot}, count=${staleCount})`
|
|
14310
15455
|
);
|
|
14311
15456
|
}
|
|
14312
|
-
|
|
14313
|
-
return state ? "initialized" : "failed";
|
|
15457
|
+
return "initialized";
|
|
14314
15458
|
} catch (err) {
|
|
14315
15459
|
appendLog(
|
|
14316
15460
|
"error",
|
|
14317
|
-
`git-traces: SessionStart failed for repo ${repoRoot}: ${formatError(err)}`
|
|
15461
|
+
`git-traces: SessionStart reconciliation failed for repo ${repoRoot}: ${formatError(err)}`
|
|
14318
15462
|
);
|
|
14319
15463
|
return "failed";
|
|
14320
15464
|
} finally {
|
|
14321
|
-
if (
|
|
15465
|
+
if (scopedLockAcquired) await releaseLock3(repoRoot, tool, sessionId);
|
|
15466
|
+
if (legacyLockAcquired) await releaseLock3(repoRoot, tool, null);
|
|
14322
15467
|
}
|
|
14323
15468
|
}
|
|
14324
15469
|
async function handleSessionStart(payload, tool) {
|
|
@@ -14345,11 +15490,24 @@ async function handleSessionStart(payload, tool) {
|
|
|
14345
15490
|
appendLog("info", `git-traces: reaped ${reaped} stale lock file(s)`);
|
|
14346
15491
|
}
|
|
14347
15492
|
const repos = await loadConfiguredRepos();
|
|
14348
|
-
|
|
15493
|
+
const prepared = [];
|
|
14349
15494
|
let skipped = 0;
|
|
14350
15495
|
let failed = 0;
|
|
14351
15496
|
for (const repo of repos) {
|
|
14352
|
-
const outcome = await
|
|
15497
|
+
const outcome = await prepareSessionStartRepo(repo, tool, sessionId);
|
|
15498
|
+
if (outcome === "initialized") prepared.push(repo);
|
|
15499
|
+
else if (outcome === "skipped") skipped++;
|
|
15500
|
+
else failed++;
|
|
15501
|
+
}
|
|
15502
|
+
const lineage = (await detectCodexLineageForHook(payload, tool, sessionId)).lineage;
|
|
15503
|
+
let initialized = 0;
|
|
15504
|
+
for (const repo of prepared) {
|
|
15505
|
+
const outcome = await completeSessionStartRepo(
|
|
15506
|
+
repo,
|
|
15507
|
+
tool,
|
|
15508
|
+
sessionId,
|
|
15509
|
+
lineage
|
|
15510
|
+
);
|
|
14353
15511
|
if (outcome === "initialized") initialized++;
|
|
14354
15512
|
else if (outcome === "skipped") skipped++;
|
|
14355
15513
|
else failed++;
|
|
@@ -14359,7 +15517,7 @@ async function handleSessionStart(payload, tool) {
|
|
|
14359
15517
|
`git-traces: SessionStart summary (session=${sessionId}, tool=${tool}, triggerRepo=${startingProject.repoRoot}, configured=${repos.length}, initialized=${initialized}, skipped=${skipped}, failed=${failed})`
|
|
14360
15518
|
);
|
|
14361
15519
|
}
|
|
14362
|
-
function buildInitialBaselineArtifactsForState(repoRoot, tool, state) {
|
|
15520
|
+
function buildInitialBaselineArtifactsForState(repoRoot, tool, state, limits) {
|
|
14363
15521
|
if (state.baselineTreeSha && state.baselineMetadata) {
|
|
14364
15522
|
return buildFrozenEpochBaselineArtifacts({
|
|
14365
15523
|
repoRoot,
|
|
@@ -14377,9 +15535,81 @@ function buildInitialBaselineArtifactsForState(repoRoot, tool, state) {
|
|
|
14377
15535
|
baselineSha: state.baselineSha,
|
|
14378
15536
|
prevHeadSha: null,
|
|
14379
15537
|
transitionKind: "initial",
|
|
14380
|
-
startedAt: state.startedAt
|
|
15538
|
+
startedAt: state.startedAt,
|
|
15539
|
+
limits,
|
|
15540
|
+
lineage: state.codexLineage
|
|
14381
15541
|
});
|
|
14382
15542
|
}
|
|
15543
|
+
function mergeLineageIntoState(state, detected) {
|
|
15544
|
+
const merged = mergeCodexLineage(state.codexLineage, detected);
|
|
15545
|
+
if (!merged || JSON.stringify(merged) === JSON.stringify(state.codexLineage)) {
|
|
15546
|
+
return false;
|
|
15547
|
+
}
|
|
15548
|
+
state.codexLineage = merged;
|
|
15549
|
+
if (state.baselineMetadata) {
|
|
15550
|
+
const baselineMetadata = { ...state.baselineMetadata };
|
|
15551
|
+
delete baselineMetadata.forkedFromThreadId;
|
|
15552
|
+
delete baselineMetadata.replayBoundary;
|
|
15553
|
+
state.baselineMetadata = {
|
|
15554
|
+
...baselineMetadata,
|
|
15555
|
+
...merged
|
|
15556
|
+
};
|
|
15557
|
+
}
|
|
15558
|
+
return true;
|
|
15559
|
+
}
|
|
15560
|
+
function reconcileSessionStateCandidates(candidates) {
|
|
15561
|
+
const { scoped, legacy } = candidates;
|
|
15562
|
+
if (!scoped) {
|
|
15563
|
+
if (!legacy) return { state: null, needsMigration: false };
|
|
15564
|
+
return { state: { ...legacy.state }, needsMigration: true };
|
|
15565
|
+
}
|
|
15566
|
+
if (!legacy) return { state: { ...scoped.state }, needsMigration: false };
|
|
15567
|
+
let authoritative = scoped;
|
|
15568
|
+
let other = legacy;
|
|
15569
|
+
const preferLegacy = scoped.state.legacyBoundaryPending === true && !hasPreparedSessionCaptureProgress(scoped.state);
|
|
15570
|
+
if (preferLegacy || legacy.mtimeMs > scoped.mtimeMs || legacy.mtimeMs === scoped.mtimeMs && (legacy.state.epoch > scoped.state.epoch || legacy.state.epoch === scoped.state.epoch && legacy.state.turnCount > scoped.state.turnCount)) {
|
|
15571
|
+
authoritative = legacy;
|
|
15572
|
+
other = scoped;
|
|
15573
|
+
}
|
|
15574
|
+
const state = { ...authoritative.state };
|
|
15575
|
+
mergeLineageIntoState(state, other.state.codexLineage);
|
|
15576
|
+
if (other.state.codexLineageChecked === true) {
|
|
15577
|
+
state.codexLineageChecked = true;
|
|
15578
|
+
}
|
|
15579
|
+
if (state.contributionId === other.state.contributionId) {
|
|
15580
|
+
state.nextEpoch = Math.max(
|
|
15581
|
+
state.nextEpoch ?? state.epoch + 1,
|
|
15582
|
+
other.state.nextEpoch ?? other.state.epoch + 1
|
|
15583
|
+
);
|
|
15584
|
+
state.codexLineageStamped = mergeCodexLineage(
|
|
15585
|
+
state.codexLineageStamped,
|
|
15586
|
+
other.state.codexLineageStamped
|
|
15587
|
+
);
|
|
15588
|
+
}
|
|
15589
|
+
return { state, needsMigration: true };
|
|
15590
|
+
}
|
|
15591
|
+
function reconcileLockedSessionState(candidates) {
|
|
15592
|
+
const reconciled = reconcileSessionStateCandidates(candidates);
|
|
15593
|
+
if (!reconciled.state) return { state: null, needsWrite: false };
|
|
15594
|
+
const boundaryCleared = reconciled.state.legacyBoundaryPending === true;
|
|
15595
|
+
delete reconciled.state.legacyBoundaryPending;
|
|
15596
|
+
return {
|
|
15597
|
+
state: reconciled.state,
|
|
15598
|
+
needsWrite: reconciled.needsMigration || boundaryCleared
|
|
15599
|
+
};
|
|
15600
|
+
}
|
|
15601
|
+
async function reserveEpoch(state, persistState) {
|
|
15602
|
+
const reservedEpoch = Math.max(
|
|
15603
|
+
state.nextEpoch ?? state.epoch + 1,
|
|
15604
|
+
state.epoch + 1
|
|
15605
|
+
);
|
|
15606
|
+
state.nextEpoch = reservedEpoch + 1;
|
|
15607
|
+
await persistState(state);
|
|
15608
|
+
return reservedEpoch;
|
|
15609
|
+
}
|
|
15610
|
+
function isTerminalContributionError(err) {
|
|
15611
|
+
return err instanceof PlatformError && err.status === 404 && err.code === "CONTRIBUTION_NOT_FOUND" || isContributionUploadNotUploaded(err);
|
|
15612
|
+
}
|
|
14383
15613
|
async function loadRepoClient(repo) {
|
|
14384
15614
|
const identity = await loadIdentity(repo.config.apiBaseUrl);
|
|
14385
15615
|
if (!identity) {
|
|
@@ -14391,14 +15621,71 @@ async function loadRepoClient(repo) {
|
|
|
14391
15621
|
}
|
|
14392
15622
|
return new PlatformClient(repo.config.apiBaseUrl, identity.sessionCookie);
|
|
14393
15623
|
}
|
|
15624
|
+
async function refreshLineageBaseline(params) {
|
|
15625
|
+
const { repo, state, tool, persistState, client, limits } = params;
|
|
15626
|
+
if (state.codexLineage === void 0 || JSON.stringify(state.codexLineage) === JSON.stringify(state.codexLineageStamped) || state.contributionId === null || !state.baselineUploaded) {
|
|
15627
|
+
return false;
|
|
15628
|
+
}
|
|
15629
|
+
try {
|
|
15630
|
+
const nextEpoch = await reserveEpoch(state, persistState);
|
|
15631
|
+
const artifacts = await openEpoch({
|
|
15632
|
+
repoRoot: repo.repoRoot,
|
|
15633
|
+
client,
|
|
15634
|
+
contributionId: state.contributionId,
|
|
15635
|
+
sessionId: state.sessionId,
|
|
15636
|
+
tool,
|
|
15637
|
+
epoch: nextEpoch,
|
|
15638
|
+
prevHeadSha: state.headSha || null,
|
|
15639
|
+
// The downstream contract has no metadata-only transition. This is a
|
|
15640
|
+
// complete baseline at the current HEAD, so use its compatible initial
|
|
15641
|
+
// baseline shape and explain the refresh only in collector state/logs.
|
|
15642
|
+
transitionKind: "initial",
|
|
15643
|
+
startedAt: state.startedAt,
|
|
15644
|
+
limits,
|
|
15645
|
+
lineage: state.codexLineage
|
|
15646
|
+
});
|
|
15647
|
+
if (!artifacts) {
|
|
15648
|
+
appendLog(
|
|
15649
|
+
"warn",
|
|
15650
|
+
`git-traces: lineage baseline refresh deferred after capture (repo=${repo.repoRoot}, project=${repo.config.projectId}, epoch=${nextEpoch})`
|
|
15651
|
+
);
|
|
15652
|
+
return false;
|
|
15653
|
+
}
|
|
15654
|
+
Object.assign(state, {
|
|
15655
|
+
baselineSha: artifacts.baselineSha,
|
|
15656
|
+
baselineTreeSha: artifacts.baselineTreeSha,
|
|
15657
|
+
lastSnapshotSha: artifacts.baselineSha,
|
|
15658
|
+
lastSnapshotTreeSha: artifacts.baselineTreeSha,
|
|
15659
|
+
epoch: nextEpoch,
|
|
15660
|
+
turnCount: 0,
|
|
15661
|
+
headSha: artifacts.headSha,
|
|
15662
|
+
omittedKeys: artifacts.omittedKeys,
|
|
15663
|
+
codexLineageStamped: state.codexLineage
|
|
15664
|
+
});
|
|
15665
|
+
await persistState(state);
|
|
15666
|
+
appendLog(
|
|
15667
|
+
"info",
|
|
15668
|
+
`git-traces: lineage metadata refresh baseline uploaded (repo=${repo.repoRoot}, project=${repo.config.projectId}, contribution=${state.contributionId}, epoch=${nextEpoch}, baseline=${artifacts.baselineSha.slice(0, 8)})`
|
|
15669
|
+
);
|
|
15670
|
+
return true;
|
|
15671
|
+
} catch (err) {
|
|
15672
|
+
if (isTerminalContributionError(err)) throw err;
|
|
15673
|
+
appendLog(
|
|
15674
|
+
"warn",
|
|
15675
|
+
`git-traces: lineage baseline refresh failed after capture; lineage remains pending (repo=${repo.repoRoot}, project=${repo.config.projectId}): ${formatError(err)}`
|
|
15676
|
+
);
|
|
15677
|
+
return false;
|
|
15678
|
+
}
|
|
15679
|
+
}
|
|
14394
15680
|
async function registerInitialContribution(params) {
|
|
14395
|
-
const { repo, state, tool, client, artifacts } = params;
|
|
15681
|
+
const { repo, state, tool, persistState, client, artifacts } = params;
|
|
14396
15682
|
const contributionId = await createGitTracesContribution({
|
|
14397
15683
|
client,
|
|
14398
15684
|
config: repo.config,
|
|
14399
15685
|
repoRoot: repo.repoRoot,
|
|
14400
15686
|
state,
|
|
14401
15687
|
tool,
|
|
15688
|
+
persistState,
|
|
14402
15689
|
now: /* @__PURE__ */ new Date(),
|
|
14403
15690
|
artifacts
|
|
14404
15691
|
});
|
|
@@ -14407,14 +15694,44 @@ async function registerInitialContribution(params) {
|
|
|
14407
15694
|
state.baselineUploaded = true;
|
|
14408
15695
|
state.baselineTreeSha = artifacts.baselineTreeSha;
|
|
14409
15696
|
state.lastSnapshotTreeSha = artifacts.baselineTreeSha;
|
|
14410
|
-
|
|
15697
|
+
state.codexLineageStamped = state.codexLineage;
|
|
15698
|
+
await persistState(state);
|
|
14411
15699
|
appendLog(
|
|
14412
15700
|
"info",
|
|
14413
15701
|
`git-traces: session registered on first changed turn (repo=${repo.repoRoot}, project=${repo.config.projectId}, contribution=${contributionId}, epoch=1, baseline=${state.baselineSha.slice(0, 8)})`
|
|
14414
15702
|
);
|
|
14415
15703
|
return true;
|
|
14416
15704
|
}
|
|
14417
|
-
|
|
15705
|
+
function reportNewlyOmittedFiles(state, omittedFiles, repoRoot) {
|
|
15706
|
+
const currentKeys = omissionKeys(omittedFiles);
|
|
15707
|
+
const previousKeys = Array.isArray(state.omittedKeys) && state.omittedKeys.every((key) => typeof key === "string") ? state.omittedKeys : void 0;
|
|
15708
|
+
if (previousKeys === void 0) {
|
|
15709
|
+
state.omittedKeys = currentKeys;
|
|
15710
|
+
return true;
|
|
15711
|
+
}
|
|
15712
|
+
const known = new Set(previousKeys);
|
|
15713
|
+
const fresh = omittedFiles.filter((file) => !known.has(omissionKey(file)));
|
|
15714
|
+
if (fresh.length > 0) {
|
|
15715
|
+
const tracked = fresh.filter((file) => file.tracked);
|
|
15716
|
+
const untracked = fresh.filter((file) => !file.tracked);
|
|
15717
|
+
if (tracked.length > 0) {
|
|
15718
|
+
appendLog(
|
|
15719
|
+
"warn",
|
|
15720
|
+
`git-traces: turn newly omitted tracked ${summarizeOmittedFiles(tracked)} \u2014 content excluded from bundle and diffs from here on (repo=${repoRoot})`
|
|
15721
|
+
);
|
|
15722
|
+
}
|
|
15723
|
+
if (untracked.length > 0) {
|
|
15724
|
+
appendLog(
|
|
15725
|
+
"info",
|
|
15726
|
+
`git-traces: turn newly skipped untracked ${summarizeOmittedFiles(untracked)} (repo=${repoRoot})`
|
|
15727
|
+
);
|
|
15728
|
+
}
|
|
15729
|
+
}
|
|
15730
|
+
const changed = currentKeys.length !== previousKeys.length || currentKeys.some((key, i) => key !== previousKeys[i]);
|
|
15731
|
+
if (changed) state.omittedKeys = currentKeys;
|
|
15732
|
+
return changed;
|
|
15733
|
+
}
|
|
15734
|
+
async function processStopRepo(repo, tool, sessionId, recordedAt, lineageCheck, options = {}) {
|
|
14418
15735
|
const { repoRoot, config } = repo;
|
|
14419
15736
|
if (!isGitRepo(repoRoot)) {
|
|
14420
15737
|
appendLog(
|
|
@@ -14424,11 +15741,56 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
|
|
|
14424
15741
|
return "skipped";
|
|
14425
15742
|
}
|
|
14426
15743
|
let state = null;
|
|
14427
|
-
let
|
|
15744
|
+
let legacyLockAcquired = false;
|
|
15745
|
+
let scopedLockAcquired = false;
|
|
15746
|
+
const persistState = sessionId ? (next) => writeSessionState2(next, tool) : (next) => writeLegacySessionState(next, tool);
|
|
14428
15747
|
try {
|
|
14429
|
-
|
|
14430
|
-
|
|
14431
|
-
|
|
15748
|
+
if (options.waitForLegacyBoundary) {
|
|
15749
|
+
await acquireBoundaryLegacyLock(repoRoot, tool);
|
|
15750
|
+
legacyLockAcquired = true;
|
|
15751
|
+
} else {
|
|
15752
|
+
const matchingLegacyHint = sessionId ? await readLegacySessionState(repoRoot, tool, sessionId) : await readSessionState2(repoRoot, tool);
|
|
15753
|
+
if (matchingLegacyHint) {
|
|
15754
|
+
try {
|
|
15755
|
+
await acquireLock3(repoRoot, tool, null, 2, 0);
|
|
15756
|
+
legacyLockAcquired = true;
|
|
15757
|
+
} catch {
|
|
15758
|
+
appendLog(
|
|
15759
|
+
"info",
|
|
15760
|
+
`git-traces: preserving repo on Stop (repo=${repoRoot}, project=${config.projectId}, reason=legacy-lock-held)`
|
|
15761
|
+
);
|
|
15762
|
+
return "skipped";
|
|
15763
|
+
}
|
|
15764
|
+
}
|
|
15765
|
+
}
|
|
15766
|
+
if (sessionId) {
|
|
15767
|
+
await acquireLock3(repoRoot, tool, sessionId);
|
|
15768
|
+
scopedLockAcquired = true;
|
|
15769
|
+
const candidates = await readSessionStateCandidates(
|
|
15770
|
+
repoRoot,
|
|
15771
|
+
tool,
|
|
15772
|
+
sessionId
|
|
15773
|
+
);
|
|
15774
|
+
if (legacyLockAcquired) {
|
|
15775
|
+
const reconciled = reconcileLockedSessionState(candidates);
|
|
15776
|
+
state = reconciled.state;
|
|
15777
|
+
if (state) {
|
|
15778
|
+
pinCanonicalStateRefs(repoRoot, state);
|
|
15779
|
+
if (reconciled.needsWrite) await writeSessionState2(state, tool);
|
|
15780
|
+
}
|
|
15781
|
+
if (state && candidates.legacy) {
|
|
15782
|
+
await deleteLegacySessionState(repoRoot, tool, state.sessionId);
|
|
15783
|
+
}
|
|
15784
|
+
} else {
|
|
15785
|
+
state = candidates.scoped?.state ?? null;
|
|
15786
|
+
}
|
|
15787
|
+
} else {
|
|
15788
|
+
if (!legacyLockAcquired) {
|
|
15789
|
+
await acquireLock3(repoRoot, tool, null, 2, 0);
|
|
15790
|
+
legacyLockAcquired = true;
|
|
15791
|
+
}
|
|
15792
|
+
state = await readSessionState2(repoRoot, tool);
|
|
15793
|
+
}
|
|
14432
15794
|
if (!state) {
|
|
14433
15795
|
appendLog(
|
|
14434
15796
|
"info",
|
|
@@ -14436,6 +15798,40 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
|
|
|
14436
15798
|
);
|
|
14437
15799
|
return "no-state";
|
|
14438
15800
|
}
|
|
15801
|
+
const cleanupState = state;
|
|
15802
|
+
const finishSuccessfulStop = async (outcome) => {
|
|
15803
|
+
if (!options.cleanupOnSuccess) return outcome;
|
|
15804
|
+
await options.onBeforeCleanup?.();
|
|
15805
|
+
cleanupSessionRefs(repoRoot, cleanupState.sessionId);
|
|
15806
|
+
await deleteSessionState2(
|
|
15807
|
+
repoRoot,
|
|
15808
|
+
tool,
|
|
15809
|
+
sessionId ? cleanupState.sessionId : null
|
|
15810
|
+
);
|
|
15811
|
+
if (sessionId && legacyLockAcquired) {
|
|
15812
|
+
await deleteLegacySessionState(repoRoot, tool, cleanupState.sessionId);
|
|
15813
|
+
}
|
|
15814
|
+
appendLog(
|
|
15815
|
+
"info",
|
|
15816
|
+
`git-traces: session ${cleanupState.sessionId} cleaned up (repo=${repoRoot}, epochCount=${cleanupState.epoch}, lastTurnCount=${cleanupState.turnCount})`
|
|
15817
|
+
);
|
|
15818
|
+
return outcome;
|
|
15819
|
+
};
|
|
15820
|
+
let lineageStateChanged = false;
|
|
15821
|
+
if (lineageCheck?.performed) {
|
|
15822
|
+
lineageStateChanged = mergeLineageIntoState(
|
|
15823
|
+
state,
|
|
15824
|
+
lineageCheck.detection?.lineage
|
|
15825
|
+
);
|
|
15826
|
+
if (tool === "codex" && lineageCheck.detection?.conclusive === true && state.codexLineageChecked !== true) {
|
|
15827
|
+
state.codexLineageChecked = true;
|
|
15828
|
+
lineageStateChanged = true;
|
|
15829
|
+
}
|
|
15830
|
+
}
|
|
15831
|
+
if (lineageStateChanged) {
|
|
15832
|
+
await persistState(state);
|
|
15833
|
+
}
|
|
15834
|
+
const limits = sessionSnapshotLimits(state);
|
|
14439
15835
|
const lastSnapshotTreeSha = state.lastSnapshotTreeSha ?? state.baselineTreeSha;
|
|
14440
15836
|
if (!lastSnapshotTreeSha) {
|
|
14441
15837
|
appendLog(
|
|
@@ -14445,6 +15841,7 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
|
|
|
14445
15841
|
return "failed";
|
|
14446
15842
|
}
|
|
14447
15843
|
const currentHeadSha = captureHeadSha(repoRoot);
|
|
15844
|
+
const lineageNeedsRefresh = state.codexLineage !== void 0 && JSON.stringify(state.codexLineage) !== JSON.stringify(state.codexLineageStamped);
|
|
14448
15845
|
if (currentHeadSha !== state.headSha) {
|
|
14449
15846
|
const client2 = await loadRepoClient(repo);
|
|
14450
15847
|
if (!client2) return "skipped";
|
|
@@ -14452,7 +15849,8 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
|
|
|
14452
15849
|
const artifacts2 = buildInitialBaselineArtifactsForState(
|
|
14453
15850
|
repoRoot,
|
|
14454
15851
|
tool,
|
|
14455
|
-
state
|
|
15852
|
+
state,
|
|
15853
|
+
limits
|
|
14456
15854
|
);
|
|
14457
15855
|
if (!artifacts2 || !canUploadEpochBaselineArtifacts(1, artifacts2)) {
|
|
14458
15856
|
return "skipped";
|
|
@@ -14461,6 +15859,7 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
|
|
|
14461
15859
|
repo,
|
|
14462
15860
|
state,
|
|
14463
15861
|
tool,
|
|
15862
|
+
persistState,
|
|
14464
15863
|
client: client2,
|
|
14465
15864
|
artifacts: artifacts2
|
|
14466
15865
|
});
|
|
@@ -14479,7 +15878,7 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
|
|
|
14479
15878
|
state.headSha || null,
|
|
14480
15879
|
currentHeadSha
|
|
14481
15880
|
);
|
|
14482
|
-
const nextEpoch = state
|
|
15881
|
+
const nextEpoch = await reserveEpoch(state, persistState);
|
|
14483
15882
|
appendLog(
|
|
14484
15883
|
"info",
|
|
14485
15884
|
`git-traces: HEAD moved (repo=${repoRoot}, project=${config.projectId}, contribution=${contributionId2}, transition=${transitionKind}, from=${state.headSha.slice(0, 8)}, to=${currentHeadSha.slice(0, 8)}, openingEpoch=${nextEpoch})`
|
|
@@ -14493,7 +15892,9 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
|
|
|
14493
15892
|
epoch: nextEpoch,
|
|
14494
15893
|
prevHeadSha: state.headSha || null,
|
|
14495
15894
|
transitionKind,
|
|
14496
|
-
startedAt: state.startedAt
|
|
15895
|
+
startedAt: state.startedAt,
|
|
15896
|
+
limits,
|
|
15897
|
+
lineage: state.codexLineage
|
|
14497
15898
|
});
|
|
14498
15899
|
if (!artifacts) return "failed";
|
|
14499
15900
|
const next = {
|
|
@@ -14504,28 +15905,57 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
|
|
|
14504
15905
|
lastSnapshotTreeSha: artifacts.baselineTreeSha,
|
|
14505
15906
|
epoch: nextEpoch,
|
|
14506
15907
|
turnCount: 0,
|
|
14507
|
-
headSha: artifacts.headSha
|
|
15908
|
+
headSha: artifacts.headSha,
|
|
15909
|
+
omittedKeys: artifacts.omittedKeys,
|
|
15910
|
+
codexLineageStamped: state.codexLineage
|
|
14508
15911
|
};
|
|
14509
|
-
await
|
|
15912
|
+
await persistState(next);
|
|
14510
15913
|
appendLog(
|
|
14511
15914
|
"info",
|
|
14512
15915
|
`git-traces: epoch baseline uploaded (repo=${repoRoot}, project=${config.projectId}, contribution=${contributionId2}, epoch=${nextEpoch}, baseline=${artifacts.baselineSha.slice(0, 8)})`
|
|
14513
15916
|
);
|
|
14514
|
-
return "uploaded";
|
|
15917
|
+
return await finishSuccessfulStop("uploaded");
|
|
14515
15918
|
}
|
|
14516
15919
|
const currentSha = captureSnapshotSha(repoRoot);
|
|
14517
|
-
const
|
|
15920
|
+
const turnOmittedFiles = [];
|
|
15921
|
+
const currentTreeSha = buildSnapshotTree(repoRoot, currentSha, {
|
|
15922
|
+
omittedFiles: turnOmittedFiles,
|
|
15923
|
+
log: false,
|
|
15924
|
+
limits
|
|
15925
|
+
});
|
|
15926
|
+
const omissionsChanged = reportNewlyOmittedFiles(
|
|
15927
|
+
state,
|
|
15928
|
+
turnOmittedFiles,
|
|
15929
|
+
repoRoot
|
|
15930
|
+
);
|
|
14518
15931
|
const patchBuffer = createTreeDiffPatchGz(
|
|
14519
15932
|
repoRoot,
|
|
14520
15933
|
lastSnapshotTreeSha,
|
|
14521
|
-
currentTreeSha
|
|
15934
|
+
currentTreeSha,
|
|
15935
|
+
{ limits }
|
|
14522
15936
|
);
|
|
14523
15937
|
if (!patchBuffer) {
|
|
15938
|
+
if (omissionsChanged) await persistState(state);
|
|
14524
15939
|
appendLog(
|
|
14525
15940
|
"info",
|
|
14526
15941
|
`git-traces: turn produced identical snapshot tree, skipping upload (repo=${repoRoot}, project=${config.projectId})`
|
|
14527
15942
|
);
|
|
14528
|
-
|
|
15943
|
+
if (lineageNeedsRefresh && state.contributionId !== null && state.baselineUploaded) {
|
|
15944
|
+
const client2 = await loadRepoClient(repo);
|
|
15945
|
+
const refreshed = client2 !== null && await refreshLineageBaseline({
|
|
15946
|
+
repo,
|
|
15947
|
+
state,
|
|
15948
|
+
tool,
|
|
15949
|
+
persistState,
|
|
15950
|
+
client: client2,
|
|
15951
|
+
limits
|
|
15952
|
+
});
|
|
15953
|
+
if (refreshed) {
|
|
15954
|
+
return await finishSuccessfulStop("uploaded");
|
|
15955
|
+
}
|
|
15956
|
+
if (options.cleanupOnSuccess) return "skipped";
|
|
15957
|
+
}
|
|
15958
|
+
return await finishSuccessfulStop("unchanged");
|
|
14529
15959
|
}
|
|
14530
15960
|
const prefix = epochPrefix2(state.epoch);
|
|
14531
15961
|
const nextTurnCount = state.turnCount + 1;
|
|
@@ -14544,7 +15974,8 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
|
|
|
14544
15974
|
const artifacts = buildInitialBaselineArtifactsForState(
|
|
14545
15975
|
repoRoot,
|
|
14546
15976
|
tool,
|
|
14547
|
-
state
|
|
15977
|
+
state,
|
|
15978
|
+
limits
|
|
14548
15979
|
);
|
|
14549
15980
|
if (!artifacts || !canUploadEpochBaselineArtifacts(1, artifacts)) {
|
|
14550
15981
|
return "skipped";
|
|
@@ -14553,6 +15984,7 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
|
|
|
14553
15984
|
repo,
|
|
14554
15985
|
state,
|
|
14555
15986
|
tool,
|
|
15987
|
+
persistState,
|
|
14556
15988
|
client,
|
|
14557
15989
|
artifacts
|
|
14558
15990
|
});
|
|
@@ -14571,6 +16003,14 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
|
|
|
14571
16003
|
`refs/hillclimb/turns/${state.sessionId}/${prefix}/${turnLabel}`,
|
|
14572
16004
|
currentSha
|
|
14573
16005
|
);
|
|
16006
|
+
pinScopedTurnSnapshot(
|
|
16007
|
+
repoRoot,
|
|
16008
|
+
state.sessionId,
|
|
16009
|
+
state.epoch,
|
|
16010
|
+
nextTurnCount,
|
|
16011
|
+
currentSha,
|
|
16012
|
+
currentTreeSha
|
|
16013
|
+
);
|
|
14574
16014
|
const uploaded = await uploadFile(
|
|
14575
16015
|
client,
|
|
14576
16016
|
contributionId,
|
|
@@ -14588,46 +16028,63 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
|
|
|
14588
16028
|
state.turnCount = nextTurnCount;
|
|
14589
16029
|
state.lastSnapshotSha = currentSha;
|
|
14590
16030
|
state.lastSnapshotTreeSha = currentTreeSha;
|
|
14591
|
-
await
|
|
16031
|
+
await persistState(state);
|
|
14592
16032
|
appendLog(
|
|
14593
16033
|
"info",
|
|
14594
16034
|
`git-traces: patch uploaded (repo=${repoRoot}, project=${config.projectId}, contribution=${contributionId}, epoch=${state.epoch}, turn=${turnLabel}, bytes=${patchBuffer.byteLength})`
|
|
14595
16035
|
);
|
|
14596
|
-
|
|
16036
|
+
const lineageStillNeedsRefresh = state.codexLineage !== void 0 && JSON.stringify(state.codexLineage) !== JSON.stringify(state.codexLineageStamped);
|
|
16037
|
+
if (lineageStillNeedsRefresh) {
|
|
16038
|
+
const refreshed = await refreshLineageBaseline({
|
|
16039
|
+
repo,
|
|
16040
|
+
state,
|
|
16041
|
+
tool,
|
|
16042
|
+
persistState,
|
|
16043
|
+
client,
|
|
16044
|
+
limits
|
|
16045
|
+
});
|
|
16046
|
+
if (!refreshed && options.cleanupOnSuccess) return "skipped";
|
|
16047
|
+
}
|
|
16048
|
+
return await finishSuccessfulStop("uploaded");
|
|
14597
16049
|
} catch (err) {
|
|
14598
16050
|
appendLog(
|
|
14599
16051
|
"error",
|
|
14600
16052
|
`git-traces: Stop failed for repo ${repoRoot}: ${formatError(err)}`
|
|
14601
16053
|
);
|
|
14602
|
-
if (state && err
|
|
16054
|
+
if (state && isTerminalContributionError(err)) {
|
|
16055
|
+
const poisoned = isContributionUploadNotUploaded(err);
|
|
14603
16056
|
if (state.baselineTreeSha) {
|
|
14604
|
-
await
|
|
14605
|
-
|
|
14606
|
-
|
|
14607
|
-
|
|
14608
|
-
|
|
14609
|
-
|
|
14610
|
-
|
|
14611
|
-
|
|
14612
|
-
},
|
|
14613
|
-
tool
|
|
14614
|
-
);
|
|
16057
|
+
await persistState({
|
|
16058
|
+
...state,
|
|
16059
|
+
contributionId: null,
|
|
16060
|
+
baselineUploaded: false,
|
|
16061
|
+
lastSnapshotSha: state.baselineSha,
|
|
16062
|
+
lastSnapshotTreeSha: state.baselineTreeSha,
|
|
16063
|
+
turnCount: 0
|
|
16064
|
+
});
|
|
14615
16065
|
appendLog(
|
|
14616
16066
|
"warn",
|
|
14617
|
-
`git-traces: cleared stale contribution state (repo=${repoRoot}, project=${config.projectId}, missingContribution=${state.contributionId})`
|
|
16067
|
+
poisoned ? `git-traces: cleared abandoned contribution state (repo=${repoRoot}, project=${config.projectId}, contribution=${state.contributionId}, reason=${err.code})` : `git-traces: cleared stale contribution state (repo=${repoRoot}, project=${config.projectId}, missingContribution=${state.contributionId})`
|
|
14618
16068
|
);
|
|
14619
16069
|
} else {
|
|
14620
16070
|
cleanupSessionRefs(repoRoot, state.sessionId);
|
|
14621
|
-
await
|
|
16071
|
+
await deleteSessionState2(
|
|
16072
|
+
repoRoot,
|
|
16073
|
+
tool,
|
|
16074
|
+
sessionId ? state.sessionId : null
|
|
16075
|
+
);
|
|
14622
16076
|
appendLog(
|
|
14623
16077
|
"warn",
|
|
14624
|
-
`git-traces: deleted stale contribution state without a frozen baseline (repo=${repoRoot}, project=${config.projectId}, missingContribution=${state.contributionId})`
|
|
16078
|
+
poisoned ? `git-traces: deleted abandoned contribution state without a frozen baseline (repo=${repoRoot}, project=${config.projectId}, contribution=${state.contributionId}, reason=${err.code})` : `git-traces: deleted stale contribution state without a frozen baseline (repo=${repoRoot}, project=${config.projectId}, missingContribution=${state.contributionId})`
|
|
14625
16079
|
);
|
|
14626
16080
|
}
|
|
14627
16081
|
}
|
|
14628
16082
|
return "failed";
|
|
14629
16083
|
} finally {
|
|
14630
|
-
if (
|
|
16084
|
+
if (scopedLockAcquired) {
|
|
16085
|
+
await releaseLock3(repoRoot, tool, sessionId);
|
|
16086
|
+
}
|
|
16087
|
+
if (legacyLockAcquired) await releaseLock3(repoRoot, tool, null);
|
|
14631
16088
|
}
|
|
14632
16089
|
}
|
|
14633
16090
|
async function handleStop(payload, tool) {
|
|
@@ -14639,12 +16096,13 @@ async function handleStop(payload, tool) {
|
|
|
14639
16096
|
const recordedAt = Date.now();
|
|
14640
16097
|
const repos = await loadConfiguredRepos();
|
|
14641
16098
|
const repoByRoot = new Map(repos.map((repo) => [repo.repoRoot, repo]));
|
|
14642
|
-
const
|
|
16099
|
+
const targetsByRoot = /* @__PURE__ */ new Map();
|
|
14643
16100
|
let missingConfig = 0;
|
|
16101
|
+
let needsLineageCheck = false;
|
|
14644
16102
|
if (sessionId) {
|
|
14645
16103
|
const storedStates = await listSessionStatesForSession(tool, sessionId);
|
|
14646
16104
|
for (const { state } of storedStates) {
|
|
14647
|
-
const repo = repoByRoot.get(
|
|
16105
|
+
const repo = repoByRoot.get(path18.resolve(state.repoRoot));
|
|
14648
16106
|
if (!repo) {
|
|
14649
16107
|
missingConfig++;
|
|
14650
16108
|
appendLog(
|
|
@@ -14653,21 +16111,53 @@ async function handleStop(payload, tool) {
|
|
|
14653
16111
|
);
|
|
14654
16112
|
continue;
|
|
14655
16113
|
}
|
|
14656
|
-
|
|
16114
|
+
if (tool === "codex" && state.codexLineageChecked !== true) {
|
|
16115
|
+
needsLineageCheck = true;
|
|
16116
|
+
}
|
|
16117
|
+
targetsByRoot.set(repo.repoRoot, repo);
|
|
16118
|
+
}
|
|
16119
|
+
for (const repo of repos) {
|
|
16120
|
+
const legacyState = await readLegacySessionState(
|
|
16121
|
+
repo.repoRoot,
|
|
16122
|
+
tool,
|
|
16123
|
+
sessionId
|
|
16124
|
+
);
|
|
16125
|
+
if (!legacyState) continue;
|
|
16126
|
+
targetsByRoot.set(repo.repoRoot, repo);
|
|
16127
|
+
if (tool === "codex" && legacyState.codexLineageChecked !== true) {
|
|
16128
|
+
needsLineageCheck = true;
|
|
16129
|
+
}
|
|
14657
16130
|
}
|
|
14658
|
-
if (
|
|
14659
|
-
|
|
16131
|
+
if (targetsByRoot.size === 0 && missingConfig === 0) {
|
|
16132
|
+
targetsByRoot.set(project.repoRoot, {
|
|
16133
|
+
repoRoot: project.repoRoot,
|
|
16134
|
+
config: project.config
|
|
16135
|
+
});
|
|
14660
16136
|
}
|
|
14661
16137
|
} else {
|
|
14662
|
-
|
|
16138
|
+
targetsByRoot.set(project.repoRoot, {
|
|
16139
|
+
repoRoot: project.repoRoot,
|
|
16140
|
+
config: project.config
|
|
16141
|
+
});
|
|
14663
16142
|
}
|
|
16143
|
+
const targets = [...targetsByRoot.values()];
|
|
16144
|
+
const lineageCheck = needsLineageCheck ? {
|
|
16145
|
+
performed: true,
|
|
16146
|
+
detection: await detectCodexLineageForHook(payload, tool, sessionId)
|
|
16147
|
+
} : { performed: false };
|
|
14664
16148
|
let uploaded = 0;
|
|
14665
16149
|
let unchanged = 0;
|
|
14666
16150
|
let skipped = missingConfig;
|
|
14667
16151
|
let noState = 0;
|
|
14668
16152
|
let failed = 0;
|
|
14669
16153
|
for (const repo of targets) {
|
|
14670
|
-
const outcome = await processStopRepo(
|
|
16154
|
+
const outcome = await processStopRepo(
|
|
16155
|
+
repo,
|
|
16156
|
+
tool,
|
|
16157
|
+
sessionId,
|
|
16158
|
+
recordedAt,
|
|
16159
|
+
lineageCheck
|
|
16160
|
+
);
|
|
14671
16161
|
if (outcome === "uploaded") uploaded++;
|
|
14672
16162
|
else if (outcome === "unchanged") unchanged++;
|
|
14673
16163
|
else if (outcome === "skipped") skipped++;
|
|
@@ -14679,49 +16169,48 @@ async function handleStop(payload, tool) {
|
|
|
14679
16169
|
`git-traces: Stop summary (session=${sessionId ?? "<none>"}, tool=${tool}, triggerRepo=${project.repoRoot}, configured=${repos.length}, frozenStates=${targets.length + missingConfig}, uploaded=${uploaded}, unchanged=${unchanged}, skipped=${skipped}, noState=${noState}, failed=${failed})`
|
|
14680
16170
|
);
|
|
14681
16171
|
}
|
|
14682
|
-
async function cleanupSessionStateForRepo(repoRoot, tool, sessionId) {
|
|
14683
|
-
let acquired = false;
|
|
14684
|
-
try {
|
|
14685
|
-
await acquireLock3(repoRoot, tool, sessionId);
|
|
14686
|
-
acquired = true;
|
|
14687
|
-
const state = await readSessionState(repoRoot, tool, sessionId);
|
|
14688
|
-
if (!state) return "no-state";
|
|
14689
|
-
cleanupSessionRefs(repoRoot, state.sessionId);
|
|
14690
|
-
await deleteSessionState(repoRoot, tool, state.sessionId);
|
|
14691
|
-
appendLog(
|
|
14692
|
-
"info",
|
|
14693
|
-
`git-traces: session ${state.sessionId} cleaned up (repo=${repoRoot}, epochCount=${state.epoch}, lastTurnCount=${state.turnCount})`
|
|
14694
|
-
);
|
|
14695
|
-
return "cleaned";
|
|
14696
|
-
} catch (err) {
|
|
14697
|
-
appendLog(
|
|
14698
|
-
"error",
|
|
14699
|
-
`git-traces: SessionEnd cleanup failed for repo ${repoRoot}: ${formatError(err)}`
|
|
14700
|
-
);
|
|
14701
|
-
return "failed";
|
|
14702
|
-
} finally {
|
|
14703
|
-
if (acquired) await releaseLock3(repoRoot, tool, sessionId);
|
|
14704
|
-
}
|
|
14705
|
-
}
|
|
14706
16172
|
async function handleSessionEnd(payload, tool) {
|
|
14707
16173
|
const cwd = resolveCwd2(payload);
|
|
14708
16174
|
const sessionId = resolveSessionId2(payload);
|
|
14709
16175
|
const project = cwd ? await findProjectForCwd(cwd) : null;
|
|
14710
16176
|
const triggerRepo = project?.repoRoot ?? cwd ?? "<none>";
|
|
14711
16177
|
const recordedAt = Date.now();
|
|
14712
|
-
const
|
|
16178
|
+
const repos = await loadConfiguredRepos();
|
|
16179
|
+
const repoByRoot = new Map(repos.map((repo) => [repo.repoRoot, repo]));
|
|
16180
|
+
const repoRoots = /* @__PURE__ */ new Set();
|
|
16181
|
+
let needsLineageCheck = false;
|
|
14713
16182
|
if (sessionId) {
|
|
14714
16183
|
const states = await listSessionStatesForSession(tool, sessionId);
|
|
14715
16184
|
for (const { state } of states) {
|
|
14716
|
-
|
|
16185
|
+
const repoRoot = path18.resolve(state.repoRoot);
|
|
16186
|
+
repoRoots.add(repoRoot);
|
|
16187
|
+
const canProcess = repoByRoot.has(repoRoot) || project && path18.resolve(project.repoRoot) === repoRoot;
|
|
16188
|
+
if (canProcess && tool === "codex" && state.codexLineageChecked !== true) {
|
|
16189
|
+
needsLineageCheck = true;
|
|
16190
|
+
}
|
|
16191
|
+
}
|
|
16192
|
+
for (const repo of repos) {
|
|
16193
|
+
const legacyState = await readLegacySessionState(
|
|
16194
|
+
repo.repoRoot,
|
|
16195
|
+
tool,
|
|
16196
|
+
sessionId
|
|
16197
|
+
);
|
|
16198
|
+
if (!legacyState) continue;
|
|
16199
|
+
repoRoots.add(repo.repoRoot);
|
|
16200
|
+
if (tool === "codex" && legacyState.codexLineageChecked !== true) {
|
|
16201
|
+
needsLineageCheck = true;
|
|
16202
|
+
}
|
|
14717
16203
|
}
|
|
14718
16204
|
}
|
|
14719
|
-
if (repoRoots.
|
|
14720
|
-
|
|
16205
|
+
if (repoRoots.size === 0 && cwd) {
|
|
16206
|
+
const fallbackRepoRoot = project?.repoRoot ?? cwd;
|
|
16207
|
+
repoRoots.add(fallbackRepoRoot);
|
|
14721
16208
|
}
|
|
14722
|
-
if (repoRoots.
|
|
14723
|
-
const
|
|
14724
|
-
|
|
16209
|
+
if (repoRoots.size === 0) return;
|
|
16210
|
+
const lineageCheck = needsLineageCheck ? {
|
|
16211
|
+
performed: true,
|
|
16212
|
+
detection: await detectCodexLineageForHook(payload, tool, sessionId)
|
|
16213
|
+
} : { performed: false };
|
|
14725
16214
|
let cleaned = 0;
|
|
14726
16215
|
let noState = 0;
|
|
14727
16216
|
let uploaded = 0;
|
|
@@ -14729,7 +16218,7 @@ async function handleSessionEnd(payload, tool) {
|
|
|
14729
16218
|
let skipped = 0;
|
|
14730
16219
|
let failed = 0;
|
|
14731
16220
|
for (const repoRoot of repoRoots) {
|
|
14732
|
-
const repo = repoByRoot.get(
|
|
16221
|
+
const repo = repoByRoot.get(path18.resolve(repoRoot)) ?? (project && path18.resolve(project.repoRoot) === path18.resolve(repoRoot) ? { repoRoot: project.repoRoot, config: project.config } : null);
|
|
14733
16222
|
if (!repo) {
|
|
14734
16223
|
skipped++;
|
|
14735
16224
|
appendLog(
|
|
@@ -14742,7 +16231,9 @@ async function handleSessionEnd(payload, tool) {
|
|
|
14742
16231
|
repo,
|
|
14743
16232
|
tool,
|
|
14744
16233
|
sessionId,
|
|
14745
|
-
recordedAt
|
|
16234
|
+
recordedAt,
|
|
16235
|
+
lineageCheck,
|
|
16236
|
+
{ waitForLegacyBoundary: true, cleanupOnSuccess: true }
|
|
14746
16237
|
);
|
|
14747
16238
|
if (finalOutcome === "uploaded") uploaded++;
|
|
14748
16239
|
else if (finalOutcome === "unchanged") unchanged++;
|
|
@@ -14764,14 +16255,11 @@ async function handleSessionEnd(payload, tool) {
|
|
|
14764
16255
|
noState++;
|
|
14765
16256
|
continue;
|
|
14766
16257
|
}
|
|
14767
|
-
|
|
14768
|
-
if (outcome === "cleaned") cleaned++;
|
|
14769
|
-
else if (outcome === "no-state") noState++;
|
|
14770
|
-
else failed++;
|
|
16258
|
+
cleaned++;
|
|
14771
16259
|
}
|
|
14772
16260
|
appendLog(
|
|
14773
16261
|
"info",
|
|
14774
|
-
`git-traces: SessionEnd summary (session=${sessionId ?? "<none>"}, tool=${tool}, triggerRepo=${triggerRepo}, states=${repoRoots.
|
|
16262
|
+
`git-traces: SessionEnd summary (session=${sessionId ?? "<none>"}, tool=${tool}, triggerRepo=${triggerRepo}, states=${repoRoots.size}, uploaded=${uploaded}, unchanged=${unchanged}, skipped=${skipped}, cleaned=${cleaned}, noState=${noState}, failed=${failed})`
|
|
14775
16263
|
);
|
|
14776
16264
|
}
|
|
14777
16265
|
|
|
@@ -14780,7 +16268,7 @@ var WORKER_ENV_FLAG2 = "HILLCLIMB_GIT_TRACES_WORKER";
|
|
|
14780
16268
|
var TOOL_ENV_FLAG2 = "HILLCLIMB_GIT_TRACES_TOOL";
|
|
14781
16269
|
var FLOW_ID_ENV2 = "HILLCLIMB_GIT_TRACES_FLOW";
|
|
14782
16270
|
function newFlowId2() {
|
|
14783
|
-
return
|
|
16271
|
+
return crypto7.randomBytes(3).toString("hex");
|
|
14784
16272
|
}
|
|
14785
16273
|
var KNOWN_TOOLS = /* @__PURE__ */ new Set([
|
|
14786
16274
|
"claude",
|
|
@@ -15022,7 +16510,7 @@ async function runGitTracesWorker() {
|
|
|
15022
16510
|
return;
|
|
15023
16511
|
}
|
|
15024
16512
|
const event = payload.hook_event_name;
|
|
15025
|
-
const
|
|
16513
|
+
const eventKind2 = classifyHookEvent2(event);
|
|
15026
16514
|
appendLog("info", `git-traces worker: handling event=${event}`);
|
|
15027
16515
|
if (tool === "claude" && typeof payload.cursor_version === "string") {
|
|
15028
16516
|
appendLog(
|
|
@@ -15033,7 +16521,7 @@ async function runGitTracesWorker() {
|
|
|
15033
16521
|
}
|
|
15034
16522
|
try {
|
|
15035
16523
|
await selfHealHook2(payload, tool);
|
|
15036
|
-
switch (
|
|
16524
|
+
switch (eventKind2) {
|
|
15037
16525
|
case "sessionStart":
|
|
15038
16526
|
await handleSessionStart(payload, tool);
|
|
15039
16527
|
break;
|
|
@@ -15055,7 +16543,7 @@ async function runGitTracesWorker() {
|
|
|
15055
16543
|
${stack}` : ""}`
|
|
15056
16544
|
);
|
|
15057
16545
|
} finally {
|
|
15058
|
-
if (
|
|
16546
|
+
if (eventKind2 === "stop" || eventKind2 === "sessionEnd") {
|
|
15059
16547
|
await recordDebugLogCompletion({
|
|
15060
16548
|
kind: "git",
|
|
15061
16549
|
tool,
|
|
@@ -15066,29 +16554,29 @@ ${stack}` : ""}`
|
|
|
15066
16554
|
}
|
|
15067
16555
|
|
|
15068
16556
|
// src/outputs/zip.ts
|
|
15069
|
-
import
|
|
15070
|
-
import
|
|
16557
|
+
import fs19 from "fs";
|
|
16558
|
+
import path20 from "path";
|
|
15071
16559
|
import archiver2 from "archiver";
|
|
15072
16560
|
|
|
15073
16561
|
// src/outputs/downloads.ts
|
|
15074
16562
|
import { execSync as execSync2 } from "child_process";
|
|
15075
|
-
import
|
|
15076
|
-
import
|
|
15077
|
-
import
|
|
16563
|
+
import fs18 from "fs";
|
|
16564
|
+
import os10 from "os";
|
|
16565
|
+
import path19 from "path";
|
|
15078
16566
|
function getDownloadsFolder() {
|
|
15079
|
-
const home =
|
|
16567
|
+
const home = os10.homedir();
|
|
15080
16568
|
if (process.platform === "linux") {
|
|
15081
16569
|
try {
|
|
15082
16570
|
const xdgDir = execSync2("xdg-user-dir DOWNLOAD", {
|
|
15083
16571
|
encoding: "utf-8",
|
|
15084
16572
|
timeout: 3e3
|
|
15085
16573
|
}).trim();
|
|
15086
|
-
if (xdgDir &&
|
|
16574
|
+
if (xdgDir && fs18.existsSync(xdgDir)) return xdgDir;
|
|
15087
16575
|
} catch {
|
|
15088
16576
|
}
|
|
15089
16577
|
}
|
|
15090
|
-
const downloads =
|
|
15091
|
-
if (
|
|
16578
|
+
const downloads = path19.join(home, "Downloads");
|
|
16579
|
+
if (fs18.existsSync(downloads)) return downloads;
|
|
15092
16580
|
return home;
|
|
15093
16581
|
}
|
|
15094
16582
|
|
|
@@ -15097,11 +16585,11 @@ function sanitizeFilename(name) {
|
|
|
15097
16585
|
return name.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
15098
16586
|
}
|
|
15099
16587
|
function getUniqueFilename(dir, base, ext) {
|
|
15100
|
-
let candidate =
|
|
15101
|
-
if (!
|
|
16588
|
+
let candidate = path20.join(dir, `${base}${ext}`);
|
|
16589
|
+
if (!fs19.existsSync(candidate)) return candidate;
|
|
15102
16590
|
let i = 1;
|
|
15103
|
-
while (
|
|
15104
|
-
candidate =
|
|
16591
|
+
while (fs19.existsSync(candidate)) {
|
|
16592
|
+
candidate = path20.join(dir, `${base}-${i}${ext}`);
|
|
15105
16593
|
i++;
|
|
15106
16594
|
}
|
|
15107
16595
|
return candidate;
|
|
@@ -15111,13 +16599,13 @@ var ZipOutput = class {
|
|
|
15111
16599
|
label = "Save as .zip to Downloads";
|
|
15112
16600
|
async emit(group, options) {
|
|
15113
16601
|
const downloadsDir = getDownloadsFolder();
|
|
15114
|
-
const repoName = sanitizeFilename(
|
|
16602
|
+
const repoName = sanitizeFilename(path20.basename(group.repoPath));
|
|
15115
16603
|
const timeRange = options.timeRange;
|
|
15116
16604
|
const rangePart = timeRange?.label ?? "all";
|
|
15117
16605
|
const epochSeconds = Math.floor(Date.now() / 1e3);
|
|
15118
16606
|
const baseName = `logs-${repoName}-${rangePart}-${epochSeconds}`;
|
|
15119
16607
|
const outputPath = getUniqueFilename(downloadsDir, baseName, ".zip");
|
|
15120
|
-
const output =
|
|
16608
|
+
const output = fs19.createWriteStream(outputPath);
|
|
15121
16609
|
const archive = archiver2("zip", { zlib: { level: 6 } });
|
|
15122
16610
|
const done = new Promise((resolve, reject) => {
|
|
15123
16611
|
output.on("close", resolve);
|
|
@@ -15311,15 +16799,15 @@ async function confirmExport(group, output) {
|
|
|
15311
16799
|
}
|
|
15312
16800
|
|
|
15313
16801
|
// src/sources/claude.ts
|
|
15314
|
-
import
|
|
15315
|
-
import
|
|
15316
|
-
import
|
|
16802
|
+
import fs20 from "fs";
|
|
16803
|
+
import os11 from "os";
|
|
16804
|
+
import path21 from "path";
|
|
15317
16805
|
import readline2 from "readline";
|
|
15318
16806
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["tool-results"]);
|
|
15319
16807
|
async function resolveRepoPath(projectDir) {
|
|
15320
|
-
const indexPath =
|
|
16808
|
+
const indexPath = path21.join(projectDir, "sessions-index.json");
|
|
15321
16809
|
try {
|
|
15322
|
-
const raw = await
|
|
16810
|
+
const raw = await fs20.promises.readFile(indexPath, "utf-8");
|
|
15323
16811
|
const data = JSON.parse(raw);
|
|
15324
16812
|
if (data.originalPath && typeof data.originalPath === "string") {
|
|
15325
16813
|
return data.originalPath;
|
|
@@ -15327,12 +16815,12 @@ async function resolveRepoPath(projectDir) {
|
|
|
15327
16815
|
} catch {
|
|
15328
16816
|
}
|
|
15329
16817
|
const cwdCounts = /* @__PURE__ */ new Map();
|
|
15330
|
-
const entries = await
|
|
16818
|
+
const entries = await fs20.promises.readdir(projectDir, {
|
|
15331
16819
|
withFileTypes: true
|
|
15332
16820
|
});
|
|
15333
16821
|
for (const entry of entries) {
|
|
15334
16822
|
if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
|
|
15335
|
-
const cwd = await extractCwdFromJsonl(
|
|
16823
|
+
const cwd = await extractCwdFromJsonl(path21.join(projectDir, entry.name));
|
|
15336
16824
|
if (cwd) {
|
|
15337
16825
|
cwdCounts.set(cwd, (cwdCounts.get(cwd) ?? 0) + 1);
|
|
15338
16826
|
}
|
|
@@ -15351,7 +16839,7 @@ async function resolveRepoPath(projectDir) {
|
|
|
15351
16839
|
return null;
|
|
15352
16840
|
}
|
|
15353
16841
|
async function extractCwdFromJsonl(filePath) {
|
|
15354
|
-
const stream =
|
|
16842
|
+
const stream = fs20.createReadStream(filePath, { encoding: "utf-8" });
|
|
15355
16843
|
const rl = readline2.createInterface({ input: stream, crlfDelay: Infinity });
|
|
15356
16844
|
try {
|
|
15357
16845
|
for await (const line of rl) {
|
|
@@ -15373,12 +16861,12 @@ async function extractCwdFromJsonl(filePath) {
|
|
|
15373
16861
|
async function collectFiles(dir, baseDir, sourceName, repoPath, results) {
|
|
15374
16862
|
let entries;
|
|
15375
16863
|
try {
|
|
15376
|
-
entries = await
|
|
16864
|
+
entries = await fs20.promises.readdir(dir, { withFileTypes: true });
|
|
15377
16865
|
} catch {
|
|
15378
16866
|
return;
|
|
15379
16867
|
}
|
|
15380
16868
|
for (const entry of entries) {
|
|
15381
|
-
const fullPath =
|
|
16869
|
+
const fullPath = path21.join(dir, entry.name);
|
|
15382
16870
|
if (entry.isDirectory()) {
|
|
15383
16871
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
15384
16872
|
await collectFiles(fullPath, baseDir, sourceName, repoPath, results);
|
|
@@ -15400,19 +16888,19 @@ function fallbackDecode(encodedName) {
|
|
|
15400
16888
|
var ClaudeSource = class {
|
|
15401
16889
|
name = "claude";
|
|
15402
16890
|
async scan() {
|
|
15403
|
-
const baseDir =
|
|
16891
|
+
const baseDir = path21.join(os11.homedir(), ".claude", "projects");
|
|
15404
16892
|
try {
|
|
15405
|
-
await
|
|
16893
|
+
await fs20.promises.access(baseDir);
|
|
15406
16894
|
} catch {
|
|
15407
16895
|
return [];
|
|
15408
16896
|
}
|
|
15409
|
-
const projectDirs = await
|
|
16897
|
+
const projectDirs = await fs20.promises.readdir(baseDir, {
|
|
15410
16898
|
withFileTypes: true
|
|
15411
16899
|
});
|
|
15412
16900
|
const dirEntries = projectDirs.filter((d) => d.isDirectory());
|
|
15413
16901
|
const resultArrays = await Promise.all(
|
|
15414
16902
|
dirEntries.map(async (dir) => {
|
|
15415
|
-
const projectPath =
|
|
16903
|
+
const projectPath = path21.join(baseDir, dir.name);
|
|
15416
16904
|
const repoPath = await resolveRepoPath(projectPath) ?? fallbackDecode(dir.name);
|
|
15417
16905
|
const files = [];
|
|
15418
16906
|
await collectFiles(
|
|
@@ -15430,12 +16918,12 @@ var ClaudeSource = class {
|
|
|
15430
16918
|
};
|
|
15431
16919
|
|
|
15432
16920
|
// src/sources/codex.ts
|
|
15433
|
-
import
|
|
15434
|
-
import
|
|
15435
|
-
import
|
|
16921
|
+
import fs21 from "fs";
|
|
16922
|
+
import os12 from "os";
|
|
16923
|
+
import path22 from "path";
|
|
15436
16924
|
import readline3 from "readline";
|
|
15437
|
-
async function
|
|
15438
|
-
const stream =
|
|
16925
|
+
async function parseSessionMeta2(filePath) {
|
|
16926
|
+
const stream = fs21.createReadStream(filePath, { encoding: "utf-8" });
|
|
15439
16927
|
const rl = readline3.createInterface({ input: stream, crlfDelay: Infinity });
|
|
15440
16928
|
try {
|
|
15441
16929
|
for await (const line of rl) {
|
|
@@ -15460,12 +16948,12 @@ async function findJsonlFiles(dir) {
|
|
|
15460
16948
|
async function walk(d) {
|
|
15461
16949
|
let entries;
|
|
15462
16950
|
try {
|
|
15463
|
-
entries = await
|
|
16951
|
+
entries = await fs21.promises.readdir(d, { withFileTypes: true });
|
|
15464
16952
|
} catch {
|
|
15465
16953
|
return;
|
|
15466
16954
|
}
|
|
15467
16955
|
for (const entry of entries) {
|
|
15468
|
-
const full =
|
|
16956
|
+
const full = path22.join(d, entry.name);
|
|
15469
16957
|
if (entry.isDirectory()) {
|
|
15470
16958
|
await walk(full);
|
|
15471
16959
|
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
@@ -15479,11 +16967,11 @@ async function findJsonlFiles(dir) {
|
|
|
15479
16967
|
async function loadHistory(historyPath) {
|
|
15480
16968
|
const map = /* @__PURE__ */ new Map();
|
|
15481
16969
|
try {
|
|
15482
|
-
await
|
|
16970
|
+
await fs21.promises.access(historyPath);
|
|
15483
16971
|
} catch {
|
|
15484
16972
|
return map;
|
|
15485
16973
|
}
|
|
15486
|
-
const stream =
|
|
16974
|
+
const stream = fs21.createReadStream(historyPath, { encoding: "utf-8" });
|
|
15487
16975
|
const rl = readline3.createInterface({ input: stream, crlfDelay: Infinity });
|
|
15488
16976
|
try {
|
|
15489
16977
|
for await (const line of rl) {
|
|
@@ -15510,22 +16998,22 @@ async function loadHistory(historyPath) {
|
|
|
15510
16998
|
var CodexSource = class {
|
|
15511
16999
|
name = "codex";
|
|
15512
17000
|
async scan() {
|
|
15513
|
-
const codexDir =
|
|
15514
|
-
const
|
|
17001
|
+
const codexDir = path22.join(os12.homedir(), ".codex");
|
|
17002
|
+
const sessionsDir2 = path22.join(codexDir, "sessions");
|
|
15515
17003
|
try {
|
|
15516
|
-
await
|
|
17004
|
+
await fs21.promises.access(sessionsDir2);
|
|
15517
17005
|
} catch {
|
|
15518
17006
|
return [];
|
|
15519
17007
|
}
|
|
15520
|
-
const historyPath =
|
|
17008
|
+
const historyPath = path22.join(codexDir, "history.jsonl");
|
|
15521
17009
|
const [jsonlFiles, historyMap] = await Promise.all([
|
|
15522
|
-
findJsonlFiles(
|
|
17010
|
+
findJsonlFiles(sessionsDir2),
|
|
15523
17011
|
loadHistory(historyPath)
|
|
15524
17012
|
]);
|
|
15525
17013
|
const results = [];
|
|
15526
17014
|
const metaResults = await Promise.all(
|
|
15527
17015
|
jsonlFiles.map(async (filePath) => {
|
|
15528
|
-
const meta = await
|
|
17016
|
+
const meta = await parseSessionMeta2(filePath);
|
|
15529
17017
|
return meta ? { filePath, meta } : null;
|
|
15530
17018
|
})
|
|
15531
17019
|
);
|
|
@@ -15540,9 +17028,9 @@ var CodexSource = class {
|
|
|
15540
17028
|
});
|
|
15541
17029
|
const historyLines = historyMap.get(meta.sessionId);
|
|
15542
17030
|
if (historyLines) {
|
|
15543
|
-
const sessionDir =
|
|
15544
|
-
const historyAbsPath =
|
|
15545
|
-
|
|
17031
|
+
const sessionDir = path22.relative(sessionsDir2, path22.dirname(filePath));
|
|
17032
|
+
const historyAbsPath = path22.join(
|
|
17033
|
+
sessionsDir2,
|
|
15546
17034
|
sessionDir,
|
|
15547
17035
|
`history-${meta.sessionId}.jsonl`
|
|
15548
17036
|
);
|
|
@@ -15561,18 +17049,18 @@ var CodexSource = class {
|
|
|
15561
17049
|
};
|
|
15562
17050
|
|
|
15563
17051
|
// src/sources/copilotChat.ts
|
|
15564
|
-
import
|
|
15565
|
-
import
|
|
15566
|
-
import
|
|
17052
|
+
import fs22 from "fs";
|
|
17053
|
+
import os13 from "os";
|
|
17054
|
+
import path23 from "path";
|
|
15567
17055
|
import { fileURLToPath } from "url";
|
|
15568
17056
|
function vsCodeUserDirs() {
|
|
15569
|
-
const home =
|
|
17057
|
+
const home = os13.homedir();
|
|
15570
17058
|
const dirs = [
|
|
15571
|
-
|
|
15572
|
-
|
|
17059
|
+
path23.join(home, "Library", "Application Support", "Code", "User"),
|
|
17060
|
+
path23.join(home, ".config", "Code", "User")
|
|
15573
17061
|
];
|
|
15574
17062
|
if (process.env.APPDATA) {
|
|
15575
|
-
dirs.push(
|
|
17063
|
+
dirs.push(path23.join(process.env.APPDATA, "Code", "User"));
|
|
15576
17064
|
}
|
|
15577
17065
|
return dirs;
|
|
15578
17066
|
}
|
|
@@ -15587,7 +17075,7 @@ function uriToFsPath(uri) {
|
|
|
15587
17075
|
async function readWorkspaceFolder(workspaceJsonPath) {
|
|
15588
17076
|
let raw;
|
|
15589
17077
|
try {
|
|
15590
|
-
raw = await
|
|
17078
|
+
raw = await fs22.promises.readFile(workspaceJsonPath, "utf-8");
|
|
15591
17079
|
} catch {
|
|
15592
17080
|
return null;
|
|
15593
17081
|
}
|
|
@@ -15609,10 +17097,10 @@ var CopilotChatSource = class {
|
|
|
15609
17097
|
async scan() {
|
|
15610
17098
|
const results = [];
|
|
15611
17099
|
for (const userDir of vsCodeUserDirs()) {
|
|
15612
|
-
const workspaceStorage =
|
|
17100
|
+
const workspaceStorage = path23.join(userDir, "workspaceStorage");
|
|
15613
17101
|
let hashDirs;
|
|
15614
17102
|
try {
|
|
15615
|
-
hashDirs = await
|
|
17103
|
+
hashDirs = await fs22.promises.readdir(workspaceStorage, {
|
|
15616
17104
|
withFileTypes: true
|
|
15617
17105
|
});
|
|
15618
17106
|
} catch {
|
|
@@ -15620,22 +17108,22 @@ var CopilotChatSource = class {
|
|
|
15620
17108
|
}
|
|
15621
17109
|
for (const hash of hashDirs) {
|
|
15622
17110
|
if (!hash.isDirectory()) continue;
|
|
15623
|
-
const wsRoot =
|
|
15624
|
-
const transcriptsDir =
|
|
17111
|
+
const wsRoot = path23.join(workspaceStorage, hash.name);
|
|
17112
|
+
const transcriptsDir = path23.join(
|
|
15625
17113
|
wsRoot,
|
|
15626
17114
|
"GitHub.copilot-chat",
|
|
15627
17115
|
"transcripts"
|
|
15628
17116
|
);
|
|
15629
17117
|
let transcriptEntries;
|
|
15630
17118
|
try {
|
|
15631
|
-
transcriptEntries = await
|
|
17119
|
+
transcriptEntries = await fs22.promises.readdir(transcriptsDir, {
|
|
15632
17120
|
withFileTypes: true
|
|
15633
17121
|
});
|
|
15634
17122
|
} catch {
|
|
15635
17123
|
continue;
|
|
15636
17124
|
}
|
|
15637
17125
|
const repoPath = await readWorkspaceFolder(
|
|
15638
|
-
|
|
17126
|
+
path23.join(wsRoot, "workspace.json")
|
|
15639
17127
|
);
|
|
15640
17128
|
if (!repoPath) continue;
|
|
15641
17129
|
for (const entry of transcriptEntries) {
|
|
@@ -15643,7 +17131,7 @@ var CopilotChatSource = class {
|
|
|
15643
17131
|
const sessionId = entry.name.slice(0, -".jsonl".length);
|
|
15644
17132
|
results.push({
|
|
15645
17133
|
sourceName: this.name,
|
|
15646
|
-
absolutePath:
|
|
17134
|
+
absolutePath: path23.join(transcriptsDir, entry.name),
|
|
15647
17135
|
repoPath,
|
|
15648
17136
|
metadata: { sessionId }
|
|
15649
17137
|
});
|
|
@@ -15683,7 +17171,7 @@ function reportRedactionStats(noun, stats) {
|
|
|
15683
17171
|
async function filterByTimeRange(group, range) {
|
|
15684
17172
|
const results = await Promise.all(
|
|
15685
17173
|
group.files.map(
|
|
15686
|
-
(f) =>
|
|
17174
|
+
(f) => fs23.promises.stat(f.absolutePath).then((s) => ({ file: f, stat: s })).catch(() => null)
|
|
15687
17175
|
)
|
|
15688
17176
|
);
|
|
15689
17177
|
const filtered = [];
|
|
@@ -15710,10 +17198,10 @@ async function runInteractive() {
|
|
|
15710
17198
|
s.start(`Scanning ${source.name} logs...`);
|
|
15711
17199
|
const allFiles = await source.scan();
|
|
15712
17200
|
const allGroups = await mergeByRepo(allFiles);
|
|
15713
|
-
const repoRoot =
|
|
17201
|
+
const repoRoot = path24.resolve(repo.root);
|
|
15714
17202
|
const matching = allGroups.filter((g) => {
|
|
15715
|
-
const resolved =
|
|
15716
|
-
return resolved === repoRoot || resolved.startsWith(repoRoot +
|
|
17203
|
+
const resolved = path24.resolve(g.repoPath);
|
|
17204
|
+
return resolved === repoRoot || resolved.startsWith(repoRoot + path24.sep);
|
|
15717
17205
|
});
|
|
15718
17206
|
if (matching.length === 0) {
|
|
15719
17207
|
s.stop(`No ${source.name} logs found for ${repo.name}.`);
|
|
@@ -15744,7 +17232,7 @@ async function runInteractive() {
|
|
|
15744
17232
|
}
|
|
15745
17233
|
}
|
|
15746
17234
|
const envFileNames = await discoverEnvFiles(repoRoot);
|
|
15747
|
-
const envFilePaths = envFileNames.map((n) =>
|
|
17235
|
+
const envFilePaths = envFileNames.map((n) => path24.join(repoRoot, n));
|
|
15748
17236
|
const additionalFiles = await promptSecretFiles(envFileNames);
|
|
15749
17237
|
const secretResult = await collectSecrets(
|
|
15750
17238
|
repoRoot,
|