@bendyline/gezel-inference 1.0.6 → 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.
- package/dist/cache/controller.d.ts +2 -0
- package/dist/cache/controller.js +38 -5
- package/dist/catalog/chat-models/index.json +1 -1
- package/dist/catalog/chat-models/lf/lfm3-1.7b-q4/manifest.json +114 -0
- package/dist/catalog/chat-models/lf/lfm3-1.7b-q4/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/chat-models/raw-index.json +1 -1
- package/dist/catalog/image-loras/index.json +1 -0
- package/dist/catalog/image-loras/kr/krea-2-ink-wash/manifest.json +31 -0
- package/dist/catalog/image-loras/kr/krea-2-ink-wash/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/kr/krea-2-isometric-3d/manifest.json +32 -0
- package/dist/catalog/image-loras/kr/krea-2-isometric-3d/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/kr/krea-2-kids-drawing/manifest.json +31 -0
- package/dist/catalog/image-loras/kr/krea-2-kids-drawing/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/kr/krea-2-pixel-art/manifest.json +31 -0
- package/dist/catalog/image-loras/kr/krea-2-pixel-art/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/kr/krea-2-soft-watercolor/manifest.json +31 -0
- package/dist/catalog/image-loras/kr/krea-2-soft-watercolor/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/kr/krea-2-turbo-4step/manifest.json +31 -0
- package/dist/catalog/image-loras/kr/krea-2-turbo-4step/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/mo/moss-image-gouache/manifest.json +29 -0
- package/dist/catalog/image-loras/mo/moss-image-gouache/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/mo/moss-image-sft-4step/manifest.json +30 -0
- package/dist/catalog/image-loras/mo/moss-image-sft-4step/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/qw/qwen-image-edit-2511-lightning/manifest.json +31 -0
- package/dist/catalog/image-loras/qw/qwen-image-edit-2511-lightning/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/raw-index.json +1 -0
- package/dist/catalog/image-loras/sd/sdxl-3d-render/manifest.json +29 -0
- package/dist/catalog/image-loras/sd/sdxl-3d-render/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/sd/sdxl-coloring-book/manifest.json +30 -0
- package/dist/catalog/image-loras/sd/sdxl-coloring-book/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/sd/sdxl-papercut/manifest.json +29 -0
- package/dist/catalog/image-loras/sd/sdxl-papercut/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/sd/sdxl-pixel-art/manifest.json +29 -0
- package/dist/catalog/image-loras/sd/sdxl-pixel-art/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-models/fl/flux-2-klein-4b-q4/manifest.json +3 -0
- package/dist/catalog/image-models/index.json +1 -1
- package/dist/catalog/image-models/qw/qwen-image-edit-2511-q4/manifest.json +44 -0
- package/dist/catalog/image-models/qw/qwen-image-edit-2511-q4/versions/1.0.0/manifest.json +36 -0
- package/dist/catalog/image-models/raw-index.json +1 -1
- package/dist/catalog/knowledge-catalogs/index.json +1 -1
- package/dist/catalog/knowledge-catalogs/raw-index.json +1 -1
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-arts/versions/2026.10.2/manifest.json +181 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-astronomy/versions/2026.10.2/manifest.json +181 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-biology/versions/2026.10.2/manifest.json +217 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-business/versions/2026.10.2/manifest.json +197 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-film-tv/versions/2026.10.2/manifest.json +197 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-food-drink/versions/2026.10.2/manifest.json +185 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-games/versions/2026.10.2/manifest.json +181 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-general/manifest.json +55 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-general/versions/2026.10.2/manifest.json +165 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-geography/versions/2026.10.2/manifest.json +217 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-history/versions/2026.10.2/manifest.json +217 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-language/versions/2026.10.2/manifest.json +181 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-law/versions/2026.10.2/manifest.json +185 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-literature/versions/2026.10.2/manifest.json +193 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-mathematics/versions/2026.10.2/manifest.json +189 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-medicine/versions/2026.10.2/manifest.json +185 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-military/versions/2026.10.2/manifest.json +189 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-music/versions/2026.10.2/manifest.json +197 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-arts/versions/2026.10.2/manifest.json +209 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-business/versions/2026.10.2/manifest.json +189 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-entertainment/versions/2026.10.2/manifest.json +201 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-military/versions/2026.10.2/manifest.json +185 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-other/versions/2026.10.2/manifest.json +165 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-politics/versions/2026.10.2/manifest.json +197 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-religion/versions/2026.10.2/manifest.json +201 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-science/versions/2026.10.2/manifest.json +205 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-sports/versions/2026.10.2/manifest.json +237 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-politics/versions/2026.10.2/manifest.json +197 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-religion-philosophy/versions/2026.10.2/manifest.json +189 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-science/versions/2026.10.2/manifest.json +185 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-sports/versions/2026.10.2/manifest.json +193 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-technology/versions/2026.10.2/manifest.json +185 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-transportation/versions/2026.10.2/manifest.json +189 -0
- package/dist/catalog/provenance.json +2 -1
- package/dist/engine-components.js +2 -2
- package/dist/engines/electron-native-reuse.d.ts +7 -1
- package/dist/engines/electron-native-reuse.js +12 -4
- package/dist/engines/native-file-manifest.json +546 -406
- package/dist/engines/native-manifest.d.ts +4 -4
- package/dist/engines/native-manifest.js +36 -36
- package/dist/engines/resolver.js +1 -0
- package/dist/http/auth-context.d.ts +12 -0
- package/dist/http/routes/image-ensure-routes.d.ts +4 -0
- package/dist/http/routes/image-ensure-routes.js +102 -0
- package/dist/http/routes/v1-image-styles.d.ts +5 -0
- package/dist/http/routes/v1-image-styles.js +91 -0
- package/dist/http/routes/v1-images.d.ts +1 -0
- package/dist/http/routes/v1-images.js +15 -93
- package/dist/inference-service.js +2 -1
- package/dist/knowledge/worker-host.js +2 -0
- package/dist/machine-engine/inspect-runtime.js +12 -3
- package/dist/memory/embed-core.js +3 -0
- package/dist/memory/embeddings.js +2 -0
- package/dist/models/image-fit.js +2 -2
- package/dist/models/weight-facts.d.ts +5 -0
- package/dist/models/weight-facts.js +5 -2
- package/dist/providers/ds4/build-provider.js +1 -1
- package/dist/providers/ds4/residency.js +3 -3
- package/dist/providers/image/img2img-support.d.ts +36 -1
- package/dist/providers/image/img2img-support.js +30 -1
- package/dist/providers/image/local-engine.d.ts +1 -0
- package/dist/providers/image/local-engine.js +8 -2
- package/dist/providers/image/local-factory.d.ts +10 -1
- package/dist/providers/image/local-factory.js +33 -6
- package/dist/providers/image/mock.d.ts +8 -1
- package/dist/providers/image/mock.js +46 -1
- package/dist/providers/image/pull-registry.d.ts +25 -3
- package/dist/providers/image/pull-registry.js +96 -34
- package/dist/providers/image/sd-cpp.d.ts +63 -4
- package/dist/providers/image/sd-cpp.js +361 -38
- package/dist/providers/image/types.d.ts +76 -4
- package/dist/providers/image/types.js +8 -0
- package/dist/providers/llama-cpp/build-provider.js +3 -2
- package/dist/providers/llama-cpp/cache-adapter.d.ts +3 -0
- package/dist/providers/llama-cpp/cache-adapter.js +18 -4
- package/dist/providers/llama-cpp/devices.js +2 -2
- package/dist/providers/llama-cpp/engine-flags.d.ts +5 -0
- package/dist/providers/llama-cpp/engine-flags.js +5 -2
- package/dist/providers/mlx/provider.js +13 -4
- package/dist/providers/mlx/python/gezel_mlx_server.py +9 -2
- package/dist/providers/mlx/python/spec_decode.py +44 -5
- package/dist/providers/native/capacity-broker.d.ts +1 -1
- package/dist/providers/native/capacity-broker.js +8 -8
- package/dist/providers/native/capacity-budget.js +5 -4
- package/dist/providers/native/device-capacity-ledger.d.ts +2 -0
- package/dist/providers/native/device-capacity-ledger.js +36 -5
- package/dist/providers/native/device-capacity.js +4 -3
- package/dist/providers/native/local-engine-runtime.js +4 -4
- package/dist/providers/native/process-identity.d.ts +2 -0
- package/dist/providers/native/process-identity.js +70 -0
- package/dist/relevance/relevance-core.js +2 -0
- package/dist/relevance/relevance-model.js +2 -0
- package/dist/relevance/settings.js +2 -2
- package/dist/runtime-lock.d.ts +43 -5
- package/dist/runtime-lock.js +189 -26
- package/dist/system/host-memory.d.ts +46 -0
- package/dist/system/host-memory.js +89 -0
- package/dist/system/memory.js +5 -4
- package/dist/transformers-cache.js +2 -0
- package/dist/utils/download-common.d.ts +1 -1
- package/package.json +8 -8
|
@@ -2,6 +2,7 @@ import { chmod, mkdir } from 'node:fs/promises';
|
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { CapacityDeniedError } from './capacity-broker.js';
|
|
5
|
+
import { readProcessIdentities } from './process-identity.js';
|
|
5
6
|
// Keep the newer builtin behind a runtime require. The current tsup/esbuild
|
|
6
7
|
// target rewrites a static `node:sqlite` import to the nonexistent npm package
|
|
7
8
|
// `sqlite`, making the daemon bundle fail before it can write runtime state.
|
|
@@ -38,9 +39,26 @@ export class DeviceCapacityLedger {
|
|
|
38
39
|
await chmod(path, 0o600);
|
|
39
40
|
db.exec('PRAGMA busy_timeout = 1000');
|
|
40
41
|
db.exec('CREATE TABLE IF NOT EXISTS claims (id TEXT PRIMARY KEY, claim TEXT NOT NULL)');
|
|
42
|
+
const snapshot = db
|
|
43
|
+
.prepare('SELECT claim FROM claims')
|
|
44
|
+
.all()
|
|
45
|
+
.map((row) => JSON.parse(String(row.claim)));
|
|
46
|
+
const pids = snapshot.flatMap((claim) => [
|
|
47
|
+
claim.ownerPid,
|
|
48
|
+
...(claim.childPid === undefined ? [] : [claim.childPid]),
|
|
49
|
+
]);
|
|
50
|
+
if (command.action === 'acquire')
|
|
51
|
+
pids.push(command.ownerPid);
|
|
52
|
+
if (command.action === 'bind')
|
|
53
|
+
pids.push(command.childPid);
|
|
54
|
+
// OS probes may spawn a helper. Never hold SQLite's writer lock while awaiting one.
|
|
55
|
+
const probe = this.options.identities ?? (this.options.alive ? undefined : readProcessIdentities);
|
|
56
|
+
const identities = command.action !== 'release' && probe
|
|
57
|
+
? await probe(pids).catch(() => new Map())
|
|
58
|
+
: new Map();
|
|
41
59
|
db.exec('BEGIN IMMEDIATE');
|
|
42
60
|
try {
|
|
43
|
-
const result = this.transact(db, command, sample);
|
|
61
|
+
const result = this.transact(db, command, sample, identities, new Map(snapshot.map((claim) => [claim.id, JSON.stringify(claim)])));
|
|
44
62
|
db.exec('COMMIT');
|
|
45
63
|
return result;
|
|
46
64
|
}
|
|
@@ -53,17 +71,27 @@ export class DeviceCapacityLedger {
|
|
|
53
71
|
db.close();
|
|
54
72
|
}
|
|
55
73
|
}
|
|
56
|
-
transact(db, command, sample) {
|
|
74
|
+
transact(db, command, sample, identities, observedClaims) {
|
|
57
75
|
const alive = this.options.alive ?? processAlive;
|
|
76
|
+
const sameProcess = (pid, identity) => {
|
|
77
|
+
if (!alive(pid))
|
|
78
|
+
return false;
|
|
79
|
+
const current = identities.get(pid);
|
|
80
|
+
return identity === undefined || current === undefined || current === identity;
|
|
81
|
+
};
|
|
58
82
|
const rows = db.prepare('SELECT claim FROM claims ORDER BY rowid').all();
|
|
59
83
|
const claims = rows.map((row) => JSON.parse(String(row.claim)));
|
|
60
84
|
const remove = db.prepare('DELETE FROM claims WHERE id = ?');
|
|
61
85
|
const save = db.prepare('UPDATE claims SET claim = ? WHERE id = ?');
|
|
62
86
|
const live = claims.filter((claim) => {
|
|
87
|
+
// Another coordinator can acquire/rebind while the OS probe is in flight.
|
|
88
|
+
// Its newer claim must not be compared with our earlier process snapshot.
|
|
89
|
+
if (observedClaims.get(claim.id) !== JSON.stringify(claim))
|
|
90
|
+
return true;
|
|
63
91
|
// An unbound grant with a dead owner could have spawned just before the
|
|
64
92
|
// owner died. Keep it conservatively; never guess that its bytes are free.
|
|
65
|
-
const stale = (claim.childPid !== undefined && !
|
|
66
|
-
(!
|
|
93
|
+
const stale = (claim.childPid !== undefined && !sameProcess(claim.childPid, claim.childIdentity)) ||
|
|
94
|
+
(!sameProcess(claim.ownerPid, claim.ownerIdentity) && claim.phase === 'waiting');
|
|
67
95
|
if (stale)
|
|
68
96
|
remove.run(claim.id);
|
|
69
97
|
return !stale;
|
|
@@ -80,6 +108,7 @@ export class DeviceCapacityLedger {
|
|
|
80
108
|
own = {
|
|
81
109
|
id: command.id,
|
|
82
110
|
ownerPid: command.ownerPid,
|
|
111
|
+
ownerIdentity: identities.get(command.ownerPid),
|
|
83
112
|
label: command.label,
|
|
84
113
|
bytes: command.bytes,
|
|
85
114
|
gpuBytes: command.gpuBytes,
|
|
@@ -95,8 +124,10 @@ export class DeviceCapacityLedger {
|
|
|
95
124
|
return { state: 'released', releaseRequested: false };
|
|
96
125
|
if (command.action === 'acquire' && command.priority === 'interactive')
|
|
97
126
|
own.priority = 'interactive';
|
|
98
|
-
if (command.action === 'bind' && own.phase !== 'waiting')
|
|
127
|
+
if (command.action === 'bind' && own.phase !== 'waiting') {
|
|
99
128
|
own.childPid = command.childPid;
|
|
129
|
+
own.childIdentity = identities.get(command.childPid);
|
|
130
|
+
}
|
|
100
131
|
if (command.action === 'ready' && own.phase === 'loading')
|
|
101
132
|
own.phase = 'ready';
|
|
102
133
|
const active = live.filter((c) => c.phase !== 'waiting');
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { readdir, stat } from 'node:fs/promises';
|
|
3
|
-
import { homedir
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { DS4_FULL_RESIDENCY_HEADROOM_BYTES, GEZEL_VERSION, NativeCapacityCommandSchema, NativeCapacityReplySchema, awakeNow, createLogger, isUnstampedDevBuild, } from '@bendyline/gezel';
|
|
6
6
|
import { readSystemServiceRuntime, systemServiceHome } from '@bendyline/gezel-client/node';
|
|
7
7
|
import { ConfigStore } from '../../fs/config-store.js';
|
|
8
8
|
import { createPinnedFetch } from '../../remotes/pinned-fetch.js';
|
|
9
|
+
import { totalMemoryBytes } from '../../system/host-memory.js';
|
|
9
10
|
import { CapacityDeniedError, availableSystemRamBytes, liveRamOsReserveBytes, } from './capacity-broker.js';
|
|
10
11
|
import { DeviceCapacityLedger } from './device-capacity-ledger.js';
|
|
11
12
|
import { measuredCapacityBudget } from './measured-budget.js';
|
|
@@ -156,7 +157,7 @@ export function staticCapacityCeilings(budget, ramBytes, configuredBytes, platfo
|
|
|
156
157
|
}
|
|
157
158
|
export async function sampleDeviceCapacity(home) {
|
|
158
159
|
const budget = await measuredCapacityBudget();
|
|
159
|
-
const ram =
|
|
160
|
+
const ram = totalMemoryBytes();
|
|
160
161
|
const config = home ? await new ConfigStore(home).readConfig() : {};
|
|
161
162
|
const ceilings = staticCapacityCeilings(budget, ram, configuredEngineMemoryBytes(config));
|
|
162
163
|
// The ledger's RAM ceiling and the OS's currently reclaimable RAM answer
|
|
@@ -213,7 +214,7 @@ async function capacityExecutor(home) {
|
|
|
213
214
|
// broker that is merely unreachable is an outage, never permission to
|
|
214
215
|
// double-book the machine — so claim the authority before rethrowing.
|
|
215
216
|
observedMachineAuthorities.add(machineHome);
|
|
216
|
-
throw error;
|
|
217
|
+
throw new Error(`Local models can't start: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
217
218
|
}
|
|
218
219
|
const skew = assessBrokerVersionSkew(identity.gezelVersion);
|
|
219
220
|
if (skew)
|
|
@@ -609,10 +609,10 @@ export class LocalEngineRuntime {
|
|
|
609
609
|
residentCtxTokens: ds4Block?.residentCtxTokens,
|
|
610
610
|
});
|
|
611
611
|
if (line) {
|
|
612
|
-
const {
|
|
612
|
+
const { totalMemoryBytes } = await import('../../system/host-memory.js');
|
|
613
613
|
bytes = ds4ProjectedResidentBytes(line, resolveDs4LaunchCtx({
|
|
614
614
|
configured: config.modelContextOverrides?.[`ds4:${modelId}`] ?? config.ds4NumCtx,
|
|
615
|
-
ramTieredCtx:
|
|
615
|
+
ramTieredCtx: totalMemoryBytes() / 1024 ** 3 >= 192 ? 262_144 : 131_072,
|
|
616
616
|
catalogMaxCtx: ds4Block?.maxLaunchCtx,
|
|
617
617
|
minViableContextTokens: minViableLocalContextTokens(),
|
|
618
618
|
}));
|
|
@@ -1046,8 +1046,8 @@ export class LocalEngineRuntime {
|
|
|
1046
1046
|
if (!installed && !hasExplicitSource && !(opts.allowUninstalled && ds4Source)) {
|
|
1047
1047
|
throw new ModelNotInstalledError(name, modelId);
|
|
1048
1048
|
}
|
|
1049
|
-
const {
|
|
1050
|
-
const ramTieredCtx =
|
|
1049
|
+
const { totalMemoryBytes } = await import('../../system/host-memory.js');
|
|
1050
|
+
const ramTieredCtx = totalMemoryBytes() / 1024 ** 3 >= 192 ? 262_144 : 131_072;
|
|
1051
1051
|
const ds4Override = config.modelContextOverrides?.[`ds4:${modelId}`];
|
|
1052
1052
|
const ds4Floor = ds4Override !== undefined ? Math.min(contextFloor, ds4Override) : contextFloor;
|
|
1053
1053
|
const effective = resolveDs4LaunchCtx({
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
const exec = promisify(execFile);
|
|
5
|
+
/**
|
|
6
|
+
* One PID the probe refuses blanks every row in its batch: macOS ps prints
|
|
7
|
+
* "process id too large" past XNU's PID_MAX, and Get-Process -Id binds Int32.
|
|
8
|
+
* A PID past the limit cannot be live, so it is left out like an absent one.
|
|
9
|
+
*/
|
|
10
|
+
const PROBE_PID_LIMIT = {
|
|
11
|
+
darwin: 99_999,
|
|
12
|
+
win32: 2_147_483_647,
|
|
13
|
+
};
|
|
14
|
+
/** Birth stamps survive coordinator restarts and distinguish a reused PID. */
|
|
15
|
+
export async function readProcessIdentities(pids) {
|
|
16
|
+
const limit = PROBE_PID_LIMIT[process.platform] ?? Number.MAX_SAFE_INTEGER;
|
|
17
|
+
const ids = [...new Set(pids)].filter((pid) => Number.isSafeInteger(pid) && pid > 0 && pid <= limit);
|
|
18
|
+
const identities = new Map();
|
|
19
|
+
if (!ids.length)
|
|
20
|
+
return identities;
|
|
21
|
+
try {
|
|
22
|
+
if (process.platform === 'linux') {
|
|
23
|
+
const boot = (await readFile('/proc/sys/kernel/random/boot_id', 'utf8')).trim();
|
|
24
|
+
await Promise.all(ids.map(async (pid) => {
|
|
25
|
+
const value = await readFile(`/proc/${pid}/stat`, 'utf8').catch(() => null);
|
|
26
|
+
if (!value)
|
|
27
|
+
return;
|
|
28
|
+
// comm may contain spaces and parentheses; fields after its final ')' start at field 3.
|
|
29
|
+
const start = value
|
|
30
|
+
.slice(value.lastIndexOf(')') + 2)
|
|
31
|
+
.trim()
|
|
32
|
+
.split(/\s+/)[19];
|
|
33
|
+
if (start && /^\d+$/.test(start))
|
|
34
|
+
identities.set(pid, `${boot}:${start}`);
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
const windows = process.platform === 'win32';
|
|
39
|
+
const { stdout } = await exec(windows ? 'powershell.exe' : 'ps', windows
|
|
40
|
+
? [
|
|
41
|
+
'-NoProfile',
|
|
42
|
+
'-NonInteractive',
|
|
43
|
+
'-Command',
|
|
44
|
+
`Get-Process -Id ${ids.join(',')} -ErrorAction SilentlyContinue | ForEach-Object { try { [string]$_.Id + ":" + $_.StartTime.ToUniversalTime().Ticks.ToString() } catch {} }`,
|
|
45
|
+
]
|
|
46
|
+
: ['-p', ids.join(','), '-o', 'pid=', '-o', 'lstart='], {
|
|
47
|
+
windowsHide: true,
|
|
48
|
+
timeout: 3000,
|
|
49
|
+
maxBuffer: 256 * 1024,
|
|
50
|
+
env: { ...process.env, TZ: 'UTC', LC_ALL: 'C', LANG: 'C' },
|
|
51
|
+
}).catch((error) => {
|
|
52
|
+
// ps (and Windows process lookup) may return live rows alongside a missing PID error.
|
|
53
|
+
if (typeof error.stdout === 'string')
|
|
54
|
+
return { stdout: error.stdout };
|
|
55
|
+
throw error;
|
|
56
|
+
});
|
|
57
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
58
|
+
const match = windows
|
|
59
|
+
? /^\s*(\d+):(\d+)\s*$/.exec(line)
|
|
60
|
+
: /^\s*(\d+)\s+(.+?)\s*$/.exec(line);
|
|
61
|
+
if (match && ids.includes(Number(match[1])))
|
|
62
|
+
identities.set(Number(match[1]), match[2]);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Access denied or an unavailable OS probe is unknown, never proof of death.
|
|
68
|
+
}
|
|
69
|
+
return identities;
|
|
70
|
+
}
|
|
@@ -3,6 +3,7 @@ import { createReadStream } from 'node:fs';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { createLogger } from '@bendyline/gezel';
|
|
5
5
|
import { transformersGraphOptions } from '@bendyline/gezel-knowledge';
|
|
6
|
+
import { disableOnnxRuntimeTelemetry } from '@bendyline/gezel/native';
|
|
6
7
|
import { PipelineLoadError } from '../memory/embed-core.js';
|
|
7
8
|
/**
|
|
8
9
|
* Cross-encoder inference: load a pinned relevance model from its local
|
|
@@ -19,6 +20,7 @@ const BATCH = 8;
|
|
|
19
20
|
const MAX_LOADED = 2;
|
|
20
21
|
const loaded = new Map();
|
|
21
22
|
async function importTransformers() {
|
|
23
|
+
disableOnnxRuntimeTelemetry();
|
|
22
24
|
return (await import('@huggingface/transformers'));
|
|
23
25
|
}
|
|
24
26
|
export async function loadRelevanceModel(model, transformers) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Worker } from 'node:worker_threads';
|
|
2
2
|
import { createLogger } from '@bendyline/gezel';
|
|
3
|
+
import { disableOnnxRuntimeTelemetry } from '@bendyline/gezel/native';
|
|
3
4
|
import { retireModelWorker } from '../utils/retire-model-worker.js';
|
|
4
5
|
import { findServiceWorkerEntry } from '../utils/service-worker-entry.js';
|
|
5
6
|
import { disposeRelevanceModels, loadRelevanceModel, scoreRelevancePairs, } from './relevance-core.js';
|
|
@@ -185,6 +186,7 @@ class RelevanceModelHost {
|
|
|
185
186
|
return null;
|
|
186
187
|
}
|
|
187
188
|
try {
|
|
189
|
+
disableOnnxRuntimeTelemetry();
|
|
188
190
|
const worker = new Worker(entry);
|
|
189
191
|
worker.on('message', (reply) => {
|
|
190
192
|
const pending = this.pending.get(reply.id);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { availableParallelism } from 'node:os';
|
|
2
1
|
import { KNOWLEDGE_FILTER_MIN_RELEVANCE, } from '@bendyline/gezel';
|
|
2
|
+
import { availableCpuCount } from '../system/host-memory.js';
|
|
3
3
|
import { relevanceModelDir } from './install.js';
|
|
4
4
|
import { DEFAULT_RELEVANCE_MODEL_ID, findRelevanceModel } from './registry.js';
|
|
5
5
|
const DEFAULT_BUDGETS = {
|
|
@@ -76,6 +76,6 @@ export function toResolvedModel(home, spec) {
|
|
|
76
76
|
maxTokens: spec.maxTokens,
|
|
77
77
|
queryMaxTokens: spec.queryMaxTokens,
|
|
78
78
|
scoreActivation: spec.scoreActivation,
|
|
79
|
-
intraOpNumThreads: Math.max(1, Math.min(4, Math.floor(
|
|
79
|
+
intraOpNumThreads: Math.max(1, Math.min(4, Math.floor(availableCpuCount() / 2))),
|
|
80
80
|
};
|
|
81
81
|
}
|
package/dist/runtime-lock.d.ts
CHANGED
|
@@ -6,24 +6,62 @@
|
|
|
6
6
|
export declare class SingleInstanceError extends Error {
|
|
7
7
|
readonly holderPid: number;
|
|
8
8
|
readonly lockPath: string;
|
|
9
|
-
|
|
9
|
+
readonly holderHost?: string | undefined;
|
|
10
|
+
constructor(holderPid: number, lockPath: string, holderHost?: string | undefined);
|
|
10
11
|
}
|
|
11
12
|
export interface RuntimeLock {
|
|
12
13
|
release(): Promise<void>;
|
|
13
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Who holds a lock. A pid alone identifies a process only inside one pid
|
|
17
|
+
* namespace on one boot: two containers sharing a volume each see the
|
|
18
|
+
* other's pid as dead (or as an unrelated process), which let a one-shot
|
|
19
|
+
* `docker run … gezel do` reclaim a serving container's live lock. The
|
|
20
|
+
* namespace, boot and host fields say when a pid check is meaningful; a
|
|
21
|
+
* holder outside our namespace is judged by the lock file's heartbeat.
|
|
22
|
+
*/
|
|
23
|
+
export interface LockIdentity {
|
|
24
|
+
pid: number;
|
|
25
|
+
hostname?: string;
|
|
26
|
+
bootId?: string;
|
|
27
|
+
pidNs?: string;
|
|
28
|
+
/** Process start time in clock ticks since boot (`/proc/<pid>/stat` field 22). */
|
|
29
|
+
startTicks?: string;
|
|
30
|
+
/** Random per-process id; tells our own heartbeat whether the lock is still ours. */
|
|
31
|
+
instance?: string;
|
|
32
|
+
}
|
|
33
|
+
export declare function procStartTicks(pid: number | 'self'): string | undefined;
|
|
34
|
+
export declare function currentLockIdentity(): LockIdentity;
|
|
35
|
+
export declare function parseLockFile(text: string): LockIdentity | null;
|
|
36
|
+
/**
|
|
37
|
+
* Whether a pid check against `holder` means anything here. Any field the
|
|
38
|
+
* holder recorded must match ours; a legacy pid-only record is assumed to be
|
|
39
|
+
* in our namespace, which is how it was judged before.
|
|
40
|
+
*/
|
|
41
|
+
export declare function sharesPidNamespace(holder: LockIdentity, me: LockIdentity): boolean;
|
|
14
42
|
/**
|
|
15
43
|
* Acquire an exclusive single-instance lock for a gezel home so two daemons
|
|
16
44
|
* can't share one `~/.gezel/` and race its on-disk state (config, sessions,
|
|
17
45
|
* tasks, runtime files). Atomic `O_CREAT|O_EXCL` create of
|
|
18
|
-
* `<runtimeDir>/lock` holding the owner
|
|
46
|
+
* `<runtimeDir>/lock` holding the owner's identity; a stale lock is
|
|
19
47
|
* reclaimed. Throws {@link SingleInstanceError} when a live daemon holds it.
|
|
20
48
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
49
|
+
* While held, the lock file's mtime is touched every few seconds: that
|
|
50
|
+
* heartbeat is how a daemon in another container (another pid namespace)
|
|
51
|
+
* is told apart from a dead one.
|
|
52
|
+
*
|
|
53
|
+
* The returned `release()` stops the heartbeat and removes the lock — call
|
|
54
|
+
* it on graceful shutdown. A hard crash leaves a stale lock the next start
|
|
55
|
+
* reclaims.
|
|
24
56
|
*/
|
|
25
57
|
export declare function acquireSingleInstanceLock(opts: {
|
|
26
58
|
runtimeDir: string;
|
|
27
59
|
lockPath: string;
|
|
28
60
|
isAlive?: (pid: number) => boolean;
|
|
61
|
+
identity?: LockIdentity;
|
|
62
|
+
startTicksOf?: (pid: number) => string | undefined;
|
|
63
|
+
now?: () => number;
|
|
64
|
+
sleep?: (ms: number) => Promise<void>;
|
|
65
|
+
heartbeatIntervalMs?: number;
|
|
66
|
+
staleAfterMs?: number;
|
|
29
67
|
}): Promise<RuntimeLock>;
|
package/dist/runtime-lock.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync, readlinkSync } from 'node:fs';
|
|
2
|
+
import { mkdir, open, readFile, rm, stat, utimes } from 'node:fs/promises';
|
|
3
|
+
import { hostname as osHostname } from 'node:os';
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
holderHost;
|
|
13
|
+
constructor(holderPid, lockPath, holderHost) {
|
|
14
|
+
super(holderHost !== undefined
|
|
15
|
+
? `Another Gezel daemon (host "${holderHost}", pid ${holderPid}) is using this home. If it runs in a container, run commands inside that container with \`docker exec <container> gezel …\` instead of starting a second container on the same volume.`
|
|
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
|
+
const HEARTBEAT_INTERVAL_MS = 10_000;
|
|
26
|
+
const STALE_AFTER_MS = 40_000;
|
|
19
27
|
function defaultIsAlive(pid) {
|
|
20
28
|
try {
|
|
21
29
|
process.kill(pid, 0);
|
|
@@ -27,55 +35,177 @@ function defaultIsAlive(pid) {
|
|
|
27
35
|
return err.code === 'EPERM';
|
|
28
36
|
}
|
|
29
37
|
}
|
|
30
|
-
|
|
38
|
+
function readTrimmed(path) {
|
|
31
39
|
try {
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
return readFileSync(path, 'utf8').trim() || undefined;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function procStartTicks(pid) {
|
|
47
|
+
if (process.platform !== 'linux')
|
|
48
|
+
return undefined;
|
|
49
|
+
const stat = readTrimmed(`/proc/${pid}/stat`);
|
|
50
|
+
if (!stat)
|
|
51
|
+
return undefined;
|
|
52
|
+
// comm (field 2) may contain spaces and parentheses; fields resume after
|
|
53
|
+
// the last ')'. starttime is field 22 overall, the 20th after comm.
|
|
54
|
+
const rest = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
|
|
55
|
+
return rest[19];
|
|
56
|
+
}
|
|
57
|
+
export function currentLockIdentity() {
|
|
58
|
+
const linux = process.platform === 'linux';
|
|
59
|
+
let pidNs;
|
|
60
|
+
if (linux) {
|
|
61
|
+
try {
|
|
62
|
+
pidNs = readlinkSync('/proc/self/ns/pid');
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
pidNs = undefined;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
pid: process.pid,
|
|
70
|
+
hostname: osHostname(),
|
|
71
|
+
bootId: linux ? readTrimmed('/proc/sys/kernel/random/boot_id') : undefined,
|
|
72
|
+
pidNs,
|
|
73
|
+
startTicks: procStartTicks('self'),
|
|
74
|
+
instance: `${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The pid stays on the first line so a daemon from before the identity
|
|
79
|
+
* record (which parses the leading integer) still reads it as held instead
|
|
80
|
+
* of stale.
|
|
81
|
+
*/
|
|
82
|
+
function serializeIdentity(identity) {
|
|
83
|
+
return `${identity.pid}\n${JSON.stringify({ v: 1, ...identity })}\n`;
|
|
84
|
+
}
|
|
85
|
+
export function parseLockFile(text) {
|
|
86
|
+
const [first, second] = text.split('\n');
|
|
87
|
+
const pid = Number.parseInt((first ?? '').trim(), 10);
|
|
88
|
+
if (!Number.isFinite(pid))
|
|
89
|
+
return null;
|
|
90
|
+
if (!second?.trim())
|
|
91
|
+
return { pid };
|
|
92
|
+
try {
|
|
93
|
+
const record = JSON.parse(second);
|
|
94
|
+
const text = (value) => (typeof value === 'string' && value ? value : undefined);
|
|
95
|
+
return {
|
|
96
|
+
pid,
|
|
97
|
+
hostname: text(record.hostname),
|
|
98
|
+
bootId: text(record.bootId),
|
|
99
|
+
pidNs: text(record.pidNs),
|
|
100
|
+
startTicks: text(record.startTicks),
|
|
101
|
+
instance: text(record.instance),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return { pid };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async function readHolder(lockPath) {
|
|
109
|
+
try {
|
|
110
|
+
return parseLockFile(await readFile(lockPath, 'utf8'));
|
|
34
111
|
}
|
|
35
112
|
catch {
|
|
36
113
|
return null;
|
|
37
114
|
}
|
|
38
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Whether a pid check against `holder` means anything here. Any field the
|
|
118
|
+
* holder recorded must match ours; a legacy pid-only record is assumed to be
|
|
119
|
+
* in our namespace, which is how it was judged before.
|
|
120
|
+
*/
|
|
121
|
+
export function sharesPidNamespace(holder, me) {
|
|
122
|
+
const same = (a, b) => !a || !b || a === b;
|
|
123
|
+
return (same(holder.hostname, me.hostname) &&
|
|
124
|
+
same(holder.bootId, me.bootId) &&
|
|
125
|
+
same(holder.pidNs, me.pidNs));
|
|
126
|
+
}
|
|
127
|
+
async function lockMtimeMs(lockPath) {
|
|
128
|
+
try {
|
|
129
|
+
return (await stat(lockPath)).mtimeMs;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* A holder outside our pid namespace is alive while its heartbeat moves.
|
|
137
|
+
* A fresh-looking mtime may be a holder that died moments ago (a killed
|
|
138
|
+
* container restarting onto its own volume), so wait until the heartbeat
|
|
139
|
+
* either advances (alive) or ages past the stale bound (dead).
|
|
140
|
+
*/
|
|
141
|
+
async function foreignHolderIsAlive(lockPath, opts) {
|
|
142
|
+
const first = await lockMtimeMs(lockPath);
|
|
143
|
+
if (first === null)
|
|
144
|
+
return false;
|
|
145
|
+
for (;;) {
|
|
146
|
+
if (opts.now() - first > opts.staleAfterMs)
|
|
147
|
+
return false;
|
|
148
|
+
await opts.sleep(1_000);
|
|
149
|
+
const current = await lockMtimeMs(lockPath);
|
|
150
|
+
if (current === null)
|
|
151
|
+
return false;
|
|
152
|
+
if (current !== first)
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
39
156
|
/**
|
|
40
157
|
* Acquire an exclusive single-instance lock for a gezel home so two daemons
|
|
41
158
|
* can't share one `~/.gezel/` and race its on-disk state (config, sessions,
|
|
42
159
|
* tasks, runtime files). Atomic `O_CREAT|O_EXCL` create of
|
|
43
|
-
* `<runtimeDir>/lock` holding the owner
|
|
160
|
+
* `<runtimeDir>/lock` holding the owner's identity; a stale lock is
|
|
44
161
|
* reclaimed. Throws {@link SingleInstanceError} when a live daemon holds it.
|
|
45
162
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
163
|
+
* While held, the lock file's mtime is touched every few seconds: that
|
|
164
|
+
* heartbeat is how a daemon in another container (another pid namespace)
|
|
165
|
+
* is told apart from a dead one.
|
|
166
|
+
*
|
|
167
|
+
* The returned `release()` stops the heartbeat and removes the lock — call
|
|
168
|
+
* it on graceful shutdown. A hard crash leaves a stale lock the next start
|
|
169
|
+
* reclaims.
|
|
49
170
|
*/
|
|
50
171
|
export async function acquireSingleInstanceLock(opts) {
|
|
51
172
|
const isAlive = opts.isAlive ?? defaultIsAlive;
|
|
173
|
+
const me = opts.identity ?? currentLockIdentity();
|
|
174
|
+
const startTicksOf = opts.startTicksOf ?? procStartTicks;
|
|
175
|
+
const now = opts.now ?? Date.now;
|
|
176
|
+
const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
177
|
+
const staleAfterMs = opts.staleAfterMs ?? STALE_AFTER_MS;
|
|
52
178
|
await mkdir(opts.runtimeDir, { recursive: true });
|
|
53
179
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
54
180
|
try {
|
|
55
181
|
const handle = await open(opts.lockPath, 'wx'); // fails with EEXIST if present
|
|
56
182
|
try {
|
|
57
|
-
await handle.writeFile(
|
|
183
|
+
await handle.writeFile(serializeIdentity(me));
|
|
58
184
|
}
|
|
59
185
|
finally {
|
|
60
186
|
await handle.close();
|
|
61
187
|
}
|
|
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
|
-
};
|
|
188
|
+
return holdLock(opts.lockPath, me, opts.heartbeatIntervalMs ?? HEARTBEAT_INTERVAL_MS);
|
|
72
189
|
}
|
|
73
190
|
catch (err) {
|
|
74
191
|
if (err.code !== 'EEXIST')
|
|
75
192
|
throw err;
|
|
76
|
-
const holder = await
|
|
77
|
-
if (holder
|
|
78
|
-
|
|
193
|
+
const holder = await readHolder(opts.lockPath);
|
|
194
|
+
if (holder) {
|
|
195
|
+
if (!sharesPidNamespace(holder, me)) {
|
|
196
|
+
if (await foreignHolderIsAlive(opts.lockPath, { now, sleep, staleAfterMs })) {
|
|
197
|
+
throw new SingleInstanceError(holder.pid, opts.lockPath, holder.hostname ?? 'unknown');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
else if (holder.pid !== me.pid && isAlive(holder.pid)) {
|
|
201
|
+
// A recorded start time that no longer matches means the pid was
|
|
202
|
+
// reused by an unrelated process after the holder died.
|
|
203
|
+
const reused = holder.startTicks !== undefined &&
|
|
204
|
+
startTicksOf(holder.pid) !== undefined &&
|
|
205
|
+
startTicksOf(holder.pid) !== holder.startTicks;
|
|
206
|
+
if (!reused)
|
|
207
|
+
throw new SingleInstanceError(holder.pid, opts.lockPath);
|
|
208
|
+
}
|
|
79
209
|
}
|
|
80
210
|
// Stale (dead holder / our own pid / unreadable) — reclaim and retry.
|
|
81
211
|
await rm(opts.lockPath, { force: true }).catch(() => { });
|
|
@@ -83,3 +213,36 @@ export async function acquireSingleInstanceLock(opts) {
|
|
|
83
213
|
}
|
|
84
214
|
throw new SingleInstanceError(-1, opts.lockPath);
|
|
85
215
|
}
|
|
216
|
+
function holdLock(lockPath, me, intervalMs) {
|
|
217
|
+
const isOurs = (holder) => holder === null ||
|
|
218
|
+
(me.instance !== undefined && holder.instance !== undefined
|
|
219
|
+
? holder.instance === me.instance
|
|
220
|
+
: holder.pid === me.pid);
|
|
221
|
+
let timer = setInterval(() => {
|
|
222
|
+
void (async () => {
|
|
223
|
+
const holder = await readHolder(lockPath);
|
|
224
|
+
if (holder === null || !isOurs(holder)) {
|
|
225
|
+
// Someone else owns the file now; touching it would keep their lock
|
|
226
|
+
// looking alive on our behalf.
|
|
227
|
+
clearInterval(timer);
|
|
228
|
+
timer = undefined;
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const at = new Date();
|
|
232
|
+
await utimes(lockPath, at, at).catch(() => { });
|
|
233
|
+
})();
|
|
234
|
+
}, intervalMs);
|
|
235
|
+
timer.unref?.();
|
|
236
|
+
return {
|
|
237
|
+
async release() {
|
|
238
|
+
if (timer)
|
|
239
|
+
clearInterval(timer);
|
|
240
|
+
timer = undefined;
|
|
241
|
+
// Only remove the lock if it's still ours — defends against a
|
|
242
|
+
// narrow race where another process reclaimed our stale lock.
|
|
243
|
+
if (isOurs(await readHolder(lockPath))) {
|
|
244
|
+
await rm(lockPath, { force: true }).catch(() => { });
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
}
|
|
@@ -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;
|