Yeachan-Heo/oh-my-codex · critical
Frozen transaction payload is not the canonical staged trans
Error message
Frozen transaction payload is not the canonical staged transaction file.
What it means
The update worker requires the payload argument to canonicalize (via realpath, no symlinks) to exactly <stage>/transaction.json. This error fires when the payload path is a symlink, resolves outside the stage, or is not the canonical transaction.json filename.
Source
Thrown at src/cli/update-worker.ts:98
async function main(): Promise<void> {
const payloadPath = process.argv[2];
const expectedDigest = process.argv[3];
let payload: DeferredUpdatePayload | null = null;
let stagedDirectory: string | null = null;
try {
const expectedWorkerDigest = process.argv[4];
if (!payloadPath || !expectedDigest || !expectedWorkerDigest) throw new Error('Frozen transaction payload is missing.');
const workerPath = await canonicalRegularFile(process.argv[1] ?? '', await realpath(join(process.argv[1] ?? '', '..')));
if (!workerPath || digest(await readFile(workerPath, 'utf-8')) !== expectedWorkerDigest) throw new Error('Frozen update worker identity changed before execution.');
const stage = await realpath(join(payloadPath, '..'));
if (!await ownerOnlyStage(stage) || !basename(stage).startsWith('omx-update-')) {
throw new Error('Frozen transaction staging directory is not an owner-only update stage.');
}
const stagedPayload = await canonicalRegularFile(payloadPath, stage);
if (!stagedPayload || stagedPayload !== join(stage, 'transaction.json')) {
throw new Error('Frozen transaction payload is not the canonical staged transaction file.');
}
const serialized = await readFile(stagedPayload, 'utf-8');
if (digest(serialized) !== expectedDigest) throw new Error('Frozen transaction payload fingerprint changed before execution.');
const parsedPayload: unknown = JSON.parse(serialized);
if (!isDeferredUpdatePayload(parsedPayload)) throw new Error('Frozen transaction payload is incomplete.');
payload = parsedPayload;
stagedDirectory = stage;
await waitForParent(payload.parentPid);
if (!await validatePackageManagerOwnership(payload.ownership)) throw new Error('Frozen manager, package root, or bin ownership validation failed before update.');
const result = payload.ownership.manager === 'npm'
? runNpmCommand(payload.ownership.npmCommand, installArgs(payload.ownership), { ...installOptions, env: payload.ownership.environment })
: spawnSync(payload.ownership.bunCommand, installArgs(payload.ownership), { ...installOptions, env: payload.ownership.environment });
if (result.error || result.status !== 0) throw new Error(String(result.stderr || result.error?.message || 'controller install failed'));
const cliEntry = await validatePackageManagerOwnership(payload.ownership);
if (!cliEntry) throw new Error('Frozen manager, package root, or bin ownership validation failed after update.');
const setup = spawnSync(process.execPath, [cliEntry, ...payload.setupArgs], { cwd: payload.cwd, env: { ...payload.ownership.environment, [SKIP_NATIVE_AGENT_REFRESH_ENV]: '1' }, stdio: 'inherit', windowsHide: true });
if (setup.error || setup.status !== 0) throw new Error(setup.error?.message || `setup exited ${setup.status}`);
await finalizeSuccessfulUpdate(payload.ownership);View on GitHub (pinned to 3ad79a8a6f)
Solutions
- Pass the exact staged path ending in /transaction.json inside the omx-update-* stage directory
- Ensure the payload is a regular file, not a symlink (cp -L if copied from elsewhere)
- Re-run the standard update flow rather than invoking update-worker.js directly
Example fix
# before node update-worker.js /tmp/omx-update-abc/txn.json ... # after node update-worker.js /tmp/omx-update-abc/transaction.json ...
Defensive patterns
Strategy: validation
Validate before calling
import { realpath } from 'node:fs/promises';
import { join } from 'node:path';
const ok = (await realpath(payloadPath)) === join(stage, 'transaction.json'); Type guard
const isCanonicalPayload = (p: string, stage: string) => p === join(stage, 'transaction.json');
Try / catch
Wrap the worker invocation in try-catch and re-run the full update flow (restaging) instead of retrying with the same payload path.
Prevention
- Never rename or symlink transaction.json inside the stage
- Always derive payloadPath as join(stage, 'transaction.json') at spawn time
- Don't invoke update-worker.js manually; use the documented update command
When it happens
Trigger: Invoking update-worker.js with a payloadPath that is a symbolic link to transaction.json, points to a differently-named file, or resolves to a path other than join(stage, 'transaction.json').
Common situations: Manually invoking the worker for debugging with a hand-crafted path, payload replaced by a symlink by backup/sync tooling, or a modified staging layout after a version change.
Related errors
- Frozen transaction staging directory is not an owner-only up
- invalid auth slot name: use 1-64 letters, numbers, '.', '_'
- agents-init target must stay inside the current working dire
- invalid detached leader payload
- invalid detached leader parent environment
AI-assisted analysis of Yeachan-Heo/oh-my-codex@3ad79a8a6f (2026-08-27).
Data as JSON: /api/errors/89dad2c89e06e4de.
Report an issue: GitHub.