@bendyline/gezel-inference 1.0.6 → 1.0.8

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 (152) 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.js +1 -0
  83. package/dist/http/auth-context.d.ts +12 -0
  84. package/dist/http/routes/image-ensure-routes.d.ts +4 -0
  85. package/dist/http/routes/image-ensure-routes.js +102 -0
  86. package/dist/http/routes/v1-image-styles.d.ts +5 -0
  87. package/dist/http/routes/v1-image-styles.js +91 -0
  88. package/dist/http/routes/v1-images.d.ts +3 -1
  89. package/dist/http/routes/v1-images.js +48 -99
  90. package/dist/inference-service.js +2 -1
  91. package/dist/knowledge/manager.d.ts +3 -27
  92. package/dist/knowledge/manager.js +8 -364
  93. package/dist/knowledge/unified-search.d.ts +61 -0
  94. package/dist/knowledge/unified-search.js +373 -0
  95. package/dist/knowledge/worker-host.js +2 -0
  96. package/dist/machine-engine/inspect-runtime.js +12 -3
  97. package/dist/memory/embed-core.js +3 -0
  98. package/dist/memory/embeddings.js +2 -0
  99. package/dist/models/image-fit.js +2 -2
  100. package/dist/models/weight-facts.d.ts +5 -0
  101. package/dist/models/weight-facts.js +5 -2
  102. package/dist/providers/ds4/build-provider.js +1 -1
  103. package/dist/providers/ds4/residency.js +3 -3
  104. package/dist/providers/image/img2img-support.d.ts +36 -1
  105. package/dist/providers/image/img2img-support.js +30 -1
  106. package/dist/providers/image/local-engine.d.ts +1 -0
  107. package/dist/providers/image/local-engine.js +8 -2
  108. package/dist/providers/image/local-factory.d.ts +10 -1
  109. package/dist/providers/image/local-factory.js +33 -6
  110. package/dist/providers/image/mock.d.ts +8 -1
  111. package/dist/providers/image/mock.js +46 -1
  112. package/dist/providers/image/pull-registry.d.ts +25 -3
  113. package/dist/providers/image/pull-registry.js +96 -34
  114. package/dist/providers/image/sd-cpp.d.ts +63 -4
  115. package/dist/providers/image/sd-cpp.js +368 -38
  116. package/dist/providers/image/types.d.ts +76 -4
  117. package/dist/providers/image/types.js +8 -0
  118. package/dist/providers/llama-cpp/build-provider.js +3 -2
  119. package/dist/providers/llama-cpp/cache-adapter.d.ts +3 -0
  120. package/dist/providers/llama-cpp/cache-adapter.js +18 -4
  121. package/dist/providers/llama-cpp/devices.js +2 -2
  122. package/dist/providers/llama-cpp/engine-flags.d.ts +5 -0
  123. package/dist/providers/llama-cpp/engine-flags.js +5 -2
  124. package/dist/providers/mlx/async-file-handoff.d.ts +9 -1
  125. package/dist/providers/mlx/async-file-handoff.js +32 -4
  126. package/dist/providers/mlx/provider.js +41 -20
  127. package/dist/providers/mlx/python/gezel_mlx_server.py +9 -2
  128. package/dist/providers/mlx/python/spec_decode.py +44 -5
  129. package/dist/providers/mlx/runtime-diagnostics.d.ts +6 -0
  130. package/dist/providers/mlx/runtime-diagnostics.js +18 -1
  131. package/dist/providers/mlx/tool-call-protocol.d.ts +4 -3
  132. package/dist/providers/mlx/tool-call-protocol.js +4 -3
  133. package/dist/providers/native/capacity-broker.d.ts +1 -1
  134. package/dist/providers/native/capacity-broker.js +8 -8
  135. package/dist/providers/native/capacity-budget.js +5 -4
  136. package/dist/providers/native/device-capacity-ledger.d.ts +5 -1
  137. package/dist/providers/native/device-capacity-ledger.js +60 -14
  138. package/dist/providers/native/device-capacity.js +4 -3
  139. package/dist/providers/native/local-engine-runtime.js +4 -4
  140. package/dist/providers/native/process-identity.d.ts +40 -0
  141. package/dist/providers/native/process-identity.js +156 -0
  142. package/dist/relevance/relevance-core.js +2 -0
  143. package/dist/relevance/relevance-model.js +2 -0
  144. package/dist/relevance/settings.js +2 -2
  145. package/dist/runtime-lock.d.ts +19 -5
  146. package/dist/runtime-lock.js +116 -26
  147. package/dist/system/host-memory.d.ts +46 -0
  148. package/dist/system/host-memory.js +89 -0
  149. package/dist/system/memory.js +5 -4
  150. package/dist/transformers-cache.js +2 -0
  151. package/dist/utils/download-common.d.ts +1 -1
  152. package/package.json +8 -8
@@ -1,3 +1,6 @@
1
+ import { type LockIdentity, currentLockIdentity, parseLockFile, procStartTicks, sharesPidNamespace } from '@bendyline/gezel/native';
2
+ export type { LockIdentity };
3
+ export { currentLockIdentity, parseLockFile, procStartTicks, sharesPidNamespace };
1
4
  /**
2
5
  * Thrown when a gezel home already has a live daemon. The CLI's `gezel
3
6
  * start` / the supervisor surface this as an actionable message rather
@@ -6,7 +9,8 @@
6
9
  export declare class SingleInstanceError extends Error {
7
10
  readonly holderPid: number;
8
11
  readonly lockPath: string;
9
- constructor(holderPid: number, lockPath: string);
12
+ readonly holderHost?: string | undefined;
13
+ constructor(holderPid: number, lockPath: string, holderHost?: string | undefined);
10
14
  }
11
15
  export interface RuntimeLock {
12
16
  release(): Promise<void>;
@@ -15,15 +19,25 @@ export interface RuntimeLock {
15
19
  * Acquire an exclusive single-instance lock for a gezel home so two daemons
16
20
  * can't share one `~/.gezel/` and race its on-disk state (config, sessions,
17
21
  * tasks, runtime files). Atomic `O_CREAT|O_EXCL` create of
18
- * `<runtimeDir>/lock` holding the owner pid; a stale lock (dead holder) is
22
+ * `<runtimeDir>/lock` holding the owner's identity; a stale lock is
19
23
  * reclaimed. Throws {@link SingleInstanceError} when a live daemon holds it.
20
24
  *
21
- * The returned `release()` removes the lock — call it on graceful shutdown.
22
- * A hard crash leaves a stale lock that the next start reclaims via the
23
- * pid-liveness check.
25
+ * While held, the lock file's mtime is touched every few seconds: that
26
+ * heartbeat is how a daemon in another container (another pid namespace)
27
+ * is told apart from a dead one.
28
+ *
29
+ * The returned `release()` stops the heartbeat and removes the lock — call
30
+ * it on graceful shutdown. A hard crash leaves a stale lock the next start
31
+ * reclaims.
24
32
  */
25
33
  export declare function acquireSingleInstanceLock(opts: {
26
34
  runtimeDir: string;
27
35
  lockPath: string;
28
36
  isAlive?: (pid: number) => boolean;
37
+ identity?: LockIdentity;
38
+ startTicksOf?: (pid: number) => string | undefined;
39
+ now?: () => number;
40
+ sleep?: (ms: number) => Promise<void>;
41
+ heartbeatIntervalMs?: number;
42
+ staleAfterMs?: number;
29
43
  }): Promise<RuntimeLock>;
@@ -1,4 +1,6 @@
1
- import { mkdir, open, readFile, rm } from 'node:fs/promises';
1
+ import { mkdir, open, readFile, rm, stat, utimes } from 'node:fs/promises';
2
+ import { LOCK_HEARTBEAT_INTERVAL_MS, LOCK_STALE_AFTER_MS, currentLockIdentity, foreignHolderMessage, parseLockFile, procStartTicks, sharesPidNamespace, } from '@bendyline/gezel/native';
3
+ export { currentLockIdentity, parseLockFile, procStartTicks, sharesPidNamespace };
2
4
  /**
3
5
  * Thrown when a gezel home already has a live daemon. The CLI's `gezel
4
6
  * start` / the supervisor surface this as an actionable message rather
@@ -7,15 +9,21 @@ import { mkdir, open, readFile, rm } from 'node:fs/promises';
7
9
  export class SingleInstanceError extends Error {
8
10
  holderPid;
9
11
  lockPath;
10
- constructor(holderPid, lockPath) {
11
- super(holderPid > 0
12
- ? `Another gezel daemon is already running for this home (pid ${holderPid}). Stop it first with \`gezel stop\`, or use a different GEZEL_HOME.`
13
- : `Could not acquire the single-instance lock at ${lockPath}.`);
12
+ holderHost;
13
+ constructor(holderPid, lockPath, holderHost) {
14
+ super(holderHost !== undefined
15
+ ? foreignHolderMessage({ pid: holderPid, hostname: holderHost })
16
+ : holderPid > 0
17
+ ? `Another gezel daemon is already running for this home (pid ${holderPid}). Stop it first with \`gezel stop\`, or use a different GEZEL_HOME.`
18
+ : `Could not acquire the single-instance lock at ${lockPath}.`);
14
19
  this.holderPid = holderPid;
15
20
  this.lockPath = lockPath;
21
+ this.holderHost = holderHost;
16
22
  this.name = 'SingleInstanceError';
17
23
  }
18
24
  }
25
+ // A holder's identity (core's lock-identity.ts) is what let a one-shot
26
+ // `docker run … gezel do` stop reclaiming a serving container's live lock.
19
27
  function defaultIsAlive(pid) {
20
28
  try {
21
29
  process.kill(pid, 0);
@@ -27,55 +35,104 @@ function defaultIsAlive(pid) {
27
35
  return err.code === 'EPERM';
28
36
  }
29
37
  }
30
- async function readHolderPid(lockPath) {
38
+ /**
39
+ * The pid stays on the first line so a daemon from before the identity
40
+ * record (which parses the leading integer) still reads it as held instead
41
+ * of stale.
42
+ */
43
+ function serializeIdentity(identity) {
44
+ return `${identity.pid}\n${JSON.stringify({ v: 1, ...identity })}\n`;
45
+ }
46
+ async function readHolder(lockPath) {
47
+ try {
48
+ return parseLockFile(await readFile(lockPath, 'utf8'));
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
54
+ async function lockMtimeMs(lockPath) {
31
55
  try {
32
- const pid = Number.parseInt((await readFile(lockPath, 'utf8')).trim(), 10);
33
- return Number.isFinite(pid) ? pid : null;
56
+ return (await stat(lockPath)).mtimeMs;
34
57
  }
35
58
  catch {
36
59
  return null;
37
60
  }
38
61
  }
62
+ /**
63
+ * A holder outside our pid namespace is alive while its heartbeat moves.
64
+ * A fresh-looking mtime may be a holder that died moments ago (a killed
65
+ * container restarting onto its own volume), so wait until the heartbeat
66
+ * either advances (alive) or ages past the stale bound (dead).
67
+ */
68
+ async function foreignHolderIsAlive(lockPath, opts) {
69
+ const first = await lockMtimeMs(lockPath);
70
+ if (first === null)
71
+ return false;
72
+ for (;;) {
73
+ if (opts.now() - first > opts.staleAfterMs)
74
+ return false;
75
+ await opts.sleep(1_000);
76
+ const current = await lockMtimeMs(lockPath);
77
+ if (current === null)
78
+ return false;
79
+ if (current !== first)
80
+ return true;
81
+ }
82
+ }
39
83
  /**
40
84
  * Acquire an exclusive single-instance lock for a gezel home so two daemons
41
85
  * can't share one `~/.gezel/` and race its on-disk state (config, sessions,
42
86
  * tasks, runtime files). Atomic `O_CREAT|O_EXCL` create of
43
- * `<runtimeDir>/lock` holding the owner pid; a stale lock (dead holder) is
87
+ * `<runtimeDir>/lock` holding the owner's identity; a stale lock is
44
88
  * reclaimed. Throws {@link SingleInstanceError} when a live daemon holds it.
45
89
  *
46
- * The returned `release()` removes the lock — call it on graceful shutdown.
47
- * A hard crash leaves a stale lock that the next start reclaims via the
48
- * pid-liveness check.
90
+ * While held, the lock file's mtime is touched every few seconds: that
91
+ * heartbeat is how a daemon in another container (another pid namespace)
92
+ * is told apart from a dead one.
93
+ *
94
+ * The returned `release()` stops the heartbeat and removes the lock — call
95
+ * it on graceful shutdown. A hard crash leaves a stale lock the next start
96
+ * reclaims.
49
97
  */
50
98
  export async function acquireSingleInstanceLock(opts) {
51
99
  const isAlive = opts.isAlive ?? defaultIsAlive;
100
+ const me = opts.identity ?? currentLockIdentity();
101
+ const startTicksOf = opts.startTicksOf ?? procStartTicks;
102
+ const now = opts.now ?? Date.now;
103
+ const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
104
+ const staleAfterMs = opts.staleAfterMs ?? LOCK_STALE_AFTER_MS;
52
105
  await mkdir(opts.runtimeDir, { recursive: true });
53
106
  for (let attempt = 0; attempt < 3; attempt++) {
54
107
  try {
55
108
  const handle = await open(opts.lockPath, 'wx'); // fails with EEXIST if present
56
109
  try {
57
- await handle.writeFile(`${process.pid}\n`);
110
+ await handle.writeFile(serializeIdentity(me));
58
111
  }
59
112
  finally {
60
113
  await handle.close();
61
114
  }
62
- return {
63
- async release() {
64
- // Only remove the lock if it's still ours — defends against a
65
- // narrow race where another process reclaimed our stale lock.
66
- const holder = await readHolderPid(opts.lockPath);
67
- if (holder === null || holder === process.pid) {
68
- await rm(opts.lockPath, { force: true }).catch(() => { });
69
- }
70
- },
71
- };
115
+ return holdLock(opts.lockPath, me, opts.heartbeatIntervalMs ?? LOCK_HEARTBEAT_INTERVAL_MS);
72
116
  }
73
117
  catch (err) {
74
118
  if (err.code !== 'EEXIST')
75
119
  throw err;
76
- const holder = await readHolderPid(opts.lockPath);
77
- if (holder !== null && holder !== process.pid && isAlive(holder)) {
78
- throw new SingleInstanceError(holder, opts.lockPath);
120
+ const holder = await readHolder(opts.lockPath);
121
+ if (holder) {
122
+ if (!sharesPidNamespace(holder, me)) {
123
+ if (await foreignHolderIsAlive(opts.lockPath, { now, sleep, staleAfterMs })) {
124
+ throw new SingleInstanceError(holder.pid, opts.lockPath, holder.hostname ?? 'unknown');
125
+ }
126
+ }
127
+ else if (holder.pid !== me.pid && isAlive(holder.pid)) {
128
+ // A recorded start time that no longer matches means the pid was
129
+ // reused by an unrelated process after the holder died.
130
+ const reused = holder.startTicks !== undefined &&
131
+ startTicksOf(holder.pid) !== undefined &&
132
+ startTicksOf(holder.pid) !== holder.startTicks;
133
+ if (!reused)
134
+ throw new SingleInstanceError(holder.pid, opts.lockPath);
135
+ }
79
136
  }
80
137
  // Stale (dead holder / our own pid / unreadable) — reclaim and retry.
81
138
  await rm(opts.lockPath, { force: true }).catch(() => { });
@@ -83,3 +140,36 @@ export async function acquireSingleInstanceLock(opts) {
83
140
  }
84
141
  throw new SingleInstanceError(-1, opts.lockPath);
85
142
  }
143
+ function holdLock(lockPath, me, intervalMs) {
144
+ const isOurs = (holder) => holder === null ||
145
+ (me.instance !== undefined && holder.instance !== undefined
146
+ ? holder.instance === me.instance
147
+ : holder.pid === me.pid);
148
+ let timer = setInterval(() => {
149
+ void (async () => {
150
+ const holder = await readHolder(lockPath);
151
+ if (holder === null || !isOurs(holder)) {
152
+ // Someone else owns the file now; touching it would keep their lock
153
+ // looking alive on our behalf.
154
+ clearInterval(timer);
155
+ timer = undefined;
156
+ return;
157
+ }
158
+ const at = new Date();
159
+ await utimes(lockPath, at, at).catch(() => { });
160
+ })();
161
+ }, intervalMs);
162
+ timer.unref?.();
163
+ return {
164
+ async release() {
165
+ if (timer)
166
+ clearInterval(timer);
167
+ timer = undefined;
168
+ // Only remove the lock if it's still ours — defends against a
169
+ // narrow race where another process reclaimed our stale lock.
170
+ if (isOurs(await readHolder(lockPath))) {
171
+ await rm(lockPath, { force: true }).catch(() => { });
172
+ }
173
+ },
174
+ };
175
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Memory and CPU as this process may actually use them. `os.totalmem()` and
3
+ * `os.availableParallelism()` describe the host; inside a container started
4
+ * with `--memory 8g --cpus 4` on a 64 GB, 16-core machine they still say 64
5
+ * and 16, so a model sized from them gets the container killed by its own
6
+ * cgroup. Every sizing decision reads these instead.
7
+ */
8
+ export interface HostLimitsProbe {
9
+ totalmem: () => number;
10
+ freemem: () => number;
11
+ constrainedMemory: () => number | undefined;
12
+ availableMemory: () => number | undefined;
13
+ availableParallelism: () => number;
14
+ /** Every logical CPU the host has, whatever this process may use. */
15
+ hostCpuCount: () => number;
16
+ readFile: (path: string) => string | undefined;
17
+ }
18
+ /**
19
+ * The cgroup memory limit when it is below the host's RAM, else null.
20
+ * Node reads cgroup v1 and v2; "no limit" comes back as 0 or as a value at
21
+ * or above physical RAM, depending on the libuv release.
22
+ */
23
+ export declare function containerMemoryLimitBytes(probe?: HostLimitsProbe): number | null;
24
+ /** RAM this process may use: host RAM, or the container's limit when lower. */
25
+ export declare function totalMemoryBytes(probe?: HostLimitsProbe): number;
26
+ /** Free RAM, never more than the container's limit leaves. */
27
+ export declare function freeMemoryBytes(probe?: HostLimitsProbe): number;
28
+ /**
29
+ * Caps a host-wide free-memory reading (`MemAvailable`, `vm_stat`) to what
30
+ * the container still has. Unchanged outside a container.
31
+ */
32
+ export declare function capToContainer(hostFreeBytes: number, probe?: HostLimitsProbe): number;
33
+ /**
34
+ * CPUs this process may keep busy: the scheduler's CPU set, further capped
35
+ * by a cgroup v2 `cpu.max` quota (or v1 `cfs_quota_us`) rounded up.
36
+ */
37
+ export declare function availableCpuCount(probe?: HostLimitsProbe): number;
38
+ export declare function cgroupCpuQuota(probe?: HostLimitsProbe): number | null;
39
+ /**
40
+ * The CPUs a container (a `--cpus` quota or a `--cpuset-cpus` set) leaves this
41
+ * process when that is fewer than the host has, else null. Engines that size
42
+ * their own thread pool from the host's cores, as llama.cpp does, oversubscribe
43
+ * such a limit badly: 32 threads under a 4-CPU quota spend most of each period
44
+ * throttled. Measured in a 4-CPU container: a 0.8B model decoded at 3.7 t/s.
45
+ */
46
+ export declare function containerCpuLimit(probe?: HostLimitsProbe): number | null;
@@ -0,0 +1,89 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { availableParallelism, cpus, freemem, totalmem } from 'node:os';
3
+ function readTrimmed(path) {
4
+ try {
5
+ return readFileSync(path, 'utf8').trim();
6
+ }
7
+ catch {
8
+ return undefined;
9
+ }
10
+ }
11
+ const nodeProbe = {
12
+ totalmem,
13
+ freemem,
14
+ constrainedMemory: () => typeof process.constrainedMemory === 'function' ? process.constrainedMemory() : undefined,
15
+ availableMemory: () => typeof process.availableMemory === 'function' ? process.availableMemory() : undefined,
16
+ availableParallelism,
17
+ hostCpuCount: () => cpus().length,
18
+ readFile: (path) => (process.platform === 'linux' ? readTrimmed(path) : undefined),
19
+ };
20
+ /**
21
+ * The cgroup memory limit when it is below the host's RAM, else null.
22
+ * Node reads cgroup v1 and v2; "no limit" comes back as 0 or as a value at
23
+ * or above physical RAM, depending on the libuv release.
24
+ */
25
+ export function containerMemoryLimitBytes(probe = nodeProbe) {
26
+ const host = probe.totalmem();
27
+ const limit = probe.constrainedMemory();
28
+ if (typeof limit === 'number' && Number.isFinite(limit) && limit > 0 && limit < host) {
29
+ return limit;
30
+ }
31
+ return null;
32
+ }
33
+ /** RAM this process may use: host RAM, or the container's limit when lower. */
34
+ export function totalMemoryBytes(probe = nodeProbe) {
35
+ return containerMemoryLimitBytes(probe) ?? probe.totalmem();
36
+ }
37
+ /** Free RAM, never more than the container's limit leaves. */
38
+ export function freeMemoryBytes(probe = nodeProbe) {
39
+ return capToContainer(probe.freemem(), probe);
40
+ }
41
+ /**
42
+ * Caps a host-wide free-memory reading (`MemAvailable`, `vm_stat`) to what
43
+ * the container still has. Unchanged outside a container.
44
+ */
45
+ export function capToContainer(hostFreeBytes, probe = nodeProbe) {
46
+ if (containerMemoryLimitBytes(probe) === null)
47
+ return hostFreeBytes;
48
+ const available = probe.availableMemory();
49
+ if (typeof available !== 'number' || !Number.isFinite(available) || available < 0) {
50
+ return hostFreeBytes;
51
+ }
52
+ return Math.min(hostFreeBytes, available);
53
+ }
54
+ /**
55
+ * CPUs this process may keep busy: the scheduler's CPU set, further capped
56
+ * by a cgroup v2 `cpu.max` quota (or v1 `cfs_quota_us`) rounded up.
57
+ */
58
+ export function availableCpuCount(probe = nodeProbe) {
59
+ const cpus = Math.max(1, probe.availableParallelism());
60
+ const quota = cgroupCpuQuota(probe);
61
+ return quota === null ? cpus : Math.max(1, Math.min(cpus, Math.ceil(quota)));
62
+ }
63
+ export function cgroupCpuQuota(probe = nodeProbe) {
64
+ const v2 = probe.readFile('/sys/fs/cgroup/cpu.max');
65
+ if (v2) {
66
+ const [max, period] = v2.split(/\s+/);
67
+ const quota = Number(max);
68
+ const per = Number(period);
69
+ if (max !== 'max' && quota > 0 && per > 0)
70
+ return quota / per;
71
+ return null;
72
+ }
73
+ const v1Quota = Number(probe.readFile('/sys/fs/cgroup/cpu/cpu.cfs_quota_us'));
74
+ const v1Period = Number(probe.readFile('/sys/fs/cgroup/cpu/cpu.cfs_period_us'));
75
+ if (v1Quota > 0 && v1Period > 0)
76
+ return v1Quota / v1Period;
77
+ return null;
78
+ }
79
+ /**
80
+ * The CPUs a container (a `--cpus` quota or a `--cpuset-cpus` set) leaves this
81
+ * process when that is fewer than the host has, else null. Engines that size
82
+ * their own thread pool from the host's cores, as llama.cpp does, oversubscribe
83
+ * such a limit badly: 32 threads under a 4-CPU quota spend most of each period
84
+ * throttled. Measured in a 4-CPU container: a 0.8B model decoded at 3.7 t/s.
85
+ */
86
+ export function containerCpuLimit(probe = nodeProbe) {
87
+ const allowed = availableCpuCount(probe);
88
+ return allowed < probe.hostCpuCount() ? allowed : null;
89
+ }
@@ -1,11 +1,12 @@
1
1
  import { execFile } from 'node:child_process';
2
- import { freemem, platform as osPlatform, totalmem } from 'node:os';
2
+ import { platform as osPlatform } from 'node:os';
3
3
  import { promisify } from 'node:util';
4
4
  import { readDeviceSystemFacts, windowsHeadlessSpawnOptions, } from '@bendyline/gezel/native';
5
5
  import { detectLlamaGpuVram, } from '../providers/llama-cpp/devices.js';
6
6
  import { autoDetectBudgetBytes, computeCapacityBudget, setDetectedGpuVramBytes, } from '../providers/native/capacity-budget.js';
7
7
  import { readAmdgpuAdapters } from './amdgpu-adapters.js';
8
8
  import { gpuCarveOutBytes, gpuVendorFromPciId, linuxGpuAddressableBytes, linuxGpuCarveOutBytes, matchGpuAdapter, } from './gpu-carve-out.js';
9
+ import { freeMemoryBytes, totalMemoryBytes } from './host-memory.js';
9
10
  const exec = promisify(execFile);
10
11
  let cachedMemoryProfile = null;
11
12
  let pendingMemoryProfile = null;
@@ -25,7 +26,7 @@ let pendingMemoryProfile = null;
25
26
  */
26
27
  export async function detectMemoryProfile() {
27
28
  const plat = osPlatform();
28
- const totalRam = totalmem();
29
+ const totalRam = totalMemoryBytes();
29
30
  if (plat === 'darwin') {
30
31
  setDetectedGpuVramBytes(null);
31
32
  return {
@@ -311,7 +312,7 @@ export function sampleMachineMemoryUsage(opts) {
311
312
  const residentModels = opts.residentModels ?? [];
312
313
  const sampledAt = opts.sampledAt ?? new Date().toISOString();
313
314
  if (mainMemory) {
314
- const freeRamBytes = clamp(finiteNonNegative(opts.darwinSystemMemory?.freeBytes ?? opts.freeRamBytes ?? freemem()), 0, totalRamBytes);
315
+ const freeRamBytes = clamp(finiteNonNegative(opts.darwinSystemMemory?.freeBytes ?? opts.freeRamBytes ?? freeMemoryBytes()), 0, totalRamBytes);
315
316
  const cachedBytes = opts.darwinSystemMemory
316
317
  ? clamp(finiteNonNegative(opts.darwinSystemMemory.cachedBytes), 0, totalRamBytes - freeRamBytes)
317
318
  : null;
@@ -512,7 +513,7 @@ function clamp(value, min, max) {
512
513
  * held back only ~3 GiB for the OS, so a 95 GB chat model and a 10 GB image
513
514
  * model were co-admitted and the image engine died with a CUDA OOM at init.
514
515
  */
515
- export function parseNvidiaMemoryProbe(stdout, systemRamBytes = totalmem()) {
516
+ export function parseNvidiaMemoryProbe(stdout, systemRamBytes = totalMemoryBytes()) {
516
517
  const devices = [];
517
518
  for (const rawLine of stdout.split(/\r?\n/)) {
518
519
  // GPU names can theoretically contain commas. Split on the final field,
@@ -19,6 +19,7 @@ import { createReadStream } from 'node:fs';
19
19
  import { mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from 'node:fs/promises';
20
20
  import { basename, dirname, join, resolve } from 'node:path';
21
21
  import { createLogger } from '@bendyline/gezel';
22
+ import { disableOnnxRuntimeTelemetry } from '@bendyline/gezel/native';
22
23
  import { isPathInside } from './fs/safe-paths.js';
23
24
  const log = createLogger('service');
24
25
  /**
@@ -330,6 +331,7 @@ export function isMissingModule(err, specifier) {
330
331
  return new RegExp(String.raw `Cannot find (?:package|module) '${escaped}'`).test(err.message);
331
332
  }
332
333
  async function defaultLoadEnv() {
334
+ disableOnnxRuntimeTelemetry();
333
335
  const mod = (await import('@huggingface/transformers'));
334
336
  return mod.env;
335
337
  }
@@ -14,7 +14,7 @@ export declare const downloadLog: import("@bendyline/gezel").Logger;
14
14
  * transparent about who we are is the right posture for a third-party client
15
15
  * fetching public weights on the user's behalf.
16
16
  */
17
- export declare const GEZEL_DOWNLOAD_UA = "gezel/0.0.0 (+https://github.com/bendyline/gezel)";
17
+ export declare const GEZEL_DOWNLOAD_UA: string;
18
18
  /**
19
19
  * Headers for an HF control-plane request. An optional operator token raises
20
20
  * public-model rate limits, but is never forwarded to Xet/CAS/CDN hosts.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bendyline/gezel-inference",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "Gezel's on-device inference runtime: native engines, local model management, embeddings, knowledge search, and OpenAI-compatible routes.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -73,16 +73,16 @@
73
73
  "!dist/**/__pycache__"
74
74
  ],
75
75
  "dependencies": {
76
- "@bendyline/gezel": "1.2.11",
77
- "@bendyline/gezel-catalog": "1.2.11",
78
- "@bendyline/gezel-client": "1.2.11",
79
- "@bendyline/gezel-knowledge": "1.2.9",
80
- "hono": "4.13.9",
76
+ "@bendyline/gezel": "1.2.13",
77
+ "@bendyline/gezel-catalog": "1.2.13",
78
+ "@bendyline/gezel-client": "1.2.13",
79
+ "@bendyline/gezel-knowledge": "1.2.10",
80
+ "hono": "4.13.14",
81
81
  "tar": "7.5.21",
82
- "undici": "8.11.2",
82
+ "undici": "8.12.3",
83
83
  "yauzl": "^3.4.0",
84
84
  "yazl": "^3.3.1",
85
- "zod": "^4.4.3"
85
+ "zod": "4.6.7"
86
86
  },
87
87
  "peerDependencies": {
88
88
  "@huggingface/transformers": "^4.3.1"