hillclimb 0.7.0 → 0.8.1
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 +2254 -606
- 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
|
);
|
|
@@ -11476,12 +12040,10 @@ function expectedKinds(tool, eventKind, payload) {
|
|
|
11476
12040
|
}
|
|
11477
12041
|
return /* @__PURE__ */ new Set(["agent", "git"]);
|
|
11478
12042
|
}
|
|
11479
|
-
|
|
11480
|
-
|
|
11481
|
-
if (!transcriptPath) return {};
|
|
11482
|
-
const resolved = path12.resolve(transcriptPath);
|
|
12043
|
+
var TRANSCRIPT_STAT_ENV = "HILLCLIMB_DEBUG_LOG_TRANSCRIPT_STAT";
|
|
12044
|
+
async function statTranscriptFingerprint(resolved) {
|
|
11483
12045
|
try {
|
|
11484
|
-
const stat = await
|
|
12046
|
+
const stat = await fs12.promises.stat(resolved);
|
|
11485
12047
|
return {
|
|
11486
12048
|
transcriptPath: resolved,
|
|
11487
12049
|
transcriptMtimeMs: stat.mtimeMs,
|
|
@@ -11491,9 +12053,47 @@ async function transcriptFingerprint(payload) {
|
|
|
11491
12053
|
return { transcriptPath: resolved };
|
|
11492
12054
|
}
|
|
11493
12055
|
}
|
|
12056
|
+
function pinnedTranscriptFingerprint(resolved) {
|
|
12057
|
+
const raw = process.env[TRANSCRIPT_STAT_ENV];
|
|
12058
|
+
if (!raw) return null;
|
|
12059
|
+
try {
|
|
12060
|
+
const pinned = JSON.parse(raw);
|
|
12061
|
+
return pinned.transcriptPath === resolved ? pinned : null;
|
|
12062
|
+
} catch {
|
|
12063
|
+
return null;
|
|
12064
|
+
}
|
|
12065
|
+
}
|
|
12066
|
+
async function transcriptFingerprint(payload) {
|
|
12067
|
+
const transcriptPath = stringOrNull(payload.transcript_path);
|
|
12068
|
+
if (!transcriptPath) return {};
|
|
12069
|
+
const resolved = path13.resolve(transcriptPath);
|
|
12070
|
+
return pinnedTranscriptFingerprint(resolved) ?? await statTranscriptFingerprint(resolved);
|
|
12071
|
+
}
|
|
12072
|
+
async function captureDebugLogParentEnv(rawPayload) {
|
|
12073
|
+
let payload;
|
|
12074
|
+
try {
|
|
12075
|
+
payload = JSON.parse(rawPayload);
|
|
12076
|
+
} catch {
|
|
12077
|
+
return {};
|
|
12078
|
+
}
|
|
12079
|
+
if (typeof payload !== "object" || payload === null) return {};
|
|
12080
|
+
const transcriptPath = stringOrNull(payload.transcript_path);
|
|
12081
|
+
if (classifyHookEvent(payload.hook_event_name) !== "stop" || stringOrNull(payload.turn_id) !== null || !transcriptPath) {
|
|
12082
|
+
return {};
|
|
12083
|
+
}
|
|
12084
|
+
return {
|
|
12085
|
+
[TRANSCRIPT_STAT_ENV]: JSON.stringify(
|
|
12086
|
+
await statTranscriptFingerprint(path13.resolve(transcriptPath))
|
|
12087
|
+
)
|
|
12088
|
+
};
|
|
12089
|
+
}
|
|
12090
|
+
async function applyDebugLogParentEnv(env, rawPayload) {
|
|
12091
|
+
delete env[TRANSCRIPT_STAT_ENV];
|
|
12092
|
+
Object.assign(env, await captureDebugLogParentEnv(rawPayload));
|
|
12093
|
+
}
|
|
11494
12094
|
async function eventContext(tool, payload) {
|
|
11495
|
-
const
|
|
11496
|
-
if (!
|
|
12095
|
+
const eventKind2 = classifyHookEvent(payload.hook_event_name);
|
|
12096
|
+
if (!eventKind2) return null;
|
|
11497
12097
|
const cwd = resolveCwd(payload);
|
|
11498
12098
|
if (!cwd) return null;
|
|
11499
12099
|
const project = await findProjectForCwd(cwd);
|
|
@@ -11504,15 +12104,15 @@ async function eventContext(tool, payload) {
|
|
|
11504
12104
|
if (!sessionId && !turnId && !transcriptPath) {
|
|
11505
12105
|
return null;
|
|
11506
12106
|
}
|
|
11507
|
-
const expected = expectedKinds(tool,
|
|
11508
|
-
const eventNonce =
|
|
12107
|
+
const expected = expectedKinds(tool, eventKind2, payload);
|
|
12108
|
+
const eventNonce = eventKind2 === "stop" && expected.size === 1 && expected.has("git") && !turnId && !transcriptPath ? crypto2.randomBytes(8).toString("hex") : null;
|
|
11509
12109
|
const fingerprint = {
|
|
11510
12110
|
schema: "debug-log-event-v1",
|
|
11511
12111
|
apiBaseUrl: project.config.apiBaseUrl,
|
|
11512
12112
|
projectId: project.config.projectId,
|
|
11513
12113
|
repoRoot: project.repoRoot,
|
|
11514
12114
|
tool,
|
|
11515
|
-
eventKind,
|
|
12115
|
+
eventKind: eventKind2,
|
|
11516
12116
|
hookEventName: payload.hook_event_name ?? null,
|
|
11517
12117
|
sessionId,
|
|
11518
12118
|
conversationId: stringOrNull(payload.conversation_id),
|
|
@@ -11521,15 +12121,19 @@ async function eventContext(tool, payload) {
|
|
|
11521
12121
|
// Only stop events without stable turn IDs need the transcript to
|
|
11522
12122
|
// disambiguate. When a turn_id exists, including mutable transcript
|
|
11523
12123
|
// mtime/size can split agent+git completions for the same logical event.
|
|
11524
|
-
|
|
12124
|
+
// Workers prefer the stat their hook parent pinned via
|
|
12125
|
+
// TRANSCRIPT_STAT_ENV, so completion-time skew between the agent and git
|
|
12126
|
+
// workers no longer splits the eventId; the residual window is the
|
|
12127
|
+
// parents' dispatch skew (see captureDebugLogParentEnv).
|
|
12128
|
+
...eventKind2 === "stop" && !turnId ? await transcriptFingerprint(payload) : {}
|
|
11525
12129
|
};
|
|
11526
|
-
const eventId =
|
|
12130
|
+
const eventId = crypto2.createHash("sha256").update(JSON.stringify(fingerprint)).digest("hex").slice(0, 32);
|
|
11527
12131
|
return {
|
|
11528
12132
|
eventId,
|
|
11529
12133
|
repoRoot: project.repoRoot,
|
|
11530
12134
|
config: project.config,
|
|
11531
12135
|
tool,
|
|
11532
|
-
eventKind,
|
|
12136
|
+
eventKind: eventKind2,
|
|
11533
12137
|
hookEventName: payload.hook_event_name ?? null,
|
|
11534
12138
|
sessionId,
|
|
11535
12139
|
expectedKinds: expected
|
|
@@ -11537,7 +12141,7 @@ async function eventContext(tool, payload) {
|
|
|
11537
12141
|
}
|
|
11538
12142
|
async function readState(eventId) {
|
|
11539
12143
|
try {
|
|
11540
|
-
const raw = await
|
|
12144
|
+
const raw = await fs12.promises.readFile(stateFile(eventId), "utf-8");
|
|
11541
12145
|
const parsed = JSON.parse(raw);
|
|
11542
12146
|
if (parsed.schemaVersion !== CURRENT_SCHEMA_VERSION) return null;
|
|
11543
12147
|
return parsed;
|
|
@@ -11546,37 +12150,132 @@ async function readState(eventId) {
|
|
|
11546
12150
|
}
|
|
11547
12151
|
}
|
|
11548
12152
|
async function writeState(state) {
|
|
11549
|
-
await
|
|
12153
|
+
await fs12.promises.mkdir(stateDir(), { recursive: true, mode: 448 });
|
|
11550
12154
|
const file = stateFile(state.eventId);
|
|
11551
12155
|
const tmp = `${file}.tmp`;
|
|
11552
|
-
await
|
|
12156
|
+
await fs12.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
|
|
11553
12157
|
mode: 384
|
|
11554
12158
|
});
|
|
11555
|
-
await
|
|
12159
|
+
await fs12.promises.rename(tmp, file);
|
|
11556
12160
|
}
|
|
11557
|
-
async function
|
|
11558
|
-
|
|
11559
|
-
|
|
12161
|
+
async function readSessionState(key) {
|
|
12162
|
+
try {
|
|
12163
|
+
const raw = await fs12.promises.readFile(
|
|
12164
|
+
stateFile(sessionStateId(key)),
|
|
12165
|
+
"utf-8"
|
|
12166
|
+
);
|
|
12167
|
+
const parsed = JSON.parse(raw);
|
|
12168
|
+
if (parsed.schemaVersion !== CURRENT_SCHEMA_VERSION || parsed.sessionKey !== key || typeof parsed.contributionId !== "string") {
|
|
12169
|
+
return null;
|
|
12170
|
+
}
|
|
12171
|
+
return parsed;
|
|
12172
|
+
} catch {
|
|
12173
|
+
return null;
|
|
12174
|
+
}
|
|
12175
|
+
}
|
|
12176
|
+
async function writeSessionState(state) {
|
|
12177
|
+
await fs12.promises.mkdir(stateDir(), { recursive: true, mode: 448 });
|
|
12178
|
+
const file = stateFile(sessionStateId(state.sessionKey));
|
|
12179
|
+
const tmp = `${file}.tmp`;
|
|
12180
|
+
await fs12.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
|
|
12181
|
+
mode: 384
|
|
12182
|
+
});
|
|
12183
|
+
await fs12.promises.rename(tmp, file);
|
|
12184
|
+
}
|
|
12185
|
+
async function deleteSessionState(key) {
|
|
12186
|
+
try {
|
|
12187
|
+
await fs12.promises.unlink(stateFile(sessionStateId(key)));
|
|
12188
|
+
} catch (err) {
|
|
12189
|
+
if (err.code !== "ENOENT") throw err;
|
|
12190
|
+
}
|
|
12191
|
+
}
|
|
12192
|
+
async function tryCreateLock(lockId) {
|
|
12193
|
+
try {
|
|
12194
|
+
const fd = await fs12.promises.open(
|
|
12195
|
+
lockFile(lockId),
|
|
12196
|
+
fs12.constants.O_CREAT | fs12.constants.O_EXCL | fs12.constants.O_WRONLY
|
|
12197
|
+
);
|
|
11560
12198
|
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
12199
|
await fd.write(String(process.pid));
|
|
12200
|
+
} finally {
|
|
11566
12201
|
await fd.close();
|
|
12202
|
+
}
|
|
12203
|
+
return true;
|
|
12204
|
+
} catch (err) {
|
|
12205
|
+
if (err.code === "EEXIST") return false;
|
|
12206
|
+
throw err;
|
|
12207
|
+
}
|
|
12208
|
+
}
|
|
12209
|
+
async function acquireLock(eventId) {
|
|
12210
|
+
await fs12.promises.mkdir(stateDir(), { recursive: true, mode: 448 });
|
|
12211
|
+
for (let i = 0; i < LOCK_RETRIES; i++) {
|
|
12212
|
+
if (await tryCreateLock(eventId)) return;
|
|
12213
|
+
if (await reapLockIfStale(lockFile(eventId), {
|
|
12214
|
+
maxAgeMs: STALE_LOCK_TTL_MS
|
|
12215
|
+
}) && await tryCreateLock(eventId)) {
|
|
11567
12216
|
return;
|
|
11568
|
-
}
|
|
11569
|
-
|
|
11570
|
-
|
|
12217
|
+
}
|
|
12218
|
+
if (i < LOCK_RETRIES - 1) await sleep2(LOCK_RETRY_DELAY_MS);
|
|
12219
|
+
}
|
|
12220
|
+
throw new Error(
|
|
12221
|
+
`Failed to acquire debug-log lock after ${DEFAULT_LOCK_WAIT_MS}ms (lock=${lockFile(eventId)})`
|
|
12222
|
+
);
|
|
12223
|
+
}
|
|
12224
|
+
async function tryAcquireLock(eventId, now) {
|
|
12225
|
+
await fs12.promises.mkdir(stateDir(), { recursive: true, mode: 448 });
|
|
12226
|
+
if (await tryCreateLock(eventId)) return true;
|
|
12227
|
+
if (await reapLockIfStale(lockFile(eventId), {
|
|
12228
|
+
maxAgeMs: STALE_LOCK_TTL_MS,
|
|
12229
|
+
now
|
|
12230
|
+
})) {
|
|
12231
|
+
return tryCreateLock(eventId);
|
|
12232
|
+
}
|
|
12233
|
+
return false;
|
|
12234
|
+
}
|
|
12235
|
+
async function sweepStaleDebugLogState(now = Date.now()) {
|
|
12236
|
+
let entries;
|
|
12237
|
+
try {
|
|
12238
|
+
entries = await fs12.promises.readdir(stateDir(), { withFileTypes: true });
|
|
12239
|
+
} catch {
|
|
12240
|
+
return;
|
|
12241
|
+
}
|
|
12242
|
+
for (const entry of entries) {
|
|
12243
|
+
if (!entry.isFile()) continue;
|
|
12244
|
+
const file = path13.join(stateDir(), entry.name);
|
|
12245
|
+
try {
|
|
12246
|
+
if (entry.name.endsWith(".lock")) {
|
|
12247
|
+
await reapLockIfStale(file, {
|
|
12248
|
+
maxAgeMs: STALE_LOCK_TTL_MS,
|
|
12249
|
+
now
|
|
12250
|
+
});
|
|
11571
12251
|
continue;
|
|
11572
12252
|
}
|
|
11573
|
-
|
|
12253
|
+
if (entry.name.startsWith("session-") && entry.name.endsWith(".json")) {
|
|
12254
|
+
const stat2 = await fs12.promises.stat(file);
|
|
12255
|
+
if (now - stat2.mtimeMs <= SESSION_STATE_TTL_MS) continue;
|
|
12256
|
+
const sessionLockId = entry.name.slice(0, -".json".length);
|
|
12257
|
+
if (!await tryAcquireLock(sessionLockId, now)) continue;
|
|
12258
|
+
try {
|
|
12259
|
+
const lockedStat = await fs12.promises.stat(file);
|
|
12260
|
+
if (now - lockedStat.mtimeMs > SESSION_STATE_TTL_MS) {
|
|
12261
|
+
await fs12.promises.unlink(file);
|
|
12262
|
+
}
|
|
12263
|
+
} finally {
|
|
12264
|
+
await releaseLock(sessionLockId);
|
|
12265
|
+
}
|
|
12266
|
+
continue;
|
|
12267
|
+
}
|
|
12268
|
+
const stat = await fs12.promises.stat(file);
|
|
12269
|
+
if (now - stat.mtimeMs > EVENT_STATE_TTL_MS) {
|
|
12270
|
+
await fs12.promises.unlink(file);
|
|
12271
|
+
}
|
|
12272
|
+
} catch {
|
|
11574
12273
|
}
|
|
11575
12274
|
}
|
|
11576
12275
|
}
|
|
11577
12276
|
async function releaseLock(eventId) {
|
|
11578
12277
|
try {
|
|
11579
|
-
await
|
|
12278
|
+
await fs12.promises.unlink(lockFile(eventId));
|
|
11580
12279
|
} catch {
|
|
11581
12280
|
}
|
|
11582
12281
|
}
|
|
@@ -11592,7 +12291,7 @@ function initialState(ctx, now) {
|
|
|
11592
12291
|
eventKind: ctx.eventKind,
|
|
11593
12292
|
hookEventName: ctx.hookEventName,
|
|
11594
12293
|
sessionId: ctx.sessionId,
|
|
11595
|
-
logDate:
|
|
12294
|
+
logDate: path13.basename(todayLogPath(), ".log"),
|
|
11596
12295
|
firstSeenAt: now.toISOString()
|
|
11597
12296
|
};
|
|
11598
12297
|
}
|
|
@@ -11654,7 +12353,7 @@ async function waitForExpectedKinds(ctx, state) {
|
|
|
11654
12353
|
}
|
|
11655
12354
|
async function buildMiddleware(repoRoot) {
|
|
11656
12355
|
const envFileNames = await discoverEnvFiles(repoRoot);
|
|
11657
|
-
const envFilePaths = envFileNames.map((n) =>
|
|
12356
|
+
const envFilePaths = envFileNames.map((n) => path13.join(repoRoot, n));
|
|
11658
12357
|
const secretResult = await collectSecrets(repoRoot, envFilePaths, []);
|
|
11659
12358
|
const middleware2 = [];
|
|
11660
12359
|
if (secretResult.values.size > 0) {
|
|
@@ -11663,21 +12362,21 @@ async function buildMiddleware(repoRoot) {
|
|
|
11663
12362
|
middleware2.push(new PatternRedactMiddleware());
|
|
11664
12363
|
return middleware2;
|
|
11665
12364
|
}
|
|
11666
|
-
async function uploadDebugLog(ctx, state) {
|
|
12365
|
+
async function uploadDebugLog(ctx, state, sessionState, onContributionCreated) {
|
|
11667
12366
|
const logPath = todayLogPath();
|
|
11668
12367
|
let content;
|
|
11669
12368
|
try {
|
|
11670
|
-
content = await
|
|
12369
|
+
content = await fs12.promises.readFile(logPath);
|
|
11671
12370
|
} catch (err) {
|
|
11672
12371
|
appendLog(
|
|
11673
12372
|
"warn",
|
|
11674
12373
|
`debug-logs: skipped upload; log file not readable (${logPath}): ${err instanceof Error ? err.message : String(err)}`
|
|
11675
12374
|
);
|
|
11676
|
-
return
|
|
12375
|
+
return { kind: "skipped" };
|
|
11677
12376
|
}
|
|
11678
12377
|
if (content.byteLength === 0) {
|
|
11679
12378
|
appendLog("info", "debug-logs: skipped upload; log file is empty");
|
|
11680
|
-
return
|
|
12379
|
+
return { kind: "skipped" };
|
|
11681
12380
|
}
|
|
11682
12381
|
const identity = await loadIdentity(ctx.config.apiBaseUrl);
|
|
11683
12382
|
if (!identity) {
|
|
@@ -11685,7 +12384,7 @@ async function uploadDebugLog(ctx, state) {
|
|
|
11685
12384
|
"warn",
|
|
11686
12385
|
`debug-logs: skipped upload; no saved login for ${ctx.config.apiBaseUrl}`
|
|
11687
12386
|
);
|
|
11688
|
-
return
|
|
12387
|
+
return { kind: "skipped" };
|
|
11689
12388
|
}
|
|
11690
12389
|
const now = /* @__PURE__ */ new Date();
|
|
11691
12390
|
appendLog(
|
|
@@ -11700,7 +12399,7 @@ async function uploadDebugLog(ctx, state) {
|
|
|
11700
12399
|
};
|
|
11701
12400
|
const group = {
|
|
11702
12401
|
repoPath: ctx.repoRoot,
|
|
11703
|
-
label:
|
|
12402
|
+
label: path13.basename(ctx.repoRoot),
|
|
11704
12403
|
files: [sourceFile],
|
|
11705
12404
|
sourceNames: [DEBUG_LOGS_SLUG],
|
|
11706
12405
|
lastModified: now
|
|
@@ -11708,27 +12407,44 @@ async function uploadDebugLog(ctx, state) {
|
|
|
11708
12407
|
const shortSession = (ctx.sessionId ?? ctx.eventId).slice(0, 12);
|
|
11709
12408
|
const epochSeconds = formatEpochSeconds(now);
|
|
11710
12409
|
const label = toolLabel(ctx.tool);
|
|
11711
|
-
const
|
|
11712
|
-
|
|
11713
|
-
|
|
11714
|
-
|
|
11715
|
-
|
|
11716
|
-
|
|
11717
|
-
|
|
11718
|
-
|
|
12410
|
+
const sessionDescription = ctx.sessionId ? {
|
|
12411
|
+
contributionTitle: `${label} debug logs ${shortSession}`,
|
|
12412
|
+
contributionBody: [
|
|
12413
|
+
`Session ID: ${ctx.sessionId}`,
|
|
12414
|
+
`Tool: ${label}`,
|
|
12415
|
+
`Repo: ${ctx.repoRoot}`,
|
|
12416
|
+
"Log snapshots: machine-wide Hillclimb daily log snapshots captured at hook events; may include entries from other sessions on this machine"
|
|
12417
|
+
].join("\n")
|
|
12418
|
+
} : {
|
|
11719
12419
|
contributionTitle: `${label} debug log ${shortSession} - ${epochSeconds}`,
|
|
11720
12420
|
contributionBody: [
|
|
11721
|
-
|
|
12421
|
+
"Session ID: <none>",
|
|
11722
12422
|
`Tool: ${label}`,
|
|
11723
12423
|
`Event: ${ctx.hookEventName ?? ctx.eventKind}`,
|
|
11724
12424
|
`Repo: ${ctx.repoRoot}`,
|
|
11725
|
-
`Log: ${
|
|
12425
|
+
`Log: ${path13.basename(logPath)}`,
|
|
11726
12426
|
`Agent done: ${state.agentDoneAt ?? "<not observed>"}`,
|
|
11727
12427
|
`Git done: ${state.gitDoneAt ?? "<not observed>"}`,
|
|
11728
12428
|
`Uploaded: ${now.toISOString()}`
|
|
11729
|
-
].join("\n")
|
|
11730
|
-
|
|
11731
|
-
|
|
12429
|
+
].join("\n")
|
|
12430
|
+
};
|
|
12431
|
+
const client = new PlatformClient(
|
|
12432
|
+
ctx.config.apiBaseUrl,
|
|
12433
|
+
identity.sessionCookie
|
|
12434
|
+
);
|
|
12435
|
+
let attemptContributionId = sessionState?.contributionId;
|
|
12436
|
+
const output = new PlatformUploadOutput({
|
|
12437
|
+
client,
|
|
12438
|
+
projectId: ctx.config.projectId,
|
|
12439
|
+
contributionTypeSlug: DEBUG_LOGS_SLUG,
|
|
12440
|
+
...sessionDescription,
|
|
12441
|
+
zipFilename: `hillclimb-debug-log-${sanitize(ctx.tool)}-${sanitize(shortSession)}-${now.getTime()}-${ctx.eventId.slice(0, 8)}-${crypto2.randomBytes(6).toString("hex")}.zip`,
|
|
12442
|
+
autoSubmit: !sessionState?.firstUploadSubmittedAt,
|
|
12443
|
+
existingContributionId: sessionState?.contributionId,
|
|
12444
|
+
onContributionCreated: onContributionCreated ? async (contributionId) => {
|
|
12445
|
+
attemptContributionId = contributionId;
|
|
12446
|
+
await onContributionCreated(contributionId);
|
|
12447
|
+
} : void 0
|
|
11732
12448
|
});
|
|
11733
12449
|
try {
|
|
11734
12450
|
const contributionId = await runPipeline(
|
|
@@ -11739,37 +12455,90 @@ async function uploadDebugLog(ctx, state) {
|
|
|
11739
12455
|
);
|
|
11740
12456
|
appendLog(
|
|
11741
12457
|
"info",
|
|
11742
|
-
`debug-logs: uploaded ${
|
|
12458
|
+
`debug-logs: uploaded ${path13.basename(logPath)} to project ${ctx.config.projectSlug} (${ctx.config.projectId}) as contribution ${contributionId}`
|
|
11743
12459
|
);
|
|
11744
|
-
return contributionId;
|
|
12460
|
+
return { kind: "uploaded", contributionId };
|
|
11745
12461
|
} catch (err) {
|
|
12462
|
+
if (attemptContributionId && err instanceof PlatformError && (err.status === 404 && err.code === "CONTRIBUTION_NOT_FOUND" || err.status === 409 && err.code === "CONTRIBUTION_UPLOAD_NOT_UPLOADED")) {
|
|
12463
|
+
appendLog(
|
|
12464
|
+
"warn",
|
|
12465
|
+
`debug-logs: contribution ${attemptContributionId} abandoned after ${err.code}; the next hook event will create a fresh contribution`
|
|
12466
|
+
);
|
|
12467
|
+
return { kind: "abandon-session" };
|
|
12468
|
+
}
|
|
11746
12469
|
if (err instanceof PlatformError && err.status === 404) {
|
|
11747
12470
|
appendLog(
|
|
11748
12471
|
"warn",
|
|
11749
12472
|
"debug-logs: upload skipped; platform does not have the debug-logs contribution type yet"
|
|
11750
12473
|
);
|
|
11751
|
-
return
|
|
12474
|
+
return { kind: "skipped" };
|
|
11752
12475
|
}
|
|
11753
12476
|
appendLog(
|
|
11754
12477
|
"error",
|
|
11755
12478
|
`debug-logs: upload failed: ${err instanceof Error ? err.message : String(err)}`
|
|
11756
12479
|
);
|
|
11757
|
-
return
|
|
12480
|
+
return { kind: "skipped" };
|
|
11758
12481
|
}
|
|
11759
12482
|
}
|
|
11760
12483
|
async function uploadOnce(ctx, state) {
|
|
11761
12484
|
await acquireLock(ctx.eventId);
|
|
11762
12485
|
try {
|
|
11763
12486
|
const latest = await readState(ctx.eventId) ?? state;
|
|
11764
|
-
if (latest.uploadedAt) return;
|
|
11765
|
-
const
|
|
11766
|
-
if (!
|
|
11767
|
-
|
|
11768
|
-
|
|
11769
|
-
|
|
11770
|
-
|
|
11771
|
-
|
|
11772
|
-
|
|
12487
|
+
if (latest.uploadedAt || latest.sessionMappingAbandonedAt) return;
|
|
12488
|
+
const key = sessionKey(ctx);
|
|
12489
|
+
if (!key) {
|
|
12490
|
+
const result = await uploadDebugLog(ctx, latest, null);
|
|
12491
|
+
if (result.kind !== "uploaded") return;
|
|
12492
|
+
await writeState({
|
|
12493
|
+
...latest,
|
|
12494
|
+
uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12495
|
+
uploadContributionId: result.contributionId
|
|
12496
|
+
});
|
|
12497
|
+
return;
|
|
12498
|
+
}
|
|
12499
|
+
const sessionLockId = sessionStateId(key);
|
|
12500
|
+
await acquireLock(sessionLockId);
|
|
12501
|
+
try {
|
|
12502
|
+
const currentSession = await readSessionState(key);
|
|
12503
|
+
const result = await uploadDebugLog(
|
|
12504
|
+
ctx,
|
|
12505
|
+
latest,
|
|
12506
|
+
currentSession,
|
|
12507
|
+
async (contributionId) => {
|
|
12508
|
+
await writeSessionState({
|
|
12509
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
12510
|
+
sessionKey: key,
|
|
12511
|
+
contributionId,
|
|
12512
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
12513
|
+
});
|
|
12514
|
+
}
|
|
12515
|
+
);
|
|
12516
|
+
if (result.kind === "abandon-session") {
|
|
12517
|
+
await deleteSessionState(key);
|
|
12518
|
+
await writeState({
|
|
12519
|
+
...latest,
|
|
12520
|
+
sessionMappingAbandonedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
12521
|
+
});
|
|
12522
|
+
return;
|
|
12523
|
+
}
|
|
12524
|
+
if (result.kind !== "uploaded") return;
|
|
12525
|
+
const uploadedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
12526
|
+
const persisted = await readSessionState(key);
|
|
12527
|
+
if (persisted?.contributionId === result.contributionId) {
|
|
12528
|
+
await writeSessionState({
|
|
12529
|
+
...persisted,
|
|
12530
|
+
firstUploadSubmittedAt: persisted.firstUploadSubmittedAt ?? uploadedAt,
|
|
12531
|
+
lastUploadAt: uploadedAt
|
|
12532
|
+
});
|
|
12533
|
+
}
|
|
12534
|
+
await writeState({
|
|
12535
|
+
...latest,
|
|
12536
|
+
uploadedAt,
|
|
12537
|
+
uploadContributionId: result.contributionId
|
|
12538
|
+
});
|
|
12539
|
+
} finally {
|
|
12540
|
+
await releaseLock(sessionLockId);
|
|
12541
|
+
}
|
|
11773
12542
|
} finally {
|
|
11774
12543
|
await releaseLock(ctx.eventId);
|
|
11775
12544
|
}
|
|
@@ -11823,7 +12592,8 @@ function buildEpochMeta(params) {
|
|
|
11823
12592
|
transcriptArchivePath: params.transcriptArchivePath,
|
|
11824
12593
|
rawByteOffset: params.cursor.rawByteOffset,
|
|
11825
12594
|
rawPrefixSha256: params.cursor.rawPrefixSha256,
|
|
11826
|
-
recordedAt: new Date(params.recordedAt).toISOString()
|
|
12595
|
+
recordedAt: new Date(params.recordedAt).toISOString(),
|
|
12596
|
+
...params.lineage
|
|
11827
12597
|
};
|
|
11828
12598
|
return Buffer.from(JSON.stringify(meta, null, 2));
|
|
11829
12599
|
}
|
|
@@ -11852,16 +12622,16 @@ async function uploadArtifact(client, contributionId, filename, mimeType, buffer
|
|
|
11852
12622
|
}
|
|
11853
12623
|
|
|
11854
12624
|
// src/transcript-cursor.ts
|
|
11855
|
-
import
|
|
11856
|
-
import
|
|
12625
|
+
import crypto3 from "crypto";
|
|
12626
|
+
import fs13 from "fs";
|
|
11857
12627
|
function sha256OfBuffer(buffer) {
|
|
11858
|
-
return
|
|
12628
|
+
return crypto3.createHash("sha256").update(buffer).digest("hex");
|
|
11859
12629
|
}
|
|
11860
12630
|
async function hashPrefix(filePath, byteLength) {
|
|
11861
|
-
const hash =
|
|
12631
|
+
const hash = crypto3.createHash("sha256");
|
|
11862
12632
|
if (byteLength === 0) return hash;
|
|
11863
12633
|
await new Promise((resolve, reject) => {
|
|
11864
|
-
const stream =
|
|
12634
|
+
const stream = fs13.createReadStream(filePath, {
|
|
11865
12635
|
start: 0,
|
|
11866
12636
|
end: byteLength - 1
|
|
11867
12637
|
});
|
|
@@ -11872,7 +12642,7 @@ async function hashPrefix(filePath, byteLength) {
|
|
|
11872
12642
|
return hash;
|
|
11873
12643
|
}
|
|
11874
12644
|
async function readRange(filePath, start, end) {
|
|
11875
|
-
const fd = await
|
|
12645
|
+
const fd = await fs13.promises.open(filePath, "r");
|
|
11876
12646
|
try {
|
|
11877
12647
|
const buffer = Buffer.alloc(end - start);
|
|
11878
12648
|
let filled = 0;
|
|
@@ -11892,7 +12662,7 @@ async function readRange(filePath, start, end) {
|
|
|
11892
12662
|
}
|
|
11893
12663
|
}
|
|
11894
12664
|
async function evaluateTranscript(filePath, cursor) {
|
|
11895
|
-
const stat = await
|
|
12665
|
+
const stat = await fs13.promises.stat(filePath);
|
|
11896
12666
|
if (stat.size < cursor.rawByteOffset) return { kind: "truncate" };
|
|
11897
12667
|
const prefixHash = await hashPrefix(filePath, cursor.rawByteOffset);
|
|
11898
12668
|
const continuation = prefixHash.copy();
|
|
@@ -11927,12 +12697,12 @@ function truncateAtLastNewline(buffer) {
|
|
|
11927
12697
|
}
|
|
11928
12698
|
async function cursorMatchesFile(filePath, cursor) {
|
|
11929
12699
|
try {
|
|
11930
|
-
const stat = await
|
|
12700
|
+
const stat = await fs13.promises.stat(filePath);
|
|
11931
12701
|
if (stat.size < cursor.rawByteOffset) return false;
|
|
11932
12702
|
const prefixHash = await hashPrefix(filePath, cursor.rawByteOffset);
|
|
11933
12703
|
if (prefixHash.digest("hex") !== cursor.rawPrefixSha256) return false;
|
|
11934
12704
|
if (cursor.rawByteOffset === 0) return true;
|
|
11935
|
-
const fd = await
|
|
12705
|
+
const fd = await fs13.promises.open(filePath, "r");
|
|
11936
12706
|
try {
|
|
11937
12707
|
const byte = Buffer.alloc(1);
|
|
11938
12708
|
const { bytesRead } = await fd.read(byte, 0, 1, cursor.rawByteOffset - 1);
|
|
@@ -11946,20 +12716,20 @@ async function cursorMatchesFile(filePath, cursor) {
|
|
|
11946
12716
|
}
|
|
11947
12717
|
|
|
11948
12718
|
// src/upload-state.ts
|
|
11949
|
-
import
|
|
11950
|
-
import
|
|
11951
|
-
import
|
|
11952
|
-
import
|
|
12719
|
+
import crypto4 from "crypto";
|
|
12720
|
+
import fs14 from "fs";
|
|
12721
|
+
import os6 from "os";
|
|
12722
|
+
import path14 from "path";
|
|
11953
12723
|
var CURRENT_SCHEMA_VERSION2 = 1;
|
|
11954
|
-
var DEFAULT_STATE_DIR =
|
|
11955
|
-
|
|
12724
|
+
var DEFAULT_STATE_DIR = path14.join(
|
|
12725
|
+
os6.homedir(),
|
|
11956
12726
|
".hillclimb",
|
|
11957
12727
|
"agent-uploads"
|
|
11958
12728
|
);
|
|
11959
|
-
var
|
|
12729
|
+
var DEFAULT_LOCK_WAIT_MS2 = 5 * 60 * 1e3;
|
|
11960
12730
|
var DEFAULT_LOCK_RETRY_DELAY_MS = 500;
|
|
11961
12731
|
var DEFAULT_STATE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
11962
|
-
var
|
|
12732
|
+
var STALE_LOCK_TTL_MS2 = 60 * 60 * 1e3;
|
|
11963
12733
|
function stateDir2() {
|
|
11964
12734
|
return process.env.HILLCLIMB_UPLOAD_STATE_DIR ?? DEFAULT_STATE_DIR;
|
|
11965
12735
|
}
|
|
@@ -11988,20 +12758,20 @@ function lockRetries() {
|
|
|
11988
12758
|
return Math.max(
|
|
11989
12759
|
1,
|
|
11990
12760
|
Math.ceil(
|
|
11991
|
-
readPositiveEnvMs("HILLCLIMB_UPLOAD_LOCK_WAIT_MS",
|
|
12761
|
+
readPositiveEnvMs("HILLCLIMB_UPLOAD_LOCK_WAIT_MS", DEFAULT_LOCK_WAIT_MS2) / lockRetryDelayMs()
|
|
11992
12762
|
)
|
|
11993
12763
|
);
|
|
11994
12764
|
}
|
|
11995
12765
|
function stateFileFor(repoRoot, tool, sessionId) {
|
|
11996
|
-
const hash =
|
|
11997
|
-
return
|
|
12766
|
+
const hash = crypto4.createHash("sha256").update(`${path14.resolve(repoRoot)}\0${tool}\0${sessionId}`).digest("hex").slice(0, 16);
|
|
12767
|
+
return path14.join(stateDir2(), `${hash}.json`);
|
|
11998
12768
|
}
|
|
11999
12769
|
function lockFileFor(repoRoot, tool, sessionId) {
|
|
12000
12770
|
return `${stateFileFor(repoRoot, tool, sessionId)}.lock`;
|
|
12001
12771
|
}
|
|
12002
12772
|
async function readUploadState(repoRoot, tool, sessionId) {
|
|
12003
12773
|
try {
|
|
12004
|
-
const raw = await
|
|
12774
|
+
const raw = await fs14.promises.readFile(
|
|
12005
12775
|
stateFileFor(repoRoot, tool, sessionId),
|
|
12006
12776
|
"utf-8"
|
|
12007
12777
|
);
|
|
@@ -12014,20 +12784,20 @@ async function readUploadState(repoRoot, tool, sessionId) {
|
|
|
12014
12784
|
}
|
|
12015
12785
|
async function writeUploadState(state) {
|
|
12016
12786
|
const file = stateFileFor(state.repoRoot, state.tool, state.sessionId);
|
|
12017
|
-
await
|
|
12787
|
+
await fs14.promises.mkdir(stateDir2(), { recursive: true, mode: 448 });
|
|
12018
12788
|
const tmp = `${file}.tmp`;
|
|
12019
|
-
await
|
|
12789
|
+
await fs14.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
|
|
12020
12790
|
mode: 384
|
|
12021
12791
|
});
|
|
12022
|
-
await
|
|
12792
|
+
await fs14.promises.rename(tmp, file);
|
|
12023
12793
|
}
|
|
12024
12794
|
async function deleteUploadState(repoRoot, tool, sessionId) {
|
|
12025
12795
|
try {
|
|
12026
|
-
await
|
|
12796
|
+
await fs14.promises.unlink(stateFileFor(repoRoot, tool, sessionId));
|
|
12027
12797
|
} catch {
|
|
12028
12798
|
}
|
|
12029
12799
|
try {
|
|
12030
|
-
await
|
|
12800
|
+
await fs14.promises.unlink(cursorFileFor(repoRoot, tool, sessionId));
|
|
12031
12801
|
} catch {
|
|
12032
12802
|
}
|
|
12033
12803
|
}
|
|
@@ -12036,7 +12806,7 @@ function cursorFileFor(repoRoot, tool, sessionId) {
|
|
|
12036
12806
|
}
|
|
12037
12807
|
async function readCursorState(repoRoot, tool, sessionId) {
|
|
12038
12808
|
try {
|
|
12039
|
-
const raw = await
|
|
12809
|
+
const raw = await fs14.promises.readFile(
|
|
12040
12810
|
cursorFileFor(repoRoot, tool, sessionId),
|
|
12041
12811
|
"utf-8"
|
|
12042
12812
|
);
|
|
@@ -12051,21 +12821,36 @@ async function readCursorState(repoRoot, tool, sessionId) {
|
|
|
12051
12821
|
}
|
|
12052
12822
|
async function writeCursorState(repoRoot, tool, sessionId, cursor) {
|
|
12053
12823
|
const file = cursorFileFor(repoRoot, tool, sessionId);
|
|
12054
|
-
await
|
|
12824
|
+
await fs14.promises.mkdir(stateDir2(), { recursive: true, mode: 448 });
|
|
12055
12825
|
const tmp = `${file}.tmp`;
|
|
12056
|
-
await
|
|
12826
|
+
await fs14.promises.writeFile(tmp, JSON.stringify(cursor, null, 2), {
|
|
12057
12827
|
mode: 384
|
|
12058
12828
|
});
|
|
12059
|
-
await
|
|
12829
|
+
await fs14.promises.rename(tmp, file);
|
|
12830
|
+
}
|
|
12831
|
+
async function setUploadSessionEndedAt(repoRoot, tool, sessionId, sessionEndedAt) {
|
|
12832
|
+
const state = await readUploadState(repoRoot, tool, sessionId);
|
|
12833
|
+
if (!state) return false;
|
|
12834
|
+
await writeUploadState({ ...state, sessionEndedAt });
|
|
12835
|
+
const now = /* @__PURE__ */ new Date();
|
|
12836
|
+
try {
|
|
12837
|
+
await fs14.promises.utimes(
|
|
12838
|
+
cursorFileFor(repoRoot, tool, sessionId),
|
|
12839
|
+
now,
|
|
12840
|
+
now
|
|
12841
|
+
);
|
|
12842
|
+
} catch {
|
|
12843
|
+
}
|
|
12844
|
+
return true;
|
|
12060
12845
|
}
|
|
12061
12846
|
async function acquireLock2(repoRoot, tool, sessionId, retries = lockRetries(), delayMs = lockRetryDelayMs()) {
|
|
12062
12847
|
const lockPath = lockFileFor(repoRoot, tool, sessionId);
|
|
12063
|
-
await
|
|
12848
|
+
await fs14.promises.mkdir(stateDir2(), { recursive: true, mode: 448 });
|
|
12064
12849
|
for (let i = 0; i < retries; i++) {
|
|
12065
12850
|
try {
|
|
12066
|
-
const fd = await
|
|
12851
|
+
const fd = await fs14.promises.open(
|
|
12067
12852
|
lockPath,
|
|
12068
|
-
|
|
12853
|
+
fs14.constants.O_CREAT | fs14.constants.O_EXCL | fs14.constants.O_WRONLY
|
|
12069
12854
|
);
|
|
12070
12855
|
try {
|
|
12071
12856
|
await fd.write(String(process.pid));
|
|
@@ -12075,6 +12860,11 @@ async function acquireLock2(repoRoot, tool, sessionId, retries = lockRetries(),
|
|
|
12075
12860
|
return;
|
|
12076
12861
|
} catch (err) {
|
|
12077
12862
|
if (err.code === "EEXIST" && i < retries - 1) {
|
|
12863
|
+
if (await reapLockIfStale(lockPath, {
|
|
12864
|
+
maxAgeMs: STALE_LOCK_TTL_MS2
|
|
12865
|
+
})) {
|
|
12866
|
+
continue;
|
|
12867
|
+
}
|
|
12078
12868
|
await new Promise((r) => setTimeout(r, delayMs));
|
|
12079
12869
|
continue;
|
|
12080
12870
|
}
|
|
@@ -12088,7 +12878,7 @@ async function acquireLock2(repoRoot, tool, sessionId, retries = lockRetries(),
|
|
|
12088
12878
|
}
|
|
12089
12879
|
async function releaseLock2(repoRoot, tool, sessionId) {
|
|
12090
12880
|
try {
|
|
12091
|
-
await
|
|
12881
|
+
await fs14.promises.unlink(lockFileFor(repoRoot, tool, sessionId));
|
|
12092
12882
|
} catch {
|
|
12093
12883
|
}
|
|
12094
12884
|
}
|
|
@@ -12100,43 +12890,27 @@ async function withUploadLock(repoRoot, tool, sessionId, fn) {
|
|
|
12100
12890
|
await releaseLock2(repoRoot, tool, sessionId);
|
|
12101
12891
|
}
|
|
12102
12892
|
}
|
|
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
12893
|
async function sweepStaleUploadStates(ttlMs = stateTtlMs(), now = Date.now()) {
|
|
12112
12894
|
let entries;
|
|
12113
12895
|
try {
|
|
12114
|
-
entries = await
|
|
12896
|
+
entries = await fs14.promises.readdir(stateDir2(), { withFileTypes: true });
|
|
12115
12897
|
} catch {
|
|
12116
12898
|
return;
|
|
12117
12899
|
}
|
|
12118
12900
|
for (const entry of entries) {
|
|
12119
12901
|
if (!entry.isFile()) continue;
|
|
12120
|
-
const file =
|
|
12902
|
+
const file = path14.join(stateDir2(), entry.name);
|
|
12121
12903
|
try {
|
|
12122
12904
|
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
|
-
}
|
|
12905
|
+
await reapLockIfStale(file, {
|
|
12906
|
+
maxAgeMs: STALE_LOCK_TTL_MS2,
|
|
12907
|
+
now
|
|
12908
|
+
});
|
|
12135
12909
|
continue;
|
|
12136
12910
|
}
|
|
12137
|
-
const st = await
|
|
12911
|
+
const st = await fs14.promises.stat(file);
|
|
12138
12912
|
if (now - st.mtimeMs > ttlMs) {
|
|
12139
|
-
await
|
|
12913
|
+
await fs14.promises.unlink(file);
|
|
12140
12914
|
}
|
|
12141
12915
|
} catch {
|
|
12142
12916
|
}
|
|
@@ -12156,13 +12930,13 @@ function formatEpochSeconds2(date) {
|
|
|
12156
12930
|
return String(Math.floor(date.getTime() / 1e3));
|
|
12157
12931
|
}
|
|
12158
12932
|
function newFlowId() {
|
|
12159
|
-
return
|
|
12933
|
+
return crypto5.randomBytes(3).toString("hex");
|
|
12160
12934
|
}
|
|
12161
12935
|
function lineHasAssistant(line) {
|
|
12162
12936
|
return line.includes('"type":"assistant"') || line.includes('"role":"assistant"') || line.includes('"type":"assistant.');
|
|
12163
12937
|
}
|
|
12164
12938
|
async function hasAssistantMessage(transcriptPath) {
|
|
12165
|
-
const stream =
|
|
12939
|
+
const stream = fs15.createReadStream(transcriptPath, { encoding: "utf-8" });
|
|
12166
12940
|
let buffer = "";
|
|
12167
12941
|
try {
|
|
12168
12942
|
for await (const chunk of stream) {
|
|
@@ -12223,53 +12997,8 @@ function summarizePayload(payload) {
|
|
|
12223
12997
|
cursor_version_present: !!payload.cursor_version
|
|
12224
12998
|
});
|
|
12225
12999
|
}
|
|
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
13000
|
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;
|
|
13001
|
+
return findCodexRolloutPath(sessionId);
|
|
12273
13002
|
}
|
|
12274
13003
|
async function selfHealHook(repoRoot, tool) {
|
|
12275
13004
|
if (process.env.HILLCLIMB_SKIP_HOOK_SELF_HEAL === "1") return;
|
|
@@ -12294,8 +13023,8 @@ function resolveCursorTranscriptPath(payload) {
|
|
|
12294
13023
|
const workspace = payload.workspace_roots?.[0];
|
|
12295
13024
|
if (!id || !workspace) return void 0;
|
|
12296
13025
|
const encoded = workspace.replace(/^\//, "").replace(/\//g, "-");
|
|
12297
|
-
return
|
|
12298
|
-
|
|
13026
|
+
return path15.join(
|
|
13027
|
+
os7.homedir(),
|
|
12299
13028
|
".cursor",
|
|
12300
13029
|
"projects",
|
|
12301
13030
|
encoded,
|
|
@@ -12322,7 +13051,7 @@ async function resolveTranscriptPath(payload, sourceTool, sessionId) {
|
|
|
12322
13051
|
async function runUploadInner(payload) {
|
|
12323
13052
|
const sessionId = resolveHookSessionId(payload);
|
|
12324
13053
|
const cwd = resolveHookCwd(payload);
|
|
12325
|
-
const
|
|
13054
|
+
const eventKind2 = classifyHookEvent(payload.hook_event_name);
|
|
12326
13055
|
const recordedAt = Date.now();
|
|
12327
13056
|
if (!sessionId || !cwd) {
|
|
12328
13057
|
appendLog(
|
|
@@ -12348,7 +13077,7 @@ async function runUploadInner(payload) {
|
|
|
12348
13077
|
payload.tool = sourceTool;
|
|
12349
13078
|
appendLog(
|
|
12350
13079
|
"info",
|
|
12351
|
-
`[${sessionId}] payload parsed (tool=${sourceTool}, cwd=${cwd}, event=${
|
|
13080
|
+
`[${sessionId}] payload parsed (tool=${sourceTool}, cwd=${cwd}, event=${eventKind2 ?? "?"})`
|
|
12352
13081
|
);
|
|
12353
13082
|
await selfHealHook(repoRoot, sourceTool);
|
|
12354
13083
|
const transcriptPath = await resolveTranscriptPath(
|
|
@@ -12363,9 +13092,9 @@ async function runUploadInner(payload) {
|
|
|
12363
13092
|
);
|
|
12364
13093
|
return false;
|
|
12365
13094
|
}
|
|
12366
|
-
const transcriptResolved =
|
|
13095
|
+
const transcriptResolved = path15.resolve(transcriptPath);
|
|
12367
13096
|
try {
|
|
12368
|
-
const stat = await
|
|
13097
|
+
const stat = await fs15.promises.stat(transcriptResolved);
|
|
12369
13098
|
if (!stat.isFile()) {
|
|
12370
13099
|
appendLog(
|
|
12371
13100
|
"warn",
|
|
@@ -12393,12 +13122,12 @@ async function runUploadInner(payload) {
|
|
|
12393
13122
|
repoRoot,
|
|
12394
13123
|
config,
|
|
12395
13124
|
sourceTool,
|
|
12396
|
-
eventKind,
|
|
13125
|
+
eventKind: eventKind2,
|
|
12397
13126
|
recordedAt
|
|
12398
13127
|
});
|
|
12399
13128
|
}
|
|
12400
13129
|
var AGENT_MAX_UPLOAD_BYTES = 1024 * 1024 * 1024;
|
|
12401
|
-
var CLI_VERSION = "0.
|
|
13130
|
+
var CLI_VERSION = "0.8.0";
|
|
12402
13131
|
function makeTranscriptGroup(repoRoot, sourceTool, transcriptPath, content) {
|
|
12403
13132
|
const sourceFile = {
|
|
12404
13133
|
sourceName: sourceTool,
|
|
@@ -12408,7 +13137,7 @@ function makeTranscriptGroup(repoRoot, sourceTool, transcriptPath, content) {
|
|
|
12408
13137
|
};
|
|
12409
13138
|
return {
|
|
12410
13139
|
repoPath: repoRoot,
|
|
12411
|
-
label:
|
|
13140
|
+
label: path15.basename(repoRoot),
|
|
12412
13141
|
files: [sourceFile],
|
|
12413
13142
|
sourceNames: [sourceTool],
|
|
12414
13143
|
lastModified: /* @__PURE__ */ new Date()
|
|
@@ -12416,7 +13145,7 @@ function makeTranscriptGroup(repoRoot, sourceTool, transcriptPath, content) {
|
|
|
12416
13145
|
}
|
|
12417
13146
|
async function buildRedactChain(repoRoot) {
|
|
12418
13147
|
const envFileNames = await discoverEnvFiles(repoRoot);
|
|
12419
|
-
const envFilePaths = envFileNames.map((n) =>
|
|
13148
|
+
const envFilePaths = envFileNames.map((n) => path15.join(repoRoot, n));
|
|
12420
13149
|
const secretResult = await collectSecrets(repoRoot, envFilePaths, []);
|
|
12421
13150
|
const chain = [];
|
|
12422
13151
|
if (secretResult.values.size > 0) {
|
|
@@ -12466,6 +13195,14 @@ async function handleUploadFailure(err, sessionId, repoRoot, sourceTool) {
|
|
|
12466
13195
|
);
|
|
12467
13196
|
return false;
|
|
12468
13197
|
}
|
|
13198
|
+
if (isContributionUploadNotUploaded(err)) {
|
|
13199
|
+
await deleteUploadState(repoRoot, sourceTool, sessionId);
|
|
13200
|
+
appendLog(
|
|
13201
|
+
"warn",
|
|
13202
|
+
`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.`
|
|
13203
|
+
);
|
|
13204
|
+
return false;
|
|
13205
|
+
}
|
|
12469
13206
|
appendLog(
|
|
12470
13207
|
"error",
|
|
12471
13208
|
`Session ${sessionId} upload failed: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -12479,13 +13216,24 @@ async function uploadSession(args) {
|
|
|
12479
13216
|
repoRoot,
|
|
12480
13217
|
config,
|
|
12481
13218
|
sourceTool,
|
|
12482
|
-
eventKind,
|
|
13219
|
+
eventKind: eventKind2,
|
|
12483
13220
|
recordedAt
|
|
12484
13221
|
} = args;
|
|
12485
|
-
const isSessionEnd =
|
|
13222
|
+
const isSessionEnd = eventKind2 === "sessionEnd";
|
|
12486
13223
|
const eventLabel = isSessionEnd ? "SessionEnd" : "Stop";
|
|
12487
13224
|
return await withUploadLock(repoRoot, sourceTool, sessionId, async () => {
|
|
12488
13225
|
const prior = await readUploadState(repoRoot, sourceTool, sessionId);
|
|
13226
|
+
const lineageDetection = sourceTool === "codex" && prior?.codexLineageChecked !== true ? await detectCodexLineage(transcriptPath) : void 0;
|
|
13227
|
+
const codexLineage = sourceTool === "codex" ? mergeCodexLineage(prior?.codexLineage, lineageDetection?.lineage) : void 0;
|
|
13228
|
+
const codexLineageChecked = sourceTool === "codex" ? prior?.codexLineageChecked === true || lineageDetection?.conclusive === true : void 0;
|
|
13229
|
+
const lineageNeedsStamp = codexLineage !== void 0 && JSON.stringify(codexLineage) !== JSON.stringify(prior?.codexLineageStamped);
|
|
13230
|
+
if (prior && sourceTool === "codex" && (JSON.stringify(codexLineage) !== JSON.stringify(prior.codexLineage) || codexLineageChecked !== prior.codexLineageChecked)) {
|
|
13231
|
+
await writeUploadState({
|
|
13232
|
+
...prior,
|
|
13233
|
+
codexLineage,
|
|
13234
|
+
codexLineageChecked
|
|
13235
|
+
});
|
|
13236
|
+
}
|
|
12489
13237
|
let cursor = null;
|
|
12490
13238
|
let adoptedLegacy = false;
|
|
12491
13239
|
let restored = null;
|
|
@@ -12539,16 +13287,34 @@ async function uploadSession(args) {
|
|
|
12539
13287
|
if (cursor) {
|
|
12540
13288
|
const evaluation = await evaluateTranscript(transcriptPath, cursor);
|
|
12541
13289
|
if (evaluation.kind === "unchanged") {
|
|
12542
|
-
|
|
12543
|
-
"
|
|
12544
|
-
|
|
12545
|
-
|
|
12546
|
-
|
|
12547
|
-
|
|
13290
|
+
if (lineageNeedsStamp && prior?.contributionId && prior.epochMeta) {
|
|
13291
|
+
mode = "meta-refresh";
|
|
13292
|
+
} else if (lineageNeedsStamp) {
|
|
13293
|
+
mode = "snapshot";
|
|
13294
|
+
transition = "state-lost";
|
|
13295
|
+
} else {
|
|
13296
|
+
appendLog(
|
|
13297
|
+
"info",
|
|
13298
|
+
`[${sessionId}] skipping ${sourceTool} ${eventLabel} upload (no new complete transcript lines past offset ${cursor.rawByteOffset}${isSessionEnd ? "; session state retained for late hooks" : ""})`
|
|
13299
|
+
);
|
|
13300
|
+
if (isSessionEnd) {
|
|
13301
|
+
await setUploadSessionEndedAt(
|
|
13302
|
+
repoRoot,
|
|
13303
|
+
sourceTool,
|
|
13304
|
+
sessionId,
|
|
13305
|
+
new Date(recordedAt).toISOString()
|
|
13306
|
+
);
|
|
13307
|
+
} else if (prior?.sessionEndedAt) {
|
|
13308
|
+
await setUploadSessionEndedAt(
|
|
13309
|
+
repoRoot,
|
|
13310
|
+
sourceTool,
|
|
13311
|
+
sessionId,
|
|
13312
|
+
void 0
|
|
13313
|
+
);
|
|
13314
|
+
}
|
|
13315
|
+
return true;
|
|
12548
13316
|
}
|
|
12549
|
-
|
|
12550
|
-
}
|
|
12551
|
-
if (evaluation.kind === "append") {
|
|
13317
|
+
} else if (evaluation.kind === "append") {
|
|
12552
13318
|
mode = "patch";
|
|
12553
13319
|
tail = evaluation.tail;
|
|
12554
13320
|
nextCursor = evaluation.nextCursor;
|
|
@@ -12580,6 +13346,57 @@ async function uploadSession(args) {
|
|
|
12580
13346
|
});
|
|
12581
13347
|
const alreadySubmitted = prior?.submitted ?? false;
|
|
12582
13348
|
const submitThisUpload = config.autoSubmit && !alreadySubmitted;
|
|
13349
|
+
if (mode === "meta-refresh" && prior?.contributionId && prior.epochMeta && cursor) {
|
|
13350
|
+
const epoch2 = prior.epoch ?? restored?.epoch ?? 1;
|
|
13351
|
+
try {
|
|
13352
|
+
await uploadArtifact(
|
|
13353
|
+
client,
|
|
13354
|
+
prior.contributionId,
|
|
13355
|
+
metaFilename(epoch2, recordedAt),
|
|
13356
|
+
"application/json",
|
|
13357
|
+
buildEpochMeta({
|
|
13358
|
+
sessionId,
|
|
13359
|
+
tool: sourceTool,
|
|
13360
|
+
cliVersion: CLI_VERSION,
|
|
13361
|
+
epoch: epoch2,
|
|
13362
|
+
transitionKind: prior.epochMeta.transitionKind,
|
|
13363
|
+
baseline: prior.epochMeta.baseline,
|
|
13364
|
+
snapshotFilename: prior.epochMeta.snapshotFilename,
|
|
13365
|
+
transcriptArchivePath: prior.epochMeta.transcriptArchivePath,
|
|
13366
|
+
cursor: {
|
|
13367
|
+
rawByteOffset: prior.epochMeta.rawByteOffset,
|
|
13368
|
+
rawPrefixSha256: prior.epochMeta.rawPrefixSha256
|
|
13369
|
+
},
|
|
13370
|
+
recordedAt,
|
|
13371
|
+
lineage: codexLineage
|
|
13372
|
+
})
|
|
13373
|
+
);
|
|
13374
|
+
} catch (err) {
|
|
13375
|
+
return handleUploadFailure(err, sessionId, repoRoot, sourceTool);
|
|
13376
|
+
}
|
|
13377
|
+
await writeUploadState({
|
|
13378
|
+
...prior,
|
|
13379
|
+
lastUploadedAt: now.toISOString(),
|
|
13380
|
+
uploadCount: prior.uploadCount + 1,
|
|
13381
|
+
codexLineage,
|
|
13382
|
+
codexLineageChecked,
|
|
13383
|
+
codexLineageStamped: codexLineage,
|
|
13384
|
+
sessionEndedAt: void 0
|
|
13385
|
+
});
|
|
13386
|
+
appendLog(
|
|
13387
|
+
"info",
|
|
13388
|
+
`[${sessionId}] refreshed ${sourceTool} lineage meta for unchanged epoch=${epoch2}`
|
|
13389
|
+
);
|
|
13390
|
+
if (isSessionEnd) {
|
|
13391
|
+
await setUploadSessionEndedAt(
|
|
13392
|
+
repoRoot,
|
|
13393
|
+
sourceTool,
|
|
13394
|
+
sessionId,
|
|
13395
|
+
new Date(recordedAt).toISOString()
|
|
13396
|
+
);
|
|
13397
|
+
}
|
|
13398
|
+
return true;
|
|
13399
|
+
}
|
|
12583
13400
|
if (mode === "patch" && cursor && tail && nextCursor && prior?.contributionId) {
|
|
12584
13401
|
const baseCursor = cursor;
|
|
12585
13402
|
const contributionId2 = prior.contributionId;
|
|
@@ -12589,6 +13406,9 @@ async function uploadSession(args) {
|
|
|
12589
13406
|
"info",
|
|
12590
13407
|
`[${sessionId}] ${sourceTool} ${eventLabel} upload \u2192 patch epoch=${epoch2} turn=${turn} (${tail.length} raw tail bytes, contribution ${contributionId2})`
|
|
12591
13408
|
);
|
|
13409
|
+
let metaUploaded2 = 0;
|
|
13410
|
+
let codexLineageStamped2 = prior.codexLineageStamped;
|
|
13411
|
+
let epochMeta2 = prior.epochMeta;
|
|
12592
13412
|
try {
|
|
12593
13413
|
const redactedTail = await redactTail(
|
|
12594
13414
|
tail,
|
|
@@ -12598,6 +13418,13 @@ async function uploadSession(args) {
|
|
|
12598
13418
|
redactChain
|
|
12599
13419
|
);
|
|
12600
13420
|
if (adoptedLegacy) {
|
|
13421
|
+
epochMeta2 = {
|
|
13422
|
+
transitionKind: "legacy-adopted",
|
|
13423
|
+
baseline: "legacy-latest-zip",
|
|
13424
|
+
transcriptArchivePath,
|
|
13425
|
+
rawByteOffset: baseCursor.rawByteOffset,
|
|
13426
|
+
rawPrefixSha256: baseCursor.rawPrefixSha256
|
|
13427
|
+
};
|
|
12601
13428
|
await uploadArtifact(
|
|
12602
13429
|
client,
|
|
12603
13430
|
contributionId2,
|
|
@@ -12612,9 +13439,12 @@ async function uploadSession(args) {
|
|
|
12612
13439
|
baseline: "legacy-latest-zip",
|
|
12613
13440
|
transcriptArchivePath,
|
|
12614
13441
|
cursor: baseCursor,
|
|
12615
|
-
recordedAt
|
|
13442
|
+
recordedAt,
|
|
13443
|
+
lineage: codexLineage
|
|
12616
13444
|
})
|
|
12617
13445
|
);
|
|
13446
|
+
metaUploaded2 = 1;
|
|
13447
|
+
codexLineageStamped2 = codexLineage;
|
|
12618
13448
|
}
|
|
12619
13449
|
await uploadArtifact(
|
|
12620
13450
|
client,
|
|
@@ -12636,7 +13466,7 @@ async function uploadSession(args) {
|
|
|
12636
13466
|
projectId: config.projectId,
|
|
12637
13467
|
contributionId: contributionId2,
|
|
12638
13468
|
submitted: submitted2,
|
|
12639
|
-
uploadCount: (prior.uploadCount ?? 0) +
|
|
13469
|
+
uploadCount: (prior.uploadCount ?? 0) + 1 + metaUploaded2,
|
|
12640
13470
|
firstUploadedAt: prior.firstUploadedAt ?? now.toISOString(),
|
|
12641
13471
|
lastUploadedAt: now.toISOString(),
|
|
12642
13472
|
// Mirrors the cursor from here on: describes the raw bytes covered
|
|
@@ -12649,7 +13479,11 @@ async function uploadSession(args) {
|
|
|
12649
13479
|
turnCount: turn,
|
|
12650
13480
|
rawByteOffset: nextCursor.rawByteOffset,
|
|
12651
13481
|
rawPrefixSha256: nextCursor.rawPrefixSha256,
|
|
12652
|
-
snapshotUploaded: true
|
|
13482
|
+
snapshotUploaded: true,
|
|
13483
|
+
codexLineage,
|
|
13484
|
+
codexLineageChecked,
|
|
13485
|
+
codexLineageStamped: codexLineageStamped2,
|
|
13486
|
+
epochMeta: epochMeta2
|
|
12653
13487
|
});
|
|
12654
13488
|
await writeCursorState(repoRoot, sourceTool, sessionId, {
|
|
12655
13489
|
contributionId: contributionId2,
|
|
@@ -12661,6 +13495,45 @@ async function uploadSession(args) {
|
|
|
12661
13495
|
});
|
|
12662
13496
|
};
|
|
12663
13497
|
await persistState(alreadySubmitted);
|
|
13498
|
+
if (!adoptedLegacy && codexLineage && epochMeta2 && JSON.stringify(codexLineage) !== JSON.stringify(prior.codexLineageStamped)) {
|
|
13499
|
+
try {
|
|
13500
|
+
await uploadArtifact(
|
|
13501
|
+
client,
|
|
13502
|
+
contributionId2,
|
|
13503
|
+
metaFilename(epoch2, recordedAt),
|
|
13504
|
+
"application/json",
|
|
13505
|
+
buildEpochMeta({
|
|
13506
|
+
sessionId,
|
|
13507
|
+
tool: sourceTool,
|
|
13508
|
+
cliVersion: CLI_VERSION,
|
|
13509
|
+
epoch: epoch2,
|
|
13510
|
+
transitionKind: epochMeta2.transitionKind,
|
|
13511
|
+
baseline: epochMeta2.baseline,
|
|
13512
|
+
snapshotFilename: epochMeta2.snapshotFilename,
|
|
13513
|
+
transcriptArchivePath: epochMeta2.transcriptArchivePath,
|
|
13514
|
+
cursor: {
|
|
13515
|
+
rawByteOffset: epochMeta2.rawByteOffset,
|
|
13516
|
+
rawPrefixSha256: epochMeta2.rawPrefixSha256
|
|
13517
|
+
},
|
|
13518
|
+
recordedAt,
|
|
13519
|
+
lineage: codexLineage
|
|
13520
|
+
})
|
|
13521
|
+
);
|
|
13522
|
+
metaUploaded2 = 1;
|
|
13523
|
+
codexLineageStamped2 = codexLineage;
|
|
13524
|
+
await persistState(alreadySubmitted);
|
|
13525
|
+
} catch (err) {
|
|
13526
|
+
appendLog(
|
|
13527
|
+
"warn",
|
|
13528
|
+
`[${sessionId}] lineage meta refresh failed after durable patch capture; lineage remains pending: ${err instanceof Error ? err.message : String(err)}`
|
|
13529
|
+
);
|
|
13530
|
+
}
|
|
13531
|
+
} else if (lineageNeedsStamp && !epochMeta2) {
|
|
13532
|
+
appendLog(
|
|
13533
|
+
"info",
|
|
13534
|
+
`[${sessionId}] lineage publication deferred after durable patch capture (pre-feature state has no reconstructable epoch metadata)`
|
|
13535
|
+
);
|
|
13536
|
+
}
|
|
12664
13537
|
let submitted = alreadySubmitted;
|
|
12665
13538
|
if (submitThisUpload) {
|
|
12666
13539
|
try {
|
|
@@ -12675,6 +13548,8 @@ async function uploadSession(args) {
|
|
|
12675
13548
|
"info",
|
|
12676
13549
|
`contribution ${contributionId2} was already submitted (409); recording locally`
|
|
12677
13550
|
);
|
|
13551
|
+
} else if (isContributionUploadNotUploaded(err)) {
|
|
13552
|
+
return handleUploadFailure(err, sessionId, repoRoot, sourceTool);
|
|
12678
13553
|
} else {
|
|
12679
13554
|
appendLog(
|
|
12680
13555
|
"warn",
|
|
@@ -12689,17 +13564,22 @@ async function uploadSession(args) {
|
|
|
12689
13564
|
`Uploaded session ${sessionId} patch epoch=${epoch2} turn=${turn} to contribution ${contributionId2} (offset ${baseCursor.rawByteOffset} \u2192 ${nextCursor.rawByteOffset})`
|
|
12690
13565
|
);
|
|
12691
13566
|
if (isSessionEnd) {
|
|
12692
|
-
await
|
|
13567
|
+
await setUploadSessionEndedAt(
|
|
13568
|
+
repoRoot,
|
|
13569
|
+
sourceTool,
|
|
13570
|
+
sessionId,
|
|
13571
|
+
new Date(recordedAt).toISOString()
|
|
13572
|
+
);
|
|
12693
13573
|
appendLog(
|
|
12694
13574
|
"info",
|
|
12695
|
-
`[${sessionId}] session complete \u2014 contribution ${contributionId2}, epoch ${epoch2}, ${turn} patch(es);
|
|
13575
|
+
`[${sessionId}] session complete \u2014 contribution ${contributionId2}, epoch ${epoch2}, ${turn} patch(es); session state retained for late hooks`
|
|
12696
13576
|
);
|
|
12697
13577
|
}
|
|
12698
13578
|
return true;
|
|
12699
13579
|
}
|
|
12700
13580
|
let raw;
|
|
12701
13581
|
try {
|
|
12702
|
-
raw = await
|
|
13582
|
+
raw = await fs15.promises.readFile(transcriptPath);
|
|
12703
13583
|
} catch (err) {
|
|
12704
13584
|
if (err.code === "ERR_FS_FILE_TOO_LARGE") {
|
|
12705
13585
|
appendLog(
|
|
@@ -12720,8 +13600,21 @@ async function uploadSession(args) {
|
|
|
12720
13600
|
"info",
|
|
12721
13601
|
`[${sessionId}] skipping ${sourceTool} ${eventLabel} upload (no complete transcript line yet)`
|
|
12722
13602
|
);
|
|
12723
|
-
if (isSessionEnd)
|
|
12724
|
-
await
|
|
13603
|
+
if (isSessionEnd) {
|
|
13604
|
+
await setUploadSessionEndedAt(
|
|
13605
|
+
repoRoot,
|
|
13606
|
+
sourceTool,
|
|
13607
|
+
sessionId,
|
|
13608
|
+
new Date(recordedAt).toISOString()
|
|
13609
|
+
);
|
|
13610
|
+
} else if (prior?.sessionEndedAt) {
|
|
13611
|
+
await setUploadSessionEndedAt(
|
|
13612
|
+
repoRoot,
|
|
13613
|
+
sourceTool,
|
|
13614
|
+
sessionId,
|
|
13615
|
+
void 0
|
|
13616
|
+
);
|
|
13617
|
+
}
|
|
12725
13618
|
return true;
|
|
12726
13619
|
}
|
|
12727
13620
|
const epoch = (prior?.epoch ?? restored?.epoch ?? 0) + 1;
|
|
@@ -12740,6 +13633,17 @@ Tool: ${toolLabel2}
|
|
|
12740
13633
|
Repo: ${repoRoot}
|
|
12741
13634
|
Uploaded: ${now.toISOString()}`;
|
|
12742
13635
|
const zipFilename = snapshotFilename(epoch, recordedAt);
|
|
13636
|
+
const epochMeta = {
|
|
13637
|
+
transitionKind: transition,
|
|
13638
|
+
baseline: "snapshot",
|
|
13639
|
+
snapshotFilename: zipFilename,
|
|
13640
|
+
transcriptArchivePath: archivePathFor({
|
|
13641
|
+
sourceName: sourceTool,
|
|
13642
|
+
absolutePath: transcriptPath
|
|
13643
|
+
}),
|
|
13644
|
+
rawByteOffset: truncated.cursor.rawByteOffset,
|
|
13645
|
+
rawPrefixSha256: truncated.cursor.rawPrefixSha256
|
|
13646
|
+
};
|
|
12743
13647
|
const group = makeTranscriptGroup(
|
|
12744
13648
|
repoRoot,
|
|
12745
13649
|
sourceTool,
|
|
@@ -12771,7 +13675,10 @@ Uploaded: ${now.toISOString()}`;
|
|
|
12771
13675
|
firstUploadedAt: prior?.firstUploadedAt ?? now.toISOString(),
|
|
12772
13676
|
lastUploadedAt: prior?.lastUploadedAt ?? now.toISOString(),
|
|
12773
13677
|
lastTranscriptSize: prior?.lastTranscriptSize,
|
|
12774
|
-
lastTranscriptSha256: prior?.lastTranscriptSha256
|
|
13678
|
+
lastTranscriptSha256: prior?.lastTranscriptSha256,
|
|
13679
|
+
codexLineage,
|
|
13680
|
+
codexLineageChecked,
|
|
13681
|
+
epochMeta
|
|
12775
13682
|
})
|
|
12776
13683
|
});
|
|
12777
13684
|
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 +13695,7 @@ Uploaded: ${now.toISOString()}`;
|
|
|
12788
13695
|
return handleUploadFailure(err, sessionId, repoRoot, sourceTool);
|
|
12789
13696
|
}
|
|
12790
13697
|
let metaUploaded = 0;
|
|
13698
|
+
let codexLineageStamped;
|
|
12791
13699
|
try {
|
|
12792
13700
|
await uploadArtifact(
|
|
12793
13701
|
client,
|
|
@@ -12804,10 +13712,12 @@ Uploaded: ${now.toISOString()}`;
|
|
|
12804
13712
|
snapshotFilename: zipFilename,
|
|
12805
13713
|
transcriptArchivePath,
|
|
12806
13714
|
cursor: truncated.cursor,
|
|
12807
|
-
recordedAt
|
|
13715
|
+
recordedAt,
|
|
13716
|
+
lineage: codexLineage
|
|
12808
13717
|
})
|
|
12809
13718
|
);
|
|
12810
13719
|
metaUploaded = 1;
|
|
13720
|
+
codexLineageStamped = codexLineage;
|
|
12811
13721
|
} catch (err) {
|
|
12812
13722
|
appendLog(
|
|
12813
13723
|
"warn",
|
|
@@ -12832,7 +13742,11 @@ Uploaded: ${now.toISOString()}`;
|
|
|
12832
13742
|
turnCount: 0,
|
|
12833
13743
|
rawByteOffset: truncated.cursor.rawByteOffset,
|
|
12834
13744
|
rawPrefixSha256: truncated.cursor.rawPrefixSha256,
|
|
12835
|
-
snapshotUploaded: true
|
|
13745
|
+
snapshotUploaded: true,
|
|
13746
|
+
codexLineage,
|
|
13747
|
+
codexLineageChecked,
|
|
13748
|
+
codexLineageStamped,
|
|
13749
|
+
epochMeta
|
|
12836
13750
|
});
|
|
12837
13751
|
await writeCursorState(repoRoot, sourceTool, sessionId, {
|
|
12838
13752
|
contributionId,
|
|
@@ -12847,10 +13761,15 @@ Uploaded: ${now.toISOString()}`;
|
|
|
12847
13761
|
`Uploaded session ${sessionId} snapshot epoch=${epoch} to project ${config.projectSlug} (${config.projectId}) as contribution ${contributionId}`
|
|
12848
13762
|
);
|
|
12849
13763
|
if (isSessionEnd) {
|
|
12850
|
-
await
|
|
13764
|
+
await setUploadSessionEndedAt(
|
|
13765
|
+
repoRoot,
|
|
13766
|
+
sourceTool,
|
|
13767
|
+
sessionId,
|
|
13768
|
+
new Date(recordedAt).toISOString()
|
|
13769
|
+
);
|
|
12851
13770
|
appendLog(
|
|
12852
13771
|
"info",
|
|
12853
|
-
`[${sessionId}] session complete \u2014 contribution ${contributionId}, epoch ${epoch} snapshot;
|
|
13772
|
+
`[${sessionId}] session complete \u2014 contribution ${contributionId}, epoch ${epoch} snapshot; session state retained for late hooks`
|
|
12854
13773
|
);
|
|
12855
13774
|
}
|
|
12856
13775
|
return true;
|
|
@@ -12904,6 +13823,7 @@ async function runUpload() {
|
|
|
12904
13823
|
[FLOW_ID_ENV]: flowId
|
|
12905
13824
|
};
|
|
12906
13825
|
if (toolArg) workerEnv[TOOL_ENV_FLAG] = toolArg;
|
|
13826
|
+
await applyDebugLogParentEnv(workerEnv, raw);
|
|
12907
13827
|
try {
|
|
12908
13828
|
const child = spawn2(process.execPath, [entrypoint, "upload"], {
|
|
12909
13829
|
detached: true,
|
|
@@ -12975,6 +13895,7 @@ async function runUploadWorker() {
|
|
|
12975
13895
|
}
|
|
12976
13896
|
try {
|
|
12977
13897
|
await sweepStaleUploadStates();
|
|
13898
|
+
await sweepStaleDebugLogState();
|
|
12978
13899
|
} catch {
|
|
12979
13900
|
}
|
|
12980
13901
|
}
|
|
@@ -12982,26 +13903,58 @@ async function runUploadWorker() {
|
|
|
12982
13903
|
|
|
12983
13904
|
// src/git-traces/index.ts
|
|
12984
13905
|
import { spawn as spawn3 } from "child_process";
|
|
12985
|
-
import
|
|
13906
|
+
import crypto7 from "crypto";
|
|
12986
13907
|
|
|
12987
13908
|
// src/git-traces/handlers.ts
|
|
12988
13909
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
12989
|
-
import
|
|
13910
|
+
import path18 from "path";
|
|
12990
13911
|
|
|
12991
13912
|
// src/git-traces/git-ops.ts
|
|
12992
13913
|
import { execFileSync as execFileSync2, spawnSync } from "child_process";
|
|
12993
|
-
import
|
|
12994
|
-
import
|
|
12995
|
-
import
|
|
13914
|
+
import fs16 from "fs";
|
|
13915
|
+
import os8 from "os";
|
|
13916
|
+
import path16 from "path";
|
|
12996
13917
|
import { gzipSync as gzipSync2 } from "zlib";
|
|
12997
13918
|
var GIT_COMMAND_TIMEOUT_MS = 12e4;
|
|
12998
13919
|
var EXEC_OPTS = {
|
|
12999
13920
|
timeout: GIT_COMMAND_TIMEOUT_MS,
|
|
13000
|
-
|
|
13921
|
+
// A rewrite diff for a text file capped at ≤100 MB must fit. This transient
|
|
13922
|
+
// allocation only grows as large as the actual command output.
|
|
13923
|
+
maxBuffer: 256 * 1024 * 1024
|
|
13001
13924
|
};
|
|
13002
13925
|
var MAX_ERROR_OUTPUT_CHARS = 2e3;
|
|
13003
|
-
var
|
|
13004
|
-
|
|
13926
|
+
var DEFAULT_SNAPSHOT_LIMITS = {
|
|
13927
|
+
maxTrackedFileBytes: 25 * 1024 * 1024,
|
|
13928
|
+
// 25 MB
|
|
13929
|
+
maxUntrackedFileBytes: 10 * 1024 * 1024,
|
|
13930
|
+
// 10 MB
|
|
13931
|
+
maxBinaryFileBytes: 1024 * 1024
|
|
13932
|
+
// 1 MB
|
|
13933
|
+
};
|
|
13934
|
+
var LEGACY_SNAPSHOT_LIMITS = {
|
|
13935
|
+
maxTrackedFileBytes: 10 * 1024 * 1024,
|
|
13936
|
+
maxUntrackedFileBytes: 10 * 1024 * 1024,
|
|
13937
|
+
maxBinaryFileBytes: 1024 * 1024
|
|
13938
|
+
};
|
|
13939
|
+
function resolveSnapshotLimits(overrides) {
|
|
13940
|
+
return {
|
|
13941
|
+
maxTrackedFileBytes: positiveIntOrDefault(
|
|
13942
|
+
overrides?.maxTrackedFileBytes,
|
|
13943
|
+
DEFAULT_SNAPSHOT_LIMITS.maxTrackedFileBytes
|
|
13944
|
+
),
|
|
13945
|
+
maxUntrackedFileBytes: positiveIntOrDefault(
|
|
13946
|
+
overrides?.maxUntrackedFileBytes,
|
|
13947
|
+
DEFAULT_SNAPSHOT_LIMITS.maxUntrackedFileBytes
|
|
13948
|
+
),
|
|
13949
|
+
maxBinaryFileBytes: positiveIntOrDefault(
|
|
13950
|
+
overrides?.maxBinaryFileBytes,
|
|
13951
|
+
DEFAULT_SNAPSHOT_LIMITS.maxBinaryFileBytes
|
|
13952
|
+
)
|
|
13953
|
+
};
|
|
13954
|
+
}
|
|
13955
|
+
function positiveIntOrDefault(value, fallback) {
|
|
13956
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 && value <= MAX_SNAPSHOT_LIMIT_OVERRIDE_BYTES ? value : fallback;
|
|
13957
|
+
}
|
|
13005
13958
|
var BINARY_SNIFF_BYTES = 8e3;
|
|
13006
13959
|
var EXCLUDED_SNAPSHOT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
13007
13960
|
// Documents
|
|
@@ -13098,8 +14051,8 @@ var EXCLUDED_SNAPSHOT_BASENAMES = /* @__PURE__ */ new Set([
|
|
|
13098
14051
|
]);
|
|
13099
14052
|
var EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
13100
14053
|
function isExcludedSnapshotPath(filePath) {
|
|
13101
|
-
if (EXCLUDED_SNAPSHOT_BASENAMES.has(
|
|
13102
|
-
return EXCLUDED_SNAPSHOT_EXTENSIONS.has(
|
|
14054
|
+
if (EXCLUDED_SNAPSHOT_BASENAMES.has(path16.basename(filePath))) return true;
|
|
14055
|
+
return EXCLUDED_SNAPSHOT_EXTENSIONS.has(path16.extname(filePath).toLowerCase());
|
|
13103
14056
|
}
|
|
13104
14057
|
function isBinaryBuffer(buffer) {
|
|
13105
14058
|
return buffer.includes(0);
|
|
@@ -13119,25 +14072,26 @@ function readTreeBlobHead(repoRoot, sha) {
|
|
|
13119
14072
|
function readWorkingFileHead(absPath) {
|
|
13120
14073
|
let fd = null;
|
|
13121
14074
|
try {
|
|
13122
|
-
fd =
|
|
14075
|
+
fd = fs16.openSync(absPath, "r");
|
|
13123
14076
|
const buffer = Buffer.alloc(BINARY_SNIFF_BYTES);
|
|
13124
|
-
const bytesRead =
|
|
14077
|
+
const bytesRead = fs16.readSync(fd, buffer, 0, BINARY_SNIFF_BYTES, 0);
|
|
13125
14078
|
return buffer.subarray(0, bytesRead);
|
|
13126
14079
|
} catch {
|
|
13127
14080
|
return null;
|
|
13128
14081
|
} finally {
|
|
13129
14082
|
if (fd !== null) {
|
|
13130
14083
|
try {
|
|
13131
|
-
|
|
14084
|
+
fs16.closeSync(fd);
|
|
13132
14085
|
} catch {
|
|
13133
14086
|
}
|
|
13134
14087
|
}
|
|
13135
14088
|
}
|
|
13136
14089
|
}
|
|
13137
|
-
function classifyOmission(filePath, sizeBytes, readHead) {
|
|
14090
|
+
function classifyOmission(filePath, sizeBytes, readHead, limits, tracked) {
|
|
14091
|
+
const maxFileBytes = tracked ? limits.maxTrackedFileBytes : limits.maxUntrackedFileBytes;
|
|
13138
14092
|
if (isExcludedSnapshotPath(filePath)) return "excluded-extension";
|
|
13139
|
-
if (sizeBytes >
|
|
13140
|
-
if (sizeBytes >
|
|
14093
|
+
if (sizeBytes > maxFileBytes) return "file-over-limit";
|
|
14094
|
+
if (sizeBytes > limits.maxBinaryFileBytes) {
|
|
13141
14095
|
const head = readHead();
|
|
13142
14096
|
if (!head) {
|
|
13143
14097
|
appendLog(
|
|
@@ -13249,12 +14203,32 @@ function deleteRef(repoRoot, refName) {
|
|
|
13249
14203
|
function captureSnapshotSha(repoRoot) {
|
|
13250
14204
|
return captureWorkingCommitSha(repoRoot);
|
|
13251
14205
|
}
|
|
13252
|
-
function
|
|
13253
|
-
|
|
13254
|
-
|
|
13255
|
-
|
|
13256
|
-
|
|
13257
|
-
|
|
14206
|
+
function formatSize(bytes) {
|
|
14207
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
14208
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
14209
|
+
}
|
|
14210
|
+
function summarizeOmittedFiles(files) {
|
|
14211
|
+
const totalBytes = files.reduce((sum, file) => sum + file.sizeBytes, 0);
|
|
14212
|
+
const top = [...files].sort((a, b) => b.sizeBytes - a.sizeBytes).slice(0, 3).map((file) => `${file.path} (${formatSize(file.sizeBytes)})`).join(", ");
|
|
14213
|
+
const more = files.length > 3 ? ` +${files.length - 3} more` : "";
|
|
14214
|
+
const plural = files.length === 1 ? "" : "s";
|
|
14215
|
+
return `${files.length} file${plural} (${formatSize(totalBytes)}): ${top}${more}`;
|
|
14216
|
+
}
|
|
14217
|
+
function logOmittedSnapshotSummary(omittedFiles) {
|
|
14218
|
+
const tracked = omittedFiles.filter((file) => file.tracked);
|
|
14219
|
+
const untracked = omittedFiles.filter((file) => !file.tracked);
|
|
14220
|
+
if (tracked.length > 0) {
|
|
14221
|
+
appendLog(
|
|
14222
|
+
"warn",
|
|
14223
|
+
`git-traces: snapshot omitted tracked ${summarizeOmittedFiles(tracked)} \u2014 content excluded from the baseline bundle and all diffs; full list in baseline.json git.omittedFiles`
|
|
14224
|
+
);
|
|
14225
|
+
}
|
|
14226
|
+
if (untracked.length > 0) {
|
|
14227
|
+
appendLog(
|
|
14228
|
+
"info",
|
|
14229
|
+
`git-traces: snapshot skipped untracked ${summarizeOmittedFiles(untracked)}; full list in baseline.json git.omittedFiles`
|
|
14230
|
+
);
|
|
14231
|
+
}
|
|
13258
14232
|
}
|
|
13259
14233
|
function parseLsTreeLongZ(output) {
|
|
13260
14234
|
const entries = [];
|
|
@@ -13272,26 +14246,24 @@ function parseLsTreeLongZ(output) {
|
|
|
13272
14246
|
}
|
|
13273
14247
|
return entries;
|
|
13274
14248
|
}
|
|
13275
|
-
function listOmittedTreeFiles(repoRoot, treeSha,
|
|
14249
|
+
function listOmittedTreeFiles(repoRoot, treeSha, limits) {
|
|
13276
14250
|
const output = gitBuffer(repoRoot, ["ls-tree", "-r", "-l", "-z", treeSha]);
|
|
13277
14251
|
const omitted = [];
|
|
13278
14252
|
for (const entry of parseLsTreeLongZ(output)) {
|
|
13279
14253
|
const reason = classifyOmission(
|
|
13280
14254
|
entry.path,
|
|
13281
14255
|
entry.sizeBytes,
|
|
13282
|
-
() => readTreeBlobHead(repoRoot, entry.sha)
|
|
14256
|
+
() => readTreeBlobHead(repoRoot, entry.sha),
|
|
14257
|
+
limits,
|
|
14258
|
+
true
|
|
13283
14259
|
);
|
|
13284
14260
|
if (!reason) continue;
|
|
13285
|
-
|
|
14261
|
+
omitted.push({
|
|
13286
14262
|
path: entry.path,
|
|
13287
14263
|
sizeBytes: entry.sizeBytes,
|
|
13288
14264
|
tracked: true,
|
|
13289
14265
|
reason,
|
|
13290
14266
|
gitObjectSha: entry.sha
|
|
13291
|
-
};
|
|
13292
|
-
omitted.push(file);
|
|
13293
|
-
recordOmittedSnapshotFile(options.omittedFiles, file, {
|
|
13294
|
-
log: options.log
|
|
13295
14267
|
});
|
|
13296
14268
|
}
|
|
13297
14269
|
return omitted;
|
|
@@ -13304,10 +14276,15 @@ function removePathsFromIndex(repoRoot, env, paths) {
|
|
|
13304
14276
|
});
|
|
13305
14277
|
}
|
|
13306
14278
|
function filterOmittedFilesFromTree(repoRoot, treeSha, options = {}) {
|
|
13307
|
-
const omittedFiles = listOmittedTreeFiles(
|
|
14279
|
+
const omittedFiles = listOmittedTreeFiles(
|
|
14280
|
+
repoRoot,
|
|
14281
|
+
treeSha,
|
|
14282
|
+
options.limits ?? DEFAULT_SNAPSHOT_LIMITS
|
|
14283
|
+
);
|
|
14284
|
+
options.omittedFiles?.push(...omittedFiles);
|
|
13308
14285
|
if (omittedFiles.length === 0) return treeSha;
|
|
13309
|
-
const tmpIndex =
|
|
13310
|
-
|
|
14286
|
+
const tmpIndex = path16.join(
|
|
14287
|
+
os8.tmpdir(),
|
|
13311
14288
|
`hillclimb-filter-${Date.now()}-${process.pid}`
|
|
13312
14289
|
);
|
|
13313
14290
|
const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
|
|
@@ -13321,12 +14298,13 @@ function filterOmittedFilesFromTree(repoRoot, treeSha, options = {}) {
|
|
|
13321
14298
|
return gitWithEnv(repoRoot, ["write-tree"], env);
|
|
13322
14299
|
} finally {
|
|
13323
14300
|
try {
|
|
13324
|
-
|
|
14301
|
+
fs16.unlinkSync(tmpIndex);
|
|
13325
14302
|
} catch {
|
|
13326
14303
|
}
|
|
13327
14304
|
}
|
|
13328
14305
|
}
|
|
13329
|
-
function buildUntrackedTree(repoRoot,
|
|
14306
|
+
function buildUntrackedTree(repoRoot, options = {}) {
|
|
14307
|
+
const limits = options.limits ?? DEFAULT_SNAPSHOT_LIMITS;
|
|
13330
14308
|
const list = git(repoRoot, [
|
|
13331
14309
|
"ls-files",
|
|
13332
14310
|
"--others",
|
|
@@ -13338,15 +14316,17 @@ function buildUntrackedTree(repoRoot, omittedFiles) {
|
|
|
13338
14316
|
for (const relPath of list.split("\0")) {
|
|
13339
14317
|
if (!relPath) continue;
|
|
13340
14318
|
try {
|
|
13341
|
-
const absPath =
|
|
13342
|
-
const stat =
|
|
14319
|
+
const absPath = path16.join(repoRoot, relPath);
|
|
14320
|
+
const stat = fs16.lstatSync(absPath);
|
|
13343
14321
|
const reason = classifyOmission(
|
|
13344
14322
|
relPath,
|
|
13345
14323
|
stat.size,
|
|
13346
|
-
() => readWorkingFileHead(absPath)
|
|
14324
|
+
() => readWorkingFileHead(absPath),
|
|
14325
|
+
limits,
|
|
14326
|
+
false
|
|
13347
14327
|
);
|
|
13348
14328
|
if (reason) {
|
|
13349
|
-
|
|
14329
|
+
options.omittedFiles?.push({
|
|
13350
14330
|
path: relPath,
|
|
13351
14331
|
sizeBytes: stat.size,
|
|
13352
14332
|
tracked: false,
|
|
@@ -13359,8 +14339,8 @@ function buildUntrackedTree(repoRoot, omittedFiles) {
|
|
|
13359
14339
|
}
|
|
13360
14340
|
}
|
|
13361
14341
|
if (kept.length === 0) return null;
|
|
13362
|
-
const tmpIndex =
|
|
13363
|
-
|
|
14342
|
+
const tmpIndex = path16.join(
|
|
14343
|
+
os8.tmpdir(),
|
|
13364
14344
|
`hillclimb-untracked-${Date.now()}-${process.pid}`
|
|
13365
14345
|
);
|
|
13366
14346
|
const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
|
|
@@ -13372,25 +14352,26 @@ function buildUntrackedTree(repoRoot, omittedFiles) {
|
|
|
13372
14352
|
return gitWithEnv(repoRoot, ["write-tree"], env);
|
|
13373
14353
|
} finally {
|
|
13374
14354
|
try {
|
|
13375
|
-
|
|
14355
|
+
fs16.unlinkSync(tmpIndex);
|
|
13376
14356
|
} catch {
|
|
13377
14357
|
}
|
|
13378
14358
|
}
|
|
13379
14359
|
}
|
|
13380
14360
|
function buildSnapshotTree(repoRoot, stashSha, options = {}) {
|
|
14361
|
+
const omittedFiles = options.omittedFiles ?? [];
|
|
14362
|
+
const limits = options.limits ?? DEFAULT_SNAPSHOT_LIMITS;
|
|
13381
14363
|
const trackedTree = git(repoRoot, ["rev-parse", `${stashSha}^{tree}`]);
|
|
13382
14364
|
const filteredTrackedTree = filterOmittedFilesFromTree(
|
|
13383
14365
|
repoRoot,
|
|
13384
14366
|
trackedTree,
|
|
13385
|
-
{
|
|
13386
|
-
omittedFiles: options.omittedFiles
|
|
13387
|
-
}
|
|
14367
|
+
{ omittedFiles, limits }
|
|
13388
14368
|
);
|
|
13389
|
-
const untrackedTree = buildUntrackedTree(repoRoot,
|
|
14369
|
+
const untrackedTree = buildUntrackedTree(repoRoot, { omittedFiles, limits });
|
|
14370
|
+
if (options.log !== false) logOmittedSnapshotSummary(omittedFiles);
|
|
13390
14371
|
if (!untrackedTree || untrackedTree === EMPTY_TREE_SHA)
|
|
13391
14372
|
return filteredTrackedTree;
|
|
13392
|
-
const tmpIndex =
|
|
13393
|
-
|
|
14373
|
+
const tmpIndex = path16.join(
|
|
14374
|
+
os8.tmpdir(),
|
|
13394
14375
|
`hillclimb-index-${Date.now()}-${process.pid}`
|
|
13395
14376
|
);
|
|
13396
14377
|
const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
|
|
@@ -13417,7 +14398,7 @@ function buildSnapshotTree(repoRoot, stashSha, options = {}) {
|
|
|
13417
14398
|
return gitWithEnv(repoRoot, ["write-tree"], env);
|
|
13418
14399
|
} finally {
|
|
13419
14400
|
try {
|
|
13420
|
-
|
|
14401
|
+
fs16.unlinkSync(tmpIndex);
|
|
13421
14402
|
} catch {
|
|
13422
14403
|
}
|
|
13423
14404
|
}
|
|
@@ -13431,8 +14412,8 @@ function createBundleFromTree(repoRoot, treeSha, sessionId, label) {
|
|
|
13431
14412
|
]);
|
|
13432
14413
|
const orphanRef = `refs/hillclimb/bundle/${sessionId}`;
|
|
13433
14414
|
pinRef(repoRoot, orphanRef, orphanCommit);
|
|
13434
|
-
const tmpFile =
|
|
13435
|
-
|
|
14415
|
+
const tmpFile = path16.join(
|
|
14416
|
+
os8.tmpdir(),
|
|
13436
14417
|
// Include the pid (like the other temp files in this module) so concurrent
|
|
13437
14418
|
// git-traces workers — e.g. two sessions, or a parent + subagent — don't
|
|
13438
14419
|
// collide on the same `git bundle create` path and its `.lock`.
|
|
@@ -13440,26 +14421,24 @@ function createBundleFromTree(repoRoot, treeSha, sessionId, label) {
|
|
|
13440
14421
|
);
|
|
13441
14422
|
try {
|
|
13442
14423
|
git(repoRoot, ["bundle", "create", tmpFile, orphanRef]);
|
|
13443
|
-
return
|
|
14424
|
+
return fs16.readFileSync(tmpFile);
|
|
13444
14425
|
} finally {
|
|
13445
14426
|
try {
|
|
13446
|
-
|
|
14427
|
+
fs16.unlinkSync(tmpFile);
|
|
13447
14428
|
} catch {
|
|
13448
14429
|
}
|
|
13449
14430
|
deleteRef(repoRoot, orphanRef);
|
|
13450
14431
|
}
|
|
13451
14432
|
}
|
|
13452
|
-
function createTreeDiffPatchGz(repoRoot, fromTreeSha, toTreeSha) {
|
|
14433
|
+
function createTreeDiffPatchGz(repoRoot, fromTreeSha, toTreeSha, options = {}) {
|
|
13453
14434
|
if (fromTreeSha === toTreeSha) return null;
|
|
13454
14435
|
const filteredFromTreeSha = filterOmittedFilesFromTree(
|
|
13455
14436
|
repoRoot,
|
|
13456
14437
|
fromTreeSha,
|
|
13457
|
-
{
|
|
13458
|
-
log: false
|
|
13459
|
-
}
|
|
14438
|
+
{ limits: options.limits }
|
|
13460
14439
|
);
|
|
13461
14440
|
const filteredToTreeSha = filterOmittedFilesFromTree(repoRoot, toTreeSha, {
|
|
13462
|
-
|
|
14441
|
+
limits: options.limits
|
|
13463
14442
|
});
|
|
13464
14443
|
if (filteredFromTreeSha === filteredToTreeSha) return null;
|
|
13465
14444
|
const diff = gitBuffer(repoRoot, [
|
|
@@ -13513,7 +14492,7 @@ function parseDirtyFilesFromStatus(status) {
|
|
|
13513
14492
|
return pathPart;
|
|
13514
14493
|
}).filter(Boolean);
|
|
13515
14494
|
}
|
|
13516
|
-
function buildBaselineMetadata(repoRoot, sessionId, tool, baselineSha, cliVersion, epoch, prevHeadSha, transitionKind, startedAt, omittedFiles = []) {
|
|
14495
|
+
function buildBaselineMetadata(repoRoot, sessionId, tool, baselineSha, cliVersion, epoch, prevHeadSha, transitionKind, startedAt, omittedFiles = [], lineage) {
|
|
13517
14496
|
const headSha = safeGit(repoRoot, ["rev-parse", "HEAD"]) ?? "unknown";
|
|
13518
14497
|
const branch = safeGit(repoRoot, ["rev-parse", "--abbrev-ref", "HEAD"]) ?? null;
|
|
13519
14498
|
const remoteUrl = safeGit(repoRoot, ["config", "--get", "remote.origin.url"]) ?? null;
|
|
@@ -13541,9 +14520,10 @@ function buildBaselineMetadata(repoRoot, sessionId, tool, baselineSha, cliVersio
|
|
|
13541
14520
|
...omittedFiles.length > 0 ? { omittedFiles } : {}
|
|
13542
14521
|
},
|
|
13543
14522
|
author: { name: authorName, email: authorEmail },
|
|
13544
|
-
hostname:
|
|
14523
|
+
hostname: os8.hostname(),
|
|
13545
14524
|
cliVersion,
|
|
13546
|
-
commits
|
|
14525
|
+
commits,
|
|
14526
|
+
...lineage
|
|
13547
14527
|
};
|
|
13548
14528
|
}
|
|
13549
14529
|
function enumerateCommits(repoRoot, fromSha, toSha) {
|
|
@@ -13630,9 +14610,9 @@ function parseCommitFiles(repoRoot, sha) {
|
|
|
13630
14610
|
oldPath
|
|
13631
14611
|
});
|
|
13632
14612
|
} else {
|
|
13633
|
-
const
|
|
13634
|
-
indexByPath.set(
|
|
13635
|
-
files.push({ path:
|
|
14613
|
+
const path25 = parts[parts.length - 1];
|
|
14614
|
+
indexByPath.set(path25, files.length);
|
|
14615
|
+
files.push({ path: path25, status, additions: 0, deletions: 0 });
|
|
13636
14616
|
}
|
|
13637
14617
|
}
|
|
13638
14618
|
for (const line of numstat.split("\n")) {
|
|
@@ -13680,7 +14660,8 @@ function cleanupSessionRefs(repoRoot, sessionId) {
|
|
|
13680
14660
|
for (const prefix of [
|
|
13681
14661
|
`refs/hillclimb/baseline/${sessionId}`,
|
|
13682
14662
|
`refs/hillclimb/turns/${sessionId}`,
|
|
13683
|
-
`refs/hillclimb/bundle/${sessionId}
|
|
14663
|
+
`refs/hillclimb/bundle/${sessionId}`,
|
|
14664
|
+
`refs/hillclimb/scoped/${sessionId}`
|
|
13684
14665
|
]) {
|
|
13685
14666
|
deleteRef(repoRoot, prefix);
|
|
13686
14667
|
try {
|
|
@@ -13696,32 +14677,51 @@ function cleanupSessionRefs(repoRoot, sessionId) {
|
|
|
13696
14677
|
}
|
|
13697
14678
|
}
|
|
13698
14679
|
}
|
|
14680
|
+
function countScopedTurnTreeRefs(repoRoot, sessionId, epochPrefix3) {
|
|
14681
|
+
const base = `refs/hillclimb/scoped/${sessionId}/turns/`;
|
|
14682
|
+
try {
|
|
14683
|
+
return git(repoRoot, [
|
|
14684
|
+
"for-each-ref",
|
|
14685
|
+
"--format=%(refname)",
|
|
14686
|
+
epochPrefix3 ? `${base}${epochPrefix3}/` : base
|
|
14687
|
+
]).split("\n").filter((ref) => ref.endsWith("/tree")).length;
|
|
14688
|
+
} catch {
|
|
14689
|
+
return 0;
|
|
14690
|
+
}
|
|
14691
|
+
}
|
|
13699
14692
|
|
|
13700
14693
|
// src/git-traces/session-state.ts
|
|
13701
|
-
import
|
|
13702
|
-
import
|
|
13703
|
-
import
|
|
13704
|
-
import
|
|
14694
|
+
import crypto6 from "crypto";
|
|
14695
|
+
import fs17 from "fs";
|
|
14696
|
+
import os9 from "os";
|
|
14697
|
+
import path17 from "path";
|
|
13705
14698
|
var CURRENT_SCHEMA_VERSION3 = 3;
|
|
13706
|
-
var DEFAULT_STATE_DIR2 =
|
|
14699
|
+
var DEFAULT_STATE_DIR2 = path17.join(os9.homedir(), ".hillclimb", "git-traces");
|
|
13707
14700
|
var LOCK_RETRIES2 = 120;
|
|
13708
14701
|
var LOCK_RETRY_DELAY_MS2 = 500;
|
|
13709
|
-
var
|
|
14702
|
+
var STALE_LOCK_TTL_MS3 = 60 * 60 * 1e3;
|
|
14703
|
+
var LockContentionError = class extends Error {
|
|
14704
|
+
constructor(lockPath, options) {
|
|
14705
|
+
super(`Lock remains held: ${lockPath}`, options);
|
|
14706
|
+
this.lockPath = lockPath;
|
|
14707
|
+
this.name = "LockContentionError";
|
|
14708
|
+
}
|
|
14709
|
+
};
|
|
13710
14710
|
function stateDir3() {
|
|
13711
14711
|
return process.env.HILLCLIMB_GIT_TRACES_STATE_DIR ?? DEFAULT_STATE_DIR2;
|
|
13712
14712
|
}
|
|
13713
14713
|
function stateFileForRepo(repoRoot, tool, sessionId) {
|
|
13714
|
-
const hash =
|
|
13715
|
-
sessionId ? `${
|
|
14714
|
+
const hash = crypto6.createHash("sha256").update(
|
|
14715
|
+
sessionId ? `${path17.resolve(repoRoot)}\0${tool}\0${sessionId}` : `${path17.resolve(repoRoot)}\0${tool}`
|
|
13716
14716
|
).digest("hex").slice(0, 16);
|
|
13717
|
-
return
|
|
14717
|
+
return path17.join(stateDir3(), `${hash}.json`);
|
|
13718
14718
|
}
|
|
13719
14719
|
function lockFileForRepo(repoRoot, tool, sessionId) {
|
|
13720
14720
|
return `${stateFileForRepo(repoRoot, tool, sessionId)}.lock`;
|
|
13721
14721
|
}
|
|
13722
14722
|
async function readStateFile(file) {
|
|
13723
14723
|
try {
|
|
13724
|
-
const raw = await
|
|
14724
|
+
const raw = await fs17.promises.readFile(file, "utf-8");
|
|
13725
14725
|
const parsed = JSON.parse(raw);
|
|
13726
14726
|
if (parsed.schemaVersion !== CURRENT_SCHEMA_VERSION3) {
|
|
13727
14727
|
return null;
|
|
@@ -13731,29 +14731,38 @@ async function readStateFile(file) {
|
|
|
13731
14731
|
return null;
|
|
13732
14732
|
}
|
|
13733
14733
|
}
|
|
14734
|
+
async function readStoredStateFile(file) {
|
|
14735
|
+
const state = await readStateFile(file);
|
|
14736
|
+
if (!state) return null;
|
|
14737
|
+
try {
|
|
14738
|
+
return { state, mtimeMs: (await fs17.promises.stat(file)).mtimeMs };
|
|
14739
|
+
} catch {
|
|
14740
|
+
return null;
|
|
14741
|
+
}
|
|
14742
|
+
}
|
|
13734
14743
|
async function listScopedSessionStates(repoRoot, tool) {
|
|
13735
14744
|
let entries;
|
|
13736
14745
|
try {
|
|
13737
|
-
entries = await
|
|
14746
|
+
entries = await fs17.promises.readdir(stateDir3(), { withFileTypes: true });
|
|
13738
14747
|
} catch {
|
|
13739
14748
|
return [];
|
|
13740
14749
|
}
|
|
13741
14750
|
const states = [];
|
|
13742
14751
|
for (const entry of entries) {
|
|
13743
14752
|
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
13744
|
-
const file =
|
|
14753
|
+
const file = path17.join(stateDir3(), entry.name);
|
|
13745
14754
|
const state = await readStateFile(file);
|
|
13746
14755
|
if (!state) continue;
|
|
13747
14756
|
if (typeof state.repoRoot !== "string" || typeof state.sessionId !== "string") {
|
|
13748
14757
|
continue;
|
|
13749
14758
|
}
|
|
13750
|
-
if (
|
|
13751
|
-
if (
|
|
14759
|
+
if (path17.resolve(state.repoRoot) !== path17.resolve(repoRoot)) continue;
|
|
14760
|
+
if (path17.resolve(file) !== path17.resolve(stateFileForRepo(repoRoot, tool, state.sessionId))) {
|
|
13752
14761
|
continue;
|
|
13753
14762
|
}
|
|
13754
14763
|
let mtimeMs = 0;
|
|
13755
14764
|
try {
|
|
13756
|
-
mtimeMs = (await
|
|
14765
|
+
mtimeMs = (await fs17.promises.stat(file)).mtimeMs;
|
|
13757
14766
|
} catch {
|
|
13758
14767
|
continue;
|
|
13759
14768
|
}
|
|
@@ -13764,26 +14773,26 @@ async function listScopedSessionStates(repoRoot, tool) {
|
|
|
13764
14773
|
async function listSessionStatesForSession(tool, sessionId) {
|
|
13765
14774
|
let entries;
|
|
13766
14775
|
try {
|
|
13767
|
-
entries = await
|
|
14776
|
+
entries = await fs17.promises.readdir(stateDir3(), { withFileTypes: true });
|
|
13768
14777
|
} catch {
|
|
13769
14778
|
return [];
|
|
13770
14779
|
}
|
|
13771
14780
|
const states = [];
|
|
13772
14781
|
for (const entry of entries) {
|
|
13773
14782
|
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
13774
|
-
const file =
|
|
14783
|
+
const file = path17.join(stateDir3(), entry.name);
|
|
13775
14784
|
const state = await readStateFile(file);
|
|
13776
14785
|
if (!state) continue;
|
|
13777
14786
|
if (typeof state.repoRoot !== "string" || typeof state.sessionId !== "string") {
|
|
13778
14787
|
continue;
|
|
13779
14788
|
}
|
|
13780
14789
|
if (state.sessionId !== sessionId) continue;
|
|
13781
|
-
if (
|
|
14790
|
+
if (path17.resolve(file) !== path17.resolve(stateFileForRepo(state.repoRoot, tool, sessionId))) {
|
|
13782
14791
|
continue;
|
|
13783
14792
|
}
|
|
13784
14793
|
let mtimeMs = 0;
|
|
13785
14794
|
try {
|
|
13786
|
-
mtimeMs = (await
|
|
14795
|
+
mtimeMs = (await fs17.promises.stat(file)).mtimeMs;
|
|
13787
14796
|
} catch {
|
|
13788
14797
|
continue;
|
|
13789
14798
|
}
|
|
@@ -13791,7 +14800,7 @@ async function listSessionStatesForSession(tool, sessionId) {
|
|
|
13791
14800
|
}
|
|
13792
14801
|
return states;
|
|
13793
14802
|
}
|
|
13794
|
-
async function
|
|
14803
|
+
async function readSessionState2(repoRoot, tool, sessionId) {
|
|
13795
14804
|
if (sessionId) {
|
|
13796
14805
|
const scoped = await readStateFile(
|
|
13797
14806
|
stateFileForRepo(repoRoot, tool, sessionId)
|
|
@@ -13802,108 +14811,141 @@ async function readSessionState(repoRoot, tool, sessionId) {
|
|
|
13802
14811
|
}
|
|
13803
14812
|
return readStateFile(stateFileForRepo(repoRoot, tool));
|
|
13804
14813
|
}
|
|
13805
|
-
async function
|
|
14814
|
+
async function readLegacySessionState(repoRoot, tool, sessionId) {
|
|
14815
|
+
const legacy = await readStateFile(stateFileForRepo(repoRoot, tool));
|
|
14816
|
+
return legacy?.sessionId === sessionId ? legacy : null;
|
|
14817
|
+
}
|
|
14818
|
+
async function readSessionStateCandidates(repoRoot, tool, sessionId) {
|
|
14819
|
+
const [scoped, legacyCandidate] = await Promise.all([
|
|
14820
|
+
readStoredStateFile(stateFileForRepo(repoRoot, tool, sessionId)),
|
|
14821
|
+
readStoredStateFile(stateFileForRepo(repoRoot, tool))
|
|
14822
|
+
]);
|
|
14823
|
+
return {
|
|
14824
|
+
scoped,
|
|
14825
|
+
legacy: legacyCandidate?.state.sessionId === sessionId ? legacyCandidate : null
|
|
14826
|
+
};
|
|
14827
|
+
}
|
|
14828
|
+
async function writeSessionState2(state, tool) {
|
|
13806
14829
|
const file = stateFileForRepo(state.repoRoot, tool, state.sessionId);
|
|
13807
|
-
await
|
|
14830
|
+
await writeStateFile(file, state);
|
|
14831
|
+
}
|
|
14832
|
+
async function writeLegacySessionState(state, tool) {
|
|
14833
|
+
await writeStateFile(stateFileForRepo(state.repoRoot, tool), state);
|
|
14834
|
+
}
|
|
14835
|
+
async function writeStateFile(file, state) {
|
|
14836
|
+
await fs17.promises.mkdir(stateDir3(), { recursive: true, mode: 448 });
|
|
13808
14837
|
const tmp = `${file}.tmp`;
|
|
13809
|
-
await
|
|
14838
|
+
await fs17.promises.writeFile(tmp, JSON.stringify(state, null, 2), {
|
|
13810
14839
|
mode: 384
|
|
13811
14840
|
});
|
|
13812
|
-
await
|
|
13813
|
-
|
|
13814
|
-
|
|
13815
|
-
|
|
13816
|
-
|
|
14841
|
+
await fs17.promises.rename(tmp, file);
|
|
14842
|
+
}
|
|
14843
|
+
async function touchSessionState(repoRoot, tool, sessionId) {
|
|
14844
|
+
const now = /* @__PURE__ */ new Date();
|
|
14845
|
+
try {
|
|
14846
|
+
await fs17.promises.utimes(
|
|
14847
|
+
stateFileForRepo(repoRoot, tool, sessionId),
|
|
14848
|
+
now,
|
|
14849
|
+
now
|
|
14850
|
+
);
|
|
14851
|
+
} catch {
|
|
14852
|
+
}
|
|
14853
|
+
}
|
|
14854
|
+
async function statSessionStateMtime(repoRoot, tool, sessionId) {
|
|
14855
|
+
try {
|
|
14856
|
+
const stat = await fs17.promises.stat(
|
|
14857
|
+
stateFileForRepo(repoRoot, tool, sessionId)
|
|
14858
|
+
);
|
|
14859
|
+
return stat.mtimeMs;
|
|
14860
|
+
} catch {
|
|
14861
|
+
return null;
|
|
13817
14862
|
}
|
|
13818
14863
|
}
|
|
13819
14864
|
async function deleteStateFile(file) {
|
|
13820
14865
|
try {
|
|
13821
|
-
await
|
|
14866
|
+
await fs17.promises.unlink(file);
|
|
13822
14867
|
} catch {
|
|
13823
14868
|
}
|
|
13824
14869
|
}
|
|
13825
|
-
async function
|
|
14870
|
+
async function deleteSessionState2(repoRoot, tool, sessionId) {
|
|
13826
14871
|
if (sessionId) {
|
|
13827
14872
|
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
14873
|
return;
|
|
13834
14874
|
}
|
|
13835
14875
|
await deleteStateFile(stateFileForRepo(repoRoot, tool));
|
|
13836
14876
|
}
|
|
14877
|
+
async function deleteLegacySessionState(repoRoot, tool, sessionId) {
|
|
14878
|
+
const legacyFile = stateFileForRepo(repoRoot, tool);
|
|
14879
|
+
const legacy = await readStateFile(legacyFile);
|
|
14880
|
+
if (legacy?.sessionId === sessionId) await deleteStateFile(legacyFile);
|
|
14881
|
+
}
|
|
13837
14882
|
async function acquireLock3(repoRoot, tool, sessionId, retries = LOCK_RETRIES2, delayMs = LOCK_RETRY_DELAY_MS2) {
|
|
13838
14883
|
const lockPath = lockFileForRepo(repoRoot, tool, sessionId);
|
|
13839
|
-
await
|
|
14884
|
+
await fs17.promises.mkdir(stateDir3(), { recursive: true, mode: 448 });
|
|
13840
14885
|
for (let i = 0; i < retries; i++) {
|
|
13841
14886
|
try {
|
|
13842
|
-
const fd = await
|
|
14887
|
+
const fd = await fs17.promises.open(
|
|
13843
14888
|
lockPath,
|
|
13844
|
-
|
|
14889
|
+
fs17.constants.O_CREAT | fs17.constants.O_EXCL | fs17.constants.O_WRONLY
|
|
13845
14890
|
);
|
|
13846
14891
|
await fd.write(String(process.pid));
|
|
13847
14892
|
await fd.close();
|
|
13848
14893
|
return fd.fd;
|
|
13849
14894
|
} catch (err) {
|
|
13850
|
-
if (err.code
|
|
14895
|
+
if (err.code !== "EEXIST") throw err;
|
|
14896
|
+
let regularLockFile = false;
|
|
14897
|
+
try {
|
|
14898
|
+
regularLockFile = (await fs17.promises.lstat(lockPath)).isFile();
|
|
14899
|
+
} catch (statErr) {
|
|
14900
|
+
if (statErr.code === "ENOENT") {
|
|
14901
|
+
i--;
|
|
14902
|
+
continue;
|
|
14903
|
+
}
|
|
14904
|
+
}
|
|
14905
|
+
if (!regularLockFile) throw err;
|
|
14906
|
+
if (i < retries - 1) {
|
|
14907
|
+
if (await reapLockIfStale(lockPath, {
|
|
14908
|
+
maxAgeMs: STALE_LOCK_TTL_MS3
|
|
14909
|
+
})) {
|
|
14910
|
+
continue;
|
|
14911
|
+
}
|
|
13851
14912
|
await new Promise((r) => setTimeout(r, delayMs));
|
|
13852
14913
|
continue;
|
|
13853
14914
|
}
|
|
13854
|
-
throw err;
|
|
14915
|
+
throw new LockContentionError(lockPath, { cause: err });
|
|
13855
14916
|
}
|
|
13856
14917
|
}
|
|
13857
14918
|
throw new Error(`Failed to acquire lock after ${retries} retries`);
|
|
13858
14919
|
}
|
|
13859
14920
|
async function releaseLock3(repoRoot, tool, sessionId) {
|
|
13860
14921
|
try {
|
|
13861
|
-
await
|
|
14922
|
+
await fs17.promises.unlink(lockFileForRepo(repoRoot, tool, sessionId));
|
|
13862
14923
|
} catch {
|
|
13863
14924
|
}
|
|
13864
14925
|
}
|
|
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()) {
|
|
14926
|
+
async function sweepStaleLockFiles(ttlMs = STALE_LOCK_TTL_MS3, now = Date.now()) {
|
|
13874
14927
|
let entries;
|
|
13875
14928
|
try {
|
|
13876
|
-
entries = await
|
|
14929
|
+
entries = await fs17.promises.readdir(stateDir3(), { withFileTypes: true });
|
|
13877
14930
|
} catch {
|
|
13878
14931
|
return 0;
|
|
13879
14932
|
}
|
|
13880
14933
|
let removed = 0;
|
|
13881
14934
|
for (const entry of entries) {
|
|
13882
14935
|
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 {
|
|
14936
|
+
const file = path17.join(stateDir3(), entry.name);
|
|
14937
|
+
if (await reapLockIfStale(file, {
|
|
14938
|
+
maxAgeMs: ttlMs,
|
|
14939
|
+
now
|
|
14940
|
+
})) {
|
|
14941
|
+
removed++;
|
|
13900
14942
|
}
|
|
13901
14943
|
}
|
|
13902
14944
|
return removed;
|
|
13903
14945
|
}
|
|
13904
14946
|
|
|
13905
14947
|
// src/git-traces/handlers.ts
|
|
13906
|
-
var CLI_VERSION2 = "0.
|
|
14948
|
+
var CLI_VERSION2 = "0.8.0";
|
|
13907
14949
|
var GIT_TRACES_SLUG = "git-traces";
|
|
13908
14950
|
var MAX_UPLOAD_BYTES = 500 * 1024 * 1024;
|
|
13909
14951
|
var STALE_SCOPED_STATE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -13920,12 +14962,12 @@ var TOOL_LABELS = {
|
|
|
13920
14962
|
async function loadConfiguredRepos() {
|
|
13921
14963
|
const file = await loadProjects();
|
|
13922
14964
|
return Object.entries(file.projects).map(([repoRoot, config]) => ({
|
|
13923
|
-
repoRoot:
|
|
14965
|
+
repoRoot: path18.resolve(repoRoot),
|
|
13924
14966
|
config
|
|
13925
14967
|
})).sort((a, b) => a.repoRoot.localeCompare(b.repoRoot));
|
|
13926
14968
|
}
|
|
13927
14969
|
function repoLabel(repoRoot) {
|
|
13928
|
-
return
|
|
14970
|
+
return path18.basename(repoRoot) || repoRoot;
|
|
13929
14971
|
}
|
|
13930
14972
|
function resolveCwd2(payload) {
|
|
13931
14973
|
return resolveHookCwd(payload);
|
|
@@ -13933,6 +14975,11 @@ function resolveCwd2(payload) {
|
|
|
13933
14975
|
function resolveSessionId2(payload) {
|
|
13934
14976
|
return resolveHookSessionId(payload);
|
|
13935
14977
|
}
|
|
14978
|
+
async function detectCodexLineageForHook(payload, tool, sessionId) {
|
|
14979
|
+
if (tool !== "codex" || !sessionId) return { conclusive: false };
|
|
14980
|
+
const transcriptPath = payload.transcript_path ?? await findCodexRolloutPath(sessionId);
|
|
14981
|
+
return transcriptPath ? detectCodexLineage(transcriptPath) : { conclusive: false };
|
|
14982
|
+
}
|
|
13936
14983
|
function epochPrefix2(epoch) {
|
|
13937
14984
|
return `epoch-${String(epoch).padStart(3, "0")}`;
|
|
13938
14985
|
}
|
|
@@ -13980,18 +15027,48 @@ async function uploadFile(client, contributionId, filename, mimeType, buffer) {
|
|
|
13980
15027
|
);
|
|
13981
15028
|
return true;
|
|
13982
15029
|
}
|
|
15030
|
+
function omissionKey(file) {
|
|
15031
|
+
return `${file.tracked ? "tracked" : "untracked"}:${file.path}`;
|
|
15032
|
+
}
|
|
15033
|
+
function omissionKeys(files) {
|
|
15034
|
+
return [...new Set(files.map(omissionKey))].sort();
|
|
15035
|
+
}
|
|
15036
|
+
function omissionKeysFromMetadata(metadata) {
|
|
15037
|
+
const git2 = metadata.git;
|
|
15038
|
+
if (!git2 || typeof git2 !== "object") return [];
|
|
15039
|
+
const omitted = git2.omittedFiles;
|
|
15040
|
+
if (!Array.isArray(omitted)) return [];
|
|
15041
|
+
const keys = omitted.map(
|
|
15042
|
+
(file) => file && typeof file === "object" ? metadataOmissionKey(file) : void 0
|
|
15043
|
+
).filter((key) => key !== void 0);
|
|
15044
|
+
return [...new Set(keys)].sort();
|
|
15045
|
+
}
|
|
15046
|
+
function metadataOmissionKey(file) {
|
|
15047
|
+
const { path: filePath, tracked } = file;
|
|
15048
|
+
if (typeof filePath !== "string") return void 0;
|
|
15049
|
+
return omissionKey({ path: filePath, tracked: tracked !== false });
|
|
15050
|
+
}
|
|
13983
15051
|
function canUploadEpochBaselineArtifacts(epoch, artifacts) {
|
|
13984
15052
|
const prefix = epochPrefix2(epoch);
|
|
13985
15053
|
return canUploadFile(`${prefix}-baseline.bundle`, artifacts.bundleBuffer) && canUploadFile(`${prefix}-baseline.json`, artifacts.metadataBuffer);
|
|
13986
15054
|
}
|
|
13987
|
-
function pinEpochBaseline(repoRoot, sessionId, epoch) {
|
|
15055
|
+
function pinEpochBaseline(repoRoot, sessionId, epoch, pinCanonicalRefs = true) {
|
|
13988
15056
|
const baselineSha = captureBaselineSha(repoRoot);
|
|
13989
15057
|
const baselineRefPrefix = `refs/hillclimb/baseline/${sessionId}/${epochPrefix2(epoch)}`;
|
|
13990
|
-
|
|
13991
|
-
|
|
15058
|
+
const scopedRefPrefix = `refs/hillclimb/scoped/${sessionId}/baseline/${epochPrefix2(epoch)}`;
|
|
15059
|
+
if (pinCanonicalRefs) {
|
|
15060
|
+
deleteRef(repoRoot, baselineRefPrefix);
|
|
15061
|
+
pinRef(repoRoot, `${baselineRefPrefix}/tracked`, baselineSha);
|
|
15062
|
+
}
|
|
15063
|
+
pinRef(repoRoot, `${scopedRefPrefix}/tracked`, baselineSha);
|
|
13992
15064
|
return { baselineSha, headSha: captureHeadSha(repoRoot) };
|
|
13993
15065
|
}
|
|
13994
|
-
function
|
|
15066
|
+
function pinScopedTreeRef(repoRoot, refName, treeSha, message) {
|
|
15067
|
+
const commit = execGit(repoRoot, ["commit-tree", treeSha, "-m", message]);
|
|
15068
|
+
pinRef(repoRoot, refName, commit);
|
|
15069
|
+
return commit;
|
|
15070
|
+
}
|
|
15071
|
+
function pinFrozenBaselineTree(repoRoot, sessionId, epoch, baselineTreeSha, pinCanonicalRefs = true) {
|
|
13995
15072
|
const prefix = epochPrefix2(epoch);
|
|
13996
15073
|
const commit = execGit(repoRoot, [
|
|
13997
15074
|
"commit-tree",
|
|
@@ -13999,12 +15076,31 @@ function pinFrozenBaselineTree(repoRoot, sessionId, epoch, baselineTreeSha) {
|
|
|
13999
15076
|
"-m",
|
|
14000
15077
|
`frozen baseline ${prefix} for session ${sessionId}`
|
|
14001
15078
|
]);
|
|
15079
|
+
if (pinCanonicalRefs) {
|
|
15080
|
+
pinRef(
|
|
15081
|
+
repoRoot,
|
|
15082
|
+
`refs/hillclimb/baseline/${sessionId}/${prefix}/tree`,
|
|
15083
|
+
commit
|
|
15084
|
+
);
|
|
15085
|
+
}
|
|
14002
15086
|
pinRef(
|
|
14003
15087
|
repoRoot,
|
|
14004
|
-
`refs/hillclimb/
|
|
15088
|
+
`refs/hillclimb/scoped/${sessionId}/baseline/${prefix}/tree`,
|
|
14005
15089
|
commit
|
|
14006
15090
|
);
|
|
14007
15091
|
}
|
|
15092
|
+
function pinScopedTurnSnapshot(repoRoot, sessionId, epoch, turnCount, snapshotSha, treeSha) {
|
|
15093
|
+
const prefix = epochPrefix2(epoch);
|
|
15094
|
+
const turnLabel = turnSuffix2(turnCount);
|
|
15095
|
+
const refPrefix = `refs/hillclimb/scoped/${sessionId}/turns/${prefix}/${turnLabel}`;
|
|
15096
|
+
pinRef(repoRoot, `${refPrefix}/tracked`, snapshotSha);
|
|
15097
|
+
pinScopedTreeRef(
|
|
15098
|
+
repoRoot,
|
|
15099
|
+
`${refPrefix}/tree`,
|
|
15100
|
+
treeSha,
|
|
15101
|
+
`protected ${turnLabel} ${prefix} for session ${sessionId}`
|
|
15102
|
+
);
|
|
15103
|
+
}
|
|
14008
15104
|
function freezeEpochBaseline(params) {
|
|
14009
15105
|
const {
|
|
14010
15106
|
repoRoot,
|
|
@@ -14013,18 +15109,23 @@ function freezeEpochBaseline(params) {
|
|
|
14013
15109
|
epoch,
|
|
14014
15110
|
prevHeadSha,
|
|
14015
15111
|
transitionKind,
|
|
14016
|
-
startedAt
|
|
15112
|
+
startedAt,
|
|
15113
|
+
limits,
|
|
15114
|
+
lineage,
|
|
15115
|
+
pinCanonicalRefs = true
|
|
14017
15116
|
} = params;
|
|
14018
15117
|
const prefix = epochPrefix2(epoch);
|
|
14019
15118
|
try {
|
|
14020
15119
|
const { baselineSha, headSha } = pinEpochBaseline(
|
|
14021
15120
|
repoRoot,
|
|
14022
15121
|
sessionId,
|
|
14023
|
-
epoch
|
|
15122
|
+
epoch,
|
|
15123
|
+
pinCanonicalRefs
|
|
14024
15124
|
);
|
|
14025
15125
|
const omittedFiles = [];
|
|
14026
15126
|
const baselineTreeSha = buildSnapshotTree(repoRoot, baselineSha, {
|
|
14027
|
-
omittedFiles
|
|
15127
|
+
omittedFiles,
|
|
15128
|
+
limits
|
|
14028
15129
|
});
|
|
14029
15130
|
const baselineMetadata = buildBaselineMetadata(
|
|
14030
15131
|
repoRoot,
|
|
@@ -14036,10 +15137,23 @@ function freezeEpochBaseline(params) {
|
|
|
14036
15137
|
prevHeadSha,
|
|
14037
15138
|
transitionKind,
|
|
14038
15139
|
startedAt,
|
|
14039
|
-
omittedFiles
|
|
15140
|
+
omittedFiles,
|
|
15141
|
+
lineage
|
|
14040
15142
|
);
|
|
14041
|
-
pinFrozenBaselineTree(
|
|
14042
|
-
|
|
15143
|
+
pinFrozenBaselineTree(
|
|
15144
|
+
repoRoot,
|
|
15145
|
+
sessionId,
|
|
15146
|
+
epoch,
|
|
15147
|
+
baselineTreeSha,
|
|
15148
|
+
pinCanonicalRefs
|
|
15149
|
+
);
|
|
15150
|
+
return {
|
|
15151
|
+
baselineSha,
|
|
15152
|
+
baselineTreeSha,
|
|
15153
|
+
baselineMetadata,
|
|
15154
|
+
headSha,
|
|
15155
|
+
omittedFiles
|
|
15156
|
+
};
|
|
14043
15157
|
} catch (err) {
|
|
14044
15158
|
appendLog(
|
|
14045
15159
|
"error",
|
|
@@ -14061,7 +15175,12 @@ function buildFrozenEpochBaselineArtifacts(params) {
|
|
|
14061
15175
|
const metadataBuffer = Buffer.from(
|
|
14062
15176
|
JSON.stringify(baselineMetadata, null, 2)
|
|
14063
15177
|
);
|
|
14064
|
-
return {
|
|
15178
|
+
return {
|
|
15179
|
+
baselineTreeSha,
|
|
15180
|
+
metadataBuffer,
|
|
15181
|
+
bundleBuffer,
|
|
15182
|
+
omittedKeys: omissionKeysFromMetadata(baselineMetadata)
|
|
15183
|
+
};
|
|
14065
15184
|
} catch (err) {
|
|
14066
15185
|
appendLog(
|
|
14067
15186
|
"error",
|
|
@@ -14079,13 +15198,16 @@ function buildEpochBaselineArtifacts(params) {
|
|
|
14079
15198
|
baselineSha,
|
|
14080
15199
|
prevHeadSha,
|
|
14081
15200
|
transitionKind,
|
|
14082
|
-
startedAt
|
|
15201
|
+
startedAt,
|
|
15202
|
+
limits,
|
|
15203
|
+
lineage
|
|
14083
15204
|
} = params;
|
|
14084
15205
|
const prefix = epochPrefix2(epoch);
|
|
14085
15206
|
try {
|
|
14086
15207
|
const omittedFiles = [];
|
|
14087
15208
|
const baselineTreeSha = buildSnapshotTree(repoRoot, baselineSha, {
|
|
14088
|
-
omittedFiles
|
|
15209
|
+
omittedFiles,
|
|
15210
|
+
limits
|
|
14089
15211
|
});
|
|
14090
15212
|
const metadata = buildBaselineMetadata(
|
|
14091
15213
|
repoRoot,
|
|
@@ -14097,7 +15219,8 @@ function buildEpochBaselineArtifacts(params) {
|
|
|
14097
15219
|
prevHeadSha,
|
|
14098
15220
|
transitionKind,
|
|
14099
15221
|
startedAt,
|
|
14100
|
-
omittedFiles
|
|
15222
|
+
omittedFiles,
|
|
15223
|
+
lineage
|
|
14101
15224
|
);
|
|
14102
15225
|
return buildFrozenEpochBaselineArtifacts({
|
|
14103
15226
|
repoRoot,
|
|
@@ -14135,7 +15258,16 @@ async function uploadEpochBaselineArtifacts(params) {
|
|
|
14135
15258
|
);
|
|
14136
15259
|
}
|
|
14137
15260
|
async function createGitTracesContribution(params) {
|
|
14138
|
-
const {
|
|
15261
|
+
const {
|
|
15262
|
+
client,
|
|
15263
|
+
config,
|
|
15264
|
+
repoRoot,
|
|
15265
|
+
state,
|
|
15266
|
+
tool,
|
|
15267
|
+
persistState,
|
|
15268
|
+
now,
|
|
15269
|
+
artifacts
|
|
15270
|
+
} = params;
|
|
14139
15271
|
const toolLabel2 = TOOL_LABELS[tool] ?? "Claude";
|
|
14140
15272
|
const epochSeconds = formatEpochSeconds3(now);
|
|
14141
15273
|
const shortId = state.sessionId.slice(0, 12);
|
|
@@ -14153,7 +15285,7 @@ Uploaded: ${now.toISOString()}`
|
|
|
14153
15285
|
contributionId = contribution.id;
|
|
14154
15286
|
state.contributionId = contributionId;
|
|
14155
15287
|
state.baselineUploaded = false;
|
|
14156
|
-
await
|
|
15288
|
+
await persistState(state);
|
|
14157
15289
|
}
|
|
14158
15290
|
const uploaded = await uploadEpochBaselineArtifacts({
|
|
14159
15291
|
client,
|
|
@@ -14172,6 +15304,12 @@ Uploaded: ${now.toISOString()}`
|
|
|
14172
15304
|
async function uploadEpochBaseline(params) {
|
|
14173
15305
|
const artifacts = buildEpochBaselineArtifacts(params);
|
|
14174
15306
|
if (!artifacts) return null;
|
|
15307
|
+
pinScopedTreeRef(
|
|
15308
|
+
params.repoRoot,
|
|
15309
|
+
`refs/hillclimb/scoped/${params.sessionId}/baseline/${epochPrefix2(params.epoch)}/tree`,
|
|
15310
|
+
artifacts.baselineTreeSha,
|
|
15311
|
+
`protected baseline ${epochPrefix2(params.epoch)} for session ${params.sessionId}`
|
|
15312
|
+
);
|
|
14175
15313
|
if (!canUploadEpochBaselineArtifacts(params.epoch, artifacts)) return null;
|
|
14176
15314
|
const uploaded = await uploadEpochBaselineArtifacts({
|
|
14177
15315
|
client: params.client,
|
|
@@ -14180,7 +15318,10 @@ async function uploadEpochBaseline(params) {
|
|
|
14180
15318
|
artifacts
|
|
14181
15319
|
});
|
|
14182
15320
|
if (!uploaded) return null;
|
|
14183
|
-
return {
|
|
15321
|
+
return {
|
|
15322
|
+
baselineTreeSha: artifacts.baselineTreeSha,
|
|
15323
|
+
omittedKeys: artifacts.omittedKeys
|
|
15324
|
+
};
|
|
14184
15325
|
}
|
|
14185
15326
|
async function openEpoch(params) {
|
|
14186
15327
|
const { baselineSha, headSha } = pinEpochBaseline(
|
|
@@ -14190,10 +15331,28 @@ async function openEpoch(params) {
|
|
|
14190
15331
|
);
|
|
14191
15332
|
const uploaded = await uploadEpochBaseline({ ...params, baselineSha });
|
|
14192
15333
|
if (!uploaded) return null;
|
|
14193
|
-
return {
|
|
15334
|
+
return {
|
|
15335
|
+
baselineSha,
|
|
15336
|
+
baselineTreeSha: uploaded.baselineTreeSha,
|
|
15337
|
+
headSha,
|
|
15338
|
+
omittedKeys: uploaded.omittedKeys
|
|
15339
|
+
};
|
|
14194
15340
|
}
|
|
14195
|
-
|
|
15341
|
+
function isPositiveInteger(value) {
|
|
15342
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
15343
|
+
}
|
|
15344
|
+
function sessionSnapshotLimits(state) {
|
|
15345
|
+
const limits = state.snapshotLimits;
|
|
15346
|
+
if (!limits || typeof limits !== "object") return LEGACY_SNAPSHOT_LIMITS;
|
|
15347
|
+
const candidate = limits;
|
|
15348
|
+
if (!isPositiveInteger(candidate.maxTrackedFileBytes) || !isPositiveInteger(candidate.maxUntrackedFileBytes) || !isPositiveInteger(candidate.maxBinaryFileBytes)) {
|
|
15349
|
+
return LEGACY_SNAPSHOT_LIMITS;
|
|
15350
|
+
}
|
|
15351
|
+
return limits;
|
|
15352
|
+
}
|
|
15353
|
+
async function initializeSession(repoRoot, tool, sessionId, limits, lineage, legacyBoundaryPending = false) {
|
|
14196
15354
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
15355
|
+
const effectiveLimits = limits ?? resolveSnapshotLimits(void 0);
|
|
14197
15356
|
const frozen = freezeEpochBaseline({
|
|
14198
15357
|
repoRoot,
|
|
14199
15358
|
sessionId,
|
|
@@ -14201,7 +15360,10 @@ async function initializeSession(repoRoot, tool, sessionId) {
|
|
|
14201
15360
|
epoch: 1,
|
|
14202
15361
|
prevHeadSha: null,
|
|
14203
15362
|
transitionKind: "initial",
|
|
14204
|
-
startedAt
|
|
15363
|
+
startedAt,
|
|
15364
|
+
limits: effectiveLimits,
|
|
15365
|
+
lineage,
|
|
15366
|
+
pinCanonicalRefs: !legacyBoundaryPending
|
|
14205
15367
|
});
|
|
14206
15368
|
if (!frozen) return null;
|
|
14207
15369
|
const state = {
|
|
@@ -14216,17 +15378,67 @@ async function initializeSession(repoRoot, tool, sessionId) {
|
|
|
14216
15378
|
lastSnapshotTreeSha: frozen.baselineTreeSha,
|
|
14217
15379
|
startedAt,
|
|
14218
15380
|
epoch: 1,
|
|
15381
|
+
nextEpoch: 2,
|
|
14219
15382
|
turnCount: 0,
|
|
14220
15383
|
headSha: frozen.headSha,
|
|
14221
|
-
repoRoot
|
|
15384
|
+
repoRoot,
|
|
15385
|
+
snapshotLimits: effectiveLimits,
|
|
15386
|
+
omittedKeys: omissionKeys(frozen.omittedFiles),
|
|
15387
|
+
codexLineage: lineage,
|
|
15388
|
+
codexLineageChecked: tool === "codex" ? false : void 0,
|
|
15389
|
+
legacyBoundaryPending: legacyBoundaryPending || void 0
|
|
14222
15390
|
};
|
|
14223
|
-
await
|
|
15391
|
+
await writeSessionState2(state, tool);
|
|
14224
15392
|
appendLog(
|
|
14225
15393
|
"info",
|
|
14226
15394
|
`git-traces: session pending (repo=${repoRoot}, baseline=${frozen.baselineSha.slice(0, 8)}, awaiting first turn)`
|
|
14227
15395
|
);
|
|
14228
15396
|
return state;
|
|
14229
15397
|
}
|
|
15398
|
+
function pinScopedStateRefs(repoRoot, state) {
|
|
15399
|
+
const prefix = epochPrefix2(state.epoch);
|
|
15400
|
+
const baselinePrefix = `refs/hillclimb/scoped/${state.sessionId}/baseline/${prefix}`;
|
|
15401
|
+
pinRef(repoRoot, `${baselinePrefix}/tracked`, state.baselineSha);
|
|
15402
|
+
if (state.baselineTreeSha) {
|
|
15403
|
+
pinScopedTreeRef(
|
|
15404
|
+
repoRoot,
|
|
15405
|
+
`${baselinePrefix}/tree`,
|
|
15406
|
+
state.baselineTreeSha,
|
|
15407
|
+
`protected baseline ${prefix} for session ${state.sessionId}`
|
|
15408
|
+
);
|
|
15409
|
+
}
|
|
15410
|
+
if (state.turnCount > 0 && state.lastSnapshotTreeSha) {
|
|
15411
|
+
pinScopedTurnSnapshot(
|
|
15412
|
+
repoRoot,
|
|
15413
|
+
state.sessionId,
|
|
15414
|
+
state.epoch,
|
|
15415
|
+
state.turnCount,
|
|
15416
|
+
state.lastSnapshotSha,
|
|
15417
|
+
state.lastSnapshotTreeSha
|
|
15418
|
+
);
|
|
15419
|
+
}
|
|
15420
|
+
}
|
|
15421
|
+
function pinCanonicalStateRefs(repoRoot, state) {
|
|
15422
|
+
pinScopedStateRefs(repoRoot, state);
|
|
15423
|
+
const prefix = epochPrefix2(state.epoch);
|
|
15424
|
+
const baselinePrefix = `refs/hillclimb/baseline/${state.sessionId}/${prefix}`;
|
|
15425
|
+
pinRef(repoRoot, `${baselinePrefix}/tracked`, state.baselineSha);
|
|
15426
|
+
if (state.baselineTreeSha) {
|
|
15427
|
+
pinScopedTreeRef(
|
|
15428
|
+
repoRoot,
|
|
15429
|
+
`${baselinePrefix}/tree`,
|
|
15430
|
+
state.baselineTreeSha,
|
|
15431
|
+
`canonical baseline ${prefix} for session ${state.sessionId}`
|
|
15432
|
+
);
|
|
15433
|
+
}
|
|
15434
|
+
if (state.turnCount > 0) {
|
|
15435
|
+
pinRef(
|
|
15436
|
+
repoRoot,
|
|
15437
|
+
`refs/hillclimb/turns/${state.sessionId}/${prefix}/${turnSuffix2(state.turnCount)}`,
|
|
15438
|
+
state.lastSnapshotSha
|
|
15439
|
+
);
|
|
15440
|
+
}
|
|
15441
|
+
}
|
|
14230
15442
|
async function cleanupStaleScopedSessionStates(repoRoot, tool, currentSessionId, options = {}) {
|
|
14231
15443
|
const nowMs = options.nowMs ?? Date.now();
|
|
14232
15444
|
const ttlMs = options.ttlMs ?? STALE_SCOPED_STATE_TTL_MS;
|
|
@@ -14235,8 +15447,15 @@ async function cleanupStaleScopedSessionStates(repoRoot, tool, currentSessionId,
|
|
|
14235
15447
|
for (const { state, mtimeMs } of states) {
|
|
14236
15448
|
if (state.sessionId === currentSessionId) continue;
|
|
14237
15449
|
const startedAtMs = Date.parse(state.startedAt);
|
|
14238
|
-
const ageBaseMs = Number.isNaN(startedAtMs) ? mtimeMs : startedAtMs;
|
|
15450
|
+
const ageBaseMs = Number.isNaN(startedAtMs) ? mtimeMs : Math.max(startedAtMs, mtimeMs);
|
|
14239
15451
|
if (nowMs - ageBaseMs <= ttlMs) continue;
|
|
15452
|
+
if (await readLegacySessionState(repoRoot, tool, state.sessionId)) {
|
|
15453
|
+
appendLog(
|
|
15454
|
+
"info",
|
|
15455
|
+
`git-traces: preserving stale scoped session ${state.sessionId} while matching legacy state exists`
|
|
15456
|
+
);
|
|
15457
|
+
continue;
|
|
15458
|
+
}
|
|
14240
15459
|
try {
|
|
14241
15460
|
await acquireLock3(repoRoot, tool, state.sessionId, 1, 0);
|
|
14242
15461
|
} catch {
|
|
@@ -14244,15 +15463,38 @@ async function cleanupStaleScopedSessionStates(repoRoot, tool, currentSessionId,
|
|
|
14244
15463
|
"info",
|
|
14245
15464
|
`git-traces: leaving session ${state.sessionId} (lock held \u2014 still active)`
|
|
14246
15465
|
);
|
|
14247
|
-
continue;
|
|
14248
|
-
}
|
|
14249
|
-
try {
|
|
15466
|
+
continue;
|
|
15467
|
+
}
|
|
15468
|
+
try {
|
|
15469
|
+
const lockedMtimeMs = await statSessionStateMtime(
|
|
15470
|
+
repoRoot,
|
|
15471
|
+
tool,
|
|
15472
|
+
state.sessionId
|
|
15473
|
+
);
|
|
15474
|
+
if (lockedMtimeMs === null) continue;
|
|
15475
|
+
const lockedBaseMs = Number.isNaN(startedAtMs) ? lockedMtimeMs : Math.max(startedAtMs, lockedMtimeMs);
|
|
15476
|
+
if (nowMs - lockedBaseMs <= ttlMs) {
|
|
15477
|
+
appendLog(
|
|
15478
|
+
"info",
|
|
15479
|
+
`git-traces: leaving session ${state.sessionId} (state refreshed before reap)`
|
|
15480
|
+
);
|
|
15481
|
+
continue;
|
|
15482
|
+
}
|
|
15483
|
+
const pinnedTurnTrees = countScopedTurnTreeRefs(
|
|
15484
|
+
repoRoot,
|
|
15485
|
+
state.sessionId,
|
|
15486
|
+
epochPrefix2(state.epoch)
|
|
15487
|
+
);
|
|
15488
|
+
const totalPinnedTurnTrees = countScopedTurnTreeRefs(
|
|
15489
|
+
repoRoot,
|
|
15490
|
+
state.sessionId
|
|
15491
|
+
);
|
|
14250
15492
|
appendLog(
|
|
14251
15493
|
"info",
|
|
14252
|
-
`git-traces: cleaning up stale scoped session ${state.sessionId}`
|
|
15494
|
+
`git-traces: cleaning up stale scoped session ${state.sessionId} (epoch=${state.epoch}, uploadedTurns=${state.turnCount}, pinnedTurnTrees=${pinnedTurnTrees}, unuploadedPinnedTurns=${Math.max(0, pinnedTurnTrees - state.turnCount)}, totalPinnedTurnTrees=${totalPinnedTurnTrees}, contribution=${state.contributionId ?? "<none>"})`
|
|
14253
15495
|
);
|
|
14254
15496
|
cleanupSessionRefs(repoRoot, state.sessionId);
|
|
14255
|
-
await
|
|
15497
|
+
await deleteSessionState2(repoRoot, tool, state.sessionId);
|
|
14256
15498
|
removed++;
|
|
14257
15499
|
} finally {
|
|
14258
15500
|
await releaseLock3(repoRoot, tool, state.sessionId);
|
|
@@ -14260,7 +15502,26 @@ async function cleanupStaleScopedSessionStates(repoRoot, tool, currentSessionId,
|
|
|
14260
15502
|
}
|
|
14261
15503
|
return removed;
|
|
14262
15504
|
}
|
|
14263
|
-
async function
|
|
15505
|
+
async function acquireBoundaryLegacyLock(repoRoot, tool, timing) {
|
|
15506
|
+
for (; ; ) {
|
|
15507
|
+
try {
|
|
15508
|
+
if (timing) {
|
|
15509
|
+
await acquireLock3(repoRoot, tool, null, timing.retries, timing.delayMs);
|
|
15510
|
+
} else {
|
|
15511
|
+
await acquireLock3(repoRoot, tool, null);
|
|
15512
|
+
}
|
|
15513
|
+
return;
|
|
15514
|
+
} catch (err) {
|
|
15515
|
+
if (!(err instanceof LockContentionError)) throw err;
|
|
15516
|
+
appendLog(
|
|
15517
|
+
"info",
|
|
15518
|
+
`git-traces: detached lifecycle worker still waiting for legacy boundary lock (repo=${repoRoot}, tool=${tool})`
|
|
15519
|
+
);
|
|
15520
|
+
timing?.onChunkExhausted?.();
|
|
15521
|
+
}
|
|
15522
|
+
}
|
|
15523
|
+
}
|
|
15524
|
+
async function prepareSessionStartRepo(repo, tool, sessionId, lineage) {
|
|
14264
15525
|
const { repoRoot } = repo;
|
|
14265
15526
|
if (!isGitRepo(repoRoot)) {
|
|
14266
15527
|
appendLog(
|
|
@@ -14269,34 +15530,72 @@ async function processSessionStartRepo(repo, tool, sessionId) {
|
|
|
14269
15530
|
);
|
|
14270
15531
|
return "skipped";
|
|
14271
15532
|
}
|
|
14272
|
-
let
|
|
15533
|
+
let scopedLockAcquired = false;
|
|
14273
15534
|
try {
|
|
14274
15535
|
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
|
-
|
|
15536
|
+
scopedLockAcquired = true;
|
|
15537
|
+
const scoped = (await readSessionStateCandidates(repoRoot, tool, sessionId)).scoped?.state;
|
|
15538
|
+
if (scoped) {
|
|
15539
|
+
pinScopedStateRefs(repoRoot, scoped);
|
|
15540
|
+
await touchSessionState(repoRoot, tool, sessionId);
|
|
15541
|
+
return "initialized";
|
|
15542
|
+
}
|
|
15543
|
+
const state = await initializeSession(
|
|
15544
|
+
repoRoot,
|
|
15545
|
+
tool,
|
|
15546
|
+
sessionId,
|
|
15547
|
+
resolveSnapshotLimits(repo.config.snapshotLimits),
|
|
15548
|
+
lineage,
|
|
15549
|
+
true
|
|
15550
|
+
);
|
|
15551
|
+
return state ? "initialized" : "failed";
|
|
15552
|
+
} catch (err) {
|
|
15553
|
+
appendLog(
|
|
15554
|
+
"error",
|
|
15555
|
+
`git-traces: SessionStart failed for repo ${repoRoot}: ${formatError(err)}`
|
|
15556
|
+
);
|
|
15557
|
+
return "failed";
|
|
15558
|
+
} finally {
|
|
15559
|
+
if (scopedLockAcquired) await releaseLock3(repoRoot, tool, sessionId);
|
|
15560
|
+
}
|
|
15561
|
+
}
|
|
15562
|
+
function hasPreparedSessionCaptureProgress(state) {
|
|
15563
|
+
return state.contributionId !== null || state.baselineUploaded === true || state.epoch > 1 || state.turnCount > 0;
|
|
15564
|
+
}
|
|
15565
|
+
async function completeSessionStartRepo(repo, tool, sessionId, lineage, timing) {
|
|
15566
|
+
const { repoRoot } = repo;
|
|
15567
|
+
let legacyLockAcquired = false;
|
|
15568
|
+
let scopedLockAcquired = false;
|
|
15569
|
+
try {
|
|
15570
|
+
await acquireBoundaryLegacyLock(repoRoot, tool, timing);
|
|
15571
|
+
legacyLockAcquired = true;
|
|
15572
|
+
await acquireLock3(repoRoot, tool, sessionId);
|
|
15573
|
+
scopedLockAcquired = true;
|
|
15574
|
+
const candidates = await readSessionStateCandidates(
|
|
15575
|
+
repoRoot,
|
|
15576
|
+
tool,
|
|
15577
|
+
sessionId
|
|
15578
|
+
);
|
|
15579
|
+
if (!candidates.scoped) {
|
|
15580
|
+
appendLog(
|
|
15581
|
+
"info",
|
|
15582
|
+
`git-traces: SessionStart state already cleaned before legacy reconciliation (repo=${repoRoot}, session=${sessionId})`
|
|
15583
|
+
);
|
|
15584
|
+
return "skipped";
|
|
15585
|
+
}
|
|
15586
|
+
const reconciled = reconcileLockedSessionState(candidates);
|
|
15587
|
+
if (!reconciled.state) return "skipped";
|
|
15588
|
+
const lineageChanged = mergeLineageIntoState(reconciled.state, lineage);
|
|
15589
|
+
pinCanonicalStateRefs(repoRoot, reconciled.state);
|
|
15590
|
+
if (reconciled.needsWrite || lineageChanged) {
|
|
15591
|
+
await writeSessionState2(reconciled.state, tool);
|
|
15592
|
+
}
|
|
15593
|
+
if (candidates.legacy) {
|
|
15594
|
+
await deleteLegacySessionState(repoRoot, tool, sessionId);
|
|
15595
|
+
appendLog(
|
|
15596
|
+
"info",
|
|
15597
|
+
`git-traces: migrated legacy session on SessionStart (repo=${repoRoot}, session=${sessionId})`
|
|
15598
|
+
);
|
|
14300
15599
|
}
|
|
14301
15600
|
const staleCount = await cleanupStaleScopedSessionStates(
|
|
14302
15601
|
repoRoot,
|
|
@@ -14309,16 +15608,16 @@ async function processSessionStartRepo(repo, tool, sessionId) {
|
|
|
14309
15608
|
`git-traces: cleaned stale scoped sessions (repo=${repoRoot}, count=${staleCount})`
|
|
14310
15609
|
);
|
|
14311
15610
|
}
|
|
14312
|
-
|
|
14313
|
-
return state ? "initialized" : "failed";
|
|
15611
|
+
return "initialized";
|
|
14314
15612
|
} catch (err) {
|
|
14315
15613
|
appendLog(
|
|
14316
15614
|
"error",
|
|
14317
|
-
`git-traces: SessionStart failed for repo ${repoRoot}: ${formatError(err)}`
|
|
15615
|
+
`git-traces: SessionStart reconciliation failed for repo ${repoRoot}: ${formatError(err)}`
|
|
14318
15616
|
);
|
|
14319
15617
|
return "failed";
|
|
14320
15618
|
} finally {
|
|
14321
|
-
if (
|
|
15619
|
+
if (scopedLockAcquired) await releaseLock3(repoRoot, tool, sessionId);
|
|
15620
|
+
if (legacyLockAcquired) await releaseLock3(repoRoot, tool, null);
|
|
14322
15621
|
}
|
|
14323
15622
|
}
|
|
14324
15623
|
async function handleSessionStart(payload, tool) {
|
|
@@ -14345,11 +15644,24 @@ async function handleSessionStart(payload, tool) {
|
|
|
14345
15644
|
appendLog("info", `git-traces: reaped ${reaped} stale lock file(s)`);
|
|
14346
15645
|
}
|
|
14347
15646
|
const repos = await loadConfiguredRepos();
|
|
14348
|
-
|
|
15647
|
+
const prepared = [];
|
|
14349
15648
|
let skipped = 0;
|
|
14350
15649
|
let failed = 0;
|
|
14351
15650
|
for (const repo of repos) {
|
|
14352
|
-
const outcome = await
|
|
15651
|
+
const outcome = await prepareSessionStartRepo(repo, tool, sessionId);
|
|
15652
|
+
if (outcome === "initialized") prepared.push(repo);
|
|
15653
|
+
else if (outcome === "skipped") skipped++;
|
|
15654
|
+
else failed++;
|
|
15655
|
+
}
|
|
15656
|
+
const lineage = (await detectCodexLineageForHook(payload, tool, sessionId)).lineage;
|
|
15657
|
+
let initialized = 0;
|
|
15658
|
+
for (const repo of prepared) {
|
|
15659
|
+
const outcome = await completeSessionStartRepo(
|
|
15660
|
+
repo,
|
|
15661
|
+
tool,
|
|
15662
|
+
sessionId,
|
|
15663
|
+
lineage
|
|
15664
|
+
);
|
|
14353
15665
|
if (outcome === "initialized") initialized++;
|
|
14354
15666
|
else if (outcome === "skipped") skipped++;
|
|
14355
15667
|
else failed++;
|
|
@@ -14359,7 +15671,7 @@ async function handleSessionStart(payload, tool) {
|
|
|
14359
15671
|
`git-traces: SessionStart summary (session=${sessionId}, tool=${tool}, triggerRepo=${startingProject.repoRoot}, configured=${repos.length}, initialized=${initialized}, skipped=${skipped}, failed=${failed})`
|
|
14360
15672
|
);
|
|
14361
15673
|
}
|
|
14362
|
-
function buildInitialBaselineArtifactsForState(repoRoot, tool, state) {
|
|
15674
|
+
function buildInitialBaselineArtifactsForState(repoRoot, tool, state, limits) {
|
|
14363
15675
|
if (state.baselineTreeSha && state.baselineMetadata) {
|
|
14364
15676
|
return buildFrozenEpochBaselineArtifacts({
|
|
14365
15677
|
repoRoot,
|
|
@@ -14377,9 +15689,81 @@ function buildInitialBaselineArtifactsForState(repoRoot, tool, state) {
|
|
|
14377
15689
|
baselineSha: state.baselineSha,
|
|
14378
15690
|
prevHeadSha: null,
|
|
14379
15691
|
transitionKind: "initial",
|
|
14380
|
-
startedAt: state.startedAt
|
|
15692
|
+
startedAt: state.startedAt,
|
|
15693
|
+
limits,
|
|
15694
|
+
lineage: state.codexLineage
|
|
14381
15695
|
});
|
|
14382
15696
|
}
|
|
15697
|
+
function mergeLineageIntoState(state, detected) {
|
|
15698
|
+
const merged = mergeCodexLineage(state.codexLineage, detected);
|
|
15699
|
+
if (!merged || JSON.stringify(merged) === JSON.stringify(state.codexLineage)) {
|
|
15700
|
+
return false;
|
|
15701
|
+
}
|
|
15702
|
+
state.codexLineage = merged;
|
|
15703
|
+
if (state.baselineMetadata) {
|
|
15704
|
+
const baselineMetadata = { ...state.baselineMetadata };
|
|
15705
|
+
delete baselineMetadata.forkedFromThreadId;
|
|
15706
|
+
delete baselineMetadata.replayBoundary;
|
|
15707
|
+
state.baselineMetadata = {
|
|
15708
|
+
...baselineMetadata,
|
|
15709
|
+
...merged
|
|
15710
|
+
};
|
|
15711
|
+
}
|
|
15712
|
+
return true;
|
|
15713
|
+
}
|
|
15714
|
+
function reconcileSessionStateCandidates(candidates) {
|
|
15715
|
+
const { scoped, legacy } = candidates;
|
|
15716
|
+
if (!scoped) {
|
|
15717
|
+
if (!legacy) return { state: null, needsMigration: false };
|
|
15718
|
+
return { state: { ...legacy.state }, needsMigration: true };
|
|
15719
|
+
}
|
|
15720
|
+
if (!legacy) return { state: { ...scoped.state }, needsMigration: false };
|
|
15721
|
+
let authoritative = scoped;
|
|
15722
|
+
let other = legacy;
|
|
15723
|
+
const preferLegacy = scoped.state.legacyBoundaryPending === true && !hasPreparedSessionCaptureProgress(scoped.state);
|
|
15724
|
+
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)) {
|
|
15725
|
+
authoritative = legacy;
|
|
15726
|
+
other = scoped;
|
|
15727
|
+
}
|
|
15728
|
+
const state = { ...authoritative.state };
|
|
15729
|
+
mergeLineageIntoState(state, other.state.codexLineage);
|
|
15730
|
+
if (other.state.codexLineageChecked === true) {
|
|
15731
|
+
state.codexLineageChecked = true;
|
|
15732
|
+
}
|
|
15733
|
+
if (state.contributionId === other.state.contributionId) {
|
|
15734
|
+
state.nextEpoch = Math.max(
|
|
15735
|
+
state.nextEpoch ?? state.epoch + 1,
|
|
15736
|
+
other.state.nextEpoch ?? other.state.epoch + 1
|
|
15737
|
+
);
|
|
15738
|
+
state.codexLineageStamped = mergeCodexLineage(
|
|
15739
|
+
state.codexLineageStamped,
|
|
15740
|
+
other.state.codexLineageStamped
|
|
15741
|
+
);
|
|
15742
|
+
}
|
|
15743
|
+
return { state, needsMigration: true };
|
|
15744
|
+
}
|
|
15745
|
+
function reconcileLockedSessionState(candidates) {
|
|
15746
|
+
const reconciled = reconcileSessionStateCandidates(candidates);
|
|
15747
|
+
if (!reconciled.state) return { state: null, needsWrite: false };
|
|
15748
|
+
const boundaryCleared = reconciled.state.legacyBoundaryPending === true;
|
|
15749
|
+
delete reconciled.state.legacyBoundaryPending;
|
|
15750
|
+
return {
|
|
15751
|
+
state: reconciled.state,
|
|
15752
|
+
needsWrite: reconciled.needsMigration || boundaryCleared
|
|
15753
|
+
};
|
|
15754
|
+
}
|
|
15755
|
+
async function reserveEpoch(state, persistState) {
|
|
15756
|
+
const reservedEpoch = Math.max(
|
|
15757
|
+
state.nextEpoch ?? state.epoch + 1,
|
|
15758
|
+
state.epoch + 1
|
|
15759
|
+
);
|
|
15760
|
+
state.nextEpoch = reservedEpoch + 1;
|
|
15761
|
+
await persistState(state);
|
|
15762
|
+
return reservedEpoch;
|
|
15763
|
+
}
|
|
15764
|
+
function isTerminalContributionError(err) {
|
|
15765
|
+
return err instanceof PlatformError && err.status === 404 && err.code === "CONTRIBUTION_NOT_FOUND" || isContributionUploadNotUploaded(err);
|
|
15766
|
+
}
|
|
14383
15767
|
async function loadRepoClient(repo) {
|
|
14384
15768
|
const identity = await loadIdentity(repo.config.apiBaseUrl);
|
|
14385
15769
|
if (!identity) {
|
|
@@ -14391,14 +15775,71 @@ async function loadRepoClient(repo) {
|
|
|
14391
15775
|
}
|
|
14392
15776
|
return new PlatformClient(repo.config.apiBaseUrl, identity.sessionCookie);
|
|
14393
15777
|
}
|
|
15778
|
+
async function refreshLineageBaseline(params) {
|
|
15779
|
+
const { repo, state, tool, persistState, client, limits } = params;
|
|
15780
|
+
if (state.codexLineage === void 0 || JSON.stringify(state.codexLineage) === JSON.stringify(state.codexLineageStamped) || state.contributionId === null || !state.baselineUploaded) {
|
|
15781
|
+
return false;
|
|
15782
|
+
}
|
|
15783
|
+
try {
|
|
15784
|
+
const nextEpoch = await reserveEpoch(state, persistState);
|
|
15785
|
+
const artifacts = await openEpoch({
|
|
15786
|
+
repoRoot: repo.repoRoot,
|
|
15787
|
+
client,
|
|
15788
|
+
contributionId: state.contributionId,
|
|
15789
|
+
sessionId: state.sessionId,
|
|
15790
|
+
tool,
|
|
15791
|
+
epoch: nextEpoch,
|
|
15792
|
+
prevHeadSha: state.headSha || null,
|
|
15793
|
+
// The downstream contract has no metadata-only transition. This is a
|
|
15794
|
+
// complete baseline at the current HEAD, so use its compatible initial
|
|
15795
|
+
// baseline shape and explain the refresh only in collector state/logs.
|
|
15796
|
+
transitionKind: "initial",
|
|
15797
|
+
startedAt: state.startedAt,
|
|
15798
|
+
limits,
|
|
15799
|
+
lineage: state.codexLineage
|
|
15800
|
+
});
|
|
15801
|
+
if (!artifacts) {
|
|
15802
|
+
appendLog(
|
|
15803
|
+
"warn",
|
|
15804
|
+
`git-traces: lineage baseline refresh deferred after capture (repo=${repo.repoRoot}, project=${repo.config.projectId}, epoch=${nextEpoch})`
|
|
15805
|
+
);
|
|
15806
|
+
return false;
|
|
15807
|
+
}
|
|
15808
|
+
Object.assign(state, {
|
|
15809
|
+
baselineSha: artifacts.baselineSha,
|
|
15810
|
+
baselineTreeSha: artifacts.baselineTreeSha,
|
|
15811
|
+
lastSnapshotSha: artifacts.baselineSha,
|
|
15812
|
+
lastSnapshotTreeSha: artifacts.baselineTreeSha,
|
|
15813
|
+
epoch: nextEpoch,
|
|
15814
|
+
turnCount: 0,
|
|
15815
|
+
headSha: artifacts.headSha,
|
|
15816
|
+
omittedKeys: artifacts.omittedKeys,
|
|
15817
|
+
codexLineageStamped: state.codexLineage
|
|
15818
|
+
});
|
|
15819
|
+
await persistState(state);
|
|
15820
|
+
appendLog(
|
|
15821
|
+
"info",
|
|
15822
|
+
`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)})`
|
|
15823
|
+
);
|
|
15824
|
+
return true;
|
|
15825
|
+
} catch (err) {
|
|
15826
|
+
if (isTerminalContributionError(err)) throw err;
|
|
15827
|
+
appendLog(
|
|
15828
|
+
"warn",
|
|
15829
|
+
`git-traces: lineage baseline refresh failed after capture; lineage remains pending (repo=${repo.repoRoot}, project=${repo.config.projectId}): ${formatError(err)}`
|
|
15830
|
+
);
|
|
15831
|
+
return false;
|
|
15832
|
+
}
|
|
15833
|
+
}
|
|
14394
15834
|
async function registerInitialContribution(params) {
|
|
14395
|
-
const { repo, state, tool, client, artifacts } = params;
|
|
15835
|
+
const { repo, state, tool, persistState, client, artifacts } = params;
|
|
14396
15836
|
const contributionId = await createGitTracesContribution({
|
|
14397
15837
|
client,
|
|
14398
15838
|
config: repo.config,
|
|
14399
15839
|
repoRoot: repo.repoRoot,
|
|
14400
15840
|
state,
|
|
14401
15841
|
tool,
|
|
15842
|
+
persistState,
|
|
14402
15843
|
now: /* @__PURE__ */ new Date(),
|
|
14403
15844
|
artifacts
|
|
14404
15845
|
});
|
|
@@ -14407,14 +15848,44 @@ async function registerInitialContribution(params) {
|
|
|
14407
15848
|
state.baselineUploaded = true;
|
|
14408
15849
|
state.baselineTreeSha = artifacts.baselineTreeSha;
|
|
14409
15850
|
state.lastSnapshotTreeSha = artifacts.baselineTreeSha;
|
|
14410
|
-
|
|
15851
|
+
state.codexLineageStamped = state.codexLineage;
|
|
15852
|
+
await persistState(state);
|
|
14411
15853
|
appendLog(
|
|
14412
15854
|
"info",
|
|
14413
15855
|
`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
15856
|
);
|
|
14415
15857
|
return true;
|
|
14416
15858
|
}
|
|
14417
|
-
|
|
15859
|
+
function reportNewlyOmittedFiles(state, omittedFiles, repoRoot) {
|
|
15860
|
+
const currentKeys = omissionKeys(omittedFiles);
|
|
15861
|
+
const previousKeys = Array.isArray(state.omittedKeys) && state.omittedKeys.every((key) => typeof key === "string") ? state.omittedKeys : void 0;
|
|
15862
|
+
if (previousKeys === void 0) {
|
|
15863
|
+
state.omittedKeys = currentKeys;
|
|
15864
|
+
return true;
|
|
15865
|
+
}
|
|
15866
|
+
const known = new Set(previousKeys);
|
|
15867
|
+
const fresh = omittedFiles.filter((file) => !known.has(omissionKey(file)));
|
|
15868
|
+
if (fresh.length > 0) {
|
|
15869
|
+
const tracked = fresh.filter((file) => file.tracked);
|
|
15870
|
+
const untracked = fresh.filter((file) => !file.tracked);
|
|
15871
|
+
if (tracked.length > 0) {
|
|
15872
|
+
appendLog(
|
|
15873
|
+
"warn",
|
|
15874
|
+
`git-traces: turn newly omitted tracked ${summarizeOmittedFiles(tracked)} \u2014 content excluded from bundle and diffs from here on (repo=${repoRoot})`
|
|
15875
|
+
);
|
|
15876
|
+
}
|
|
15877
|
+
if (untracked.length > 0) {
|
|
15878
|
+
appendLog(
|
|
15879
|
+
"info",
|
|
15880
|
+
`git-traces: turn newly skipped untracked ${summarizeOmittedFiles(untracked)} (repo=${repoRoot})`
|
|
15881
|
+
);
|
|
15882
|
+
}
|
|
15883
|
+
}
|
|
15884
|
+
const changed = currentKeys.length !== previousKeys.length || currentKeys.some((key, i) => key !== previousKeys[i]);
|
|
15885
|
+
if (changed) state.omittedKeys = currentKeys;
|
|
15886
|
+
return changed;
|
|
15887
|
+
}
|
|
15888
|
+
async function processStopRepo(repo, tool, sessionId, recordedAt, lineageCheck, options = {}) {
|
|
14418
15889
|
const { repoRoot, config } = repo;
|
|
14419
15890
|
if (!isGitRepo(repoRoot)) {
|
|
14420
15891
|
appendLog(
|
|
@@ -14424,11 +15895,56 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
|
|
|
14424
15895
|
return "skipped";
|
|
14425
15896
|
}
|
|
14426
15897
|
let state = null;
|
|
14427
|
-
let
|
|
15898
|
+
let legacyLockAcquired = false;
|
|
15899
|
+
let scopedLockAcquired = false;
|
|
15900
|
+
const persistState = sessionId ? (next) => writeSessionState2(next, tool) : (next) => writeLegacySessionState(next, tool);
|
|
14428
15901
|
try {
|
|
14429
|
-
|
|
14430
|
-
|
|
14431
|
-
|
|
15902
|
+
if (options.waitForLegacyBoundary) {
|
|
15903
|
+
await acquireBoundaryLegacyLock(repoRoot, tool);
|
|
15904
|
+
legacyLockAcquired = true;
|
|
15905
|
+
} else {
|
|
15906
|
+
const matchingLegacyHint = sessionId ? await readLegacySessionState(repoRoot, tool, sessionId) : await readSessionState2(repoRoot, tool);
|
|
15907
|
+
if (matchingLegacyHint) {
|
|
15908
|
+
try {
|
|
15909
|
+
await acquireLock3(repoRoot, tool, null, 2, 0);
|
|
15910
|
+
legacyLockAcquired = true;
|
|
15911
|
+
} catch {
|
|
15912
|
+
appendLog(
|
|
15913
|
+
"info",
|
|
15914
|
+
`git-traces: preserving repo on Stop (repo=${repoRoot}, project=${config.projectId}, reason=legacy-lock-held)`
|
|
15915
|
+
);
|
|
15916
|
+
return "skipped";
|
|
15917
|
+
}
|
|
15918
|
+
}
|
|
15919
|
+
}
|
|
15920
|
+
if (sessionId) {
|
|
15921
|
+
await acquireLock3(repoRoot, tool, sessionId);
|
|
15922
|
+
scopedLockAcquired = true;
|
|
15923
|
+
const candidates = await readSessionStateCandidates(
|
|
15924
|
+
repoRoot,
|
|
15925
|
+
tool,
|
|
15926
|
+
sessionId
|
|
15927
|
+
);
|
|
15928
|
+
if (legacyLockAcquired) {
|
|
15929
|
+
const reconciled = reconcileLockedSessionState(candidates);
|
|
15930
|
+
state = reconciled.state;
|
|
15931
|
+
if (state) {
|
|
15932
|
+
pinCanonicalStateRefs(repoRoot, state);
|
|
15933
|
+
if (reconciled.needsWrite) await writeSessionState2(state, tool);
|
|
15934
|
+
}
|
|
15935
|
+
if (state && candidates.legacy) {
|
|
15936
|
+
await deleteLegacySessionState(repoRoot, tool, state.sessionId);
|
|
15937
|
+
}
|
|
15938
|
+
} else {
|
|
15939
|
+
state = candidates.scoped?.state ?? null;
|
|
15940
|
+
}
|
|
15941
|
+
} else {
|
|
15942
|
+
if (!legacyLockAcquired) {
|
|
15943
|
+
await acquireLock3(repoRoot, tool, null, 2, 0);
|
|
15944
|
+
legacyLockAcquired = true;
|
|
15945
|
+
}
|
|
15946
|
+
state = await readSessionState2(repoRoot, tool);
|
|
15947
|
+
}
|
|
14432
15948
|
if (!state) {
|
|
14433
15949
|
appendLog(
|
|
14434
15950
|
"info",
|
|
@@ -14436,6 +15952,40 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
|
|
|
14436
15952
|
);
|
|
14437
15953
|
return "no-state";
|
|
14438
15954
|
}
|
|
15955
|
+
const cleanupState = state;
|
|
15956
|
+
const finishSuccessfulStop = async (outcome) => {
|
|
15957
|
+
if (!options.cleanupOnSuccess) return outcome;
|
|
15958
|
+
await options.onBeforeCleanup?.();
|
|
15959
|
+
cleanupSessionRefs(repoRoot, cleanupState.sessionId);
|
|
15960
|
+
await deleteSessionState2(
|
|
15961
|
+
repoRoot,
|
|
15962
|
+
tool,
|
|
15963
|
+
sessionId ? cleanupState.sessionId : null
|
|
15964
|
+
);
|
|
15965
|
+
if (sessionId && legacyLockAcquired) {
|
|
15966
|
+
await deleteLegacySessionState(repoRoot, tool, cleanupState.sessionId);
|
|
15967
|
+
}
|
|
15968
|
+
appendLog(
|
|
15969
|
+
"info",
|
|
15970
|
+
`git-traces: session ${cleanupState.sessionId} cleaned up (repo=${repoRoot}, epochCount=${cleanupState.epoch}, lastTurnCount=${cleanupState.turnCount})`
|
|
15971
|
+
);
|
|
15972
|
+
return outcome;
|
|
15973
|
+
};
|
|
15974
|
+
let lineageStateChanged = false;
|
|
15975
|
+
if (lineageCheck?.performed) {
|
|
15976
|
+
lineageStateChanged = mergeLineageIntoState(
|
|
15977
|
+
state,
|
|
15978
|
+
lineageCheck.detection?.lineage
|
|
15979
|
+
);
|
|
15980
|
+
if (tool === "codex" && lineageCheck.detection?.conclusive === true && state.codexLineageChecked !== true) {
|
|
15981
|
+
state.codexLineageChecked = true;
|
|
15982
|
+
lineageStateChanged = true;
|
|
15983
|
+
}
|
|
15984
|
+
}
|
|
15985
|
+
if (lineageStateChanged) {
|
|
15986
|
+
await persistState(state);
|
|
15987
|
+
}
|
|
15988
|
+
const limits = sessionSnapshotLimits(state);
|
|
14439
15989
|
const lastSnapshotTreeSha = state.lastSnapshotTreeSha ?? state.baselineTreeSha;
|
|
14440
15990
|
if (!lastSnapshotTreeSha) {
|
|
14441
15991
|
appendLog(
|
|
@@ -14445,6 +15995,7 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
|
|
|
14445
15995
|
return "failed";
|
|
14446
15996
|
}
|
|
14447
15997
|
const currentHeadSha = captureHeadSha(repoRoot);
|
|
15998
|
+
const lineageNeedsRefresh = state.codexLineage !== void 0 && JSON.stringify(state.codexLineage) !== JSON.stringify(state.codexLineageStamped);
|
|
14448
15999
|
if (currentHeadSha !== state.headSha) {
|
|
14449
16000
|
const client2 = await loadRepoClient(repo);
|
|
14450
16001
|
if (!client2) return "skipped";
|
|
@@ -14452,7 +16003,8 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
|
|
|
14452
16003
|
const artifacts2 = buildInitialBaselineArtifactsForState(
|
|
14453
16004
|
repoRoot,
|
|
14454
16005
|
tool,
|
|
14455
|
-
state
|
|
16006
|
+
state,
|
|
16007
|
+
limits
|
|
14456
16008
|
);
|
|
14457
16009
|
if (!artifacts2 || !canUploadEpochBaselineArtifacts(1, artifacts2)) {
|
|
14458
16010
|
return "skipped";
|
|
@@ -14461,6 +16013,7 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
|
|
|
14461
16013
|
repo,
|
|
14462
16014
|
state,
|
|
14463
16015
|
tool,
|
|
16016
|
+
persistState,
|
|
14464
16017
|
client: client2,
|
|
14465
16018
|
artifacts: artifacts2
|
|
14466
16019
|
});
|
|
@@ -14479,7 +16032,7 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
|
|
|
14479
16032
|
state.headSha || null,
|
|
14480
16033
|
currentHeadSha
|
|
14481
16034
|
);
|
|
14482
|
-
const nextEpoch = state
|
|
16035
|
+
const nextEpoch = await reserveEpoch(state, persistState);
|
|
14483
16036
|
appendLog(
|
|
14484
16037
|
"info",
|
|
14485
16038
|
`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 +16046,9 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
|
|
|
14493
16046
|
epoch: nextEpoch,
|
|
14494
16047
|
prevHeadSha: state.headSha || null,
|
|
14495
16048
|
transitionKind,
|
|
14496
|
-
startedAt: state.startedAt
|
|
16049
|
+
startedAt: state.startedAt,
|
|
16050
|
+
limits,
|
|
16051
|
+
lineage: state.codexLineage
|
|
14497
16052
|
});
|
|
14498
16053
|
if (!artifacts) return "failed";
|
|
14499
16054
|
const next = {
|
|
@@ -14504,28 +16059,61 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
|
|
|
14504
16059
|
lastSnapshotTreeSha: artifacts.baselineTreeSha,
|
|
14505
16060
|
epoch: nextEpoch,
|
|
14506
16061
|
turnCount: 0,
|
|
14507
|
-
headSha: artifacts.headSha
|
|
16062
|
+
headSha: artifacts.headSha,
|
|
16063
|
+
omittedKeys: artifacts.omittedKeys,
|
|
16064
|
+
codexLineageStamped: state.codexLineage
|
|
14508
16065
|
};
|
|
14509
|
-
await
|
|
16066
|
+
await persistState(next);
|
|
14510
16067
|
appendLog(
|
|
14511
16068
|
"info",
|
|
14512
16069
|
`git-traces: epoch baseline uploaded (repo=${repoRoot}, project=${config.projectId}, contribution=${contributionId2}, epoch=${nextEpoch}, baseline=${artifacts.baselineSha.slice(0, 8)})`
|
|
14513
16070
|
);
|
|
14514
|
-
return "uploaded";
|
|
16071
|
+
return await finishSuccessfulStop("uploaded");
|
|
14515
16072
|
}
|
|
14516
16073
|
const currentSha = captureSnapshotSha(repoRoot);
|
|
14517
|
-
const
|
|
16074
|
+
const turnOmittedFiles = [];
|
|
16075
|
+
const currentTreeSha = buildSnapshotTree(repoRoot, currentSha, {
|
|
16076
|
+
omittedFiles: turnOmittedFiles,
|
|
16077
|
+
log: false,
|
|
16078
|
+
limits
|
|
16079
|
+
});
|
|
16080
|
+
const omissionsChanged = reportNewlyOmittedFiles(
|
|
16081
|
+
state,
|
|
16082
|
+
turnOmittedFiles,
|
|
16083
|
+
repoRoot
|
|
16084
|
+
);
|
|
14518
16085
|
const patchBuffer = createTreeDiffPatchGz(
|
|
14519
16086
|
repoRoot,
|
|
14520
16087
|
lastSnapshotTreeSha,
|
|
14521
|
-
currentTreeSha
|
|
16088
|
+
currentTreeSha,
|
|
16089
|
+
{ limits }
|
|
14522
16090
|
);
|
|
14523
16091
|
if (!patchBuffer) {
|
|
16092
|
+
if (omissionsChanged) {
|
|
16093
|
+
await persistState(state);
|
|
16094
|
+
} else {
|
|
16095
|
+
await touchSessionState(state.repoRoot, tool, sessionId);
|
|
16096
|
+
}
|
|
14524
16097
|
appendLog(
|
|
14525
16098
|
"info",
|
|
14526
16099
|
`git-traces: turn produced identical snapshot tree, skipping upload (repo=${repoRoot}, project=${config.projectId})`
|
|
14527
16100
|
);
|
|
14528
|
-
|
|
16101
|
+
if (lineageNeedsRefresh && state.contributionId !== null && state.baselineUploaded) {
|
|
16102
|
+
const client2 = await loadRepoClient(repo);
|
|
16103
|
+
const refreshed = client2 !== null && await refreshLineageBaseline({
|
|
16104
|
+
repo,
|
|
16105
|
+
state,
|
|
16106
|
+
tool,
|
|
16107
|
+
persistState,
|
|
16108
|
+
client: client2,
|
|
16109
|
+
limits
|
|
16110
|
+
});
|
|
16111
|
+
if (refreshed) {
|
|
16112
|
+
return await finishSuccessfulStop("uploaded");
|
|
16113
|
+
}
|
|
16114
|
+
if (options.cleanupOnSuccess) return "skipped";
|
|
16115
|
+
}
|
|
16116
|
+
return await finishSuccessfulStop("unchanged");
|
|
14529
16117
|
}
|
|
14530
16118
|
const prefix = epochPrefix2(state.epoch);
|
|
14531
16119
|
const nextTurnCount = state.turnCount + 1;
|
|
@@ -14544,7 +16132,8 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
|
|
|
14544
16132
|
const artifacts = buildInitialBaselineArtifactsForState(
|
|
14545
16133
|
repoRoot,
|
|
14546
16134
|
tool,
|
|
14547
|
-
state
|
|
16135
|
+
state,
|
|
16136
|
+
limits
|
|
14548
16137
|
);
|
|
14549
16138
|
if (!artifacts || !canUploadEpochBaselineArtifacts(1, artifacts)) {
|
|
14550
16139
|
return "skipped";
|
|
@@ -14553,6 +16142,7 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
|
|
|
14553
16142
|
repo,
|
|
14554
16143
|
state,
|
|
14555
16144
|
tool,
|
|
16145
|
+
persistState,
|
|
14556
16146
|
client,
|
|
14557
16147
|
artifacts
|
|
14558
16148
|
});
|
|
@@ -14571,6 +16161,14 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
|
|
|
14571
16161
|
`refs/hillclimb/turns/${state.sessionId}/${prefix}/${turnLabel}`,
|
|
14572
16162
|
currentSha
|
|
14573
16163
|
);
|
|
16164
|
+
pinScopedTurnSnapshot(
|
|
16165
|
+
repoRoot,
|
|
16166
|
+
state.sessionId,
|
|
16167
|
+
state.epoch,
|
|
16168
|
+
nextTurnCount,
|
|
16169
|
+
currentSha,
|
|
16170
|
+
currentTreeSha
|
|
16171
|
+
);
|
|
14574
16172
|
const uploaded = await uploadFile(
|
|
14575
16173
|
client,
|
|
14576
16174
|
contributionId,
|
|
@@ -14588,46 +16186,63 @@ async function processStopRepo(repo, tool, sessionId, recordedAt) {
|
|
|
14588
16186
|
state.turnCount = nextTurnCount;
|
|
14589
16187
|
state.lastSnapshotSha = currentSha;
|
|
14590
16188
|
state.lastSnapshotTreeSha = currentTreeSha;
|
|
14591
|
-
await
|
|
16189
|
+
await persistState(state);
|
|
14592
16190
|
appendLog(
|
|
14593
16191
|
"info",
|
|
14594
16192
|
`git-traces: patch uploaded (repo=${repoRoot}, project=${config.projectId}, contribution=${contributionId}, epoch=${state.epoch}, turn=${turnLabel}, bytes=${patchBuffer.byteLength})`
|
|
14595
16193
|
);
|
|
14596
|
-
|
|
16194
|
+
const lineageStillNeedsRefresh = state.codexLineage !== void 0 && JSON.stringify(state.codexLineage) !== JSON.stringify(state.codexLineageStamped);
|
|
16195
|
+
if (lineageStillNeedsRefresh) {
|
|
16196
|
+
const refreshed = await refreshLineageBaseline({
|
|
16197
|
+
repo,
|
|
16198
|
+
state,
|
|
16199
|
+
tool,
|
|
16200
|
+
persistState,
|
|
16201
|
+
client,
|
|
16202
|
+
limits
|
|
16203
|
+
});
|
|
16204
|
+
if (!refreshed && options.cleanupOnSuccess) return "skipped";
|
|
16205
|
+
}
|
|
16206
|
+
return await finishSuccessfulStop("uploaded");
|
|
14597
16207
|
} catch (err) {
|
|
14598
16208
|
appendLog(
|
|
14599
16209
|
"error",
|
|
14600
16210
|
`git-traces: Stop failed for repo ${repoRoot}: ${formatError(err)}`
|
|
14601
16211
|
);
|
|
14602
|
-
if (state && err
|
|
16212
|
+
if (state && isTerminalContributionError(err)) {
|
|
16213
|
+
const poisoned = isContributionUploadNotUploaded(err);
|
|
14603
16214
|
if (state.baselineTreeSha) {
|
|
14604
|
-
await
|
|
14605
|
-
|
|
14606
|
-
|
|
14607
|
-
|
|
14608
|
-
|
|
14609
|
-
|
|
14610
|
-
|
|
14611
|
-
|
|
14612
|
-
},
|
|
14613
|
-
tool
|
|
14614
|
-
);
|
|
16215
|
+
await persistState({
|
|
16216
|
+
...state,
|
|
16217
|
+
contributionId: null,
|
|
16218
|
+
baselineUploaded: false,
|
|
16219
|
+
lastSnapshotSha: state.baselineSha,
|
|
16220
|
+
lastSnapshotTreeSha: state.baselineTreeSha,
|
|
16221
|
+
turnCount: 0
|
|
16222
|
+
});
|
|
14615
16223
|
appendLog(
|
|
14616
16224
|
"warn",
|
|
14617
|
-
`git-traces: cleared stale contribution state (repo=${repoRoot}, project=${config.projectId}, missingContribution=${state.contributionId})`
|
|
16225
|
+
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
16226
|
);
|
|
14619
16227
|
} else {
|
|
14620
16228
|
cleanupSessionRefs(repoRoot, state.sessionId);
|
|
14621
|
-
await
|
|
16229
|
+
await deleteSessionState2(
|
|
16230
|
+
repoRoot,
|
|
16231
|
+
tool,
|
|
16232
|
+
sessionId ? state.sessionId : null
|
|
16233
|
+
);
|
|
14622
16234
|
appendLog(
|
|
14623
16235
|
"warn",
|
|
14624
|
-
`git-traces: deleted stale contribution state without a frozen baseline (repo=${repoRoot}, project=${config.projectId}, missingContribution=${state.contributionId})`
|
|
16236
|
+
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
16237
|
);
|
|
14626
16238
|
}
|
|
14627
16239
|
}
|
|
14628
16240
|
return "failed";
|
|
14629
16241
|
} finally {
|
|
14630
|
-
if (
|
|
16242
|
+
if (scopedLockAcquired) {
|
|
16243
|
+
await releaseLock3(repoRoot, tool, sessionId);
|
|
16244
|
+
}
|
|
16245
|
+
if (legacyLockAcquired) await releaseLock3(repoRoot, tool, null);
|
|
14631
16246
|
}
|
|
14632
16247
|
}
|
|
14633
16248
|
async function handleStop(payload, tool) {
|
|
@@ -14639,12 +16254,13 @@ async function handleStop(payload, tool) {
|
|
|
14639
16254
|
const recordedAt = Date.now();
|
|
14640
16255
|
const repos = await loadConfiguredRepos();
|
|
14641
16256
|
const repoByRoot = new Map(repos.map((repo) => [repo.repoRoot, repo]));
|
|
14642
|
-
const
|
|
16257
|
+
const targetsByRoot = /* @__PURE__ */ new Map();
|
|
14643
16258
|
let missingConfig = 0;
|
|
16259
|
+
let needsLineageCheck = false;
|
|
14644
16260
|
if (sessionId) {
|
|
14645
16261
|
const storedStates = await listSessionStatesForSession(tool, sessionId);
|
|
14646
16262
|
for (const { state } of storedStates) {
|
|
14647
|
-
const repo = repoByRoot.get(
|
|
16263
|
+
const repo = repoByRoot.get(path18.resolve(state.repoRoot));
|
|
14648
16264
|
if (!repo) {
|
|
14649
16265
|
missingConfig++;
|
|
14650
16266
|
appendLog(
|
|
@@ -14653,21 +16269,53 @@ async function handleStop(payload, tool) {
|
|
|
14653
16269
|
);
|
|
14654
16270
|
continue;
|
|
14655
16271
|
}
|
|
14656
|
-
|
|
16272
|
+
if (tool === "codex" && state.codexLineageChecked !== true) {
|
|
16273
|
+
needsLineageCheck = true;
|
|
16274
|
+
}
|
|
16275
|
+
targetsByRoot.set(repo.repoRoot, repo);
|
|
14657
16276
|
}
|
|
14658
|
-
|
|
14659
|
-
|
|
16277
|
+
for (const repo of repos) {
|
|
16278
|
+
const legacyState = await readLegacySessionState(
|
|
16279
|
+
repo.repoRoot,
|
|
16280
|
+
tool,
|
|
16281
|
+
sessionId
|
|
16282
|
+
);
|
|
16283
|
+
if (!legacyState) continue;
|
|
16284
|
+
targetsByRoot.set(repo.repoRoot, repo);
|
|
16285
|
+
if (tool === "codex" && legacyState.codexLineageChecked !== true) {
|
|
16286
|
+
needsLineageCheck = true;
|
|
16287
|
+
}
|
|
16288
|
+
}
|
|
16289
|
+
if (targetsByRoot.size === 0 && missingConfig === 0) {
|
|
16290
|
+
targetsByRoot.set(project.repoRoot, {
|
|
16291
|
+
repoRoot: project.repoRoot,
|
|
16292
|
+
config: project.config
|
|
16293
|
+
});
|
|
14660
16294
|
}
|
|
14661
16295
|
} else {
|
|
14662
|
-
|
|
16296
|
+
targetsByRoot.set(project.repoRoot, {
|
|
16297
|
+
repoRoot: project.repoRoot,
|
|
16298
|
+
config: project.config
|
|
16299
|
+
});
|
|
14663
16300
|
}
|
|
16301
|
+
const targets = [...targetsByRoot.values()];
|
|
16302
|
+
const lineageCheck = needsLineageCheck ? {
|
|
16303
|
+
performed: true,
|
|
16304
|
+
detection: await detectCodexLineageForHook(payload, tool, sessionId)
|
|
16305
|
+
} : { performed: false };
|
|
14664
16306
|
let uploaded = 0;
|
|
14665
16307
|
let unchanged = 0;
|
|
14666
16308
|
let skipped = missingConfig;
|
|
14667
16309
|
let noState = 0;
|
|
14668
16310
|
let failed = 0;
|
|
14669
16311
|
for (const repo of targets) {
|
|
14670
|
-
const outcome = await processStopRepo(
|
|
16312
|
+
const outcome = await processStopRepo(
|
|
16313
|
+
repo,
|
|
16314
|
+
tool,
|
|
16315
|
+
sessionId,
|
|
16316
|
+
recordedAt,
|
|
16317
|
+
lineageCheck
|
|
16318
|
+
);
|
|
14671
16319
|
if (outcome === "uploaded") uploaded++;
|
|
14672
16320
|
else if (outcome === "unchanged") unchanged++;
|
|
14673
16321
|
else if (outcome === "skipped") skipped++;
|
|
@@ -14679,49 +16327,48 @@ async function handleStop(payload, tool) {
|
|
|
14679
16327
|
`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
16328
|
);
|
|
14681
16329
|
}
|
|
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
16330
|
async function handleSessionEnd(payload, tool) {
|
|
14707
16331
|
const cwd = resolveCwd2(payload);
|
|
14708
16332
|
const sessionId = resolveSessionId2(payload);
|
|
14709
16333
|
const project = cwd ? await findProjectForCwd(cwd) : null;
|
|
14710
16334
|
const triggerRepo = project?.repoRoot ?? cwd ?? "<none>";
|
|
14711
16335
|
const recordedAt = Date.now();
|
|
14712
|
-
const
|
|
16336
|
+
const repos = await loadConfiguredRepos();
|
|
16337
|
+
const repoByRoot = new Map(repos.map((repo) => [repo.repoRoot, repo]));
|
|
16338
|
+
const repoRoots = /* @__PURE__ */ new Set();
|
|
16339
|
+
let needsLineageCheck = false;
|
|
14713
16340
|
if (sessionId) {
|
|
14714
16341
|
const states = await listSessionStatesForSession(tool, sessionId);
|
|
14715
16342
|
for (const { state } of states) {
|
|
14716
|
-
|
|
16343
|
+
const repoRoot = path18.resolve(state.repoRoot);
|
|
16344
|
+
repoRoots.add(repoRoot);
|
|
16345
|
+
const canProcess = repoByRoot.has(repoRoot) || project && path18.resolve(project.repoRoot) === repoRoot;
|
|
16346
|
+
if (canProcess && tool === "codex" && state.codexLineageChecked !== true) {
|
|
16347
|
+
needsLineageCheck = true;
|
|
16348
|
+
}
|
|
16349
|
+
}
|
|
16350
|
+
for (const repo of repos) {
|
|
16351
|
+
const legacyState = await readLegacySessionState(
|
|
16352
|
+
repo.repoRoot,
|
|
16353
|
+
tool,
|
|
16354
|
+
sessionId
|
|
16355
|
+
);
|
|
16356
|
+
if (!legacyState) continue;
|
|
16357
|
+
repoRoots.add(repo.repoRoot);
|
|
16358
|
+
if (tool === "codex" && legacyState.codexLineageChecked !== true) {
|
|
16359
|
+
needsLineageCheck = true;
|
|
16360
|
+
}
|
|
14717
16361
|
}
|
|
14718
16362
|
}
|
|
14719
|
-
if (repoRoots.
|
|
14720
|
-
|
|
16363
|
+
if (repoRoots.size === 0 && cwd) {
|
|
16364
|
+
const fallbackRepoRoot = project?.repoRoot ?? cwd;
|
|
16365
|
+
repoRoots.add(fallbackRepoRoot);
|
|
14721
16366
|
}
|
|
14722
|
-
if (repoRoots.
|
|
14723
|
-
const
|
|
14724
|
-
|
|
16367
|
+
if (repoRoots.size === 0) return;
|
|
16368
|
+
const lineageCheck = needsLineageCheck ? {
|
|
16369
|
+
performed: true,
|
|
16370
|
+
detection: await detectCodexLineageForHook(payload, tool, sessionId)
|
|
16371
|
+
} : { performed: false };
|
|
14725
16372
|
let cleaned = 0;
|
|
14726
16373
|
let noState = 0;
|
|
14727
16374
|
let uploaded = 0;
|
|
@@ -14729,7 +16376,7 @@ async function handleSessionEnd(payload, tool) {
|
|
|
14729
16376
|
let skipped = 0;
|
|
14730
16377
|
let failed = 0;
|
|
14731
16378
|
for (const repoRoot of repoRoots) {
|
|
14732
|
-
const repo = repoByRoot.get(
|
|
16379
|
+
const repo = repoByRoot.get(path18.resolve(repoRoot)) ?? (project && path18.resolve(project.repoRoot) === path18.resolve(repoRoot) ? { repoRoot: project.repoRoot, config: project.config } : null);
|
|
14733
16380
|
if (!repo) {
|
|
14734
16381
|
skipped++;
|
|
14735
16382
|
appendLog(
|
|
@@ -14742,7 +16389,9 @@ async function handleSessionEnd(payload, tool) {
|
|
|
14742
16389
|
repo,
|
|
14743
16390
|
tool,
|
|
14744
16391
|
sessionId,
|
|
14745
|
-
recordedAt
|
|
16392
|
+
recordedAt,
|
|
16393
|
+
lineageCheck,
|
|
16394
|
+
{ waitForLegacyBoundary: true, cleanupOnSuccess: true }
|
|
14746
16395
|
);
|
|
14747
16396
|
if (finalOutcome === "uploaded") uploaded++;
|
|
14748
16397
|
else if (finalOutcome === "unchanged") unchanged++;
|
|
@@ -14764,14 +16413,11 @@ async function handleSessionEnd(payload, tool) {
|
|
|
14764
16413
|
noState++;
|
|
14765
16414
|
continue;
|
|
14766
16415
|
}
|
|
14767
|
-
|
|
14768
|
-
if (outcome === "cleaned") cleaned++;
|
|
14769
|
-
else if (outcome === "no-state") noState++;
|
|
14770
|
-
else failed++;
|
|
16416
|
+
cleaned++;
|
|
14771
16417
|
}
|
|
14772
16418
|
appendLog(
|
|
14773
16419
|
"info",
|
|
14774
|
-
`git-traces: SessionEnd summary (session=${sessionId ?? "<none>"}, tool=${tool}, triggerRepo=${triggerRepo}, states=${repoRoots.
|
|
16420
|
+
`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
16421
|
);
|
|
14776
16422
|
}
|
|
14777
16423
|
|
|
@@ -14780,7 +16426,7 @@ var WORKER_ENV_FLAG2 = "HILLCLIMB_GIT_TRACES_WORKER";
|
|
|
14780
16426
|
var TOOL_ENV_FLAG2 = "HILLCLIMB_GIT_TRACES_TOOL";
|
|
14781
16427
|
var FLOW_ID_ENV2 = "HILLCLIMB_GIT_TRACES_FLOW";
|
|
14782
16428
|
function newFlowId2() {
|
|
14783
|
-
return
|
|
16429
|
+
return crypto7.randomBytes(3).toString("hex");
|
|
14784
16430
|
}
|
|
14785
16431
|
var KNOWN_TOOLS = /* @__PURE__ */ new Set([
|
|
14786
16432
|
"claude",
|
|
@@ -14951,6 +16597,13 @@ async function runGitTraces() {
|
|
|
14951
16597
|
appendLog("error", "git-traces: process.argv[1] is empty");
|
|
14952
16598
|
return;
|
|
14953
16599
|
}
|
|
16600
|
+
const workerEnv = {
|
|
16601
|
+
...process.env,
|
|
16602
|
+
[WORKER_ENV_FLAG2]: "1",
|
|
16603
|
+
[TOOL_ENV_FLAG2]: tool,
|
|
16604
|
+
[FLOW_ID_ENV2]: flowId
|
|
16605
|
+
};
|
|
16606
|
+
await applyDebugLogParentEnv(workerEnv, raw);
|
|
14954
16607
|
try {
|
|
14955
16608
|
const child = spawn3(
|
|
14956
16609
|
process.execPath,
|
|
@@ -14958,12 +16611,7 @@ async function runGitTraces() {
|
|
|
14958
16611
|
{
|
|
14959
16612
|
detached: true,
|
|
14960
16613
|
stdio: ["pipe", "ignore", "ignore"],
|
|
14961
|
-
env:
|
|
14962
|
-
...process.env,
|
|
14963
|
-
[WORKER_ENV_FLAG2]: "1",
|
|
14964
|
-
[TOOL_ENV_FLAG2]: tool,
|
|
14965
|
-
[FLOW_ID_ENV2]: flowId
|
|
14966
|
-
}
|
|
16614
|
+
env: workerEnv
|
|
14967
16615
|
}
|
|
14968
16616
|
);
|
|
14969
16617
|
child.on("error", (err) => {
|
|
@@ -15022,7 +16670,7 @@ async function runGitTracesWorker() {
|
|
|
15022
16670
|
return;
|
|
15023
16671
|
}
|
|
15024
16672
|
const event = payload.hook_event_name;
|
|
15025
|
-
const
|
|
16673
|
+
const eventKind2 = classifyHookEvent2(event);
|
|
15026
16674
|
appendLog("info", `git-traces worker: handling event=${event}`);
|
|
15027
16675
|
if (tool === "claude" && typeof payload.cursor_version === "string") {
|
|
15028
16676
|
appendLog(
|
|
@@ -15033,7 +16681,7 @@ async function runGitTracesWorker() {
|
|
|
15033
16681
|
}
|
|
15034
16682
|
try {
|
|
15035
16683
|
await selfHealHook2(payload, tool);
|
|
15036
|
-
switch (
|
|
16684
|
+
switch (eventKind2) {
|
|
15037
16685
|
case "sessionStart":
|
|
15038
16686
|
await handleSessionStart(payload, tool);
|
|
15039
16687
|
break;
|
|
@@ -15055,7 +16703,7 @@ async function runGitTracesWorker() {
|
|
|
15055
16703
|
${stack}` : ""}`
|
|
15056
16704
|
);
|
|
15057
16705
|
} finally {
|
|
15058
|
-
if (
|
|
16706
|
+
if (eventKind2 === "stop" || eventKind2 === "sessionEnd") {
|
|
15059
16707
|
await recordDebugLogCompletion({
|
|
15060
16708
|
kind: "git",
|
|
15061
16709
|
tool,
|
|
@@ -15066,29 +16714,29 @@ ${stack}` : ""}`
|
|
|
15066
16714
|
}
|
|
15067
16715
|
|
|
15068
16716
|
// src/outputs/zip.ts
|
|
15069
|
-
import
|
|
15070
|
-
import
|
|
16717
|
+
import fs19 from "fs";
|
|
16718
|
+
import path20 from "path";
|
|
15071
16719
|
import archiver2 from "archiver";
|
|
15072
16720
|
|
|
15073
16721
|
// src/outputs/downloads.ts
|
|
15074
16722
|
import { execSync as execSync2 } from "child_process";
|
|
15075
|
-
import
|
|
15076
|
-
import
|
|
15077
|
-
import
|
|
16723
|
+
import fs18 from "fs";
|
|
16724
|
+
import os10 from "os";
|
|
16725
|
+
import path19 from "path";
|
|
15078
16726
|
function getDownloadsFolder() {
|
|
15079
|
-
const home =
|
|
16727
|
+
const home = os10.homedir();
|
|
15080
16728
|
if (process.platform === "linux") {
|
|
15081
16729
|
try {
|
|
15082
16730
|
const xdgDir = execSync2("xdg-user-dir DOWNLOAD", {
|
|
15083
16731
|
encoding: "utf-8",
|
|
15084
16732
|
timeout: 3e3
|
|
15085
16733
|
}).trim();
|
|
15086
|
-
if (xdgDir &&
|
|
16734
|
+
if (xdgDir && fs18.existsSync(xdgDir)) return xdgDir;
|
|
15087
16735
|
} catch {
|
|
15088
16736
|
}
|
|
15089
16737
|
}
|
|
15090
|
-
const downloads =
|
|
15091
|
-
if (
|
|
16738
|
+
const downloads = path19.join(home, "Downloads");
|
|
16739
|
+
if (fs18.existsSync(downloads)) return downloads;
|
|
15092
16740
|
return home;
|
|
15093
16741
|
}
|
|
15094
16742
|
|
|
@@ -15097,11 +16745,11 @@ function sanitizeFilename(name) {
|
|
|
15097
16745
|
return name.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
15098
16746
|
}
|
|
15099
16747
|
function getUniqueFilename(dir, base, ext) {
|
|
15100
|
-
let candidate =
|
|
15101
|
-
if (!
|
|
16748
|
+
let candidate = path20.join(dir, `${base}${ext}`);
|
|
16749
|
+
if (!fs19.existsSync(candidate)) return candidate;
|
|
15102
16750
|
let i = 1;
|
|
15103
|
-
while (
|
|
15104
|
-
candidate =
|
|
16751
|
+
while (fs19.existsSync(candidate)) {
|
|
16752
|
+
candidate = path20.join(dir, `${base}-${i}${ext}`);
|
|
15105
16753
|
i++;
|
|
15106
16754
|
}
|
|
15107
16755
|
return candidate;
|
|
@@ -15111,13 +16759,13 @@ var ZipOutput = class {
|
|
|
15111
16759
|
label = "Save as .zip to Downloads";
|
|
15112
16760
|
async emit(group, options) {
|
|
15113
16761
|
const downloadsDir = getDownloadsFolder();
|
|
15114
|
-
const repoName = sanitizeFilename(
|
|
16762
|
+
const repoName = sanitizeFilename(path20.basename(group.repoPath));
|
|
15115
16763
|
const timeRange = options.timeRange;
|
|
15116
16764
|
const rangePart = timeRange?.label ?? "all";
|
|
15117
16765
|
const epochSeconds = Math.floor(Date.now() / 1e3);
|
|
15118
16766
|
const baseName = `logs-${repoName}-${rangePart}-${epochSeconds}`;
|
|
15119
16767
|
const outputPath = getUniqueFilename(downloadsDir, baseName, ".zip");
|
|
15120
|
-
const output =
|
|
16768
|
+
const output = fs19.createWriteStream(outputPath);
|
|
15121
16769
|
const archive = archiver2("zip", { zlib: { level: 6 } });
|
|
15122
16770
|
const done = new Promise((resolve, reject) => {
|
|
15123
16771
|
output.on("close", resolve);
|
|
@@ -15311,15 +16959,15 @@ async function confirmExport(group, output) {
|
|
|
15311
16959
|
}
|
|
15312
16960
|
|
|
15313
16961
|
// src/sources/claude.ts
|
|
15314
|
-
import
|
|
15315
|
-
import
|
|
15316
|
-
import
|
|
16962
|
+
import fs20 from "fs";
|
|
16963
|
+
import os11 from "os";
|
|
16964
|
+
import path21 from "path";
|
|
15317
16965
|
import readline2 from "readline";
|
|
15318
16966
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["tool-results"]);
|
|
15319
16967
|
async function resolveRepoPath(projectDir) {
|
|
15320
|
-
const indexPath =
|
|
16968
|
+
const indexPath = path21.join(projectDir, "sessions-index.json");
|
|
15321
16969
|
try {
|
|
15322
|
-
const raw = await
|
|
16970
|
+
const raw = await fs20.promises.readFile(indexPath, "utf-8");
|
|
15323
16971
|
const data = JSON.parse(raw);
|
|
15324
16972
|
if (data.originalPath && typeof data.originalPath === "string") {
|
|
15325
16973
|
return data.originalPath;
|
|
@@ -15327,12 +16975,12 @@ async function resolveRepoPath(projectDir) {
|
|
|
15327
16975
|
} catch {
|
|
15328
16976
|
}
|
|
15329
16977
|
const cwdCounts = /* @__PURE__ */ new Map();
|
|
15330
|
-
const entries = await
|
|
16978
|
+
const entries = await fs20.promises.readdir(projectDir, {
|
|
15331
16979
|
withFileTypes: true
|
|
15332
16980
|
});
|
|
15333
16981
|
for (const entry of entries) {
|
|
15334
16982
|
if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue;
|
|
15335
|
-
const cwd = await extractCwdFromJsonl(
|
|
16983
|
+
const cwd = await extractCwdFromJsonl(path21.join(projectDir, entry.name));
|
|
15336
16984
|
if (cwd) {
|
|
15337
16985
|
cwdCounts.set(cwd, (cwdCounts.get(cwd) ?? 0) + 1);
|
|
15338
16986
|
}
|
|
@@ -15351,7 +16999,7 @@ async function resolveRepoPath(projectDir) {
|
|
|
15351
16999
|
return null;
|
|
15352
17000
|
}
|
|
15353
17001
|
async function extractCwdFromJsonl(filePath) {
|
|
15354
|
-
const stream =
|
|
17002
|
+
const stream = fs20.createReadStream(filePath, { encoding: "utf-8" });
|
|
15355
17003
|
const rl = readline2.createInterface({ input: stream, crlfDelay: Infinity });
|
|
15356
17004
|
try {
|
|
15357
17005
|
for await (const line of rl) {
|
|
@@ -15373,12 +17021,12 @@ async function extractCwdFromJsonl(filePath) {
|
|
|
15373
17021
|
async function collectFiles(dir, baseDir, sourceName, repoPath, results) {
|
|
15374
17022
|
let entries;
|
|
15375
17023
|
try {
|
|
15376
|
-
entries = await
|
|
17024
|
+
entries = await fs20.promises.readdir(dir, { withFileTypes: true });
|
|
15377
17025
|
} catch {
|
|
15378
17026
|
return;
|
|
15379
17027
|
}
|
|
15380
17028
|
for (const entry of entries) {
|
|
15381
|
-
const fullPath =
|
|
17029
|
+
const fullPath = path21.join(dir, entry.name);
|
|
15382
17030
|
if (entry.isDirectory()) {
|
|
15383
17031
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
15384
17032
|
await collectFiles(fullPath, baseDir, sourceName, repoPath, results);
|
|
@@ -15400,19 +17048,19 @@ function fallbackDecode(encodedName) {
|
|
|
15400
17048
|
var ClaudeSource = class {
|
|
15401
17049
|
name = "claude";
|
|
15402
17050
|
async scan() {
|
|
15403
|
-
const baseDir =
|
|
17051
|
+
const baseDir = path21.join(os11.homedir(), ".claude", "projects");
|
|
15404
17052
|
try {
|
|
15405
|
-
await
|
|
17053
|
+
await fs20.promises.access(baseDir);
|
|
15406
17054
|
} catch {
|
|
15407
17055
|
return [];
|
|
15408
17056
|
}
|
|
15409
|
-
const projectDirs = await
|
|
17057
|
+
const projectDirs = await fs20.promises.readdir(baseDir, {
|
|
15410
17058
|
withFileTypes: true
|
|
15411
17059
|
});
|
|
15412
17060
|
const dirEntries = projectDirs.filter((d) => d.isDirectory());
|
|
15413
17061
|
const resultArrays = await Promise.all(
|
|
15414
17062
|
dirEntries.map(async (dir) => {
|
|
15415
|
-
const projectPath =
|
|
17063
|
+
const projectPath = path21.join(baseDir, dir.name);
|
|
15416
17064
|
const repoPath = await resolveRepoPath(projectPath) ?? fallbackDecode(dir.name);
|
|
15417
17065
|
const files = [];
|
|
15418
17066
|
await collectFiles(
|
|
@@ -15430,12 +17078,12 @@ var ClaudeSource = class {
|
|
|
15430
17078
|
};
|
|
15431
17079
|
|
|
15432
17080
|
// src/sources/codex.ts
|
|
15433
|
-
import
|
|
15434
|
-
import
|
|
15435
|
-
import
|
|
17081
|
+
import fs21 from "fs";
|
|
17082
|
+
import os12 from "os";
|
|
17083
|
+
import path22 from "path";
|
|
15436
17084
|
import readline3 from "readline";
|
|
15437
|
-
async function
|
|
15438
|
-
const stream =
|
|
17085
|
+
async function parseSessionMeta2(filePath) {
|
|
17086
|
+
const stream = fs21.createReadStream(filePath, { encoding: "utf-8" });
|
|
15439
17087
|
const rl = readline3.createInterface({ input: stream, crlfDelay: Infinity });
|
|
15440
17088
|
try {
|
|
15441
17089
|
for await (const line of rl) {
|
|
@@ -15460,12 +17108,12 @@ async function findJsonlFiles(dir) {
|
|
|
15460
17108
|
async function walk(d) {
|
|
15461
17109
|
let entries;
|
|
15462
17110
|
try {
|
|
15463
|
-
entries = await
|
|
17111
|
+
entries = await fs21.promises.readdir(d, { withFileTypes: true });
|
|
15464
17112
|
} catch {
|
|
15465
17113
|
return;
|
|
15466
17114
|
}
|
|
15467
17115
|
for (const entry of entries) {
|
|
15468
|
-
const full =
|
|
17116
|
+
const full = path22.join(d, entry.name);
|
|
15469
17117
|
if (entry.isDirectory()) {
|
|
15470
17118
|
await walk(full);
|
|
15471
17119
|
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
@@ -15479,11 +17127,11 @@ async function findJsonlFiles(dir) {
|
|
|
15479
17127
|
async function loadHistory(historyPath) {
|
|
15480
17128
|
const map = /* @__PURE__ */ new Map();
|
|
15481
17129
|
try {
|
|
15482
|
-
await
|
|
17130
|
+
await fs21.promises.access(historyPath);
|
|
15483
17131
|
} catch {
|
|
15484
17132
|
return map;
|
|
15485
17133
|
}
|
|
15486
|
-
const stream =
|
|
17134
|
+
const stream = fs21.createReadStream(historyPath, { encoding: "utf-8" });
|
|
15487
17135
|
const rl = readline3.createInterface({ input: stream, crlfDelay: Infinity });
|
|
15488
17136
|
try {
|
|
15489
17137
|
for await (const line of rl) {
|
|
@@ -15510,22 +17158,22 @@ async function loadHistory(historyPath) {
|
|
|
15510
17158
|
var CodexSource = class {
|
|
15511
17159
|
name = "codex";
|
|
15512
17160
|
async scan() {
|
|
15513
|
-
const codexDir =
|
|
15514
|
-
const
|
|
17161
|
+
const codexDir = path22.join(os12.homedir(), ".codex");
|
|
17162
|
+
const sessionsDir2 = path22.join(codexDir, "sessions");
|
|
15515
17163
|
try {
|
|
15516
|
-
await
|
|
17164
|
+
await fs21.promises.access(sessionsDir2);
|
|
15517
17165
|
} catch {
|
|
15518
17166
|
return [];
|
|
15519
17167
|
}
|
|
15520
|
-
const historyPath =
|
|
17168
|
+
const historyPath = path22.join(codexDir, "history.jsonl");
|
|
15521
17169
|
const [jsonlFiles, historyMap] = await Promise.all([
|
|
15522
|
-
findJsonlFiles(
|
|
17170
|
+
findJsonlFiles(sessionsDir2),
|
|
15523
17171
|
loadHistory(historyPath)
|
|
15524
17172
|
]);
|
|
15525
17173
|
const results = [];
|
|
15526
17174
|
const metaResults = await Promise.all(
|
|
15527
17175
|
jsonlFiles.map(async (filePath) => {
|
|
15528
|
-
const meta = await
|
|
17176
|
+
const meta = await parseSessionMeta2(filePath);
|
|
15529
17177
|
return meta ? { filePath, meta } : null;
|
|
15530
17178
|
})
|
|
15531
17179
|
);
|
|
@@ -15540,9 +17188,9 @@ var CodexSource = class {
|
|
|
15540
17188
|
});
|
|
15541
17189
|
const historyLines = historyMap.get(meta.sessionId);
|
|
15542
17190
|
if (historyLines) {
|
|
15543
|
-
const sessionDir =
|
|
15544
|
-
const historyAbsPath =
|
|
15545
|
-
|
|
17191
|
+
const sessionDir = path22.relative(sessionsDir2, path22.dirname(filePath));
|
|
17192
|
+
const historyAbsPath = path22.join(
|
|
17193
|
+
sessionsDir2,
|
|
15546
17194
|
sessionDir,
|
|
15547
17195
|
`history-${meta.sessionId}.jsonl`
|
|
15548
17196
|
);
|
|
@@ -15561,18 +17209,18 @@ var CodexSource = class {
|
|
|
15561
17209
|
};
|
|
15562
17210
|
|
|
15563
17211
|
// src/sources/copilotChat.ts
|
|
15564
|
-
import
|
|
15565
|
-
import
|
|
15566
|
-
import
|
|
17212
|
+
import fs22 from "fs";
|
|
17213
|
+
import os13 from "os";
|
|
17214
|
+
import path23 from "path";
|
|
15567
17215
|
import { fileURLToPath } from "url";
|
|
15568
17216
|
function vsCodeUserDirs() {
|
|
15569
|
-
const home =
|
|
17217
|
+
const home = os13.homedir();
|
|
15570
17218
|
const dirs = [
|
|
15571
|
-
|
|
15572
|
-
|
|
17219
|
+
path23.join(home, "Library", "Application Support", "Code", "User"),
|
|
17220
|
+
path23.join(home, ".config", "Code", "User")
|
|
15573
17221
|
];
|
|
15574
17222
|
if (process.env.APPDATA) {
|
|
15575
|
-
dirs.push(
|
|
17223
|
+
dirs.push(path23.join(process.env.APPDATA, "Code", "User"));
|
|
15576
17224
|
}
|
|
15577
17225
|
return dirs;
|
|
15578
17226
|
}
|
|
@@ -15587,7 +17235,7 @@ function uriToFsPath(uri) {
|
|
|
15587
17235
|
async function readWorkspaceFolder(workspaceJsonPath) {
|
|
15588
17236
|
let raw;
|
|
15589
17237
|
try {
|
|
15590
|
-
raw = await
|
|
17238
|
+
raw = await fs22.promises.readFile(workspaceJsonPath, "utf-8");
|
|
15591
17239
|
} catch {
|
|
15592
17240
|
return null;
|
|
15593
17241
|
}
|
|
@@ -15609,10 +17257,10 @@ var CopilotChatSource = class {
|
|
|
15609
17257
|
async scan() {
|
|
15610
17258
|
const results = [];
|
|
15611
17259
|
for (const userDir of vsCodeUserDirs()) {
|
|
15612
|
-
const workspaceStorage =
|
|
17260
|
+
const workspaceStorage = path23.join(userDir, "workspaceStorage");
|
|
15613
17261
|
let hashDirs;
|
|
15614
17262
|
try {
|
|
15615
|
-
hashDirs = await
|
|
17263
|
+
hashDirs = await fs22.promises.readdir(workspaceStorage, {
|
|
15616
17264
|
withFileTypes: true
|
|
15617
17265
|
});
|
|
15618
17266
|
} catch {
|
|
@@ -15620,22 +17268,22 @@ var CopilotChatSource = class {
|
|
|
15620
17268
|
}
|
|
15621
17269
|
for (const hash of hashDirs) {
|
|
15622
17270
|
if (!hash.isDirectory()) continue;
|
|
15623
|
-
const wsRoot =
|
|
15624
|
-
const transcriptsDir =
|
|
17271
|
+
const wsRoot = path23.join(workspaceStorage, hash.name);
|
|
17272
|
+
const transcriptsDir = path23.join(
|
|
15625
17273
|
wsRoot,
|
|
15626
17274
|
"GitHub.copilot-chat",
|
|
15627
17275
|
"transcripts"
|
|
15628
17276
|
);
|
|
15629
17277
|
let transcriptEntries;
|
|
15630
17278
|
try {
|
|
15631
|
-
transcriptEntries = await
|
|
17279
|
+
transcriptEntries = await fs22.promises.readdir(transcriptsDir, {
|
|
15632
17280
|
withFileTypes: true
|
|
15633
17281
|
});
|
|
15634
17282
|
} catch {
|
|
15635
17283
|
continue;
|
|
15636
17284
|
}
|
|
15637
17285
|
const repoPath = await readWorkspaceFolder(
|
|
15638
|
-
|
|
17286
|
+
path23.join(wsRoot, "workspace.json")
|
|
15639
17287
|
);
|
|
15640
17288
|
if (!repoPath) continue;
|
|
15641
17289
|
for (const entry of transcriptEntries) {
|
|
@@ -15643,7 +17291,7 @@ var CopilotChatSource = class {
|
|
|
15643
17291
|
const sessionId = entry.name.slice(0, -".jsonl".length);
|
|
15644
17292
|
results.push({
|
|
15645
17293
|
sourceName: this.name,
|
|
15646
|
-
absolutePath:
|
|
17294
|
+
absolutePath: path23.join(transcriptsDir, entry.name),
|
|
15647
17295
|
repoPath,
|
|
15648
17296
|
metadata: { sessionId }
|
|
15649
17297
|
});
|
|
@@ -15683,7 +17331,7 @@ function reportRedactionStats(noun, stats) {
|
|
|
15683
17331
|
async function filterByTimeRange(group, range) {
|
|
15684
17332
|
const results = await Promise.all(
|
|
15685
17333
|
group.files.map(
|
|
15686
|
-
(f) =>
|
|
17334
|
+
(f) => fs23.promises.stat(f.absolutePath).then((s) => ({ file: f, stat: s })).catch(() => null)
|
|
15687
17335
|
)
|
|
15688
17336
|
);
|
|
15689
17337
|
const filtered = [];
|
|
@@ -15710,10 +17358,10 @@ async function runInteractive() {
|
|
|
15710
17358
|
s.start(`Scanning ${source.name} logs...`);
|
|
15711
17359
|
const allFiles = await source.scan();
|
|
15712
17360
|
const allGroups = await mergeByRepo(allFiles);
|
|
15713
|
-
const repoRoot =
|
|
17361
|
+
const repoRoot = path24.resolve(repo.root);
|
|
15714
17362
|
const matching = allGroups.filter((g) => {
|
|
15715
|
-
const resolved =
|
|
15716
|
-
return resolved === repoRoot || resolved.startsWith(repoRoot +
|
|
17363
|
+
const resolved = path24.resolve(g.repoPath);
|
|
17364
|
+
return resolved === repoRoot || resolved.startsWith(repoRoot + path24.sep);
|
|
15717
17365
|
});
|
|
15718
17366
|
if (matching.length === 0) {
|
|
15719
17367
|
s.stop(`No ${source.name} logs found for ${repo.name}.`);
|
|
@@ -15744,7 +17392,7 @@ async function runInteractive() {
|
|
|
15744
17392
|
}
|
|
15745
17393
|
}
|
|
15746
17394
|
const envFileNames = await discoverEnvFiles(repoRoot);
|
|
15747
|
-
const envFilePaths = envFileNames.map((n) =>
|
|
17395
|
+
const envFilePaths = envFileNames.map((n) => path24.join(repoRoot, n));
|
|
15748
17396
|
const additionalFiles = await promptSecretFiles(envFileNames);
|
|
15749
17397
|
const secretResult = await collectSecrets(
|
|
15750
17398
|
repoRoot,
|