akemon 0.3.6 → 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 (51) hide show
  1. package/DATA_POLICY.md +11 -3
  2. package/README.md +133 -21
  3. package/dist/akemon-home.js +56 -0
  4. package/dist/akemon-message.js +107 -0
  5. package/dist/best-effort.js +8 -0
  6. package/dist/cli.js +1188 -100
  7. package/dist/cognitive-artifact-store.js +101 -0
  8. package/dist/cognitive-event-log.js +47 -0
  9. package/dist/config.js +45 -9
  10. package/dist/context.js +27 -6
  11. package/dist/core/contracts/layers.js +1 -0
  12. package/dist/core/contracts/permission.js +1 -0
  13. package/dist/core/contracts/workspace.js +1 -0
  14. package/dist/core-cognitive-module.js +768 -0
  15. package/dist/engine-peripheral.js +127 -26
  16. package/dist/engine-routing.js +58 -17
  17. package/dist/interactive-session.js +361 -0
  18. package/dist/local-interconnect.js +156 -0
  19. package/dist/local-registry.js +178 -0
  20. package/dist/mcp-server.js +4 -1
  21. package/dist/memory-proposal.js +379 -0
  22. package/dist/memory-recorder.js +368 -0
  23. package/dist/orphan-scan.js +36 -24
  24. package/dist/passive-reflection-cognitive-module.js +172 -0
  25. package/dist/peripheral-registry.js +235 -0
  26. package/dist/permission-audit.js +132 -0
  27. package/dist/relay-client.js +68 -9
  28. package/dist/relay-mode.js +34 -0
  29. package/dist/relay-peripheral.js +139 -49
  30. package/dist/runtime-platform.js +122 -0
  31. package/dist/secretariat/client.js +87 -0
  32. package/dist/self.js +15 -6
  33. package/dist/server.js +3675 -512
  34. package/dist/social-discovery.js +231 -0
  35. package/dist/software-agent-peripheral.js +185 -244
  36. package/dist/software-agent-transport.js +177 -0
  37. package/dist/task-module.js +243 -0
  38. package/dist/task-registry.js +756 -0
  39. package/dist/vendor/xterm/addon-fit.js +2 -0
  40. package/dist/vendor/xterm/addon-search.js +2 -0
  41. package/dist/vendor/xterm/addon-web-links.js +2 -0
  42. package/dist/vendor/xterm/xterm.css +285 -0
  43. package/dist/vendor/xterm/xterm.js +2 -0
  44. package/dist/work-memory.js +59 -15
  45. package/dist/workbench-peripheral-guide.js +79 -0
  46. package/dist/workbench-session.js +1074 -0
  47. package/dist/workbench.html +4011 -0
  48. package/package.json +8 -3
  49. package/scripts/build.cjs +24 -0
  50. package/scripts/check-architecture-baseline.cjs +68 -0
  51. 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 = [];
@@ -189,192 +289,2825 @@ export async function handleSoftwareAgentRunHttp(req, res, deps) {
189
289
  const envelope = await readOwnerSoftwareAgentEnvelope(req, res, deps);
190
290
  if (!envelope)
191
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);
192
302
  try {
193
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
+ });
194
312
  writeJsonResponse(res, 200, redactSecrets(result), true);
195
313
  }
196
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
+ });
197
323
  const busy = String(err.message || "").includes("busy");
198
324
  writeJsonResponse(res, busy ? 409 : 500, { error: err.message || String(err) });
199
325
  }
200
326
  }
201
- export async function handleSoftwareAgentRunStreamHttp(req, res, deps) {
327
+ export async function handleLocalAkemonMessageHttp(req, res, deps) {
202
328
  if (!requireOwnerRequest(req, res, deps.options))
203
329
  return;
204
- const softwareAgent = requireSoftwareAgent(res, deps.softwareAgent);
205
- if (!softwareAgent)
206
- return;
207
- const envelope = await readOwnerSoftwareAgentEnvelope(req, res, deps);
208
- if (!envelope)
209
- return;
210
- const abortController = new AbortController();
211
- let responseFinished = false;
212
- let streamStarted = false;
213
- res.on("close", () => {
214
- if (!responseFinished)
215
- abortController.abort();
216
- });
217
- const ensureStreamStarted = () => {
218
- if (streamStarted)
219
- return;
220
- streamStarted = true;
221
- res.writeHead(200, {
222
- "Content-Type": "application/x-ndjson; charset=utf-8",
223
- "Cache-Control": "no-cache",
224
- "X-Accel-Buffering": "no",
225
- });
226
- res.flushHeaders?.();
227
- };
330
+ let body;
228
331
  try {
229
- await softwareAgent.sendTask(envelope, {
230
- signal: abortController.signal,
231
- observer: {
232
- onStart(event) {
233
- ensureStreamStarted();
234
- writeSoftwareAgentStreamEvent(res, {
235
- type: "start",
236
- taskId: event.taskId,
237
- commandLine: event.commandLine,
238
- contextSessionId: event.contextSessionId,
239
- contextPacketPath: event.contextPacketPath,
240
- workMemoryDir: event.workMemoryDir,
241
- });
242
- },
243
- onStream(event) {
244
- ensureStreamStarted();
245
- writeSoftwareAgentStreamEvent(res, {
246
- type: event.stream,
247
- taskId: event.taskId,
248
- chunk: event.chunk,
249
- });
250
- },
251
- onEnd(event) {
252
- ensureStreamStarted();
253
- writeSoftwareAgentStreamEvent(res, {
254
- type: "end",
255
- taskId: event.taskId,
256
- exitCode: event.exitCode,
257
- durationMs: event.durationMs,
258
- result: event.result,
259
- contextSessionId: event.contextSessionId,
260
- contextPacketPath: event.contextPacketPath,
261
- workMemoryDir: event.workMemoryDir,
262
- });
263
- },
264
- },
265
- });
332
+ body = await readJsonBody(req);
266
333
  }
267
334
  catch (err) {
268
- if (!streamStarted) {
269
- const busy = String(err.message || "").includes("busy");
270
- writeJsonResponse(res, busy ? 409 : 500, { error: err.message || String(err) });
271
- responseFinished = true;
272
- return;
273
- }
274
- writeSoftwareAgentStreamEvent(res, {
275
- type: "error",
276
- error: err.message || String(err),
277
- });
278
- }
279
- finally {
280
- responseFinished = true;
281
- if (streamStarted && !res.writableEnded)
282
- res.end();
283
- }
284
- }
285
- export async function handleSoftwareAgentStatusHttp(req, res, deps) {
286
- if (!requireOwnerRequest(req, res, deps.options))
287
- return;
288
- const softwareAgent = requireSoftwareAgent(res, deps.softwareAgent);
289
- if (!softwareAgent)
290
- return;
291
- writeJsonResponse(res, 200, softwareAgent.getState(), true);
292
- }
293
- export async function handleSoftwareAgentTasksHttp(req, res, deps) {
294
- if (!requireOwnerRequest(req, res, deps.options))
295
- return;
296
- const url = new URL(req.url || "/", "http://127.0.0.1");
297
- const basePath = "/self/software-agent/tasks";
298
- const taskLedgerDir = softwareAgentTaskLedgerDir(deps.workdir, deps.agentName);
299
- if (url.pathname === basePath) {
300
- const limit = readPositiveIntQuery(url.searchParams.get("limit"), 20, 100);
301
- const tasks = listSoftwareAgentTaskRecords(taskLedgerDir, limit, {
302
- contextSessionId: url.searchParams.get("session") || undefined,
303
- });
304
- writeJsonResponse(res, 200, { tasks }, true);
335
+ writeJsonResponse(res, 400, { error: err.message || "Invalid request body" });
305
336
  return;
306
337
  }
307
- if (url.pathname.startsWith(`${basePath}/`)) {
308
- const taskId = decodeURIComponent(url.pathname.slice(basePath.length + 1));
309
- if (!taskId || taskId.includes("/")) {
310
- writeJsonResponse(res, 400, { error: "Invalid software-agent task id" });
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
+ });
311
346
  return;
312
347
  }
313
- const task = readSoftwareAgentTaskRecord(taskLedgerDir, taskId);
314
- if (!task) {
315
- writeJsonResponse(res, 404, { error: "Software-agent task not found" });
316
- return;
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,
378
+ });
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.",
405
+ },
406
+ allowedActions: message.permissions.allowedActions,
407
+ forbiddenActions: message.permissions.forbiddenActions,
408
+ references: {
409
+ messageId: message.id,
410
+ conversationId: message.conversationId,
411
+ },
412
+ });
317
413
  }
318
- let contextSession;
319
- if (readBooleanQuery(url.searchParams.get("includeContext")) && task.contextSession?.sessionId) {
320
- try {
321
- contextSession = readSoftwareAgentContextSession(softwareAgentContextSessionDir(deps.workdir, deps.agentName), task.contextSession.sessionId, { includeContextPacket: true });
322
- }
323
- catch {
324
- contextSession = null;
325
- }
414
+ catch (error) {
415
+ logBestEffortError("local message audit append", error);
326
416
  }
327
- writeJsonResponse(res, 200, { task, ...(contextSession ? { contextSession } : {}) }, true);
328
- return;
329
- }
330
- writeJsonResponse(res, 404, { error: "Software-agent task endpoint not found" });
331
- }
332
- export async function handleSoftwareAgentContextSessionsHttp(req, res, deps) {
333
- if (!requireOwnerRequest(req, res, deps.options))
334
- return;
335
- const url = new URL(req.url || "/", "http://127.0.0.1");
336
- const basePath = "/self/software-agent/sessions";
337
- const contextSessionDir = softwareAgentContextSessionDir(deps.workdir, deps.agentName);
338
- if (url.pathname === basePath) {
339
- const limit = readPositiveIntQuery(url.searchParams.get("limit"), 20, 100);
340
- const sessions = listSoftwareAgentContextSessions(contextSessionDir, limit);
341
- writeJsonResponse(res, 200, { sessions }, true);
342
- return;
343
- }
344
- if (url.pathname.startsWith(`${basePath}/`)) {
345
- const sessionId = decodeURIComponent(url.pathname.slice(basePath.length + 1));
346
- if (!sessionId || sessionId.includes("/")) {
347
- writeJsonResponse(res, 400, { error: "Invalid software-agent context session id" });
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
+ });
348
474
  return;
349
475
  }
350
- let session;
351
- try {
352
- session = readSoftwareAgentContextSession(contextSessionDir, sessionId, {
353
- includeContextPacket: readBooleanQuery(url.searchParams.get("includeContext")),
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,
354
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;
355
639
  }
356
- catch (err) {
357
- writeJsonResponse(res, 400, { error: err.message || "Invalid software-agent context session id" });
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);
358
663
  return;
359
664
  }
360
- if (!session) {
361
- writeJsonResponse(res, 404, { error: "Software-agent context session not found" });
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);
362
835
  return;
363
836
  }
364
- writeJsonResponse(res, 200, { session }, true);
365
- return;
366
- }
367
- writeJsonResponse(res, 404, { error: "Software-agent context session endpoint not found" });
368
- }
369
- function softwareAgentTaskLedgerDir(workdir, agentName) {
370
- return join(workdir, ".akemon", "agents", agentName, "software-agent", "tasks");
371
- }
372
- function softwareAgentContextSessionDir(workdir, agentName) {
373
- return join(workdir, ".akemon", "agents", agentName, "software-agent", "sessions");
374
- }
375
- function readPositiveIntQuery(value, fallback, max) {
376
- if (!value)
377
- return fallback;
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,
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 } : {}),
1132
+ },
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,
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);
1235
+ }
1236
+ catch (err) {
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
+ });
1259
+ return;
1260
+ }
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
+ },
1313
+ });
1314
+ eventIds.push(akemonEvent.id);
1315
+ }
1316
+ catch (error) {
1317
+ logBestEffortError("local chat event append", error);
1318
+ }
1319
+ return eventIds;
1320
+ }
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
+ };
1329
+ }
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}` });
1363
+ return;
1364
+ }
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
+ });
1390
+ return;
1391
+ }
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);
1458
+ }
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;
1470
+ }
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");
3107
+ }
3108
+ function readPositiveIntQuery(value, fallback, max) {
3109
+ if (!value)
3110
+ return fallback;
378
3111
  const parsed = Number(value);
379
3112
  if (!Number.isInteger(parsed) || parsed <= 0)
380
3113
  return fallback;
@@ -383,6 +3116,22 @@ function readPositiveIntQuery(value, fallback, max) {
383
3116
  function readBooleanQuery(value) {
384
3117
  return value === "1" || value === "true" || value === "yes";
385
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
+ }
386
3135
  function readOptionalBooleanBody(value, field) {
387
3136
  if (value === undefined || value === null)
388
3137
  return false;
@@ -403,6 +3152,12 @@ function writeSoftwareAgentStreamEvent(res, event) {
403
3152
  return;
404
3153
  res.write(`${JSON.stringify(redactSecrets(event))}\n`);
405
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
+ }
406
3161
  export async function handleSoftwareAgentResetHttp(req, res, deps) {
407
3162
  if (!requireOwnerRequest(req, res, deps.options))
408
3163
  return;
@@ -414,40 +3169,399 @@ export async function handleSoftwareAgentResetHttp(req, res, deps) {
414
3169
  writeJsonResponse(res, 200, { ok: true, state: softwareAgent.getState() }, true);
415
3170
  }
416
3171
  catch (err) {
417
- writeJsonResponse(res, 500, { error: err.message || String(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" });
3375
+ }
3376
+ catch (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 });
418
3382
  }
419
3383
  }
420
- import { RelayPeripheral } from "./relay-peripheral.js";
421
- import { EnginePeripheral, LLM_ENGINES as LLM_ENGINES_SET } from "./engine-peripheral.js";
422
- import { EngineQueue } from "./engine-queue.js";
423
- import { BioStateModule } from "./bio-module.js";
424
- import { MemoryModule } from "./memory-module.js";
425
- import { RoleModule } from "./role-module.js";
426
- import { TaskModule } from "./task-module.js";
427
- import { SocialModule } from "./social-module.js";
428
- import { LongTermModule } from "./longterm-module.js";
429
- import { ReflectionModule } from "./reflection-module.js";
430
- import { ScriptModule } from "./script-module.js";
431
- import { FileEventLog, PersistentEventBus } from "./event-bus.js";
432
- import { CodexSoftwareAgentPeripheral, createOwnerTaskEnvelope, listSoftwareAgentContextSessions, listSoftwareAgentTaskRecords, readSoftwareAgentContextSession, readSoftwareAgentTaskRecord, } from "./software-agent-peripheral.js";
433
- import { buildSoftwareAgentMemorySummary } from "./software-agent-memory.js";
434
- import { buildWorkMemoryContext, workMemoryDir } from "./work-memory.js";
435
- import { SIG, sig } from "./types.js";
436
- import { loadConversation, listConversations, buildLLMContext } from "./context.js";
437
- import { redactSecrets } from "./redaction.js";
438
- import { createMcpServer, initMcpProxy, createMcpProxyServer } from "./mcp-server.js";
439
- import { autoRoute, runCollaborativeQuery } from "./agent-utils.js";
440
3384
  // createMcpServer, initMcpProxy, createMcpProxyServer → see mcp-server.ts
441
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
+ }
442
3556
  // ---------------------------------------------------------------------------
443
3557
  // Engine execution — delegates to EnginePeripheral (V2 Step 3)
444
3558
  // ---------------------------------------------------------------------------
445
3559
  /** Unified engine runner — delegates to EnginePeripheral */
446
- 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) {
447
3561
  if (!_engineP) {
448
3562
  throw new Error("Engine peripheral not initialized");
449
3563
  }
450
- 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);
451
3565
  // Sync trace back to module-level for reporting
452
3566
  result.then(() => { lastEngineTrace = _engineP.lastTrace; }).catch(() => { lastEngineTrace = _engineP.lastTrace; });
453
3567
  return result;
@@ -456,14 +3570,18 @@ function runEngine(engine, model, allowAll, task, workdir, extraAllowedTools, re
456
3570
  // pullFromRelay → see relay-peripheral.ts
457
3571
  export async function serve(options) {
458
3572
  const workdir = options.workdir || process.cwd();
3573
+ let registryHeartbeat = null;
459
3574
  // Reclaim stale CLI children from any previous daemon crash before starting.
460
3575
  await scanAndKillOrphans();
461
- // V2: Relay peripheral — unified relay API access
462
- const relay = new RelayPeripheral({
463
- httpUrl: options.relayHttp || "",
464
- secretKey: options.secretKey || "",
465
- agentName: options.agentName,
466
- });
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;
467
3585
  // V2: Engine peripheral — unified engine execution
468
3586
  const engineP = new EnginePeripheral({
469
3587
  engine: options.engine || "claude",
@@ -475,23 +3593,54 @@ export async function serve(options) {
475
3593
  rawMaxRounds: 20,
476
3594
  relay: options.relayHttp ? { http: options.relayHttp, agentName: options.agentName } : undefined,
477
3595
  });
3596
+ options.relay = relay ?? undefined;
3597
+ options.enginePeripheral = engineP;
3598
+ _engineP = engineP;
478
3599
  // Expose port to engine subprocesses so they can callback to local MCP server
479
3600
  process.env.AKEMON_PORT = String(options.port);
480
3601
  if (options.key)
481
3602
  process.env.AKEMON_KEY = options.key;
482
- // Initialize MCP proxy if --mcp-server specified
3603
+ // Initialize MCP proxy lazily so HTTP startup is not gated by child MCP boot.
483
3604
  let mcpProxy = null;
484
- if (options.mcpServer) {
485
- try {
486
- mcpProxy = await initMcpProxy(options.mcpServer, workdir);
487
- }
488
- catch (err) {
489
- console.error(`[mcp-proxy] Failed to start child MCP server: ${err.message}`);
490
- 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
+ });
491
3621
  }
492
- }
3622
+ return mcpProxyPromise;
3623
+ };
493
3624
  const sessions = new Map();
494
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
+ });
495
3644
  // Build deps for MCP server
496
3645
  const mcpDeps = {
497
3646
  runEngine,
@@ -509,61 +3658,37 @@ export async function serve(options) {
509
3658
  emitTaskCompleted,
510
3659
  };
511
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 = [];
512
3673
  const httpServer = createServer(async (req, res) => {
513
3674
  // Suppress noisy polling endpoints from log
514
3675
  const isQuiet = req.url === "/self/state" || req.url?.startsWith("/self/state?");
515
3676
  if (!isQuiet)
516
3677
  console.log(`[http] ${req.method} ${req.url} session=${req.headers["mcp-session-id"] || "none"}`);
517
3678
  try {
518
- if (req.url === "/self/software-agent/run-stream" && req.method === "POST") {
519
- await handleSoftwareAgentRunStreamHttp(req, res, {
520
- options,
521
- workdir,
522
- agentName: options.agentName,
523
- softwareAgent: codexSoftwareAgent,
524
- });
525
- return;
526
- }
527
- if (req.url === "/self/software-agent/run" && req.method === "POST") {
528
- await handleSoftwareAgentRunHttp(req, res, {
529
- options,
530
- workdir,
531
- agentName: options.agentName,
532
- softwareAgent: codexSoftwareAgent,
533
- });
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
+ })) {
534
3689
  return;
535
3690
  }
536
- if (req.url === "/self/software-agent/status" && req.method === "GET") {
537
- await handleSoftwareAgentStatusHttp(req, res, {
538
- options,
539
- softwareAgent: codexSoftwareAgent,
540
- });
541
- return;
542
- }
543
- const requestPath = req.url?.split("?")[0] || "";
544
- if (req.method === "GET"
545
- && (requestPath === "/self/software-agent/sessions" || requestPath.startsWith("/self/software-agent/sessions/"))) {
546
- await handleSoftwareAgentContextSessionsHttp(req, res, {
547
- options,
548
- workdir,
549
- agentName: options.agentName,
550
- });
551
- return;
552
- }
553
- if (req.method === "GET"
554
- && (requestPath === "/self/software-agent/tasks" || requestPath.startsWith("/self/software-agent/tasks/"))) {
555
- await handleSoftwareAgentTasksHttp(req, res, {
556
- options,
557
- workdir,
558
- agentName: options.agentName,
559
- });
560
- return;
561
- }
562
- if (req.url === "/self/software-agent/reset" && req.method === "POST") {
563
- await handleSoftwareAgentResetHttp(req, res, {
564
- options,
565
- softwareAgent: codexSoftwareAgent,
566
- });
3691
+ if (await handleLocalUiAssetRoute(req, res)) {
567
3692
  return;
568
3693
  }
569
3694
  // Auth check
@@ -576,89 +3701,13 @@ export async function serve(options) {
576
3701
  return;
577
3702
  }
578
3703
  }
579
- // Live agent life visualization
580
- if ((req.url === "/live" || req.url === "/live/") && req.method === "GET") {
581
- try {
582
- const { readFile: rf } = await import("fs/promises");
583
- const { fileURLToPath } = await import("url");
584
- const { dirname, join: pjoin } = await import("path");
585
- const __filename = fileURLToPath(import.meta.url);
586
- const __dirname = dirname(__filename);
587
- let html;
588
- try {
589
- html = await rf(pjoin(__dirname, "live.html"), "utf-8");
590
- }
591
- catch {
592
- html = await rf(pjoin(__dirname, "..", "src", "live.html"), "utf-8");
593
- }
594
- res.writeHead(200, { "Content-Type": "text/html" }).end(html);
595
- }
596
- catch (err) {
597
- res.writeHead(500).end("Live page not found: " + err.message);
598
- }
599
- return;
600
- }
601
- // Self-state API (no auth required for local monitoring)
602
- if (req.url === "/self/state" && req.method === "GET") {
603
- const state = await getSelfState(workdir, options.agentName);
604
- // Enrich with credits from relay (best-effort)
605
- if (relay.connected) {
606
- try {
607
- const agents = await relay.listAgents({ online: true, public: true });
608
- const self = agents.find((a) => a.name === options.agentName);
609
- state.credits = self?.credits ?? 0;
610
- state.level = self?.level ?? 0;
611
- }
612
- catch {
613
- state.credits = null;
614
- }
615
- }
616
- res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(state, null, 2));
617
- return;
618
- }
619
- // Revive endpoint — owner brings a forced-offline agent back
620
- if (req.url === "/self/revive" && req.method === "POST") {
621
- await reviveAgent(workdir, options.agentName);
622
- res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify({ ok: true, message: "Agent revived. Energy=50, Hunger=50." }));
623
- return;
624
- }
625
- if (req.url?.startsWith("/self/task-history") && req.method === "GET") {
626
- const url = new URL(req.url, `http://localhost`);
627
- const limit = parseInt(url.searchParams.get("limit") || "50") || 50;
628
- const history = await loadTaskHistory(workdir, options.agentName, limit);
629
- res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(history, null, 2));
630
- return;
631
- }
632
- if (req.url === "/self/directives" && req.method === "GET") {
633
- const dirs = await loadDirectives(workdir, options.agentName);
634
- res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(dirs, null, 2));
635
- return;
636
- }
637
- if (req.url === "/self/canvas" && req.method === "GET") {
638
- const entries = await loadRecentCanvasEntries(workdir, options.agentName, 10);
639
- res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(entries, null, 2));
640
- return;
641
- }
642
- if (req.url === "/self/conversations" && req.method === "GET") {
643
- const list = await listConversations(workdir, options.agentName);
644
- res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(list, null, 2));
645
- return;
646
- }
647
- if (req.url?.startsWith("/self/conversation/") && req.method === "GET") {
648
- const convId = decodeURIComponent(req.url.slice("/self/conversation/".length));
649
- if (!convId) {
650
- res.writeHead(400).end("Missing conversation ID");
651
- return;
652
- }
653
- const conv = await loadConversation(workdir, options.agentName, convId);
654
- const config = await loadAgentConfig(workdir, options.agentName);
655
- const budget = config.context_budget ?? 4096;
656
- const { recentStartIndex } = buildLLMContext(conv, budget);
657
- res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify({
658
- summary: conv.summary,
659
- rounds: conv.rounds,
660
- recentStartIndex,
661
- }, null, 2));
3704
+ if (await handleSelfHttpRoute(req, res, {
3705
+ workdir,
3706
+ agentName: options.agentName,
3707
+ relay,
3708
+ peripherals,
3709
+ runtimePeripheralRecords,
3710
+ })) {
662
3711
  return;
663
3712
  }
664
3713
  // Track publisher ID per session
@@ -687,8 +3736,9 @@ export async function serve(options) {
687
3736
  publisherIds.delete(sid);
688
3737
  }
689
3738
  };
690
- if (mcpProxy) {
691
- const proxyServer = createMcpProxyServer(mcpProxy, options.agentName);
3739
+ const activeMcpProxy = await ensureMcpProxy();
3740
+ if (activeMcpProxy) {
3741
+ const proxyServer = createMcpProxyServer(activeMcpProxy, options.agentName);
692
3742
  await proxyServer.connect(transport);
693
3743
  }
694
3744
  else {
@@ -703,7 +3753,7 @@ export async function serve(options) {
703
3753
  relayHttp: options.relayHttp,
704
3754
  secretKey: options.secretKey,
705
3755
  publisherIds,
706
- relay,
3756
+ relay: relay ?? undefined,
707
3757
  }, mcpDeps);
708
3758
  await mcpServer.connect(transport);
709
3759
  }
@@ -726,212 +3776,325 @@ export async function serve(options) {
726
3776
  console.log(`Akemon MCP server running on port ${options.port}`);
727
3777
  console.log(`Agent: ${options.agentName}`);
728
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();
729
3795
  });
730
- // Initialize agent config + consciousness (world knowledge + bio-state + guide)
731
- initAgentConfig(workdir, options.agentName).catch(err => console.log(`[self] Config init failed: ${err}`));
732
- loadAgentConfig(workdir, options.agentName).then(c => {
733
- const flags = Object.entries(c).filter(([, v]) => v).map(([k]) => k).join(", ");
734
- console.log(`[config] Features: ${flags || "(none)"}`);
735
- }).catch(() => { });
736
- initWorld(workdir, options.agentName, options.engine || "unknown").catch(err => console.log(`[self] World init failed: ${err}`));
737
- initBioState(workdir, options.agentName).catch(err => console.log(`[self] Bio init failed: ${err}`));
738
- if (options.relayHttp) {
739
- initGuide(workdir, options.agentName, options.relayHttp).catch(err => console.log(`[self] Guide init failed: ${err}`));
740
- }
741
- // Pull games/notes/pages from relay to restore local data
742
- if (options.relayHttp) {
743
- relay.pullFromRelay(workdir, options.agentName).catch(err => console.log(`[sync] Pull from relay failed: ${err}`));
744
- }
745
- // V2: Shared module context + persistent EventBus.
746
- // This is the durable spine for module/peripheral/engine activity; recovery
747
- // side effects are intentionally deferred until the event schema stabilizes.
748
- const eventLogDir = join(workdir, ".akemon", "agents", options.agentName, "events");
749
- await mkdir(eventLogDir, { recursive: true });
750
- const eventLogPath = join(eventLogDir, "events.jsonl");
751
- const bus = new PersistentEventBus(new FileEventLog(eventLogPath));
752
- console.log(`[v2] Event log: ${eventLogPath}`);
753
- codexSoftwareAgent = new CodexSoftwareAgentPeripheral({
754
- workdir,
755
- model: process.env.AKEMON_CODEX_MODEL,
756
- sandbox: "workspace-write",
757
- taskLedgerDir: softwareAgentTaskLedgerDir(workdir, options.agentName),
758
- contextSessionDir: softwareAgentContextSessionDir(workdir, options.agentName),
759
- workMemoryDir: workMemoryDir(workdir, options.agentName),
760
- envPolicy: options.softwareAgentEnvPolicy,
761
- envAllowlist: options.softwareAgentEnvAllowlist,
762
- });
763
- // Peripheral registry — Core routes by capability
764
- const peripherals = [relay, engineP, codexSoftwareAgent];
765
- for (const peripheral of peripherals) {
766
- await peripheral.start(bus);
767
- }
768
- // requestCompute: acquire the engine slot (priority-aware), execute with a
769
- // hard timeout, and release. The slot release and subprocess kill are both
770
- // driven by the same AbortController so a stuck engine can't hold the lock.
771
- async function requestCompute(req) {
772
- // Load latest agent config for routing table (fast file read, changes rarely)
773
- const agentCfg = await loadAgentConfig(workdir, options.agentName);
774
- const routing = agentCfg.engine_routing;
775
- // user_manual tasks also hold a subscription-CLI semaphore to cap claude CLI concurrency
776
- const isUserManual = req.origin === "user_manual";
777
- if (isUserManual) {
778
- try {
779
- await engineQueue.acquireUserManualSlot(ENGINE_WAIT_DEADLINE_MS);
780
- }
781
- catch (err) {
782
- return { success: false, error: err.message || "User manual slot timeout" };
783
- }
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}`));
784
3809
  }
785
- try {
786
- 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}`));
787
3813
  }
788
- catch (err) {
789
- if (isUserManual)
790
- engineQueue.releaseUserManualSlot();
791
- return { success: false, error: err.message || "Engine busy timeout" };
792
- }
793
- const prompt = req.context
794
- ? `${req.context}\n\n---\n\n${req.question}`
795
- : req.question;
796
- const abortController = new AbortController();
797
- 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
+ ];
798
3882
  try {
799
- 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);
800
- emitTokenUsage(prompt.length, response.length);
801
- return { success: true, response };
3883
+ await upsertPeripheralRecords(options.agentName, runtimePeripheralRecords.map((record) => ({ ...record, status: "configured" })));
802
3884
  }
803
- catch (err) {
804
- const msg = abortController.signal.aborted
805
- ? `Engine execution timeout (${Math.round(ENGINE_EXEC_TIMEOUT_MS / 60000)} min)`
806
- : (err.message || String(err));
807
- return { success: false, error: msg };
3885
+ catch (error) {
3886
+ logBestEffortError("peripheral registry sync", error);
808
3887
  }
809
- finally {
810
- clearTimeout(timer);
811
- engineQueue.release();
812
- if (isUserManual)
813
- engineQueue.releaseUserManualSlot();
3888
+ for (const peripheral of peripherals) {
3889
+ await peripheral.start(runtimeBus);
814
3890
  }
815
- }
816
- const moduleCtx = {
817
- bus,
818
- agentName: options.agentName,
819
- workdir,
820
- getPeripherals: (capability) => peripherals.filter(p => p.capabilities.includes(capability)),
821
- sendTo: async (capability, signal) => {
822
- const p = peripherals.find(p => p.capabilities.includes(capability));
823
- return p ? p.send(signal) : null;
824
- },
825
- requestCompute,
826
- getPromptContributions: () => {
827
- const contributions = [];
828
- for (const m of allModules) {
829
- if (m.promptContribution) {
830
- const c = m.promptContribution();
831
- if (c)
832
- 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" };
833
3907
  }
834
3908
  }
835
- return contributions;
836
- },
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)`);
837
4054
  };
838
- // V2: Conditionally load modules based on --with/--without
839
- const enabled = options.enabledModules ?? ["biostate", "memory", "role", "task", "social", "longterm", "reflection", "script"];
840
- const loadedModules = [];
841
- const allModules = [];
842
- if (enabled.includes("biostate")) {
843
- const bioModule = new BioStateModule();
844
- await bioModule.start(moduleCtx);
845
- options.bioModule = bioModule;
846
- allModules.push(bioModule);
847
- loadedModules.push("biostate");
848
- }
849
- if (enabled.includes("memory")) {
850
- const memoryModule = new MemoryModule();
851
- if (options.cycleInterval)
852
- memoryModule.cycleIntervalMs = options.cycleInterval * 60 * 1000;
853
- await memoryModule.start(moduleCtx);
854
- options.memoryModule = memoryModule;
855
- allModules.push(memoryModule);
856
- loadedModules.push("memory");
857
- }
858
- if (enabled.includes("role")) {
859
- const roleModule = new RoleModule();
860
- await roleModule.start(moduleCtx);
861
- allModules.push(roleModule);
862
- loadedModules.push("role");
863
- }
864
- if (enabled.includes("task")) {
865
- const taskModule = new TaskModule();
866
- taskModule.relayHttp = options.relayHttp || "";
867
- taskModule.secretKey = options.secretKey || "";
868
- taskModule.engine = options.engine || "";
869
- taskModule.model = options.model;
870
- taskModule.allowAll = options.allowAll;
871
- taskModule.notifyUrl = options.notifyUrl;
872
- await taskModule.start(moduleCtx);
873
- _taskModule = taskModule;
874
- allModules.push(taskModule);
875
- loadedModules.push("task");
876
- }
877
- if (enabled.includes("social")) {
878
- const socialModule = new SocialModule();
879
- await socialModule.start(moduleCtx);
880
- allModules.push(socialModule);
881
- loadedModules.push("social");
882
- }
883
- if (enabled.includes("longterm")) {
884
- const longtermModule = new LongTermModule();
885
- await longtermModule.start(moduleCtx);
886
- allModules.push(longtermModule);
887
- loadedModules.push("longterm");
888
- }
889
- if (enabled.includes("reflection")) {
890
- const reflectionModule = new ReflectionModule();
891
- await reflectionModule.start(moduleCtx);
892
- allModules.push(reflectionModule);
893
- loadedModules.push("reflection");
894
- }
895
- if (enabled.includes("script")) {
896
- const scriptModule = new ScriptModule();
897
- scriptModule.scriptName = options.scriptName || "daily-life";
898
- scriptModule.relayHttp = options.relayHttp || "";
899
- scriptModule.secretKey = options.secretKey || "";
900
- await scriptModule.start(moduleCtx);
901
- allModules.push(scriptModule);
902
- loadedModules.push("script");
903
- }
904
- console.log(`[v2] Modules: ${loadedModules.join(", ") || "(none)"}`);
905
- // Inject peripherals into options
906
- options.relay = relay;
907
- options.enginePeripheral = engineP;
908
- _engineP = engineP;
909
- _bus = bus;
910
- // V2: Emit agent start lifecycle event
911
- bus.emit(SIG.AGENT_START, sig(SIG.AGENT_START, {
912
- agentName: options.agentName,
913
- engine: options.engine,
914
- modules: loadedModules,
915
- }));
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);
916
4061
  // Graceful shutdown — kill engine children first, then stop modules.
917
4062
  // Engine children are killed via process group signal so any sub-forks
918
4063
  // (e.g. opencode's internal Node workers) are also terminated.
919
4064
  const shutdown = async () => {
920
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
+ }
921
4074
  if (_engineP)
922
4075
  _engineP.killAllChildren();
923
- bus.emit(SIG.AGENT_STOP, sig(SIG.AGENT_STOP, {}));
4076
+ bus?.emit(SIG.AGENT_STOP, sig(SIG.AGENT_STOP, {}));
924
4077
  for (const m of allModules) {
925
4078
  try {
926
4079
  await m.stop();
927
4080
  }
928
- 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);
929
4090
  }
930
4091
  try {
931
4092
  await codexSoftwareAgent?.stop();
932
4093
  }
933
- catch { }
934
- bus.getLog().close();
4094
+ catch (error) {
4095
+ logBestEffortError("software-agent stop", error);
4096
+ }
4097
+ bus?.getLog().close();
935
4098
  process.exit(0);
936
4099
  };
937
4100
  process.on("SIGINT", shutdown);