dsh-loop-engine 0.1.7-rc3 → 0.1.7-rc4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,76 +1,76 @@
1
- # dsh-loop-engine
2
-
3
- [![npm version](https://img.shields.io/npm/v/dsh-loop-engine?color=cb3837)](https://www.npmjs.com/package/dsh-loop-engine)
4
-
5
- Pick the agent loop engine of **dsh web** the same way you pick a model — **per session**: the built-in `in-process` loop (the default), or one of the four hosted engines `claude-code`, `codex`, `pi`, `kimi`. The engine is the plugin's own per-session record, so **any open, idle session can be moved to another engine** — between two hosted engines it is an **in-place handover**, and a move involving `in-process` releases that session's agent and reloads the page (the `dsh web` process never restarts). Sessions are independent, so one chat can run Codex while another runs Kimi. None of this changes anything in the main repository.
6
-
7
- ## Install
8
-
9
- ```sh
10
- dsh plugin --profile web add dsh-loop-engine
11
- ```
12
-
13
- Boot `dsh web` so the profile recomposes with the plugin, then open **Settings → Loop engine**. One boot is enough: the router retries for a bounded window while the base bundle's `agent-loop` row still holds the factory slot.
14
-
15
- > Installing rewrites one **engine-agnostic** managed block in `cordis.patch.yml`: it disables the base bundle's `agent-loop` row so the plugin's own router can own the process's single agent-factory slot, and every other byte you wrote in that file is preserved. Upgrading from an earlier release needs no hand edit — a block that named the one pinned engine is rewritten to this form on the next boot, and seeds the Settings default with that engine.
16
-
17
- > **pnpm users:** pnpm 10+ blocks dependency build scripts by default, so the install may fail with `ERR_PNPM_IGNORED_BUILDS` naming `esbuild`, `@google/genai`, and `protobufjs` (all reached through the engine SDKs). Allow them and retry — `pnpm approve-builds`, or `allowBuilds` in `pnpm-workspace.yaml`. Only the installing project can grant this; the plugin cannot pre-approve its own dependencies.
18
-
19
- ### Running against a harness source checkout
20
-
21
- A **published** dsh needs no extra setup. Booting the harness from its **source checkout** (`cd deepseek-harness && pnpm dsh web`) takes one more step: bridge the profile's harness peer packages to the checkout source with `file:` shims, so both halves share one module instance (a scope minted through one instance is invisible to the other, and session resume fails with `agent-presets: refusing to compose an unscoped context`). The peer list includes `@deepseek-ai/dsh-agent-loop`. The runnable shim steps are in [docs/source-checkout.md](docs/source-checkout.md); why every `@deepseek-ai/*` package must stay a single instance is in [docs/architecture.md](docs/architecture.md). Installing the plugin as a local **`link:`** checkout sidesteps this entirely.
22
-
23
- ## Version compatibility
24
-
25
- dsh-loop-engine is versioned **in lockstep with the harness it targets**: `<harness version>-rcN`. From `0.1.7-rc1` on, **one release serves two harness generations**: the 0.1.5 line (`>=0.1.5-rc.1 <0.1.6-0`, all sharing the old settings API) and the 0.1.7 line (`>=0.1.7-rc.1 <0.1.8-0`, whose `rc.1` and `rc.2` are both verified). It detects the running generation at load and takes the matching code path, so a single published package installs onto either. Every harness package it consumes declares that union range in `peerDependencies`; a harness outside it fails loudly at boot or session resume. Within a covered line a new `rc` needs no plugin release unless the API surfaces it depends on moved — `docs/compatibility.md` §1 lists the surfaces and the diff command that decides it. `0.1.5-rc3` … `0.1.5-rc5` target harness `0.1.5-rc.2`; `0.1.5-rc1`/`0.1.5-rc2` target `0.1.5-rc.1`; `1.0.0-rc8` … `1.0.0-rc15` target `0.1.2-rc.1`; `1.0.0-rc7` and earlier target `0.1.1-rc.2`.
26
-
27
- ### Requirements
28
-
29
- - **Claude Code**: the Claude Code CLI installed and logged in on the host.
30
- - **Codex**: authenticated via `codex login` on the host, or a `CODEX_API_KEY` environment entry.
31
- - **Pi**: authenticated the way `pi` expects (its own `~/.pi/agent/auth.json`, or the provider's API-key environment variable such as `ANTHROPIC_API_KEY`).
32
- - **Kimi Code**: the `kimi` CLI installed and logged in (e.g. `kimi login`), and reachable on `PATH` (or pinned to an absolute path via `kimiBin` in the composition entry).
33
- - Running the harness from a source checkout needs the extra `dsh-agent-loop` `file:` shim above.
34
-
35
- ## Usage
36
-
37
- **The engine is chosen per session.** Pick `Claude Code`, `Codex`, `Pi`, or `Kimi Code` on the new-session screen's preset chip (beside the workspace picker); keep the deployment's own default (usually `standard`) to run the built-in in-process loop. Different sessions can run different engines at the same time, and a child agent inherits the engine of the agent that spawned it.
38
-
39
- - **Settings → Loop engine** sets the default for **new** sessions only — it lands immediately, with no restart and no page reload, and sessions already running are unaffected. Choosing **In-process** restores whatever default the plugin replaced.
40
- - **Moving a session you are in** uses the composer's engine selector (enable *Show the engine selector in the chat page*), which calls the plugin's own `remote.loopEngine.select`. Any open, idle session can move — including one that has already run many turns.
41
- - **Between two hosted engines the move is an in-place handover**: the session stays open, only the agent is replaced — it applies on the spot, with nothing to confirm.
42
- - **A move involving `in-process` cannot be handed over in place** (the harness loop neither accepts a session it did not create nor hands a live one over), so the composer asks first: the switch reloads the page, and this page's scroll position and unsent draft go with it (the conversation record does not). On confirm the host RELEASES that session's agent and answers `reload: true`; the page reloads itself and reopens the same session, which is what makes the host build it on the recorded engine. The process never restarts.
43
- - A session that is **mid-turn is refused** rather than interrupted, and a **subagent's** session cannot be moved at all.
44
- - **Models**: every hosted engine shares ONE `external` group in the model menu, holding exactly one entry, `default` — meaning "the engine decides". Picking a **real dsh model** hands it to the engine **with its endpoint and credential** (whether the engine can serve it is the engine's business — a refusal is reported, not swallowed). `in-process` sessions use dsh's models as usual.
45
- - **Uninstall**: `dsh plugin --profile web remove dsh-loop-engine`, then manually delete the plugin's managed block from the profile's `cordis.patch.yml` — the block outlives the plugin, and while it is present the base `agent-loop` row stays disabled, leaving the profile with no agent factory.
46
-
47
- ### What a hosted engine takes over
48
-
49
- - Its preset is a copy of `standard` minus the dsh-native rows an external engine replaces — dsh's `/plan`, `/compact` (and auto-compaction), the model-facing goal tool, the human `/goal` command, and the dsh skill rows (one stripped preset per engine, under `$DSH_HOME/.agent-presets/loop-engine-<engine>/`).
50
- - The engine's own slash commands and skill catalog are registered into **that agent's own scope**, so two sessions on different engines never see each other's menus, and the whole surface is released with the agent.
51
- - Engine-agnostic dsh commands (`/export`, `/feedback`, `/permission`) keep working and stay.
52
-
53
- ## Known limitations
54
-
55
- - **The engine record lives in the plugin's sidecar, `$DSH_HOME/.loop-engine/engines.json`, not in the session log.** Changing machine or `DSH_HOME` loses it, and the session falls back gracefully to the preset mapping. See [docs/per-session-engine.md](docs/per-session-engine.md) §5.5 and [docs/architecture.md](docs/architecture.md) §3.9.
56
- - **A move involving `in-process` reloads the page**, because the harness loop neither hands over a live session nor adopts one it did not create. See [docs/per-session-engine.md](docs/per-session-engine.md) §5.2/§5.4.
57
- - **The engine is settled at session creation / blank period**; once a turn has run, changing engines rebuilds that session's agent (idle only). See [docs/per-session-engine.md](docs/per-session-engine.md) §5.
58
- - **Old sessions carrying the legacy single preset id (`loop-engine`)** show as "legacy hosted engine" and need one rebuild before the new semantics take over. See [docs/per-session-engine.md](docs/per-session-engine.md) §7.
59
- - **Under a hosted engine a dsh model pick matters only if you pick a real dsh model** — `default` means "hand it back to the engine". See [docs/per-session-engine.md](docs/per-session-engine.md) §5.2.
60
- - **Same-engine sessions share that CLI's own auth directory**, with no lock added by the plugin. See [docs/per-session-engine.md](docs/per-session-engine.md) §6.
61
- - **Switching to `in-process` leaves the shared `external` provider group in the model menu** (the catalog is not scoped per session). See [docs/architecture.md](docs/architecture.md) §3.6.
62
-
63
- ## Where the details live
64
-
65
- - [docs/per-session-engine.md](docs/per-session-engine.md) — the full user-visible behavior of per-session engines.
66
- - [docs/source-checkout.md](docs/source-checkout.md) — the `file:` shims a source-launched harness needs.
67
- - [docs/architecture.md](docs/architecture.md) — plugin core: the single factory slot, the managed block, routing, per-session engine facts, the provider route.
68
- - [docs/compatibility.md](docs/compatibility.md) — **start here when the harness upgrades**: every generation-dependent code site, the four techniques for spanning them, and the checklist for adding the next generation.
69
- - [docs/driver-core.md](docs/driver-core.md) — the shared driver infrastructure.
70
- - [docs/engine-claude.md](docs/engine-claude.md), [docs/engine-codex.md](docs/engine-codex.md), [docs/engine-kimi.md](docs/engine-kimi.md), [docs/engine-pi.md](docs/engine-pi.md) — per-engine internals.
71
- - [docs/optimization-backlog.md](docs/optimization-backlog.md) — known issues and the optimization list.
72
- - [docs/proposals/](docs/proposals/) — main-repo proposals: `append-ignorable-events.md`, `harness-agent-handover.md`, and the two model-selection ones (`dsh-model-into-hosted-engines.md`, `per-session-model-for-hosted-engines.md`).
73
-
74
- ## License
75
-
76
- MIT
1
+ # dsh-loop-engine
2
+
3
+ [![npm version](https://img.shields.io/npm/v/dsh-loop-engine?color=cb3837)](https://www.npmjs.com/package/dsh-loop-engine)
4
+
5
+ Pick the agent loop engine of **dsh web** the same way you pick a model — **per session**: the built-in `in-process` loop (the default), or one of the four hosted engines `claude-code`, `codex`, `pi`, `kimi`. The engine is the plugin's own per-session record, so **any open, idle session can be moved to another engine** — between two hosted engines it is an **in-place handover**, and a move involving `in-process` releases that session's agent and reloads the page (the `dsh web` process never restarts). Sessions are independent, so one chat can run Codex while another runs Kimi. None of this changes anything in the main repository.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ dsh plugin --profile web add dsh-loop-engine
11
+ ```
12
+
13
+ Boot `dsh web` so the profile recomposes with the plugin, then open **Settings → Loop engine**. One boot is enough: the router retries for a bounded window while the base bundle's `agent-loop` row still holds the factory slot.
14
+
15
+ > Installing rewrites one **engine-agnostic** managed block in `cordis.patch.yml`: it disables the base bundle's `agent-loop` row so the plugin's own router can own the process's single agent-factory slot, and every other byte you wrote in that file is preserved. Upgrading from an earlier release needs no hand edit — a block that named the one pinned engine is rewritten to this form on the next boot, and seeds the Settings default with that engine.
16
+
17
+ > **pnpm users:** pnpm 10+ blocks dependency build scripts by default, so the install may fail with `ERR_PNPM_IGNORED_BUILDS` naming `esbuild`, `@google/genai`, and `protobufjs` (all reached through the engine SDKs). Allow them and retry — `pnpm approve-builds`, or `allowBuilds` in `pnpm-workspace.yaml`. Only the installing project can grant this; the plugin cannot pre-approve its own dependencies.
18
+
19
+ ### Running against a harness source checkout
20
+
21
+ A **published** dsh needs no extra setup. Booting the harness from its **source checkout** (`cd deepseek-harness && pnpm dsh web`) takes one more step: bridge the profile's harness peer packages to the checkout source with `file:` shims, so both halves share one module instance (a scope minted through one instance is invisible to the other, and session resume fails with `agent-presets: refusing to compose an unscoped context`). The peer list includes `@deepseek-ai/dsh-agent-loop`. The runnable shim steps are in [docs/source-checkout.md](docs/source-checkout.md); why every `@deepseek-ai/*` package must stay a single instance is in [docs/architecture.md](docs/architecture.md). Installing the plugin as a local **`link:`** checkout sidesteps this entirely.
22
+
23
+ ## Version compatibility
24
+
25
+ dsh-loop-engine is versioned **in lockstep with the harness it targets**: `<harness version>-rcN`. From `0.1.7-rc1` on, **one release serves two harness generations**: the 0.1.5 line (`>=0.1.5-rc.1 <0.1.6-0`, all sharing the old settings API) and the 0.1.7 line (`>=0.1.7-rc.1 <0.1.8-0`, whose `rc.1` and `rc.2` are both verified). It detects the running generation at load and takes the matching code path, so a single published package installs onto either. Every harness package it consumes declares that union range in `peerDependencies`; a harness outside it fails loudly at boot or session resume. Within a covered line a new `rc` needs no plugin release unless the API surfaces it depends on moved — `docs/compatibility.md` §1 lists the surfaces and the diff command that decides it. `0.1.5-rc3` … `0.1.5-rc5` target harness `0.1.5-rc.2`; `0.1.5-rc1`/`0.1.5-rc2` target `0.1.5-rc.1`; `1.0.0-rc8` … `1.0.0-rc15` target `0.1.2-rc.1`; `1.0.0-rc7` and earlier target `0.1.1-rc.2`.
26
+
27
+ ### Requirements
28
+
29
+ - **Claude Code**: the Claude Code CLI installed and logged in on the host.
30
+ - **Codex**: authenticated via `codex login` on the host, or a `CODEX_API_KEY` environment entry.
31
+ - **Pi**: authenticated the way `pi` expects (its own `~/.pi/agent/auth.json`, or the provider's API-key environment variable such as `ANTHROPIC_API_KEY`).
32
+ - **Kimi Code**: the `kimi` CLI installed and logged in (e.g. `kimi login`), and reachable on `PATH` (or pinned to an absolute path via `kimiBin` in the composition entry).
33
+ - Running the harness from a source checkout needs the extra `dsh-agent-loop` `file:` shim above.
34
+
35
+ ## Usage
36
+
37
+ **The engine is chosen per session.** Pick `Claude Code`, `Codex`, `Pi`, or `Kimi Code` on the new-session screen's preset chip (beside the workspace picker); keep the deployment's own default (usually `standard`) to run the built-in in-process loop. Different sessions can run different engines at the same time, and a child agent inherits the engine of the agent that spawned it.
38
+
39
+ - **Settings → Loop engine** sets the default for **new** sessions only — it lands immediately, with no restart and no page reload, and sessions already running are unaffected. Choosing **In-process** restores whatever default the plugin replaced.
40
+ - **Moving a session you are in** uses the composer's engine selector (enable *Show the engine selector in the chat page*), which calls the plugin's own `remote.loopEngine.select`. Any open, idle session can move — including one that has already run many turns.
41
+ - **Between two hosted engines the move is an in-place handover**: the session stays open, only the agent is replaced — it applies on the spot, with nothing to confirm.
42
+ - **A move involving `in-process` cannot be handed over in place** (the harness loop neither accepts a session it did not create nor hands a live one over), so the composer asks first: the switch reloads the page, and this page's scroll position and unsent draft go with it (the conversation record does not). On confirm the host RELEASES that session's agent and answers `reload: true`; the page reloads itself and reopens the same session, which is what makes the host build it on the recorded engine. The process never restarts.
43
+ - A session that is **mid-turn is refused** rather than interrupted, and a **subagent's** session cannot be moved at all.
44
+ - **Models**: every hosted engine shares ONE `external` group in the model menu, holding exactly one entry, `default` — meaning "the engine decides". Picking a **real dsh model** hands it to the engine **with its endpoint and credential** (whether the engine can serve it is the engine's business — a refusal is reported, not swallowed). `in-process` sessions use dsh's models as usual.
45
+ - **Uninstall**: `dsh plugin --profile web remove dsh-loop-engine`, then manually delete the plugin's managed block from the profile's `cordis.patch.yml` — the block outlives the plugin, and while it is present the base `agent-loop` row stays disabled, leaving the profile with no agent factory.
46
+
47
+ ### What a hosted engine takes over
48
+
49
+ - Its preset is a copy of `standard` minus the dsh-native rows an external engine replaces — dsh's `/plan`, `/compact` (and auto-compaction), the model-facing goal tool, the human `/goal` command, and the dsh skill rows (one stripped preset per engine). The preset is a harness-generation-dependent artifact: a directory under `$DSH_HOME/.agent-presets/loop-engine-<engine>/` on the 0.1.5 line, and an `@deepseek-ai/dsh-agent-preset` row inserted into the profile patch on the 0.1.7 line, which no longer reads that directory. See [docs/architecture.md](docs/architecture.md) §3.5.
50
+ - The engine's own slash commands and skill catalog are registered into **that agent's own scope**, so two sessions on different engines never see each other's menus, and the whole surface is released with the agent.
51
+ - Engine-agnostic dsh commands (`/export`, `/feedback`, `/permission`) keep working and stay.
52
+
53
+ ## Known limitations
54
+
55
+ - **The engine record lives in the plugin's sidecar, `$DSH_HOME/.loop-engine/engines.json`, not in the session log.** Changing machine or `DSH_HOME` loses it, and the session falls back gracefully to the preset mapping. See [docs/per-session-engine.md](docs/per-session-engine.md) §5.5 and [docs/architecture.md](docs/architecture.md) §3.9.
56
+ - **A move involving `in-process` reloads the page**, because the harness loop neither hands over a live session nor adopts one it did not create. See [docs/per-session-engine.md](docs/per-session-engine.md) §5.2/§5.4.
57
+ - **The engine is settled at session creation / blank period**; once a turn has run, changing engines rebuilds that session's agent (idle only). See [docs/per-session-engine.md](docs/per-session-engine.md) §5.
58
+ - **Old sessions carrying the legacy single preset id (`loop-engine`)** show as "legacy hosted engine" and need one rebuild before the new semantics take over. See [docs/per-session-engine.md](docs/per-session-engine.md) §7.
59
+ - **Under a hosted engine a dsh model pick matters only if you pick a real dsh model** — `default` means "hand it back to the engine". See [docs/per-session-engine.md](docs/per-session-engine.md) §5.2.
60
+ - **Same-engine sessions share that CLI's own auth directory**, with no lock added by the plugin. See [docs/per-session-engine.md](docs/per-session-engine.md) §6.
61
+ - **Switching to `in-process` leaves the shared `external` provider group in the model menu** (the catalog is not scoped per session). See [docs/architecture.md](docs/architecture.md) §3.6.
62
+
63
+ ## Where the details live
64
+
65
+ - [docs/per-session-engine.md](docs/per-session-engine.md) — the full user-visible behavior of per-session engines.
66
+ - [docs/source-checkout.md](docs/source-checkout.md) — the `file:` shims a source-launched harness needs.
67
+ - [docs/architecture.md](docs/architecture.md) — plugin core: the single factory slot, the managed block, routing, per-session engine facts, the provider route.
68
+ - [docs/compatibility.md](docs/compatibility.md) — **start here when the harness upgrades**: every generation-dependent code site, the four techniques for spanning them, and the checklist for adding the next generation.
69
+ - [docs/driver-core.md](docs/driver-core.md) — the shared driver infrastructure.
70
+ - [docs/engine-claude.md](docs/engine-claude.md), [docs/engine-codex.md](docs/engine-codex.md), [docs/engine-kimi.md](docs/engine-kimi.md), [docs/engine-pi.md](docs/engine-pi.md) — per-engine internals.
71
+ - [docs/optimization-backlog.md](docs/optimization-backlog.md) — known issues and the optimization list.
72
+ - [docs/proposals/](docs/proposals/) — main-repo proposals: `append-ignorable-events.md`, `harness-agent-handover.md`, and the two model-selection ones (`dsh-model-into-hosted-engines.md`, `per-session-model-for-hosted-engines.md`).
73
+
74
+ ## License
75
+
76
+ MIT
package/README.zh.md CHANGED
@@ -46,7 +46,7 @@ dsh plugin --profile web add dsh-loop-engine
46
46
 
47
47
  ### 托管引擎接管什么
48
48
 
49
- - 它的预设是 `standard` 的副本,去掉了外部引擎会替代掉的 dsh 原生行——dsh 的 `/plan`、`/compact`(与自动压缩)、模型可见的 goal 工具、人类 `/goal` 命令、以及 dsh skill 行(每引擎一份剥离后的 preset,位于 `$DSH_HOME/.agent-presets/loop-engine-<engine>/`)。
49
+ - 它的预设是 `standard` 的副本,去掉了外部引擎会替代掉的 dsh 原生行——dsh 的 `/plan`、`/compact`(与自动压缩)、模型可见的 goal 工具、人类 `/goal` 命令、以及 dsh skill 行(每引擎一份剥离后的 preset)。preset 的**载体随 harness 代际而变**:0.1.5 上是 `$DSH_HOME/.agent-presets/loop-engine-<engine>/` 目录,0.1.7 上是插进 profile patch 的一行 `@deepseek-ai/dsh-agent-preset`(0.1.7 已不再读那个目录)。详见 [docs/architecture.md](docs/architecture.md) §3.5。
50
50
  - 引擎自己的斜杠命令与技能目录会注册进**这个 agent 自己的 scope**,因此两个跑不同引擎的会话互相看不到对方的菜单,整份表面随 agent 一起回收。
51
51
  - 与引擎无关的 dsh 命令(`/export`、`/feedback`、`/permission`)照常可用、保留在菜单里。
52
52
 
package/lib/index.js CHANGED
@@ -5856,6 +5856,64 @@ var MANAGED_HEADER = `# Managed by dsh-loop-engine: the deployment's "${SOURCE_P
5856
5856
  # from "${SOURCE_PRESET_ID}" on boot \u2014 hand edits are overwritten. The preset id
5857
5857
  # names the engine this session runs.
5858
5858
  `;
5859
+ var PRESET_ROWS_BEGIN = "# -- dsh-loop-engine presets --";
5860
+ var PRESET_ROWS_END = "# -- /dsh-loop-engine presets --";
5861
+ var ENGINE_PRESET_ORDER = 100;
5862
+ var PLUGIN_LIST_INDENT = " ".repeat(10);
5863
+ function pluginListLines(composition) {
5864
+ const text = composition.replace(/\n+$/, "");
5865
+ const lines = text.split("\n");
5866
+ if (!lines.some((line) => line.startsWith("- "))) return [" plugins: []"];
5867
+ return [" plugins:", ...lines.map((line) => line.trim() === "" ? "" : `${PLUGIN_LIST_INDENT}${line}`)];
5868
+ }
5869
+ function renderEnginePresetRow(engine, composition) {
5870
+ const id = enginePresetId(engine);
5871
+ return [
5872
+ "- insert:",
5873
+ ` - id: preset-${id}`,
5874
+ ` name: '@deepseek-ai/dsh-agent-preset'`,
5875
+ " config:",
5876
+ ` id: ${id}`,
5877
+ ` order: ${ENGINE_PRESET_ORDER}`,
5878
+ ...pluginListLines(stripPresetRows(composition))
5879
+ ].join("\n");
5880
+ }
5881
+ function renderPresetRows(composition) {
5882
+ return [
5883
+ PRESET_ROWS_BEGIN,
5884
+ "# One `@deepseek-ai/dsh-agent-preset` declaration per hosted engine: the",
5885
+ `# deployment's "${SOURCE_PRESET_ID}" preset minus the dsh-native command/skill rows a`,
5886
+ `# hosted loop engine replaces. Regenerated from "${SOURCE_PRESET_ID}" on boot \u2014 hand`,
5887
+ "# edits are overwritten. The preset id names the engine a session runs.",
5888
+ ...HOSTED_ENGINE_IDS.map((engine) => renderEnginePresetRow(engine, composition)),
5889
+ `${PRESET_ROWS_END}
5890
+ `
5891
+ ].join("\n");
5892
+ }
5893
+ function presetRowsSpan(text) {
5894
+ const begin = text.indexOf(PRESET_ROWS_BEGIN);
5895
+ if (begin === -1) return { head: text, tail: "", present: false, blankBefore: false };
5896
+ const endAt = text.indexOf(PRESET_ROWS_END, begin);
5897
+ const spanEnd = endAt === -1 ? text.length : endAt + PRESET_ROWS_END.length + 1;
5898
+ const before = text.slice(0, begin);
5899
+ const blankBefore = before.endsWith("\n\n");
5900
+ return {
5901
+ head: blankBefore ? before.slice(0, -1) : before,
5902
+ tail: text.slice(spanEnd),
5903
+ present: true,
5904
+ blankBefore
5905
+ };
5906
+ }
5907
+ function applyEnginePresetRows(text, composition) {
5908
+ const region = renderPresetRows(composition);
5909
+ const span = presetRowsSpan(text);
5910
+ if (!span.present) {
5911
+ if (text === "") return region;
5912
+ return dropSeedPlaceholder(`${ensureTrailingNewline(text)}
5913
+ ${region}`);
5914
+ }
5915
+ return dropSeedPlaceholder(`${span.head}${span.blankBefore ? "\n" : ""}${region}${span.tail}`);
5916
+ }
5859
5917
  function managedMetadata(engine) {
5860
5918
  return `name: ${ENGINE_DISPLAY_NAMES[engine]}
5861
5919
  description: ${ENGINE_DISPLAY_NAMES[engine]}, with the dsh-native commands and skills it replaces removed.
@@ -5937,6 +5995,18 @@ ${stripPresetRows(composition)}`;
5937
5995
  }
5938
5996
  return changed;
5939
5997
  }
5998
+ async function readTextOrEmpty(path) {
5999
+ try {
6000
+ return await readFile(path, "utf8");
6001
+ } catch {
6002
+ return "";
6003
+ }
6004
+ }
6005
+ async function ensureEnginePresetRows(patchPath, source) {
6006
+ const composition = await readComposition(source, SOURCE_PRESET_ID);
6007
+ const current = await readTextOrEmpty(patchPath);
6008
+ return writeIfDifferent(patchPath, applyEnginePresetRows(current, composition));
6009
+ }
5940
6010
 
5941
6011
  // src/router-loop.ts
5942
6012
  import AgentLoop, { DEFAULT_MAX_PARALLEL_TOOL_CALLS } from "@deepseek-ai/dsh-agent-loop";
@@ -7444,6 +7514,10 @@ function resolvePatchPath(config) {
7444
7514
  config.patchFilename ?? "cordis.patch.yml"
7445
7515
  );
7446
7516
  }
7517
+ function authorHostedPresets(patchPath, presets) {
7518
+ if (LEGACY_HARNESS) return ensureEnginePresets(resolveDshHome2(), presets);
7519
+ return ensureEnginePresetRows(patchPath, presets);
7520
+ }
7447
7521
  function isMissing2(error) {
7448
7522
  return error?.code === "ENOENT";
7449
7523
  }
@@ -7611,7 +7685,7 @@ function apply(ctx, config) {
7611
7685
  let lastError;
7612
7686
  for (let attempt2 = 0; attempt2 <= PRESET_SOURCE_ATTEMPTS; attempt2 += 1) {
7613
7687
  try {
7614
- return await ensureEnginePresets(resolveDshHome2(), presets);
7688
+ return await authorHostedPresets(patchPath, presets);
7615
7689
  } catch (error) {
7616
7690
  lastError = error;
7617
7691
  if (attempt2 < PRESET_SOURCE_ATTEMPTS) {
@@ -15,16 +15,20 @@
15
15
  * Because the router owns the slot, the plugin keeps the base bundle's
16
16
  * `agent-loop` row disabled for as long as it is composed. That managed block
17
17
  * lives in the profile's `cordis.patch.yml` (see `patch-manager.ts`) and is the
18
- * plugin's only footprint in the harness's configuration; the block names no
19
- * engine, because the engine is a per-session decision.
18
+ * plugin's only footprint in the harness's configuration alongside the preset
19
+ * rows below; the block names no engine, because the engine is a per-session
20
+ * decision.
20
21
  *
21
- * The plugin authors one preset per hosted engine into the user preset root
22
- * (`$DSH_HOME/.agent-presets/loop-engine-<engine>`, see `preset.ts`), each a
23
- * copy of `standard` minus the dsh-native command and skill rows an external
24
- * engine replaces, and it serves every hosted engine's provider route label in
25
- * the llm registry (`provider-route.ts`): an engine logs its own label into
26
- * each session's request/header, and the web host refuses a turn whose session
27
- * selection names a provider no adapter serves. The preset is now only the
22
+ * The plugin authors one preset per hosted engine — each a copy of `standard`
23
+ * minus the dsh-native command and skill rows an external engine replaces — and
24
+ * it serves every hosted engine's provider route label in the llm registry
25
+ * (`provider-route.ts`): an engine logs its own label into each session's
26
+ * request/header, and the web host refuses a turn whose session selection names
27
+ * a provider no adapter serves. WHERE that copy lives depends on the harness
28
+ * generation — a directory under `$DSH_HOME/.agent-presets/loop-engine-<engine>`
29
+ * on the 0.1.5 line, a `@deepseek-ai/dsh-agent-preset` insert row in the profile
30
+ * patch on the 0.1.7 line, which stopped reading the directory (see
31
+ * `preset.ts`, {@link authorHostedPresets}). The preset is now only the
28
32
  * session's agent-plane composition (and the engine's own default for a session
29
33
  * with no record) — it is no longer the thing that decides a running session's
30
34
  * engine.
@@ -69,6 +69,26 @@ export declare function hasManagedBlock(text: string): boolean;
69
69
  * engine this build does not recognize.
70
70
  */
71
71
  export declare function legacyBlockEngineOf(text: string): LoopEngineId | undefined;
72
+ /** Normalize a file so a managed span sits on its own lines with a blank separator. */
73
+ export declare function ensureTrailingNewline(text: string): string;
74
+ /**
75
+ * The profile seed template (`cordis.patch.yml` on a fresh profile) is a lone
76
+ * root-level empty flow sequence `[]`. The plugin's managed block is itself a
77
+ * root-level block sequence of loader entries, so a block coexisting with a
78
+ * surviving `[]` is TWO root collections in one document — YAML the harness
79
+ * rejects with "end of the stream or a document separator is expected", and the
80
+ * web app then fails to boot. Remove a whole-line root `[]` placeholder so the
81
+ * managed block is the sole top-level collection. Anchored to column 0 so an
82
+ * indented `[]` that is a real value inside an entry's nested config is never
83
+ * touched.
84
+ *
85
+ * Shared with the preset-rows region (`./preset.ts`): both regions are
86
+ * root-level collections appended to the same file, so either one can be the
87
+ * first thing that meets a surviving seed.
88
+ * @param text - the patch-file text.
89
+ * @returns the text without a whole-line root `[]` placeholder.
90
+ */
91
+ export declare function dropSeedPlaceholder(text: string): string;
72
92
  /**
73
93
  * Produce the next patch-file text carrying the managed block, preserving every
74
94
  * byte outside the managed span. Appends the span when absent and replaces it
@@ -10,9 +10,29 @@
10
10
  * advisory prompt text an external engine never assembles, dsh `/compact`
11
11
  * cannot shrink a context the engine's child process holds, and dsh skills
12
12
  * would sit next to the engine's own catalog. Those rows live inside the
13
- * agent-preset composition, which a profile patch cannot reach, so the plugin
14
- * authors a stripped preset per engine into the user preset root
15
- * (`$DSH_HOME/.agent-presets/<id>`).
13
+ * agent-preset composition, which a profile patch cannot otherwise reach, so
14
+ * the plugin authors a stripped copy of that composition per engine.
15
+ *
16
+ * WHICH mechanism carries that copy is the running harness generation's
17
+ * business, and the two share everything but the carrier:
18
+ *
19
+ * - the 0.1.5 line reads presets from a DIRECTORY per preset under the user
20
+ * preset root (`$DSH_HOME/.agent-presets/<id>`), so there it is two files
21
+ * per engine ({@link ensureEnginePresets});
22
+ * - the 0.1.7 line replaced that with composed plugin rows: a preset is one
23
+ * `@deepseek-ai/dsh-agent-preset` row whose `config.plugins` list IS the
24
+ * composition, declared wherever a composition is declared. Nothing reads
25
+ * `.agent-presets` any more, so there it is four `insert` rows in the
26
+ * profile patch the plugin already manages
27
+ * ({@link ensureEnginePresetRows}). The rows mirror the shipped shape
28
+ * (`packages/bundle/web-app/presets/standard.patch.yml` in the harness),
29
+ * with this plugin's own id, order, and stripped composition.
30
+ *
31
+ * Both mechanisms regenerate from the current `standard` composition on every
32
+ * boot: text on disk is never authoritative, so a harness upgrade that changes
33
+ * `standard` flows through. Neither re-parses YAML — the strip is a line
34
+ * transform that preserves everything it does not drop byte for byte, comments
35
+ * included, and the row form only re-indents that text.
16
36
  *
17
37
  * The preset id is ALSO the per-session engine selector: the harness resolves
18
38
  * one preset per session and hands its id to the agent factory at create time
@@ -21,12 +41,6 @@
21
41
  * makes "session A on Codex, session B on Kimi, concurrently" expressible in a
22
42
  * harness that admits exactly one AgentFactory.
23
43
  *
24
- * The presets are REGENERATED from the current `standard` composition on every
25
- * boot: text on disk is never authoritative, so a harness upgrade that changes
26
- * `standard` flows through. The file is plain YAML the loader already accepts —
27
- * the strip is a line transform that preserves everything it does not drop byte
28
- * for byte, comments included.
29
- *
30
44
  * @module dsh-loop-engine/preset
31
45
  */
32
46
  export { HOSTED_PRESET_PREFIX, SOURCE_PRESET_ID, engineOfPreset, enginePresetId, hostedEngineOf, sessionEngineOf, } from './agent-preset-ids.ts';
@@ -56,6 +70,47 @@ export declare const HOSTED_PRESET_IDS: readonly string[];
56
70
  * its context and its own `/compact` (Claude, Kimi).
57
71
  */
58
72
  export declare const STRIPPED_ROWS: readonly ["skill-filesystem", "tool-skill", "tool-goal", "command-goal", "planning", "compaction"];
73
+ /**
74
+ * Begin marker of the plugin-managed preset-rows region inside a profile patch
75
+ * file. The region is what registers the hosted engines' presets on the 0.1.7
76
+ * line, where a preset is a composed row rather than a directory.
77
+ *
78
+ * Deliberately NOT the managed block's marker: the two regions are located and
79
+ * rewritten independently, so editing one never rewrites the other's bytes.
80
+ */
81
+ export declare const PRESET_ROWS_BEGIN = "# -- dsh-loop-engine presets --";
82
+ /** End marker of the plugin-managed preset-rows region inside a profile patch file. */
83
+ export declare const PRESET_ROWS_END = "# -- /dsh-loop-engine presets --";
84
+ /**
85
+ * Roster order of every preset this plugin authors. The shipped presets occupy
86
+ * 1..4 (`standard` 1, `ptc` 2, `minimal` 3, `cordis` 4), and the roster sorts
87
+ * by order and then by id, so one constant past that range puts every managed
88
+ * preset after every shipped one — the deployment's own presets stay at the top
89
+ * of the picker and the engines' copies trail it, which is also where the
90
+ * order they are listed in (selection order) is readable.
91
+ */
92
+ export declare const ENGINE_PRESET_ORDER = 100;
93
+ /**
94
+ * The managed preset-rows region: one preset row per hosted engine, bracketed
95
+ * by the markers that make it locatable and rewritable.
96
+ * @param composition - the source `standard` composition.
97
+ * @returns the region's text, ending in a newline.
98
+ */
99
+ export declare function renderPresetRows(composition: string): string;
100
+ /**
101
+ * Produce the next patch-file text carrying the preset-rows region, preserving
102
+ * every byte outside that span — including the managed block, which is located
103
+ * by its own markers. Appends the region when absent and replaces it when
104
+ * present, so an unchanged composition leaves the text byte for byte identical.
105
+ *
106
+ * The region is a root-level collection like the managed block, so a leftover
107
+ * seed `[]` is dropped with it: two root collections in one document is YAML
108
+ * the harness rejects, and the profile would stop booting.
109
+ * @param text - current patch-file text.
110
+ * @param composition - the source `standard` composition.
111
+ * @returns the rewritten patch-file text.
112
+ */
113
+ export declare function applyEnginePresetRows(text: string, composition: string): string;
59
114
  /**
60
115
  * Remove top-level entries by id from a preset composition, preserving every
61
116
  * other byte. Each entry owns the comment/blank run directly above its opener
@@ -87,4 +142,24 @@ export interface PresetCompositionSource {
87
142
  * @throws when the source preset cannot be read or the writes fail.
88
143
  */
89
144
  export declare function ensureEnginePresets(dshHome: string, source: PresetCompositionSource): Promise<boolean>;
145
+ /**
146
+ * Regenerate every hosted engine's preset as an `insert` row in the profile
147
+ * patch the plugin already manages, from the roster's `standard` preset.
148
+ *
149
+ * This is the 0.1.7 mechanism: a preset there is a composed plugin row, and the
150
+ * user preset root is not read at all — so a preset written as a directory
151
+ * would simply never register, leaving the roster without the id every hosted
152
+ * session records. The rows go in the profile's own `cordis.patch.yml` because
153
+ * that is the one composition layer the plugin owns: it is already the file the
154
+ * managed block lives in, it is applied after the bundle's own preset rows, and
155
+ * a rewrite of it is what the harness's live patch reload picks up.
156
+ *
157
+ * Idempotent: an up-to-date region is left byte for byte alone, so a rewrite
158
+ * that changes nothing does not touch the file's stamp.
159
+ * @param patchPath - absolute path of the profile's patch file.
160
+ * @param source - the roster's composition reader.
161
+ * @returns whether the patch file was written.
162
+ * @throws when the source preset cannot be read or the write fails.
163
+ */
164
+ export declare function ensureEnginePresetRows(patchPath: string, source: PresetCompositionSource): Promise<boolean>;
90
165
  //# sourceMappingURL=preset.d.ts.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-loop-engine",
3
3
  "description": "Web-switchable agent loop engine selection for the DeepSeek Harness - out-of-tree plugin (Claude Code / Codex / Pi / Kimi Code drivers) maintained by @kuun993, zero main-repo changes",
4
- "version": "0.1.7-rc3",
4
+ "version": "0.1.7-rc4",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },