@coreplane/switchboard 1.250.0 → 1.252.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.
Files changed (79) hide show
  1. package/dist/assets/config/config.example.yaml +13 -2
  2. package/dist/assets/deploy/cloudflare/preflight.mjs +19 -21
  3. package/dist/assets/deploy/cloudflare/worker.ts +6 -3
  4. package/dist/assets/deploy/cloudflare-memory/worker.ts +108 -12
  5. package/dist/assets/deploy/cloudflare-resident/memoryGuard.ts +212 -0
  6. package/dist/assets/deploy/cloudflare-resident/refresh.ts +1 -1
  7. package/dist/assets/deploy/cloudflare-resident/worker.ts +317 -56
  8. package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +4 -2
  9. package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.d.mts +31 -0
  10. package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.mjs +119 -0
  11. package/dist/assets/package-lock.json +3 -3
  12. package/dist/assets/package.json +3 -2
  13. package/dist/assets/project.json +13 -9
  14. package/dist/assets/source.json +3 -3
  15. package/dist/assets/src/agents/registry.ts +5 -5
  16. package/dist/assets/src/core/authz/policy.ts +4 -0
  17. package/dist/assets/src/core/authz/resource.ts +6 -2
  18. package/dist/assets/src/core/authz/types.ts +2 -0
  19. package/dist/assets/src/core/budgets.ts +22 -0
  20. package/dist/assets/src/core/coordinator/contract.ts +42 -0
  21. package/dist/assets/src/core/coordinator/driver.ts +150 -12
  22. package/dist/assets/src/core/drain.ts +50 -0
  23. package/dist/assets/src/core/memory/engine.ts +98 -0
  24. package/dist/assets/src/core/memory/scorer.ts +12 -4
  25. package/dist/assets/src/core/memory/types.ts +69 -12
  26. package/dist/assets/src/core/modelCard.ts +51 -7
  27. package/dist/assets/src/core/modelPricing.ts +111 -1
  28. package/dist/assets/src/core/modelProxy/usage.ts +88 -0
  29. package/dist/assets/src/core/modelRegistry.ts +15 -1
  30. package/dist/assets/src/core/provider.ts +49 -0
  31. package/dist/assets/src/core/refusal.ts +6 -6
  32. package/dist/assets/src/core/reviewVerdict.ts +4 -0
  33. package/dist/assets/src/core/runEvents.ts +51 -2
  34. package/dist/assets/src/core/runFriction.ts +7 -2
  35. package/dist/assets/src/core/runLedger/types.ts +23 -4
  36. package/dist/assets/src/core/runRecord.ts +16 -0
  37. package/dist/assets/src/core/runUsage.ts +67 -13
  38. package/dist/assets/src/core/schedules.ts +3 -0
  39. package/dist/assets/src/core/ship/contract.ts +45 -5
  40. package/dist/assets/src/core/ship/coordinator.ts +441 -48
  41. package/dist/assets/src/core/ship/renewal.ts +10 -5
  42. package/dist/assets/src/core/trace/attrs.ts +24 -0
  43. package/dist/assets/src/core/types.ts +5 -5
  44. package/dist/assets/src/core/verbosity.ts +48 -0
  45. package/dist/assets/src/deploy/liveGate.ts +40 -13
  46. package/dist/assets/src/deploy/restart.ts +11 -12
  47. package/dist/assets/src/execution/residentDepCache.ts +50 -1
  48. package/dist/assets/src/execution/residentDepsStore.ts +40 -2
  49. package/dist/assets/src/execution/residentRefresh.ts +55 -3
  50. package/dist/assets/src/execution/residentSteps.ts +4 -0
  51. package/dist/assets/src/execution/sandboxErrors.ts +8 -0
  52. package/dist/assets/web/dist/.vite/manifest.json +58 -52
  53. package/dist/assets/web/dist/assets/DeliveryPage-3ELQWM0r.js +1 -0
  54. package/dist/assets/web/dist/assets/{HomePage-AnycA57D.js → HomePage-BG_ok-K2.js} +2 -2
  55. package/dist/assets/web/dist/assets/{PendingTurnRow-BuRre8it.js → PendingTurnRow-ChCQOLgZ.js} +1 -1
  56. package/dist/assets/web/dist/assets/{ResidentDetailPage-Cb3sFkfj.js → ResidentDetailPage-C9y3nbo8.js} +1 -1
  57. package/dist/assets/web/dist/assets/{ResidentsIndexPage-BZ6n6UxF.js → ResidentsIndexPage-i1RG9e7g.js} +1 -1
  58. package/dist/assets/web/dist/assets/RunFoldRow-D3wVpzBa.js +1 -0
  59. package/dist/assets/web/dist/assets/RunRoutePage-B3IirUVi.js +9 -0
  60. package/dist/assets/web/dist/assets/RunsIndexPage-DiFmtGaJ.js +1 -0
  61. package/dist/assets/web/dist/assets/{ScheduledPage-CBUxbeqN.js → ScheduledPage-DvYwM2TE.js} +1 -1
  62. package/dist/assets/web/dist/assets/{SettingsPage-CBTnZ9Qv.js → SettingsPage-Bo6yCyXZ.js} +1 -1
  63. package/dist/assets/web/dist/assets/{StatusDot-BnRjWzFN.js → StatusDot-CAfS1AUi.js} +1 -1
  64. package/dist/assets/web/dist/assets/{Tooltip-Brge0wnd.js → Tooltip-tZoum_T-.js} +1 -1
  65. package/dist/assets/web/dist/assets/{UnitRoutePage-o6sLju16.js → UnitRoutePage-BmdOHwNn.js} +1 -1
  66. package/dist/assets/web/dist/assets/budgets-CbIyPAER.js +1 -0
  67. package/dist/assets/web/dist/assets/{dist-rgAhsmE-.js → dist-DfbEpHXR.js} +1 -1
  68. package/dist/assets/web/dist/assets/indexRow-BT0cPVRw.js +1 -0
  69. package/dist/assets/web/dist/assets/{main-CeRuGONy.js → main-5Gm_1Gv8.js} +2 -2
  70. package/dist/assets/web/dist/assets/sseReplay-DmyMXfRC.js +11 -0
  71. package/dist/cli.js +3319 -1015
  72. package/package.json +1 -1
  73. package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.sh +0 -68
  74. package/dist/assets/web/dist/assets/DeliveryPage-CIfBiINK.js +0 -1
  75. package/dist/assets/web/dist/assets/RunFoldRow-BRXkjkgO.js +0 -1
  76. package/dist/assets/web/dist/assets/RunRoutePage-psSMI3fN.js +0 -9
  77. package/dist/assets/web/dist/assets/RunsIndexPage-68YT_RWt.js +0 -1
  78. package/dist/assets/web/dist/assets/indexRow-BmK74Vp1.js +0 -1
  79. package/dist/assets/web/dist/assets/sseReplay-DXC7kGbN.js +0 -9
@@ -10,7 +10,7 @@
10
10
  // catalog lives in ./modelRegistry.ts.
11
11
 
12
12
  import { EFFORT_LEVELS, type Effort } from "../effort.js";
13
- import { vendorOf, wireOf, type ProviderConfig, type Wire } from "./provider.js";
13
+ import { billerHarnessProvider, vendorOf, wireOf, type ProviderConfig, type Wire } from "./provider.js";
14
14
  import type { RegistryCard } from "./modelRegistry.js";
15
15
 
16
16
  /** Which layer named a field: the operator's block, the registry card, or the
@@ -32,11 +32,23 @@ export type LevelMap = Record<Effort, LevelWord | "refused"> | "unknown";
32
32
  export type InputSupport = boolean | "unknown";
33
33
  export type CacheRule = "automatic" | "markers" | "none" | "unknown";
34
34
 
35
+ /** One long-context tier of a card's rate (pi's rule, `calculateCost`): when
36
+ * the request's input side (input + cache reads + cache writes) exceeds
37
+ * `inputTokensAbove`, the WHOLE request re-rates at the tier. */
38
+ export interface CardPriceTier {
39
+ inputTokensAbove: number;
40
+ input: number;
41
+ output: number;
42
+ cacheRead: number;
43
+ cacheWrite: number;
44
+ }
45
+
35
46
  export interface CardPrice {
36
47
  input: number;
37
48
  output: number;
38
49
  cacheRead: number;
39
50
  cacheWrite: number;
51
+ tiers?: readonly CardPriceTier[];
40
52
  }
41
53
 
42
54
  export interface ModelCard {
@@ -123,14 +135,30 @@ function levelMapOf(map: Record<string, string | null> | undefined, reasoning: b
123
135
  return out;
124
136
  }
125
137
 
126
- function priceOf(
127
- raw: { input?: number; output?: number; cacheRead?: number; cacheWrite?: number } | undefined,
128
- ): CardPrice | undefined {
138
+ type RawPrice = {
139
+ input?: number;
140
+ output?: number;
141
+ cacheRead?: number;
142
+ cacheWrite?: number;
143
+ tiers?: Array<{ inputTokensAbove?: number } & Omit<RawPrice, "tiers">>;
144
+ };
145
+
146
+ function priceOf(raw: RawPrice | undefined): CardPrice | undefined {
129
147
  if (!raw) return undefined;
130
148
  const { input, output, cacheRead, cacheWrite } = raw;
131
149
  if (input === undefined || output === undefined || cacheRead === undefined || cacheWrite === undefined)
132
150
  return undefined;
133
- return { input, output, cacheRead, cacheWrite };
151
+ // A tier missing a field cannot re-rate the whole request; it is dropped,
152
+ // never guessed at the base rate.
153
+ const tiers = (raw.tiers ?? []).filter(
154
+ (t): t is CardPriceTier =>
155
+ t.inputTokensAbove !== undefined &&
156
+ t.input !== undefined &&
157
+ t.output !== undefined &&
158
+ t.cacheRead !== undefined &&
159
+ t.cacheWrite !== undefined,
160
+ );
161
+ return { input, output, cacheRead, cacheWrite, ...(tiers.length > 0 ? { tiers } : {}) };
134
162
  }
135
163
 
136
164
  /**
@@ -335,13 +363,29 @@ export function decideControls(card: ModelCard, asked: AskedControls): ControlDe
335
363
  why: windowVouched ? "" : `no layer names the window; compacting at ${card.window}`,
336
364
  });
337
365
 
338
- const cacheNative = card.cache !== "unknown";
366
+ // A `markers` rule needs a harness-side write that places the markers
367
+ // (record 0052's amendment: the harness write names the biller's own
368
+ // provider). The Anthropic wire's own packages place per-block breakpoints;
369
+ // on the chat wire a biller the table names caches the aggregator's own way
370
+ // — the pinned OpenCode binary exempts the openrouter route from per-block
371
+ // placement, so the write carries OpenRouter's top-level
372
+ // `cache_control: { type: "ephemeral" }` via `settings.extraBody` (measured
373
+ // in `opencode/testing/realDriver.test.ts`), and pi's compat sends the
374
+ // per-block markers. A biller served generically degrades, never a silent
375
+ // `native`.
376
+ const markersUnplaced =
377
+ card.cache === "markers" && card.wire === "openai-chat" && billerHarnessProvider(card.block) === undefined;
378
+ const cacheNative = card.cache !== "unknown" && !markersUnplaced;
339
379
  decisions.push({
340
380
  control: "cache",
341
381
  outcome: cacheNative ? "native" : "degraded",
342
382
  applied: card.cache,
343
383
  vouched: cacheNative,
344
- why: cacheNative ? "" : `no layer names ${card.model}'s cache rule`,
384
+ why: cacheNative
385
+ ? ""
386
+ : markersUnplaced
387
+ ? `no harness-side provider vouches for the "${card.block}" biller's cache markers; the rule goes out unvouched`
388
+ : `no layer names ${card.model}'s cache rule`,
345
389
  });
346
390
 
347
391
  return decisions;
@@ -1,4 +1,6 @@
1
- import { parseModelRef } from "./provider.js";
1
+ import { parseModelRef, type TokenUsage } from "./provider.js";
2
+ import type { CardPrice } from "./modelCard.js";
3
+ import type { ReportedCost } from "./modelProxy/usage.js";
2
4
  import type { ModelUsage, RunUsage } from "./runUsage.js";
3
5
 
4
6
  // The price of a model's tokens (docs/reference/specs/costs.md): one table
@@ -148,6 +150,105 @@ export function modelPriceOf(ref: string, prices: ModelPriceTable = NO_PRICES):
148
150
  return { input: list.input, output: list.output, cacheRead: list.cacheRead, cacheWrite: list.cacheWrite5m };
149
151
  }
150
152
 
153
+ // ---- one turn, priced at the proxy (model-proxy.md item 6) -----------------------
154
+
155
+ /** Which layer priced the turn: the provider's own reported cost, the
156
+ * operator's table (`costs.prices` or a block's `models.<id>.price`), the
157
+ * registry card (the Anthropic family list folds into this layer), or none —
158
+ * an unpriced turn is never $0. */
159
+ export type PriceSource = "provider" | "operator" | "registry" | "none";
160
+
161
+ /** The meter row's dollars: `usd` when a layer priced the turn, `feeUsd` on a
162
+ * BYOK turn (the aggregator's fee — inside `usd` when the vendor's charge was
163
+ * reported beside it, otherwise beside the layer that priced the tokens),
164
+ * and the layer. */
165
+ export interface TurnPrice {
166
+ usd?: number;
167
+ feeUsd?: number;
168
+ priceSource: PriceSource;
169
+ }
170
+
171
+ /** What `priceTurn` reads of the run's model card: the ref the spans name,
172
+ * the card's rate when a layer named one, and which layer did
173
+ * (`ModelCard.provenance.price`). */
174
+ export interface TurnPriceCard {
175
+ ref: string;
176
+ price?: CardPrice;
177
+ pricedBy?: "operator" | "registry" | "wire";
178
+ }
179
+
180
+ const turnTokensUsd = (u: TokenUsage, p: { input: number; output: number; cacheRead: number; cacheWrite: number }) =>
181
+ (u.inputTokens * p.input +
182
+ u.outputTokens * p.output +
183
+ (u.cacheReadTokens ?? 0) * p.cacheRead +
184
+ (u.cacheWriteTokens ?? 0) * p.cacheWrite) /
185
+ 1_000_000;
186
+
187
+ /** A card rate over one turn's counts, tiers by pi's rule (`calculateCost`):
188
+ * the input side is input + cache reads + cache writes, and the WHOLE request
189
+ * re-rates at the highest tier whose threshold it exceeds. */
190
+ export function cardPriceUsd(usage: TokenUsage, price: CardPrice): number {
191
+ const inputSide = usage.inputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0);
192
+ let rates: { input: number; output: number; cacheRead: number; cacheWrite: number } = price;
193
+ let matched = -1;
194
+ for (const tier of price.tiers ?? []) {
195
+ if (inputSide > tier.inputTokensAbove && tier.inputTokensAbove > matched) {
196
+ rates = tier;
197
+ matched = tier.inputTokensAbove;
198
+ }
199
+ }
200
+ return turnTokensUsd(usage, rates);
201
+ }
202
+
203
+ /**
204
+ * One turn's price, by record 0052's precedence: a provider-reported cost is
205
+ * stored as reported (`provider`; on BYOK the fee and the vendor's upstream
206
+ * charge sum into `usd` with `feeUsd` beside), else the operator's layer
207
+ * (`costs.prices` for the exact ref, or the card's operator-named rate), else
208
+ * the registry layer (the card's rate with its tiers, or the Anthropic family
209
+ * list — costs.md item 4b's fallback, folded here), else `none` — a turn no
210
+ * layer prices, and a stream broken before its usage arrived, stays unpriced.
211
+ */
212
+ export function priceTurn(
213
+ card: TurnPriceCard,
214
+ reported: ReportedCost | undefined,
215
+ usage: TokenUsage | undefined,
216
+ prices: ModelPriceTable = NO_PRICES,
217
+ ): TurnPrice {
218
+ if (reported) {
219
+ if (!reported.byok) return { usd: reported.cost, priceSource: "provider" };
220
+ if (reported.upstreamCost !== undefined) {
221
+ return { usd: reported.cost + reported.upstreamCost, priceSource: "provider", feeUsd: reported.cost };
222
+ }
223
+ // A BYOK chunk without the vendor's charge: the fee is not the turn's
224
+ // dollars, so it rides beside whatever layer below prices the tokens —
225
+ // never as an authoritative figure that understates the turn.
226
+ return { ...priceTurn(card, undefined, usage, prices), feeUsd: reported.cost };
227
+ }
228
+ if (!usage) return { priceSource: "none" };
229
+ const operator = prices[card.ref];
230
+ if (operator) return { usd: turnTokensUsd(usage, operator), priceSource: "operator" };
231
+ if (card.price) {
232
+ return {
233
+ usd: cardPriceUsd(usage, card.price),
234
+ priceSource: card.pricedBy === "operator" ? "operator" : "registry",
235
+ };
236
+ }
237
+ const list = anthropicPriceOf(modelIdOf(card.ref));
238
+ if (list) {
239
+ return {
240
+ usd: turnTokensUsd(usage, {
241
+ input: list.input,
242
+ output: list.output,
243
+ cacheRead: list.cacheRead,
244
+ cacheWrite: list.cacheWrite5m,
245
+ }),
246
+ priceSource: "registry",
247
+ };
248
+ }
249
+ return { priceSource: "none" };
250
+ }
251
+
151
252
  /** What a model's counted tokens cost at a price, USD. */
152
253
  export function modelUsageUsd(m: ModelUsage, p: ModelPrice): number {
153
254
  return (
@@ -211,6 +312,15 @@ export function llmUsdOfUsage(
211
312
  let unpricedTokens = 0;
212
313
  const byModel: Record<string, PricedModelUsage> = {};
213
314
  for (const [ref, m] of Object.entries(usage.byModel)) {
315
+ // A model whose spans priced their own turns (model-proxy item 6) keeps
316
+ // the recorded figure — a provider-reported cost beats any table — and
317
+ // null (a turn without a figure) reads as unpriced, never re-priced here.
318
+ if (m.usd !== undefined) {
319
+ if (m.usd === null) unpricedTokens += m.inputTokens + m.outputTokens + m.cacheReadTokens + m.cacheWriteTokens;
320
+ else usd += m.usd;
321
+ byModel[ref] = { ...m, usd: m.usd };
322
+ continue;
323
+ }
214
324
  const price = modelPriceOf(ref, prices);
215
325
  const priced = price ? modelUsageUsd(m, price) : undefined;
216
326
  if (priced === undefined) unpricedTokens += m.inputTokens + m.outputTokens + m.cacheReadTokens + m.cacheWriteTokens;
@@ -0,0 +1,88 @@
1
+ // A provider's usage object as the proxy's meter reads it
2
+ // (docs/reference/specs/model-proxy.md item 6): the three wire shapes the proxy
3
+ // forwards — Anthropic's `message.usage`, the Chat Completions `usage` and the
4
+ // Responses API's `usage` — each
5
+ // normalized to the one `TokenUsage` every `model.turn` span carries. Undefined
6
+ // unless both core counts are numbers: a malformed or absent usage never fails
7
+ // a completion, it only leaves the turn unmetered. The first two readers were the
8
+ // native adapters' until record 0032's series deleted the adapters; the proxy
9
+ // is their one reader now.
10
+
11
+ import type { TokenUsage } from "../provider.js";
12
+
13
+ /** Anthropic `message.usage` → TokenUsage. */
14
+ export function usageFromAnthropic(u: unknown): TokenUsage | undefined {
15
+ if (typeof u !== "object" || u === null) return undefined;
16
+ const o = u as Record<string, unknown>;
17
+ if (typeof o.input_tokens !== "number" || typeof o.output_tokens !== "number") return undefined;
18
+ const usage: TokenUsage = { inputTokens: o.input_tokens, outputTokens: o.output_tokens };
19
+ if (typeof o.cache_read_input_tokens === "number") usage.cacheReadTokens = o.cache_read_input_tokens;
20
+ if (typeof o.cache_creation_input_tokens === "number") usage.cacheWriteTokens = o.cache_creation_input_tokens;
21
+ return usage;
22
+ }
23
+
24
+ /** OpenAI-style `usage` → TokenUsage. `prompt_tokens_details.cached_tokens` is
25
+ * the cache-read count where the server reports one. */
26
+ export function usageFromOpenAI(u: unknown): TokenUsage | undefined {
27
+ if (typeof u !== "object" || u === null) return undefined;
28
+ const o = u as Record<string, unknown>;
29
+ if (typeof o.prompt_tokens !== "number" || typeof o.completion_tokens !== "number") return undefined;
30
+ const usage: TokenUsage = { inputTokens: o.prompt_tokens, outputTokens: o.completion_tokens };
31
+ const details = o.prompt_tokens_details;
32
+ if (typeof details === "object" && details !== null) {
33
+ const cached = (details as Record<string, unknown>).cached_tokens;
34
+ if (typeof cached === "number") usage.cacheReadTokens = cached;
35
+ }
36
+ return usage;
37
+ }
38
+
39
+ /** A provider-reported cost beside the counters (model-proxy.md item 6): the
40
+ * OpenRouter final chunk's `usage.cost` in USD — on a BYOK turn (`is_byok`)
41
+ * the aggregator's fee alone, with what the vendor billed the operator's own
42
+ * key in `cost_details.upstream_inference_cost`. Read off the same usage
43
+ * object the counters come from; a usage without a finite `cost` reports
44
+ * none (Anthropic's wire never carries one). */
45
+ export interface ReportedCost {
46
+ /** The figure as reported, stored as reported (record 0052). */
47
+ cost: number;
48
+ /** OpenRouter's `is_byok`: the turn billed the operator's own vendor key. */
49
+ byok?: boolean;
50
+ /** `cost_details.upstream_inference_cost`: the vendor's own charge on a BYOK turn. */
51
+ upstreamCost?: number;
52
+ }
53
+
54
+ /** The cost fields of one wire `usage` object, or undefined when it carries none. */
55
+ export function reportedCostOf(u: unknown): ReportedCost | undefined {
56
+ if (typeof u !== "object" || u === null) return undefined;
57
+ const o = u as Record<string, unknown>;
58
+ if (typeof o.cost !== "number" || !Number.isFinite(o.cost)) return undefined;
59
+ const details = o.cost_details;
60
+ const upstream =
61
+ typeof details === "object" && details !== null
62
+ ? (details as Record<string, unknown>).upstream_inference_cost
63
+ : undefined;
64
+ return {
65
+ cost: o.cost,
66
+ ...(o.is_byok === true ? { byok: true } : {}),
67
+ ...(typeof upstream === "number" && Number.isFinite(upstream) ? { upstreamCost: upstream } : {}),
68
+ };
69
+ }
70
+
71
+ /** The Responses API's `usage` → TokenUsage. `input_tokens_details.cached_tokens`
72
+ * is the cache-read count and `input_tokens_details.cache_write_tokens` the
73
+ * cache-write count where a server reports one (OpenAI itself never does; a
74
+ * gateway on the wire may). `output_tokens_details.reasoning_tokens` rides
75
+ * inside `output_tokens` — the shape is read, never a fifth counter. */
76
+ export function usageFromResponses(u: unknown): TokenUsage | undefined {
77
+ if (typeof u !== "object" || u === null) return undefined;
78
+ const o = u as Record<string, unknown>;
79
+ if (typeof o.input_tokens !== "number" || typeof o.output_tokens !== "number") return undefined;
80
+ const usage: TokenUsage = { inputTokens: o.input_tokens, outputTokens: o.output_tokens };
81
+ const details = o.input_tokens_details;
82
+ if (typeof details === "object" && details !== null) {
83
+ const { cached_tokens, cache_write_tokens } = details as Record<string, unknown>;
84
+ if (typeof cached_tokens === "number") usage.cacheReadTokens = cached_tokens;
85
+ if (typeof cache_write_tokens === "number") usage.cacheWriteTokens = cache_write_tokens;
86
+ }
87
+ return usage;
88
+ }
@@ -23,7 +23,21 @@ export interface RegistryCard {
23
23
  * not named (the card's reader applies pi's rule). */
24
24
  thinkingLevelMap?: Record<string, string | null>;
25
25
  input?: string[];
26
- cost?: { input?: number; output?: number; cacheRead?: number; cacheWrite?: number };
26
+ cost?: {
27
+ input?: number;
28
+ output?: number;
29
+ cacheRead?: number;
30
+ cacheWrite?: number;
31
+ /** pi's long-context tiers: the whole request re-rates at the highest tier
32
+ * whose threshold the input side (input + cache reads + cache writes) exceeds. */
33
+ tiers?: Array<{
34
+ inputTokensAbove?: number;
35
+ input?: number;
36
+ output?: number;
37
+ cacheRead?: number;
38
+ cacheWrite?: number;
39
+ }>;
40
+ };
27
41
  contextWindow?: number;
28
42
  maxTokens?: number;
29
43
  compat?: Record<string, unknown>;
@@ -209,6 +209,55 @@ export function vendorOf(
209
209
  return { block, model, vendor: block, vendorId: model, vendorSource: "block" };
210
210
  }
211
211
 
212
+ /** The harness-side provider a block's biller implies (record 0052's
213
+ * amendment: the harness write names the biller's own provider, never a
214
+ * generic alias). Keyed by the biller — the block's name — for the billers
215
+ * whose protocol a harness bundles a provider for. The wires with a package
216
+ * of their own (`anthropic-messages` → `@ai-sdk/anthropic`,
217
+ * `openai-responses` → `@ai-sdk/openai`) need no entry: the wire names the
218
+ * package. A chat-wire biller not here is served generically
219
+ * (`@ai-sdk/openai-compatible`), under which a `markers` cache rule cannot be
220
+ * vouched for (`decideControls`): the generic provider places no cache
221
+ * breakpoints. */
222
+ export interface BillerHarnessProvider {
223
+ /** The AI SDK package OpenCode's configuration names for the biller
224
+ * (`openCodeProviderPackage` adds the `aisdk:` prefix). */
225
+ openCodePackage: string;
226
+ /** The compat words pi keys on the biller's identity, copied once from pi's
227
+ * own completions detection of that biller and never inferred from a URL at
228
+ * run time: through the proxy pi sees the bot's URL, so `piModelsJson` must
229
+ * say the words the biller's own base URL would have made pi detect. */
230
+ piCompat: {
231
+ /** How the wire spells reasoning: `reasoning: { effort }` under
232
+ * `"openrouter"`, never the completions shape's flat `reasoning_effort`. */
233
+ thinkingFormat: string;
234
+ /** How a session id would ride the headers, were affinity ever turned on. */
235
+ sessionAffinityFormat: string;
236
+ /** The vendor-qualified id prefixes the biller grants the developer role:
237
+ * any other id is told `supportsDeveloperRole: false`, as pi's own
238
+ * detection would say against the biller directly. */
239
+ developerRoleIdPrefixes: readonly string[];
240
+ };
241
+ }
242
+
243
+ /** The biller-to-provider table: one row per biller a harness speaks natively
244
+ * — OpenCode's package (U44) and pi's compat words (U45) side by side. */
245
+ export const BILLER_HARNESS_PROVIDERS: Readonly<Record<string, BillerHarnessProvider>> = {
246
+ openrouter: {
247
+ openCodePackage: "@openrouter/ai-sdk-provider",
248
+ piCompat: {
249
+ thinkingFormat: "openrouter",
250
+ sessionAffinityFormat: "openrouter",
251
+ developerRoleIdPrefixes: ["anthropic/", "openai/"],
252
+ },
253
+ },
254
+ };
255
+
256
+ /** The biller's harness-side provider, or undefined when it is served generically. */
257
+ export function billerHarnessProvider(biller: string | undefined): BillerHarnessProvider | undefined {
258
+ return biller === undefined ? undefined : BILLER_HARNESS_PROVIDERS[biller];
259
+ }
260
+
212
261
  /** The env var Anthropic's own SDK reads when an `anthropic` provider block names none. */
213
262
  export const ANTHROPIC_API_KEY_ENV = "ANTHROPIC_API_KEY";
214
263
 
@@ -56,6 +56,8 @@ const CAUSE_OF = {
56
56
  which_branch: "request",
57
57
  workspace_lost: "system",
58
58
  ship_budget: "request",
59
+ // one pipeline per thread (record 0060): the host key's claim answered thread-live
60
+ ship_thread_live: "request",
59
61
  setup_failed: "system",
60
62
  // the click on a confirmation (confirm.ts)
61
63
  confirmation_used: "request",
@@ -85,6 +87,7 @@ const CAUSE_OF = {
85
87
  ship_preflight_head_unknown: "system",
86
88
  ship_preflight_closed_resume: "request",
87
89
  ship_preflight_no_task: "request",
90
+ ship_preflight_base_missing: "request",
88
91
  // the plan hand-off's fifteen sentences (two share `plan_history_unavailable`)
89
92
  plan_base_unknown: "request",
90
93
  plan_routed_seed: "request",
@@ -100,12 +103,9 @@ const CAUSE_OF = {
100
103
  plan_runner_conflict: "system",
101
104
  plan_instance_orphaned: "system",
102
105
  plan_start_failed: "system",
103
- // the directive and resolve parsers' thrown errors (A6 of the inventory)
104
- directive_agent: "request",
105
- directive_effort: "request",
106
- directive_budget: "request",
107
- directive_severity: "request",
108
- directive_renewals: "request",
106
+ // the resolve parser's thrown errors (A6 of the inventory); the directive
107
+ // parser no longer refuses — a token whose value is outside its vocabulary
108
+ // is text (the interim grammar, src/directives.ts)
109
109
  provider_unknown: "request",
110
110
  // the model card's refusal (record 0052, model-proxy item 11): a control the
111
111
  // resolved card does not take, named before any card or model call
@@ -108,6 +108,10 @@ export interface Finding {
108
108
  line?: number;
109
109
  /** One line naming the issue; the full explanation lives in the prose. */
110
110
  title: string;
111
+ /** Machine provenance: true only on a check finding the ship round's checks
112
+ * step itself appended (ship/coordinator.ts `checkFinding`) — never set from
113
+ * a reviewer's input, whatever id the reviewer chose. */
114
+ check?: true;
111
115
  }
112
116
 
113
117
  export interface ReviewVerdict {
@@ -397,6 +397,10 @@ export type ShipRoundOutcome =
397
397
  | "approve"
398
398
  | "request_changes"
399
399
  | "no_verdict"
400
+ /** The round's checks step read a failed check at the reviewed head (record
401
+ * 0055): the failures become check findings and the findings step runs as
402
+ * for any changes-requested round. */
403
+ | "checks_failed"
400
404
  | "aborted"
401
405
  | "stopped"
402
406
  /** The coding round ended at its lease with the unit unfinished and the row
@@ -536,6 +540,11 @@ export type RunEvent =
536
540
  callId?: string;
537
541
  spanId?: string;
538
542
  logIndex?: number;
543
+ /** The bound the call itself declared, in ms (a bash `timeout`, clamped
544
+ * by nothing here — pi runs an unbounded call until the loop's end).
545
+ * The stall signal (docs/reference/specs/live-view.md item 32) marks a
546
+ * call past it; absent when the call declared none. */
547
+ boundMs?: number;
539
548
  seq?: number;
540
549
  at?: number;
541
550
  }
@@ -671,8 +680,12 @@ export type RunEvent =
671
680
  * and — for a repo run — the repo, ref, PR number and PR head as resolved
672
681
  * BEFORE the first model turn (`RepoContext`). Published by the dispatcher
673
682
  * right after `input`, once per run, so the run page can head its Request
674
- * block with linked `owner/repo · ref · #PR · sha`. Additive: every
675
- * consumer that only knows the other types keeps working. */
683
+ * block with linked `owner/repo · ref · #PR · sha`. A HOSTED ship parent
684
+ * (record 0060) carries a second one at the hand-off naming its runner
685
+ * instance (`instanceId`): `run_meta` may repeat on such a run, and every
686
+ * reader of a run's instance id resolves it from the LAST `run_meta`
687
+ * carrying one. Additive: every consumer that only knows the other types
688
+ * keeps working. */
676
689
  | {
677
690
  type: "run_meta";
678
691
  agent: string;
@@ -865,6 +878,23 @@ export type RunEvent =
865
878
  * `coordinator_tag` is — so the thread's owner rule can find the instance
866
879
  * from the page's ship run. Additive: unknown → ignored. */
867
880
  | { type: "ship_handoff"; instanceId: string; seq?: number; at?: number }
881
+ /** A coordinator child interrupted under a deploy roll (docs/reference/specs/run-history.md
882
+ * item 47a): the run closes `interrupted` for a restart from its request —
883
+ * a workspace lost across a bot roll, the resident's container replaced
884
+ * with the relaunch refused — and the record says so as a typed fact
885
+ * beside the `resumed`/`sandbox_restarted` notes, `reason` the refusal or
886
+ * note in one line. Published straight to the registry by the reattach
887
+ * path and the run loop's interruption; the parent's wait settles on its
888
+ * Workflow twin (`child-interrupted-<runId>`) beside `run-finished-<runId>`
889
+ * and confirms by `read-record`. Additive: unknown → ignored. */
890
+ | { type: "child_interrupted"; parentInstanceId: string; reason: string; seq?: number; at?: number }
891
+ /** A coordinator child resumed across a deploy roll (run-history item 47a):
892
+ * the same run re-attached its workspace and carries on under its record,
893
+ * tag and budget. Published straight to the registry by the reattach path;
894
+ * the Workflow twin (`child-resumed-<runId>`) tells the parent's wait to
895
+ * keep waiting rather than walk out the chunk asking. Additive: unknown →
896
+ * ignored. */
897
+ | { type: "child_resumed"; parentInstanceId: string; summary: string; seq?: number; at?: number }
868
898
  /** The review post-step's outcome when the verdict landed
869
899
  * (docs/reference/specs/agent-review.md item 18): the pull request it was
870
900
  * posted to, the head it was pinned to (the carried head after a rebase,
@@ -905,6 +935,25 @@ export type RunEvent =
905
935
  seq?: number;
906
936
  at?: number;
907
937
  }
938
+ /** A hosted ship parent's unit fact (record 0060; docs/reference/specs/agent-ship.md
939
+ * item 17): the runner's routes write it to the parent run through the
940
+ * coordinator's `hostPublish` — `unit-start` publishes state `started` with
941
+ * the unit's thread key and lead, `round` the round's outcome as the unit's
942
+ * state, `unit-end` the ending's kind with its report — carrying the pull
943
+ * request number when the unit's row knows one. The run page draws it as a
944
+ * step whose detail is the report; the friction analyzer counts it as a
945
+ * side fact, never activity. Additive: unknown → ignored. */
946
+ | {
947
+ type: "ship_unit";
948
+ unit: string;
949
+ state: string;
950
+ threadKey?: string;
951
+ lead?: string;
952
+ report?: string;
953
+ pr?: number;
954
+ seq?: number;
955
+ at?: number;
956
+ }
908
957
  /** The request router's decision (docs/reference/specs/routing-and-config.md
909
958
  * item 21): the preset a plain message was routed to, the one-line reason
910
959
  * the router gave (redacted, capped — the same text the card's `routed:`
@@ -413,8 +413,10 @@ export function analyzeRunFriction(events: readonly RunEvent[], opts: FrictionOp
413
413
  // call that already produced its own tool pair, and artifact beside the
414
414
  // attach_file call (or the dispatcher's staging) that moved the file;
415
415
  // review_artifact, pr_description, pr_opened, review_posted, the ship_round
416
- // boundaries and the router's route are published by the dispatcher/pipeline
417
- // outside the model loop entirely. Counting any of them would distort the story.
416
+ // boundaries, the hosted parent's ship_unit facts (record 0060 — written by
417
+ // the runner's routes, not the model) and the router's route are published
418
+ // by the dispatcher/pipeline outside the model loop entirely. Counting any
419
+ // of them would distort the story.
418
420
  if (
419
421
  ev.type === "skill_use" ||
420
422
  ev.type === "artifact" ||
@@ -423,8 +425,11 @@ export function analyzeRunFriction(events: readonly RunEvent[], opts: FrictionOp
423
425
  ev.type === "pr_opened" ||
424
426
  ev.type === "coordinator_tag" ||
425
427
  ev.type === "ship_handoff" ||
428
+ ev.type === "child_interrupted" ||
429
+ ev.type === "child_resumed" ||
426
430
  ev.type === "review_posted" ||
427
431
  ev.type === "ship_round" ||
432
+ ev.type === "ship_unit" ||
428
433
  ev.type === "route" ||
429
434
  ev.type === "refusal" ||
430
435
  ev.type === "reference" ||
@@ -59,6 +59,14 @@ export interface LiveRunMeta {
59
59
  /** The app that relayed the request for the person (authorization.md item 14): a resume or restart keeps app ∩ person at the gates. */
60
60
  postedBy?: string;
61
61
  effort?: string;
62
+ /** A ship pipeline's parent (record 0060): claimed under the host key
63
+ * (`hostKey.ts`) while `threadKey` here names the thread itself, so every
64
+ * record, notice and rebuilt handle files by the metadata's thread and the
65
+ * occupancy readers that key on the ledger's key column never see it. */
66
+ hosted?: true;
67
+ /** The run's index label (the registry sets it at create): on the row so a
68
+ * hosted run listed from the ledger reads as its registry view does. */
69
+ label?: string;
62
70
  ref?: string;
63
71
  headSha?: string;
64
72
  pr?: number;
@@ -96,10 +104,10 @@ export interface LiveRunMeta {
96
104
  request?: Record<string, unknown>;
97
105
  /** The router's decision when it chose the run's preset (routing-and-config
98
106
  * item 21) — the same fields the record's `route` event carries. On the row
99
- * so a resume repaints the card as it was (` · routed: <reason>`, the
100
- * parts, the override footer on the close) and a reclaim closing the run
101
- * knows it was routed without reading the events. Absent for a preset a
102
- * person, a scope or the default chose. */
107
+ * so a resume repaints the card as it was (the `route reason:` note at
108
+ * debug, the parts) and a record built from the row knows it was routed
109
+ * without reading the events. Absent for a preset a person, a scope or the
110
+ * default chose. */
103
111
  route?: {
104
112
  preset: string;
105
113
  reason: string;
@@ -113,6 +121,17 @@ export interface LiveRunMeta {
113
121
  * the checklist, the pushed branch, the review head, the infra counters). */
114
122
  export type RunState = Record<string, unknown>;
115
123
 
124
+ /** A hosted ship parent's hosting fact on the row's state (record 0060): the
125
+ * runner instance the run hosts and the deadline past which a reclaim closes
126
+ * the row `interrupted` instead of re-hosting it. Set by the ship branch once
127
+ * the instance exists; read by the boot reclaim. */
128
+ export interface HostingState {
129
+ instanceId: string;
130
+ /** Epoch ms: the hand-off time plus the instance's `caps.maxMinutes` plus
131
+ * `HOSTED_DEADLINE_MARGIN_MINUTES`. */
132
+ until: number;
133
+ }
134
+
116
135
  export interface LiveRunRow {
117
136
  runId: string;
118
137
  threadKey: string;
@@ -39,6 +39,12 @@ import {
39
39
  // reached `finish`.
40
40
  export type RunStatus = "completed" | "stopped_soft" | "stopped_hard" | "failed" | "interrupted";
41
41
 
42
+ /** How every store-only reader renders a record still in its provisional
43
+ * window (run-history.md item 27's third state): not `interrupted` (a real
44
+ * terminal state) and not `running` (the store cannot know). One string for
45
+ * the CLI's `runs list`/`runs get` and the web's index row and run page. */
46
+ export const PROVISIONAL_LABEL = "unfinished — no finish recorded";
47
+
42
48
  const RUN_STATUSES: readonly RunStatus[] = ["completed", "stopped_soft", "stopped_hard", "failed", "interrupted"];
43
49
 
44
50
  /** Every `runs.*` id: checked before any store call. */
@@ -115,6 +121,13 @@ export interface RunRecord {
115
121
  stepCount?: number;
116
122
  schema?: number;
117
123
  status: RunStatus;
124
+ /** Present on a tombstone record still in its provisional window — the
125
+ * start-of-run `interrupted` or the drain-deadline upgrade — before the
126
+ * run's final write lands. Absent on every final (finished) record. A
127
+ * store-only reader must render a provisional record as "unfinished — no
128
+ * finish recorded" rather than as `interrupted`, because the run may still
129
+ * be live in a registry the reader cannot see (run-history.md item 27). */
130
+ provisional?: true;
118
131
  /** The failure by name, when a `failed` run has one (item 57):
119
132
  * `policy_refusal`, the provider refused the run's model call under its
120
133
  * usage policy. Absent on a run that did not fail, on one that failed for
@@ -944,6 +957,9 @@ export function isRunRecord(v: unknown): v is RunRecord {
944
957
  return false;
945
958
  }
946
959
  if (!RUN_STATUSES.includes(r.status as RunStatus)) return false;
960
+ // A provisional tombstone carries `provisional: true`; any other value (false,
961
+ // a string, etc.) is a malformed record — only the presence of the flag matters.
962
+ if (r.provisional !== undefined && r.provisional !== true) return false;
947
963
  if (!isFiniteNumber(r.eventCount) || !isFiniteNumber(r.storedEventCount)) return false;
948
964
  if (typeof r.truncated !== "boolean") return false;
949
965
  if (!Array.isArray(r.events)) return false;