Yeachan-Heo/oh-my-codex · critical · UltragoalError

Refusing durable ultragoal mutation after writable lifecycle

Error message

Refusing durable ultragoal mutation after writable lifecycle authority drift while waiting for the mutation lock: before lock ${describeWritableAuthority(beforeLock)}; after lock ${describeWritableAuthority(afterLock)}.

What it means

Before and after acquiring the mutation lock, the library snapshots the writable lifecycle authority (session binding). If they differ, a session publication (e.g. SessionStart) landed while this process waited for the lock, and the mutation would write into state whose ownership just changed — so it refuses with both before/after snapshots for diagnosis. This is a deliberate TOCTOU guard preserving durable-state integrity.

Source

Thrown at src/ultragoal/artifacts.ts:964

      handle = await open(lockPath, 'wx');
      await handle.writeFile(JSON.stringify({ pid: process.pid, createdAt: iso() }));
      break;
    } catch (error) {
      const code = (error as NodeJS.ErrnoException).code;
      if (code !== 'EEXIST') throw error;
      await sleep(Math.min(25 + attempt * 5, 250));
    }
  }
  if (!handle) {
    throw new UltragoalError(`Timed out waiting for ultragoal mutation lock at ${repoRelative(cwd, lockPath)}.`);
  }
  try {
    // The post-lock comparison addresses pointer changes while waiting for this
    // lock only. A SessionStart publication can still land after it and before
    // the operation's filesystem writes.
    const afterLock = await assertUltragoalWritableLifecycleAuthority(cwd, options);
    if (!writableAuthorityEquals(beforeLock, afterLock)) {
      throw new UltragoalError(
        `Refusing durable ultragoal mutation after writable lifecycle authority drift while waiting for the mutation lock: before lock ${describeWritableAuthority(beforeLock)}; after lock ${describeWritableAuthority(afterLock)}.`,
      );
    }
    return await operation();
  } finally {
    await handle.close().catch(() => undefined);
    await rm(lockPath, { force: true }).catch(() => undefined);
  }
}

async function appendLedger(cwd: string, entry: UltragoalLedgerEntry): Promise<void> {
  await mkdir(ultragoalDir(cwd), { recursive: true });
  const path = ultragoalLedgerPath(cwd);
  await appendFile(path, `${JSON.stringify(entry)}\n`);
}

/** Pure plan read: no durable writes, no migration. */
async function readUltragoalPlanFile(cwd: string): Promise<UltragoalPlan> {

View on GitHub (pinned to 3ad79a8a6f)

Solutions

  1. Retry the whole mutation after re-reading the current session binding (export the new OMX_SESSION_ID) — the drift is usually a legitimate session rotation
  2. Coordinate process lifecycle: do not start/restart sessions while ultragoal mutations are in flight (barrier or queue around SessionStart)
  3. Inspect the before/after snapshots in the message to confirm which side is authoritative, then align the environment to it
  4. If drift recurs, check for two competing session publishers (duplicate orchestrators) writing session.json

Example fix

// before
await appendStory(state, goal); // another session published while we waited -> refused

// after
await refreshSessionBinding(); // re-read session.json -> OMX_SESSION_ID
await appendStory(state, goal); // now consistent under current authority
Defensive patterns

Strategy: retry

Type guard

function isAuthorityDriftError(e: unknown): boolean {
  return e instanceof UltragoalError && e.message.includes('authority drift while waiting for the mutation lock');
}

Try / catch

try {
  return await mutateUltragoalState(...);
} catch (e) {
  if (isAuthorityDriftError(e)) {
    await refreshSessionBinding(); // align OMX_SESSION_ID to the 'after lock' snapshot in the message
    return mutateUltragoalState(...); // safe: pre-lock check re-runs under new authority
  }
  throw e;
}

Prevention

When it happens

Trigger: A process acquires the mutation lock after waiting, and assertUltragoalWritableLifecycleAuthority returns a different authority than the pre-lock snapshot — typically because a concurrent SessionStart publication rewrote session.json/OMX_SESSION_ID authority in the gap between the two checks.

Common situations: Two sessions starting simultaneously against the same repo; a new bench bootstrapping (publishing SessionStart) while an old one is mid-mutation; orchestration scripts restarting agents without coordinating with in-flight ultragoal writes.

Related errors


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