modelrig 0.8.0 → 0.8.1
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.
- package/CHANGELOG.md +11 -0
- package/README.md +23 -0
- package/dist/cli.js +0 -0
- package/docs/llms.txt +1 -1
- package/docs/quickstart.md +8 -2
- package/package.json +1 -1
- package/dist/telemetry/supabase-exporter.d.ts +0 -64
- package/dist/telemetry/supabase-exporter.d.ts.map +0 -1
- package/dist/telemetry/supabase-exporter.js +0 -211
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,17 @@ published package diverge.
|
|
|
10
10
|
This project adheres to [Semantic Versioning](https://semver.org/). Dates are
|
|
11
11
|
UTC.
|
|
12
12
|
|
|
13
|
+
## [0.8.1] — 2026-08-24
|
|
14
|
+
|
|
15
|
+
Docs — deploy guidance for the native `better-sqlite3` dependency. The SDK loads
|
|
16
|
+
a native SQLite buffer on import (the local telemetry row written before each run
|
|
17
|
+
returns), so the README Install section and `quickstart.md` now tell customers to
|
|
18
|
+
run on a Node runtime (not Edge/Workers), mark `modelrig` external when bundling
|
|
19
|
+
server code (Next.js `serverExternalPackages`, esbuild `--external:better-sqlite3`,
|
|
20
|
+
webpack `externals`), and set `MODELRIG_API_KEY` on every service/worker/cron;
|
|
21
|
+
plus a short monorepo workspace-source caveat. Docs/packaging only — no SDK
|
|
22
|
+
behaviour change.
|
|
23
|
+
|
|
13
24
|
## [0.8.0] — 2026-08-24
|
|
14
25
|
|
|
15
26
|
Packaging (GS-3) — the tarball now ships the customer-facing runs docs
|
package/README.md
CHANGED
|
@@ -22,6 +22,29 @@ npm i modelrig
|
|
|
22
22
|
> macOS and Linux on x64/arm64 (nearly everyone); an unusual platform will
|
|
23
23
|
> compile it at install time and needs a C++ toolchain. Node ≥ 20 is assumed.
|
|
24
24
|
|
|
25
|
+
`modelrig` is a normal dependency: your existing `npm install` (or `pnpm` /
|
|
26
|
+
`yarn`) step installs it **prebuilt** — there is no separate "build modelrig"
|
|
27
|
+
step. Three things to get right when you deploy:
|
|
28
|
+
|
|
29
|
+
- **Run it on a Node runtime, not an Edge/Workers runtime.** `better-sqlite3`
|
|
30
|
+
is a native module and loads on import, so an Edge runtime can't run it. A
|
|
31
|
+
standard Node host — server, container, or a Node serverless function — works
|
|
32
|
+
after `npm install`.
|
|
33
|
+
- **If your deploy _bundles_ server code, mark modelrig external** so the native
|
|
34
|
+
binary is loaded at runtime instead of bundled:
|
|
35
|
+
- **Next.js** — `serverExternalPackages: ['modelrig']` in `next.config.js`
|
|
36
|
+
(Next 13/14: `experimental.serverComponentsExternalPackages`)
|
|
37
|
+
- **esbuild** — `--external:better-sqlite3` (and `--external:modelrig`)
|
|
38
|
+
- **webpack** — add `better-sqlite3` to `externals`
|
|
39
|
+
- **Set `MODELRIG_API_KEY` on every service, worker, and cron** that runs a rig
|
|
40
|
+
— each process reports its own telemetry, so the key belongs on all of them,
|
|
41
|
+
not just your web service.
|
|
42
|
+
|
|
43
|
+
(In a monorepo that vendors ModelRig as a workspace package — the SDK source
|
|
44
|
+
rather than the published tarball — build that package before the app that
|
|
45
|
+
imports it, so its `dist/` types resolve. Consuming the published package needs
|
|
46
|
+
none of this.)
|
|
47
|
+
|
|
25
48
|
## Quickstart
|
|
26
49
|
|
|
27
50
|
This runs with **only a provider key** — no account, no signup.
|
package/dist/cli.js
CHANGED
|
File without changes
|
package/docs/llms.txt
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
> grounding, and caching measured empirically, with a capability-aware
|
|
5
5
|
> gateway that routes by them. Registry data CC BY 4.0; code Apache-2.0.
|
|
6
6
|
|
|
7
|
-
_This reference is as of modelrig v0.8.
|
|
7
|
+
_This reference is as of modelrig v0.8.1 (the published npm package). Any surface below that a newer workspace adds is marked "unreleased — workspace only" until its release; nothing is so marked as of this version._
|
|
8
8
|
|
|
9
9
|
## Docs
|
|
10
10
|
|
package/docs/quickstart.md
CHANGED
|
@@ -87,8 +87,14 @@ reviewed. The full autonomy ladder and same-model guarantees are in the
|
|
|
87
87
|
pnpm add modelrig # or npm i modelrig — Node >= 20
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
-
It
|
|
91
|
-
toolchain.
|
|
90
|
+
It installs a native module (`better-sqlite3`) — prebuilt for macOS/Linux on
|
|
91
|
+
x64/arm64; an unusual platform compiles it and needs a C++ toolchain.
|
|
92
|
+
|
|
93
|
+
**Deploying:** `modelrig` installs prebuilt (no separate build step), but it
|
|
94
|
+
needs a **Node** runtime (not Edge/Workers), and if your deploy bundles server
|
|
95
|
+
code you must mark it external — Next.js `serverExternalPackages: ['modelrig']`,
|
|
96
|
+
esbuild `--external:better-sqlite3`, webpack `externals`. Set `MODELRIG_API_KEY`
|
|
97
|
+
on every service, worker, and cron that runs a rig.
|
|
92
98
|
|
|
93
99
|
```ts
|
|
94
100
|
import { createRig, loadConfigFromEnv } from "modelrig";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "modelrig",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "ModelRig — capability-aware AI gateway: routes-as-code, provider adapters, typed failures, budget envelopes, and local-first telemetry.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Async batch exporter: SQLite telemetry → ModelRig Supabase project.
|
|
3
|
-
* Spec §5 (cloud sink) + §6 criteria 8–9. Fire-and-forget from the run loop:
|
|
4
|
-
* the inference path NEVER blocks on, or fails because of, this sink.
|
|
5
|
-
* At-least-once delivery with idempotent ids; unexported rows queue locally in
|
|
6
|
-
* SQLite (the inferences table itself, behind a rowid watermark) and flush on
|
|
7
|
-
* recovery; watermark bookkeeping per source instance.
|
|
8
|
-
*
|
|
9
|
-
* Transport is raw PostgREST over fetch (dependency-light, service-role key).
|
|
10
|
-
*/
|
|
11
|
-
import type { RigManifest } from "../routes/manifest";
|
|
12
|
-
import type { ServeabilityWarning } from "../routes/serveability";
|
|
13
|
-
import type { ExportableTelemetryStore } from "./store";
|
|
14
|
-
export interface ExportedRouteMirror {
|
|
15
|
-
readonly route: string;
|
|
16
|
-
readonly version: number;
|
|
17
|
-
readonly candidates: ReadonlyArray<{
|
|
18
|
-
readonly provider: string;
|
|
19
|
-
readonly model: string;
|
|
20
|
-
}>;
|
|
21
|
-
/** Capture opt-in FLAG (Phase 3 WS4) — status only; capture content never
|
|
22
|
-
* has a cloud surface. */
|
|
23
|
-
readonly capture?: boolean;
|
|
24
|
-
}
|
|
25
|
-
export interface ExporterConfig {
|
|
26
|
-
readonly supabaseUrl: string;
|
|
27
|
-
/** Service-role key — server-side only; never reaches the console. */
|
|
28
|
-
readonly serviceRoleKey: string;
|
|
29
|
-
readonly sourceId: string;
|
|
30
|
-
/**
|
|
31
|
-
* Tenant for every row this exporter writes (Phase 6 WS1). Defaults to the
|
|
32
|
-
* house org, which is the right answer for a self-hosted control plane —
|
|
33
|
-
* there is exactly one org there and everything belongs to it.
|
|
34
|
-
*
|
|
35
|
-
* This is DIRECT mode only. In api mode the server resolves org and project
|
|
36
|
-
* from the `rig_sk_` key and stamps them itself: a client that could choose
|
|
37
|
-
* its own tenant would not be a tenant boundary (T3).
|
|
38
|
-
*/
|
|
39
|
-
readonly orgId?: string;
|
|
40
|
-
/** Finer scope inside the org. Null is legitimate — not every rig has one. */
|
|
41
|
-
readonly projectId?: string | null;
|
|
42
|
-
readonly batchSize?: number;
|
|
43
|
-
readonly intervalMs?: number;
|
|
44
|
-
/** Loaded route bundles, mirrored to the `routes` table for the console. */
|
|
45
|
-
readonly routes?: readonly ExportedRouteMirror[];
|
|
46
|
-
/** Rig manifest, mirrored to the `rig_manifests` table for the console
|
|
47
|
-
* (dimension NAMES only — tag values never leave the telemetry rows). */
|
|
48
|
-
readonly manifest?: RigManifest | null;
|
|
49
|
-
/** Inert-candidate warnings for THIS deployment (F1) — mirrored onto the
|
|
50
|
-
* manifest row so console /setup can say "2 candidates inert here, set
|
|
51
|
-
* XAI_API_KEY" instead of the fact living only in a boot log. Names only:
|
|
52
|
-
* route, provider, model, env var. */
|
|
53
|
-
readonly serveability?: readonly ServeabilityWarning[];
|
|
54
|
-
/** Injectable for tests; defaults to global fetch. */
|
|
55
|
-
readonly fetchFn?: typeof fetch;
|
|
56
|
-
}
|
|
57
|
-
export interface Exporter {
|
|
58
|
-
start(): void;
|
|
59
|
-
/** Flush pending rows now (used by tests and process-exit hooks). */
|
|
60
|
-
flush(): Promise<void>;
|
|
61
|
-
stop(): Promise<void>;
|
|
62
|
-
}
|
|
63
|
-
export declare function createSupabaseExporter(store: ExportableTelemetryStore, config: ExporterConfig): Exporter;
|
|
64
|
-
//# sourceMappingURL=supabase-exporter.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"supabase-exporter.d.ts","sourceRoot":"","sources":["../../src/telemetry/supabase-exporter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAIlE,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,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,sEAAsE;IACtE,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;;;;;;OAQG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,8EAA8E;IAC9E,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,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;IACvD,sDAAsD;IACtD,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACjC;AAED,MAAM,WAAW,QAAQ;IACvB,KAAK,IAAI,IAAI,CAAC;IACd,qEAAqE;IACrE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAiFD,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,wBAAwB,EAC/B,MAAM,EAAE,cAAc,GACrB,QAAQ,CA4KV"}
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Async batch exporter: SQLite telemetry → ModelRig Supabase project.
|
|
4
|
-
* Spec §5 (cloud sink) + §6 criteria 8–9. Fire-and-forget from the run loop:
|
|
5
|
-
* the inference path NEVER blocks on, or fails because of, this sink.
|
|
6
|
-
* At-least-once delivery with idempotent ids; unexported rows queue locally in
|
|
7
|
-
* SQLite (the inferences table itself, behind a rowid watermark) and flush on
|
|
8
|
-
* recovery; watermark bookkeeping per source instance.
|
|
9
|
-
*
|
|
10
|
-
* Transport is raw PostgREST over fetch (dependency-light, service-role key).
|
|
11
|
-
*/
|
|
12
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
-
exports.createSupabaseExporter = createSupabaseExporter;
|
|
14
|
-
const constants_1 = require("../tenancy/constants");
|
|
15
|
-
const DEFAULT_BATCH_SIZE = 200;
|
|
16
|
-
const DEFAULT_INTERVAL_MS = 10000;
|
|
17
|
-
/** After a sink failure, skip this many intervals, doubling per consecutive
|
|
18
|
-
* failure (simple exponential backoff, capped). */
|
|
19
|
-
const BACKOFF_MAX_SKIPS = 32;
|
|
20
|
-
function toRemote(row, tenancy) {
|
|
21
|
-
return {
|
|
22
|
-
...tenancy,
|
|
23
|
-
id: row.id,
|
|
24
|
-
ts: row.ts,
|
|
25
|
-
route: row.route,
|
|
26
|
-
route_version: row.routeVersion,
|
|
27
|
-
provider: row.provider,
|
|
28
|
-
model: row.model,
|
|
29
|
-
requested_tier: row.requestedTier,
|
|
30
|
-
served_tier: row.servedTier,
|
|
31
|
-
tokens_in: row.tokensIn,
|
|
32
|
-
tokens_out: row.tokensOut,
|
|
33
|
-
tokens_cached: row.tokensCached,
|
|
34
|
-
cost_estimate_usd: row.costEstimateUsd,
|
|
35
|
-
latency_ms: row.latencyMs,
|
|
36
|
-
ttfb_ms: row.ttfbMs,
|
|
37
|
-
failure_class: row.failureClass,
|
|
38
|
-
attempts: JSON.parse(row.attemptsJson),
|
|
39
|
-
tags: JSON.parse(row.tagsJson),
|
|
40
|
-
fingerprint_cell: row.fingerprintCell,
|
|
41
|
-
served_variant: row.servedVariant,
|
|
42
|
-
repaired_by: row.repairedBy,
|
|
43
|
-
pricing_missing: row.pricingMissing,
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
function feedbackToRemote(row, tenancy) {
|
|
47
|
-
return {
|
|
48
|
-
org_id: tenancy.org_id,
|
|
49
|
-
project_id: tenancy.project_id,
|
|
50
|
-
id: row.id,
|
|
51
|
-
ts: row.ts,
|
|
52
|
-
inference_id: row.inferenceId,
|
|
53
|
-
episode_key: row.episodeKey,
|
|
54
|
-
verdict: row.verdict,
|
|
55
|
-
score: row.score,
|
|
56
|
-
note: row.note,
|
|
57
|
-
source: row.source,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
function createSupabaseExporter(store, config) {
|
|
61
|
-
const batchSize = config.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
62
|
-
const intervalMs = config.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
63
|
-
const fetchFn = config.fetchFn ?? fetch;
|
|
64
|
-
const base = config.supabaseUrl.replace(/\/$/, "");
|
|
65
|
-
const tenancy = {
|
|
66
|
-
org_id: config.orgId ?? constants_1.HOUSE_ORG_ID,
|
|
67
|
-
project_id: config.projectId ?? null,
|
|
68
|
-
source_id: config.sourceId,
|
|
69
|
-
};
|
|
70
|
-
let timer = null;
|
|
71
|
-
let flushing = false;
|
|
72
|
-
let skipsRemaining = 0;
|
|
73
|
-
let consecutiveFailures = 0;
|
|
74
|
-
let routesMirrored = false;
|
|
75
|
-
let manifestMirrored = false;
|
|
76
|
-
async function post(table, body, upsert) {
|
|
77
|
-
const response = await fetchFn(`${base}/rest/v1/${table}`, {
|
|
78
|
-
method: "POST",
|
|
79
|
-
headers: {
|
|
80
|
-
"content-type": "application/json",
|
|
81
|
-
apikey: config.serviceRoleKey,
|
|
82
|
-
authorization: `Bearer ${config.serviceRoleKey}`,
|
|
83
|
-
prefer: upsert
|
|
84
|
-
? "resolution=merge-duplicates,return=minimal"
|
|
85
|
-
: "resolution=ignore-duplicates,return=minimal",
|
|
86
|
-
},
|
|
87
|
-
body: JSON.stringify(body),
|
|
88
|
-
});
|
|
89
|
-
if (!response.ok) {
|
|
90
|
-
throw new Error(`sink POST ${table} → ${response.status} ${await response.text().catch(() => "")}`);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
async function flushOnce() {
|
|
94
|
-
// Routes mirror first (idempotent upsert; retried until it succeeds once).
|
|
95
|
-
if (!routesMirrored && config.routes !== undefined && config.routes.length > 0) {
|
|
96
|
-
await post("routes", config.routes.map((r) => ({
|
|
97
|
-
org_id: tenancy.org_id,
|
|
98
|
-
route: r.route,
|
|
99
|
-
version: r.version,
|
|
100
|
-
candidates: r.candidates,
|
|
101
|
-
capture: r.capture ?? false,
|
|
102
|
-
updated_at: new Date().toISOString(),
|
|
103
|
-
})), true);
|
|
104
|
-
routesMirrored = true;
|
|
105
|
-
}
|
|
106
|
-
// Manifest mirror (idempotent upsert, once per process — like routes).
|
|
107
|
-
if (!manifestMirrored && config.manifest !== undefined && config.manifest !== null) {
|
|
108
|
-
await post("rig_manifests", [
|
|
109
|
-
{
|
|
110
|
-
org_id: tenancy.org_id,
|
|
111
|
-
source_id: config.sourceId,
|
|
112
|
-
name: config.manifest.name,
|
|
113
|
-
dimensions: config.manifest.dimensions,
|
|
114
|
-
contribution: config.manifest.contribution,
|
|
115
|
-
// F1: names only — route, provider/model, and the env var to set.
|
|
116
|
-
serveability: (config.serveability ?? []).map((w) => ({
|
|
117
|
-
route: w.route,
|
|
118
|
-
inert: w.inert.map((c) => ({ provider: c.provider, model: c.model })),
|
|
119
|
-
serveable_count: w.serveableCount,
|
|
120
|
-
fix_hint: w.fixHint,
|
|
121
|
-
})),
|
|
122
|
-
updated_at: new Date().toISOString(),
|
|
123
|
-
},
|
|
124
|
-
], true);
|
|
125
|
-
manifestMirrored = true;
|
|
126
|
-
}
|
|
127
|
-
// Inferences past the watermark, in batches. Watermark advances ONLY
|
|
128
|
-
// after a confirmed write (at-least-once; remote id conflicts ignored).
|
|
129
|
-
for (;;) {
|
|
130
|
-
const watermark = store.getExportWatermark(config.sourceId);
|
|
131
|
-
const pending = store.readInferencesAfter(watermark, batchSize);
|
|
132
|
-
if (pending.length === 0)
|
|
133
|
-
break;
|
|
134
|
-
await post("inferences", pending.map(({ row }) => toRemote(row, tenancy)), false);
|
|
135
|
-
store.setExportWatermark(config.sourceId, pending[pending.length - 1].rowid);
|
|
136
|
-
await post("export_watermarks", [{ org_id: tenancy.org_id, source_id: config.sourceId, last_exported_ts: new Date().toISOString() }], true);
|
|
137
|
-
if (pending.length < batchSize)
|
|
138
|
-
break;
|
|
139
|
-
}
|
|
140
|
-
// Feedback mirror (Phase 4 WS2): same at-least-once watermark dance as
|
|
141
|
-
// inferences, behind its own export_state key so the two streams never
|
|
142
|
-
// block each other.
|
|
143
|
-
const feedbackSource = `${config.sourceId}:feedback`;
|
|
144
|
-
for (;;) {
|
|
145
|
-
const watermark = store.getExportWatermark(feedbackSource);
|
|
146
|
-
const pending = store.readFeedbackAfter(watermark, batchSize);
|
|
147
|
-
if (pending.length === 0)
|
|
148
|
-
break;
|
|
149
|
-
await post("feedback", pending.map(({ row }) => feedbackToRemote(row, tenancy)), false);
|
|
150
|
-
store.setExportWatermark(feedbackSource, pending[pending.length - 1].rowid);
|
|
151
|
-
if (pending.length < batchSize)
|
|
152
|
-
break;
|
|
153
|
-
}
|
|
154
|
-
// Envelopes: small table, full upsert each flush.
|
|
155
|
-
const envelopes = store.listEnvelopes();
|
|
156
|
-
if (envelopes.length > 0) {
|
|
157
|
-
await post("envelopes", envelopes.map((e) => ({
|
|
158
|
-
org_id: tenancy.org_id,
|
|
159
|
-
envelope_key: e.envelopeKey,
|
|
160
|
-
budget_usd: e.budgetUsd,
|
|
161
|
-
spent_usd: e.spentUsd,
|
|
162
|
-
status: e.status,
|
|
163
|
-
exported_at: new Date().toISOString(),
|
|
164
|
-
})), true);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
async function guardedFlush() {
|
|
168
|
-
if (flushing)
|
|
169
|
-
return;
|
|
170
|
-
flushing = true;
|
|
171
|
-
try {
|
|
172
|
-
await flushOnce();
|
|
173
|
-
consecutiveFailures = 0;
|
|
174
|
-
}
|
|
175
|
-
catch (err) {
|
|
176
|
-
// Swallow-and-log, ALWAYS — sink failures must never reach the run loop.
|
|
177
|
-
consecutiveFailures += 1;
|
|
178
|
-
skipsRemaining = Math.min(2 ** consecutiveFailures, BACKOFF_MAX_SKIPS);
|
|
179
|
-
console.warn(`[modelrig exporter] sink flush failed (attempt ${consecutiveFailures}, backing off ` +
|
|
180
|
-
`${skipsRemaining} intervals): ${err instanceof Error ? err.message : String(err)}`);
|
|
181
|
-
}
|
|
182
|
-
finally {
|
|
183
|
-
flushing = false;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
return {
|
|
187
|
-
start() {
|
|
188
|
-
if (timer !== null)
|
|
189
|
-
return;
|
|
190
|
-
timer = setInterval(() => {
|
|
191
|
-
if (skipsRemaining > 0) {
|
|
192
|
-
skipsRemaining -= 1;
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
void guardedFlush();
|
|
196
|
-
}, intervalMs);
|
|
197
|
-
// Never keep the process alive for telemetry export.
|
|
198
|
-
timer.unref?.();
|
|
199
|
-
},
|
|
200
|
-
async flush() {
|
|
201
|
-
await guardedFlush();
|
|
202
|
-
},
|
|
203
|
-
async stop() {
|
|
204
|
-
if (timer !== null) {
|
|
205
|
-
clearInterval(timer);
|
|
206
|
-
timer = null;
|
|
207
|
-
}
|
|
208
|
-
await guardedFlush();
|
|
209
|
-
},
|
|
210
|
-
};
|
|
211
|
-
}
|