Conversational completion relay

August 23, 2026 ยท View on GitHub

Codex Process Jobs can wake the persistent Codex task that launched a detached command without turning the command into a subagent.

Delivery flow

  1. $codex-process-jobs:start records the user-visible task that performs the launch and owns completion. CPJ does not delegate local process execution or monitoring to a spawned subagent. Legacy records from earlier Dev builds can still identify a child launch thread and route completion to the highest user-visible ancestor. CPJ then launches the ordinary OS command in a detached process group.
  2. The worker records the terminal state before attempting notification, so status and result remain available even if notification fails.
  3. The notifier first invokes official codex queue with the validated owning task ID and sanitized completion sentence. Codex CLI 0.149.0 introduced this queue for messages that should run after an active writer releases the task, and an idle ordinary TUI wakes without a daemon or special invocation. Exit zero means Codex durably accepted the message; CPJ records notification.transport: codex-queue and does not try another transport. A timeout, signal, or output overflow is acceptance-uncertain and likewise never falls through. A missing binary or unsupported command is safe to fall back.
  4. For an explicitly opted-in older CLI task, the notifier can next check Codex's official shared local App Server Unix socket. The user must have started codex app-server daemon before the ordinary TUI session began; CPJ never starts or installs it automatically. This v0.3.0 experiment remains a compatibility fallback, not a requirement on Codex 0.149.0.
  5. For a local macOS Codex App task or a macOS or Linux VS Code task, a separate lightweight notifier can attempt Codex's private same-user IPC router. It verifies private socket ownership and permissions, targets the validated owning task ID, waits for a settled idle boundary, and confirms the returned turn ID reaches durable task_complete.
  6. If every guarded live path is unavailable before possible acceptance, the notifier falls back to a separate local codex app-server connection. A failure after possible acceptance never starts a competing second turn.
  7. A notifier may atomically claim up to 20 compatible terminal siblings owned by the same task and deliver them in one turn. The concise user-facing notice begins with CPJ background job and contains only one sanitized job id, terminal status, and exit code per record. It never interpolates the command, working directory, job label, environment, stdout, stderr, or agent instructions. The hook still accepts the earlier unbranded prefix for in-flight jobs and upgrades.
  8. In the default auto mode, App, VS Code, remote, queue-woken CLI, and confirmed legacy live CLI surfaces ask Codex to inspect bounded saved output with result --peek, summarize the evidence, and continue only a clear next step already authorized and still in scope from the prior conversation. Otherwise Codex recommends one next step and asks. The default portable CLI path and unknown surfaces request only a short acknowledgment in the direct completion turn. The first eligible hook boundary then gives a default-path CLI-owned job the same bounded inspection contract, so the first turn the TUI user actually sees carries the App-equivalent content; unknown surfaces stay report-only. A durable execution-host preference at $CODEX_HOME/process-jobs/config.json can select report, inspect, or auto; set it with node scripts/job.mjs config --completion-mode <mode>. CODEX_PROCESS_JOBS_COMPLETION_MODE has higher precedence. Invalid environment values or invalid preference files fail closed to report. The consent-gated UserPromptSubmit hook recognizes only the exact concise notice grammar and verifies every stated value against a same-task terminal record whose notification is currently delivering or was accepted by the exact codex-queue transport. It then injects one fixed hidden report, inspect, or Goal-continuation policy. The model uses the normal namespaced result skill when that policy calls for --peek; no structured skill attachment is sent through the private protocol. If the hook is disabled or untrusted, the direct turn safely degrades to reporting the visible terminal status while the durable result remains available. For jobs explicitly launched with --goal-mode, the fixed Goal instruction takes precedence over this surface preference: inspect result --peek, then continue already-authorized in-scope work if the Goal remains active; otherwise recommend one next step and ask.
  9. Consent-gated PostToolUse, Stop, and UserPromptSubmit hooks share the same terminal-result claim logic. A terminal job can therefore surface after a supported local tool call during an active turn, as a one-time stop continuation, or on the first eligible ordinary non-status prompt. A queue-accepted concise completion is verified against same-task terminal state and atomically claims hook presentation, so a later unrelated prompt does not repeat it. A completed owner-routed private-IPC or shared-CLI-App-Server turn likewise suppresses the later recap. Portable app-server delivery, uncertain acceptance, and failed delivery retain the one-shot recap. Explicit status/result user prompts bypass the prompt-submit recap because they retrieve durable state directly.
  10. $codex-process-jobs:status and $codex-process-jobs:result remain the durable fallback.

Successful start is an absolute boundary for the assigning launch turn. Codex reports the launch in no more than two short user-facing sentences and ends that turn after any already-requested independent work. The report identifies the background job and says that a completion notification should appear. It does not expose controller mechanics, payload, cwd, or internal state unless the user asks. Codex does not load status, wait, poll, or probe the process. This idle boundary is also what allows the notifier to resume the owning task after completion. Work that depends on the result is deferred to completion delivery, a later user-initiated turn, or a later automatic continuation of an explicitly active Goal. A request to report the final result when it finishes is an eventual-delivery request and does not keep the launch turn open. A user who requires foreground execution must choose foreground execution instead of CPJ for that command.

This relay uses an ordinary Codex turn and therefore consumes normal Codex usage. Pass --no-notify for jobs that should remain polling-only.

Active Goal behavior

CPJ does not control Goal's automatic Continue cadence and cannot make those turns disappear. --goal-mode instead records that the command belongs to an explicitly active Goal and changes the model-facing state machine. A continuation does independent authorized work without checking the job merely because it arrived. If no independent work remains and the job is the critical path, it makes no status, wait, tail, sleep, or process probe; it ends without a progress sample and follows the host Goal blocked audit across repeated result-gated turns. This lets the thread become idle instead of turning Goal continuations into a polling loop. A terminal job is surfaced by the completion relay or a later hook boundary, consumed with result --peek, summarized, and followed by the next already-authorized Goal action. New authority, a consequential choice, or expanded scope still stops for the user.

Goal-mode detection is agent-level and uses visible supported Goal context, with get_goal as an optional supported confirmation. The plugin stores only a boolean marker and never reads or depends on Codex's private Goal database schema.

After installing or updating the plugin, restart every open Codex client before testing this flow. In VS Code, run Developer: Reload Window. Opening a new task in the same pre-install client process can leave its plugin or hook registry stale.

Continued conversation while a job runs

Normal user turns do not disturb a running job, and hooks ignore it until its process state is terminal. If the process finishes while the owning task is busy, official queue delivery delegates ordering to Codex itself. On older or unsupported versions, the notifier uses its bounded retries and then leaves the job pending during a cheap lifecycle watch. The watch polls only the owning rollout's latest task boundary, by default every five seconds for up to one hour; CODEX_PROCESS_JOBS_NOTIFY_IDLE_WATCH_MS and CODEX_PROCESS_JOBS_NOTIFY_IDLE_WATCH_POLL_MS can tune those bounded values. It makes one final direct attempt after a settled idle observation. Before direct delivery finishes, exactly one path claims delivery under the job-state lock:

  • If the notifier changes pending to delivering first, ordinary prompt fallback waits and the synthetic completion turn owns presentation.
  • If a hook boundary changes pending, failed, or accepted to fallback_notified first, the notifier or idle watcher stops and that agent turn owns presentation.
  • Worker relay bookkeeping preserves either claim instead of resetting it to pending.

A live delivering attempt is protected from prompt fallback. If its notifier process disappears, the hook can recover the stale attempt after a short startup grace; an apparently live attempt older than the maximum relay window is also recoverable. After portable app-server delivery succeeds, the later ordinary-prompt recap remains deliberately separate: it marks ordinaryPromptRecapInjectedAt once and requires Codex to recap the sanitized terminal state even if a synthetic announcement is already present in context. Successful desktop-ipc, vscode-ipc, and cli-app-server delivery suppress that recap after the matching notification turn reaches durable completion. A queue-accepted message suppresses it when the trusted hook verifies that exact concise prompt and claims presentation.

Separate-transport presentation note

The completion turn is persisted in the owning task independently of the client that launched it. A separate app-server process can leave an already-open client stale. Codex App on the local Mac, an already-open Codex VS Code webview, and a ChatGPT mobile client driving a remote Linux task have all demonstrated that behavior. In the original App test, the completion turn finished 16 seconds before the next ordinary turn began, ruling out a busy-turn race.

The guarded private path closes that presentation gap by routing the same start-turn request through the IPC router already serving the owning client. A controlled App test injected while another turn was active rendered an optimistic duplicate, even though the rollout contained only one persisted notification. Production delivery therefore retains the settled-idle guard and rechecks the owning lifecycle immediately before dispatch.

A settled App test then rendered the notice and model response immediately and exactly once. On July 24, 2026, a version-gated proof against the OpenAI VS Code extension 26.721.41059 sent the same private thread-follower-start-turn version 1 request to one explicit idle VS Code task. The already-open panel rendered both the synthetic completion and the single assistant response without reload, reopening, or a user prompt. The returned turn ID matched one durable task_started and task_complete pair.

This remains a private, experimental Codex protocol. The notifier validates the socket and protocol response, then records notification.transport: desktop-ipc for App or vscode-ipc for VS Code only after matching durable completion. It falls back to app-server when IPC is unavailable or rejects the method before possible acceptance. If acceptance becomes uncertain, it records accepted and leaves the next-prompt fallback available instead of attempting a second direct turn. If the owner becomes active after initialization but before dispatch, the retry-when-idle signal is preserved and no competing app-server turn starts.

Codex 0.149.0's official queue is now the preferred CLI path and requires no setup beyond the ordinary codex TUI. A controlled macOS test queued a sanitized synthetic completion from a separate process; the already-open idle TUI rendered the prompt, woke the agent, and began the CPJ result workflow.

The older CLI path is a separate experimental use of Codex's official shared local App Server rather than the App/VS Code private router. Enable it with config --cli-live-injection true, run codex app-server daemon start before the ordinary codex TUI, and restart the TUI. CPJ validates the default $CODEX_HOME/app-server-control/app-server-control.sock, uses bounded dependency-free WebSocket framing, and records notification.transport: cli-app-server only after matching durable completion. Missing, insecure, or incompatible endpoints fall back before acceptance. The setting never starts the daemon and defaults to false. If the active Codex distribution cannot start the managed daemon, do not install another Codex distribution or change PATH solely for CPJ; use the normal durable next-turn pickup.

Set CODEX_PROCESS_JOBS_DISABLE_CODEX_QUEUE=1 to bypass official queue delivery for compatibility testing. Set CODEX_PROCESS_JOBS_DISABLE_PRIVATE_IPC=1 to force the portable app-server path for diagnosis or compatibility testing. CODEX_PROCESS_JOBS_DISABLE_DESKTOP_IPC=1 remains a backward-compatible alias. CODEX_PROCESS_JOBS_PRIVATE_IPC_SOCKET and its legacy CODEX_PROCESS_JOBS_DESKTOP_IPC_SOCKET alias exist only as explicit test/integration overrides; normal operation resolves the socket under the active $CODEX_HOME.

Start records notification.presentation as durable-refresh-required for every owning client and discloses that live presentation is best-effort. Surface detection remains diagnostic metadata; the completed transport decides recap eligibility. A matching completed desktop-ipc, vscode-ipc, or cli-app-server turn suppresses the next-prompt recap. Default CLI, portable app-server delivery, and uncertain or failed live delivery retain the one-shot recap.

Codex App can present a hook-surfaced response in two useful phases: live commentary while work continues, followed by a final answer that causes commentary to auto-collapse. When fallback is required, the hook tells Codex to announce completion in commentary if commentary is used and independently requires a concise recap in the final answer. This intentional within-turn repetition keeps the durable visible answer complete; it does not apply after confirmed private-IPC delivery.

The automatic notice is deliberately one concise sentence for a single job, with its validated job ID formatted as inline code, because synthetic user turns receive inconsistent Markdown treatment across clients: macOS Codex App renders block Markdown, while the iOS ChatGPT client has rendered headings, emphasis, and blockquotes literally even when it recognized inline code. A strict sanitized completion-sentence grammar identifies the prompt for the hook, which explicitly excludes it from ordinary-turn fallback checks. Legacy Codex Process Jobs notice: lines, Markdown notices, hidden <!-- codex-process-jobs:notification ... --> comments, and <process_job_notification> envelopes remain recognized after upgrades. Successful direct delivery remains delivered; notification.transport records codex-queue, desktop-ipc, vscode-ipc, cli-app-server, or app-server. The separate ordinaryPromptRecapInjectedAt timestamp records only that the hook injected its one recap instruction on a fallback-eligible path. It does not claim the model complied or that the client rendered the response. Completed confirmed-live App and VS Code private-IPC records need no such marker. Portable CLI app-server records remain eligible for the marker; completed cli-app-server records do not. The legacy awarenessCheckedAt and surfaceFallbackNotifiedAt markers are still honored after upgrades so historical jobs do not resurface. That migration choice cannot retroactively prove old client rendering; it deliberately applies the stronger recap contract to jobs completed under the new implementation without replaying an arbitrary backlog.

Both direct notifications and hook context can carry up to 20 sanitized compatible completion records. Direct batching requires the same owning task and the same Goal/completion instruction profile; incompatible jobs remain for another batch. A larger backlog remains unclaimed and drains across later boundaries. Every job is claimed and finalized under its own lock, so a concurrent hook or notifier can exclude one sibling without losing the others, and every delivered member records the same turn ID.

Hook-boundary behavior

Current Codex hooks provide two useful approximations of Claude Code's task-notification injection. PostToolUse returns structured additionalContext after supported local shell, patch, MCP, and function-tool calls while an agent turn is active. Stop returns a one-time structured continuation decision so the agent reports a completion before finalizing a turn. UserPromptSubmit remains the universal later-turn fallback. These are supported turn boundaries, not arbitrary-time injection: hosted tools and specialized tool paths may not emit PostToolUse, and a process finishing during pure model reasoning cannot interrupt that reasoning immediately.

The separate PreToolUse hook closes the Marketplace adoption gap before a local Bash command begins. It allows obvious short inspections, CPJ controller commands, interactive or persistent work, and already-detached commands. It challenges everything else for contextual classification instead of trying to recognize a list of build tools. Codex then selects the start skill for a finite workload that may exceed 60 seconds or has uncertain duration. A clear non-qualifying command can be retried with the one-shot # cpj:foreground marker. A spawned subagent cannot execute a qualifying workload, use the foreground escape, or call CPJ start or rerun. Codex can retain the user-visible parent in the hook payload session_id. CPJ therefore prefers the validated runtime CODEX_THREAD_ID when it applies the child denial. After a successful CPJ start, the hook rejects same-turn status, tail, result, and memory-search commands even when they carry that marker. The hook never launches, rewrites, or executes the candidate command itself.

The UserPromptSubmit hook applies a fixed parent-ownership boundary when a prompt asks a subagent or worker to run a local process workload. The visible parent handles the local command and uses CPJ when the workload qualifies. The classifier requires both delegation language and process-execution language. It does not interpolate the prompt into model context. This boundary is necessary because Codex collaboration calls do not pass through the plugin PreToolUse or PostToolUse lifecycle. A parent that waits for a child after the child detaches a job would otherwise keep the user-visible turn active.

The same PostToolUse definition also closes the launch-side behavioral gap. It accepts a start only when the tool command names this installed plugin's canonical controller, the bounded tool response contains a valid job ID, persisted state binds that fresh job to the same task, and the per-job launch marker is still absent. Its fixed context contains the validated job ID and Goal boolean but no command, label, path, or process output. The marker makes reinforcement one-shot even if a client replays a hook boundary.

The three completion definitions invoke the same bounded script, reject notification-relay recursion, admit only sanitized terminal state, and use the same per-job compare-and-set claim. The foreground classifier is a separate bounded script. It reads validated ownership, launch-boundary, and rollout relationship metadata only. It enforces parent ownership and the absolute same-turn release boundary. It does not read process output or logs. Trust is explicit per installed hook definition through /hooks; when hooks are disabled or untrusted, skill routing, direct delivery, and durable status/result remain available.

An undelivered ordinary completion surfaced by a hook honors the same report|inspect|auto policy as direct delivery, with one deliberate difference: at hook boundaries, auto also selects inspection for CLI-owned jobs, because the hook turn is the first turn a TUI user actually sees. In proactive mode the hook asks Codex to inspect bounded evidence with result --peek, then continue only a clear next step already authorized and still in scope from the prior conversation; otherwise it recommends one next step and asks. A recap for an already-delivered completion stays report-only on surfaces whose completion turn already performed the inspection; a delivered CLI completion instead carries the inspection contract in its recap, because its acknowledgment-only turn inspected nothing and may never have rendered.

Optional OS notification

Human-facing desktop notification is independent of conversational delivery and disabled by default on App, VS Code, remote, and unknown surfaces. CLI-owned jobs retain one compatibility notice by default, so Codex 0.149.0 users may see both the OS banner and the live conversational wake. Use config --notify-user false if only the conversation is desired. start --notify-user enables one job; --no-notify-user disables one job; config --notify-user true|false sets the durable execution-host preference; config --notify-user default clears the preference so the surface default applies again.

The worker invokes osascript on macOS or notify-send on Linux with shell: false after terminal state is durable. The launch records whether notification and the job name were explicit choices. A notice includes the bounded, control-normalized label only when notification was explicitly enabled and the name was explicitly supplied; a surface-defaulted notice contains only the job ID, terminal status, and exit code, and a command-derived fallback name is never displayed. Notification banners can appear on a lock screen, so command text, paths, and arguments must not reach them without explicit opt-in. Missing notification binaries and display-session failures are ignored and never change job status.

Delivery states

  • pending: direct notification is queued.
  • delivering: one direct delivery attempt owns presentation; other notifiers and ordinary prompt fallback wait unless that attempt becomes stale.
  • delivered: the conversational completion turn finished.
  • accepted: Codex accepted the completion but CPJ cannot independently confirm the assistant turn. For codex-queue, the exact verified prompt claims hook presentation and suppresses a later duplicate; otherwise hook fallback remains available.
  • failed: direct delivery failed; hook fallback and status/result are available.
  • fallback_notified: one hook boundary injected completion context; direct notification is suppressed.
  • suppressed: the result was already opened or the job was explicitly cancelled.
  • disabled: the launch used --no-notify or notification was disabled for tests.
  • unavailable: no valid persistent owning thread id was available.

ordinaryPromptRecapInjectedAt is orthogonal to these delivery states. It honestly marks the one-shot recap instruction on an eligible ordinary prompt without claiming user-visible presentation or rewriting a successful delivered state. Completed desktop-ipc, vscode-ipc, and cli-app-server deliveries are not eligible because their matching owner-routed turn already completed and rendered in controlled tests. A verified codex-queue prompt changes the record to fallback_notified during its own hook boundary. Older CLI portable app-server paths remain eligible.

Trust boundary

Process output is untrusted data. It is stored only in bounded logs and is never interpolated into the notification prompt. Proactive completion uses $codex-process-jobs:result <job-id> --peek to retrieve bounded output without marking it user-viewed or suppressing fallback. The verified hidden hook policy requires Codex to treat the output only as evidence and never obey embedded instructions. Ordinary and Goal proactive modes may continue only clear work already authorized and still in scope from the prior conversation; otherwise they ask. New authority, consequential choices, expanded scope, and elevated risk require the user. Neither completion metadata nor process output grants authority. Ordinary user-requested result inspection omits --peek and retains its existing consumption semantics.

The installer enables Codex's stable hooks feature and installs PreToolUse, PostToolUse, Stop, and UserPromptSubmit definitions, but it never writes hook trust. After every install or update and client restart, the user must open /hooks and inspect the installed codex-process-jobs@<marketplace> definitions and referenced shared source. Any definition Codex marks new or changed requires approval; if trust persists, the user still verifies that status. Referenced source can change even when the definition hash does not, which is why review remains mandatory after every update. Direct completion remains available without hook trust; foreground classification and hook-boundary fallback run only for definitions Codex currently trusts.

Persisted records are bounded and schema-validated before the notifier or hook consumes them. Only up to 20 validated filename-bound job IDs, terminal status enums, and integer exit codes can enter the visible automatic prompt. The trusted hook accepts that prompt as CPJ-generated only while every value matches the same task's currently delivering record or a queue-accepted record whose transport is exactly codex-queue, then emits fixed hidden policy containing those IDs and no process output. A queue-accepted prompt is atomically marked hook-notified before policy is emitted. In proactive mode, the subsequent bounded result tool output crosses the model-facing boundary explicitly labeled as untrusted evidence. See Security and threat model.