@ljwei-stak/dsh-model-router 0.24.0 → 0.25.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.
@@ -43,6 +43,24 @@ const BASE_ENV_KEYS = Object.freeze([
43
43
  ])
44
44
 
45
45
  const READ_ONLY_INSTRUCTION = 'Read the task supplied on standard input. Answer in the task language. Do not modify files.'
46
+ const executionBoundaryFailure = error => ['ROUTER_CHECKPOINT_FAILED', 'ROUTER_BUDGET_CHANGED'].includes(error?.code)
47
+ const usageFields = ['inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheWriteTokens']
48
+
49
+ /** Subscription evidence contains reported usage only, never prices or secrets. */
50
+ function subscriptionReceipt(result, { provider, model, toolId = null, channel = 'harness-llm', now = Date.now } = {}) {
51
+ const usage = result?.usage && typeof result.usage === 'object' && !Array.isArray(result.usage)
52
+ ? Object.fromEntries(usageFields.filter(key => typeof result.usage[key] === 'number'
53
+ && Number.isFinite(result.usage[key]) && result.usage[key] >= 0).map(key => [key, result.usage[key]])) : null
54
+ const notStarted = result?.notStarted === true || result?.blocked === true
55
+ const incomplete = result?.usageIncomplete === true || !usage
56
+ || !['inputTokens', 'outputTokens'].every(key => Object.hasOwn(usage, key))
57
+ || usageFields.some(key => result?.usage?.[key] !== undefined && !Object.hasOwn(usage ?? {}, key))
58
+ return { provider: String(provider ?? ''), model: String(reportedCliModel(result?.actualModel ?? result?.reportedModel) ?? model ?? ''),
59
+ toolId, channel, usage: usage && Object.keys(usage).length ? usage : null,
60
+ ...(incomplete ? { usageIncomplete: true } : {}), ...(notStarted ? { notStarted: true } : {}),
61
+ finishedAt: typeof result?.finishedAt === 'number' && Number.isFinite(result.finishedAt)
62
+ && result.finishedAt >= 0 ? result.finishedAt : now() }
63
+ }
46
64
 
47
65
  function additionalApiFields(value) {
48
66
  if (!value || typeof value !== 'object') return {}
@@ -409,6 +427,7 @@ export function looksLikeLoginFailure(detail) {
409
427
  }
410
428
 
411
429
  const count = value => Number.isFinite(value) && value >= 0 ? Math.round(value) : 0
430
+ const knownCount = value => typeof value === 'number' && Number.isFinite(value) && value >= 0
412
431
 
413
432
  /** Token usage reported by the CLI, in the Harness TokenUsage shape (disjoint cache counts). */
414
433
  export function usageFromOutput(format, stdout = '') {
@@ -422,11 +441,14 @@ export function usageFromOutput(format, stdout = '') {
422
441
  ? Object.keys(result.modelUsage) : []
423
442
  return {
424
443
  usage: {
425
- inputTokens: count(usage.input_tokens),
426
- outputTokens: count(usage.output_tokens),
444
+ ...(knownCount(usage.input_tokens) ? { inputTokens: count(usage.input_tokens) } : {}),
445
+ ...(knownCount(usage.output_tokens) ? { outputTokens: count(usage.output_tokens) } : {}),
427
446
  cacheReadTokens: count(usage.cache_read_input_tokens),
428
447
  cacheWriteTokens: count(usage.cache_creation_input_tokens),
429
448
  },
449
+ ...(!knownCount(usage.input_tokens) || !knownCount(usage.output_tokens)
450
+ || ['cache_read_input_tokens', 'cache_creation_input_tokens'].some(key => usage[key] !== undefined && !knownCount(usage[key]))
451
+ ? { usageIncomplete: true } : {}),
430
452
  ...(Number.isFinite(reported) && reported >= 0 ? { reportedCostUsd: reported } : {}),
431
453
  ...(models.length === 1 && /^[A-Za-z0-9][A-Za-z0-9._:/\[\]-]{0,199}$/.test(models[0]) ? { reportedModel: models[0] } : {}),
432
454
  }
@@ -436,24 +458,33 @@ export function usageFromOutput(format, stdout = '') {
436
458
  if (!completed) return null
437
459
  const cached = count(completed.usage.cached_input_tokens)
438
460
  return { usage: {
439
- inputTokens: Math.max(0, count(completed.usage.input_tokens) - cached),
440
- outputTokens: count(completed.usage.output_tokens),
461
+ ...(knownCount(completed.usage.input_tokens) ? { inputTokens: Math.max(0, count(completed.usage.input_tokens) - cached) } : {}),
462
+ ...(knownCount(completed.usage.output_tokens) ? { outputTokens: count(completed.usage.output_tokens) } : {}),
441
463
  cacheReadTokens: cached,
442
464
  cacheWriteTokens: 0,
443
- } }
465
+ }, ...(!knownCount(completed.usage.input_tokens) || !knownCount(completed.usage.output_tokens)
466
+ || completed.usage.cached_input_tokens !== undefined && (!knownCount(completed.usage.cached_input_tokens)
467
+ || completed.usage.cached_input_tokens > completed.usage.input_tokens) ? { usageIncomplete: true } : {}) }
444
468
  }
445
469
  if (format === 'gemini-json') {
446
470
  const models = JSON.parse(stdout)?.stats?.models
447
- if (!models || typeof models !== 'object') return null
471
+ if (!models || typeof models !== 'object' || Array.isArray(models) || !Object.keys(models).length) return null
448
472
  let input = 0
449
473
  let output = 0
450
474
  let cached = 0
475
+ let inputKnown = true, outputKnown = true, incomplete = false
451
476
  for (const entry of Object.values(models)) {
477
+ inputKnown &&= knownCount(entry?.tokens?.prompt)
478
+ outputKnown &&= knownCount(entry?.tokens?.candidates)
479
+ incomplete ||= entry?.tokens?.cached !== undefined && (!knownCount(entry.tokens.cached)
480
+ || entry.tokens.cached > entry.tokens.prompt)
452
481
  input += count(entry?.tokens?.prompt)
453
482
  output += count(entry?.tokens?.candidates)
454
483
  cached += count(entry?.tokens?.cached)
455
484
  }
456
- return { usage: { inputTokens: Math.max(0, input - cached), outputTokens: output, cacheReadTokens: cached, cacheWriteTokens: 0 } }
485
+ return { usage: { ...(inputKnown ? { inputTokens: Math.max(0, input - cached) } : {}),
486
+ ...(outputKnown ? { outputTokens: output } : {}), cacheReadTokens: cached, cacheWriteTokens: 0 },
487
+ ...(!inputKnown || !outputKnown || incomplete ? { usageIncomplete: true } : {}) }
457
488
  }
458
489
  } catch { /* usage is optional */ }
459
490
  return null
@@ -621,7 +652,7 @@ function captureProcess(spawnImpl, file, args, { cwd, env, stdin, timeoutMs, sig
621
652
  })
622
653
  }
623
654
 
624
- async function useApi(apiFallback, { route, task, signal, reason, adapter, preference, exitCode = null, timedOut = false, detail = '', raw = '', skipped = false, attempted = false }) {
655
+ async function useApi(apiFallback, { route, task, signal, reason, adapter, preference, exitCode = null, timedOut = false, detail = '', raw = '', skipped = false, attempted = false, subscriptionResult = null }) {
625
656
  const provider = String(route?.provider ?? '')
626
657
  const model = String(route?.model ?? '')
627
658
  const fallback = {
@@ -637,7 +668,7 @@ async function useApi(apiFallback, { route, task, signal, reason, adapter, prefe
637
668
  }
638
669
  }
639
670
  try {
640
- const api = await apiFallback({ route, task, signal, reason, detail, raw: String(raw ?? '').slice(-40_000), toolId: adapter?.id ?? null, attempted, timedOut, exitCode })
671
+ const api = await apiFallback({ route, task, signal, reason, detail, raw: String(raw ?? '').slice(-40_000), toolId: adapter?.id ?? null, attempted, timedOut, exitCode, subscriptionResult })
641
672
  // A billing-aware fallback can pause the step for the user's decision.
642
673
  if (api?.paused) {
643
674
  return {
@@ -646,6 +677,7 @@ async function useApi(apiFallback, { route, task, signal, reason, adapter, prefe
646
677
  pause: { ...api.pause, ...(detail ? { detail } : {}), ...(fallback.loginRequired ? { loginRequired: true } : {}) },
647
678
  ...reasoningExecutionFields(api),
648
679
  ...additionalApiFields(api),
680
+ ...(api?.billing ? { billing: api.billing } : {}),
649
681
  error: api.error,
650
682
  }
651
683
  }
@@ -674,6 +706,7 @@ async function useApi(apiFallback, { route, task, signal, reason, adapter, prefe
674
706
  error: api?.ok === true && answer.trim() ? undefined : (api?.error || reason),
675
707
  }
676
708
  } catch (error) {
709
+ if (executionBoundaryFailure(error)) throw error
677
710
  if (signal?.aborted) {
678
711
  return {
679
712
  ok: false, provider, model, preference, channel: 'harness-llm', toolId: adapter?.id ?? null,
@@ -833,6 +866,8 @@ export async function executeAssignedTask({
833
866
  return useApi(apiFallback, {
834
867
  route, task: prompt, signal, adapter, preference, exitCode: verified.exitCode ?? null,
835
868
  timedOut: verified.status === 'timed-out', attempted: true,
869
+ subscriptionResult: { usage: verified.usage, usageIncomplete: verified.usageIncomplete,
870
+ notStarted: verified.notStarted === true, actualModel: verified.actualModel, reportedModel: verified.reportedModel },
836
871
  reason: verified.error || verified.reason || '官方 CLI 执行失败,已回退模型目录 API。',
837
872
  detail: redactDiagnostic(verifiedDiagnostic(verified), childEnvironment(adapter, credentials, credentialMode).secret),
838
873
  raw: [verified.error, verified.reason, verified.detail, verified.stderrTail, verified.stdoutTail].filter(item => typeof item === 'string').join('\n'),
@@ -861,7 +896,7 @@ export async function executeAssignedTask({
861
896
  : probe.timedOut ? `${adapter.label} 检测超时,已回退模型目录 API。`
862
897
  : `${adapter.label} 无法运行,已回退模型目录 API。`
863
898
  if (probe.cancelled) {
864
- return { ok: false, provider, model, preference, channel: 'official-cli', toolId: adapter.id, answer: '', fallback: null, timedOut: false, exitCode: null, error: reason, cancelled: true }
899
+ return { ok: false, provider, model, preference, channel: 'official-cli', toolId: adapter.id, answer: '', fallback: null, timedOut: false, exitCode: null, error: reason, cancelled: true, notStarted: true }
865
900
  }
866
901
  return useApi(apiFallback, { route, task: prompt, signal, adapter, preference, reason })
867
902
  }
@@ -871,11 +906,14 @@ export async function executeAssignedTask({
871
906
  ...(Number.isSafeInteger(stopGraceMs) && stopGraceMs >= 10 && stopGraceMs <= STOP_GRACE_MS ? { stopGraceMs } : {}),
872
907
  })
873
908
  if (run.cancelled) {
874
- return { ok: false, provider, model, preference, channel: 'official-cli', toolId: adapter.id, answer: '', fallback: null, timedOut: false, exitCode: run.exitCode, error: '执行已取消。', cancelled: true }
909
+ const usage = usageFromOutput(adapter.format, run.stdout)
910
+ return { ok: false, provider, model, preference, channel: 'official-cli', toolId: adapter.id, answer: '', fallback: null, timedOut: false, exitCode: run.exitCode, error: '执行已取消。', cancelled: true,
911
+ credentialSource, ...(usage?.usage ? { usage: usage.usage } : {}), ...(usage?.usageIncomplete ? { usageIncomplete: true } : {}) }
875
912
  }
876
913
  if (!run.ok) {
877
914
  return useApi(apiFallback, {
878
915
  route, task: prompt, signal, adapter, preference, exitCode: run.exitCode, timedOut: run.timedOut, attempted: true,
916
+ subscriptionResult: usageFromOutput(adapter.format, run.stdout),
879
917
  reason: run.timedOut ? `${adapter.label} 执行超时,已回退模型目录 API。`
880
918
  : run.outputLimit ? `${adapter.label} 输出超过上限,已回退模型目录 API。`
881
919
  : `${adapter.label} 执行失败,已回退模型目录 API。`,
@@ -889,6 +927,7 @@ export async function executeAssignedTask({
889
927
  if (!parsed.ok) {
890
928
  return useApi(apiFallback, {
891
929
  route, task: prompt, signal, adapter, preference, exitCode: run.exitCode, attempted: true,
930
+ subscriptionResult: usageFromOutput(adapter.format, run.stdout),
892
931
  reason: `${parsed.error} 已回退模型目录 API。`,
893
932
  detail: failureDetail(adapter.format, run.stdout, run.stderr, secret),
894
933
  raw: `${run.stdout.slice(-20_000)}\n${run.stderr.slice(-20_000)}`,
@@ -896,7 +935,8 @@ export async function executeAssignedTask({
896
935
  }
897
936
  const answer = redact(parsed.answer, secret)
898
937
  if (!answer.trim()) {
899
- return useApi(apiFallback, { route, task: prompt, signal, adapter, preference, exitCode: run.exitCode, attempted: true, reason: '官方 CLI 没有返回文本,已回退模型目录 API。' })
938
+ return useApi(apiFallback, { route, task: prompt, signal, adapter, preference, exitCode: run.exitCode, attempted: true,
939
+ subscriptionResult: usageFromOutput(adapter.format, run.stdout), reason: '官方 CLI 没有返回文本,已回退模型目录 API。' })
900
940
  }
901
941
  return officialSuccess({
902
942
  route, adapter, preference, answer, exitCode: run.exitCode,
@@ -954,14 +994,26 @@ const failureSummary = text => `${String(text ?? '').replace(/,?已(?:回退
954
994
  * - subscription-only: never fall back to an API key.
955
995
  * - api-only: never use the subscription.
956
996
  */
957
- export async function executeRouteWithBilling({ route, billing = null, apiFallback, ...options } = {}) {
997
+ export async function executeRouteWithBilling({ route, billing = null, apiFallback, onSubscriptionResult, ...options } = {}) {
958
998
  const plan = billingPlan(route, billing?.routes ?? [])
999
+ const subscriptionAttempts = []
1000
+ const recordSubscription = async receipt => {
1001
+ // This executor attempts the subscription once, then may use its API
1002
+ // fallback. Keep a bounded vector so unrelated result fields cannot grow it.
1003
+ if (subscriptionAttempts.length >= 2) throw new Error('订阅尝试证据超过本步上限。')
1004
+ subscriptionAttempts.push(receipt)
1005
+ const event = { ...structuredClone(receipt), billing: 'subscription', subscriptionAttempts: structuredClone(subscriptionAttempts) }
1006
+ await awaitExecutionCheckpoint(onSubscriptionResult, event,
1007
+ () => ({ status: 'failed', packages: [{ ...event, ok: false, ran: !receipt.notStarted }] }), 'subscription-result')
1008
+ }
959
1009
  // Every result names the billing mode it ran under, whichever branch produced it.
960
- const result = await routeWithBilling(plan, { route, billing, apiFallback, ...options })
961
- return { ...result, billingMode: result?.billingMode ?? plan.mode }
1010
+ const result = await routeWithBilling(plan, { route, billing, apiFallback, ...options, onSubscriptionResult: recordSubscription })
1011
+ return { ...result, billingMode: result?.billingMode ?? plan.mode,
1012
+ ...(subscriptionAttempts.length ? { subscriptionAttempts,
1013
+ ...(result?.billing ? {} : { billing: 'subscription' }) } : {}) }
962
1014
  }
963
1015
 
964
- async function routeWithBilling(plan, { route, billing = null, apiFallback, ...options } = {}) {
1016
+ async function routeWithBilling(plan, { route, billing = null, apiFallback, onSubscriptionResult, ...options } = {}) {
965
1017
  if (!billing) return executeAssignedTask({ ...options, route, apiFallback })
966
1018
  const now = typeof billing.now === 'function' ? billing.now : Date.now
967
1019
  const sub = plan.subscription
@@ -972,7 +1024,7 @@ async function routeWithBilling(plan, { route, billing = null, apiFallback, ...o
972
1024
  const base = { provider, model, billingMode: plan.mode }
973
1025
  const refuse = (error, extra = {}) => ({
974
1026
  ok: false, ...base, preference: executionPreference(route), channel: sub?.kind === 'plan-key' ? 'harness-llm' : 'official-cli',
975
- toolId: sub?.toolId ?? null, answer: '', fallback: null, error, ...extra,
1027
+ toolId: sub?.toolId ?? null, answer: '', fallback: null, notStarted: true, error, ...extra,
976
1028
  })
977
1029
  const viaApi = async (reason, note, skipped = false) => {
978
1030
  if (!apiRoute || (sub?.kind === 'plan-key' && apiRoute.provider === sub.route.provider && apiRoute.model === sub.route.model)) {
@@ -1016,11 +1068,16 @@ async function routeWithBilling(plan, { route, billing = null, apiFallback, ...o
1016
1068
  }
1017
1069
 
1018
1070
  if (sub.kind === 'plan-key') {
1071
+ if (typeof apiFallback !== 'function') return refuse('编程套餐执行器不可用,此步骤未启动。', { notStarted: true, billing: 'subscription' })
1019
1072
  const reasoningRoute = routeForReasoningSwitch(route, sub.route)
1020
1073
  const originalEffort = typeof route?.reasoningEffort === 'string' && route.reasoningEffort ? route.reasoningEffort : null
1021
1074
  let attempt
1022
1075
  try { attempt = await apiFallback({ route: reasoningRoute, task: options.task, signal: options.signal, reason: '编程套餐(订阅)', subscription: true }) }
1023
- catch (error) { attempt = { ok: false, error: String(error?.message ?? error) } }
1076
+ catch (error) {
1077
+ if (executionBoundaryFailure(error)) throw error
1078
+ attempt = { ok: false, error: String(error?.message ?? error) }
1079
+ }
1080
+ await onSubscriptionResult(subscriptionReceipt(attempt, { ...sub.route, now }))
1024
1081
  const droppedEffort = originalEffort && reasoningRoute.reasoningEffort !== originalEffort ? originalEffort : null
1025
1082
  if (attempt?.ok === true && String(attempt.answer ?? '').trim()) {
1026
1083
  return {
@@ -1083,8 +1140,15 @@ async function routeWithBilling(plan, { route, billing = null, apiFallback, ...o
1083
1140
  // No subscription login: the existing CLI-with-key / catalog API behaviour.
1084
1141
  return executeAssignedTask({ ...options, route, apiFallback })
1085
1142
  }
1143
+ const confirmedSubscription = retrySubscription || login === 'subscription' || plan.mode === 'subscription-only'
1086
1144
  if (retrySubscription) options = { ...options, skipOfficial: null }
1087
1145
  const guarded = async args => {
1146
+ if (args?.attempted === true && !confirmedSubscription) {
1147
+ const pause = pauseFor('CLI 计费方式尚未核对,已暂停,未启动额外 API 调用。', '请先体检确认账号订阅或 API Key,再选择重试方式。')
1148
+ return { ok: false, paused: true, billing: 'unknown', pause, error: pause.reason }
1149
+ }
1150
+ if (args?.attempted === true && confirmedSubscription) await onSubscriptionResult(subscriptionReceipt(args.subscriptionResult,
1151
+ { provider, model, toolId: args.toolId ?? sub.toolId, channel: 'official-cli', now }))
1088
1152
  const vendor = vendorKey({ toolId: args?.toolId ?? sub.toolId })
1089
1153
  const info = classify(`${args?.detail ?? ''}
1090
1154
  ${args?.raw ?? ''}`, vendor)
@@ -1112,13 +1176,16 @@ ${args?.raw ?? ''}`, vendor)
1112
1176
  const pause = pauseFor(summary, args?.detail ?? '', { timedOut: args?.timedOut === true, exitCode: args?.exitCode ?? null })
1113
1177
  return { ok: false, paused: true, pause, error: pause.reason }
1114
1178
  }
1115
- return apiFallback(args)
1179
+ return { ...await apiFallback(args), billing: 'api' }
1116
1180
  }
1117
1181
  const result = await executeAssignedTask({ ...options, route, apiFallback: guarded, credentialMode: 'session-only' })
1182
+ if (confirmedSubscription && result.channel === 'official-cli' && (result.ok === true || result.cancelled && !result.notStarted)) {
1183
+ await onSubscriptionResult(subscriptionReceipt(result, { provider, model, toolId: sub.toolId, channel: 'official-cli', now }))
1184
+ }
1118
1185
  return {
1119
1186
  ...result,
1120
1187
  billingMode: plan.mode,
1121
- ...(result.ok && result.channel === 'official-cli' ? { billing: 'subscription' } : {}),
1188
+ ...(result.channel === 'official-cli' && !result.billing ? { billing: confirmedSubscription ? 'subscription' : 'unknown' } : {}),
1122
1189
  }
1123
1190
  }
1124
1191
 
@@ -1233,6 +1300,7 @@ export async function observeExecutionProgress(hook, value) {
1233
1300
  export async function awaitExecutionCheckpoint(hook, value, partial, stage) {
1234
1301
  if (typeof hook !== 'function') return
1235
1302
  try { await hook(value) } catch (cause) {
1303
+ if (executionBoundaryFailure(cause) && cause.execution) throw cause
1236
1304
  const budgetChanged = cause?.code === 'ROUTER_BUDGET_CHANGED'
1237
1305
  const error = new Error(budgetChanged ? String(cause.message) : `执行检查点保存失败:${String(cause?.message ?? cause)}`, { cause })
1238
1306
  error.code = budgetChanged ? 'ROUTER_BUDGET_CHANGED' : 'ROUTER_CHECKPOINT_FAILED'
@@ -1324,7 +1392,7 @@ async function runManualTool(target, prompt, options) {
1324
1392
  */
1325
1393
  async function runPackages({ packages, task, routingBypassed, routes, previous = [], targets = null, overrides = {}, choices = {}, options }) {
1326
1394
  const routeList = Array.isArray(routes) ? routes : []
1327
- const { credentialsFor, onPackageStart, onPackage, onStage, onCheckpointStart, onCheckpointResult, billing, ...runOptions } = options
1395
+ const { credentialsFor, onPackageStart, onPackage, onStage, onCheckpointStart, onCheckpointResult, onSubscriptionResult, billing, ...runOptions } = options
1328
1396
  await observeExecutionProgress(onStage, 'preparing')
1329
1397
  const effectivePackages = checkedPackageSelections(packages, routeList, overrides, runOptions.runVerified, targets)
1330
1398
  const results = []
@@ -1406,6 +1474,11 @@ async function runPackages({ packages, task, routingBypassed, routes, previous =
1406
1474
  else {
1407
1475
  result = await executeRouteWithBilling({
1408
1476
  ...runOptions,
1477
+ onSubscriptionResult: async event => {
1478
+ event.id = item.id
1479
+ await awaitExecutionCheckpoint(onSubscriptionResult, event,
1480
+ () => ({ ...partial(), packages: [...results, { ...event, ok: false, ran: !event.notStarted }] }), 'subscription-result')
1481
+ },
1409
1482
  billing: billing ? { ...billing, routes: billing.routes ?? routeList, ...(choices[item.id] ? { choice: choices[item.id] } : {}) } : null,
1410
1483
  credentials, route, task: promptText,
1411
1484
  })
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.25.1 — 2026-10-24
4
+
5
+ - Add DeepSeek Harness Desktop 0.2.1-rc.3 to the exact Host peer range while retaining the existing 0.2.0-rc.1, 0.2.0-rc.2 and 0.2.1-rc.2 declarations.
6
+ - Pin the published 0.2.1-rc.3 SDK dependency graph for source installation and regenerate the Desktop client against that SDK. Compatibility validation covers the plugin's Host/client contracts in addition to the version declarations.
7
+ - Refresh current installation examples and workbench documentation for 0.25.1. Existing API monetary budgets, independent subscription usage, progress/cancellation and schema v3 state remain in place.
8
+
9
+ Validation evidence and limits are tracked in the [0.25.1 acceptance report](docs/releases/0.25.1-validation.md). Offline SDK/build tests do not establish installed-Desktop operation or real vendor inference; earlier execution evidence retains its original version and date.
10
+
11
+ ## 0.25.0 — 2026-10-24
12
+
13
+ - Separate API financial accounting from subscription usage. API costs and API reservations govern monetary budgets; subscriptions track calls and reported input/output/cache tokens, with incomplete or missing usage retained as unknown.
14
+ - Keep confirmed subscription execution independent of exhausted API budgets or missing API prices. Before an actual API call or subscription-to-API fallback, estimate the actual API route and acquire the applicable confirmation and atomic reservation. A CLI using an API key remains API-billed.
15
+ - Preserve subscription-attempt receipts alongside API fallback results and retain both ledgers across retry replacement and history trimming. Old reference-dollar fields remain readable without converting them into current subscription money or fabricated token counts.
16
+ - Show API money and subscription quantities separately in the workbench, execution preview and history. Subscription tokens do not imply vendor allowance, remaining quota or percentage. Additional API quality/review calls still incur API charges.
17
+ - Upgrade persistent state to schema v3 while migrating readable v1/v2 histories and protected execution state. Fully stop all Hosts before updating and keep state backups before downgrading.
18
+
19
+ Final validation and release evidence are tracked in the [0.25.0 acceptance report](docs/releases/0.25.0-validation.md). No new real vendor inference or installed-DSH end-to-end claim is made by these changes.
20
+
3
21
  ## 0.24.0 — 2026-10-24
4
22
 
5
23
  - Reserve remaining execution estimates in shared persistent state before admission. Daily/monthly checks account for recorded API spending plus other active or interrupted reservations, preventing concurrent tasks from independently admitting the same estimated capacity. Unknown estimates or unresolved costs under a finite budget require explicit confirmation.
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: 3829f616ecffbfbc4d1793d4e3f2bc4eac6c9fc0
4
- README.zh.md: 7505f5c1dad20079be2a9bb47a40fd70b5d2bdf4
3
+ README.md: 8ed6d21e04be401519e21fe6b5d6fd04832e9e4a
4
+ README.zh.md: f2052443f9569bbd7311abb15c416472fdd41899
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.24.0** (`@ljwei-stak/dsh-model-router`). Concurrent executions now reserve remaining estimated budget in shared persistent state. Required checkpoints save each package before launch and after its receipt; interrupted executions retain their budget hold and unresolved cost evidence. The workbench separates recorded spending from reserved estimates. Windows/Linux Node 24 CI runs the full test, client build and package-integrity checks. 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.
13
+ Current version: **0.25.1** (`@ljwei-stak/dsh-model-router`). This compatibility update builds against the **0.2.1-rc.3** SDK and adds that Host version to the existing peer range. API calls use monetary accounting and daily/monthly budget reservations; subscriptions use a separate ledger of calls and reported tokens. Confirmed subscription execution is not blocked only because the API budget is exhausted or API prices are missing. A switch to an API must be priced and admitted before that API call; mixed results retain both channel receipts. The workbench separates API spending/reservations from subscription usage, with unknown counters kept unknown. 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 covers **DeepSeek Harness Desktop 0.2.0-rc.1 / 0.2.0-rc.2 / 0.2.1-rc.2 / 0.2.1-rc.3**, 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,26 +27,26 @@ 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 versioned npm package with:
28
28
 
29
29
  ```sh
30
- dsh plugin --profile web add @ljwei-stak/dsh-model-router@0.24.0
30
+ dsh plugin --profile web add @ljwei-stak/dsh-model-router@0.25.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.24.0
36
+ dsh plugin --profile desktop add @ljwei-stak/dsh-model-router@0.25.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.24.0` on npm](https://www.npmjs.com/package/@ljwei-stak/dsh-model-router). 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.25.1` on npm](https://www.npmjs.com/package/@ljwei-stak/dsh-model-router). 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.24.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.24.0 acceptance report](docs/releases/0.24.0-validation.md) for budget reservations, checkpoints 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.25.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.25.0 acceptance report](docs/releases/0.25.0-validation.md) for API/subscription separation and checkpoints, and the [0.25.1 acceptance report](docs/releases/0.25.1-validation.md) for SDK compatibility 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
 
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.
49
+ **Compatibility:** Declared host support is DeepSeek Harness Desktop **0.2.0-rc.1**, **0.2.0-rc.2**, **0.2.1-rc.2**, and **0.2.1-rc.3**. Source builds pin the published **0.2.1-rc.3** SDK packages; retaining an older peer declaration does not turn offline tests into installed-Desktop acceptance. 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
50
 
51
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)
52
52
 
@@ -63,7 +63,7 @@ You can also paste the package spec into **Harness Desktop → Plugins → Add p
63
63
  | Editable CLI execution | A sequential CLI team from the workbench, or one supported CLI from a session, using approved isolated Git worktrees and conditional patch integration. |
64
64
  | Official tools | Detect, install/update, check launch readiness and inspect login status for twelve registry tools; installation jobs have logs and cancellation. |
65
65
  | Interactive terminal | PowerShell/system shell and all twelve registry CLI targets (ZCode TUI on verified Windows desktop installs), including fixed login commands, live output and multiple sessions. |
66
- | Costs and subscriptions | Daily/monthly checks include persistent concurrent budget reservations; recorded spending and remaining estimates are separate. Subscription-first routing, quota cooldown and explicit handling of other subscription failures remain available. |
66
+ | Costs and subscriptions | API money and concurrent budget reservations are separate from subscription calls/tokens. Unknown subscription usage stays unknown; API fallback is re-estimated and admitted before the paid call. |
67
67
  | History and quality | Per-package results, dependency views, failure details, rerun/reassignment, ratings and optional stronger-model review. |
68
68
  | Execution boundaries | Shows what each route can read/write and whether its process uses the Harness sandbox. |
69
69
 
@@ -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.24.0
81
+ @ljwei-stak/dsh-model-router@0.25.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.24.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.25.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.24.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.25.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
 
@@ -102,7 +102,7 @@ The workbench groups the workflow into four navigation tabs. Its header summariz
102
102
  | **任务与执行** — Task and execution | Describe the task, generate advice, choose routed read-only execution or official CLI teamwork, preview and confirm, then inspect history. |
103
103
  | **模型配置** — Model configuration | Refresh/search the model directory and edit exact-route profiles. |
104
104
  | **官方工具** — Official tools | Complete onboarding checks, inspect/install tools and use the interactive terminal. |
105
- | **预算与安全** — Budget and security | Set routing/cost/review policies and inspect subscription status and execution boundaries. |
105
+ | **预算与安全** — Budget and security | Manage API money/budgets and separate subscription usage; inspect subscription state and execution boundaries. |
106
106
 
107
107
  Switching tabs keeps the panels mounted, preserving in-progress installation and terminal state.
108
108
 
@@ -351,18 +351,21 @@ Implementation: [official-team-runtime.mjs](.dsh-plugin/shared/official-team-run
351
351
 
352
352
  ## Costs, subscriptions and quality
353
353
 
354
- - **Budgets:** per-run planning target plus daily/monthly checks. Admission atomically checks recorded API spending plus other executions' remaining reservations in the same DSH_HOME, then reserves the new execution. Each saved receipt replaces its estimate with actual accounting; remaining holds are released only after the executor settles. Over-budget routing can try the economy preset or pause for confirmation. A finite daily/monthly limit with unknown prices, unresolved actual costs or unknown reservations requires explicit confirmation. Each later paid call rechecks fresh actual spending and remaining reservations. If an earlier charge exceeds its estimate or becomes unknown under a finite budget, later calls pause. A budget stop retains completed receipts and releases unused reservations after worker cleanup; crash or storage failures retain their holds. These local estimates do not establish a hard provider billing cap.
354
+ - **API budgets:** per-run planning targets and daily/monthly monetary checks apply to API-billed execution. Admission atomically checks recorded API spending plus other API reservations, then reserves the new API estimate. Unknown API prices, costs or reservations under a finite limit require explicit confirmation. Later API calls recheck fresh spending and holds. Confirmed subscriptions do not consume API money or fail only because its budget is full or its price is missing. Switching a subscription to API is a new monetary admission: estimate the actual API route, obtain any required confirmation, and reserve atomically before the call. Local estimates are not a hard provider billing cap.
355
355
  - **Checkpoints and interruption:** routed executions, retries/reassignment, official teams, single-tool runs and session consultations use the durable lifecycle. Optional reviews and quality upgrades reserve extra capacity before another call. Before a quality upgrade starts, the first receipt and the pending additional attempt are saved, so interruption cannot erase the first charge or imply the second attempt was free. The UI displays actual spending and reservations separately; a reservation is not an invoice. Confirmed dead owners retain interrupted holds and unknown usage evidence until reconciled; elapsed time alone does not free them, and no call is automatically replayed.
356
356
  - **Subscription-first:** the default is to prefer a detected CLI account subscription or a configured coding-plan route. API-key environment variables are removed for CLI subscription attempts. `billing` can be `subscription-first`, `api-only` or `subscription-only`; `subscription` can be `cli-login`, `plan-key` or `none`.
357
- - **Fallback:** detected quota exhaustion/rate limits mark a subscription in cooldown and can retry the routed step on its configured API route, subject to policy and availability. Other attempted subscription failures pause by default (`onSubscriptionFailure: "ask"`): choose API retry, subscription retry or cancel in history. A subscription-to-API route change must pass a fresh estimate and reservation check for that actual API route; a missing price or exceeded budget stops the call. Managed editable/single-tool/team runs do not automatically retry on the API.
358
- - **Accounting:** API costs count toward budgets. Subscription runs display a reference cost separately. A valid CLI-reported amount, including zero, takes precedence. Token-based CLI cost requires complete usage and an actual reported model matching a configured model or its explicit CLI mapping; unknown or mismatched identity keeps cost unknown. The recommendation's price alone cannot establish actual CLI cost. Provider bills remain authoritative. Version 0.15.0 retains spending from evicted detailed runs and replaced retry results, so trimming history or retrying a step does not erase earlier charges. Costs already discarded by older versions cannot be reconstructed.
357
+ - **Fallback:** detected quota exhaustion/rate limits mark a subscription in cooldown and can retry the routed step on its configured API route, subject to policy and availability. Other attempted subscription failures pause by default (`onSubscriptionFailure: "ask"`): choose API retry, subscription retry or cancel in history. A subscription-to-API route change must pass a fresh estimate and reservation check for the actual API route. A missing price or exceeded finite budget requires the applicable confirmation; without it, the API call stops. Managed editable/single-tool/team runs do not automatically retry on the API.
358
+ - **Subscription usage:** record daily/monthly calls, reported input/output/cache tokens and unknown/incomplete-usage counts, with provider/tool/model groups. Missing counters are not zero. A subscription-to-API attempt keeps the subscription usage receipt as well as the API financial result. Historical reference-dollar fields may remain in old records, but current subscription statistics do not convert them to money or fabricate missing token data. Token counts do not establish a vendor subscription allowance, remaining quota or percentage.
359
+ - **API accounting:** valid CLI-reported money, including zero, takes precedence for API-billed results. Otherwise token-based CLI cost requires complete usage and an actual reported model matching configured pricing or an explicit CLI mapping. Unknown or mismatched identity keeps API cost unknown; the recommended price does not prove actual cost. A CLI using an API key still belongs to API accounting. Provider bills remain authoritative. Replaced retry receipts and evicted history retain prior accounting.
359
360
  - **Quality and preference:** optional `off`/`sample`/`always` stronger-model review can add paid calls; it is not treated as human feedback. Explicit ratings adjust separate subjective utility for the exact route and task type, using time decay and low-sample shrinkage (default limit ±0.04, configurable up to ±0.1). They do not change objective quality scores or their hard floors; learning can be disabled, ratings withdrawn, or its starting point reset.
360
361
 
361
362
  Coding-plan keys and endpoints stay in Harness provider settings. A `plan-key` profile may name an exact configured `apiRoute` as its fallback; the profile editor does not accept credentials or arbitrary commands. Vendor plan eligibility and terms must be checked with the vendor.
362
363
 
363
364
  ### Actual cost versus planning estimate
364
365
 
365
- A valid CLI-reported USD amount, including explicit zero, wins over token-price calculation. Otherwise, token-based CLI accounting requires complete normalized usage, known nonnegative prices, and a reported actual model matching the priced route or its explicit CLI mapping:
366
+ The formula below applies to API-billed results. Algorithmic route-price comparisons are planning inputs; they do not turn a subscription into an API bill. The execution preview separates API money from estimated subscription tokens.
367
+
368
+ For API-billed results, a valid CLI-reported USD amount, including explicit zero, wins over token-price calculation. Otherwise, token-based CLI accounting requires complete normalized usage, known nonnegative prices, and a reported actual model matching the priced route or its explicit CLI mapping:
366
369
 
367
370
  <!-- formula:actual-cost -->
368
371
  ```math
@@ -373,7 +376,7 @@ $`C_a`$ is `actualUsd`; $`I_u`$, $`O`$, $`R`$, and $`W`$ correspond to `inputTok
373
376
 
374
377
  These ledger counters are disjoint after adapter normalization; do not add cached tokens to an inclusive input count. Missing optional cache prices use the input price. Incomplete usage, absent prices or unknown/unmatched identity keep the amount `null`, not zero. Complete terminal evidence can survive a failed/cancelled/timed-out run; partial events are marked `usageIncomplete`.
375
378
 
376
- For example, a complete API result with 1,000 uncached input tokens at \$1/million and 500 output tokens at \$5/million costs \$0.0035. A subscription result can show that amount as `referenceCostUsd` while `costUsd` stays null; reference cost is not added to API spending. A provider-reported \$0 is explicit evidence for that attempt, not proof that earlier unknown attempts were free. Actual provider bills are authoritative. No automatic API fallback in official-team mode prevents a router fallback; a vendor CLI may still use its own configured API credentials.
379
+ For example, a complete API result with 1,000 uncached input tokens at \$1/million and 500 output tokens at \$5/million costs \$0.0035. A subscription result instead records the reported tokens and call count; it is not converted to API money. A provider-reported \$0 for an API attempt is explicit evidence for that attempt, not proof that earlier unknown attempts were free. Actual provider bills are authoritative. Official-team mode does not automatically fall back to API, but a vendor CLI using its own API credentials still incurs API-billed usage.
377
380
 
378
381
  <a id="routing"></a>
379
382
 
@@ -452,13 +455,13 @@ Implementation: [router.mjs](.dsh-plugin/shared/router.mjs), [assignment-solver.
452
455
 
453
456
  ## Data and privacy
454
457
 
455
- Router profiles/preferences are held in the Harness plugin settings. Runtime state is stored in `<DSH_HOME>/model-router/state.json` (default `~/.dsh/model-router/state.json`): health/onboarding, subscription cooldown, ratings, up to 200 recent runs, up to 100 execution reservations, 50 terminal metadata records and opt-in public data snapshots. Runs referenced by a reservation are protected from history trimming. Applied rates and data/feedback policy versions are frozen in run records. Preferences are local to that shared DSH_HOME, not authenticated per-user accounts; public data requests do not upload tasks or feedback.
458
+ Router profiles/preferences are held in the Harness plugin settings. Runtime state is stored in `<DSH_HOME>/model-router/state.json` (default `~/.dsh/model-router/state.json`): health/onboarding, subscription cooldown and separate usage aggregates, ratings, up to 200 recent runs, up to 100 execution reservations, 50 terminal metadata records and opt-in public data snapshots. Runs referenced by a reservation are protected from history trimming. Applied rates and data/feedback policy versions are frozen in run records. Preferences are local to that shared DSH_HOME, not authenticated per-user accounts; public data requests do not upload tasks or feedback.
456
459
 
457
460
  Version 0.15.0 also retains bounded spending aggregates for up to 90 populated calendar dates and 24 months. These contain amounts/counts, not task or response content.
458
461
 
459
462
  Run history includes task text, output excerpts, route/workspace, failures and costs. Stored task text is capped at 20,000 characters and each answer at 4,000; reruns can therefore depend on truncated stored context. The plugin does not store API keys in model profiles or terminal transcripts. Task execution still sends the selected task/context to the chosen provider or CLI.
460
463
 
461
- State writes use a lock and atomic replacement. State schema v2 preserves execution ownership, reservations and checkpoints, and upgrades readable v1 state without discarding its ledger. Older plugin versions reject v2 state; fully stop every Host process before upgrading and back up state before any downgrade. Unparseable state is preserved as a `state.json.corrupt-<timestamp>` backup and surfaced as a notice. Back up state/profile data before migrations and avoid sharing unredacted histories or logs.
464
+ State writes use a lock and atomic replacement. Schema v3 preserves separate subscription usage and channel-aware reservations while migrating readable v1/v2 state without discarding API history, holds or old reference fields. Historical missing tokens stay unknown. Older plugin versions reject v3 state; fully stop every Host process before upgrading and back up state before downgrading. Unparseable state is preserved as a `state.json.corrupt-<timestamp>` backup and surfaced as a notice. Avoid sharing unredacted histories or logs.
462
465
 
463
466
  <a id="development"></a>
464
467
 
@@ -496,7 +499,8 @@ Open [the local preview](http://127.0.0.1:4173). It uses mock models, tools and
496
499
  | `shared/official-tool-executor.mjs`, `official-team-runtime.mjs` | Trusted entry, permissions/process capture, dependency execution and patch integration. |
497
500
  | `shared/task-executors.mjs`, `subscription-billing.mjs` | Routed CLI/API execution, subscription policy and fallback. |
498
501
  | `shared/run-ledger.mjs`, `router-state.mjs` | Identity/usage/cost records, retained spending and locked atomic persistence. |
499
- | `shared/budget-reservations.mjs`, `execution-lifecycle.mjs` | Cross-process budget admission, remaining reservations, mandatory checkpoints and interrupted-owner evidence. |
502
+ | `shared/budget-reservations.mjs`, `execution-lifecycle.mjs` | Cross-process API budget admission, channel-aware reservations, mandatory checkpoints and interrupted-owner evidence. |
503
+ | `shared/subscription-usage.mjs` | Independent observed subscription usage, calendar aggregation and retry/history retention. |
500
504
  | Other `.dsh-plugin/shared/` modules | Terminal contracts, tool health, public snapshots and bounded feedback. |
501
505
  | `.dsh-plugin/client.js` | Generated Desktop client bundle; rebuild after client changes. |
502
506
  | `scripts/` and `tests/` | Build/package/verification helpers and regression tests. |
@@ -532,13 +536,14 @@ When maintaining these docs, update both languages in the same change: matching
532
536
  | Symptom | Check |
533
537
  | --- | --- |
534
538
  | No routes or planning disabled | Configure Harness Models, refresh the directory and wait for tool detection. |
535
- | Cost unknown | Supply both input/output USD prices for each compared route. |
539
+ | API cost unknown | Supply both input/output USD prices for each compared API route and inspect usage/model evidence. |
540
+ | Subscription usage unknown | The vendor did not report complete counters, or the historical record lacks them. Do not infer tokens from old dollar references or invent remaining quota. |
536
541
  | CLI installed but not executable | Review launch-readiness reason, platform/mode support and installation source. |
537
542
  | Login/entitlement still fails | Check the vendor account, CLI login, network and actual model access; installation alone is insufficient. |
538
543
  | Frontend says Host is older | Fully quit Harness, including the tray process, then restart. |
539
544
  | Preview differs from old advice | Directory, login, quota or budget state changed; review the new preview. |
540
545
  | Budget remains reserved after interruption | Check the original task, retained worktree and provider bill. The hold is not automatically cleared or replayed, and this release has no manual reconciliation/release control. |
541
- | Older plugin cannot read state | Schema v2 contains protected reservations. Restore a compatible version; do not erase the ledger to bypass its checks. |
546
+ | Older plugin cannot read state | Schema v3 preserves protected execution and separate subscription usage. Restore a compatible version; do not erase accounting to bypass checks. |
542
547
  | Terminal TUI does not render | Check whether the card reports PTY or pipe fallback. |
543
548
  | Logged in, but no executable team route | Check `readyModes`, platform, selected install and permission mode; terminal login alone does not establish managed capability. |
544
549
  | Skills/MCP missing in Claude read-only | Restricted mode disables them; editable/native choices have different permissions and require the corresponding confirmation. |