Skip to content

fix(dsh-runtime): emit a failed result frame when model selection derivation throws - #7082

Open
mrzhangkris wants to merge 2 commits into
nexu-io:mainfrom
mrzhangkris:fix/dsh-runtime-silent-exit-on-bad-selection
Open

fix(dsh-runtime): emit a failed result frame when model selection derivation throws#7082
mrzhangkris wants to merge 2 commits into
nexu-io:mainfrom
mrzhangkris:fix/dsh-runtime-silent-exit-on-bad-selection

Conversation

@mrzhangkris

Copy link
Copy Markdown
Contributor

Why

Found while auditing @open-design/dsh-runtime v0.1.0 (the DeepSeek Harness runtime plugin this app ships). The pain: model selection derivation (currentSelection() / ReasoningEffortId() / SessionId()) runs outside execute's error path. A throw there — e.g. the default-model service is unavailable, or a host-supplied value is rejected — rejects the task promise with no result frame at all; serve then exits the process silently (exit 0) and the host waits on a response that never comes. I reproduced the silent exit against a real dsh --profile open-design --stdio session before fixing: only the ready frame was emitted, then the process vanished.

What users will see

Nothing in the app UI. Protocol robustness: an execute whose model selection cannot be derived now receives an explicit failed result frame (DSH_PROFILE_INVALID_MODEL_SELECTION) instead of a silently exiting stdio process, so Open Design (and any other host) always gets exactly one result per execute — the protocol contract in the plugin README.

Surface area

  • UI
  • Keyboard shortcut
  • CLI / env var
  • API / contract — no wire-shape change: a new error.code value (DSH_PROFILE_INVALID_MODEL_SELECTION) may appear on already-failed result frames.
  • Extension point
  • i18n keys
  • New top-level dependency
  • Default behavior change
  • None — internal robustness fix plus tests.

Screenshots

N/A.

Bug fix verification

  • Test path that reproduces the bug: packages/dsh-runtime/tests/protocol.test.ts → "emits a failed result frame when model selection derivation throws (no silent exit)" (mocks agentDefaultModel.currentSelection() to throw and asserts the execute resolves with exactly one failed result frame).
  • Did the test go red on main and green on this branch? Yes. On main the test fails (the throw rejects the task promise before any frame is written); on this branch all 13 tests pass.
  • Also reproduced live on main behavior via dsh --profile open-design --stdio before the fix: ready frame only, then silent exit. After the fix the same host-supplied invalid value returns an explicit failed result frame.
  • pnpm --filter @open-design/dsh-runtime typecheck passes.

laomo added 2 commits August 18, 2026 21:33
Non-browser HTTP clients can forge Sec-Fetch-Site trivially, so a
no-Origin request carrying 'Sec-Fetch-Site: same-origin' must not widen
the host check to the full OD_ALLOWED_ORIGINS allow-list on the
non-loopback path. Only a loopback/private-LAN host (or an explicitly
configured IP-literal origin) may omit the Origin header; reverse-proxy
deployments with a hostname in OD_ALLOWED_ORIGINS must send Origin or
use bearer auth instead.

Fixes nexu-io#7041
…ivation throws

Model selection (provider/id → resolved selection) was derived before
the execute error path, so a throw there (e.g. default-model service
unavailable, or a rejected host-supplied value) rejected the task
promise with no result frame at all; serve then exited the process
silently and the host waited on a result that never arrived. Derive
inside a guarded block that emits an explicit failed frame, and make the
serve loop convert any stray rejection into a failed frame as defense in
depth so the 'exactly one result per execute' protocol contract holds.
@lefarcen
lefarcen requested a review from nettee August 18, 2026 13:41
@lefarcen lefarcen added size/M PR changes 100-300 lines risk/high High risk: apps/desktop, daemon, auth, migration, workflows, package deps type/bugfix Bug fix needs-validation Runtime change detected; needs human or /explore agent validation. labels Aug 18, 2026
@lefarcen

Copy link
Copy Markdown
Contributor

Queued for QA validation — this touches runtime behavior, so we'll want a manual QA pass before merge.

Also, it looks like you already have #7081 open with the same origin-validation change that's included here. If that daemon fix is meant to land with this runtime work, worth consolidating so the review scope stays clear.

One small PR-body follow-up: could you add a short Validation section summarizing what you ran? The bug-fix verification notes are already useful; this would just make the final checklist explicit.

@nettee nettee left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed the DSH runtime error framing and daemon origin hardening. Focused validation passed: pnpm guard, workspace pnpm typecheck, DSH runtime tests/typecheck, and daemon origin tests/typecheck. I found one protocol-blocking cleanup path and one cancellation edge case; both are detailed inline below.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

// unhandled rejection (which would make the process exit silently and
// leave the host waiting on a result that never arrives).
void task
.catch((error: unknown) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Blocking: this fallback unconditionally synthesizes another terminal result whenever execute rejects, but execute can reject after it has already written its terminal result: the outer try writes the result before the finally, where disposeEvent() and cleanup still run. A disposer/cleanup error therefore produces two result frames (the original terminal frame plus this fallback). The DSH protocol requires exactly one terminal result, and the daemon marks any frame after finished as fatal, so a successful or failed run can be turned into a fatal protocol error. Track a terminal-frame-written state shared with execute, or make cleanup errors non-rejecting, and emit this fallback only when no result was written; add a serve-level cleanup-error test that asserts one result.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

? { ...baseSelection, reasoningEffort: ReasoningEffortId(request.reasoning_effort) }
: baseSelection;
} catch (error: unknown) {
writeFrame(output, {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Non-blocking: this new selection-error branch bypasses cancellation semantics. serve aborts taskAbort as soon as it handles a cancel, and the existing execute catches explicitly check signal.aborted before emitting a terminal frame. If currentSelection() throws while that signal is already aborted (for example, a settings/default-model service is unavailable during an immediate cancel), this branch emits DSH_PROFILE_INVALID_MODEL_SELECTION instead of cancelled, so a user cancellation is reported as a failed run. Check signal.aborted before writing this failed frame and add a serve-level execute-plus-cancel regression test.

🔁 Powered by Looper · runner=reviewer · agent=codex · An autonomous AI dev team for your GitHub repos.

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

Labels

needs-validation Runtime change detected; needs human or /explore agent validation. risk/high High risk: apps/desktop, daemon, auth, migration, workflows, package deps size/M PR changes 100-300 lines type/bugfix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants