@coreplane/switchboard 1.251.0 → 1.253.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 (167) hide show
  1. package/dist/assets/config/config.example.yaml +28 -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/runMetricsSink.ts +31 -0
  5. package/dist/assets/deploy/cloudflare-memory/worker.ts +223 -38
  6. package/dist/assets/deploy/cloudflare-memory/wrangler.template.jsonc +12 -0
  7. package/dist/assets/deploy/cloudflare-resident/memoryGuard.ts +212 -0
  8. package/dist/assets/deploy/cloudflare-resident/refresh.ts +1 -1
  9. package/dist/assets/deploy/cloudflare-resident/worker.ts +317 -56
  10. package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +4 -2
  11. package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.d.mts +31 -0
  12. package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.mjs +119 -0
  13. package/dist/assets/deploy/profile.example.json +1 -1
  14. package/dist/assets/package-lock.json +3 -3
  15. package/dist/assets/package.json +3 -2
  16. package/dist/assets/project.json +13 -9
  17. package/dist/assets/source.json +3 -3
  18. package/dist/assets/src/agents/registry.ts +5 -5
  19. package/dist/assets/src/core/authz/policy.ts +8 -0
  20. package/dist/assets/src/core/budgets.ts +39 -0
  21. package/dist/assets/src/core/coordinator/contract.ts +93 -1
  22. package/dist/assets/src/core/coordinator/driver.ts +236 -21
  23. package/dist/assets/src/core/drain.ts +50 -0
  24. package/dist/assets/src/core/memory/engine.ts +123 -4
  25. package/dist/assets/src/core/memory/scorer.ts +12 -4
  26. package/dist/assets/src/core/memory/types.ts +73 -12
  27. package/dist/assets/src/core/modelCard.ts +32 -4
  28. package/dist/assets/src/core/modelPricing.ts +111 -1
  29. package/dist/assets/src/core/modelProxy/usage.ts +88 -0
  30. package/dist/assets/src/core/modelRegistry.ts +15 -1
  31. package/dist/assets/src/core/refusal.ts +4 -7
  32. package/dist/assets/src/core/reviewVerdict.ts +4 -0
  33. package/dist/assets/src/core/runEvents.ts +82 -3
  34. package/dist/assets/src/core/runFriction.ts +8 -2
  35. package/dist/assets/src/core/runLedger/types.ts +11 -0
  36. package/dist/assets/src/core/runMetrics.ts +237 -0
  37. package/dist/assets/src/core/runRecord.ts +46 -0
  38. package/dist/assets/src/core/runUsage.ts +67 -13
  39. package/dist/assets/src/core/schedules.ts +3 -0
  40. package/dist/assets/src/core/ship/contract.ts +34 -14
  41. package/dist/assets/src/core/ship/coordinator.ts +485 -55
  42. package/dist/assets/src/core/ship/renewal.ts +10 -5
  43. package/dist/assets/src/core/trace/attrs.ts +24 -0
  44. package/dist/assets/src/core/types.ts +5 -5
  45. package/dist/assets/src/core/verbosity.ts +48 -0
  46. package/dist/assets/src/deploy/liveGate.ts +40 -13
  47. package/dist/assets/src/deploy/profile.ts +16 -0
  48. package/dist/assets/src/deploy/restart.ts +11 -12
  49. package/dist/assets/src/execution/residentDepCache.ts +50 -1
  50. package/dist/assets/src/execution/residentDepsStore.ts +40 -2
  51. package/dist/assets/src/execution/residentRefresh.ts +55 -3
  52. package/dist/assets/src/execution/residentSteps.ts +4 -0
  53. package/dist/assets/src/execution/sandboxErrors.ts +8 -0
  54. package/dist/assets/web/dist/.vite/manifest.json +444 -430
  55. package/dist/assets/web/dist/assets/{AppShell-DAtHiuI6.js → AppShell-Bmz8nzz3.js} +1 -1
  56. package/dist/assets/web/dist/assets/{CostsPage-BoKzYa4B.js → CostsPage-CxTKdpzU.js} +1 -1
  57. package/dist/assets/web/dist/assets/{DeliveryPage-DUXd-Sl-.js → DeliveryPage-C7jaEJip.js} +1 -1
  58. package/dist/assets/web/dist/assets/HomePage-cWbupQzy.js +3 -0
  59. package/dist/assets/web/dist/assets/{InputMenu-C6aPpe30.js → InputMenu-C9E7YoII.js} +1 -1
  60. package/dist/assets/web/dist/assets/{NotFoundPage-DtE-GgTk.js → NotFoundPage-4nlrq7DP.js} +1 -1
  61. package/dist/assets/web/dist/assets/{PendingTurnRow-DDhMhrI7.js → PendingTurnRow-C-pUgyyX.js} +1 -1
  62. package/dist/assets/web/dist/assets/PlanePage-BNy4tSgJ.js +1 -0
  63. package/dist/assets/web/dist/assets/{ResidentDetailPage-BnEoOnGQ.js → ResidentDetailPage-Dbtiq6_J.js} +1 -1
  64. package/dist/assets/web/dist/assets/{ResidentsIndexPage-Dxpgf-l-.js → ResidentsIndexPage-CKtz9H1o.js} +1 -1
  65. package/dist/assets/web/dist/assets/RunFoldRow-sYFtuK2G.js +1 -0
  66. package/dist/assets/web/dist/assets/RunRoutePage-Clh68nw9.js +9 -0
  67. package/dist/assets/web/dist/assets/RunsIndexPage-DPDbcOA_.js +1 -0
  68. package/dist/assets/web/dist/assets/{RunsTabs-YSbUu5py.js → RunsTabs-DotpuCSP.js} +1 -1
  69. package/dist/assets/web/dist/assets/{ScheduledPage-B_GgeJrb.js → ScheduledPage-C8FYoZy5.js} +1 -1
  70. package/dist/assets/web/dist/assets/{SettingSelect-BIzAsLk1.js → SettingSelect-CSiBQCSX.js} +1 -1
  71. package/dist/assets/web/dist/assets/{SettingsPage-BXX4R113.js → SettingsPage-CkYIhLUl.js} +1 -1
  72. package/dist/assets/web/dist/assets/{StatusDot-BOaw8le9.js → StatusDot-C0TUgnC1.js} +1 -1
  73. package/dist/assets/web/dist/assets/{Tooltip-DYZZ4l4V.js → Tooltip-CN5cZdl3.js} +1 -1
  74. package/dist/assets/web/dist/assets/{UnitRoutePage-BaSW5Odq.js → UnitRoutePage-D_EtAl8r.js} +1 -1
  75. package/dist/assets/web/dist/assets/{angular-html-BjeQZdCq.js → angular-html-qW40ZL9x.js} +1 -1
  76. package/dist/assets/web/dist/assets/{angular-ts-ZGdORrL2.js → angular-ts-PUtZU8Cq.js} +1 -1
  77. package/dist/assets/web/dist/assets/{apl-DhvV_X93.js → apl-D3fC7Qjd.js} +1 -1
  78. package/dist/assets/web/dist/assets/{astro-DiRSE4Ug.js → astro-Bi3JFHhh.js} +1 -1
  79. package/dist/assets/web/dist/assets/{blade-D8Fr5Wvq.js → blade-CSpsf9Z9.js} +1 -1
  80. package/dist/assets/web/dist/assets/budgets-CbIyPAER.js +1 -0
  81. package/dist/assets/web/dist/assets/{c-BwbC64D4.js → c-DE_b1oWP.js} +1 -1
  82. package/dist/assets/web/dist/assets/{chapel-DgmqRK97.js → chapel-CWXd9giL.js} +1 -1
  83. package/dist/assets/web/dist/assets/{cobol-DJod2RNK.js → cobol-CnMOmWNF.js} +1 -1
  84. package/dist/assets/web/dist/assets/{coffee-D1Y2CgBY.js → coffee-B9WvURu8.js} +1 -1
  85. package/dist/assets/web/dist/assets/{cpp-CsO97YOM.js → cpp-BZAJXlWI.js} +1 -1
  86. package/dist/assets/web/dist/assets/{crystal-CJ7m2tOc.js → crystal-Bm48wAlb.js} +1 -1
  87. package/dist/assets/web/dist/assets/{css-B2M-NKoj.js → css-WtMeC3ab.js} +1 -1
  88. package/dist/assets/web/dist/assets/{dist-BCVXeBJ9.js → dist-B1Mf2JbM.js} +2 -2
  89. package/dist/assets/web/dist/assets/{edge-CFVW-m9B.js → edge-BmwNa6Ss.js} +1 -1
  90. package/dist/assets/web/dist/assets/{elixir-B2mtRgCr.js → elixir-BotfP-Mn.js} +1 -1
  91. package/dist/assets/web/dist/assets/{elm-2teVAwCi.js → elm-DAFc6laN.js} +1 -1
  92. package/dist/assets/web/dist/assets/{erb-LAbeNqen.js → erb-QkOON3bB.js} +1 -1
  93. package/dist/assets/web/dist/assets/{git-rebase-DnhzH_sp.js → git-rebase-BPs-dd73.js} +1 -1
  94. package/dist/assets/web/dist/assets/{glimmer-js-DPKjXf1-.js → glimmer-js-D8eooepk.js} +1 -1
  95. package/dist/assets/web/dist/assets/{glimmer-ts-CABXnt5z.js → glimmer-ts-B0bRiDVg.js} +1 -1
  96. package/dist/assets/web/dist/assets/{glsl-Dz4-3-gJ.js → glsl-Bv6bMe1n.js} +1 -1
  97. package/dist/assets/web/dist/assets/{graphql-ByWUD1DL.js → graphql-Bh9y-n9o.js} +1 -1
  98. package/dist/assets/web/dist/assets/{hack-CdmOK2K-.js → hack-DSEbn7Ir.js} +1 -1
  99. package/dist/assets/web/dist/assets/{haml-DzegMpd2.js → haml-0NW1-z1f.js} +1 -1
  100. package/dist/assets/web/dist/assets/{handlebars-B5NeiP0e.js → handlebars-BfgCHFbK.js} +1 -1
  101. package/dist/assets/web/dist/assets/{html-Zg_NHLv9.js → html-CQHc0gQ5.js} +1 -1
  102. package/dist/assets/web/dist/assets/{html-derivative-DZf9JNzh.js → html-derivative-B0Tm7wLP.js} +1 -1
  103. package/dist/assets/web/dist/assets/{http-CVFrQZeN.js → http-GAuU3aOF.js} +1 -1
  104. package/dist/assets/web/dist/assets/{hurl-L5brWAJe.js → hurl-CrDFbF5B.js} +1 -1
  105. package/dist/assets/web/dist/assets/indexRow-BxKswdlK.js +1 -0
  106. package/dist/assets/web/dist/assets/{java-hFphyZ4R.js → java-DYv5KhUe.js} +1 -1
  107. package/dist/assets/web/dist/assets/{javascript-CAJO1WKX.js → javascript-32SiLGg0.js} +1 -1
  108. package/dist/assets/web/dist/assets/{jinja-N4A4srXI.js → jinja--gJDUGYa.js} +1 -1
  109. package/dist/assets/web/dist/assets/{jison-D3BZa_iU.js → jison-BlKLGzZH.js} +1 -1
  110. package/dist/assets/web/dist/assets/{json-DxD1Qh8W.js → json-BICanD1N.js} +1 -1
  111. package/dist/assets/web/dist/assets/{jsx-CHhag69S.js → jsx-CgW3-uFT.js} +1 -1
  112. package/dist/assets/web/dist/assets/{julia-CrhkZ6Tt.js → julia-BErRpwwL.js} +1 -1
  113. package/dist/assets/web/dist/assets/{just-D0LFOuhq.js → just-CordHv0h.js} +1 -1
  114. package/dist/assets/web/dist/assets/{latex-BHAlwCXK.js → latex-D7LVPLbi.js} +1 -1
  115. package/dist/assets/web/dist/assets/{liquid-CkaLfCWh.js → liquid-3ehz2i4U.js} +1 -1
  116. package/dist/assets/web/dist/assets/{lua-B1h4aAzP.js → lua-C9ppSVrm.js} +1 -1
  117. package/dist/assets/web/dist/assets/main-Cv0q9y56.js +28 -0
  118. package/dist/assets/web/dist/assets/main-DKFCM-sy.css +1 -0
  119. package/dist/assets/web/dist/assets/{marko-D6CG4hvX.js → marko-xBZLUBEJ.js} +1 -1
  120. package/dist/assets/web/dist/assets/{mdc-CVQzimAU.js → mdc-BJxGoVgD.js} +1 -1
  121. package/dist/assets/web/dist/assets/{nginx-h-9Ir54h.js → nginx-B-sZmpsb.js} +1 -1
  122. package/dist/assets/web/dist/assets/{nim-DHVI30sp.js → nim--s9wRABK.js} +1 -1
  123. package/dist/assets/web/dist/assets/{org-AasAXfMF.js → org-DoUvssoI.js} +1 -1
  124. package/dist/assets/web/dist/assets/{perl-BVi5aTEy.js → perl-B-vGHKOy.js} +1 -1
  125. package/dist/assets/web/dist/assets/{php-qOkCeUKr.js → php-CerWx_Xb.js} +1 -1
  126. package/dist/assets/web/dist/assets/{pug-8TnUGvEX.js → pug-CzKrBMb1.js} +1 -1
  127. package/dist/assets/web/dist/assets/{qml-CpJbhoxn.js → qml-Bs8L8I6k.js} +1 -1
  128. package/dist/assets/web/dist/assets/{r-JkUNcAJW.js → r-w6uG6mob.js} +1 -1
  129. package/dist/assets/web/dist/assets/{razor-CtU4ws5a.js → razor-dz2oj2bd.js} +1 -1
  130. package/dist/assets/web/dist/assets/{regexp-J2zA6ceN.js → regexp-CfQdprnY.js} +1 -1
  131. package/dist/assets/web/dist/assets/{rst-CMCvf-yZ.js → rst-DAOaX2tb.js} +1 -1
  132. package/dist/assets/web/dist/assets/{ruby-ChLBY_iR.js → ruby-QcCe4udB.js} +1 -1
  133. package/dist/assets/web/dist/assets/{sas-cwn4x2VY.js → sas-BrM4lAAc.js} +1 -1
  134. package/dist/assets/web/dist/assets/{scss-jSrJlzLI.js → scss-DVbJXoUb.js} +1 -1
  135. package/dist/assets/web/dist/assets/{shellscript-D5nAd0gs.js → shellscript-09fP2hZf.js} +1 -1
  136. package/dist/assets/web/dist/assets/{shellsession-BZY4-Tdh.js → shellsession-Qgr8xjZF.js} +1 -1
  137. package/dist/assets/web/dist/assets/{soy-COppPOKr.js → soy-DIYeNTuz.js} +1 -1
  138. package/dist/assets/web/dist/assets/{sql-Blc_6dOA.js → sql-CzLR9msV.js} +1 -1
  139. package/dist/assets/web/dist/assets/sseReplay-BYo2EZKy.js +11 -0
  140. package/dist/assets/web/dist/assets/{stata-DRv3OY-h.js → stata-zCY1ZzH0.js} +1 -1
  141. package/dist/assets/web/dist/assets/{surrealql-qMv_eeJ6.js → surrealql-CJIjTRV4.js} +1 -1
  142. package/dist/assets/web/dist/assets/{svelte-d1Edb6Rz.js → svelte-C5OTPFZg.js} +1 -1
  143. package/dist/assets/web/dist/assets/{templ-CUMSIOXG.js → templ-DYj-UICF.js} +1 -1
  144. package/dist/assets/web/dist/assets/{tex-BGZQA-Ei.js → tex-xcie-3Gd.js} +1 -1
  145. package/dist/assets/web/dist/assets/{ts-tags-Z9ulxVsq.js → ts-tags-CoN86VTt.js} +1 -1
  146. package/dist/assets/web/dist/assets/{tsx-oI4bI3Vf.js → tsx-n3-No_Br.js} +1 -1
  147. package/dist/assets/web/dist/assets/{twig-D_RXwBIX.js → twig-DPiJ7Vp1.js} +1 -1
  148. package/dist/assets/web/dist/assets/{typescript-BgqQDYoN.js → typescript-BP6HBaOp.js} +1 -1
  149. package/dist/assets/web/dist/assets/{typst-5MW-KRDd.js → typst-Do9ZjsVP.js} +1 -1
  150. package/dist/assets/web/dist/assets/{vue-CZhHHsPm.js → vue-CzAeXNRY.js} +1 -1
  151. package/dist/assets/web/dist/assets/{vue-html-DlYy2d8v.js → vue-html-D7iYmYjd.js} +1 -1
  152. package/dist/assets/web/dist/assets/{vue-vine-CJDnx-qn.js → vue-vine-DtrMRsJx.js} +1 -1
  153. package/dist/assets/web/dist/assets/{xml-BRBDk65Q.js → xml-DGJHo1c0.js} +1 -1
  154. package/dist/assets/web/dist/assets/{xsl-BlCykPxo.js → xsl-Q00kvNgW.js} +1 -1
  155. package/dist/assets/web/dist/assets/{yaml-BqW48szf.js → yaml-F-UGKaiO.js} +1 -1
  156. package/dist/cli.js +5623 -2455
  157. package/package.json +1 -1
  158. package/dist/assets/deploy/cloudflare-sandbox/runtime-supervisor.sh +0 -68
  159. package/dist/assets/web/dist/assets/HomePage-mSiqEEcN.js +0 -2
  160. package/dist/assets/web/dist/assets/RunFoldRow-CSo4-vld.js +0 -1
  161. package/dist/assets/web/dist/assets/RunRoutePage-9klVWhSF.js +0 -9
  162. package/dist/assets/web/dist/assets/RunsIndexPage-BplMIgaw.js +0 -1
  163. package/dist/assets/web/dist/assets/budgets-BvWYKPsY.js +0 -1
  164. package/dist/assets/web/dist/assets/indexRow-Bde9OZxG.js +0 -1
  165. package/dist/assets/web/dist/assets/main-Dkcbtu3u.js +0 -28
  166. package/dist/assets/web/dist/assets/main-zbP_dTjR.css +0 -1
  167. package/dist/assets/web/dist/assets/sseReplay-DPwdsaok.js +0 -9
@@ -36,9 +36,11 @@ export interface MemoryRecord {
36
36
  supersedes?: string;
37
37
  /** `evicted`: dropped by the per-scope cap (least recently used);
38
38
  * `superseded`: replaced by a newer record; `forgotten`: removed by a human
39
- * via `memory forget`. Both are soft deletes — the row stays for
40
- * provenance but is invisible to retrieval, list, and dedup. */
41
- status: "active" | "superseded" | "forgotten" | "evicted";
39
+ * via `memory forget`; `swept`: retired by `memory sweep` because the text
40
+ * carries a status/change-description marker (`rejectionMarkers`). All are
41
+ * soft deletes — the row stays for provenance but is invisible to
42
+ * retrieval, list, and dedup. */
43
+ status: "active" | "superseded" | "forgotten" | "evicted" | "swept";
42
44
  }
43
45
 
44
46
  /** What the reflection extractor emits. The store assigns id/timestamps/useCount/
@@ -52,8 +54,51 @@ export interface MemoryCandidate {
52
54
  sourceRunId?: string;
53
55
  confidence?: number;
54
56
  supersedes?: string;
57
+ /** id of a shown record this fact restates: the store bumps that record
58
+ * (`useCount`, `lastUsedAt`, the higher `confidence`) instead of inserting
59
+ * a twin; a target that is missing or not active falls through to the
60
+ * plain dedup-or-insert path. */
61
+ restates?: string;
55
62
  }
56
63
 
64
+ /** What one `write` batch actually did, per candidate action — the seam's
65
+ * receipt (the counters on the `[memory]` outcome line are these plus the
66
+ * parse gate's own). */
67
+ export interface WriteCounts {
68
+ /** Candidates minted as new active records. */
69
+ inserted: number;
70
+ /** Candidates whose normalized text bumped an existing active record. */
71
+ deduped: number;
72
+ /** Candidates that bumped the shown record they restate (no insert). */
73
+ restated: number;
74
+ /** Records flipped to `superseded` by a candidate's pointer. */
75
+ superseded: number;
76
+ /** Records the per-scope cap evicted inside the same batch. */
77
+ evicted: number;
78
+ }
79
+
80
+ /** Narrowing filters for `MemoryStore.list` — each narrows, never ranks. */
81
+ export interface MemoryListOptions {
82
+ /** Whole-token text/keyword filter (the human command's `<words>`). */
83
+ query?: string;
84
+ /** Keep only records of this kind (the repository window lists facts). */
85
+ kind?: MemoryRecord["kind"];
86
+ }
87
+
88
+ /** What one sweep did — or, on the durable path, why it could not. The
89
+ * failure is a VALUE, never a throw: the one expected failure is an older
90
+ * Memory Worker generation without the `/sweep` route (a 404), which the
91
+ * command must report as a ⚠️ line, not crash on. */
92
+ export type SweepOutcome =
93
+ | {
94
+ ok: true;
95
+ /** Active facts the gate marked (flipped to `swept`, or merely counted under `dryRun`). */
96
+ swept: number;
97
+ /** The marked record ids — always safe to show (ids carry no record text). */
98
+ ids: string[];
99
+ }
100
+ | { ok: false; error: string };
101
+
57
102
  /** A retrieval request: which resource, what to match, how many at most. */
58
103
  export interface MemoryQuery {
59
104
  scopeKey: string;
@@ -71,19 +116,29 @@ export interface MemoryStore {
71
116
  retrieve(q: MemoryQuery, trace?: TraceOptions): Promise<MemoryRecord[]>;
72
117
  /** Persist distilled candidates. Dedup (identical normalized text → bump
73
118
  * `useCount`) and supersede (`supersedes` id → old record soft-deleted) live
74
- * inside the store. Driven by the post-run reflection pass (reflection.ts). */
75
- write(scopeKey: string, records: MemoryCandidate[]): Promise<void>;
76
- /** Human view: a scope's ACTIVE records, newest first, at most
77
- * `limit`. Unlike `retrieve` this never bumps usage. */
78
- /** `query`: when given, only records that a query token hits
119
+ * inside the store. Driven by the post-run reflection pass (reflection.ts).
120
+ * Answers what the batch did (`WriteCounts`), so the caller's outcome line
121
+ * reports the store's actions, never a guess. */
122
+ write(scopeKey: string, records: MemoryCandidate[]): Promise<WriteCounts>;
123
+ /** Human view and the repository window's read: a scope's ACTIVE records,
124
+ * newest first, at most `limit`. Unlike `retrieve` this never bumps usage.
125
+ * `opts.query`: when given, only records that a query token hits
79
126
  * (whole-token, text or keywords) are listed — the filter narrows, it
80
- * never ranks or bumps usage. */
81
- list(scopeKey: string, limit: number, query?: string): Promise<MemoryRecord[]>;
127
+ * never ranks or bumps usage. `opts.kind`: when given, only records of
128
+ * that kind (the repository window lists facts, never summaries). */
129
+ list(scopeKey: string, limit: number, opts?: MemoryListOptions): Promise<MemoryRecord[]>;
82
130
  /** Human control: soft-delete one ACTIVE record of this scope
83
131
  * (`status: "forgotten"`, row kept for provenance). Resolves true when a
84
132
  * record was forgotten, false when the id names nothing active in this
85
133
  * scope — a foreign-scope id can never be forgotten through another scope. */
86
134
  forget(scopeKey: string, id: string): Promise<boolean>;
135
+ /** Human control: retire this scope's ACTIVE facts whose text the write
136
+ * gate would reject today (`rejectionMarkers`, engine.ts) — `status:
137
+ * "swept"`, rows kept for provenance, summaries never touched. Under
138
+ * `dryRun` nothing flips and the marked ids are answered. Idempotent: a
139
+ * second sweep answers 0. Failures come back as `{ok: false}`, never a
140
+ * throw (an older Worker without the route is a reported failure). */
141
+ sweep(scopeKey: string, opts?: { dryRun?: boolean }): Promise<SweepOutcome>;
87
142
  }
88
143
 
89
144
  /** The resources memory is scoped to. `org` is the shared resource every
@@ -98,10 +153,16 @@ export type MemoryScope = "org" | "user" | "repo" | "channel";
98
153
  export interface MemoryConfig {
99
154
  /** Master switch. Default false → `NullMemoryStore` → zero behavior change. */
100
155
  enabled?: boolean;
101
- /** Max records retrieved/injected per request. Default 8. */
156
+ /** Max records retrieved/injected per request. Default 32. */
102
157
  limit?: number;
103
- /** Hard token budget for the injected block. Default ~800. */
158
+ /** Hard token budget for the injected block. Default ~3000. */
104
159
  maxTokens?: number;
160
+ /** The repository window: a run bound to a repository leads its block with
161
+ * that repository's newest facts — at most this many, read with
162
+ * `list(repoScope, repoWindow, { kind: "fact" })` — ahead of the keyword
163
+ * hits, under the same budget. Default 24; `0` disables the window (the
164
+ * repository scope is retrieved by keyword like the others). */
165
+ repoWindow?: number;
105
166
  /** Per-scope cap on ACTIVE records. A write that would leave a scope
106
167
  * over the cap evicts the least recently used records (soft delete, status
107
168
  * `evicted`) down to it, inside the same write. Default 500. */
@@ -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
  /**
@@ -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>;
@@ -87,6 +87,7 @@ const CAUSE_OF = {
87
87
  ship_preflight_head_unknown: "system",
88
88
  ship_preflight_closed_resume: "request",
89
89
  ship_preflight_no_task: "request",
90
+ ship_preflight_base_missing: "request",
90
91
  // the plan hand-off's fifteen sentences (two share `plan_history_unavailable`)
91
92
  plan_base_unknown: "request",
92
93
  plan_routed_seed: "request",
@@ -102,13 +103,9 @@ const CAUSE_OF = {
102
103
  plan_runner_conflict: "system",
103
104
  plan_instance_orphaned: "system",
104
105
  plan_start_failed: "system",
105
- // the directive and resolve parsers' thrown errors (A6 of the inventory)
106
- directive_agent: "request",
107
- directive_effort: "request",
108
- directive_budget: "request",
109
- directive_severity: "request",
110
- directive_renewals: "request",
111
- directive_verbosity: "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)
112
109
  provider_unknown: "request",
113
110
  // the model card's refusal (record 0052, model-proxy item 11): a control the
114
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,11 +397,19 @@ 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
403
407
  * showing progress, and the grant renewed: the next segment opens (decision 0046). */
404
- | "continued";
408
+ | "continued"
409
+ /** The unit idles instead of ending (record 0051): emitted once the wake
410
+ * lands (that plan's fifth unit); in the vocabulary now so rows written
411
+ * then read beside today's — the old kinds stay for rows already written. */
412
+ | "idle";
405
413
 
406
414
  /**
407
415
  * One event in a run's stream. `seq` is stamped by `RunRegistry.publish` — a
@@ -536,6 +544,11 @@ export type RunEvent =
536
544
  callId?: string;
537
545
  spanId?: string;
538
546
  logIndex?: number;
547
+ /** The bound the call itself declared, in ms (a bash `timeout`, clamped
548
+ * by nothing here — pi runs an unbounded call until the loop's end).
549
+ * The stall signal (docs/reference/specs/live-view.md item 32) marks a
550
+ * call past it; absent when the call declared none. */
551
+ boundMs?: number;
539
552
  seq?: number;
540
553
  at?: number;
541
554
  }
@@ -671,8 +684,12 @@ export type RunEvent =
671
684
  * and — for a repo run — the repo, ref, PR number and PR head as resolved
672
685
  * BEFORE the first model turn (`RepoContext`). Published by the dispatcher
673
686
  * 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. */
687
+ * block with linked `owner/repo · ref · #PR · sha`. A HOSTED ship parent
688
+ * (record 0060) carries a second one at the hand-off naming its runner
689
+ * instance (`instanceId`): `run_meta` may repeat on such a run, and every
690
+ * reader of a run's instance id resolves it from the LAST `run_meta`
691
+ * carrying one. Additive: every consumer that only knows the other types
692
+ * keeps working. */
676
693
  | {
677
694
  type: "run_meta";
678
695
  agent: string;
@@ -865,6 +882,23 @@ export type RunEvent =
865
882
  * `coordinator_tag` is — so the thread's owner rule can find the instance
866
883
  * from the page's ship run. Additive: unknown → ignored. */
867
884
  | { type: "ship_handoff"; instanceId: string; seq?: number; at?: number }
885
+ /** A coordinator child interrupted under a deploy roll (docs/reference/specs/run-history.md
886
+ * item 47a): the run closes `interrupted` for a restart from its request —
887
+ * a workspace lost across a bot roll, the resident's container replaced
888
+ * with the relaunch refused — and the record says so as a typed fact
889
+ * beside the `resumed`/`sandbox_restarted` notes, `reason` the refusal or
890
+ * note in one line. Published straight to the registry by the reattach
891
+ * path and the run loop's interruption; the parent's wait settles on its
892
+ * Workflow twin (`child-interrupted-<runId>`) beside `run-finished-<runId>`
893
+ * and confirms by `read-record`. Additive: unknown → ignored. */
894
+ | { type: "child_interrupted"; parentInstanceId: string; reason: string; seq?: number; at?: number }
895
+ /** A coordinator child resumed across a deploy roll (run-history item 47a):
896
+ * the same run re-attached its workspace and carries on under its record,
897
+ * tag and budget. Published straight to the registry by the reattach path;
898
+ * the Workflow twin (`child-resumed-<runId>`) tells the parent's wait to
899
+ * keep waiting rather than walk out the chunk asking. Additive: unknown →
900
+ * ignored. */
901
+ | { type: "child_resumed"; parentInstanceId: string; summary: string; seq?: number; at?: number }
868
902
  /** The review post-step's outcome when the verdict landed
869
903
  * (docs/reference/specs/agent-review.md item 18): the pull request it was
870
904
  * posted to, the head it was pinned to (the carried head after a rebase,
@@ -905,6 +939,25 @@ export type RunEvent =
905
939
  seq?: number;
906
940
  at?: number;
907
941
  }
942
+ /** A hosted ship parent's unit fact (record 0060; docs/reference/specs/agent-ship.md
943
+ * item 17): the runner's routes write it to the parent run through the
944
+ * coordinator's `hostPublish` — `unit-start` publishes state `started` with
945
+ * the unit's thread key and lead, `round` the round's outcome as the unit's
946
+ * state, `unit-end` the ending's kind with its report — carrying the pull
947
+ * request number when the unit's row knows one. The run page draws it as a
948
+ * step whose detail is the report; the friction analyzer counts it as a
949
+ * side fact, never activity. Additive: unknown → ignored. */
950
+ | {
951
+ type: "ship_unit";
952
+ unit: string;
953
+ state: string;
954
+ threadKey?: string;
955
+ lead?: string;
956
+ report?: string;
957
+ pr?: number;
958
+ seq?: number;
959
+ at?: number;
960
+ }
908
961
  /** The request router's decision (docs/reference/specs/routing-and-config.md
909
962
  * item 21): the preset a plain message was routed to, the one-line reason
910
963
  * the router gave (redacted, capped — the same text the card's `routed:`
@@ -954,6 +1007,32 @@ export type RunEvent =
954
1007
  seq?: number;
955
1008
  at?: number;
956
1009
  }
1010
+ /** The operator's decision beside the routed request ([record 0057](../../docs/decisions/0057-the-operator-is-the-one-door-a-model-binds-every-chat-input-and-deterministic-code-authorizes-fences-and-executes.md);
1011
+ * the one-door plan's operator unit; run-history item 60): one per admitted chat
1012
+ * event under `routing.operator: shadow` or `on`, published beside the
1013
+ * `route` event. The decision is binds, a question or a refusal; a bind's
1014
+ * `line` is redacted and cut like the receipt (`ROUTE_RECEIPT_CAP`), never
1015
+ * the message text; `intake` carries the intake gate's verdict when the
1016
+ * gate is present; `latencyMs` and `outputTokens` feed the replay's median
1017
+ * rows. Under `shadow` nothing runs from it. Additive: unknown → ignored. */
1018
+ | {
1019
+ type: "operator";
1020
+ mode: "shadow" | "on";
1021
+ outcome: "binds" | "question" | "refusal";
1022
+ reason: string;
1023
+ binds?: ReadonlyArray<{ line: string; reason: string }>;
1024
+ question?: string;
1025
+ /** A question's proposed line, redacted and cut like the receipt — what
1026
+ * the next turn's "yes" binds (`bindFromAnswer`). */
1027
+ proposal?: string;
1028
+ refusalCause?: string;
1029
+ refusalText?: string;
1030
+ intake?: { verdict: string; reason: string };
1031
+ latencyMs?: number;
1032
+ outputTokens?: number;
1033
+ seq?: number;
1034
+ at?: number;
1035
+ }
957
1036
  /** A refusal the door made ([record 0054](../../docs/decisions/0054-a-refusal-the-person-caused-is-one-question-with-a-best-guess.md),
958
1037
  * as amended: every refusal is a run record; run-history.md item 2): the
959
1038
  * code, its one cause, and the sentence the person read — redacted and
@@ -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,9 +425,13 @@ 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" ||
434
+ ev.type === "operator" ||
429
435
  ev.type === "refusal" ||
430
436
  ev.type === "reference" ||
431
437
  ev.type === "lease" ||
@@ -121,6 +121,17 @@ export interface LiveRunMeta {
121
121
  * the checklist, the pushed branch, the review head, the infra counters). */
122
122
  export type RunState = Record<string, unknown>;
123
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
+
124
135
  export interface LiveRunRow {
125
136
  runId: string;
126
137
  threadKey: string;