@ljwei-stak/dsh-model-router 0.22.2 → 0.23.1

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.
@@ -0,0 +1,169 @@
1
+ /**
2
+ * In-memory Host workbench jobs. Only package identity and state enter progress;
3
+ * answers and the rest of the workbench response stay in the final result.
4
+ */
5
+ import { randomUUID } from 'node:crypto'
6
+
7
+ const ACTIVE = new Set(['running', 'cancelling'])
8
+ const PACKAGE_DONE = new Set(['succeeded', 'failed', 'cancelled', 'blocked', 'skipped'])
9
+ const FAILURE = 'Run failed. Please try again.'
10
+
11
+ const copy = value => JSON.parse(JSON.stringify(value))
12
+ const label = (value, fallback = '') => typeof value === 'string' ? value : fallback
13
+
14
+ function positiveLimit(value, name) {
15
+ if (!Number.isSafeInteger(value) || value < 1) throw new TypeError(`${name} must be a positive integer.`)
16
+ return value
17
+ }
18
+
19
+ function completionStatus(item) {
20
+ if (item.cancelled === true || item.status === 'cancelled') return 'cancelled'
21
+ if (item.blocked === true || item.status === 'blocked') return 'blocked'
22
+ if (item.notStarted === true || item.status === 'skipped') return 'skipped'
23
+ if (item.status === 'succeeded' || item.status === 'failed') return item.status
24
+ return item.ok === true ? 'succeeded' : 'failed'
25
+ }
26
+
27
+ /**
28
+ * start() returns before invoking run(). Cancellation requests abort the same
29
+ * signal given to run(), but the job remains active until run actually settles.
30
+ * maxJobs bounds all retained jobs, and only terminal jobs may be evicted.
31
+ */
32
+ export function createRunJobs({ now = Date.now, maxJobs = 50, maxActive = 4 } = {}) {
33
+ if (typeof now !== 'function') throw new TypeError('now must be a function.')
34
+ positiveLimit(maxJobs, 'maxJobs')
35
+ positiveLimit(maxActive, 'maxActive')
36
+ const jobs = new Map()
37
+ let disposed = false
38
+
39
+ // A clock supplied by a Host must not turn a settled worker into an unhandled
40
+ // rejection or make a snapshot impossible to send over JSON.
41
+ const timestamp = () => {
42
+ try {
43
+ const value = now()
44
+ if (typeof value === 'number' && Number.isFinite(value)) return value
45
+ } catch { /* retain a usable timestamp if the injected clock fails */ }
46
+ return Date.now()
47
+ }
48
+ const snapshot = job => copy(job.snapshot)
49
+ const find = jobId => {
50
+ const job = jobs.get(jobId)
51
+ if (!job) throw new Error('Unknown run job id.')
52
+ return job
53
+ }
54
+
55
+ function refreshProgress(job) {
56
+ const progress = job.snapshot.progress
57
+ progress.total = progress.packages.length
58
+ progress.completed = progress.packages.filter(item => PACKAGE_DONE.has(item.status)).length
59
+ const current = progress.packages.findLast(item => item.status === 'running')
60
+ progress.current = current ? { id: current.id, name: current.name } : null
61
+ }
62
+
63
+ function packageFor(job, value) {
64
+ if (!value || typeof value !== 'object') return null
65
+ const id = label(value.id)
66
+ if (!id) return null
67
+ let item = job.snapshot.progress.packages.find(item => item.id === id)
68
+ if (!item) {
69
+ item = { id, name: label(value.name, id), status: 'pending' }
70
+ job.snapshot.progress.packages.push(item)
71
+ }
72
+ return item
73
+ }
74
+
75
+ function progressFor(job, event) {
76
+ if (!ACTIVE.has(job.snapshot.status) || !event || typeof event !== 'object') return
77
+ // Progress is advisory: malformed events must not break the actual worker.
78
+ try {
79
+ if (event.type === 'stage') {
80
+ if (typeof event.stage === 'string') job.snapshot.progress.stage = event.stage.slice(0, 120)
81
+ return
82
+ }
83
+ if (event.type !== 'package-start' && event.type !== 'package-complete') return
84
+ const item = packageFor(job, event.package)
85
+ if (!item || PACKAGE_DONE.has(item.status)) return
86
+ item.status = event.type === 'package-start' ? 'running' : completionStatus(event.package)
87
+ refreshProgress(job)
88
+ } catch { /* progress cannot affect the final workbench response */ }
89
+ }
90
+
91
+ function finish(job, status, result = null) {
92
+ if (!ACTIVE.has(job.snapshot.status)) return
93
+ const state = job.snapshot
94
+ state.status = status
95
+ state.finishedAt = timestamp()
96
+ state.result = result
97
+ state.progress.stage = status
98
+ for (const item of state.progress.packages) {
99
+ if (PACKAGE_DONE.has(item.status)) continue
100
+ // Pending packages were never observed running; once the worker settles,
101
+ // they cannot start. An interrupted running package has a known outcome.
102
+ item.status = status === 'cancelled' ? 'cancelled'
103
+ : item.status === 'running' ? (status === 'failed' ? 'failed' : 'succeeded') : 'skipped'
104
+ }
105
+ refreshProgress(job)
106
+ if (status === 'failed') state.error = FAILURE
107
+ }
108
+
109
+ function fail(job) {
110
+ finish(job, job.controller.signal.aborted ? 'cancelled' : 'failed')
111
+ }
112
+
113
+ function requestCancel(job) {
114
+ if (ACTIVE.has(job.snapshot.status) && !job.snapshot.cancelRequested) {
115
+ job.snapshot.cancelRequested = true
116
+ job.snapshot.status = 'cancelling'
117
+ job.snapshot.progress.stage = 'cancelling'
118
+ job.controller.abort()
119
+ }
120
+ return snapshot(job)
121
+ }
122
+
123
+ return {
124
+ start({ run, packages = [] } = {}) {
125
+ if (disposed) throw new Error('Run job manager is disposed.')
126
+ if (typeof run !== 'function') throw new TypeError('Run job requires a run function.')
127
+ if ([...jobs.values()].filter(job => ACTIVE.has(job.snapshot.status)).length >= maxActive) {
128
+ throw new Error('Too many active run jobs.')
129
+ }
130
+ // Evict the oldest terminal snapshots first. A cancelling worker still
131
+ // occupies capacity because its process/cleanup may still be running.
132
+ if (jobs.size >= maxJobs) {
133
+ const terminal = [...jobs].find(([, job]) => !ACTIVE.has(job.snapshot.status))
134
+ if (!terminal) throw new Error('Run job capacity reached; active jobs are still running.')
135
+ jobs.delete(terminal[0])
136
+ }
137
+ const job = {
138
+ controller: new AbortController(),
139
+ snapshot: {
140
+ jobId: randomUUID(), status: 'running', startedAt: timestamp(), finishedAt: null,
141
+ cancelRequested: false,
142
+ progress: { total: 0, completed: 0, current: null, stage: 'starting', packages: [] },
143
+ result: null,
144
+ },
145
+ }
146
+ for (const item of Array.isArray(packages) ? packages : []) packageFor(job, item)
147
+ refreshProgress(job)
148
+ jobs.set(job.snapshot.jobId, job)
149
+ // Both rejection paths are attached before the worker can run. Result
150
+ // cloning also detaches the worker's mutable response from stored state.
151
+ Promise.resolve().then(() => run({
152
+ signal: job.controller.signal,
153
+ onProgress: event => progressFor(job, event),
154
+ })).then(result => {
155
+ const value = result == null ? null : copy(result)
156
+ const cancelled = value?.status === 'cancelled' || value?.execution?.status === 'cancelled'
157
+ finish(job, cancelled ? 'cancelled' : 'completed', value)
158
+ }, () => fail(job)).catch(() => fail(job))
159
+ return snapshot(job)
160
+ },
161
+ status(jobId) { return snapshot(find(jobId)) },
162
+ cancel(jobId) { return requestCancel(find(jobId)) },
163
+ dispose() {
164
+ if (disposed) return
165
+ disposed = true
166
+ for (const job of jobs.values()) requestCancel(job)
167
+ },
168
+ }
169
+ }
@@ -1223,6 +1223,12 @@ function aggregateOf(results) {
1223
1223
  return { status, packages: results, aggregate }
1224
1224
  }
1225
1225
 
1226
+ /** Host-only observation hooks must never discard execution or billing evidence. */
1227
+ export async function observeExecutionProgress(hook, value) {
1228
+ if (typeof hook !== 'function') return
1229
+ try { await hook(value) } catch { /* Progress reporting does not control execution. */ }
1230
+ }
1231
+
1226
1232
  function checkedPackageSelections(packages, routes, overrides, runVerified, targets) {
1227
1233
  const preceding = new Set()
1228
1234
  const selected = {}
@@ -1299,10 +1305,12 @@ async function runManualTool(target, prompt, options) {
1299
1305
  */
1300
1306
  async function runPackages({ packages, task, routingBypassed, routes, previous = [], targets = null, overrides = {}, choices = {}, options }) {
1301
1307
  const routeList = Array.isArray(routes) ? routes : []
1302
- const { credentialsFor, onPackage, billing, ...runOptions } = options
1308
+ const { credentialsFor, onPackageStart, onPackage, onStage, billing, ...runOptions } = options
1309
+ await observeExecutionProgress(onStage, 'preparing')
1303
1310
  const effectivePackages = checkedPackageSelections(packages, routeList, overrides, runOptions.runVerified, targets)
1304
1311
  const results = []
1305
1312
  const ranIds = []
1313
+ if (!runOptions.signal?.aborted) await observeExecutionProgress(onStage, 'executing')
1306
1314
  for (const item of effectivePackages) {
1307
1315
  const kept = previous.find(result => result.id === item.id)
1308
1316
  if (targets && !targets.has(item.id) && kept) {
@@ -1315,14 +1323,17 @@ async function runPackages({ packages, task, routingBypassed, routes, previous =
1315
1323
  const provider = target?.kind === 'tool' ? 'official-tool' : target?.provider ?? override?.provider ?? item.recommendedProvider
1316
1324
  const model = target?.kind === 'tool' ? target.toolId : target?.model ?? override?.model ?? item.recommendedModel
1317
1325
  const identity = { ...selection, ...(item.type ? { type: item.type } : {}), ...(item.purpose ? { purpose: item.purpose } : {}) }
1318
- if (runOptions.signal?.aborted) {
1326
+ const cancelledBeforeStart = async () => {
1319
1327
  // Cancelled: later packages are not started (no CLI process, no API spend).
1320
1328
  const cancelled = { id: item.id, name: item.name, ok: false, provider, model,
1321
1329
  channel: target?.kind === 'tool' ? 'official-cli' : 'harness-llm', ...(target?.kind === 'tool' ? { toolId: target.toolId } : {}),
1322
1330
  ...identity, answer: '', fallback: null, cancelled: true, notStarted: true, error: '执行已取消,此步骤未开始。' }
1323
1331
  results.push(cancelled)
1324
1332
  ranIds.push(item.id)
1325
- if (typeof onPackage === 'function') await onPackage(cancelled)
1333
+ await observeExecutionProgress(onPackage, cancelled)
1334
+ }
1335
+ if (runOptions.signal?.aborted) {
1336
+ await cancelledBeforeStart()
1326
1337
  continue
1327
1338
  }
1328
1339
  const unmet = (item.dependsOn ?? []).filter(id => results.find(result => result.id === id)?.ok !== true)
@@ -1338,7 +1349,7 @@ async function runPackages({ packages, task, routingBypassed, routes, previous =
1338
1349
  }
1339
1350
  results.push(blocked)
1340
1351
  ranIds.push(item.id)
1341
- if (typeof onPackage === 'function') await onPackage(blocked)
1352
+ await observeExecutionProgress(onPackage, blocked)
1342
1353
  continue
1343
1354
  }
1344
1355
  const plannedRoute = routeList.find(candidate => candidate.provider === provider && candidate.model === model)
@@ -1347,13 +1358,24 @@ async function runPackages({ packages, task, routingBypassed, routes, previous =
1347
1358
  : target?.kind === 'tool' ? { provider, model } : reasoningRouteForAssignment(plannedRoute, item, override)
1348
1359
  if (target?.kind === 'api') delete route.reasoningEffort
1349
1360
  const promptText = routingBypassed ? checkedTask(task) : packagePrompt(task, item, results)
1361
+ const credentials = !target ? (typeof credentialsFor === 'function' ? await credentialsFor(route) : runOptions.credentials ?? null) : null
1362
+ if (runOptions.signal?.aborted) {
1363
+ await cancelledBeforeStart()
1364
+ continue
1365
+ }
1350
1366
  const startedAt = Date.now()
1367
+ await observeExecutionProgress(onPackageStart, { ...item, ...identity, provider, model, startedAt,
1368
+ channel: target?.kind === 'tool' ? 'official-cli' : 'harness-llm',
1369
+ ...(target?.kind === 'tool' ? { toolId: target.toolId } : {}) })
1370
+ if (runOptions.signal?.aborted) {
1371
+ await cancelledBeforeStart()
1372
+ continue
1373
+ }
1351
1374
  let result
1352
1375
  if (target?.kind === 'tool') result = await runManualTool(target, promptText, runOptions)
1353
1376
  else if (target?.kind === 'api') result = { ...await useApi(runOptions.apiFallback, { route, task: promptText,
1354
1377
  signal: runOptions.signal, preference: 'api', reason: '按工作包手动选择模型目录 API。' }), fallback: null }
1355
1378
  else {
1356
- const credentials = typeof credentialsFor === 'function' ? await credentialsFor(route) : runOptions.credentials ?? null
1357
1379
  result = await executeRouteWithBilling({
1358
1380
  ...runOptions,
1359
1381
  billing: billing ? { ...billing, routes: billing.routes ?? routeList, ...(choices[item.id] ? { choice: choices[item.id] } : {}) } : null,
@@ -1382,7 +1404,7 @@ async function runPackages({ packages, task, routingBypassed, routes, previous =
1382
1404
  }
1383
1405
  results.push(entry)
1384
1406
  ranIds.push(item.id)
1385
- if (typeof onPackage === 'function') await onPackage(entry)
1407
+ await observeExecutionProgress(onPackage, entry)
1386
1408
  }
1387
1409
  return { ...aggregateOf(results), ranIds }
1388
1410
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.23.1 — 2026-10-23
4
+
5
+ - Check retry budgets against the target and every unfinished downstream package that will execute. Reassignment uses the selected route estimate; successful downstream packages are excluded and unknown prices remain unknown.
6
+ - Bound workbench preview, progress and cancel requests to 15 seconds, abort stalled transport waits, retry progress against the same job and allow an unacknowledged cancellation to be retried. Never automatically replay task starts.
7
+ - Preserve late-response fences and clear request lifetimes on panel close. Add offline Host budget, real SDK cancellation transport and hung-request UI regressions.
8
+
9
+
10
+ ## 0.23.0 — 2026-10-23
11
+
12
+ - Return an observable Host job when a workbench execution starts. Show elapsed time, the current work package and per-package states, with status reads and cancellation through the typed desktop gateway.
13
+ - Propagate cancellation to API streams and official CLI teams, wait for execution cleanup, retain completed results and reported usage, and prevent cancelled editable worktrees from starting integration. Closing the panel does not cancel execution.
14
+ - Keep preview and permission checks before job acceptance, preserve legacy synchronous execution helpers, and cover job lifecycles, gateway boundaries, cancellation accounting and UI races with offline fixtures.
15
+
16
+ - Preserve the incomplete-history budget marker when a corrupt-state recovery fails before a later read or write commits its pending notice, so missing charge history cannot become a zero-spend assumption.
17
+
3
18
  ## 0.22.2 — 2026-10-19
4
19
 
5
20
  - Render the README's mathematical symbols and expressions in running prose as inline math in both languages, while retaining code formatting for actual API fields. Keep ordinary display equations on one line.
package/README.i18n.yaml CHANGED
@@ -1,4 +1,4 @@
1
1
  # README translation pairing for this repository.
2
2
  # Reviewed together; Git blob hashes identify the exact synchronized texts.
3
- README.md: e73aada4df85277816f1d06c66d76c4e2c8a45b7
4
- README.zh.md: ff34db2cb3b95a5472d8e165c447ab36e844cd66
3
+ README.md: 3bb1e3507fc7451861fb3463f52f68827fb71690
4
+ README.zh.md: 1d15eb0bf9c5ebeac41b74d349b0a709f16288c8
package/README.md CHANGED
@@ -2,15 +2,53 @@
2
2
 
3
3
  [English](README.md) · [简体中文](README.zh.md)
4
4
 
5
+ [![npm version](https://flat.badgen.net/npm/v/@ljwei-stak/dsh-model-router)](https://www.npmjs.com/package/@ljwei-stak/dsh-model-router) [![dshfind](https://dshfind.com/api/badge/alice-marx/dsh-model-router)](https://dshfind.com/en/plugins/alice-marx/dsh-model-router?ref=badge) [![Listed in the DSH plugin directory](https://dsh-plugin.org/badges/listed.svg)](https://dsh-plugin.org/plugins/alice-marx/dsh-model-router) [![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com)
6
+
7
+ [![dshfind](https://dshfind.com/api/card/Alice-Marx/dsh-model-router)](https://dshfind.com/en/plugins/Alice-Marx/dsh-model-router?ref=badge)
8
+
9
+ Want release notes only? Click **Watch → Custom → Releases** (you do not need Watching all activity).
10
+
5
11
  A cost-aware model-routing workbench for **DeepSeek Harness Desktop**. Plan locally, compare configured routes, inspect execution permissions, and run dependent work packages through model APIs or supported official vendor CLIs.
6
12
 
7
- Current version: **0.22.2** (`@ljwei-stak/dsh-model-router`). Choose algorithm recommendations or manually assign each decomposed package to an exact model API or official tool, preserving dependencies, refreshed cost previews and retry targets. Routing now supports task-specific quality evidence, joint model/effort selection, historical P90 resource estimates and dependency-aware search. Manual mixed execution is read-only; tools use their configured default model, while the existing editable official-team workflow remains available. Compatibility remains **DeepSeek Harness Desktop 0.2.0-rc.1 / 0.2.0-rc.2 / 0.2.1-rc.2**, including Hermes/Pi installation and terminal support; neither has a verified managed runner yet. Real vendor execution evidence remains tied to **0.20.0**, with no new model experiments in this release. The router and [GAL](https://github.com/Alice-Marx/deepseek-harness-galgame) have been independent plugins since 0.12.0.
13
+ Current version: **0.23.1** (`@ljwei-stak/dsh-model-router`). Retries now check the whole unfinished downstream group against the budget, and stalled progress/cancel requests recover after a bounded wait. Workbench execution shows live elapsed time and per-package progress, supports cancellation with completed results and reported costs retained, and reconnects to the active job when the panel is reopened. Choose algorithm recommendations or manually assign each decomposed package to an exact model API or official tool, preserving dependencies, refreshed cost previews and retry targets. Routing now supports task-specific quality evidence, joint model/effort selection, historical P90 resource estimates and dependency-aware search. Manual mixed execution is read-only; tools use their configured default model, while the existing editable official-team workflow remains available. Compatibility remains **DeepSeek Harness Desktop 0.2.0-rc.1 / 0.2.0-rc.2 / 0.2.1-rc.2**, including Hermes/Pi installation and terminal support; neither has a verified managed runner yet. Real vendor execution evidence remains tied to **0.20.0**, with no new model experiments in this release. The router and [GAL](https://github.com/Alice-Marx/deepseek-harness-galgame) have been independent plugins since 0.12.0.
8
14
 
9
15
  Official CLI teamwork runs packages sequentially with dependency context and no automatic model-directory API fallback. Editable teams explicitly choose the Harness sandbox or the vendor CLI's normal environment. In the latter, Git worktrees collect edits but do not restrict OS access. CLIs retain their configured model unless explicitly mapped. See [real Windows validation and its limits](docs/releases/0.20.0-validation.md).
10
16
 
11
17
  Both README versions describe the same interfaces, formulas, defaults and limits. [Changelog](CHANGELOG.md) preserves release history. More walkthroughs: [workbench](docs/WORKBENCH_USER_GUIDE.zh.md), [installation](docs/INSTALLATION_GUIDE.zh.md), [migration](docs/archive/MIGRATION.md), and [experiment protocol](docs/ROUTING_SELFTEST_STEPS.zh.md).
12
18
 
13
- **Contents:** [Capabilities](#capabilities) · [Install](#install) · [Workbench](#workbench) · [Architecture](#architecture) · [Session tools](#session-tools) · [Official tools](#official-tools) · [Team lifecycle](#team-lifecycle) · [Costs](#costs) · [Routing algorithm](#routing) · [Data](#data) · [Development](#development) · [Troubleshooting](#troubleshooting)
19
+ <a id="quick-evaluation"></a>
20
+
21
+ ## Quick evaluation
22
+
23
+ **One-line value:** A cost-aware DSH workbench for planning tasks, routing model calls, and running dependent work packages with explicit execution permissions.
24
+
25
+ **Category:** **Models & Reasoning** (模型与推理), the best fit in the [community directory's category list](https://www.alldsh.com/categories/); this is a community taxonomy, not an official DSH classification.
26
+
27
+ **Install (DSH Plugin Hub / web profile):** With the DSH CLI available, install the published npm package with:
28
+
29
+ ```sh
30
+ dsh plugin --profile web add @ljwei-stak/dsh-model-router@0.23.1
31
+ ```
32
+
33
+ **Install in Harness Desktop:** Launch Desktop once to initialize its `desktop` profile, quit Desktop fully, then run:
34
+
35
+ ```sh
36
+ dsh plugin --profile desktop add @ljwei-stak/dsh-model-router@0.23.1
37
+ ```
38
+
39
+ You can also paste the package spec into **Harness Desktop → Plugins → Add plugin**. Restart Desktop after installation. A global `npm install -g` by itself does not register a Desktop plugin.
40
+
41
+ **Package and entry point:** [`@ljwei-stak/dsh-model-router@0.23.1` on npm](https://www.npmjs.com/package/@ljwei-stak/dsh-model-router) is published. The root [`package.json`](package.json) declares the package and bundle patch; [`.dsh-plugin/index.mjs`](.dsh-plugin/index.mjs) exports the required `apply(ctx)` entry point. License: [MIT](LICENSE).
42
+
43
+ **Configuration:** There is no supported import of a user-authored JSON/YAML configuration file. Configure providers, models, and credentials on Harness's **Models** page; configure routing profiles and preferences in the plugin settings. Persistent router state is stored in `$DSH_HOME/model-router/state.json` (default: `~/.dsh/model-router/state.json`). Provider CLIs retain their own login and configuration. The router does not copy API keys into model profiles or terminal logs.
44
+
45
+ **Screenshots and evidence:** The [workbench screenshot](docs/assets/workbench-task-preview.jpg) uses fixture models, prices, and health data; it is a simulated UI preview, not a live Harness or model run. See the [routing workflow diagram](docs/assets/routing-workflow.svg) for the execution flow. The [real Windows validation report](docs/releases/0.20.0-validation.md) documents specific 0.20.0 executions and their limits; 0.23.1 has no verified end-to-end vendor run. The [live progress screenshot](docs/assets/workbench-run-progress.png) is an offline simulation; see the [0.23.1 acceptance report](docs/releases/0.23.1-validation.md) for cancellation, recovery and validation boundaries.
46
+
47
+ **Permissions and risks:** Planning is local and makes no model call. Execution sends the selected task/context to the chosen model API or vendor CLI and may incur charges. The default Harness-restricted environment separates read-only runs from approved `workspace-write` runs. An explicitly selected `cli-native` editable team runs with the current OS user's permissions, outside the Harness process sandbox; a Git worktree collects changes but does not restrict OS access. External providers and CLI services apply their own data handling and billing terms.
48
+
49
+ **Compatibility:** Declared host support is DeepSeek Harness Desktop **0.2.0-rc.1**, **0.2.0-rc.2**, and **0.2.1-rc.2**. Node.js **>=22.19** and pnpm **10.34.6** are source development/build requirements, not plugin runtime prerequisites. Compatibility beyond the declared host versions is not claimed.
50
+
51
+ **Contents:** [Quick evaluation](#quick-evaluation) · [Capabilities](#capabilities) · [Install](#install) · [Workbench](#workbench) · [Architecture](#architecture) · [Session tools](#session-tools) · [Official tools](#official-tools) · [Team lifecycle](#team-lifecycle) · [Costs](#costs) · [Routing algorithm](#routing) · [Data](#data) · [Development](#development) · [Troubleshooting](#troubleshooting)
14
52
 
15
53
  <a id="capabilities"></a>
16
54
 
@@ -40,16 +78,16 @@ Generating a route recommendation is local: it does not start a vendor CLI or ma
40
78
  Open **Plugins → Add plugin** and enter:
41
79
 
42
80
  ```text
43
- @ljwei-stak/dsh-model-router@0.22.2
81
+ @ljwei-stak/dsh-model-router@0.23.1
44
82
  ```
45
83
 
46
- Use the official HTTPS npm source `https://registry.npmjs.org/` when a mirror lacks that exact version. Enable the plugin, then fully quit Harness (including its tray process) and restart it. Plugin details should show **0.22.2** and the sidebar should contain **模型路由** (Model Router). A global `npm install -g` alone does not register a Desktop plugin.
84
+ Use the official HTTPS npm source `https://registry.npmjs.org/` when a mirror lacks that exact version. Enable the plugin, then fully quit Harness (including its tray process) and restart it. Plugin details should show **0.23.1** and the sidebar should contain **模型路由** (Model Router). A global `npm install -g` alone does not register a Desktop plugin.
47
85
 
48
86
  You can instead install a local `.tgz` archive or its extracted inner `package` directory through the plugin manager. Both must contain `package.json` and `.dsh-plugin`. See [releases](https://github.com/Alice-Marx/dsh-model-router/releases) for archived builds, or build your own checkout below.
49
87
 
50
88
  ### Upgrade from the old package name
51
89
 
52
- The old name `@ljwei-stak/model-router-galgame` ended at 0.13.0. Remove that plugin entry while retaining your profile/application data, then add `@ljwei-stak/dsh-model-router@0.22.2`. Do not keep both entries installed: they share the internal plugin id `model-router-galgame` and tool names. The retained id lets existing router settings and history carry over.
90
+ The old name `@ljwei-stak/model-router-galgame` ended at 0.13.0. Remove that plugin entry while retaining your profile/application data, then add `@ljwei-stak/dsh-model-router@0.23.1`. Do not keep both entries installed: they share the internal plugin id `model-router-galgame` and tool names. The retained id lets existing router settings and history carry over.
53
91
 
54
92
  For the combined 0.11.x package, back up the profile and export the GAL saves you want to preserve before upgrading. Story playback, artwork, music and saves now belong to the independent `@ljwei-stak/dsh-galgame` package. The detailed save limitations are in [MIGRATION.md](docs/archive/MIGRATION.md).
55
93
 
@@ -79,6 +117,10 @@ Switching tabs keeps the panels mounted, preserving in-progress installation and
79
117
  5. In **在工作台执行**, choose **模型路由(只读)** for the existing routed executor, or first select **团队分工** and choose **官方工具协作**. For official teamwork, select **只读检查** (read-only) or **编辑文件** (edit files). Editable teams also choose **Harness 受限环境** (Harness sandbox, the default) or **厂商 CLI 正常环境** (normal vendor environment, outside the Harness sandbox). Enter an existing absolute workspace path, or reuse the last run's path. Click **预览执行计划**, review each package, its permissions and required confirmations, then explicitly confirm execution. The Host rechecks the plan before starting; changed conditions can require confirmation again.
80
118
  6. Inspect **执行记录与子任务** for the actual channel, response, failures, cost and ratings. Official teamwork replans using only installed CLIs ready for the selected permission mode. Edits require approval and a clean Git repository; successful worktree changes are conditionally integrated. An unavailable or failed CLI never silently becomes an API call in this mode.
81
119
 
120
+ After execution starts, the workbench shows elapsed time, the current package and each package's state. **取消执行** requests cancellation of the active call and stops later packages from starting; **正在取消** remains visible until the executor finishes cleanup. Completed results and reported usage remain in history. Cancelling cannot undo charges already incurred or changes already integrated. Closing the panel does not cancel a run; reopening it in the same browser session can reconnect while the Host retains that job. Host restarts clear these in-memory jobs, and completed ledger records remain available.
121
+
122
+ Preview, progress and cancellation requests stop waiting after 15 seconds. Progress reads retry against the same job; an unacknowledged cancellation can be retried explicitly. A transport timeout does not establish that the Host task stopped and never automatically resubmits `startRun`. Before retrying a recorded step, budget checks include that step and all unfinished downstream packages that will execute; reassignment uses the selected route estimate and unknown prices remain unknown. The [timeout recovery screenshot](docs/assets/workbench-run-timeout.png) is an offline simulation.
123
+
82
124
  Changing task text, planning mode, profiles, directory or relevant settings invalidates the prior recommendation. Changing execution inputs also discards the pending or completed preview, so execution requires a new preview of the current request. Previewing execution uses current Host state and can differ from a previous local plan.
83
125
 
84
126
  Example task:
@@ -151,6 +193,8 @@ Replace the workspace with an existing clean Git root. Call `previewRun(request)
151
193
 
152
194
  Native editing normally requires `workspace-write` and `unsandboxed`; an exceeded budget adds `over-budget`. Do not hardcode this list or populate it without consent. `startRun` replans and repeats preflight; it does not consume a frozen preview token. New confirmation requirements return `needs-confirmation` with `missing` and updated reasons. Changing task, mode, workspace or profiles requires a fresh preview. Workbench requests use saved route `cliModel` mappings; temporary `cliModelsJson` bindings belong to the session tool below.
153
195
 
196
+ An accepted `startRun` returns a job snapshot with `jobId`, `status` and `progress` before execution finishes. Poll `runStatus({ jobId })`; request cancellation with `cancelRun({ jobId })`. Job states are `running`, `cancelling`, `completed`, `cancelled` or `failed`. A terminal snapshot's `result` contains the final workbench response and ledger `runId` when saved. `completed` means the worker settled; inspect `result.status` for the actual task outcome. Progress contains package identities and states, not answer text. A failed poll does not establish that execution stopped, and clients must not retry `startRun` automatically.
197
+
154
198
  <a id="session-tools"></a>
155
199
 
156
200
  ## Session tools
package/README.zh.md CHANGED
@@ -2,15 +2,53 @@
2
2
 
3
3
  [English](README.md) · [简体中文](README.zh.md)
4
4
 
5
+ [![npm 版本](https://flat.badgen.net/npm/v/@ljwei-stak/dsh-model-router)](https://www.npmjs.com/package/@ljwei-stak/dsh-model-router) [![dshfind](https://dshfind.com/api/badge/alice-marx/dsh-model-router?lang=zh)](https://dshfind.com/zh/plugins/alice-marx/dsh-model-router?ref=badge) [![已收录至 DSH 插件目录](https://dsh-plugin.org/badges/listed.svg)](https://dsh-plugin.org/plugins/alice-marx/dsh-model-router) [![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com)
6
+
7
+ [![dshfind](https://dshfind.com/api/card/Alice-Marx/dsh-model-router?lang=zh)](https://dshfind.com/zh/plugins/Alice-Marx/dsh-model-router?ref=badge)
8
+
9
+ 想跟进版本更新?点右上角 **Watch → Custom → 勾选 Releases**(不必 Watching 全部活动)。
10
+
5
11
  面向 **DeepSeek Harness Desktop** 的成本感知模型路由工作台。在本地规划任务、比较已配置路线、检查执行权限,并通过模型 API 或支持的厂商官方 CLI 执行有依赖关系的工作包。
6
12
 
7
- 当前版本:**0.22.2**(`@ljwei-stak/dsh-model-router`)。新增“按算法推荐分配 / 手动调整分配”:拆分后的工作包可逐个指定模型 API 或官方工具,保留依赖、重新预览费用,并保存重跑目标。同时接入任务相关质量证据、模型与推理档位联合选择、历史资源 P90 估计与依赖感知搜索。手动混合执行当前只读;官方工具使用自身默认模型,原有官方团队编辑流程保留。继续兼容 **DeepSeek Harness Desktop 0.2.0-rc.1 / 0.2.0-rc.2 / 0.2.1-rc.2**,沿用 Hermes/Pi 安装与终端支持;两者仍无经过验证的托管执行器。真实厂商执行证据仍见 **0.20.0** 报告,本版未新增模型实测。路由器与 [GAL](https://github.com/Alice-Marx/deepseek-harness-galgame) 自 0.12.0 起已拆分为独立插件。
13
+ 当前版本:**0.23.1**(`@ljwei-stak/dsh-model-router`)。重跑预算现在覆盖实际会执行的未完成下游,进度和取消请求挂起后可恢复。工作台支持实时耗时、逐工作包进度和取消,保留已完成结果与已报告费用;关闭重开面板可接管原任务。支持“按算法推荐分配 / 手动调整分配”:拆分后的工作包可逐个指定模型 API 或官方工具,保留依赖、重新预览费用,并保存重跑目标。同时接入任务相关质量证据、模型与推理档位联合选择、历史资源 P90 估计与依赖感知搜索。手动混合执行当前只读;官方工具使用自身默认模型,原有官方团队编辑流程保留。继续兼容 **DeepSeek Harness Desktop 0.2.0-rc.1 / 0.2.0-rc.2 / 0.2.1-rc.2**,沿用 Hermes/Pi 安装与终端支持;两者仍无经过验证的托管执行器。真实厂商执行证据仍见 **0.20.0** 报告,本版未新增模型实测。路由器与 [GAL](https://github.com/Alice-Marx/deepseek-harness-galgame) 自 0.12.0 起已拆分为独立插件。
8
14
 
9
15
  官方 CLI 协作按依赖顺序逐包执行、传递依赖上下文,不会自动回退到模型目录 API。可编辑团队需明确选择 Harness 沙箱或厂商 CLI 正常环境。后者通过 Git worktree 收集修改,但不限制操作系统访问。未显式设置模型映射时,各 CLI 沿用自身配置的模型。参见[真实 Windows 验证及其局限](docs/releases/0.20.0-validation.md)。
10
16
 
11
17
  两个 README 版本同步说明接口、公式、默认值与限制。[更新日志](CHANGELOG.md)保留版本历史。更多操作说明:[工作台](docs/WORKBENCH_USER_GUIDE.zh.md)、[安装](docs/INSTALLATION_GUIDE.zh.md)、[迁移](docs/archive/MIGRATION.md)、[实验规程](docs/ROUTING_SELFTEST_STEPS.zh.md)。
12
18
 
13
- **目录:** [功能](#capabilities) · [安装](#install) · [工作台](#workbench) · [架构](#architecture) · [会话工具](#session-tools) · [官方工具](#official-tools) · [团队生命周期](#team-lifecycle) · [费用](#costs) · [路由算法](#routing) · [数据](#data) · [开发](#development) · [排障](#troubleshooting)
19
+ <a id="quick-evaluation"></a>
20
+
21
+ ## 提交前快速评估
22
+
23
+ **一句话价值:** 面向 DSH 的成本感知模型路由工作台,可本地规划任务、选择模型路线,并在明确权限下执行有依赖关系的工作包。
24
+
25
+ **类别:** **模型与推理(Models & Reasoning)**,是[社区目录分类](https://www.alldsh.com/categories/)中最贴切的一类;这是社区分类,并非 DSH 官方分类。
26
+
27
+ **安装(DSH Plugin Hub / web profile):** 确保 DSH CLI 可用,然后安装已发布的 npm 包:
28
+
29
+ ```sh
30
+ dsh plugin --profile web add @ljwei-stak/dsh-model-router@0.23.1
31
+ ```
32
+
33
+ **安装到 Harness Desktop:** 首次先启动一次 Desktop 初始化 `desktop` 配置,再彻底退出 Desktop,运行:
34
+
35
+ ```sh
36
+ dsh plugin --profile desktop add @ljwei-stak/dsh-model-router@0.23.1
37
+ ```
38
+
39
+ 也可以在 **Harness Desktop → 插件 → 添加插件** 中粘贴包名。安装后重启 Desktop。单独运行全局 `npm install -g` 不会注册 Desktop 插件。
40
+
41
+ **发布包与入口:** [npm 上的 `@ljwei-stak/dsh-model-router@0.23.1`](https://www.npmjs.com/package/@ljwei-stak/dsh-model-router) 已发布。根目录[`package.json`](package.json)声明包名和 bundle patch;[`.dsh-plugin/index.mjs`](.dsh-plugin/index.mjs)导出所需的 `apply(ctx)` 入口。[许可证:MIT](LICENSE)。
42
+
43
+ **配置文件与配置入口:** 当前不支持导入用户编写的 JSON/YAML 配置文件。Provider、模型和凭据在 Harness **模型**页面配置;路由档案与偏好在插件设置中配置。路由器持久状态存于 `$DSH_HOME/model-router/state.json`(默认:`~/.dsh/model-router/state.json`)。Provider CLI 沿用各自的登录和配置;路由器不会把 API 密钥复制进模型档案或终端日志。
44
+
45
+ **截图与证据:** [工作台截图](docs/assets/workbench-task-preview.jpg)使用固定的模型、价格和健康状态数据,是模拟 UI 预览,不是实时 Harness 或模型运行。[路由流程图](docs/assets/routing-workflow.svg)展示执行路径。[真实 Windows 验证报告](docs/releases/0.20.0-validation.md)记录了 0.20.0 的具体执行及局限;0.23.1 尚无经过验证的端到端厂商运行。[实时进度截图](docs/assets/workbench-run-progress.png)是离线模拟;取消、重开恢复与验证边界见[0.23.1 验收报告](docs/releases/0.23.1-validation.md)。
46
+
47
+ **权限与风险:** 任务规划在本地完成,不调用模型。实际执行会把选定任务和上下文发送到所选模型 API 或厂商 CLI,并可能产生费用。默认使用 Harness 受限环境;只读运行不修改工作区,`workspace-write` 需要用户明确批准。明确选择 `cli-native` 的可编辑团队会在 Harness 进程沙箱之外,以当前操作系统用户权限运行;Git worktree 只收集修改,不限制操作系统访问。外部 Provider 和 CLI 服务各自适用其数据处理及计费规则。
48
+
49
+ **兼容性:** 声明支持 DeepSeek Harness Desktop **0.2.0-rc.1**、**0.2.0-rc.2**、**0.2.1-rc.2**。Node.js **>=22.19** 与 pnpm **10.34.6** 仅为源码开发/构建要求,不是插件运行时前置条件;不承诺超出上述 Host 版本的兼容性。
50
+
51
+ **目录:** [快速评估](#quick-evaluation) · [功能](#capabilities) · [安装](#install) · [工作台](#workbench) · [架构](#architecture) · [会话工具](#session-tools) · [官方工具](#official-tools) · [团队生命周期](#team-lifecycle) · [费用](#costs) · [路由算法](#routing) · [数据](#data) · [开发](#development) · [排障](#troubleshooting)
14
52
 
15
53
  <a id="capabilities"></a>
16
54
 
@@ -40,16 +78,16 @@
40
78
  打开 **插件 → 添加插件**,输入:
41
79
 
42
80
  ```text
43
- @ljwei-stak/dsh-model-router@0.22.2
81
+ @ljwei-stak/dsh-model-router@0.23.1
44
82
  ```
45
83
 
46
- 镜像缺少该精确版本时,使用官方 HTTPS npm 源 `https://registry.npmjs.org/`。启用插件,然后完全退出 Harness(包括托盘进程)并重启。插件详情应显示 **0.22.2**,侧栏应出现 **模型路由**。仅执行全局 `npm install -g` 不会把插件注册到 Desktop。
84
+ 镜像缺少该精确版本时,使用官方 HTTPS npm 源 `https://registry.npmjs.org/`。启用插件,然后完全退出 Harness(包括托盘进程)并重启。插件详情应显示 **0.23.1**,侧栏应出现 **模型路由**。仅执行全局 `npm install -g` 不会把插件注册到 Desktop。
47
85
 
48
86
  也可以通过插件管理器安装本地 `.tgz` 压缩包,或解压后的内部 `package` 目录。两者都必须包含 `package.json` 和 `.dsh-plugin`。历史构建见 [Releases](https://github.com/Alice-Marx/dsh-model-router/releases),也可按下文从源码构建。
49
87
 
50
88
  ### 从旧包名升级
51
89
 
52
- 旧名称 `@ljwei-stak/model-router-galgame` 停留在 0.13.0。保留配置档/应用数据,移除旧插件条目,再添加 `@ljwei-stak/dsh-model-router@0.22.2`。不要同时保留两条插件:它们共用内部插件 id `model-router-galgame` 和工具名。保留该 id 能让现有路由设置与历史记录继续使用。
90
+ 旧名称 `@ljwei-stak/model-router-galgame` 停留在 0.13.0。保留配置档/应用数据,移除旧插件条目,再添加 `@ljwei-stak/dsh-model-router@0.23.1`。不要同时保留两条插件:它们共用内部插件 id `model-router-galgame` 和工具名。保留该 id 能让现有路由设置与历史记录继续使用。
53
91
 
54
92
  从合包 0.11.x 升级前,先备份配置档,并导出需要保留的 GAL 存档。剧情播放、立绘、音乐与存档现属于独立的 `@ljwei-stak/dsh-galgame`。详细存档限制见 [MIGRATION.md](docs/archive/MIGRATION.md)。
55
93
 
@@ -79,6 +117,10 @@
79
117
  5. 在 **在工作台执行** 中,选择 **模型路由(只读)** 使用现有路由执行器;或先选择 **团队分工**,再选择 **官方工具协作**。官方协作可选择 **只读检查** 或 **编辑文件**。可编辑团队还需选择 **Harness 受限环境**(默认沙箱)或 **厂商 CLI 正常环境**(在 Harness 沙箱外运行)。填入已存在的工作区绝对路径,或沿用上次运行路径。点击 **预览执行计划**,查看各包、权限与所需确认,然后显式确认执行。Host 启动前会重新检查计划;条件变化可能要求重新确认。
80
118
  6. 在 **执行记录与子任务** 查看实际通道、回答、失败、费用与评分。官方协作仅使用已安装、且在所选权限模式下就绪的 CLI 重新规划。编辑需要批准和干净的 Git 仓库;成功的 worktree 修改有条件地合入。该模式下,不可用或失败的 CLI 不会静默变成 API 调用。
81
119
 
120
+ 执行开始后,工作台显示已耗时、当前工作包与每包状态。**取消执行**会请求停止当前调用,并阻止后续工作包启动;执行器完成收尾前持续显示**正在取消**。已完成结果和已报告用量仍保留在历史中。取消不能撤销已经产生的费用或已经整合的改动。关闭面板不会取消执行;同一浏览器会话重新打开后,可在 Host 仍保留该任务时重新连接。Host 重启会清除内存任务,已保存的历史记录仍可查看。
121
+
122
+ 预览、进度和取消请求最长等待 15 秒。进度读取超时后继续查询同一任务;取消未确认时可手动重试。网络等待超时不代表 Host 任务已停止,也不会自动重发 `startRun`。重跑历史步骤前,预算覆盖该步及实际会执行的全部未完成下游;改派使用所选路线估价,未知价格仍保持未知。[超时恢复截图](docs/assets/workbench-run-timeout.png)为离线模拟。
123
+
82
124
  修改任务文本、规划模式、路线配置、目录或相关设置,会使旧建议失效。修改执行输入也会丢弃待确认或已完成的预览,必须对当前请求重新预览。执行预览使用 Host 当前状态,因此可能与先前本地计划不同。
83
125
 
84
126
  任务示例:
@@ -151,6 +193,8 @@ flowchart TD
151
193
 
152
194
  正常环境编辑通常要求 `workspace-write` 和 `unsandboxed` 确认;超预算还会增加 `over-budget`。不要硬编码该列表,也不要未经同意自动填入。`startRun` 会重新规划和预检,不会消费一个冻结的预览令牌。出现新的确认要求时,返回 `needs-confirmation`,并给出 `missing` 与更新后的原因。修改任务、模式、工作区或路线配置后必须重新预览。工作台请求使用已保存路线的 `cliModel` 映射;临时 `cliModelsJson` 绑定属于下文的会话工具。
153
195
 
196
+ `startRun` 接受执行后,在任务完成前返回含 `jobId`、`status` 和 `progress` 的任务快照。通过 `runStatus({ jobId })` 读取状态,通过 `cancelRun({ jobId })` 请求取消。任务状态为 `running`、`cancelling`、`completed`、`cancelled` 或 `failed`。终态快照的 `result` 包含最终工作台响应,以及成功保存后的账本 `runId`。`completed` 表示后台执行已结束,任务实际结果仍查看 `result.status`。进度仅包含工作包身份与状态,不包含回答正文。读取状态失败不代表执行已停止,客户端不得因此自动重发 `startRun`。
197
+
154
198
  <a id="session-tools"></a>
155
199
 
156
200
  ## 会话工具
@@ -1,6 +1,6 @@
1
1
  # 模型路由工作台:配置、规划与实际执行
2
2
 
3
- 适用版本:`@ljwei-stak/dsh-model-router@0.22.2`。声明兼容 DeepSeek Harness Desktop `0.2.0-rc.1 / 0.2.0-rc.2 / 0.2.1-rc.2`。安装与开发见[中文 README](../README.zh.md),旧包迁移见[迁移说明](archive/MIGRATION.md)。
3
+ 适用版本:`@ljwei-stak/dsh-model-router@0.23.1`。声明兼容 DeepSeek Harness Desktop `0.2.0-rc.1 / 0.2.0-rc.2 / 0.2.1-rc.2`。安装与开发见[中文 README](../README.zh.md),旧包迁移见[迁移说明](archive/MIGRATION.md)。
4
4
 
5
5
  0.22.0 新增“按算法推荐分配 / 手动调整分配”:生成任务建议后,可为每个工作包选择模型 API 或官方工具;修改后重新预览费用与渠道,再确认执行。任务依赖与重跑目标保留。手动混合执行当前只读,官方工具使用自身默认模型;编辑文件继续选择官方工具协作。详见[手动分配说明](MANUAL_TASK_ASSIGNMENT.zh.md)。Hermes 自定义程序目录、原配置/登录/历史位置,以及工具页顶部终端的行为继续保留。
6
6
 
@@ -93,6 +93,10 @@ Windows 独立工作树保存在用户私有临时目录中,按原仓库路径
93
93
  4. 明确确认执行后才调用模型/CLI。期间状态变化可能导致宿主拒绝旧确认,需重新审阅。
94
94
  5. 在下方**执行记录与子任务**看结果、状态、实际渠道、原始失败原因和费用。实际运行可能计费。
95
95
 
96
+ 执行期间会显示当前工作包、逐包状态、已处理数量和已耗时。点击**取消执行**后,先显示**正在取消**;后台收到请求后停止当前调用,等待进程或模型流收尾,再保存已完成结果、已报告用量和最终状态。取消不会撤销已经产生的费用;可编辑团队尚未整合时会保留独立工作树,取消后不开始新的整合步骤,已经完成的整合无法通过取消撤回。
97
+
98
+ 状态读取失败时保留当前任务并提示重试,不自动再次启动执行。关闭工作台仅停止界面读取;同一浏览器会话重新打开时可恢复仍在 Host 内存中的任务。Host 重启或任务快照被清理后,界面会提示查看执行记录,不能恢复尚未保存的运行过程。浏览器只保存任务 ID,不保存任务正文、回答或凭据。
99
+
96
100
  修改任务、规划模式、执行方式、任务权限、路由方案或工作区等输入后,正在请求或已经返回的旧预览会被丢弃。重新预览并核对当前请求后才能确认执行。官方协作不代替缺失的 CLI,不支持当前权限的工具也不会进入该模式的计划。
97
101
 
98
102
  **预览不会启动模型,确认会启动。** 模型 API 将任务文本发给供应商;CLI 可能读取工作区或其他其权限允许的文件。只读不等于所有 CLI 只能读取工作区,先看**预算与安全 → 安全边界**。
@@ -281,3 +285,5 @@ Windows Codex 在 Harness 环境仅支持只读:其原 `CODEX_HOME` 状态仍
281
285
  | 工具名在会话中不可用 | 确认插件启用,重新开始会话;记录宿主/插件版本与脱敏错误。 |
282
286
 
283
287
  问题反馈请附宿主/插件版本、执行模式、工具状态及脱敏错误,勿提供密钥。源码、安装和资料入口见[中文 README](../README.zh.md)。
288
+
289
+ 执行恢复:预览、进度与取消请求等待超过15秒会终止该次网络等待;进度会重试同一任务,取消未确认可手动重试,任务不会自动重新启动。历史步骤重跑前,预算按目标及未成功的下游步骤合计;整组超限时先确认再运行,改派按所选路线重估,缺失价格不视为零费用。
@@ -0,0 +1,30 @@
1
+ # 0.23.0 release acceptance
2
+
3
+ Date: 2026-10-23. Package: `@ljwei-stak/dsh-model-router@0.23.0`.
4
+
5
+ This release adds observable workbench execution and cancellation, retaining the plugin identity, saved configuration, execution history and supported DSH SDK versions. It also fixes the incomplete-history marker after a failed corrupt-state recovery.
6
+
7
+ ## Accepted behavior
8
+
9
+ - `startRun` returns a Host job snapshot after fresh preview, budget and permission checks; strict `runStatus` and `cancelRun` requests accept only a job ID. Existing internal synchronous execution helpers remain available.
10
+ - The workbench shows elapsed time, current package and individual package states. Status transport failures retry serially without losing cancellation. Old status/cancel responses cannot replace newer progress, a terminal result or another job.
11
+ - Cancellation reaches the same AbortSignal used by API streams and official CLI teams. The job stays cancelling until execution and cleanup settle. Already completed answers and reported usage/cost remain in the execution ledger, including cancellation during optional result review. Cancelled editable teams do not begin integration.
12
+ - Closing the panel does not cancel execution. Reopening in the same browser reconnects using only an opaque job ID in session storage. Reopening before the start acknowledgement waits for and takes over the original job, preventing duplicate launch. A failed launch is never automatically resubmitted.
13
+ - If corrupt-state recovery fails before it can commit a notice, a later read/write preserves the incomplete-history marker. Budget checks continue to treat lost cost history as incomplete.
14
+
15
+ ## Validation
16
+
17
+ - Full suite: 1035 tests, 1032 passed, 0 failed, 3 existing platform-dependent skips. Skipped PTY/process checks do not count as execution acceptance.
18
+ - Fifty new feature tests cover job lifecycle and capacity, a real Host and typed gateway with inert adapters, execution progress, cancellation accounting, optional review cancellation and UI races/reconnection.
19
+ - The strict frozen pnpm installation, actual client build, generated-client consistency, paired README checks, workbench preview compilation and `git diff --check` passed. README formulas were not changed.
20
+ - Browser inspection of the actual workbench component with offline fixtures covered running progress, cancelling cleanup and the final cancelled ledger. Screenshots were inspected; browser warning/error logs were empty. The [progress screenshot](../assets/workbench-run-progress.png) uses simulated tasks and prices.
21
+ - An independent GPT read-only review checked the runtime package file/import closure, strict RPCs, cancellation/accounting paths, sensitive-token patterns and the pending-start panel recovery race.
22
+ - The release process compares every package member against the exact release commit's Git blobs, then downloads the npm payload and GitHub Release asset to verify their bytes. Commit IDs and tarball hashes are recorded in the external publication audit.
23
+
24
+ ## Boundaries
25
+
26
+ Jobs are held in Host memory, with at most four active jobs and fifty retained snapshots. A Host restart loses live job snapshots; the workbench directs users to the persisted execution history instead of starting another job. Host disposal requests cancellation; a running adapter must honor its AbortSignal before cancellation can finish. Costs already incurred remain chargeable, and unavailable prices/usage remain unknown.
27
+
28
+ This release was not tested end to end in an installed DSH Desktop or with real paid provider/vendor CLI inference. No new provider execution claims are made. Earlier real execution evidence remains in the dated [0.20.0 report](0.20.0-validation.md), and Hermes/Pi managed execution remains unverified.
29
+
30
+ Upgrade by fully quitting Harness, including its tray and Host process, updating the plugin to 0.23.0, and restarting.
@@ -0,0 +1,23 @@
1
+ # 0.23.1 release acceptance
2
+
3
+ Date: 2026-10-23. Package: `@ljwei-stak/dsh-model-router@0.23.1`.
4
+
5
+ ## Reproduced problems and fixes
6
+
7
+ - A retry could pass a daily budget of USD 0.5 with a USD 0.2 target, then also start its USD 0.4 downstream package without approval. Retry budgets now cover the target plus every unfinished downstream package that will execute. Both routed and official-team retries use that same set; completed downstream packages are excluded. Changed routes use the selected route estimate, unchanged routes retain their planned reasoning effort estimate and unknown prices remain unknown under the existing unknown-price budget policy.
8
+ - A never-settling progress request permanently stopped polling; a never-settling cancel request permanently disabled the cancel button. Preview/progress/cancel now have a 15-second deadline and a real SDK transport AbortSignal outside the business JSON. Progress retries use the same job, cancellation can be retried explicitly and late replies cannot replace newer state. Task starts are never automatically replayed.
9
+
10
+ ## Validation
11
+
12
+ - Full suite: 1055 tests, 1052 passed, 0 failed, 3 existing platform-dependent skips.
13
+ - Twenty new regressions: 11 offline Host retry-budget tests, 6 actual SDK transport tests and 3 hung-request UI tests. Budget rejection asserts zero model calls; approval, transitive downstream, completed-package exclusion, reassignment, planned effort and unknown prices are covered.
14
+ - Strict frozen pnpm installation, actual client build, generated-client consistency, README translation/formula checks, workbench preview compilation, npm dry-run and diff checks passed. Formula content was not changed.
15
+ - Actual workbench browser inspection with the offline hanging-request scenario confirmed resumed package progress after a stalled status read, a re-enabled cancel button with explicit failure feedback, and successful retry cancellation with completed results retained. Browser warning/error logs were empty. The [timeout screenshot](../assets/workbench-run-timeout.png) is simulated, with no model, credentials or real workspace access.
16
+ - An independent GPT review checked estimate/actual-retry set consistency, reasoning effort, unknown prices, SDK signal wiring, cleanup and late-response fences.
17
+ - Every packaged file is checked against the exact release commit's Git blobs; npm and GitHub Release assets are anonymously downloaded for byte comparison, with digests in the external publication audit.
18
+
19
+ ## Boundaries
20
+
21
+ A transport deadline only stops waiting for the request. It does not prove that a cancellation reached Host or that the task stopped; the UI keeps the original job and checks status. Execution/cancellation cleanup can exceed 15 seconds, and a pending start is not retried automatically. Jobs remain in Host memory and do not survive a Host restart. Reported costs already incurred remain chargeable.
22
+
23
+ No real paid-provider or vendor CLI inference or installed-DSH end-to-end acceptance was performed in this patch. Prior execution evidence remains dated in the [0.20.0 report](0.20.0-validation.md). Fully quit Harness, including tray/Host processes, update the plugin, then restart.