akemon 0.2.23 → 0.2.24
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/engine-peripheral.js +37 -33
- package/dist/engine-queue.js +95 -0
- package/dist/reflection-module.js +1 -1
- package/dist/server.js +45 -39
- package/dist/task-module.js +1 -1
- package/package.json +1 -1
|
@@ -24,9 +24,6 @@ export class EnginePeripheral {
|
|
|
24
24
|
tags = ["engine", "llm"];
|
|
25
25
|
config;
|
|
26
26
|
bus = null;
|
|
27
|
-
/** Engine mutual exclusion — only one engine process at a time */
|
|
28
|
-
busy = false;
|
|
29
|
-
busySince = 0;
|
|
30
27
|
/** Last execution trace (for error reporting) */
|
|
31
28
|
lastTrace = [];
|
|
32
29
|
constructor(config) {
|
|
@@ -70,38 +67,15 @@ export class EnginePeripheral {
|
|
|
70
67
|
}, this.id);
|
|
71
68
|
}
|
|
72
69
|
// ---------------------------------------------------------------------------
|
|
73
|
-
// Engine mutex helpers
|
|
74
|
-
// ---------------------------------------------------------------------------
|
|
75
|
-
acquire() {
|
|
76
|
-
if (this.busy)
|
|
77
|
-
return false;
|
|
78
|
-
this.busy = true;
|
|
79
|
-
this.busySince = Date.now();
|
|
80
|
-
return true;
|
|
81
|
-
}
|
|
82
|
-
release() {
|
|
83
|
-
this.busy = false;
|
|
84
|
-
this.busySince = 0;
|
|
85
|
-
}
|
|
86
|
-
/** Watchdog: reset if stuck for > 10 min */
|
|
87
|
-
checkStuck() {
|
|
88
|
-
if (this.busy && this.busySince > 0 && Date.now() - this.busySince > 10 * 60 * 1000) {
|
|
89
|
-
console.log(`[watchdog] engine stuck for ${Math.round((Date.now() - this.busySince) / 1000)}s, force-resetting`);
|
|
90
|
-
this.release();
|
|
91
|
-
return true;
|
|
92
|
-
}
|
|
93
|
-
return false;
|
|
94
|
-
}
|
|
95
|
-
// ---------------------------------------------------------------------------
|
|
96
70
|
// Unified engine runner
|
|
97
71
|
// ---------------------------------------------------------------------------
|
|
98
|
-
async runEngine(task, allowAll, extraAllowedTools) {
|
|
72
|
+
async runEngine(task, allowAll, extraAllowedTools, signal) {
|
|
99
73
|
const { engine, model, workdir } = this.config;
|
|
100
74
|
if (engine === "raw") {
|
|
101
75
|
return this.runRawEngine(task);
|
|
102
76
|
}
|
|
103
77
|
const cmd = buildEngineCommand(engine, model, allowAll ?? this.config.allowAll, extraAllowedTools);
|
|
104
|
-
return runCommand(cmd.cmd, cmd.args, task, workdir, cmd.stdinMode);
|
|
78
|
+
return runCommand(cmd.cmd, cmd.args, task, workdir, cmd.stdinMode, signal);
|
|
105
79
|
}
|
|
106
80
|
// ---------------------------------------------------------------------------
|
|
107
81
|
// Raw engine: OpenAI-compatible API with tool call loop
|
|
@@ -390,7 +364,7 @@ function buildEngineCommand(engine, model, allowAll, extraAllowedTools) {
|
|
|
390
364
|
return { cmd: engine, args: [], stdinMode: true };
|
|
391
365
|
}
|
|
392
366
|
}
|
|
393
|
-
function runCommand(cmd, args, task, cwd, stdinMode = true) {
|
|
367
|
+
function runCommand(cmd, args, task, cwd, stdinMode = true, signal) {
|
|
394
368
|
return new Promise((resolve, reject) => {
|
|
395
369
|
const { CLAUDECODE, ...cleanEnv } = process.env;
|
|
396
370
|
const finalArgs = stdinMode ? args : [...args, task];
|
|
@@ -399,8 +373,30 @@ function runCommand(cmd, args, task, cwd, stdinMode = true) {
|
|
|
399
373
|
cwd,
|
|
400
374
|
env: cleanEnv,
|
|
401
375
|
stdio: [stdinMode ? "pipe" : "ignore", "pipe", "pipe"],
|
|
402
|
-
timeout: 300_000,
|
|
403
376
|
});
|
|
377
|
+
// Abort → SIGTERM, then SIGKILL after a grace period so a hung engine can't
|
|
378
|
+
// hold the slot past the caller's deadline.
|
|
379
|
+
let aborted = false;
|
|
380
|
+
const onAbort = () => {
|
|
381
|
+
if (aborted)
|
|
382
|
+
return;
|
|
383
|
+
aborted = true;
|
|
384
|
+
console.log(`[${cmd}] aborted, killing pid=${child.pid}`);
|
|
385
|
+
try {
|
|
386
|
+
child.kill("SIGTERM");
|
|
387
|
+
}
|
|
388
|
+
catch { }
|
|
389
|
+
setTimeout(() => { try {
|
|
390
|
+
child.kill("SIGKILL");
|
|
391
|
+
}
|
|
392
|
+
catch { } }, 3000).unref();
|
|
393
|
+
};
|
|
394
|
+
if (signal) {
|
|
395
|
+
if (signal.aborted)
|
|
396
|
+
onAbort();
|
|
397
|
+
else
|
|
398
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
399
|
+
}
|
|
404
400
|
if (stdinMode && child.stdin) {
|
|
405
401
|
child.stdin.on("error", () => { });
|
|
406
402
|
child.stdin.write(task);
|
|
@@ -414,12 +410,17 @@ function runCommand(cmd, args, task, cwd, stdinMode = true) {
|
|
|
414
410
|
child.stderr?.on("data", (chunk) => {
|
|
415
411
|
stderr += chunk.toString();
|
|
416
412
|
});
|
|
417
|
-
child.on("close", (code) => {
|
|
418
|
-
|
|
413
|
+
child.on("close", (code, killSignal) => {
|
|
414
|
+
signal?.removeEventListener("abort", onAbort);
|
|
415
|
+
console.log(`[${cmd}] exit=${code}${killSignal ? ` signal=${killSignal}` : ""} stdout=${stdout.length}b stderr=${stderr.length}b`);
|
|
419
416
|
if (stderr)
|
|
420
417
|
console.log(`[${cmd}] stderr:\n${stderr}`);
|
|
421
418
|
if (stdout)
|
|
422
419
|
console.log(`[${cmd}] stdout:\n${stdout}`);
|
|
420
|
+
if (aborted) {
|
|
421
|
+
reject(new Error(`${cmd} aborted`));
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
423
424
|
const output = stdout.trim();
|
|
424
425
|
if (output) {
|
|
425
426
|
resolve(output);
|
|
@@ -428,6 +429,9 @@ function runCommand(cmd, args, task, cwd, stdinMode = true) {
|
|
|
428
429
|
reject(new Error(`${cmd} exited with code ${code}, no stdout`));
|
|
429
430
|
}
|
|
430
431
|
});
|
|
431
|
-
child.on("error",
|
|
432
|
+
child.on("error", (err) => {
|
|
433
|
+
signal?.removeEventListener("abort", onAbort);
|
|
434
|
+
reject(err);
|
|
435
|
+
});
|
|
432
436
|
});
|
|
433
437
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EngineQueue — priority-aware single-slot scheduler for the local engine.
|
|
3
|
+
*
|
|
4
|
+
* Only one engine subprocess may run at a time. All modules (orders, digestion,
|
|
5
|
+
* reflection, script activities, MCP ad-hoc calls, …) go through this queue so
|
|
6
|
+
* that higher-priority work does not starve behind background busywork.
|
|
7
|
+
*
|
|
8
|
+
* Ordering:
|
|
9
|
+
* - When the slot is free, acquire() returns immediately.
|
|
10
|
+
* - Otherwise callers wait; when the running job releases the slot, the
|
|
11
|
+
* highest-priority waiter wins (FIFO within the same priority).
|
|
12
|
+
* - If the caller's deadline elapses first, acquire() rejects with
|
|
13
|
+
* "Engine busy timeout" and the caller is removed from the queue.
|
|
14
|
+
*
|
|
15
|
+
* Priority semantics (keep in sync with types.ts ComputeRequest.priority):
|
|
16
|
+
* - high — user-waiting work (orders, user tasks, direct MCP calls)
|
|
17
|
+
* - normal — periodic self-maintenance that must not starve (digestion,
|
|
18
|
+
* reflection)
|
|
19
|
+
* - low — background enrichment (platform tasks, script activities,
|
|
20
|
+
* long-term, identity compression)
|
|
21
|
+
*/
|
|
22
|
+
const PRIORITY_RANK = { high: 3, normal: 2, low: 1 };
|
|
23
|
+
export class EngineQueue {
|
|
24
|
+
busy = false;
|
|
25
|
+
busySince = 0;
|
|
26
|
+
waiters = [];
|
|
27
|
+
/** Wait up to `deadlineMs` for the slot, then take it. */
|
|
28
|
+
acquire(priority, deadlineMs) {
|
|
29
|
+
if (!this.busy) {
|
|
30
|
+
this.busy = true;
|
|
31
|
+
this.busySince = Date.now();
|
|
32
|
+
return Promise.resolve();
|
|
33
|
+
}
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const waiter = {
|
|
36
|
+
priority,
|
|
37
|
+
enqueuedAt: Date.now(),
|
|
38
|
+
resolve,
|
|
39
|
+
reject,
|
|
40
|
+
timer: setTimeout(() => {
|
|
41
|
+
const idx = this.waiters.indexOf(waiter);
|
|
42
|
+
if (idx >= 0)
|
|
43
|
+
this.waiters.splice(idx, 1);
|
|
44
|
+
reject(new Error(`Engine busy timeout (${Math.round(deadlineMs / 60000)} min)`));
|
|
45
|
+
}, deadlineMs),
|
|
46
|
+
};
|
|
47
|
+
this.waiters.push(waiter);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/** Release the slot and hand it to the best waiter, if any. */
|
|
51
|
+
release() {
|
|
52
|
+
const next = this.pickNext();
|
|
53
|
+
if (!next) {
|
|
54
|
+
this.busy = false;
|
|
55
|
+
this.busySince = 0;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
this.waiters.splice(this.waiters.indexOf(next), 1);
|
|
59
|
+
clearTimeout(next.timer);
|
|
60
|
+
this.busySince = Date.now();
|
|
61
|
+
next.resolve();
|
|
62
|
+
}
|
|
63
|
+
/** Take the slot synchronously (used by MCP fast-path when !isBusy). */
|
|
64
|
+
tryAcquire() {
|
|
65
|
+
if (this.busy)
|
|
66
|
+
return false;
|
|
67
|
+
this.busy = true;
|
|
68
|
+
this.busySince = Date.now();
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
isBusy() {
|
|
72
|
+
return this.busy;
|
|
73
|
+
}
|
|
74
|
+
queueDepth() {
|
|
75
|
+
return this.waiters.length;
|
|
76
|
+
}
|
|
77
|
+
/** How long has the current holder held the slot, in ms. 0 if free. */
|
|
78
|
+
heldMs() {
|
|
79
|
+
return this.busy && this.busySince ? Date.now() - this.busySince : 0;
|
|
80
|
+
}
|
|
81
|
+
pickNext() {
|
|
82
|
+
if (this.waiters.length === 0)
|
|
83
|
+
return null;
|
|
84
|
+
let best = this.waiters[0];
|
|
85
|
+
for (let i = 1; i < this.waiters.length; i++) {
|
|
86
|
+
const w = this.waiters[i];
|
|
87
|
+
const wr = PRIORITY_RANK[w.priority];
|
|
88
|
+
const br = PRIORITY_RANK[best.priority];
|
|
89
|
+
if (wr > br || (wr === br && w.enqueuedAt < best.enqueuedAt)) {
|
|
90
|
+
best = w;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return best;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -183,7 +183,7 @@ ${discText}`,
|
|
|
183
183
|
- Lower confidence on disproven beliefs
|
|
184
184
|
|
|
185
185
|
Reply ONLY JSON: {"discoveries":[{"capability":"skill or lesson","confidence":0.0-1.0,"evidence":"what supports this"}]}`,
|
|
186
|
-
priority: "
|
|
186
|
+
priority: "normal",
|
|
187
187
|
});
|
|
188
188
|
if (result.success && result.response) {
|
|
189
189
|
const parsed = extractJson(result.response);
|
package/dist/server.js
CHANGED
|
@@ -7,10 +7,15 @@ import { initWorld, initBioState, initGuide, getSelfState, loadRecentCanvasEntri
|
|
|
7
7
|
// V2: module-level instances (set in serve())
|
|
8
8
|
let _engineP = null;
|
|
9
9
|
let _bus = null;
|
|
10
|
-
// Engine mutual exclusion —
|
|
11
|
-
|
|
12
|
-
let engineBusySince = 0;
|
|
10
|
+
// Engine mutual exclusion — priority queue so modules can't starve each other
|
|
11
|
+
const engineQueue = new EngineQueue();
|
|
13
12
|
let lastEngineTrace = [];
|
|
13
|
+
// How long a caller will wait for the engine slot before giving up.
|
|
14
|
+
const ENGINE_WAIT_DEADLINE_MS = 5 * 60 * 1000;
|
|
15
|
+
// Hard cap on a single engine run. Beyond this the subprocess is aborted so
|
|
16
|
+
// the slot is returned to the queue. Shorter than the old 8 min — opencode
|
|
17
|
+
// runs that take longer almost always hang forever.
|
|
18
|
+
const ENGINE_EXEC_TIMEOUT_MS = 3 * 60 * 1000;
|
|
14
19
|
// ---------------------------------------------------------------------------
|
|
15
20
|
// V2 Event helpers — emit signals to EventBus
|
|
16
21
|
// ---------------------------------------------------------------------------
|
|
@@ -74,6 +79,7 @@ function promptOwner(task, isHuman) {
|
|
|
74
79
|
}
|
|
75
80
|
import { RelayPeripheral } from "./relay-peripheral.js";
|
|
76
81
|
import { EnginePeripheral, LLM_ENGINES as LLM_ENGINES_SET } from "./engine-peripheral.js";
|
|
82
|
+
import { EngineQueue } from "./engine-queue.js";
|
|
77
83
|
import { BioStateModule } from "./bio-module.js";
|
|
78
84
|
import { MemoryModule } from "./memory-module.js";
|
|
79
85
|
import { RoleModule } from "./role-module.js";
|
|
@@ -92,11 +98,11 @@ const LLM_ENGINES = LLM_ENGINES_SET;
|
|
|
92
98
|
// Engine execution — delegates to EnginePeripheral (V2 Step 3)
|
|
93
99
|
// ---------------------------------------------------------------------------
|
|
94
100
|
/** Unified engine runner — delegates to EnginePeripheral */
|
|
95
|
-
function runEngine(engine, model, allowAll, task, workdir, extraAllowedTools, relay) {
|
|
101
|
+
function runEngine(engine, model, allowAll, task, workdir, extraAllowedTools, relay, signal) {
|
|
96
102
|
if (!_engineP) {
|
|
97
103
|
throw new Error("Engine peripheral not initialized");
|
|
98
104
|
}
|
|
99
|
-
const result = _engineP.runEngine(task, allowAll, extraAllowedTools);
|
|
105
|
+
const result = _engineP.runEngine(task, allowAll, extraAllowedTools, signal);
|
|
100
106
|
// Sync trace back to module-level for reporting
|
|
101
107
|
result.then(() => { lastEngineTrace = _engineP.lastTrace; }).catch(() => { lastEngineTrace = _engineP.lastTrace; });
|
|
102
108
|
return result;
|
|
@@ -146,8 +152,13 @@ export async function serve(options) {
|
|
|
146
152
|
promptOwner,
|
|
147
153
|
runCollaborativeQuery: (task, selfName, relayHttp, engine, model, allowAll, workdir, relay) => runCollaborativeQuery(task, selfName, relayHttp, engine, model, allowAll, workdir, runEngine, relay),
|
|
148
154
|
autoRoute,
|
|
149
|
-
isEngineBusy: () =>
|
|
150
|
-
setEngineBusy: (busy) => {
|
|
155
|
+
isEngineBusy: () => engineQueue.isBusy(),
|
|
156
|
+
setEngineBusy: (busy) => {
|
|
157
|
+
if (busy)
|
|
158
|
+
engineQueue.tryAcquire();
|
|
159
|
+
else
|
|
160
|
+
engineQueue.release();
|
|
161
|
+
},
|
|
151
162
|
emitTaskCompleted,
|
|
152
163
|
};
|
|
153
164
|
const httpServer = createServer(async (req, res) => {
|
|
@@ -338,45 +349,35 @@ export async function serve(options) {
|
|
|
338
349
|
const bus = new SimpleEventBus();
|
|
339
350
|
// Peripheral registry — Core routes by capability
|
|
340
351
|
const peripherals = [relay, engineP];
|
|
341
|
-
// requestCompute:
|
|
352
|
+
// requestCompute: acquire the engine slot (priority-aware), execute with a
|
|
353
|
+
// hard timeout, and release. The slot release and subprocess kill are both
|
|
354
|
+
// driven by the same AbortController so a stuck engine can't hold the lock.
|
|
342
355
|
async function requestCompute(req) {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
while (engineBusy) {
|
|
346
|
-
// If engine has been busy for >10 min, it's stuck — force release
|
|
347
|
-
if (engineBusySince && Date.now() - engineBusySince > 10 * 60 * 1000) {
|
|
348
|
-
console.log(`[engine] Force-releasing stuck engine lock (busy for ${Math.round((Date.now() - engineBusySince) / 60000)}min)`);
|
|
349
|
-
engineBusy = false;
|
|
350
|
-
engineBusySince = 0;
|
|
351
|
-
break;
|
|
352
|
-
}
|
|
353
|
-
if (Date.now() > deadline) {
|
|
354
|
-
return { success: false, error: "Engine busy timeout (5 min)" };
|
|
355
|
-
}
|
|
356
|
-
await new Promise(r => setTimeout(r, 2000));
|
|
356
|
+
try {
|
|
357
|
+
await engineQueue.acquire(req.priority, ENGINE_WAIT_DEADLINE_MS);
|
|
357
358
|
}
|
|
358
|
-
|
|
359
|
-
|
|
359
|
+
catch (err) {
|
|
360
|
+
return { success: false, error: err.message || "Engine busy timeout" };
|
|
361
|
+
}
|
|
362
|
+
const prompt = req.context
|
|
363
|
+
? `${req.context}\n\n---\n\n${req.question}`
|
|
364
|
+
: req.question;
|
|
365
|
+
const abortController = new AbortController();
|
|
366
|
+
const timer = setTimeout(() => abortController.abort(), ENGINE_EXEC_TIMEOUT_MS);
|
|
360
367
|
try {
|
|
361
|
-
const
|
|
362
|
-
? `${req.context}\n\n---\n\n${req.question}`
|
|
363
|
-
: req.question;
|
|
364
|
-
// Hard timeout: if engine doesn't respond in 8 min, give up
|
|
365
|
-
const engineTimeout = new Promise((_, reject) => setTimeout(() => reject(new Error("Engine execution timeout (8 min)")), 8 * 60 * 1000));
|
|
366
|
-
const response = await Promise.race([
|
|
367
|
-
runEngine(options.engine || "claude", options.model, options.allowAll, prompt, workdir, req.tools, req.relay),
|
|
368
|
-
engineTimeout,
|
|
369
|
-
]);
|
|
370
|
-
// Track token usage via EventBus
|
|
368
|
+
const response = await runEngine(options.engine || "claude", options.model, options.allowAll, prompt, workdir, req.tools, req.relay, abortController.signal);
|
|
371
369
|
emitTokenUsage(prompt.length, response.length);
|
|
372
370
|
return { success: true, response };
|
|
373
371
|
}
|
|
374
372
|
catch (err) {
|
|
375
|
-
|
|
373
|
+
const msg = abortController.signal.aborted
|
|
374
|
+
? `Engine execution timeout (${Math.round(ENGINE_EXEC_TIMEOUT_MS / 60000)} min)`
|
|
375
|
+
: (err.message || String(err));
|
|
376
|
+
return { success: false, error: msg };
|
|
376
377
|
}
|
|
377
378
|
finally {
|
|
378
|
-
|
|
379
|
-
|
|
379
|
+
clearTimeout(timer);
|
|
380
|
+
engineQueue.release();
|
|
380
381
|
}
|
|
381
382
|
}
|
|
382
383
|
const moduleCtx = {
|
|
@@ -505,8 +506,13 @@ export async function serveStdio(agentName, workdir) {
|
|
|
505
506
|
promptOwner,
|
|
506
507
|
runCollaborativeQuery: (task, selfName, relayHttp, engine, model, allowAll, workdir, relay) => runCollaborativeQuery(task, selfName, relayHttp, engine, model, allowAll, workdir, runEngine, relay),
|
|
507
508
|
autoRoute,
|
|
508
|
-
isEngineBusy: () =>
|
|
509
|
-
setEngineBusy: (busy) => {
|
|
509
|
+
isEngineBusy: () => engineQueue.isBusy(),
|
|
510
|
+
setEngineBusy: (busy) => {
|
|
511
|
+
if (busy)
|
|
512
|
+
engineQueue.tryAcquire();
|
|
513
|
+
else
|
|
514
|
+
engineQueue.release();
|
|
515
|
+
},
|
|
510
516
|
emitTaskCompleted,
|
|
511
517
|
};
|
|
512
518
|
const mcpServer = createMcpServer({ workdir: dir, agentName, publisherIds: new Map() }, stdioMcpDeps);
|
package/dist/task-module.js
CHANGED
|
@@ -557,7 +557,7 @@ Complete this task. Use the environment info above and tools (curl, etc.) as nee
|
|
|
557
557
|
const result = await this.ctx.requestCompute({
|
|
558
558
|
context,
|
|
559
559
|
question,
|
|
560
|
-
priority: "
|
|
560
|
+
priority: "low",
|
|
561
561
|
tools: ["Bash(curl *)"],
|
|
562
562
|
relay: this.relayHttp ? { http: this.relayHttp, agentName } : undefined,
|
|
563
563
|
});
|