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

Timed out waiting for ultragoal mutation lock at ${repoRelat

Error message

Timed out waiting for ultragoal mutation lock at ${repoRelative(cwd, lockPath)}.

What it means

Durable ultragoal mutations serialize through a file lock (open with O_EXCL in a retry loop with backoff capped at 250ms). If the lock file at lockPath cannot be acquired within the allotted attempts — because another process holds it or a stale lock file was left behind after a crash — this UltragoalError is thrown so the caller can retry rather than block forever.

Source

Thrown at src/ultragoal/artifacts.ts:956

  options: { allowUnboundEnvironment?: boolean } = {},
): Promise<T> {
  const beforeLock = await assertUltragoalWritableLifecycleAuthority(cwd, options);
  await mkdir(ultragoalDir(cwd), { recursive: true });
  const lockPath = join(ultragoalDir(cwd), ULTRAGOAL_MUTATION_LOCK);
  let handle: Awaited<ReturnType<typeof open>> | undefined;
  for (let attempt = 0; attempt < 100; attempt += 1) {
    try {
      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);
  }
}

View on GitHub (pinned to 3ad79a8a6f)

Solutions

  1. Retry the operation with backoff — the lock is advisory and short-lived, so transient contention resolves itself
  2. Check for a stale lock file at the repo-relative path in the error message and delete it if no other process holds it (lsof/fuser to confirm)
  3. Serialize ultragoal mutations across your processes (queue them) instead of racing many writers
  4. If contention is chronic, reduce the number of concurrent mutators or lengthen your own outer timeout around the call

Example fix

// before
await appendStory(state, goal); // under contention -> timed out waiting for lock

// after
for (let i = 0; i < 5; i++) {
  try { await appendStory(state, goal); break; }
  catch (e) { if (!isLockTimeout(e) || i === 4) throw e; await sleep(500 * 2 ** i); }
}
Defensive patterns

Strategy: retry

Validate before calling

import { existsSync } from 'node:fs';

function lockLooksStale(lockPath: string): boolean {
  // only treat as stale if no other ultragoal process is running
  return existsSync(lockPath);
}

Type guard

function isLockTimeoutError(e: unknown): boolean {
  return e instanceof UltragoalError && e.message.includes('Timed out waiting for ultragoal mutation lock');
}

Try / catch

for (let attempt = 0; attempt < 5; attempt++) {
  try {
    return await mutateUltragoalState(...);
  } catch (e) {
    if (!isLockTimeoutError(e) || attempt === 4) throw e;
    await sleep(250 * 2 ** attempt);
  }
}

Prevention

When it happens

Trigger: Calling any mutation wrapped by withUltragoalMutationLock while another long-running process (concurrent bench, another agent) holds the lock; or after a hard crash left the lock file on disk without an owner, exhausting retries with EEXIST every time.

Common situations: Parallel agents/benches mutating the same ultragoal state; a SIGKILLed process leaving a stale lock file; NFS/network filesystems where exclusive create semantics are flaky; very slow filesystems exceeding the total retry window.

Understand the failure class

Related errors


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