@bendyline/gezel-inference 1.0.7 → 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.
- package/dist/http/routes/v1-images.d.ts +2 -1
- package/dist/http/routes/v1-images.js +36 -9
- package/dist/knowledge/manager.d.ts +3 -27
- package/dist/knowledge/manager.js +8 -364
- package/dist/knowledge/unified-search.d.ts +61 -0
- package/dist/knowledge/unified-search.js +373 -0
- package/dist/providers/image/sd-cpp.js +16 -9
- package/dist/providers/mlx/async-file-handoff.d.ts +9 -1
- package/dist/providers/mlx/async-file-handoff.js +32 -4
- package/dist/providers/mlx/provider.js +28 -16
- package/dist/providers/mlx/runtime-diagnostics.d.ts +6 -0
- package/dist/providers/mlx/runtime-diagnostics.js +18 -1
- package/dist/providers/mlx/tool-call-protocol.d.ts +4 -3
- package/dist/providers/mlx/tool-call-protocol.js +4 -3
- package/dist/providers/native/device-capacity-ledger.d.ts +4 -2
- package/dist/providers/native/device-capacity-ledger.js +35 -20
- package/dist/providers/native/process-identity.d.ts +38 -0
- package/dist/providers/native/process-identity.js +86 -0
- package/dist/runtime-lock.d.ts +3 -27
- package/dist/runtime-lock.js +7 -80
- package/package.json +4 -4
|
@@ -39,9 +39,10 @@ export declare function applyCallableToolGrammar(body: Record<string, unknown>,
|
|
|
39
39
|
*
|
|
40
40
|
* This deliberately checks presence only. A present-but-wrongly-typed value
|
|
41
41
|
* remains the MCP validator's job; the MLX Hermes grammar promises only that
|
|
42
|
-
* every declared required key is emitted before
|
|
43
|
-
* missing key
|
|
44
|
-
*
|
|
42
|
+
* every declared required key is emitted before a constrained call can close.
|
|
43
|
+
* A missing key alone does not establish a decoder fault: calls salvaged from
|
|
44
|
+
* ordinary prose were not subject to that grammar. The caller must preserve
|
|
45
|
+
* their origin when deciding whether to disable speculative decoding.
|
|
45
46
|
*/
|
|
46
47
|
export declare function missingTopLevelRequiredToolArgs(schema: Record<string, unknown> | undefined, args: Record<string, unknown>): string[];
|
|
47
48
|
export declare function hermesRequiredArgGrammarRequested(body: Record<string, unknown>): boolean;
|
|
@@ -59,9 +59,10 @@ export function applyCallableToolGrammar(body, bridges) {
|
|
|
59
59
|
*
|
|
60
60
|
* This deliberately checks presence only. A present-but-wrongly-typed value
|
|
61
61
|
* remains the MCP validator's job; the MLX Hermes grammar promises only that
|
|
62
|
-
* every declared required key is emitted before
|
|
63
|
-
* missing key
|
|
64
|
-
*
|
|
62
|
+
* every declared required key is emitted before a constrained call can close.
|
|
63
|
+
* A missing key alone does not establish a decoder fault: calls salvaged from
|
|
64
|
+
* ordinary prose were not subject to that grammar. The caller must preserve
|
|
65
|
+
* their origin when deciding whether to disable speculative decoding.
|
|
65
66
|
*/
|
|
66
67
|
export function missingTopLevelRequiredToolArgs(schema, args) {
|
|
67
68
|
const required = schema?.required;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { NativeCapacityCommand, NativeCapacityReply } from '@bendyline/gezel';
|
|
2
|
-
import { readProcessIdentities } from './process-identity.js';
|
|
2
|
+
import { ProcessIdentityCache, processAlive, type readProcessIdentities } from './process-identity.js';
|
|
3
|
+
export { processAlive };
|
|
3
4
|
export interface DeviceCapacitySample {
|
|
4
5
|
budgetBytes: number;
|
|
5
6
|
gpuBudgetBytes: number;
|
|
@@ -14,7 +15,6 @@ export interface DeviceCapacitySample {
|
|
|
14
15
|
availableRamBytes?: number;
|
|
15
16
|
serializeLoads: boolean;
|
|
16
17
|
}
|
|
17
|
-
export declare function processAlive(pid: number): boolean;
|
|
18
18
|
/**
|
|
19
19
|
* SQLite's process lock makes check-and-reserve atomic across GEZEL_HOME values.
|
|
20
20
|
* Reservations survive a broker restart. A dead parent does not free the memory
|
|
@@ -22,11 +22,13 @@ export declare function processAlive(pid: number): boolean;
|
|
|
22
22
|
*/
|
|
23
23
|
export declare class DeviceCapacityLedger {
|
|
24
24
|
private readonly options;
|
|
25
|
+
private readonly identityCache;
|
|
25
26
|
constructor(options: {
|
|
26
27
|
directory: string;
|
|
27
28
|
sample: () => Promise<DeviceCapacitySample>;
|
|
28
29
|
alive?: (pid: number) => boolean;
|
|
29
30
|
identities?: typeof readProcessIdentities;
|
|
31
|
+
identityCache?: ProcessIdentityCache;
|
|
30
32
|
});
|
|
31
33
|
execute(command: NativeCapacityCommand): Promise<NativeCapacityReply>;
|
|
32
34
|
private transact;
|
|
@@ -2,7 +2,8 @@ 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 {
|
|
5
|
+
import { ProcessIdentityCache, processAlive, sharedProcessIdentities, } from './process-identity.js';
|
|
6
|
+
export { processAlive };
|
|
6
7
|
// Keep the newer builtin behind a runtime require. The current tsup/esbuild
|
|
7
8
|
// target rewrites a static `node:sqlite` import to the nonexistent npm package
|
|
8
9
|
// `sqlite`, making the daemon bundle fail before it can write runtime state.
|
|
@@ -11,15 +12,6 @@ const { DatabaseSync } = nodeRequire('node:sqlite');
|
|
|
11
12
|
function gb(bytes) {
|
|
12
13
|
return `${(bytes / 1024 ** 3).toFixed(1)} GB`;
|
|
13
14
|
}
|
|
14
|
-
export function processAlive(pid) {
|
|
15
|
-
try {
|
|
16
|
-
process.kill(pid, 0);
|
|
17
|
-
return true;
|
|
18
|
-
}
|
|
19
|
-
catch (error) {
|
|
20
|
-
return error.code !== 'ESRCH';
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
15
|
/**
|
|
24
16
|
* SQLite's process lock makes check-and-reserve atomic across GEZEL_HOME values.
|
|
25
17
|
* Reservations survive a broker restart. A dead parent does not free the memory
|
|
@@ -27,8 +19,18 @@ export function processAlive(pid) {
|
|
|
27
19
|
*/
|
|
28
20
|
export class DeviceCapacityLedger {
|
|
29
21
|
options;
|
|
22
|
+
identityCache;
|
|
30
23
|
constructor(options) {
|
|
31
24
|
this.options = options;
|
|
25
|
+
// An injected reader still goes through a cache, so tests see the reads
|
|
26
|
+
// production makes. An injected `alive` alone checks liveness only.
|
|
27
|
+
this.identityCache =
|
|
28
|
+
options.identityCache ??
|
|
29
|
+
(options.identities
|
|
30
|
+
? new ProcessIdentityCache({ read: options.identities, alive: options.alive })
|
|
31
|
+
: options.alive
|
|
32
|
+
? undefined
|
|
33
|
+
: sharedProcessIdentities);
|
|
32
34
|
}
|
|
33
35
|
async execute(command) {
|
|
34
36
|
const sample = await this.options.sample();
|
|
@@ -43,18 +45,31 @@ export class DeviceCapacityLedger {
|
|
|
43
45
|
.prepare('SELECT claim FROM claims')
|
|
44
46
|
.all()
|
|
45
47
|
.map((row) => JSON.parse(String(row.claim)));
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
]);
|
|
50
|
-
if (command.action === 'acquire')
|
|
51
|
-
pids.push(command.ownerPid);
|
|
48
|
+
const record = [];
|
|
49
|
+
if (command.action === 'acquire' && !snapshot.some((claim) => claim.id === command.id))
|
|
50
|
+
record.push(command.ownerPid);
|
|
52
51
|
if (command.action === 'bind')
|
|
53
|
-
|
|
52
|
+
record.push(command.childPid);
|
|
53
|
+
// Stamps only decide which reservations are stale, and a stale one costs
|
|
54
|
+
// someone only while a request waits for memory. With no request
|
|
55
|
+
// waiting, liveness alone prunes, so the status check that runs before
|
|
56
|
+
// every engine request reads no stamps.
|
|
57
|
+
const confirm = new Map();
|
|
58
|
+
const stamp = (pid, identity) => {
|
|
59
|
+
if (pid !== undefined && identity !== undefined)
|
|
60
|
+
confirm.set(pid, [...(confirm.get(pid) ?? []), identity]);
|
|
61
|
+
};
|
|
62
|
+
if (command.action === 'acquire' || snapshot.some((claim) => claim.phase === 'waiting')) {
|
|
63
|
+
for (const claim of snapshot) {
|
|
64
|
+
stamp(claim.childPid, claim.childIdentity);
|
|
65
|
+
// An owner's stamp is only consulted while its claim is queued.
|
|
66
|
+
if (claim.phase === 'waiting')
|
|
67
|
+
stamp(claim.ownerPid, claim.ownerIdentity);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
54
70
|
// OS probes may spawn a helper. Never hold SQLite's writer lock while awaiting one.
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
? await probe(pids).catch(() => new Map())
|
|
71
|
+
const identities = command.action !== 'release' && this.identityCache
|
|
72
|
+
? await this.identityCache.lookup(record, confirm).catch(() => new Map())
|
|
58
73
|
: new Map();
|
|
59
74
|
db.exec('BEGIN IMMEDIATE');
|
|
60
75
|
try {
|
|
@@ -1,2 +1,40 @@
|
|
|
1
|
+
export declare function processAlive(pid: number): boolean;
|
|
1
2
|
/** Birth stamps survive coordinator restarts and distinguish a reused PID. */
|
|
2
3
|
export declare function readProcessIdentities(pids: readonly number[]): Promise<ReadonlyMap<number, string>>;
|
|
4
|
+
/**
|
|
5
|
+
* How long a stamp read for a still-live PID is trusted. A PID can die and be
|
|
6
|
+
* reused between two liveness checks; this bounds how long such a dead
|
|
7
|
+
* engine's reservation outlives it. That is the safe direction: memory looks
|
|
8
|
+
* taken for a few seconds longer, never free twice.
|
|
9
|
+
*/
|
|
10
|
+
export declare const VERIFIED_IDENTITY_TTL_MS = 10000;
|
|
11
|
+
export interface ProcessIdentityCacheOptions {
|
|
12
|
+
read?: (pids: readonly number[]) => Promise<ReadonlyMap<number, string>>;
|
|
13
|
+
alive?: (pid: number) => boolean;
|
|
14
|
+
now?: () => number;
|
|
15
|
+
ttlMs?: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Reading a birth stamp starts `ps`, or PowerShell on Windows. The capacity
|
|
19
|
+
* ledger used to read every PID it holds on each command, which ran before
|
|
20
|
+
* every engine request. A stamp read for a PID that is still alive is reused
|
|
21
|
+
* for a short while, and the PIDs that do need reading go in one batch.
|
|
22
|
+
*
|
|
23
|
+
* A cached stamp may only confirm the stamp a reservation already recorded.
|
|
24
|
+
* A stamp about to be recorded, or one that disagrees, is read again: another
|
|
25
|
+
* coordinator may have recorded a newer process at that PID, and trusting an
|
|
26
|
+
* older stamp there would free a live engine's memory.
|
|
27
|
+
*/
|
|
28
|
+
export declare class ProcessIdentityCache {
|
|
29
|
+
private readonly options;
|
|
30
|
+
private readonly verified;
|
|
31
|
+
constructor(options?: ProcessIdentityCacheOptions);
|
|
32
|
+
/**
|
|
33
|
+
* `record`: PIDs whose stamp is about to be written onto a reservation.
|
|
34
|
+
* `confirm`: each PID's stamps as reservations recorded them.
|
|
35
|
+
* PIDs that are dead, or whose stamp cannot be read, are left out.
|
|
36
|
+
*/
|
|
37
|
+
lookup(record: Iterable<number>, confirm: ReadonlyMap<number, readonly string[]>): Promise<ReadonlyMap<number, string>>;
|
|
38
|
+
}
|
|
39
|
+
/** One cache per process: every ledger in it asks about the same OS processes. */
|
|
40
|
+
export declare const sharedProcessIdentities: ProcessIdentityCache;
|
|
@@ -11,6 +11,15 @@ const PROBE_PID_LIMIT = {
|
|
|
11
11
|
darwin: 99_999,
|
|
12
12
|
win32: 2_147_483_647,
|
|
13
13
|
};
|
|
14
|
+
export function processAlive(pid) {
|
|
15
|
+
try {
|
|
16
|
+
process.kill(pid, 0);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
return error.code !== 'ESRCH';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
14
23
|
/** Birth stamps survive coordinator restarts and distinguish a reused PID. */
|
|
15
24
|
export async function readProcessIdentities(pids) {
|
|
16
25
|
const limit = PROBE_PID_LIMIT[process.platform] ?? Number.MAX_SAFE_INTEGER;
|
|
@@ -68,3 +77,80 @@ export async function readProcessIdentities(pids) {
|
|
|
68
77
|
}
|
|
69
78
|
return identities;
|
|
70
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* How long a stamp read for a still-live PID is trusted. A PID can die and be
|
|
82
|
+
* reused between two liveness checks; this bounds how long such a dead
|
|
83
|
+
* engine's reservation outlives it. That is the safe direction: memory looks
|
|
84
|
+
* taken for a few seconds longer, never free twice.
|
|
85
|
+
*/
|
|
86
|
+
export const VERIFIED_IDENTITY_TTL_MS = 10_000;
|
|
87
|
+
/**
|
|
88
|
+
* Reading a birth stamp starts `ps`, or PowerShell on Windows. The capacity
|
|
89
|
+
* ledger used to read every PID it holds on each command, which ran before
|
|
90
|
+
* every engine request. A stamp read for a PID that is still alive is reused
|
|
91
|
+
* for a short while, and the PIDs that do need reading go in one batch.
|
|
92
|
+
*
|
|
93
|
+
* A cached stamp may only confirm the stamp a reservation already recorded.
|
|
94
|
+
* A stamp about to be recorded, or one that disagrees, is read again: another
|
|
95
|
+
* coordinator may have recorded a newer process at that PID, and trusting an
|
|
96
|
+
* older stamp there would free a live engine's memory.
|
|
97
|
+
*/
|
|
98
|
+
export class ProcessIdentityCache {
|
|
99
|
+
options;
|
|
100
|
+
verified = new Map();
|
|
101
|
+
constructor(options = {}) {
|
|
102
|
+
this.options = options;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* `record`: PIDs whose stamp is about to be written onto a reservation.
|
|
106
|
+
* `confirm`: each PID's stamps as reservations recorded them.
|
|
107
|
+
* PIDs that are dead, or whose stamp cannot be read, are left out.
|
|
108
|
+
*/
|
|
109
|
+
async lookup(record, confirm) {
|
|
110
|
+
const alive = this.options.alive ?? processAlive;
|
|
111
|
+
const ttl = this.options.ttlMs ?? VERIFIED_IDENTITY_TTL_MS;
|
|
112
|
+
const now = (this.options.now ?? (() => performance.now()))();
|
|
113
|
+
for (const [pid, entry] of this.verified) {
|
|
114
|
+
if (now - entry.at >= ttl)
|
|
115
|
+
this.verified.delete(pid);
|
|
116
|
+
}
|
|
117
|
+
const live = (pid) => {
|
|
118
|
+
if (alive(pid))
|
|
119
|
+
return true;
|
|
120
|
+
// Forget a PID seen dead, so whatever process takes it next is read.
|
|
121
|
+
this.verified.delete(pid);
|
|
122
|
+
return false;
|
|
123
|
+
};
|
|
124
|
+
const identities = new Map();
|
|
125
|
+
const unread = new Set();
|
|
126
|
+
for (const pid of record)
|
|
127
|
+
if (live(pid))
|
|
128
|
+
unread.add(pid);
|
|
129
|
+
for (const [pid, stamps] of confirm) {
|
|
130
|
+
if (unread.has(pid) || !live(pid))
|
|
131
|
+
continue;
|
|
132
|
+
const cached = this.verified.get(pid)?.identity;
|
|
133
|
+
if (cached !== undefined && stamps.every((stamp) => stamp === cached)) {
|
|
134
|
+
identities.set(pid, cached);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
unread.add(pid);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (unread.size === 0)
|
|
141
|
+
return identities;
|
|
142
|
+
const read = await (this.options.read ?? readProcessIdentities)([...unread]).catch(() => new Map());
|
|
143
|
+
for (const pid of unread) {
|
|
144
|
+
const identity = read.get(pid);
|
|
145
|
+
if (identity === undefined) {
|
|
146
|
+
this.verified.delete(pid);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
this.verified.set(pid, { identity, at: now });
|
|
150
|
+
identities.set(pid, identity);
|
|
151
|
+
}
|
|
152
|
+
return identities;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/** One cache per process: every ledger in it asks about the same OS processes. */
|
|
156
|
+
export const sharedProcessIdentities = new ProcessIdentityCache();
|
package/dist/runtime-lock.d.ts
CHANGED
|
@@ -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
|
|
@@ -12,33 +15,6 @@ export declare class SingleInstanceError extends Error {
|
|
|
12
15
|
export interface RuntimeLock {
|
|
13
16
|
release(): Promise<void>;
|
|
14
17
|
}
|
|
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;
|
|
42
18
|
/**
|
|
43
19
|
* Acquire an exclusive single-instance lock for a gezel home so two daemons
|
|
44
20
|
* can't share one `~/.gezel/` and race its on-disk state (config, sessions,
|
package/dist/runtime-lock.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { readFileSync, readlinkSync } from 'node:fs';
|
|
2
1
|
import { mkdir, open, readFile, rm, stat, utimes } from 'node:fs/promises';
|
|
3
|
-
import {
|
|
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 };
|
|
4
4
|
/**
|
|
5
5
|
* Thrown when a gezel home already has a live daemon. The CLI's `gezel
|
|
6
6
|
* start` / the supervisor surface this as an actionable message rather
|
|
@@ -12,7 +12,7 @@ export class SingleInstanceError extends Error {
|
|
|
12
12
|
holderHost;
|
|
13
13
|
constructor(holderPid, lockPath, holderHost) {
|
|
14
14
|
super(holderHost !== undefined
|
|
15
|
-
?
|
|
15
|
+
? foreignHolderMessage({ pid: holderPid, hostname: holderHost })
|
|
16
16
|
: holderPid > 0
|
|
17
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
18
|
: `Could not acquire the single-instance lock at ${lockPath}.`);
|
|
@@ -22,8 +22,8 @@ export class SingleInstanceError extends Error {
|
|
|
22
22
|
this.name = 'SingleInstanceError';
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
-
|
|
26
|
-
|
|
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.
|
|
27
27
|
function defaultIsAlive(pid) {
|
|
28
28
|
try {
|
|
29
29
|
process.kill(pid, 0);
|
|
@@ -35,45 +35,6 @@ function defaultIsAlive(pid) {
|
|
|
35
35
|
return err.code === 'EPERM';
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
-
function readTrimmed(path) {
|
|
39
|
-
try {
|
|
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
38
|
/**
|
|
78
39
|
* The pid stays on the first line so a daemon from before the identity
|
|
79
40
|
* record (which parses the leading integer) still reads it as held instead
|
|
@@ -82,29 +43,6 @@ export function currentLockIdentity() {
|
|
|
82
43
|
function serializeIdentity(identity) {
|
|
83
44
|
return `${identity.pid}\n${JSON.stringify({ v: 1, ...identity })}\n`;
|
|
84
45
|
}
|
|
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
46
|
async function readHolder(lockPath) {
|
|
109
47
|
try {
|
|
110
48
|
return parseLockFile(await readFile(lockPath, 'utf8'));
|
|
@@ -113,17 +51,6 @@ async function readHolder(lockPath) {
|
|
|
113
51
|
return null;
|
|
114
52
|
}
|
|
115
53
|
}
|
|
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
54
|
async function lockMtimeMs(lockPath) {
|
|
128
55
|
try {
|
|
129
56
|
return (await stat(lockPath)).mtimeMs;
|
|
@@ -174,7 +101,7 @@ export async function acquireSingleInstanceLock(opts) {
|
|
|
174
101
|
const startTicksOf = opts.startTicksOf ?? procStartTicks;
|
|
175
102
|
const now = opts.now ?? Date.now;
|
|
176
103
|
const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
177
|
-
const staleAfterMs = opts.staleAfterMs ??
|
|
104
|
+
const staleAfterMs = opts.staleAfterMs ?? LOCK_STALE_AFTER_MS;
|
|
178
105
|
await mkdir(opts.runtimeDir, { recursive: true });
|
|
179
106
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
180
107
|
try {
|
|
@@ -185,7 +112,7 @@ export async function acquireSingleInstanceLock(opts) {
|
|
|
185
112
|
finally {
|
|
186
113
|
await handle.close();
|
|
187
114
|
}
|
|
188
|
-
return holdLock(opts.lockPath, me, opts.heartbeatIntervalMs ??
|
|
115
|
+
return holdLock(opts.lockPath, me, opts.heartbeatIntervalMs ?? LOCK_HEARTBEAT_INTERVAL_MS);
|
|
189
116
|
}
|
|
190
117
|
catch (err) {
|
|
191
118
|
if (err.code !== 'EEXIST')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bendyline/gezel-inference",
|
|
3
|
-
"version": "1.0.
|
|
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,9 +73,9 @@
|
|
|
73
73
|
"!dist/**/__pycache__"
|
|
74
74
|
],
|
|
75
75
|
"dependencies": {
|
|
76
|
-
"@bendyline/gezel": "1.2.
|
|
77
|
-
"@bendyline/gezel-catalog": "1.2.
|
|
78
|
-
"@bendyline/gezel-client": "1.2.
|
|
76
|
+
"@bendyline/gezel": "1.2.13",
|
|
77
|
+
"@bendyline/gezel-catalog": "1.2.13",
|
|
78
|
+
"@bendyline/gezel-client": "1.2.13",
|
|
79
79
|
"@bendyline/gezel-knowledge": "1.2.10",
|
|
80
80
|
"hono": "4.13.14",
|
|
81
81
|
"tar": "7.5.21",
|