modelrig 0.1.0 → 0.2.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 (309) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/README.md +41 -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.map +1 -1
  17. package/dist/adapters/gemini.js +46 -6
  18. package/dist/adapters/grok.d.ts +9 -0
  19. package/dist/adapters/grok.d.ts.map +1 -1
  20. package/dist/adapters/grok.js +67 -3
  21. package/dist/adapters/openai-compat.d.ts +6 -0
  22. package/dist/adapters/openai-compat.d.ts.map +1 -1
  23. package/dist/adapters/openai-compat.js +58 -1
  24. package/dist/adapters/openai-stream.d.ts +58 -0
  25. package/dist/adapters/openai-stream.d.ts.map +1 -0
  26. package/dist/adapters/openai-stream.js +120 -0
  27. package/dist/adapters/openai.d.ts +6 -0
  28. package/dist/adapters/openai.d.ts.map +1 -1
  29. package/dist/adapters/openai.js +60 -1
  30. package/dist/adapters/sampling.d.ts +28 -0
  31. package/dist/adapters/sampling.d.ts.map +1 -0
  32. package/dist/adapters/sampling.js +34 -0
  33. package/dist/adapters/tool-mapping.d.ts +92 -0
  34. package/dist/adapters/tool-mapping.d.ts.map +1 -0
  35. package/dist/adapters/tool-mapping.js +143 -0
  36. package/dist/artifacts/artifact.d.ts +137 -0
  37. package/dist/artifacts/artifact.d.ts.map +1 -0
  38. package/dist/artifacts/artifact.js +377 -0
  39. package/dist/artifacts/content-uploader.d.ts +87 -0
  40. package/dist/artifacts/content-uploader.d.ts.map +1 -0
  41. package/dist/artifacts/content-uploader.js +124 -0
  42. package/dist/artifacts/index.d.ts +63 -0
  43. package/dist/artifacts/index.d.ts.map +1 -0
  44. package/dist/artifacts/index.js +80 -0
  45. package/dist/artifacts/run-context.d.ts +129 -0
  46. package/dist/artifacts/run-context.d.ts.map +1 -0
  47. package/dist/artifacts/run-context.js +208 -0
  48. package/dist/artifacts/scrub.d.ts +60 -0
  49. package/dist/artifacts/scrub.d.ts.map +1 -0
  50. package/dist/artifacts/scrub.js +139 -0
  51. package/dist/artifacts/step.d.ts +26 -0
  52. package/dist/artifacts/step.d.ts.map +1 -0
  53. package/dist/artifacts/step.js +37 -0
  54. package/dist/artifacts/taint.d.ts +63 -0
  55. package/dist/artifacts/taint.d.ts.map +1 -0
  56. package/dist/artifacts/taint.js +76 -0
  57. package/dist/bakeoff/cache-sim.d.ts +99 -0
  58. package/dist/bakeoff/cache-sim.d.ts.map +1 -0
  59. package/dist/bakeoff/cache-sim.js +126 -0
  60. package/dist/bakeoff/eval-cases.d.ts +96 -0
  61. package/dist/bakeoff/eval-cases.d.ts.map +1 -0
  62. package/dist/bakeoff/eval-cases.js +105 -0
  63. package/dist/bakeoff/report.d.ts +41 -0
  64. package/dist/bakeoff/report.d.ts.map +1 -0
  65. package/dist/bakeoff/report.js +142 -0
  66. package/dist/bakeoff/samples.d.ts +15 -0
  67. package/dist/bakeoff/samples.d.ts.map +1 -1
  68. package/dist/bakeoff/samples.js +20 -0
  69. package/dist/cli.d.ts +1 -3
  70. package/dist/cli.d.ts.map +1 -1
  71. package/dist/cli.js +134 -116
  72. package/dist/config.d.ts +100 -0
  73. package/dist/config.d.ts.map +1 -1
  74. package/dist/config.js +104 -0
  75. package/dist/deprecations.d.ts +32 -0
  76. package/dist/deprecations.d.ts.map +1 -0
  77. package/dist/deprecations.js +59 -0
  78. package/dist/gateway/http-protocol.d.ts +175 -0
  79. package/dist/gateway/http-protocol.d.ts.map +1 -0
  80. package/dist/gateway/http-protocol.js +105 -0
  81. package/dist/index.d.ts +105 -18
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js +266 -138
  84. package/dist/init/finder.d.ts +15 -0
  85. package/dist/init/finder.d.ts.map +1 -1
  86. package/dist/init/finder.js +46 -0
  87. package/dist/init/init-cli.d.ts +10 -0
  88. package/dist/init/init-cli.d.ts.map +1 -0
  89. package/dist/init/init-cli.js +86 -0
  90. package/dist/init/observe-cli.d.ts +19 -0
  91. package/dist/init/observe-cli.d.ts.map +1 -0
  92. package/dist/init/observe-cli.js +73 -0
  93. package/dist/init/observe.d.ts +81 -0
  94. package/dist/init/observe.d.ts.map +1 -0
  95. package/dist/init/observe.js +245 -0
  96. package/dist/registry/build.d.ts +94 -1
  97. package/dist/registry/build.d.ts.map +1 -1
  98. package/dist/registry/build.js +125 -4
  99. package/dist/registry/cache-policy.d.ts +94 -0
  100. package/dist/registry/cache-policy.d.ts.map +1 -0
  101. package/dist/registry/cache-policy.js +238 -0
  102. package/dist/registry/call-notes.d.ts.map +1 -1
  103. package/dist/registry/call-notes.js +40 -2
  104. package/dist/registry/effective-rate.d.ts +151 -0
  105. package/dist/registry/effective-rate.d.ts.map +1 -0
  106. package/dist/registry/effective-rate.js +156 -0
  107. package/dist/registry/facts.d.ts.map +1 -1
  108. package/dist/registry/facts.js +16 -0
  109. package/dist/registry/parity.d.ts +26 -0
  110. package/dist/registry/parity.d.ts.map +1 -1
  111. package/dist/registry/parity.js +43 -2
  112. package/dist/registry/pricing-shape.d.ts +113 -0
  113. package/dist/registry/pricing-shape.d.ts.map +1 -0
  114. package/dist/registry/pricing-shape.js +123 -0
  115. package/dist/registry/pricing.d.ts +25 -1
  116. package/dist/registry/pricing.d.ts.map +1 -1
  117. package/dist/registry/pricing.js +64 -6
  118. package/dist/rigindex/bootstrap.d.ts +36 -0
  119. package/dist/rigindex/bootstrap.d.ts.map +1 -0
  120. package/dist/rigindex/bootstrap.js +97 -0
  121. package/dist/rigindex/fit.d.ts +24 -0
  122. package/dist/rigindex/fit.d.ts.map +1 -0
  123. package/dist/rigindex/fit.js +76 -0
  124. package/dist/rigindex/gates.d.ts +52 -0
  125. package/dist/rigindex/gates.d.ts.map +1 -0
  126. package/dist/rigindex/gates.js +109 -0
  127. package/dist/rigindex/graph.d.ts +31 -0
  128. package/dist/rigindex/graph.d.ts.map +1 -0
  129. package/dist/rigindex/graph.js +120 -0
  130. package/dist/rigindex/index.d.ts +23 -0
  131. package/dist/rigindex/index.d.ts.map +1 -0
  132. package/dist/rigindex/index.js +112 -0
  133. package/dist/rigindex/types.d.ts +76 -0
  134. package/dist/rigindex/types.d.ts.map +1 -0
  135. package/dist/rigindex/types.js +8 -0
  136. package/dist/routes/cache-lint.d.ts +41 -0
  137. package/dist/routes/cache-lint.d.ts.map +1 -0
  138. package/dist/routes/cache-lint.js +81 -0
  139. package/dist/routes/loader.d.ts +37 -0
  140. package/dist/routes/loader.d.ts.map +1 -1
  141. package/dist/routes/loader.js +138 -0
  142. package/dist/routes/resolved-bundle.d.ts +79 -0
  143. package/dist/routes/resolved-bundle.d.ts.map +1 -0
  144. package/dist/routes/resolved-bundle.js +148 -0
  145. package/dist/routes/sampling-config.d.ts +32 -0
  146. package/dist/routes/sampling-config.d.ts.map +1 -0
  147. package/dist/routes/sampling-config.js +91 -0
  148. package/dist/run/ajv.d.ts +22 -0
  149. package/dist/run/ajv.d.ts.map +1 -0
  150. package/dist/run/ajv.js +32 -0
  151. package/dist/run/cache-handle.d.ts +58 -0
  152. package/dist/run/cache-handle.d.ts.map +1 -0
  153. package/dist/run/cache-handle.js +54 -0
  154. package/dist/run/cache-stamp.d.ts +65 -0
  155. package/dist/run/cache-stamp.d.ts.map +1 -0
  156. package/dist/run/cache-stamp.js +0 -0
  157. package/dist/run/capture-write.d.ts +33 -0
  158. package/dist/run/capture-write.d.ts.map +1 -0
  159. package/dist/run/capture-write.js +55 -0
  160. package/dist/run/cost.d.ts.map +1 -1
  161. package/dist/run/cost.js +1 -1
  162. package/dist/run/dispatch.d.ts +99 -0
  163. package/dist/run/dispatch.d.ts.map +1 -0
  164. package/dist/run/dispatch.js +191 -0
  165. package/dist/run/raw.d.ts +88 -0
  166. package/dist/run/raw.d.ts.map +1 -0
  167. package/dist/run/raw.js +286 -0
  168. package/dist/run/record-attempt.d.ts +40 -0
  169. package/dist/run/record-attempt.d.ts.map +1 -0
  170. package/dist/run/record-attempt.js +99 -0
  171. package/dist/run/resolve.d.ts +75 -2
  172. package/dist/run/resolve.d.ts.map +1 -1
  173. package/dist/run/resolve.js +126 -4
  174. package/dist/run/run.d.ts +14 -0
  175. package/dist/run/run.d.ts.map +1 -1
  176. package/dist/run/run.js +0 -0
  177. package/dist/run/stream.d.ts +49 -0
  178. package/dist/run/stream.d.ts.map +1 -0
  179. package/dist/run/stream.js +87 -0
  180. package/dist/run/validate.d.ts.map +1 -1
  181. package/dist/run/validate.js +6 -7
  182. package/dist/savings/cache-ledger.d.ts +108 -0
  183. package/dist/savings/cache-ledger.d.ts.map +1 -0
  184. package/dist/savings/cache-ledger.js +174 -0
  185. package/dist/savings/cache-reconcile.d.ts +64 -0
  186. package/dist/savings/cache-reconcile.d.ts.map +1 -0
  187. package/dist/savings/cache-reconcile.js +59 -0
  188. package/dist/savings/cache-stats.d.ts +63 -0
  189. package/dist/savings/cache-stats.d.ts.map +1 -0
  190. package/dist/savings/cache-stats.js +108 -0
  191. package/dist/savings/emit.d.ts +13 -0
  192. package/dist/savings/emit.d.ts.map +1 -1
  193. package/dist/savings/emit.js +25 -0
  194. package/dist/savings/swap-execute.d.ts +14 -0
  195. package/dist/savings/swap-execute.d.ts.map +1 -1
  196. package/dist/savings/swap-execute.js +44 -1
  197. package/dist/savings/swaps-client.d.ts +14 -0
  198. package/dist/savings/swaps-client.d.ts.map +1 -1
  199. package/dist/savings/swaps-client.js +16 -2
  200. package/dist/settings/api-fetcher.d.ts +39 -0
  201. package/dist/settings/api-fetcher.d.ts.map +1 -0
  202. package/dist/settings/api-fetcher.js +92 -0
  203. package/dist/settings/index.d.ts +10 -0
  204. package/dist/settings/index.d.ts.map +1 -0
  205. package/dist/settings/index.js +15 -0
  206. package/dist/settings/task-settings.d.ts +91 -0
  207. package/dist/settings/task-settings.d.ts.map +1 -0
  208. package/dist/settings/task-settings.js +116 -0
  209. package/dist/status.d.ts +77 -0
  210. package/dist/status.d.ts.map +1 -0
  211. package/dist/status.js +113 -0
  212. package/dist/telemetry/artifact-store.d.ts +48 -0
  213. package/dist/telemetry/artifact-store.d.ts.map +1 -0
  214. package/dist/telemetry/artifact-store.js +189 -0
  215. package/dist/telemetry/cache-miss-streak.d.ts +79 -0
  216. package/dist/telemetry/cache-miss-streak.d.ts.map +1 -0
  217. package/dist/telemetry/cache-miss-streak.js +99 -0
  218. package/dist/telemetry/capture.d.ts +3 -0
  219. package/dist/telemetry/capture.d.ts.map +1 -1
  220. package/dist/telemetry/capture.js +15 -8
  221. package/dist/telemetry/exporter-setup.d.ts +28 -0
  222. package/dist/telemetry/exporter-setup.d.ts.map +1 -0
  223. package/dist/telemetry/exporter-setup.js +34 -0
  224. package/dist/telemetry/exporter.d.ts +75 -0
  225. package/dist/telemetry/exporter.d.ts.map +1 -1
  226. package/dist/telemetry/exporter.js +324 -47
  227. package/dist/telemetry/health.d.ts +76 -0
  228. package/dist/telemetry/health.d.ts.map +1 -0
  229. package/dist/telemetry/health.js +92 -0
  230. package/dist/telemetry/ingest-protocol.d.ts +152 -3
  231. package/dist/telemetry/ingest-protocol.d.ts.map +1 -1
  232. package/dist/telemetry/ingest-protocol.js +204 -4
  233. package/dist/telemetry/load-error-report.d.ts +24 -0
  234. package/dist/telemetry/load-error-report.d.ts.map +1 -0
  235. package/dist/telemetry/load-error-report.js +50 -0
  236. package/dist/telemetry/otel-map.d.ts +121 -0
  237. package/dist/telemetry/otel-map.d.ts.map +1 -0
  238. package/dist/telemetry/otel-map.js +224 -0
  239. package/dist/telemetry/otel-normalize.d.ts +104 -0
  240. package/dist/telemetry/otel-normalize.d.ts.map +1 -0
  241. package/dist/telemetry/otel-normalize.js +530 -0
  242. package/dist/telemetry/otlp-emitter.d.ts +52 -0
  243. package/dist/telemetry/otlp-emitter.d.ts.map +1 -0
  244. package/dist/telemetry/otlp-emitter.js +144 -0
  245. package/dist/telemetry/otlp-proto.d.ts +29 -0
  246. package/dist/telemetry/otlp-proto.d.ts.map +1 -0
  247. package/dist/telemetry/otlp-proto.js +266 -0
  248. package/dist/telemetry/sinks.d.ts +37 -1
  249. package/dist/telemetry/sinks.d.ts.map +1 -1
  250. package/dist/telemetry/sinks.js +121 -30
  251. package/dist/telemetry/store.d.ts +28 -1
  252. package/dist/telemetry/store.d.ts.map +1 -1
  253. package/dist/telemetry/store.js +218 -3
  254. package/dist/tenancy/api-key-format.d.ts +15 -5
  255. package/dist/tenancy/api-key-format.d.ts.map +1 -1
  256. package/dist/tenancy/api-key-format.js +24 -5
  257. package/dist/types/config-teaching.d.ts +62 -0
  258. package/dist/types/config-teaching.d.ts.map +1 -0
  259. package/dist/types/config-teaching.js +183 -0
  260. package/dist/types/provider.d.ts +122 -1
  261. package/dist/types/provider.d.ts.map +1 -1
  262. package/dist/types/provider.js +21 -1
  263. package/dist/types/route.d.ts +79 -1
  264. package/dist/types/route.d.ts.map +1 -1
  265. package/dist/types/telemetry.d.ts +117 -0
  266. package/dist/types/telemetry.d.ts.map +1 -1
  267. package/dist/validate-cli.d.ts +12 -0
  268. package/dist/validate-cli.d.ts.map +1 -0
  269. package/dist/validate-cli.js +57 -0
  270. package/dist/verify/capture.d.ts +66 -0
  271. package/dist/verify/capture.d.ts.map +1 -0
  272. package/dist/verify/capture.js +180 -0
  273. package/dist/verify/cli-run.d.ts +23 -0
  274. package/dist/verify/cli-run.d.ts.map +1 -0
  275. package/dist/verify/cli-run.js +57 -0
  276. package/dist/verify/diff.d.ts +48 -0
  277. package/dist/verify/diff.d.ts.map +1 -0
  278. package/dist/verify/diff.js +157 -0
  279. package/dist/verify/index.d.ts +17 -0
  280. package/dist/verify/index.d.ts.map +1 -0
  281. package/dist/verify/index.js +24 -0
  282. package/dist/verify/report.d.ts +14 -0
  283. package/dist/verify/report.d.ts.map +1 -0
  284. package/dist/verify/report.js +88 -0
  285. package/dist/verify/types.d.ts +101 -0
  286. package/dist/verify/types.d.ts.map +1 -0
  287. package/dist/verify/types.js +17 -0
  288. package/dist/verify/verify.d.ts +29 -0
  289. package/dist/verify/verify.d.ts.map +1 -0
  290. package/dist/verify/verify.js +148 -0
  291. package/dist/watch/evaluate.d.ts +162 -3
  292. package/dist/watch/evaluate.d.ts.map +1 -1
  293. package/dist/watch/evaluate.js +61 -0
  294. package/dist/watch/monitor.d.ts +116 -0
  295. package/dist/watch/monitor.d.ts.map +1 -0
  296. package/dist/watch/monitor.js +199 -0
  297. package/dist/watch/proposals.d.ts +20 -4
  298. package/dist/watch/proposals.d.ts.map +1 -1
  299. package/dist/watch/proposals.js +31 -3
  300. package/dist/watch/run.d.ts +33 -0
  301. package/dist/watch/run.d.ts.map +1 -1
  302. package/dist/watch/run.js +39 -0
  303. package/package.json +7 -1
  304. package/registry/LICENSE +20 -0
  305. package/registry/leaderboard.json +3639 -0
  306. package/registry/registry.json +7263 -0
  307. package/dist/telemetry/supabase-exporter.d.ts +0 -64
  308. package/dist/telemetry/supabase-exporter.d.ts.map +0 -1
  309. package/dist/telemetry/supabase-exporter.js +0 -211
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ /**
3
+ * Build-and-start the telemetry exporter from resolved config. Extracted from
4
+ * index.ts (the SDK entrypoint) so that file stays under the LOC cap while
5
+ * Phase 2 adds the settings resolver + boot banner beside it — the exporter
6
+ * wiring is mechanical and has one caller, so it lives here rather than inline.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.startTelemetryExporter = startTelemetryExporter;
10
+ const node_os_1 = require("node:os");
11
+ const exporter_1 = require("./exporter");
12
+ const sinks_1 = require("./sinks");
13
+ /**
14
+ * Construct the sink for the configured mode, wire the exporter, and start its
15
+ * flush loop. Returns the running exporter (the caller stops it on close()).
16
+ */
17
+ function startTelemetryExporter(telemetry, exportConfig, extras) {
18
+ const sourceId = exportConfig.sourceId ?? (0, node_os_1.hostname)();
19
+ const sink = (0, sinks_1.buildTelemetrySink)(exportConfig, sourceId);
20
+ const exporter = (0, exporter_1.createTelemetryExporter)(telemetry, {
21
+ sink,
22
+ sourceId,
23
+ ...(exportConfig.batchSize !== undefined ? { batchSize: exportConfig.batchSize } : {}),
24
+ ...(exportConfig.intervalMs !== undefined ? { intervalMs: exportConfig.intervalMs } : {}),
25
+ routes: extras.routes,
26
+ manifest: extras.manifest,
27
+ serveability: extras.serveability,
28
+ artifacts: extras.artifactsEnabled,
29
+ ...(extras.artifactStore !== null ? { artifactStore: extras.artifactStore } : {}),
30
+ ...(extras.onArtifactsFlushed !== undefined ? { onArtifactsFlushed: extras.onArtifactsFlushed } : {}),
31
+ });
32
+ exporter.start();
33
+ return exporter;
34
+ }
@@ -17,8 +17,13 @@
17
17
  * holds the one mapper both of them write rows through.
18
18
  */
19
19
  import type { RigManifest } from "../routes/manifest";
20
+ import type { ResolvedRouteBundle } from "../routes/resolved-bundle";
21
+ import type { RouteBundle } from "../types/route";
20
22
  import type { ServeabilityWarning } from "../routes/serveability";
23
+ import type { ConfigFailedState } from "../types/config-teaching";
24
+ import type { IngestBatch } from "./ingest-protocol";
21
25
  import type { TelemetrySink } from "./sinks";
26
+ import type { ArtifactStore } from "./artifact-store";
22
27
  import type { ExportableTelemetryStore } from "./store";
23
28
  export interface ExportedRouteMirror {
24
29
  readonly route: string;
@@ -30,7 +35,21 @@ export interface ExportedRouteMirror {
30
35
  /** Capture opt-in FLAG (Phase 3 WS4) — status only; capture content never
31
36
  * has a cloud surface. */
32
37
  readonly capture?: boolean;
38
+ /** Phase D (spec §13): the RESOLVED bundle (schema + prompt template inlined)
39
+ * the hosted gateway serves, and its content hash. Built by `index.ts` from
40
+ * the already-loaded (file-resolved) bundle via `resolveRouteBundle`. */
41
+ readonly bundle?: ResolvedRouteBundle;
42
+ readonly bundleSha?: string;
33
43
  }
44
+ /**
45
+ * Build the `routes` mirror payload from loaded bundles (Phase D, spec §13).
46
+ * Each `b` is ALREADY file-resolved — the loader inlined the schema + prompt
47
+ * files at createRig() and FAILED THERE (teaching RouteConfigError) if one was
48
+ * missing, so nothing unresolved reaches here and the gateway never needs the
49
+ * customer's filesystem (§13.1 rule 6). Lives here rather than in index.ts to
50
+ * keep that file under the LOC cap.
51
+ */
52
+ export declare function buildExportedRouteMirrors(bundles: readonly RouteBundle[]): ExportedRouteMirror[];
34
53
  export interface ExporterConfig {
35
54
  /** Where batches go. `sinks.ts` builds one of these from config. */
36
55
  readonly sink: TelemetrySink;
@@ -47,6 +66,43 @@ export interface ExporterConfig {
47
66
  * XAI_API_KEY" instead of the fact living only in a boot log. Names only:
48
67
  * route, provider, model, env var. */
49
68
  readonly serveability?: readonly ServeabilityWarning[];
69
+ /** Load-time RouteConfigError diagnostics for THIS deployment (follow-up #2)
70
+ * — mirrored onto the manifest row so /setup's "Define your first route" step
71
+ * renders `failed` with what we looked for and found, instead of silence.
72
+ * Absent/empty means the load was clean, and sending `[]` clears a stale
73
+ * failure from a prior broken deploy. */
74
+ readonly loadErrors?: readonly ConfigFailedState[];
75
+ /**
76
+ * Artifact / execution-graph namespace active (Expansion E2). When true the
77
+ * exporter announces protocol v2 and ships the `runs`/`steps` streams; when
78
+ * false (the default) it announces v1 and ships nothing new — byte-identical
79
+ * to before v2 existed (spec §1.3/§1.4). The wire version is a property of the
80
+ * EXPORTER, not the batch: once artifacts are on, every batch is v2, so an old
81
+ * server 422s loudly rather than silently dropping artifact sections.
82
+ */
83
+ readonly artifacts?: boolean;
84
+ /** The local artifact-metadata buffer (chunk B). A SEPARATE handle from the
85
+ * telemetry store; present only when the namespace is enabled. Its rows ride
86
+ * the `:artifacts`/`:edges`/`:evals` watermark streams (watermarks live in the
87
+ * main store's export_state, rows are read from here). */
88
+ readonly artifactStore?: ArtifactStore;
89
+ /**
90
+ * Post-flush hook fired AFTER a confirmed export of the artifacts stream (E4
91
+ * chunk 0, flag 2). The artifact api wires this to `retryPendingContent`: a
92
+ * content upload that 404'd because it raced its own metadata export is now
93
+ * safe to re-attempt, because the metadata row just landed in the control
94
+ * plane. It runs only once the artifact drains have all returned (a throw in
95
+ * any of them aborts the flush before this, so "confirmed export" holds), and
96
+ * it must never throw — a rejection here is swallowed like any flush failure.
97
+ */
98
+ readonly onArtifactsFlushed?: () => Promise<void>;
99
+ }
100
+ /** Poison-pill counters (E2 hardening Phase 2, spec §2.3.5): per-stream row
101
+ * counts the exporter has SKIPPED past because the batch was rejected
102
+ * deterministically K times running. In-memory, per instance; empty `{}` on a
103
+ * healthy exporter. Mirrors the `IngestOutcome.reasons` map shape. */
104
+ export interface ExporterStats {
105
+ readonly poisonSkipped: Readonly<Record<string, number>>;
50
106
  }
51
107
  export interface Exporter {
52
108
  /** Which lane this rig's telemetry is travelling in. Read by the boot
@@ -57,6 +113,25 @@ export interface Exporter {
57
113
  /** Flush pending rows now (used by tests and process-exit hooks). */
58
114
  flush(): Promise<void>;
59
115
  stop(): Promise<void>;
116
+ /** Poison batches this exporter has skipped past, per stream (additive; E2
117
+ * hardening Phase 2). The `/status` snapshot and any operator dashboard read
118
+ * it; a healthy exporter returns `{ poisonSkipped: {} }`. */
119
+ stats(): ExporterStats;
60
120
  }
121
+ /** The flush cadence. Exported so the settings resolver (§1.5) can beat on the
122
+ * SAME interval — "refreshes on the exporter interval" without a second knob. */
123
+ export declare const DEFAULT_INTERVAL_MS = 10000;
61
124
  export declare function createTelemetryExporter(store: ExportableTelemetryStore, config: ExporterConfig): Exporter;
125
+ /**
126
+ * The one-shot manifest batch that carries a load-time config failure to the
127
+ * console (follow-up #2), for the case `createRig` throws BEFORE the exporter
128
+ * loop exists (a route bundle that fails to load never reaches the loop).
129
+ *
130
+ * It is a manifest-only batch: no inferences, no watermark. The manifest carries
131
+ * whatever `rig.yaml` could still provide (name/dimensions/contribution — the
132
+ * routes failing to load does not stop rig.yaml loading) plus the load errors;
133
+ * `serveability` is `[]` because it cannot be computed without loaded routes.
134
+ * A caller sends it fire-and-forget through the same sink the run loop uses.
135
+ */
136
+ export declare function buildLoadErrorBatch(sourceId: string, manifest: Pick<RigManifest, "name" | "dimensions" | "contribution"> | null, loadErrors: readonly ConfigFailedState[]): IngestBatch;
62
137
  //# sourceMappingURL=exporter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"exporter.d.ts","sourceRoot":"","sources":["../../src/telemetry/exporter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAIlE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7C,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,SAAS,CAAC;AAExD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC;QAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC1F;8BAC0B;IAC1B,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,cAAc;IAC7B,oEAAoE;IACpE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,4EAA4E;IAC5E,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,mBAAmB,EAAE,CAAC;IACjD;6EACyE;IACzE,QAAQ,CAAC,QAAQ,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IACvC;;;0CAGsC;IACtC,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,mBAAmB,EAAE,CAAC;CACxD;AAED,MAAM,WAAW,QAAQ;IACvB;;oCAEgC;IAChC,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,KAAK,CAAC;IAChC,KAAK,IAAI,IAAI,CAAC;IACd,qEAAqE;IACrE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAoDD,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,wBAAwB,EAC/B,MAAM,EAAE,cAAc,GACrB,QAAQ,CAuKV"}
1
+ {"version":3,"file":"exporter.d.ts","sourceRoot":"","sources":["../../src/telemetry/exporter.ts"],"names":[],"mappings":"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"}
@@ -18,13 +18,51 @@
18
18
  * holds the one mapper both of them write rows through.
19
19
  */
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.DEFAULT_INTERVAL_MS = void 0;
22
+ exports.buildExportedRouteMirrors = buildExportedRouteMirrors;
21
23
  exports.createTelemetryExporter = createTelemetryExporter;
24
+ exports.buildLoadErrorBatch = buildLoadErrorBatch;
25
+ const resolved_bundle_1 = require("../routes/resolved-bundle");
22
26
  const ingest_protocol_1 = require("./ingest-protocol");
27
+ const sinks_1 = require("./sinks");
28
+ /**
29
+ * Build the `routes` mirror payload from loaded bundles (Phase D, spec §13).
30
+ * Each `b` is ALREADY file-resolved — the loader inlined the schema + prompt
31
+ * files at createRig() and FAILED THERE (teaching RouteConfigError) if one was
32
+ * missing, so nothing unresolved reaches here and the gateway never needs the
33
+ * customer's filesystem (§13.1 rule 6). Lives here rather than in index.ts to
34
+ * keep that file under the LOC cap.
35
+ */
36
+ function buildExportedRouteMirrors(bundles) {
37
+ return bundles.map((b) => {
38
+ const bundle = (0, resolved_bundle_1.resolveRouteBundle)(b);
39
+ return {
40
+ route: b.route,
41
+ version: b.version,
42
+ candidates: b.candidates.map((c) => ({ provider: c.provider, model: c.model })),
43
+ capture: b.capture,
44
+ bundle,
45
+ bundleSha: (0, resolved_bundle_1.hashResolvedBundle)(bundle),
46
+ };
47
+ });
48
+ }
23
49
  const DEFAULT_BATCH_SIZE = 200;
24
- const DEFAULT_INTERVAL_MS = 10000;
50
+ /** The flush cadence. Exported so the settings resolver (§1.5) can beat on the
51
+ * SAME interval — "refreshes on the exporter interval" without a second knob. */
52
+ exports.DEFAULT_INTERVAL_MS = 10000;
25
53
  /** After a sink failure, skip this many intervals, doubling per consecutive
26
54
  * failure (simple exponential backoff, capped). */
27
55
  const BACKOFF_MAX_SKIPS = 32;
56
+ /**
57
+ * Consecutive DETERMINISTIC failures on the SAME stuck batch before the
58
+ * exporter gives up and skips past it (E2 hardening Phase 2, spec §2.3.3).
59
+ * Five failures with the global exponential backoff between them is a
60
+ * multi-minute self-heal window — long enough for a still-deploying migration
61
+ * that adds a missing column to land, short enough to bound the wedge. A
62
+ * TRANSIENT failure never counts toward this, so an outage of any length can
63
+ * never trip it.
64
+ */
65
+ const POISON_SKIP_THRESHOLD = 5;
28
66
  function toWireInference(row) {
29
67
  return {
30
68
  id: row.id,
@@ -48,6 +86,92 @@ function toWireInference(row) {
48
86
  served_variant: row.servedVariant,
49
87
  repaired_by: row.repairedBy,
50
88
  pricing_missing: row.pricingMissing,
89
+ // Cache accounting (WS-A) — always emitted by a current SDK. Requires the
90
+ // `inferences` cache columns (migration 00049) to exist server-side; apply
91
+ // 00049 BEFORE deploying this build (migrate-first), else the insert 4xx's
92
+ // and the exporter's poison-skip drops the batch after its self-heal window.
93
+ tokens_cache_write: row.tokensCacheWrite ?? 0,
94
+ tokens_cache_write_1h: row.tokensCacheWrite1h ?? 0,
95
+ cache_key: row.cacheKey ?? null,
96
+ prefix_fingerprint: row.prefixFingerprint ?? null,
97
+ };
98
+ }
99
+ function toWireRun(row) {
100
+ return {
101
+ id: row.id,
102
+ pipeline: row.pipeline,
103
+ pipeline_version: row.pipelineVersion,
104
+ environment: row.environment,
105
+ episode_key: row.episodeKey,
106
+ started_at: row.startedAt,
107
+ ended_at: row.endedAt,
108
+ status: row.status,
109
+ };
110
+ }
111
+ function toWireStep(row) {
112
+ return {
113
+ run_id: row.runId,
114
+ step_key: row.stepKey,
115
+ attempt_no: row.attemptNo,
116
+ task: row.task,
117
+ provenance: row.provenance,
118
+ inference_id: row.inferenceId,
119
+ span_trace_id: row.spanTraceId,
120
+ span_id: row.spanId,
121
+ started_at: row.startedAt,
122
+ ended_at: row.endedAt,
123
+ failure_class: row.failureClass,
124
+ };
125
+ }
126
+ function toWireArtifact(row) {
127
+ return {
128
+ id: row.id,
129
+ run_id: row.runId,
130
+ step_key: row.stepKey,
131
+ attempt_no: row.attemptNo,
132
+ task: row.task,
133
+ name: row.name,
134
+ type: row.type,
135
+ version: row.version,
136
+ supersedes_artifact_id: row.supersedesArtifactId,
137
+ declared_schema: row.declaredSchema ?? null,
138
+ fingerprint_cell: row.fingerprintCell,
139
+ content_ref: row.contentRef,
140
+ content_hash: row.contentHash,
141
+ content_bytes: row.contentBytes,
142
+ data_classification: row.dataClassification,
143
+ expires_at: row.expiresAt,
144
+ // Per-row scrub findings (E4 chunk 0) — spread-iff-present so a null/absent
145
+ // scrub never widens the batch beyond what older servers accept.
146
+ ...(row.scrub !== undefined && row.scrub !== null ? { scrub: row.scrub } : {}),
147
+ };
148
+ }
149
+ function toWireEdge(row) {
150
+ return { from_artifact_id: row.fromArtifactId, to_artifact_id: row.toArtifactId, kind: row.kind };
151
+ }
152
+ function toWireEvalCase(row) {
153
+ return {
154
+ id: row.id,
155
+ task: row.task,
156
+ source_artifact_id: row.sourceArtifactId,
157
+ corrected_artifact_id: row.correctedArtifactId,
158
+ origin: row.origin,
159
+ note: row.note,
160
+ created_by: row.createdBy,
161
+ };
162
+ }
163
+ function toWireEvaluation(row) {
164
+ return {
165
+ id: row.id,
166
+ artifact_id: row.artifactId,
167
+ run_id: row.runId,
168
+ evaluator: row.evaluator,
169
+ evaluator_kind: row.evaluatorKind,
170
+ score: row.score,
171
+ passed: row.passed,
172
+ label: row.label,
173
+ explanation: row.explanation,
174
+ created_by: row.createdBy,
51
175
  };
52
176
  }
53
177
  function toWireFeedback(row) {
@@ -69,14 +193,25 @@ function toWireFeedback(row) {
69
193
  }
70
194
  function createTelemetryExporter(store, config) {
71
195
  const batchSize = config.batchSize ?? DEFAULT_BATCH_SIZE;
72
- const intervalMs = config.intervalMs ?? DEFAULT_INTERVAL_MS;
196
+ const intervalMs = config.intervalMs ?? exports.DEFAULT_INTERVAL_MS;
73
197
  const { sink, sourceId } = config;
198
+ // The wire version this exporter announces (spec §1.3). v2 iff the artifact
199
+ // namespace is active; otherwise v1, byte-identical to before v2 existed.
200
+ const wireVersion = config.artifacts === true ? ingest_protocol_1.INGEST_PROTOCOL_VERSION : ingest_protocol_1.INGEST_PROTOCOL_VERSION_V1;
74
201
  let timer = null;
75
202
  let flushing = false;
76
203
  let skipsRemaining = 0;
77
204
  let consecutiveFailures = 0;
78
205
  let routesMirrored = false;
79
206
  let manifestMirrored = false;
207
+ // Poison-pill state (E2 hardening Phase 2). Per stream key, the deterministic
208
+ // failure count for the CURRENTLY-STUCK batch — which is identified by its
209
+ // watermark, since the read is always "rows after W, limit N", so an
210
+ // unadvanced watermark IS the same batch. Reset the instant the watermark
211
+ // moves (a success OR a skip). `poisonSkipped` is the cumulative, per-stream
212
+ // count of rows dropped this way — the never-silent surface `stats()` reads.
213
+ const poisonState = new Map();
214
+ const poisonSkipped = {};
80
215
  /**
81
216
  * The once-per-process mirrors, folded into whichever batch goes out first.
82
217
  *
@@ -92,6 +227,9 @@ function createTelemetryExporter(store, config) {
92
227
  version: r.version,
93
228
  candidates: r.candidates,
94
229
  capture: r.capture ?? false,
230
+ // Phase D: carry the resolved bundle + hash when the caller built them
231
+ // (index.ts always does; a caller that doesn't stays metadata-only).
232
+ ...(r.bundle !== undefined ? { bundle: r.bundle, bundle_sha: r.bundleSha } : {}),
95
233
  }));
96
234
  }
97
235
  if (!manifestMirrored && config.manifest !== undefined && config.manifest !== null) {
@@ -106,6 +244,9 @@ function createTelemetryExporter(store, config) {
106
244
  serveable_count: w.serveableCount,
107
245
  fix_hint: w.fixHint,
108
246
  })),
247
+ // Follow-up #2: the describeConfigFailure shape, or [] on a clean load
248
+ // (which clears a stale failure from a prior broken deploy).
249
+ load_errors: [...(config.loadErrors ?? [])],
109
250
  };
110
251
  }
111
252
  return header;
@@ -118,56 +259,161 @@ function createTelemetryExporter(store, config) {
118
259
  if (header.manifest !== undefined)
119
260
  manifestMirrored = true;
120
261
  }
262
+ /**
263
+ * Send one batch for a stream, applying the poison-pill state machine
264
+ * (spec §2.3.3). Returns:
265
+ * "sent" the send succeeded — the caller advances the watermark and marks
266
+ * the header delivered.
267
+ * "skip" a deterministic poison was skipped after K failures — the caller
268
+ * advances the watermark PAST this batch without it ever landing
269
+ * (the rows are counted + logged, never retried).
270
+ * THROWS the underlying error when the caller must abort this flush and let
271
+ * the global backoff apply: a transient failure (retry forever, never counted)
272
+ * OR a deterministic one still inside its self-heal window (failCount < K).
273
+ *
274
+ * A poison in one stream never blocks another: the skip breaks only its own
275
+ * stuck batch, and the caller then continues to the next batch and the next
276
+ * stream (spec §2.3.3).
277
+ */
278
+ async function sendOrPoison(stream, watermark, rowCount, batch) {
279
+ try {
280
+ await sink.send(batch);
281
+ // A success advances the watermark → this stream is unstuck; drop its
282
+ // failure record so a later, unrelated batch starts fresh.
283
+ poisonState.delete(stream);
284
+ return "sent";
285
+ }
286
+ catch (err) {
287
+ // Anything that is not a classified SinkSendError is treated as TRANSIENT
288
+ // — we never poison-skip (drop data) on an error we cannot reason about.
289
+ const retriable = err instanceof sinks_1.SinkSendError ? err.retriable : true;
290
+ if (retriable) {
291
+ // Transient: do NOT touch failCount. Rethrow so the existing global
292
+ // backoff owns it and the stream retries forever (at-least-once).
293
+ throw err;
294
+ }
295
+ // Deterministic. Count failures against THIS stuck batch (keyed by W); a
296
+ // watermark that no longer matches means a different batch, so reset.
297
+ const prior = poisonState.get(stream);
298
+ const failCount = (prior !== undefined && prior.watermark === watermark ? prior.failCount : 0) + 1;
299
+ if (failCount < POISON_SKIP_THRESHOLD) {
300
+ // Self-heal window: rethrow (retry next flush, with backoff). A
301
+ // still-deploying migration that adds the missing column lands here.
302
+ poisonState.set(stream, { watermark, failCount });
303
+ throw err;
304
+ }
305
+ // POISON. Advance past this batch WITHOUT sending, count it, log LOUDLY,
306
+ // reset — the stream (and every later stream) keeps flowing.
307
+ poisonState.delete(stream);
308
+ poisonSkipped[stream] = (poisonSkipped[stream] ?? 0) + rowCount;
309
+ const status = err instanceof sinks_1.SinkSendError ? err.status : undefined;
310
+ console.warn(`[modelrig exporter] POISON-SKIP: stream "${stream}" batch at watermark ${watermark} ` +
311
+ `(${rowCount} row(s), status ${status ?? "network"}) was rejected deterministically ` +
312
+ `${POISON_SKIP_THRESHOLD}× running — advancing the watermark PAST it WITHOUT writing. ` +
313
+ `These rows are dropped and will NOT be retried; investigate the rejection and backfill.`);
314
+ return "skip";
315
+ }
316
+ }
121
317
  async function flushOnce() {
122
318
  let header = pendingHeader();
123
- // Inferences past the watermark, in batches. The watermark advances ONLY
124
- // after a confirmed send (at-least-once; the sink's upsert makes the
125
- // duplicate harmless).
126
- for (;;) {
127
- const watermark = store.getExportWatermark(sourceId);
128
- const pending = store.readInferencesAfter(watermark, batchSize);
129
- if (pending.length === 0)
130
- break;
131
- await sink.send({
132
- v: ingest_protocol_1.INGEST_PROTOCOL_VERSION,
133
- source: sourceId,
134
- ...header,
135
- inferences: pending.map(({ row }) => toWireInference(row)),
136
- watermarkAt: new Date().toISOString(),
137
- });
138
- markHeaderDelivered(header);
139
- header = {};
140
- store.setExportWatermark(sourceId, pending[pending.length - 1].rowid);
141
- if (pending.length < batchSize)
142
- break;
319
+ /**
320
+ * Drain one watermarked stream to exhaustion, batch by batch, through the
321
+ * poison-pill state machine. Every stream (inferences, feedback, runs,
322
+ * steps, artifacts, edges, evals) is the SAME at-least-once dance behind its
323
+ * own export_state key — factored here so the watermark/header/poison
324
+ * bookkeeping lives in exactly one place and no stream can drift from
325
+ * another. A "skip" advances the watermark past a poison batch WITHOUT
326
+ * having sent it; a "sent" advances it and delivers the once-per-flush
327
+ * header. Reassigning the captured `header` is intentional: the header rides
328
+ * the first CONFIRMED send, so a skipped (unsent) batch leaves it for the
329
+ * next one.
330
+ */
331
+ async function drainStream(streamKey, read, toSection) {
332
+ for (;;) {
333
+ const watermark = store.getExportWatermark(streamKey);
334
+ const pending = read(watermark, batchSize);
335
+ if (pending.length === 0)
336
+ break;
337
+ const outcome = await sendOrPoison(streamKey, watermark, pending.length, {
338
+ v: wireVersion,
339
+ source: sourceId,
340
+ ...header,
341
+ ...toSection(pending.map((p) => p.row)),
342
+ });
343
+ if (outcome === "sent") {
344
+ markHeaderDelivered(header);
345
+ header = {};
346
+ }
347
+ // Advance past this batch on BOTH a send and a poison-skip — a skip's
348
+ // whole purpose is to move the watermark past the batch that will never
349
+ // land, so the stream keeps flowing (spec §2.3.3).
350
+ store.setExportWatermark(streamKey, pending[pending.length - 1].rowid);
351
+ if (pending.length < batchSize)
352
+ break;
353
+ }
143
354
  }
144
- // Feedback: same at-least-once watermark dance, behind its own export_state
145
- // key so the two streams never block each other.
146
- const feedbackSource = `${sourceId}:feedback`;
147
- for (;;) {
148
- const watermark = store.getExportWatermark(feedbackSource);
149
- const pending = store.readFeedbackAfter(watermark, batchSize);
150
- if (pending.length === 0)
151
- break;
152
- await sink.send({
153
- v: ingest_protocol_1.INGEST_PROTOCOL_VERSION,
154
- source: sourceId,
155
- ...header,
156
- feedback: pending.map(({ row }) => toWireFeedback(row)),
157
- });
158
- markHeaderDelivered(header);
159
- header = {};
160
- store.setExportWatermark(feedbackSource, pending[pending.length - 1].rowid);
161
- if (pending.length < batchSize)
162
- break;
355
+ // Inferences past the watermark. The watermark advances after a confirmed
356
+ // send (at-least-once; the sink's upsert makes the duplicate harmless) OR a
357
+ // poison-skip (the batch will never land). Inferences also carry watermarkAt.
358
+ await drainStream(sourceId, (w, n) => store.readInferencesAfter(w, n), (rows) => ({
359
+ inferences: rows.map((row) => toWireInference(row)),
360
+ watermarkAt: new Date().toISOString(),
361
+ }));
362
+ // Feedback, runs, steps, artifacts, edges, evals: the same dance, each
363
+ // behind its own export_state key so no stream ever blocks another — a
364
+ // poison in one is skipped past without touching the rest.
365
+ await drainStream(`${sourceId}:feedback`, (w, n) => store.readFeedbackAfter(w, n), (rows) => ({
366
+ feedback: rows.map((row) => toWireFeedback(row)),
367
+ }));
368
+ // Execution graph (Expansion E2 chunk A) — ONLY when the artifact namespace
369
+ // is active. When artifacts are off these streams never drain and no
370
+ // runs/steps section is ever built (byte-identical to before v2 existed).
371
+ if (config.artifacts === true) {
372
+ await drainStream(`${sourceId}:runs`, (w, n) => store.readRunsAfter(w, n), (rows) => ({
373
+ runs: rows.map((row) => toWireRun(row)),
374
+ }));
375
+ await drainStream(`${sourceId}:steps`, (w, n) => store.readStepsAfter(w, n), (rows) => ({
376
+ steps: rows.map((row) => toWireStep(row)),
377
+ }));
378
+ // Artifact metadata (chunk B): rows live in the SEPARATE artifact store;
379
+ // watermarks live in the main store's export_state, keyed `:artifacts` etc.
380
+ const artStore = config.artifactStore;
381
+ if (artStore !== undefined) {
382
+ await drainStream(`${sourceId}:artifacts`, (w, n) => artStore.readArtifactsAfter(w, n), (rows) => ({
383
+ artifacts: rows.map((row) => toWireArtifact(row)),
384
+ }));
385
+ await drainStream(`${sourceId}:edges`, (w, n) => artStore.readEdgesAfter(w, n), (rows) => ({
386
+ artifact_edges: rows.map((row) => toWireEdge(row)),
387
+ }));
388
+ await drainStream(`${sourceId}:evals`, (w, n) => artStore.readEvaluationsAfter(w, n), (rows) => ({
389
+ artifact_evaluations: rows.map((row) => toWireEvaluation(row)),
390
+ }));
391
+ // Promoted eval cases (E4 chunk A) — same at-least-once dance behind its
392
+ // own watermark key.
393
+ await drainStream(`${sourceId}:eval_cases`, (w, n) => artStore.readEvalCasesAfter(w, n), (rows) => ({
394
+ eval_cases: rows.map((row) => toWireEvalCase(row)),
395
+ }));
396
+ // Post-flush hook (E4 chunk 0, flag 2): the artifacts stream is now
397
+ // confirmed exported (every drainStream above returned without throwing),
398
+ // so any content upload that 404'd racing its own metadata can re-attempt
399
+ // — the row is durable. Reached ONLY on a clean artifact flush; a poison
400
+ // rethrow above skips it, so we never retry content while metadata export
401
+ // is failing. Never throws (its rejection would be swallowed anyway).
402
+ if (config.onArtifactsFlushed !== undefined) {
403
+ await config.onArtifactsFlushed();
404
+ }
405
+ }
163
406
  }
164
- // Envelopes: small table, full upsert each flush. This send also carries
165
- // any header the two loops above never got the chance to — a rig that has
166
- // routes but has not run yet must still appear in the console.
407
+ // Envelopes: small table, full upsert each flush (no watermark). This send
408
+ // also carries any header the streams above never got the chance to — a rig
409
+ // that has routes but has not run yet must still appear in the console.
410
+ // A deterministic failure here poison-skips too (keyed by a synthetic 0
411
+ // watermark, since there is no cursor to advance): after K it is counted +
412
+ // logged and this flush moves on, rather than wedging the header forever.
167
413
  const envelopes = store.listEnvelopes();
168
414
  if (envelopes.length > 0 || Object.keys(header).length > 0) {
169
- await sink.send({
170
- v: ingest_protocol_1.INGEST_PROTOCOL_VERSION,
415
+ const outcome = await sendOrPoison(`${sourceId}:envelopes`, 0, Math.max(envelopes.length, 1), {
416
+ v: wireVersion,
171
417
  source: sourceId,
172
418
  ...header,
173
419
  ...(envelopes.length > 0
@@ -181,7 +427,8 @@ function createTelemetryExporter(store, config) {
181
427
  }
182
428
  : {}),
183
429
  });
184
- markHeaderDelivered(header);
430
+ if (outcome === "sent")
431
+ markHeaderDelivered(header);
185
432
  }
186
433
  }
187
434
  async function guardedFlush() {
@@ -229,5 +476,35 @@ function createTelemetryExporter(store, config) {
229
476
  }
230
477
  await guardedFlush();
231
478
  },
479
+ stats() {
480
+ // A COPY, so a reader can never mutate the exporter's live counters.
481
+ return { poisonSkipped: { ...poisonSkipped } };
482
+ },
483
+ };
484
+ }
485
+ /**
486
+ * The one-shot manifest batch that carries a load-time config failure to the
487
+ * console (follow-up #2), for the case `createRig` throws BEFORE the exporter
488
+ * loop exists (a route bundle that fails to load never reaches the loop).
489
+ *
490
+ * It is a manifest-only batch: no inferences, no watermark. The manifest carries
491
+ * whatever `rig.yaml` could still provide (name/dimensions/contribution — the
492
+ * routes failing to load does not stop rig.yaml loading) plus the load errors;
493
+ * `serveability` is `[]` because it cannot be computed without loaded routes.
494
+ * A caller sends it fire-and-forget through the same sink the run loop uses.
495
+ */
496
+ function buildLoadErrorBatch(sourceId, manifest, loadErrors) {
497
+ return {
498
+ // A manifest-only batch carries no artifact sections — announce v1 so it is
499
+ // accepted by any server vintage, even one predating protocol v2.
500
+ v: ingest_protocol_1.INGEST_PROTOCOL_VERSION_V1,
501
+ source: sourceId,
502
+ manifest: {
503
+ name: manifest?.name ?? sourceId,
504
+ dimensions: manifest?.dimensions ?? [],
505
+ contribution: manifest?.contribution ?? false,
506
+ serveability: [],
507
+ load_errors: [...loadErrors],
508
+ },
232
509
  };
233
510
  }