hillclimb 0.8.4 → 0.8.5
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 +263 -21
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -13945,7 +13945,7 @@ async function runUploadWorker() {
|
|
|
13945
13945
|
}
|
|
13946
13946
|
|
|
13947
13947
|
// src/git-traces/index.ts
|
|
13948
|
-
import { spawn as
|
|
13948
|
+
import { spawn as spawn4 } from "child_process";
|
|
13949
13949
|
import crypto7 from "crypto";
|
|
13950
13950
|
|
|
13951
13951
|
// src/git-traces/handlers.ts
|
|
@@ -13953,11 +13953,11 @@ import { execFileSync as execFileSync3 } from "child_process";
|
|
|
13953
13953
|
import path18 from "path";
|
|
13954
13954
|
|
|
13955
13955
|
// src/git-traces/git-ops.ts
|
|
13956
|
-
import { execFileSync as execFileSync2, spawnSync } from "child_process";
|
|
13956
|
+
import { execFileSync as execFileSync2, spawn as spawn3, spawnSync } from "child_process";
|
|
13957
13957
|
import fs16 from "fs";
|
|
13958
13958
|
import os8 from "os";
|
|
13959
13959
|
import path16 from "path";
|
|
13960
|
-
import {
|
|
13960
|
+
import { createGzip } from "zlib";
|
|
13961
13961
|
var GIT_COMMAND_TIMEOUT_MS = 12e4;
|
|
13962
13962
|
var EXEC_OPTS = {
|
|
13963
13963
|
timeout: GIT_COMMAND_TIMEOUT_MS,
|
|
@@ -14185,7 +14185,7 @@ function createGitError(args, err) {
|
|
|
14185
14185
|
const failure = err;
|
|
14186
14186
|
const command = formatGitCommand(args);
|
|
14187
14187
|
const isTimeout = failure.code === "ETIMEDOUT";
|
|
14188
|
-
const message = isTimeout ? `${command} timed out after ${GIT_COMMAND_TIMEOUT_MS}ms` : formatGitFailure(command, failure, err);
|
|
14188
|
+
const message = isTimeout ? `${command} timed out after ${GIT_COMMAND_TIMEOUT_MS}ms` : failure.code === "ENOBUFS" ? `${command} output exceeded the ${formatSize(failure.maxBufferBytes ?? EXEC_OPTS.maxBuffer)} limit` : formatGitFailure(command, failure, err);
|
|
14189
14189
|
const wrapped = new Error(message);
|
|
14190
14190
|
wrapped.isGitTimeout = isTimeout;
|
|
14191
14191
|
return wrapped;
|
|
@@ -14477,7 +14477,111 @@ function createBundleFromTree(repoRoot, treeSha, sessionId, label) {
|
|
|
14477
14477
|
deleteRef(repoRoot, orphanRef);
|
|
14478
14478
|
}
|
|
14479
14479
|
}
|
|
14480
|
-
|
|
14480
|
+
var MAX_PATCH_GZ_BYTES = 500 * 1024 * 1024;
|
|
14481
|
+
function gitGzipStream(repoRoot, args, maxGzBytes) {
|
|
14482
|
+
return new Promise((resolve, reject) => {
|
|
14483
|
+
const child = spawn3("git", args, {
|
|
14484
|
+
cwd: repoRoot,
|
|
14485
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
14486
|
+
windowsHide: true
|
|
14487
|
+
});
|
|
14488
|
+
const gzip = createGzip();
|
|
14489
|
+
const gzChunks = [];
|
|
14490
|
+
const stderrChunks = [];
|
|
14491
|
+
let gzBytes = 0;
|
|
14492
|
+
let rawBytes = 0;
|
|
14493
|
+
let stderrBytes = 0;
|
|
14494
|
+
let settled = false;
|
|
14495
|
+
let timedOut = false;
|
|
14496
|
+
let overflowed = false;
|
|
14497
|
+
let streamError = null;
|
|
14498
|
+
let exitCode = null;
|
|
14499
|
+
let exitSignal = null;
|
|
14500
|
+
let childClosed = false;
|
|
14501
|
+
let gzipDone = false;
|
|
14502
|
+
const timer = setTimeout(() => {
|
|
14503
|
+
timedOut = true;
|
|
14504
|
+
child.kill("SIGTERM");
|
|
14505
|
+
}, GIT_COMMAND_TIMEOUT_MS);
|
|
14506
|
+
const finish = () => {
|
|
14507
|
+
if (settled || !childClosed || !gzipDone) return;
|
|
14508
|
+
settled = true;
|
|
14509
|
+
clearTimeout(timer);
|
|
14510
|
+
if (timedOut) {
|
|
14511
|
+
reject(createGitError(args, { code: "ETIMEDOUT" }));
|
|
14512
|
+
} else if (overflowed) {
|
|
14513
|
+
reject(
|
|
14514
|
+
createGitError(args, { code: "ENOBUFS", maxBufferBytes: maxGzBytes })
|
|
14515
|
+
);
|
|
14516
|
+
} else if (streamError) {
|
|
14517
|
+
reject(createGitError(args, streamError));
|
|
14518
|
+
} else if (exitCode !== 0 || exitSignal) {
|
|
14519
|
+
reject(
|
|
14520
|
+
createGitError(args, {
|
|
14521
|
+
status: exitCode,
|
|
14522
|
+
signal: exitSignal,
|
|
14523
|
+
stderr: Buffer.concat(stderrChunks)
|
|
14524
|
+
})
|
|
14525
|
+
);
|
|
14526
|
+
} else {
|
|
14527
|
+
resolve({ gz: Buffer.concat(gzChunks), rawBytes });
|
|
14528
|
+
}
|
|
14529
|
+
};
|
|
14530
|
+
child.stdout.on("data", (chunk) => {
|
|
14531
|
+
rawBytes += chunk.length;
|
|
14532
|
+
});
|
|
14533
|
+
child.stdout.on("error", (err) => {
|
|
14534
|
+
streamError ??= err;
|
|
14535
|
+
gzipDone = true;
|
|
14536
|
+
finish();
|
|
14537
|
+
});
|
|
14538
|
+
child.stdout.pipe(gzip);
|
|
14539
|
+
gzip.on("data", (chunk) => {
|
|
14540
|
+
gzBytes += chunk.length;
|
|
14541
|
+
if (gzBytes > maxGzBytes) {
|
|
14542
|
+
if (!overflowed) {
|
|
14543
|
+
overflowed = true;
|
|
14544
|
+
child.stdout.unpipe(gzip);
|
|
14545
|
+
child.stdout.resume();
|
|
14546
|
+
child.kill("SIGTERM");
|
|
14547
|
+
gzip.destroy();
|
|
14548
|
+
gzipDone = true;
|
|
14549
|
+
finish();
|
|
14550
|
+
}
|
|
14551
|
+
return;
|
|
14552
|
+
}
|
|
14553
|
+
gzChunks.push(chunk);
|
|
14554
|
+
});
|
|
14555
|
+
gzip.on("end", () => {
|
|
14556
|
+
gzipDone = true;
|
|
14557
|
+
finish();
|
|
14558
|
+
});
|
|
14559
|
+
gzip.on("error", (err) => {
|
|
14560
|
+
if (!overflowed) streamError ??= err;
|
|
14561
|
+
gzipDone = true;
|
|
14562
|
+
finish();
|
|
14563
|
+
});
|
|
14564
|
+
child.stderr.on("data", (chunk) => {
|
|
14565
|
+
if (stderrBytes < 16 * 1024) {
|
|
14566
|
+
stderrChunks.push(chunk);
|
|
14567
|
+
stderrBytes += chunk.length;
|
|
14568
|
+
}
|
|
14569
|
+
});
|
|
14570
|
+
child.on("error", (err) => {
|
|
14571
|
+
streamError ??= err;
|
|
14572
|
+
childClosed = true;
|
|
14573
|
+
gzipDone = true;
|
|
14574
|
+
finish();
|
|
14575
|
+
});
|
|
14576
|
+
child.on("close", (code, signal) => {
|
|
14577
|
+
exitCode = code;
|
|
14578
|
+
exitSignal = signal;
|
|
14579
|
+
childClosed = true;
|
|
14580
|
+
finish();
|
|
14581
|
+
});
|
|
14582
|
+
});
|
|
14583
|
+
}
|
|
14584
|
+
async function createTreeDiffPatchGz(repoRoot, fromTreeSha, toTreeSha, options = {}) {
|
|
14481
14585
|
if (fromTreeSha === toTreeSha) return null;
|
|
14482
14586
|
const filteredFromTreeSha = filterOmittedFilesFromTree(
|
|
14483
14587
|
repoRoot,
|
|
@@ -14488,14 +14592,13 @@ function createTreeDiffPatchGz(repoRoot, fromTreeSha, toTreeSha, options = {}) {
|
|
|
14488
14592
|
limits: options.limits
|
|
14489
14593
|
});
|
|
14490
14594
|
if (filteredFromTreeSha === filteredToTreeSha) return null;
|
|
14491
|
-
const
|
|
14492
|
-
|
|
14493
|
-
"--binary",
|
|
14494
|
-
|
|
14495
|
-
|
|
14496
|
-
|
|
14497
|
-
|
|
14498
|
-
return Buffer.from(gzipSync2(diff));
|
|
14595
|
+
const { gz, rawBytes } = await gitGzipStream(
|
|
14596
|
+
repoRoot,
|
|
14597
|
+
["diff", "--binary", filteredFromTreeSha, filteredToTreeSha],
|
|
14598
|
+
options.maxPatchGzBytes ?? MAX_PATCH_GZ_BYTES
|
|
14599
|
+
);
|
|
14600
|
+
if (rawBytes === 0) return null;
|
|
14601
|
+
return gz;
|
|
14499
14602
|
}
|
|
14500
14603
|
function detectTransitionKind(repoRoot, prevHeadSha, nextHeadSha) {
|
|
14501
14604
|
if (!prevHeadSha) return "initial";
|
|
@@ -14996,6 +15099,7 @@ var CLI_VERSION2 = "0.8.0";
|
|
|
14996
15099
|
var GIT_TRACES_SLUG = "git-traces";
|
|
14997
15100
|
var MAX_UPLOAD_BYTES = 500 * 1024 * 1024;
|
|
14998
15101
|
var STALE_SCOPED_STATE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
15102
|
+
var REBASELINE_AFTER_DIFF_FAILURES = 2;
|
|
14999
15103
|
function formatEpochSeconds3(date) {
|
|
15000
15104
|
return String(Math.floor(date.getTime() / 1e3));
|
|
15001
15105
|
}
|
|
@@ -15879,6 +15983,107 @@ async function refreshLineageBaseline(params) {
|
|
|
15879
15983
|
return false;
|
|
15880
15984
|
}
|
|
15881
15985
|
}
|
|
15986
|
+
async function recoverFromWedgedDiff(params) {
|
|
15987
|
+
const {
|
|
15988
|
+
repo,
|
|
15989
|
+
state,
|
|
15990
|
+
tool,
|
|
15991
|
+
persistState,
|
|
15992
|
+
limits,
|
|
15993
|
+
currentSha,
|
|
15994
|
+
currentTreeSha,
|
|
15995
|
+
turnOmittedFiles
|
|
15996
|
+
} = params;
|
|
15997
|
+
const { repoRoot, config } = repo;
|
|
15998
|
+
if (state.contributionId === null || !state.baselineUploaded) {
|
|
15999
|
+
const prefix = epochPrefix2(state.epoch);
|
|
16000
|
+
pinRef(
|
|
16001
|
+
repoRoot,
|
|
16002
|
+
`refs/hillclimb/baseline/${state.sessionId}/${prefix}/tracked`,
|
|
16003
|
+
currentSha
|
|
16004
|
+
);
|
|
16005
|
+
pinRef(
|
|
16006
|
+
repoRoot,
|
|
16007
|
+
`refs/hillclimb/scoped/${state.sessionId}/baseline/${prefix}/tracked`,
|
|
16008
|
+
currentSha
|
|
16009
|
+
);
|
|
16010
|
+
pinFrozenBaselineTree(
|
|
16011
|
+
repoRoot,
|
|
16012
|
+
state.sessionId,
|
|
16013
|
+
state.epoch,
|
|
16014
|
+
currentTreeSha
|
|
16015
|
+
);
|
|
16016
|
+
const metadata = buildBaselineMetadata(
|
|
16017
|
+
repoRoot,
|
|
16018
|
+
state.sessionId,
|
|
16019
|
+
tool,
|
|
16020
|
+
currentSha,
|
|
16021
|
+
CLI_VERSION2,
|
|
16022
|
+
state.epoch,
|
|
16023
|
+
null,
|
|
16024
|
+
"initial",
|
|
16025
|
+
state.startedAt,
|
|
16026
|
+
turnOmittedFiles,
|
|
16027
|
+
state.codexLineage
|
|
16028
|
+
);
|
|
16029
|
+
Object.assign(state, {
|
|
16030
|
+
baselineSha: currentSha,
|
|
16031
|
+
baselineTreeSha: currentTreeSha,
|
|
16032
|
+
baselineMetadata: metadata,
|
|
16033
|
+
lastSnapshotSha: currentSha,
|
|
16034
|
+
lastSnapshotTreeSha: currentTreeSha,
|
|
16035
|
+
headSha: captureHeadSha(repoRoot),
|
|
16036
|
+
turnCount: 0,
|
|
16037
|
+
omittedKeys: omissionKeys(turnOmittedFiles)
|
|
16038
|
+
});
|
|
16039
|
+
delete state.diffFailureCount;
|
|
16040
|
+
await persistState(state);
|
|
16041
|
+
appendLog(
|
|
16042
|
+
"warn",
|
|
16043
|
+
`git-traces: wedge-recovery replaced the unuploaded ${prefix} baseline at the current state; the wedged interval is not recoverable (repo=${repoRoot}, project=${config.projectId}, session=${state.sessionId})`
|
|
16044
|
+
);
|
|
16045
|
+
return "skipped";
|
|
16046
|
+
}
|
|
16047
|
+
const client = await loadRepoClient(repo);
|
|
16048
|
+
if (!client) return "skipped";
|
|
16049
|
+
const nextEpoch = await reserveEpoch(state, persistState);
|
|
16050
|
+
appendLog(
|
|
16051
|
+
"warn",
|
|
16052
|
+
`git-traces: wedge-recovery abandoning the incremental chain after ${state.diffFailureCount} consecutive diff failures (repo=${repoRoot}, project=${config.projectId}, contribution=${state.contributionId}, abandonedEpoch=${state.epoch}, lastUploadedTurn=${state.turnCount}, openingEpoch=${nextEpoch})`
|
|
16053
|
+
);
|
|
16054
|
+
const artifacts = await openEpoch({
|
|
16055
|
+
repoRoot,
|
|
16056
|
+
client,
|
|
16057
|
+
contributionId: state.contributionId,
|
|
16058
|
+
sessionId: state.sessionId,
|
|
16059
|
+
tool,
|
|
16060
|
+
epoch: nextEpoch,
|
|
16061
|
+
prevHeadSha: state.headSha || null,
|
|
16062
|
+
transitionKind: "initial",
|
|
16063
|
+
startedAt: state.startedAt,
|
|
16064
|
+
limits,
|
|
16065
|
+
lineage: state.codexLineage
|
|
16066
|
+
});
|
|
16067
|
+
if (!artifacts) return "failed";
|
|
16068
|
+
Object.assign(state, {
|
|
16069
|
+
baselineSha: artifacts.baselineSha,
|
|
16070
|
+
baselineTreeSha: artifacts.baselineTreeSha,
|
|
16071
|
+
lastSnapshotSha: artifacts.baselineSha,
|
|
16072
|
+
lastSnapshotTreeSha: artifacts.baselineTreeSha,
|
|
16073
|
+
epoch: nextEpoch,
|
|
16074
|
+
turnCount: 0,
|
|
16075
|
+
headSha: artifacts.headSha,
|
|
16076
|
+
omittedKeys: artifacts.omittedKeys,
|
|
16077
|
+
codexLineageStamped: state.codexLineage
|
|
16078
|
+
});
|
|
16079
|
+
delete state.diffFailureCount;
|
|
16080
|
+
await persistState(state);
|
|
16081
|
+
appendLog(
|
|
16082
|
+
"info",
|
|
16083
|
+
`git-traces: wedge-recovery baseline uploaded (repo=${repoRoot}, project=${config.projectId}, contribution=${state.contributionId}, epoch=${nextEpoch}, baseline=${artifacts.baselineSha.slice(0, 8)})`
|
|
16084
|
+
);
|
|
16085
|
+
return "uploaded";
|
|
16086
|
+
}
|
|
15882
16087
|
async function registerInitialContribution(params) {
|
|
15883
16088
|
const { repo, state, tool, persistState, client, artifacts } = params;
|
|
15884
16089
|
const contributionId = await createGitTracesContribution({
|
|
@@ -16111,6 +16316,7 @@ async function processStopRepo(repo, tool, sessionId, recordedAt, lineageCheck,
|
|
|
16111
16316
|
omittedKeys: artifacts.omittedKeys,
|
|
16112
16317
|
codexLineageStamped: state.codexLineage
|
|
16113
16318
|
};
|
|
16319
|
+
delete next.diffFailureCount;
|
|
16114
16320
|
await persistState(next);
|
|
16115
16321
|
appendLog(
|
|
16116
16322
|
"info",
|
|
@@ -16125,19 +16331,55 @@ async function processStopRepo(repo, tool, sessionId, recordedAt, lineageCheck,
|
|
|
16125
16331
|
log: false,
|
|
16126
16332
|
limits
|
|
16127
16333
|
});
|
|
16334
|
+
const previousOmittedKeys = state.omittedKeys;
|
|
16128
16335
|
const omissionsChanged = reportNewlyOmittedFiles(
|
|
16129
16336
|
state,
|
|
16130
16337
|
turnOmittedFiles,
|
|
16131
16338
|
repoRoot
|
|
16132
16339
|
);
|
|
16133
|
-
const
|
|
16134
|
-
|
|
16135
|
-
|
|
16136
|
-
|
|
16137
|
-
|
|
16138
|
-
|
|
16340
|
+
const attemptWedgeRecovery = async () => {
|
|
16341
|
+
const outcome = await recoverFromWedgedDiff({
|
|
16342
|
+
repo,
|
|
16343
|
+
state: cleanupState,
|
|
16344
|
+
tool,
|
|
16345
|
+
persistState,
|
|
16346
|
+
limits,
|
|
16347
|
+
currentSha,
|
|
16348
|
+
currentTreeSha,
|
|
16349
|
+
turnOmittedFiles
|
|
16350
|
+
});
|
|
16351
|
+
return outcome === "uploaded" ? await finishSuccessfulStop("uploaded") : outcome;
|
|
16352
|
+
};
|
|
16353
|
+
if ((state.diffFailureCount ?? 0) >= REBASELINE_AFTER_DIFF_FAILURES) {
|
|
16354
|
+
appendLog(
|
|
16355
|
+
"warn",
|
|
16356
|
+
`git-traces: skipping turn diff after ${state.diffFailureCount} consecutive failures; retrying wedge recovery (repo=${repoRoot}, project=${config.projectId})`
|
|
16357
|
+
);
|
|
16358
|
+
return await attemptWedgeRecovery();
|
|
16359
|
+
}
|
|
16360
|
+
let patchBuffer;
|
|
16361
|
+
try {
|
|
16362
|
+
patchBuffer = await createTreeDiffPatchGz(
|
|
16363
|
+
repoRoot,
|
|
16364
|
+
lastSnapshotTreeSha,
|
|
16365
|
+
currentTreeSha,
|
|
16366
|
+
{ limits }
|
|
16367
|
+
);
|
|
16368
|
+
} catch (err) {
|
|
16369
|
+
const failures = (state.diffFailureCount ?? 0) + 1;
|
|
16370
|
+
state.diffFailureCount = failures;
|
|
16371
|
+
await persistState({ ...state, omittedKeys: previousOmittedKeys });
|
|
16372
|
+
appendLog(
|
|
16373
|
+
"error",
|
|
16374
|
+
`git-traces: turn diff failed (repo=${repoRoot}, project=${config.projectId}, consecutiveFailures=${failures}): ${formatError(err)}`
|
|
16375
|
+
);
|
|
16376
|
+
if (failures < REBASELINE_AFTER_DIFF_FAILURES) return "failed";
|
|
16377
|
+
return await attemptWedgeRecovery();
|
|
16378
|
+
}
|
|
16379
|
+
const diffFailuresCleared = state.diffFailureCount !== void 0;
|
|
16380
|
+
delete state.diffFailureCount;
|
|
16139
16381
|
if (!patchBuffer) {
|
|
16140
|
-
if (omissionsChanged) {
|
|
16382
|
+
if (omissionsChanged || diffFailuresCleared) {
|
|
16141
16383
|
await persistState(state);
|
|
16142
16384
|
} else {
|
|
16143
16385
|
await touchSessionState(state.repoRoot, tool, sessionId);
|
|
@@ -16653,7 +16895,7 @@ async function runGitTraces() {
|
|
|
16653
16895
|
};
|
|
16654
16896
|
await applyDebugLogParentEnv(workerEnv, raw);
|
|
16655
16897
|
try {
|
|
16656
|
-
const child =
|
|
16898
|
+
const child = spawn4(
|
|
16657
16899
|
process.execPath,
|
|
16658
16900
|
[entrypoint, "git-traces", `--tool=${tool}`],
|
|
16659
16901
|
{
|