@ljwei-stak/dsh-model-router 0.23.0 → 0.24.0
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.
- package/.dsh-plugin/client.js +118 -45
- package/.dsh-plugin/index.mjs +318 -59
- package/.dsh-plugin/shared/budget-reservations.mjs +254 -0
- package/.dsh-plugin/shared/execution-lifecycle.mjs +141 -0
- package/.dsh-plugin/shared/official-team-runtime.mjs +37 -13
- package/.dsh-plugin/shared/official-tools-remote.mjs +5 -4
- package/.dsh-plugin/shared/official-tools-runtime.mjs +3 -2
- package/.dsh-plugin/shared/router-state.mjs +23 -8
- package/.dsh-plugin/shared/run-ledger.mjs +6 -1
- package/.dsh-plugin/shared/task-executors.mjs +32 -3
- package/.dsh-plugin/shared/zcode-bundle.mjs +3 -3
- package/CHANGELOG.md +20 -0
- package/README.i18n.yaml +2 -2
- package/README.md +27 -17
- package/README.zh.md +26 -16
- package/docs/WORKBENCH_USER_GUIDE.zh.md +3 -1
- package/docs/assets/workbench-budget-reservations.png +0 -0
- package/docs/assets/workbench-run-timeout.png +0 -0
- package/docs/releases/0.23.1-validation.md +23 -0
- package/docs/releases/0.24.0-validation.md +41 -0
- package/package.json +9 -3
|
@@ -17,9 +17,11 @@ import { mkdir, open, readFile, rename, stat, unlink, writeFile } from 'node:fs/
|
|
|
17
17
|
import { homedir } from 'node:os'
|
|
18
18
|
import { dirname, isAbsolute, join, resolve } from 'node:path'
|
|
19
19
|
import { archiveSpending, sanitizeArchivedSpending } from './run-ledger.mjs'
|
|
20
|
+
import { sanitizeBudgetReservations } from './budget-reservations.mjs'
|
|
20
21
|
|
|
21
22
|
export const MAX_RUNS = 200
|
|
22
|
-
|
|
23
|
+
// Older Hosts must refuse v2 instead of dropping durable execution holds.
|
|
24
|
+
const STATE_VERSION = 2
|
|
23
25
|
|
|
24
26
|
/** Same DSH home rule as the Host: DSH_HOME (with ~ expansion) or ~/.dsh. */
|
|
25
27
|
export function resolveStateHome(env = process.env, home = homedir(), cwd = process.cwd()) {
|
|
@@ -41,7 +43,7 @@ const LOCK_STALE_MS = 15_000
|
|
|
41
43
|
const LOCK_TIMEOUT_MS = 10_000
|
|
42
44
|
|
|
43
45
|
function emptyState() {
|
|
44
|
-
return { version: STATE_VERSION, historyIncomplete: false, onboarding: { completedAt: null }, health: null, quota: {}, authFailures: {}, runs: [], dynamicData: { liveBench: null, pricing: null, status: {}, revision: '' }, archivedSpending: sanitizeArchivedSpending(), notices: [], terminalSessions: [] }
|
|
46
|
+
return { version: STATE_VERSION, historyIncomplete: false, onboarding: { completedAt: null }, health: null, quota: {}, authFailures: {}, runs: [], budgetReservations: {}, dynamicData: { liveBench: null, pricing: null, status: {}, revision: '' }, archivedSpending: sanitizeArchivedSpending(), notices: [], terminalSessions: [] }
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
function sanitizeNotices(value) {
|
|
@@ -51,9 +53,16 @@ function sanitizeNotices(value) {
|
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
function retainRuns(state, maxRuns) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
// A partial run must stay addressable until its hold is reconciled. Archiving
|
|
57
|
+
// it and later upserting a checkpoint would count its receipts twice.
|
|
58
|
+
const protectedIds = new Set(Object.values(state.budgetReservations ?? {}).map(entry => entry.runId))
|
|
59
|
+
let remove = Math.max(0, state.runs.length - Math.max(maxRuns, protectedIds.size))
|
|
60
|
+
const evicted = []
|
|
61
|
+
state.runs = state.runs.filter(run => {
|
|
62
|
+
if (remove > 0 && !protectedIds.has(run.id)) { remove -= 1; evicted.push(run); return false }
|
|
63
|
+
return true
|
|
64
|
+
})
|
|
65
|
+
state.archivedSpending = archiveSpending(evicted, state.archivedSpending)
|
|
57
66
|
return state
|
|
58
67
|
}
|
|
59
68
|
|
|
@@ -94,19 +103,21 @@ function invalidRunSpending(run) {
|
|
|
94
103
|
}
|
|
95
104
|
|
|
96
105
|
function sanitize(value, maxRuns) {
|
|
97
|
-
if (!value || typeof value !== 'object' || Array.isArray(value) || value.version
|
|
106
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || ![1, STATE_VERSION].includes(value.version)
|
|
98
107
|
|| !Array.isArray(value.runs)) {
|
|
99
108
|
const error = new Error('模型路由状态版本或结构不受支持;原文件未修改,请先恢复或升级兼容版本。')
|
|
100
109
|
error.code = 'ROUTER_STATE_UNSUPPORTED'
|
|
101
110
|
throw error
|
|
102
111
|
}
|
|
103
112
|
const runs = value.runs.filter(run => run && typeof run.id === 'string')
|
|
113
|
+
const held = sanitizeBudgetReservations(value.budgetReservations)
|
|
104
114
|
return retainRuns({
|
|
105
115
|
version: STATE_VERSION,
|
|
106
116
|
// Unlike bounded notices, an incomplete-history marker cannot silently
|
|
107
117
|
// expire. Restoring a verified state file is required to recover budgets.
|
|
108
118
|
historyIncomplete: value.historyIncomplete === true || runs.length !== value.runs.length
|
|
109
|
-
|| invalidArchivedSpending(value.archivedSpending) || runs.some(invalidRunSpending)
|
|
119
|
+
|| invalidArchivedSpending(value.archivedSpending) || runs.some(invalidRunSpending) || held.invalid
|
|
120
|
+
|| Object.values(held.reservations).some(entry => !runs.some(run => run.id === entry.runId))
|
|
110
121
|
|| sanitizeNotices(value.notices).some(notice => notice.kind === 'state-corrupt'),
|
|
111
122
|
onboarding: { completedAt: Number.isFinite(value.onboarding?.completedAt) ? value.onboarding.completedAt : null },
|
|
112
123
|
health: value.health && Array.isArray(value.health.tools) ? value.health : null,
|
|
@@ -114,6 +125,7 @@ function sanitize(value, maxRuns) {
|
|
|
114
125
|
authFailures: Object.fromEntries(Object.entries(value.authFailures && typeof value.authFailures === 'object' && !Array.isArray(value.authFailures) ? value.authFailures : {})
|
|
115
126
|
.filter(([, entry]) => Number.isFinite(entry?.at))),
|
|
116
127
|
runs,
|
|
128
|
+
budgetReservations: held.reservations,
|
|
117
129
|
dynamicData: value.dynamicData && typeof value.dynamicData === 'object' && !Array.isArray(value.dynamicData) ? value.dynamicData : { liveBench: null, pricing: null, status: {}, revision: '' },
|
|
118
130
|
archivedSpending: sanitizeArchivedSpending(value.archivedSpending),
|
|
119
131
|
notices: sanitizeNotices(value.notices),
|
|
@@ -172,7 +184,10 @@ async function acquireLock(lockFile, { staleMs = LOCK_STALE_MS, timeoutMs = LOCK
|
|
|
172
184
|
if (await readFile(lockFile, 'utf8').catch(() => null) === owner) await unlink(lockFile).catch(() => {})
|
|
173
185
|
}
|
|
174
186
|
} catch (error) {
|
|
175
|
-
|
|
187
|
+
// Windows can report a sharing/delete-pending race as EPERM while
|
|
188
|
+
// another owner closes and removes its lock. Retry within the same
|
|
189
|
+
// bounded wait; never treat this as permission to remove a live lock.
|
|
190
|
+
if (error?.code !== 'EEXIST' && !(process.platform === 'win32' && error?.code === 'EPERM')) throw error
|
|
176
191
|
const info = await stat(lockFile).catch(() => null)
|
|
177
192
|
const owner = await readFile(lockFile, 'utf8').catch(() => null)
|
|
178
193
|
if (info && owner !== null && now() - info.mtimeMs > staleMs && ownerIsDead(ownerPid(owner))) {
|
|
@@ -13,6 +13,7 @@ export const MAX_STORED_TASK = 20_000
|
|
|
13
13
|
export const MAX_QUALITY_BIAS = 0.04
|
|
14
14
|
|
|
15
15
|
const finite = value => typeof value === 'number' && Number.isFinite(value)
|
|
16
|
+
const receiptTimestamp = value => finite(value) && value >= 0 && Number.isFinite(new Date(value).getTime())
|
|
16
17
|
const nonEmptyText = value => (typeof value === 'string' && value.length > 0 ? value : null)
|
|
17
18
|
const routeKey = (provider, model) => `${String(provider ?? '')}\u0000${String(model ?? '')}`
|
|
18
19
|
|
|
@@ -382,7 +383,7 @@ function storedPackage(planned, result, pricing, finishedAt, billingFor = billin
|
|
|
382
383
|
// silently rewrite historical spend calculated from usage.
|
|
383
384
|
pricingSnapshot: pricing ? { ...pricing } : null,
|
|
384
385
|
...cost,
|
|
385
|
-
finishedAt,
|
|
386
|
+
finishedAt: receiptTimestamp(result?.finishedAt) ? result.finishedAt : finishedAt,
|
|
386
387
|
review: planned.review ?? null,
|
|
387
388
|
rating: planned.rating ?? null,
|
|
388
389
|
}
|
|
@@ -488,6 +489,7 @@ export function teamExecutionResults(plan, execution) {
|
|
|
488
489
|
...reasoningExecutionFields(result),
|
|
489
490
|
...(finite(result.durationMs) && result.durationMs >= 0 ? { durationMs: result.durationMs } : {}),
|
|
490
491
|
...(finite(result.inputChars) && result.inputChars >= 0 ? { inputChars: result.inputChars } : {}),
|
|
492
|
+
...(receiptTimestamp(result.finishedAt) ? { finishedAt: result.finishedAt } : {}),
|
|
491
493
|
requestedCliModel: result.requestedCliModel ?? null,
|
|
492
494
|
actualModel: result.actualModel ?? null,
|
|
493
495
|
...(result.reportedModel !== undefined ? { reportedModel: result.reportedModel } : {}),
|
|
@@ -541,6 +543,8 @@ export function buildToolRunRecord({ toolId, toolLabel, provider, model, cliMode
|
|
|
541
543
|
status: result?.status ?? 'pending',
|
|
542
544
|
packages: [{
|
|
543
545
|
id: packageId, ok: succeeded, cancelled: result?.status === 'cancelled',
|
|
546
|
+
...(result?.notStarted === true || result?.status === 'unsupported' ? { notStarted: true } : {}),
|
|
547
|
+
...(result?.blocked === true ? { blocked: true } : {}),
|
|
544
548
|
provider: plan.directRoute.provider, model: plan.directRoute.model,
|
|
545
549
|
channel: 'official-cli', toolId,
|
|
546
550
|
requestedCliModel: Object.hasOwn(result ?? {}, 'requestedModel') ? result.requestedModel : cliModel,
|
|
@@ -550,6 +554,7 @@ export function buildToolRunRecord({ toolId, toolLabel, provider, model, cliMode
|
|
|
550
554
|
...(typeof result?.source === 'string' ? { source: result.source } : {}),
|
|
551
555
|
...(typeof result?.sandboxEnforcement === 'string' ? { sandboxEnforcement: result.sandboxEnforcement } : {}),
|
|
552
556
|
...(result?.usageIncomplete === true ? { usageIncomplete: true } : {}),
|
|
557
|
+
...(receiptTimestamp(result?.finishedAt) ? { finishedAt: result.finishedAt } : {}),
|
|
553
558
|
answer: String(result?.finalText ?? ''),
|
|
554
559
|
error: succeeded ? null : String(result?.error ?? result?.reason ?? result?.status ?? '官方 CLI 未成功完成。'),
|
|
555
560
|
...(!succeeded && typeof result?.diagnostic === 'string' && result.diagnostic.trim()
|
|
@@ -1229,6 +1229,25 @@ export async function observeExecutionProgress(hook, value) {
|
|
|
1229
1229
|
try { await hook(value) } catch { /* Progress reporting does not control execution. */ }
|
|
1230
1230
|
}
|
|
1231
1231
|
|
|
1232
|
+
/** Required Host persistence is a dispatch boundary, unlike progress observers. */
|
|
1233
|
+
export async function awaitExecutionCheckpoint(hook, value, partial, stage) {
|
|
1234
|
+
if (typeof hook !== 'function') return
|
|
1235
|
+
try { await hook(value) } catch (cause) {
|
|
1236
|
+
const budgetChanged = cause?.code === 'ROUTER_BUDGET_CHANGED'
|
|
1237
|
+
const error = new Error(budgetChanged ? String(cause.message) : `执行检查点保存失败:${String(cause?.message ?? cause)}`, { cause })
|
|
1238
|
+
error.code = budgetChanged ? 'ROUTER_BUDGET_CHANGED' : 'ROUTER_CHECKPOINT_FAILED'
|
|
1239
|
+
if (budgetChanged) {
|
|
1240
|
+
error.budget = cause.budget
|
|
1241
|
+
error.runId = cause.runId
|
|
1242
|
+
error.reason = cause.reason
|
|
1243
|
+
}
|
|
1244
|
+
error.checkpointStage = stage
|
|
1245
|
+
// The caller can retain paid receipts even when the persistence sink failed.
|
|
1246
|
+
error.execution = typeof partial === 'function' ? partial() : partial
|
|
1247
|
+
throw error
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1232
1251
|
function checkedPackageSelections(packages, routes, overrides, runVerified, targets) {
|
|
1233
1252
|
const preceding = new Set()
|
|
1234
1253
|
const selected = {}
|
|
@@ -1305,11 +1324,12 @@ async function runManualTool(target, prompt, options) {
|
|
|
1305
1324
|
*/
|
|
1306
1325
|
async function runPackages({ packages, task, routingBypassed, routes, previous = [], targets = null, overrides = {}, choices = {}, options }) {
|
|
1307
1326
|
const routeList = Array.isArray(routes) ? routes : []
|
|
1308
|
-
const { credentialsFor, onPackageStart, onPackage, onStage, billing, ...runOptions } = options
|
|
1327
|
+
const { credentialsFor, onPackageStart, onPackage, onStage, onCheckpointStart, onCheckpointResult, billing, ...runOptions } = options
|
|
1309
1328
|
await observeExecutionProgress(onStage, 'preparing')
|
|
1310
1329
|
const effectivePackages = checkedPackageSelections(packages, routeList, overrides, runOptions.runVerified, targets)
|
|
1311
1330
|
const results = []
|
|
1312
1331
|
const ranIds = []
|
|
1332
|
+
const partial = () => ({ ...aggregateOf([...results]), status: runOptions.signal?.aborted ? 'cancelled' : 'failed', ranIds: [...ranIds] })
|
|
1313
1333
|
if (!runOptions.signal?.aborted) await observeExecutionProgress(onStage, 'executing')
|
|
1314
1334
|
for (const item of effectivePackages) {
|
|
1315
1335
|
const kept = previous.find(result => result.id === item.id)
|
|
@@ -1330,6 +1350,7 @@ async function runPackages({ packages, task, routingBypassed, routes, previous =
|
|
|
1330
1350
|
...identity, answer: '', fallback: null, cancelled: true, notStarted: true, error: '执行已取消,此步骤未开始。' }
|
|
1331
1351
|
results.push(cancelled)
|
|
1332
1352
|
ranIds.push(item.id)
|
|
1353
|
+
await awaitExecutionCheckpoint(onCheckpointResult, cancelled, partial, 'result')
|
|
1333
1354
|
await observeExecutionProgress(onPackage, cancelled)
|
|
1334
1355
|
}
|
|
1335
1356
|
if (runOptions.signal?.aborted) {
|
|
@@ -1349,6 +1370,7 @@ async function runPackages({ packages, task, routingBypassed, routes, previous =
|
|
|
1349
1370
|
}
|
|
1350
1371
|
results.push(blocked)
|
|
1351
1372
|
ranIds.push(item.id)
|
|
1373
|
+
await awaitExecutionCheckpoint(onCheckpointResult, blocked, partial, 'result')
|
|
1352
1374
|
await observeExecutionProgress(onPackage, blocked)
|
|
1353
1375
|
continue
|
|
1354
1376
|
}
|
|
@@ -1364,9 +1386,15 @@ async function runPackages({ packages, task, routingBypassed, routes, previous =
|
|
|
1364
1386
|
continue
|
|
1365
1387
|
}
|
|
1366
1388
|
const startedAt = Date.now()
|
|
1367
|
-
|
|
1389
|
+
const starting = { ...item, ...identity, provider, model, startedAt,
|
|
1368
1390
|
channel: target?.kind === 'tool' ? 'official-cli' : 'harness-llm',
|
|
1369
|
-
...(target?.kind === 'tool' ? { toolId: target.toolId } : {}) }
|
|
1391
|
+
...(target?.kind === 'tool' ? { toolId: target.toolId } : {}) }
|
|
1392
|
+
await observeExecutionProgress(onPackageStart, starting)
|
|
1393
|
+
if (runOptions.signal?.aborted) {
|
|
1394
|
+
await cancelledBeforeStart()
|
|
1395
|
+
continue
|
|
1396
|
+
}
|
|
1397
|
+
await awaitExecutionCheckpoint(onCheckpointStart, starting, partial, 'start')
|
|
1370
1398
|
if (runOptions.signal?.aborted) {
|
|
1371
1399
|
await cancelledBeforeStart()
|
|
1372
1400
|
continue
|
|
@@ -1404,6 +1432,7 @@ async function runPackages({ packages, task, routingBypassed, routes, previous =
|
|
|
1404
1432
|
}
|
|
1405
1433
|
results.push(entry)
|
|
1406
1434
|
ranIds.push(item.id)
|
|
1435
|
+
await awaitExecutionCheckpoint(onCheckpointResult, entry, partial, 'result')
|
|
1407
1436
|
await observeExecutionProgress(onPackage, entry)
|
|
1408
1437
|
}
|
|
1409
1438
|
return { ...aggregateOf(results), ranIds }
|
|
@@ -99,7 +99,7 @@ function rootFromCommand(value) {
|
|
|
99
99
|
const trimmed = value.trim()
|
|
100
100
|
const quoted = /^"([^"]+)"/.exec(trimmed)
|
|
101
101
|
const executable = quoted?.[1] ?? /^(.+?\.(?:exe|ico))(?=,|\s|$)/i.exec(trimmed)?.[1]
|
|
102
|
-
return executable && win32.isAbsolute(executable) ? dirname(executable) : null
|
|
102
|
+
return executable && win32.isAbsolute(executable) ? win32.dirname(executable) : null
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
/** Explicit selection is authoritative, even when the selected bundle is missing. */
|
|
@@ -125,8 +125,8 @@ export function zcodeDiscoveryCandidates({ installDir = '', env = process.env, r
|
|
|
125
125
|
}
|
|
126
126
|
for (const parent of [env.LOCALAPPDATA, env.ProgramFiles, env['ProgramFiles(x86)'], env.ProgramW6432]) {
|
|
127
127
|
if (typeof parent !== 'string' || !win32.isAbsolute(parent)) continue
|
|
128
|
-
add(join(parent, 'Programs', 'ZCode'), 'standard-directory')
|
|
129
|
-
add(join(parent, 'ZCode'), 'standard-directory')
|
|
128
|
+
add(win32.join(parent, 'Programs', 'ZCode'), 'standard-directory')
|
|
129
|
+
add(win32.join(parent, 'ZCode'), 'standard-directory')
|
|
130
130
|
}
|
|
131
131
|
return candidates
|
|
132
132
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.24.0 — 2026-10-24
|
|
4
|
+
|
|
5
|
+
- 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.
|
|
6
|
+
- Save mandatory package checkpoints before external calls and after receipts, with actual usage replacing the completed estimate before another paid step. Apply the lifecycle to routed runs, retries/reassignment, official teams, single-tool runs and consultations; optional review and quality-upgrade calls reserve extra capacity first. Checkpoint failures stop later calls and retain evidence.
|
|
7
|
+
- Recheck fresh actual spending and remaining reservations before each later paid call. Earlier costs above estimate or unresolved under a finite budget stop downstream calls; after worker cleanup, a budget stop retains completed receipts and releases unused holds. Subscription-to-API fallback re-estimates the actual route and rejects missing-price or over-budget calls. Save the first receipt and pending extra attempt before a quality upgrade starts.
|
|
8
|
+
- Preserve interrupted reservations and unknown usage when an execution owner is confirmed dead. Protect referenced run records from trimming, block rerun/reassignment while a hold exists, and distinguish actual spending from reserved estimates in the UI. No automatic replay, time-based hold expiry or manual reconciliation/release control is added.
|
|
9
|
+
- Upgrade state schema from v1 to v2 while preserving the ledger. Older plugin versions reject v2 state, so fully stop all Host processes before updating and back up state before a downgrade.
|
|
10
|
+
- Add Windows/Linux Node 24 CI for strict frozen dependency installation, the complete test suite, committed and rebuilt client consistency, offline preview compilation, and npm archive verification against raw Git blobs and the runtime import closure.
|
|
11
|
+
|
|
12
|
+
Validation evidence and pending release checks are tracked in the [0.24.0 acceptance report](docs/releases/0.24.0-validation.md). No new real provider or vendor CLI inference is claimed. Budget reservations are local estimates, not a provider billing cap; interrupted fees must be reconciled before their capacity can safely be reused.
|
|
13
|
+
- Preserve observed receipt timestamps across checkpoints; handle Windows lock contention and declared-platform PATH/ZCode paths, with canonical temporary-path and explicit adapter-capability CI fixtures.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## 0.23.1 — 2026-10-23
|
|
17
|
+
|
|
18
|
+
- 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.
|
|
19
|
+
- 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.
|
|
20
|
+
- Preserve late-response fences and clear request lifetimes on panel close. Add offline Host budget, real SDK cancellation transport and hung-request UI regressions.
|
|
21
|
+
|
|
22
|
+
|
|
3
23
|
## 0.23.0 — 2026-10-23
|
|
4
24
|
|
|
5
25
|
- 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:
|
|
4
|
-
README.zh.md:
|
|
3
|
+
README.md: 3829f616ecffbfbc4d1793d4e3f2bc4eac6c9fc0
|
|
4
|
+
README.zh.md: 7505f5c1dad20079be2a9bb47a40fd70b5d2bdf4
|
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.
|
|
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.
|
|
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
|
|
|
@@ -24,25 +24,25 @@ Both README versions describe the same interfaces, formulas, defaults and limits
|
|
|
24
24
|
|
|
25
25
|
**Category:** **Models & Reasoning** (模型与推理), the best fit in the [community directory's category list](https://www.alldsh.com/categories/); this is a community taxonomy, not an official DSH classification.
|
|
26
26
|
|
|
27
|
-
**Install (DSH Plugin Hub / web profile):** With the DSH CLI available, install the
|
|
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.
|
|
30
|
+
dsh plugin --profile web add @ljwei-stak/dsh-model-router@0.24.0
|
|
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.
|
|
36
|
+
dsh plugin --profile desktop add @ljwei-stak/dsh-model-router@0.24.0
|
|
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.
|
|
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).
|
|
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.
|
|
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.
|
|
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
|
|
|
@@ -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 budget
|
|
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. |
|
|
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.
|
|
81
|
+
@ljwei-stak/dsh-model-router@0.24.0
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
|
@@ -117,7 +117,11 @@ Switching tabs keeps the panels mounted, preserving in-progress installation and
|
|
|
117
117
|
5. In **在工作台执行**, choose **模型路由(只读)** for the existing routed executor, or first select **团队分工** and choose **官方工具协作**. For official teamwork, select **只读检查** (read-only) or **编辑文件** (edit files). Editable teams also choose **Harness 受限环境** (Harness sandbox, the default) or **厂商 CLI 正常环境** (normal vendor environment, outside the Harness sandbox). Enter an existing absolute workspace path, or reuse the last run's path. Click **预览执行计划**, review each package, its permissions and required confirmations, then explicitly confirm execution. The Host rechecks the plan before starting; changed conditions can require confirmation again.
|
|
118
118
|
6. Inspect **执行记录与子任务** for the actual channel, response, failures, cost and ratings. Official teamwork replans using only installed CLIs ready for the selected permission mode. Edits require approval and a clean Git repository; successful worktree changes are conditionally integrated. An unavailable or failed CLI never silently becomes an API call in this mode.
|
|
119
119
|
|
|
120
|
-
After execution starts, the workbench shows elapsed time, the current package and each package's state. **取消执行** requests cancellation of the active call and stops later packages from starting; **正在取消** remains visible until the executor finishes cleanup. Completed results and reported usage remain in history. Cancelling cannot undo charges already incurred or changes already integrated. Closing the panel does not cancel a run; reopening it in the same browser session can reconnect while the Host retains that job. Host restarts clear these in-memory jobs
|
|
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; durably saved package receipts and budget reservations remain. A confirmed dead execution owner is marked `interrupted`, without automatically resuming calls or releasing its hold.
|
|
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
|
+
|
|
124
|
+
History shows active and interrupted budget reservations separately from actual spending. Runs with a reservation cannot be rerun or reassigned. If a Host crash or checkpoint failure leaves a reservation, first inspect the original task, retained worktree and provider bill; this release has no manual reconciliation/release control. Do not delete state to make the capacity appear free.
|
|
121
125
|
|
|
122
126
|
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
127
|
|
|
@@ -337,9 +341,9 @@ A worktree separates collected changes; only the selected Harness environment su
|
|
|
337
341
|
|
|
338
342
|
### Failure and retry
|
|
339
343
|
|
|
340
|
-
History stores package status, exit code, bounded scrubbed diagnostics, entry/model evidence and available usage/cost. A later adapter exception does not discard earlier results. `cli-completed` means the runtime completed its process/patch workflow; deliverables still need acceptance checks. `blocked`, `cancelled`, `incomplete` and `integration-pending` distinguish preflight, interruption, package failure and integration problems.
|
|
344
|
+
Required checkpoints persist package launch state before an external call and its available result, usage and cost before the next paid step. A checkpoint failure stops later steps and preserves the reservation for reconciliation. History stores package status, exit code, bounded scrubbed diagnostics, entry/model evidence and available usage/cost. A later adapter exception does not discard earlier results. `cli-completed` means the runtime completed its process/patch workflow; deliverables still need acceptance checks. `blocked`, `cancelled`, `incomplete` and `integration-pending` distinguish preflight, interruption, package failure and integration problems.
|
|
341
345
|
|
|
342
|
-
A retry targets a failed package and unfinished downstream packages, preserving other successful results. Eligible editable `incomplete/cancelled` runs require the recorded source, retained worktree and baseline: the Host validates them, creates a fresh worktree, seeds prior unintegrated edits, and continues. Already integrated or `integration-pending` teams cannot use this step-resume path. Native retry again requires write and unsandboxed consent (`confirmWrite` and `confirmUnsandboxed` in the workbench rerun request); disabling the legacy `confirmUnsandboxedCli` setting cannot bypass it. Previous attempt charges remain in spending totals.
|
|
346
|
+
A retry targets a failed package and unfinished downstream packages, preserving other successful results. Eligible editable `incomplete/cancelled` runs require the recorded source, retained worktree and baseline: the Host validates them, creates a fresh worktree, seeds prior unintegrated edits, and continues. Already integrated or `integration-pending` teams cannot use this step-resume path. Native retry again requires write and unsandboxed consent (`confirmWrite` and `confirmUnsandboxed` in the workbench rerun request); disabling the legacy `confirmUnsandboxedCli` setting cannot bypass it. Previous attempt charges remain in spending totals. This retry path requires no active or interrupted reservation on the original run; a crash checkpoint is evidence to reconcile, not an automatic resume token.
|
|
343
347
|
|
|
344
348
|
Implementation: [official-team-runtime.mjs](.dsh-plugin/shared/official-team-runtime.mjs), [official-tool-executor.mjs](.dsh-plugin/shared/official-tool-executor.mjs), and [run-ledger.mjs](.dsh-plugin/shared/run-ledger.mjs).
|
|
345
349
|
|
|
@@ -347,9 +351,10 @@ Implementation: [official-team-runtime.mjs](.dsh-plugin/shared/official-team-run
|
|
|
347
351
|
|
|
348
352
|
## Costs, subscriptions and quality
|
|
349
353
|
|
|
350
|
-
- **Budgets:** per-run planning target plus daily/monthly checks. Over-budget routing can try the economy preset or pause for confirmation.
|
|
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.
|
|
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.
|
|
351
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`.
|
|
352
|
-
- **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. Managed editable/single-tool/team runs do not automatically retry on the API.
|
|
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.
|
|
353
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.
|
|
354
359
|
- **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.
|
|
355
360
|
|
|
@@ -447,13 +452,13 @@ Implementation: [router.mjs](.dsh-plugin/shared/router.mjs), [assignment-solver.
|
|
|
447
452
|
|
|
448
453
|
## Data and privacy
|
|
449
454
|
|
|
450
|
-
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, 50 terminal metadata records and opt-in public data snapshots. 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.
|
|
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.
|
|
451
456
|
|
|
452
457
|
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.
|
|
453
458
|
|
|
454
459
|
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.
|
|
455
460
|
|
|
456
|
-
State writes use a lock and atomic replacement. 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.
|
|
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.
|
|
457
462
|
|
|
458
463
|
<a id="development"></a>
|
|
459
464
|
|
|
@@ -491,6 +496,7 @@ Open [the local preview](http://127.0.0.1:4173). It uses mock models, tools and
|
|
|
491
496
|
| `shared/official-tool-executor.mjs`, `official-team-runtime.mjs` | Trusted entry, permissions/process capture, dependency execution and patch integration. |
|
|
492
497
|
| `shared/task-executors.mjs`, `subscription-billing.mjs` | Routed CLI/API execution, subscription policy and fallback. |
|
|
493
498
|
| `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. |
|
|
494
500
|
| Other `.dsh-plugin/shared/` modules | Terminal contracts, tool health, public snapshots and bounded feedback. |
|
|
495
501
|
| `.dsh-plugin/client.js` | Generated Desktop client bundle; rebuild after client changes. |
|
|
496
502
|
| `scripts/` and `tests/` | Build/package/verification helpers and regression tests. |
|
|
@@ -501,6 +507,8 @@ Open [the local preview](http://127.0.0.1:4173). It uses mock models, tools and
|
|
|
501
507
|
|
|
502
508
|
`check:client` rebuilds in memory and compares the generated output with the committed bundle; `prepack` refuses a stale bundle. pnpm 10 has no `pnpm peers check` command: use the strict install above. Unavailable/incompatible host SDK packages may require the installed Harness runtime for local integration testing; [setup-harness-dev.mjs](scripts/setup-harness-dev.mjs) links missing SDK packages from an explicitly supplied Host `node_modules`, and [start-harness-preview.mjs](scripts/start-harness-preview.mjs) starts a separate local web Host profile. These helpers do not replace dependency verification or Desktop acceptance checks.
|
|
503
509
|
|
|
510
|
+
GitHub Actions runs a Node 24 matrix on Windows and Linux for the frozen strict dependency install, full test suite, committed-client consistency, actual client build and offline preview compilation. It packs the candidate and checks every file against raw Git blobs plus the runtime import closure. These checks use local fixtures and do not exercise real provider billing. After committing a local candidate, reproduce package verification with `node scripts/check-package.mjs` following `npm run pack:local`.
|
|
511
|
+
|
|
504
512
|
Legacy compatibility/research files and dated reports are preserved for reference; their presence does not make every older integration an active feature of the current packaged plugin. Releases and synchronization follow [AGENTS.md](AGENTS.md); use versioned packages or verified release tags to reproduce a release.
|
|
505
513
|
|
|
506
514
|
### Verification layers and known limits
|
|
@@ -529,6 +537,8 @@ When maintaining these docs, update both languages in the same change: matching
|
|
|
529
537
|
| Login/entitlement still fails | Check the vendor account, CLI login, network and actual model access; installation alone is insufficient. |
|
|
530
538
|
| Frontend says Host is older | Fully quit Harness, including the tray process, then restart. |
|
|
531
539
|
| Preview differs from old advice | Directory, login, quota or budget state changed; review the new preview. |
|
|
540
|
+
| 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. |
|
|
532
542
|
| Terminal TUI does not render | Check whether the card reports PTY or pipe fallback. |
|
|
533
543
|
| Logged in, but no executable team route | Check `readyModes`, platform, selected install and permission mode; terminal login alone does not establish managed capability. |
|
|
534
544
|
| Skills/MCP missing in Claude read-only | Restricted mode disables them; editable/native choices have different permissions and require the corresponding confirmation. |
|