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

  1. Re-run GC when no agent session is actively writing (stop running sessions or retry later)
  2. 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
  3. 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

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


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/56dde4a5c5a6163e. Report an issue: GitHub.