refactor(agents): link dsh shared policy as a home-layer patch (#388)

dsh rewrites a profile's cordis.patch.yml at runtime, so linking the web
profile put GUI state in the checkout and let runtime writes clobber manual
edits. Link the stable policy as the $DSH_HOME home layer instead: dsh applies
it after every profile and only reads it.

The host-plane compaction-basic row is disabled on the web surface and each
agent preset owns its own copy, so the top-level override was inert. Drop it;
the model contextWindow/maxTokens are the effective knobs.

- add home/base/tui/agents/dsh/cordis.patch.yml, linked from default.nix
- drop the ~/.dsh/profiles/web link and the tracked web/ profile
- keep runtime state and private/local model routes in the profile patch
- update the eval test and README
This commit is contained in:
ryan4yin | 二花 authored and GitHub committed 2026-10-06 14:04:55 +08:00
1 parent 205f7c622b
commit 28e0b156ce
10 files changed
+120 -151

No files matched your search

+2 -2
View File
@@ -13,8 +13,8 @@ Nix dependency; this module only wires it into place.
- `packages.nix`: agent CLIs (`codex`, `opencode2`, `pi`) from the `llm-agents` flake input, plus
`pkgs.mcp-grafana` for the Grafana MCP server.
- `env.nix`: telemetry and auto-update opt-outs for the agents.
- `dsh/`: the dsh `web` profile that lives in `~/.dsh`. See [dsh/README.md](./dsh/README.md) for the
link granularity, the tracked members and which hosts import it.
- `dsh/`: the shared dsh home-layer patch linked to `~/.dsh/cordis.patch.yml`. See
[dsh/README.md](./dsh/README.md) for why the shared layer is linked and the profile patch is not.
## Deployed rule targets
+38 -20
View File
@@ -1,26 +1,21 @@
# dsh
The dsh (DeepSeek Harness) `web` profile, linked into `~/.dsh/profiles/web` from the checked-in
files under [`web/`](./web).
Shared dsh (DeepSeek Harness) policy, linked into `~/.dsh/cordis.patch.yml`.
## Why the link targets the directory
## Layers
dsh persists Settings through `cordis.patch.yml` with an atomic write (a random-suffix sibling, then
a rename), and that rename replaces a symlinked target instead of writing through to its referent. A
file-level link — store or out-of-store — is destroyed by the first Settings save, while a directory
link keeps the write inside the checkout. Same reasoning as `home/base/tui/tuios`, one level up.
dsh composes a profile from bundle patches, the profile's own `cordis.patch.yml`, then
`$DSH_HOME/cordis.patch.yml` (the "home" layer) and `--patch` overlays. The home layer is the only
one shared by every profile and surface, so the stable policy lives here.
| Profile member | Tracked | Role |
| --------------------- | ------- | ------------------------------------------ |
| `cordis.patch.yml` | yes | the user patch layer the Settings UI edits |
| `package.json` | yes | the profile's `dsh.profile.bundles` list |
| `pnpm-workspace.yaml` | yes | the profile's pnpm settings |
| `cordis.yml` | no | rewritten on every boot |
| `node_modules/` | no | written by `dsh plugin --profile web ...` |
| Layer | Path | Writer | Tracked |
| ------- | ----------------------------------------- | ----------------------------- | ------- |
| home | `~/.dsh/cordis.patch.yml` | nix-config; dsh only reads it | yes |
| profile | `~/.dsh/profiles/<name>/cordis.patch.yml` | dsh, at runtime | no |
`web/.gitignore` owns the untracked rows. As with the rules links in the parent module, the
trade-off is a config that stays writable in the checkout: a Settings change and a commit are the
same edit, and the configuration is shared by every host that deploys the profile.
dsh writes the profile patch but only reads the home layer, so linking the home layer is safe while
linking the profile patch would put runtime state in the checkout. Declared settings are edited
here, not in the Settings UI.
## Which hosts deploy it
@@ -31,11 +26,34 @@ host that ships dsh needs one of those two wirings.
## Default model route
`web/cordis.patch.yml` pins `agent-default-model` to the API-key route (`deepseek-official`). The
`cordis.patch.yml` pins `agent-default-model` to the API-key route (`deepseek-official`). The
account route (`deepseek-account`) never falls back to an API key, so a host holding only
`DEEPSEEK_API_KEY` fails every freshly created session with `ACCOUNT_SIGN_IN_REQUIRED`. A host that
signs in instead can select an account model per session, or override the field in its own
`$DSH_HOME/cordis.patch.yml`, which the home layer applies last and which therefore wins.
signs in instead can select an account model per session.
## Compaction
The web and desktop surfaces give each agent preset its own compaction service, so a top-level
`compaction-basic` row here never reaches a session. The effective knobs are the model
`contextWindow` and `maxTokens`: the `400000`/`32000` values set here place the first compaction
near 300k tokens.
## Private and local model routes
Endpoints that must not enter this repository — LAN/self-hosted servers and staging gateways — do
not belong in the home layer. Declare them locally as a separate pi-ai instance in the profile
patch, which dsh does not deploy from here:
```yaml
# ~/.dsh/profiles/<name>/cordis.patch.yml
- insert:
- id: llm-pi-ai-private
name: "@deepseek-ai/dsh-llm-pi-ai"
config:
providers: { ... }
```
The private instance coexists with the shared one under its own id.
## Running it
+70
View File
@@ -0,0 +1,70 @@
# Shared dsh policy. Linked to ~/.dsh/cordis.patch.yml and applied as the
# $DSH_HOME ("home") layer, after each profile's own patch, so it covers every
# profile and surface. dsh only reads it — keep runtime preferences and local
# model routes in the profile patch (see README.md).
- id: agent-default-model
name: "@deepseek-ai/dsh-agent-default-model"
config:
provider: deepseek-official
model: deepseek-flash
reasoningEffort: high
- id: locale
name: "@deepseek-ai/dsh-client-locale"
config:
preference: zh
- id: agent-preset-registry
name: "@deepseek-ai/dsh-agent-preset-registry"
config:
default: standard
selectedDefault: ptc
- id: llm-deepseek
name: "@deepseek-ai/dsh-llm-deepseek-api-key"
config:
models:
- id: deepseek-flash
name: DeepSeek-V41-Flash
inputModalities:
- text
- image
systemPromptUpdate: in-history
toolUpdate: addition-only
contextWindow: 400000
maxTokens: 32000
- id: deepseek-v4-pro
name: DeepSeek-V4-Pro
description:
Stronger agentic coding, knowledge, and difficult reasoning; suited to complex or
quality-critical tasks at higher cost.
contextWindow: 500000
inputModalities:
- text
- id: llm-deepseek-account
name: "@deepseek-ai/dsh-llm-deepseek-account"
config:
models:
- id: deepseek-flash
name: DeepSeek-V41-Flash
inputModalities:
- text
- image
systemPromptUpdate: in-history
toolUpdate: addition-only
contextWindow: 400000
maxTokens: 32000
- id: deepseek-v4-pro
name: DeepSeek-V4-Pro
description:
Stronger agentic coding, knowledge, and difficult reasoning; suited to complex or
quality-critical tasks at higher cost.
contextWindow: 500000
inputModalities:
- text
- id: agent-loop
name: "@deepseek-ai/dsh-agent-loop"
config:
agents: []
maxParallelToolCalls: 8
- id: subagent
name: "@deepseek-ai/dsh-subagent"
config:
maxActiveSubagents: 3
+4 -13
View File
@@ -3,19 +3,10 @@
...
}:
let
# dsh persists Settings through the profile's cordis.patch.yml with an atomic
# write (a random-suffix sibling, then a rename), and that rename replaces a
# symlinked target instead of writing through to its referent. A file-level
# link, store or out-of-store, therefore survives only until the first save.
# Linking the whole profile directory keeps the write inside the checkout and
# leaves the directory symlink intact. Same reasoning as
# `home/base/tui/tuios`, one level up; the checkout path is hardcoded like the
# repo's other out-of-store links.
profileDir = "${config.home.homeDirectory}/nix-config/home/base/tui/agents/dsh/web";
# Out-of-store so repo edits apply without a rebuild (see README.md).
sharedPatch = "${config.home.homeDirectory}/nix-config/home/base/tui/agents/dsh/cordis.patch.yml";
in
{
# The `web` profile's configuration, owned by hand and by the Settings UI.
# Generated members of the profile (cordis.yml, node_modules, lock files) are
# gitignored inside that directory rather than tracked.
home.file.".dsh/profiles/web".source = config.lib.file.mkOutOfStoreSymlink profileDir;
# The shared dsh policy, read as $DSH_HOME/cordis.patch.yml.
home.file.".dsh/cordis.patch.yml".source = config.lib.file.mkOutOfStoreSymlink sharedPatch;
}
-10
View File
@@ -1,10 +0,0 @@
# Regenerated by dsh on every boot (always the empty root entry list).
cordis.yml
# Written by `dsh plugin --profile web ...`.
node_modules/
pnpm-lock.yaml
# Transient siblings of the atomic-write and file-lock protocols.
*.tmp
*.lock
@@ -1,77 +0,0 @@
# Your patch layer for this dsh profile, applied after every bundle layer:
# a top-level YAML array of loader patch entries (id-targeted config
# overrides, disables, and insert lists; `!!js` expressions allowed).
- id: ui-settings-general
name: "@deepseek-ai/dsh-client-ui-settings-general"
config:
welcomeNoticeVersion: 2026-09-28.1
- id: agent-default-model
name: "@deepseek-ai/dsh-agent-default-model"
config:
provider: deepseek-official
model: deepseek-flash
reasoningEffort: high
- id: ui-settings-account
name: "@deepseek-ai/dsh-client-ui-settings-account"
config:
version: 1
step: done
purpose: development
process: standard
completion: completed
usage: detailed
developerTools: true
- id: ui-chat
name: "@deepseek-ai/dsh-client-ui-chat"
config:
transcriptView: standard
performanceUsage: detailed
- id: ui-settings
name: "@deepseek-ai/dsh-client-ui-settings"
config:
enabled: true
- id: locale
name: "@deepseek-ai/dsh-client-locale"
config:
preference: zh
- id: agent-preset-registry
name: "@deepseek-ai/dsh-agent-preset-registry"
config:
default: standard
selectedDefault: ptc
- id: llm-deepseek-account
name: "@deepseek-ai/dsh-llm-deepseek-account"
config:
models:
- id: deepseek-flash
name: DeepSeek-V41-Flash
inputModalities:
- text
- image
systemPromptUpdate: in-history
toolUpdate: addition-only
contextWindow: 1000000
- id: deepseek-v4-pro
name: DeepSeek-V4-Pro
description: Stronger agentic coding, knowledge, and difficult reasoning; suited to complex or quality-critical tasks at higher cost.
contextWindow: 500000
inputModalities:
- text
- id: agent-loop
name: "@deepseek-ai/dsh-agent-loop"
config:
agents: []
maxParallelToolCalls: 8
- id: subagent
name: "@deepseek-ai/dsh-subagent"
config:
maxActiveSubagents: 3
- id: compaction-basic
name: "@deepseek-ai/dsh-compaction-basic"
config:
thresholdRatio: 0.35
retainRatio: 0.16
headroomTokens: 65536
compactionRetries: 1
maxOverflowRetries: 1
auto: true
-14
View File
@@ -1,14 +0,0 @@
{
"name": "dsh-profile-web",
"private": true,
"dependencies": {},
"dsh": {
"profile": {
"bundles": [
"@deepseek-ai/dsh-base",
"@deepseek-ai/dsh-web-app",
"@deepseek-ai/dsh-experimental-auto-review"
]
}
}
}
@@ -1,5 +0,0 @@
packages:
- .
nodeLinker: hoisted
autoInstallPeers: false