akemon 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -164,11 +164,16 @@ akemon serve --name my-agent --engine claude
164
164
 
165
165
  # In another terminal, ask the local software peripheral to work in the repo
166
166
  akemon software-agent "Add one focused test and run the relevant test command."
167
+
168
+ # Review recent software-agent runs
169
+ akemon software-agent-tasks --limit 5
167
170
  ```
168
171
 
169
172
  This is different from `--engine`: engines are replaceable compute, while software agents are external software bodies with their own repo context, skills, tools, and execution loop.
170
173
 
171
- Current Batch 5 status: the Codex integration uses `codex exec` as a one-shot baseline, not a true persistent interactive session yet. It is owner-only, local-only, one task at a time, and every call is wrapped in an explicit task envelope with workdir, memory scope, risk level, allowed actions, and forbidden actions.
174
+ Current Batch 5 status: the Codex integration uses `codex exec` as a one-shot baseline, not a true persistent interactive session yet. It is owner-only, local-only, one task at a time, streams local stdout/stderr by default, and every call is wrapped in an explicit task envelope with workdir, memory scope, risk level, allowed actions, and forbidden actions.
175
+
176
+ Software-agent tasks default to the `akemon serve` workdir boundary. Use `--allow-outside-workdir` only when you explicitly want the software agent to run outside that root. Each run is recorded under `.akemon/agents/<name>/software-agent/tasks/` with the envelope, result, output summaries, and git worktree status.
172
177
 
173
178
  ## Serve Options
174
179
 
package/dist/cli.js CHANGED
@@ -14,6 +14,166 @@ const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-
14
14
  const RELAY_WS = "wss://relay.akemon.dev";
15
15
  const RELAY_HTTP = "https://relay.akemon.dev";
16
16
  const program = new Command();
17
+ function parsePortOption(port) {
18
+ const value = typeof port === "number" ? port : parseInt(String(port || "3000"));
19
+ return Number.isInteger(value) && value > 0 ? value : 3000;
20
+ }
21
+ function clampPositiveInt(value, fallback, max) {
22
+ const parsed = typeof value === "number" ? value : Number(value);
23
+ if (!Number.isInteger(parsed) || parsed <= 0)
24
+ return fallback;
25
+ return Math.min(parsed, max);
26
+ }
27
+ function printSoftwareAgentTaskList(tasks) {
28
+ if (!tasks.length) {
29
+ console.log("No software-agent tasks found.");
30
+ return;
31
+ }
32
+ for (const task of tasks) {
33
+ const result = task.result?.success === true ? "ok" : task.result?.success === false ? "error" : "pending";
34
+ const duration = typeof task.durationMs === "number" ? `${task.durationMs}ms` : "-";
35
+ const git = task.workdirStatus?.isRepo
36
+ ? (task.workdirStatus.dirty ? `dirty:${task.workdirStatus.changedFiles?.length || 0}` : "clean")
37
+ : "no-git";
38
+ const goal = truncateOneLine(task.envelope?.goal || "", 90);
39
+ console.log(`${task.taskId} ${task.status}/${result} ${duration} ${git} ${task.updatedAt || task.startedAt}`);
40
+ if (goal)
41
+ console.log(` ${goal}`);
42
+ }
43
+ }
44
+ function truncateOneLine(value, max) {
45
+ const oneLine = value.replace(/\s+/g, " ").trim();
46
+ if (oneLine.length <= max)
47
+ return oneLine;
48
+ return `${oneLine.slice(0, Math.max(0, max - 3))}...`;
49
+ }
50
+ async function callLocalOwnerEndpoint(path, opts, init = {}) {
51
+ const res = await fetchLocalOwnerEndpoint(path, opts, init);
52
+ const text = await res.text();
53
+ let data;
54
+ try {
55
+ data = text ? JSON.parse(text) : {};
56
+ }
57
+ catch {
58
+ data = { output: text };
59
+ }
60
+ if (!res.ok || data.success === false) {
61
+ console.error(data.error || text || `Request failed with HTTP ${res.status}`);
62
+ process.exit(1);
63
+ }
64
+ return data;
65
+ }
66
+ async function fetchLocalOwnerEndpoint(path, opts, init = {}) {
67
+ const credentials = await getOrCreateRelayCredentials();
68
+ const port = parsePortOption(opts.port);
69
+ const headers = {
70
+ Authorization: `Bearer ${credentials.secretKey}`,
71
+ };
72
+ if (init.body !== undefined)
73
+ headers["Content-Type"] = "application/json";
74
+ let res;
75
+ try {
76
+ res = await fetch(`http://127.0.0.1:${port}${path}`, {
77
+ ...init,
78
+ headers: {
79
+ ...headers,
80
+ ...init.headers,
81
+ },
82
+ });
83
+ }
84
+ catch (error) {
85
+ const cause = error.cause;
86
+ if (error instanceof TypeError && error.message === "fetch failed" && cause?.message === "bad port") {
87
+ console.error(`Port ${port} cannot be used for the local akemon serve connection. Choose a different --port.`);
88
+ process.exit(1);
89
+ }
90
+ if (error instanceof TypeError && error.message === "fetch failed") {
91
+ console.error(`Cannot connect to local akemon serve on port ${port}. Start it with: akemon serve --port ${port}`);
92
+ process.exit(1);
93
+ }
94
+ throw error;
95
+ }
96
+ return res;
97
+ }
98
+ async function streamLocalOwnerEndpoint(path, opts, body) {
99
+ const res = await fetchLocalOwnerEndpoint(path, opts, {
100
+ method: "POST",
101
+ body: JSON.stringify(body),
102
+ });
103
+ if (!res.ok) {
104
+ const text = await res.text();
105
+ let data;
106
+ try {
107
+ data = text ? JSON.parse(text) : {};
108
+ }
109
+ catch {
110
+ data = { error: text };
111
+ }
112
+ console.error(data.error || text || `Request failed with HTTP ${res.status}`);
113
+ process.exit(1);
114
+ }
115
+ if (!res.body)
116
+ return;
117
+ const decoder = new TextDecoder();
118
+ let buffer = "";
119
+ let failed = false;
120
+ const reader = res.body.getReader();
121
+ while (true) {
122
+ const { done, value } = await reader.read();
123
+ if (done)
124
+ break;
125
+ buffer += decoder.decode(value, { stream: true });
126
+ const lines = buffer.split(/\r?\n/);
127
+ buffer = lines.pop() || "";
128
+ for (const line of lines) {
129
+ if (handleSoftwareAgentStreamLine(line))
130
+ failed = true;
131
+ }
132
+ }
133
+ buffer += decoder.decode();
134
+ if (buffer.trim() && handleSoftwareAgentStreamLine(buffer))
135
+ failed = true;
136
+ if (failed)
137
+ process.exit(1);
138
+ }
139
+ function handleSoftwareAgentStreamLine(line) {
140
+ const trimmed = line.trim();
141
+ if (!trimmed)
142
+ return false;
143
+ let event;
144
+ try {
145
+ event = JSON.parse(trimmed);
146
+ }
147
+ catch {
148
+ process.stderr.write(`${trimmed}\n`);
149
+ return true;
150
+ }
151
+ if (event.type === "start" && event.taskId) {
152
+ process.stderr.write(`[software-agent] started ${event.taskId}\n`);
153
+ return false;
154
+ }
155
+ if (event.type === "stdout" && typeof event.chunk === "string") {
156
+ process.stdout.write(event.chunk);
157
+ return false;
158
+ }
159
+ if (event.type === "stderr" && typeof event.chunk === "string") {
160
+ process.stderr.write(event.chunk);
161
+ return false;
162
+ }
163
+ if (event.type === "end") {
164
+ const result = event.result;
165
+ if (result?.success === false && result.error) {
166
+ process.stderr.write(`${result.error}\n`);
167
+ return true;
168
+ }
169
+ return false;
170
+ }
171
+ if (event.type === "error") {
172
+ process.stderr.write(`${event.error || "Software-agent stream failed"}\n`);
173
+ return true;
174
+ }
175
+ return false;
176
+ }
17
177
  program
18
178
  .name("akemon")
19
179
  .description("Agent work marketplace — train your agent, let it work for others")
@@ -138,15 +298,15 @@ program
138
298
  .argument("<goal...>", "Task goal to send to the software agent")
139
299
  .option("-p, --port <port>", "Local akemon serve port", "3000")
140
300
  .option("-w, --workdir <path>", "Workdir for the software agent (default: serve workdir)")
301
+ .option("--allow-outside-workdir", "Allow the software agent workdir to be outside the serve workdir")
141
302
  .option("--role-scope <scope>", "Role scope: owner|public|order|agent|system", "owner")
142
303
  .option("--memory-scope <scope>", "Memory scope: none|public|task|owner", "owner")
143
304
  .option("--risk <level>", "Risk level: low|medium|high", "medium")
144
305
  .option("--memory-summary <text>", "Pre-filtered memory/context text to include")
145
306
  .option("--deliverable <text>", "Expected output shape")
146
307
  .option("--timeout-ms <ms>", "Task timeout in milliseconds")
308
+ .option("--no-stream", "Disable local streaming and wait for the final response")
147
309
  .action(async (goalParts, opts) => {
148
- const credentials = await getOrCreateRelayCredentials();
149
- const port = parseInt(opts.port) || 3000;
150
310
  const body = {
151
311
  goal: goalParts.join(" "),
152
312
  roleScope: opts.roleScope,
@@ -155,6 +315,8 @@ program
155
315
  };
156
316
  if (opts.workdir)
157
317
  body.workdir = opts.workdir;
318
+ if (opts.allowOutsideWorkdir)
319
+ body.allowOutsideWorkdir = true;
158
320
  if (opts.memorySummary)
159
321
  body.memorySummary = opts.memorySummary;
160
322
  if (opts.deliverable)
@@ -167,31 +329,59 @@ program
167
329
  }
168
330
  body.timeoutMs = timeoutMs;
169
331
  }
170
- const res = await fetch(`http://127.0.0.1:${port}/self/software-agent/run`, {
332
+ if (opts.stream !== false) {
333
+ await streamLocalOwnerEndpoint("/self/software-agent/run-stream", opts, body);
334
+ return;
335
+ }
336
+ const data = await callLocalOwnerEndpoint("/self/software-agent/run", opts, {
171
337
  method: "POST",
172
- headers: {
173
- Authorization: `Bearer ${credentials.secretKey}`,
174
- "Content-Type": "application/json",
175
- },
176
338
  body: JSON.stringify(body),
177
339
  });
178
- const text = await res.text();
179
- let data;
180
- try {
181
- data = text ? JSON.parse(text) : {};
182
- }
183
- catch {
184
- data = { output: text };
185
- }
186
- if (!res.ok || data.success === false) {
187
- console.error(data.error || text || `Request failed with HTTP ${res.status}`);
188
- process.exit(1);
189
- }
190
340
  if (data.output)
191
341
  console.log(data.output);
192
342
  else
193
343
  console.log(JSON.stringify(data, null, 2));
194
344
  });
345
+ program
346
+ .command("software-agent-status")
347
+ .description("Show the owner-only local software-agent peripheral state")
348
+ .option("-p, --port <port>", "Local akemon serve port", "3000")
349
+ .action(async (opts) => {
350
+ const data = await callLocalOwnerEndpoint("/self/software-agent/status", opts, {
351
+ method: "GET",
352
+ });
353
+ console.log(JSON.stringify(data, null, 2));
354
+ });
355
+ program
356
+ .command("software-agent-tasks")
357
+ .description("List recent owner-only software-agent task ledger records")
358
+ .argument("[taskId]", "Task id to inspect")
359
+ .option("-p, --port <port>", "Local akemon serve port", "3000")
360
+ .option("-l, --limit <n>", "Maximum recent tasks to list", "20")
361
+ .option("--json", "Print raw JSON")
362
+ .action(async (taskId, opts) => {
363
+ const path = taskId
364
+ ? `/self/software-agent/tasks/${encodeURIComponent(taskId)}`
365
+ : `/self/software-agent/tasks?limit=${clampPositiveInt(opts.limit, 20, 100)}`;
366
+ const data = await callLocalOwnerEndpoint(path, opts, {
367
+ method: "GET",
368
+ });
369
+ if (opts.json || taskId) {
370
+ console.log(JSON.stringify(taskId ? data.task : data, null, 2));
371
+ return;
372
+ }
373
+ printSoftwareAgentTaskList(Array.isArray(data.tasks) ? data.tasks : []);
374
+ });
375
+ program
376
+ .command("software-agent-reset")
377
+ .description("Reset the owner-only local software-agent peripheral session")
378
+ .option("-p, --port <port>", "Local akemon serve port", "3000")
379
+ .action(async (opts) => {
380
+ const data = await callLocalOwnerEndpoint("/self/software-agent/reset", opts, {
381
+ method: "POST",
382
+ });
383
+ console.log(JSON.stringify(data, null, 2));
384
+ });
195
385
  program
196
386
  .command("dashboard")
197
387
  .description("Open your agent dashboard in the browser")
package/dist/server.js CHANGED
@@ -141,6 +141,12 @@ export async function handleSoftwareAgentRunHttp(req, res, deps) {
141
141
  let envelope;
142
142
  try {
143
143
  envelope = createOwnerTaskEnvelope(body, deps.workdir);
144
+ envelope.memorySummary = await buildSoftwareAgentMemorySummary({
145
+ workdir: deps.workdir,
146
+ agentName: deps.agentName,
147
+ envelope,
148
+ request: body,
149
+ });
144
150
  }
145
151
  catch (err) {
146
152
  res.writeHead(400, { "Content-Type": "application/json" })
@@ -158,6 +164,196 @@ export async function handleSoftwareAgentRunHttp(req, res, deps) {
158
164
  .end(JSON.stringify({ error: err.message || String(err) }));
159
165
  }
160
166
  }
167
+ export async function handleSoftwareAgentRunStreamHttp(req, res, deps) {
168
+ if (!isOwnerRequest(req, deps.options)) {
169
+ res.writeHead(401, { "Content-Type": "application/json" })
170
+ .end(JSON.stringify({ error: "Owner token required" }));
171
+ return;
172
+ }
173
+ if (!deps.softwareAgent) {
174
+ res.writeHead(503, { "Content-Type": "application/json" })
175
+ .end(JSON.stringify({ error: "Software agent peripheral not ready" }));
176
+ return;
177
+ }
178
+ let body;
179
+ try {
180
+ body = await readJsonBody(req);
181
+ }
182
+ catch (err) {
183
+ res.writeHead(400, { "Content-Type": "application/json" })
184
+ .end(JSON.stringify({ error: err.message || "Invalid request body" }));
185
+ return;
186
+ }
187
+ let envelope;
188
+ try {
189
+ envelope = createOwnerTaskEnvelope(body, deps.workdir);
190
+ envelope.memorySummary = await buildSoftwareAgentMemorySummary({
191
+ workdir: deps.workdir,
192
+ agentName: deps.agentName,
193
+ envelope,
194
+ request: body,
195
+ });
196
+ }
197
+ catch (err) {
198
+ res.writeHead(400, { "Content-Type": "application/json" })
199
+ .end(JSON.stringify({ error: err.message || "Invalid software-agent envelope" }));
200
+ return;
201
+ }
202
+ const abortController = new AbortController();
203
+ let responseFinished = false;
204
+ let streamStarted = false;
205
+ res.on("close", () => {
206
+ if (!responseFinished)
207
+ abortController.abort();
208
+ });
209
+ const ensureStreamStarted = () => {
210
+ if (streamStarted)
211
+ return;
212
+ streamStarted = true;
213
+ res.writeHead(200, {
214
+ "Content-Type": "application/x-ndjson; charset=utf-8",
215
+ "Cache-Control": "no-cache",
216
+ "X-Accel-Buffering": "no",
217
+ });
218
+ res.flushHeaders?.();
219
+ };
220
+ try {
221
+ await deps.softwareAgent.sendTask(envelope, {
222
+ signal: abortController.signal,
223
+ observer: {
224
+ onStart(event) {
225
+ ensureStreamStarted();
226
+ writeSoftwareAgentStreamEvent(res, {
227
+ type: "start",
228
+ taskId: event.taskId,
229
+ commandLine: event.commandLine,
230
+ });
231
+ },
232
+ onStream(event) {
233
+ ensureStreamStarted();
234
+ writeSoftwareAgentStreamEvent(res, {
235
+ type: event.stream,
236
+ taskId: event.taskId,
237
+ chunk: event.chunk,
238
+ });
239
+ },
240
+ onEnd(event) {
241
+ ensureStreamStarted();
242
+ writeSoftwareAgentStreamEvent(res, {
243
+ type: "end",
244
+ taskId: event.taskId,
245
+ result: event.result,
246
+ });
247
+ },
248
+ },
249
+ });
250
+ }
251
+ catch (err) {
252
+ if (!streamStarted) {
253
+ const busy = String(err.message || "").includes("busy");
254
+ res.writeHead(busy ? 409 : 500, { "Content-Type": "application/json" })
255
+ .end(JSON.stringify({ error: err.message || String(err) }));
256
+ responseFinished = true;
257
+ return;
258
+ }
259
+ writeSoftwareAgentStreamEvent(res, {
260
+ type: "error",
261
+ error: err.message || String(err),
262
+ });
263
+ }
264
+ finally {
265
+ responseFinished = true;
266
+ if (streamStarted && !res.writableEnded)
267
+ res.end();
268
+ }
269
+ }
270
+ export async function handleSoftwareAgentStatusHttp(req, res, deps) {
271
+ if (!isOwnerRequest(req, deps.options)) {
272
+ res.writeHead(401, { "Content-Type": "application/json" })
273
+ .end(JSON.stringify({ error: "Owner token required" }));
274
+ return;
275
+ }
276
+ if (!deps.softwareAgent) {
277
+ res.writeHead(503, { "Content-Type": "application/json" })
278
+ .end(JSON.stringify({ error: "Software agent peripheral not ready" }));
279
+ return;
280
+ }
281
+ res.writeHead(200, { "Content-Type": "application/json" })
282
+ .end(JSON.stringify(deps.softwareAgent.getState(), null, 2));
283
+ }
284
+ export async function handleSoftwareAgentTasksHttp(req, res, deps) {
285
+ if (!isOwnerRequest(req, deps.options)) {
286
+ res.writeHead(401, { "Content-Type": "application/json" })
287
+ .end(JSON.stringify({ error: "Owner token required" }));
288
+ return;
289
+ }
290
+ const url = new URL(req.url || "/", "http://127.0.0.1");
291
+ const basePath = "/self/software-agent/tasks";
292
+ const taskLedgerDir = softwareAgentTaskLedgerDir(deps.workdir, deps.agentName);
293
+ if (url.pathname === basePath) {
294
+ const limit = readPositiveIntQuery(url.searchParams.get("limit"), 20, 100);
295
+ const tasks = listSoftwareAgentTaskRecords(taskLedgerDir, limit);
296
+ res.writeHead(200, { "Content-Type": "application/json" })
297
+ .end(JSON.stringify({ tasks }, null, 2));
298
+ return;
299
+ }
300
+ if (url.pathname.startsWith(`${basePath}/`)) {
301
+ const taskId = decodeURIComponent(url.pathname.slice(basePath.length + 1));
302
+ if (!taskId || taskId.includes("/")) {
303
+ res.writeHead(400, { "Content-Type": "application/json" })
304
+ .end(JSON.stringify({ error: "Invalid software-agent task id" }));
305
+ return;
306
+ }
307
+ const task = readSoftwareAgentTaskRecord(taskLedgerDir, taskId);
308
+ if (!task) {
309
+ res.writeHead(404, { "Content-Type": "application/json" })
310
+ .end(JSON.stringify({ error: "Software-agent task not found" }));
311
+ return;
312
+ }
313
+ res.writeHead(200, { "Content-Type": "application/json" })
314
+ .end(JSON.stringify({ task }, null, 2));
315
+ return;
316
+ }
317
+ res.writeHead(404, { "Content-Type": "application/json" })
318
+ .end(JSON.stringify({ error: "Software-agent task endpoint not found" }));
319
+ }
320
+ function softwareAgentTaskLedgerDir(workdir, agentName) {
321
+ return join(workdir, ".akemon", "agents", agentName, "software-agent", "tasks");
322
+ }
323
+ function readPositiveIntQuery(value, fallback, max) {
324
+ if (!value)
325
+ return fallback;
326
+ const parsed = Number(value);
327
+ if (!Number.isInteger(parsed) || parsed <= 0)
328
+ return fallback;
329
+ return Math.min(parsed, max);
330
+ }
331
+ function writeSoftwareAgentStreamEvent(res, event) {
332
+ if (res.destroyed)
333
+ return;
334
+ res.write(`${JSON.stringify(event)}\n`);
335
+ }
336
+ export async function handleSoftwareAgentResetHttp(req, res, deps) {
337
+ if (!isOwnerRequest(req, deps.options)) {
338
+ res.writeHead(401, { "Content-Type": "application/json" })
339
+ .end(JSON.stringify({ error: "Owner token required" }));
340
+ return;
341
+ }
342
+ if (!deps.softwareAgent) {
343
+ res.writeHead(503, { "Content-Type": "application/json" })
344
+ .end(JSON.stringify({ error: "Software agent peripheral not ready" }));
345
+ return;
346
+ }
347
+ try {
348
+ await deps.softwareAgent.resetSession();
349
+ res.writeHead(200, { "Content-Type": "application/json" })
350
+ .end(JSON.stringify({ ok: true, state: deps.softwareAgent.getState() }, null, 2));
351
+ }
352
+ catch (err) {
353
+ res.writeHead(500, { "Content-Type": "application/json" })
354
+ .end(JSON.stringify({ error: err.message || String(err) }));
355
+ }
356
+ }
161
357
  import { RelayPeripheral } from "./relay-peripheral.js";
162
358
  import { EnginePeripheral, LLM_ENGINES as LLM_ENGINES_SET } from "./engine-peripheral.js";
163
359
  import { EngineQueue } from "./engine-queue.js";
@@ -170,7 +366,8 @@ import { LongTermModule } from "./longterm-module.js";
170
366
  import { ReflectionModule } from "./reflection-module.js";
171
367
  import { ScriptModule } from "./script-module.js";
172
368
  import { FileEventLog, PersistentEventBus } from "./event-bus.js";
173
- import { CodexSoftwareAgentPeripheral, createOwnerTaskEnvelope } from "./software-agent-peripheral.js";
369
+ import { CodexSoftwareAgentPeripheral, createOwnerTaskEnvelope, listSoftwareAgentTaskRecords, readSoftwareAgentTaskRecord, } from "./software-agent-peripheral.js";
370
+ import { buildSoftwareAgentMemorySummary } from "./software-agent-memory.js";
174
371
  import { SIG, sig } from "./types.js";
175
372
  import { loadConversation, listConversations, buildLLMContext } from "./context.js";
176
373
  import { createMcpServer, initMcpProxy, createMcpProxyServer } from "./mcp-server.js";
@@ -253,10 +450,44 @@ export async function serve(options) {
253
450
  if (!isQuiet)
254
451
  console.log(`[http] ${req.method} ${req.url} session=${req.headers["mcp-session-id"] || "none"}`);
255
452
  try {
453
+ if (req.url === "/self/software-agent/run-stream" && req.method === "POST") {
454
+ await handleSoftwareAgentRunStreamHttp(req, res, {
455
+ options,
456
+ workdir,
457
+ agentName: options.agentName,
458
+ softwareAgent: codexSoftwareAgent,
459
+ });
460
+ return;
461
+ }
256
462
  if (req.url === "/self/software-agent/run" && req.method === "POST") {
257
463
  await handleSoftwareAgentRunHttp(req, res, {
258
464
  options,
259
465
  workdir,
466
+ agentName: options.agentName,
467
+ softwareAgent: codexSoftwareAgent,
468
+ });
469
+ return;
470
+ }
471
+ if (req.url === "/self/software-agent/status" && req.method === "GET") {
472
+ await handleSoftwareAgentStatusHttp(req, res, {
473
+ options,
474
+ softwareAgent: codexSoftwareAgent,
475
+ });
476
+ return;
477
+ }
478
+ const requestPath = req.url?.split("?")[0] || "";
479
+ if (req.method === "GET"
480
+ && (requestPath === "/self/software-agent/tasks" || requestPath.startsWith("/self/software-agent/tasks/"))) {
481
+ await handleSoftwareAgentTasksHttp(req, res, {
482
+ options,
483
+ workdir,
484
+ agentName: options.agentName,
485
+ });
486
+ return;
487
+ }
488
+ if (req.url === "/self/software-agent/reset" && req.method === "POST") {
489
+ await handleSoftwareAgentResetHttp(req, res, {
490
+ options,
260
491
  softwareAgent: codexSoftwareAgent,
261
492
  });
262
493
  return;
@@ -449,10 +680,13 @@ export async function serve(options) {
449
680
  workdir,
450
681
  model: process.env.AKEMON_CODEX_MODEL,
451
682
  sandbox: "workspace-write",
683
+ taskLedgerDir: softwareAgentTaskLedgerDir(workdir, options.agentName),
452
684
  });
453
- await codexSoftwareAgent.start(bus);
454
685
  // Peripheral registry — Core routes by capability
455
686
  const peripherals = [relay, engineP, codexSoftwareAgent];
687
+ for (const peripheral of peripherals) {
688
+ await peripheral.start(bus);
689
+ }
456
690
  // requestCompute: acquire the engine slot (priority-aware), execute with a
457
691
  // hard timeout, and release. The slot release and subprocess kill are both
458
692
  // driven by the same AbortController so a stuck engine can't hold the lock.