modelrig 0.1.0 → 0.3.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 (310) hide show
  1. package/CHANGELOG.md +158 -0
  2. package/README.md +53 -11
  3. package/data/PROVENANCE.md +19 -0
  4. package/data/pricing-manual.json +22 -0
  5. package/data/pricing-map.json +46362 -0
  6. package/data/pricing-overlay.json +419 -0
  7. package/dist/adapters/anthropic.d.ts +50 -6
  8. package/dist/adapters/anthropic.d.ts.map +1 -1
  9. package/dist/adapters/anthropic.js +123 -29
  10. package/dist/adapters/build-adapters.d.ts +11 -0
  11. package/dist/adapters/build-adapters.d.ts.map +1 -0
  12. package/dist/adapters/build-adapters.js +45 -0
  13. package/dist/adapters/deepseek.d.ts +6 -0
  14. package/dist/adapters/deepseek.d.ts.map +1 -1
  15. package/dist/adapters/deepseek.js +54 -1
  16. package/dist/adapters/gemini.d.ts +10 -0
  17. package/dist/adapters/gemini.d.ts.map +1 -1
  18. package/dist/adapters/gemini.js +78 -7
  19. package/dist/adapters/grok.d.ts +9 -0
  20. package/dist/adapters/grok.d.ts.map +1 -1
  21. package/dist/adapters/grok.js +67 -3
  22. package/dist/adapters/openai-compat.d.ts +6 -0
  23. package/dist/adapters/openai-compat.d.ts.map +1 -1
  24. package/dist/adapters/openai-compat.js +58 -1
  25. package/dist/adapters/openai-stream.d.ts +58 -0
  26. package/dist/adapters/openai-stream.d.ts.map +1 -0
  27. package/dist/adapters/openai-stream.js +120 -0
  28. package/dist/adapters/openai.d.ts +6 -0
  29. package/dist/adapters/openai.d.ts.map +1 -1
  30. package/dist/adapters/openai.js +60 -1
  31. package/dist/adapters/sampling.d.ts +28 -0
  32. package/dist/adapters/sampling.d.ts.map +1 -0
  33. package/dist/adapters/sampling.js +34 -0
  34. package/dist/adapters/tool-mapping.d.ts +92 -0
  35. package/dist/adapters/tool-mapping.d.ts.map +1 -0
  36. package/dist/adapters/tool-mapping.js +143 -0
  37. package/dist/artifacts/artifact.d.ts +137 -0
  38. package/dist/artifacts/artifact.d.ts.map +1 -0
  39. package/dist/artifacts/artifact.js +377 -0
  40. package/dist/artifacts/content-uploader.d.ts +87 -0
  41. package/dist/artifacts/content-uploader.d.ts.map +1 -0
  42. package/dist/artifacts/content-uploader.js +124 -0
  43. package/dist/artifacts/index.d.ts +63 -0
  44. package/dist/artifacts/index.d.ts.map +1 -0
  45. package/dist/artifacts/index.js +80 -0
  46. package/dist/artifacts/run-context.d.ts +129 -0
  47. package/dist/artifacts/run-context.d.ts.map +1 -0
  48. package/dist/artifacts/run-context.js +208 -0
  49. package/dist/artifacts/scrub.d.ts +60 -0
  50. package/dist/artifacts/scrub.d.ts.map +1 -0
  51. package/dist/artifacts/scrub.js +139 -0
  52. package/dist/artifacts/step.d.ts +26 -0
  53. package/dist/artifacts/step.d.ts.map +1 -0
  54. package/dist/artifacts/step.js +37 -0
  55. package/dist/artifacts/taint.d.ts +63 -0
  56. package/dist/artifacts/taint.d.ts.map +1 -0
  57. package/dist/artifacts/taint.js +76 -0
  58. package/dist/bakeoff/cache-sim.d.ts +99 -0
  59. package/dist/bakeoff/cache-sim.d.ts.map +1 -0
  60. package/dist/bakeoff/cache-sim.js +126 -0
  61. package/dist/bakeoff/eval-cases.d.ts +96 -0
  62. package/dist/bakeoff/eval-cases.d.ts.map +1 -0
  63. package/dist/bakeoff/eval-cases.js +105 -0
  64. package/dist/bakeoff/report.d.ts +41 -0
  65. package/dist/bakeoff/report.d.ts.map +1 -0
  66. package/dist/bakeoff/report.js +142 -0
  67. package/dist/bakeoff/samples.d.ts +15 -0
  68. package/dist/bakeoff/samples.d.ts.map +1 -1
  69. package/dist/bakeoff/samples.js +20 -0
  70. package/dist/cli.d.ts +1 -3
  71. package/dist/cli.d.ts.map +1 -1
  72. package/dist/cli.js +134 -116
  73. package/dist/config.d.ts +100 -0
  74. package/dist/config.d.ts.map +1 -1
  75. package/dist/config.js +104 -0
  76. package/dist/deprecations.d.ts +32 -0
  77. package/dist/deprecations.d.ts.map +1 -0
  78. package/dist/deprecations.js +59 -0
  79. package/dist/gateway/http-protocol.d.ts +175 -0
  80. package/dist/gateway/http-protocol.d.ts.map +1 -0
  81. package/dist/gateway/http-protocol.js +105 -0
  82. package/dist/index.d.ts +106 -19
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +266 -138
  85. package/dist/init/finder.d.ts +15 -0
  86. package/dist/init/finder.d.ts.map +1 -1
  87. package/dist/init/finder.js +46 -0
  88. package/dist/init/init-cli.d.ts +10 -0
  89. package/dist/init/init-cli.d.ts.map +1 -0
  90. package/dist/init/init-cli.js +86 -0
  91. package/dist/init/observe-cli.d.ts +19 -0
  92. package/dist/init/observe-cli.d.ts.map +1 -0
  93. package/dist/init/observe-cli.js +73 -0
  94. package/dist/init/observe.d.ts +81 -0
  95. package/dist/init/observe.d.ts.map +1 -0
  96. package/dist/init/observe.js +245 -0
  97. package/dist/registry/build.d.ts +94 -1
  98. package/dist/registry/build.d.ts.map +1 -1
  99. package/dist/registry/build.js +125 -4
  100. package/dist/registry/cache-policy.d.ts +94 -0
  101. package/dist/registry/cache-policy.d.ts.map +1 -0
  102. package/dist/registry/cache-policy.js +238 -0
  103. package/dist/registry/call-notes.d.ts.map +1 -1
  104. package/dist/registry/call-notes.js +40 -2
  105. package/dist/registry/effective-rate.d.ts +151 -0
  106. package/dist/registry/effective-rate.d.ts.map +1 -0
  107. package/dist/registry/effective-rate.js +156 -0
  108. package/dist/registry/facts.d.ts.map +1 -1
  109. package/dist/registry/facts.js +16 -0
  110. package/dist/registry/parity.d.ts +26 -0
  111. package/dist/registry/parity.d.ts.map +1 -1
  112. package/dist/registry/parity.js +43 -2
  113. package/dist/registry/pricing-shape.d.ts +113 -0
  114. package/dist/registry/pricing-shape.d.ts.map +1 -0
  115. package/dist/registry/pricing-shape.js +123 -0
  116. package/dist/registry/pricing.d.ts +25 -1
  117. package/dist/registry/pricing.d.ts.map +1 -1
  118. package/dist/registry/pricing.js +64 -6
  119. package/dist/rigindex/bootstrap.d.ts +36 -0
  120. package/dist/rigindex/bootstrap.d.ts.map +1 -0
  121. package/dist/rigindex/bootstrap.js +97 -0
  122. package/dist/rigindex/fit.d.ts +24 -0
  123. package/dist/rigindex/fit.d.ts.map +1 -0
  124. package/dist/rigindex/fit.js +76 -0
  125. package/dist/rigindex/gates.d.ts +52 -0
  126. package/dist/rigindex/gates.d.ts.map +1 -0
  127. package/dist/rigindex/gates.js +109 -0
  128. package/dist/rigindex/graph.d.ts +31 -0
  129. package/dist/rigindex/graph.d.ts.map +1 -0
  130. package/dist/rigindex/graph.js +120 -0
  131. package/dist/rigindex/index.d.ts +23 -0
  132. package/dist/rigindex/index.d.ts.map +1 -0
  133. package/dist/rigindex/index.js +112 -0
  134. package/dist/rigindex/types.d.ts +76 -0
  135. package/dist/rigindex/types.d.ts.map +1 -0
  136. package/dist/rigindex/types.js +8 -0
  137. package/dist/routes/cache-lint.d.ts +41 -0
  138. package/dist/routes/cache-lint.d.ts.map +1 -0
  139. package/dist/routes/cache-lint.js +81 -0
  140. package/dist/routes/loader.d.ts +48 -0
  141. package/dist/routes/loader.d.ts.map +1 -1
  142. package/dist/routes/loader.js +167 -0
  143. package/dist/routes/resolved-bundle.d.ts +79 -0
  144. package/dist/routes/resolved-bundle.d.ts.map +1 -0
  145. package/dist/routes/resolved-bundle.js +148 -0
  146. package/dist/routes/sampling-config.d.ts +32 -0
  147. package/dist/routes/sampling-config.d.ts.map +1 -0
  148. package/dist/routes/sampling-config.js +91 -0
  149. package/dist/run/ajv.d.ts +22 -0
  150. package/dist/run/ajv.d.ts.map +1 -0
  151. package/dist/run/ajv.js +32 -0
  152. package/dist/run/cache-handle.d.ts +58 -0
  153. package/dist/run/cache-handle.d.ts.map +1 -0
  154. package/dist/run/cache-handle.js +54 -0
  155. package/dist/run/cache-stamp.d.ts +65 -0
  156. package/dist/run/cache-stamp.d.ts.map +1 -0
  157. package/dist/run/cache-stamp.js +0 -0
  158. package/dist/run/capture-write.d.ts +33 -0
  159. package/dist/run/capture-write.d.ts.map +1 -0
  160. package/dist/run/capture-write.js +55 -0
  161. package/dist/run/cost.d.ts.map +1 -1
  162. package/dist/run/cost.js +1 -1
  163. package/dist/run/dispatch.d.ts +99 -0
  164. package/dist/run/dispatch.d.ts.map +1 -0
  165. package/dist/run/dispatch.js +191 -0
  166. package/dist/run/raw.d.ts +130 -0
  167. package/dist/run/raw.d.ts.map +1 -0
  168. package/dist/run/raw.js +363 -0
  169. package/dist/run/record-attempt.d.ts +40 -0
  170. package/dist/run/record-attempt.d.ts.map +1 -0
  171. package/dist/run/record-attempt.js +99 -0
  172. package/dist/run/resolve.d.ts +75 -2
  173. package/dist/run/resolve.d.ts.map +1 -1
  174. package/dist/run/resolve.js +126 -4
  175. package/dist/run/run.d.ts +14 -0
  176. package/dist/run/run.d.ts.map +1 -1
  177. package/dist/run/run.js +0 -0
  178. package/dist/run/stream.d.ts +49 -0
  179. package/dist/run/stream.d.ts.map +1 -0
  180. package/dist/run/stream.js +87 -0
  181. package/dist/run/validate.d.ts.map +1 -1
  182. package/dist/run/validate.js +6 -7
  183. package/dist/savings/cache-ledger.d.ts +108 -0
  184. package/dist/savings/cache-ledger.d.ts.map +1 -0
  185. package/dist/savings/cache-ledger.js +174 -0
  186. package/dist/savings/cache-reconcile.d.ts +64 -0
  187. package/dist/savings/cache-reconcile.d.ts.map +1 -0
  188. package/dist/savings/cache-reconcile.js +59 -0
  189. package/dist/savings/cache-stats.d.ts +63 -0
  190. package/dist/savings/cache-stats.d.ts.map +1 -0
  191. package/dist/savings/cache-stats.js +108 -0
  192. package/dist/savings/emit.d.ts +13 -0
  193. package/dist/savings/emit.d.ts.map +1 -1
  194. package/dist/savings/emit.js +25 -0
  195. package/dist/savings/swap-execute.d.ts +14 -0
  196. package/dist/savings/swap-execute.d.ts.map +1 -1
  197. package/dist/savings/swap-execute.js +44 -1
  198. package/dist/savings/swaps-client.d.ts +14 -0
  199. package/dist/savings/swaps-client.d.ts.map +1 -1
  200. package/dist/savings/swaps-client.js +16 -2
  201. package/dist/settings/api-fetcher.d.ts +39 -0
  202. package/dist/settings/api-fetcher.d.ts.map +1 -0
  203. package/dist/settings/api-fetcher.js +92 -0
  204. package/dist/settings/index.d.ts +10 -0
  205. package/dist/settings/index.d.ts.map +1 -0
  206. package/dist/settings/index.js +15 -0
  207. package/dist/settings/task-settings.d.ts +91 -0
  208. package/dist/settings/task-settings.d.ts.map +1 -0
  209. package/dist/settings/task-settings.js +116 -0
  210. package/dist/status.d.ts +77 -0
  211. package/dist/status.d.ts.map +1 -0
  212. package/dist/status.js +113 -0
  213. package/dist/telemetry/artifact-store.d.ts +48 -0
  214. package/dist/telemetry/artifact-store.d.ts.map +1 -0
  215. package/dist/telemetry/artifact-store.js +189 -0
  216. package/dist/telemetry/cache-miss-streak.d.ts +79 -0
  217. package/dist/telemetry/cache-miss-streak.d.ts.map +1 -0
  218. package/dist/telemetry/cache-miss-streak.js +99 -0
  219. package/dist/telemetry/capture.d.ts +3 -0
  220. package/dist/telemetry/capture.d.ts.map +1 -1
  221. package/dist/telemetry/capture.js +15 -8
  222. package/dist/telemetry/exporter-setup.d.ts +28 -0
  223. package/dist/telemetry/exporter-setup.d.ts.map +1 -0
  224. package/dist/telemetry/exporter-setup.js +34 -0
  225. package/dist/telemetry/exporter.d.ts +75 -0
  226. package/dist/telemetry/exporter.d.ts.map +1 -1
  227. package/dist/telemetry/exporter.js +324 -47
  228. package/dist/telemetry/health.d.ts +76 -0
  229. package/dist/telemetry/health.d.ts.map +1 -0
  230. package/dist/telemetry/health.js +92 -0
  231. package/dist/telemetry/ingest-protocol.d.ts +152 -3
  232. package/dist/telemetry/ingest-protocol.d.ts.map +1 -1
  233. package/dist/telemetry/ingest-protocol.js +204 -4
  234. package/dist/telemetry/load-error-report.d.ts +24 -0
  235. package/dist/telemetry/load-error-report.d.ts.map +1 -0
  236. package/dist/telemetry/load-error-report.js +50 -0
  237. package/dist/telemetry/otel-map.d.ts +121 -0
  238. package/dist/telemetry/otel-map.d.ts.map +1 -0
  239. package/dist/telemetry/otel-map.js +224 -0
  240. package/dist/telemetry/otel-normalize.d.ts +104 -0
  241. package/dist/telemetry/otel-normalize.d.ts.map +1 -0
  242. package/dist/telemetry/otel-normalize.js +530 -0
  243. package/dist/telemetry/otlp-emitter.d.ts +52 -0
  244. package/dist/telemetry/otlp-emitter.d.ts.map +1 -0
  245. package/dist/telemetry/otlp-emitter.js +144 -0
  246. package/dist/telemetry/otlp-proto.d.ts +29 -0
  247. package/dist/telemetry/otlp-proto.d.ts.map +1 -0
  248. package/dist/telemetry/otlp-proto.js +266 -0
  249. package/dist/telemetry/sinks.d.ts +37 -1
  250. package/dist/telemetry/sinks.d.ts.map +1 -1
  251. package/dist/telemetry/sinks.js +121 -30
  252. package/dist/telemetry/store.d.ts +28 -1
  253. package/dist/telemetry/store.d.ts.map +1 -1
  254. package/dist/telemetry/store.js +218 -3
  255. package/dist/tenancy/api-key-format.d.ts +15 -5
  256. package/dist/tenancy/api-key-format.d.ts.map +1 -1
  257. package/dist/tenancy/api-key-format.js +24 -5
  258. package/dist/types/config-teaching.d.ts +62 -0
  259. package/dist/types/config-teaching.d.ts.map +1 -0
  260. package/dist/types/config-teaching.js +183 -0
  261. package/dist/types/provider.d.ts +147 -1
  262. package/dist/types/provider.d.ts.map +1 -1
  263. package/dist/types/provider.js +21 -1
  264. package/dist/types/route.d.ts +79 -1
  265. package/dist/types/route.d.ts.map +1 -1
  266. package/dist/types/telemetry.d.ts +117 -0
  267. package/dist/types/telemetry.d.ts.map +1 -1
  268. package/dist/validate-cli.d.ts +12 -0
  269. package/dist/validate-cli.d.ts.map +1 -0
  270. package/dist/validate-cli.js +57 -0
  271. package/dist/verify/capture.d.ts +66 -0
  272. package/dist/verify/capture.d.ts.map +1 -0
  273. package/dist/verify/capture.js +180 -0
  274. package/dist/verify/cli-run.d.ts +23 -0
  275. package/dist/verify/cli-run.d.ts.map +1 -0
  276. package/dist/verify/cli-run.js +57 -0
  277. package/dist/verify/diff.d.ts +48 -0
  278. package/dist/verify/diff.d.ts.map +1 -0
  279. package/dist/verify/diff.js +157 -0
  280. package/dist/verify/index.d.ts +17 -0
  281. package/dist/verify/index.d.ts.map +1 -0
  282. package/dist/verify/index.js +24 -0
  283. package/dist/verify/report.d.ts +14 -0
  284. package/dist/verify/report.d.ts.map +1 -0
  285. package/dist/verify/report.js +88 -0
  286. package/dist/verify/types.d.ts +101 -0
  287. package/dist/verify/types.d.ts.map +1 -0
  288. package/dist/verify/types.js +17 -0
  289. package/dist/verify/verify.d.ts +29 -0
  290. package/dist/verify/verify.d.ts.map +1 -0
  291. package/dist/verify/verify.js +148 -0
  292. package/dist/watch/evaluate.d.ts +162 -3
  293. package/dist/watch/evaluate.d.ts.map +1 -1
  294. package/dist/watch/evaluate.js +61 -0
  295. package/dist/watch/monitor.d.ts +116 -0
  296. package/dist/watch/monitor.d.ts.map +1 -0
  297. package/dist/watch/monitor.js +199 -0
  298. package/dist/watch/proposals.d.ts +20 -4
  299. package/dist/watch/proposals.d.ts.map +1 -1
  300. package/dist/watch/proposals.js +31 -3
  301. package/dist/watch/run.d.ts +33 -0
  302. package/dist/watch/run.d.ts.map +1 -1
  303. package/dist/watch/run.js +39 -0
  304. package/package.json +7 -1
  305. package/registry/LICENSE +20 -0
  306. package/registry/leaderboard.json +3639 -0
  307. package/registry/registry.json +7263 -0
  308. package/dist/telemetry/supabase-exporter.d.ts +0 -64
  309. package/dist/telemetry/supabase-exporter.d.ts.map +0 -1
  310. package/dist/telemetry/supabase-exporter.js +0 -211
@@ -21,10 +21,39 @@
21
21
  * cost with no payer.
22
22
  */
23
23
  Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.SinkSendError = void 0;
24
25
  exports.createDirectSink = createDirectSink;
25
26
  exports.createApiSink = createApiSink;
27
+ exports.buildTelemetrySink = buildTelemetrySink;
26
28
  const node_zlib_1 = require("node:zlib");
27
29
  const ingest_protocol_1 = require("./ingest-protocol");
30
+ const constants_1 = require("../tenancy/constants");
31
+ /**
32
+ * A sink send failed — the typed error both transports throw (E2 hardening
33
+ * Phase 2, spec §2.3.1). It carries the HTTP status the sink observed (absent
34
+ * for a network-level throw) and a `retriable` verdict the exporter branches on:
35
+ *
36
+ * retriable network throw (no status) · any 5xx · a 429 (overload)
37
+ * → the same batch may land next time; retry forever, never drop.
38
+ * NOT every other 4xx, including the server's deterministic 422
39
+ * → the same bytes will be rejected identically forever; the
40
+ * exporter poison-skips it after a bounded self-heal window.
41
+ *
42
+ * The message is leak-safe BY CONSTRUCTION — status + table only, NEVER the
43
+ * PostgREST/response body. An exporter's warnings are the likeliest place for a
44
+ * secret or a foreign row to reach someone else's log aggregator (C2's leak
45
+ * gate), so the body a failed write returns is logged nowhere.
46
+ */
47
+ class SinkSendError extends Error {
48
+ constructor(message, status) {
49
+ super(message);
50
+ this.name = "SinkSendError";
51
+ if (status !== undefined)
52
+ this.status = status;
53
+ this.retriable = status === undefined || status >= 500 || status === 429;
54
+ }
55
+ }
56
+ exports.SinkSendError = SinkSendError;
28
57
  function createDirectSink(config) {
29
58
  const base = config.supabaseUrl.replace(/\/$/, "");
30
59
  const fetchFn = config.fetchFn ?? fetch;
@@ -32,23 +61,35 @@ function createDirectSink(config) {
32
61
  mode: "direct",
33
62
  async send(batch) {
34
63
  for (const write of (0, ingest_protocol_1.stampBatch)(batch, config.tenancy)) {
35
- const response = await fetchFn(
36
- // S5: the upsert names a real unique constraint. A single-argument
37
- // upsert targets the primary key and 23505s later.
38
- `${base}/rest/v1/${write.table}?on_conflict=${encodeURIComponent(write.onConflict)}`, {
39
- method: "POST",
40
- headers: {
41
- "content-type": "application/json",
42
- apikey: config.serviceRoleKey,
43
- authorization: `Bearer ${config.serviceRoleKey}`,
44
- prefer: write.merge
45
- ? "resolution=merge-duplicates,return=minimal"
46
- : "resolution=ignore-duplicates,return=minimal",
47
- },
48
- body: JSON.stringify(write.rows),
49
- });
64
+ let response;
65
+ try {
66
+ response = await fetchFn(
67
+ // S5: the upsert names a real unique constraint. A single-argument
68
+ // upsert targets the primary key and 23505s later.
69
+ `${base}/rest/v1/${write.table}?on_conflict=${encodeURIComponent(write.onConflict)}`, {
70
+ method: "POST",
71
+ headers: {
72
+ "content-type": "application/json",
73
+ apikey: config.serviceRoleKey,
74
+ authorization: `Bearer ${config.serviceRoleKey}`,
75
+ prefer: write.merge
76
+ ? "resolution=merge-duplicates,return=minimal"
77
+ : "resolution=ignore-duplicates,return=minimal",
78
+ },
79
+ body: JSON.stringify(write.rows),
80
+ });
81
+ }
82
+ catch {
83
+ // Network-level throw — no status, so TRANSIENT (the exporter retries
84
+ // forever). The raw error can carry the control-plane host, so it is
85
+ // NOT echoed (C2); the table + "network error" is the whole message.
86
+ throw new SinkSendError(`sink POST ${write.table} → network error`, undefined);
87
+ }
50
88
  if (!response.ok) {
51
- throw new Error(`sink POST ${write.table} → ${response.status} ${await response.text().catch(() => "")}`);
89
+ // Status + table only — never the PostgREST body (it carries the
90
+ // control-plane host and, on some failures, header fragments; C2).
91
+ // The status is what the exporter classifies on (spec §2.3.1).
92
+ throw new SinkSendError(`sink POST ${write.table} → ${response.status}`, response.status);
52
93
  }
53
94
  }
54
95
  },
@@ -62,22 +103,37 @@ function createApiSink(config) {
62
103
  return {
63
104
  mode: "api",
64
105
  async send(batch) {
65
- const json = JSON.stringify({ ...batch, v: ingest_protocol_1.INGEST_PROTOCOL_VERSION });
106
+ // Trust the version the EXPORTER set (spec §1.3): it announces v2 only
107
+ // when the artifact namespace is active, v1 otherwise. Overriding it here
108
+ // would force every batch to the max version and break the v1-byte-
109
+ // identical guarantee (and 422 v1 rigs against a strict old server).
110
+ const json = JSON.stringify(batch);
66
111
  const compress = Buffer.byteLength(json) >= GZIP_MIN_BYTES;
67
- const response = await fetchFn(url, {
68
- method: "POST",
69
- headers: {
70
- "content-type": "application/json",
71
- authorization: `Bearer ${config.apiKey}`,
72
- ...(compress ? { "content-encoding": "gzip" } : {}),
73
- },
74
- body: compress ? (0, node_zlib_1.gzipSync)(json) : json,
75
- });
112
+ let response;
113
+ try {
114
+ response = await fetchFn(url, {
115
+ method: "POST",
116
+ headers: {
117
+ "content-type": "application/json",
118
+ authorization: `Bearer ${config.apiKey}`,
119
+ ...(compress ? { "content-encoding": "gzip" } : {}),
120
+ },
121
+ body: compress ? (0, node_zlib_1.gzipSync)(json) : json,
122
+ });
123
+ }
124
+ catch {
125
+ // Network-level throw — no status ⇒ TRANSIENT (retry forever). The raw
126
+ // error can carry the endpoint host, so it is not echoed (C2).
127
+ throw new SinkSendError("ingest POST → network error", undefined);
128
+ }
76
129
  if (!response.ok) {
77
- // The key itself never appears in the message — C2's leak gate covers
78
- // failure text, and an exporter's warnings are the likeliest place for
79
- // a secret to reach someone else's log aggregator.
80
- throw new Error(`ingest POST → ${response.status} ${await response.text().catch(() => "")}`);
130
+ // Status only — never the response body. The key never appears in the
131
+ // message (C2's leak gate covers failure text), and an exporter's
132
+ // warnings are the likeliest place for a secret to reach someone else's
133
+ // log aggregator. The status is the exporter's transient-vs-poison
134
+ // signal: the server sends 422 for a deterministic control-plane
135
+ // rejection, 503 for a transient one (spec §2.2/§2.3.1).
136
+ throw new SinkSendError(`ingest POST → ${response.status}`, response.status);
81
137
  }
82
138
  if (config.onOutcome !== undefined) {
83
139
  const outcome = (await response.json().catch(() => null));
@@ -87,3 +143,38 @@ function createApiSink(config) {
87
143
  },
88
144
  };
89
145
  }
146
+ /**
147
+ * Build the telemetry transport for the configured export mode. One place, so
148
+ * the run-loop exporter and the load-error reporter build it the same way —
149
+ * telemetry-ingestion-boundary.md §4's "one loop, two transports". (Lived in
150
+ * index.ts until Phase D; moved here — its natural home — to keep that file
151
+ * under the LOC cap.)
152
+ */
153
+ function buildTelemetrySink(exportConfig, sourceId) {
154
+ return exportConfig.mode === "api"
155
+ ? createApiSink({
156
+ ingestUrl: exportConfig.ingestUrl,
157
+ apiKey: exportConfig.apiKey,
158
+ // A batch the server partly refused comes back 200. Without this, a
159
+ // customer whose rows are being dropped would find out by noticing a
160
+ // gap in a chart weeks later.
161
+ onOutcome: (outcome) => {
162
+ const dropped = Object.values(outcome.dropped).reduce((a, b) => a + b, 0);
163
+ if (dropped === 0)
164
+ return;
165
+ const reasons = Object.entries(outcome.reasons)
166
+ .map(([reason, count]) => `${reason}×${count}`)
167
+ .join(", ");
168
+ console.warn(`[modelrig exporter] ingest dropped ${dropped} row(s): ${reasons}`);
169
+ },
170
+ })
171
+ : createDirectSink({
172
+ supabaseUrl: exportConfig.supabaseUrl,
173
+ serviceRoleKey: exportConfig.serviceRoleKey,
174
+ tenancy: {
175
+ org_id: exportConfig.orgId ?? constants_1.HOUSE_ORG_ID,
176
+ project_id: exportConfig.projectId ?? null,
177
+ source_id: sourceId,
178
+ },
179
+ });
180
+ }
@@ -9,7 +9,8 @@
9
9
  * separate handle. ExportableTelemetryStore MUST NOT gain capture accessors;
10
10
  * the export-isolation tests are the tripwire.
11
11
  */
12
- import type { EnvelopeRow, FeedbackRow, TelemetryRow, TelemetryStore } from "../types/telemetry";
12
+ import type { EnvelopeRow, FeedbackRow, RunRow, StepRow, TelemetryRow, TelemetryStore } from "../types/telemetry";
13
+ import { type EndpointHealth } from "./health";
13
14
  /**
14
15
  * Superset of the TelemetryStore contract with the enumeration hooks the
15
16
  * Supabase exporter needs (rowid-watermarked reads + local export state).
@@ -27,12 +28,38 @@ export interface ExportableTelemetryStore extends TelemetryStore {
27
28
  rowid: number;
28
29
  row: FeedbackRow;
29
30
  }>;
31
+ /** Record a run (E2 chunk A) — run.start/run.end. Upsert: run.end overwrites
32
+ * the running projection with its terminal status/ended_at. */
33
+ recordRun(row: RunRow): void;
34
+ /** Record one step attempt (E2 chunk A) — a fact, insert-or-ignore on the PK. */
35
+ recordStep(row: StepRow): void;
36
+ /** Runs past the watermark, oldest first (E2 exporter stream). */
37
+ readRunsAfter(afterRowid: number, limit: number): Array<{
38
+ rowid: number;
39
+ row: RunRow;
40
+ }>;
41
+ /** Steps past the watermark, oldest first (E2 exporter stream). */
42
+ readStepsAfter(afterRowid: number, limit: number): Array<{
43
+ rowid: number;
44
+ row: StepRow;
45
+ }>;
46
+ /** The ground-truth inference id for a (run, step) — the LATEST attempt with a
47
+ * non-null inference_id, or null (E4 chunk A: bake-off rehydration resolves an
48
+ * eval case's source step to the inference that keys its captured input). */
49
+ getStepInferenceId(runId: string, stepKey: string): string | null;
30
50
  /** Newest-first local feedback read (console/dev inspection). */
31
51
  listFeedback(limit: number): FeedbackRow[];
32
52
  listEnvelopes(): EnvelopeRow[];
33
53
  /** Local exporter watermark (per source instance), 0 when unset. */
34
54
  getExportWatermark(sourceId: string): number;
35
55
  setExportWatermark(sourceId: string, lastRowid: number): void;
56
+ /**
57
+ * Observed-health aggregate (G-4) — per (provider, model) over the last
58
+ * `windowMinutes`: p50/p90/p99 latency + TTFB and recent error-rate, gated to
59
+ * `null` below `minN`. Advisory operational health for routing + the console;
60
+ * NEVER an input to RigIndex rank (spec §4.4).
61
+ */
62
+ readEndpointHealth(windowMinutes: number, minN: number): EndpointHealth[];
36
63
  }
37
64
  export declare function openTelemetryStore(dbPath: string): ExportableTelemetryStore;
38
65
  //# sourceMappingURL=store.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/telemetry/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAGjG;;;;;GAKG;AACH,MAAM,WAAW,wBAAyB,SAAQ,cAAc;IAC9D,qEAAqE;IACrE,mBAAmB,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,YAAY,CAAA;KAAE,CAAC,CAAC;IACpG,uEAAuE;IACvE,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,WAAW,CAAA;KAAE,CAAC,CAAC;IACjG,iEAAiE;IACjE,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,EAAE,CAAC;IAC3C,aAAa,IAAI,WAAW,EAAE,CAAC;IAC/B,oEAAoE;IACpE,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAC7C,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/D;AAyKD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,wBAAwB,CAkN3E"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/telemetry/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH,OAAO,KAAK,EACV,WAAW,EACX,WAAW,EACX,MAAM,EACN,OAAO,EACP,YAAY,EACZ,cAAc,EACf,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAA2B,KAAK,cAAc,EAAuB,MAAM,UAAU,CAAC;AAE7F;;;;;GAKG;AACH,MAAM,WAAW,wBAAyB,SAAQ,cAAc;IAC9D,qEAAqE;IACrE,mBAAmB,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,YAAY,CAAA;KAAE,CAAC,CAAC;IACpG,uEAAuE;IACvE,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,WAAW,CAAA;KAAE,CAAC,CAAC;IACjG;mEAC+D;IAC/D,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,iFAAiF;IACjF,UAAU,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAAC;IAC/B,kEAAkE;IAClE,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACxF,mEAAmE;IACnE,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAC1F;;iFAE6E;IAC7E,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAClE,iEAAiE;IACjE,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,EAAE,CAAC;IAC3C,aAAa,IAAI,WAAW,EAAE,CAAC;IAC/B,oEAAoE;IACpE,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAC7C,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9D;;;;;OAKG;IACH,kBAAkB,CAAC,aAAa,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,cAAc,EAAE,CAAC;CAC3E;AA6RD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,wBAAwB,CA8V3E"}
@@ -18,6 +18,7 @@ exports.openTelemetryStore = openTelemetryStore;
18
18
  const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
19
19
  const node_fs_1 = require("node:fs");
20
20
  const node_path_1 = require("node:path");
21
+ const health_1 = require("./health");
21
22
  const SCHEMA_SQL = `
22
23
  CREATE TABLE IF NOT EXISTS inferences (
23
24
  id TEXT PRIMARY KEY,
@@ -40,9 +41,18 @@ CREATE TABLE IF NOT EXISTS inferences (
40
41
  fingerprint_cell TEXT,
41
42
  served_variant TEXT,
42
43
  repaired_by TEXT,
43
- pricing_missing INTEGER NOT NULL DEFAULT 0
44
+ pricing_missing INTEGER NOT NULL DEFAULT 0,
45
+ -- v10 (WS-A cache accounting): cache-write buckets + cache-key/prefix
46
+ -- substrate. Fresh here; v9-and-earlier gain them via the ALTER ladder below.
47
+ tokens_cache_write INTEGER NOT NULL DEFAULT 0,
48
+ tokens_cache_write_1h INTEGER NOT NULL DEFAULT 0,
49
+ cache_key TEXT,
50
+ prefix_fingerprint TEXT
44
51
  );
45
52
  CREATE INDEX IF NOT EXISTS idx_inferences_route_ts ON inferences (route, ts);
53
+ -- v8 (G-4 observed-health): per-endpoint windowed aggregation filters by
54
+ -- (provider, model, ts). Index-only; no data change.
55
+ CREATE INDEX IF NOT EXISTS idx_inferences_provider_model_ts ON inferences (provider, model, ts);
46
56
 
47
57
  CREATE TABLE IF NOT EXISTS captures (
48
58
  inference_id TEXT PRIMARY KEY,
@@ -84,7 +94,77 @@ CREATE TABLE IF NOT EXISTS feedback (
84
94
  );
85
95
  CREATE INDEX IF NOT EXISTS idx_feedback_inference ON feedback (inference_id);
86
96
  CREATE INDEX IF NOT EXISTS idx_feedback_episode ON feedback (episode_key);
97
+
98
+ -- v9 (Expansion E2 chunk A): the execution graph's local buffer.
99
+ --
100
+ -- runs is a PROJECTION (PK on id): run.start inserts a running row and run.end
101
+ -- UPDATEs it to a terminal status in place -- a run is genuinely mutable
102
+ -- (start -> end). A rowid watermark cannot re-export an updated row (UPDATE does
103
+ -- not move rowid), so the exporter waters off export_seq, a monotonic counter
104
+ -- bumped on EVERY write (insert or update). run.end bumps it, so the terminal
105
+ -- state re-exports; and because there is one row per id, a batch never carries
106
+ -- the same (org_id,id) twice -- which a merge=true upsert (DO UPDATE) would
107
+ -- reject with "cannot affect row a second time". The server merge collapses
108
+ -- the two exports into the one mutable row.
109
+ CREATE TABLE IF NOT EXISTS runs (
110
+ id TEXT PRIMARY KEY,
111
+ pipeline TEXT NOT NULL,
112
+ pipeline_version TEXT,
113
+ environment TEXT,
114
+ episode_key TEXT,
115
+ started_at TEXT NOT NULL,
116
+ ended_at TEXT,
117
+ status TEXT NOT NULL CHECK (status IN ('running','succeeded','failed','abandoned')),
118
+ export_seq INTEGER NOT NULL
119
+ );
120
+ CREATE INDEX IF NOT EXISTS idx_runs_export_seq ON runs (export_seq);
121
+
122
+ -- steps are FACTS -- one row per attempt. The composite PK makes a re-emit a
123
+ -- harmless INSERT-OR-IGNORE (server is merge=false too). rowid still exists (not
124
+ -- WITHOUT ROWID), so the watermark read works identically to the other streams.
125
+ CREATE TABLE IF NOT EXISTS steps (
126
+ run_id TEXT NOT NULL,
127
+ step_key TEXT NOT NULL,
128
+ attempt_no INTEGER NOT NULL,
129
+ task TEXT,
130
+ provenance TEXT NOT NULL CHECK (provenance IN ('ground-truth','attested')),
131
+ inference_id TEXT,
132
+ span_trace_id TEXT,
133
+ span_id TEXT,
134
+ started_at TEXT NOT NULL,
135
+ ended_at TEXT,
136
+ failure_class TEXT,
137
+ PRIMARY KEY (run_id, step_key, attempt_no)
138
+ );
139
+ CREATE INDEX IF NOT EXISTS idx_steps_run ON steps (run_id);
87
140
  `;
141
+ function toRunRow(r) {
142
+ return {
143
+ id: r.id,
144
+ pipeline: r.pipeline,
145
+ pipelineVersion: r.pipeline_version,
146
+ environment: r.environment,
147
+ episodeKey: r.episode_key,
148
+ startedAt: r.started_at,
149
+ endedAt: r.ended_at,
150
+ status: r.status,
151
+ };
152
+ }
153
+ function toStepRow(r) {
154
+ return {
155
+ runId: r.run_id,
156
+ stepKey: r.step_key,
157
+ attemptNo: r.attempt_no,
158
+ task: r.task,
159
+ provenance: r.provenance,
160
+ inferenceId: r.inference_id,
161
+ spanTraceId: r.span_trace_id,
162
+ spanId: r.span_id,
163
+ startedAt: r.started_at,
164
+ endedAt: r.ended_at,
165
+ failureClass: r.failure_class,
166
+ };
167
+ }
88
168
  function toFeedbackRow(r) {
89
169
  return {
90
170
  id: r.id,
@@ -127,6 +207,12 @@ function toTelemetryRow(r) {
127
207
  servedVariant: r.served_variant,
128
208
  repairedBy: r.repaired_by,
129
209
  pricingMissing: r.pricing_missing !== 0,
210
+ // `?? 0` / `?? null`: a v9-or-earlier database returns undefined for a v10
211
+ // ALTER column, and those rows genuinely predate cache accounting (WS-A).
212
+ tokensCacheWrite: r.tokens_cache_write ?? 0,
213
+ tokensCacheWrite1h: r.tokens_cache_write_1h ?? 0,
214
+ cacheKey: r.cache_key ?? null,
215
+ prefixFingerprint: r.prefix_fingerprint ?? null,
130
216
  };
131
217
  }
132
218
  function toEnvelopeRow(r) {
@@ -201,15 +287,48 @@ function openTelemetryStore(dbPath) {
201
287
  }
202
288
  db.pragma("user_version = 7");
203
289
  }
290
+ // v8 (G-4 observed-health): idx_inferences_provider_model_ts — index-only, no
291
+ // data change. SCHEMA_SQL's CREATE INDEX IF NOT EXISTS handles fresh AND
292
+ // migrating databases identically (it ran above), so the bump only stamps the
293
+ // version. Kept explicit rather than folded into v5's bare-stamp branch so the
294
+ // migration ladder reads one rung per schema epoch.
295
+ if (db.pragma("user_version", { simple: true }) < 8) {
296
+ db.exec("CREATE INDEX IF NOT EXISTS idx_inferences_provider_model_ts ON inferences (provider, model, ts)");
297
+ db.pragma("user_version = 8");
298
+ }
299
+ // v9 (Expansion E2 chunk A): runs + steps local outbox. Purely additive —
300
+ // SCHEMA_SQL's CREATE TABLE IF NOT EXISTS handles fresh AND migrating
301
+ // databases identically (it ran above), so the bump only stamps the version.
302
+ // A rig that never enables the artifact namespace simply never writes to them.
303
+ if (db.pragma("user_version", { simple: true }) < 9) {
304
+ db.pragma("user_version = 9");
305
+ }
306
+ // v10 (WS-A cache accounting): cache-write buckets + cache-key/prefix
307
+ // substrate. Additive; old rows read 0 / null (true of them — recorded before
308
+ // caching was accounted). Same ALTER pattern as v2–v6, folded over a list.
309
+ if (db.pragma("user_version", { simple: true }) < 10) {
310
+ const cols = db.prepare("PRAGMA table_info(inferences)").all();
311
+ const add = (name, ddl) => {
312
+ if (!cols.some((c) => c.name === name))
313
+ db.exec(`ALTER TABLE inferences ADD COLUMN ${ddl}`);
314
+ };
315
+ add("tokens_cache_write", "tokens_cache_write INTEGER NOT NULL DEFAULT 0");
316
+ add("tokens_cache_write_1h", "tokens_cache_write_1h INTEGER NOT NULL DEFAULT 0");
317
+ add("cache_key", "cache_key TEXT");
318
+ add("prefix_fingerprint", "prefix_fingerprint TEXT");
319
+ db.pragma("user_version = 10");
320
+ }
204
321
  const insertInference = db.prepare(`
205
322
  INSERT INTO inferences (
206
323
  id, ts, route, route_version, provider, model, requested_tier, served_tier,
207
324
  tokens_in, tokens_out, tokens_cached, cost_estimate_usd, latency_ms, ttfb_ms,
208
- failure_class, attempts_json, tags_json, fingerprint_cell, served_variant, repaired_by, pricing_missing
325
+ failure_class, attempts_json, tags_json, fingerprint_cell, served_variant, repaired_by, pricing_missing,
326
+ tokens_cache_write, tokens_cache_write_1h, cache_key, prefix_fingerprint
209
327
  ) VALUES (
210
328
  @id, @ts, @route, @route_version, @provider, @model, @requested_tier, @served_tier,
211
329
  @tokens_in, @tokens_out, @tokens_cached, @cost_estimate_usd, @latency_ms, @ttfb_ms,
212
- @failure_class, @attempts_json, @tags_json, @fingerprint_cell, @served_variant, @repaired_by, @pricing_missing
330
+ @failure_class, @attempts_json, @tags_json, @fingerprint_cell, @served_variant, @repaired_by, @pricing_missing,
331
+ @tokens_cache_write, @tokens_cache_write_1h, @cache_key, @prefix_fingerprint
213
332
  )
214
333
  `);
215
334
  const selectByTag = db.prepare(`
@@ -236,6 +355,32 @@ function openTelemetryStore(dbPath) {
236
355
  SET spent_usd = spent_usd + ?,
237
356
  status = CASE WHEN spent_usd + ? >= budget_usd THEN 'exhausted' ELSE status END
238
357
  WHERE envelope_key = ?
358
+ `);
359
+ // Upsert the run projection and bump export_seq on every write, so run.end's
360
+ // terminal state re-exports past the `:runs` watermark. better-sqlite3 is
361
+ // synchronous, so the MAX(export_seq)+1 read is race-free.
362
+ const nextRunSeq = db.prepare("SELECT COALESCE(MAX(export_seq), 0) + 1 AS n FROM runs");
363
+ const upsertRun = db.prepare(`
364
+ INSERT INTO runs (id, pipeline, pipeline_version, environment, episode_key, started_at, ended_at, status, export_seq)
365
+ VALUES (@id, @pipeline, @pipeline_version, @environment, @episode_key, @started_at, @ended_at, @status, @export_seq)
366
+ ON CONFLICT (id) DO UPDATE SET
367
+ pipeline = excluded.pipeline,
368
+ pipeline_version = excluded.pipeline_version,
369
+ environment = excluded.environment,
370
+ episode_key = excluded.episode_key,
371
+ started_at = excluded.started_at,
372
+ ended_at = excluded.ended_at,
373
+ status = excluded.status,
374
+ export_seq = excluded.export_seq
375
+ `);
376
+ const insertStep = db.prepare(`
377
+ INSERT OR IGNORE INTO steps (
378
+ run_id, step_key, attempt_no, task, provenance, inference_id, span_trace_id, span_id,
379
+ started_at, ended_at, failure_class
380
+ ) VALUES (
381
+ @run_id, @step_key, @attempt_no, @task, @provenance, @inference_id, @span_trace_id, @span_id,
382
+ @started_at, @ended_at, @failure_class
383
+ )
239
384
  `);
240
385
  return {
241
386
  record(row) {
@@ -261,6 +406,13 @@ function openTelemetryStore(dbPath) {
261
406
  served_variant: row.servedVariant,
262
407
  repaired_by: row.repairedBy,
263
408
  pricing_missing: row.pricingMissing ? 1 : 0,
409
+ // `?? 0` / `?? null`: the fields are optional on TelemetryRow, and
410
+ // better-sqlite3 rejects an `undefined` bound param — a row that omits
411
+ // them is a zero-write / no-key row (fail-closed default).
412
+ tokens_cache_write: row.tokensCacheWrite ?? 0,
413
+ tokens_cache_write_1h: row.tokensCacheWrite1h ?? 0,
414
+ cache_key: row.cacheKey ?? null,
415
+ prefix_fingerprint: row.prefixFingerprint ?? null,
264
416
  });
265
417
  },
266
418
  recordFeedback(row) {
@@ -283,6 +435,57 @@ function openTelemetryStore(dbPath) {
283
435
  .all(afterRowid, limit);
284
436
  return rows.map((r) => ({ rowid: r.__rowid, row: toFeedbackRow(r) }));
285
437
  },
438
+ recordRun(row) {
439
+ const seq = nextRunSeq.get().n;
440
+ upsertRun.run({
441
+ id: row.id,
442
+ pipeline: row.pipeline,
443
+ pipeline_version: row.pipelineVersion,
444
+ environment: row.environment,
445
+ episode_key: row.episodeKey,
446
+ started_at: row.startedAt,
447
+ ended_at: row.endedAt,
448
+ status: row.status,
449
+ export_seq: seq,
450
+ });
451
+ },
452
+ recordStep(row) {
453
+ insertStep.run({
454
+ run_id: row.runId,
455
+ step_key: row.stepKey,
456
+ attempt_no: row.attemptNo,
457
+ task: row.task,
458
+ provenance: row.provenance,
459
+ inference_id: row.inferenceId,
460
+ span_trace_id: row.spanTraceId,
461
+ span_id: row.spanId,
462
+ started_at: row.startedAt,
463
+ ended_at: row.endedAt,
464
+ failure_class: row.failureClass,
465
+ });
466
+ },
467
+ readRunsAfter(afterSeq, limit) {
468
+ // Watermark is export_seq (bumped on every write), NOT rowid — a run.end
469
+ // UPDATE keeps the rowid but advances export_seq, so its terminal state
470
+ // re-exports. The exporter treats the returned handle opaquely.
471
+ const rows = db
472
+ .prepare("SELECT export_seq AS __seq, * FROM runs WHERE export_seq > ? ORDER BY export_seq LIMIT ?")
473
+ .all(afterSeq, limit);
474
+ return rows.map((r) => ({ rowid: r.__seq, row: toRunRow(r) }));
475
+ },
476
+ readStepsAfter(afterRowid, limit) {
477
+ const rows = db
478
+ .prepare("SELECT rowid AS __rowid, * FROM steps WHERE rowid > ? ORDER BY rowid LIMIT ?")
479
+ .all(afterRowid, limit);
480
+ return rows.map((r) => ({ rowid: r.__rowid, row: toStepRow(r) }));
481
+ },
482
+ getStepInferenceId(runId, stepKey) {
483
+ const r = db
484
+ .prepare("SELECT inference_id FROM steps WHERE run_id = ? AND step_key = ? AND inference_id IS NOT NULL " +
485
+ "ORDER BY attempt_no DESC LIMIT 1")
486
+ .get(runId, stepKey);
487
+ return r === undefined ? null : r.inference_id;
488
+ },
286
489
  listFeedback(limit) {
287
490
  const rows = db
288
491
  .prepare("SELECT * FROM feedback ORDER BY ts DESC, rowid DESC LIMIT ?")
@@ -325,6 +528,18 @@ function openTelemetryStore(dbPath) {
325
528
  db.prepare(`INSERT INTO export_state (source_id, last_rowid) VALUES (?, ?)
326
529
  ON CONFLICT (source_id) DO UPDATE SET last_rowid = excluded.last_rowid`).run(sourceId, lastRowid);
327
530
  },
531
+ readEndpointHealth(windowMinutes, minN) {
532
+ // Capture `now` once so the SQL prefilter and the in-memory window agree.
533
+ // ts is ISO-8601 UTC (…Z), so lexical >= is chronological >= — the prefilter
534
+ // is exact, and aggregateEndpointHealth re-checks the window defensively.
535
+ const now = Date.now();
536
+ const windowStartIso = new Date(now - windowMinutes * 60000).toISOString();
537
+ const rows = db
538
+ .prepare(`SELECT provider, model, ts, latency_ms, ttfb_ms, failure_class
539
+ FROM inferences WHERE ts >= ?`)
540
+ .all(windowStartIso);
541
+ return (0, health_1.aggregateEndpointHealth)(rows, { windowMinutes, minN, now });
542
+ },
328
543
  close() {
329
544
  db.close();
330
545
  },
@@ -24,12 +24,22 @@ export declare const API_KEY_SECRET_BYTES = 32;
24
24
  /** base64url length of API_KEY_SECRET_BYTES — the minimum a real key's body has. */
25
25
  export declare const API_KEY_BODY_MIN_LENGTH = 43;
26
26
  /**
27
- * The granted authorities. Mirrors migration 00020's CHECK constraint — a
28
- * test pins that the two agree, because a scope that exists here and not
29
- * there is a key that cannot be written, and one that exists there and not
30
- * here is an authority nothing enforces.
27
+ * The granted authorities. Mirrors migration 00020's CHECK constraint (widened
28
+ * by 00033 for content custody) — a test pins that the two agree, because a
29
+ * scope that exists here and not there is a key that cannot be written, and one
30
+ * that exists there and not here is an authority nothing enforces.
31
+ *
32
+ * `artifact-write`/`artifact-read` (E2-C, 00033): request a content upload grant
33
+ * / mint a content download URL. Both are ordinary (non-elevated) machine
34
+ * grants — they move an org's own artifact bytes, they do not spend money or
35
+ * approve swaps (contrast decision/oracle-act, gated to admin/owner minters).
36
+ *
37
+ * `task-settings` (settings-plane, 00043): PATCH a task's governance settings
38
+ * (capture / artifacts / content / scrub). Held by a coding agent flipping
39
+ * settings for the user. READING settings needs no scope — any org-scoped key
40
+ * may read, since settings ride the already-readable task row.
31
41
  */
32
- export declare const API_KEY_SCOPES: readonly ["ingest", "query", "feedback", "decision", "oracle-act"];
42
+ export declare const API_KEY_SCOPES: readonly ["ingest", "query", "feedback", "decision", "oracle-act", "artifact-write", "artifact-read", "task-settings"];
33
43
  export type ApiKeyScope = (typeof API_KEY_SCOPES)[number];
34
44
  /** sha256 of the WHOLE presented string, prefix included — so a caller cannot
35
45
  * strip the prefix and land on a different row. */
@@ -1 +1 @@
1
- {"version":3,"file":"api-key-format.d.ts","sourceRoot":"","sources":["../../src/tenancy/api-key-format.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH;;;GAGG;AACH,eAAO,MAAM,cAAc,YAAY,CAAC;AAExC;;6EAE6E;AAC7E,eAAO,MAAM,oBAAoB,KAAK,CAAC;AAEvC,oFAAoF;AACpF,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAE1C;;;;;GAKG;AACH,eAAO,MAAM,cAAc,oEAAqE,CAAC;AAEjG,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC;AAE1D;mDACmD;AACnD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAI1D;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAGtD;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,WAAW,EAAE,CAIvD;AAED;gEACgE;AAChE,wBAAgB,QAAQ,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,EAAE,QAAQ,EAAE,WAAW,GAAG,OAAO,CAExF"}
1
+ {"version":3,"file":"api-key-format.d.ts","sourceRoot":"","sources":["../../src/tenancy/api-key-format.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH;;;GAGG;AACH,eAAO,MAAM,cAAc,YAAY,CAAC;AAExC;;6EAE6E;AAC7E,eAAO,MAAM,oBAAoB,KAAK,CAAC;AAEvC,oFAAoF;AACpF,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAE1C;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,cAAc,wHASjB,CAAC;AAEX,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC;AAE1D;mDACmD;AACnD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAI1D;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAGtD;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,WAAW,EAAE,CAIvD;AAED;gEACgE;AAChE,wBAAgB,QAAQ,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,EAAE,QAAQ,EAAE,WAAW,GAAG,OAAO,CAExF"}
@@ -33,12 +33,31 @@ exports.API_KEY_SECRET_BYTES = 32;
33
33
  /** base64url length of API_KEY_SECRET_BYTES — the minimum a real key's body has. */
34
34
  exports.API_KEY_BODY_MIN_LENGTH = 43;
35
35
  /**
36
- * The granted authorities. Mirrors migration 00020's CHECK constraint — a
37
- * test pins that the two agree, because a scope that exists here and not
38
- * there is a key that cannot be written, and one that exists there and not
39
- * here is an authority nothing enforces.
36
+ * The granted authorities. Mirrors migration 00020's CHECK constraint (widened
37
+ * by 00033 for content custody) — a test pins that the two agree, because a
38
+ * scope that exists here and not there is a key that cannot be written, and one
39
+ * that exists there and not here is an authority nothing enforces.
40
+ *
41
+ * `artifact-write`/`artifact-read` (E2-C, 00033): request a content upload grant
42
+ * / mint a content download URL. Both are ordinary (non-elevated) machine
43
+ * grants — they move an org's own artifact bytes, they do not spend money or
44
+ * approve swaps (contrast decision/oracle-act, gated to admin/owner minters).
45
+ *
46
+ * `task-settings` (settings-plane, 00043): PATCH a task's governance settings
47
+ * (capture / artifacts / content / scrub). Held by a coding agent flipping
48
+ * settings for the user. READING settings needs no scope — any org-scoped key
49
+ * may read, since settings ride the already-readable task row.
40
50
  */
41
- exports.API_KEY_SCOPES = ["ingest", "query", "feedback", "decision", "oracle-act"];
51
+ exports.API_KEY_SCOPES = [
52
+ "ingest",
53
+ "query",
54
+ "feedback",
55
+ "decision",
56
+ "oracle-act",
57
+ "artifact-write",
58
+ "artifact-read",
59
+ "task-settings",
60
+ ];
42
61
  /** sha256 of the WHOLE presented string, prefix included — so a caller cannot
43
62
  * strip the prefix and land on a different row. */
44
63
  function hashApiKeySecret(secret) {