hillclimb 0.8.12 → 0.8.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.js +158 -33
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -14361,7 +14361,7 @@ async function uploadArtifact(client, contributionId, filename, mimeType, buffer
|
|
|
14361
14361
|
}
|
|
14362
14362
|
|
|
14363
14363
|
// package.json
|
|
14364
|
-
var version = "0.8.
|
|
14364
|
+
var version = "0.8.13";
|
|
14365
14365
|
|
|
14366
14366
|
// src/version.ts
|
|
14367
14367
|
var CLI_VERSION = version;
|
|
@@ -16120,11 +16120,20 @@ function deleteRef(repoRoot, refName) {
|
|
|
16120
16120
|
function captureSnapshotSha(repoRoot) {
|
|
16121
16121
|
return captureWorkingCommitSha(repoRoot);
|
|
16122
16122
|
}
|
|
16123
|
-
function
|
|
16124
|
-
|
|
16125
|
-
|
|
16126
|
-
|
|
16127
|
-
|
|
16123
|
+
function snapshotBlobInventory(repoRoot, treeish) {
|
|
16124
|
+
const blobs = /* @__PURE__ */ new Map();
|
|
16125
|
+
for (const entry of gitBuffer(repoRoot, [
|
|
16126
|
+
"ls-tree",
|
|
16127
|
+
"-r",
|
|
16128
|
+
"-l",
|
|
16129
|
+
"-z",
|
|
16130
|
+
treeish
|
|
16131
|
+
]).toString("utf-8").split("\0")) {
|
|
16132
|
+
const [, type, sha, size] = entry.slice(0, entry.indexOf(" ")).trim().split(/\s+/);
|
|
16133
|
+
if (type === "blob" && sha && /^\d+$/.test(size ?? ""))
|
|
16134
|
+
blobs.set(sha, Number(size));
|
|
16135
|
+
}
|
|
16136
|
+
return blobs;
|
|
16128
16137
|
}
|
|
16129
16138
|
function formatSize(bytes) {
|
|
16130
16139
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
@@ -17320,6 +17329,8 @@ async function sweepStaleLockFiles(ttlMs = STALE_LOCK_TTL_MS3, now = Date.now())
|
|
|
17320
17329
|
// src/git-traces/pending-captures.ts
|
|
17321
17330
|
var MAX_CAPTURES = 128;
|
|
17322
17331
|
var MAX_PENDING_BYTES2 = 2 * 1024 * 1024 * 1024;
|
|
17332
|
+
var PendingCaptureLimitError = class extends Error {
|
|
17333
|
+
};
|
|
17323
17334
|
var activeCapture = new AsyncLocalStorage();
|
|
17324
17335
|
function currentPendingCapture(repoRoot) {
|
|
17325
17336
|
const context = activeCapture.getStore();
|
|
@@ -17410,10 +17421,7 @@ function listPendingCaptures2(repoRoot, tool, sessionId) {
|
|
|
17410
17421
|
function hasPendingCaptures(repoRoot, tool, sessionId) {
|
|
17411
17422
|
return listPendingCaptures2(repoRoot, tool, sessionId).length > 0;
|
|
17412
17423
|
}
|
|
17413
|
-
function
|
|
17414
|
-
const first = records[0];
|
|
17415
|
-
if (!first) return 0;
|
|
17416
|
-
const directory = queueDirectory(first.repoRoot, first.tool, first.sessionId);
|
|
17424
|
+
function queueDiskBytes(directory) {
|
|
17417
17425
|
let diskBytes = 0;
|
|
17418
17426
|
if (fs19.existsSync(directory)) {
|
|
17419
17427
|
for (const entry of fs19.readdirSync(directory, { withFileTypes: true })) {
|
|
@@ -17428,11 +17436,57 @@ function pendingBytes(records) {
|
|
|
17428
17436
|
}
|
|
17429
17437
|
}
|
|
17430
17438
|
}
|
|
17431
|
-
return diskBytes
|
|
17432
|
-
|
|
17439
|
+
return diskBytes;
|
|
17440
|
+
}
|
|
17441
|
+
function chargedBytes(record) {
|
|
17442
|
+
return record.retainedBytes ?? record.treeBytes;
|
|
17443
|
+
}
|
|
17444
|
+
function pendingBytes(records) {
|
|
17445
|
+
const first = records[0];
|
|
17446
|
+
if (!first) return 0;
|
|
17447
|
+
return queueDiskBytes(
|
|
17448
|
+
queueDirectory(first.repoRoot, first.tool, first.sessionId)
|
|
17449
|
+
) + records.reduce(
|
|
17450
|
+
(sum, record) => sum + chargedBytes(record) + (fs19.existsSync(path18.join(captureDirectory(record), "capture.json")) ? 0 : Buffer.byteLength(JSON.stringify(record))),
|
|
17433
17451
|
0
|
|
17434
17452
|
);
|
|
17435
17453
|
}
|
|
17454
|
+
function measureTreeRetention(repoRoot, treeSha, bases, context) {
|
|
17455
|
+
const finish = startCaptureStage("tree-inventory", context);
|
|
17456
|
+
let status = "failed";
|
|
17457
|
+
let details;
|
|
17458
|
+
try {
|
|
17459
|
+
const blobs = snapshotBlobInventory(repoRoot, treeSha);
|
|
17460
|
+
const shared = /* @__PURE__ */ new Set();
|
|
17461
|
+
let basesRead = 0;
|
|
17462
|
+
for (const base of new Set(
|
|
17463
|
+
bases.filter((base2) => !!base2)
|
|
17464
|
+
)) {
|
|
17465
|
+
try {
|
|
17466
|
+
const baseBlobs = base === treeSha ? blobs : snapshotBlobInventory(repoRoot, base);
|
|
17467
|
+
for (const sha of baseBlobs.keys()) shared.add(sha);
|
|
17468
|
+
basesRead++;
|
|
17469
|
+
} catch (err) {
|
|
17470
|
+
if (!isMissingObjectError(err)) throw err;
|
|
17471
|
+
appendLog(
|
|
17472
|
+
"warn",
|
|
17473
|
+
`git-traces: capture base is missing for retention accounting; charging unshared content (repo=${repoRoot}, base=${base})`
|
|
17474
|
+
);
|
|
17475
|
+
}
|
|
17476
|
+
}
|
|
17477
|
+
let treeBytes = 0;
|
|
17478
|
+
let retainedBytes = 0;
|
|
17479
|
+
for (const [sha, size] of blobs) {
|
|
17480
|
+
treeBytes += size;
|
|
17481
|
+
if (!shared.has(sha)) retainedBytes += size;
|
|
17482
|
+
}
|
|
17483
|
+
status = "ok";
|
|
17484
|
+
details = `treeBytes=${treeBytes}, treeFiles=${blobs.size}, retainedBytes=${retainedBytes}, bases=${basesRead}`;
|
|
17485
|
+
return { treeBytes, treeFiles: blobs.size, retainedBytes };
|
|
17486
|
+
} finally {
|
|
17487
|
+
finish(status, details);
|
|
17488
|
+
}
|
|
17489
|
+
}
|
|
17436
17490
|
async function withCaptureQueueLock(repoRoot, tool, sessionId, work) {
|
|
17437
17491
|
const lockTool = `${tool}:capture-queue-v1`;
|
|
17438
17492
|
await acquireLock3(repoRoot, lockTool, sessionId);
|
|
@@ -17456,17 +17510,33 @@ function storePendingCapture(params) {
|
|
|
17456
17510
|
params.tool,
|
|
17457
17511
|
params.sessionId
|
|
17458
17512
|
);
|
|
17513
|
+
const retention = measureTreeRetention(
|
|
17514
|
+
params.repoRoot,
|
|
17515
|
+
input.treeSha,
|
|
17516
|
+
[
|
|
17517
|
+
existing.at(-1)?.treeSha,
|
|
17518
|
+
params.seed.lastSnapshotTreeSha,
|
|
17519
|
+
params.seed.baselineTreeSha,
|
|
17520
|
+
input.headSha
|
|
17521
|
+
],
|
|
17522
|
+
`repo=${params.repoRoot}, tool=${params.tool}, session=${params.sessionId}, recordedAt=${params.recordedAt}`
|
|
17523
|
+
);
|
|
17459
17524
|
const record = {
|
|
17460
17525
|
...identity,
|
|
17461
17526
|
...input,
|
|
17527
|
+
...retention,
|
|
17462
17528
|
version: 1,
|
|
17463
17529
|
id: crypto8.randomUUID(),
|
|
17464
17530
|
sequence: Math.max(Date.now(), (existing.at(-1)?.sequence ?? 0) + 1),
|
|
17465
17531
|
uploads: {}
|
|
17466
17532
|
};
|
|
17467
|
-
|
|
17468
|
-
|
|
17469
|
-
|
|
17533
|
+
const pendingBefore = queueDiskBytes(
|
|
17534
|
+
queueDirectory(params.repoRoot, params.tool, params.sessionId)
|
|
17535
|
+
) + existing.reduce((sum, item) => sum + chargedBytes(item), 0);
|
|
17536
|
+
const incomingBytes = retention.retainedBytes + Buffer.byteLength(JSON.stringify(record));
|
|
17537
|
+
if (existing.length >= MAX_CAPTURES || pendingBefore + incomingBytes > MAX_PENDING_BYTES2) {
|
|
17538
|
+
throw new PendingCaptureLimitError(
|
|
17539
|
+
`Pending Git capture limit reached (count=${existing.length}, maxCount=${MAX_CAPTURES}, pendingBytes=${pendingBefore}, incomingBytes=${incomingBytes}, treeBytes=${retention.treeBytes}, maxBytes=${MAX_PENDING_BYTES2}); existing captures retained`
|
|
17470
17540
|
);
|
|
17471
17541
|
}
|
|
17472
17542
|
const directory = captureDirectory(record);
|
|
@@ -17508,7 +17578,7 @@ function storePendingCapture(params) {
|
|
|
17508
17578
|
durableDetails = `capture=${record.id}, durableCapturedAt=${durableCapturedAt}`;
|
|
17509
17579
|
appendLog(
|
|
17510
17580
|
"info",
|
|
17511
|
-
`git-traces: capture queued (repo=${record.repoRoot}, session=${record.sessionId}, capture=${record.id}, recordedAt=${record.recordedAt}, captureStartedAt=${record.captureStartedAt}, capturedAt=${record.capturedAt}, sourceCapturedAt=${record.capturedAt}, durableCapturedAt=${durableCapturedAt}, pending=${existing.length + 1}, oldestPendingAgeMs=${Date.now() - (existing[0]?.capturedAt ?? record.capturedAt)})`
|
|
17581
|
+
`git-traces: capture queued (repo=${record.repoRoot}, session=${record.sessionId}, capture=${record.id}, recordedAt=${record.recordedAt}, captureStartedAt=${record.captureStartedAt}, capturedAt=${record.capturedAt}, sourceCapturedAt=${record.capturedAt}, durableCapturedAt=${durableCapturedAt}, pending=${existing.length + 1}, oldestPendingAgeMs=${Date.now() - (existing[0]?.capturedAt ?? record.capturedAt)}, treeBytes=${record.treeBytes}, treeFiles=${record.treeFiles}, retainedBytes=${record.retainedBytes}, pendingBytes=${pendingBefore + incomingBytes})`
|
|
17512
17582
|
);
|
|
17513
17583
|
return record;
|
|
17514
17584
|
} finally {
|
|
@@ -17694,6 +17764,8 @@ var TOOL_LABELS = {
|
|
|
17694
17764
|
"copilot-chat": "GitHub Copilot Chat",
|
|
17695
17765
|
opencode: "opencode"
|
|
17696
17766
|
};
|
|
17767
|
+
var PendingQueueConflictError = class extends Error {
|
|
17768
|
+
};
|
|
17697
17769
|
async function loadConfiguredRepos() {
|
|
17698
17770
|
const file = await loadProjects();
|
|
17699
17771
|
return Object.entries(file.projects).map(([repoRoot, config]) => ({
|
|
@@ -19456,7 +19528,7 @@ async function queueStopCapture(repo, tool, sessionId, recordedAt, sessionEnd =
|
|
|
19456
19528
|
const queued = listPendingCaptures2(repo.repoRoot, tool, sessionId);
|
|
19457
19529
|
const state = candidates.scoped?.state ?? queued[0]?.seed;
|
|
19458
19530
|
if (candidates.legacy && queued.length > 0)
|
|
19459
|
-
throw new
|
|
19531
|
+
throw new PendingQueueConflictError(
|
|
19460
19532
|
"Legacy state appeared while Git captures were pending; preserve the queue for session reconciliation"
|
|
19461
19533
|
);
|
|
19462
19534
|
if (!state || candidates.legacy || !state.baselineTreeSha || !state.baselineMetadata)
|
|
@@ -19518,7 +19590,6 @@ async function queueStopCapture(repo, tool, sessionId, recordedAt, sessionEnd =
|
|
|
19518
19590
|
headSha,
|
|
19519
19591
|
captureStartedAt,
|
|
19520
19592
|
capturedAt,
|
|
19521
|
-
treeBytes: snapshotTreeBytes(repo.repoRoot, treeSha),
|
|
19522
19593
|
metadata,
|
|
19523
19594
|
omittedFiles,
|
|
19524
19595
|
scan: scanOut[0]
|
|
@@ -19545,13 +19616,30 @@ async function queueStopCapture(repo, tool, sessionId, recordedAt, sessionEnd =
|
|
|
19545
19616
|
}
|
|
19546
19617
|
);
|
|
19547
19618
|
} catch (err) {
|
|
19619
|
+
if (err instanceof PendingCaptureLimitError) {
|
|
19620
|
+
appendLog(
|
|
19621
|
+
"error",
|
|
19622
|
+
`git-traces: Stop capture rejected for repo ${repo.repoRoot} (session=${sessionId}, sessionEnd=${sessionEnd}): ${formatError(err)}`
|
|
19623
|
+
);
|
|
19624
|
+
return "rejected";
|
|
19625
|
+
}
|
|
19548
19626
|
appendLog(
|
|
19549
19627
|
"error",
|
|
19550
19628
|
`git-traces: Stop failed for repo ${repo.repoRoot} during immutable capture (session=${sessionId}): ${formatError(err)}`
|
|
19551
19629
|
);
|
|
19552
|
-
return "failed";
|
|
19630
|
+
return err instanceof PendingQueueConflictError ? "conflict" : "failed";
|
|
19553
19631
|
}
|
|
19554
19632
|
}
|
|
19633
|
+
async function drainRetainedCaptures(repo, tool, sessionId, capture, lineageCheck) {
|
|
19634
|
+
if (capture === "conflict" || !sessionId || !hasPendingCaptures(repo.repoRoot, tool, sessionId))
|
|
19635
|
+
return void 0;
|
|
19636
|
+
const outcome = await drainStopCaptures(repo, tool, sessionId, lineageCheck);
|
|
19637
|
+
appendLog(
|
|
19638
|
+
outcome === "failed" ? "warn" : "info",
|
|
19639
|
+
`git-traces: retained captures drained after ${capture} capture (repo=${repo.repoRoot}, session=${sessionId}, outcome=${outcome})`
|
|
19640
|
+
);
|
|
19641
|
+
return outcome;
|
|
19642
|
+
}
|
|
19555
19643
|
async function drainStopCaptures(repo, tool, sessionId, lineageCheck) {
|
|
19556
19644
|
const drainTool = `${tool}:capture-drain-v1`;
|
|
19557
19645
|
try {
|
|
@@ -19566,7 +19654,7 @@ async function drainStopCaptures(repo, tool, sessionId, lineageCheck) {
|
|
|
19566
19654
|
"info",
|
|
19567
19655
|
`git-traces: capture pending behind active drainer (repo=${repo.repoRoot}, session=${sessionId})`
|
|
19568
19656
|
);
|
|
19569
|
-
return "
|
|
19657
|
+
return "pending";
|
|
19570
19658
|
}
|
|
19571
19659
|
let locked = true;
|
|
19572
19660
|
let uploaded = false;
|
|
@@ -19750,10 +19838,12 @@ async function handleStop(payload, tool) {
|
|
|
19750
19838
|
let uploaded = 0;
|
|
19751
19839
|
let unchanged = 0;
|
|
19752
19840
|
let skipped = missingConfig;
|
|
19841
|
+
let pending = 0;
|
|
19753
19842
|
let noState = missingStateRoots.filter(
|
|
19754
19843
|
(root) => !targetsByRoot.has(root)
|
|
19755
19844
|
).length;
|
|
19756
19845
|
let failed = initializationFailed;
|
|
19846
|
+
let rejected = 0;
|
|
19757
19847
|
const captures = /* @__PURE__ */ new Map();
|
|
19758
19848
|
for (const repo of targetList) {
|
|
19759
19849
|
captures.set(
|
|
@@ -19762,23 +19852,33 @@ async function handleStop(payload, tool) {
|
|
|
19762
19852
|
);
|
|
19763
19853
|
}
|
|
19764
19854
|
for (const repo of targetList) {
|
|
19765
|
-
const capture = captures.get(repo.repoRoot);
|
|
19766
|
-
|
|
19767
|
-
|
|
19768
|
-
tool,
|
|
19769
|
-
|
|
19770
|
-
|
|
19771
|
-
|
|
19772
|
-
|
|
19855
|
+
const capture = captures.get(repo.repoRoot) ?? "failed";
|
|
19856
|
+
let outcome;
|
|
19857
|
+
if (capture === "queued" && sessionId) {
|
|
19858
|
+
outcome = await drainStopCaptures(repo, tool, sessionId, lineageCheck);
|
|
19859
|
+
} else if (capture === "queued" || capture === "direct") {
|
|
19860
|
+
outcome = await processStopRepo(
|
|
19861
|
+
repo,
|
|
19862
|
+
tool,
|
|
19863
|
+
sessionId,
|
|
19864
|
+
recordedAt,
|
|
19865
|
+
lineageCheck
|
|
19866
|
+
);
|
|
19867
|
+
} else {
|
|
19868
|
+
outcome = "failed";
|
|
19869
|
+
if (capture === "rejected") rejected++;
|
|
19870
|
+
await drainRetainedCaptures(repo, tool, sessionId, capture, lineageCheck);
|
|
19871
|
+
}
|
|
19773
19872
|
if (outcome === "uploaded") uploaded++;
|
|
19774
19873
|
else if (outcome === "unchanged") unchanged++;
|
|
19775
19874
|
else if (outcome === "skipped") skipped++;
|
|
19875
|
+
else if (outcome === "pending") pending++;
|
|
19776
19876
|
else if (outcome === "no-state") noState++;
|
|
19777
19877
|
else failed++;
|
|
19778
19878
|
}
|
|
19779
19879
|
appendLog(
|
|
19780
19880
|
"info",
|
|
19781
|
-
`git-traces: Stop summary (session=${sessionId ?? "<none>"}, tool=${tool}, triggerRepo=${project.repoRoot}, configured=${repos.length}, frozenStates=${frozenStates}, uploaded=${uploaded}, unchanged=${unchanged}, skipped=${skipped}, noState=${noState}, failed=${failed})`
|
|
19881
|
+
`git-traces: Stop summary (session=${sessionId ?? "<none>"}, tool=${tool}, triggerRepo=${project.repoRoot}, configured=${repos.length}, frozenStates=${frozenStates}, uploaded=${uploaded}, unchanged=${unchanged}, skipped=${skipped}, noState=${noState}, failed=${failed}, rejected=${rejected}, pending=${pending})`
|
|
19782
19882
|
);
|
|
19783
19883
|
}
|
|
19784
19884
|
async function cleanupSessionStateForRepo(repoRoot, tool, sessionId) {
|
|
@@ -19871,7 +19971,9 @@ async function handleSessionEnd(payload, tool) {
|
|
|
19871
19971
|
let uploaded = 0;
|
|
19872
19972
|
let unchanged = 0;
|
|
19873
19973
|
let skipped = 0;
|
|
19974
|
+
let pending = 0;
|
|
19874
19975
|
let failed = 0;
|
|
19976
|
+
let rejected = 0;
|
|
19875
19977
|
const finalCaptures = /* @__PURE__ */ new Map();
|
|
19876
19978
|
for (const repoRoot of repoRoots) {
|
|
19877
19979
|
const repo = repoByRoot.get(path19.resolve(repoRoot)) ?? (project && path19.resolve(project.repoRoot) === path19.resolve(repoRoot) ? { repoRoot: project.repoRoot, config: project.config } : null);
|
|
@@ -19891,8 +19993,24 @@ async function handleSessionEnd(payload, tool) {
|
|
|
19891
19993
|
);
|
|
19892
19994
|
continue;
|
|
19893
19995
|
}
|
|
19894
|
-
const finalCapture = finalCaptures.get(repoRoot);
|
|
19895
|
-
|
|
19996
|
+
const finalCapture = finalCaptures.get(repoRoot) ?? "failed";
|
|
19997
|
+
if (finalCapture !== "queued" && finalCapture !== "direct") {
|
|
19998
|
+
failed++;
|
|
19999
|
+
if (finalCapture === "rejected") rejected++;
|
|
20000
|
+
appendLog(
|
|
20001
|
+
"warn",
|
|
20002
|
+
`git-traces: preserving SessionEnd state for repo ${repoRoot} after ${finalCapture} final capture`
|
|
20003
|
+
);
|
|
20004
|
+
await drainRetainedCaptures(
|
|
20005
|
+
repo,
|
|
20006
|
+
tool,
|
|
20007
|
+
sessionId,
|
|
20008
|
+
finalCapture,
|
|
20009
|
+
lineageCheck
|
|
20010
|
+
);
|
|
20011
|
+
continue;
|
|
20012
|
+
}
|
|
20013
|
+
const finalOutcome = finalCapture === "queued" && sessionId ? await drainStopCaptures(repo, tool, sessionId, lineageCheck) : await processStopRepo(
|
|
19896
20014
|
repo,
|
|
19897
20015
|
tool,
|
|
19898
20016
|
sessionId,
|
|
@@ -19902,7 +20020,14 @@ async function handleSessionEnd(payload, tool) {
|
|
|
19902
20020
|
);
|
|
19903
20021
|
if (finalOutcome === "uploaded") uploaded++;
|
|
19904
20022
|
else if (finalOutcome === "unchanged") unchanged++;
|
|
19905
|
-
else if (finalOutcome === "
|
|
20023
|
+
else if (finalOutcome === "pending") {
|
|
20024
|
+
pending++;
|
|
20025
|
+
appendLog(
|
|
20026
|
+
"info",
|
|
20027
|
+
`git-traces: final capture queued behind active drainer for repo ${repoRoot}`
|
|
20028
|
+
);
|
|
20029
|
+
continue;
|
|
20030
|
+
} else if (finalOutcome === "skipped") {
|
|
19906
20031
|
skipped++;
|
|
19907
20032
|
appendLog(
|
|
19908
20033
|
"warn",
|
|
@@ -19924,7 +20049,7 @@ async function handleSessionEnd(payload, tool) {
|
|
|
19924
20049
|
}
|
|
19925
20050
|
appendLog(
|
|
19926
20051
|
"info",
|
|
19927
|
-
`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})`
|
|
20052
|
+
`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}, rejected=${rejected}, pending=${pending})`
|
|
19928
20053
|
);
|
|
19929
20054
|
}
|
|
19930
20055
|
|