@mlx-node/server 0.0.9 → 0.0.12

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 (53) hide show
  1. package/dist/chat-session-warm-reuse.d.ts +10 -12
  2. package/dist/chat-session-warm-reuse.d.ts.map +1 -1
  3. package/dist/chat-session-warm-reuse.js +10 -12
  4. package/dist/endpoints/messages.d.ts +2 -2
  5. package/dist/endpoints/messages.d.ts.map +1 -1
  6. package/dist/endpoints/messages.js +492 -349
  7. package/dist/endpoints/responses.d.ts +1 -1
  8. package/dist/endpoints/responses.d.ts.map +1 -1
  9. package/dist/endpoints/responses.js +1149 -1055
  10. package/dist/handler.d.ts.map +1 -1
  11. package/dist/handler.js +1 -1
  12. package/dist/health.d.ts +4 -6
  13. package/dist/health.d.ts.map +1 -1
  14. package/dist/host/discover.d.ts +1 -2
  15. package/dist/host/discover.d.ts.map +1 -1
  16. package/dist/host/discover.js +3 -6
  17. package/dist/host/index.d.ts +6 -1
  18. package/dist/host/index.d.ts.map +1 -1
  19. package/dist/host/index.js +3 -2
  20. package/dist/index.d.ts +2 -4
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +1 -9
  23. package/dist/mappers/anthropic-request.d.ts.map +1 -1
  24. package/dist/mappers/anthropic-request.js +5 -1
  25. package/dist/mappers/request.d.ts +12 -2
  26. package/dist/mappers/request.d.ts.map +1 -1
  27. package/dist/mappers/request.js +26 -2
  28. package/dist/model-work-coordinator.d.ts +50 -0
  29. package/dist/model-work-coordinator.d.ts.map +1 -1
  30. package/dist/model-work-coordinator.js +161 -0
  31. package/dist/registry.d.ts +15 -0
  32. package/dist/registry.d.ts.map +1 -1
  33. package/dist/registry.js +51 -0
  34. package/dist/server.d.ts +21 -3
  35. package/dist/server.d.ts.map +1 -1
  36. package/dist/server.js +30 -6
  37. package/dist/session-registry.d.ts +266 -86
  38. package/dist/session-registry.d.ts.map +1 -1
  39. package/dist/session-registry.js +421 -107
  40. package/dist/streaming.d.ts +37 -2
  41. package/dist/streaming.d.ts.map +1 -1
  42. package/dist/streaming.js +122 -1
  43. package/dist/transport-visibility.d.ts +5 -4
  44. package/dist/transport-visibility.d.ts.map +1 -1
  45. package/dist/transport-visibility.js +5 -4
  46. package/dist/types-anthropic.d.ts +7 -0
  47. package/dist/types-anthropic.d.ts.map +1 -1
  48. package/dist/types.d.ts +9 -0
  49. package/dist/types.d.ts.map +1 -1
  50. package/package.json +4 -4
  51. package/dist/presets.d.ts +0 -82
  52. package/dist/presets.d.ts.map +0 -1
  53. package/dist/presets.js +0 -98
@@ -21,18 +21,13 @@
21
21
  * by response id — no secondary keying on model name because the
22
22
  * registry is already scoped per model.
23
23
  *
24
- * - **Single-warm-session invariant.** `ChatSession<M>` is a thin
25
- * JS wrapper it does NOT own any native KV cache. The cache
26
- * lives on the underlying `SessionCapableModel` (one shared
27
- * `cached_token_history` / `caches` vector per model instance).
28
- * Any call that runs a turn overwrites that shared native state,
29
- * silently invalidating every other `ChatSession` wrapper
30
- * pointing at the same model. Caching multiple wrappers per
31
- * model is therefore an illusion: at most ONE matches real
32
- * native state (whichever ran most recently). To prevent
33
- * cross-session corruption this registry holds at most ONE
34
- * entry — both `getOrCreate` and `adopt` clear the map before
35
- * returning or inserting.
24
+ * - **Single-warm-session invariant.** The JS warm registry retains at
25
+ * most ONE `ChatSession` entry both `getOrCreate` and `adopt` clear
26
+ * the map before returning or inserting. This remains the sole safe
27
+ * reuse mechanism for flat-cache models whose native cache is one
28
+ * mutable vector. Block-paged schedulers instead isolate live turns by
29
+ * cache owner and reuse verified physical blocks through the native
30
+ * prefix table; those models may run fresh JS sessions concurrently.
36
31
  *
37
32
  * - **Lease semantics on hit.** Clear-on-hit also gives single-
38
33
  * flight lease semantics: two overlapping requests referencing
@@ -43,14 +38,13 @@
43
38
  * `ChatSession`'s single-flight "concurrent send() not allowed"
44
39
  * guard.
45
40
  *
46
- * - **Instructions / prefix-state change also misses.** Each entry
47
- * records the `instructions` string used to adopt it.
48
- * `getOrCreate` compares the caller's `requestedInstructions`
49
- * against the cached value; mismatch forces cold replay so the
50
- * new prefix state is re-primed instead of silently reusing a
51
- * stale warmed prompt. The OpenAI `instructions` field and the
52
- * Anthropic `system` field both flow through the same parameter
53
- * — the registry does not care which is which.
41
+ * - **Prefix compatibility changes miss.** Each entry records its
42
+ * `instructions` plus an opaque fingerprint of the cache salt used to
43
+ * adopt it. `getOrCreate` compares both against the new request;
44
+ * mismatch forces owner release and cold replay instead of reusing a
45
+ * stale prompt or changing the security domain of one live native
46
+ * request. OpenAI `instructions` and Anthropic `system` share the same
47
+ * parameter; both endpoints also thread their mapped `cache_salt`.
54
48
  *
55
49
  * - **Cache miss fallback.** On a miss (eviction, interleaved turn
56
50
  * on a different chain, restart, lease-on-hit) the endpoint
@@ -73,32 +67,20 @@
73
67
  * concurrent mutation by design. `sweep()` can be scheduled
74
68
  * via `setInterval` without colliding with in-flight calls.
75
69
  *
76
- * - **Per-model execution mutex.** A dispatch that spans multiple
77
- * awaits (map -> prefill -> decode -> persist -> adopt) is NOT
78
- * atomic from the registry's POV. Two requests against the
79
- * same model would both receive a `ChatSession` pointing at
80
- * the same native model; even though the lease-on-hit clear
81
- * prevents sharing one `ChatSession` object, the native KV
82
- * cache is a single mutable resource and two parallel
83
- * `primeHistory()` / `send*()` calls would race. Whichever
84
- * finished last would win `adopt()`, poisoning the hot path
85
- * for every subsequent chained turn.
86
- *
87
- * `withExclusive(fn)` serializes every per-model dispatch via
88
- * a FIFO `execLock` chain. `/v1/responses` and `/v1/messages`
89
- * wrap the full `getOrCreate -> run -> adopt/drop` span in one
90
- * `withExclusive` so at most one request holds the model at a
91
- * time. A weaker epoch-token scheme would let the losing
92
- * `adopt()` no-op but the native KV would already be wrong.
70
+ * - **Per-model admission lane.** Flat and not-yet-batched families use
71
+ * `withExclusive(fn)`, the original FIFO mutex across the full dispatch.
72
+ * A model that explicitly reports `maxConcurrentSequences() > 1` uses
73
+ * `withAdmission(fn)`, a counting semaphore sized to that native
74
+ * scheduler. Per-session serialization still lives in `ChatSession`;
75
+ * only independent sessions share the model lane.
93
76
  */
94
77
  import type { ChatConfig } from '@mlx-node/core';
95
78
  import { ChatSession, type SessionCapableModel } from '@mlx-node/lm';
96
79
  /**
97
- * Test-only hook used by the scoping unit tests to simulate a
98
- * server restart: resets the module-scoped HMAC nonce (so every
99
- * previously stored tier-2 key misses) and clears the silent-miss
100
- * dedupe cache so tests can re-exercise the once-per-key diagnostic
101
- * path.
80
+ * Test-only hook used by the scoping unit tests to simulate a server
81
+ * restart: resets the module-scoped HMAC nonce (so every previously stored
82
+ * tier-2 key and cache-salt fingerprint misses) and clears the silent-miss
83
+ * dedupe cache so tests can re-exercise the once-per-key diagnostic path.
102
84
  *
103
85
  * **Not exported from the package's public `index.ts` surface** —
104
86
  * exporting it there would let downstream consumers nuke tier-2
@@ -152,11 +134,21 @@ export interface SessionRegistryOptions {
152
134
  * synchronously so the endpoint layer can emit HTTP 429 and the
153
135
  * client can retry later.
154
136
  *
155
- * Default: `undefined` (unbounded — current behaviour). Opt-in per
156
- * {@link ServerConfig.maxQueueDepthPerModel} or the
157
- * `MLX_MAX_QUEUE_DEPTH_PER_MODEL` env var.
137
+ * Default at THIS layer: `undefined` (unbounded)a directly
138
+ * constructed `SessionRegistry` has no cap. `createServer` supplies
139
+ * its own default of 16 per {@link ServerConfig.maxQueueDepthPerModel}
140
+ * (or the `MLX_MAX_QUEUE_DEPTH_PER_MODEL` env var; config
141
+ * `'unbounded'` opts out), so server-allocated registries are capped
142
+ * unless the operator explicitly opted out.
158
143
  */
159
144
  maxQueueDepth?: number;
145
+ /**
146
+ * Number of independent dispatches the native model scheduler can advance
147
+ * concurrently. Values below two retain the legacy exclusive FIFO. The
148
+ * model registry derives this from `maxConcurrentSequences()`; direct
149
+ * construction defaults to one.
150
+ */
151
+ maxConcurrentDispatches?: number;
160
152
  /**
161
153
  * Optional sampling defaults applied to every `ChatSession` this
162
154
  * registry allocates. Forwarded verbatim into `new ChatSession(model,
@@ -184,9 +176,33 @@ export interface SessionRegistryOptions {
184
176
  * can reliably catch it without racing the chain.
185
177
  */
186
178
  export declare class QueueFullError extends Error {
187
- readonly queuedCount: number;
179
+ readonly queueDepth: number;
180
+ readonly preDispatchAdmissions: number;
181
+ readonly admissionFootprint: number;
188
182
  readonly limit: number;
189
- constructor(queuedCount: number, limit: number);
183
+ constructor(queueDepth: number, preDispatchAdmissions: number, limit: number);
184
+ }
185
+ /**
186
+ * Admission permit returned by
187
+ * {@link SessionRegistry.beginPreDispatchAdmission}. Represents exactly
188
+ * ONE unit of the per-model admission budget, and ends in exactly one of
189
+ * two terminal states:
190
+ *
191
+ * - **handed off**: passed as the second argument of
192
+ * `withExclusive(fn, permit)` or `withAdmission(fn, permit)`, which
193
+ * consumes it atomically as that
194
+ * call's admission — the unit converts into the waiter charge (or is
195
+ * retired when the caller wins the runner slot). `release()` becomes
196
+ * a no-op afterwards.
197
+ * - **released**: `release()` frees the unit without a dispatch. Every
198
+ * exit between admission and lock placement must do this — the
199
+ * recommended pattern is one unconditional `release()` in a
200
+ * `finally`, which is safe on every path because `release()` is
201
+ * idempotent and a no-op after handoff.
202
+ */
203
+ export interface PreDispatchAdmission {
204
+ /** Idempotent: free the budget unit unless already handed off. */
205
+ release(): void;
190
206
  }
191
207
  /**
192
208
  * Result of {@link SessionRegistry.getOrCreate}. `hit` reflects whether
@@ -203,6 +219,7 @@ export declare class SessionRegistry {
203
219
  private readonly model;
204
220
  private readonly ttlSec;
205
221
  private readonly maxQueueDepth;
222
+ private readonly maxConcurrentDispatches;
206
223
  /**
207
224
  * Per-model sampling defaults forwarded into every new `ChatSession`
208
225
  * via its `defaultConfig` constructor option. `undefined` preserves
@@ -232,6 +249,25 @@ export declare class SessionRegistry {
232
249
  * runner-slot caller against the waiter cap.
233
250
  */
234
251
  private queuedCount;
252
+ /**
253
+ * Requests admitted by {@link beginPreDispatchAdmission} whose permit
254
+ * is still outstanding — parked in the `ModelWorkCoordinator` writer
255
+ * queue (host mode), blocked in pre-lock store lookups
256
+ * (`previous_response_id` continuations), or anywhere else between the
257
+ * endpoint gate and resident-lane placement. None of that parking is
258
+ * visible to `queuedCount`; this counter is what lets the gate bound
259
+ * it. Decremented ONLY by the permit itself: `release()` on a bail-out
260
+ * or the atomic consume inside the selected lane on handoff.
261
+ */
262
+ private preDispatchAdmits;
263
+ /**
264
+ * Consume hooks for outstanding permits, keyed by permit identity.
265
+ * Registry-scoped on purpose: both execution lanes consult THIS map, so a
266
+ * permit minted by a different registry is simply not found and the
267
+ * call falls back to normal waiter charging — a cross-registry handoff
268
+ * cannot corrupt either registry's counters.
269
+ */
270
+ private readonly permitConsumers;
235
271
  /**
236
272
  * Holds AT MOST ONE entry under the single-warm invariant (see the
237
273
  * module-level rustdoc). `getOrCreate` and `adopt` both clear the
@@ -240,6 +276,15 @@ export declare class SessionRegistry {
240
276
  * turn on another cached entry.
241
277
  */
242
278
  private readonly entries;
279
+ /**
280
+ * Eviction is synchronous at the map boundary, but releasing a native
281
+ * scheduler owner is asynchronous. Start every disposal immediately and
282
+ * retain its promise so endpoint admission lanes can wait for command-order
283
+ * visibility before dispatching a replacement turn.
284
+ */
285
+ private readonly pendingDisposals;
286
+ private readonly disposalBySession;
287
+ private readonly failedDisposals;
243
288
  /**
244
289
  * Shared sentinel representing "the execution chain is idle" — a
245
290
  * pre-resolved promise. `execLock` starts at this value and is
@@ -265,6 +310,10 @@ export declare class SessionRegistry {
265
310
  * cleanly from the idle state.
266
311
  */
267
312
  private execLock;
313
+ /** Active holders in the continuous-batching admission lane. */
314
+ private activeAdmissions;
315
+ /** FIFO waiters parked behind the continuous-batching admission limit. */
316
+ private readonly admissionWaiters;
268
317
  constructor(opts: SessionRegistryOptions);
269
318
  /**
270
319
  * Construct a fresh `ChatSession` bound to this registry's model and
@@ -275,19 +324,117 @@ export declare class SessionRegistry {
275
324
  * pinned sampling knobs by picking a cold-replay path.
276
325
  */
277
326
  private newSession;
327
+ private scheduleDispose;
328
+ /**
329
+ * Dispose one leased session while retaining failed cleanup for a later
330
+ * registry flush. `ChatSession.dispose()` removes successful owners as it
331
+ * goes, so a retry only revisits owners whose native release failed.
332
+ */
333
+ disposeSession(session: ChatSession<SessionCapableModel>): Promise<void>;
334
+ /** Remove every cached entry except an optional session being leased. */
335
+ private evictEntriesExcept;
336
+ /**
337
+ * Wait until every disposal scheduled so far has settled, retrying each
338
+ * failure discovered during this flush once. A persistent failure remains
339
+ * recorded for a later flush instead of spinning forever. Disposals log and
340
+ * absorb their own failures so cleanup cannot rewrite a response that has
341
+ * already reached the client.
342
+ */
343
+ flushPendingDisposals(): Promise<void>;
278
344
  /**
279
- * Number of requests currently WAITING to acquire the per-model
280
- * execution mutex. Does NOT include the one actively running inside
281
- * `fn`. Primarily for tests and diagnostics.
345
+ * Disposals started but not yet settled each one is an in-flight
346
+ * native `releaseCacheOwner` round-trip plus disposals whose initial
347
+ * attempt and bounded retry both failed and remain owed to the native
348
+ * scheduler ({@link failedDisposals}), retried by the next
349
+ * {@link flushPendingDisposals}. Endpoints await that flush before
350
+ * leaving the admission lane, but it runs after the response has
351
+ * finished, so an observer keyed on request completion can still beat
352
+ * the release. `adopt`/`drop`/`sweep` schedule synchronously, so once
353
+ * the request counters read zero any disposal those requests will ever
354
+ * cause is already counted here. Primarily for diagnostics/tests.
355
+ */
356
+ get pendingDisposalCount(): number;
357
+ /**
358
+ * Number of requests waiting for this model's selected admission lane.
359
+ * Active dispatches are not included. Primarily for diagnostics/tests.
282
360
  */
283
361
  get queueDepth(): number;
284
362
  /**
285
- * Configured waiter cap for this model's execution mutex, or `undefined`
363
+ * Configured waiter cap for this model's admission lane, or `undefined`
286
364
  * when unbounded. Paired with {@link queueDepth} so a readiness probe can
287
365
  * tell "3 waiters, unbounded" (fine) from "3 waiters, cap of 3" (the next
288
366
  * request gets a 429) without reaching into private state.
289
367
  */
290
368
  get queueDepthLimit(): number | undefined;
369
+ /** Native continuous-batching capacity used by the endpoint route switch. */
370
+ get concurrentAdmissionLimit(): number;
371
+ /**
372
+ * Outstanding pre-dispatch permits — requests admitted by
373
+ * {@link beginPreDispatchAdmission} that have neither handed their
374
+ * permit to the selected execution lane nor released it yet. These permits,
375
+ * queued callers, and active dispatches share one bounded budget; see
376
+ * {@link assertAdmissionCapacity}. For probes and diagnostics.
377
+ */
378
+ get preDispatchAdmitCount(): number;
379
+ /**
380
+ * Single source of truth for the per-model admission budget — the
381
+ * ONLY place the cap arithmetic lives. Every admission path calls
382
+ * this: {@link beginPreDispatchAdmission} before minting a permit,
383
+ * and both execution lanes for every non-handed-off caller. A handed-off
384
+ * permit skips the call for its OWN
385
+ * token only: that token was charged here at acquisition and its
386
+ * conversion keeps the total constant, so re-checking would
387
+ * double-charge an already-admitted request. It never exempts anyone
388
+ * else — all other outstanding state stays counted for every caller
389
+ * that did not pay.
390
+ *
391
+ * Budget invariant: active dispatches + queued callers + outstanding
392
+ * permits never exceed `maxConcurrentDispatches + maxQueueDepth`.
393
+ * On the exclusive lane `maxConcurrentDispatches` is one, exactly the
394
+ * original runner entitlement. On the batched lane it is the native
395
+ * scheduler's sequence capacity.
396
+ *
397
+ * Charging stays at the call sites (`preDispatchAdmits += 1` at the
398
+ * gate, `queuedCount += 1` for waiters, and the selected lane's active
399
+ * count for runners); every admitted unit is counted by exactly one
400
+ * at any time, which is what makes the footprint sum complete across
401
+ * any interleaving of permitted and permitless callers.
402
+ *
403
+ * Throws {@link QueueFullError} — reporting the footprint and the
404
+ * cap — when the caller does not fit; returns normally otherwise.
405
+ * No-op when the registry is unbounded.
406
+ */
407
+ private assertAdmissionCapacity;
408
+ /**
409
+ * Endpoint-side early admission against this registry's cap, taken BEFORE
410
+ * the request enters any pre-dispatch parking spot the resident lane cannot
411
+ * see. Resident host traffic deliberately bypasses the model-load writer so
412
+ * continuous batching remains reachable, but a continuation can still block
413
+ * in `await store.getChain(...)` (or other pre-lock work) with `queuedCount`
414
+ * unchanged. This permit keeps all such work inside the same bounded budget.
415
+ *
416
+ * Accounting: pre-dispatch permits, active dispatches, and queued callers
417
+ * draw from ONE budget: `maxQueueDepth` waiter slots plus the selected
418
+ * lane's active capacity. Both lanes and this early gate route through
419
+ * {@link assertAdmissionCapacity}, so permitted and permitless arrivals
420
+ * cannot double-spend a slot.
421
+ *
422
+ * Throws {@link QueueFullError} synchronously when over cap (the
423
+ * caller maps it to the same 429 envelope as resident-lane rejection
424
+ * reject). On admission returns a {@link PreDispatchAdmission} permit
425
+ * the caller must RETAIN through ALL pre-lock asynchronous work and
426
+ * then hand to `withExclusive(fn, permit)` or `withAdmission(fn, permit)`,
427
+ * which consumes it
428
+ * atomically as that call's admission — one budget, one token per
429
+ * request, never double-counted. Releasing the permit early instead
430
+ * of handing it off re-opens the hole this gate closes: the request
431
+ * would be counted by NEITHER counter while parked, arrivals would
432
+ * refill the budget, and the resident lane would then admit a second
433
+ * full waiter budget on top. `release()` belongs on bail-out paths
434
+ * only (idempotent, no-op after handoff — an unconditional `finally`
435
+ * release is the recommended shape).
436
+ */
437
+ beginPreDispatchAdmission(): PreDispatchAdmission;
291
438
  /**
292
439
  * Current sampling defaults applied to every new `ChatSession` this
293
440
  * registry allocates. Exposed primarily for tests and diagnostics.
@@ -316,11 +463,11 @@ export declare class SessionRegistry {
316
463
  *
317
464
  * 1. **Tier 1 — `previousResponseId`.** The existing hot path:
318
465
  * exact id match on a live, non-expired entry whose stored
319
- * `instructions` are byte-equal to `requestedInstructions`. On
466
+ * `instructions` and cache-salt fingerprint match the request. On
320
467
  * a match the entry is leased out (single-use: removed from the
321
468
  * map so a concurrent second request cannot share the live
322
- * `ChatSession`). On a miss — unknown id, expired, or
323
- * instructions drift — the method falls through to a FRESH
469
+ * `ChatSession`). On a miss — unknown id, expired, instructions
470
+ * drift, or cache-salt drift — the method falls through to a FRESH
324
471
  * session regardless of whether tier 2 would have hit.
325
472
  *
326
473
  * `previousResponseId` wins unconditionally when supplied. The
@@ -340,9 +487,9 @@ export declare class SessionRegistry {
340
487
  * is to key on the client-supplied `prompt_cache_key`. Scans
341
488
  * for any live, non-expired entry whose stored
342
489
  * `promptCacheKey` is non-null AND byte-equal to the caller's
343
- * `promptCacheKey` AND whose stored `instructions` are byte-
344
- * equal. Empty string is treated as a distinct key from
345
- * `null` — an opt-out sentinel from a client that forgot to
490
+ * `promptCacheKey`, whose stored `instructions` are byte-equal,
491
+ * AND whose cache-salt fingerprint matches. Empty string is treated
492
+ * as a distinct key from `null` — an opt-out sentinel from a client that forgot to
346
493
  * thread the key must NOT collide with another client that
347
494
  * did set it to empty. On a match the entry is leased out
348
495
  * (same single-use semantics as tier 1). On a miss, fall
@@ -358,7 +505,7 @@ export declare class SessionRegistry {
358
505
  * native `cachedTokens > 0` confirms the prefix-cache machinery
359
506
  * actually reused the cached tokens).
360
507
  */
361
- getOrCreate(previousResponseId: string | null, requestedInstructions: string | null, promptCacheKey?: string | null): SessionLookupResult;
508
+ getOrCreate(previousResponseId: string | null, requestedInstructions: string | null, promptCacheKey?: string | null, requestedCacheSalt?: string | null): SessionLookupResult;
362
509
  /**
363
510
  * Allocate a fresh `ChatSession` bound to this registry's model
364
511
  * without touching the warm slot. Intended for the `/v1/messages`
@@ -423,17 +570,18 @@ export declare class SessionRegistry {
423
570
  * registry's own warm slot.
424
571
  *
425
572
  * Behaviour: walk the registry's at-most-one warm entry. If it is
426
- * non-expired AND its stored `instructions` are byte-equal to
427
- * `requestedInstructions`, lease it out (single-use `entries.clear()`
428
- * before return, mirroring the tier-1 / tier-2 lease-on-hit
429
- * semantics). Otherwise clear the map and return a fresh session.
573
+ * non-expired, its stored `instructions` are byte-equal to
574
+ * `requestedInstructions`, AND its stored cache salt equals
575
+ * `requestedCacheSalt`, lease it out (single-use `entries.clear()`
576
+ * before return, mirroring the tier-1 / tier-2 lease-on-hit semantics).
577
+ * Otherwise clear the map and return a fresh session.
430
578
  *
431
579
  * Crucially, this lookup IGNORES `entry.promptCacheKey` and ignores
432
580
  * the entry's prior `previousResponseId` keying — any warm slot is
433
- * fair game for `/v1/messages` reuse. The byte-equal `instructions`
434
- * compare is the SOLE correctness gate: a system prompt change
435
- * forces cold replay so the new prefix state is re-primed instead
436
- * of silently reusing a stale warmed prompt.
581
+ * fair game for `/v1/messages` reuse. Byte-equal instructions and cache
582
+ * salt are the correctness gates: a system prompt or prefix-cache
583
+ * security-domain change forces cold replay instead of reusing stale state
584
+ * or asking the native adapter to mutate a live request's salt.
437
585
  *
438
586
  * **Adoption sentinel.** `/v1/messages` adopts back under the literal
439
587
  * sentinel id `'__msg_warm__'`. That sentinel will never appear as a
@@ -469,7 +617,7 @@ export declare class SessionRegistry {
469
617
  * `responses.ts` (around the `runSessionNonStreaming` /
470
618
  * `runSessionStreaming` branches) describes.
471
619
  */
472
- getOrCreateWarmAny(requestedInstructions: string | null): SessionLookupResult;
620
+ getOrCreateWarmAny(requestedInstructions: string | null, requestedCacheSalt?: string | null): SessionLookupResult;
473
621
  /**
474
622
  * Insert a session under a newly allocated response id. Clears the
475
623
  * map before inserting to keep the single-warm invariant explicit
@@ -478,6 +626,8 @@ export declare class SessionRegistry {
478
626
  * `instructions` is the prefix/system state used for this turn;
479
627
  * stored on the entry and compared on the next `getOrCreate` to
480
628
  * detect prefix changes that must force a cold replay.
629
+ * `cacheSalt` is HMAC-fingerprinted before storage and compared alongside
630
+ * the prefix so the raw security-domain key is not retained.
481
631
  *
482
632
  * `promptCacheKey` is the client-supplied conversation-chain key
483
633
  * that enables the registry's tier-2 lookup for stateless agent
@@ -488,7 +638,7 @@ export declare class SessionRegistry {
488
638
  * key-equality on both sides can hit tier 2). See
489
639
  * {@link SessionRegistry.getOrCreate} for the precedence rules.
490
640
  */
491
- adopt(responseId: string, session: ChatSession<SessionCapableModel>, instructions: string | null, promptCacheKey?: string | null | undefined): void;
641
+ adopt(responseId: string, session: ChatSession<SessionCapableModel>, instructions: string | null, promptCacheKey?: string | null | undefined, cacheSalt?: string | null | undefined): void;
492
642
  /**
493
643
  * Remove a session by response id. No-op if the key is not present.
494
644
  */
@@ -515,33 +665,63 @@ export declare class SessionRegistry {
515
665
  * the new tail, awaits the old tail, then runs `fn`. The
516
666
  * `finally` releases regardless of whether `fn` threw.
517
667
  *
518
- * **Admission control.** When `maxQueueDepth` is configured and the
519
- * current number of waiters (`queuedCount`, excluding the active
520
- * holder) is already at or above the cap, the call throws
521
- * {@link QueueFullError} synchronously SYNCHRONOUSLY from the
522
- * caller's perspective, not merely before `await prev`. The wrapper
523
- * is deliberately NOT declared `async` so the admission gate
524
- * throws on the caller's stack frame, letting endpoint handlers
525
- * wrap the call site in a plain try/catch without racing promise
668
+ * **Admission control.** Every non-handed-off call waiter AND
669
+ * runner routes through {@link assertAdmissionCapacity}, the single
670
+ * source of truth for the budget: the combined admission footprint
671
+ * (`queuedCount + preDispatchAdmits`) plus this caller must fit
672
+ * within `maxQueueDepth` plus the idle-chain runner entitlement.
673
+ * Over-budget calls throw {@link QueueFullError} SYNCHRONOUSLY
674
+ * from the caller's perspective, not merely before `await prev`. The
675
+ * wrapper is deliberately NOT declared `async` so the admission gate
676
+ * throws on the caller's stack frame, letting endpoint handlers wrap
677
+ * the call site in a plain try/catch without racing promise
526
678
  * microtasks. On acceptance the async body takes over via the
527
679
  * returned `Promise<T>`.
528
680
  *
529
- * The cap is "waiters-only" — a cap of N permits one running
530
- * dispatch plus N queued ones, rejecting the (N+1)th waiter. The
531
- * default (undefined) preserves the original unbounded behaviour.
681
+ * A cap of N permits one running dispatch plus N admitted-but-not-
682
+ * running requests (queued waiters and outstanding permits
683
+ * combined), rejecting the next. The default (undefined) preserves
684
+ * the original unbounded behaviour.
532
685
  *
533
686
  * **Runner-slot admission.** Whether a given caller counts as the
534
687
  * runner slot or as a waiter is decided up front by comparing
535
688
  * `execLock` against the idle sentinel `initialLock`. If they are
536
- * identical, nobody is currently in-flight and this caller wins
537
- * the runner slot: it is not counted against the waiter cap and
538
- * never touches `queuedCount`. Otherwise it is a waiter and the
539
- * normal cap check / increment / decrement cycle applies. This is
540
- * what keeps a synchronous burst such as `Promise.all([fn, fn])`
689
+ * identical, nobody is currently in-flight and this caller is
690
+ * admitted against the extra runner entitlement rather than a waiter
691
+ * slot — it never touches `queuedCount`. Otherwise it is a waiter
692
+ * and the normal charge / increment / decrement cycle applies. This
693
+ * is what keeps a synchronous burst such as `Promise.all([fn, fn])`
541
694
  * admissible under `maxQueueDepth = 1` — Call 1 is the runner,
542
- * Call 2 is the one allowed waiter, Call 3 would throw.
695
+ * Call 2 is the one allowed waiter, Call 3 would throw. The runner
696
+ * path is NOT exempt from the budget: while the chain is idle,
697
+ * {@link beginPreDispatchAdmission} lends out `maxQueueDepth + 1`
698
+ * permits precisely because one of them is entitled to become the
699
+ * runner, so a permitless call that would take that seat while the
700
+ * whole runner-plus-waiter capacity is already spoken for must
701
+ * reject — otherwise the outstanding permits would later convert on
702
+ * top of it and breach the cap.
703
+ *
704
+ * **Permit handoff.** A caller that was already admitted by
705
+ * {@link beginPreDispatchAdmission} passes its permit as the second
706
+ * argument; the permit is consumed atomically as this call's
707
+ * admission instead of charging `queuedCount` a second time — one
708
+ * budget, one token per request. See the handoff comment in the
709
+ * body and {@link PreDispatchAdmission}.
710
+ */
711
+ withExclusive<T>(fn: () => Promise<T>, permit?: PreDispatchAdmission): Promise<T>;
712
+ /**
713
+ * Admit one dispatch to the model's continuous-batching lane.
714
+ *
715
+ * Admission rejection is synchronous, matching {@link withExclusive} and
716
+ * preserving the endpoint's existing QueueFullError-to-429 mapping. Once
717
+ * accepted, at most {@link concurrentAdmissionLimit} closures run at once;
718
+ * excess accepted callers wait FIFO and contribute to {@link queueDepth}.
719
+ * A pre-dispatch permit is consumed atomically into either an active slot or
720
+ * a queued slot, so the early endpoint gate and this semaphore share one
721
+ * bounded budget.
543
722
  */
544
- withExclusive<T>(fn: () => Promise<T>): Promise<T>;
723
+ withAdmission<T>(fn: () => Promise<T>, permit?: PreDispatchAdmission): Promise<T>;
724
+ private _runAdmission;
545
725
  /**
546
726
  * Async tail of {@link withExclusive}. Kept separate so the public
547
727
  * wrapper stays a plain (non-async) function whose admission-gate
@@ -1 +1 @@
1
- {"version":3,"file":"session-registry.d.ts","sourceRoot":"","sources":["../src/session-registry.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"session-registry.d.ts","sourceRoot":"","sources":["../src/session-registry.ts"],"names":[],"mappings":"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"}