@sema-ai/registry-core 0.10.3
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 +131 -0
- package/README.md +84 -0
- package/dist/api/auth-bridge.d.ts +332 -0
- package/dist/api/auth-bridge.d.ts.map +1 -0
- package/dist/api/auth-bridge.js +211 -0
- package/dist/api/auth-bridge.js.map +1 -0
- package/dist/api/auth.d.ts +258 -0
- package/dist/api/auth.d.ts.map +1 -0
- package/dist/api/auth.js +204 -0
- package/dist/api/auth.js.map +1 -0
- package/dist/api/scopes.d.ts +353 -0
- package/dist/api/scopes.d.ts.map +1 -0
- package/dist/api/scopes.js +229 -0
- package/dist/api/scopes.js.map +1 -0
- package/dist/bundle.d.ts +14 -0
- package/dist/bundle.d.ts.map +1 -0
- package/dist/bundle.js +68 -0
- package/dist/bundle.js.map +1 -0
- package/dist/config-fns.d.ts +236 -0
- package/dist/config-fns.d.ts.map +1 -0
- package/dist/config-fns.js +272 -0
- package/dist/config-fns.js.map +1 -0
- package/dist/cross-domain.d.ts +35 -0
- package/dist/cross-domain.d.ts.map +1 -0
- package/dist/cross-domain.js +119 -0
- package/dist/cross-domain.js.map +1 -0
- package/dist/file-edit.d.ts +37 -0
- package/dist/file-edit.d.ts.map +1 -0
- package/dist/file-edit.js +126 -0
- package/dist/file-edit.js.map +1 -0
- package/dist/file-store.d.ts +39 -0
- package/dist/file-store.d.ts.map +1 -0
- package/dist/file-store.js +142 -0
- package/dist/file-store.js.map +1 -0
- package/dist/fleet.d.ts +479 -0
- package/dist/fleet.d.ts.map +1 -0
- package/dist/fleet.js +303 -0
- package/dist/fleet.js.map +1 -0
- package/dist/hash.d.ts +33 -0
- package/dist/hash.d.ts.map +1 -0
- package/dist/hash.js +60 -0
- package/dist/hash.js.map +1 -0
- package/dist/hooks.d.ts +5478 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +628 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/local-load.d.ts +33 -0
- package/dist/local-load.d.ts.map +1 -0
- package/dist/local-load.js +164 -0
- package/dist/local-load.js.map +1 -0
- package/dist/migrate.d.ts +245 -0
- package/dist/migrate.d.ts.map +1 -0
- package/dist/migrate.js +543 -0
- package/dist/migrate.js.map +1 -0
- package/dist/node.d.ts +12 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +12 -0
- package/dist/node.js.map +1 -0
- package/dist/reader.d.ts +19 -0
- package/dist/reader.d.ts.map +1 -0
- package/dist/reader.js +2 -0
- package/dist/reader.js.map +1 -0
- package/dist/remote-exec.d.ts +275 -0
- package/dist/remote-exec.d.ts.map +1 -0
- package/dist/remote-exec.js +183 -0
- package/dist/remote-exec.js.map +1 -0
- package/dist/resolve-roster.d.ts +29 -0
- package/dist/resolve-roster.d.ts.map +1 -0
- package/dist/resolve-roster.js +105 -0
- package/dist/resolve-roster.js.map +1 -0
- package/dist/safety-merge-spec.d.ts +328 -0
- package/dist/safety-merge-spec.d.ts.map +1 -0
- package/dist/safety-merge-spec.js +65 -0
- package/dist/safety-merge-spec.js.map +1 -0
- package/dist/scheduler-store-node.d.ts +15 -0
- package/dist/scheduler-store-node.d.ts.map +1 -0
- package/dist/scheduler-store-node.js +43 -0
- package/dist/scheduler-store-node.js.map +1 -0
- package/dist/scheduler-store.d.ts +65 -0
- package/dist/scheduler-store.d.ts.map +1 -0
- package/dist/scheduler-store.js +59 -0
- package/dist/scheduler-store.js.map +1 -0
- package/dist/secret-refs.d.ts +34 -0
- package/dist/secret-refs.d.ts.map +1 -0
- package/dist/secret-refs.js +49 -0
- package/dist/secret-refs.js.map +1 -0
- package/dist/sha256.d.ts +17 -0
- package/dist/sha256.d.ts.map +1 -0
- package/dist/sha256.js +115 -0
- package/dist/sha256.js.map +1 -0
- package/dist/skills-manifest.d.ts +13 -0
- package/dist/skills-manifest.d.ts.map +1 -0
- package/dist/skills-manifest.js +55 -0
- package/dist/skills-manifest.js.map +1 -0
- package/dist/types.d.ts +9911 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1727 -0
- package/dist/types.js.map +1 -0
- package/package.json +130 -0
package/dist/fleet.js
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sema-ai/registry-core/fleet — the PURE scheduling/fleet contract for the sema-center control plane
|
|
3
|
+
* (design: cc-parity CONTROL-PLANE-SCHEDULING-DESIGN.md, clay 拍 2026-07-04 — "one contract, three orchestrators").
|
|
4
|
+
*
|
|
5
|
+
* Seven domains (rolling update / discovery / traffic / failover / LB / quota / token governance) are ONE
|
|
6
|
+
* data-plane lifecycle contract reused by three deployment forms — whoever HOLDS the engine handle orchestrates,
|
|
7
|
+
* but all three read THESE shapes:
|
|
8
|
+
* - TOC shell (engineLifecycleManager): health readiness + upgrade classification + local blue-green demo
|
|
9
|
+
* - TOB (user k8s / fixed baseurl): preStop→drain semantics + readiness probe = !draining
|
|
10
|
+
* - center (orchestrator reconciler): instance discovery table + /api/dispatch gateway + rolling primitive
|
|
11
|
+
*
|
|
12
|
+
* 🔴 Contract discipline (same rules as scheduler-store):
|
|
13
|
+
* - ABSTRACT: types + zod wire shapes + PURE decision functions. ZERO IO, ZERO node:* — browser-safe barrel.
|
|
14
|
+
* - FINAL-STATE: shapes codify what is SHIPPED on the wire (server 1.96.0 drain: /health `version`/`draining`/
|
|
15
|
+
* `drainingSince`, billable submit 503+Retry-After) — no transitional aliases, breaking changes over shims
|
|
16
|
+
* (clay 拍: no temporary compatibility design; the target is long-term stability).
|
|
17
|
+
* - EXTENSIBLE: enums + optional fields externalize decisions (add an LB strategy = add an enum value,
|
|
18
|
+
* the skeleton does not move).
|
|
19
|
+
*/
|
|
20
|
+
import { z } from "zod";
|
|
21
|
+
import { fnv1a64 } from "./hash.js";
|
|
22
|
+
// ── Domain 1: drain protocol (graceful rolling update — the ground every form stands on) ──────────────────
|
|
23
|
+
/** Lifecycle of a draining instance. `running`→`draining` on the stop signal; `drained` once in-flight hits
|
|
24
|
+
* zero (clean) or the grace deadline forces the issue. Exit always follows `drained`. */
|
|
25
|
+
export const DRAIN_PHASES = ["running", "draining", "drained"];
|
|
26
|
+
/** Shipped default (server 1.96.0): generous by design — the only unrecoverable loss on restart is the
|
|
27
|
+
* interactive turn currently streaming, so we wait long rather than cut short. Deployments override via
|
|
28
|
+
* DRAIN_GRACE_MS; center overrides per-worker through the worker spec. */
|
|
29
|
+
export const DEFAULT_DRAIN_GRACE_MS = 600_000;
|
|
30
|
+
/** Hard floor — a grace below this is treated as this (protects against a fat-fingered 0). */
|
|
31
|
+
export const DRAIN_GRACE_FLOOR_MS = 10_000;
|
|
32
|
+
/** `Retry-After` (seconds) on the 503 a draining instance returns for NEW billable submissions. Read paths,
|
|
33
|
+
* steer/cancel/respond stay open — only new work is refused. */
|
|
34
|
+
export const DRAIN_RETRY_AFTER_SECONDS = 15;
|
|
35
|
+
/** The drain state machine's single decision point: keep waiting, exit clean (drained), or force the exit
|
|
36
|
+
* (grace exhausted with legs still in flight — durable legs resume cross-instance via resumeFromRunId). */
|
|
37
|
+
export function drainVerdict(s) {
|
|
38
|
+
if (s.inflight <= 0)
|
|
39
|
+
return "exit_clean";
|
|
40
|
+
const grace = Math.max(DRAIN_GRACE_FLOOR_MS, s.graceMs);
|
|
41
|
+
return s.nowMs - s.startedMs >= grace ? "exit_forced" : "wait";
|
|
42
|
+
}
|
|
43
|
+
/** The `/health` self-description every orchestrator reads (shipped shape, server 1.96.0). Liveness and
|
|
44
|
+
* readiness are deliberately SPLIT: `status` stays ok while draining (the process is alive); routability is
|
|
45
|
+
* `isReadyForTraffic`. Unknown fields pass through — health is a self-description, not a closed record. */
|
|
46
|
+
export const InstanceHealth = z
|
|
47
|
+
.object({
|
|
48
|
+
instanceId: z.string().min(1),
|
|
49
|
+
/** build self-description — "did the restart actually land new code" (upgrade classification) */
|
|
50
|
+
version: z.string().min(1),
|
|
51
|
+
/** absent = not draining */
|
|
52
|
+
draining: z.boolean().optional(),
|
|
53
|
+
/** epoch ms, present iff draining */
|
|
54
|
+
drainingSince: z.number().optional(),
|
|
55
|
+
})
|
|
56
|
+
.passthrough();
|
|
57
|
+
/** Readiness = the routability predicate (k8s readiness probe, gateway health filter, shell seam switch). */
|
|
58
|
+
export function isReadyForTraffic(h) {
|
|
59
|
+
return h.draining !== true;
|
|
60
|
+
}
|
|
61
|
+
// ── Domain 2: instance discovery (worker announces itself; reconciler observation is the SECOND source) ───
|
|
62
|
+
/** Heartbeat cadence the announcement declares (center judges staleness against the DECLARED cadence, so a
|
|
63
|
+
* slow-heartbeat worker is not falsely reaped). */
|
|
64
|
+
export const DEFAULT_HEARTBEAT_MS = 30_000;
|
|
65
|
+
/** An instance is stale once silent for this many missed heartbeats (aligned with the reconciler's own
|
|
66
|
+
* stale window: 2× cadence, not a fixed wall-clock). */
|
|
67
|
+
export const STALE_HEARTBEAT_MULTIPLIER = 2;
|
|
68
|
+
/** Register/heartbeat upsert (worker → center, D2: worker-active). ONE shape for both — registration is the
|
|
69
|
+
* first heartbeat; every heartbeat re-announces the full row (idempotent upsert, no drift between a
|
|
70
|
+
* "register" and a "renew" schema). `draining:true` is the instance SELF-removing from the routable set the
|
|
71
|
+
* moment the stop signal lands — faster than any observer. */
|
|
72
|
+
export const InstanceAnnouncement = z.object({
|
|
73
|
+
instanceId: z.string().min(1),
|
|
74
|
+
/** worker (deployment) this instance belongs to — the dispatch key */
|
|
75
|
+
worker: z.string().min(1),
|
|
76
|
+
/** reachable base URL for the deployment form: k8s pod IP:port / docker net alias / 127.0.0.1:<engine.port> */
|
|
77
|
+
address: z.string().min(1),
|
|
78
|
+
version: z.string().min(1),
|
|
79
|
+
/** self-declared: bootDone && !draining */
|
|
80
|
+
ready: z.boolean(),
|
|
81
|
+
draining: z.boolean().default(false),
|
|
82
|
+
heartbeatMs: z.number().int().positive().default(DEFAULT_HEARTBEAT_MS),
|
|
83
|
+
});
|
|
84
|
+
/** Explicit goodbye (stop signal handler) — removes the row immediately instead of waiting out staleness. */
|
|
85
|
+
export const InstanceDeregistration = z.object({
|
|
86
|
+
instanceId: z.string().min(1),
|
|
87
|
+
worker: z.string().min(1),
|
|
88
|
+
reason: z.enum(["draining", "shutdown", "superseded"]).optional(),
|
|
89
|
+
});
|
|
90
|
+
/** The stored discovery-table row = last announcement + center-side bookkeeping. `observedReady` is the
|
|
91
|
+
* reconciler's observation (phase Running) — the SECOND source; absent where no reconciler exists (TOC/TOB). */
|
|
92
|
+
export const InstanceRegistration = InstanceAnnouncement.extend({
|
|
93
|
+
registeredMs: z.number().int().nonnegative(),
|
|
94
|
+
lastSeenMs: z.number().int().nonnegative(),
|
|
95
|
+
observedReady: z.boolean().optional(),
|
|
96
|
+
});
|
|
97
|
+
/** Silent past the declared cadence × multiplier ⇒ stale (auto-removed from the routable set). */
|
|
98
|
+
export function isInstanceStale(r, nowMs) {
|
|
99
|
+
return nowMs - r.lastSeenMs > STALE_HEARTBEAT_MULTIPLIER * r.heartbeatMs;
|
|
100
|
+
}
|
|
101
|
+
/** Health fusion: routable ⇔ self-declared ready ∧ not draining ∧ not stale ∧ observer does not veto.
|
|
102
|
+
* Two sources merge as an AND — EITHER source's "not ready" removes the instance (fail-closed). */
|
|
103
|
+
export function isRoutable(r, nowMs) {
|
|
104
|
+
return r.ready && !r.draining && r.observedReady !== false && !isInstanceStale(r, nowMs);
|
|
105
|
+
}
|
|
106
|
+
export function routableInstances(rs, nowMs) {
|
|
107
|
+
return rs.filter((r) => isRoutable(r, nowMs));
|
|
108
|
+
}
|
|
109
|
+
// ── Domain 3: dispatch policy (traffic + failover + LB — three domains, one gateway) ──────────────────────
|
|
110
|
+
export const DISPATCH_STRATEGIES = ["round-robin", "least-inflight", "consistent-hash"];
|
|
111
|
+
/** Per-worker dispatch policy (center-configured; the gateway is OPTIONAL — D3: TOC/self-maintained TOB go
|
|
112
|
+
* direct, sema-managed fleets route through /api/dispatch). Strategy is an enum on purpose: a new LB
|
|
113
|
+
* algorithm is a new value, not a new skeleton. */
|
|
114
|
+
export const DispatchPolicy = z.object({
|
|
115
|
+
strategy: z.enum(DISPATCH_STRATEGIES).default("round-robin"),
|
|
116
|
+
/** live-steer legs must return to their instance (cross-replica live-steer is 409 by design) — when true,
|
|
117
|
+
* a request carrying an affinity key pins via consistent hash regardless of `strategy`. If the pinned
|
|
118
|
+
* instance dies, the run resumes elsewhere via resumeFromRunId (affinity avoids the gap; resume bounds it). */
|
|
119
|
+
sessionAffinity: z.boolean().default(true),
|
|
120
|
+
/** gateway-level concurrency gate (per worker) */
|
|
121
|
+
maxInflight: z.number().int().positive().optional(),
|
|
122
|
+
/** per-actor fairness gate */
|
|
123
|
+
perActorMaxInflight: z.number().int().positive().optional(),
|
|
124
|
+
/** cost clamp applied to submissions passing the gateway */
|
|
125
|
+
maxCostUsd: z.number().finite().nonnegative().optional(),
|
|
126
|
+
});
|
|
127
|
+
/** 🔴 Submit idempotency — the precondition that makes failover re-routing SAFE: a new-run submission MUST
|
|
128
|
+
* carry the client-minted durable taskId, and the server dedupes on it. Without this, "landed but the
|
|
129
|
+
* response was lost" re-routes into a duplicate run. Re-route freely: reads (always) and submissions
|
|
130
|
+
* (iff keyed). In-flight interactive turns are NEVER auto-retried (tool side effects are not idempotent). */
|
|
131
|
+
export const SUBMIT_IDEMPOTENCY_KEY = "taskId";
|
|
132
|
+
/** Stable uint32 of the affinity key (fnv1a64 hex head — single hashing source with the rest of the contract). */
|
|
133
|
+
function affinityHash(key) {
|
|
134
|
+
return parseInt(fnv1a64(key).slice(0, 8), 16) >>> 0;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The single instance-selection decision, PURE and deterministic:
|
|
138
|
+
* - candidates are sorted by instanceId first ⇒ the pick is stable under input reordering;
|
|
139
|
+
* - `affinityKey` (+ sessionAffinity or consistent-hash strategy) ⇒ stable hash pick — same key, same
|
|
140
|
+
* instance, for as long as the candidate set holds (live-steer affinity);
|
|
141
|
+
* - round-robin consumes `cursor` (caller-held monotonic counter);
|
|
142
|
+
* - least-inflight breaks ties by lowest instanceId.
|
|
143
|
+
* Returns null on an empty candidate set (caller surfaces "no routable instance").
|
|
144
|
+
*/
|
|
145
|
+
export function pickInstance(policy, candidates, opts = {}) {
|
|
146
|
+
if (candidates.length === 0)
|
|
147
|
+
return null;
|
|
148
|
+
const sorted = [...candidates].sort((a, b) => (a.instanceId < b.instanceId ? -1 : a.instanceId > b.instanceId ? 1 : 0));
|
|
149
|
+
if (opts.affinityKey !== undefined && (policy.sessionAffinity || policy.strategy === "consistent-hash")) {
|
|
150
|
+
return sorted[affinityHash(opts.affinityKey) % sorted.length];
|
|
151
|
+
}
|
|
152
|
+
switch (policy.strategy) {
|
|
153
|
+
case "least-inflight": {
|
|
154
|
+
let best = sorted[0];
|
|
155
|
+
for (const c of sorted)
|
|
156
|
+
if ((c.inflight ?? 0) < (best.inflight ?? 0))
|
|
157
|
+
best = c;
|
|
158
|
+
return best;
|
|
159
|
+
}
|
|
160
|
+
case "consistent-hash": // no key supplied — degrade to round-robin rather than pin everyone to one node
|
|
161
|
+
case "round-robin":
|
|
162
|
+
return sorted[(opts.cursor ?? 0) % sorted.length];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// ── Domain 4: quota lease (D4: AP by default — pre-authorized grant + reconciliation, CP is per-tier opt-in) ─
|
|
166
|
+
/** A pre-authorized slice of a principal's global budget, granted by center to ONE worker: the worker deducts
|
|
167
|
+
* locally (no per-call round-trip), renews when running low, and center reconciles reported consumption
|
|
168
|
+
* against the grant after the fact — the roaming/CDN pattern.
|
|
169
|
+
*
|
|
170
|
+
* 0.6.1 双轴 slice(EXPERT-REDESIGN §4-2/§4-6,黑板 [542] service 回执"发布即接单"):slice = **{weightedTokens,
|
|
171
|
+
* costMicroUsd}** 双轴,带 TTL 不变。token 轴单位=**加权 tokens**(配额轴,ModelEntry.quotaWeight 的
|
|
172
|
+
* weight-at-burn 折算——与 UsageEntry.weightedTokens 同一时点同口径,扣减与聚合两路径不歪)。判定语义在
|
|
173
|
+
* 类型层定死:**任一轴耗尽 = 拒绝**({@link leaseExhausted} deny-wins)、**任一轴达 80% = 提前续租**
|
|
174
|
+
* ({@link shouldRenewLease})。$ 轴名 `costMicroUsd` 沿用 0.5 既定名不改(service $ 单轴已 SHIPPED)。
|
|
175
|
+
* 0.7.1(BREAKING,黑板 [551] 回执收账):0.5 轴名 `tokens` 的 preprocess shim 已撤——service 1.150.0
|
|
176
|
+
* 已切 typed 双轴,legacy 别名成死分支;老形状租约 parse 后 tokens 键被 strip(= 该轴不受此租约治理)。 */
|
|
177
|
+
export const QuotaLease = z.object({
|
|
178
|
+
leaseId: z.string().min(1),
|
|
179
|
+
principal: z.string().min(1),
|
|
180
|
+
worker: z.string().min(1),
|
|
181
|
+
/** granted dimensions — either or both; a dimension absent = not governed by this lease.
|
|
182
|
+
* `weightedTokens` = 加权 tokens(配额轴)。 */
|
|
183
|
+
grant: z.object({
|
|
184
|
+
weightedTokens: z.number().int().nonnegative().optional(),
|
|
185
|
+
costMicroUsd: z.number().int().nonnegative().optional(),
|
|
186
|
+
}),
|
|
187
|
+
consumed: z
|
|
188
|
+
.object({
|
|
189
|
+
weightedTokens: z.number().int().nonnegative().default(0),
|
|
190
|
+
costMicroUsd: z.number().int().nonnegative().default(0),
|
|
191
|
+
})
|
|
192
|
+
.default({ weightedTokens: 0, costMicroUsd: 0 }),
|
|
193
|
+
issuedMs: z.number().int().nonnegative(),
|
|
194
|
+
expiresMs: z.number().int().nonnegative(),
|
|
195
|
+
});
|
|
196
|
+
/** Remaining per granted dimension; null = the dimension is not governed by this lease. Never negative. */
|
|
197
|
+
export function leaseRemaining(l) {
|
|
198
|
+
return {
|
|
199
|
+
weightedTokens: l.grant.weightedTokens === undefined ? null : Math.max(0, l.grant.weightedTokens - l.consumed.weightedTokens),
|
|
200
|
+
costMicroUsd: l.grant.costMicroUsd === undefined ? null : Math.max(0, l.grant.costMicroUsd - l.consumed.costMicroUsd),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/** Exhausted ⇔ ANY governed dimension is used up (deny-wins across the two axes — 任一轴耗尽即拒), or the
|
|
204
|
+
* lease expired. */
|
|
205
|
+
export function leaseExhausted(l, nowMs) {
|
|
206
|
+
if (nowMs >= l.expiresMs)
|
|
207
|
+
return true;
|
|
208
|
+
const r = leaseRemaining(l);
|
|
209
|
+
return (r.weightedTokens !== null && r.weightedTokens <= 0) || (r.costMicroUsd !== null && r.costMicroUsd <= 0);
|
|
210
|
+
}
|
|
211
|
+
/** Renew EARLY — at `renewAtFraction` of ANY governed dimension (default 80% — 任一轴达 80% 即提前续租) or
|
|
212
|
+
* of the lease lifetime — so the refresh round-trip overlaps remaining headroom instead of blocking a call
|
|
213
|
+
* at zero. */
|
|
214
|
+
export function shouldRenewLease(l, nowMs, opts = {}) {
|
|
215
|
+
const f = opts.renewAtFraction ?? 0.8;
|
|
216
|
+
if (l.grant.weightedTokens !== undefined && l.grant.weightedTokens > 0 && l.consumed.weightedTokens / l.grant.weightedTokens >= f)
|
|
217
|
+
return true;
|
|
218
|
+
if (l.grant.costMicroUsd !== undefined && l.grant.costMicroUsd > 0 && l.consumed.costMicroUsd / l.grant.costMicroUsd >= f)
|
|
219
|
+
return true;
|
|
220
|
+
const life = l.expiresMs - l.issuedMs;
|
|
221
|
+
return life > 0 && (nowMs - l.issuedMs) / life >= f;
|
|
222
|
+
}
|
|
223
|
+
/** Reconciliation (center side): consumption is MONOTONE — the reconciled row takes the max of what center
|
|
224
|
+
* believed and what the worker reported (a lost report can only under-count, never roll back).
|
|
225
|
+
* 0.7.1:legacy `tokens` report alias removed with the QuotaLease shim(同一收账点,[551] 回执). */
|
|
226
|
+
export function reconcileLease(l, reported) {
|
|
227
|
+
return {
|
|
228
|
+
...l,
|
|
229
|
+
consumed: {
|
|
230
|
+
weightedTokens: Math.max(l.consumed.weightedTokens, reported.weightedTokens ?? 0),
|
|
231
|
+
costMicroUsd: Math.max(l.consumed.costMicroUsd, reported.costMicroUsd ?? 0),
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
// ── Domain 5: usage report (worker → center — the metering feed the global view aggregates) ───────────────
|
|
236
|
+
/** One principal×model line in a report window (mirrors the worker's modelUsageTracker accumulation). */
|
|
237
|
+
export const UsageEntry = z.object({
|
|
238
|
+
principal: z.string().min(1),
|
|
239
|
+
model: z.string().min(1),
|
|
240
|
+
inputTokens: z.number().int().nonnegative(),
|
|
241
|
+
outputTokens: z.number().int().nonnegative(),
|
|
242
|
+
costMicroUsd: z.number().int().nonnegative(),
|
|
243
|
+
runs: z.number().int().nonnegative().optional(),
|
|
244
|
+
/** 加权 tokens(配额轴计账值,EXPERT-REDESIGN §4-1):=(input+output)×记账时的 quotaWeight,
|
|
245
|
+
* **weight-at-burn 持久化**——倍率 publish 只影响此后记账,不回算窗口内历史(余量/恢复时间不跳变)。
|
|
246
|
+
* 0.6.0 additive optional:旧 worker 不报 → 聚合按 quotaWeight=1 兜底折算。 */
|
|
247
|
+
weightedTokens: z.number().int().nonnegative().optional(),
|
|
248
|
+
/** 记账时固化的该模型 quotaWeight(审计口径,与 weightedTokens 同时点)——lease 扣减与 registry 聚合两
|
|
249
|
+
* 路径按同一时点折算(同口径,§11-1)。 */
|
|
250
|
+
quotaWeightAtUse: z.number().finite().nonnegative().optional(),
|
|
251
|
+
});
|
|
252
|
+
/** Periodic batch (worker → center POST). Windows are half-open [start, end) so adjacent batches never
|
|
253
|
+
* double-count a boundary instant. */
|
|
254
|
+
export const UsageReport = z.object({
|
|
255
|
+
worker: z.string().min(1),
|
|
256
|
+
instanceId: z.string().min(1),
|
|
257
|
+
windowStartMs: z.number().int().nonnegative(),
|
|
258
|
+
windowEndMs: z.number().int().nonnegative(),
|
|
259
|
+
entries: z.array(UsageEntry),
|
|
260
|
+
});
|
|
261
|
+
/** 额度清零事件(EXPERT-REDESIGN §4-4,契约随 E1 冻结、E4 消费):申请刷新获批即写一条;5h/周双窗聚合
|
|
262
|
+
* **只计最后一次 reset 之后**的 usage 报文(usage_report 是计费审计事实,全量留存,绝不删)。拒绝逻辑/
|
|
263
|
+
* 恢复时间/用量卡读数全部感知 reset;批准生效延迟 ≤ 一次租约 TTL(旧租约续租时放行)。 */
|
|
264
|
+
export const QuotaResetEvent = z.object({
|
|
265
|
+
id: z.string().min(1),
|
|
266
|
+
principal: z.string().min(1),
|
|
267
|
+
/** 清零生效时刻(ISO)——双窗聚合的下边界。 */
|
|
268
|
+
reset_at: z.string().min(1),
|
|
269
|
+
/** 批准人(console username)。 */
|
|
270
|
+
decided_by: z.string().min(1),
|
|
271
|
+
/** 关联的 quota_refresh_request id;admin 直接清零(无申请)时可缺。 */
|
|
272
|
+
request_id: z.string().min(1).optional(),
|
|
273
|
+
});
|
|
274
|
+
/** 单条 entry 的配额轴计账值:weightedTokens 原值优先;缺失(旧 worker 不报)按 quotaWeightAtUse(缺省 1)
|
|
275
|
+
* 兜底折算 (input+output)×weight(四舍五入保持整数口径)。 */
|
|
276
|
+
function entryWeightedTokens(e) {
|
|
277
|
+
return e.weightedTokens ?? Math.round((e.inputTokens + e.outputTokens) * (e.quotaWeightAtUse ?? 1));
|
|
278
|
+
}
|
|
279
|
+
/** Center-side fold: per-principal totals ACROSS workers/instances (the "one user, many workers, one bill"
|
|
280
|
+
* view). Pure — storage/dedup of replayed batches is the caller's concern (key batches by
|
|
281
|
+
* worker+instanceId+windowStartMs). */
|
|
282
|
+
export function aggregateUsage(reports) {
|
|
283
|
+
const out = {};
|
|
284
|
+
for (const rep of reports) {
|
|
285
|
+
for (const e of rep.entries) {
|
|
286
|
+
const wt = entryWeightedTokens(e);
|
|
287
|
+
const p = (out[e.principal] ??= { inputTokens: 0, outputTokens: 0, costMicroUsd: 0, runs: 0, weightedTokens: 0, byModel: {} });
|
|
288
|
+
p.inputTokens += e.inputTokens;
|
|
289
|
+
p.outputTokens += e.outputTokens;
|
|
290
|
+
p.costMicroUsd += e.costMicroUsd;
|
|
291
|
+
p.runs += e.runs ?? 0;
|
|
292
|
+
p.weightedTokens += wt;
|
|
293
|
+
const m = (p.byModel[e.model] ??= { inputTokens: 0, outputTokens: 0, costMicroUsd: 0, runs: 0, weightedTokens: 0 });
|
|
294
|
+
m.inputTokens += e.inputTokens;
|
|
295
|
+
m.outputTokens += e.outputTokens;
|
|
296
|
+
m.costMicroUsd += e.costMicroUsd;
|
|
297
|
+
m.runs += e.runs ?? 0;
|
|
298
|
+
m.weightedTokens += wt;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return out;
|
|
302
|
+
}
|
|
303
|
+
//# sourceMappingURL=fleet.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fleet.js","sourceRoot":"","sources":["../src/fleet.ts"],"names":[],"mappings":"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"}
|
package/dist/hash.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny stable content hash — shared by `workerSpecHash` (reconciler drift detection) and the per-worker
|
|
3
|
+
* `/effective` ETag (so an unrelated roster change doesn't churn a worker's cache). Zero-dep, deterministic.
|
|
4
|
+
*
|
|
5
|
+
* FNV-1a **64-bit** (BigInt) → 16 hex. NOT cryptographic; it's a change-detector. 64-bit pushes the
|
|
6
|
+
* birthday-collision boundary to ~5B distinct inputs — a collision would silently SUPPRESS a needed
|
|
7
|
+
* container restart (drift) or serve a stale 304 (ETag), so we don't want the 32-bit ~77k boundary here
|
|
8
|
+
* (council finding). Output length isn't a contract (values are only compared for equality before/after).
|
|
9
|
+
*/
|
|
10
|
+
/** Max nesting stableStringify will hash. A real EffectiveConfig nests ~10-15 deep; this generous cap turns a
|
|
11
|
+
* pathologically deep OR cyclic value (e.g. a poisoned `model.extraBody` = z.unknown()) into a DETERMINISTIC
|
|
12
|
+
* throw instead of a `RangeError: Maximum call stack` that would crash every scoped-worker `/effective` poll
|
|
13
|
+
* and local `getVersion`/`getEffective` (adversarial JSON-DoS — a cycle also reaches this cap). Round-3 fix. */
|
|
14
|
+
export declare const MAX_HASH_DEPTH = 200;
|
|
15
|
+
/** Wrapper headroom reserved between the extraBody validator's cap and the hash cap. CRITICAL: stableStringify
|
|
16
|
+
* always hashes extraBody NESTED inside a wrapper — `#versionOf` hashes `{models:{models:[{…,extraBody}]}}` and
|
|
17
|
+
* the scoped ETag hashes the same shape, putting the extraBody value root at hash-depth ~4. So a value the
|
|
18
|
+
* extraBody guard accepts at ITS depth D sits at hash-depth `D + wrapper`; if the guard counted to the SAME 200
|
|
19
|
+
* the wrapper overflow would let a depth-196–199 extraBody validate+persist (fail-OPEN) yet crash every later
|
|
20
|
+
* getVersion/getEffective (hard-DoS poison pill) — the two caps were each hardened in isolation but never
|
|
21
|
+
* reconciled (R18 codex). The real max wrapper is ~4–6; 32 is a deliberate ≫ margin. */
|
|
22
|
+
export declare const MAX_HASH_WRAPPER_RESERVE = 32;
|
|
23
|
+
/** Depth cap for a standalone `extraBody` value, reconciled with the hash cap MINUS the wrapper reserve so that
|
|
24
|
+
* ANYTHING the validator accepts is guaranteed hashable in every context it's later hashed in. 168 ≫ any real
|
|
25
|
+
* extraBody (OpenAI-style request params nest <10 deep). */
|
|
26
|
+
export declare const MAX_EXTRA_BODY_DEPTH: number;
|
|
27
|
+
/** Deterministic JSON with sorted object keys (so key order never affects the hash). */
|
|
28
|
+
export declare function stableStringify(v: unknown, depth?: number): string;
|
|
29
|
+
/** FNV-1a 64-bit of a string → 16-char hex. */
|
|
30
|
+
export declare function fnv1a64(s: string): string;
|
|
31
|
+
/** Stable hash of any JSON-able value. */
|
|
32
|
+
export declare function stableHash(v: unknown): string;
|
|
33
|
+
//# sourceMappingURL=hash.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../src/hash.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;iHAGiH;AACjH,eAAO,MAAM,cAAc,MAAM,CAAC;AAElC;;;;;;yFAMyF;AACzF,eAAO,MAAM,wBAAwB,KAAK,CAAC;AAE3C;;6DAE6D;AAC7D,eAAO,MAAM,oBAAoB,QAA4C,CAAC;AAE9E,wFAAwF;AACxF,wBAAgB,eAAe,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,SAAI,GAAG,MAAM,CAW7D;AAMD,+CAA+C;AAC/C,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAOzC;AAED,0CAA0C;AAC1C,wBAAgB,UAAU,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAE7C"}
|
package/dist/hash.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny stable content hash — shared by `workerSpecHash` (reconciler drift detection) and the per-worker
|
|
3
|
+
* `/effective` ETag (so an unrelated roster change doesn't churn a worker's cache). Zero-dep, deterministic.
|
|
4
|
+
*
|
|
5
|
+
* FNV-1a **64-bit** (BigInt) → 16 hex. NOT cryptographic; it's a change-detector. 64-bit pushes the
|
|
6
|
+
* birthday-collision boundary to ~5B distinct inputs — a collision would silently SUPPRESS a needed
|
|
7
|
+
* container restart (drift) or serve a stale 304 (ETag), so we don't want the 32-bit ~77k boundary here
|
|
8
|
+
* (council finding). Output length isn't a contract (values are only compared for equality before/after).
|
|
9
|
+
*/
|
|
10
|
+
/** Max nesting stableStringify will hash. A real EffectiveConfig nests ~10-15 deep; this generous cap turns a
|
|
11
|
+
* pathologically deep OR cyclic value (e.g. a poisoned `model.extraBody` = z.unknown()) into a DETERMINISTIC
|
|
12
|
+
* throw instead of a `RangeError: Maximum call stack` that would crash every scoped-worker `/effective` poll
|
|
13
|
+
* and local `getVersion`/`getEffective` (adversarial JSON-DoS — a cycle also reaches this cap). Round-3 fix. */
|
|
14
|
+
export const MAX_HASH_DEPTH = 200;
|
|
15
|
+
/** Wrapper headroom reserved between the extraBody validator's cap and the hash cap. CRITICAL: stableStringify
|
|
16
|
+
* always hashes extraBody NESTED inside a wrapper — `#versionOf` hashes `{models:{models:[{…,extraBody}]}}` and
|
|
17
|
+
* the scoped ETag hashes the same shape, putting the extraBody value root at hash-depth ~4. So a value the
|
|
18
|
+
* extraBody guard accepts at ITS depth D sits at hash-depth `D + wrapper`; if the guard counted to the SAME 200
|
|
19
|
+
* the wrapper overflow would let a depth-196–199 extraBody validate+persist (fail-OPEN) yet crash every later
|
|
20
|
+
* getVersion/getEffective (hard-DoS poison pill) — the two caps were each hardened in isolation but never
|
|
21
|
+
* reconciled (R18 codex). The real max wrapper is ~4–6; 32 is a deliberate ≫ margin. */
|
|
22
|
+
export const MAX_HASH_WRAPPER_RESERVE = 32;
|
|
23
|
+
/** Depth cap for a standalone `extraBody` value, reconciled with the hash cap MINUS the wrapper reserve so that
|
|
24
|
+
* ANYTHING the validator accepts is guaranteed hashable in every context it's later hashed in. 168 ≫ any real
|
|
25
|
+
* extraBody (OpenAI-style request params nest <10 deep). */
|
|
26
|
+
export const MAX_EXTRA_BODY_DEPTH = MAX_HASH_DEPTH - MAX_HASH_WRAPPER_RESERVE;
|
|
27
|
+
/** Deterministic JSON with sorted object keys (so key order never affects the hash). */
|
|
28
|
+
export function stableStringify(v, depth = 0) {
|
|
29
|
+
if (depth > MAX_HASH_DEPTH)
|
|
30
|
+
throw new Error(`stableStringify: value nested deeper than ${MAX_HASH_DEPTH} — refusing to hash (a config value is pathologically deep or cyclic)`);
|
|
31
|
+
// `JSON.stringify(undefined)` returns the value `undefined` (not a string) → would crash fnv1a64 on
|
|
32
|
+
// `.length`. Optionals are normally stripped by zod, but guard so a future default-less optional can't
|
|
33
|
+
// crash every scoped-worker poll. Treat undefined like null for hashing.
|
|
34
|
+
if (v === undefined)
|
|
35
|
+
return "null";
|
|
36
|
+
if (v === null || typeof v !== "object")
|
|
37
|
+
return JSON.stringify(v);
|
|
38
|
+
if (Array.isArray(v))
|
|
39
|
+
return `[${v.map((x) => stableStringify(x, depth + 1)).join(",")}]`;
|
|
40
|
+
const obj = v;
|
|
41
|
+
const keys = Object.keys(obj).sort();
|
|
42
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k], depth + 1)}`).join(",")}}`;
|
|
43
|
+
}
|
|
44
|
+
const FNV64_OFFSET = 0xcbf29ce484222325n;
|
|
45
|
+
const FNV64_PRIME = 0x100000001b3n;
|
|
46
|
+
const U64 = 0xffffffffffffffffn;
|
|
47
|
+
/** FNV-1a 64-bit of a string → 16-char hex. */
|
|
48
|
+
export function fnv1a64(s) {
|
|
49
|
+
let h = FNV64_OFFSET;
|
|
50
|
+
for (let i = 0; i < s.length; i++) {
|
|
51
|
+
h ^= BigInt(s.charCodeAt(i));
|
|
52
|
+
h = (h * FNV64_PRIME) & U64;
|
|
53
|
+
}
|
|
54
|
+
return h.toString(16).padStart(16, "0");
|
|
55
|
+
}
|
|
56
|
+
/** Stable hash of any JSON-able value. */
|
|
57
|
+
export function stableHash(v) {
|
|
58
|
+
return fnv1a64(stableStringify(v));
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=hash.js.map
|
package/dist/hash.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hash.js","sourceRoot":"","sources":["../src/hash.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;iHAGiH;AACjH,MAAM,CAAC,MAAM,cAAc,GAAG,GAAG,CAAC;AAElC;;;;;;yFAMyF;AACzF,MAAM,CAAC,MAAM,wBAAwB,GAAG,EAAE,CAAC;AAE3C;;6DAE6D;AAC7D,MAAM,CAAC,MAAM,oBAAoB,GAAG,cAAc,GAAG,wBAAwB,CAAC;AAE9E,wFAAwF;AACxF,MAAM,UAAU,eAAe,CAAC,CAAU,EAAE,KAAK,GAAG,CAAC;IACnD,IAAI,KAAK,GAAG,cAAc;QAAE,MAAM,IAAI,KAAK,CAAC,6CAA6C,cAAc,uEAAuE,CAAC,CAAC;IAChL,oGAAoG;IACpG,uGAAuG;IACvG,yEAAyE;IACzE,IAAI,CAAC,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC;IACnC,IAAI,CAAC,KAAK,IAAI,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;IAClE,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,eAAe,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;IAC1F,MAAM,GAAG,GAAG,CAA4B,CAAC;IACzC,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IACrC,OAAO,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,eAAe,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;AACxG,CAAC;AAED,MAAM,YAAY,GAAG,mBAAmB,CAAC;AACzC,MAAM,WAAW,GAAG,cAAc,CAAC;AACnC,MAAM,GAAG,GAAG,mBAAmB,CAAC;AAEhC,+CAA+C;AAC/C,MAAM,UAAU,OAAO,CAAC,CAAS;IAC/B,IAAI,CAAC,GAAG,YAAY,CAAC;IACrB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAClC,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;QAC7B,CAAC,GAAG,CAAC,CAAC,GAAG,WAAW,CAAC,GAAG,GAAG,CAAC;IAC9B,CAAC;IACD,OAAO,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;AAC1C,CAAC;AAED,0CAA0C;AAC1C,MAAM,UAAU,UAAU,CAAC,CAAU;IACnC,OAAO,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC;AACrC,CAAC"}
|