@bendyline/gezel-inference 1.0.5 → 1.0.7

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 (144) hide show
  1. package/dist/cache/controller.d.ts +2 -0
  2. package/dist/cache/controller.js +38 -5
  3. package/dist/catalog/chat-models/index.json +1 -1
  4. package/dist/catalog/chat-models/lf/lfm3-1.7b-q4/manifest.json +114 -0
  5. package/dist/catalog/chat-models/lf/lfm3-1.7b-q4/versions/1.0.0/manifest.json +15 -0
  6. package/dist/catalog/chat-models/raw-index.json +1 -1
  7. package/dist/catalog/image-loras/index.json +1 -0
  8. package/dist/catalog/image-loras/kr/krea-2-ink-wash/manifest.json +31 -0
  9. package/dist/catalog/image-loras/kr/krea-2-ink-wash/versions/1.0.0/manifest.json +15 -0
  10. package/dist/catalog/image-loras/kr/krea-2-isometric-3d/manifest.json +32 -0
  11. package/dist/catalog/image-loras/kr/krea-2-isometric-3d/versions/1.0.0/manifest.json +15 -0
  12. package/dist/catalog/image-loras/kr/krea-2-kids-drawing/manifest.json +31 -0
  13. package/dist/catalog/image-loras/kr/krea-2-kids-drawing/versions/1.0.0/manifest.json +15 -0
  14. package/dist/catalog/image-loras/kr/krea-2-pixel-art/manifest.json +31 -0
  15. package/dist/catalog/image-loras/kr/krea-2-pixel-art/versions/1.0.0/manifest.json +15 -0
  16. package/dist/catalog/image-loras/kr/krea-2-soft-watercolor/manifest.json +31 -0
  17. package/dist/catalog/image-loras/kr/krea-2-soft-watercolor/versions/1.0.0/manifest.json +15 -0
  18. package/dist/catalog/image-loras/kr/krea-2-turbo-4step/manifest.json +31 -0
  19. package/dist/catalog/image-loras/kr/krea-2-turbo-4step/versions/1.0.0/manifest.json +15 -0
  20. package/dist/catalog/image-loras/mo/moss-image-gouache/manifest.json +29 -0
  21. package/dist/catalog/image-loras/mo/moss-image-gouache/versions/1.0.0/manifest.json +15 -0
  22. package/dist/catalog/image-loras/mo/moss-image-sft-4step/manifest.json +30 -0
  23. package/dist/catalog/image-loras/mo/moss-image-sft-4step/versions/1.0.0/manifest.json +15 -0
  24. package/dist/catalog/image-loras/qw/qwen-image-edit-2511-lightning/manifest.json +31 -0
  25. package/dist/catalog/image-loras/qw/qwen-image-edit-2511-lightning/versions/1.0.0/manifest.json +15 -0
  26. package/dist/catalog/image-loras/raw-index.json +1 -0
  27. package/dist/catalog/image-loras/sd/sdxl-3d-render/manifest.json +29 -0
  28. package/dist/catalog/image-loras/sd/sdxl-3d-render/versions/1.0.0/manifest.json +15 -0
  29. package/dist/catalog/image-loras/sd/sdxl-coloring-book/manifest.json +30 -0
  30. package/dist/catalog/image-loras/sd/sdxl-coloring-book/versions/1.0.0/manifest.json +15 -0
  31. package/dist/catalog/image-loras/sd/sdxl-papercut/manifest.json +29 -0
  32. package/dist/catalog/image-loras/sd/sdxl-papercut/versions/1.0.0/manifest.json +15 -0
  33. package/dist/catalog/image-loras/sd/sdxl-pixel-art/manifest.json +29 -0
  34. package/dist/catalog/image-loras/sd/sdxl-pixel-art/versions/1.0.0/manifest.json +15 -0
  35. package/dist/catalog/image-models/fl/flux-2-klein-4b-q4/manifest.json +3 -0
  36. package/dist/catalog/image-models/index.json +1 -1
  37. package/dist/catalog/image-models/qw/qwen-image-edit-2511-q4/manifest.json +44 -0
  38. package/dist/catalog/image-models/qw/qwen-image-edit-2511-q4/versions/1.0.0/manifest.json +36 -0
  39. package/dist/catalog/image-models/raw-index.json +1 -1
  40. package/dist/catalog/knowledge-catalogs/index.json +1 -1
  41. package/dist/catalog/knowledge-catalogs/raw-index.json +1 -1
  42. package/dist/catalog/knowledge-catalogs/wi/wikipedia-arts/versions/2026.10.2/manifest.json +181 -0
  43. package/dist/catalog/knowledge-catalogs/wi/wikipedia-astronomy/versions/2026.10.2/manifest.json +181 -0
  44. package/dist/catalog/knowledge-catalogs/wi/wikipedia-biology/versions/2026.10.2/manifest.json +217 -0
  45. package/dist/catalog/knowledge-catalogs/wi/wikipedia-business/versions/2026.10.2/manifest.json +197 -0
  46. package/dist/catalog/knowledge-catalogs/wi/wikipedia-film-tv/versions/2026.10.2/manifest.json +197 -0
  47. package/dist/catalog/knowledge-catalogs/wi/wikipedia-food-drink/versions/2026.10.2/manifest.json +185 -0
  48. package/dist/catalog/knowledge-catalogs/wi/wikipedia-games/versions/2026.10.2/manifest.json +181 -0
  49. package/dist/catalog/knowledge-catalogs/wi/wikipedia-general/manifest.json +55 -0
  50. package/dist/catalog/knowledge-catalogs/wi/wikipedia-general/versions/2026.10.2/manifest.json +165 -0
  51. package/dist/catalog/knowledge-catalogs/wi/wikipedia-geography/versions/2026.10.2/manifest.json +217 -0
  52. package/dist/catalog/knowledge-catalogs/wi/wikipedia-history/versions/2026.10.2/manifest.json +217 -0
  53. package/dist/catalog/knowledge-catalogs/wi/wikipedia-language/versions/2026.10.2/manifest.json +181 -0
  54. package/dist/catalog/knowledge-catalogs/wi/wikipedia-law/versions/2026.10.2/manifest.json +185 -0
  55. package/dist/catalog/knowledge-catalogs/wi/wikipedia-literature/versions/2026.10.2/manifest.json +193 -0
  56. package/dist/catalog/knowledge-catalogs/wi/wikipedia-mathematics/versions/2026.10.2/manifest.json +189 -0
  57. package/dist/catalog/knowledge-catalogs/wi/wikipedia-medicine/versions/2026.10.2/manifest.json +185 -0
  58. package/dist/catalog/knowledge-catalogs/wi/wikipedia-military/versions/2026.10.2/manifest.json +189 -0
  59. package/dist/catalog/knowledge-catalogs/wi/wikipedia-music/versions/2026.10.2/manifest.json +197 -0
  60. package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-arts/versions/2026.10.2/manifest.json +209 -0
  61. package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-business/versions/2026.10.2/manifest.json +189 -0
  62. package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-entertainment/versions/2026.10.2/manifest.json +201 -0
  63. package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-military/versions/2026.10.2/manifest.json +185 -0
  64. package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-other/versions/2026.10.2/manifest.json +165 -0
  65. package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-politics/versions/2026.10.2/manifest.json +197 -0
  66. package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-religion/versions/2026.10.2/manifest.json +201 -0
  67. package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-science/versions/2026.10.2/manifest.json +205 -0
  68. package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-sports/versions/2026.10.2/manifest.json +237 -0
  69. package/dist/catalog/knowledge-catalogs/wi/wikipedia-politics/versions/2026.10.2/manifest.json +197 -0
  70. package/dist/catalog/knowledge-catalogs/wi/wikipedia-religion-philosophy/versions/2026.10.2/manifest.json +189 -0
  71. package/dist/catalog/knowledge-catalogs/wi/wikipedia-science/versions/2026.10.2/manifest.json +185 -0
  72. package/dist/catalog/knowledge-catalogs/wi/wikipedia-sports/versions/2026.10.2/manifest.json +193 -0
  73. package/dist/catalog/knowledge-catalogs/wi/wikipedia-technology/versions/2026.10.2/manifest.json +185 -0
  74. package/dist/catalog/knowledge-catalogs/wi/wikipedia-transportation/versions/2026.10.2/manifest.json +189 -0
  75. package/dist/catalog/provenance.json +2 -1
  76. package/dist/engine-components.js +2 -2
  77. package/dist/engines/electron-native-reuse.d.ts +7 -1
  78. package/dist/engines/electron-native-reuse.js +12 -4
  79. package/dist/engines/native-file-manifest.json +546 -406
  80. package/dist/engines/native-manifest.d.ts +4 -4
  81. package/dist/engines/native-manifest.js +36 -36
  82. package/dist/engines/resolver.d.ts +1 -1
  83. package/dist/engines/resolver.js +48 -51
  84. package/dist/http/auth-context.d.ts +12 -0
  85. package/dist/http/routes/image-ensure-routes.d.ts +4 -0
  86. package/dist/http/routes/image-ensure-routes.js +102 -0
  87. package/dist/http/routes/v1-image-styles.d.ts +5 -0
  88. package/dist/http/routes/v1-image-styles.js +91 -0
  89. package/dist/http/routes/v1-images.d.ts +1 -0
  90. package/dist/http/routes/v1-images.js +15 -93
  91. package/dist/inference-service.js +2 -1
  92. package/dist/knowledge/worker-host.js +2 -0
  93. package/dist/machine-engine/inspect-runtime.js +12 -3
  94. package/dist/memory/embed-core.js +3 -0
  95. package/dist/memory/embeddings.js +2 -0
  96. package/dist/models/image-fit.js +2 -2
  97. package/dist/models/weight-facts.d.ts +5 -0
  98. package/dist/models/weight-facts.js +5 -2
  99. package/dist/providers/ds4/build-provider.js +1 -1
  100. package/dist/providers/ds4/residency.js +3 -3
  101. package/dist/providers/image/img2img-support.d.ts +36 -1
  102. package/dist/providers/image/img2img-support.js +30 -1
  103. package/dist/providers/image/local-engine.d.ts +1 -0
  104. package/dist/providers/image/local-engine.js +8 -2
  105. package/dist/providers/image/local-factory.d.ts +10 -1
  106. package/dist/providers/image/local-factory.js +33 -6
  107. package/dist/providers/image/mock.d.ts +8 -1
  108. package/dist/providers/image/mock.js +46 -1
  109. package/dist/providers/image/pull-registry.d.ts +25 -3
  110. package/dist/providers/image/pull-registry.js +96 -34
  111. package/dist/providers/image/sd-cpp.d.ts +63 -4
  112. package/dist/providers/image/sd-cpp.js +361 -38
  113. package/dist/providers/image/types.d.ts +76 -4
  114. package/dist/providers/image/types.js +8 -0
  115. package/dist/providers/llama-cpp/build-provider.js +3 -2
  116. package/dist/providers/llama-cpp/cache-adapter.d.ts +3 -0
  117. package/dist/providers/llama-cpp/cache-adapter.js +18 -4
  118. package/dist/providers/llama-cpp/devices.js +2 -2
  119. package/dist/providers/llama-cpp/engine-flags.d.ts +5 -0
  120. package/dist/providers/llama-cpp/engine-flags.js +5 -2
  121. package/dist/providers/llama-cpp/provider.js +20 -4
  122. package/dist/providers/mlx/provider.js +13 -4
  123. package/dist/providers/mlx/python/gezel_mlx_server.py +9 -2
  124. package/dist/providers/mlx/python/spec_decode.py +44 -5
  125. package/dist/providers/native/capacity-broker.d.ts +1 -1
  126. package/dist/providers/native/capacity-broker.js +8 -8
  127. package/dist/providers/native/capacity-budget.js +5 -4
  128. package/dist/providers/native/device-capacity-ledger.d.ts +2 -0
  129. package/dist/providers/native/device-capacity-ledger.js +36 -5
  130. package/dist/providers/native/device-capacity.js +4 -3
  131. package/dist/providers/native/local-engine-runtime.js +4 -4
  132. package/dist/providers/native/process-identity.d.ts +2 -0
  133. package/dist/providers/native/process-identity.js +70 -0
  134. package/dist/relevance/relevance-core.js +2 -0
  135. package/dist/relevance/relevance-model.js +2 -0
  136. package/dist/relevance/settings.js +2 -2
  137. package/dist/runtime-lock.d.ts +43 -5
  138. package/dist/runtime-lock.js +189 -26
  139. package/dist/system/host-memory.d.ts +46 -0
  140. package/dist/system/host-memory.js +89 -0
  141. package/dist/system/memory.js +5 -4
  142. package/dist/transformers-cache.js +2 -0
  143. package/dist/utils/download-common.d.ts +1 -1
  144. package/package.json +8 -8
@@ -1,4 +1,4 @@
1
- import type { ImageAspectRatio, ImageModelAuxiliaryRole, ImageQuality } from '@bendyline/gezel';
1
+ import type { ImageAspectRatio, ImageLoraPrompt, ImageLoraPurpose, ImageLoraRender, ImageModelAuxiliaryRole, ImageQuality } from '@bendyline/gezel';
2
2
  export type { ImageModelAuxiliaryRole } from '@bendyline/gezel';
3
3
  /**
4
4
  * ImageProvider — the service-side abstraction for local image generation.
@@ -69,12 +69,21 @@ export interface ImageGenerationInput {
69
69
  seed?: number;
70
70
  /**
71
71
  * Source images for image editing / reference-driven generation.
72
- * Cloud providers accept multiple; sd-cpp's img2img path uses the
73
- * first only (the others are dropped with a warning).
72
+ * Cloud providers accept multiple; a local model that reads references
73
+ * takes up to its limit, and sd-cpp's img2img path uses the first only
74
+ * (the others are dropped with a warning).
74
75
  */
75
76
  inputImages?: ImageInputBytes[];
76
- /** Img2img strength (0 = keep source, 1 = ignore source). sd-cpp only. */
77
+ /** Img2img strength (0 = keep source, 1 = ignore source). sd-cpp img2img only. */
77
78
  strength?: number;
79
+ /**
80
+ * LoRAs to apply, by catalog id: a style, a fast mode. The engine reads each
81
+ * one's file, strength, prompt words and render settings from its own
82
+ * install, so an engine that predates LoRAs renders correctly without them.
83
+ */
84
+ loras?: Array<{
85
+ id: string;
86
+ }>;
78
87
  /**
79
88
  * Optional callback fired as the engine advances through sampling
80
89
  * steps. Today only the local sd-cpp provider emits these; cloud
@@ -95,6 +104,11 @@ export declare class ImageGenerationCancelledError extends Error {
95
104
  readonly code = "image_generation_cancelled";
96
105
  constructor();
97
106
  }
107
+ /** A model made only for editing was asked for a picture with nothing to edit. */
108
+ export declare class ImageSourceRequiredError extends Error {
109
+ readonly code = "image_source_required";
110
+ constructor(modelName: string);
111
+ }
98
112
  /**
99
113
  * The engine could not fit the render into graphics memory. sd-server plans
100
114
  * placement at launch and can misjudge free memory held by another engine;
@@ -120,6 +134,11 @@ export interface ImageGenerationMeta {
120
134
  img2imgSkippedReason?: string;
121
135
  /** Provider limits or adjustments to the requested image settings. */
122
136
  settingsNotes?: string[];
137
+ /** The LoRAs the engine applied; absent when none. */
138
+ loras?: Array<{
139
+ id: string;
140
+ name: string;
141
+ }>;
123
142
  }
124
143
  export interface ImageGenerationOutput {
125
144
  png: Buffer;
@@ -134,6 +153,45 @@ export interface InstalledImageModelInfo {
134
153
  weightsKind?: 'checkpoint' | 'diffusion-model';
135
154
  /** Explicit catalog capability recorded at pull time; absent → resolve via the ladder. */
136
155
  supportsImg2Img?: boolean;
156
+ /**
157
+ * Resolved editing capability (see `resolveImageEditCapability`), so a
158
+ * caller across the machine broker or a paired server can choose a model
159
+ * without the engine's assessment map.
160
+ */
161
+ editMode?: 'reference' | 'img2img';
162
+ maxSourceImages?: number;
163
+ editOnly?: boolean;
164
+ /**
165
+ * Installed LoRAs that fit this model. Carried on the model listing because
166
+ * that listing already reaches the product daemon from a machine broker or
167
+ * a paired server; an engine that predates LoRAs lists none.
168
+ */
169
+ loras?: InstalledImageLoraSummary[];
170
+ }
171
+ /** An installed LoRA as the model listing names it. */
172
+ export interface InstalledImageLoraSummary {
173
+ id: string;
174
+ name: string;
175
+ purpose: ImageLoraPurpose;
176
+ /** The steps a speed LoRA renders in. */
177
+ steps?: number;
178
+ }
179
+ export interface InstalledImageLoraInfo extends InstalledImageLoraSummary {
180
+ baseModels: string[];
181
+ approxSizeBytes: number;
182
+ installedAt: string;
183
+ }
184
+ /** What a LoRA install needs from its catalog entry; persisted beside the file. */
185
+ export interface ImageLoraPullSpec {
186
+ downloadUrl: string;
187
+ sha256: string;
188
+ approxSizeBytes: number;
189
+ name: string;
190
+ baseModels: string[];
191
+ purpose: ImageLoraPurpose;
192
+ strength: number;
193
+ prompt?: ImageLoraPrompt;
194
+ render?: ImageLoraRender;
137
195
  }
138
196
  export type ImageModelPullEvent = {
139
197
  type: 'progress';
@@ -187,6 +245,12 @@ export interface ImageModelPullSpec {
187
245
  * weights-kind default.
188
246
  */
189
247
  supportsImg2Img?: boolean;
248
+ /** Catalog `referenceImages`, persisted the same way. */
249
+ referenceImages?: {
250
+ max: number;
251
+ };
252
+ /** Catalog `editOnly`, persisted the same way. */
253
+ editOnly?: boolean;
190
254
  /** Auxiliary files (VAE, text encoders) downloaded alongside the unet. */
191
255
  auxiliaryFiles: ImageModelAuxiliaryPullSpec[];
192
256
  }
@@ -225,6 +289,14 @@ export interface ImageProvider {
225
289
  */
226
290
  pullModel(id: string, spec: ImageModelPullSpec, signal?: AbortSignal): AsyncIterable<ImageModelPullEvent>;
227
291
  deleteModel(id: string): Promise<void>;
292
+ /**
293
+ * LoRA management for an engine that applies them (the local sd-cpp
294
+ * engine). Rendering never needs these: the model listing carries the
295
+ * installed LoRAs and `generate` takes their ids.
296
+ */
297
+ pullLora?(id: string, spec: ImageLoraPullSpec, signal?: AbortSignal): AsyncIterable<ImageModelPullEvent>;
298
+ listLoras?(): Promise<InstalledImageLoraInfo[]>;
299
+ deleteLora?(id: string): Promise<void>;
228
300
  /**
229
301
  * Probe the underlying engine for reachability without doing real
230
302
  * work. Implementations should be fast (sub-second) and never throw
@@ -6,6 +6,14 @@ export class ImageGenerationCancelledError extends Error {
6
6
  this.name = 'ImageGenerationCancelledError';
7
7
  }
8
8
  }
9
+ /** A model made only for editing was asked for a picture with nothing to edit. */
10
+ export class ImageSourceRequiredError extends Error {
11
+ code = 'image_source_required';
12
+ constructor(modelName) {
13
+ super(`${modelName} changes a picture you give it. Attach the picture to change, or pick a model that draws new ones.`);
14
+ this.name = 'ImageSourceRequiredError';
15
+ }
16
+ }
9
17
  /**
10
18
  * The engine could not fit the render into graphics memory. sd-server plans
11
19
  * placement at launch and can misjudge free memory held by another engine;
@@ -1,4 +1,3 @@
1
- import { availableParallelism } from 'node:os';
2
1
  import { delimiter, dirname, join } from 'node:path';
3
2
  import { DEFAULT_LOCAL_ENGINE_IDLE_TIMEOUT_MS, createLogger, estimateLlamaCppResidentBytes, llamaCppBackendExclusion, llamaCppBackendLabel, } from '@bendyline/gezel';
4
3
  import { LLAMA_QUARANTINE_STRIKES, clearLlamaStartupStrikes, recordLlamaStartupCrash, unrunnableBuildExit, } from '@bendyline/gezel/native';
@@ -6,6 +5,7 @@ import { gezelPaths } from '@bendyline/gezel/paths';
6
5
  import { mmprojBudgetBytes, nativeVisionEnabledFor } from '../../chat/vision-capability.js';
7
6
  import { effectiveEngineRelease, isEnginePinned } from '../../engines/native-manifest.js';
8
7
  import { engineInstallPhase } from '../../engines/registry.js';
8
+ import { availableCpuCount, containerCpuLimit } from '../../system/host-memory.js';
9
9
  import { localDeviceSettingsLabel, noModelYetMessage, notReadyError, } from '../active-install-message.js';
10
10
  import { resolveCatalogLlamaCppEngineConfig, resolveCatalogReasoningBudget, } from '../catalog-model-config.js';
11
11
  import { CapacityDeniedError, NOT_ENOUGH_MEMORY_MESSAGE, availableSystemRamBytes, formatContextCapacityDenial, minViableLocalContextTokens, planAdaptiveContextGrowth, pressureIdleGraceMs, resolveLlamaCppContextRequirement, } from '../native/capacity-broker.js';
@@ -1352,7 +1352,8 @@ export async function buildLlamaCppProvider(opts) {
1352
1352
  reasoningFormat: process.env.GEZEL_LLAMA_REASONING_FORMAT?.trim() || undefined,
1353
1353
  reasoningPreserve,
1354
1354
  backend: llamaBuildMetadata?.backend ?? process.env.GEZEL_LLAMA_SERVER_BACKEND,
1355
- hostCpuCount: availableParallelism(),
1355
+ hostCpuCount: availableCpuCount(),
1356
+ containerCpuLimit: containerCpuLimit() ?? undefined,
1356
1357
  });
1357
1358
  return {
1358
1359
  command: binary,
@@ -201,6 +201,7 @@ export declare class LlamaCppCacheAdapter implements EngineCacheAdapter {
201
201
  prepareForSend(sessionId: string, systemPrompt?: string, layers?: SystemPromptLayers): Promise<void>;
202
202
  buildRequestExtras(sessionId: string): Record<string, unknown>;
203
203
  evict(sessionIds: readonly string[]): Promise<void>;
204
+ private evictUnlocked;
204
205
  markEphemeral(sessionId: string): void;
205
206
  reportUsage(): Promise<readonly EngineCacheUsage[]>;
206
207
  warm(_sessionId: string, _messages: readonly CacheWarmMessage[]): Promise<void>;
@@ -210,6 +211,7 @@ export declare class LlamaCppCacheAdapter implements EngineCacheAdapter {
210
211
  * sessions for the next boot.
211
212
  */
212
213
  flushAll(): Promise<number>;
214
+ private flushAllUnlocked;
213
215
  /**
214
216
  * Async path: allocate a slot for `sessionId`, save the LRU victim
215
217
  * (if any) to disk, and try to restore the new slot from disk
@@ -217,6 +219,7 @@ export declare class LlamaCppCacheAdapter implements EngineCacheAdapter {
217
219
  * {@link prepareForSend}.
218
220
  */
219
221
  private allocateSlot;
222
+ private withAllocationLock;
220
223
  private assertEngineGeneration;
221
224
  private allocateSlotUnlocked;
222
225
  /**
@@ -258,7 +258,11 @@ export class LlamaCppCacheAdapter {
258
258
  };
259
259
  }
260
260
  async evict(sessionIds) {
261
- const generation = this.engineGeneration;
261
+ return this.withAllocationLock((generation) => this.evictUnlocked(sessionIds, generation));
262
+ }
263
+ async evictUnlocked(sessionIds, generation) {
264
+ if (generation !== this.engineGeneration)
265
+ return;
262
266
  // Save before forgetting so the session can resume on next access.
263
267
  // Persistence is best-effort; failures are logged and the local
264
268
  // mapping is still cleared (controller view stays consistent).
@@ -352,7 +356,11 @@ export class LlamaCppCacheAdapter {
352
356
  async flushAll() {
353
357
  if (!this.slotSavePath)
354
358
  return 0;
355
- const generation = this.engineGeneration;
359
+ return this.withAllocationLock((generation) => this.flushAllUnlocked(generation));
360
+ }
361
+ async flushAllUnlocked(generation) {
362
+ if (generation !== this.engineGeneration)
363
+ return 0;
356
364
  let saved = 0;
357
365
  for (const [sessionId, slot] of this.sessionToSlot) {
358
366
  if (await this.saveSlotForSession(slot, sessionId))
@@ -369,6 +377,13 @@ export class LlamaCppCacheAdapter {
369
377
  * {@link prepareForSend}.
370
378
  */
371
379
  async allocateSlot(sessionId) {
380
+ return this.withAllocationLock((generation) => {
381
+ this.assertEngineGeneration(generation);
382
+ return this.allocateSlotUnlocked(sessionId, generation);
383
+ });
384
+ }
385
+ // Eviction and idle flushing must not save a slot after a handoff has rebound it.
386
+ async withAllocationLock(work) {
372
387
  const generation = this.engineGeneration;
373
388
  const previous = this.allocationTail;
374
389
  let release;
@@ -377,8 +392,7 @@ export class LlamaCppCacheAdapter {
377
392
  });
378
393
  await previous;
379
394
  try {
380
- this.assertEngineGeneration(generation);
381
- return await this.allocateSlotUnlocked(sessionId, generation);
395
+ return await work(generation);
382
396
  }
383
397
  finally {
384
398
  release();
@@ -17,11 +17,11 @@
17
17
  */
18
18
  import { execFile } from 'node:child_process';
19
19
  import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
20
- import { totalmem } from 'node:os';
21
20
  import { delimiter, dirname, join } from 'node:path';
22
21
  import { promisify } from 'node:util';
23
22
  import { windowsHeadlessSpawnOptions } from '@bendyline/gezel/native';
24
23
  import { gezelHome } from '@bendyline/gezel/paths';
24
+ import { totalMemoryBytes } from '../../system/host-memory.js';
25
25
  const execFileAsync = promisify(execFile);
26
26
  const LINE_RE = /^\s*(\S+?):\s*(.+?)\s*\((\d+)\s*MiB,\s*(\d+)\s*MiB\s+free\)\s*$/;
27
27
  /**
@@ -158,7 +158,7 @@ export function llamaBinaryFingerprint(binaryPath) {
158
158
  export async function probeLlamaDevices(opts) {
159
159
  const { binaryPath, home, force } = opts;
160
160
  const now = opts.nowIso ?? (() => new Date().toISOString());
161
- const systemRamBytes = opts.systemRamBytes ?? totalmem();
161
+ const systemRamBytes = opts.systemRamBytes ?? totalMemoryBytes();
162
162
  const path = cachePath(home);
163
163
  const binary = llamaBinaryFingerprint(binaryPath);
164
164
  if (!force && existsSync(path)) {
@@ -133,6 +133,11 @@ export interface EngineFlagInput {
133
133
  backend?: string | undefined;
134
134
  /** Logical CPUs available to the daemon; caps the GPU-resident thread default. */
135
135
  hostCpuCount?: number | undefined;
136
+ /**
137
+ * CPUs a container leaves the daemon, when fewer than the host has (see
138
+ * `containerCpuLimit`). Without it llama.cpp sizes its pool from the host.
139
+ */
140
+ containerCpuLimit?: number | undefined;
136
141
  }
137
142
  /** Default `--cache-reuse` chunk when neither config nor manifest set it. */
138
143
  export declare const DEFAULT_CACHE_REUSE = 256;
@@ -100,7 +100,7 @@ function normalizeFlagKey(key) {
100
100
  * `args` array.
101
101
  */
102
102
  export function buildLlamaCppEngineArgs(input) {
103
- const { config, perModel, planner, kvCacheType, slots, ggufHasMtp, installedDraftModelPath, reasoningFormat, reasoningPreserve, backend, hostCpuCount, } = input;
103
+ const { config, perModel, planner, kvCacheType, slots, ggufHasMtp, installedDraftModelPath, reasoningFormat, reasoningPreserve, backend, hostCpuCount, containerCpuLimit, } = input;
104
104
  const args = [];
105
105
  // ── GPU-layer offload (`--n-gpu-layers`) ──────────────────────────
106
106
  const nGpuLayers = config.llamaCppNGpuLayers ?? perModel?.nGpuLayers ?? planner?.nGpuLayers;
@@ -203,9 +203,12 @@ export function buildLlamaCppEngineArgs(input) {
203
203
  !(typeof nCpuMoe === 'number' && nCpuMoe > 0) &&
204
204
  !(typeof nCpuFfn === 'number' && nCpuFfn > 0) &&
205
205
  !extraArgsOwnThreads(config.llamaCppExtraArgs);
206
+ // In a container that allows fewer CPUs than the host has, the all-cores
207
+ // default oversubscribes the quota and spends each period throttled.
206
208
  const threads = config.llamaCppThreads ??
207
209
  perModel?.threads ??
208
- (gpuResident ? gpuResidentThreads(hostCpuCount) : undefined);
210
+ (gpuResident ? gpuResidentThreads(hostCpuCount) : undefined) ??
211
+ (extraArgsOwnThreads(config.llamaCppExtraArgs) ? undefined : containerCpuLimit);
209
212
  if (typeof threads === 'number')
210
213
  args.push('--threads', String(threads));
211
214
  const batchSize = config.llamaCppBatchSize ?? perModel?.batchSize;
@@ -32,6 +32,13 @@ import { classifyStartupLine } from './stdout-parser.js';
32
32
  export * from '@bendyline/gezel/local-loop';
33
33
  export { isFileRepairTurn as isScenarioFileRepairTurn } from '@bendyline/gezel/local-loop';
34
34
  const log = createLogger('llama-cpp');
35
+ /**
36
+ * How long shutdown waits for the slot caches to reach disk. The app gives
37
+ * the whole daemon 15 s to quit, and a save the engine never answers must
38
+ * not spend all of it: an unsaved or half-written slot only means a fresh
39
+ * prefill next time, because a restore that fails falls back to one.
40
+ */
41
+ const SHUTDOWN_FLUSH_BUDGET_MS = 8_000;
35
42
  export class LlamaCppProvider {
36
43
  name = 'llama-cpp';
37
44
  queue;
@@ -627,12 +634,21 @@ export class LlamaCppProvider {
627
634
  // llama-server exits the slot save endpoint is gone. flushAll is
628
635
  // best-effort; failures during shutdown shouldn't block the stop.
629
636
  if (this.cacheAdapter) {
637
+ let timer;
638
+ const budget = new Promise((resolve) => {
639
+ timer = setTimeout(() => resolve('timeout'), SHUTDOWN_FLUSH_BUDGET_MS);
640
+ timer.unref?.();
641
+ });
630
642
  try {
631
- await this.cacheAdapter.flushAll();
643
+ // Best-effort persistence: a failure only means the next boot pays
644
+ // the prefill cost on resume.
645
+ const outcome = await Promise.race([this.cacheAdapter.flushAll().catch(() => 0), budget]);
646
+ if (outcome === 'timeout') {
647
+ log.warn(`slot cache save still running after ${SHUTDOWN_FLUSH_BUDGET_MS}ms at shutdown; stopping the engine`);
648
+ }
632
649
  }
633
- catch {
634
- // Best-effort persistence — failure here just means the next
635
- // boot pays the prefill cost on resume, same as before.
650
+ finally {
651
+ clearTimeout(timer);
636
652
  }
637
653
  }
638
654
  await this.supervisor?.stop();
@@ -73,7 +73,7 @@ export { buildMidStreamDropMessage, computePreFirstByteBudgetMs, estimatePromptT
73
73
  import { FILE_REPAIR_MUTATION_TOOLS, FILE_REPAIR_READ_TOOLS, applyConstrainedTurnShape, disableTemplateThinking, fileRepairTargetPath, isSourceFileRepairPrompt, requiredToolOnly, writeFileOnlyTools, } from '../constrained-turn.js';
74
74
  import { hasExplicitFullFileRewriteWording } from '../direct-file-work-prompt.js';
75
75
  import { buildImmediateFileWriteNoMutationNudge, buildPrerequisiteRepairReadNudge, buildScenarioRepairNoMutationNudge, completeWorkspaceReadPaths, extractPrerequisiteRepairReadPaths, normalizeWorkspacePathForCompare, remainingPrerequisiteRepairReadPaths, } from '../file-repair-policy.js';
76
- import { LOCAL_TURN_LIMITS, LocalTurnPolicy, compactLocalTurn, planFileTurn, repairTools, turnCheckpoint, } from '../local-turn-policy.js';
76
+ import { ConstrainedWriteRecovery, LOCAL_TURN_LIMITS, LocalTurnPolicy, compactLocalTurn, planFileTurn, repairTools, turnCheckpoint, } from '../local-turn-policy.js';
77
77
  const MAX_TOOL_LOOP_TURNS = LOCAL_TURN_LIMITS.iterations;
78
78
  // Hard per-turn ceiling when the caller doesn't pass `opts.timeoutMs`.
79
79
  // 10 minutes matches what the manager passes for local providers in
@@ -984,6 +984,7 @@ class MlxSession extends StreamingSessionBase {
984
984
  // validation error every iteration, "diagnosing" the schema in
985
985
  // chat, and re-trying the same wrong shape forever. See the
986
986
  // tracker's docstring for the threshold rationale.
987
+ const writeRecovery = new ConstrainedWriteRecovery();
987
988
  const failureTracker = new ToolFailureTracker({
988
989
  surgicalEditsAvailable,
989
990
  delegationAvailable,
@@ -1164,7 +1165,9 @@ class MlxSession extends StreamingSessionBase {
1164
1165
  log.debug(`turn#${seq}.${turn} required-call turn: ${opts.requiredTool} only`);
1165
1166
  }
1166
1167
  }
1167
- const fileTurnPlan = planFileTurn(prompt, tools, opts?.fileTurnIntent);
1168
+ const fileTurnPlan = writeRecovery.active
1169
+ ? { kind: 'ordinary' }
1170
+ : planFileTurn(prompt, tools, opts?.fileTurnIntent);
1168
1171
  const unreadInputs = unreadRequiredInputs(requiredInputs, requiredInputReads);
1169
1172
  const immediateFileWriteTurn = fileTurnPlan.kind === 'create-file' && unreadInputs.length === 0;
1170
1173
  if (fileTurnPlan.kind === 'create-file' &&
@@ -1219,6 +1222,11 @@ class MlxSession extends StreamingSessionBase {
1219
1222
  (typeof body.max_tokens !== 'number' || body.max_tokens < FILE_WRITE_MIN_TOKENS)) {
1220
1223
  body.max_tokens = FILE_WRITE_MIN_TOKENS;
1221
1224
  }
1225
+ if (writeRecovery.lookupPending) {
1226
+ requestTools = tools?.filter((tool) => chatCompletionToolName(tool) === writeRecovery.lookupPending);
1227
+ applyConstrainedTurnShape(body);
1228
+ log.debug(`turn#${seq}.${turn} grounding recovery: research-only surface=${writeRecovery.lookupPending}`);
1229
+ }
1222
1230
  if (requestTools && requestTools.length > 0)
1223
1231
  body.tools = requestTools;
1224
1232
  // Tool-schema size breakdown (opt-in: GEZEL_PROMPT_BREAKDOWN=1). The
@@ -2803,6 +2811,7 @@ class MlxSession extends StreamingSessionBase {
2803
2811
  }
2804
2812
  }
2805
2813
  const tracked = failureTracker.recordResult(call.function.name, output);
2814
+ writeRecovery.recordResult(call.function.name, output, tracked, tools);
2806
2815
  terminalActionClosing ??= terminalToolClosingText(this.deps.terminalToolPolicy, call.function.name, args, output);
2807
2816
  // Layer the same-args repeat tracker on top of the failure
2808
2817
  // tracker. The failure tracker catches "tool keeps erroring";
@@ -2877,7 +2886,7 @@ class MlxSession extends StreamingSessionBase {
2877
2886
  });
2878
2887
  }
2879
2888
  this.turnPolicy.checkpoint();
2880
- if (fileRepairTurn && repairMutationSucceeded) {
2889
+ if (fileRepairTurn && repairMutationSucceeded && !writeRecovery.active) {
2881
2890
  const closing = 'Updated the requested file.';
2882
2891
  this.messages.push({ role: 'assistant', content: closing });
2883
2892
  return closing;
@@ -2898,7 +2907,7 @@ class MlxSession extends StreamingSessionBase {
2898
2907
  log.info(`turn#${seq} END terminal-tool-bail afterMs=${Date.now() - start} loopTurns=${turn + 1}`);
2899
2908
  return fullText;
2900
2909
  }
2901
- if (immediateFileWritePaths.length > 0) {
2910
+ if (immediateFileWritePaths.length > 0 && !writeRecovery.active) {
2902
2911
  // File was truncated mid-content and we still have continuation
2903
2912
  // budget — keep the turn alive instead of bailing on a partial.
2904
2913
  // The tool result already carries the "append the rest" hint;
@@ -2759,10 +2759,17 @@ class BatchEngine:
2759
2759
  cache_seed.trim_layers(cache_layers, excess)
2760
2760
  except Exception: # noqa: BLE001 — snapshot path covers hybrids
2761
2761
  pass
2762
+ cache_tokens = full_prompt + [int(t) for t in sub.token_ids]
2763
+ if cache_seed.all_trimmable(cache_layers):
2764
+ cache_tokens = spec_decode.cached_token_prefix(cache_layers, cache_tokens)
2765
+ if cache_tokens is None:
2766
+ # Leave any prior valid entry intact rather than persisting
2767
+ # cache layers whose token boundary cannot be established.
2768
+ cache_layers = None
2762
2769
  r = types.SimpleNamespace(
2763
2770
  finish_reason=finish_reason,
2764
- prompt_cache=list(cache_layers),
2765
- all_tokens=full_prompt + [int(t) for t in sub.token_ids],
2771
+ prompt_cache=list(cache_layers) if cache_layers is not None else None,
2772
+ all_tokens=cache_tokens,
2766
2773
  token=sub.token_ids[-1] if sub.token_ids else 0,
2767
2774
  )
2768
2775
  self._finish(sub, r)
@@ -117,8 +117,11 @@ def resolve_spec(args, model, text_tower: Optional[str], log=print) -> Optional[
117
117
  except Exception as exc: # noqa: BLE001
118
118
  _off(log, f"drafter incompatible with target: {exc}")
119
119
  return None
120
- if not install_chunked_draft_prefill(
121
- drafter, int(getattr(args, "prefill_step_size", 0) or 0), log=log
120
+ if (
121
+ callable(getattr(drafter, "prefill_from_target_hidden", None))
122
+ and not install_chunked_draft_prefill(
123
+ drafter, int(getattr(args, "prefill_step_size", 0) or 0), log=log
124
+ )
122
125
  ):
123
126
  log(
124
127
  "[spec] drafter has no incremental prefill surface — long prompts "
@@ -240,6 +243,11 @@ def draft_prefill_gate(spec: Optional[SpecState], prompt_len: int) -> Tuple[bool
240
243
  throw that work away."""
241
244
  if spec is None:
242
245
  return True, ""
246
+ # Shared-KV drafters (e.g. Gemma's assistant) have no separate prompt
247
+ # prefill. Both round loops skip this optional hook, so there is no
248
+ # quadratic drafter prefill to limit for that capability shape.
249
+ if not callable(getattr(spec.drafter, "prefill_from_target_hidden", None)):
250
+ return True, ""
243
251
  if getattr(spec.drafter, "_gezel_chunked_prefill", False):
244
252
  return True, ""
245
253
  if int(prompt_len) > UNCHUNKED_DRAFT_PREFILL_LIMIT:
@@ -278,9 +286,21 @@ def cache_matches_tower(layers, tower) -> bool:
278
286
  return False
279
287
  if layers is None or len(layers) != len(expect):
280
288
  return False
281
- return all(
282
- type(a).__name__ == type(b).__name__ for a, b in zip(layers, expect)
283
- )
289
+
290
+ def matches(actual, expected):
291
+ if type(actual).__name__ == type(expected).__name__:
292
+ return True
293
+ # Upstream MTP deliberately wraps a rotating cache in this subclass
294
+ # to roll back rejected drafts. It is still the same target layout;
295
+ # rejecting our own saved cache here forces every follow-up cold.
296
+ # Do not admit arbitrary subclasses (e.g. batch-shaped cache adapters).
297
+ return (
298
+ type(expected).__name__ == "RotatingKVCache"
299
+ and type(actual).__name__ == "BufferedRotatingKVCache"
300
+ and isinstance(actual, type(expected))
301
+ )
302
+
303
+ return all(matches(a, b) for a, b in zip(layers, expect))
284
304
 
285
305
 
286
306
  def clone_layers(layers) -> List[Any]:
@@ -294,6 +314,25 @@ def clone_layers(layers) -> List[Any]:
294
314
  return out
295
315
 
296
316
 
317
+ def cached_token_prefix(layers, tokens: Sequence[int]) -> Optional[List[int]]:
318
+ """Describe only tokens actually fed into a trimmable target cache.
319
+
320
+ A final bonus token can be emitted before it is fed back to the target.
321
+ Saving it as cached makes the next seed fail its offset consistency guard.
322
+ Unknown or disagreeing offsets must remain a miss, never guessed state.
323
+ """
324
+ try:
325
+ offsets = [int(c.offset) for c in layers]
326
+ except (AttributeError, TypeError, ValueError):
327
+ return None
328
+ if not offsets or len(set(offsets)) != 1:
329
+ return None
330
+ count = offsets[0]
331
+ if count <= 0 or count > len(tokens):
332
+ return None
333
+ return list(tokens[:count])
334
+
335
+
297
336
  def chunked_prefill_steps(
298
337
  tower,
299
338
  cache,
@@ -31,7 +31,7 @@ export interface CapacityBrokerOptions {
31
31
  * tests and for users who want to opt out of the budget entirely.
32
32
  */
33
33
  budgetBytes?: number;
34
- /** Test seam — defaults to {@link totalmem}. */
34
+ /** Test seam — defaults to {@link totalMemoryBytes}. */
35
35
  systemRamBytes?: () => number;
36
36
  /**
37
37
  * Whether this host shares one memory pool between CPU and GPU, which
@@ -18,8 +18,8 @@
18
18
  */
19
19
  import { execFileSync } from 'node:child_process';
20
20
  import { readFileSync } from 'node:fs';
21
- import { freemem, totalmem } from 'node:os';
22
21
  import { CONSTRAINED_LOCAL_CONTEXT_FLOOR_TOKENS, LLAMA_CPP_COMPUTE_HEADROOM, LOCAL_CONTEXT_FLOOR_TOKENS, MLX_COMPUTE_HEADROOM, createLogger, estimateLlamaCppResidentBytes, estimateMlxResidentBytes, localContextFloorTokens, } from '@bendyline/gezel';
22
+ import { capToContainer, freeMemoryBytes, totalMemoryBytes } from '../../system/host-memory.js';
23
23
  import { autoAllowRamSpillover, computeCapacityBudget, fastMemoryBudgetBytes, getDetectedGpuVramBytes, } from './capacity-budget.js';
24
24
  export { autoAllowRamSpillover, autoDetectBudgetBytes, computeCapacityBudget, getDetectedGpuVramBytes, pressureIdleGraceMs, setDetectedGpuVramBytes, } from './capacity-budget.js';
25
25
  export { fastMemoryBudgetBytes };
@@ -156,7 +156,7 @@ export class CapacityBroker {
156
156
  explicitAllowRamSpillover;
157
157
  reservations = new Map();
158
158
  constructor(opts = {}) {
159
- this.systemRamBytes = (opts.systemRamBytes ?? totalmem)();
159
+ this.systemRamBytes = (opts.systemRamBytes ?? totalMemoryBytes)();
160
160
  this.unifiedMemory = opts.unifiedMemory;
161
161
  this.gpuVramBytes = opts.gpuVramBytes;
162
162
  this.reservedGpuBytes = opts.reservedGpuBytes;
@@ -467,24 +467,24 @@ export function availableSystemRamBytes() {
467
467
  // vm_stat categories overlap (file-backed pages span active+inactive),
468
468
  // so the raw sum can exceed physical RAM; cap at 90% of totalmem — the
469
469
  // consumers already hold back their own OS reserves on top.
470
- const cap = totalmem() * 0.9;
470
+ const cap = totalMemoryBytes() * 0.9;
471
471
  try {
472
472
  if (process.platform === 'darwin') {
473
473
  const out = execFileSync('/usr/bin/vm_stat', { encoding: 'utf8', timeout: 3_000 });
474
474
  const parsed = parseVmStatAvailableBytes(out);
475
475
  if (parsed !== null)
476
- return Math.max(Math.min(parsed, cap), freemem());
476
+ return capToContainer(Math.max(Math.min(parsed, cap), freeMemoryBytes()));
477
477
  }
478
478
  else if (process.platform === 'linux') {
479
479
  const parsed = parseMeminfoAvailableBytes(readFileSync('/proc/meminfo', 'utf8'));
480
480
  if (parsed !== null)
481
- return Math.max(Math.min(parsed, cap), freemem());
481
+ return capToContainer(Math.max(Math.min(parsed, cap), freeMemoryBytes()));
482
482
  }
483
483
  }
484
484
  catch {
485
485
  // Probe failure falls through to the conservative number.
486
486
  }
487
- return freemem();
487
+ return freeMemoryBytes();
488
488
  }
489
489
  /** Pure vm_stat parser (exported for tests). Returns null when the output
490
490
  * doesn't look like vm_stat. */
@@ -664,7 +664,7 @@ export function llamaCppSlotCeiling(opts) {
664
664
  * going to free 5 GiB, because nothing was holding it but the estimate.
665
665
  * Planning and admission must read one number.
666
666
  */
667
- export function liveRamOsReserveBytes(totalRamBytes = totalmem()) {
667
+ export function liveRamOsReserveBytes(totalRamBytes = totalMemoryBytes()) {
668
668
  return Math.min(4 * GIB, Math.max(2 * GIB, totalRamBytes * 0.1));
669
669
  }
670
670
  /**
@@ -769,7 +769,7 @@ export function minViableLocalContextTokens(opts) {
769
769
  const override = envMinContextTokens();
770
770
  if (override !== null)
771
771
  return override;
772
- const systemRamBytes = opts?.systemRamBytes ?? totalmem();
772
+ const systemRamBytes = opts?.systemRamBytes ?? totalMemoryBytes();
773
773
  const budget = opts?.budget ?? computeCapacityBudget({ systemRamBytes });
774
774
  return localContextFloorTokens({
775
775
  totalRamBytes: systemRamBytes,
@@ -1,4 +1,5 @@
1
- import { arch, platform, totalmem } from 'node:os';
1
+ import { arch, platform } from 'node:os';
2
+ import { totalMemoryBytes } from '../../system/host-memory.js';
2
3
  const GIB = 1024 ** 3;
3
4
  /**
4
5
  * Ceiling on the auto-derived SYSTEM-RAM share, however much RAM the host
@@ -152,7 +153,7 @@ export function isUnifiedMemoryHost() {
152
153
  * `GEZEL_CAPACITY_BUDGET_GB` overrides the total, including the setting.
153
154
  */
154
155
  export function computeCapacityBudget(input = {}) {
155
- const systemRamBytes = input.systemRamBytes ?? totalmem();
156
+ const systemRamBytes = input.systemRamBytes ?? totalMemoryBytes();
156
157
  // Reading the ambient probe is for callers that know nothing about the
157
158
  // host. One that passes `unifiedMemory` has already described it.
158
159
  const measuredVram = input.gpuVramBytes !== undefined
@@ -269,7 +270,7 @@ function envOverrideBytes() {
269
270
  * (Settings → the engine pages) is the dial for owners who want that traded
270
271
  * either way.
271
272
  */
272
- export function autoDetectBudgetBytes(systemRamBytes = totalmem(), opts = {}) {
273
+ export function autoDetectBudgetBytes(systemRamBytes = totalMemoryBytes(), opts = {}) {
273
274
  return computeCapacityBudget({ systemRamBytes, ...opts }).budgetBytes;
274
275
  }
275
276
  /**
@@ -277,7 +278,7 @@ export function autoDetectBudgetBytes(systemRamBytes = totalmem(), opts = {}) {
277
278
  * the whole budget otherwise. KV slots, prompt caches, and anything else
278
279
  * that must live where the compute happens size against this.
279
280
  */
280
- export function fastMemoryBudgetBytes(systemRamBytes = totalmem(), opts = {}) {
281
+ export function fastMemoryBudgetBytes(systemRamBytes = totalMemoryBytes(), opts = {}) {
281
282
  return computeCapacityBudget({ systemRamBytes, ...opts }).fastBytes;
282
283
  }
283
284
  /** Fast pool at or below which a host is treated as accelerator-constrained. */
@@ -1,4 +1,5 @@
1
1
  import type { NativeCapacityCommand, NativeCapacityReply } from '@bendyline/gezel';
2
+ import { readProcessIdentities } from './process-identity.js';
2
3
  export interface DeviceCapacitySample {
3
4
  budgetBytes: number;
4
5
  gpuBudgetBytes: number;
@@ -25,6 +26,7 @@ export declare class DeviceCapacityLedger {
25
26
  directory: string;
26
27
  sample: () => Promise<DeviceCapacitySample>;
27
28
  alive?: (pid: number) => boolean;
29
+ identities?: typeof readProcessIdentities;
28
30
  });
29
31
  execute(command: NativeCapacityCommand): Promise<NativeCapacityReply>;
30
32
  private transact;