hillclimb 0.8.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 +184 -24
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -12040,10 +12040,8 @@ function expectedKinds(tool, eventKind2, payload) {
|
|
|
12040
12040
|
}
|
|
12041
12041
|
return /* @__PURE__ */ new Set(["agent", "git"]);
|
|
12042
12042
|
}
|
|
12043
|
-
|
|
12044
|
-
|
|
12045
|
-
if (!transcriptPath) return {};
|
|
12046
|
-
const resolved = path13.resolve(transcriptPath);
|
|
12043
|
+
var TRANSCRIPT_STAT_ENV = "HILLCLIMB_DEBUG_LOG_TRANSCRIPT_STAT";
|
|
12044
|
+
async function statTranscriptFingerprint(resolved) {
|
|
12047
12045
|
try {
|
|
12048
12046
|
const stat = await fs12.promises.stat(resolved);
|
|
12049
12047
|
return {
|
|
@@ -12055,6 +12053,44 @@ async function transcriptFingerprint(payload) {
|
|
|
12055
12053
|
return { transcriptPath: resolved };
|
|
12056
12054
|
}
|
|
12057
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
|
+
}
|
|
12058
12094
|
async function eventContext(tool, payload) {
|
|
12059
12095
|
const eventKind2 = classifyHookEvent(payload.hook_event_name);
|
|
12060
12096
|
if (!eventKind2) return null;
|
|
@@ -12085,6 +12121,10 @@ async function eventContext(tool, payload) {
|
|
|
12085
12121
|
// Only stop events without stable turn IDs need the transcript to
|
|
12086
12122
|
// disambiguate. When a turn_id exists, including mutable transcript
|
|
12087
12123
|
// mtime/size can split agent+git completions for the same logical event.
|
|
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).
|
|
12088
12128
|
...eventKind2 === "stop" && !turnId ? await transcriptFingerprint(payload) : {}
|
|
12089
12129
|
};
|
|
12090
12130
|
const eventId = crypto2.createHash("sha256").update(JSON.stringify(fingerprint)).digest("hex").slice(0, 32);
|
|
@@ -12373,7 +12413,7 @@ async function uploadDebugLog(ctx, state, sessionState, onContributionCreated) {
|
|
|
12373
12413
|
`Session ID: ${ctx.sessionId}`,
|
|
12374
12414
|
`Tool: ${label}`,
|
|
12375
12415
|
`Repo: ${ctx.repoRoot}`,
|
|
12376
|
-
"Log snapshots:
|
|
12416
|
+
"Log snapshots: machine-wide Hillclimb daily log snapshots captured at hook events; may include entries from other sessions on this machine"
|
|
12377
12417
|
].join("\n")
|
|
12378
12418
|
} : {
|
|
12379
12419
|
contributionTitle: `${label} debug log ${shortSession} - ${epochSeconds}`,
|
|
@@ -12788,6 +12828,21 @@ async function writeCursorState(repoRoot, tool, sessionId, cursor) {
|
|
|
12788
12828
|
});
|
|
12789
12829
|
await fs14.promises.rename(tmp, file);
|
|
12790
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;
|
|
12845
|
+
}
|
|
12791
12846
|
async function acquireLock2(repoRoot, tool, sessionId, retries = lockRetries(), delayMs = lockRetryDelayMs()) {
|
|
12792
12847
|
const lockPath = lockFileFor(repoRoot, tool, sessionId);
|
|
12793
12848
|
await fs14.promises.mkdir(stateDir2(), { recursive: true, mode: 448 });
|
|
@@ -13240,10 +13295,22 @@ async function uploadSession(args) {
|
|
|
13240
13295
|
} else {
|
|
13241
13296
|
appendLog(
|
|
13242
13297
|
"info",
|
|
13243
|
-
`[${sessionId}] skipping ${sourceTool} ${eventLabel} upload (no new complete transcript lines past offset ${cursor.rawByteOffset}${isSessionEnd ? ";
|
|
13298
|
+
`[${sessionId}] skipping ${sourceTool} ${eventLabel} upload (no new complete transcript lines past offset ${cursor.rawByteOffset}${isSessionEnd ? "; session state retained for late hooks" : ""})`
|
|
13244
13299
|
);
|
|
13245
13300
|
if (isSessionEnd) {
|
|
13246
|
-
await
|
|
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
|
+
);
|
|
13247
13314
|
}
|
|
13248
13315
|
return true;
|
|
13249
13316
|
}
|
|
@@ -13313,14 +13380,20 @@ async function uploadSession(args) {
|
|
|
13313
13380
|
uploadCount: prior.uploadCount + 1,
|
|
13314
13381
|
codexLineage,
|
|
13315
13382
|
codexLineageChecked,
|
|
13316
|
-
codexLineageStamped: codexLineage
|
|
13383
|
+
codexLineageStamped: codexLineage,
|
|
13384
|
+
sessionEndedAt: void 0
|
|
13317
13385
|
});
|
|
13318
13386
|
appendLog(
|
|
13319
13387
|
"info",
|
|
13320
13388
|
`[${sessionId}] refreshed ${sourceTool} lineage meta for unchanged epoch=${epoch2}`
|
|
13321
13389
|
);
|
|
13322
13390
|
if (isSessionEnd) {
|
|
13323
|
-
await
|
|
13391
|
+
await setUploadSessionEndedAt(
|
|
13392
|
+
repoRoot,
|
|
13393
|
+
sourceTool,
|
|
13394
|
+
sessionId,
|
|
13395
|
+
new Date(recordedAt).toISOString()
|
|
13396
|
+
);
|
|
13324
13397
|
}
|
|
13325
13398
|
return true;
|
|
13326
13399
|
}
|
|
@@ -13491,10 +13564,15 @@ async function uploadSession(args) {
|
|
|
13491
13564
|
`Uploaded session ${sessionId} patch epoch=${epoch2} turn=${turn} to contribution ${contributionId2} (offset ${baseCursor.rawByteOffset} \u2192 ${nextCursor.rawByteOffset})`
|
|
13492
13565
|
);
|
|
13493
13566
|
if (isSessionEnd) {
|
|
13494
|
-
await
|
|
13567
|
+
await setUploadSessionEndedAt(
|
|
13568
|
+
repoRoot,
|
|
13569
|
+
sourceTool,
|
|
13570
|
+
sessionId,
|
|
13571
|
+
new Date(recordedAt).toISOString()
|
|
13572
|
+
);
|
|
13495
13573
|
appendLog(
|
|
13496
13574
|
"info",
|
|
13497
|
-
`[${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`
|
|
13498
13576
|
);
|
|
13499
13577
|
}
|
|
13500
13578
|
return true;
|
|
@@ -13522,8 +13600,21 @@ async function uploadSession(args) {
|
|
|
13522
13600
|
"info",
|
|
13523
13601
|
`[${sessionId}] skipping ${sourceTool} ${eventLabel} upload (no complete transcript line yet)`
|
|
13524
13602
|
);
|
|
13525
|
-
if (isSessionEnd)
|
|
13526
|
-
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
|
+
}
|
|
13527
13618
|
return true;
|
|
13528
13619
|
}
|
|
13529
13620
|
const epoch = (prior?.epoch ?? restored?.epoch ?? 0) + 1;
|
|
@@ -13670,10 +13761,15 @@ Uploaded: ${now.toISOString()}`;
|
|
|
13670
13761
|
`Uploaded session ${sessionId} snapshot epoch=${epoch} to project ${config.projectSlug} (${config.projectId}) as contribution ${contributionId}`
|
|
13671
13762
|
);
|
|
13672
13763
|
if (isSessionEnd) {
|
|
13673
|
-
await
|
|
13764
|
+
await setUploadSessionEndedAt(
|
|
13765
|
+
repoRoot,
|
|
13766
|
+
sourceTool,
|
|
13767
|
+
sessionId,
|
|
13768
|
+
new Date(recordedAt).toISOString()
|
|
13769
|
+
);
|
|
13674
13770
|
appendLog(
|
|
13675
13771
|
"info",
|
|
13676
|
-
`[${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`
|
|
13677
13773
|
);
|
|
13678
13774
|
}
|
|
13679
13775
|
return true;
|
|
@@ -13727,6 +13823,7 @@ async function runUpload() {
|
|
|
13727
13823
|
[FLOW_ID_ENV]: flowId
|
|
13728
13824
|
};
|
|
13729
13825
|
if (toolArg) workerEnv[TOOL_ENV_FLAG] = toolArg;
|
|
13826
|
+
await applyDebugLogParentEnv(workerEnv, raw);
|
|
13730
13827
|
try {
|
|
13731
13828
|
const child = spawn2(process.execPath, [entrypoint, "upload"], {
|
|
13732
13829
|
detached: true,
|
|
@@ -14580,6 +14677,18 @@ function cleanupSessionRefs(repoRoot, sessionId) {
|
|
|
14580
14677
|
}
|
|
14581
14678
|
}
|
|
14582
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
|
+
}
|
|
14583
14692
|
|
|
14584
14693
|
// src/git-traces/session-state.ts
|
|
14585
14694
|
import crypto6 from "crypto";
|
|
@@ -14731,6 +14840,27 @@ async function writeStateFile(file, state) {
|
|
|
14731
14840
|
});
|
|
14732
14841
|
await fs17.promises.rename(tmp, file);
|
|
14733
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;
|
|
14862
|
+
}
|
|
14863
|
+
}
|
|
14734
14864
|
async function deleteStateFile(file) {
|
|
14735
14865
|
try {
|
|
14736
14866
|
await fs17.promises.unlink(file);
|
|
@@ -15317,7 +15447,7 @@ async function cleanupStaleScopedSessionStates(repoRoot, tool, currentSessionId,
|
|
|
15317
15447
|
for (const { state, mtimeMs } of states) {
|
|
15318
15448
|
if (state.sessionId === currentSessionId) continue;
|
|
15319
15449
|
const startedAtMs = Date.parse(state.startedAt);
|
|
15320
|
-
const ageBaseMs = Number.isNaN(startedAtMs) ? mtimeMs : startedAtMs;
|
|
15450
|
+
const ageBaseMs = Number.isNaN(startedAtMs) ? mtimeMs : Math.max(startedAtMs, mtimeMs);
|
|
15321
15451
|
if (nowMs - ageBaseMs <= ttlMs) continue;
|
|
15322
15452
|
if (await readLegacySessionState(repoRoot, tool, state.sessionId)) {
|
|
15323
15453
|
appendLog(
|
|
@@ -15336,9 +15466,32 @@ async function cleanupStaleScopedSessionStates(repoRoot, tool, currentSessionId,
|
|
|
15336
15466
|
continue;
|
|
15337
15467
|
}
|
|
15338
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
|
+
);
|
|
15339
15492
|
appendLog(
|
|
15340
15493
|
"info",
|
|
15341
|
-
`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>"})`
|
|
15342
15495
|
);
|
|
15343
15496
|
cleanupSessionRefs(repoRoot, state.sessionId);
|
|
15344
15497
|
await deleteSessionState2(repoRoot, tool, state.sessionId);
|
|
@@ -15384,6 +15537,7 @@ async function prepareSessionStartRepo(repo, tool, sessionId, lineage) {
|
|
|
15384
15537
|
const scoped = (await readSessionStateCandidates(repoRoot, tool, sessionId)).scoped?.state;
|
|
15385
15538
|
if (scoped) {
|
|
15386
15539
|
pinScopedStateRefs(repoRoot, scoped);
|
|
15540
|
+
await touchSessionState(repoRoot, tool, sessionId);
|
|
15387
15541
|
return "initialized";
|
|
15388
15542
|
}
|
|
15389
15543
|
const state = await initializeSession(
|
|
@@ -15935,7 +16089,11 @@ async function processStopRepo(repo, tool, sessionId, recordedAt, lineageCheck,
|
|
|
15935
16089
|
{ limits }
|
|
15936
16090
|
);
|
|
15937
16091
|
if (!patchBuffer) {
|
|
15938
|
-
if (omissionsChanged)
|
|
16092
|
+
if (omissionsChanged) {
|
|
16093
|
+
await persistState(state);
|
|
16094
|
+
} else {
|
|
16095
|
+
await touchSessionState(state.repoRoot, tool, sessionId);
|
|
16096
|
+
}
|
|
15939
16097
|
appendLog(
|
|
15940
16098
|
"info",
|
|
15941
16099
|
`git-traces: turn produced identical snapshot tree, skipping upload (repo=${repoRoot}, project=${config.projectId})`
|
|
@@ -16439,6 +16597,13 @@ async function runGitTraces() {
|
|
|
16439
16597
|
appendLog("error", "git-traces: process.argv[1] is empty");
|
|
16440
16598
|
return;
|
|
16441
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);
|
|
16442
16607
|
try {
|
|
16443
16608
|
const child = spawn3(
|
|
16444
16609
|
process.execPath,
|
|
@@ -16446,12 +16611,7 @@ async function runGitTraces() {
|
|
|
16446
16611
|
{
|
|
16447
16612
|
detached: true,
|
|
16448
16613
|
stdio: ["pipe", "ignore", "ignore"],
|
|
16449
|
-
env:
|
|
16450
|
-
...process.env,
|
|
16451
|
-
[WORKER_ENV_FLAG2]: "1",
|
|
16452
|
-
[TOOL_ENV_FLAG2]: tool,
|
|
16453
|
-
[FLOW_ID_ENV2]: flowId
|
|
16454
|
-
}
|
|
16614
|
+
env: workerEnv
|
|
16455
16615
|
}
|
|
16456
16616
|
);
|
|
16457
16617
|
child.on("error", (err) => {
|