Yeachan-Heo/oh-my-codex · error · QuestionSubmitError
question_not_open
question_not_open
Error message
Timed out acquiring submit lock for ${recordPath} What it means
withQuestionSubmitLock acquires a directory-based lock (mkdir, recovering stale locks on EEXIST). If the lock dir still exists after the deadline and cannot be recovered as stale, it throws QuestionSubmitError with code question_not_open. This guards a record against concurrent submissions.
Source
Thrown at src/question/state.ts:277
const ownerToken = lockOwnerToken();
const deadline = Date.now() + QUESTION_SUBMIT_LOCK_TIMEOUT_MS;
await mkdir(dirname(lockDir), { recursive: true });
while (true) {
try {
await mkdir(lockDir);
try {
await writeFile(ownerPath, ownerToken, 'utf8');
} catch (error) {
await rm(lockDir, { recursive: true, force: true });
throw error;
}
break;
} catch (error) {
const err = error as NodeJS.ErrnoException;
if (err.code !== 'EEXIST') throw error;
if (await maybeRecoverStaleQuestionLock(lockDir)) continue;
if (Date.now() > deadline) {
throw new QuestionSubmitError('question_not_open', `Timed out acquiring submit lock for ${recordPath}`);
}
await sleep(25);
}
}
try {
return await fn();
} finally {
try {
const currentOwner = await readFile(ownerPath, 'utf8');
if (currentOwner.trim() === ownerToken) {
await rm(lockDir, { recursive: true, force: true });
}
} catch {
}
}
}
View on GitHub (pinned to 3ad79a8a6f)
Solutions
- Check whether a stale lock directory remains at the lock path and remove it if its owner process is dead
- Prevent duplicate submissions upstream (debounce/disable submit button while a request is in flight)
- Increase the lock timeout if updates legitimately take long
- If a stale-lock PID check is failing due to PID reuse, clear the lock manually or restart the offending process
Example fix
// before
await withQuestionSubmitLock(recordPath, () => submit(recordPath, answers));
// after
// debounce: only one in-flight submit per record
let submitting = false;
if (submitting) return;
submitting = true;
try { await withQuestionSubmitLock(recordPath, () => submit(recordPath, answers)); }
finally { submitting = false; } Defensive patterns
Strategy: retry
Validate before calling
// before submit, check for an obviously stale lock dir owned by a dead process await maybeClearDeadLock(lockDirFor(recordPath));
Try / catch
try { await withQuestionSubmitLock(p, fn); } catch (e) { if ((e as QuestionSubmitError).code === 'question_not_open') { await maybeClearDeadLock(lockDirFor(p)); return withQuestionSubmitLock(p, fn); // one retry } throw e; } Prevention
- Debounce/dedupe submit actions so only one lock attempt runs
- Clear orphaned lock dirs after crashes
- Keep update callbacks fast to minimize lock hold time
When it happens
Trigger: Two callers submit/mark prompting on the same recordPath simultaneously and the loser waits past the timeout; or a crashed process left a lock directory whose mtime/owner info prevents maybeRecoverStaleQuestionLock from classifying it stale.
Common situations: Duplicate submit events (double-click, retry storm); a killed process leaving a stale lock the recovery heuristic (e.g. PID liveness check) won't reclaim because the PID was reused; NFS or container filesystems where stale-lock detection fails; overly long-held locks during slow writes.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- autoresearch_active_run_exists
- Timed out acquiring task claim lock for ${teamName}/${taskId
- autoresearch_active_mode_exists
- timed out waiting for tmux extended-keys lease lock: ${lockP
- [native-assets] publication-lock-timeout: ${path}; elapsed=$
AI-assisted analysis of Yeachan-Heo/oh-my-codex@3ad79a8a6f (2026-08-27).
Data as JSON: /api/errors/5f09d947e793502a.
Report an issue: GitHub.