Skip to content

feat: add task --thread <id> to resume a specific Codex thread - #719

Open
y-cruce wants to merge 5 commits into
openai:mainfrom
y-cruce:feat/task-thread
Open

feat: add task --thread <id> to resume a specific Codex thread#719
y-cruce wants to merge 5 commits into
openai:mainfrom
y-cruce:feat/task-thread

Conversation

@y-cruce

@y-cruce y-cruce commented Sep 3, 2026

Copy link
Copy Markdown

Summary

task --resume-last can only continue the newest finished task thread of the current Claude session in the repo. When a caller drives several Codex threads in one session (for example one thread per problem, or a review that ran as a task in between), the older threads become unreachable even though the app-server can resume them by id.

This adds task --thread <id> to resume a specific thread through the same path --resume-last uses.

Changes

  • plugins/codex/scripts/codex-companion.mjs: parse --thread, validate it (non-empty, not flag-like, mutually exclusive with --resume/--resume-last/--fresh), pass resumeThreadId through buildTaskRequest/executeTaskRun in both foreground and --background modes. The thread id is not pre-written into the job record; it is stored by the existing Thread ready progress handling after a successful resume, so a failed resume of an unknown id does not pollute the next --resume-last.
  • tests/runtime.test.mjs: resume by id (foreground and background), flag not leaked into the prompt, conflicting and invalid values rejected, failed resume does not pre-write the id.
  • tests/commands.test.mjs, README.md, plugins/codex/commands/rescue.md, plugins/codex/agents/codex-rescue.md, plugins/codex/skills/codex-cli-runtime/SKILL.md: document the flag and its routing.

No changes under lib/; --resume-last, --resume, and --fresh behave as before. Version not bumped.

Verification

  • npm test: the new tests pass; no new failures relative to the clean tree on the same machine (the remaining failures there are environment-related setup/status/result/resolveStateDir cases that fail identically without this change).
  • Manual: with a shared app-server running and a newer thread being the task-resume-candidate, task --thread <older-id> "..." resumed the older thread (Thread ready (<older-id>)) and Codex answered from that thread's context.

Notes

--thread intentionally bypasses the current-session filter used by --resume-last. It does not check repository ownership of the thread; the resume request carries the current cwd.

@y-cruce
y-cruce requested a review from a team September 3, 2026 03:37
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-09T05:00:32.374130Z 251ef06 New commits
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@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.

--thread accepts any known thread ID without checking the thread’s repository. That can resume context from repo A while executing with repo B’s current cwd, which risks cross-repo context leakage and wrong-repo edits. Please verify the saved thread cwd/repository before resuming, with an explicit override only if cross-repo resume is intended.

@y-cruce

y-cruce commented Sep 3, 2026

Copy link
Copy Markdown
Author

Good catch, thanks. --resume-last is implicitly scoped to the current workspace through the job records, and --thread skipped that.

Added in the latest commit: before resuming, the thread id must appear in the current workspace's tracked task jobs (any Claude session). Otherwise the run fails before any resume request is sent, with an error pointing to --allow-other-repo, which is the explicit override for an intentional cross-repo resume. The check runs in executeTaskRun, so foreground and --background behave the same, and a rejected run leaves the job failed without a threadId.

Threads created outside the plugin (plain codex CLI) are not tracked and therefore also need the override; the error text and the docs say so.

Tests cover: tracked thread from another Claude session resumes; untracked thread rejected in foreground and background; --allow-other-repo resumes an untracked thread; --allow-other-repo with an unknown id still fails at Codex without pre-writing the id.

@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: 8ecb158f7b

ℹ️ 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 on lines +359 to +360
if (listJobs(workspaceRoot).some((job) => job.threadId === threadId)) {
return;

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 foreign-thread provenance after an override

When a thread from repo A is first resumed in repo B with --allow-other-repo, the successful run records that thread ID in repo B's job state. On a later invocation this predicate therefore accepts task --thread <id> without the required override, even though the thread still contains repo A's history. Track whether a thread was admitted through the override, or verify its original workspace, rather than treating every local job record as proof of repository ownership.

Useful? React with 👍 / 👎.

Comment on lines +474 to +475
if (request.resumeThreadId && !request.allowOtherRepo) {
requireTrackedThreadForWorkspace(workspaceRoot, request.resumeThreadId);

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 Validate the tracked thread before inserting the new job

When the state already contains the maximum 50 jobs and the requested thread appears only in the oldest record, runTrackedJob (or the background enqueue path) inserts the new job before this validation runs. saveState then prunes that oldest record via pruneJobs(...).slice(0, MAX_JOBS), so this check rejects a thread that was tracked when the command started and incorrectly demands --allow-other-repo. Validate against the pre-insertion state or preserve thread ownership separately from the bounded job history.

Useful? React with 👍 / 👎.

@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.

Rechecked the current 9b63301 head against the repository-scope guard. The original cross-repo concern is improved, but two ownership edges remain:

  1. A foreign thread resumed once with --allow-other-repo is then written into this repository's bounded job history. A later task --thread <id> sees that local job record and accepts the thread without the override, even though its original provenance is still another repository. The override should not permanently convert foreign provenance into local ownership.
  2. handleTask() creates/persists the new job before executeTaskRun() calls requireTrackedThreadForWorkspace(). If the requested thread exists only in the oldest record at the 50-job cap, creating the new job can prune the very evidence the validation needs and incorrectly reject a thread that was tracked at command start.

Could thread ownership/provenance be stored separately from bounded job history, or validated from a pre-insertion snapshot, with foreign-thread provenance retained across overridden resumes?

@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: 9b6330164a

ℹ️ 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 on lines +99 to +102
const state = this.state(p.threadId);
if (state.turnId !== p.turnId) return false;
const question = { requestId: message.id, ...p, expiresAt: Date.now() + this.inputTimeoutMs };
state.questions.push(question);

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 Expose subagent questions through the parent job

When a task's collaboration subagent calls request_user_input, this stores the question under the child thread ID. However, liveStatus and sendLiveCommand query only the parent job's thread, so /codex:status cannot display the question and /codex:answer cannot find it; after ten minutes the timer interrupts the child turn. Associate child-thread questions with the owning parent job or search the active thread tree when reporting and answering questions.

Useful? React with 👍 / 👎.

Comment on lines +30 to +32
if (job.jobClass !== "task" || job.status !== "running" || !job.threadId) {
throw new Error("This command requires a running task with a known thread and turn.");
}

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 Permit answers for review jobs with pending questions

When an adversarial-review turn invokes request_user_input, the shared broker accepts and exposes that request because the feature and handler apply globally, but this guard rejects /codex:answer for every review job. The displayed question therefore cannot be answered and remains blocked until the ten-minute timeout interrupts the review. Allow answer for any running job with a matching pending broker request while retaining the task-only restriction for live messages, or prevent these review turns from issuing input requests.

Useful? React with 👍 / 👎.

xingjian.ym added 2 commits September 9, 2026 12:46
- Task threads declare a `notify_director` dynamic tool (experimentalApi enabled at
  initialize). The broker answers `item/tool/call` at once, stores the note on the
  thread's live state, and `status --wait` returns early with `hasNotifications`,
  acknowledging the notes it returned. Notes are capped at 400 characters and logged
  as `notified` progress.
- `events --cwd <repo> [--poll-ms]` streams one line per job event (DONE, FAILED,
  QUESTION, NOTIFIED, STALLED) for a long-running monitor, acknowledging notes it
  delivers.
- `task --sandbox <read-only|workspace-write|danger-full-access>` and `--network`
  override the sandbox derived from `--write`; the mode is persisted and rendered.
…solation

- The shared broker tracks stream ownership per thread instead of one active
  stream, so several tasks run concurrently in one repository; notifications are
  routed to the owning socket, non-streaming requests are serialized, and busy is
  returned only when another socket owns the target thread.
- The broker exits after 10 minutes without connections or streams
  (`--idle-timeout-ms`); `ensureBrokerSession` recovers from a dead broker and
  serializes cold starts. Job store writes are atomic and locked.
- `events` and `status --wait` detect a dead owner process or an unreachable broker
  and mark the job failed; `--stall-ms` (default 15 minutes) reports STALLED.
- Tests use isolated plugin data and broker endpoints and shut down only the brokers
  they created; previously a test run could send shutdown to a live session's broker.

@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: 251ef06f89

ℹ️ 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 on lines +37 to +39
const reason = (options.ownerAlive ?? ownerProcessAlive)(job.pid) === false
? "owner process exited"
: failures > 2 ? "broker unreachable" : null;

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 Keep direct reviews out of broker-failure detection

When a native or adversarial review falls back to a direct app-server in withAppServer (for example, because the configured broker endpoint is unavailable), its owner process and Codex turn can remain healthy while liveStatus necessarily reports the broker unavailable. After three polls, this branch marks that running review failed, so status --wait can return a false failure and events can permanently emit FAILED before the direct review completes. Persist the transport used by the job, or only use broker reachability as a liveness signal for jobs that require the broker.

Useful? React with 👍 / 👎.

Comment on lines +110 to +113
const notification = { id: crypto.randomBytes(6).toString("hex"), message: p.arguments.message,
turnId: p.turnId, receivedAt: new Date().toISOString() };
state.notifications.push(notification);
this.notify({ method: "companion/notification", params: { threadId: p.threadId, ...notification } });

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 Route subagent notifications to the owning task

When a collaboration child invokes the newly added notify_director tool, state is the child thread's state and the notification is stored there. /codex:status, events, and notification acknowledgement query only the parent job's thread ID, so the tool reports successful delivery but the note never appears through those user-facing paths and remains unacknowledged. Associate the notification with the root/owning task state or aggregate child-thread notifications when taking the parent snapshot.

Useful? React with 👍 / 👎.

@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.

Rechecked current head 251ef06. The two thread-ownership issues from my previous review are still present:

  1. requireTrackedThreadForWorkspace() still treats any local job record containing the thread ID as repository ownership. A foreign thread resumed once with --allow-other-repo is persisted into this repositorys

@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.

Rechecked current head 251ef06. The two thread-ownership issues from my previous review are still present:

  1. requireTrackedThreadForWorkspace() still treats any local job record containing the thread ID as repository ownership. A foreign thread resumed once with --allow-other-repo is persisted into this repository's job history, so a later task --thread <id> can resume it without the override. The override still effectively converts foreign provenance into local ownership.
  2. Validation still happens after job-state mutation. Foreground runTrackedJob() writes/upserts the new running record before executeTaskRun() calls requireTrackedThreadForWorkspace(), and background enqueueBackgroundTask() does the same before the worker validates. Because upsertJob() prunes the bounded history, the 50-job edge can still discard the oldest record that proves the requested thread was tracked at command start.

The new broker/liveness changes do not address either ownership boundary. I would keep thread provenance separate from bounded job history, or validate from an immutable pre-insertion snapshot and retain foreign provenance explicitly across overridden resumes.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants