akemon 0.3.5 → 0.3.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.
Files changed (54) hide show
  1. package/DATA_POLICY.md +128 -0
  2. package/README.md +156 -19
  3. package/TRADEMARK.md +74 -0
  4. package/dist/akemon-home.js +56 -0
  5. package/dist/akemon-message.js +107 -0
  6. package/dist/best-effort.js +8 -0
  7. package/dist/cli.js +1411 -132
  8. package/dist/cognitive-artifact-store.js +101 -0
  9. package/dist/cognitive-event-log.js +47 -0
  10. package/dist/config.js +45 -9
  11. package/dist/context.js +27 -6
  12. package/dist/core/contracts/layers.js +1 -0
  13. package/dist/core/contracts/permission.js +1 -0
  14. package/dist/core/contracts/workspace.js +1 -0
  15. package/dist/core-cognitive-module.js +768 -0
  16. package/dist/engine-peripheral.js +127 -26
  17. package/dist/engine-routing.js +58 -17
  18. package/dist/interactive-session.js +361 -0
  19. package/dist/local-interconnect.js +156 -0
  20. package/dist/local-registry.js +178 -0
  21. package/dist/mcp-server.js +4 -1
  22. package/dist/memory-proposal.js +379 -0
  23. package/dist/memory-recorder.js +368 -0
  24. package/dist/orphan-scan.js +36 -24
  25. package/dist/passive-reflection-cognitive-module.js +172 -0
  26. package/dist/peripheral-registry.js +235 -0
  27. package/dist/permission-audit.js +132 -0
  28. package/dist/relay-client.js +68 -9
  29. package/dist/relay-mode.js +34 -0
  30. package/dist/relay-peripheral.js +139 -49
  31. package/dist/runtime-platform.js +122 -0
  32. package/dist/secretariat/client.js +87 -0
  33. package/dist/self.js +15 -6
  34. package/dist/server.js +3695 -439
  35. package/dist/social-discovery.js +231 -0
  36. package/dist/software-agent-peripheral.js +314 -235
  37. package/dist/software-agent-result-cli.js +69 -0
  38. package/dist/software-agent-stream-cli.js +23 -0
  39. package/dist/software-agent-transport.js +177 -0
  40. package/dist/task-module.js +243 -0
  41. package/dist/task-registry.js +756 -0
  42. package/dist/vendor/xterm/addon-fit.js +2 -0
  43. package/dist/vendor/xterm/addon-search.js +2 -0
  44. package/dist/vendor/xterm/addon-web-links.js +2 -0
  45. package/dist/vendor/xterm/xterm.css +285 -0
  46. package/dist/vendor/xterm/xterm.js +2 -0
  47. package/dist/work-memory.js +339 -0
  48. package/dist/workbench-peripheral-guide.js +79 -0
  49. package/dist/workbench-session.js +1074 -0
  50. package/dist/workbench.html +4011 -0
  51. package/package.json +11 -4
  52. package/scripts/build.cjs +24 -0
  53. package/scripts/check-architecture-baseline.cjs +68 -0
  54. package/scripts/test.cjs +38 -0
package/dist/server.js CHANGED
@@ -2,12 +2,48 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
2
2
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
3
3
  import { exec } from "child_process";
4
4
  import { scanAndKillOrphans } from "./orphan-scan.js";
5
+ import { registerLocalInstance, touchLocalInstance, unregisterLocalInstance, } from "./local-registry.js";
6
+ import { appendLocalPeerContact, createLocalAkemonResponseMessage, requireLocalAkemonMessageEnvelope, submitLocalAkemonMessageToMcp, } from "./local-interconnect.js";
7
+ import { runLocalCoreCmChat, } from "./core-cognitive-module.js";
8
+ import { runPassiveReflectionOnce, } from "./passive-reflection-cognitive-module.js";
9
+ import { readCognitiveArtifacts, } from "./cognitive-artifact-store.js";
10
+ import { actorRef, textFromAkemonMessage } from "./akemon-message.js";
11
+ import { appendPermissionAuditRecord } from "./permission-audit.js";
12
+ import { logBestEffortError } from "./best-effort.js";
5
13
  import { timingSafeEqual } from "node:crypto";
6
14
  import { createServer } from "http";
7
15
  import { createInterface } from "readline";
8
- import { mkdir } from "fs/promises";
9
- import { join } from "path";
16
+ import { mkdir, readFile } from "fs/promises";
17
+ import { dirname, join } from "path";
18
+ import { fileURLToPath } from "url";
10
19
  import { initWorld, initBioState, initGuide, getSelfState, loadRecentCanvasEntries, initAgentConfig, loadAgentConfig, loadDirectives, loadTaskHistory, reviveAgent, } from "./self.js";
20
+ import { RelayPeripheral } from "./relay-peripheral.js";
21
+ import { EnginePeripheral, LLM_ENGINES as LLM_ENGINES_SET } from "./engine-peripheral.js";
22
+ import { EngineQueue } from "./engine-queue.js";
23
+ import { BioStateModule } from "./bio-module.js";
24
+ import { MemoryModule } from "./memory-module.js";
25
+ import { RoleModule } from "./role-module.js";
26
+ import { TaskModule } from "./task-module.js";
27
+ import { SocialModule } from "./social-module.js";
28
+ import { LongTermModule } from "./longterm-module.js";
29
+ import { ReflectionModule } from "./reflection-module.js";
30
+ import { ScriptModule } from "./script-module.js";
31
+ import { FileEventLog, PersistentEventBus } from "./event-bus.js";
32
+ import { CodexSoftwareAgentPeripheral, createOwnerTaskEnvelope, listSoftwareAgentContextSessions, listSoftwareAgentTaskRecords, readSoftwareAgentContextSession, readSoftwareAgentTaskRecord, softwareAgentTaskRecordPath, } from "./software-agent-peripheral.js";
33
+ import { normalizeWorkbenchInputRequest, normalizeWorkbenchResizeRequest, normalizeWorkbenchStartRequest, normalizeWorkbenchInputMode, normalizeWorkbenchTool, } from "./workbench-session.js";
34
+ import { executeInteractiveSessionAction, InteractiveSessionManager, InteractiveSessionPeripheral, } from "./interactive-session.js";
35
+ import { buildSoftwareAgentMemorySummary } from "./software-agent-memory.js";
36
+ import { buildWorkMemoryContext, workMemoryDir } from "./work-memory.js";
37
+ import { executeMemoryActionProposalWithResult, } from "./memory-proposal.js";
38
+ import { buildPeripheralExploreBriefing, createRuntimePeripheralRecord, loadPeripheralRecords, mergePeripheralRecords, upsertPeripheralRecords, } from "./peripheral-registry.js";
39
+ import { SIG, sig } from "./types.js";
40
+ import { appendRound, loadConversation, listConversations, buildLLMContext } from "./context.js";
41
+ import { redactSecrets } from "./redaction.js";
42
+ import { appendCognitiveEvent, createCognitiveEventId, } from "./cognitive-event-log.js";
43
+ import { createTaskSurfaceView, listTaskRegistryRecords, readTaskRegistryRecord, upsertTaskRegistryRecord, upsertTaskRegistryRecordFromEvent, } from "./task-registry.js";
44
+ import { createMcpServer, initMcpProxy, createMcpProxyServer } from "./mcp-server.js";
45
+ import { autoRoute, runCollaborativeQuery } from "./agent-utils.js";
46
+ const SERVER_DIR = dirname(fileURLToPath(import.meta.url));
11
47
  // V2: module-level instances (set in serve())
12
48
  let _engineP = null;
13
49
  let _bus = null;
@@ -20,6 +56,7 @@ const ENGINE_WAIT_DEADLINE_MS = 5 * 60 * 1000;
20
56
  // the slot is returned to the queue. Shorter than the old 8 min — opencode
21
57
  // runs that take longer almost always hang forever.
22
58
  const ENGINE_EXEC_TIMEOUT_MS = 3 * 60 * 1000;
59
+ const LOCAL_CHAT_FOLLOW_UP_TARGET_MAX_AGE_MS = 30 * 60 * 1000;
23
60
  // ---------------------------------------------------------------------------
24
61
  // V2 Event helpers — emit signals to EventBus
25
62
  // ---------------------------------------------------------------------------
@@ -37,6 +74,7 @@ function emitTokenUsage(promptLen, resultLen, tokenLimit = 0) {
37
74
  }));
38
75
  }
39
76
  }
77
+ const DEFAULT_SEND_INPUT_WAIT_MS = 600;
40
78
  // V2: TaskModule ref for push notifications
41
79
  let _taskModule = null;
42
80
  export function onOrderNotify(orderId) {
@@ -117,6 +155,68 @@ function requireSoftwareAgent(res, softwareAgent) {
117
155
  writeJsonResponse(res, 503, { error: "Software agent peripheral not ready" });
118
156
  return null;
119
157
  }
158
+ const VENDOR_XTERM_NODE_MODULES = {
159
+ "vendor/xterm/xterm.js": join("@xterm", "xterm", "lib", "xterm.js"),
160
+ "vendor/xterm/xterm.css": join("@xterm", "xterm", "css", "xterm.css"),
161
+ "vendor/xterm/addon-fit.js": join("@xterm", "addon-fit", "lib", "addon-fit.js"),
162
+ "vendor/xterm/addon-web-links.js": join("@xterm", "addon-web-links", "lib", "addon-web-links.js"),
163
+ "vendor/xterm/addon-search.js": join("@xterm", "addon-search", "lib", "addon-search.js"),
164
+ };
165
+ async function readUiAsset(relativePath) {
166
+ const candidates = [
167
+ join(SERVER_DIR, relativePath),
168
+ join(SERVER_DIR, "..", "src", relativePath),
169
+ ];
170
+ const nodeModulesPath = VENDOR_XTERM_NODE_MODULES[relativePath];
171
+ if (nodeModulesPath) {
172
+ candidates.push(join(SERVER_DIR, "..", "node_modules", nodeModulesPath));
173
+ }
174
+ let lastError;
175
+ for (const candidate of candidates) {
176
+ try {
177
+ return await readFile(candidate);
178
+ }
179
+ catch (error) {
180
+ lastError = error;
181
+ }
182
+ }
183
+ throw lastError instanceof Error ? lastError : new Error(`UI asset not found: ${relativePath}`);
184
+ }
185
+ export async function handleLocalUiAssetRoute(req, res) {
186
+ const requestPath = req.url?.split("?")[0] || "";
187
+ const routeMap = {
188
+ "/live": { path: "live.html", contentType: "text/html; charset=utf-8", notFound: "Live page not found" },
189
+ "/live/": { path: "live.html", contentType: "text/html; charset=utf-8", notFound: "Live page not found" },
190
+ "/workbench": { path: "workbench.html", contentType: "text/html; charset=utf-8", notFound: "Workbench page not found" },
191
+ "/workbench/": { path: "workbench.html", contentType: "text/html; charset=utf-8", notFound: "Workbench page not found" },
192
+ "/vendor/xterm/xterm.css": { path: "vendor/xterm/xterm.css", contentType: "text/css; charset=utf-8", notFound: "xterm CSS not found" },
193
+ "/vendor/xterm/xterm.js": { path: "vendor/xterm/xterm.js", contentType: "application/javascript; charset=utf-8", notFound: "xterm JS not found" },
194
+ "/vendor/xterm/addon-fit.js": { path: "vendor/xterm/addon-fit.js", contentType: "application/javascript; charset=utf-8", notFound: "xterm addon-fit not found" },
195
+ "/vendor/xterm/addon-web-links.js": { path: "vendor/xterm/addon-web-links.js", contentType: "application/javascript; charset=utf-8", notFound: "xterm addon-web-links not found" },
196
+ "/vendor/xterm/addon-search.js": { path: "vendor/xterm/addon-search.js", contentType: "application/javascript; charset=utf-8", notFound: "xterm addon-search not found" },
197
+ };
198
+ const route = routeMap[requestPath];
199
+ if (!route || req.method !== "GET")
200
+ return false;
201
+ try {
202
+ const asset = await readUiAsset(route.path);
203
+ res.writeHead(200, {
204
+ "Content-Type": route.contentType,
205
+ "Cache-Control": requestPath.startsWith("/vendor/") ? "public, max-age=3600" : "no-store",
206
+ }).end(asset);
207
+ }
208
+ catch (err) {
209
+ res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" })
210
+ .end(`${route.notFound}: ${err.message || String(err)}`);
211
+ }
212
+ return true;
213
+ }
214
+ function requireWorkbench(res, workbench) {
215
+ if (workbench)
216
+ return workbench;
217
+ writeJsonResponse(res, 503, { error: "Workbench session manager not ready" });
218
+ return null;
219
+ }
120
220
  function readJsonBody(req, maxBytes = 256 * 1024) {
121
221
  return new Promise((resolve, reject) => {
122
222
  const chunks = [];
@@ -163,6 +263,16 @@ async function readOwnerSoftwareAgentEnvelope(req, res, deps) {
163
263
  envelope,
164
264
  request: body,
165
265
  });
266
+ if (readOptionalBooleanBody(body?.includeWorkMemoryContext, "includeWorkMemoryContext")) {
267
+ const workContext = await buildWorkMemoryContext({
268
+ workdir: deps.workdir,
269
+ agentName: deps.agentName,
270
+ purpose: `software-agent task: ${envelope.goal}`,
271
+ budget: readOptionalPositiveIntBody(body?.workMemoryContextBudget, "workMemoryContextBudget"),
272
+ });
273
+ envelope.workMemoryDir = workContext.workMemoryDir;
274
+ envelope.workMemoryContext = workContext.text;
275
+ }
166
276
  return envelope;
167
277
  }
168
278
  catch (err) {
@@ -179,134 +289,2821 @@ export async function handleSoftwareAgentRunHttp(req, res, deps) {
179
289
  const envelope = await readOwnerSoftwareAgentEnvelope(req, res, deps);
180
290
  if (!envelope)
181
291
  return;
292
+ ensureSoftwareAgentEnvelopeTaskId(envelope);
293
+ const taskEventIds = [];
294
+ const taskStartEventId = await appendSoftwareAgentTaskStartEvent({
295
+ agentName: deps.agentName,
296
+ workdir: deps.workdir,
297
+ envelope,
298
+ softwareAgentId: deps.softwareAgent?.id,
299
+ });
300
+ if (taskStartEventId)
301
+ taskEventIds.push(taskStartEventId);
182
302
  try {
183
303
  const result = await softwareAgent.sendTask(envelope);
304
+ await appendSoftwareAgentTaskEndEvents({
305
+ agentName: deps.agentName,
306
+ workdir: deps.workdir,
307
+ envelope,
308
+ result,
309
+ softwareAgentId: deps.softwareAgent?.id,
310
+ sourceEventIds: taskEventIds,
311
+ });
184
312
  writeJsonResponse(res, 200, redactSecrets(result), true);
185
313
  }
186
314
  catch (err) {
315
+ await appendSoftwareAgentTaskFailureEvent({
316
+ agentName: deps.agentName,
317
+ workdir: deps.workdir,
318
+ envelope,
319
+ softwareAgentId: deps.softwareAgent?.id,
320
+ sourceEventIds: taskEventIds,
321
+ error: err.message || String(err),
322
+ });
187
323
  const busy = String(err.message || "").includes("busy");
188
324
  writeJsonResponse(res, busy ? 409 : 500, { error: err.message || String(err) });
189
325
  }
190
326
  }
191
- export async function handleSoftwareAgentRunStreamHttp(req, res, deps) {
327
+ export async function handleLocalAkemonMessageHttp(req, res, deps) {
192
328
  if (!requireOwnerRequest(req, res, deps.options))
193
329
  return;
194
- const softwareAgent = requireSoftwareAgent(res, deps.softwareAgent);
195
- if (!softwareAgent)
196
- return;
197
- const envelope = await readOwnerSoftwareAgentEnvelope(req, res, deps);
198
- if (!envelope)
330
+ let body;
331
+ try {
332
+ body = await readJsonBody(req);
333
+ }
334
+ catch (err) {
335
+ writeJsonResponse(res, 400, { error: err.message || "Invalid request body" });
199
336
  return;
200
- const abortController = new AbortController();
201
- let responseFinished = false;
202
- let streamStarted = false;
203
- res.on("close", () => {
204
- if (!responseFinished)
205
- abortController.abort();
206
- });
207
- const ensureStreamStarted = () => {
208
- if (streamStarted)
337
+ }
338
+ let responseSent = false;
339
+ let acceptedAsyncTask = null;
340
+ try {
341
+ const message = requireLocalAkemonMessageEnvelope(body);
342
+ if (message.target.kind !== "agent" || message.target.id !== deps.agentName) {
343
+ writeJsonResponse(res, 400, {
344
+ error: `Message target ${message.target.kind}:${message.target.id} does not match this Akemon (${deps.agentName})`,
345
+ });
209
346
  return;
210
- streamStarted = true;
211
- res.writeHead(200, {
212
- "Content-Type": "application/x-ndjson; charset=utf-8",
213
- "Cache-Control": "no-cache",
214
- "X-Accel-Buffering": "no",
347
+ }
348
+ const text = textFromAkemonMessage(message);
349
+ const followUpForTaskId = readLocalChatFollowUpTaskId(message);
350
+ const memoryCommand = !followUpForTaskId ? parseMemoryChatCommand({
351
+ agentName: deps.agentName,
352
+ workdir: deps.workdir || process.cwd(),
353
+ text,
354
+ }) : null;
355
+ const reflectionCommand = !followUpForTaskId && !memoryCommand
356
+ ? executeReflectionChatCommand({
357
+ agentName: deps.agentName,
358
+ workdir: deps.workdir || process.cwd(),
359
+ text,
360
+ })
361
+ : null;
362
+ const workbenchCommand = !followUpForTaskId
363
+ && deps.workbench
364
+ && !memoryCommand
365
+ && !reflectionCommand
366
+ ? parseWorkbenchChatCommandAction(text)
367
+ : null;
368
+ const usesCoreCmPath = !followUpForTaskId && !memoryCommand && !reflectionCommand && !workbenchCommand && deps.requestCompute;
369
+ const waitForCompletion = body?.wait !== false && body?.async !== true;
370
+ await appendLocalPeerContact({
371
+ schemaVersion: 1,
372
+ ts: new Date().toISOString(),
373
+ ownerAgent: deps.agentName,
374
+ peerAgent: message.source.id,
375
+ direction: "received",
376
+ messageId: message.id,
377
+ conversationId: message.conversationId,
215
378
  });
216
- res.flushHeaders?.();
217
- };
218
- try {
219
- await softwareAgent.sendTask(envelope, {
220
- signal: abortController.signal,
221
- observer: {
222
- onStart(event) {
223
- ensureStreamStarted();
224
- writeSoftwareAgentStreamEvent(res, {
225
- type: "start",
226
- taskId: event.taskId,
227
- commandLine: event.commandLine,
228
- });
379
+ try {
380
+ await appendPermissionAuditRecord(deps.agentName, {
381
+ actionKind: "akemon-message",
382
+ action: "local-message.receive",
383
+ requestedBy: message.source,
384
+ performedBy: actorRef("agent", deps.agentName, "local"),
385
+ target: message.target,
386
+ riskLevel: message.permissions.riskLevel,
387
+ memoryScope: message.memoryScope,
388
+ transport: "local",
389
+ decision: {
390
+ result: "allowed",
391
+ mode: message.permissions.requiresOwnerApproval === true ? "owner-approved" : "automatic",
392
+ reason: followUpForTaskId
393
+ ? "Owner-only local message queued as a follow-up for an active Akemon task."
394
+ : memoryCommand
395
+ ? "Owner-only local message matched a memory command."
396
+ : reflectionCommand
397
+ ? "Owner-only local message matched a Reflection CM command."
398
+ : workbenchCommand
399
+ ? "Owner-only local message matched a Workbench command."
400
+ : usesCoreCmPath
401
+ ? "Owner-only local message entered the Core CM chat path."
402
+ : message.permissions.requiresOwnerApproval === true
403
+ ? "Message requested owner approval before local task execution."
404
+ : "Message matched this local Akemon and was forwarded through the local MCP task path.",
229
405
  },
230
- onStream(event) {
231
- ensureStreamStarted();
232
- writeSoftwareAgentStreamEvent(res, {
233
- type: event.stream,
234
- taskId: event.taskId,
235
- chunk: event.chunk,
236
- });
406
+ allowedActions: message.permissions.allowedActions,
407
+ forbiddenActions: message.permissions.forbiddenActions,
408
+ references: {
409
+ messageId: message.id,
410
+ conversationId: message.conversationId,
237
411
  },
238
- onEnd(event) {
239
- ensureStreamStarted();
240
- writeSoftwareAgentStreamEvent(res, {
241
- type: "end",
242
- taskId: event.taskId,
243
- exitCode: event.exitCode,
244
- durationMs: event.durationMs,
245
- result: event.result,
412
+ });
413
+ }
414
+ catch (error) {
415
+ logBestEffortError("local message audit append", error);
416
+ }
417
+ if (!waitForCompletion && usesCoreCmPath) {
418
+ const workdir = deps.workdir || process.cwd();
419
+ const taskId = createLocalChatTaskId(message);
420
+ const task = await upsertTaskRegistryRecord({
421
+ workdir,
422
+ agentName: deps.agentName,
423
+ patch: {
424
+ taskId,
425
+ source: "local_chat",
426
+ status: "pending",
427
+ stage: "queued",
428
+ objective: text,
429
+ kind: "chat",
430
+ visibility: "primary",
431
+ route: "core_cm",
432
+ conversationId: message.conversationId,
433
+ refs: [{
434
+ conversationId: message.conversationId,
435
+ messageId: message.id,
436
+ taskId,
437
+ }],
438
+ data: {
439
+ source: "local_chat",
440
+ objective: text,
441
+ ownerTextChars: text.length,
442
+ acceptedAsync: true,
443
+ },
444
+ },
445
+ });
446
+ acceptedAsyncTask = {
447
+ taskId,
448
+ workdir,
449
+ conversationId: message.conversationId,
450
+ messageId: message.id,
451
+ };
452
+ writeJsonResponse(res, 202, {
453
+ success: true,
454
+ accepted: true,
455
+ queued: true,
456
+ async: true,
457
+ wait: false,
458
+ message,
459
+ output: "Akemon accepted this request and will report through the task record.",
460
+ task: createTaskResponseRecord(task),
461
+ taskView: createTaskSurfaceView(task),
462
+ }, true);
463
+ responseSent = true;
464
+ }
465
+ if (followUpForTaskId) {
466
+ await handleLocalChatFollowUpHttp({
467
+ res,
468
+ agentName: deps.agentName,
469
+ workdir: deps.workdir || process.cwd(),
470
+ message,
471
+ taskId: followUpForTaskId,
472
+ text,
473
+ });
474
+ return;
475
+ }
476
+ if (memoryCommand) {
477
+ const workdir = deps.workdir || process.cwd();
478
+ const taskId = createLocalChatTaskId(message);
479
+ const taskEventIds = [];
480
+ const memoryTaskData = {
481
+ source: "local_chat_command",
482
+ objective: text,
483
+ action: "proposal" in memoryCommand ? memoryCommand.proposal.action : "usage",
484
+ };
485
+ await appendLocalTaskStageEvent({
486
+ taskEventIds,
487
+ agentName: deps.agentName,
488
+ workdir,
489
+ conversationId: message.conversationId,
490
+ taskId,
491
+ phase: "received",
492
+ stage: "queued",
493
+ status: "pending",
494
+ route: "memory_cm",
495
+ summary: "Owner memory command queued.",
496
+ refs: [{
497
+ conversationId: message.conversationId,
498
+ messageId: message.id,
499
+ taskId,
500
+ }],
501
+ data: memoryTaskData,
502
+ });
503
+ await appendLocalTaskStageEvent({
504
+ taskEventIds,
505
+ agentName: deps.agentName,
506
+ workdir,
507
+ conversationId: message.conversationId,
508
+ taskId,
509
+ phase: "routed",
510
+ stage: "framing",
511
+ status: "running",
512
+ route: "memory_cm",
513
+ summary: "Akemon routed the owner request to Memory CM.",
514
+ refs: [{
515
+ conversationId: message.conversationId,
516
+ messageId: message.id,
517
+ taskId,
518
+ }],
519
+ data: memoryTaskData,
520
+ });
521
+ await appendLocalTaskStageEvent({
522
+ taskEventIds,
523
+ agentName: deps.agentName,
524
+ workdir,
525
+ conversationId: message.conversationId,
526
+ taskId,
527
+ phase: "delegated",
528
+ stage: "executing",
529
+ status: "running",
530
+ route: "memory_cm",
531
+ summary: "Memory CM is executing the owner command.",
532
+ refs: [{
533
+ conversationId: message.conversationId,
534
+ messageId: message.id,
535
+ taskId,
536
+ }],
537
+ data: memoryTaskData,
538
+ });
539
+ const memoryResult = "proposal" in memoryCommand
540
+ ? await executeMemoryActionProposalWithResult({
541
+ agentName: deps.agentName,
542
+ workdir,
543
+ proposal: memoryCommand.proposal,
544
+ })
545
+ : null;
546
+ const output = memoryResult ? memoryResult.output : ("output" in memoryCommand ? memoryCommand.output : "");
547
+ const response = createLocalAkemonResponseMessage({
548
+ request: message,
549
+ responderAgent: deps.agentName,
550
+ result: output,
551
+ });
552
+ const mainChatEventIds = await appendLocalChatRound({
553
+ agentName: deps.agentName,
554
+ workdir,
555
+ conversationId: message.conversationId,
556
+ ownerMessageId: message.id,
557
+ akemonMessageId: response.id,
558
+ taskId,
559
+ userText: text,
560
+ agentText: output,
561
+ });
562
+ const cmEventIds = [];
563
+ if (memoryResult) {
564
+ const cmEventId = await appendMemoryActionCmEvent({
565
+ agentName: deps.agentName,
566
+ workdir,
567
+ conversationId: message.conversationId,
568
+ ownerMessageId: message.id,
569
+ akemonMessageId: response.id,
570
+ sourceEventIds: [...taskEventIds, ...mainChatEventIds],
571
+ taskId,
572
+ memoryAction: memoryResult,
573
+ });
574
+ if (cmEventId)
575
+ cmEventIds.push(cmEventId);
576
+ }
577
+ await appendLocalTaskStageEvent({
578
+ taskEventIds,
579
+ agentName: deps.agentName,
580
+ workdir,
581
+ conversationId: message.conversationId,
582
+ taskId,
583
+ phase: "reported",
584
+ stage: "reviewing",
585
+ status: "running",
586
+ route: "memory_cm",
587
+ summary: "Akemon is preparing the memory command report.",
588
+ refs: [{
589
+ eventIds: [...taskEventIds, ...mainChatEventIds, ...cmEventIds],
590
+ conversationId: message.conversationId,
591
+ messageId: response.id,
592
+ taskId,
593
+ }],
594
+ data: memoryResult
595
+ ? createMemoryActionTaskData(memoryResult, "local_chat_command")
596
+ : { source: "local_chat_command", action: "usage" },
597
+ });
598
+ const memoryTaskBlocked = !!(memoryResult && ["not_found", "invalid"].includes(memoryResult.status));
599
+ const taskEndEventId = await appendLocalTaskEvent({
600
+ agentName: deps.agentName,
601
+ workdir,
602
+ conversationId: message.conversationId,
603
+ taskId,
604
+ phase: memoryTaskBlocked ? "failed" : "completed",
605
+ stage: memoryTaskBlocked ? "blocked" : "done",
606
+ status: memoryTaskBlocked ? "failed" : "succeeded",
607
+ route: "memory_cm",
608
+ summary: memoryResult
609
+ ? summarizeMemoryActionExecution(memoryResult)
610
+ : "Akemon returned memory command usage guidance.",
611
+ refs: [{
612
+ eventIds: [...taskEventIds, ...mainChatEventIds, ...cmEventIds],
613
+ conversationId: message.conversationId,
614
+ messageId: response.id,
615
+ memoryProposalId: memoryResult?.resolvedProposalId || memoryResult?.proposalId,
616
+ taskId,
617
+ }],
618
+ data: memoryResult
619
+ ? createMemoryActionTaskData(memoryResult, "local_chat_command")
620
+ : { source: "local_chat_command", action: "usage" },
621
+ });
622
+ if (taskEndEventId)
623
+ taskEventIds.push(taskEndEventId);
624
+ const task = await readLocalTaskResponseRecord({ workdir, agentName: deps.agentName, taskId });
625
+ writeJsonResponse(res, 200, {
626
+ success: true,
627
+ message,
628
+ response,
629
+ output,
630
+ task: createOptionalTaskResponseRecord(task),
631
+ taskView: createTaskViewResponse(task),
632
+ eventIds: createLocalChatEventIds({
633
+ mainChat: mainChatEventIds,
634
+ cm: cmEventIds,
635
+ task: taskEventIds,
636
+ }),
637
+ }, true);
638
+ return;
639
+ }
640
+ if (reflectionCommand) {
641
+ const output = await reflectionCommand;
642
+ const response = createLocalAkemonResponseMessage({
643
+ request: message,
644
+ responderAgent: deps.agentName,
645
+ result: output,
646
+ });
647
+ const mainChatEventIds = await appendLocalChatRound({
648
+ agentName: deps.agentName,
649
+ workdir: deps.workdir || process.cwd(),
650
+ conversationId: message.conversationId,
651
+ ownerMessageId: message.id,
652
+ akemonMessageId: response.id,
653
+ userText: text,
654
+ agentText: output,
655
+ });
656
+ writeJsonResponse(res, 200, {
657
+ success: true,
658
+ message,
659
+ response,
660
+ output,
661
+ eventIds: createLocalChatEventIds({ mainChat: mainChatEventIds }),
662
+ }, true);
663
+ return;
664
+ }
665
+ if (workbenchCommand) {
666
+ const workdir = deps.workdir || process.cwd();
667
+ const taskId = createLocalChatTaskId(message);
668
+ const taskEventIds = [];
669
+ const workbenchTaskData = {
670
+ source: "local_chat_command",
671
+ objective: text,
672
+ capability: "action" in workbenchCommand ? workbenchCommand.action.capability : "invalid_command",
673
+ };
674
+ await appendLocalTaskStageEvent({
675
+ taskEventIds,
676
+ agentName: deps.agentName,
677
+ workdir,
678
+ conversationId: message.conversationId,
679
+ taskId,
680
+ phase: "received",
681
+ stage: "queued",
682
+ status: "pending",
683
+ route: "interactive-session",
684
+ summary: "Owner Workbench command queued.",
685
+ refs: [{
686
+ conversationId: message.conversationId,
687
+ messageId: message.id,
688
+ taskId,
689
+ }],
690
+ data: workbenchTaskData,
691
+ });
692
+ await appendLocalTaskStageEvent({
693
+ taskEventIds,
694
+ agentName: deps.agentName,
695
+ workdir,
696
+ conversationId: message.conversationId,
697
+ taskId,
698
+ phase: "delegated",
699
+ stage: "dispatching",
700
+ status: "running",
701
+ route: "interactive-session",
702
+ summary: "action" in workbenchCommand
703
+ ? `Owner asked Akemon to run interactive-session.${workbenchCommand.action.capability}.`
704
+ : "Owner entered an invalid Workbench command.",
705
+ refs: [{
706
+ conversationId: message.conversationId,
707
+ messageId: message.id,
708
+ taskId,
709
+ }],
710
+ data: workbenchTaskData,
711
+ });
712
+ const observation = "action" in workbenchCommand
713
+ ? await executeInteractiveSessionViaPeripheral(deps.workbench, deps.interactiveSessionPeripheral, workbenchCommand.action)
714
+ : createInvalidInteractiveSessionObservation(workbenchCommand.error);
715
+ const peripheralEventId = "action" in workbenchCommand
716
+ ? await appendInteractiveSessionPeripheralEvent({
717
+ agentName: deps.agentName,
718
+ workdir,
719
+ conversationId: message.conversationId,
720
+ ownerMessageId: message.id,
721
+ taskId,
722
+ action: workbenchCommand.action,
723
+ observation,
724
+ })
725
+ : null;
726
+ const peripheralEventIds = peripheralEventId ? [peripheralEventId] : [];
727
+ const output = observation.summary;
728
+ const response = createLocalAkemonResponseMessage({
729
+ request: message,
730
+ responderAgent: deps.agentName,
731
+ result: output,
732
+ });
733
+ const mainChatEventIds = await appendLocalChatRound({
734
+ agentName: deps.agentName,
735
+ workdir: deps.workdir || process.cwd(),
736
+ conversationId: message.conversationId,
737
+ ownerMessageId: message.id,
738
+ akemonMessageId: response.id,
739
+ taskId,
740
+ userText: text,
741
+ agentText: output,
742
+ });
743
+ await appendLocalTaskStageEvent({
744
+ taskEventIds,
745
+ agentName: deps.agentName,
746
+ workdir,
747
+ conversationId: message.conversationId,
748
+ taskId,
749
+ phase: "observed",
750
+ stage: "observing",
751
+ status: "running",
752
+ route: "interactive-session",
753
+ summary: observation.ok
754
+ ? "Akemon observed the interactive-session result."
755
+ : "Akemon observed an interactive-session error.",
756
+ refs: [{
757
+ eventIds: [...taskEventIds, ...peripheralEventIds],
758
+ conversationId: message.conversationId,
759
+ messageId: response.id,
760
+ taskId,
761
+ sessionId: interactiveSessionObservationSessionId(observation, "action" in workbenchCommand ? workbenchCommand.action : undefined),
762
+ }],
763
+ data: createInteractiveSessionTaskData({
764
+ source: "local_chat_command",
765
+ action: "action" in workbenchCommand ? workbenchCommand.action : undefined,
766
+ observation,
767
+ outputChars: output.length,
768
+ }),
769
+ });
770
+ await appendLocalTaskStageEvent({
771
+ taskEventIds,
772
+ agentName: deps.agentName,
773
+ workdir,
774
+ conversationId: message.conversationId,
775
+ taskId,
776
+ phase: "reported",
777
+ stage: "reviewing",
778
+ status: "running",
779
+ route: "interactive-session",
780
+ summary: "Akemon is preparing the interactive-session report.",
781
+ refs: [{
782
+ eventIds: [...taskEventIds, ...mainChatEventIds, ...peripheralEventIds],
783
+ conversationId: message.conversationId,
784
+ messageId: response.id,
785
+ taskId,
786
+ sessionId: interactiveSessionObservationSessionId(observation, "action" in workbenchCommand ? workbenchCommand.action : undefined),
787
+ }],
788
+ data: createInteractiveSessionTaskData({
789
+ source: "local_chat_command",
790
+ action: "action" in workbenchCommand ? workbenchCommand.action : undefined,
791
+ observation,
792
+ outputChars: output.length,
793
+ }),
794
+ });
795
+ const taskEndEventId = await appendLocalTaskEvent({
796
+ agentName: deps.agentName,
797
+ workdir,
798
+ conversationId: message.conversationId,
799
+ taskId,
800
+ phase: observation.ok ? "completed" : "failed",
801
+ stage: observation.ok ? "done" : "blocked",
802
+ status: observation.ok ? "succeeded" : "failed",
803
+ route: "interactive-session",
804
+ summary: summarizeInteractiveSessionTaskCompletion(observation.capability, observation.ok),
805
+ refs: [{
806
+ eventIds: [...taskEventIds, ...mainChatEventIds, ...peripheralEventIds],
807
+ conversationId: message.conversationId,
808
+ messageId: response.id,
809
+ taskId,
810
+ sessionId: interactiveSessionObservationSessionId(observation, "action" in workbenchCommand ? workbenchCommand.action : undefined),
811
+ }],
812
+ data: createInteractiveSessionTaskData({
813
+ source: "local_chat_command",
814
+ action: "action" in workbenchCommand ? workbenchCommand.action : undefined,
815
+ observation,
816
+ outputChars: output.length,
817
+ }),
818
+ });
819
+ if (taskEndEventId)
820
+ taskEventIds.push(taskEndEventId);
821
+ const task = await readLocalTaskResponseRecord({ workdir, agentName: deps.agentName, taskId });
822
+ writeJsonResponse(res, 200, {
823
+ success: true,
824
+ message,
825
+ response,
826
+ output,
827
+ task: createOptionalTaskResponseRecord(task),
828
+ taskView: createTaskViewResponse(task),
829
+ eventIds: createLocalChatEventIds({
830
+ mainChat: mainChatEventIds,
831
+ peripheral: peripheralEventIds,
832
+ task: taskEventIds,
833
+ }),
834
+ }, true);
835
+ return;
836
+ }
837
+ if (deps.requestCompute) {
838
+ const ownerLanguage = (await loadAgentConfig(deps.workdir || process.cwd(), deps.agentName)).owner_language;
839
+ const workdir = deps.workdir || process.cwd();
840
+ const taskId = createLocalChatTaskId(message);
841
+ const taskEventIds = [];
842
+ const coreTaskData = {
843
+ source: "local_chat",
844
+ objective: text,
845
+ ownerTextChars: text.length,
846
+ };
847
+ await appendLocalTaskStageEvent({
848
+ taskEventIds,
849
+ agentName: deps.agentName,
850
+ workdir,
851
+ conversationId: message.conversationId,
852
+ taskId,
853
+ phase: "received",
854
+ stage: "queued",
855
+ status: "pending",
856
+ route: "core_cm",
857
+ summary: "Owner request queued for Core CM.",
858
+ refs: [{
859
+ conversationId: message.conversationId,
860
+ messageId: message.id,
861
+ taskId,
862
+ }],
863
+ data: coreTaskData,
864
+ });
865
+ await appendLocalTaskStageEvent({
866
+ taskEventIds,
867
+ agentName: deps.agentName,
868
+ workdir,
869
+ conversationId: message.conversationId,
870
+ taskId,
871
+ phase: "routed",
872
+ stage: "framing",
873
+ status: "running",
874
+ route: "core_cm",
875
+ summary: "Core CM is framing the owner request.",
876
+ refs: [{
877
+ conversationId: message.conversationId,
878
+ messageId: message.id,
879
+ taskId,
880
+ }],
881
+ data: coreTaskData,
882
+ });
883
+ const peripheralEventIds = [];
884
+ const computeEventIds = [];
885
+ const dispatchCmEventIds = [];
886
+ const dispatchRecordsSeen = [];
887
+ let ownerTextForDispatch = text;
888
+ const requestCoreCmCompute = async (req) => {
889
+ const computeResult = await deps.requestCompute({
890
+ ...req,
891
+ taskId: req.taskId || taskId,
892
+ refs: [
893
+ ...(req.refs || []),
894
+ {
895
+ conversationId: message.conversationId,
896
+ messageId: message.id,
897
+ taskId,
898
+ },
899
+ ],
900
+ });
901
+ if (computeResult.audit?.eventId)
902
+ computeEventIds.push(computeResult.audit.eventId);
903
+ return computeResult;
904
+ };
905
+ const executeCoreCmInteractiveSessionProposal = async (proposal) => {
906
+ const existingTask = await readTaskRegistryRecord({
907
+ workdir,
908
+ agentName: deps.agentName,
909
+ taskId,
910
+ });
911
+ const dispatchRecord = createCoreCmDispatchRecord({
912
+ proposal,
913
+ ownerText: ownerTextForDispatch,
914
+ taskId,
915
+ conversationId: message.conversationId,
916
+ });
917
+ dispatchRecordsSeen.push(dispatchRecord);
918
+ const dispatchRecords = appendTaskDispatchRecord(existingTask, dispatchRecord);
919
+ const dispatchCmEventId = await appendCoreCmDispatchRecordEvent({
920
+ agentName: deps.agentName,
921
+ workdir,
922
+ conversationId: message.conversationId,
923
+ ownerMessageId: message.id,
924
+ taskId,
925
+ dispatchRecord,
926
+ sourceEventIds: taskEventIds,
927
+ });
928
+ if (dispatchCmEventId)
929
+ dispatchCmEventIds.push(dispatchCmEventId);
930
+ await appendLocalTaskStageEvent({
931
+ taskEventIds,
932
+ agentName: deps.agentName,
933
+ workdir,
934
+ conversationId: message.conversationId,
935
+ taskId,
936
+ phase: "delegated",
937
+ stage: "dispatching",
938
+ status: "running",
939
+ route: "core_cm",
940
+ summary: `Core CM dispatched ${proposal.capability} to interactive-session.`,
941
+ refs: [{
942
+ conversationId: message.conversationId,
943
+ messageId: message.id,
944
+ taskId,
945
+ }],
946
+ data: {
947
+ ...coreTaskData,
948
+ peripheral: proposal.peripheral,
949
+ capability: proposal.capability,
950
+ dispatchRecord,
951
+ dispatchRecords,
952
+ dispatchRecordCount: dispatchRecords.length,
953
+ latestDispatchTarget: dispatchRecord.targetPeripheral,
954
+ latestDispatchCapability: dispatchRecord.capability,
955
+ },
956
+ });
957
+ const observation = await executeInteractiveSessionActionProposal(deps.workbench, deps.interactiveSessionPeripheral, proposal);
958
+ const eventId = await appendInteractiveSessionPeripheralEvent({
959
+ agentName: deps.agentName,
960
+ workdir: deps.workdir || process.cwd(),
961
+ conversationId: message.conversationId,
962
+ ownerMessageId: message.id,
963
+ taskId,
964
+ action: proposal,
965
+ observation,
966
+ });
967
+ if (eventId)
968
+ peripheralEventIds.push(eventId);
969
+ await appendLocalTaskStageEvent({
970
+ taskEventIds,
971
+ agentName: deps.agentName,
972
+ workdir,
973
+ conversationId: message.conversationId,
974
+ taskId,
975
+ phase: "observed",
976
+ stage: "observing",
977
+ status: "running",
978
+ route: "core_cm",
979
+ summary: observation.ok
980
+ ? "Core CM observed the peripheral result."
981
+ : "Core CM observed a peripheral error.",
982
+ refs: [{
983
+ eventIds: eventId ? [eventId] : [],
984
+ conversationId: message.conversationId,
985
+ messageId: message.id,
986
+ taskId,
987
+ sessionId: interactiveSessionObservationSessionId(observation, proposal),
988
+ }],
989
+ data: {
990
+ ...coreTaskData,
991
+ peripheral: proposal.peripheral,
992
+ capability: proposal.capability,
993
+ observationOk: observation.ok,
994
+ },
995
+ });
996
+ return observation;
997
+ };
998
+ let result = await runLocalCoreCmChat({
999
+ agentName: deps.agentName,
1000
+ workdir,
1001
+ text,
1002
+ conversationId: message.conversationId,
1003
+ ownerLanguage,
1004
+ interactiveSessions: deps.workbench,
1005
+ requestCompute: requestCoreCmCompute,
1006
+ executeInteractiveSessionActionProposal: executeCoreCmInteractiveSessionProposal,
1007
+ });
1008
+ let absorbedFollowUps = [];
1009
+ const queuedFollowUps = await readQueuedActionableFollowUps({
1010
+ workdir,
1011
+ agentName: deps.agentName,
1012
+ taskId,
1013
+ });
1014
+ if (queuedFollowUps.length) {
1015
+ const followUpText = formatCoreCmQueuedFollowUpText({
1016
+ originalText: text,
1017
+ currentOutput: result.output,
1018
+ followUps: queuedFollowUps,
1019
+ });
1020
+ const previousOwnerTextForDispatch = ownerTextForDispatch;
1021
+ ownerTextForDispatch = followUpText;
1022
+ try {
1023
+ const followUpResult = await runLocalCoreCmChat({
1024
+ agentName: deps.agentName,
1025
+ workdir,
1026
+ text: followUpText,
1027
+ conversationId: message.conversationId,
1028
+ ownerLanguage,
1029
+ interactiveSessions: deps.workbench,
1030
+ requestCompute: requestCoreCmCompute,
1031
+ executeInteractiveSessionActionProposal: executeCoreCmInteractiveSessionProposal,
246
1032
  });
1033
+ result = mergeCoreCmQueuedFollowUpResult({
1034
+ base: result,
1035
+ followUp: followUpResult,
1036
+ followUps: queuedFollowUps,
1037
+ ownerLanguage,
1038
+ });
1039
+ absorbedFollowUps = queuedFollowUps;
1040
+ }
1041
+ finally {
1042
+ ownerTextForDispatch = previousOwnerTextForDispatch;
1043
+ }
1044
+ }
1045
+ const output = result.output;
1046
+ const response = createLocalAkemonResponseMessage({
1047
+ request: message,
1048
+ responderAgent: deps.agentName,
1049
+ result: output,
1050
+ });
1051
+ const mainChatEventIds = await appendLocalChatRound({
1052
+ agentName: deps.agentName,
1053
+ workdir: deps.workdir || process.cwd(),
1054
+ conversationId: message.conversationId,
1055
+ ownerMessageId: message.id,
1056
+ akemonMessageId: response.id,
1057
+ taskId,
1058
+ userText: text,
1059
+ agentText: output,
1060
+ });
1061
+ const cmEventIds = await appendCoreCmResultEvents({
1062
+ agentName: deps.agentName,
1063
+ workdir: deps.workdir || process.cwd(),
1064
+ conversationId: message.conversationId,
1065
+ ownerMessageId: message.id,
1066
+ akemonMessageId: response.id,
1067
+ sourceEventIds: [...dispatchCmEventIds, ...mainChatEventIds, ...peripheralEventIds],
1068
+ taskId,
1069
+ processNotes: result.processNotes,
1070
+ memoryProposal: result.memoryProposal,
1071
+ memoryAction: result.memoryAction,
1072
+ });
1073
+ const existingReviewTask = await readTaskRegistryRecord({
1074
+ workdir,
1075
+ agentName: deps.agentName,
1076
+ taskId,
1077
+ });
1078
+ const followUpTaskData = createAbsorbedFollowUpTaskData(existingReviewTask, absorbedFollowUps);
1079
+ const reviewRecord = createCoreCmReviewRecord({
1080
+ result,
1081
+ output,
1082
+ taskId,
1083
+ conversationId: message.conversationId,
1084
+ dispatchRecord: dispatchRecordsSeen[dispatchRecordsSeen.length - 1],
1085
+ });
1086
+ const reviewRecords = appendTaskReviewRecord(existingReviewTask, reviewRecord);
1087
+ const reviewCmEventId = await appendCoreCmReviewRecordEvent({
1088
+ agentName: deps.agentName,
1089
+ workdir: deps.workdir || process.cwd(),
1090
+ conversationId: message.conversationId,
1091
+ ownerMessageId: message.id,
1092
+ akemonMessageId: response.id,
1093
+ taskId,
1094
+ reviewRecord,
1095
+ sourceEventIds: [...dispatchCmEventIds, ...mainChatEventIds, ...peripheralEventIds, ...cmEventIds, ...computeEventIds],
1096
+ });
1097
+ const reviewCmEventIds = reviewCmEventId ? [reviewCmEventId] : [];
1098
+ await appendLocalTaskStageEvent({
1099
+ taskEventIds,
1100
+ agentName: deps.agentName,
1101
+ workdir,
1102
+ conversationId: message.conversationId,
1103
+ taskId,
1104
+ phase: "reported",
1105
+ stage: "reviewing",
1106
+ status: "running",
1107
+ route: "core_cm",
1108
+ summary: "Core CM is reviewing the result for the owner report.",
1109
+ refs: [{
1110
+ eventIds: [...taskEventIds, ...dispatchCmEventIds, ...mainChatEventIds, ...peripheralEventIds, ...cmEventIds, ...reviewCmEventIds, ...computeEventIds],
1111
+ conversationId: message.conversationId,
1112
+ messageId: response.id,
1113
+ taskId,
1114
+ ...(result.memoryProposal?.id ? { memoryProposalId: result.memoryProposal.id } : {}),
1115
+ }],
1116
+ data: {
1117
+ ...coreTaskData,
1118
+ ...followUpTaskData,
1119
+ outputChars: output.length,
1120
+ peripheralEventCount: peripheralEventIds.length,
1121
+ cmEventCount: dispatchCmEventIds.length + cmEventIds.length + reviewCmEventIds.length,
1122
+ computeEventCount: computeEventIds.length,
1123
+ reviewRecord,
1124
+ reviewRecords,
1125
+ reviewRecordCount: reviewRecords.length,
1126
+ latestReviewQuality: reviewRecord.resultQuality,
1127
+ latestCompletionDecision: reviewRecord.completionDecision,
1128
+ followUpNeeded: reviewRecord.followUpNeeded,
1129
+ memoryProposalRefs: reviewRecord.memoryProposalRefs,
1130
+ reportText: reviewRecord.reportText,
1131
+ ...(result.memoryProposal?.id ? { memoryProposalId: result.memoryProposal.id } : {}),
247
1132
  },
248
- },
1133
+ });
1134
+ const taskEndEventId = await appendLocalTaskEvent({
1135
+ agentName: deps.agentName,
1136
+ workdir,
1137
+ conversationId: message.conversationId,
1138
+ taskId,
1139
+ phase: "completed",
1140
+ stage: "done",
1141
+ status: "succeeded",
1142
+ route: "core_cm",
1143
+ summary: summarizeCoreCmTaskOutcome({
1144
+ peripheralEventIds,
1145
+ cmEventIds,
1146
+ memoryProposal: result.memoryProposal,
1147
+ memoryAction: result.memoryAction,
1148
+ }),
1149
+ refs: [{
1150
+ eventIds: [...taskEventIds, ...dispatchCmEventIds, ...mainChatEventIds, ...peripheralEventIds, ...cmEventIds, ...reviewCmEventIds, ...computeEventIds],
1151
+ conversationId: message.conversationId,
1152
+ messageId: response.id,
1153
+ taskId,
1154
+ ...(result.memoryProposal?.id ? { memoryProposalId: result.memoryProposal.id } : {}),
1155
+ }],
1156
+ data: {
1157
+ outputChars: output.length,
1158
+ peripheralEventCount: peripheralEventIds.length,
1159
+ cmEventCount: dispatchCmEventIds.length + cmEventIds.length + reviewCmEventIds.length,
1160
+ computeEventCount: computeEventIds.length,
1161
+ latestReviewQuality: reviewRecord.resultQuality,
1162
+ latestCompletionDecision: reviewRecord.completionDecision,
1163
+ followUpNeeded: reviewRecord.followUpNeeded,
1164
+ ...(result.memoryProposal?.id ? { memoryProposalId: result.memoryProposal.id } : {}),
1165
+ ...(result.memoryAction ? { memoryAction: createMemoryActionTaskData(result.memoryAction, "core_cm") } : {}),
1166
+ },
1167
+ });
1168
+ if (taskEndEventId)
1169
+ taskEventIds.push(taskEndEventId);
1170
+ const task = await readLocalTaskResponseRecord({ workdir, agentName: deps.agentName, taskId });
1171
+ const eventIds = createLocalChatEventIds({
1172
+ mainChat: mainChatEventIds,
1173
+ peripheral: peripheralEventIds,
1174
+ cm: [...dispatchCmEventIds, ...cmEventIds, ...reviewCmEventIds],
1175
+ compute: computeEventIds,
1176
+ task: taskEventIds,
1177
+ });
1178
+ const cmItems = createCoreCmProcessItems(result.processNotes, eventIds);
1179
+ if (!responseSent) {
1180
+ writeJsonResponse(res, 200, {
1181
+ success: true,
1182
+ message,
1183
+ response,
1184
+ output,
1185
+ task: createOptionalTaskResponseRecord(task),
1186
+ taskView: createTaskViewResponse(task),
1187
+ memoryProposal: result.memoryProposal || null,
1188
+ eventIds,
1189
+ cmItems,
1190
+ }, true);
1191
+ }
1192
+ if (deps.passiveReflection) {
1193
+ const passiveReflectionEventIds = createLocalChatEventIds({
1194
+ mainChat: mainChatEventIds,
1195
+ peripheral: peripheralEventIds,
1196
+ cm: result.memoryProposal || result.memoryAction ? cmEventIds : [],
1197
+ compute: computeEventIds,
1198
+ task: taskEventIds,
1199
+ });
1200
+ schedulePassiveReflection({
1201
+ agentName: deps.agentName,
1202
+ workdir: deps.workdir || process.cwd(),
1203
+ conversationId: message.conversationId,
1204
+ eventIds: passiveReflectionEventIds,
1205
+ requestCompute: deps.requestCompute,
1206
+ });
1207
+ }
1208
+ return;
1209
+ }
1210
+ const output = await submitLocalAkemonMessageToMcp({
1211
+ localPort: deps.localPort,
1212
+ message,
1213
+ });
1214
+ const response = createLocalAkemonResponseMessage({
1215
+ request: message,
1216
+ responderAgent: deps.agentName,
1217
+ result: output,
249
1218
  });
1219
+ const mainChatEventIds = await appendLocalChatRound({
1220
+ agentName: deps.agentName,
1221
+ workdir: deps.workdir || process.cwd(),
1222
+ conversationId: message.conversationId,
1223
+ ownerMessageId: message.id,
1224
+ akemonMessageId: response.id,
1225
+ userText: text,
1226
+ agentText: output,
1227
+ });
1228
+ writeJsonResponse(res, 200, {
1229
+ success: true,
1230
+ message,
1231
+ response,
1232
+ output,
1233
+ eventIds: createLocalChatEventIds({ mainChat: mainChatEventIds }),
1234
+ }, true);
250
1235
  }
251
1236
  catch (err) {
252
- if (!streamStarted) {
253
- const busy = String(err.message || "").includes("busy");
254
- writeJsonResponse(res, busy ? 409 : 500, { error: err.message || String(err) });
255
- responseFinished = true;
1237
+ if (responseSent && acceptedAsyncTask) {
1238
+ await appendLocalTaskEvent({
1239
+ agentName: deps.agentName,
1240
+ workdir: acceptedAsyncTask.workdir,
1241
+ conversationId: acceptedAsyncTask.conversationId,
1242
+ taskId: acceptedAsyncTask.taskId,
1243
+ phase: "failed",
1244
+ stage: "blocked",
1245
+ status: "failed",
1246
+ route: "core_cm",
1247
+ summary: err.message || "Core CM task failed after async acceptance.",
1248
+ refs: [{
1249
+ conversationId: acceptedAsyncTask.conversationId,
1250
+ messageId: acceptedAsyncTask.messageId,
1251
+ taskId: acceptedAsyncTask.taskId,
1252
+ }],
1253
+ data: {
1254
+ source: "local_chat",
1255
+ acceptedAsync: true,
1256
+ error: err.message || String(err),
1257
+ },
1258
+ });
256
1259
  return;
257
1260
  }
258
- writeSoftwareAgentStreamEvent(res, {
259
- type: "error",
260
- error: err.message || String(err),
1261
+ writeJsonResponse(res, 400, { error: err.message || "Invalid local Akemon message" });
1262
+ }
1263
+ }
1264
+ async function appendLocalChatRound(input) {
1265
+ const eventIds = [];
1266
+ if (!input.conversationId)
1267
+ return eventIds;
1268
+ try {
1269
+ await appendRound(input.workdir, input.agentName, input.conversationId, input.userText, input.agentText);
1270
+ }
1271
+ catch (error) {
1272
+ logBestEffortError("local chat conversation append", error);
1273
+ }
1274
+ try {
1275
+ const ownerEvent = await appendCognitiveEvent({
1276
+ workdir: input.workdir,
1277
+ agentName: input.agentName,
1278
+ event: {
1279
+ stream: "main_chat",
1280
+ taskId: input.taskId,
1281
+ conversationId: input.conversationId,
1282
+ role: "owner",
1283
+ text: input.userText,
1284
+ messageId: input.ownerMessageId,
1285
+ refs: [
1286
+ {
1287
+ conversationId: input.conversationId,
1288
+ messageId: input.ownerMessageId,
1289
+ taskId: input.taskId,
1290
+ },
1291
+ ],
1292
+ },
1293
+ });
1294
+ eventIds.push(ownerEvent.id);
1295
+ const akemonEvent = await appendCognitiveEvent({
1296
+ workdir: input.workdir,
1297
+ agentName: input.agentName,
1298
+ event: {
1299
+ stream: "main_chat",
1300
+ taskId: input.taskId,
1301
+ conversationId: input.conversationId,
1302
+ role: "akemon",
1303
+ text: input.agentText,
1304
+ messageId: input.akemonMessageId,
1305
+ refs: [
1306
+ {
1307
+ conversationId: input.conversationId,
1308
+ messageId: input.akemonMessageId,
1309
+ taskId: input.taskId,
1310
+ },
1311
+ ],
1312
+ },
261
1313
  });
1314
+ eventIds.push(akemonEvent.id);
262
1315
  }
263
- finally {
264
- responseFinished = true;
265
- if (streamStarted && !res.writableEnded)
266
- res.end();
1316
+ catch (error) {
1317
+ logBestEffortError("local chat event append", error);
267
1318
  }
1319
+ return eventIds;
268
1320
  }
269
- export async function handleSoftwareAgentStatusHttp(req, res, deps) {
270
- if (!requireOwnerRequest(req, res, deps.options))
271
- return;
272
- const softwareAgent = requireSoftwareAgent(res, deps.softwareAgent);
273
- if (!softwareAgent)
274
- return;
275
- writeJsonResponse(res, 200, softwareAgent.getState(), true);
1321
+ function createLocalChatEventIds(input) {
1322
+ return {
1323
+ mainChat: input.mainChat || [],
1324
+ peripheral: input.peripheral || [],
1325
+ cm: input.cm || [],
1326
+ compute: input.compute || [],
1327
+ task: input.task || [],
1328
+ };
276
1329
  }
277
- export async function handleSoftwareAgentTasksHttp(req, res, deps) {
278
- if (!requireOwnerRequest(req, res, deps.options))
279
- return;
280
- const url = new URL(req.url || "/", "http://127.0.0.1");
281
- const basePath = "/self/software-agent/tasks";
282
- const taskLedgerDir = softwareAgentTaskLedgerDir(deps.workdir, deps.agentName);
283
- if (url.pathname === basePath) {
284
- const limit = readPositiveIntQuery(url.searchParams.get("limit"), 20, 100);
285
- const tasks = listSoftwareAgentTaskRecords(taskLedgerDir, limit);
286
- writeJsonResponse(res, 200, { tasks }, true);
1330
+ function createCoreCmProcessItems(processNotes, eventIds) {
1331
+ return (processNotes || [])
1332
+ .map((note) => (typeof note === "string" ? note.trim() : ""))
1333
+ .filter(Boolean)
1334
+ .map((text) => ({
1335
+ type: "process",
1336
+ title: "Core CM",
1337
+ text,
1338
+ status: "info",
1339
+ eventIds,
1340
+ }));
1341
+ }
1342
+ function createLocalChatTaskId(message) {
1343
+ const suffix = message.id || `${message.conversationId || "local"}_${Date.now()}`;
1344
+ return `task_${suffix.replace(/[^a-zA-Z0-9_.:-]/g, "_")}`;
1345
+ }
1346
+ function readLocalChatFollowUpTaskId(message) {
1347
+ const value = message.metadata?.followUpForTaskId;
1348
+ if (typeof value !== "string")
1349
+ return null;
1350
+ const taskId = value.trim();
1351
+ if (!taskId || taskId.includes("/") || taskId.length > 220)
1352
+ return null;
1353
+ return taskId;
1354
+ }
1355
+ async function handleLocalChatFollowUpHttp(input) {
1356
+ const existing = await readTaskRegistryRecord({
1357
+ workdir: input.workdir,
1358
+ agentName: input.agentName,
1359
+ taskId: input.taskId,
1360
+ });
1361
+ if (existing && (existing.stage === "done" || existing.stage === "blocked")) {
1362
+ writeJsonResponse(input.res, 409, { error: `Task ${input.taskId} is already ${existing.stage}` });
287
1363
  return;
288
1364
  }
289
- if (url.pathname.startsWith(`${basePath}/`)) {
290
- const taskId = decodeURIComponent(url.pathname.slice(basePath.length + 1));
291
- if (!taskId || taskId.includes("/")) {
292
- writeJsonResponse(res, 400, { error: "Invalid software-agent task id" });
293
- return;
294
- }
295
- const task = readSoftwareAgentTaskRecord(taskLedgerDir, taskId);
296
- if (!task) {
297
- writeJsonResponse(res, 404, { error: "Software-agent task not found" });
298
- return;
299
- }
300
- writeJsonResponse(res, 200, { task }, true);
1365
+ if (existing && isExpiredLocalChatFollowUpTarget(existing)) {
1366
+ await appendLocalTaskEvent({
1367
+ agentName: input.agentName,
1368
+ workdir: input.workdir,
1369
+ conversationId: existing.conversationId || input.message.conversationId,
1370
+ taskId: input.taskId,
1371
+ phase: "failed",
1372
+ stage: "blocked",
1373
+ status: "failed",
1374
+ route: existing.route || "core_cm",
1375
+ summary: "Akemon stopped recovering a stale local chat task.",
1376
+ refs: [{
1377
+ conversationId: input.message.conversationId,
1378
+ messageId: input.message.id,
1379
+ taskId: input.taskId,
1380
+ }],
1381
+ data: {
1382
+ source: "local_chat_stale_recovery",
1383
+ staleTaskCreatedAt: existing.createdAt,
1384
+ staleTaskUpdatedAt: existing.updatedAt,
1385
+ },
1386
+ });
1387
+ writeJsonResponse(input.res, 409, {
1388
+ error: `Task ${input.taskId} is stale and can no longer accept follow-ups. Send a new message instead.`,
1389
+ });
301
1390
  return;
302
1391
  }
303
- writeJsonResponse(res, 404, { error: "Software-agent task endpoint not found" });
1392
+ const followUp = createLocalChatFollowUp(input.text);
1393
+ const followUps = appendTaskFollowUp(existing, followUp);
1394
+ const output = formatLocalChatFollowUpAck(input.text, followUp.kind);
1395
+ const response = createLocalAkemonResponseMessage({
1396
+ request: input.message,
1397
+ responderAgent: input.agentName,
1398
+ result: output,
1399
+ });
1400
+ const taskEventIds = [];
1401
+ const mainChatEventIds = await appendLocalChatRound({
1402
+ agentName: input.agentName,
1403
+ workdir: input.workdir,
1404
+ conversationId: input.message.conversationId,
1405
+ ownerMessageId: input.message.id,
1406
+ akemonMessageId: response.id,
1407
+ taskId: input.taskId,
1408
+ userText: input.text,
1409
+ agentText: output,
1410
+ });
1411
+ await appendLocalTaskStageEvent({
1412
+ taskEventIds,
1413
+ agentName: input.agentName,
1414
+ workdir: input.workdir,
1415
+ conversationId: input.message.conversationId,
1416
+ taskId: input.taskId,
1417
+ phase: "received",
1418
+ stage: existing?.stage || "queued",
1419
+ status: existing?.status && existing.status !== "succeeded" && existing.status !== "failed"
1420
+ ? existing.status
1421
+ : "running",
1422
+ route: existing?.route || "core_cm",
1423
+ summary: `Owner ${followUp.kind} follow-up queued.`,
1424
+ refs: [{
1425
+ eventIds: mainChatEventIds,
1426
+ conversationId: input.message.conversationId,
1427
+ messageId: input.message.id,
1428
+ taskId: input.taskId,
1429
+ }],
1430
+ data: {
1431
+ source: "local_chat_follow_up",
1432
+ followUp,
1433
+ followUps,
1434
+ followUpCount: followUps.length,
1435
+ latestFollowUpKind: followUp.kind,
1436
+ latestFollowUpText: followUp.text,
1437
+ },
1438
+ });
1439
+ const task = await readLocalTaskResponseRecord({
1440
+ workdir: input.workdir,
1441
+ agentName: input.agentName,
1442
+ taskId: input.taskId,
1443
+ });
1444
+ writeJsonResponse(input.res, 200, {
1445
+ success: true,
1446
+ queued: true,
1447
+ message: input.message,
1448
+ response,
1449
+ output,
1450
+ followUp,
1451
+ task: createOptionalTaskResponseRecord(task),
1452
+ taskView: createTaskViewResponse(task),
1453
+ eventIds: createLocalChatEventIds({
1454
+ mainChat: mainChatEventIds,
1455
+ task: taskEventIds,
1456
+ }),
1457
+ }, true);
304
1458
  }
305
- function softwareAgentTaskLedgerDir(workdir, agentName) {
306
- return join(workdir, ".akemon", "agents", agentName, "software-agent", "tasks");
1459
+ function isExpiredLocalChatFollowUpTarget(task) {
1460
+ if (task.route !== "core_cm")
1461
+ return false;
1462
+ if (task.stage === "done" || task.stage === "blocked")
1463
+ return false;
1464
+ if (task.status === "succeeded" || task.status === "failed" || task.status === "info")
1465
+ return false;
1466
+ const createdAt = Date.parse(task.createdAt || "");
1467
+ if (!Number.isFinite(createdAt))
1468
+ return false;
1469
+ return Date.now() - createdAt > LOCAL_CHAT_FOLLOW_UP_TARGET_MAX_AGE_MS;
307
1470
  }
308
- function softwareAgentContextSessionDir(workdir, agentName) {
309
- return join(workdir, ".akemon", "agents", agentName, "software-agent", "sessions");
1471
+ function createLocalChatFollowUp(text) {
1472
+ return {
1473
+ id: createCognitiveEventId("followup"),
1474
+ kind: classifyLocalChatFollowUp(text),
1475
+ text: truncateEventText(text.trim(), 1000),
1476
+ createdAt: new Date().toISOString(),
1477
+ status: "queued",
1478
+ };
1479
+ }
1480
+ function appendTaskFollowUp(existing, followUp) {
1481
+ const current = Array.isArray(existing?.data?.followUps)
1482
+ ? existing.data.followUps
1483
+ .map(normalizeTaskFollowUp)
1484
+ .filter((item) => !!item)
1485
+ : [];
1486
+ return [...current, followUp].slice(-20);
1487
+ }
1488
+ function normalizeTaskFollowUp(value) {
1489
+ if (!value || typeof value !== "object")
1490
+ return null;
1491
+ const item = value;
1492
+ const text = typeof item.text === "string" ? item.text.trim() : "";
1493
+ if (!text)
1494
+ return null;
1495
+ return {
1496
+ id: typeof item.id === "string" && item.id ? item.id : createCognitiveEventId("followup"),
1497
+ kind: isLocalChatFollowUpKind(item.kind) ? item.kind : "note",
1498
+ text: truncateEventText(text, 1000),
1499
+ createdAt: typeof item.createdAt === "string" && item.createdAt ? item.createdAt : new Date().toISOString(),
1500
+ status: isLocalChatFollowUpStatus(item.status) ? item.status : "queued",
1501
+ absorbedAt: typeof item.absorbedAt === "string" && item.absorbedAt ? item.absorbedAt : undefined,
1502
+ };
1503
+ }
1504
+ function classifyLocalChatFollowUp(text) {
1505
+ const normalized = text.trim().toLowerCase();
1506
+ if (/^(correction|actually|instead|wait|no,|not that)\b/.test(normalized)
1507
+ || /^(不是|不对|等等|等下|更正|纠正|改成)/.test(text.trim())) {
1508
+ return "correction";
1509
+ }
1510
+ if (/(constraint|requirement|must|must not|do not|don't|only|never)\b/.test(normalized)
1511
+ || /(不要|不能|必须|只能|不允许|限制|约束)/.test(text)) {
1512
+ return "constraint";
1513
+ }
1514
+ if (/[??]\s*$/.test(text)
1515
+ || /^(why|how|what|when|where|can|could|should|will|is|are)\b/.test(normalized)
1516
+ || /^(为什么|怎么|如何|是否|能不能|可以|会不会)/.test(text.trim())) {
1517
+ return "question";
1518
+ }
1519
+ return "note";
1520
+ }
1521
+ function isLocalChatFollowUpKind(value) {
1522
+ return value === "correction" || value === "constraint" || value === "question" || value === "note";
1523
+ }
1524
+ function isLocalChatFollowUpStatus(value) {
1525
+ return value === "queued" || value === "absorbed";
1526
+ }
1527
+ function formatLocalChatFollowUpAck(text, kind) {
1528
+ if (shouldUseChineseOwnerText(undefined, text)) {
1529
+ const labels = {
1530
+ correction: "修正",
1531
+ constraint: "约束",
1532
+ question: "问题",
1533
+ note: "补充",
1534
+ };
1535
+ return `已把这条${labels[kind]}加入当前任务队列。`;
1536
+ }
1537
+ return `Queued this ${kind} for the current task.`;
1538
+ }
1539
+ async function readQueuedActionableFollowUps(input) {
1540
+ const task = await readTaskRegistryRecord(input);
1541
+ if (!Array.isArray(task?.data?.followUps))
1542
+ return [];
1543
+ return task.data.followUps
1544
+ .map(normalizeTaskFollowUp)
1545
+ .filter((item) => !!item
1546
+ && item.status !== "absorbed"
1547
+ && isActionableQueuedFollowUpKind(item.kind));
1548
+ }
1549
+ function createAbsorbedFollowUpTaskData(existing, absorbedFollowUps) {
1550
+ const absorbedIds = new Set(absorbedFollowUps.map((followUp) => followUp.id).filter(Boolean));
1551
+ if (!absorbedIds.size)
1552
+ return {};
1553
+ const now = new Date().toISOString();
1554
+ const currentFollowUps = Array.isArray(existing?.data?.followUps)
1555
+ ? existing.data.followUps
1556
+ .map(normalizeTaskFollowUp)
1557
+ .filter((item) => !!item)
1558
+ : absorbedFollowUps;
1559
+ const followUps = currentFollowUps.map((followUp) => absorbedIds.has(followUp.id)
1560
+ ? {
1561
+ ...followUp,
1562
+ status: "absorbed",
1563
+ absorbedAt: followUp.absorbedAt || now,
1564
+ }
1565
+ : followUp);
1566
+ const absorbedFollowUpIds = followUps
1567
+ .filter((followUp) => followUp.status === "absorbed")
1568
+ .map((followUp) => followUp.id);
1569
+ return {
1570
+ followUps,
1571
+ followUpCount: followUps.length,
1572
+ pendingFollowUpCount: followUps.filter((followUp) => followUp.status !== "absorbed").length,
1573
+ absorbedFollowUpCount: absorbedFollowUpIds.length,
1574
+ absorbedFollowUpIds,
1575
+ };
1576
+ }
1577
+ function isActionableQueuedFollowUpKind(kind) {
1578
+ return kind === "question" || kind === "constraint" || kind === "correction";
1579
+ }
1580
+ function formatCoreCmQueuedFollowUpText(input) {
1581
+ const lines = [
1582
+ "Queued owner follow-up(s) arrived while Akemon was handling the current task.",
1583
+ "",
1584
+ "Original owner request:",
1585
+ input.originalText,
1586
+ "",
1587
+ "Current Akemon result for the original request:",
1588
+ input.currentOutput || "(no result yet)",
1589
+ "",
1590
+ "Queued owner follow-up(s) to incorporate now:",
1591
+ ...input.followUps.map((followUp, index) => `${index + 1}. [${followUp.kind}] ${followUp.text}`),
1592
+ "",
1593
+ "Before completing the task, produce the final owner-facing report.",
1594
+ "If a follow-up is a constraint, apply it when possible and state whether it changed the final answer or next action.",
1595
+ "If a follow-up is a correction, revise the final answer when possible and state whether the earlier result needed correction.",
1596
+ "If a follow-up is a question, answer it directly using the current result and available context.",
1597
+ "If the task already performed an action that cannot be undone, say that clearly and propose the safest next step.",
1598
+ ];
1599
+ return lines.join("\n");
1600
+ }
1601
+ function mergeCoreCmQueuedFollowUpResult(input) {
1602
+ return {
1603
+ output: mergeCoreCmQueuedFollowUpOutput(input),
1604
+ processNotes: [
1605
+ ...(input.base.processNotes || []),
1606
+ ...(input.followUp.processNotes || []),
1607
+ ],
1608
+ memoryProposal: input.followUp.memoryProposal || input.base.memoryProposal,
1609
+ memoryAction: input.followUp.memoryAction || input.base.memoryAction,
1610
+ review: input.followUp.review || input.base.review,
1611
+ };
1612
+ }
1613
+ function mergeCoreCmQueuedFollowUpOutput(input) {
1614
+ const baseOutput = input.base.output.trim();
1615
+ const followUpOutput = input.followUp.output.trim();
1616
+ if (!baseOutput)
1617
+ return followUpOutput;
1618
+ if (!followUpOutput)
1619
+ return baseOutput;
1620
+ const ownerText = input.followUps.map((followUp) => followUp.text).join("\n");
1621
+ const includesQuestion = input.followUps.some((followUp) => followUp.kind === "question");
1622
+ const includesRevision = input.followUps.some((followUp) => followUp.kind === "constraint" || followUp.kind === "correction");
1623
+ if (includesQuestion && includesRevision) {
1624
+ const mixedLabel = shouldUseChineseOwnerText(input.ownerLanguage, ownerText)
1625
+ ? "关于你排队的后续问题、约束和修正:"
1626
+ : "Queued follow-up answer with applied correction/constraint:";
1627
+ return `${baseOutput}\n\n${mixedLabel}\n${followUpOutput}`;
1628
+ }
1629
+ if (includesRevision) {
1630
+ const revisionLabel = shouldUseChineseOwnerText(input.ownerLanguage, ownerText)
1631
+ ? "已根据你排队的修正/约束更新最终报告:"
1632
+ : "Updated final report after queued correction/constraint:";
1633
+ return `${revisionLabel}\n${followUpOutput}`;
1634
+ }
1635
+ const label = shouldUseChineseOwnerText(input.ownerLanguage, ownerText)
1636
+ ? "关于你排队的后续问题:"
1637
+ : "Queued follow-up answer:";
1638
+ return `${baseOutput}\n\n${label}\n${followUpOutput}`;
1639
+ }
1640
+ function shouldUseChineseOwnerText(_ownerLanguage, ownerText) {
1641
+ return /[\u3400-\u9fff]/.test(ownerText || "");
1642
+ }
1643
+ function createCoreCmDispatchRecord(input) {
1644
+ return {
1645
+ id: createCognitiveEventId("dispatch"),
1646
+ createdAt: new Date().toISOString(),
1647
+ taskId: input.taskId,
1648
+ conversationId: input.conversationId,
1649
+ targetPeripheral: input.proposal.peripheral,
1650
+ capability: input.proposal.capability,
1651
+ brief: coreCmDispatchBrief(input.proposal, input.ownerText),
1652
+ contextBoundary: {
1653
+ includes: [
1654
+ "owner request",
1655
+ "current interactive-session context",
1656
+ "relevant Akemon memory summary",
1657
+ ],
1658
+ excludes: [
1659
+ "unbounded owner context",
1660
+ "direct Akemon self-memory writes",
1661
+ "peripheral-owned private state outside the requested capability",
1662
+ ],
1663
+ },
1664
+ permissionBoundary: {
1665
+ allowed: [`interactive-session.${input.proposal.capability}`],
1666
+ forbidden: [
1667
+ "memory writes without a Memory CM proposal",
1668
+ "actions outside the selected interactive-session capability",
1669
+ ],
1670
+ requiresOwnerApproval: false,
1671
+ },
1672
+ expectedDeliverable: expectedInteractiveSessionDeliverable(input.proposal.capability),
1673
+ reason: input.proposal.reason,
1674
+ argsSummary: summarizeInteractiveSessionDispatchArgs(input.proposal),
1675
+ };
1676
+ }
1677
+ function coreCmDispatchBrief(proposal, ownerText) {
1678
+ if (proposal.reason?.trim())
1679
+ return truncateEventText(proposal.reason.trim(), 260);
1680
+ const target = proposal.peripheral === "workbench" ? "interactive-session" : proposal.peripheral;
1681
+ const ownerBrief = truncateEventText(ownerText.trim(), 140);
1682
+ return ownerBrief
1683
+ ? `Use ${target}.${proposal.capability} for: ${ownerBrief}`
1684
+ : `Use ${target}.${proposal.capability}.`;
1685
+ }
1686
+ function expectedInteractiveSessionDeliverable(capability) {
1687
+ switch (capability) {
1688
+ case "list_sessions":
1689
+ return "List available interactive sessions and their owner-relevant state.";
1690
+ case "inspect_session":
1691
+ case "capture_output":
1692
+ return "Return session status and recent output relevant to the owner request.";
1693
+ case "start_session":
1694
+ return "Return started session metadata and how the owner can observe or control it.";
1695
+ case "send_input":
1696
+ return "Return input delivery status and any observed output after the requested wait.";
1697
+ case "set_input_mode":
1698
+ return "Return confirmation of the session input mode change.";
1699
+ case "stop_session":
1700
+ return "Return confirmation that the session was asked to stop and current session state.";
1701
+ case "resize_session":
1702
+ return "Return confirmation of the terminal size update.";
1703
+ default:
1704
+ return "Return the peripheral observation needed for Core CM review.";
1705
+ }
1706
+ }
1707
+ function summarizeInteractiveSessionDispatchArgs(proposal) {
1708
+ const args = proposal.args || {};
1709
+ const summary = {};
1710
+ for (const [key, value] of Object.entries(args)) {
1711
+ if (key === "input" && typeof value === "string") {
1712
+ summary.inputChars = value.length;
1713
+ summary.inputPreview = truncateEventText(value.replace(/\s+/g, " ").trim(), 120);
1714
+ }
1715
+ else if (key === "args" && Array.isArray(value)) {
1716
+ summary.argsCount = value.length;
1717
+ }
1718
+ else if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
1719
+ summary[key] = value;
1720
+ }
1721
+ }
1722
+ return Object.keys(summary).length ? summary : undefined;
1723
+ }
1724
+ function appendTaskDispatchRecord(existing, dispatchRecord) {
1725
+ const current = Array.isArray(existing?.data?.dispatchRecords)
1726
+ ? existing.data.dispatchRecords
1727
+ .map(normalizeCoreCmDispatchRecord)
1728
+ .filter((item) => !!item)
1729
+ : [];
1730
+ return [...current, dispatchRecord].slice(-20);
1731
+ }
1732
+ function normalizeCoreCmDispatchRecord(value) {
1733
+ if (!value || typeof value !== "object")
1734
+ return null;
1735
+ const record = value;
1736
+ if (typeof record.id !== "string" || typeof record.targetPeripheral !== "string" || typeof record.capability !== "string")
1737
+ return null;
1738
+ return {
1739
+ id: record.id,
1740
+ createdAt: typeof record.createdAt === "string" ? record.createdAt : new Date().toISOString(),
1741
+ taskId: typeof record.taskId === "string" ? record.taskId : "",
1742
+ conversationId: typeof record.conversationId === "string" ? record.conversationId : undefined,
1743
+ targetPeripheral: record.targetPeripheral,
1744
+ capability: record.capability,
1745
+ brief: typeof record.brief === "string" ? record.brief : `${record.targetPeripheral}.${record.capability}`,
1746
+ contextBoundary: normalizeDispatchBoundary(record.contextBoundary),
1747
+ permissionBoundary: normalizePermissionBoundary(record.permissionBoundary),
1748
+ expectedDeliverable: typeof record.expectedDeliverable === "string" ? record.expectedDeliverable : "Return a peripheral observation.",
1749
+ reason: typeof record.reason === "string" ? record.reason : undefined,
1750
+ argsSummary: record.argsSummary && typeof record.argsSummary === "object" && !Array.isArray(record.argsSummary)
1751
+ ? record.argsSummary
1752
+ : undefined,
1753
+ };
1754
+ }
1755
+ function normalizeDispatchBoundary(value) {
1756
+ const record = value && typeof value === "object" ? value : {};
1757
+ return {
1758
+ includes: Array.isArray(record.includes) ? record.includes.filter((item) => typeof item === "string") : [],
1759
+ excludes: Array.isArray(record.excludes) ? record.excludes.filter((item) => typeof item === "string") : [],
1760
+ };
1761
+ }
1762
+ function normalizePermissionBoundary(value) {
1763
+ const record = value && typeof value === "object" ? value : {};
1764
+ return {
1765
+ allowed: Array.isArray(record.allowed) ? record.allowed.filter((item) => typeof item === "string") : [],
1766
+ forbidden: Array.isArray(record.forbidden) ? record.forbidden.filter((item) => typeof item === "string") : [],
1767
+ requiresOwnerApproval: record.requiresOwnerApproval === true,
1768
+ };
1769
+ }
1770
+ async function appendCoreCmDispatchRecordEvent(input) {
1771
+ if (!input.conversationId)
1772
+ return null;
1773
+ try {
1774
+ const event = await appendCognitiveEvent({
1775
+ workdir: input.workdir,
1776
+ agentName: input.agentName,
1777
+ event: {
1778
+ stream: "cm",
1779
+ taskId: input.taskId,
1780
+ conversationId: input.conversationId,
1781
+ cm: "core",
1782
+ itemType: "dispatch_record",
1783
+ status: "info",
1784
+ summary: `Core CM dispatched ${input.dispatchRecord.capability} to ${input.dispatchRecord.targetPeripheral}.`,
1785
+ data: {
1786
+ dispatchRecord: input.dispatchRecord,
1787
+ },
1788
+ refs: [{
1789
+ eventIds: input.sourceEventIds,
1790
+ conversationId: input.conversationId,
1791
+ messageId: input.ownerMessageId,
1792
+ taskId: input.taskId,
1793
+ }],
1794
+ },
1795
+ });
1796
+ return event.id;
1797
+ }
1798
+ catch (error) {
1799
+ logBestEffortError("Core CM dispatch event append", error);
1800
+ return null;
1801
+ }
1802
+ }
1803
+ function createCoreCmReviewRecord(input) {
1804
+ const review = input.result.review;
1805
+ const completionDecision = normalizeCoreCmCompletionDecision(review?.completionDecision)
1806
+ || (input.output.trim() ? "complete" : "blocked");
1807
+ const resultQuality = normalizeCoreCmResultQuality(review?.resultQuality)
1808
+ || (completionDecision === "blocked" ? "failed" : "good");
1809
+ const followUpNeeded = typeof review?.followUpNeeded === "boolean"
1810
+ ? review.followUpNeeded
1811
+ : completionDecision !== "complete";
1812
+ const memoryProposalRefs = createCoreCmReviewMemoryProposalRefs(input.result);
1813
+ return {
1814
+ id: createCognitiveEventId("review"),
1815
+ createdAt: new Date().toISOString(),
1816
+ taskId: input.taskId,
1817
+ conversationId: input.conversationId,
1818
+ source: review ? "core_cm_observation_review" : "core_cm_task_report",
1819
+ resultQuality,
1820
+ completionDecision,
1821
+ followUpNeeded,
1822
+ memoryProposalRefs,
1823
+ reportText: truncateEventText(review?.reportText || input.output, 4000),
1824
+ summary: review?.summary || `Core CM reviewed the report with ${completionDecision} completion.`,
1825
+ dispatchRecordId: input.dispatchRecord?.id,
1826
+ targetPeripheral: input.dispatchRecord?.targetPeripheral,
1827
+ capability: input.dispatchRecord?.capability,
1828
+ };
1829
+ }
1830
+ function createCoreCmReviewMemoryProposalRefs(input) {
1831
+ if (input.memoryProposal?.id) {
1832
+ return [{
1833
+ memoryProposalId: input.memoryProposal.id,
1834
+ source: input.memoryProposal.source,
1835
+ summary: input.memoryProposal.summary,
1836
+ }];
1837
+ }
1838
+ const memoryActionProposalId = input.memoryAction?.resolvedProposalId || input.memoryAction?.proposalId;
1839
+ if (!memoryActionProposalId)
1840
+ return [];
1841
+ return [{
1842
+ memoryProposalId: memoryActionProposalId,
1843
+ source: "memory_action",
1844
+ summary: `Memory action ${input.memoryAction?.action || "unknown"} ${input.memoryAction?.status || "completed"}.`,
1845
+ }];
1846
+ }
1847
+ function appendTaskReviewRecord(existing, reviewRecord) {
1848
+ const current = Array.isArray(existing?.data?.reviewRecords)
1849
+ ? existing.data.reviewRecords
1850
+ .map(normalizeCoreCmReviewRecord)
1851
+ .filter((item) => !!item)
1852
+ : [];
1853
+ return [...current, reviewRecord].slice(-20);
1854
+ }
1855
+ function normalizeCoreCmReviewRecord(value) {
1856
+ if (!value || typeof value !== "object")
1857
+ return null;
1858
+ const record = value;
1859
+ if (typeof record.id !== "string" || typeof record.taskId !== "string")
1860
+ return null;
1861
+ const completionDecision = normalizeCoreCmCompletionDecision(record.completionDecision) || "complete";
1862
+ const resultQuality = normalizeCoreCmResultQuality(record.resultQuality) || "good";
1863
+ return {
1864
+ id: record.id,
1865
+ createdAt: typeof record.createdAt === "string" ? record.createdAt : new Date().toISOString(),
1866
+ taskId: record.taskId,
1867
+ conversationId: typeof record.conversationId === "string" ? record.conversationId : undefined,
1868
+ source: record.source === "core_cm_observation_review" ? "core_cm_observation_review" : "core_cm_task_report",
1869
+ resultQuality,
1870
+ completionDecision,
1871
+ followUpNeeded: record.followUpNeeded === true,
1872
+ memoryProposalRefs: normalizeCoreCmReviewMemoryRefs(record.memoryProposalRefs),
1873
+ reportText: typeof record.reportText === "string" ? record.reportText : "",
1874
+ summary: typeof record.summary === "string" ? record.summary : undefined,
1875
+ dispatchRecordId: typeof record.dispatchRecordId === "string" ? record.dispatchRecordId : undefined,
1876
+ targetPeripheral: typeof record.targetPeripheral === "string" ? record.targetPeripheral : undefined,
1877
+ capability: typeof record.capability === "string" ? record.capability : undefined,
1878
+ };
1879
+ }
1880
+ function normalizeCoreCmReviewMemoryRefs(value) {
1881
+ if (!Array.isArray(value))
1882
+ return [];
1883
+ return value.flatMap((item) => {
1884
+ if (!item || typeof item !== "object")
1885
+ return [];
1886
+ const record = item;
1887
+ if (typeof record.memoryProposalId !== "string" || !record.memoryProposalId)
1888
+ return [];
1889
+ return [{
1890
+ memoryProposalId: record.memoryProposalId,
1891
+ source: typeof record.source === "string" ? record.source : undefined,
1892
+ summary: typeof record.summary === "string" ? record.summary : undefined,
1893
+ }];
1894
+ });
1895
+ }
1896
+ function normalizeCoreCmResultQuality(value) {
1897
+ return value === "good" || value === "partial" || value === "failed" || value === "uncertain"
1898
+ ? value
1899
+ : undefined;
1900
+ }
1901
+ function normalizeCoreCmCompletionDecision(value) {
1902
+ return value === "complete" || value === "partial" || value === "blocked" || value === "needs_follow_up"
1903
+ ? value
1904
+ : undefined;
1905
+ }
1906
+ async function appendCoreCmReviewRecordEvent(input) {
1907
+ if (!input.conversationId)
1908
+ return null;
1909
+ try {
1910
+ const memoryProposalId = input.reviewRecord.memoryProposalRefs[0]?.memoryProposalId;
1911
+ const event = await appendCognitiveEvent({
1912
+ workdir: input.workdir,
1913
+ agentName: input.agentName,
1914
+ event: {
1915
+ stream: "cm",
1916
+ taskId: input.taskId,
1917
+ conversationId: input.conversationId,
1918
+ cm: "core",
1919
+ itemType: "review_record",
1920
+ status: input.reviewRecord.completionDecision === "blocked" ? "failed" : "info",
1921
+ summary: `Core CM reviewed the report: ${input.reviewRecord.completionDecision} (${input.reviewRecord.resultQuality}).`,
1922
+ memoryProposalId,
1923
+ data: {
1924
+ reviewRecord: input.reviewRecord,
1925
+ },
1926
+ refs: [{
1927
+ eventIds: input.sourceEventIds,
1928
+ conversationId: input.conversationId,
1929
+ messageId: input.akemonMessageId || input.ownerMessageId,
1930
+ taskId: input.taskId,
1931
+ memoryProposalId,
1932
+ }],
1933
+ },
1934
+ });
1935
+ return event.id;
1936
+ }
1937
+ catch (error) {
1938
+ logBestEffortError("Core CM review event append", error);
1939
+ return null;
1940
+ }
1941
+ }
1942
+ async function appendLocalTaskEvent(input) {
1943
+ try {
1944
+ const event = await appendCognitiveEvent({
1945
+ workdir: input.workdir,
1946
+ agentName: input.agentName,
1947
+ event: {
1948
+ stream: "task",
1949
+ taskId: input.taskId,
1950
+ conversationId: input.conversationId,
1951
+ phase: input.phase,
1952
+ stage: input.stage,
1953
+ status: input.status,
1954
+ summary: input.summary,
1955
+ route: input.route,
1956
+ refs: input.refs,
1957
+ data: input.data,
1958
+ },
1959
+ });
1960
+ try {
1961
+ await upsertTaskRegistryRecordFromEvent({
1962
+ workdir: input.workdir,
1963
+ agentName: input.agentName,
1964
+ event: event,
1965
+ });
1966
+ }
1967
+ catch (error) {
1968
+ logBestEffortError("task registry upsert", error);
1969
+ }
1970
+ return event.id;
1971
+ }
1972
+ catch (error) {
1973
+ logBestEffortError("local task event append", error);
1974
+ return null;
1975
+ }
1976
+ }
1977
+ async function appendLocalTaskStageEvent(input) {
1978
+ const eventId = await appendLocalTaskEvent(input);
1979
+ if (eventId)
1980
+ input.taskEventIds.push(eventId);
1981
+ }
1982
+ async function readLocalTaskResponseRecord(input) {
1983
+ try {
1984
+ return await readTaskRegistryRecord(input);
1985
+ }
1986
+ catch (error) {
1987
+ logBestEffortError("task registry read", error);
1988
+ return null;
1989
+ }
1990
+ }
1991
+ function createTaskResponseRecord(task) {
1992
+ return {
1993
+ ...task,
1994
+ view: createTaskSurfaceView(task),
1995
+ };
1996
+ }
1997
+ function createOptionalTaskResponseRecord(task) {
1998
+ return task ? createTaskResponseRecord(task) : null;
1999
+ }
2000
+ function createTaskViewResponse(task) {
2001
+ return task ? createTaskSurfaceView(task) : null;
2002
+ }
2003
+ function summarizeCoreCmTaskOutcome(input) {
2004
+ if (input.memoryAction) {
2005
+ return `Core CM completed the request through Memory CM: ${summarizeMemoryActionExecution(input.memoryAction)}`;
2006
+ }
2007
+ if (input.memoryProposal?.id) {
2008
+ return "Core CM completed the request and produced a memory proposal.";
2009
+ }
2010
+ if (input.peripheralEventIds.length > 0) {
2011
+ return "Core CM completed the request after observing a peripheral result.";
2012
+ }
2013
+ if (input.cmEventIds.length > 0) {
2014
+ return "Core CM completed the request with internal CM notes.";
2015
+ }
2016
+ return "Core CM completed the request with a direct answer.";
2017
+ }
2018
+ async function appendMemoryActionCmEvent(input) {
2019
+ if (!input.conversationId)
2020
+ return null;
2021
+ try {
2022
+ const event = await appendCognitiveEvent({
2023
+ workdir: input.workdir,
2024
+ agentName: input.agentName,
2025
+ event: {
2026
+ stream: "cm",
2027
+ taskId: input.taskId,
2028
+ conversationId: input.conversationId,
2029
+ cm: "memory",
2030
+ itemType: "memory_action",
2031
+ status: cmStatusForMemoryAction(input.memoryAction),
2032
+ summary: summarizeMemoryActionExecution(input.memoryAction),
2033
+ memoryProposalId: input.memoryAction.resolvedProposalId || input.memoryAction.proposalId,
2034
+ data: createMemoryActionTaskData(input.memoryAction, "memory_cm"),
2035
+ refs: [{
2036
+ eventIds: input.sourceEventIds,
2037
+ conversationId: input.conversationId,
2038
+ messageId: input.akemonMessageId || input.ownerMessageId,
2039
+ memoryProposalId: input.memoryAction.resolvedProposalId || input.memoryAction.proposalId,
2040
+ taskId: input.taskId,
2041
+ }],
2042
+ },
2043
+ });
2044
+ return event.id;
2045
+ }
2046
+ catch (error) {
2047
+ logBestEffortError("memory action CM event append", error);
2048
+ return null;
2049
+ }
2050
+ }
2051
+ function cmStatusForMemoryAction(result) {
2052
+ if (result.status === "accepted")
2053
+ return "accepted";
2054
+ if (result.status === "rejected")
2055
+ return "rejected";
2056
+ if (result.status === "revised")
2057
+ return "revised";
2058
+ if (result.status === "not_found" || result.status === "invalid")
2059
+ return "failed";
2060
+ return "info";
2061
+ }
2062
+ function summarizeMemoryActionExecution(result) {
2063
+ if (result.status === "accepted")
2064
+ return "Memory CM accepted a pending memory proposal.";
2065
+ if (result.status === "rejected")
2066
+ return "Memory CM rejected a pending memory proposal.";
2067
+ if (result.status === "revised")
2068
+ return "Memory CM revised a pending memory proposal.";
2069
+ if (result.status === "listed")
2070
+ return "Memory CM listed pending memory proposals.";
2071
+ if (result.status === "shown")
2072
+ return "Memory CM showed current memory.";
2073
+ if (result.status === "not_found")
2074
+ return "Memory CM could not find the requested pending memory proposal.";
2075
+ if (result.status === "invalid")
2076
+ return "Memory CM could not apply the requested memory action.";
2077
+ return `Memory CM completed ${result.action}.`;
2078
+ }
2079
+ function createMemoryActionTaskData(result, source) {
2080
+ return {
2081
+ source,
2082
+ action: result.action,
2083
+ status: result.status,
2084
+ proposalId: result.proposalId,
2085
+ resolvedProposalId: result.resolvedProposalId,
2086
+ scope: result.scope,
2087
+ selfCount: result.selfCount,
2088
+ workCount: result.workCount,
2089
+ writtenWorkMemoryPathCount: result.writtenWorkMemoryPaths?.length || 0,
2090
+ outputChars: result.output.length,
2091
+ error: result.error,
2092
+ };
2093
+ }
2094
+ async function appendOwnerDirectInteractiveSessionEvents(input) {
2095
+ const eventIds = { task: [], peripheral: [] };
2096
+ if (!input.agentName || !input.workdir)
2097
+ return eventIds;
2098
+ const taskId = createOwnerDirectTaskId("interactive_session", input.action.capability);
2099
+ const taskStartEventId = await appendLocalTaskEvent({
2100
+ agentName: input.agentName,
2101
+ workdir: input.workdir,
2102
+ taskId,
2103
+ phase: "delegated",
2104
+ stage: "dispatching",
2105
+ status: "running",
2106
+ route: "interactive-session",
2107
+ summary: `Owner requested ${interactiveSessionCapabilityOwnerLabel(input.action.capability)}.`,
2108
+ refs: [{
2109
+ taskId,
2110
+ sessionId: interactiveSessionObservationSessionId(input.observation, input.action),
2111
+ }],
2112
+ data: {
2113
+ source: input.source,
2114
+ objective: interactiveSessionCapabilityOwnerLabel(input.action.capability),
2115
+ capability: input.action.capability,
2116
+ },
2117
+ });
2118
+ if (taskStartEventId)
2119
+ eventIds.task.push(taskStartEventId);
2120
+ const peripheralEventId = await appendInteractiveSessionPeripheralEvent({
2121
+ agentName: input.agentName,
2122
+ workdir: input.workdir,
2123
+ taskId,
2124
+ action: input.action,
2125
+ observation: input.observation,
2126
+ });
2127
+ if (peripheralEventId)
2128
+ eventIds.peripheral.push(peripheralEventId);
2129
+ const taskEndEventId = await appendLocalTaskEvent({
2130
+ agentName: input.agentName,
2131
+ workdir: input.workdir,
2132
+ taskId,
2133
+ phase: input.observation.ok ? "completed" : "failed",
2134
+ stage: input.observation.ok ? "done" : "blocked",
2135
+ status: input.observation.ok ? "succeeded" : "failed",
2136
+ route: "interactive-session",
2137
+ summary: summarizeInteractiveSessionTaskCompletion(input.action.capability, input.observation.ok),
2138
+ refs: [{
2139
+ taskId,
2140
+ eventIds: [...eventIds.task, ...eventIds.peripheral],
2141
+ sessionId: interactiveSessionObservationSessionId(input.observation, input.action),
2142
+ }],
2143
+ data: createInteractiveSessionTaskData({
2144
+ source: input.source,
2145
+ action: input.action,
2146
+ observation: input.observation,
2147
+ }),
2148
+ });
2149
+ if (taskEndEventId)
2150
+ eventIds.task.push(taskEndEventId);
2151
+ return eventIds;
2152
+ }
2153
+ function createOwnerDirectTaskId(kind, label) {
2154
+ const safeLabel = label.replace(/[^a-zA-Z0-9_.:-]/g, "_").slice(0, 60) || "task";
2155
+ return createCognitiveEventId(`task_${kind}_${safeLabel}`);
2156
+ }
2157
+ function summarizeInteractiveSessionTaskCompletion(capability, ok) {
2158
+ const label = interactiveSessionCapabilityOwnerLabel(capability);
2159
+ return ok ? `${label} completed.` : `${label} failed.`;
2160
+ }
2161
+ function interactiveSessionCapabilityOwnerLabel(capability) {
2162
+ switch (capability) {
2163
+ case "start_session": return "terminal start";
2164
+ case "send_input": return "terminal input";
2165
+ case "resize_session": return "terminal resize";
2166
+ case "stop_session": return "terminal stop";
2167
+ case "inspect_session": return "terminal inspect";
2168
+ case "set_input_mode": return "terminal input mode change";
2169
+ case "list_sessions": return "terminal session list";
2170
+ default: return `terminal action ${capability}`;
2171
+ }
2172
+ }
2173
+ function ensureSoftwareAgentEnvelopeTaskId(envelope) {
2174
+ if (!envelope.taskId)
2175
+ envelope.taskId = createCognitiveEventId("task_software_agent");
2176
+ return envelope.taskId;
2177
+ }
2178
+ async function appendSoftwareAgentTaskStartEvent(input) {
2179
+ const taskId = ensureSoftwareAgentEnvelopeTaskId(input.envelope);
2180
+ return appendLocalTaskEvent({
2181
+ agentName: input.agentName,
2182
+ workdir: input.workdir,
2183
+ taskId,
2184
+ phase: "delegated",
2185
+ stage: "dispatching",
2186
+ status: "running",
2187
+ route: "software-agent",
2188
+ summary: `Owner delegated a software-agent task: ${truncateEventText(input.envelope.goal || input.envelope.purpose || taskId, 180)}`,
2189
+ refs: [{ taskId }],
2190
+ data: {
2191
+ source: input.envelope.sourceModule,
2192
+ softwareAgentId: input.softwareAgentId || "software-agent",
2193
+ purpose: input.envelope.purpose,
2194
+ goal: input.envelope.goal,
2195
+ goalChars: (input.envelope.goal || "").length,
2196
+ workdir: input.envelope.workdir,
2197
+ roleScope: input.envelope.roleScope,
2198
+ memoryScope: input.envelope.memoryScope,
2199
+ riskLevel: input.envelope.riskLevel,
2200
+ contextSessionId: input.envelope.contextSessionId,
2201
+ workMemoryDir: input.envelope.workMemoryDir,
2202
+ taskRecordPath: softwareAgentTaskRecordPath(softwareAgentTaskLedgerDir(input.workdir, input.agentName), taskId),
2203
+ },
2204
+ });
2205
+ }
2206
+ async function appendSoftwareAgentTaskEndEvents(input) {
2207
+ const eventIds = { task: [], peripheral: [] };
2208
+ const taskId = input.result.taskId || ensureSoftwareAgentEnvelopeTaskId(input.envelope);
2209
+ const taskRecordPath = softwareAgentTaskRecordPath(softwareAgentTaskLedgerDir(input.workdir, input.agentName), taskId);
2210
+ try {
2211
+ const peripheralEvent = await appendCognitiveEvent({
2212
+ workdir: input.workdir,
2213
+ agentName: input.agentName,
2214
+ event: {
2215
+ stream: "peripheral",
2216
+ taskId,
2217
+ peripheral: input.softwareAgentId || "software-agent",
2218
+ capability: "send_task",
2219
+ ok: input.result.success,
2220
+ summary: input.result.success
2221
+ ? `Software-agent task ${taskId} completed.`
2222
+ : `Software-agent task ${taskId} failed.`,
2223
+ observation: {
2224
+ taskId,
2225
+ success: input.result.success,
2226
+ exitCode: input.result.exitCode,
2227
+ durationMs: input.result.durationMs,
2228
+ outputChars: (input.result.output || "").length,
2229
+ error: input.result.error,
2230
+ contextSessionId: input.result.contextSessionId,
2231
+ contextPacketPath: input.result.contextPacketPath,
2232
+ workMemoryDir: input.result.workMemoryDir,
2233
+ taskRecordPath,
2234
+ },
2235
+ refs: [{
2236
+ taskId,
2237
+ eventIds: input.sourceEventIds || [],
2238
+ }],
2239
+ },
2240
+ });
2241
+ eventIds.peripheral.push(peripheralEvent.id);
2242
+ }
2243
+ catch (error) {
2244
+ logBestEffortError("software-agent peripheral event append", error);
2245
+ }
2246
+ const taskEndEventId = await appendLocalTaskEvent({
2247
+ agentName: input.agentName,
2248
+ workdir: input.workdir,
2249
+ taskId,
2250
+ phase: input.result.success ? "completed" : "failed",
2251
+ stage: input.result.success ? "done" : "blocked",
2252
+ status: input.result.success ? "succeeded" : "failed",
2253
+ route: "software-agent",
2254
+ summary: input.result.success
2255
+ ? `Software-agent task ${taskId} completed.`
2256
+ : `Software-agent task ${taskId} failed.`,
2257
+ refs: [{
2258
+ taskId,
2259
+ eventIds: [...(input.sourceEventIds || []), ...eventIds.peripheral],
2260
+ }],
2261
+ data: {
2262
+ softwareAgentId: input.softwareAgentId || "software-agent",
2263
+ success: input.result.success,
2264
+ exitCode: input.result.exitCode,
2265
+ durationMs: input.result.durationMs,
2266
+ outputChars: (input.result.output || "").length,
2267
+ error: input.result.error,
2268
+ contextSessionId: input.result.contextSessionId,
2269
+ contextPacketPath: input.result.contextPacketPath,
2270
+ workMemoryDir: input.result.workMemoryDir,
2271
+ taskRecordPath,
2272
+ },
2273
+ });
2274
+ if (taskEndEventId)
2275
+ eventIds.task.push(taskEndEventId);
2276
+ return eventIds;
2277
+ }
2278
+ async function appendSoftwareAgentTaskFailureEvent(input) {
2279
+ const taskId = ensureSoftwareAgentEnvelopeTaskId(input.envelope);
2280
+ return appendLocalTaskEvent({
2281
+ agentName: input.agentName,
2282
+ workdir: input.workdir,
2283
+ taskId,
2284
+ phase: "failed",
2285
+ stage: "blocked",
2286
+ status: "failed",
2287
+ route: "software-agent",
2288
+ summary: `Software-agent task ${taskId} failed before producing a result.`,
2289
+ refs: [{
2290
+ taskId,
2291
+ eventIds: input.sourceEventIds || [],
2292
+ }],
2293
+ data: {
2294
+ softwareAgentId: input.softwareAgentId || "software-agent",
2295
+ error: input.error,
2296
+ taskRecordPath: softwareAgentTaskRecordPath(softwareAgentTaskLedgerDir(input.workdir, input.agentName), taskId),
2297
+ },
2298
+ });
2299
+ }
2300
+ function truncateEventText(value, maxChars) {
2301
+ const text = value.trim();
2302
+ if (text.length <= maxChars)
2303
+ return text;
2304
+ return `${text.slice(0, Math.max(0, maxChars - 3))}...`;
2305
+ }
2306
+ async function appendInteractiveSessionOutcomeEvent(input) {
2307
+ const { snapshot } = input.event;
2308
+ const taskId = `task_interactive_session_${snapshot.sessionId}`;
2309
+ await appendLocalTaskEvent({
2310
+ agentName: input.agentName,
2311
+ workdir: input.workdir,
2312
+ taskId,
2313
+ phase: snapshot.status === "failed" ? "failed" : "completed",
2314
+ stage: snapshot.status === "failed" ? "blocked" : "done",
2315
+ status: snapshot.status === "failed" ? "failed" : "succeeded",
2316
+ route: "interactive-session",
2317
+ summary: `Interactive session ${snapshot.sessionId} ended with status ${snapshot.status}.`,
2318
+ refs: [{
2319
+ taskId,
2320
+ sessionId: snapshot.sessionId,
2321
+ }],
2322
+ data: {
2323
+ source: "interactive_session_exit",
2324
+ sessionId: snapshot.sessionId,
2325
+ tool: snapshot.tool,
2326
+ status: snapshot.status,
2327
+ command: snapshot.commandLineDisplay,
2328
+ workdir: snapshot.workdir,
2329
+ logPath: snapshot.logPath,
2330
+ outputChars: snapshot.outputChars,
2331
+ exitCode: input.event.exitCode,
2332
+ signal: input.event.signal,
2333
+ startedAt: snapshot.startedAt,
2334
+ endedAt: snapshot.endedAt,
2335
+ },
2336
+ });
2337
+ }
2338
+ function schedulePassiveReflection(input) {
2339
+ setTimeout(() => {
2340
+ runPassiveReflectionOnce(input).catch((error) => {
2341
+ logBestEffortError("passive reflection", error);
2342
+ });
2343
+ }, 0);
2344
+ }
2345
+ async function appendComputeAuditEvent(input) {
2346
+ const summary = {
2347
+ origin: input.req.origin,
2348
+ taskKind: input.req.taskKind,
2349
+ purpose: input.req.purpose,
2350
+ priority: input.req.priority,
2351
+ routeId: input.invocation?.routeId,
2352
+ resourceId: input.invocation?.resourceId,
2353
+ resourceKind: input.invocation?.resourceKind,
2354
+ provider: input.invocation?.provider,
2355
+ protocol: input.invocation?.protocol,
2356
+ engine: input.invocation?.engine,
2357
+ model: input.invocation?.model,
2358
+ reasoningEffort: input.invocation?.reasoningEffort,
2359
+ verbosity: input.invocation?.verbosity,
2360
+ command: input.invocation?.command,
2361
+ inputChars: input.promptChars,
2362
+ outputChars: input.outputChars,
2363
+ durationMs: input.durationMs,
2364
+ success: input.success,
2365
+ ...(input.error ? { error: input.error } : {}),
2366
+ };
2367
+ try {
2368
+ const event = await appendCognitiveEvent({
2369
+ workdir: input.workdir,
2370
+ agentName: input.agentName,
2371
+ event: {
2372
+ stream: "compute",
2373
+ taskId: input.req.taskId,
2374
+ ...summary,
2375
+ refs: input.req.refs || [],
2376
+ },
2377
+ });
2378
+ return { ...summary, eventId: event.id };
2379
+ }
2380
+ catch (error) {
2381
+ logBestEffortError("compute audit event append", error);
2382
+ return summary;
2383
+ }
2384
+ }
2385
+ async function appendInteractiveSessionPeripheralEvent(input) {
2386
+ try {
2387
+ const sessionId = interactiveSessionObservationSessionId(input.observation, input.action);
2388
+ const event = await appendCognitiveEvent({
2389
+ workdir: input.workdir,
2390
+ agentName: input.agentName,
2391
+ event: {
2392
+ stream: "peripheral",
2393
+ taskId: input.taskId,
2394
+ conversationId: input.conversationId,
2395
+ peripheral: input.observation.peripheral,
2396
+ capability: input.observation.capability,
2397
+ ok: input.observation.ok,
2398
+ summary: input.observation.summary,
2399
+ sessionId,
2400
+ actionReason: input.action.reason,
2401
+ observation: {
2402
+ capability: input.observation.capability,
2403
+ ok: input.observation.ok,
2404
+ summary: input.observation.summary,
2405
+ data: input.observation.data,
2406
+ error: input.observation.error,
2407
+ },
2408
+ refs: [
2409
+ {
2410
+ conversationId: input.conversationId,
2411
+ messageId: input.ownerMessageId,
2412
+ sessionId,
2413
+ taskId: input.taskId,
2414
+ },
2415
+ ],
2416
+ },
2417
+ });
2418
+ return event.id;
2419
+ }
2420
+ catch (error) {
2421
+ logBestEffortError("interactive session peripheral event append", error);
2422
+ return null;
2423
+ }
2424
+ }
2425
+ async function appendCoreCmResultEvents(input) {
2426
+ const eventIds = [];
2427
+ if (!input.conversationId)
2428
+ return eventIds;
2429
+ try {
2430
+ const processNotes = (input.processNotes || [])
2431
+ .map((note) => (typeof note === "string" ? note.trim() : ""))
2432
+ .filter(Boolean);
2433
+ for (let index = 0; index < processNotes.length; index += 1) {
2434
+ const event = await appendCognitiveEvent({
2435
+ workdir: input.workdir,
2436
+ agentName: input.agentName,
2437
+ event: {
2438
+ stream: "cm",
2439
+ taskId: input.taskId,
2440
+ conversationId: input.conversationId,
2441
+ cm: "core",
2442
+ itemType: "process_note",
2443
+ status: "info",
2444
+ summary: processNotes[index],
2445
+ data: {
2446
+ order: index + 1,
2447
+ },
2448
+ refs: [
2449
+ {
2450
+ eventIds: input.sourceEventIds,
2451
+ conversationId: input.conversationId,
2452
+ messageId: input.akemonMessageId || input.ownerMessageId,
2453
+ taskId: input.taskId,
2454
+ },
2455
+ ],
2456
+ },
2457
+ });
2458
+ eventIds.push(event.id);
2459
+ }
2460
+ if (input.memoryAction) {
2461
+ const memoryActionEventId = await appendMemoryActionCmEvent({
2462
+ agentName: input.agentName,
2463
+ workdir: input.workdir,
2464
+ conversationId: input.conversationId,
2465
+ ownerMessageId: input.ownerMessageId,
2466
+ akemonMessageId: input.akemonMessageId,
2467
+ sourceEventIds: input.sourceEventIds,
2468
+ taskId: input.taskId,
2469
+ memoryAction: input.memoryAction,
2470
+ });
2471
+ if (memoryActionEventId)
2472
+ eventIds.push(memoryActionEventId);
2473
+ }
2474
+ if (!input.memoryProposal)
2475
+ return eventIds;
2476
+ const proposal = input.memoryProposal;
2477
+ const event = await appendCognitiveEvent({
2478
+ workdir: input.workdir,
2479
+ agentName: input.agentName,
2480
+ event: {
2481
+ stream: "cm",
2482
+ taskId: input.taskId,
2483
+ conversationId: input.conversationId,
2484
+ cm: "memory",
2485
+ itemType: "memory_proposal",
2486
+ status: "pending",
2487
+ summary: proposal.summary || "Akemon Memory CM created a pending memory proposal.",
2488
+ memoryProposalId: proposal.id,
2489
+ data: {
2490
+ source: proposal.source,
2491
+ selfCount: Array.isArray(proposal.self) ? proposal.self.length : 0,
2492
+ workCount: Array.isArray(proposal.work) ? proposal.work.length : 0,
2493
+ },
2494
+ refs: [
2495
+ {
2496
+ eventIds: input.sourceEventIds,
2497
+ conversationId: input.conversationId,
2498
+ messageId: input.akemonMessageId || input.ownerMessageId,
2499
+ memoryProposalId: proposal.id,
2500
+ taskId: input.taskId,
2501
+ },
2502
+ ],
2503
+ },
2504
+ });
2505
+ eventIds.push(event.id);
2506
+ }
2507
+ catch (error) {
2508
+ logBestEffortError("Core CM result event append", error);
2509
+ }
2510
+ return eventIds;
2511
+ }
2512
+ async function executeInteractiveSessionViaPeripheral(workbench, peripheral, action) {
2513
+ return peripheral ? peripheral.execute(action) : executeInteractiveSessionAction(workbench, action);
2514
+ }
2515
+ function interactiveSessionObservationSessionId(observation, action) {
2516
+ const observedSession = observation.data?.session;
2517
+ if (observedSession && typeof observedSession === "object") {
2518
+ const sessionId = observedSession.sessionId;
2519
+ if (typeof sessionId === "string" && sessionId)
2520
+ return sessionId;
2521
+ }
2522
+ const args = action?.args;
2523
+ return typeof args?.sessionId === "string" && args.sessionId ? args.sessionId : undefined;
2524
+ }
2525
+ function createInvalidInteractiveSessionObservation(message) {
2526
+ return {
2527
+ kind: "peripheral_observation",
2528
+ peripheral: "interactive-session",
2529
+ capability: "list_sessions",
2530
+ ok: false,
2531
+ summary: message,
2532
+ error: message,
2533
+ };
2534
+ }
2535
+ function createInteractiveSessionTaskData(input) {
2536
+ return {
2537
+ source: input.source,
2538
+ capability: input.observation.capability,
2539
+ ok: input.observation.ok,
2540
+ sessionId: interactiveSessionObservationSessionId(input.observation, input.action),
2541
+ outputChars: input.outputChars,
2542
+ error: input.observation.error,
2543
+ };
2544
+ }
2545
+ async function executeInteractiveSessionActionProposal(workbench, peripheral, proposal) {
2546
+ const action = {
2547
+ capability: proposal.capability,
2548
+ args: proposal.args,
2549
+ reason: proposal.reason,
2550
+ };
2551
+ if (peripheral)
2552
+ return peripheral.execute(action);
2553
+ if (!workbench) {
2554
+ return {
2555
+ kind: "peripheral_observation",
2556
+ peripheral: "interactive-session",
2557
+ capability: proposal.capability,
2558
+ ok: false,
2559
+ reason: proposal.reason,
2560
+ summary: "I could not use interactive sessions because the adapter is not available in this runtime.",
2561
+ error: "Interactive session adapter is not available in this runtime.",
2562
+ };
2563
+ }
2564
+ return executeInteractiveSessionAction(workbench, action);
2565
+ }
2566
+ function parseMemoryChatCommand(input) {
2567
+ const trimmed = input.text.trim();
2568
+ if (!trimmed.startsWith("/memory"))
2569
+ return null;
2570
+ const tokens = splitWorkbenchCommandLine(trimmed.slice("/memory".length).trim());
2571
+ const action = tokens[0] || "pending";
2572
+ if (["pending", "list", "list_pending"].includes(action)) {
2573
+ return {
2574
+ proposal: {
2575
+ kind: "memory_action",
2576
+ action: "list_pending",
2577
+ proposalId: tokens[1] || "latest",
2578
+ scope: "all",
2579
+ },
2580
+ };
2581
+ }
2582
+ if (["show", "current", "status", "what"].includes(action)) {
2583
+ return {
2584
+ proposal: {
2585
+ kind: "memory_action",
2586
+ action: "show_memory",
2587
+ scope: parseMemoryCommandScope(tokens[1]),
2588
+ },
2589
+ };
2590
+ }
2591
+ if (["revise", "update", "edit"].includes(action)) {
2592
+ let proposalId = tokens[1] || "latest";
2593
+ let rest = tokens.slice(2);
2594
+ if (isMemoryCommandScope(proposalId)) {
2595
+ rest = tokens.slice(1);
2596
+ proposalId = "latest";
2597
+ }
2598
+ const scope = isMemoryCommandScope(rest[0]) ? parseMemoryCommandScope(rest.shift()) : "work";
2599
+ const revisedText = rest.join(" ").trim();
2600
+ if (!revisedText) {
2601
+ return { output: "Usage: /memory revise [proposalId|latest] [self|work] <replacement memory text>" };
2602
+ }
2603
+ return {
2604
+ proposal: {
2605
+ kind: "memory_action",
2606
+ action: "revise_pending",
2607
+ proposalId,
2608
+ scope,
2609
+ memory: scope === "self" ? { self: [revisedText] } : { work: [revisedText] },
2610
+ },
2611
+ };
2612
+ }
2613
+ if (["accept", "remember", "accept_pending"].includes(action)) {
2614
+ return {
2615
+ proposal: {
2616
+ kind: "memory_action",
2617
+ action: "accept_pending",
2618
+ proposalId: tokens[1] || "latest",
2619
+ scope: parseMemoryCommandScope(tokens[2]),
2620
+ },
2621
+ };
2622
+ }
2623
+ if (["reject", "ignore", "reject_pending"].includes(action)) {
2624
+ return {
2625
+ proposal: {
2626
+ kind: "memory_action",
2627
+ action: "reject_pending",
2628
+ proposalId: tokens[1] || "latest",
2629
+ scope: "all",
2630
+ },
2631
+ };
2632
+ }
2633
+ return { output: "Usage: /memory pending | /memory show [all|self|work] | /memory revise [proposalId|latest] [self|work] <text> | /memory accept [proposalId|latest] [all|self|work] | /memory reject [proposalId|latest]" };
2634
+ }
2635
+ function parseMemoryCommandScope(value) {
2636
+ return value === "self" || value === "work" || value === "all" ? value : "all";
2637
+ }
2638
+ function isMemoryCommandScope(value) {
2639
+ return value === "self" || value === "work" || value === "all";
2640
+ }
2641
+ function executeReflectionChatCommand(input) {
2642
+ const trimmed = input.text.trim();
2643
+ const command = trimmed.match(/^\/([a-zA-Z][a-zA-Z0-9_-]*)/)?.[1] || "";
2644
+ if (!["reflection", "reflections"].includes(command))
2645
+ return null;
2646
+ const tokens = splitWorkbenchCommandLine(trimmed.slice(command.length + 1).trim());
2647
+ const action = tokens[0] || "show";
2648
+ if (!["show", "list", "latest", "recent"].includes(action)) {
2649
+ return Promise.resolve("Usage: /reflection show [count]");
2650
+ }
2651
+ const countToken = action === "latest" ? "1" : tokens[1];
2652
+ const count = readReflectionCount(countToken);
2653
+ return formatRecentReflectionRecords(input.workdir, input.agentName, count);
2654
+ }
2655
+ async function formatRecentReflectionRecords(workdir, agentName, count) {
2656
+ const records = await readCognitiveArtifacts({
2657
+ workdir,
2658
+ agentName,
2659
+ kind: "reflection",
2660
+ limit: count,
2661
+ });
2662
+ if (!records.length) {
2663
+ return "Reflection CM 目前还没有留下反思记录。它只会在主聊天产生外设观察或 CM 事件后,异步记录高价值发现。";
2664
+ }
2665
+ const newestFirst = [...records].reverse();
2666
+ const lines = newestFirst.flatMap((record, index) => {
2667
+ const score = typeof record.valueScore === "number" ? `,价值 ${Math.round(record.valueScore * 100)}%` : "";
2668
+ const header = `${index + 1}. ${formatShortTimestamp(record.createdAt)},Reflection CM 发现:${record.insight}`;
2669
+ const details = [
2670
+ `原因:${record.reasoning}`,
2671
+ ...(record.suggestedAction ? [`建议:${record.suggestedAction}`] : []),
2672
+ `信心:${formatConfidence(record.confidence)}${score}`,
2673
+ ];
2674
+ return [header, ...details, ""];
2675
+ });
2676
+ return [`最近 ${records.length} 条 Reflection CM 记录:`, "", ...lines].join("\n").trim();
2677
+ }
2678
+ function readReflectionCount(token) {
2679
+ const count = Number(token || 5);
2680
+ if (!Number.isFinite(count) || count <= 0)
2681
+ return 5;
2682
+ return Math.max(1, Math.min(20, Math.floor(count)));
2683
+ }
2684
+ function formatShortTimestamp(value) {
2685
+ const date = new Date(value);
2686
+ if (Number.isNaN(date.getTime()))
2687
+ return value;
2688
+ return date.toLocaleString("zh-CN", {
2689
+ year: "numeric",
2690
+ month: "2-digit",
2691
+ day: "2-digit",
2692
+ hour: "2-digit",
2693
+ minute: "2-digit",
2694
+ hour12: false,
2695
+ });
2696
+ }
2697
+ function formatConfidence(value) {
2698
+ if (value === "high")
2699
+ return "高";
2700
+ if (value === "low")
2701
+ return "低";
2702
+ return "中";
2703
+ }
2704
+ function parseWorkbenchChatCommandAction(text) {
2705
+ const trimmed = text.trim();
2706
+ if (!trimmed.startsWith("/"))
2707
+ return null;
2708
+ const command = trimmed.match(/^\/([a-zA-Z][a-zA-Z0-9_-]*)/)?.[1] || "";
2709
+ if (!["start", "sessions", "input", "mode", "input-mode", "stop", "resize"].includes(command))
2710
+ return null;
2711
+ try {
2712
+ switch (command) {
2713
+ case "sessions":
2714
+ return {
2715
+ action: {
2716
+ capability: "list_sessions",
2717
+ reason: "owner used /sessions in Akemon chat",
2718
+ },
2719
+ };
2720
+ case "start": {
2721
+ const tokens = splitWorkbenchCommandLine(trimmed.slice("/start".length).trim());
2722
+ if (!tokens.length) {
2723
+ throw new Error("Usage: /start <codex|claude|cursor|shell|custom> [args...]");
2724
+ }
2725
+ const tool = normalizeWorkbenchTool(tokens[0]);
2726
+ const request = tool === "custom"
2727
+ ? {
2728
+ tool,
2729
+ command: tokens[1],
2730
+ args: tokens.slice(2),
2731
+ }
2732
+ : {
2733
+ tool,
2734
+ args: tokens.slice(1),
2735
+ };
2736
+ return {
2737
+ action: {
2738
+ capability: "start_session",
2739
+ args: request,
2740
+ reason: "owner used /start in Akemon chat",
2741
+ },
2742
+ };
2743
+ }
2744
+ case "input": {
2745
+ const match = trimmed.match(/^\/input\s+(\S+)\s+([\s\S]+)$/);
2746
+ if (!match)
2747
+ throw new Error("Usage: /input <sessionId> <text>");
2748
+ return {
2749
+ action: {
2750
+ capability: "send_input",
2751
+ args: {
2752
+ sessionId: match[1],
2753
+ input: match[2],
2754
+ },
2755
+ reason: "owner used /input in Akemon chat",
2756
+ },
2757
+ };
2758
+ }
2759
+ case "mode":
2760
+ case "input-mode": {
2761
+ const tokens = splitWorkbenchCommandLine(trimmed.slice(command === "mode" ? "/mode".length : "/input-mode".length).trim());
2762
+ if (tokens.length !== 2)
2763
+ throw new Error(`Usage: /${command} <sessionId|active|latest> <line|tui>`);
2764
+ return {
2765
+ action: {
2766
+ capability: "set_input_mode",
2767
+ args: {
2768
+ sessionId: tokens[0],
2769
+ inputMode: normalizeWorkbenchInputMode(tokens[1]),
2770
+ },
2771
+ reason: `owner used /${command} in Akemon chat`,
2772
+ },
2773
+ };
2774
+ }
2775
+ case "stop": {
2776
+ const tokens = splitWorkbenchCommandLine(trimmed.slice("/stop".length).trim());
2777
+ if (tokens.length !== 1)
2778
+ throw new Error("Usage: /stop <sessionId>");
2779
+ return {
2780
+ action: {
2781
+ capability: "stop_session",
2782
+ args: { sessionId: tokens[0] },
2783
+ reason: "owner used /stop in Akemon chat",
2784
+ },
2785
+ };
2786
+ }
2787
+ case "resize": {
2788
+ const tokens = splitWorkbenchCommandLine(trimmed.slice("/resize".length).trim());
2789
+ if (tokens.length !== 3)
2790
+ throw new Error("Usage: /resize <sessionId> <cols> <rows>");
2791
+ const cols = Number(tokens[1]);
2792
+ const rows = Number(tokens[2]);
2793
+ if (!Number.isInteger(cols) || cols <= 0 || !Number.isInteger(rows) || rows <= 0) {
2794
+ throw new Error("Usage: /resize <sessionId> <cols> <rows>");
2795
+ }
2796
+ return {
2797
+ action: {
2798
+ capability: "resize_session",
2799
+ args: {
2800
+ sessionId: tokens[0],
2801
+ cols,
2802
+ rows,
2803
+ },
2804
+ reason: "owner used /resize in Akemon chat",
2805
+ },
2806
+ };
2807
+ }
2808
+ default:
2809
+ return null;
2810
+ }
2811
+ }
2812
+ catch (error) {
2813
+ return { error: `I could not complete this Workbench action: ${error?.message || String(error)}` };
2814
+ }
2815
+ }
2816
+ function splitWorkbenchCommandLine(input) {
2817
+ const tokens = [];
2818
+ let current = "";
2819
+ let quote = null;
2820
+ let escaping = false;
2821
+ for (const char of input) {
2822
+ if (escaping) {
2823
+ current += char;
2824
+ escaping = false;
2825
+ continue;
2826
+ }
2827
+ if (char === "\\") {
2828
+ escaping = true;
2829
+ continue;
2830
+ }
2831
+ if (quote) {
2832
+ if (char === quote)
2833
+ quote = null;
2834
+ else
2835
+ current += char;
2836
+ continue;
2837
+ }
2838
+ if (char === "\"" || char === "'") {
2839
+ quote = char;
2840
+ continue;
2841
+ }
2842
+ if (/\s/.test(char)) {
2843
+ if (current) {
2844
+ tokens.push(current);
2845
+ current = "";
2846
+ }
2847
+ continue;
2848
+ }
2849
+ current += char;
2850
+ }
2851
+ if (escaping)
2852
+ throw new Error("Invalid Workbench command: trailing escape");
2853
+ if (quote)
2854
+ throw new Error("Invalid Workbench command: unterminated quote");
2855
+ if (current)
2856
+ tokens.push(current);
2857
+ return tokens;
2858
+ }
2859
+ export async function handleSoftwareAgentRunStreamHttp(req, res, deps) {
2860
+ if (!requireOwnerRequest(req, res, deps.options))
2861
+ return;
2862
+ const softwareAgent = requireSoftwareAgent(res, deps.softwareAgent);
2863
+ if (!softwareAgent)
2864
+ return;
2865
+ const envelope = await readOwnerSoftwareAgentEnvelope(req, res, deps);
2866
+ if (!envelope)
2867
+ return;
2868
+ ensureSoftwareAgentEnvelopeTaskId(envelope);
2869
+ const taskEventIds = [];
2870
+ const taskStartEventId = await appendSoftwareAgentTaskStartEvent({
2871
+ agentName: deps.agentName,
2872
+ workdir: deps.workdir,
2873
+ envelope,
2874
+ softwareAgentId: deps.softwareAgent?.id,
2875
+ });
2876
+ if (taskStartEventId)
2877
+ taskEventIds.push(taskStartEventId);
2878
+ const abortController = new AbortController();
2879
+ let responseFinished = false;
2880
+ let streamStarted = false;
2881
+ res.on("close", () => {
2882
+ if (!responseFinished)
2883
+ abortController.abort();
2884
+ });
2885
+ const ensureStreamStarted = () => {
2886
+ if (streamStarted)
2887
+ return;
2888
+ streamStarted = true;
2889
+ res.writeHead(200, {
2890
+ "Content-Type": "application/x-ndjson; charset=utf-8",
2891
+ "Cache-Control": "no-cache",
2892
+ "X-Accel-Buffering": "no",
2893
+ });
2894
+ res.flushHeaders?.();
2895
+ };
2896
+ try {
2897
+ const result = await softwareAgent.sendTask(envelope, {
2898
+ signal: abortController.signal,
2899
+ observer: {
2900
+ onStart(event) {
2901
+ ensureStreamStarted();
2902
+ writeSoftwareAgentStreamEvent(res, {
2903
+ type: "start",
2904
+ taskId: event.taskId,
2905
+ commandLine: event.commandLine,
2906
+ contextSessionId: event.contextSessionId,
2907
+ contextPacketPath: event.contextPacketPath,
2908
+ workMemoryDir: event.workMemoryDir,
2909
+ });
2910
+ },
2911
+ onStream(event) {
2912
+ ensureStreamStarted();
2913
+ writeSoftwareAgentStreamEvent(res, {
2914
+ type: event.stream,
2915
+ taskId: event.taskId,
2916
+ chunk: event.chunk,
2917
+ });
2918
+ },
2919
+ onEnd(event) {
2920
+ ensureStreamStarted();
2921
+ writeSoftwareAgentStreamEvent(res, {
2922
+ type: "end",
2923
+ taskId: event.taskId,
2924
+ exitCode: event.exitCode,
2925
+ durationMs: event.durationMs,
2926
+ result: event.result,
2927
+ contextSessionId: event.contextSessionId,
2928
+ contextPacketPath: event.contextPacketPath,
2929
+ workMemoryDir: event.workMemoryDir,
2930
+ });
2931
+ },
2932
+ },
2933
+ });
2934
+ await appendSoftwareAgentTaskEndEvents({
2935
+ agentName: deps.agentName,
2936
+ workdir: deps.workdir,
2937
+ envelope,
2938
+ result,
2939
+ softwareAgentId: deps.softwareAgent?.id,
2940
+ sourceEventIds: taskEventIds,
2941
+ });
2942
+ }
2943
+ catch (err) {
2944
+ await appendSoftwareAgentTaskFailureEvent({
2945
+ agentName: deps.agentName,
2946
+ workdir: deps.workdir,
2947
+ envelope,
2948
+ softwareAgentId: deps.softwareAgent?.id,
2949
+ sourceEventIds: taskEventIds,
2950
+ error: err.message || String(err),
2951
+ });
2952
+ if (!streamStarted) {
2953
+ const busy = String(err.message || "").includes("busy");
2954
+ writeJsonResponse(res, busy ? 409 : 500, { error: err.message || String(err) });
2955
+ responseFinished = true;
2956
+ return;
2957
+ }
2958
+ writeSoftwareAgentStreamEvent(res, {
2959
+ type: "error",
2960
+ error: err.message || String(err),
2961
+ });
2962
+ }
2963
+ finally {
2964
+ responseFinished = true;
2965
+ if (streamStarted && !res.writableEnded)
2966
+ res.end();
2967
+ }
2968
+ }
2969
+ export async function handleSoftwareAgentStatusHttp(req, res, deps) {
2970
+ if (!requireOwnerRequest(req, res, deps.options))
2971
+ return;
2972
+ const softwareAgent = requireSoftwareAgent(res, deps.softwareAgent);
2973
+ if (!softwareAgent)
2974
+ return;
2975
+ writeJsonResponse(res, 200, softwareAgent.getState(), true);
2976
+ }
2977
+ export async function handleSoftwareAgentTasksHttp(req, res, deps) {
2978
+ if (!requireOwnerRequest(req, res, deps.options))
2979
+ return;
2980
+ const url = new URL(req.url || "/", "http://127.0.0.1");
2981
+ const basePath = "/self/software-agent/tasks";
2982
+ const taskLedgerDir = softwareAgentTaskLedgerDir(deps.workdir, deps.agentName);
2983
+ if (url.pathname === basePath) {
2984
+ const limit = readPositiveIntQuery(url.searchParams.get("limit"), 20, 100);
2985
+ const tasks = listSoftwareAgentTaskRecords(taskLedgerDir, limit, {
2986
+ contextSessionId: url.searchParams.get("session") || undefined,
2987
+ });
2988
+ writeJsonResponse(res, 200, { tasks }, true);
2989
+ return;
2990
+ }
2991
+ if (url.pathname.startsWith(`${basePath}/`)) {
2992
+ const taskId = decodeURIComponent(url.pathname.slice(basePath.length + 1));
2993
+ if (!taskId || taskId.includes("/")) {
2994
+ writeJsonResponse(res, 400, { error: "Invalid software-agent task id" });
2995
+ return;
2996
+ }
2997
+ const task = readSoftwareAgentTaskRecord(taskLedgerDir, taskId);
2998
+ if (!task) {
2999
+ writeJsonResponse(res, 404, { error: "Software-agent task not found" });
3000
+ return;
3001
+ }
3002
+ let contextSession;
3003
+ if (readBooleanQuery(url.searchParams.get("includeContext")) && task.contextSession?.sessionId) {
3004
+ try {
3005
+ contextSession = readSoftwareAgentContextSession(softwareAgentContextSessionDir(deps.workdir, deps.agentName), task.contextSession.sessionId, { includeContextPacket: true });
3006
+ }
3007
+ catch {
3008
+ contextSession = null;
3009
+ }
3010
+ }
3011
+ writeJsonResponse(res, 200, { task, ...(contextSession ? { contextSession } : {}) }, true);
3012
+ return;
3013
+ }
3014
+ writeJsonResponse(res, 404, { error: "Software-agent task endpoint not found" });
3015
+ }
3016
+ export async function handleLocalTasksHttp(req, res, deps) {
3017
+ if (!requireOwnerRequest(req, res, deps.options))
3018
+ return;
3019
+ const url = new URL(req.url || "/", "http://127.0.0.1");
3020
+ const basePath = "/self/tasks";
3021
+ if (url.pathname === basePath) {
3022
+ const status = readTaskStatusQuery(url.searchParams.get("status"));
3023
+ const route = url.searchParams.get("route") || undefined;
3024
+ const visibility = readTaskVisibilityQuery(url.searchParams.get("visibility"));
3025
+ const parentTaskId = url.searchParams.get("parentTaskId") || undefined;
3026
+ const tasks = await listTaskRegistryRecords({
3027
+ workdir: deps.workdir,
3028
+ agentName: deps.agentName,
3029
+ limit: readPositiveIntQuery(url.searchParams.get("limit"), 50, 200),
3030
+ status,
3031
+ route,
3032
+ visibility,
3033
+ parentTaskId,
3034
+ });
3035
+ const taskViews = tasks.map(createTaskSurfaceView);
3036
+ writeJsonResponse(res, 200, {
3037
+ tasks: tasks.map(createTaskResponseRecord),
3038
+ taskViews,
3039
+ }, true);
3040
+ return;
3041
+ }
3042
+ if (url.pathname.startsWith(`${basePath}/`)) {
3043
+ const taskId = decodeURIComponent(url.pathname.slice(basePath.length + 1));
3044
+ if (!taskId || taskId.includes("/")) {
3045
+ writeJsonResponse(res, 400, { error: "Invalid task id" });
3046
+ return;
3047
+ }
3048
+ const task = await readTaskRegistryRecord({
3049
+ workdir: deps.workdir,
3050
+ agentName: deps.agentName,
3051
+ taskId,
3052
+ });
3053
+ if (!task) {
3054
+ writeJsonResponse(res, 404, { error: "Task not found" });
3055
+ return;
3056
+ }
3057
+ writeJsonResponse(res, 200, {
3058
+ task: createTaskResponseRecord(task),
3059
+ taskView: createTaskSurfaceView(task),
3060
+ }, true);
3061
+ return;
3062
+ }
3063
+ writeJsonResponse(res, 404, { error: "Task endpoint not found" });
3064
+ }
3065
+ export async function handleSoftwareAgentContextSessionsHttp(req, res, deps) {
3066
+ if (!requireOwnerRequest(req, res, deps.options))
3067
+ return;
3068
+ const url = new URL(req.url || "/", "http://127.0.0.1");
3069
+ const basePath = "/self/software-agent/sessions";
3070
+ const contextSessionDir = softwareAgentContextSessionDir(deps.workdir, deps.agentName);
3071
+ if (url.pathname === basePath) {
3072
+ const limit = readPositiveIntQuery(url.searchParams.get("limit"), 20, 100);
3073
+ const sessions = listSoftwareAgentContextSessions(contextSessionDir, limit);
3074
+ writeJsonResponse(res, 200, { sessions }, true);
3075
+ return;
3076
+ }
3077
+ if (url.pathname.startsWith(`${basePath}/`)) {
3078
+ const sessionId = decodeURIComponent(url.pathname.slice(basePath.length + 1));
3079
+ if (!sessionId || sessionId.includes("/")) {
3080
+ writeJsonResponse(res, 400, { error: "Invalid software-agent context session id" });
3081
+ return;
3082
+ }
3083
+ let session;
3084
+ try {
3085
+ session = readSoftwareAgentContextSession(contextSessionDir, sessionId, {
3086
+ includeContextPacket: readBooleanQuery(url.searchParams.get("includeContext")),
3087
+ });
3088
+ }
3089
+ catch (err) {
3090
+ writeJsonResponse(res, 400, { error: err.message || "Invalid software-agent context session id" });
3091
+ return;
3092
+ }
3093
+ if (!session) {
3094
+ writeJsonResponse(res, 404, { error: "Software-agent context session not found" });
3095
+ return;
3096
+ }
3097
+ writeJsonResponse(res, 200, { session }, true);
3098
+ return;
3099
+ }
3100
+ writeJsonResponse(res, 404, { error: "Software-agent context session endpoint not found" });
3101
+ }
3102
+ function softwareAgentTaskLedgerDir(workdir, agentName) {
3103
+ return join(workdir, ".akemon", "agents", agentName, "software-agent", "tasks");
3104
+ }
3105
+ function softwareAgentContextSessionDir(workdir, agentName) {
3106
+ return join(workdir, ".akemon", "agents", agentName, "software-agent", "sessions");
310
3107
  }
311
3108
  function readPositiveIntQuery(value, fallback, max) {
312
3109
  if (!value)
@@ -316,11 +3113,51 @@ function readPositiveIntQuery(value, fallback, max) {
316
3113
  return fallback;
317
3114
  return Math.min(parsed, max);
318
3115
  }
3116
+ function readBooleanQuery(value) {
3117
+ return value === "1" || value === "true" || value === "yes";
3118
+ }
3119
+ function readTaskStatusQuery(value) {
3120
+ if (value === "pending"
3121
+ || value === "running"
3122
+ || value === "waiting"
3123
+ || value === "succeeded"
3124
+ || value === "failed"
3125
+ || value === "info") {
3126
+ return value;
3127
+ }
3128
+ return undefined;
3129
+ }
3130
+ function readTaskVisibilityQuery(value) {
3131
+ if (value === "primary" || value === "technical")
3132
+ return value;
3133
+ return undefined;
3134
+ }
3135
+ function readOptionalBooleanBody(value, field) {
3136
+ if (value === undefined || value === null)
3137
+ return false;
3138
+ if (typeof value !== "boolean")
3139
+ throw new Error(`Invalid ${field}: expected boolean`);
3140
+ return value;
3141
+ }
3142
+ function readOptionalPositiveIntBody(value, field) {
3143
+ if (value === undefined || value === null)
3144
+ return undefined;
3145
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
3146
+ throw new Error(`Invalid ${field}: expected positive integer`);
3147
+ }
3148
+ return value;
3149
+ }
319
3150
  function writeSoftwareAgentStreamEvent(res, event) {
320
3151
  if (res.destroyed)
321
3152
  return;
322
3153
  res.write(`${JSON.stringify(redactSecrets(event))}\n`);
323
3154
  }
3155
+ function writeWorkbenchSseEvent(res, eventName, data) {
3156
+ if (res.destroyed)
3157
+ return;
3158
+ res.write(`event: ${eventName}\n`);
3159
+ res.write(`data: ${JSON.stringify(redactSecrets(data))}\n\n`);
3160
+ }
324
3161
  export async function handleSoftwareAgentResetHttp(req, res, deps) {
325
3162
  if (!requireOwnerRequest(req, res, deps.options))
326
3163
  return;
@@ -328,43 +3165,403 @@ export async function handleSoftwareAgentResetHttp(req, res, deps) {
328
3165
  if (!softwareAgent)
329
3166
  return;
330
3167
  try {
331
- await softwareAgent.resetSession();
332
- writeJsonResponse(res, 200, { ok: true, state: softwareAgent.getState() }, true);
3168
+ await softwareAgent.resetSession();
3169
+ writeJsonResponse(res, 200, { ok: true, state: softwareAgent.getState() }, true);
3170
+ }
3171
+ catch (err) {
3172
+ writeJsonResponse(res, 500, { error: err.message || String(err) });
3173
+ }
3174
+ }
3175
+ export async function handleWorkbenchSessionsHttp(req, res, deps) {
3176
+ if (!requireOwnerRequest(req, res, deps.options))
3177
+ return;
3178
+ const workbench = requireWorkbench(res, deps.workbench);
3179
+ if (!workbench)
3180
+ return;
3181
+ await workbench.ready();
3182
+ const url = new URL(req.url || "/", "http://127.0.0.1");
3183
+ const basePath = "/self/workbench/sessions";
3184
+ const suffix = url.pathname === basePath ? "" : url.pathname.slice(basePath.length + 1);
3185
+ const [sessionId, action] = suffix ? suffix.split("/") : ["", ""];
3186
+ try {
3187
+ if (url.pathname === basePath && req.method === "GET") {
3188
+ writeJsonResponse(res, 200, {
3189
+ sessions: workbench.listSessions(),
3190
+ activeSessionId: workbench.getActiveSessionId(),
3191
+ ...(deps.agentName ? { agentName: deps.agentName } : {}),
3192
+ }, true);
3193
+ return;
3194
+ }
3195
+ if (url.pathname === basePath && req.method === "POST") {
3196
+ const body = await readJsonBody(req);
3197
+ const action = {
3198
+ capability: "start_session",
3199
+ args: normalizeWorkbenchStartRequest(body),
3200
+ reason: "owner used Workbench HTTP start",
3201
+ };
3202
+ const observation = await executeInteractiveSessionViaPeripheral(workbench, deps.interactiveSessionPeripheral, action);
3203
+ await appendOwnerDirectInteractiveSessionEvents({
3204
+ agentName: deps.agentName,
3205
+ workdir: deps.workdir,
3206
+ source: "workbench_http",
3207
+ action,
3208
+ observation,
3209
+ });
3210
+ if (!observation.ok)
3211
+ throw new Error(observation.error || observation.summary);
3212
+ const sessionId = interactiveSessionObservationSessionId(observation, action);
3213
+ const session = sessionId ? workbench.getSession(sessionId) : null;
3214
+ writeJsonResponse(res, 201, { session }, true);
3215
+ return;
3216
+ }
3217
+ if (!sessionId || sessionId.includes("/") || sessionId.includes("\\")) {
3218
+ writeJsonResponse(res, 404, { error: "Workbench session endpoint not found" });
3219
+ return;
3220
+ }
3221
+ if (!action && req.method === "GET") {
3222
+ const session = workbench.getSession(sessionId);
3223
+ if (!session) {
3224
+ writeJsonResponse(res, 404, { error: "Workbench session not found" });
3225
+ return;
3226
+ }
3227
+ writeJsonResponse(res, 200, { session }, true);
3228
+ return;
3229
+ }
3230
+ if (action === "activate" && req.method === "POST") {
3231
+ const session = workbench.setActiveSession(sessionId);
3232
+ writeJsonResponse(res, 200, { ok: true, activeSessionId: session.sessionId, session }, true);
3233
+ return;
3234
+ }
3235
+ if (action === "tail" && req.method === "GET") {
3236
+ const session = workbench.getSession(sessionId);
3237
+ if (!session) {
3238
+ writeJsonResponse(res, 404, { error: "Workbench session not found" });
3239
+ return;
3240
+ }
3241
+ writeJsonResponse(res, 200, { sessionId, tail: session.tail }, true);
3242
+ return;
3243
+ }
3244
+ if (action === "stream" && req.method === "GET") {
3245
+ const session = workbench.getSession(sessionId);
3246
+ if (!session) {
3247
+ writeJsonResponse(res, 404, { error: "Workbench session not found" });
3248
+ return;
3249
+ }
3250
+ res.writeHead(200, {
3251
+ "Content-Type": "text/event-stream; charset=utf-8",
3252
+ "Cache-Control": "no-cache",
3253
+ "Connection": "keep-alive",
3254
+ "X-Accel-Buffering": "no",
3255
+ });
3256
+ res.flushHeaders?.();
3257
+ writeWorkbenchSseEvent(res, "snapshot", { session });
3258
+ let done = false;
3259
+ let unsubscribe = () => { };
3260
+ const cleanup = () => {
3261
+ if (done)
3262
+ return;
3263
+ done = true;
3264
+ unsubscribe();
3265
+ };
3266
+ unsubscribe = workbench.subscribe((event) => {
3267
+ if (event.sessionId !== sessionId || done)
3268
+ return;
3269
+ if (event.type === "output") {
3270
+ writeWorkbenchSseEvent(res, "output", event);
3271
+ return;
3272
+ }
3273
+ if (event.type === "status") {
3274
+ writeWorkbenchSseEvent(res, "status", event);
3275
+ return;
3276
+ }
3277
+ writeWorkbenchSseEvent(res, "exit", event);
3278
+ cleanup();
3279
+ if (!res.writableEnded)
3280
+ res.end();
3281
+ });
3282
+ res.on("close", cleanup);
3283
+ const latestSession = workbench.getSession(sessionId) || session;
3284
+ if (latestSession.status !== "running") {
3285
+ writeWorkbenchSseEvent(res, "exit", {
3286
+ type: "exit",
3287
+ sessionId,
3288
+ exitCode: latestSession.exitCode,
3289
+ signal: latestSession.signal,
3290
+ snapshot: latestSession,
3291
+ });
3292
+ cleanup();
3293
+ if (!res.writableEnded)
3294
+ res.end();
3295
+ }
3296
+ return;
3297
+ }
3298
+ if (action === "input" && req.method === "POST") {
3299
+ const body = await readJsonBody(req);
3300
+ const session = workbench.writeInput(sessionId, normalizeWorkbenchInputRequest(body));
3301
+ writeJsonResponse(res, 200, { ok: true, session }, true);
3302
+ return;
3303
+ }
3304
+ if (action === "input-mode" && req.method === "POST") {
3305
+ const body = await readJsonBody(req);
3306
+ const interactiveAction = {
3307
+ capability: "set_input_mode",
3308
+ args: {
3309
+ sessionId,
3310
+ inputMode: normalizeWorkbenchInputMode(body?.inputMode),
3311
+ },
3312
+ reason: "owner used Workbench HTTP input-mode",
3313
+ };
3314
+ const observation = await executeInteractiveSessionViaPeripheral(workbench, deps.interactiveSessionPeripheral, interactiveAction);
3315
+ await appendOwnerDirectInteractiveSessionEvents({
3316
+ agentName: deps.agentName,
3317
+ workdir: deps.workdir,
3318
+ source: "workbench_http",
3319
+ action: interactiveAction,
3320
+ observation,
3321
+ });
3322
+ if (!observation.ok)
3323
+ throw new Error(observation.error || observation.summary);
3324
+ const session = workbench.getSession(sessionId);
3325
+ writeJsonResponse(res, 200, { ok: true, session }, true);
3326
+ return;
3327
+ }
3328
+ if (action === "resize" && req.method === "POST") {
3329
+ const body = await readJsonBody(req);
3330
+ const resize = normalizeWorkbenchResizeRequest(body);
3331
+ const interactiveAction = {
3332
+ capability: "resize_session",
3333
+ args: {
3334
+ sessionId,
3335
+ cols: resize.cols,
3336
+ rows: resize.rows,
3337
+ },
3338
+ reason: "owner used Workbench HTTP resize",
3339
+ };
3340
+ const observation = await executeInteractiveSessionViaPeripheral(workbench, deps.interactiveSessionPeripheral, interactiveAction);
3341
+ await appendOwnerDirectInteractiveSessionEvents({
3342
+ agentName: deps.agentName,
3343
+ workdir: deps.workdir,
3344
+ source: "workbench_http",
3345
+ action: interactiveAction,
3346
+ observation,
3347
+ });
3348
+ if (!observation.ok)
3349
+ throw new Error(observation.error || observation.summary);
3350
+ const session = workbench.getSession(sessionId);
3351
+ writeJsonResponse(res, 200, { ok: true, session }, true);
3352
+ return;
3353
+ }
3354
+ if (action === "stop" && req.method === "POST") {
3355
+ const interactiveAction = {
3356
+ capability: "stop_session",
3357
+ args: { sessionId },
3358
+ reason: "owner used Workbench HTTP stop",
3359
+ };
3360
+ const observation = await executeInteractiveSessionViaPeripheral(workbench, deps.interactiveSessionPeripheral, interactiveAction);
3361
+ await appendOwnerDirectInteractiveSessionEvents({
3362
+ agentName: deps.agentName,
3363
+ workdir: deps.workdir,
3364
+ source: "workbench_http",
3365
+ action: interactiveAction,
3366
+ observation,
3367
+ });
3368
+ if (!observation.ok)
3369
+ throw new Error(observation.error || observation.summary);
3370
+ const session = workbench.getSession(sessionId);
3371
+ writeJsonResponse(res, 200, { ok: true, session }, true);
3372
+ return;
3373
+ }
3374
+ writeJsonResponse(res, 404, { error: "Workbench session endpoint not found" });
333
3375
  }
334
3376
  catch (err) {
335
- writeJsonResponse(res, 500, { error: err.message || String(err) });
3377
+ const message = err.message || String(err);
3378
+ const status = /not found/i.test(message) ? 404
3379
+ : /limit reached|not running/i.test(message) ? 409
3380
+ : 400;
3381
+ writeJsonResponse(res, status, { error: message });
336
3382
  }
337
3383
  }
338
- import { RelayPeripheral } from "./relay-peripheral.js";
339
- import { EnginePeripheral, LLM_ENGINES as LLM_ENGINES_SET } from "./engine-peripheral.js";
340
- import { EngineQueue } from "./engine-queue.js";
341
- import { BioStateModule } from "./bio-module.js";
342
- import { MemoryModule } from "./memory-module.js";
343
- import { RoleModule } from "./role-module.js";
344
- import { TaskModule } from "./task-module.js";
345
- import { SocialModule } from "./social-module.js";
346
- import { LongTermModule } from "./longterm-module.js";
347
- import { ReflectionModule } from "./reflection-module.js";
348
- import { ScriptModule } from "./script-module.js";
349
- import { FileEventLog, PersistentEventBus } from "./event-bus.js";
350
- import { CodexSoftwareAgentPeripheral, createOwnerTaskEnvelope, listSoftwareAgentTaskRecords, readSoftwareAgentTaskRecord, } from "./software-agent-peripheral.js";
351
- import { buildSoftwareAgentMemorySummary } from "./software-agent-memory.js";
352
- import { SIG, sig } from "./types.js";
353
- import { loadConversation, listConversations, buildLLMContext } from "./context.js";
354
- import { redactSecrets } from "./redaction.js";
355
- import { createMcpServer, initMcpProxy, createMcpProxyServer } from "./mcp-server.js";
356
- import { autoRoute, runCollaborativeQuery } from "./agent-utils.js";
357
3384
  // createMcpServer, initMcpProxy, createMcpProxyServer → see mcp-server.ts
358
3385
  const LLM_ENGINES = LLM_ENGINES_SET;
3386
+ async function handleOwnerActionHttpRoute(req, res, deps) {
3387
+ const requestPath = req.url?.split("?")[0] || "";
3388
+ if (requestPath === "/self/workbench/sessions"
3389
+ || requestPath.startsWith("/self/workbench/sessions/")) {
3390
+ await handleWorkbenchSessionsHttp(req, res, {
3391
+ options: deps.options,
3392
+ workbench: deps.workbench,
3393
+ interactiveSessionPeripheral: deps.interactiveSessionPeripheral,
3394
+ agentName: deps.agentName,
3395
+ workdir: deps.workdir,
3396
+ });
3397
+ return true;
3398
+ }
3399
+ if (req.url === "/self/software-agent/run-stream" && req.method === "POST") {
3400
+ await handleSoftwareAgentRunStreamHttp(req, res, {
3401
+ options: deps.options,
3402
+ workdir: deps.workdir,
3403
+ agentName: deps.agentName,
3404
+ softwareAgent: deps.softwareAgent,
3405
+ });
3406
+ return true;
3407
+ }
3408
+ if (req.url === "/self/software-agent/run" && req.method === "POST") {
3409
+ await handleSoftwareAgentRunHttp(req, res, {
3410
+ options: deps.options,
3411
+ workdir: deps.workdir,
3412
+ agentName: deps.agentName,
3413
+ softwareAgent: deps.softwareAgent,
3414
+ });
3415
+ return true;
3416
+ }
3417
+ if (req.url === "/self/software-agent/status" && req.method === "GET") {
3418
+ await handleSoftwareAgentStatusHttp(req, res, {
3419
+ options: deps.options,
3420
+ softwareAgent: deps.softwareAgent,
3421
+ });
3422
+ return true;
3423
+ }
3424
+ if (req.method === "GET"
3425
+ && (requestPath === "/self/tasks" || requestPath.startsWith("/self/tasks/"))) {
3426
+ await handleLocalTasksHttp(req, res, {
3427
+ options: deps.options,
3428
+ workdir: deps.workdir,
3429
+ agentName: deps.agentName,
3430
+ });
3431
+ return true;
3432
+ }
3433
+ if (req.method === "GET"
3434
+ && (requestPath === "/self/software-agent/sessions" || requestPath.startsWith("/self/software-agent/sessions/"))) {
3435
+ await handleSoftwareAgentContextSessionsHttp(req, res, {
3436
+ options: deps.options,
3437
+ workdir: deps.workdir,
3438
+ agentName: deps.agentName,
3439
+ });
3440
+ return true;
3441
+ }
3442
+ if (req.method === "GET"
3443
+ && (requestPath === "/self/software-agent/tasks" || requestPath.startsWith("/self/software-agent/tasks/"))) {
3444
+ await handleSoftwareAgentTasksHttp(req, res, {
3445
+ options: deps.options,
3446
+ workdir: deps.workdir,
3447
+ agentName: deps.agentName,
3448
+ });
3449
+ return true;
3450
+ }
3451
+ if (req.url === "/self/software-agent/reset" && req.method === "POST") {
3452
+ await handleSoftwareAgentResetHttp(req, res, {
3453
+ options: deps.options,
3454
+ softwareAgent: deps.softwareAgent,
3455
+ });
3456
+ return true;
3457
+ }
3458
+ if (req.url === "/self/message" && req.method === "POST") {
3459
+ await handleLocalAkemonMessageHttp(req, res, {
3460
+ options: deps.options,
3461
+ agentName: deps.agentName,
3462
+ workdir: deps.workdir,
3463
+ localPort: deps.localPort,
3464
+ workbench: deps.workbench,
3465
+ interactiveSessionPeripheral: deps.interactiveSessionPeripheral,
3466
+ requestCompute: deps.requestCompute,
3467
+ passiveReflection: true,
3468
+ });
3469
+ return true;
3470
+ }
3471
+ return false;
3472
+ }
3473
+ async function handleSelfHttpRoute(req, res, deps) {
3474
+ const requestPath = req.url?.split("?")[0] || "";
3475
+ if (req.url === "/self/state" && req.method === "GET") {
3476
+ const state = await getSelfState(deps.workdir, deps.agentName);
3477
+ if (deps.relay?.connected) {
3478
+ try {
3479
+ const agents = await deps.relay.listAgents({ online: true, public: true });
3480
+ const self = agents.find((a) => a.name === deps.agentName);
3481
+ state.credits = self?.credits ?? 0;
3482
+ state.level = self?.level ?? 0;
3483
+ }
3484
+ catch {
3485
+ state.credits = null;
3486
+ }
3487
+ }
3488
+ res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(state, null, 2));
3489
+ return true;
3490
+ }
3491
+ if (requestPath === "/self/peripherals/explore" && req.method === "GET") {
3492
+ const url = new URL(req.url || requestPath, `http://localhost`);
3493
+ const id = url.searchParams.get("id") || undefined;
3494
+ const configured = await loadPeripheralRecords(deps.agentName);
3495
+ const records = mergePeripheralRecords(configured, deps.runtimePeripheralRecords);
3496
+ const briefing = await buildPeripheralExploreBriefing({
3497
+ records,
3498
+ runtimePeripherals: deps.peripherals,
3499
+ id,
3500
+ });
3501
+ res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(briefing, null, 2));
3502
+ return true;
3503
+ }
3504
+ if (requestPath === "/self/peripherals" && req.method === "GET") {
3505
+ const configured = await loadPeripheralRecords(deps.agentName);
3506
+ const records = mergePeripheralRecords(configured, deps.runtimePeripheralRecords);
3507
+ res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify({ records }, null, 2));
3508
+ return true;
3509
+ }
3510
+ if (req.url === "/self/revive" && req.method === "POST") {
3511
+ await reviveAgent(deps.workdir, deps.agentName);
3512
+ res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify({ ok: true, message: "Agent revived. Energy=50, Hunger=50." }));
3513
+ return true;
3514
+ }
3515
+ if (req.url?.startsWith("/self/task-history") && req.method === "GET") {
3516
+ const url = new URL(req.url, `http://localhost`);
3517
+ const limit = parseInt(url.searchParams.get("limit") || "50") || 50;
3518
+ const history = await loadTaskHistory(deps.workdir, deps.agentName, limit);
3519
+ res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(history, null, 2));
3520
+ return true;
3521
+ }
3522
+ if (req.url === "/self/directives" && req.method === "GET") {
3523
+ const dirs = await loadDirectives(deps.workdir, deps.agentName);
3524
+ res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(dirs, null, 2));
3525
+ return true;
3526
+ }
3527
+ if (req.url === "/self/canvas" && req.method === "GET") {
3528
+ const entries = await loadRecentCanvasEntries(deps.workdir, deps.agentName, 10);
3529
+ res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(entries, null, 2));
3530
+ return true;
3531
+ }
3532
+ if (req.url === "/self/conversations" && req.method === "GET") {
3533
+ const list = await listConversations(deps.workdir, deps.agentName);
3534
+ res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(list, null, 2));
3535
+ return true;
3536
+ }
3537
+ if (req.url?.startsWith("/self/conversation/") && req.method === "GET") {
3538
+ const convId = decodeURIComponent(req.url.slice("/self/conversation/".length));
3539
+ if (!convId) {
3540
+ res.writeHead(400).end("Missing conversation ID");
3541
+ return true;
3542
+ }
3543
+ const conv = await loadConversation(deps.workdir, deps.agentName, convId);
3544
+ const config = await loadAgentConfig(deps.workdir, deps.agentName);
3545
+ const budget = config.context_budget ?? 4096;
3546
+ const { recentStartIndex } = buildLLMContext(conv, budget);
3547
+ res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify({
3548
+ summary: conv.summary,
3549
+ rounds: conv.rounds,
3550
+ recentStartIndex,
3551
+ }, null, 2));
3552
+ return true;
3553
+ }
3554
+ return false;
3555
+ }
359
3556
  // ---------------------------------------------------------------------------
360
3557
  // Engine execution — delegates to EnginePeripheral (V2 Step 3)
361
3558
  // ---------------------------------------------------------------------------
362
3559
  /** Unified engine runner — delegates to EnginePeripheral */
363
- function runEngine(engine, model, allowAll, task, workdir, extraAllowedTools, relay, signal, origin, routing, taskId, routeRequest) {
3560
+ function runEngine(engine, model, allowAll, task, workdir, extraAllowedTools, relay, signal, origin, routing, taskId, routeRequest, engineResources) {
364
3561
  if (!_engineP) {
365
3562
  throw new Error("Engine peripheral not initialized");
366
3563
  }
367
- const result = _engineP.runEngine(task, allowAll, extraAllowedTools, signal, origin, routing, taskId, routeRequest);
3564
+ const result = _engineP.runEngine(task, allowAll, extraAllowedTools, signal, origin, routing, taskId, routeRequest, engineResources);
368
3565
  // Sync trace back to module-level for reporting
369
3566
  result.then(() => { lastEngineTrace = _engineP.lastTrace; }).catch(() => { lastEngineTrace = _engineP.lastTrace; });
370
3567
  return result;
@@ -373,14 +3570,18 @@ function runEngine(engine, model, allowAll, task, workdir, extraAllowedTools, re
373
3570
  // pullFromRelay → see relay-peripheral.ts
374
3571
  export async function serve(options) {
375
3572
  const workdir = options.workdir || process.cwd();
3573
+ let registryHeartbeat = null;
376
3574
  // Reclaim stale CLI children from any previous daemon crash before starting.
377
3575
  await scanAndKillOrphans();
378
- // V2: Relay peripheral — unified relay API access
379
- const relay = new RelayPeripheral({
380
- httpUrl: options.relayHttp || "",
381
- secretKey: options.secretKey || "",
382
- agentName: options.agentName,
383
- });
3576
+ // V2: Relay peripheral — unified relay API access. Local-only mode does not
3577
+ // register this peripheral, so modules cannot accidentally use relay APIs.
3578
+ const relay = options.relayHttp
3579
+ ? new RelayPeripheral({
3580
+ httpUrl: options.relayHttp,
3581
+ secretKey: options.secretKey || "",
3582
+ agentName: options.agentName,
3583
+ })
3584
+ : null;
384
3585
  // V2: Engine peripheral — unified engine execution
385
3586
  const engineP = new EnginePeripheral({
386
3587
  engine: options.engine || "claude",
@@ -392,23 +3593,54 @@ export async function serve(options) {
392
3593
  rawMaxRounds: 20,
393
3594
  relay: options.relayHttp ? { http: options.relayHttp, agentName: options.agentName } : undefined,
394
3595
  });
3596
+ options.relay = relay ?? undefined;
3597
+ options.enginePeripheral = engineP;
3598
+ _engineP = engineP;
395
3599
  // Expose port to engine subprocesses so they can callback to local MCP server
396
3600
  process.env.AKEMON_PORT = String(options.port);
397
3601
  if (options.key)
398
3602
  process.env.AKEMON_KEY = options.key;
399
- // Initialize MCP proxy if --mcp-server specified
3603
+ // Initialize MCP proxy lazily so HTTP startup is not gated by child MCP boot.
400
3604
  let mcpProxy = null;
401
- if (options.mcpServer) {
402
- try {
403
- mcpProxy = await initMcpProxy(options.mcpServer, workdir);
404
- }
405
- catch (err) {
406
- console.error(`[mcp-proxy] Failed to start child MCP server: ${err.message}`);
407
- process.exit(1);
3605
+ let mcpProxyPromise = null;
3606
+ const ensureMcpProxy = () => {
3607
+ if (!options.mcpServer)
3608
+ return Promise.resolve(null);
3609
+ if (mcpProxy)
3610
+ return Promise.resolve(mcpProxy);
3611
+ if (!mcpProxyPromise) {
3612
+ mcpProxyPromise = initMcpProxy(options.mcpServer, workdir)
3613
+ .then((proxy) => {
3614
+ mcpProxy = proxy;
3615
+ return proxy;
3616
+ })
3617
+ .catch((err) => {
3618
+ console.error(`[mcp-proxy] Failed to start child MCP server: ${err.message || String(err)}`);
3619
+ process.exit(1);
3620
+ });
408
3621
  }
409
- }
3622
+ return mcpProxyPromise;
3623
+ };
410
3624
  const sessions = new Map();
411
3625
  const publisherIds = new Map();
3626
+ const interactiveSessions = new InteractiveSessionManager({
3627
+ agentName: options.agentName,
3628
+ baseWorkdir: workdir,
3629
+ });
3630
+ const workbench = interactiveSessions;
3631
+ const interactiveSessionPeripheral = new InteractiveSessionPeripheral(interactiveSessions);
3632
+ workbench.ready()
3633
+ .then(() => console.log("[workbench] session recovery ready"))
3634
+ .catch((error) => logBestEffortError("workbench session recovery", error));
3635
+ workbench.subscribe((event) => {
3636
+ if (event.type !== "exit")
3637
+ return;
3638
+ appendInteractiveSessionOutcomeEvent({
3639
+ agentName: options.agentName,
3640
+ workdir,
3641
+ event,
3642
+ }).catch((error) => logBestEffortError("interactive session outcome event append", error));
3643
+ });
412
3644
  // Build deps for MCP server
413
3645
  const mcpDeps = {
414
3646
  runEngine,
@@ -426,52 +3658,37 @@ export async function serve(options) {
426
3658
  emitTaskCompleted,
427
3659
  };
428
3660
  let codexSoftwareAgent = null;
3661
+ let peripherals = [
3662
+ ...(relay ? [relay] : []),
3663
+ engineP,
3664
+ interactiveSessionPeripheral,
3665
+ ];
3666
+ let runtimePeripheralRecords = [];
3667
+ let requestCompute = async () => ({
3668
+ success: false,
3669
+ error: "Akemon runtime is still initializing. Please retry in a moment.",
3670
+ });
3671
+ let bus = null;
3672
+ const allModules = [];
429
3673
  const httpServer = createServer(async (req, res) => {
430
3674
  // Suppress noisy polling endpoints from log
431
3675
  const isQuiet = req.url === "/self/state" || req.url?.startsWith("/self/state?");
432
3676
  if (!isQuiet)
433
3677
  console.log(`[http] ${req.method} ${req.url} session=${req.headers["mcp-session-id"] || "none"}`);
434
3678
  try {
435
- if (req.url === "/self/software-agent/run-stream" && req.method === "POST") {
436
- await handleSoftwareAgentRunStreamHttp(req, res, {
437
- options,
438
- workdir,
439
- agentName: options.agentName,
440
- softwareAgent: codexSoftwareAgent,
441
- });
3679
+ if (await handleOwnerActionHttpRoute(req, res, {
3680
+ options,
3681
+ workdir,
3682
+ agentName: options.agentName,
3683
+ localPort: options.port,
3684
+ softwareAgent: codexSoftwareAgent,
3685
+ workbench,
3686
+ interactiveSessionPeripheral,
3687
+ requestCompute,
3688
+ })) {
442
3689
  return;
443
3690
  }
444
- if (req.url === "/self/software-agent/run" && req.method === "POST") {
445
- await handleSoftwareAgentRunHttp(req, res, {
446
- options,
447
- workdir,
448
- agentName: options.agentName,
449
- softwareAgent: codexSoftwareAgent,
450
- });
451
- return;
452
- }
453
- if (req.url === "/self/software-agent/status" && req.method === "GET") {
454
- await handleSoftwareAgentStatusHttp(req, res, {
455
- options,
456
- softwareAgent: codexSoftwareAgent,
457
- });
458
- return;
459
- }
460
- const requestPath = req.url?.split("?")[0] || "";
461
- if (req.method === "GET"
462
- && (requestPath === "/self/software-agent/tasks" || requestPath.startsWith("/self/software-agent/tasks/"))) {
463
- await handleSoftwareAgentTasksHttp(req, res, {
464
- options,
465
- workdir,
466
- agentName: options.agentName,
467
- });
468
- return;
469
- }
470
- if (req.url === "/self/software-agent/reset" && req.method === "POST") {
471
- await handleSoftwareAgentResetHttp(req, res, {
472
- options,
473
- softwareAgent: codexSoftwareAgent,
474
- });
3691
+ if (await handleLocalUiAssetRoute(req, res)) {
475
3692
  return;
476
3693
  }
477
3694
  // Auth check
@@ -484,89 +3701,13 @@ export async function serve(options) {
484
3701
  return;
485
3702
  }
486
3703
  }
487
- // Live agent life visualization
488
- if ((req.url === "/live" || req.url === "/live/") && req.method === "GET") {
489
- try {
490
- const { readFile: rf } = await import("fs/promises");
491
- const { fileURLToPath } = await import("url");
492
- const { dirname, join: pjoin } = await import("path");
493
- const __filename = fileURLToPath(import.meta.url);
494
- const __dirname = dirname(__filename);
495
- let html;
496
- try {
497
- html = await rf(pjoin(__dirname, "live.html"), "utf-8");
498
- }
499
- catch {
500
- html = await rf(pjoin(__dirname, "..", "src", "live.html"), "utf-8");
501
- }
502
- res.writeHead(200, { "Content-Type": "text/html" }).end(html);
503
- }
504
- catch (err) {
505
- res.writeHead(500).end("Live page not found: " + err.message);
506
- }
507
- return;
508
- }
509
- // Self-state API (no auth required for local monitoring)
510
- if (req.url === "/self/state" && req.method === "GET") {
511
- const state = await getSelfState(workdir, options.agentName);
512
- // Enrich with credits from relay (best-effort)
513
- if (relay.connected) {
514
- try {
515
- const agents = await relay.listAgents({ online: true, public: true });
516
- const self = agents.find((a) => a.name === options.agentName);
517
- state.credits = self?.credits ?? 0;
518
- state.level = self?.level ?? 0;
519
- }
520
- catch {
521
- state.credits = null;
522
- }
523
- }
524
- res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(state, null, 2));
525
- return;
526
- }
527
- // Revive endpoint — owner brings a forced-offline agent back
528
- if (req.url === "/self/revive" && req.method === "POST") {
529
- await reviveAgent(workdir, options.agentName);
530
- res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify({ ok: true, message: "Agent revived. Energy=50, Hunger=50." }));
531
- return;
532
- }
533
- if (req.url?.startsWith("/self/task-history") && req.method === "GET") {
534
- const url = new URL(req.url, `http://localhost`);
535
- const limit = parseInt(url.searchParams.get("limit") || "50") || 50;
536
- const history = await loadTaskHistory(workdir, options.agentName, limit);
537
- res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(history, null, 2));
538
- return;
539
- }
540
- if (req.url === "/self/directives" && req.method === "GET") {
541
- const dirs = await loadDirectives(workdir, options.agentName);
542
- res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(dirs, null, 2));
543
- return;
544
- }
545
- if (req.url === "/self/canvas" && req.method === "GET") {
546
- const entries = await loadRecentCanvasEntries(workdir, options.agentName, 10);
547
- res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(entries, null, 2));
548
- return;
549
- }
550
- if (req.url === "/self/conversations" && req.method === "GET") {
551
- const list = await listConversations(workdir, options.agentName);
552
- res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(list, null, 2));
553
- return;
554
- }
555
- if (req.url?.startsWith("/self/conversation/") && req.method === "GET") {
556
- const convId = decodeURIComponent(req.url.slice("/self/conversation/".length));
557
- if (!convId) {
558
- res.writeHead(400).end("Missing conversation ID");
559
- return;
560
- }
561
- const conv = await loadConversation(workdir, options.agentName, convId);
562
- const config = await loadAgentConfig(workdir, options.agentName);
563
- const budget = config.context_budget ?? 4096;
564
- const { recentStartIndex } = buildLLMContext(conv, budget);
565
- res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify({
566
- summary: conv.summary,
567
- rounds: conv.rounds,
568
- recentStartIndex,
569
- }, null, 2));
3704
+ if (await handleSelfHttpRoute(req, res, {
3705
+ workdir,
3706
+ agentName: options.agentName,
3707
+ relay,
3708
+ peripherals,
3709
+ runtimePeripheralRecords,
3710
+ })) {
570
3711
  return;
571
3712
  }
572
3713
  // Track publisher ID per session
@@ -595,8 +3736,9 @@ export async function serve(options) {
595
3736
  publisherIds.delete(sid);
596
3737
  }
597
3738
  };
598
- if (mcpProxy) {
599
- const proxyServer = createMcpProxyServer(mcpProxy, options.agentName);
3739
+ const activeMcpProxy = await ensureMcpProxy();
3740
+ if (activeMcpProxy) {
3741
+ const proxyServer = createMcpProxyServer(activeMcpProxy, options.agentName);
600
3742
  await proxyServer.connect(transport);
601
3743
  }
602
3744
  else {
@@ -611,7 +3753,7 @@ export async function serve(options) {
611
3753
  relayHttp: options.relayHttp,
612
3754
  secretKey: options.secretKey,
613
3755
  publisherIds,
614
- relay,
3756
+ relay: relay ?? undefined,
615
3757
  }, mcpDeps);
616
3758
  await mcpServer.connect(transport);
617
3759
  }
@@ -634,211 +3776,325 @@ export async function serve(options) {
634
3776
  console.log(`Akemon MCP server running on port ${options.port}`);
635
3777
  console.log(`Agent: ${options.agentName}`);
636
3778
  console.log(`Workdir: ${workdir}`);
3779
+ console.log("[startup] HTTP ready; runtime initialization continues in background");
3780
+ registerLocalInstance({
3781
+ name: options.agentName,
3782
+ port: options.port,
3783
+ workdir,
3784
+ mode: options.relayHttp ? "relay" : "local-only",
3785
+ relayHttp: options.relayHttp,
3786
+ }).then((record) => {
3787
+ console.log(`[local] Registered ${record.name} at ${record.endpoint}`);
3788
+ }).catch((err) => {
3789
+ console.log(`[local] Instance registry update failed: ${err.message || String(err)}`);
3790
+ });
3791
+ registryHeartbeat = setInterval(() => {
3792
+ touchLocalInstance(options.agentName).catch(() => { });
3793
+ }, 15_000);
3794
+ void ensureMcpProxy();
637
3795
  });
638
- // Initialize agent config + consciousness (world knowledge + bio-state + guide)
639
- initAgentConfig(workdir, options.agentName).catch(err => console.log(`[self] Config init failed: ${err}`));
640
- loadAgentConfig(workdir, options.agentName).then(c => {
641
- const flags = Object.entries(c).filter(([, v]) => v).map(([k]) => k).join(", ");
642
- console.log(`[config] Features: ${flags || "(none)"}`);
643
- }).catch(() => { });
644
- initWorld(workdir, options.agentName, options.engine || "unknown").catch(err => console.log(`[self] World init failed: ${err}`));
645
- initBioState(workdir, options.agentName).catch(err => console.log(`[self] Bio init failed: ${err}`));
646
- if (options.relayHttp) {
647
- initGuide(workdir, options.agentName, options.relayHttp).catch(err => console.log(`[self] Guide init failed: ${err}`));
648
- }
649
- // Pull games/notes/pages from relay to restore local data
650
- if (options.relayHttp) {
651
- relay.pullFromRelay(workdir, options.agentName).catch(err => console.log(`[sync] Pull from relay failed: ${err}`));
652
- }
653
- // V2: Shared module context + persistent EventBus.
654
- // This is the durable spine for module/peripheral/engine activity; recovery
655
- // side effects are intentionally deferred until the event schema stabilizes.
656
- const eventLogDir = join(workdir, ".akemon", "agents", options.agentName, "events");
657
- await mkdir(eventLogDir, { recursive: true });
658
- const eventLogPath = join(eventLogDir, "events.jsonl");
659
- const bus = new PersistentEventBus(new FileEventLog(eventLogPath));
660
- console.log(`[v2] Event log: ${eventLogPath}`);
661
- codexSoftwareAgent = new CodexSoftwareAgentPeripheral({
662
- workdir,
663
- model: process.env.AKEMON_CODEX_MODEL,
664
- sandbox: "workspace-write",
665
- taskLedgerDir: softwareAgentTaskLedgerDir(workdir, options.agentName),
666
- contextSessionDir: softwareAgentContextSessionDir(workdir, options.agentName),
667
- envPolicy: options.softwareAgentEnvPolicy,
668
- envAllowlist: options.softwareAgentEnvAllowlist,
669
- });
670
- // Peripheral registry — Core routes by capability
671
- const peripherals = [relay, engineP, codexSoftwareAgent];
672
- for (const peripheral of peripherals) {
673
- await peripheral.start(bus);
674
- }
675
- // requestCompute: acquire the engine slot (priority-aware), execute with a
676
- // hard timeout, and release. The slot release and subprocess kill are both
677
- // driven by the same AbortController so a stuck engine can't hold the lock.
678
- async function requestCompute(req) {
679
- // Load latest agent config for routing table (fast file read, changes rarely)
680
- const agentCfg = await loadAgentConfig(workdir, options.agentName);
681
- const routing = agentCfg.engine_routing;
682
- // user_manual tasks also hold a subscription-CLI semaphore to cap claude CLI concurrency
683
- const isUserManual = req.origin === "user_manual";
684
- if (isUserManual) {
685
- try {
686
- await engineQueue.acquireUserManualSlot(ENGINE_WAIT_DEADLINE_MS);
687
- }
688
- catch (err) {
689
- return { success: false, error: err.message || "User manual slot timeout" };
690
- }
3796
+ const initializeRuntime = async () => {
3797
+ const runtimeStartedAt = Date.now();
3798
+ console.log("[startup] Runtime initialization background started");
3799
+ // Initialize agent config + consciousness (world knowledge + bio-state + guide)
3800
+ initAgentConfig(workdir, options.agentName).catch(err => console.log(`[self] Config init failed: ${err}`));
3801
+ loadAgentConfig(workdir, options.agentName).then(c => {
3802
+ const flags = Object.entries(c).filter(([, v]) => v).map(([k]) => k).join(", ");
3803
+ console.log(`[config] Features: ${flags || "(none)"}`);
3804
+ }).catch(() => { });
3805
+ initWorld(workdir, options.agentName, engineP.engineName || "unknown").catch(err => console.log(`[self] World init failed: ${err}`));
3806
+ initBioState(workdir, options.agentName).catch(err => console.log(`[self] Bio init failed: ${err}`));
3807
+ if (options.relayHttp) {
3808
+ initGuide(workdir, options.agentName, options.relayHttp).catch(err => console.log(`[self] Guide init failed: ${err}`));
691
3809
  }
692
- try {
693
- await engineQueue.acquire(req.priority, ENGINE_WAIT_DEADLINE_MS);
3810
+ // Pull games/notes/pages from relay to restore local data
3811
+ if (relay) {
3812
+ relay.pullFromRelay(workdir, options.agentName).catch(err => console.log(`[sync] Pull from relay failed: ${err}`));
694
3813
  }
695
- catch (err) {
696
- if (isUserManual)
697
- engineQueue.releaseUserManualSlot();
698
- return { success: false, error: err.message || "Engine busy timeout" };
699
- }
700
- const prompt = req.context
701
- ? `${req.context}\n\n---\n\n${req.question}`
702
- : req.question;
703
- const abortController = new AbortController();
704
- const timer = setTimeout(() => abortController.abort(), ENGINE_EXEC_TIMEOUT_MS);
3814
+ // V2: Shared module context + persistent EventBus.
3815
+ // This is the durable spine for module/peripheral/engine activity; recovery
3816
+ // side effects are intentionally deferred until the event schema stabilizes.
3817
+ const eventLogDir = join(workdir, ".akemon", "agents", options.agentName, "events");
3818
+ await mkdir(eventLogDir, { recursive: true });
3819
+ const eventLogPath = join(eventLogDir, "events.jsonl");
3820
+ bus = new PersistentEventBus(new FileEventLog(eventLogPath));
3821
+ const runtimeBus = bus;
3822
+ console.log(`[v2] Event log: ${eventLogPath}`);
3823
+ codexSoftwareAgent = new CodexSoftwareAgentPeripheral({
3824
+ agentName: options.agentName,
3825
+ workdir,
3826
+ model: process.env.AKEMON_CODEX_MODEL,
3827
+ sandbox: "workspace-write",
3828
+ taskLedgerDir: softwareAgentTaskLedgerDir(workdir, options.agentName),
3829
+ contextSessionDir: softwareAgentContextSessionDir(workdir, options.agentName),
3830
+ workMemoryDir: workMemoryDir(workdir, options.agentName),
3831
+ envPolicy: options.softwareAgentEnvPolicy,
3832
+ envAllowlist: options.softwareAgentEnvAllowlist,
3833
+ });
3834
+ // Peripheral registry — Core routes by capability
3835
+ peripherals = [
3836
+ ...(relay ? [relay] : []),
3837
+ engineP,
3838
+ codexSoftwareAgent,
3839
+ interactiveSessionPeripheral,
3840
+ ];
3841
+ runtimePeripheralRecords = [
3842
+ ...(relay ? [createRuntimePeripheralRecord(relay, {
3843
+ type: "relay",
3844
+ riskLevel: "high",
3845
+ allowedActions: ["relay:read", "relay:publish", "relay:order", "relay:market"],
3846
+ url: options.relayHttp,
3847
+ exploreDescription: "Summarizes relay orders, tasks, market, and public network feed.",
3848
+ status: "available",
3849
+ })] : []),
3850
+ createRuntimePeripheralRecord(engineP, {
3851
+ type: "engine",
3852
+ riskLevel: "low",
3853
+ allowedActions: ["compute:text"],
3854
+ startCommand: engineP.engineName,
3855
+ exploreDescription: "Primary text compute engine used through Akemon's engine queue.",
3856
+ status: "available",
3857
+ }),
3858
+ createRuntimePeripheralRecord(codexSoftwareAgent, {
3859
+ type: "software-agent",
3860
+ riskLevel: "high",
3861
+ allowedActions: ["repo:inspect", "repo:edit", "command:run", "skill:use"],
3862
+ startCommand: "codex exec",
3863
+ exploreDescription: "Owner-local Codex CLI software-agent peripheral for repository work.",
3864
+ status: "available",
3865
+ }),
3866
+ createRuntimePeripheralRecord(interactiveSessionPeripheral, {
3867
+ type: "interactive-session",
3868
+ riskLevel: "high",
3869
+ allowedActions: [
3870
+ "session:list",
3871
+ "session:inspect",
3872
+ "session:start",
3873
+ "session:input",
3874
+ "session:resize",
3875
+ "session:stop",
3876
+ "session:capture-output",
3877
+ ],
3878
+ exploreDescription: "Owner-local interactive sessions for Codex, Claude, shell, cursor, and custom PTY processes.",
3879
+ status: "available",
3880
+ }),
3881
+ ];
705
3882
  try {
706
- const response = await runEngine(options.engine || "claude", options.model, options.allowAll, prompt, workdir, req.tools, req.relay, abortController.signal, req.origin, routing, req.taskId, req.engineHints);
707
- emitTokenUsage(prompt.length, response.length);
708
- return { success: true, response };
3883
+ await upsertPeripheralRecords(options.agentName, runtimePeripheralRecords.map((record) => ({ ...record, status: "configured" })));
709
3884
  }
710
- catch (err) {
711
- const msg = abortController.signal.aborted
712
- ? `Engine execution timeout (${Math.round(ENGINE_EXEC_TIMEOUT_MS / 60000)} min)`
713
- : (err.message || String(err));
714
- return { success: false, error: msg };
3885
+ catch (error) {
3886
+ logBestEffortError("peripheral registry sync", error);
715
3887
  }
716
- finally {
717
- clearTimeout(timer);
718
- engineQueue.release();
719
- if (isUserManual)
720
- engineQueue.releaseUserManualSlot();
3888
+ for (const peripheral of peripherals) {
3889
+ await peripheral.start(runtimeBus);
721
3890
  }
722
- }
723
- const moduleCtx = {
724
- bus,
725
- agentName: options.agentName,
726
- workdir,
727
- getPeripherals: (capability) => peripherals.filter(p => p.capabilities.includes(capability)),
728
- sendTo: async (capability, signal) => {
729
- const p = peripherals.find(p => p.capabilities.includes(capability));
730
- return p ? p.send(signal) : null;
731
- },
732
- requestCompute,
733
- getPromptContributions: () => {
734
- const contributions = [];
735
- for (const m of allModules) {
736
- if (m.promptContribution) {
737
- const c = m.promptContribution();
738
- if (c)
739
- contributions.push(c);
3891
+ // requestCompute: acquire the engine slot (priority-aware), execute with a
3892
+ // hard timeout, and release. The slot release and subprocess kill are both
3893
+ // driven by the same AbortController so a stuck engine can't hold the lock.
3894
+ requestCompute = async (req) => {
3895
+ // Load latest agent config for routing table (fast file read, changes rarely)
3896
+ const agentCfg = await loadAgentConfig(workdir, options.agentName);
3897
+ const routing = agentCfg.engine_routing;
3898
+ const engineResources = agentCfg.engine_resources;
3899
+ // user_manual tasks also hold a subscription-CLI semaphore to cap claude CLI concurrency
3900
+ const isUserManual = req.origin === "user_manual";
3901
+ if (isUserManual) {
3902
+ try {
3903
+ await engineQueue.acquireUserManualSlot(ENGINE_WAIT_DEADLINE_MS);
3904
+ }
3905
+ catch (err) {
3906
+ return { success: false, error: err.message || "User manual slot timeout" };
740
3907
  }
741
3908
  }
742
- return contributions;
743
- },
3909
+ try {
3910
+ await engineQueue.acquire(req.priority, ENGINE_WAIT_DEADLINE_MS);
3911
+ }
3912
+ catch (err) {
3913
+ if (isUserManual)
3914
+ engineQueue.releaseUserManualSlot();
3915
+ return { success: false, error: err.message || "Engine busy timeout" };
3916
+ }
3917
+ const prompt = req.context
3918
+ ? `${req.context}\n\n---\n\n${req.question}`
3919
+ : req.question;
3920
+ const abortController = new AbortController();
3921
+ const timer = setTimeout(() => abortController.abort(), ENGINE_EXEC_TIMEOUT_MS);
3922
+ const startedAt = Date.now();
3923
+ let outputChars = 0;
3924
+ let success = false;
3925
+ let errorSummary;
3926
+ let result;
3927
+ try {
3928
+ const response = await runEngine(options.engine || "claude", options.model, options.allowAll, prompt, workdir, req.tools, req.relay, abortController.signal, req.origin, routing, req.taskId, {
3929
+ ...req.engineHints,
3930
+ ...(req.taskKind ? { taskKind: req.taskKind } : {}),
3931
+ }, engineResources);
3932
+ success = true;
3933
+ outputChars = response.length;
3934
+ emitTokenUsage(prompt.length, response.length);
3935
+ result = { success: true, response };
3936
+ }
3937
+ catch (err) {
3938
+ const msg = abortController.signal.aborted
3939
+ ? `Engine execution timeout (${Math.round(ENGINE_EXEC_TIMEOUT_MS / 60000)} min)`
3940
+ : (err.message || String(err));
3941
+ errorSummary = msg;
3942
+ result = { success: false, error: msg };
3943
+ }
3944
+ finally {
3945
+ clearTimeout(timer);
3946
+ engineQueue.release();
3947
+ if (isUserManual)
3948
+ engineQueue.releaseUserManualSlot();
3949
+ const audit = await appendComputeAuditEvent({
3950
+ agentName: options.agentName,
3951
+ workdir,
3952
+ req,
3953
+ promptChars: prompt.length,
3954
+ outputChars,
3955
+ durationMs: Date.now() - startedAt,
3956
+ success,
3957
+ error: errorSummary,
3958
+ invocation: _engineP?.lastInvocation || null,
3959
+ });
3960
+ result.audit = audit;
3961
+ }
3962
+ return result;
3963
+ };
3964
+ const moduleCtx = {
3965
+ bus: runtimeBus,
3966
+ agentName: options.agentName,
3967
+ workdir,
3968
+ getPeripherals: (capability) => peripherals.filter(p => p.capabilities.includes(capability)),
3969
+ sendTo: async (capability, signal) => {
3970
+ const p = peripherals.find(p => p.capabilities.includes(capability));
3971
+ return p ? p.send(signal) : null;
3972
+ },
3973
+ requestCompute,
3974
+ getPromptContributions: () => {
3975
+ const contributions = [];
3976
+ for (const m of allModules) {
3977
+ if (m.promptContribution) {
3978
+ const c = m.promptContribution();
3979
+ if (c)
3980
+ contributions.push(c);
3981
+ }
3982
+ }
3983
+ return contributions;
3984
+ },
3985
+ };
3986
+ // V2: Conditionally load modules based on --with/--without
3987
+ const enabled = options.enabledModules ?? ["biostate", "memory", "role", "task", "social", "longterm", "reflection", "script"];
3988
+ const loadedModules = [];
3989
+ _bus = bus;
3990
+ const startModule = async (id, create, afterCreate) => {
3991
+ if (!enabled.includes(id))
3992
+ return;
3993
+ const startedAt = Date.now();
3994
+ console.log(`[v2] Module init: ${id}...`);
3995
+ const module = create();
3996
+ afterCreate?.(module);
3997
+ await module.start(moduleCtx);
3998
+ allModules.push(module);
3999
+ loadedModules.push(id);
4000
+ console.log(`[v2] Module ready: ${id} (${Date.now() - startedAt}ms)`);
4001
+ };
4002
+ const startModules = async () => {
4003
+ const startedAt = Date.now();
4004
+ console.log(`[v2] Module init background: ${enabled.join(", ") || "(none)"}`);
4005
+ await startModule("biostate", () => {
4006
+ const bioModule = new BioStateModule();
4007
+ options.bioModule = bioModule;
4008
+ return bioModule;
4009
+ });
4010
+ await startModule("memory", () => {
4011
+ const memoryModule = new MemoryModule();
4012
+ if (options.cycleInterval)
4013
+ memoryModule.cycleIntervalMs = options.cycleInterval * 60 * 1000;
4014
+ options.memoryModule = memoryModule;
4015
+ return memoryModule;
4016
+ });
4017
+ await startModule("role", () => new RoleModule());
4018
+ await startModule("task", () => {
4019
+ const taskModule = new TaskModule();
4020
+ taskModule.relayHttp = options.relayHttp || "";
4021
+ taskModule.secretKey = options.secretKey || "";
4022
+ taskModule.engine = options.engine || "";
4023
+ taskModule.model = options.model;
4024
+ taskModule.allowAll = options.allowAll;
4025
+ taskModule.notifyUrl = options.notifyUrl;
4026
+ _taskModule = taskModule;
4027
+ return taskModule;
4028
+ });
4029
+ await startModule("social", () => new SocialModule());
4030
+ await startModule("longterm", () => new LongTermModule());
4031
+ await startModule("reflection", () => new ReflectionModule());
4032
+ await startModule("script", () => {
4033
+ const scriptModule = new ScriptModule();
4034
+ scriptModule.scriptName = options.scriptName || "daily-life";
4035
+ scriptModule.relayHttp = options.relayHttp || "";
4036
+ scriptModule.secretKey = options.secretKey || "";
4037
+ return scriptModule;
4038
+ });
4039
+ console.log(`[v2] Modules: ${loadedModules.join(", ") || "(none)"} (${Date.now() - startedAt}ms)`);
4040
+ // V2: Emit agent start lifecycle event after modules have subscribed.
4041
+ runtimeBus.emit(SIG.AGENT_START, sig(SIG.AGENT_START, {
4042
+ agentName: options.agentName,
4043
+ engine: engineP.engineName,
4044
+ modules: loadedModules,
4045
+ }));
4046
+ };
4047
+ setTimeout(() => {
4048
+ startModules().catch((error) => {
4049
+ console.error(`[v2] Module init failed: ${error instanceof Error ? error.message : String(error)}`);
4050
+ process.exit(1);
4051
+ });
4052
+ }, 100);
4053
+ console.log(`[startup] Runtime core ready (${Date.now() - runtimeStartedAt}ms)`);
744
4054
  };
745
- // V2: Conditionally load modules based on --with/--without
746
- const enabled = options.enabledModules ?? ["biostate", "memory", "role", "task", "social", "longterm", "reflection", "script"];
747
- const loadedModules = [];
748
- const allModules = [];
749
- if (enabled.includes("biostate")) {
750
- const bioModule = new BioStateModule();
751
- await bioModule.start(moduleCtx);
752
- options.bioModule = bioModule;
753
- allModules.push(bioModule);
754
- loadedModules.push("biostate");
755
- }
756
- if (enabled.includes("memory")) {
757
- const memoryModule = new MemoryModule();
758
- if (options.cycleInterval)
759
- memoryModule.cycleIntervalMs = options.cycleInterval * 60 * 1000;
760
- await memoryModule.start(moduleCtx);
761
- options.memoryModule = memoryModule;
762
- allModules.push(memoryModule);
763
- loadedModules.push("memory");
764
- }
765
- if (enabled.includes("role")) {
766
- const roleModule = new RoleModule();
767
- await roleModule.start(moduleCtx);
768
- allModules.push(roleModule);
769
- loadedModules.push("role");
770
- }
771
- if (enabled.includes("task")) {
772
- const taskModule = new TaskModule();
773
- taskModule.relayHttp = options.relayHttp || "";
774
- taskModule.secretKey = options.secretKey || "";
775
- taskModule.engine = options.engine || "";
776
- taskModule.model = options.model;
777
- taskModule.allowAll = options.allowAll;
778
- taskModule.notifyUrl = options.notifyUrl;
779
- await taskModule.start(moduleCtx);
780
- _taskModule = taskModule;
781
- allModules.push(taskModule);
782
- loadedModules.push("task");
783
- }
784
- if (enabled.includes("social")) {
785
- const socialModule = new SocialModule();
786
- await socialModule.start(moduleCtx);
787
- allModules.push(socialModule);
788
- loadedModules.push("social");
789
- }
790
- if (enabled.includes("longterm")) {
791
- const longtermModule = new LongTermModule();
792
- await longtermModule.start(moduleCtx);
793
- allModules.push(longtermModule);
794
- loadedModules.push("longterm");
795
- }
796
- if (enabled.includes("reflection")) {
797
- const reflectionModule = new ReflectionModule();
798
- await reflectionModule.start(moduleCtx);
799
- allModules.push(reflectionModule);
800
- loadedModules.push("reflection");
801
- }
802
- if (enabled.includes("script")) {
803
- const scriptModule = new ScriptModule();
804
- scriptModule.scriptName = options.scriptName || "daily-life";
805
- scriptModule.relayHttp = options.relayHttp || "";
806
- scriptModule.secretKey = options.secretKey || "";
807
- await scriptModule.start(moduleCtx);
808
- allModules.push(scriptModule);
809
- loadedModules.push("script");
810
- }
811
- console.log(`[v2] Modules: ${loadedModules.join(", ") || "(none)"}`);
812
- // Inject peripherals into options
813
- options.relay = relay;
814
- options.enginePeripheral = engineP;
815
- _engineP = engineP;
816
- _bus = bus;
817
- // V2: Emit agent start lifecycle event
818
- bus.emit(SIG.AGENT_START, sig(SIG.AGENT_START, {
819
- agentName: options.agentName,
820
- engine: options.engine,
821
- modules: loadedModules,
822
- }));
4055
+ setTimeout(() => {
4056
+ initializeRuntime().catch((error) => {
4057
+ console.error(`[startup] Runtime init failed: ${error instanceof Error ? error.message : String(error)}`);
4058
+ process.exit(1);
4059
+ });
4060
+ }, 0);
823
4061
  // Graceful shutdown — kill engine children first, then stop modules.
824
4062
  // Engine children are killed via process group signal so any sub-forks
825
4063
  // (e.g. opencode's internal Node workers) are also terminated.
826
4064
  const shutdown = async () => {
827
4065
  console.log("[v2] Shutting down...");
4066
+ if (registryHeartbeat)
4067
+ clearInterval(registryHeartbeat);
4068
+ try {
4069
+ await unregisterLocalInstance(options.agentName);
4070
+ }
4071
+ catch (error) {
4072
+ logBestEffortError("local instance unregister", error);
4073
+ }
828
4074
  if (_engineP)
829
4075
  _engineP.killAllChildren();
830
- bus.emit(SIG.AGENT_STOP, sig(SIG.AGENT_STOP, {}));
4076
+ bus?.emit(SIG.AGENT_STOP, sig(SIG.AGENT_STOP, {}));
831
4077
  for (const m of allModules) {
832
4078
  try {
833
4079
  await m.stop();
834
4080
  }
835
- catch { }
4081
+ catch (error) {
4082
+ logBestEffortError(`module stop ${m.id}`, error);
4083
+ }
4084
+ }
4085
+ try {
4086
+ await workbench.shutdown();
4087
+ }
4088
+ catch (error) {
4089
+ logBestEffortError("workbench shutdown", error);
836
4090
  }
837
4091
  try {
838
4092
  await codexSoftwareAgent?.stop();
839
4093
  }
840
- catch { }
841
- bus.getLog().close();
4094
+ catch (error) {
4095
+ logBestEffortError("software-agent stop", error);
4096
+ }
4097
+ bus?.getLog().close();
842
4098
  process.exit(0);
843
4099
  };
844
4100
  process.on("SIGINT", shutdown);