PTC Auto-Assign (ptc-auto preset)
A resident DSH agent preset: in PTC (Code Mode) sessions, it automatically assigns the model and reasoning effort for every request according to the current work phase.
中文文档: README.zh-CN.md
Rules (from PROJECT.md §7 recommended config)
| Phase | Trigger | Model | Reasoning effort |
|---|---|---|---|
| Plan/architecture | Plan mode on, or the message contains 设计/架构/规划/方案/计划/接口/重构 (or English design/architecture/plan/refactor, etc.) |
deepseek-v4-pro |
max |
| Implement/fix tests | Default (no keyword hit) | deepseek-v4-pro |
high |
| Mechanical edits | Message contains 文案/机械性/补全/整理/重命名/格式化/迁移/注释/标注 (or English rename/formatting, etc.) |
deepseek-v4-flash |
high |
All keywords and gears are configurable in the ptc-assign row's config inside
agent.cordis.yml — no code changes needed.
Install
Copy this whole directory to:
${DSH_HOME:-$HOME/.dsh}/.agent-presets/ptc-auto/
(The preset id = the directory name ptc-auto; renaming the directory = changing the id.)
After copying, start a new session and pick 「PTC 模式(自动分配)」 in the preset picker
(recommended: "set as default"). Verify with /ptc status.
You can also let an AI deploy it — paste this to your AI assistant:
Clone https://github.com/tkdfs/dsh-ptc-auto-preset into
${DSH_HOME:-$HOME/.dsh}/.agent-presets/ptc-auto/, keeping the directory nameptc-auto.
The clone target is all it takes; the directory name becomes the preset id. Then start a new session and pick the preset in the picker.
Usage
- Auto-effective: every message shifts gears by phase before it is sent; the first message already applies.
/ptc status: show the current gear, the classification basis and diagnostics (claims/preSteps/requestSteps/trace/lastError)./ptc auto|plan|implement|mechanical|off: manually pin a gear or turn auto-assign off.- Model-side tool
ptc_assign: invoked by the model when the user asks in natural language to adjust the gear. - Manual model-selection protection: after manually picking a model in the UI, the plugin
detects the deviation and auto-pauses (pins);
/ptc autoresumes. - Known display lag: see "Known issue: UI picker display lag" below.
Implementation mechanism (must read for maintainers)
- In Web sessions, the model is decided by the "session selection layer" (api-proxy's
installModelSelection), which registers before the preset mounts and wraps the outermostagent/requestwaterfall — any waterfall rewrite registered later gets overridden by it. - Therefore this plugin does not compete on the waterfall; instead, at
agent/inbox/claimed(when a message is claimed, before the system prompt is assembled), it writes the target config into the session's persistedrequest/headerevent — which is exactly the source the session selection layer reads. After writing, the request immediately executes with the assigned config, keeping the UI picker, session log, and restart recovery consistent automatically. agent/pre-stepis kept as a backup write point (after assembly; on Web it usually no-ops due to skip-same, but it still works on runtimes without a session selection layer).
Maintenance conventions (pitfalls we hit; must follow)
- When changing plugin code you must rename the file:
ptc-assign.vN.js, and update that row'sname:inagent.cordis.yml. Why: the resident agent only detects a new generation by the mtime ofagent.cordis.yml, and Node ESM caches plugin modules by URL — editing a file in place leaves the new generation loading old code (cost us several rounds of debugging). - For rule gears, only edit the
configinagent.cordis.yml; after touching the yml, start a new session and the new generation applies. - Verify:
agentPresets.standingKeyFor('ptc-auto')mount check (mounted OK), then a real-session round (or a controlled mock agent).
Debug history (2026-08-14)
| Symptom | Root cause | Fix |
|---|---|---|
| Auto-assign never took effect; every request used the session default gear | The session selection layer overrode the waterfall rewrite | Rewrite the persisted request/header |
| New generation didn't load new plugin code | ESM caches by URL + generation detection only looks at agent.cordis.yml | Versioned filenames + touch the yml |
| pre-step header write was one step too late | System prompt assembly happens before pre-step | Move the main write point to inbox/claimed |
| Misclassification ("好的" judged as plan gear) | Injected messages (skill catalog, runtime snapshot, etc.) were treated as the last user message | Only accept human messages with source.kind === 'user' |
Verification result (reconciled against real session logs): the synthesized header
(sys:false) is written before the loop's own request header; all three test segments hit —
implement→pro+high, mechanical→flash+high, plan→pro+max.
Known issue: UI picker display lag
Symptom: when the first message (or the first after each gear shift) is sent, the model picker in the UI still shows the session's old gear, making it look like "auto-assign didn't take effect" — yet the reply's actual reasoning depth / log records are the new gear.
Verdict: purely a display issue, not an execution issue. Reconciled message-by-message
against session logs: the synthesized request header is written before the loop's own request
header, and requests actually execute with the assigned gear (evidence: the first plan-mode
reply is already a full max-gear deep-thinking stream; request/header's
provider/model/reasoningEffort are all correct).
Root cause: the UI model picker is pull-based; it re-fetches the session model from the server only when:
- opening the picker menu;
- manually selecting a model;
- reconnecting after a disconnect;
- the adapter catalog (llm/adapters-updated) or the settings document (settings/document-updated) changes.
This plugin is an external writer (it directly mutates the session's persisted
request/header), and the product has no "request header changed → push a picker refresh"
channel, so the picker stays at the last pulled state.
Impact & workaround:
- Impact: display layer only; it does not affect actual model/reasoning-effort execution, nor session logs/restart recovery.
- Workaround: to check the real gear — open the picker menu once (triggers a re-pull) or just
/ptc status.
Alternative fix (not adopted): build a resident client-side sync plugin (subscribe to
session events → re-pull sessions.models) so the picker refreshes in real time on every
message. Not adopted because: it requires building a client plugin package, bundling it,
integrating into the deployment and maintaining it across upgrades — cost is disproportionate
to the benefit; the behavior itself is correct, and the display just lags one "open the menu"
gesture.
File list
preset.yml— preset display metadata (name/description)agent.cordis.yml— composition file: full PTC mode (code preset) capabilities + theptc-assignrowplugins/ptc-assign.v4.js— plugin implementation: claimed/pre-step writes, the/ptccommand, theptc_assigntool, diagnostic state
No comments yet. Be the first to write one.