@ljwei-stak/dsh-model-router 0.23.0 → 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.
@@ -4030,6 +4030,26 @@ var STAGE_TEXT = Object.freeze({
4030
4030
  });
4031
4031
  var ACTIVE_JOB_KEY = "dsh-model-router:active-run-job";
4032
4032
  var isJobSnapshot = (value) => Boolean(text2(value?.jobId) && JOB_STATUSES.has(value.status));
4033
+ var REMOTE_WAIT_MS = 15e3;
4034
+ async function waitForRemote(operation, controllers) {
4035
+ const controller = new AbortController();
4036
+ controllers.add(controller);
4037
+ let timer;
4038
+ try {
4039
+ return await new Promise((resolve, reject) => {
4040
+ controller.signal.onabort = () => {
4041
+ clearTimeout(timer);
4042
+ reject(new Error("\u5DE5\u4F5C\u53F0\u8BF7\u6C42\u7B49\u5F85\u8D85\u65F6\uFF0C\u8BF7\u5728\u8FDE\u63A5\u6062\u590D\u540E\u91CD\u8BD5\u3002"));
4043
+ };
4044
+ timer = setTimeout(() => controller.abort(), REMOTE_WAIT_MS);
4045
+ Promise.resolve(operation(controller.signal)).then(resolve, reject);
4046
+ });
4047
+ } finally {
4048
+ clearTimeout(timer);
4049
+ controller.signal.onabort = null;
4050
+ controllers.delete(controller);
4051
+ }
4052
+ }
4033
4053
  function savedJob() {
4034
4054
  try {
4035
4055
  return typeof sessionStorage === "undefined" ? "" : text2(sessionStorage.getItem(ACTIVE_JOB_KEY));
@@ -4084,6 +4104,7 @@ function RunLauncher({ task, mode, directRoute, budgetUsd, defaultPreset, ledger
4084
4104
  const launchSequence = import_react3.default.useRef(0);
4085
4105
  const launching = import_react3.default.useRef(false);
4086
4106
  const awaitingLaunch = import_react3.default.useRef(null);
4107
+ const pendingRemotes = import_react3.default.useRef(/* @__PURE__ */ new Set());
4087
4108
  const onFinished = import_react3.default.useRef(onStarted);
4088
4109
  onFinished.current = onStarted;
4089
4110
  const inputSignature = JSON.stringify([task, mode, directRoute?.provider, directRoute?.model, budgetUsd, runPreset, workspace, lastWorkspace, planningRevision, disabledReason, runExecutor, runMode, runEnvironment, assignmentMode, packagePlanKey2, packageAssignments]);
@@ -4140,6 +4161,8 @@ function RunLauncher({ task, mode, directRoute, budgetUsd, defaultPreset, ledger
4140
4161
  }
4141
4162
  return () => {
4142
4163
  mounted.current = false;
4164
+ for (const controller of pendingRemotes.current) controller.abort();
4165
+ pendingRemotes.current.clear();
4143
4166
  launchListeners.delete(receiveLaunch);
4144
4167
  pendingRequest.current += 1;
4145
4168
  activeRun.current = null;
@@ -4217,7 +4240,7 @@ function RunLauncher({ task, mode, directRoute, budgetUsd, defaultPreset, ledger
4217
4240
  if (disposed || activeRun.current !== active || active.terminal) return;
4218
4241
  const sequence = ++active.requestSequence;
4219
4242
  try {
4220
- const value = unwrapRemote(await runStatus({ jobId: runningJobId }), "\u6682\u65F6\u65E0\u6CD5\u8BFB\u53D6\u6267\u884C\u8FDB\u5EA6\u3002");
4243
+ const value = unwrapRemote(await waitForRemote((signal) => runStatus({ jobId: runningJobId }, signal), pendingRemotes.current), "\u6682\u65F6\u65E0\u6CD5\u8BFB\u53D6\u6267\u884C\u8FDB\u5EA6\u3002");
4221
4244
  if (disposed) return;
4222
4245
  if (!isJobSnapshot(value) || value.jobId !== runningJobId) throw new Error("\u6682\u65F6\u65E0\u6CD5\u8BFB\u53D6\u6267\u884C\u8FDB\u5EA6\u3002");
4223
4246
  acceptSnapshot(value, active.token, sequence);
@@ -4281,7 +4304,7 @@ function RunLauncher({ task, mode, directRoute, budgetUsd, defaultPreset, ledger
4281
4304
  const signature2 = inputSignature;
4282
4305
  setPhase({ kind: "previewing" });
4283
4306
  try {
4284
- const value = unwrapRemote(await previewRun(snapshot), "\u6267\u884C\u9884\u89C8\u5931\u8D25\u3002");
4307
+ const value = unwrapRemote(await waitForRemote((signal) => previewRun(snapshot, signal), pendingRemotes.current), "\u6267\u884C\u9884\u89C8\u5931\u8D25\u3002");
4285
4308
  if (mounted.current && current === pendingRequest.current && signature2 === latestSignature.current) setPhase({ kind: "preview", value, request: snapshot, signature: signature2 });
4286
4309
  } catch (error) {
4287
4310
  if (mounted.current && current === pendingRequest.current && signature2 === latestSignature.current) setPhase({ kind: "error", message: text2(error?.message) || "\u6267\u884C\u9884\u89C8\u5931\u8D25\u3002" });
@@ -4342,7 +4365,7 @@ function RunLauncher({ task, mode, directRoute, budgetUsd, defaultPreset, ledger
4342
4365
  const sequence = ++active.requestSequence;
4343
4366
  setPhase((previous) => previous.kind === "running" ? { ...previous, cancelPending: true, cancelError: "" } : previous);
4344
4367
  try {
4345
- const value = unwrapRemote(await cancelRun({ jobId: active.jobId }), "\u53D6\u6D88\u8BF7\u6C42\u672A\u786E\u8BA4\u3002");
4368
+ const value = unwrapRemote(await waitForRemote((signal) => cancelRun({ jobId: active.jobId }, signal), pendingRemotes.current), "\u53D6\u6D88\u8BF7\u6C42\u672A\u786E\u8BA4\u3002");
4346
4369
  if (!isJobSnapshot(value) || value.jobId !== active.jobId) throw new Error("\u53D6\u6D88\u8BF7\u6C42\u672A\u786E\u8BA4\u3002");
4347
4370
  acceptSnapshot(value, active.token, sequence, true);
4348
4371
  } catch {
@@ -13863,7 +13886,7 @@ var xterm_default = `/**
13863
13886
  `;
13864
13887
 
13865
13888
  // .dsh-plugin/client/host-version.mjs
13866
- var ROUTER_CLIENT_VERSION = true ? "0.23.0" : "";
13889
+ var ROUTER_CLIENT_VERSION = true ? "0.23.1" : "";
13867
13890
  var STALE_HOST_MESSAGE = "\u63D2\u4EF6\u540E\u53F0\u7248\u672C\u8F83\u65E7\uFF0C\u8BF7\u5B8C\u5168\u9000\u51FA\u5E76\u91CD\u542F Harness\uFF08\u5305\u62EC\u6258\u76D8\u56FE\u6807\uFF09\u540E\u518D\u4F7F\u7528\u3002";
13868
13891
  function isMissingRemoteMethod(message) {
13869
13892
  const value = String(message ?? "");
@@ -15874,7 +15897,7 @@ var terminalWriteCodec = strictCodec(`${OFFICIAL_TOOLS_REMOTE_PACKAGE}#TerminalW
15874
15897
  var terminalResizeCodec = strictCodec(`${OFFICIAL_TOOLS_REMOTE_PACKAGE}#TerminalResize`, parseTerminalResize);
15875
15898
  var terminalStopCodec = strictCodec(`${OFFICIAL_TOOLS_REMOTE_PACKAGE}#TerminalStop`, parseTerminalStop);
15876
15899
  var runRequestCodec = strictCodec(`${OFFICIAL_TOOLS_REMOTE_PACKAGE}#RunRequest`, parseRunRequest);
15877
- function descriptor(method, parameters, result) {
15900
+ function descriptor(method, parameters, result, cancellable = false) {
15878
15901
  return Object.freeze({
15879
15902
  id: `${OFFICIAL_TOOLS_REMOTE_PACKAGE}#${OFFICIAL_TOOLS_REMOTE_NAMESPACE}/${method}`,
15880
15903
  service: OFFICIAL_TOOLS_REMOTE_NAMESPACE,
@@ -15882,7 +15905,8 @@ function descriptor(method, parameters, result) {
15882
15905
  method,
15883
15906
  invocation: { kind: "direct" },
15884
15907
  parameters,
15885
- result
15908
+ result,
15909
+ ...cancellable ? { cancellation: { parameter: "signal" } } : {}
15886
15910
  });
15887
15911
  }
15888
15912
  var toolIdParameter = Object.freeze({
@@ -15908,10 +15932,10 @@ var OFFICIAL_TOOLS_REMOTE_DESCRIPTORS = Object.freeze([
15908
15932
  descriptor("rerunStep", [jsonParameter("request", rerunRequestCodec)], anyObjectCodec("RerunResult")),
15909
15933
  descriptor("boundaries", [], anyObjectCodec("SecurityBoundaries")),
15910
15934
  // Workbench "start a run": plan preview with cost estimate and confirmation reasons, then execute.
15911
- descriptor("previewRun", [jsonParameter("request", runRequestCodec)], anyObjectCodec("RunPreview")),
15935
+ descriptor("previewRun", [jsonParameter("request", runRequestCodec)], anyObjectCodec("RunPreview"), true),
15912
15936
  descriptor("startRun", [jsonParameter("request", runRequestCodec)], anyObjectCodec("RunStarted")),
15913
- descriptor("runStatus", [jsonParameter("request", runJobRequestCodec)], anyObjectCodec("RunJobStatus")),
15914
- descriptor("cancelRun", [jsonParameter("request", runJobRequestCodec)], anyObjectCodec("RunJobStatus")),
15937
+ descriptor("runStatus", [jsonParameter("request", runJobRequestCodec)], anyObjectCodec("RunJobStatus"), true),
15938
+ descriptor("cancelRun", [jsonParameter("request", runJobRequestCodec)], anyObjectCodec("RunJobStatus"), true),
15915
15939
  // Workbench "官方工具终端": interactive shell / fixed official CLI sessions.
15916
15940
  // Output streams through long-poll reads; input and resize are unary calls.
15917
15941
  descriptor("terminalInfo", [], anyObjectCodec("TerminalInfo")),
@@ -16363,10 +16387,10 @@ function registerUi(ctx) {
16363
16387
  rateResult: (request) => officialToolsRemote.rateResult(request),
16364
16388
  rerunStep: (request) => officialToolsRemote.rerunStep(request),
16365
16389
  loadBoundaries: () => officialToolsRemote.boundaries(),
16366
- previewRun: (request) => officialToolsRemote.previewRun(request),
16390
+ previewRun: (request, signal) => officialToolsRemote.previewRun(request, signal),
16367
16391
  startRun: (request) => officialToolsRemote.startRun(request),
16368
- runStatus: (request) => officialToolsRemote.runStatus(request),
16369
- cancelRun: (request) => officialToolsRemote.cancelRun(request),
16392
+ runStatus: (request, signal) => officialToolsRemote.runStatus(request, signal),
16393
+ cancelRun: (request, signal) => officialToolsRemote.cancelRun(request, signal),
16370
16394
  terminalApi: Object.freeze({
16371
16395
  terminalInfo: () => officialToolsRemote.terminalInfo(),
16372
16396
  terminalStart: (request) => officialToolsRemote.terminalStart(request),
@@ -4,7 +4,7 @@ import { homedir } from 'node:os'
4
4
  import { isAbsolute } from 'node:path'
5
5
  import z from '@deepseek-ai/schemastery'
6
6
  import { defineTool } from '@deepseek-ai/dsh-tools'
7
- import { DEFAULT_ROUTER_SETTINGS, modelMetadata } from './shared/router.mjs'
7
+ import { DEFAULT_ROUTER_SETTINGS, estimateAssignedPackage, modelMetadata } from './shared/router.mjs'
8
8
  import { DEFAULT_ROUTING_PRESET, normalizeRoutingPreset, routingPreset } from './shared/routing-presets.mjs'
9
9
  import { HEALTH_CACHE_MS, apiKeyEnvPresent, healthCache, runHealthCheck, subscriptionLoginOf } from './shared/tool-health.mjs'
10
10
  import { latestVersions } from './shared/latest-versions.mjs'
@@ -995,6 +995,31 @@ export function rerunSupport(run) {
995
995
  return { ok: true, kind: run.kind === 'team' ? 'team' : 'assign', writes: false }
996
996
  }
997
997
 
998
+ /** Estimate exactly the packages a retry will start, retaining unknown prices. */
999
+ function rerunEstimates(run, ids, targetId, override, routes, dataOptions) {
1000
+ const estimates = new Map()
1001
+ for (const item of run.packages.filter(item => ids.has(item.id))) {
1002
+ const assigned = item.id === targetId ? override : null
1003
+ const provider = assigned?.provider ?? item.provider ?? item.recommendedProvider
1004
+ const model = assigned?.model ?? item.model ?? item.recommendedModel
1005
+ const changedRoute = provider !== item.recommendedProvider || model !== item.recommendedModel
1006
+ const reassignsRoute = assigned && (assigned.provider !== (item.provider ?? item.recommendedProvider)
1007
+ || assigned.model !== (item.model ?? item.recommendedModel))
1008
+ let estimate = item.estimatedCost
1009
+ // An unchanged route can retain its planned effort; changing identity uses
1010
+ // the new adapter's default effort, just like rerunAssignmentPackage.
1011
+ if (reassignsRoute || changedRoute) {
1012
+ const route = routes.find(route => route.provider === provider && route.model === model)
1013
+ estimate = route ? estimateAssignedPackage(route, item, run.task,
1014
+ run.decision?.complexity?.band ?? 'balanced', dataOptions).estimatedCost : null
1015
+ }
1016
+ estimates.set(item.id, Number.isFinite(estimate) && estimate >= 0 ? estimate : null)
1017
+ }
1018
+ const values = [...estimates.values()]
1019
+ const total = values.every(value => value !== null) ? values.reduce((sum, value) => sum + value, 0) : null
1020
+ return { byId: estimates, total: Number.isFinite(total) ? total : null }
1021
+ }
1022
+
998
1023
  export async function rerunRecordedStep(ctx, config = {}, { runId, packageId, provider, model, confirmOverBudget = false, confirmWrite = false, confirmUnsandboxed = false, subscriptionChoice = null, workspace, root, signal, ...options } = {}) {
999
1024
  const { run, saved } = await storedRun(runId)
1000
1025
  const target = run.packages.find(item => item.id === text(packageId))
@@ -1033,16 +1058,17 @@ export async function rerunRecordedStep(ctx, config = {}, { runId, packageId, pr
1033
1058
  return { paused: true, needsConfirmation: missingConfirmations, run,
1034
1059
  budget: null, message: '请确认续跑所需的文件修改与执行环境权限后继续;厂商 CLI 正常环境以当前用户权限运行,独立 Git 工作树不是操作系统读写边界。' }
1035
1060
  }
1036
- const sameRoute = !override || (override.provider === target.recommendedProvider && override.model === target.recommendedModel)
1037
- const budget = budgetFor(config, saved, sameRoute ? target.estimatedCost : null)
1061
+ const retryIds = new Set([target.id, ...downstreamPackageIds(run.packages, target.id)
1062
+ .filter(id => run.packages.find(item => item.id === id)?.ok !== true)])
1063
+ const estimates = rerunEstimates(run, retryIds, target.id, override, routes, planDataOptions(refreshed, config))
1064
+ const budget = budgetFor(config, saved, estimates.total)
1038
1065
  if (budget.exceeded && confirmOverBudget !== true) {
1039
1066
  return { paused: true, budget: { ...budget, paused: true, message: `${budget.message} 已暂停重跑,请确认后再试。` }, run }
1040
1067
  }
1041
1068
  const cwd = text(workspace) || run.workspace
1042
1069
  if (!cwd || !(await fileExists(cwd))) throw new Error(`原运行的工作区 ${cwd || '(未记录)'} 不可用,无法重跑该步骤;请恢复该目录,或在原工作区的会话中调用 model_router_rerun_step。`)
1043
- if (run.kind === 'team') return rerunTeamStep(ctx, { run, target, override, routes, budget, cwd, root: text(root) || cwd, signal, options, config })
1044
- const retryIds = new Set([target.id, ...downstreamPackageIds(run.packages, target.id)
1045
- .filter(id => run.packages.find(item => item.id === id)?.ok !== true)])
1070
+ if (run.kind === 'team') return rerunTeamStep(ctx, { run, target, override, routes, budget,
1071
+ ids: [...retryIds], estimates: estimates.byId, cwd, root: text(root) || cwd, signal, options, config })
1046
1072
  const manualRetries = Object.fromEntries(run.packages.filter(item => retryIds.has(item.id) && item.manualAssignment)
1047
1073
  .map(item => [item.id, item.id === target.id && override ? { kind: 'api', ...override } : item.manualAssignment]))
1048
1074
  if (Object.keys(manualRetries).length) {
@@ -1089,6 +1115,9 @@ export async function rerunRecordedStep(ctx, config = {}, { runId, packageId, pr
1089
1115
  })
1090
1116
  const updated = await routerStateStore().updateRun(run.id, current => {
1091
1117
  mergeRerun(current, execution, { rerunIds: execution.ranIds, pricingFor: item => routePricing(routes, item), billingFor: billingForResult })
1118
+ for (const item of current.packages) {
1119
+ if (execution.ranIds.includes(item.id)) item.estimatedCost = estimates.byId.get(item.id) ?? null
1120
+ }
1092
1121
  })
1093
1122
  const awaiting = pausedSteps(execution)
1094
1123
  return { run: updated, execution, budget, ...(awaiting.length ? { awaitingConfirmation: awaiting } : {}) }
@@ -1114,7 +1143,7 @@ async function cancelPausedStep(runId, packageId) {
1114
1143
  }
1115
1144
 
1116
1145
  /** Stored team packages in plan shape; `override` reassigns the retried package. */
1117
- function teamPlanFromRun(run, ids, override, targetId) {
1146
+ function teamPlanFromRun(run, ids, override, targetId, estimates) {
1118
1147
  const workPackages = run.packages.filter(item => ids.includes(item.id)).map(item => ({
1119
1148
  id: item.id, name: item.name, objective: item.objective, dependsOn: item.dependsOn ?? [],
1120
1149
  type: item.type ?? 'general', purpose: item.purpose ?? 'execution',
@@ -1124,7 +1153,7 @@ function teamPlanFromRun(run, ids, override, targetId) {
1124
1153
  ...(item.plannedReasoningEffort && !(item.id === targetId && override
1125
1154
  && (override.provider !== item.recommendedProvider || override.model !== item.recommendedModel))
1126
1155
  ? { recommendedReasoningEffort: item.plannedReasoningEffort } : {}),
1127
- estimatedCost: item.estimatedCost ?? null,
1156
+ estimatedCost: estimates.get(item.id) ?? null,
1128
1157
  }))
1129
1158
  return { mode: 'team', team: { workPackages } }
1130
1159
  }
@@ -1133,13 +1162,10 @@ function teamPlanFromRun(run, ids, override, targetId) {
1133
1162
  * Read-only team retry: the failed step plus downstream steps that had not
1134
1163
  * succeeded run through the same signed runner; earlier answers are context.
1135
1164
  */
1136
- async function rerunTeamStep(ctx, { run, target, override, routes, budget, cwd, root, signal, options, config = {} }) {
1165
+ async function rerunTeamStep(ctx, { run, target, override, routes, budget, ids, estimates, cwd, root, signal, options, config = {} }) {
1137
1166
  const mode = run.executionMode === 'workspace-write' ? 'workspace-write' : 'read-only'
1138
1167
  const executionEnvironment = teamExecutionEnvironment(run.executionEnvironment, mode)
1139
- const downstream = downstreamPackageIds(run.packages, target.id)
1140
- .filter(id => !run.packages.find(item => item.id === id)?.ok)
1141
- const ids = [target.id, ...downstream]
1142
- const plan = teamPlanFromRun(run, ids, override, target.id)
1168
+ const plan = teamPlanFromRun(run, ids, override, target.id, estimates)
1143
1169
  const cliModels = Object.fromEntries(Object.entries(run.cliModels ?? {}).filter(([key]) => ids.includes(key) && !(override && key === target.id)))
1144
1170
  if (override) {
1145
1171
  const reassignedPlan = { team: { workPackages: plan.team.workPackages.filter(item => item.id === target.id) } }
@@ -1166,6 +1192,9 @@ async function rerunTeamStep(ctx, { run, target, override, routes, budget, cwd,
1166
1192
  }
1167
1193
  const updated = await routerStateStore().updateRun(run.id, current => {
1168
1194
  mergeRerun(current, converted, { rerunIds: ids, pricingFor: item => routePricing(routes, item), billingFor: billingForResult })
1195
+ for (const item of current.packages) {
1196
+ if (ids.includes(item.id)) item.estimatedCost = estimates.get(item.id) ?? null
1197
+ }
1169
1198
  for (const [id, note] of quotaNotes) {
1170
1199
  const stored = current.packages.find(item => item.id === id)
1171
1200
  if (stored) stored.billingSwitch = note
@@ -205,7 +205,7 @@ const terminalStopCodec = strictCodec(`${OFFICIAL_TOOLS_REMOTE_PACKAGE}#Terminal
205
205
 
206
206
  const runRequestCodec = strictCodec(`${OFFICIAL_TOOLS_REMOTE_PACKAGE}#RunRequest`, parseRunRequest)
207
207
 
208
- function descriptor(method, parameters, result) {
208
+ function descriptor(method, parameters, result, cancellable = false) {
209
209
  return Object.freeze({
210
210
  id: `${OFFICIAL_TOOLS_REMOTE_PACKAGE}#${OFFICIAL_TOOLS_REMOTE_NAMESPACE}/${method}`,
211
211
  service: OFFICIAL_TOOLS_REMOTE_NAMESPACE,
@@ -214,6 +214,7 @@ function descriptor(method, parameters, result) {
214
214
  invocation: { kind: 'direct' },
215
215
  parameters,
216
216
  result,
217
+ ...(cancellable ? { cancellation: { parameter: 'signal' } } : {}),
217
218
  })
218
219
  }
219
220
 
@@ -238,10 +239,10 @@ export const OFFICIAL_TOOLS_REMOTE_DESCRIPTORS = Object.freeze([
238
239
  descriptor('rerunStep', [jsonParameter('request', rerunRequestCodec)], anyObjectCodec('RerunResult')),
239
240
  descriptor('boundaries', [], anyObjectCodec('SecurityBoundaries')),
240
241
  // Workbench "start a run": plan preview with cost estimate and confirmation reasons, then execute.
241
- descriptor('previewRun', [jsonParameter('request', runRequestCodec)], anyObjectCodec('RunPreview')),
242
+ descriptor('previewRun', [jsonParameter('request', runRequestCodec)], anyObjectCodec('RunPreview'), true),
242
243
  descriptor('startRun', [jsonParameter('request', runRequestCodec)], anyObjectCodec('RunStarted')),
243
- descriptor('runStatus', [jsonParameter('request', runJobRequestCodec)], anyObjectCodec('RunJobStatus')),
244
- descriptor('cancelRun', [jsonParameter('request', runJobRequestCodec)], anyObjectCodec('RunJobStatus')),
244
+ descriptor('runStatus', [jsonParameter('request', runJobRequestCodec)], anyObjectCodec('RunJobStatus'), true),
245
+ descriptor('cancelRun', [jsonParameter('request', runJobRequestCodec)], anyObjectCodec('RunJobStatus'), true),
245
246
  // Workbench "官方工具终端": interactive shell / fixed official CLI sessions.
246
247
  // Output streams through long-poll reads; input and resize are unary calls.
247
248
  descriptor('terminalInfo', [], anyObjectCodec('TerminalInfo')),
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
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
+
3
10
  ## 0.23.0 — 2026-10-23
4
11
 
5
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.
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: da3995c07c716f6ac776b93e0753c67afd4e5057
4
- README.zh.md: fa5f947d45c7473c4ec9fed6670f37b797a39ff5
3
+ README.md: 3bb1e3507fc7451861fb3463f52f68827fb71690
4
+ README.zh.md: 1d15eb0bf9c5ebeac41b74d349b0a709f16288c8
package/README.md CHANGED
@@ -10,7 +10,7 @@ Want release notes only? Click **Watch → Custom → Releases** (you do not nee
10
10
 
11
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.
12
12
 
13
- Current version: **0.23.0** (`@ljwei-stak/dsh-model-router`). Workbench execution now 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.
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.
14
14
 
15
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).
16
16
 
@@ -27,22 +27,22 @@ Both README versions describe the same interfaces, formulas, defaults and limits
27
27
  **Install (DSH Plugin Hub / web profile):** With the DSH CLI available, install the published npm package with:
28
28
 
29
29
  ```sh
30
- dsh plugin --profile web add @ljwei-stak/dsh-model-router@0.23.0
30
+ dsh plugin --profile web add @ljwei-stak/dsh-model-router@0.23.1
31
31
  ```
32
32
 
33
33
  **Install in Harness Desktop:** Launch Desktop once to initialize its `desktop` profile, quit Desktop fully, then run:
34
34
 
35
35
  ```sh
36
- dsh plugin --profile desktop add @ljwei-stak/dsh-model-router@0.23.0
36
+ dsh plugin --profile desktop add @ljwei-stak/dsh-model-router@0.23.1
37
37
  ```
38
38
 
39
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
40
 
41
- **Package and entry point:** [`@ljwei-stak/dsh-model-router@0.23.0` 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).
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
42
 
43
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
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.0 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.0 acceptance report](docs/releases/0.23.0-validation.md) for cancellation, recovery and validation boundaries.
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
46
 
47
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
48
 
@@ -78,16 +78,16 @@ Generating a route recommendation is local: it does not start a vendor CLI or ma
78
78
  Open **Plugins → Add plugin** and enter:
79
79
 
80
80
  ```text
81
- @ljwei-stak/dsh-model-router@0.23.0
81
+ @ljwei-stak/dsh-model-router@0.23.1
82
82
  ```
83
83
 
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.0** 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.
85
85
 
86
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.
87
87
 
88
88
  ### Upgrade from the old package name
89
89
 
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.0`. 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.
91
91
 
92
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).
93
93
 
@@ -119,6 +119,8 @@ Switching tabs keeps the panels mounted, preserving in-progress installation and
119
119
 
120
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
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
+
122
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.
123
125
 
124
126
  Example task:
package/README.zh.md CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  面向 **DeepSeek Harness Desktop** 的成本感知模型路由工作台。在本地规划任务、比较已配置路线、检查执行权限,并通过模型 API 或支持的厂商官方 CLI 执行有依赖关系的工作包。
12
12
 
13
- 当前版本:**0.23.0**(`@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 起已拆分为独立插件。
14
14
 
15
15
  官方 CLI 协作按依赖顺序逐包执行、传递依赖上下文,不会自动回退到模型目录 API。可编辑团队需明确选择 Harness 沙箱或厂商 CLI 正常环境。后者通过 Git worktree 收集修改,但不限制操作系统访问。未显式设置模型映射时,各 CLI 沿用自身配置的模型。参见[真实 Windows 验证及其局限](docs/releases/0.20.0-validation.md)。
16
16
 
@@ -27,22 +27,22 @@
27
27
  **安装(DSH Plugin Hub / web profile):** 确保 DSH CLI 可用,然后安装已发布的 npm 包:
28
28
 
29
29
  ```sh
30
- dsh plugin --profile web add @ljwei-stak/dsh-model-router@0.23.0
30
+ dsh plugin --profile web add @ljwei-stak/dsh-model-router@0.23.1
31
31
  ```
32
32
 
33
33
  **安装到 Harness Desktop:** 首次先启动一次 Desktop 初始化 `desktop` 配置,再彻底退出 Desktop,运行:
34
34
 
35
35
  ```sh
36
- dsh plugin --profile desktop add @ljwei-stak/dsh-model-router@0.23.0
36
+ dsh plugin --profile desktop add @ljwei-stak/dsh-model-router@0.23.1
37
37
  ```
38
38
 
39
39
  也可以在 **Harness Desktop → 插件 → 添加插件** 中粘贴包名。安装后重启 Desktop。单独运行全局 `npm install -g` 不会注册 Desktop 插件。
40
40
 
41
- **发布包与入口:** [npm 上的 `@ljwei-stak/dsh-model-router@0.23.0`](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)。
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
42
 
43
43
  **配置文件与配置入口:** 当前不支持导入用户编写的 JSON/YAML 配置文件。Provider、模型和凭据在 Harness **模型**页面配置;路由档案与偏好在插件设置中配置。路由器持久状态存于 `$DSH_HOME/model-router/state.json`(默认:`~/.dsh/model-router/state.json`)。Provider CLI 沿用各自的登录和配置;路由器不会把 API 密钥复制进模型档案或终端日志。
44
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.0 尚无经过验证的端到端厂商运行。[实时进度截图](docs/assets/workbench-run-progress.png)是离线模拟;取消、重开恢复与验证边界见[0.23.0 验收报告](docs/releases/0.23.0-validation.md)。
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
46
 
47
47
  **权限与风险:** 任务规划在本地完成,不调用模型。实际执行会把选定任务和上下文发送到所选模型 API 或厂商 CLI,并可能产生费用。默认使用 Harness 受限环境;只读运行不修改工作区,`workspace-write` 需要用户明确批准。明确选择 `cli-native` 的可编辑团队会在 Harness 进程沙箱之外,以当前操作系统用户权限运行;Git worktree 只收集修改,不限制操作系统访问。外部 Provider 和 CLI 服务各自适用其数据处理及计费规则。
48
48
 
@@ -78,16 +78,16 @@ dsh plugin --profile desktop add @ljwei-stak/dsh-model-router@0.23.0
78
78
  打开 **插件 → 添加插件**,输入:
79
79
 
80
80
  ```text
81
- @ljwei-stak/dsh-model-router@0.23.0
81
+ @ljwei-stak/dsh-model-router@0.23.1
82
82
  ```
83
83
 
84
- 镜像缺少该精确版本时,使用官方 HTTPS npm 源 `https://registry.npmjs.org/`。启用插件,然后完全退出 Harness(包括托盘进程)并重启。插件详情应显示 **0.23.0**,侧栏应出现 **模型路由**。仅执行全局 `npm install -g` 不会把插件注册到 Desktop。
84
+ 镜像缺少该精确版本时,使用官方 HTTPS npm 源 `https://registry.npmjs.org/`。启用插件,然后完全退出 Harness(包括托盘进程)并重启。插件详情应显示 **0.23.1**,侧栏应出现 **模型路由**。仅执行全局 `npm install -g` 不会把插件注册到 Desktop。
85
85
 
86
86
  也可以通过插件管理器安装本地 `.tgz` 压缩包,或解压后的内部 `package` 目录。两者都必须包含 `package.json` 和 `.dsh-plugin`。历史构建见 [Releases](https://github.com/Alice-Marx/dsh-model-router/releases),也可按下文从源码构建。
87
87
 
88
88
  ### 从旧包名升级
89
89
 
90
- 旧名称 `@ljwei-stak/model-router-galgame` 停留在 0.13.0。保留配置档/应用数据,移除旧插件条目,再添加 `@ljwei-stak/dsh-model-router@0.23.0`。不要同时保留两条插件:它们共用内部插件 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 能让现有路由设置与历史记录继续使用。
91
91
 
92
92
  从合包 0.11.x 升级前,先备份配置档,并导出需要保留的 GAL 存档。剧情播放、立绘、音乐与存档现属于独立的 `@ljwei-stak/dsh-galgame`。详细存档限制见 [MIGRATION.md](docs/archive/MIGRATION.md)。
93
93
 
@@ -119,6 +119,8 @@ dsh plugin --profile desktop add @ljwei-stak/dsh-model-router@0.23.0
119
119
 
120
120
  执行开始后,工作台显示已耗时、当前工作包与每包状态。**取消执行**会请求停止当前调用,并阻止后续工作包启动;执行器完成收尾前持续显示**正在取消**。已完成结果和已报告用量仍保留在历史中。取消不能撤销已经产生的费用或已经整合的改动。关闭面板不会取消执行;同一浏览器会话重新打开后,可在 Host 仍保留该任务时重新连接。Host 重启会清除内存任务,已保存的历史记录仍可查看。
121
121
 
122
+ 预览、进度和取消请求最长等待 15 秒。进度读取超时后继续查询同一任务;取消未确认时可手动重试。网络等待超时不代表 Host 任务已停止,也不会自动重发 `startRun`。重跑历史步骤前,预算覆盖该步及实际会执行的全部未完成下游;改派使用所选路线估价,未知价格仍保持未知。[超时恢复截图](docs/assets/workbench-run-timeout.png)为离线模拟。
123
+
122
124
  修改任务文本、规划模式、路线配置、目录或相关设置,会使旧建议失效。修改执行输入也会丢弃待确认或已完成的预览,必须对当前请求重新预览。执行预览使用 Host 当前状态,因此可能与先前本地计划不同。
123
125
 
124
126
  任务示例:
@@ -1,6 +1,6 @@
1
1
  # 模型路由工作台:配置、规划与实际执行
2
2
 
3
- 适用版本:`@ljwei-stak/dsh-model-router@0.23.0`。声明兼容 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
 
@@ -285,3 +285,5 @@ Windows Codex 在 Harness 环境仅支持只读:其原 `CODEX_HOME` 状态仍
285
285
  | 工具名在会话中不可用 | 确认插件启用,重新开始会话;记录宿主/插件版本与脱敏错误。 |
286
286
 
287
287
  问题反馈请附宿主/插件版本、执行模式、工具状态及脱敏错误,勿提供密钥。源码、安装和资料入口见[中文 README](../README.zh.md)。
288
+
289
+ 执行恢复:预览、进度与取消请求等待超过15秒会终止该次网络等待;进度会重试同一任务,取消未确认可手动重试,任务不会自动重新启动。历史步骤重跑前,预算按目标及未成功的下游步骤合计;整组超限时先确认再运行,改派按所选路线重估,缺失价格不视为零费用。
@@ -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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ljwei-stak/dsh-model-router",
3
- "version": "0.23.0",
3
+ "version": "0.23.1",
4
4
  "type": "module",
5
5
  "description": "Model Router for official DeepSeek Harness Desktop: cost-aware task routing, dependent work packages and official vendor CLI discovery, installation and execution. GAL is available separately as @ljwei-stak/dsh-galgame.",
6
6
  "repository": {
@@ -104,7 +104,9 @@
104
104
  "docs/MANUAL_TASK_ASSIGNMENT.zh.md",
105
105
  "docs/releases/0.22.0-validation.md",
106
106
  "docs/releases/0.23.0-validation.md",
107
- "docs/assets/workbench-run-progress.png"
107
+ "docs/assets/workbench-run-progress.png",
108
+ "docs/releases/0.23.1-validation.md",
109
+ "docs/assets/workbench-run-timeout.png"
108
110
  ],
109
111
  "scripts": {
110
112
  "build:client": "node scripts/build-client.mjs",
@@ -116,7 +118,7 @@
116
118
  "experiment:routing-summary": "node scripts/summarize-routing-search.mjs",
117
119
  "experiment:routing-adaptive": "node scripts/adaptive-routing-study.mjs",
118
120
  "audit:livebench": "node scripts/livebench-aggregate-audit.mjs",
119
- "test": "node scripts/check-readme.mjs && node --test tests/adaptive-*.test.mjs tests/dynamic-data.test.mjs tests/router.test.mjs tests/routing-optimizer.test.mjs tests/routing-evaluation.test.mjs tests/routing-paired-statistics.test.mjs tests/routing-selftest-tools.test.mjs tests/livebench-aggregate-audit.test.mjs tests/livebench-prior-study.test.mjs tests/routing-search-experiment.test.mjs tests/routing-regression-study.test.mjs tests/routing-search-summary.test.mjs tests/harness-*.test.mjs tests/official-tools.test.mjs tests/official-tool-result.test.mjs tests/native-environment.test.mjs tests/native-entry-trust.test.mjs tests/tool-failure-ledger.test.mjs tests/official-team-runtime.test.mjs tests/model-profiles.test.mjs tests/model-profile-editor.test.mjs tests/tool-install-state.test.mjs tests/tool-install-sdk-ui.test.mjs tests/vendor-adapters.test.mjs tests/desktop-compat.test.mjs tests/plugin-split.test.mjs tests/install-settings-host.test.mjs tests/task-executors.test.mjs tests/tool-install-preferences.test.mjs tests/roadmap.test.mjs tests/insights-state.test.mjs tests/billing-team-ledger.test.mjs tests/subscription-billing.test.mjs tests/subscription-host.test.mjs tests/test-pass-regressions.test.mjs tests/roadmap-gaps.test.mjs tests/cordis-inject.test.mjs tests/cli-terminal.test.mjs tests/cli-terminal-gateway.test.mjs tests/latest-versions.test.mjs tests/vendor-installer-validation.test.mjs tests/install-entry-selection.test.mjs tests/script-directory-safety.test.mjs tests/zcode-config.test.mjs tests/zcode-runtime.test.mjs tests/client-loader.test.mjs tests/hermes-pi.test.mjs tests/effort-execution.test.mjs tests/routing-evidence.test.mjs tests/routing-query-effort.test.mjs tests/routing-search-frontier.test.mjs tests/quality-gates.test.mjs tests/algorithm-host-integration.test.mjs tests/quality-attempt-ledger.test.mjs tests/manual-package-execution.test.mjs tests/manual-plan.test.mjs tests/manual-assignment-ui.test.mjs tests/manual-assignment-host.test.mjs tests/readme-math.test.mjs tests/run-jobs.test.mjs tests/run-progress-host.test.mjs tests/run-progress-ui.test.mjs tests/execution-progress.test.mjs",
121
+ "test": "node scripts/check-readme.mjs && node --test tests/adaptive-*.test.mjs tests/dynamic-data.test.mjs tests/router.test.mjs tests/routing-optimizer.test.mjs tests/routing-evaluation.test.mjs tests/routing-paired-statistics.test.mjs tests/routing-selftest-tools.test.mjs tests/livebench-aggregate-audit.test.mjs tests/livebench-prior-study.test.mjs tests/routing-search-experiment.test.mjs tests/routing-regression-study.test.mjs tests/routing-search-summary.test.mjs tests/harness-*.test.mjs tests/official-tools.test.mjs tests/official-tool-result.test.mjs tests/native-environment.test.mjs tests/native-entry-trust.test.mjs tests/tool-failure-ledger.test.mjs tests/official-team-runtime.test.mjs tests/model-profiles.test.mjs tests/model-profile-editor.test.mjs tests/tool-install-state.test.mjs tests/tool-install-sdk-ui.test.mjs tests/vendor-adapters.test.mjs tests/desktop-compat.test.mjs tests/plugin-split.test.mjs tests/install-settings-host.test.mjs tests/task-executors.test.mjs tests/tool-install-preferences.test.mjs tests/roadmap.test.mjs tests/insights-state.test.mjs tests/billing-team-ledger.test.mjs tests/subscription-billing.test.mjs tests/subscription-host.test.mjs tests/test-pass-regressions.test.mjs tests/roadmap-gaps.test.mjs tests/cordis-inject.test.mjs tests/cli-terminal.test.mjs tests/cli-terminal-gateway.test.mjs tests/latest-versions.test.mjs tests/vendor-installer-validation.test.mjs tests/install-entry-selection.test.mjs tests/script-directory-safety.test.mjs tests/zcode-config.test.mjs tests/zcode-runtime.test.mjs tests/client-loader.test.mjs tests/hermes-pi.test.mjs tests/effort-execution.test.mjs tests/routing-evidence.test.mjs tests/routing-query-effort.test.mjs tests/routing-search-frontier.test.mjs tests/quality-gates.test.mjs tests/algorithm-host-integration.test.mjs tests/quality-attempt-ledger.test.mjs tests/manual-package-execution.test.mjs tests/manual-plan.test.mjs tests/manual-assignment-ui.test.mjs tests/manual-assignment-host.test.mjs tests/readme-math.test.mjs tests/run-jobs.test.mjs tests/run-progress-host.test.mjs tests/run-progress-ui.test.mjs tests/execution-progress.test.mjs tests/rerun-budget-host.test.mjs tests/run-progress-transport.test.mjs",
120
122
  "prepack": "node scripts/prepack.mjs",
121
123
  "pack:local": "npm pack --pack-destination dist",
122
124
  "test:routing-algorithm": "node --test tests/effort-execution.test.mjs tests/routing-evidence.test.mjs tests/routing-query-effort.test.mjs tests/routing-search-frontier.test.mjs tests/quality-gates.test.mjs tests/algorithm-host-integration.test.mjs tests/quality-attempt-ledger.test.mjs",