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

  1. Pass the exact staged path ending in /transaction.json inside the omx-update-* stage directory
  2. Ensure the payload is a regular file, not a symlink (cp -L if copied from elsewhere)
  3. 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

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


AI-assisted analysis of Yeachan-Heo/oh-my-codex@3ad79a8a6f (2026-08-27). Data as JSON: /api/errors/89dad2c89e06e4de. Report an issue: GitHub.