Workflow & Automation
dsh-codex-workflow
kui123456789/dsh-codex-workflow · ★ 7
Coordinates Codex planning and independent review while the original DSH session implements and tests changes.
安装
dsh plugin --profile web add github:kui123456789/dsh-codex-workflowGitHub 源码安装:首次需按提示配置 allowBuilds 构建授权后重试
For install and environment setup, plus plugin development tutorials, see DSH community docs ↗
README
Table of contents
- Execution split (ephemeral review surface)
- Requirements
- Upgrading after a DSH update
- Install
- Build and verify
- Codex-led flow (preferred)
- Reviewer writer-lock semantics
- Model metadata follows its backend process
- Host compatibility (DSH 0.1.7 contract, introduced in 1.1.3)
- Transient upstream failures are retried (1.1.1)
DeepSeek Harness plugin that gives Codex read-only planning/review roles while DSH remains the sole executor. Two flows share the same workflow engine:
From a source checkout, run pnpm demo and open http://127.0.0.1:4175 for a standalone local visual demo. It uses simulated events to demonstrate planning, execution, review and same-thread recovery without connecting to DSH or a model. See the demo guide.
- Codex-led bridge (preferred) — the Codex task that produced the plan sends it to the exact live DSH session through a durable SQLite bridge; DSH implements it, and the plugin runs the review in private App Server ephemeral threads before returning the verdict to the original DSH session (the originating Codex task is never resumed or written; it receives the outcome through the bridge relay).
- DSH-led tools — the DSH agent can autonomously start
codex_workflow_startfor complex development tasks, or the user can request it explicitly; implementation and review still run through the same workflow engine.
No browser is opened or controlled anywhere in the product path; no network listener, MCP, hooks, or skills are involved. Browser clicking is a development-only workaround and is not part of the plugin.
Execution split (ephemeral review surface)
Planner and Reviewer turns run through the NPM Codex CLI's codex app-server --stdio, but plugin-created threads use thread/start { ephemeral: true }. They remain available for the current App Server process, do not receive Desktop names, and do not appear in thread/list. Failure recovery and clarification answers require the original process and handle. A lost original thread reports an error; neither path reconstructs a replacement Planner. Structured conversion and authority alignment continue to use codex exec --ephemeral or an equivalent private App Server turn.
Planner and Reviewer runs use DSH's subagents.start() on legacy hosts; on activation-only hosts such as 0.2.1-rc.3, this plugin owns the native one-shot mirror lifecycle. Each gets a native child Session (origin: "subagent") while the NPM Codex App Server executes the task in an ephemeral thread. The child Session receives native assistant stream frames and a final message. The browser opens DSH's built-in subagentchat right-sidebar resource for that child, so Codex appears as a child agent conversation. Workflow status and final review metadata use the codexWorkflow projection on legacy hosts or plugin-owned SQLite state on activation-only hosts. Private reasoning text and raw tool output are not forwarded. The plugin never opens or focuses Codex Desktop.
Concurrent SDK callers share one App Server initialization, with independent cancellation of each caller's wait. Cancelling one observer preserves the original process for observers still waiting; only the last withdrawal abandons initialization and cleans its original process. A later independent request waits for that cleanup before initializing again. Permanent plugin stop prevents another bootstrap. These initialization rules do not restore a lost ephemeral Planner or authorize a replacement workflow.
Each ordinary control RPC releases its pending entry, timer and abort listener once on response, error, timeout, cancellation or stop. Stdin stream errors belong to the original App Server child; a late error after stop cannot crash the host or invalidate a successor. Ordinary write backpressure keeps the request pending. Local write completion and stream cleanup do not confirm that a remote model turn has stopped.
Persistent-source conversion forks also retain their original thread/fork creation reply after caller abort or timeout. Repeating the same conversion waits for that reply and uses the original fork; changed conversion input and simultaneous consumers are rejected. No model starts before creation is confirmed. An abandoned public fork call releases only its own late subscription; a failed unsubscribe keeps its cleanup barrier until an actual ACK, with concurrent cleanup retries sharing one flight. Healthy explicit public fork calls remain independent. Default ephemeral Planner/Reviewer conversions run directly on their original threads and do not need a fork. These are process-local resource rules, not recovery after process loss. See the fork creation research.
Subscription cleanup requires a valid thread/unsubscribe response status: unsubscribed, notSubscribed or notLoaded. Missing, unknown or null responses preserve the original local owner and usage. An abandoned fork stays guarded until a valid original acknowledgement, so malformed replies cannot authorize another fork or source model turn. Subscription release does not prove that an active turn stopped. See the response validation research.
Completed persistent-source conversions retain their output separately when fork cleanup fails. The first successful result remains valid; a same-context retry cleans the original fork and returns a protected cached copy without repeating the model or its callbacks. Changed conversion inputs, owners or a lost original process cannot borrow that cache. Cleanup observers share one flight, and withdrawing one observer leaves the others intact. Pending resources prevent idle shutdown; explicit source unsubscribe may finish its own fork cleanup. External CLI handoff reports unconfirmed cleanup instead of closing an active or uncertain server. This process-local cache cannot reconstruct lost ephemeral threads. See the completed-task recovery research.
An accepted model start has a separate lifetime from its caller's wait. If turn/start or review/start has been submitted but its reply has not arrived, caller timeout or cancellation retains the original reply route, child, thread owner and stage. Status reports start_receipt_pending with model continuation unavailable. For DSH-led planning and foreground review, wait for the reply or use codex_workflow_resume only to inspect the retained operation. A genuine late reply lets resume reattach the original turn with fresh callbacks and no replacement model request. Bridge reviews use their original submission's background retry for the same adoption; they do not use foreground resume or require resubmitting unchanged material. start_receipt_invalid keeps an unusable reply guarded until original cleanup is confirmed. Cancel/stop release ownership only after actual completion, exact interruption ACK or verified exit of the captured original process instances. These rules never reconstruct a lost ephemeral thread.
Outstanding fork creation also retains its captured original process tree. SDK stop confirms those instances have exited, including a backend surviving launcher exit; signal delivery alone is insufficient. Safe termination uses the opened original Windows process handle with birth validation, or Linux pidfds through an existing Python 3.9+ runtime. Missing evidence or unavailable safe APIs keep cleanup unconfirmed. A later stop retries only that same cleanup without bootstrapping another server or model. Successful repeated stops retain their shared settled promise. See the process lifetime research.
Recoverable idle shutdown also confirms the captured original process tree before reporting external CLI handoff complete or allowing a successor. A launcher exit alone is insufficient. Failed retirement remains owned for cleanup of those same instances; a later explicit start first awaits that cleanup. Cancelling one caller only withdraws its wait, and permanent stop keeps its stopped latch through cleanup retries. These process rules do not recreate a lost ephemeral Planner or Reviewer. See the idle retirement research.
Rejected initialization keeps the same retirement responsibility. The original handshake error remains primary; auxiliary cleanup failure preserves its original process barrier. A later explicit startup waits for the rejected attempt's cleanup and settlement, then starts independently. It cannot rejoin the old failed initializer or bootstrap over an unknown lost-launcher tree. A genuine spawn failure without an OS PID does not invent a process record. See the initializer retirement research.
Permanent SDK stop also retains ordinary App Server backend ownership when no fork was created. It shares one original retirement census and close with initializer/idle cleanup, settles pending observers, and confirms captured original instances exited before returning success. Denied cleanup permits only another stop of those same instances; missing evidence remains unconfirmed and is never backfilled after launcher exit. The stopped client cannot bootstrap a successor. See the permanent-stop research.
Successful initialization records the original process identities before admitting work. An unexpected launcher exit retains that ownership: cleanup discovers later descendants only through surviving originals with matching births, confirms captured instances exited, then permits an explicitly requested successor. Unknown original identities or denied cleanup block this path; a reused PID cannot lend its descendants to the old cleanup. Identity collection is part of initialization and cannot be interrupted by the idle timer. A fresh metadata process still cannot restore an old ephemeral Planner/Reviewer. See the unexpected-exit research.
Closing or failing the original stdout invalidates RPC availability even if its OS process is still alive. Original observers receive the first output failure, pending model receipts and unconfirmed turns retain cleanup ownership, and further model/metadata calls cannot silently bootstrap another server. An ordinary close allows a separate 100ms exit-observation window so a real process exit keeps its exit code and stderr; explicit read errors are handled directly. Retired stream errors cannot invalidate a successor. Physical retirement still requires the original process-tree proof. See the output lifetime research.
On Windows, kill(pid, 0) can report EPERM even after an old PID disappears. For complete saved birth identities, a successful identity query that omits that PID triggers a separate bounded .NET process-absence check. Only a validated absence receipt confirms it; EPERM, omitted rows, denied handles and failed helpers alone remain unknown. Descendant refresh uses the same check and never invents missing original identities or signals a process during inspection. See the absence-proof research.
Original stdin and stdout failures share one first-cause transport guard. An initialize ACK does not publish readiness if stdin fails while original process identities are still being collected. Shared startup observers receive that same physical failure; cancelling one observer keeps its own cancellation reason without hiding the failure from another. After readiness, broken stdin invalidates model/RPC availability while retaining its process and exact active-turn cleanup responsibility. A later output error cannot replace the first input cause, and retired stream events cannot poison a successor. See the initialization transport research.
Node spawn can return a ChildProcess without stdio on EMFILE/ENFILE. The transport keeps that original child and skips readline construction until a real output exists; the SDK installs error/lifecycle listeners and waits for the original failure receipt before any RPC. That wait uses the existing control-RPC budget and caller cancellation. No-PID failures do not invent cleanup records; an actual child with incomplete stdio is retired using its original process evidence. A late failed receipt cannot invalidate an independent successor. See the missing-stdio research.
stderr is a diagnostic channel. A Readable error there is consumed while healthy stdin/stdout RPCs, initialization and the original thread remain usable. If that current process later exits, its diagnostic notes stderr diagnostic stream unavailable; the private stream-error contents are not appended. A retired stderr error cannot add that marker to a successor. Original process-tree retirement remains required. See the diagnostic-stream research.
Each original child decodes stderr incrementally as UTF-8, preserving Chinese characters and emoji split across OS chunks. Its diagnostic flushes observed bytes after its stderr ends or the original exit drain reaches its 100ms bound. The existing 16,384 UTF-16-unit tail budget remains; clipping drops a partial leading surrogate rather than showing half an emoji. Truly incomplete source bytes use Node's replacement-character semantics. Late retired data still cannot enter successor errors. See the UTF-8 diagnostic research.
The CLI audit dispatcher separately enforces its maxOutputBytes retention limit (default 64 KiB per stream): stdout overflow rejects the run, while stderr keeps only the newest complete UTF-8 code points within the limit. Both streams check final decoder output, including replacement characters for genuinely incomplete source bytes. This bounds retained diagnostics, not total process memory or OS pipe buffering; it preserves the existing CLI thread and error classification paths.
CLI stderr Readable errors are consumed separately from model output. Healthy stdout results still complete on the original process; an actual failed exit keeps its original classification and adds a fixed diagnostic-stream-unavailable note outside retained source bytes. Raw stream error messages cannot change model retry classification, and retired stderr errors cannot annotate a later process's failure. This does not repair a broken pipe or restore lost diagnostics.
CLI stdin/stdout errors affect prompt delivery or model output. The dispatcher consumes these events, preserves the first original error or prior cancellation, terminates that original child, and waits for its close and any admitted thread identity binding before rejecting. Synchronous prompt-write failures follow the same cleanup path. Output received after a pending failure cannot bind a new thread identity; retired stream errors are consumed without affecting a later independent process. This handles explicit stream errors, not silent pipe stalls or proof that all descendants have exited.
Caller cancellation remains observed while an already closed CLI process's original thread identity is still being saved. The dispatcher waits for that admitted binding to settle, then reports the first pending error or original cancellation and removes its abort listener. Process close does not end the binding's ownership; cancellation does not skip persistence or start a replacement. Model timers still stop at process close, so this is not a new hard deadline for a binding callback that never settles.
When a CLI caller waits for another run on the same Reviewer thread, its AbortSignal can cancel that wait before the owner completes. The queued caller creates no child and cannot kill the owner's process, cancel its binding or abort another waiter. Its own listener is removed on cancellation or admission; unrelated threads remain independent. This is caller-signal cancellation, not a new queue scheduler, fairness guarantee or rewrite of workflow/submission cancellation.
Known CLI thread IDs reserve admission synchronously within their dispatcher before waiting. At most one caller may start a CLI process on that thread; the reservation lasts through original process close and admitted identity binding. Cancelled reservations remain ordered behind their predecessor until it releases, so later callers cannot bypass a live owner. Synchronous spawn failure also releases the reservation, and different thread IDs remain independent. This does not serialize whole business review rounds, unknown new thread creation or other dispatcher processes.
CLI cancel(workflowId), cancelSubmission(workflowId, submissionId) and dispatcher stop find calls awaiting admission before they own a child. Each pending call has its own cancellation source combined with the caller's AbortSignal; the first reason wins. Matching queued calls reject without killing another owner or peer. Original active children retain their existing cancellation and binding cleanup; cancelled pending calls cannot start later.
Cancellation also remains registered throughout each CLI transient retry operation, including backoff after its previous child has exited. Matching workflow/submission cancellation aborts that wait and prevents another model process; unrelated submissions remain independent. Dispatcher stop waits for owned retry operations to unwind, and overlapping cancellation paths terminate each original child once. New explicit calls can reuse the same identity after cancellation. See the retry cancellation research.
After a captured original process tree is signaled, its exit-confirmation polling grace uses monotonic elapsed time. System clock changes cannot extend or shorten that grace. Cleanup still needs an independent original-instance exit proof; unavailable evidence remains unconfirmed. Native process queries and termination keep their separate bounds, so the polling grace is not a deadline for the entire teardown. See the cleanup clock research.
Before idle or permanent retirement closes its original native child, an unavailable process census permits one more read only while that same child remains live and owned by the SDK. Both attempts retain the census's existing 4-second bound. Repeated unavailability or a retired owner keeps the original cleanup unconfirmed; it never backfills missing old evidence after closure or starts a replacement backend. See the retirement census investigation.
Failed original retirement, fork or lost-process cleanup appends a compact census summary and exposes frozen cleanupDiagnostics: fixed observation codes, elapsed milliseconds, a numeric exit code and whitelisted errno/signals. Lost-process refresh distinguishes incomplete original/current identities, unavailable capture, a birth-domain mismatch and conflicting descendant identity. refreshed means the evidence was refreshed; independent whole-tree exit confirmation is still required. query_terminated does not alone prove timeout or exit. Raw errors, commands, environment, stdout and stderr are excluded. Diagnostics cannot authorize continuation or change the original cleanup barrier. See the census diagnostic record and lost-process refresh record.
Fork creation and conversion cleanup failures carry the same bounded census summary. The original fork barrier, child identity and no-replacement rule remain unchanged; only the diagnostic evidence is shared.
Lost-process cleanup retains its original startup census observations as a frozen prefix, followed by only the current refresh attempt. Repeated cleanup does not accumulate prior attempts. If the original launcher retires before startup capture returns, owner_retired records that observation while the returned process evidence remains PID-only; captured late identities are discarded. The same prefix appears on that startup retirement barrier. Cancellation alone is not evidence of owner retirement. See the startup census record.
An ephemeral Reviewer whose DSH binding callback temporarily fails remains retained on the same thread. The next request retries the binding callback before starting a model turn, so a transient local persistence error cannot spend a model request or create a replacement Reviewer. Durable/non-ephemeral binding failures keep their terminal cleanup behavior. See the Reviewer binding recovery record.
If cancellation, lease loss, stop, or original-thread loss makes that retention ineligible while the binding callback is still pending, the claimed Reviewer is released immediately. This prevents a leaked subscription or App Server pin when no recoverable record can be published.
Complete public CLI calls retain the same cancellation ownership across readable-output capture and result callbacks. An admitted callback finishes before the call or dispatcher stop settles; cancellation then blocks the next model stage and successful result delivery. Cached input also enters this scope before capture, so a pre-aborted request performs no capture or model work. Callback failures keep their original cause. See the pipeline cancellation research.
When an admitted CLI operation passes its cancelled signal to the next stage after dispatcher stop, it reports that signal's original reason. The retained signal must belong to the same workflow/submission still registered in this dispatcher. New stopped requests and displaced identities retain the stopped admission error; no cancelled handoff starts a model. Non-Error caller reasons retain the existing codex CLI audit aborted fallback. See the first-cause handoff research.
For redirected Windows verification logs, set $env:DSH_WORKFLOW_TEST_REPORTER = "tap" before pnpm verify or pnpm release:check. TAP includes ASCII summary lines such as # tests 1448 and # fail 0, which avoid console damage to the spec reporter's Unicode symbols. The setting accepts spec or tap; unset or empty uses Node's default. Explicit --test-reporter flags, including multiple reporters and their destinations, take precedence. Test selection, concurrency and exit codes are unchanged. Always inspect the actual exit code and completed summary; reporter output can vary across Node versions and is not a replacement for process completion evidence.
Shared Reviewer test fixtures accept the test's t.signal for their phase waits and register t.after cleanup. Cancellation leaves a held wait with the original reason; cleanup joins one disposal and stops the owned manager before waiting for its callbacks. The existing case limits remain unchanged, and a timed-out test remains cancelled. This prevents a failed local fixture from keeping a verification worker alive; it does not classify real provider failures or change workflow recovery. See the test cancellation research.
Manager lease heartbeats contain exceptions from SQLite renewal. A failed renewal leaves ownership unconfirmed, so that heartbeat stops and invokes its existing loss callback once. Active review cleanup follows the original workflow's cancellation path; no replacement model request is started. Successful renewal and unrelated leases remain independent. Explicit renew() and release() still report their own storage failures to callers. See the lease renewal failure research.
Shared CLI failure summaries retain the original head and tail within 2,048 UTF-8 bytes, including the omission marker. The marker counts removed source bytes, and truncation preserves complete Unicode code points. Workflow best-effort failures use the same rule with a 200-byte summary. When a custom budget cannot fit a marker, only a complete prefix is retained. Classification still uses the full original error. See the diagnostic byte-budget research.
Actual App Server exit immediately invalidates the original thread. Its bounded stderr tail can still receive buffered original bytes until stderr ends or a separate 100ms diagnostic deadline expires. An open inherited pipe reports stderr diagnostic stream did not finish before exit deadline; this deadline does not prove descendants exited. Successor admission and stop join the original failure settlement before cleanup. Existing RPC/turn timers stay armed, and retired bytes cannot enter successor errors. See the exit/stream research.
Validation failures in parsed input/turn frames are isolated from the host when they belong to one registered original turn. The first error is terminal: it interrupts that original pair, preserving its thread owner and cleanup handle until ACK or original-process exit. Failed ACKs wait for cleanup; resume sends no model continuation. Frames without a unique owned registration or pending request are consumed without selecting another turn or discarding pending receipts. Non-JSON noise and unknown notifications keep their existing compatibility behavior.
If a scoped protocol error arrives before the original turn/start or review/start reply, the SDK retains it against the unique original pending request. Only the actual reply can confirm its thread/turn pair, including a detached review's destination. The earliest matching error then enters the same terminal cleanup path, ahead of successful output; foreign hints cannot suppress it. Unscoped or ambiguous frames cannot select a pending writer. A reply that never arrives or cannot be validated keeps the original operation unconfirmed; message ids never authorize a fabricated cleanup handle. Caller cancellation preserves late-reply reconciliation rather than declaring the remote operation ended.
Unconfirmed cleanup captures original process identities once: Windows uses CIM creation time in canonical FILETIME units, and Linux uses boot ID plus /proc start ticks. Windows capture excludes a stale parent-PID link when the child's valid creation time predates the current parent instance; an unrelated older process cannot enter the original tree through a reused parent PID. Missing or incomparable birth metadata keeps the conservative capture rule. Later exit checks distinguish a new instance at a reused PID from the captured original. Missing identities or unavailable metadata keep cleanup unconfirmed; legacy PID lists can still confirm when every captured PID is absent. These checks release original resources and do not restore lost ephemeral threads. Other platforms retain the conservative legacy behavior.
Windows process-tree captures for one original backend run in sequence. A queued request checks that its evidence is still needed and the same native child is still owned before taking its own fresh snapshot; results are never reused across captures. The native query budget remains four seconds, while waiting behind another capture can add to caller latency. Different backends retain independent capture queues.
Windows CIM queries prefer PowerShell 7 when it is already installed at the standard Program Files location, and otherwise use the built-in Windows PowerShell. Both read the same Win32_Process.CreationDate identity source. The plugin does not install a shell or change PATH.
Requirements
- DeepSeek Harness
0.1.7-rc.2(the development and type-check baseline; the declared peer range also accepts0.2.0-rc.2,0.2.1-rc.2, and0.2.1-rc.3) - Node.js
^22.19.0or>=24 - Codex CLI with a valid ChatGPT login and App Server support (verified by
pnpm doctor)
Upgrading after a DSH update
The plugin is developed and type-checked against DSH 0.1.7-rc.2 with cordis 4.0.4; its declared peer range also accepts DSH 0.2.0-rc.2, 0.2.1-rc.2, and 0.2.1-rc.3. Run pnpm host:check against the operator's installed core packages (isolated temp storage, no live profile/session/model) before treating a host as verified; the check also asks the host's own evaluatePluginCompatibility to judge the declared range. Set DSH_CODEX_HOST_PACKAGE_JSON to the desktop application's bundled @deepseek-ai/dsh/package.json when the desktop and CLI installations differ. Use an isolated build when a live profile links to this checkout.
On activation-only hosts such as DSH 0.2.1-rc.3, sidebar state lives in the plugin's coord.sqlite and is read through the authenticated workflow-state RPC; no custom workflow events are appended to the host's Session log. Hosts with the legacy subagent API keep their existing projection path. Codex one-shot child conversations remain native Sessions, with this plugin owning their lifecycle on activation-only hosts; they do not start the parent model loop. Existing historical chat logs are never rewritten: legacy logs containing unsupported required event types need separate migration before the new host can reopen them.
Two things matter when the host moves:
- The host judges the peer range itself. At profile startup
dshchecks every@deepseek-ai/dsh*peer against the running version and asks for an exact-version exemption when one is unsatisfied (dsh plugin allow-version). Its documented semantics let prereleases participate in ranges, so a range is not judged by plain npm semver — that is why the gate reuses the host's evaluator instead of re-implementing it.pnpm doctorreports the same verdict (SKIPPED, never passed, when no DSH profile exists), sopnpm release:checkinherits it. - Source installs must refresh their own dependencies.
pnpm install(pluspnpm build) after a host upgrade; upgradingdshalone does not update a linked plugin'snode_modules, and the plugin'ssrcis type-checked against the versions pinned in its owndevDependencies.
Run pnpm host:check to load and unload the built plugin using the actual DSH
core packages installed under $DSH_HOME/profiles. It checks that all nine
tools exist when activation resolves and disappear on unload, using temporary
storage and an empty agent registry. It does not restart the live profile or
perform a model call. For another installation, set
DSH_CODEX_HOST_PACKAGE_JSON to its @deepseek-ai/dsh/package.json path.
After the checks, load the new build through the normal operator-controlled
DSH profile restart; existing workflow storage requires no migration.
Install
dsh plugin --profile web add dsh-codex-workflow
Restart the DSH web profile after installation. For source installs during development, pass the local project directory to the same command.
Build and verify
In-process transient retry budgets use a shared monotonic elapsed clock for control RPCs, readable Planner/Reviewer turns and CLI attempts. Clock corrections preserve the original budget; caller cancellation keeps its original cause, and exhausted retries keep their last actual outcome. See the clock regression tests and research evidence. The allowance is measured separately for each retry operation.
pnpm install
pnpm verify
pnpm host:check
pnpm host:recovery:check
pnpm doctor
pnpm test runs every matching suite with four independent test-file processes by default. Set DSH_WORKFLOW_TEST_CONCURRENCY to an integer from 1 to 4 when memory is limited; this also applies to verify and release:check. Concurrency inside each scenario and its timeouts remain unchanged. For a machine with more resources, an explicit command is pnpm exec tsx --test --test-concurrency=8 test/*.test.ts.
$env:DSH_WORKFLOW_TEST_CONCURRENCY = "2"
pnpm verify
The pre-1.1.0 manual harnesses (pnpm lifecycle:accept, pnpm review-only:accept) were REMOVED: they drove the legacy CLI-visible wiring, predated the dedicated-Reviewer and App-Server-visible layouts, and could no longer be trusted as acceptance evidence. The gates that replaced them are the automated ones above plus pnpm host:check and an independent review round.
pnpm host:recovery:check exercises the installed DSH ToolRuntime, AgentRegistry, SessionStore, SubagentRuntime, session projections and JSONL persistence in isolated storage. Eleven scripted local scenarios cover Planner/Reviewer failure recovery and cleanup, native and completed-question clarification answers/cancellation, a submitted native answer aborted before interruption confirmation, and ordinary Planner/Reviewer waits with unconfirmed interruption. They assert the original child/thread, retained settings, balanced pending tools and one-time cleanup. After child disposal, an independent persistence instance reads the original parent/child journals and checks their complete events, messages and catalog identity (persistedColdReplay: true). The script does not run a real App Server, call a real model or validate the browser UI.
The browser selects the latest review's native child in waiting and completed states, so the summary tab does not hide continuation messages after activeReview clears. test/reviewer-sidebar.test.ts covers that transition, completed-history replay, genuine new review rounds and the legacy childless fallback. See DSH same-session acceptance for the separate browser and real-provider verification boundaries.
The review summary labels paused, failed, cancelled and stale progress from the declared process state; existing history alone does not indicate completion. Cards count review attempts, including recovery attempts, rather than implying a new business review round. Planner clarification and failure states also have distinct headings. A paused heading does not establish continuation eligibility: query codex_workflow_status before resuming.
Planner events also carry an optional process state: a recoverable wait displays “规划已暂停”, same-thread continuation publishes running, and a ready plan publishes completed. Terminal failures awaiting cleanup retain their failure heading. The projection cache uses state version 2 to refold original logs; legacy logs without the optional field remain readable and do not invent a pause or a resumable thread.
While a sidebar projection is loading, the summary shows “正在加载工作流状态” using a stable immutable snapshot. Once the original store supplies data, its subscription renders that data; a loaded empty workflow still shows “等待审查”. This loading guard does not start agents or recover unavailable model threads.
Sidebar unloading retires its scanner before releasing listeners and retry timers. Already captured notifications cannot reopen a tab or subscribe to another session after unload; session switches release the previous projection observer once. test/sidebar-disposal.test.ts covers retirement, repeated disposal and normal live discovery. The adopted lifecycle mechanism and evidence are documented in the research note.
Sidebar auto-opening gives each original child or summary target one initial attempt and up to eight retries within its original 15-second retry window. After that allowance is exhausted, ordinary projection notifications keep it exhausted. A distinct target has a separate allowance; explicit Host sidebar actions remain available. See the sidebar retry tests and mechanism comparison.
For verification while DSH uses this checkout, run pnpm verify:isolated with development dependencies installed. It runs verify, release, installed-host and recovery checks in an independent snapshot, then checks the original Git diff. Evidence is retained in a sibling .codex-workflow-verification/isolated-* directory; summary.json, logs/ and artifacts.json identify the tested input and output. Original source or canonical build content/metadata drift rejects the result. Development pnpm build can update a linked host through HMR; isolated verification returns an artifact path for a separate controlled delivery.
Isolated test gates also retain logs/verify.tests.jsonl and logs/release-check.tests.jsonl from a companion Node reporter. Each receipt records its original run id, source directory, Node version, every test file's summary, the cumulative summary and stream completion. The gate requires its actual command exit code, matching file inventory and complete passing results; missing receipts, premature zero exits, skips/TODOs or async warnings reject it. Human reporter flags and their ordered destinations remain available. Receipt files are created exclusively, and independent descendant launchers cannot write to their parent's scoped receipt.
Codex-led flow (preferred)
From the Codex task that owns the plan, dispatch it to the live DSH session:
# 1. Find the live DSH session for this workspace
dsh-codex-workflow sessions --cwd $PWD --json
# 2. Dispatch the plan (payload enters through stdin, never arguments)
$payload = '{"task":"实现搜索功能","planMarkdown":"<proposed_plan>…</proposed_plan>","assumptions":[]}'
$payload | dsh-codex-workflow dispatch --cwd $PWD --codex-thread $env:CODEX_THREAD_ID --stdin
The bridge resolves the exact session (explicit --dsh-session wins; otherwise the cwd must match exactly one live session, and ambiguity fails loudly). DSH receives the plan as a plugin relay message and implements it. When done, DSH calls codex_workflow_submit; the plugin validates the exact stored Codex task id read-only, then runs the readable audit in a fresh private Reviewer thread. The originating task is never resumed and never written:
Planner/source Codex task --App Server--> readable plan
private ephemeral Reviewer thread --App Server thread/start + review/start--> readable Markdown review/rewrite
private ephemeral conversion --codex exec --ephemeral --output-schema--> normalization/alignment JSON
Planner and Reviewer threads created by 1.1.7 are ephemeral. Each Planner or Reviewer starts with thread/start { ephemeral: true }, is used by the current NPM Codex CLI App Server process, and is never named or persisted as a new plannerThreadId/reviewerThreadId. Ephemeral ids do not appear in thread/list, so Codex Desktop does not load them or hold their writer locks. A new review round creates a fresh Reviewer; failure recovery within one round keeps its original Reviewer. Legacy ids remain readable for diagnostics only; they are not resumed, renamed or handed off. codex_workflow_resume rejects a lost original process instead of reconstructing its context. The visible turn receives the configured <reviewerModel> and <reviewerEffort>; a blank model uses the resolved default.
The DSH session history is human-readable. Visible Planner/Reviewer turns never carry an outputSchema: each review is readable Markdown (VERDICT / FINDINGS with severity, blocking and file:line / TEST GAPS / SUMMARY) in the original task's language. Private conversion uses the same effective model with low effort: Planner and default foreground Reviewer conversion run on their original ephemeral App Server thread; authority checks use private seeds or the explicitly selected non-persistent CLI transport. These operations never create a durable Desktop task or persist internal ids into plannerThreadId, reviewerThreadId, or codexThreadId.
Every review turn carries its own full context — Git included. DSH-led, bridge callback, first review and re-review all reuse the existing review prompt generator. The review/start custom target (and the App Server turn on the callback path) carries workflow identity, original task, approved plan, PREVIOUS APPLIED REVIEW, current fix summary, implementation summary, changed files, test results, bounded evidence, review scope, and the item-by-item authority gate. Git workspaces require independent read-only git status/git diff; non-Git workspaces use the same contract without inventing a different Reviewer policy.
Review authority alignment (1.0.10). After the visible review is normalized into the structured verdict, an invisible private conversion turn (same read-only conversion machinery, low effort; in-place for ephemeral sources) checks every finding/test gap against an authority hierarchy: 1. a REPRODUCIBLE critical/high correctness/security/data-corruption defect (must carry concrete file:line + failing-scenario evidence) > 2. the ORIGINAL TASK and its explicit constraints (file scope, exact test counts, dependency limits, acceptance method) > 3. the APPROVED PLAN > 4. the previously applied findings and the current fix summary > 5. generic quality suggestions. Ordinary scope/test-count/verification-method conflicts resolve in the plan's favor: automated tests, STATIC CHECKS and REAL COMMAND verification are all formal evidence of a requirement — the Reviewer may no longer demand automated tests for behavior the task/plan verifies by real commands, and may not demand changes that exceed the task/plan's explicit bounds (a level-1 exception with reproducible evidence is the only override). The Planner contract mirrors this: it must never strengthen "tests cover A and B" into "exactly two tests" unless the user explicitly limited the count.
The visible review is contract-checked before it reaches DSH. The readable Markdown is streamed into a native DSH child Session and the browser opens its subagentchat right-sidebar resource. The owning parent Session still receives namespaced dsh-codex-workflow/* workflow events for status and recovery. A structured ReviewResult envelope is never accepted as visible output; a display violation runs one corrective private turn. Nothing on the completion path calls the Desktop opener or any navigation/refresh API.
Planner and Reviewer model instructions identify the current DSH child Session and Codex Review right sidebar as the readable output destination. The foreground native review target, Reviewer developer instructions and Bridge callback carry this same display contract. The hidden App Server thread is the execution transport; its readable output remains Markdown in the task's language, while structured conversion stays in its separate internal stage.
The browser client retries a failed native-sidebar open for a bounded window and never falls back to the custom Codex Review tab while a child session exists. When no child session is available, it uses the public sidebarRight.openTab page API for the diagnostic fallback. Closing an already opened tab is treated as a user choice and does not repeatedly reopen that same child during the current page lifetime; a refresh or a later child session creates a new opening opportunity.
Each new review round has its own native child session, so multiple rounds may leave multiple subagent tabs in the right-sidebar history. A recoverable model failure stays inside the current child session and current App Server Reviewer thread; it does not create another child or replay the work in a fresh thread. The plugin does not close older tabs because the public sidebar API does not expose a reliable content-address lookup or the tab id returned by openResource.
Conflicts never cost the user a review cycle and never ask DSH to change code. A conflict does not overwrite latestReview, increment reviewCycles, enter fixing, or send a fix instruction. One reconciliation turn may rewrite the complete verdict on the same ephemeral Reviewer; its prompt contains an explicit preservation manifest for every non-conflicting finding/test gap. Deterministic multiset checks reject deletion, field changes, duplicate-count drift and unrelated additions before re-alignment. A successful correction applies as one business cycle; two consecutive unresolved contract conflicts block without consuming a cycle.
Reviewer writer-lock semantics
Before a new App Server review round, the plugin uses a newly created ephemeral Reviewer already held by that App Server client. If the current model turn fails with a recoverable run error, the same Reviewer is continued once and can remain waiting for codex_workflow_resume; it is never replaced during that round. The plugin never resumes a legacy Desktop task, never names the ephemeral thread and never invokes the Desktop opener. Legacy opener fields remain compatible but audit records keep desktopOpenState: "disabled".
Desktop cannot lock a new 1.1.7 review. Because plugin-created Planner and Reviewer threads are ephemeral and omitted from thread/list, opening an old Desktop task cannot block a new review. Control RPC failures remain bounded and retryable, but no retry resumes or reuses a legacy Desktop task.
Ephemeral threads are released with the App Server process lifecycle. Cancellation and teardown interrupt the exact active turn or private conversion turn before the client stops; no Desktop task is reopened or refreshed.
Model metadata follows its backend process
The model directory, default model and catalog-derived reasoning effort are cached for the current App Server process. Repeated reads in that process keep using its cache. Concurrent catalog observers in one generation share one metadata RPC, while each caller keeps its own cancellation boundary. Public model-list results are detached snapshots, so a caller cannot mutate the generation's cached model choices. After an actual process exit or idle shutdown, a metadata request prepares the new server and reads its own directory; new independent work uses that server's defaults. Explicit model/effort overrides retain priority. Cache reset invalidates the old namespace, and checks after asynchronous catalog/default/Plan-mode lookups prevent late old results from repopulating the new cache.
Before opening an existing coordination database for schema migration, the store creates a .pre-schema-v2.bak copy. A failed backup now stops initialization and preserves the original database for manual recovery; the error is not downgraded into a best-effort warning.
An admitted Planner/Reviewer keeps its saved model, effort and original thread owner through continuation. Refreshing metadata does not retarget that run to a new process or create another workflow/child. Initialization must succeed before metadata or thread operations are admitted: a rejected or malformed initialize response preserves its primary cause and cleans only its failed original child. A later independent SDK request can retry initialization; this does not reconstruct an interrupted Planner. See the model catalog iteration and fixed cache research.
Host compatibility (DSH 0.1.7 contract, introduced in 1.1.3)
DSH 0.1.7 replaced the shared catch-all plugin message source with a merge-extensible sum type: MessageSourceMap is augmented by each producer in its own module, and MessageSource.kind answers who produced this while form answers what kind of thing it is (the two axes are deliberately independent). There is no plugin kind to borrow any more.
The plugin therefore declares its own kind in src/message-source.ts — exactly the way first-party producers such as dsh-agent-instructions do — and builds every injected message from it (relaySource() for a plan handoff / verdict / outcome, noticeSource(summary) for a one-off notice). A cast would have compiled, but it would have hidden the contract from the compiler and left the harness unable to attribute this plugin's context to a declared producer.
Transient upstream failures are retried (1.1.1)
Capacity and throttling problems are temporary, so they never end a workflow any more. One shared policy (src/transient-retry.ts) classifies server_overloaded / "Selected model is at capacity" / 429 / usage limit / 502-504 / dropped
…