eklavya 1.23.0 → 1.24.1
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/assets/dashboard.html +3 -3
- package/dist/cli.js +93 -11
- package/dist/cli.js.map +1 -1
- package/dist/config.js +3 -5
- package/dist/config.js.map +1 -1
- package/dist/hooks/lib.js +6 -0
- package/dist/hooks/lib.js.map +1 -1
- package/dist/hooks/memory-lib.js +60 -7
- package/dist/hooks/memory-lib.js.map +1 -1
- package/dist/memory/provider.js +175 -58
- package/dist/memory/provider.js.map +1 -1
- package/dist/memory/reservation.js +131 -0
- package/dist/memory/reservation.js.map +1 -0
- package/dist/memory/store.js +38 -9
- package/dist/memory/store.js.map +1 -1
- package/dist/memory/summarize.js.map +1 -1
- package/dist/memory/worker.js +29 -12
- package/dist/memory/worker.js.map +1 -1
- package/dist/onboard.js +38 -4
- package/dist/onboard.js.map +1 -1
- package/dist/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/plugin/hooks/CLAUDE.md +13 -3
- package/dist/plugin/hooks/run.mjs +6 -0
- package/dist/tools/config_tools.js +3 -3
- package/dist/tools/config_tools.js.map +1 -1
- package/package.json +1 -2
package/dist/hooks/memory-lib.js
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The hooks' half of memory capture.
|
|
3
|
+
*
|
|
4
|
+
* Every function here swallows its own failures. A hook must never break a
|
|
5
|
+
* session (`hooks/CLAUDE.md`), and memory is the half of this plugin a
|
|
6
|
+
* developer is *least* willing to lose a session to: it runs on every tool
|
|
7
|
+
* call, so a throw here is a throw on every tool call.
|
|
8
|
+
*/
|
|
9
|
+
import { spawn } from 'node:child_process';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
1
11
|
import { projectKey } from '../store.js';
|
|
2
12
|
import { identityFor } from '../memory/identity.js';
|
|
3
13
|
import { capture, drainSpool } from '../memory/capture.js';
|
|
4
|
-
import { batchSession, pendingEventCount } from '../memory/store.js';
|
|
14
|
+
import { batchSession, hasClaimableJob, pendingEventCount } from '../memory/store.js';
|
|
5
15
|
import { processPending, writeSessionSummary } from '../memory/worker.js';
|
|
6
16
|
import { recall, recallForPrompt } from '../memory/recall.js';
|
|
7
17
|
import { notify, queuePausedAlert, sessionWrapUp } from '../memory/notify.js';
|
|
8
18
|
import { countEntries } from '../memory/store.js';
|
|
9
19
|
import { queueDepth } from '../memory/worker.js';
|
|
20
|
+
import { isInternalObserver, releaseWorker, renewWorker, reserveWorker } from '../memory/reservation.js';
|
|
10
21
|
export function identityOf(input, cwd, sid) {
|
|
11
22
|
const identity = identityFor({
|
|
12
23
|
cwd,
|
|
@@ -35,7 +46,7 @@ export function record(db, resolved, identity, event) {
|
|
|
35
46
|
export function batchIfFull(db, resolved, identity) {
|
|
36
47
|
try {
|
|
37
48
|
const max = resolved.config.memory.batch_max_events;
|
|
38
|
-
if (pendingEventCount(db, identity.project) < max)
|
|
49
|
+
if (pendingEventCount(db, identity.project, identity.sessionId) < max)
|
|
39
50
|
return;
|
|
40
51
|
batchSession(db, {
|
|
41
52
|
project: identity.project,
|
|
@@ -51,10 +62,10 @@ export function batchIfFull(db, resolved, identity) {
|
|
|
51
62
|
/**
|
|
52
63
|
* The session seam: close what is open and summarise it.
|
|
53
64
|
*
|
|
54
|
-
* With a provider configured
|
|
55
|
-
* inside a Stop hook would make the developer wait for
|
|
56
|
-
* turn, which PRD LRN-04 forbids
|
|
57
|
-
*
|
|
65
|
+
* With a provider configured the hook never summarises itself: waiting on a
|
|
66
|
+
* model inside a Stop hook would make the developer wait for it to finish a
|
|
67
|
+
* turn, which PRD LRN-04 forbids. It hands the queue to a detached worker
|
|
68
|
+
* instead and returns.
|
|
58
69
|
*/
|
|
59
70
|
export async function flushAtSeam(db, resolved, identity) {
|
|
60
71
|
try {
|
|
@@ -64,14 +75,56 @@ export async function flushAtSeam(db, resolved, identity) {
|
|
|
64
75
|
reason: 'session_seam',
|
|
65
76
|
maxEvents: resolved.config.memory.batch_max_events,
|
|
66
77
|
});
|
|
67
|
-
if (resolved.config.providers.observer)
|
|
78
|
+
if (resolved.config.providers.observer) {
|
|
79
|
+
// A paused queue is waiting on the developer (a login, a usage limit, a
|
|
80
|
+
// missing `claude`). Launching per seam would relearn that once a turn;
|
|
81
|
+
// `eklavya memory process` is the explicit resume.
|
|
82
|
+
if (hasClaimableJob(db) && queueDepth(db).paused === 0)
|
|
83
|
+
drainInBackground(db);
|
|
68
84
|
return;
|
|
85
|
+
}
|
|
69
86
|
await processPending(db, resolved.config, { maxJobs: 2 });
|
|
70
87
|
}
|
|
71
88
|
catch {
|
|
72
89
|
/* Nothing summarised is evidence still on disk, not evidence lost. */
|
|
73
90
|
}
|
|
74
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Starts `eklavya memory process --no-resume` detached and returns at once, so
|
|
94
|
+
* a model summarises the queue without the hook waiting on it.
|
|
95
|
+
*
|
|
96
|
+
* Only after winning the one worker reservation (`reservation.ts`): seams that
|
|
97
|
+
* close together used to spawn a worker each, and each worker's `claude -p`
|
|
98
|
+
* ran these hooks and spawned more. The loser leaves its batch queued for the
|
|
99
|
+
* worker already running. The token travels to the child, which adopts the
|
|
100
|
+
* slot rather than competing for it. `--no-resume` leaves paused jobs paused:
|
|
101
|
+
* un-pausing stays an explicit act.
|
|
102
|
+
*/
|
|
103
|
+
function drainInBackground(db) {
|
|
104
|
+
if (isInternalObserver())
|
|
105
|
+
return;
|
|
106
|
+
const token = reserveWorker(db);
|
|
107
|
+
if (!token)
|
|
108
|
+
return;
|
|
109
|
+
try {
|
|
110
|
+
const cli = fileURLToPath(new URL('../cli.js', import.meta.url));
|
|
111
|
+
const child = spawn(process.execPath, [cli, 'memory', 'process', '--no-resume', '--max', '4', '--worker-token', token], {
|
|
112
|
+
detached: true,
|
|
113
|
+
stdio: 'ignore',
|
|
114
|
+
windowsHide: true,
|
|
115
|
+
});
|
|
116
|
+
child.on('error', () => releaseWorker(db, token));
|
|
117
|
+
if (!child.pid) {
|
|
118
|
+
releaseWorker(db, token);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
renewWorker(db, token, { pid: child.pid });
|
|
122
|
+
child.unref();
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
releaseWorker(db, token);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
75
128
|
/** Replays anything the spool holds. Idempotent; safe to call every session. */
|
|
76
129
|
export function replaySpool(db) {
|
|
77
130
|
try {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory-lib.js","sourceRoot":"","sources":["../../src/hooks/memory-lib.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"memory-lib.js","sourceRoot":"","sources":["../../src/hooks/memory-lib.ts"],"names":[],"mappings":"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"}
|
package/dist/memory/provider.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import os from 'node:os';
|
|
1
3
|
import { z } from 'zod';
|
|
4
|
+
import { OBSERVER_ENV } from './reservation.js';
|
|
2
5
|
export class ProviderError extends Error {
|
|
3
6
|
errorClass;
|
|
4
7
|
constructor(errorClass, message) {
|
|
@@ -67,6 +70,175 @@ function renderEvidence(input) {
|
|
|
67
70
|
})
|
|
68
71
|
.join('\n');
|
|
69
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Inside the worker's 120s claim lease: a run that outlived it would see its job
|
|
75
|
+
* claimed by the next worker and summarised twice. A summary takes seconds, so
|
|
76
|
+
* one this slow is a hung child, not a slow model.
|
|
77
|
+
*/
|
|
78
|
+
const TIMEOUT_MS = 100_000;
|
|
79
|
+
/** SIGTERM to SIGKILL, for a tree that ignores the polite ask. */
|
|
80
|
+
const KILL_GRACE_MS = 5_000;
|
|
81
|
+
const MAX_STDOUT = 16 * 1024 * 1024;
|
|
82
|
+
/**
|
|
83
|
+
* The flags that make `claude -p` a summariser and nothing else: no tools, no
|
|
84
|
+
* MCP servers, no hooks — this plugin's hooks included, or the summariser's own
|
|
85
|
+
* session would be captured, batched and summarised in turn — and no transcript
|
|
86
|
+
* left behind.
|
|
87
|
+
*/
|
|
88
|
+
export function claudeArgs(model) {
|
|
89
|
+
return [
|
|
90
|
+
'-p',
|
|
91
|
+
'--model', model,
|
|
92
|
+
'--output-format', 'json',
|
|
93
|
+
'--json-schema', JSON.stringify(OUTPUT_SCHEMA),
|
|
94
|
+
'--system-prompt', SYSTEM,
|
|
95
|
+
'--tools', '',
|
|
96
|
+
'--strict-mcp-config',
|
|
97
|
+
'--no-session-persistence',
|
|
98
|
+
// Plugins, hooks, MCP servers and CLAUDE.md off, auth kept. Belt and braces
|
|
99
|
+
// only: correctness rests on OBSERVER_ENV, because hooks ran through
|
|
100
|
+
// `disableAllHooks` before.
|
|
101
|
+
'--safe-mode',
|
|
102
|
+
// A null helper overrides one in the developer's settings, which would bill an API key.
|
|
103
|
+
'--settings', JSON.stringify({ disableAllHooks: true, apiKeyHelper: null }),
|
|
104
|
+
];
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The structured output out of `claude -p --output-format json`, or the
|
|
108
|
+
* ProviderError that says why there is none. Pure, so the error classes are
|
|
109
|
+
* testable without a model.
|
|
110
|
+
*/
|
|
111
|
+
export function readResult(stdout) {
|
|
112
|
+
let envelope;
|
|
113
|
+
try {
|
|
114
|
+
envelope = JSON.parse(stdout);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
throw new ProviderError('malformed', 'claude -p printed something that is not JSON');
|
|
118
|
+
}
|
|
119
|
+
if (envelope.is_error || envelope.subtype !== 'success') {
|
|
120
|
+
const message = String(envelope.result ?? envelope.subtype ?? 'claude -p failed');
|
|
121
|
+
throw new ProviderError(classifyMessage(message, envelope.api_error_status), message);
|
|
122
|
+
}
|
|
123
|
+
if (envelope.structured_output === undefined) {
|
|
124
|
+
throw new ProviderError('malformed', 'claude -p returned no structured output');
|
|
125
|
+
}
|
|
126
|
+
return envelope.structured_output;
|
|
127
|
+
}
|
|
128
|
+
function classifyMessage(message, status) {
|
|
129
|
+
if (status === 401 || status === 403 || /log ?in|auth|credential/i.test(message))
|
|
130
|
+
return 'auth';
|
|
131
|
+
if (status === 429 || /usage limit|rate limit|quota/i.test(message))
|
|
132
|
+
return 'quota';
|
|
133
|
+
if (/context|too long|max.*token/i.test(message))
|
|
134
|
+
return 'overflow';
|
|
135
|
+
if (/refus|declin/i.test(message))
|
|
136
|
+
return 'permanent';
|
|
137
|
+
return 'transient';
|
|
138
|
+
}
|
|
139
|
+
const NOT_THE_SUBSCRIPTION = [
|
|
140
|
+
'ANTHROPIC_API_KEY',
|
|
141
|
+
'ANTHROPIC_AUTH_TOKEN',
|
|
142
|
+
'CLAUDE_CODE_USE_BEDROCK',
|
|
143
|
+
'CLAUDE_CODE_USE_VERTEX',
|
|
144
|
+
];
|
|
145
|
+
/**
|
|
146
|
+
* Runs the configured Claude model through Claude Code (`claude -p`), on the
|
|
147
|
+
* developer's own subscription. Every variable that would route it elsewhere —
|
|
148
|
+
* an API key or token, Bedrock, Vertex — is stripped from the child's
|
|
149
|
+
* environment, so a stray one in the shell can never turn this into metered
|
|
150
|
+
* API traffic.
|
|
151
|
+
*
|
|
152
|
+
* `OBSERVER_ENV` is set on the way in, and it is the guard that does not depend
|
|
153
|
+
* on Claude Code: hooks inherit it, and every Eklavya hook and worker returns at
|
|
154
|
+
* once when it sees it. `disableAllHooks` asked Claude Code the same thing,
|
|
155
|
+
* and 2.1.280 ran the hooks anyway; `--safe-mode` is a second ask, not a
|
|
156
|
+
* guarantee.
|
|
157
|
+
*
|
|
158
|
+
* The child leads its own process group, so a timeout or a cancel ends the
|
|
159
|
+
* whole tree — `claude` and anything it started — not just the direct child.
|
|
160
|
+
* SIGTERM first, SIGKILL after `graceMs`, and the promise settles only once the
|
|
161
|
+
* child has exited, so the caller never releases its slot beside a live tree.
|
|
162
|
+
*/
|
|
163
|
+
export function runClaude(model, prompt, opts = {}) {
|
|
164
|
+
const env = { ...process.env, [OBSERVER_ENV]: '1' };
|
|
165
|
+
for (const name of NOT_THE_SUBSCRIPTION)
|
|
166
|
+
delete env[name];
|
|
167
|
+
const timeoutMs = opts.timeoutMs ?? TIMEOUT_MS;
|
|
168
|
+
const graceMs = opts.graceMs ?? KILL_GRACE_MS;
|
|
169
|
+
return new Promise((resolve, reject) => {
|
|
170
|
+
if (opts.signal?.aborted)
|
|
171
|
+
return reject(new ProviderError('cancelled', 'cancelled before claude started'));
|
|
172
|
+
// ponytail: no shell, so on Windows only a native `claude.exe` is found, not
|
|
173
|
+
// npm's `claude.cmd` shim — cmd.exe would mangle the JSON arguments. Resolve
|
|
174
|
+
// the shim's cli.js and run it with node if Windows npm installs need this.
|
|
175
|
+
const child = spawn('claude', claudeArgs(model), {
|
|
176
|
+
env,
|
|
177
|
+
cwd: os.tmpdir(),
|
|
178
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
179
|
+
detached: process.platform !== 'win32',
|
|
180
|
+
windowsHide: true,
|
|
181
|
+
});
|
|
182
|
+
let stdout = '';
|
|
183
|
+
let ended = null;
|
|
184
|
+
let force;
|
|
185
|
+
const signalTree = (sig) => {
|
|
186
|
+
try {
|
|
187
|
+
// Negative pid: the whole group. Windows has no groups; the child alone.
|
|
188
|
+
// ponytail: Windows leaves grandchildren; `taskkill /T /F` if that matters.
|
|
189
|
+
if (process.platform === 'win32' || !child.pid)
|
|
190
|
+
child.kill(sig);
|
|
191
|
+
else
|
|
192
|
+
process.kill(-child.pid, sig);
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
/* Already gone. */
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
const end = (why) => {
|
|
199
|
+
if (ended)
|
|
200
|
+
return;
|
|
201
|
+
ended = why;
|
|
202
|
+
signalTree('SIGTERM');
|
|
203
|
+
force = setTimeout(() => signalTree('SIGKILL'), graceMs);
|
|
204
|
+
};
|
|
205
|
+
const timer = setTimeout(() => end(new ProviderError('transient', `claude -p did not finish in ${timeoutMs / 1000}s`)), timeoutMs);
|
|
206
|
+
const onAbort = () => end(new ProviderError('cancelled', 'memory processing was turned off'));
|
|
207
|
+
opts.signal?.addEventListener('abort', onAbort, { once: true });
|
|
208
|
+
opts.onSpawn?.(child.pid ?? null);
|
|
209
|
+
child.stdout.setEncoding('utf8');
|
|
210
|
+
child.stdout.on('data', (d) => {
|
|
211
|
+
stdout += d;
|
|
212
|
+
if (stdout.length > MAX_STDOUT)
|
|
213
|
+
end(new ProviderError('malformed', 'claude -p printed more than 16 MB'));
|
|
214
|
+
});
|
|
215
|
+
child.stdin.on('error', () => { });
|
|
216
|
+
child.stdin.end(prompt);
|
|
217
|
+
let spawnError = null;
|
|
218
|
+
child.once('error', (err) => {
|
|
219
|
+
spawnError = err;
|
|
220
|
+
});
|
|
221
|
+
child.once('close', () => {
|
|
222
|
+
clearTimeout(timer);
|
|
223
|
+
if (force)
|
|
224
|
+
clearTimeout(force);
|
|
225
|
+
opts.signal?.removeEventListener('abort', onAbort);
|
|
226
|
+
// The group may outlive its leader: make sure nothing it started is left.
|
|
227
|
+
if (ended)
|
|
228
|
+
signalTree('SIGKILL');
|
|
229
|
+
opts.onSpawn?.(null);
|
|
230
|
+
if (spawnError?.code === 'ENOENT') {
|
|
231
|
+
return reject(new ProviderError('missing', 'claude is not on the PATH the hooks see'));
|
|
232
|
+
}
|
|
233
|
+
if (ended)
|
|
234
|
+
return reject(ended);
|
|
235
|
+
if (spawnError)
|
|
236
|
+
return reject(new ProviderError('transient', spawnError.message));
|
|
237
|
+
// A failed run still prints its JSON envelope; that says more than the exit code.
|
|
238
|
+
resolve(stdout);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
}
|
|
70
242
|
export class ProviderSummarizer {
|
|
71
243
|
config;
|
|
72
244
|
id;
|
|
@@ -74,52 +246,11 @@ export class ProviderSummarizer {
|
|
|
74
246
|
this.config = config;
|
|
75
247
|
this.id = `${config.kind}:${config.model}`;
|
|
76
248
|
}
|
|
77
|
-
async summarize(input) {
|
|
249
|
+
async summarize(input, opts = {}) {
|
|
78
250
|
if (!input.events.length)
|
|
79
251
|
return [];
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
throw new ProviderError('auth', `${this.config.api_key_env} is not set`);
|
|
83
|
-
}
|
|
84
|
-
const { default: Anthropic } = await import('@anthropic-ai/sdk');
|
|
85
|
-
const client = new Anthropic({ apiKey });
|
|
86
|
-
let response;
|
|
87
|
-
try {
|
|
88
|
-
response = await client.messages.create({
|
|
89
|
-
model: this.config.model,
|
|
90
|
-
max_tokens: 16000,
|
|
91
|
-
system: SYSTEM,
|
|
92
|
-
output_config: { format: { type: 'json_schema', schema: OUTPUT_SCHEMA } },
|
|
93
|
-
messages: [
|
|
94
|
-
{
|
|
95
|
-
role: 'user',
|
|
96
|
-
content: `<evidence project="${input.project}" session="${input.sessionId}">\n${renderEvidence(input)}\n</evidence>`,
|
|
97
|
-
},
|
|
98
|
-
],
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
catch (error) {
|
|
102
|
-
throw new ProviderError(classify(error, Anthropic), error instanceof Error ? error.message : String(error));
|
|
103
|
-
}
|
|
104
|
-
if (response.stop_reason === 'refusal') {
|
|
105
|
-
// A refusal is a deliberate, final no. Retrying spends money to be told
|
|
106
|
-
// the same thing, so it is terminal rather than transient.
|
|
107
|
-
throw new ProviderError('permanent', 'the provider declined to summarise this batch');
|
|
108
|
-
}
|
|
109
|
-
if (response.stop_reason === 'max_tokens') {
|
|
110
|
-
throw new ProviderError('overflow', 'the summary was truncated by max_tokens');
|
|
111
|
-
}
|
|
112
|
-
const text = response.content
|
|
113
|
-
.map((block) => (block.type === 'text' ? block.text : ''))
|
|
114
|
-
.join('');
|
|
115
|
-
let parsed;
|
|
116
|
-
try {
|
|
117
|
-
parsed = JSON.parse(text);
|
|
118
|
-
}
|
|
119
|
-
catch {
|
|
120
|
-
throw new ProviderError('malformed', 'the provider returned text that is not JSON');
|
|
121
|
-
}
|
|
122
|
-
const result = ResultSchema.safeParse(parsed);
|
|
252
|
+
const stdout = await runClaude(this.config.model, `<evidence project="${input.project}" session="${input.sessionId}">\n${renderEvidence(input)}\n</evidence>`, opts);
|
|
253
|
+
const result = ResultSchema.safeParse(readResult(stdout));
|
|
123
254
|
if (!result.success) {
|
|
124
255
|
throw new ProviderError('malformed', `the provider output failed validation: ${result.error.message.slice(0, 200)}`);
|
|
125
256
|
}
|
|
@@ -138,18 +269,4 @@ export class ProviderSummarizer {
|
|
|
138
269
|
}));
|
|
139
270
|
}
|
|
140
271
|
}
|
|
141
|
-
function classify(error, sdk) {
|
|
142
|
-
if (error instanceof sdk.AuthenticationError || error instanceof sdk.PermissionDeniedError)
|
|
143
|
-
return 'auth';
|
|
144
|
-
if (error instanceof sdk.RateLimitError)
|
|
145
|
-
return 'quota';
|
|
146
|
-
if (error instanceof sdk.BadRequestError) {
|
|
147
|
-
return /context|too long|max.*token/i.test(error.message) ? 'overflow' : 'permanent';
|
|
148
|
-
}
|
|
149
|
-
if (error instanceof sdk.APIConnectionError)
|
|
150
|
-
return 'transient';
|
|
151
|
-
if (error instanceof sdk.APIError)
|
|
152
|
-
return error.status && error.status >= 500 ? 'transient' : 'permanent';
|
|
153
|
-
return 'transient';
|
|
154
|
-
}
|
|
155
272
|
//# sourceMappingURL=provider.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../../src/memory/provider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../../src/memory/provider.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* One background memory worker per installation, and none inside Eklavya's own
|
|
4
|
+
* helpers.
|
|
5
|
+
*
|
|
6
|
+
* The incident this exists for: the observer runs `claude -p`, that helper ran
|
|
7
|
+
* the plugin's hooks despite `disableAllHooks`, its seams started more workers,
|
|
8
|
+
* and each worker started another helper — 165 workers and 169 Haiku processes
|
|
9
|
+
* on a 16 GB Mac before it stopped answering. Two independent guards, so either
|
|
10
|
+
* one alone would have held:
|
|
11
|
+
*
|
|
12
|
+
* 1. `OBSERVER_ENV` marks every process descended from a helper. Every hook,
|
|
13
|
+
* the launcher and `memory process` return before touching anything.
|
|
14
|
+
* 2. A single reservation row in the shared database. Checking "is there
|
|
15
|
+
* work" and then spawning let every seam that closed together spawn its
|
|
16
|
+
* own worker; now the check and the claim are one IMMEDIATE transaction,
|
|
17
|
+
* and only the winner spawns.
|
|
18
|
+
*/
|
|
19
|
+
export const OBSERVER_ENV = 'EKLAVYA_INTERNAL_OBSERVER';
|
|
20
|
+
export function isInternalObserver(env = process.env) {
|
|
21
|
+
return Boolean(env[OBSERVER_ENV]);
|
|
22
|
+
}
|
|
23
|
+
const KEY = 'memory_worker';
|
|
24
|
+
/**
|
|
25
|
+
* Longer than one provider call can live (100s deadline plus the kill grace),
|
|
26
|
+
* and renewed before every job and while a call runs, so a live worker never
|
|
27
|
+
* lapses.
|
|
28
|
+
*/
|
|
29
|
+
export const WORKER_LEASE_MS = 180_000;
|
|
30
|
+
/**
|
|
31
|
+
* An expired lease whose recorded processes still answer `kill(pid, 0)` is not
|
|
32
|
+
* taken over — a replacement beside a live provider tree is the overlap this
|
|
33
|
+
* file forbids. Past this long, a pid that still answers is assumed reused.
|
|
34
|
+
*
|
|
35
|
+
* ponytail: pid liveness, not process identity. Compare process start times if
|
|
36
|
+
* a reused pid ever holds a reservation for fifteen minutes.
|
|
37
|
+
*/
|
|
38
|
+
const TAKEOVER_CEILING_MS = 15 * 60_000;
|
|
39
|
+
function read(db) {
|
|
40
|
+
const row = db.prepare('SELECT value FROM meta WHERE key = ?').get(KEY);
|
|
41
|
+
if (!row)
|
|
42
|
+
return null;
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(row.value);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function write(db, holder) {
|
|
51
|
+
db.prepare('INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)').run(KEY, JSON.stringify(holder));
|
|
52
|
+
}
|
|
53
|
+
function alive(pid) {
|
|
54
|
+
if (!pid)
|
|
55
|
+
return false;
|
|
56
|
+
try {
|
|
57
|
+
process.kill(pid, 0);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
// EPERM: it exists, it is just not ours to signal.
|
|
62
|
+
return err.code === 'EPERM';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Whether `holder` still owns the slot at `now`. */
|
|
66
|
+
function held(holder, now) {
|
|
67
|
+
if (!holder)
|
|
68
|
+
return false;
|
|
69
|
+
const until = Date.parse(holder.until);
|
|
70
|
+
if (until > now)
|
|
71
|
+
return true;
|
|
72
|
+
if (now - until > TAKEOVER_CEILING_MS)
|
|
73
|
+
return false;
|
|
74
|
+
return alive(holder.pid) || alive(holder.child);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Takes the one worker slot, or returns null when someone holds it. Atomic
|
|
78
|
+
* across processes: IMMEDIATE takes the write lock before the read, so two
|
|
79
|
+
* callers cannot both see the slot free.
|
|
80
|
+
*/
|
|
81
|
+
export function reserveWorker(db, pid = null, now = Date.now()) {
|
|
82
|
+
return db.transaction(() => {
|
|
83
|
+
if (held(read(db), now))
|
|
84
|
+
return null;
|
|
85
|
+
const token = crypto.randomUUID();
|
|
86
|
+
const stamp = new Date(now).toISOString();
|
|
87
|
+
write(db, {
|
|
88
|
+
token,
|
|
89
|
+
pid,
|
|
90
|
+
child: null,
|
|
91
|
+
job: null,
|
|
92
|
+
until: new Date(now + WORKER_LEASE_MS).toISOString(),
|
|
93
|
+
heartbeat: stamp,
|
|
94
|
+
});
|
|
95
|
+
return token;
|
|
96
|
+
}).immediate();
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Extends the lease and records what the worker is doing. False when the slot
|
|
100
|
+
* is no longer this token's — the caller must stop, not carry on beside the
|
|
101
|
+
* worker that replaced it.
|
|
102
|
+
*/
|
|
103
|
+
export function renewWorker(db, token, patch = {}, now = Date.now()) {
|
|
104
|
+
return db.transaction(() => {
|
|
105
|
+
const holder = read(db);
|
|
106
|
+
if (!holder || holder.token !== token)
|
|
107
|
+
return false;
|
|
108
|
+
write(db, {
|
|
109
|
+
...holder,
|
|
110
|
+
...patch,
|
|
111
|
+
until: new Date(now + WORKER_LEASE_MS).toISOString(),
|
|
112
|
+
heartbeat: new Date(now).toISOString(),
|
|
113
|
+
});
|
|
114
|
+
return true;
|
|
115
|
+
}).immediate();
|
|
116
|
+
}
|
|
117
|
+
/** Gives the slot back. A no-op unless `token` holds it. */
|
|
118
|
+
export function releaseWorker(db, token) {
|
|
119
|
+
try {
|
|
120
|
+
db.prepare("DELETE FROM meta WHERE key = ? AND json_extract(value, '$.token') = ?").run(KEY, token);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
/* The lease expires on its own. */
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** The current holder, for `memory status` and `doctor`. Null when the slot is free. */
|
|
127
|
+
export function workerStatus(db, now = Date.now()) {
|
|
128
|
+
const holder = read(db);
|
|
129
|
+
return held(holder, now) ? holder : null;
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=reservation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reservation.js","sourceRoot":"","sources":["../../src/memory/reservation.ts"],"names":[],"mappings":"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"}
|