@ljwei-stak/dsh-model-router 0.23.1 → 0.25.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.
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Durable execution holds. Every transition uses router-state's one file-lock
3
+ * transaction, so another Host cannot approve against an out-of-date balance.
4
+ * A hold is a remaining estimate, never an invoice or proof of a zero charge.
5
+ */
6
+ import { randomUUID } from 'node:crypto'
7
+ import { budgetCheck, spending } from './run-ledger.mjs'
8
+
9
+ export const MAX_EXECUTION_RESERVATIONS = 100
10
+ const record = value => value !== null && typeof value === 'object' && !Array.isArray(value)
11
+ const amount = value => typeof value === 'number' && Number.isFinite(value) && value >= 0
12
+ const identifier = value => typeof value === 'string' && value.length > 0 && value.length <= 512 && !/[\u0000-\u001f\u007f]/u.test(value)
13
+ const timestamp = value => amount(value) && Number.isFinite(new Date(value).getTime())
14
+
15
+ /** Unknown/corrupt holds make history incomplete; silently losing them is unsafe. */
16
+ export function sanitizeBudgetReservations(value) {
17
+ if (value === undefined) return { reservations: {}, invalid: false }
18
+ if (!record(value)) return { reservations: {}, invalid: true }
19
+ const reservations = {}
20
+ let invalid = false
21
+ for (const [key, entry] of Object.entries(value)) {
22
+ if (!identifier(key) || !record(entry) || entry.id !== key || entry.runId !== key
23
+ || !identifier(entry.ownerToken) || !Number.isSafeInteger(entry.ownerPid) || entry.ownerPid < 1
24
+ || !['active', 'interrupted'].includes(entry.status) || !timestamp(entry.createdAt) || !timestamp(entry.updatedAt)
25
+ || !Array.isArray(entry.packages) || !Array.isArray(entry.completedIds)
26
+ || entry.completedIds.some(id => !identifier(id))
27
+ || entry.packages.some(item => !record(item) || !identifier(item.id)
28
+ || (item.billing !== undefined && !['api', 'subscription'].includes(item.billing))
29
+ || (item.estimatedCostUsd !== null && !amount(item.estimatedCostUsd)) || typeof item.started !== 'boolean')
30
+ || new Set(entry.packages.map(item => item.id)).size !== entry.packages.length
31
+ || entry.packages.some(item => entry.completedIds.includes(item.id))) {
32
+ invalid = true
33
+ continue
34
+ }
35
+ reservations[key] = {
36
+ id: key, runId: key, ownerToken: entry.ownerToken, ownerPid: entry.ownerPid,
37
+ status: entry.status, createdAt: entry.createdAt, updatedAt: entry.updatedAt,
38
+ ...(timestamp(entry.interruptedAt) ? { interruptedAt: entry.interruptedAt } : {}),
39
+ packages: entry.packages.map(item => ({ id: item.id, estimatedCostUsd: item.estimatedCostUsd, started: item.started, ...(item.billing ? { billing: item.billing } : {}) })),
40
+ completedIds: [...new Set(entry.completedIds)],
41
+ }
42
+ }
43
+ return { reservations, invalid }
44
+ }
45
+
46
+ /** Live and unreconciled interrupted work occupies today's and this month's room. */
47
+ export function reservationSpending(state, { excludeReservationId = null } = {}) {
48
+ let reservedUsd = 0
49
+ let unknownReservations = 0
50
+ for (const entry of Object.values(state?.budgetReservations ?? {})) {
51
+ if (!record(entry) || entry.id === excludeReservationId) continue
52
+ for (const item of Array.isArray(entry.packages) ? entry.packages : []) {
53
+ if (item.billing === 'subscription') continue
54
+ if (amount(item.estimatedCostUsd)) reservedUsd += item.estimatedCostUsd
55
+ if (!amount(item.estimatedCostUsd) || (entry.status === 'interrupted' && item.started)) unknownReservations += 1
56
+ }
57
+ }
58
+ return { today: reservedUsd, month: reservedUsd, unknownToday: unknownReservations,
59
+ unknownMonth: unknownReservations, reservedUsd, unknownReservations }
60
+ }
61
+
62
+ function normalizedEstimates(value, run) {
63
+ const values = value ?? (run?.packages ?? []).map(item => ({ id: item.id, estimatedCostUsd: item.estimatedCost ?? null }))
64
+ if (!Array.isArray(values) || values.some(item => !record(item) || !identifier(item.id))) {
65
+ throw new TypeError('工作包预占必须包含有效的工作包 ID。')
66
+ }
67
+ const seen = new Set()
68
+ return values.map(item => {
69
+ if (seen.has(item.id)) throw new TypeError('工作包预占 ID 不能重复。')
70
+ seen.add(item.id)
71
+ const estimatedCostUsd = Object.hasOwn(item, 'estimatedCostUsd') ? item.estimatedCostUsd : item.estimatedCost ?? null
72
+ if (estimatedCostUsd !== null && !amount(estimatedCostUsd)) throw new TypeError('工作包预估费用必须是非负有限数字或 null。')
73
+ if (item.billing !== undefined && !['api', 'subscription'].includes(item.billing)) throw new TypeError('预占计费通道无效。')
74
+ return { id: item.id, estimatedCostUsd: item.billing === 'subscription' ? 0 : estimatedCostUsd,
75
+ started: item.started === true, ...(item.billing ? { billing: item.billing } : {}) }
76
+ })
77
+ }
78
+
79
+ function checkedRun(run, id = run?.id) {
80
+ if (!record(run) || !identifier(id) || run.id !== id || !Array.isArray(run.packages)) {
81
+ throw new TypeError('执行检查点必须包含对应的完整运行记录。')
82
+ }
83
+ return structuredClone(run)
84
+ }
85
+
86
+ function upsertRun(current, run) {
87
+ const index = current.runs.findIndex(item => item.id === run.id)
88
+ if (index < 0) current.runs.push(run)
89
+ else current.runs[index] = run
90
+ }
91
+
92
+ function ownedReservation(current, reservationId, ownerToken) {
93
+ const entry = current.budgetReservations?.[reservationId]
94
+ if (!entry || entry.ownerToken !== ownerToken || entry.status !== 'active') {
95
+ const error = new Error('执行预占不存在、已中断或属于其他执行;原记录未修改。')
96
+ error.code = 'ROUTER_RESERVATION_OWNERSHIP'
97
+ throw error
98
+ }
99
+ return entry
100
+ }
101
+
102
+ function estimateTotal(packages) {
103
+ packages = packages.filter(item => item.billing !== 'subscription')
104
+ if (packages.some(item => item.estimatedCostUsd === null)) return null
105
+ const total = packages.reduce((sum, item) => sum + item.estimatedCostUsd, 0)
106
+ return Number.isFinite(total) ? total : null
107
+ }
108
+
109
+ function currentBudget(current, at, packages, options, excludeReservationId = null) {
110
+ const recorded = spending(current.runs, at, current.archivedSpending)
111
+ const holds = reservationSpending(current, { excludeReservationId })
112
+ const spent = { ...recorded, today: recorded.today + holds.today, month: recorded.month + holds.month,
113
+ unknownToday: recorded.unknownToday + holds.unknownToday, unknownMonth: recorded.unknownMonth + holds.unknownMonth }
114
+ const budget = budgetCheck({ estimateUsd: estimateTotal(packages), spent,
115
+ dailyLimitUsd: options.dailyLimitUsd, monthlyLimitUsd: options.monthlyLimitUsd })
116
+ const apiExposure = packages.some(item => item.billing !== 'subscription')
117
+ budget.apiExposure = apiExposure
118
+ if (!apiExposure) { budget.exceeded = null; budget.message = '订阅按用量单独统计,不占用 API 金额预算。' }
119
+ const unknownBudget = apiExposure && budget.limited && (!budget.estimateKnown
120
+ || (options.dailyLimitUsd > 0 && spent.unknownToday > 0)
121
+ || (options.monthlyLimitUsd > 0 && spent.unknownMonth > 0))
122
+ if (unknownBudget && !budget.exceeded) budget.message = '存在未核对费用或缺少单价,无法保证本次执行符合预算;请先核对账单或明确确认继续。'
123
+ return { budget, spent, recorded, unknownBudget, ...holds }
124
+ }
125
+
126
+ /** false means definitely dead; permission failures and PID reuse stay conservative. */
127
+ function processAlive(pid) {
128
+ try { process.kill(pid, 0); return true }
129
+ catch (error) { return error?.code === 'ESRCH' ? false : null }
130
+ }
131
+
132
+ export function createBudgetReservations({ state, now = Date.now, pid = process.pid, isProcessAlive = processAlive,
133
+ maxReservations = MAX_EXECUTION_RESERVATIONS } = {}) {
134
+ if (!state || typeof state.update !== 'function' || typeof state.read !== 'function') throw new TypeError('需要持久状态存储。')
135
+ if (!Number.isSafeInteger(pid) || pid < 1) throw new TypeError('需要有效的执行进程 ID。')
136
+ if (!Number.isInteger(maxReservations) || maxReservations < 1 || maxReservations > MAX_EXECUTION_RESERVATIONS) throw new RangeError('执行预占数量上限无效。')
137
+ const snapshot = async () => reservationSpending(await state.read())
138
+ const beginExecution = options => {
139
+ const run = checkedRun(options?.run)
140
+ const ownerToken = options.ownerToken ?? randomUUID()
141
+ if (!identifier(ownerToken)) throw new TypeError('需要有效的执行所有权标记。')
142
+ const packages = normalizedEstimates(options.packageEstimates, run)
143
+ return state.update(current => {
144
+ const checks = currentBudget(current, now(), packages, options)
145
+ if (current.historyIncomplete && checks.budget.apiExposure) return { accepted: false, reason: 'history-incomplete', ...checks }
146
+ current.budgetReservations ??= {}
147
+ if (current.budgetReservations[run.id]) return { accepted: false, reason: 'active-run', ...checks }
148
+ if (Object.keys(current.budgetReservations).length >= maxReservations) return { accepted: false, reason: 'reservation-capacity', ...checks }
149
+ if (checks.budget.exceeded && options.confirmOverBudget !== true) return { accepted: false, reason: 'budget', ...checks }
150
+ if (checks.unknownBudget && options.confirmOverBudget !== true) return { accepted: false, reason: 'budget-unknown', ...checks }
151
+ const at = now()
152
+ const entry = { id: run.id, runId: run.id, ownerToken, ownerPid: pid, status: 'active',
153
+ createdAt: at, updatedAt: at, packages, completedIds: [] }
154
+ current.budgetReservations[run.id] = entry
155
+ upsertRun(current, run)
156
+ return { accepted: true, reservationId: entry.id, ownerToken, run, ...checks }
157
+ })
158
+ }
159
+ const checkpointExecution = ({ reservationId, ownerToken, run: nextRun, completedIds = [], startedIds = [],
160
+ checkBudget = false, budgetRun: affordabilityRun, ...options }) => {
161
+ const run = checkedRun(nextRun, reservationId)
162
+ const budgetRun = affordabilityRun === undefined ? run : checkedRun(affordabilityRun, reservationId)
163
+ if (![completedIds, startedIds].every(ids => Array.isArray(ids) && ids.every(identifier))) throw new TypeError('检查点工作包 ID 无效。')
164
+ return state.update(current => {
165
+ const entry = ownedReservation(current, reservationId, ownerToken)
166
+ const completed = new Set([...entry.completedIds, ...completedIds])
167
+ const started = new Set(startedIds)
168
+ const known = new Set([...entry.completedIds, ...entry.packages.map(item => item.id)])
169
+ if ([...completed, ...started].some(id => !known.has(id))) throw new TypeError('检查点包含未预占的工作包。')
170
+ const remaining = entry.packages.filter(item => !completed.has(item.id)).map(item => ({ ...item, started: item.started || started.has(item.id) }))
171
+ if (checkBudget) {
172
+ // Receipts may exceed their original estimates, or another execution
173
+ // may have settled meanwhile. Recheck the full remaining occupancy at
174
+ // this dispatch boundary without double-counting our previous hold.
175
+ const prospective = { ...current, runs: current.runs.map(item => item.id === run.id ? budgetRun : item) }
176
+ const checks = currentBudget(prospective, now(), remaining, options, entry.id)
177
+ const starting = entry.packages.filter(item => started.has(item.id) && !item.started)
178
+ if (starting.length && starting.every(item => item.billing === 'subscription')) {
179
+ // A later API step retains its hold and will be checked at its own
180
+ // dispatch boundary; it cannot make this subscription consume money.
181
+ Object.assign(checks.budget, { apiExposure: false, exceeded: null, estimateUsd: 0, estimateKnown: true,
182
+ message: '当前订阅步骤单独统计用量,不占用 API 金额预算。' })
183
+ checks.unknownBudget = false
184
+ }
185
+ if (current.historyIncomplete && checks.budget.apiExposure) return { accepted: false, reason: 'history-incomplete', ...checks }
186
+ if (checks.budget.exceeded && options.confirmOverBudget !== true) return { accepted: false, reason: 'budget', ...checks }
187
+ if (checks.unknownBudget && options.confirmOverBudget !== true) return { accepted: false, reason: 'budget-unknown', ...checks }
188
+ }
189
+ entry.packages = remaining
190
+ entry.completedIds = [...completed]
191
+ entry.updatedAt = now()
192
+ upsertRun(current, run)
193
+ return { accepted: true, reservationId, run, ...reservationSpending(current) }
194
+ })
195
+ }
196
+ const resizeExecution = options => {
197
+ const packages = normalizedEstimates(options.packageEstimates)
198
+ return state.update(current => {
199
+ const entry = ownedReservation(current, options.reservationId, options.ownerToken)
200
+ const checks = currentBudget(current, now(), packages, options, entry.id)
201
+ if (current.historyIncomplete && checks.budget.apiExposure) return { accepted: false, reason: 'history-incomplete', ...checks }
202
+ if (checks.budget.exceeded && options.confirmOverBudget !== true) return { accepted: false, reason: 'budget', ...checks }
203
+ if (checks.unknownBudget && options.confirmOverBudget !== true) return { accepted: false, reason: 'budget-unknown', ...checks }
204
+ if (packages.some(item => entry.completedIds.includes(item.id))) throw new TypeError('已完成工作包不能重新加入剩余预占。')
205
+ // An already-started call may still incur charges; resizing cannot erase it.
206
+ for (const previous of entry.packages.filter(item => item.started)) {
207
+ const next = packages.find(item => item.id === previous.id)
208
+ if (!next) throw new TypeError('执行中的工作包必须先保存结果,才能释放其预占。')
209
+ if (previous.billing !== 'subscription' && next.billing === 'subscription') {
210
+ throw new TypeError('执行中的 API 调用不能通过改为订阅释放金额预占;请先保存收据。')
211
+ }
212
+ next.started = true
213
+ }
214
+ entry.packages = packages
215
+ entry.updatedAt = now()
216
+ return { accepted: true, reservationId: entry.id, ...checks }
217
+ })
218
+ }
219
+ const finishExecution = ({ reservationId, ownerToken, run: nextRun }) => {
220
+ const run = checkedRun(nextRun, reservationId)
221
+ return state.update(current => {
222
+ ownedReservation(current, reservationId, ownerToken)
223
+ upsertRun(current, run)
224
+ delete current.budgetReservations[reservationId]
225
+ return { reservationId, run, ...reservationSpending(current) }
226
+ })
227
+ }
228
+ const markInterrupted = (current, entry, run, at) => {
229
+ entry.status = 'interrupted'
230
+ entry.interruptedAt = at
231
+ entry.updatedAt = at
232
+ if (run) {
233
+ run.status = 'interrupted'
234
+ run.finishedAt = at
235
+ for (const pending of entry.packages.filter(item => item.started)) {
236
+ const item = [...run.packages, ...(run.reviews ?? [])].find(item => item.id === pending.id)
237
+ if (!item || (!item.inFlight && !['pending', 'running'].includes(item.status))
238
+ || amount(item.costUsd) || amount(item.knownCostUsd) || Array.isArray(item.qualityAttempts)) continue
239
+ // We know a request began, but never observed its receipt. Keep that
240
+ // uncertainty in the ledger as well as the durable estimate hold.
241
+ item.status = 'interrupted'
242
+ item.inFlight = false
243
+ item.ran = true
244
+ item.costUsd = null
245
+ item.costSource = 'interrupted-usage-unknown'
246
+ item.billing = pending.billing === 'subscription' || item.billing === 'subscription' ? 'subscription' : 'api'
247
+ item.finishedAt = at
248
+ }
249
+ run.interruption = { at, unreconciled: true, pendingPackageIds: entry.packages.map(item => item.id),
250
+ startedPackageIds: entry.packages.filter(item => item.started).map(item => item.id) }
251
+ } else current.historyIncomplete = true
252
+ }
253
+ const interruptExecution = ({ reservationId, ownerToken, run: nextRun }) => {
254
+ const run = checkedRun(nextRun, reservationId)
255
+ return state.update(current => {
256
+ const entry = ownedReservation(current, reservationId, ownerToken)
257
+ markInterrupted(current, entry, run, now())
258
+ upsertRun(current, run)
259
+ return { reservationId, run, ...reservationSpending(current) }
260
+ })
261
+ }
262
+ const recoverInterruptedExecutions = () => state.update(async current => {
263
+ const recovered = []
264
+ for (const entry of Object.values(current.budgetReservations ?? {})) {
265
+ if (entry.status !== 'active' || await isProcessAlive(entry.ownerPid) !== false) continue
266
+ const run = current.runs.find(item => item.id === entry.runId)
267
+ markInterrupted(current, entry, run, now())
268
+ recovered.push({ reservationId: entry.id, runId: entry.runId, ...entry })
269
+ }
270
+ return { recovered, ...reservationSpending(current) }
271
+ })
272
+ return { beginExecution, checkpointExecution, resizeExecution, finishExecution, interruptExecution, recoverInterruptedExecutions, snapshot }
273
+ }
@@ -0,0 +1,61 @@
1
+ /** API money and subscription quantities are separate dimensions. */
2
+ import { plannedPackages } from './run-ledger.mjs'
3
+ import { billingPlan } from './subscription-billing.mjs'
4
+ import { toolForProvider } from './official-tool-registry.mjs'
5
+ import { estimateAssignedPackage } from './router.mjs'
6
+
7
+ export function initialFunding(route, { billing = null, officialCli = false, toolId = null,
8
+ loginBillingFor = () => 'unknown', apiKeyFor = () => false, forceApi = false, executionChannel = null } = {}) {
9
+ const id = toolId ?? toolForProvider(route?.provider)?.id
10
+ if (forceApi) return { billing: 'api', route }
11
+ if (officialCli) return { billing: !apiKeyFor(id) && loginBillingFor(id) === 'subscription' ? 'subscription' : 'api', route }
12
+ if (!billing) return { billing: 'api', route }
13
+ const policy = billingPlan(route, billing.routes ?? [])
14
+ if (policy.mode === 'subscription-only') return { billing: 'subscription', route }
15
+ if (policy.mode === 'api-only' || billing.choice === 'api') return { billing: 'api', route: policy.apiRoute ?? route }
16
+ const sub = policy.subscription
17
+ if (!sub) return { billing: 'api', route }
18
+ if (billing.quota?.status?.(sub.key)) return { billing: 'api', route: policy.apiRoute ?? route }
19
+ if (sub.kind === 'plan-key') return { billing: 'subscription', route }
20
+ if (billing.choice === 'subscription') return { billing: 'subscription', route }
21
+ const login = billing.loginBillingFor?.(sub.toolId) ?? loginBillingFor(sub.toolId)
22
+ return { billing: route?.execution === 'api' || executionChannel === 'harness-llm'
23
+ || login !== 'subscription' ? 'api' : 'subscription', route }
24
+ }
25
+
26
+ export function attachPlanAccounting(plan, task, routes = [], context = {}) {
27
+ const entries = plannedPackages(plan, task).map((item, index) => {
28
+ const selected = item.manualAssignment?.kind === 'api' ? item.manualAssignment
29
+ : { provider: item.recommendedProvider, model: item.recommendedModel }
30
+ const route = routes.find(route => route.provider === selected.provider && route.model === selected.model) ?? selected
31
+ const funding = initialFunding(route, { ...context, executionChannel: item.executionChannel ?? plan.executionChannel,
32
+ ...(context.billingChoices?.[item.id] && context.billing ? { billing: { ...context.billing, choice: context.billingChoices[item.id] } } : {}),
33
+ ...(item.manualAssignment?.kind === 'api' ? { forceApi: true } : {}),
34
+ ...(item.manualAssignment?.kind === 'tool' ? { officialCli: true, toolId: item.manualAssignment.toolId } : {}) })
35
+ const resource = plan.costBreakdown?.[index] ?? estimateAssignedPackage(route, item, task, plan.complexity?.band ?? 'balanced')
36
+ const changed = funding.route?.provider !== route.provider || funding.route?.model !== route.model
37
+ const estimate = changed || plan.subscriptionPricedRoutes?.includes(`${route.provider}/${route.model}`)
38
+ ? estimateAssignedPackage(funding.route, item, task, plan.complexity?.band ?? 'balanced').estimatedCost
39
+ : item.estimatedCost ?? null
40
+ return { id: item.id, billing: funding.billing,
41
+ apiEstimatedCostUsd: funding.billing === 'subscription' ? 0 : estimate,
42
+ estimatedUsage: { inputTokens: resource.inputTokens ?? null, outputTokens: resource.outputTokens ?? null } }
43
+ })
44
+ const api = entries.filter(item => item.billing === 'api')
45
+ plan.accounting = { packages: entries, apiExposure: api.length > 0,
46
+ apiEstimatedCostUsd: api.some(item => !Number.isFinite(item.apiEstimatedCostUsd)) ? null
47
+ : api.reduce((sum, item) => sum + item.apiEstimatedCostUsd, 0) }
48
+ return plan.accounting
49
+ }
50
+
51
+ export const accountingEstimates = plan => plan.accounting.packages.map(item => ({ id: item.id,
52
+ billing: item.billing, estimatedCostUsd: item.apiEstimatedCostUsd }))
53
+
54
+ /** Zero marginal API expense for subscription routes; never rewrite real prices. */
55
+ export function planningFundingPrices(routes, context, channelForRoute = () => null) {
56
+ const subscriptionPricedRoutes = routes.filter(route => initialFunding(route, {
57
+ ...context, executionChannel: channelForRoute(route),
58
+ }).billing === 'subscription').map(route => `${route.provider}/${route.model}`)
59
+ return { subscriptionPricedRoutes, pricing: Object.fromEntries(subscriptionPricedRoutes.map(key => [key,
60
+ { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }])) }
61
+ }
@@ -0,0 +1,143 @@
1
+ /** Required durable execution state, independent of advisory UI observers. */
2
+ import { randomUUID } from 'node:crypto'
3
+ import { createBudgetReservations } from './budget-reservations.mjs'
4
+
5
+ const receiptTimestamp = value => typeof value === 'number' && Number.isFinite(value) && value >= 0
6
+ && Number.isFinite(new Date(value).getTime())
7
+
8
+ export async function beginExecutionLifecycle({ state, buildRun, packageEstimates, settings = {}, confirmOverBudget = false }) {
9
+ const ownerToken = randomUUID()
10
+ const manager = createBudgetReservations({ state })
11
+ await manager.recoverInterruptedExecutions()
12
+ const started = new Map(), results = new Map(), completed = new Set()
13
+ const estimates = new Map(packageEstimates.map(item => [item.id, { estimatedCostUsd: item.estimatedCostUsd ?? item.estimatedCost ?? null, billing: item.billing ?? 'api' }]))
14
+ let workspace = {}, poisoned = null, closed = false
15
+ const current = (status = 'running') => {
16
+ const receipts = [...started].map(([id, item]) => results.get(id) ?? {
17
+ ...item, id, ok: false, notStarted: true, answer: '', channel: item.channel ?? null,
18
+ })
19
+ for (const [id, result] of results) if (!started.has(id)) receipts.push(result)
20
+ const run = buildRun({ status, results: receipts, workspace })
21
+ if (status === 'running' || status === 'interrupted') {
22
+ run.status = status
23
+ if (status === 'running') run.finishedAt = null
24
+ for (const item of run.packages ?? []) {
25
+ if (!started.has(item.id) || completed.has(item.id)) continue
26
+ item.status = 'running'; item.inFlight = true
27
+ if (!results.has(item.id) || results.get(item.id)?.notStarted) item.billing = estimates.get(item.id)?.billing ?? 'api'
28
+ if (!results.has(item.id)) Object.assign(item, { ran: false, costUsd: null, costSource: 'pending-receipt' })
29
+ }
30
+ }
31
+ run.checkpoint = { at: Date.now(), completedIds: [...completed], startedIds: [...started.keys()] }
32
+ return run
33
+ }
34
+ const initial = current()
35
+ const accepted = await manager.beginExecution({ run: initial, packageEstimates, ownerToken, ...settings, confirmOverBudget })
36
+ if (!accepted.accepted) return accepted
37
+ const identity = { reservationId: accepted.reservationId, ownerToken }
38
+ const rejectBudget = changed => {
39
+ const error = new Error(changed.budget?.message || '费用或预占已变化,已暂停启动后续步骤,请重新核对预算。')
40
+ error.code = 'ROUTER_BUDGET_CHANGED'
41
+ error.budget = { ...changed.budget, paused: true, exceeded: changed.budget?.exceeded ?? 'unknown' }
42
+ error.runId = initial.id
43
+ error.reason = changed.reason
44
+ throw error
45
+ }
46
+ const required = async operation => {
47
+ if (poisoned) throw poisoned
48
+ try { return await operation() }
49
+ catch (cause) {
50
+ poisoned = new Error('执行检查点保存失败,已停止启动后续步骤;请核对历史记录与费用。', { cause })
51
+ poisoned.code = 'ROUTER_CHECKPOINT_FAILED'
52
+ throw poisoned
53
+ }
54
+ }
55
+ const save = run => required(() => manager.checkpointExecution({ ...identity, run,
56
+ completedIds: [...completed], startedIds: [...started.keys()] }))
57
+ const preserveReceiptTimes = run => {
58
+ for (const item of run.packages ?? []) {
59
+ const receipt = results.get(item.id)
60
+ if (receiptTimestamp(receipt?.finishedAt)) item.finishedAt = receipt.finishedAt
61
+ }
62
+ return run
63
+ }
64
+ return {
65
+ ...accepted,
66
+ id: initial.id,
67
+ current,
68
+ results: () => [...results.values()],
69
+ reservationFor: id => estimates.get(id),
70
+ activePackageId: () => [...started.keys()].findLast(id => !completed.has(id)),
71
+ async onCheckpointStart(item) {
72
+ const previous = started.get(item.id)
73
+ const alreadyStarted = started.has(item.id)
74
+ started.set(item.id, item)
75
+ const changed = await required(() => manager.checkpointExecution({ ...identity, run: current(),
76
+ completedIds: [...completed], startedIds: [...started.keys()], checkBudget: true, ...settings, confirmOverBudget }))
77
+ if (changed.accepted === false) {
78
+ if (alreadyStarted) started.set(item.id, previous)
79
+ else started.delete(item.id)
80
+ rejectBudget(changed)
81
+ }
82
+ },
83
+ async onCheckpointResult(result) {
84
+ // Mutate the executor's receipt so later aggregate/final builds use the
85
+ // same observed settlement time, even after another day or month starts.
86
+ if (!receiptTimestamp(result.finishedAt)) result.finishedAt = Date.now()
87
+ results.set(result.id, result); completed.add(result.id); await save(current())
88
+ },
89
+ async onWorkspace(value) { workspace = { ...workspace, ...value }; await save(current()) },
90
+ async partialResult(result, { checkBudget = false } = {}) {
91
+ // The next attempt's unknown receipt is durable dispatch evidence, not
92
+ // an already-observed unknown invoice. Authorize against the previous
93
+ // observed receipt plus its newly reserved estimate, then save both in
94
+ // the same locked transaction before starting that attempt.
95
+ const budgetRun = checkBudget ? current() : undefined
96
+ const hadResult = results.has(result.id), previous = results.get(result.id)
97
+ if (!receiptTimestamp(result.finishedAt)) result.finishedAt = Date.now()
98
+ results.set(result.id, result)
99
+ const changed = await required(() => manager.checkpointExecution({ ...identity, run: current(), budgetRun,
100
+ completedIds: [...completed], startedIds: [...started.keys()], checkBudget, ...settings, confirmOverBudget: false }))
101
+ if (changed.accepted === false) {
102
+ if (hadResult) results.set(result.id, previous)
103
+ else results.delete(result.id)
104
+ rejectBudget(changed)
105
+ }
106
+ },
107
+ async saveRun(run, { completedIds = [], startedIds = [] } = {}) {
108
+ const previousCompleted = new Set(completed), previousStarted = new Map(started)
109
+ for (const id of completedIds) completed.add(id)
110
+ for (const id of startedIds) if (!started.has(id)) started.set(id, { id })
111
+ const changed = await required(() => manager.checkpointExecution({ ...identity, run,
112
+ completedIds: [...completed], startedIds: [...started.keys()], checkBudget: startedIds.length > 0,
113
+ ...settings, confirmOverBudget: false }))
114
+ if (changed.accepted === false) {
115
+ completed.clear(); for (const id of previousCompleted) completed.add(id)
116
+ started.clear(); for (const [id, item] of previousStarted) started.set(id, item)
117
+ rejectBudget(changed)
118
+ }
119
+ },
120
+ async reserveExtra(id, estimatedCostUsd, { confirmOverBudget: extraConfirmation = false } = {}) {
121
+ return required(async () => {
122
+ const next = new Map(estimates); next.set(id, { estimatedCostUsd, billing: 'api' })
123
+ const changed = await manager.resizeExecution({ ...identity,
124
+ packageEstimates: [...next].filter(([key]) => !completed.has(key)).map(([key, value]) => ({ id: key, ...value })),
125
+ ...settings, confirmOverBudget: extraConfirmation === true })
126
+ if (changed.accepted) { estimates.clear(); for (const [key, value] of next) estimates.set(key, value) }
127
+ return changed
128
+ })
129
+ },
130
+ async finish(run) {
131
+ if (poisoned) throw poisoned
132
+ const value = await required(() => manager.finishExecution({ ...identity, run: preserveReceiptTimes(run) }))
133
+ closed = true
134
+ return value
135
+ },
136
+ async interrupt(run) {
137
+ if (closed) return
138
+ // Keep the reservation when storage itself remains unavailable. It is
139
+ // never safe to turn an unconfirmed paid receipt into free capacity.
140
+ try { await manager.interruptExecution({ ...identity, run: preserveReceiptTimes(run) }) } catch { /* original hold stays durable */ }
141
+ },
142
+ }
143
+ }
@@ -11,7 +11,7 @@ import { tmpdir } from 'node:os'
11
11
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
12
12
  import { runOfficialTool, officialToolExecutionCapabilities, officialToolReadiness } from './official-tool-executor.mjs'
13
13
  import { toolForProvider } from './official-tool-registry.mjs'
14
- import { observeExecutionProgress, redactDiagnostic, verifiedDiagnostic } from './task-executors.mjs'
14
+ import { awaitExecutionCheckpoint, observeExecutionProgress, redactDiagnostic, verifiedDiagnostic } from './task-executors.mjs'
15
15
  import { unsupportedCliReasoning } from './reasoning-execution.mjs'
16
16
 
17
17
  const MAX_GIT_OUTPUT = 64_000_000
@@ -313,8 +313,10 @@ function packagePrompt(task, item, completed) {
313
313
  }
314
314
 
315
315
  /** Preflight every assignment before any model request or workspace write. */
316
- export function executableTeamPackages(plan, installedIds = [], mode = 'read-only', executionEnvironment = 'harness') {
317
- const capabilities = new Map(officialToolExecutionCapabilities({ executionEnvironment }).map(item => [item.id, item]))
316
+ export function executableTeamPackages(plan, installedIds = [], mode = 'read-only', executionEnvironment = 'harness',
317
+ executionCapabilities = officialToolExecutionCapabilities({ executionEnvironment })) {
318
+ if (!Array.isArray(executionCapabilities)) throw new TypeError('runtime capabilities must be an array')
319
+ const capabilities = new Map(executionCapabilities.map(item => [item.id, item]))
318
320
  const packages = Array.isArray(plan?.team?.workPackages) ? plan.team.workPackages : []
319
321
  const blocking = []
320
322
  const assigned = packages.map(item => {
@@ -415,7 +417,7 @@ async function seedWorktree(isolated, seedFrom, signal) {
415
417
  }
416
418
 
417
419
  /** Sequential execution preserves DAG dependencies and avoids edit conflicts. */
418
- export async function runOfficialTeam({ plan, task, workspace, allowedRoot, mode = 'read-only', executionEnvironment = 'harness', installedIds = [], cliModels, signal, sandbox, runtime, previous = [], onlyIds = null, seedFrom = null, onPackageStart, onPackage, onStage }) {
420
+ export async function runOfficialTeam({ plan, task, workspace, allowedRoot, mode = 'read-only', executionEnvironment = 'harness', installedIds = [], cliModels, signal, sandbox, runtime, previous = [], onlyIds = null, seedFrom = null, onPackageStart, onPackage, onStage, onCheckpointStart, onCheckpointResult, onWorkspace }) {
419
421
  await observeExecutionProgress(onStage, 'preparing')
420
422
  if (!['harness', 'cli-native'].includes(executionEnvironment)
421
423
  || (executionEnvironment === 'cli-native' && mode !== 'workspace-write')) throw new TypeError('invalid team CLI execution environment')
@@ -427,7 +429,10 @@ export async function runOfficialTeam({ plan, task, workspace, allowedRoot, mode
427
429
  if (retry && mode !== 'read-only' && !(seedFrom && typeof seedFrom.workspace === 'string' && typeof seedFrom.baseCommit === 'string')) {
428
430
  throw new TypeError('an editable team retry needs the earlier isolated worktree (seedFrom)')
429
431
  }
430
- const selected = executableTeamPackages(plan, installedIds, mode, executionEnvironment)
432
+ // An injected adapter can declare its own capabilities as well as readiness
433
+ // and execution. The Host supplies no runtime override and always checks the
434
+ // actual platform registry before launching any official CLI.
435
+ const selected = executableTeamPackages(plan, installedIds, mode, executionEnvironment, runtime?.capabilities)
431
436
  const assigned = retry ? selected.assigned.filter(item => onlyIds.includes(item.id)) : selected.assigned
432
437
  const blocking = retry ? selected.blocking.filter(item => onlyIds.includes(item.id)) : selected.blocking
433
438
  if (assigned.length === 0) return { status: 'blocked', blocking: [{ reason: '计划没有工作包。' }], results: [] }
@@ -448,6 +453,11 @@ export async function runOfficialTeam({ plan, task, workspace, allowedRoot, mode
448
453
  if (signal?.aborted) return { status: 'cancelled', workspace: source, results: [] }
449
454
  const isolated = mode === 'workspace-write' ? await isolatedWorktree(source, signal, allowedRoot) : null
450
455
  const runWorkspace = isolated?.workspace ?? source
456
+ const completed = []
457
+ const partial = () => ({ status: signal?.aborted ? 'cancelled' : 'incomplete', workspace: runWorkspace,
458
+ results: [...completed], ...(isolated ? { baseCommit: isolated.baseCommit } : {}) })
459
+ await awaitExecutionCheckpoint(onWorkspace, { workspace: runWorkspace, sourceWorkspace: source,
460
+ isolatedWorkspace: isolated?.workspace ?? null, baseCommit: isolated?.baseCommit ?? null }, partial, 'workspace')
451
461
  let seeded = null
452
462
  if (isolated && retry) {
453
463
  try { seeded = await seedWorktree(isolated, seedFrom, signal) }
@@ -456,7 +466,6 @@ export async function runOfficialTeam({ plan, task, workspace, allowedRoot, mode
456
466
  return { status: 'blocked', blocking: [{ reason: String(error?.message ?? error) }], results: [] }
457
467
  }
458
468
  }
459
- const completed = []
460
469
  const cancelledRun = () => ({ status: 'cancelled', workspace: runWorkspace, results: completed,
461
470
  ...(isolated ? { baseCommit: isolated.baseCommit } : {}), ...(seeded ? { seeded } : {}) })
462
471
  const context = retry ? (Array.isArray(previous) ? previous : []).filter(item => item?.id && typeof item.finalText === 'string') : []
@@ -466,8 +475,11 @@ export async function runOfficialTeam({ plan, task, workspace, allowedRoot, mode
466
475
  const cliModel = teamCliModel(item, configuredCliModels)
467
476
  const prompt = packagePrompt(task, item, [...context, ...completed])
468
477
  const startedAt = Date.now()
469
- await observeExecutionProgress(onPackageStart, { ...item, provider: item.recommendedProvider,
470
- model: item.recommendedModel, requestedCliModel: cliModel, startedAt })
478
+ const starting = { ...item, provider: item.recommendedProvider,
479
+ model: item.recommendedModel, requestedCliModel: cliModel, startedAt }
480
+ await observeExecutionProgress(onPackageStart, starting)
481
+ if (signal?.aborted) return cancelledRun()
482
+ await awaitExecutionCheckpoint(onCheckpointStart, starting, partial, 'start')
471
483
  if (signal?.aborted) return cancelledRun()
472
484
  let result
473
485
  try {
@@ -503,6 +515,11 @@ export async function runOfficialTeam({ plan, task, workspace, allowedRoot, mode
503
515
  actualModel: reportedModel,
504
516
  ...(result.reportedModel !== undefined ? { reportedModel: result.reportedModel } : {}),
505
517
  ...(result.usageIncomplete === true ? { usageIncomplete: true } : {}),
518
+ ...(result.notStarted === true || result.status === 'unsupported' ? { notStarted: true } : {}),
519
+ ...(result.blocked === true ? { blocked: true } : {}),
520
+ ...(['api', 'subscription'].includes(result.billing) ? { billing: result.billing } : {}),
521
+ ...(['configured-api-key', 'process-environment', 'cli-session'].includes(result.credentialSource)
522
+ ? { credentialSource: result.credentialSource } : {}),
506
523
  ...(typeof result.source === 'string' ? { source: result.source } : {}),
507
524
  ...(typeof result.sandboxEnforcement === 'string' ? { sandboxEnforcement: result.sandboxEnforcement } : {}),
508
525
  executionEnvironment,
@@ -518,6 +535,7 @@ export async function runOfficialTeam({ plan, task, workspace, allowedRoot, mode
518
535
  error: modelMismatch ? '官方 CLI 实际模型与指定模型不一致。'
519
536
  : (result.error || result.reason) ? executionDiagnostic(result.error || result.reason) : null,
520
537
  outputTail: result.status === 'succeeded' ? null : executionDiagnostic(verifiedDiagnostic(result)) || null })
538
+ await awaitExecutionCheckpoint(onCheckpointResult, completed.at(-1), partial, 'result')
521
539
  await observeExecutionProgress(onPackage, completed.at(-1))
522
540
  if (signal?.aborted) return cancelledRun()
523
541
  if (packageStatus !== 'succeeded') return { status: packageStatus === 'cancelled' ? 'cancelled' : 'incomplete', workspace: runWorkspace, results: completed,
@@ -544,7 +562,7 @@ export async function runOfficialTeam({ plan, task, workspace, allowedRoot, mode
544
562
  }
545
563
 
546
564
  /** One editable CLI task also uses the same isolated workspace and merge gate. */
547
- export async function runOfficialTask({ toolId, task, workspace, allowedRoot, modelId, mode = 'read-only', signal, sandbox, runtime }) {
565
+ export async function runOfficialTask({ toolId, task, workspace, allowedRoot, modelId, mode = 'read-only', signal, sandbox, runtime, onCheckpointStart, onCheckpointResult, onWorkspace }) {
548
566
  const source = await approvedSource(workspace, allowedRoot)
549
567
  if (signal?.aborted) return { toolId, status: 'cancelled', notStarted: true, finalText: '' }
550
568
  if (typeof sandbox?.confine !== 'function') return { toolId, status: 'unsupported', reason: '官方 Harness 进程沙箱不可用。' }
@@ -555,18 +573,29 @@ export async function runOfficialTask({ toolId, task, workspace, allowedRoot, mo
555
573
  reason: readiness.reason ?? readiness.modeReasons?.[mode] ?? `官方 CLI 尚未就绪,或不支持 ${mode} 模式。` }
556
574
  if (signal?.aborted) return { toolId, status: 'cancelled', notStarted: true, finalText: '' }
557
575
  const isolated = mode === 'workspace-write' ? await isolatedWorktree(source, signal, allowedRoot) : null
576
+ const runWorkspace = isolated?.workspace ?? source
577
+ const location = isolated ? { isolatedWorkspace: isolated.workspace, baseCommit: isolated.baseCommit } : {}
558
578
  let result
579
+ const partial = () => ({ id: 'direct', toolId, status: signal?.aborted ? 'cancelled' : 'failed',
580
+ notStarted: !result, finalText: '', ...result, ...location })
581
+ await awaitExecutionCheckpoint(onWorkspace, { workspace: runWorkspace, sourceWorkspace: source,
582
+ isolatedWorkspace: isolated?.workspace ?? null, baseCommit: isolated?.baseCommit ?? null }, partial, 'workspace')
583
+ if (signal?.aborted) return { ...partial(), status: 'cancelled', notStarted: true }
584
+ await awaitExecutionCheckpoint(onCheckpointStart, { id: 'direct', toolId, model: modelId ?? null,
585
+ workspace: runWorkspace, startedAt: Date.now() }, partial, 'start')
586
+ if (signal?.aborted) return { ...partial(), status: 'cancelled', notStarted: true }
559
587
  try {
560
- result = await (runtime?.runTool ?? runOfficialTool)({ toolId, task, modelId, workspace: isolated?.workspace ?? source,
588
+ result = await (runtime?.runTool ?? runOfficialTool)({ toolId, task, modelId, workspace: runWorkspace,
561
589
  mode, isolatedRoot: isolated?.isolatedRoot, signal, sandbox })
562
590
  if (!result || typeof result.status !== 'string') throw new TypeError('官方 CLI 执行器没有返回有效状态。')
563
591
  } catch (error) {
564
592
  result = executionException(error, signal)
565
593
  }
566
594
  result = withFailureDiagnostic(result)
567
- if (signal?.aborted) return { ...result, status: 'cancelled',
568
- ...(isolated ? { isolatedWorkspace: isolated.workspace, baseCommit: isolated.baseCommit } : {}) }
569
- if (!isolated || result.status !== 'succeeded') return { ...result, ...(isolated ? { isolatedWorkspace: isolated.workspace, baseCommit: isolated.baseCommit } : {}) }
595
+ result = { ...result, ...location, ...(signal?.aborted ? { status: 'cancelled' } : {}) }
596
+ await awaitExecutionCheckpoint(onCheckpointResult, { ...result, id: 'direct' }, partial, 'result')
597
+ if (signal?.aborted) return { ...result, status: 'cancelled' }
598
+ if (!isolated || result.status !== 'succeeded') return result
570
599
  try {
571
600
  const integration = await integrateWorktree(source, isolated.workspace, isolated.baseCommit, signal)
572
601
  return { ...result,
@@ -14,7 +14,7 @@
14
14
  import { spawn } from 'node:child_process'
15
15
  import { mkdir, mkdtemp, readFile, realpath, rename, rm, stat, writeFile } from 'node:fs/promises'
16
16
  import { homedir, tmpdir } from 'node:os'
17
- import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
17
+ import { dirname, isAbsolute, join, posix, relative, resolve, sep, win32 } from 'node:path'
18
18
  import {
19
19
  DEFAULT_NPM_REGISTRY,
20
20
  defaultInstallMethod,
@@ -1042,7 +1042,8 @@ let prefixPromise = null
1042
1042
  /** Add one directory to this process's PATH unless it is already there. */
1043
1043
  export function appendToProcessPath(directory, { prepend = false, env = process.env, platform = process.platform } = {}) {
1044
1044
  const target = String(directory ?? '').trim()
1045
- if (!target || !isAbsolute(target)) return
1045
+ const paths = platform === 'win32' ? win32 : posix
1046
+ if (!target || !paths.isAbsolute(target)) return
1046
1047
  const separator = platform === 'win32' ? ';' : ':'
1047
1048
  const key = Object.keys(env).find(name => name.toUpperCase() === 'PATH') ?? 'PATH'
1048
1049
  const parts = String(env[key] ?? '').split(separator).filter(Boolean)