Skip to content

fix: persist background jobs before worker spawn - #733

Open
fscfede-beep wants to merge 7 commits into
openai:mainfrom
fscfede-beep:fix/background-job-write-before-spawn-620
Open

fix: persist background jobs before worker spawn#733
fscfede-beep wants to merge 7 commits into
openai:mainfrom
fscfede-beep:fix/background-job-write-before-spawn-620

Conversation

@fscfede-beep

Copy link
Copy Markdown

Summary

Fixes #620.

  • persist the queued job record and index before starting the detached worker, so task-worker can never race a missing bootstrap record
  • start the queued record with pid: null; the worker claims the record with its own PID when runTrackedJob starts
  • move the initial running job-file/index writes inside runTrackedJob's existing try so startup persistence failures enter the normal failure-bookkeeping path
  • add a deterministic ordering regression that requires both durable writes to precede spawnDetachedTaskWorker and requires the runTrackedJob try boundary to cover its initial writes

This keeps the background protocol unchanged while removing the measured Windows race where the child could reach readStoredJob hundreds of milliseconds before the parent wrote the record.

Validation

Windows 11 / Node 26.3.1:

  • test-first ordering regression: RED on upstream/main, because spawnDetachedTaskWorker precedes writeJobFile / upsertJob
  • ordering regression after patch: 1 passed, 0 failed
  • existing detached-worker end-to-end runtime: 1 passed, 0 failed
  • full tests/commands.test.mjs: 9 passed, 0 failed
  • broader runtime pattern background|cancel: 5 passed, 1 failed; the single failure is the existing Windows taskkill "operation not permitted" case and reproduces identically on untouched upstream/main
  • node --check plugins/codex/scripts/codex-companion.mjs
  • node --check plugins/codex/scripts/lib/tracked-jobs.mjs
  • git diff --check

Fresh collision check immediately before publication found no open PR for #620.

@fscfede-beep
fscfede-beep requested a review from a team September 4, 2026 18:50

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ce6563b305

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/codex/scripts/codex-companion.mjs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f8452a16df

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/codex/scripts/codex-companion.mjs

@sylvesterkaczmarek sylvesterkaczmarek left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Persist-before-spawn fixes the original child-beats-parent race, and the worker's queued-status check correctly makes cancellation successor-safe. One failure path is now exposed: if spawnDetachedTaskWorker() itself throws after writeJobFile/upsertJob, the caller gets an error but a durable queued, pid: null job remains with no worker to claim it. Please transition or remove that bootstrap record on spawn failure under the same job ownership boundary, so this does not trade a missing-record race for a permanently stranded queue entry.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2f35b690dc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/codex/scripts/codex-companion.mjs
Comment thread plugins/codex/scripts/codex-companion.mjs Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d7a435dbf6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

return { ...job, status: "removed", phase: "removed", pid: null };
}
const currentJob = readStoredJobOrNull(job.workspaceRoot, job.id) ?? job;
if (isJobCancellationRequested(job.workspaceRoot, job.id) || currentJob.status !== "queued") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Serialize spawn-failure handling with stop markers

When an emitted spawn error overlaps /codex:cancel or SessionEnd, this marker/status check and the subsequent failed-record writes are not atomic. If the callback passes this guard just before the other process publishes its marker and terminalizes/removes the job, it can resume afterward and overwrite cancelled with failed or recreate a job that session cleanup removed. The fresh evidence is that failTrackedJobLaunch performs its only cancellation/removal checks before the writes at lines 167-168; coordinate the transition or revalidate without allowing a check-to-write race.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 0d8cb00. I reproduced both check→write races deterministically by triggering cancellation/removal from error.toString() after the initial guard but before the failed-record write. RED on d7a435d: both cases ended as failed. The fix now revalidates durable terminal markers after persisting failed: removal deletes job file/state; cancellation rewrites canonical cancelled state, so terminal authority wins even if it arrives in that window. Validation on Windows: tracked-jobs + commands + state 20/20 PASS; targeted runtime background|cancel|SessionEnd 6 relevant cases PASS with one known Windows taskkill failure. I reproduced that exact taskkill ... operation not permitted failure on untouched upstream/main (db52e28), confirming it is baseline and unrelated. node --check and git diff --check clean. @codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0d8cb00272

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

cancelledAt: completedAt,
errorMessage: "Cancelled by user."
};
writeJobFile(job.workspaceRoot, job.id, cancelledRecord);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recheck removal after persisting startup cancellation

When /codex:cancel and SessionEnd overlap during the queued-worker handoff, the worker can pass the removal check at line 217, observe the cancellation marker, and then have session cleanup publish .removed and delete the job before this write. If process termination is delayed or fails, this branch recreates the cancelled job file and index after cleanup and never revalidates the removal marker, unlike failTrackedJobLaunch; recheck removal after these writes and delete the artifacts/state so session removal remains authoritative.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in d5fda68. The startup-cancellation branch now rechecks the durable removal marker after persisting cancelled; if SessionEnd won in that window, it deletes the per-job file, removes the indexed state, and returns removedExecution instead of recreating terminalized work. Added a RED structural regression on 0d8cb00 requiring removal revalidation after the cancellation writes; GREEN on this commit, plus existing behavioral coverage. Fresh validation: tracked-jobs + commands + state 21/21 PASS. Targeted runtime background|cancel|SessionEnd has 6 relevant cases PASS and one Windows taskkill failure; I reproduced that exact failure again on untouched upstream/main (db52e28), confirming baseline. node --check/git diff --check clean. @codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d5fda68121

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const cwd = resolveCommandCwd(options);
const reference = positionals[0] ?? "";
const { workspaceRoot, job } = resolveCancelableJob(cwd, reference, { env: process.env });
markJobCancellationRequested(workspaceRoot, job.id);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve SessionEnd removal in the cancel handler

When /codex:cancel pauses in interruptAppServerTurn and SessionEnd runs in that window, cleanup publishes .removed and deletes the job, but this handler subsequently writes and indexes cancelled unconditionally, recreating the removed artifacts. A late worker will not repair this because it reads the recreated cancelled record and returns before runTrackedJob. The fresh evidence after the line-232 fix is that removal revalidation was added only to the worker's startup-cancellation branch; handleCancel still needs to recheck removal after its cancellation writes and delete the per-job file and indexed state when SessionEnd won.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d5fda68121

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

};
}
const execution = await runner();
if (isJobRemovalRequested(job.workspaceRoot, job.id)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recheck removal after persisting completion

When cleanupSessionJobs starts just after this check, the worker can write the completed job file with pid: null, after which SessionEnd reads the still-running index entry, cannot terminate the worker using that file, and deletes the job before the worker's subsequent upsertJob. The upsert then recreates the session's job index without its result file, so cleanup is undone and /codex:result cannot retrieve the advertised completed result. Revalidate the removal marker after the completion file/index writes or serialize terminalization with removal.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants