dsh-loop-engine
Pick dsh web's agent loop engine per session — the built-in in-process loop (the default), or one of the hosted engines claude-code / codex / pi / kimi. Sessions are independent: one chat can run Codex while another runs Kimi. Any open, idle session can move engines — between two hosted engines the handover is in place, and a move involving in-process reloads the page (the dsh web process never restarts). Nothing in the main repository changes.
Install
dsh plugin --profile web add dsh-loop-engine
Boot dsh web once (the router retries for a bounded window while the base bundle still holds the agent-factory slot), then open Settings → Loop engine.
Install notes
Requirements
| Engine | Needs, on the host |
|---|
| Claude Code | the Claude Code CLI installed and logged in |
| Codex | codex login, or a CODEX_API_KEY environment entry |
| Pi | whatever pi expects (its own ~/.pi/agent/auth.json, or the provider's API-key variable) |
| Kimi Code | the kimi CLI installed and logged in, on PATH (or pinned via kimiBin) |
Usage
- New session — pick the engine on the preset chip beside the workspace picker, or keep the deployment's default (
standard) for the in-process loop. A child agent inherits its parent's engine.
- Settings → Loop engine sets the default for new sessions. It lands immediately; running sessions are unaffected. In-process restores whatever default the plugin replaced.
- Move a session you are in with the composer's engine selector (enable Show the engine selector in the chat page).
- Hosted → hosted: in place, applies on the spot.
- Anything involving
in-process: the composer asks first, then the page reloads and reopens the same session (scroll position and unsent draft are lost; the record is not).
- A mid-turn session is refused, and a subagent's session cannot be moved.
- The header chip beside the preset label names the engine the session on screen actually runs; Show the engine badge in the conversation header turns it off (and the header keeps no trace of the plugin).
- Models — all hosted engines share one
external group whose single entry, default, means "the engine decides". Picking a real dsh model hands it to the engine together with its endpoint and credential; whether the engine can serve it is the engine's business, and a refusal is reported rather than swallowed. in-process sessions use dsh's models normally.
childIdleMs (composition entry, milliseconds, default 0 = off) — Kimi and Codex keep one child process per session across steps; this closes it after that much idle time and respawns it on the next step. Only the child is closed, so nothing reloads. Pi and Claude Code spawn per step and are unaffected.
engineContextWindow (composition entry, tokens, default 262144) — the context window dsh assumes for the shared hosted route. It is what dsh's auto-compaction measures a hosted session against (it fires at 80% of this by default), because a hosted engine reports no capacity of its own. Set it to the smallest window among the engines you run.
summarizerProvider / summarizerModel (composition entry, set together or not at all) — the real dsh model a hosted session's compaction summarizes through, e.g. deepseek-official / deepseek-flash. Compaction is dsh's own (its preset keeps the group), but the summarizing is an ordinary llm request, and a hosted session's routed model is the plugin's placeholder route — so without this pair a hosted session can never be folded down: every attempt logs a warning and the turn continues. The pair only serves that call; the engine still answers every user turn itself. Composing just one of the two (or an empty string) is refused at boot, not silently ignored.
Version compatibility
One release serves the 0.1.5 line (>=0.1.5-rc.1 <0.1.6-0), the 0.1.7 line (>=0.1.7-rc.1 <0.1.8-0), the 0.2.0 line (>=0.2.0-rc.1 <0.2.1-0) and the 0.2.1 line (>=0.2.1-rc.1 <0.2.2-0): the plugin detects the running generation at load and takes the matching code path. Those union ranges are what it declares in peerDependencies, and a harness outside them fails loudly at boot or session resume.
Read the version as <harness line>-rcN, where rcN is this plugin's own release counter for that line — not the harness's rc number. Within a covered line a new harness rc needs no plugin release unless an API surface moved; docs/compatibility.md §1 lists those surfaces and the diff command that decides it.
| Plugin | Harness |
|---|
0.2.1-rc1 | 0.2.1-rc.1, 0.2.1-rc.2 (and every earlier line it still covers) |
0.2.0-rc1 | 0.2.0-rc.1, 0.2.0-rc.2 |
0.1.7-rc1 … 0.1.7-rc5 | 0.1.7-rc.1, 0.1.7-rc.2 |
0.1.5-rc3 … 0.1.5-rc5 | 0.1.5-rc.2 |
0.1.5-rc1 / 0.1.5-rc2 | 0.1.5-rc.1 |
1.0.0-rc8 … 1.0.0-rc15 | 0.1.2-rc.1 |
1.0.0-rc7 and earlier | 0.1.1-rc.2 |
0.2.1-rc1 is the first release that serves four lines at once. Two harness changes force it: the 0.2.1 line deleted @deepseek-ai/dsh-invariants and every <package>/invariant subpath, and it republished the JSONL durability backend as a namespace plugin with no default export. The plugin's ./invariant companion now declares the host registry's shape locally, so one build mounts on every line — and, because dsh skips a whole bundle whose @deepseek-ai/dsh-* peers do not satisfy the running version, dropping that package from peerDependencies is what lets the plugin load on 0.2.1 at all.
Known limitations
- It replaces the harness's agent factory. One facade serves every session, so the plugin promises to stay out of the way: any session it cannot answer for — including sessions with nothing to do with hosted engines — degrades to the
in-process loop instead of failing to open. See docs/per-session-engine.md §6.
- The engine record is a sidecar,
$DSH_HOME/.loop-engine/engines.json, not part of the session log: changing machine or DSH_HOME loses it and the session falls back to its preset. See §5.5.
- Moving a session involving
in-process reloads the page — the harness loop neither hands over a live session nor adopts one it did not create. See §5.2/§5.4.
- Sessions that already ran a turn can still move, but only while idle, and the move rebuilds that session's agent. See §5.
- Old sessions with the legacy preset id
loop-engine read as "legacy hosted engine" until rebuilt once. See §7.
- Same-engine sessions share that CLI's own auth directory, with no lock added by the plugin. See §6.
- Switching back to
in-process leaves the shared external group in the model menu (the catalog is not scoped per session).
Where the details live
License
MIT