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.
@@ -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
- console.log(`[${cmd}] exit=${code} stdout=${stdout.length}b stderr=${stderr.length}b`);
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", reject);
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: "low",
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 — module-level state (unified in Step 6)
11
- let engineBusy = false;
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: () => engineBusy,
150
- setEngineBusy: (busy) => { engineBusy = busy; engineBusySince = busy ? Date.now() : 0; },
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: queue for engine, execute, return result
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
- // Wait for engine to become free (poll with backoff, max 5 min)
344
- const deadline = Date.now() + 5 * 60 * 1000;
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
- engineBusy = true;
359
- engineBusySince = Date.now();
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 prompt = req.context
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
- return { success: false, error: err.message || String(err) };
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
- engineBusy = false;
379
- engineBusySince = 0;
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: () => engineBusy,
509
- setEngineBusy: (busy) => { engineBusy = busy; engineBusySince = busy ? Date.now() : 0; },
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);
@@ -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: "normal",
560
+ priority: "low",
561
561
  tools: ["Bash(curl *)"],
562
562
  relay: this.relayHttp ? { http: this.relayHttp, agentName } : undefined,
563
563
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akemon",
3
- "version": "0.2.23",
3
+ "version": "0.2.24",
4
4
  "description": "Agent work marketplace — train your agent, let it work for others",
5
5
  "type": "module",
6
6
  "license": "MIT",