can1357/oh-my-pi · error · Error
session header changed during stats cleanup: ${session.path}
Error message
session header changed during stats cleanup: ${session.path} What it means
During stats cleanup, inside the stats-sync lock, each retained session's lineage header is re-read and its id compared to the id captured earlier. A mismatch means the session file was rewritten/replaced (e.g. by compaction or a new session) while cleanup was in flight, so the run aborts to avoid deleting or relinking the wrong file.
Source
Thrown at packages/coding-agent/src/cli/gc-cli.ts:1203
archivedByPath.set(path.resolve(session.path), session);
}
} catch (error) {
result.errors.push(`stats cleanup scan: ${errorMessage(error)}`);
}
try {
retainedSessions = await listActiveSessions(sessionsRoot);
} catch (error) {
result.errors.push(`stats cleanup scan: ${errorMessage(error)}`);
return;
}
try {
await withStatsSyncLock(dbPath, async () => {
const retainedStatsSessions = await Promise.all(
retainedSessions.map(async session => {
const header = await readSessionLineageHeader(session.path);
if (!header || header.id !== session.id) {
throw new Error(`session header changed during stats cleanup: ${session.path}`);
}
return {
path: session.path,
id: session.id,
parentSession: header.parentSession,
historicalPaths: managedHistoricalSessionPaths(header, session.path, sessionsRoot),
identities: createStatsIdentities(),
};
}),
);
const context = buildStatsCleanupPlans([...archivedByPath.values()], retainedStatsSessions, dbPath);
await populateStatsTransferTargets(context);
result.statsRowsDeleted = reconcileStatsRowsForSessions(dbPath, context.plans);
});
} catch (error) {
result.errors.push(`stats cleanup: ${errorMessage(error)}`);
}
}View on GitHub (pinned to 9690622007)
Solutions
- Re-run GC when no agent session is actively writing (stop running sessions or retry later)
- Verify the session file at the path in the message is healthy — re-read its header and confirm the id matches what the session index recorded
- Rebuild/refresh the stats session index so captured ids match current files, then retry
Defensive patterns
Strategy: retry
Validate before calling
import * as fs from "node:fs/promises";
// Before cleanup, confirm each retained session header still matches the recorded id
for (const s of retainedSessions) {
const header = await readSessionLineageHeader(s.path);
if (!header || header.id !== s.id) throw new Error(`stale index entry: ${s.path}`);
} Try / catch
try {
await runStatsCleanup(retainedSessions);
} catch (err) {
if (err instanceof Error && err.message.startsWith("session header changed during stats cleanup:")) {
// a live session rotated its file — refresh the session index and retry when idle
await refreshSessionIndex();
await runStatsCleanup(await listRetainedSessions());
} else throw err;
} Prevention
- Run stats cleanup/GC when no interactive agent sessions are actively writing
- Always perform cleanup inside the stats-sync lock
- Refresh the session index immediately before cleanup so captured ids are current
- Retry cleanup on this error instead of forcing deletion
When it happens
Trigger: A retained session file's header changes between listing and cleanup: the session was compacted/rewritten, its id rotated, or the file was truncated/corrupted concurrently by an active agent process.
Common situations: Running `omp gc` while an interactive coding-agent session is live and rotating its session file; a crash-recovery rewrite altering the header; manually editing or resuming a session during cleanup.
Related errors
- OAuth refresh ownership was lost before persistence
- No credential with id=${id}
- WAL checkpoint failed for ${dbPath}: busy=${result.busy}, wa
- GC already running: ${lockPath}
- Daemon ${spec.name} is already starting
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/56dde4a5c5a6163e.
Report an issue: GitHub.