Yeachan-Heo/oh-my-codex · error · Error

Refusing cancellation because state content changed: ${chang

Error message

Refusing cancellation because state content changed: ${change.entry.path}.

What it means

During cancellation of an in-flight write, the CLI re-opens the target state file and compares its content against the originalContent snapshot recorded when the change was staged. If the bytes differ, it refuses to roll the file back, because blindly reverting would clobber concurrent modifications made by another process between the write attempt and the cancellation.

Source

Thrown at src/cli/index.ts:8670

      .map((mode) => {
        const entry = states.get(mode);
        if (!entry) throw new Error(`Missing frozen cancellation entry for ${mode}.`);
        return { mode, entry, nextContent: JSON.stringify(entry.state, null, 2) };
      });
    const opened: Array<{ mode: string; entry: (typeof states extends Map<string, infer T> ? T : never); nextContent: string; handle: Awaited<ReturnType<typeof open>> }> = [];
    try {
      for (const change of orderedChanges) {
        assertRunAuthority();
        const handle = await open(change.entry.path, fsConstants.O_RDWR | fsConstants.O_NOFOLLOW);
        const currentStat = await handle.stat();
        const currentContent = await handle.readFile({ encoding: "utf-8" });
        if (!currentStat.isFile() || currentStat.dev !== change.entry.dev || currentStat.ino !== change.entry.ino) {
          await handle.close();
          throw new Error(`Refusing cancellation because state identity changed: ${change.entry.path}.`);
        }
        if (currentContent !== change.entry.originalContent) {
          await handle.close();
          throw new Error(`Refusing cancellation because state content changed: ${change.entry.path}.`);
        }
        opened.push({ ...change, handle });
      }

      const committed: typeof opened = [];
      const cancellationTestWriteFailureMode = options.testFaults?.writeFailureMode;
      const cancellationTestRollbackFailureMode = options.testFaults?.rollbackFailureMode;
      try {
        for (const openedEntry of opened) {
          assertRunAuthority();
          committed.push(openedEntry);
          if (cancellationTestWriteFailureMode === openedEntry.mode) {
            throw new Error(`Injected cancellation write failure for ${openedEntry.mode}.`);
          }
          await openedEntry.handle.truncate(0);
          await openedEntry.handle.write(openedEntry.nextContent, 0, "utf-8");
          await openedEntry.handle.sync();
        }

View on GitHub (pinned to 3ad79a8a6f)

Solutions

  1. Ensure only one process mutates the state directory at a time (lock file or serialize invocations)
  2. Re-read the current file content and re-stage the change instead of cancelling a stale entry
  3. If the external modification is expected, discard the journal entry explicitly and start a fresh operation
  4. Check for background sync/watcher tools rewriting state files and exclude the directory

Example fix

// before
await cancelChanges(entries); // throws: content changed

// after
if (await currentContentMatches(entry)) {
  await cancelChanges(entries);
} else {
  await discardJournalEntry(entry); // restage from current content
}
Defensive patterns

Strategy: validation

Validate before calling

import { promises as fs } from "node:fs";
// Before cancelling, confirm each entry still matches its snapshot
for (const change of pendingChanges) {
  const current = await fs.readFile(change.entry.path, "utf8").catch(() => null);
  if (current !== change.entry.originalContent) {
    // restage from current content or abort cancellation
    throw new Error(`Stale journal entry for ${change.entry.path}`);
  }
}
await cancelChanges(pendingChanges);

Type guard

function isFreshChange(change: ChangeEntry, currentContent: string | null): boolean {
  return currentContent !== null && currentContent === change.originalContent;
}

Try / catch

try {
  await cancelChanges(changes);
} catch (e) {
  if (/Refusing cancellation because state content changed/.test(String(e))) {
    // restage changes against current disk content instead of retrying blindly
  } else throw e;
}

Prevention

When it happens

Trigger: Calling the cancellation/rollback path (cancelChanges / cancellation flow in src/cli/index.ts) for a state file whose on-disk content no longer equals change.entry.originalContent — i.e. some other writer (another CLI invocation, editor, watcher daemon) touched the file after the change was recorded but before cancellation completed.

Common situations: Two CLI processes operating on the same state directory concurrently; a file watcher or sync tool (Dropbox/IDE) rewriting the file mid-operation; stale change journal entries reused after an interrupted run; mtime-preserving copies that alter content.

Related errors


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