LLMSkillHub
mirroredCopied from BotHarness/DeepSeekBot under MIT. BotHarness has not claimed this page, and is not affiliated with LLM SkillHub.

botharness / dsh-dev

Use when developing, running, or debugging BotHarness against a local DeepSeek Harness instance — the dev loop (profile install, build, restart, HMR), DSH_HOME/profile management, the web `/api` transport contract, plugin endpoint wiring, and diagnosing 404/connection/白屏 failures ("works in tests, broken in DSH"). Trigger on dsh profile, web-dev, dev server, bridge RPC, /api 404, plugin not loading, HMR, settings/模型/插件页报错.

0
SKILL.md
52.9 KB

DSH development & debugging

BotHarness is a plugin layer inside DSH: unit tests do not cover the real boot. Every trap in this skill was invisible to pnpm test and only appeared in a running dsh --profile web-dev. Validate against the running shell, not the test suite.

Proactive Client verification

After every isolated launch, Client reload and relevant UI action, read the real browser console and DOM yourself before accepting the result; do not wait for Human-copied errors. Read [the Client diagnostic loop](../../../docs/dev/guides/client-startup-diagnostics.md) for the agent-runnable reader, bounded attempt history, background-tab/freshness limits and native RC1 reproductions. Treat API health, Client shell evidence and browser-control failures as separate signals. Preserve first failure and each retry; recovery by reload does not establish a fix.

Ordinary localhost pages do not open the application-defined development-refresh SSE. Add ?botharness-dev-reload=1 only while iterating on Client builds, then remove it for first-run or long-request UX verification.

Prime directive: /api belongs to the API gateway

The web client calls /api/<endpoint> over plain HTTP (fetch + auth cookie). The route and its single interceptor are owned by @deepseek-ai/dsh-api-gateway, which claims endpoints from the typert registry (all native controllers register there).

  • Never call connection.rpc.intercept('/api', …) from a plugin. The slot is taken; a second interceptor shadows every native API (settings/describe, llm/listProviders, plugins, directoryPicker/list …) with a plain-text 404 not found while your own endpoints keep working.
  • Expose plugin endpoints through the gateway/typert claim path: a TypertRemoteService subclass with a typertRemote binding and remote-method markers (SRC, no codegen). botharness/* migrated this way in #50 (packages/core/src/bridge/rpc.ts); native @deepseek-ai/dsh-api-* controllers are the reference.
  • Wire contract: POST /api/<namespace>/<method> with { type: "client-request", rpcId, method, payload: { args: {…named args} } }; undefined fields are rejected by the decoder. Failures come back as a server-response envelope (ok:false), not HTTP errors.

Dev loop (WSL, fnm node)

Use the worktree's local @deepseek-ai/dsh@0.2.0-rc.2, not a global dsh binary. The launcher checks the installed version, creates an isolated Web Profile, links all Bundle members as dependencies, enables only the deepseekbot umbrella, and probes the authenticated API.

corepack pnpm build
node scripts/dev-instance.mjs --home /tmp/bh-020-web --port 31967 --json
corepack pnpm dev:client

The launch summary includes the Host PID and local login URL. For Client edits, open the local Web tab with ?botharness-dev-reload=1: tsdown rebuilds the linked Client bundle, and the opted-in development listener refreshes the page when the RC publishes a rebuilt frame. The Desktop Client HMR stream replaces the linked @botharness/ui Fiber; BotHarness hands off the active Bot/Channel selection during replacement. The Web development listener retains a full-page refresh fallback. For Host edits, build, stop that exact PID, then start the helper again with the same home and port; Host hot replacement is disabled in the pinned RC. Follow docs/client-bridge.md §7 for the complete steps and measured timings.

The helper injects a shared machine-local DeepSeek key when available; node scripts/dev-secret.mjs check reports its source without a value. Existing Profile credentials are another DSH source. Confirm model access with a real DM reply. node scripts/dev-secret.mjs adopt-profile --home <existing-DSH_HOME> copies only the DeepSeek reference into a private local file for later isolated Profiles.

Profile model

  • Isolated DSH_HOME (one directory per worktree/port); profiles live in $DSH_HOME/profiles/<name>: package.json (dsh.profile.bundles), cordis.yml, user patch layer cordis.patch.yml, node_modules (our bundles via link:).
  • dsh web … initializes the web template (dsh-base + dsh-web-app); a bare dsh --profile <new> initializes the minimal template (dsh-base only). Our web-dev = web template + deepseekbot.
  • Always pass DSH_HOME — without it commands silently target ~/.dsh and initialize profiles there.
  • cordis.patch.yml is the bisect tool: disable a plugin (- id: botharness-core / disabled: true) or override config, no reinstall needed.

Pitfall log

When a DSH-side bug or trap is diagnosed, record it here (or in the playbook) in the same change. Entries:

#TrapSymptomCause / fix
1/api single interceptorNative APIs 404 not found; custom plugin endpoint worksResolved in #50: api-gateway owns the slot and claims endpoints from the typert registry; botharness/* now registers a TypertRemoteService (SRC markers). Do not reintroduce connection.rpc.intercept('/api', …).
2Half-booted instanceUI loads, every API 404restart raced/lost plugin layer; restart again and verify with the cookie probe (playbook §1)
3pkill -f "<pattern>" self-matchcommand dies with no outputthe wrapper's own cmdline contains the pattern; use [x] trick (pkill -f "[n]o-open")
4Shell variables eaten in nested wrappers$VAR/$(…) come out emptywrite a script file for anything with variables; avoid inline $ in loop commands
5DSH_HOME omittedprofiles/installs land in ~/.dshalways set DSH_HOME for every dsh command
6Standard decorators not emittablevitest SyntaxError on raw @Remote; tsdown passes decorators through untouchedthis toolchain (oxc/tsdown) can't compile TC39 decorators — write the @deepseek-ai/dsh-typert-protocol/remote-methods descriptor manually (version: 1, invocation: { kind: "direct" }) and lock its shape with a test (#50)
7bAgent created without an agent preset gets an empty tool layerA BotHarness Orchestrator/Assignment Agent can only call the tools its own setup registered (e.g. channel_send, report_to_orchestrator) — no file/bash/shell tools, so the model reports "no tools"dsh-agent-presets: every session must join a preset (CreateAgentOptions.meta.agentPreset); an agent published without one "resolves against the empty global layer". Pass the bot's preset or a shipped default (standard) when creating agents
7sidebar.panellist row has no toggle or geometry seamClicking the active entry re-runs selectPanel(id) (a no-op), so the mode never exits; the shell row spans the full content box, 2px wider per side than the 新会话 rowOwn the row from inside the glyph: render a wrapper span, target the shell button with button:has(.bh-panel-glyph) (width: auto; margin-inline: 2px = native .newSession insets), and while active render an inset: 0 hit layer whose onClickCapture stops React propagation and calls ctx.layout.selectPanel(null) (layout must be declared in the plugin's inject)
8Menu autoFocus + portal drops the initial focusThe menu renders but no row is focused (arrow keys dead, submenu closed)The primitive's autoFocus effect runs before its placement re-render clears the list's visibility: hidden, so .focus() on the hidden row is ignored. Re-focus the placed list in a setTimeout(0) after mount (see ChannelMoveMenu)
9Layout shift inside dragstart kills the native dragdragstart fires, then a bare dragend with no drop; puppeteer's mouse.drag() hangs waiting for dragInterceptedChromium aborts the gesture when the dragstart dispatch synchronously moves the source row (e.g. revealing a display: none drop zone via React's discrete-event flush). Arm layout-taking drop surfaces one task later (setTimeout(0)); absolutely-positioned markers are safe synchronously. Diagnosed on the empty-section drop zone (#56 follow-up)
10Bundle member promoted to a top-level bundleBoot fails with duplicate loader entry id: botharness-coreThe deepseekbot umbrella patch already inserts core, client, browser and computer. Link all five packages into the profile, but keep only deepseekbot under dsh.profile.bundles; do not pass any member through dsh plugin add.
11dsh.client.inject is bundle grouping, not service orderingClient bundle fails to import: bundle … loaded without registering "<pkg>" via __ModuleLoader__.load, often preceded by duplicate factory registration for a native moduleListing another plugin's package in dsh.client.inject makes the client-modules loader bundle that package again in your entry's group; a module that already registered cannot register twice. Keep inject: [] for cross-package service dependencies and express them only with ctx.inject(['<service>']) at runtime. Diagnosed in #164.
12Typert descriptor survives Host HMRA client using a newly added RPC field gets args fields do not match the descriptor: unexpected "<field>", although the linked Host build contains that fieldRemote method descriptors are registered at boot and are not replaced by Host HMR. After changing a TypertRemoteService method's parameter shape, build and restart DSH, then verify the endpoint with a no-write API probe.
13Connection Fetch route registered without its /api prefixAn authenticated GET to the stream returns generic not found even though the plugin loads and unary RPC worksconnection.fetch.register({ path }) matches the full pathname. Register /api/botharness/stream, not /botharness/stream; verify the exact route in a running Host. This is distinct from and does not replace the API gateway's RPC interceptor (#141).
14ctx.uiWorkspace.pickDirectory() throws on a deployment without a pickerTypeError: Cannot read properties of undefined (reading 'pick') at call time, although ctx.inject(['uiWorkspace']) succeeded and the workspace service answersThe ui-workspace client service is provided even when no directory-picker pair is mounted (the web-app bundle's -auto chooser mounts one asynchronously; a profile can also end up with none). Treat the picker as optional: probe it, catch the failure, and offer a manual path fallback instead of a dead button. Diagnosed in #168.

| 13b | One-shot login token needs two visits | hand-rolled curl/fetch against /api gets 401, then 404 not found, although the server is healthy | The first visit arms the login, the second returns the session cookie. The launch token remains valid for that Host process; the helper also saves a private cookie jar for automated probes. Use the login URL in a browser or the jar for authenticated API calls. |
| 14b | Driving the bridge by hand | plugin endpoints look absent (404 not found) while native /api/settings/describe answers 200 | The bridge namespace is botharness (the short n in an early comment is stale): POST /api/botharness/<method> with method: "botharness/<method>". Probe native and plugin endpoints separately before blaming the plugin layer. |
| 15 | setsid on macOS | a manual restart script dies with command not found: setsid | This checkout also runs on darwin. Use scripts/dev-instance.mjs (detached spawn) or a ( nohup … & ) subshell; never a bare &. |
| 16 | Streaming Fetch route also registered for GET | POST file upload commits, but an authenticated GET for the file returns an empty HTTP 400 | In pinned DSH connection's HTTP bridge, requestBody: 'streaming' constructs a Fetch Request with a body for every method; Fetch rejects a GET body before the handler runs. Register distinct exact paths: a streaming POST upload route and a buffered GET download route. Rebuild and restart the Host after changing route registration, then verify both methods in a real browser (#146). |
| 17 | No process-injected dev key is mistaken for no model access | dev-instance.mjs reported “model calls will fail,” yet a PersonaBot DM received a real streamed DeepSeek reply | dsh-credentials-local resolves inherited environment before $DSH_HOME/.credentials.yaml, then project/home .env. The AX helper now distinguishes a shared injected key from a profile credential. Confirm the profile source without printing values, then prove usability with a real DM → model → committed Channel reply. |
| 18 | Runtime skill without explicit source lists but never loads | Skill appears in <available_skills>; every model skill({name}) fails Error: loaded skill "…" source must be a string | Pinned dsh-skill register() defaults provider (→ "runtime") but not source, while validateDefinition on the get() path requires both as strings. Always pass explicit source (e.g. 'runtime') and provider in ctx.skills.register(). Diagnosed live from an exported session zip (session.v3.jsonl tool/result) in #248. |
| 19 | Newer layout facet assumed present crashes client boot | Failed to load plugins@botharness/ui: can't access property "subscribe", ctx.layout.panelInfo is undefined; whole browser UI dead | panelInfo exists in DSH 0.1.7-rc.2 but the composed layout service in some profiles lacks the facet. Guard with a panelInfoOf helper: skip view-persist/restore when absent (degraded boot beats dead boot). Contract test boots apply with a panelInfo-less layout stub. Diagnosed live in #357. |

| 19b | Global DSH CLI shadows the worktree's RC2 dependency | The Profile serves 0.1.5 Web assets while the plugin was built against 0.1.7; Bot mode crashes with React's invalid element error because runtime icon exports differ | Launch through scripts/dev-instance.mjs, which runs and version-checks the target worktree's local CLI. Verify the browser's served Web asset and Profile before diagnosing UI data. |
| 20 | RC2 Web fallback for BotHarness Client rebuilt frames | pnpm dev:client rebuilds in ~0.1 s and /plugins/events broadcasts the new revision, but the open Bot mode still shows the old tree | Open the local Web tab. BotHarness's development listener performs a full page refresh on its own rebuilt frame; reopen Bot mode after refresh. |
| 21 | Windows staged SQLite migration enters recovery | A fresh official Desktop profile can show Operational database is in recovery mode: migration-failed while adding a Workspace/Session; the same fresh owner test fails on Windows | Node on Windows returns EPERM for fsyncSync on a file opened 'r' and for directory fsync. The owner now opens the staged database 'r+' before syncing and skips unsupported directory sync on win32. Reproduce with the fresh mountOperationalDatabase test plus a real Desktop session. |
| 22 | RC2 Client Modules strips a package name ending in /client; Desktop document reload after HMR can use a stale graph | @botharness/client shows prefetch("@botharness/client") — not a graph entry in Built-in Plugins. Renaming the unpublished package to @botharness/ui enables native linked-bundle HMR and the DM updates in place. After a later full document reload, Web boot may fail with @botharness/ui: import failed | Upstream stripClientSuffix() treats the package suffix as an export subpath. With the corrected ID, a fresh Desktop document can still receive the old boot revision after HMR: old bundle URL 404, current graph URL 200. The plugin cannot recover before import. Keep Client development on native HMR, avoid full document reload after a rebuild, and cold-restart app plus Host after stopping the watcher when boot fails. Preserve the plugin/Profile; inspect %APPDATA%\@deepseek-ai\dsh-desktop\logs\crash-*-web-boot.log. The packaged Desktop may omit the development-only Reload Page menu. See docs/client-bridge.md §7 and ADR-0066. |
| 23 | New BotHarness Orchestrator tool omitted from the internal Messaging allowlist | The model calls list_bot_contacts, but the turn parks on a Human tool-approval card before the Bot DM workflow starts | Add Host-checked internal Messaging tools to BOT_TOOL_NAMES in packages/core/src/workspaces/grant-execution.ts; keep their sender and Channel membership checks in the owning Host command. Verify with a real model turn, not only an adapter fixture (#279). |
| 24 | Fresh WSL Web Profile cannot create DSH's default Workspace | Native DSH shows “Unable to create default workspace” even when the Bot Client is healthy | xdg-user-dir DOCUMENTS can return $HOME; DSH rejects Home as its Documents directory. For an isolated QA Profile, set workspace-controller config.documentsDirectory to an existing Documents directory in cordis.patch.yml, restart that Host, and verify a native Workspace appears. This is DSH first-use setup, not a Bot Channel failure. |
| 25 | Durable Bot Inbox recovery races DSH Agent Loop factory registration on cold start | An unobserved Assignment notice is replayed at BotHarness construction, then the Channel shows no agent factory registered (load an agent-loop plugin) even though a later DM works | ctx.agents may exist before dsh-agent-loop calls setFactory(). Treat that specific pre-execution failure as transient: leave the Inbox Admission retryable, retry with a bounded delay, and avoid publishing a premature Session failure card. Verify a pending notice across a real Host restart. |
| 26 | DSH 0.2.0 UI timing and localization break English-only e2e selectors | Browser e2e times out at .bh-root, the create-dialog name input, or the send button although the same flow passed on 0.1.7 RC2 | The first-run notice button is localized (继续), menu items render a frame after their trigger, and the BotHarness composer swaps its textarea for the rich [role="textbox"] once mention candidates load. In e2e scripts: dismiss with ['Continue', '继续'], wait for [role="menuitem"]/dialog selectors before clicking, and target the rich textbox after the composer settles. Diagnosed in #419/#420. |
| 27 | Windows isolated dev launcher cannot spawn pnpm and can time out before DSH serves | spawnSync pnpm ENOENT under fnm PowerShell, or no token URL after 45000ms while the Host later becomes healthy | Windows may expose only pnpm.ps1, which Node cannot spawn directly; resolve an installed Corepack shim on PATH and invoke its JS through the current Node executable. When Corepack is absent, invoke the installed pnpm JS or executable. Store log/cookie files under tmpdir() and allow a longer token wait for Windows startup. A missing model key can also leave a DM turn running; verify Memory Inbox startup admission independently of a real model reply. |
| 28 | Windows Git subprocess stalls the DSH Host | Memory checkpoint requests take 77–169 seconds and unrelated Channel/API loads time out; a git.exe child stays alive with near-zero CPU. A standalone Git invocation against the same isolated repo takes milliseconds | Node's synchronous Git launcher in the Windows Host can attach a console process. Set windowsHide: true on every Memory execFileSync('git', …) path. After the fix, the same checkpoint read took about 1.4 seconds, and changed-state captures took a few seconds. Keep separate DSH_HOME values for concurrent QA instances so their SQLite leases and Git repositories remain isolated. |
| 29 | WSL shared key is invisible to a Windows-launched instance | An isolated Host reports MISSING_CREDENTIAL despite a key in WSL ~/.config/botharness/dev.env | The AX helper resolves homedir() in its own OS. In the Windows terminal, use node scripts/dev-secret.mjs adopt-env --from <WSL-dev.env-path> once, then check and restart that Host. The helper creates only the Windows user's private shared file and never prints the key; verify with a real DM reply. |
| 30 | Agent-scoped native Subagent tools cannot be replaced or globally restricted by name | Registering another subagent in the Bot Agent scope fails with tool "subagent" is already registered in this scope; tools.restrict({ deny: ['subagent'] }) says the name is not a known global tool | The pinned DSH preset registers native Subagent tools in Agent scope. Register Bot-specific names in that scope, guard native execution, and call DSH Subagent Service after checking the current Bot plan. A child Session uses DSH's native never approval override; its inherited Workspace Grant, mode, and cwd still need Host validation (#508). |
| 31 | Session Remote namespace injected without its parent | Client boot fails before Bot mode appears after adding ctx.remote.session | Declare both remote and remote.session in the Client Plugin dependency list, as the native open-in-app Plugin does. A namespace declaration alone does not establish the parent Remote Proxy. Verify the real shell boot after a Host restart; unit Context stubs must model this capability too (#574). |
| 32 | Native token-usage projection mistaken for an independently reported total | E2E compares Profile total to an absent tokenUsage.totals.totalTokens and fails despite matching provider buckets | DSH 0.2.0 RC1 projection totals contain four disjoint token buckets, not an independent total. Compare each known bucket; sum them only when all four are known. For failed or retried settlements, use the preceding durable request/header or request/context route rather than a later successful Assistant source (#503). |
| 33 | Bot settings navigation stops matching its decorated button | The native settings dialog opens, but the Bot settings shortcut cannot select its section after the Bot icon is installed | The first-party icon adds a third child to the original two-child nav cell. Exclude only that owned icon when resolving the label; do not require the decorated button to keep its original child count or assume its first span is text. Verify repeated settings opening in the real shell (#621). Decoration must be idempotent: compare the requested markup with a per-element cache, because serialized innerHTML also contains the injected media styles and otherwise an observer can continually rewrite its own DOM. |
| 34 | Isolated Provider registered but native Consumer activated in the same pending Fiber | Native tool-fs says its confining filesystem has no sandboxPolicy although provide ran; preset-only service lookup may also miss Host-provided fs/Shell | Cordis get exposes a Provider only after its Fiber activates. Await the isolated Provider Fiber, then mount native Consumers as child Plugins and await their activation. Resolve agentPresets.serviceFor first and fall back to public Host capabilities when inherited. Native tools capture Policy at registration, so isolation alone does not replace existing closures. Wrap the actual tools/execute body for per-call context: tools/pre-execute returns before body dispatch. Verified against pinned DSH 0.2.0 RC1 with actual native reads, writes and approved Shell outside Session cwd (#632). |
| 51 | Packaged Profile install refuses ignored build scripts | pnpm install in the Profile fails ERR_PNPM_IGNORED_BUILDS for agent-browser once Browser joins the product | pnpm 12 requires explicit build approval per workspace. The packaged Profile installs the same tree the repo workspace adjudicated, so packagedWorkspaceSettings carries the root allowBuilds map (explicit Profile entries keep precedence; a drift test locks the copy to pnpm-workspace.yaml). Verified with a real --product-artifacts install into an isolated home (#1328). |

| 28b | Rebooting an isolated Profile drops its optional IM Bundle | dsh-im settings and transport disappear after running the AX helper again | The helper used to replace all dependencies and bundles. It now retains non-BotHarness dependencies and extra Bundle entries while refreshing its own linked packages. Keep the same task-owned home and verify native plus BotHarness APIs after restart. |

| 33 | Core Host types change Client Context augmentation order | Adding a shared Core DTO type import makes Client ctx.sessions.list() resolve to a Host function requiring arguments | Keep the Client session-controller type import before shared Core DTO imports in client/actions.ts; verify pnpm typecheck and the running native Client. Host and Client Context augmentations share a module name; DTO imports can change declaration order (#12). |

| 34 | Optional Provider Service is published before its account registry is ready | The persisted receive grant is present after restart, but BotHarness shows reception unavailable; enabling again works | Service availability alone is not account readiness. Restore the same explicit grant with a bounded retry only for transient provider-unavailable; retain its token, revision and group scope, cancel on disposal, and verify automatic intake with a real Host restart (#12). |

| 35 | Native approval requested from an idle Agent probe | A direct ctx.tools.execute probe returns Human approval is unavailable and no DM card appears | Pinned DSH 0.2.0 RC1 approval.request requires an open Turn to enclose approval/asked and approval/decided. Exercise first approval with a real model Turn, then use native probes for already-authorized operation guards. Verify pending approval cancellation with another real Turn; a completed Agent is insufficient (#460). |
| 36 | SRC Remote signature omits a newly supported named argument | Profile Save fails with gateway/arguments-invalid: unexpected "delivery", although internal bridge-method tests pass | The Gateway builds its named-argument descriptor from the public Remote method signature. Add and forward the argument in that method, not only the owning implementation; verify through /api and cover the public adapter plus durable owner (#528). |
| 37 | A saved Bot model preset differs from the resumed native Session request | QA Profile selects Pro/off but the next request/header still records Flash/high in an existing Orchestrator Session | Verify the actual request/header, not only the Profile preset or composer label. In the isolated DSH 0.2.0 RC1 QA Profile, explicitly selecting Pro/off in the native Session chooser emitted model/selection; the next request used Pro/off and completed, including after Host restart. The cause of the preset/resumed-Session mismatch remains unconfirmed; preserve original failures and do not silently rewrite the Session or add an IM-specific model workaround (#613). |

| 38 | Native configForms scope methods lose their receiver in React | Settings crashes inside getSnapshot/subscribe despite a ready native scope | Pinned DSH 0.2.0 RC1 scope methods use their instance. Pass stable useMemo closures that call scope.getSnapshot() and scope.subscribe(listener), rather than unbound methods, to useSyncExternalStore. Verify native volatile persistence after Host restart and cover a class-based scope fixture (#726). |

| 39 | WebServer prefix registration includes a trailing slash | Extension POST/OPTIONS falls through to generic 405 even though the Plugin and authenticated API work | Pinned DSH 0.2.0 RC1 WebServer matches pathname === prefix or pathname.startsWith(prefix + '/'). Register /my-plugin/extension, then call /my-plugin/extension/pair. Check preflight on the running raw HTTP route. Connection fetch requests carry transport URLs, so enforce actual network peer checks at the WebServer adapter rather than interpreting Request.url as the bind address (#741). |

| 40 | A native Tool approval does not prove Shell execution | A real approved Orchestrator bash call can still return Path is outside this Session's authorized workspace; a subsequent escalation request is refused | Treat native approval and execution as separate evidence. Inspect the actual persisted Tool result and Session cwd/authorized Workspace, including macOS path aliases. Do not relax guards or infer a successful command from the working Projection alone. In #751's pinned DSH 0.2.0 RC1 QA, the granted Assignment path executed while another owned Session retained a native approval; the Orchestrator refusal's underlying workspace mismatch remains unconfirmed. |

| 41 | Nested native Menus compete for Escape and focus return | Escape closes only the inner menu and focusing its trigger reopens the hovered submenu, leaving the sidebar settings menu visible | In pinned DSH 0.2.0 RC1, each Menu owns document key listeners and consumes Escape before returning focus. For coordinated sidebar settings, use one Menu for dismissal and keyboard walking, with MenuSurface and MenuItemButton for nested cards. Verify Escape after repeated selections in the installed shell, including overlay disclosure restoration (#807). |

| 42 | Retrying a timed-out isolated launch leaves two Hosts for one home | API health responds, but Bot commands fail with lease-unavailable | A prior launch can leave its detached Host alive after a token wait fails. Before retrying, inspect that exact task home/port and its botharness.writer.json PID; stop only the task-owned Hosts, then cold-launch once. An authenticated API health probe alone does not prove the operational database writer lease is ready. Observed in #811 QA. |

| 43 | A third-party tour shares Escape and focus with a native Modal | Escape dismisses both the tour and the native settings; closing the tour returns focus behind a newly opened Modal | Pinned DSH 0.2.0 RC1 native Modal handles Escape on document bubble and captures its invoking control. Give the tour capture-phase Escape ownership, remove its listener on dismissal, and retarget the actual visible native dialog after asynchronous opening. Driver.js 1.4.0 restores old focus after onDestroyed, so restore focus to the foreground dialog in a microtask after that callback. Verify the actual packaged Client with keyboard navigation and keep reduced-motion behavior covered (#824). |

| 44 | Reused QA Session retains its frozen application role prompt | A newly accepted Memory edit is on disk, but the real model still follows the earlier QA no-reply instruction after Host restart | A persisted Session can retain application-defined role instructions for its lifetime. Verify the actual system/message and Tool results, not only the file or handled Inbox state. For a new QA behavior, start a new application-owned Session through its normal lifecycle; never rewrite Session events or loosen authority. In #905, the first real voice transcript was received but the existing Session deliberately made no external reply. The replacement QA Bot uses the same paired account only after the earlier receiver is stopped. |
| 45 | Self-update through the Plugin Manager loses its own answer | Bot settings' Update now installs the newer deepseekbot (profile package.json and node_modules change), but the click never shows a result and the Web page reloads into the API-key prompt; DSH's own Plugins page refuses the same spec as already-installed | Pinned RC1 pluginManager.installBundle('deepseekbot@<v>') upgrades in place and returns restart-required, but the profile HMR reloads the Web document after pnpm, so the in-flight bridge reply is lost and the reloaded page serves the new Client against the old Host. Never rely on the RPC reply: derive the pending restart from listBundles()' deepseekbot version versus the version snapshotted at module load, and verify with a real old-version profile, a click, the reload, and a restart (#986). |
| 46 | A plugin cannot restart DSH through any RC API | dsh web and Desktop expose no restart; appExit only stops the tree, and the launcher has no supervisor to start it again | Restart from the plugin by keeping the process alive: call ctx.appExit(0), and on beforeExit (8 s fallback) spawn process.execPath with the same execArgv and argv (plus --no-open for web) as a non-detached child with inherited stdio. The terminal keeps the session and Ctrl+C reaches both through the process group. The signed browser cookie survives, so the Client polls releaseInfo until a new version answers and reloads. Desktop is unverified, so its Client shows quit-and-reopen instead (#986). |

| 47 | PersonaBot creation passes a BotHarness model-preset ID as the DSH Agent preset | First native Orchestrator Turn fails with Unknown agent preset before any Tool executes | PersonaBot preset selects a DSH Agent tool preset (for example standard), not a BotHarness model preset. Apply the latter with modelPresetApply and verify the actual model request independently. Correct the QA configuration through normal Bot update, preserve the failed Turn and use a fresh authorized event; do not treat model-preset selection as native Tool availability (#906). |

| 48 | Native Go adapter forwards Session identity without its required request header | A correctly configured DSH 0.2.0 RC1 Go request fails with HTTP 400 MissingSessionID | Native llm-pi-ai passes GenerateOptions.sessionId to the SDK but omits x-opencode-session. The reviewed pnpm patch supplies the actual Session ID only for Go, strips case-insensitive static collisions, and preserves native attribution and other Provider headers. The AX helper carries this pinned patch into an independent CLI per opted-in home. Never use a fixed/random substitute header; retain the failed Turn and verify a fresh real model → channel_send → committed DM reply. Requalify before changing DSH version or removing the patch (#1079; [guide](../../../docs/agents/ax-model.md)). |

| 49 | Windows Modify permission is insufficient for native workspace-write provisioning | Approved native Shell calls fail before launch with SetNamedSecurityInfoW failed (Win32 5): grantWrite(...) | The RC1 Windows ACL backend needs effective WRITE_DAC and WRITE_OWNER; being the directory owner with Modify can still lack the latter. Use the installed diagnose-windows-sandbox-acl skill to inspect effective access. With explicit Human approval for exact QA directories, use its backed-up scoped repair, then verify the original confined Shell operation. Preserve owner, deny rules and sandbox policy; follow its restore-and-stop path if verification fails. In #911, real native Orchestrator and Assignment pwsh 30-second waits both succeeded after the approved repairs of their respective QA directories. |

| 50 | Local browser permission display is mistaken for successful tool access | A listed allowed origin still returns ERRBLOCKEDBY_CLIENT, or DOM reads work while screenshots/input time out | Qualify exact-origin authorization, listener ownership, Host transport, navigation and actual capture separately. Preserve each result and stop bounded retries without new evidence; see the [AX browser runbook](../../../docs/agents/ax-browser.md) (#1225). |

| 51 | A new BotHarness-owned Tool is registered but omitted from the execution policy | The real Orchestrator pauses on a Human approval card for an own-Inbox read, although the Tool contract test passes | grant-execution.ts classifies checked application-owned Tools through BOT_TOOL_NAMES; registration alone does not exempt a name from generic operation approval. Add only the Host-scoped Tool whose own authority checks are implemented, cover requiresHumanToolApproval, then build/restart the exact owned Host and verify a fresh real model call. Preserve the first approval card rather than approving it to hide the omission (#1278). |

Reference

In pinned RC1, defineTool nested object properties mark requiredness with per-property required: true; an explicit JSON Schema required array is rejected by the authoring DSL. Native bash output is a structured foreground/background/promoted union, so an interception must preserve the registered result schema instead of returning a string as value. #1380 verified both failures through a real PersonaBot; tool-registration/schema and real runtime checks cover the correction.

Native MCP stdio uses SDK-owned spawn, not the Shell/Subprocess Provider. Its automatic protocol negotiation starts a disposable probe sibling before the live server: a fixed Docker container name conflicts, while a per-owner label lets teardown remove every sibling. Native dsh-scope owns a module-local Symbol and WeakMaps; bundling another copy makes native Agent Contexts appear unscoped to the copy. Keep Scope external and resolve the same pinned peer as the Host; verify two real Agents concurrently, not only a raw-source single-Agent test. On Windows the preset's Shell Provider may be PowerShell even behind the bash Consumer name; launch a trusted executable with Subprocess argv instead of assuming POSIX Host syntax. The pinned Node image must actually include the Guest shell. These were diagnosed in #1380.

Native Tool Registry schemas(scope) and get(name, scope) include inherited global and ancestor registrations. A name prefix does not establish an execution boundary; MCP server-name reservations are scoped too. For controlled installation, snapshot pre-install identities and exclude identities visible globally or in every ancestor from the post-install qualification. Verify inherited Host sentinels, including those registered during asynchronous startup, are denied while concurrent Agent-owned Guest tools execute. Use public scopeParentOf/scopeChainOf rather than private Registry internals. This was reproduced against RC1's native Registry and real Docker in #1380.

Pinned DSH 0.2.0 RC1 selects one directory picker interaction at boot. A loopback Windows Host commonly serves native; uiWorkspace.listDirectory() then throws DirectoryBrowseError with rpcError.code = directory-picker/unavailable. A consumer may fall back to uiWorkspace.pickDirectory() only for this capability refusal; unreadable directories and other failures remain errors, and cancellation returns no selection. Authorization still follows an explicit Human choice.

For isolated browser E2E on Windows, pin the browse pair in that task-owned Profile overlay: disable the directory-picker auto entry, then use a Patch insert list to compose @deepseek-ai/dsh-host-directory-picker-browse and @deepseek-ai/dsh-client-ui-directory-picker-browse. A bare new id/name patch does not insert a Plugin, and changing the name on an existing patch does not swap its implementation. Stop and restart that exact Host, retaining loopback bind and one backend. Verify the running folder-selection path. See the upstream picker contract.

For multi-window headless checks, set the intended viewport on each new page before entering the UI. A fresh page defaults to a narrow viewport, where the DSH sidebar can collapse to icons; a selector waiting for visible Bot-mode text then times out despite a loaded, authenticated shell. Use independent browser contexts with the authorized local session when validating stale replies, and submit through the visible Client action.

DSH 0.2.0 provider token reports use disjoint uncached input, output, cache-read and cache-write buckets. Optional cache fields may be absent even when an exact total is reported: absence is unknown, not zero. deriveTurnTokenUsage may return no result when exact lifecycle or usage evidence is incomplete; never substitute token-meter context estimates for observed provider consumption. Read the durable Assistant settlement's actual source route rather than a configured model. Verified with the installed token-meter contracts and a real model request in #499.

Pre-execution refusal can leave an application-owned Session ID without a DSH Session. Before retrying that reserved identity, use Session Persistence stat(id) to distinguish an absent stored Session from an existing one: create only when stat returns undefined, and propagate storage failures. Do not infer persistent existence from the live Session Store or blindly resume a reserved ID. Verified against DSH 0.2.0-rc.1 in #500 with a rejected model route followed by a successful Human repair.

  • DSH source checkout: reference/deepseek-harness (gitignored, in this worktree) pinned to the installed tag — currently dsh-v0.2.0-rc.2. Read the TypeScript source (packages/typert, packages/*connection*, packages/*gateway*, apps/cli/reference/README.md) when the how/why matters; the installed lib/*.js is bundled output. Re-pin when DSH is bumped: git fetch --depth 1 origin tag <tag> && git checkout <tag>.
  • references/debugging-playbook.md — boot verification, status-code semantics, WS mux probe, bisect recipes, headless puppeteer probe.
  • references/probe-web.mjs — headless browser probe (console errors, failed requests, WS, internal fetch); run from the repo.
  • dsh-ui skill — in-harness UI rules; docs/client-bridge.md §7 — dev loop; ADR-0023 / #50 — bridge transport contract.

Additional verified pitfall — #1425

An exact DSH peer in any composed Plugin can reject installation before its code is imported.
The build pin is not the supported Host floor. Product declarations use >=0.2.0-0 (1.2.x releases declared >=0.1.7-0; the floor moved in #1511) with native
prerelease semantics and no future-version ceiling; preflight acceptance is not runtime qualification.
Check Core and the product-managed Provider together. Preserve each release train's immutable
Provider source and runtime digest, and qualify its own packages rather than borrowing a maintenance
release's E2E result. Do not use compatibility exemptions as release evidence (#1425).

Additional verified pitfall — #1138

In #1459, two real composer tabs showed repeated recovery checks; the store regression independently reproduced redundant durable writes for unchanged recovered text. Publishing a new identical version makes peer tabs request validation again. A no-op validation must retain the durable version, while removal of an invalid reference is persisted once. Keep transient validated reference metadata in the local snapshot without publishing a new unchanged record; verify both idle synchronization and explicit conflict selection against the rebuilt Host. A passing HTTP API probe alone does not qualify a browser whose request slots are occupied.

For a fresh pinned RC1 local Profile, let native initialization create the Profile directory before applying a task Patch. Precreating the directory makes the native default-profile initializer refuse its destination; initialization also creates an empty cordis.patch.yml, so merge intended entries instead of moving another file over it. Preserve native entries, then restart the helper's exact verified Host PID. This was reproduced in #1381.

The pinned native local Credentials Provider retains record objects in its cache. A trusted private-input owner that clears its incoming submission object must copy the values into the atomic grant payload before handing it to the Provider; otherwise clearing the request also clears the cached grant. Validate the published Host through an actual credential consumer, not only disk serialization. A Fiber's disposal must also drain already-started recovery writes before its storage Providers can close. These native ownership and lifecycle interactions were verified in #1381; the request settlement policy remains application-defined.

A new root overlay must budget its long-lived Connection Fetch streams with existing pages. In official DSH 0.2.0 RC1 Windows Web, adding a separate companion SSE alongside the Bot-mode Activity, Channel and roster consumers left real DM sending pending until Bot mode exited. Reusing the companion's full Activity snapshot for the shared Client Activity store and closing the redundant Activity SSE restored three genuine DM submissions within Bot mode. The symptom is consistent with the per-origin HTTP/1 connection limit; an exact wire connection count was not measured. Validate an actual DM reply while both the overlay and owning page remain visible. On removal, restore ordinary Activity demand; keep listener/frame/heartbeat disposal bounded and never weaken authentication to work around the symptom.

Additional verified pitfalls — #1339

Browser-bundled dependencies can defer process.env.NODE_ENV reads until a control first renders. In #1377, the RC1 Client booted and jsdom interactions passed, but opening the new table crashed the native main Slot with ReferenceError: process is not defined. Define the production environment constant at browser build time, retain native React/primitive baseline externals, and verify the built Bundle contains no runtime Node environment read plus the actual dialog in the running shell. Do not add a global process shim or count a successful initial boot as proof of the interaction.

On native Windows 11 with DSH 0.2.0 RC1, a real pinned Chrome cold download produced Host progress while the panel's observation took about 81 seconds and showed only Opening. Separate authenticated observations returned bytes immediately. Making the duplicate localhost development-refresh SSE opt-in restored panel percent/MB and 4–5ms observation responses during another real cold download. Preserve ordinary Bot-mode streams and native admission; do not add another progress SSE or treat successful Host polling as Client evidence.

Pinned Chrome 154.0.8037.57 can expose DevTools before its first window exists on a fresh Windows browser profile. Explicit Target.createTarget(newWindow: false) then refuses with Failed to open new tab - no browser is open (a CDP response, not stderr). Omit newWindow for background tabs so Chrome selects an existing window or creates the first one, retaining background: true and focus: false; verify with the real fallback E2E. A task-owned binary marked Zone.Identifier / ZoneId=3 spawned successfully, while moving it immediately before native spawn produced ENOENT / errno -4058 and the existing startup-spawn-failed cause/action panel. These observations do not justify a new Windows stderr kind or a claim that every antivirus behaves the same.

Provider configuration must be checked against the composed production Controller, not only its bare class. In #1111, dsh-im workspace decoration attached asynchronous catalogs to registrationStatus(): the native QR had been generated, but setup reported setup-failed when it read a Promise as status. Await the public Controller result and exercise the actual wrapper plus async catalogs through the public setup transport. Verify generation/poll/cancel again in the packaged Host; this does not qualify a new platform pairing.
A native credential write may commit before its response fails. Application-defined owners must recognize success only by canonical readback of their newly generated opaque revision and exact target grant, rather than treating any current record as success. In #1382, injected post-commit failures proved that unconditionally failing could leave local revocation committed without attempting a running-use stop. Account activation must precede configured Channel publication, with both proofs required at consumption. Startup recovery must finish before Owner mutation APIs admit new grants, otherwise cleanup can mistake an in-flight replacement for an orphan. These behaviors were verified against the pinned RC1 local Credentials Provider; application account lifecycle policy stays outside DSH Context.

Qualify a passive model-request probe against the actual selected adapter. In #1383, a DeepSeek-only URL filter would miss the native Go route; the probe now recognizes the observed /messages, /chat/completions and /responses paths and records whether the body was available. Clone a Request only when no body override exists; non-string overrides are incomplete evidence, not a clean scan of the original body. Windows NODE_OPTIONS=--import requires a file:/// URL, and a synthetic canary file belongs outside DSH_HOME so storage scans do not mistake test instrumentation for leakage. The probe preserves fetch payloads and authentication headers and never records either; unsupported routes or encodings remain unqualified. Verified against pinned RC1 Go requests and isolated body-override regressions.

In pinned RC1, an exported native createMcpToolDefinition input schema with additionalProperties: false did not itself reject extra arguments at the adapter entry. Enforce the admitted parameter set in a trusted Consumer before resolving confidential values or invoking a private transport. Verify a real native Tool call with an extra parameter, not only schema inspection. The SDK stdio Client also merges selected default environment names when env is supplied; explicitly override inherited names when a fixed child must exclude ambient credentials. Both behaviors were verified in #1384 with the installed native adapter and SDK.

Cross-platform Profile migration needs a separate path-metadata audit. In #1386, the qualified dsh-im Provider returned feishu-startup-config-invalid and no accounts because its copied integrations/dsh-feishu/workspaces.json contained an absolute Windows workspace that is invalid on Linux. The native management connection.status surfaced the startup error; translating only the audited source-home workspace field and restarting the exact task-owned Host restored the same account and sole receiver. Native Session header cwd/project keys and application ownership metadata also need bounded translation; preserve immutable Session Event frames and the native credential store. A Linux bind-mounted workspace additionally needs the guest UID's POSIX access: Docker Desktop success does not qualify Host umask, directory traversal or file-write behavior. See the [Linux qualification guide](../../../docs/dev/guides/private-input-linux.md).

Additional verified pitfalls — #1490

Application-defined long-lived SSE responses share the browser's HTTP/1.1 per-origin budget (about six) with native HMR. With six held, every ordinary request in every same-origin document timed out while an independent authenticated request still answered at once; the identity, model and message loading states are consequences, not a rejected token. Count long-lived responses per document (HMR included) before adding one, test with one more document than you expect to support, and carry application streams as stream-mode Remote methods on the Gateway's multiplexed socket (ADR-0181). In-flight EventSource requests do not appear in Resource Timing; read the network list instead.

A source-declared stream method needs more than a unary bridge method. Host: mark it mode: 'stream' and name its final parameter signal. Client: ctx.remote.$mount a contribution whose descriptor repeats namespace, method, mode, parameters and cancellation; every parameter needs a strict codec (create returning an object with parse), while the result may stay src-json. Open it through ctx.remote.$stream. The mounted namespace is its own Cordis service: read it with ctx.get('remote.<namespace>') from the mounting Plugin's Context. Reading .<namespace> off a captured ctx.remote object returned nothing, and the stream never opened with no console error. The supervisor retries carrier loss only; a normal Host end or a thrown item is terminal, so the application retries it. Guard $mount/$stream for older Hosts. Verify with an authenticated open frame on /api/remote.mux and with a real page refresh after a Host change, not with unit stubs alone.

To use the Gateway Client's stream helpers (RemoteSnapshotStream, RemoteJournalStream, RemoteStreamCarrierError) from an application Client bundle, declare @deepseek-ai/dsh-api-gateway/client under dsh.client.external in the package manifest and externalize the same ID in the bundler, as the official workspace and session controllers do; dsh.client.inject can stay empty. That module is a browser closure bundle and throws window is not defined when a Node test imports it, so alias it in the test runner to the package's per-file modules under lib/types/client/. Return a RemoteStreamCarrierError from ended(accepted) once the baseline was accepted so a normal Host end is retried by the supervisor; any other error makes the snapshot stream terminal (#1498).

A RemoteError thrown by a stream method before or between items reaches the Client as an error frame with its code and ends that logical stream as a terminal failure, not a carrier loss; map application refusals (unknown subject, cursor ahead of the log, invalid cursor) to stable codes and let the consumer decide whether to reload state. When a stream replaces a legacy route, make the subscription a single typed frame source and let both the stream and the route encode it, so their replay and baseline rules cannot drift (#1500).

Additional verified pitfall — #1528

The pinned client-modules loader claims every <style> without data-plugin for the module materializing at that moment (lazy chunks included) and removes those tags when that module is replaced, invalidated or pruned. A plugin that injects an untagged sheet from an effect can therefore lose it to another plugin's reload while it stays active: its UI renders unstyled and DSH's own UI, whose sheets are tagged, looks fine. Set data-plugin to the injecting module's id (and a data-plugin-css key) on every runtime <style> (#1528).