akemon 0.3.3 → 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
@@ -18,21 +18,37 @@ function parsePortOption(port) {
18
18
  const value = typeof port === "number" ? port : parseInt(String(port || "3000"));
19
19
  return Number.isInteger(value) && value > 0 ? value : 3000;
20
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
+ }
21
50
  async function callLocalOwnerEndpoint(path, opts, init = {}) {
22
- const credentials = await getOrCreateRelayCredentials();
23
- const port = parsePortOption(opts.port);
24
- const headers = {
25
- Authorization: `Bearer ${credentials.secretKey}`,
26
- };
27
- if (init.body !== undefined)
28
- headers["Content-Type"] = "application/json";
29
- const res = await fetch(`http://127.0.0.1:${port}${path}`, {
30
- ...init,
31
- headers: {
32
- ...headers,
33
- ...init.headers,
34
- },
35
- });
51
+ const res = await fetchLocalOwnerEndpoint(path, opts, init);
36
52
  const text = await res.text();
37
53
  let data;
38
54
  try {
@@ -47,6 +63,117 @@ async function callLocalOwnerEndpoint(path, opts, init = {}) {
47
63
  }
48
64
  return data;
49
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
+ }
50
177
  program
51
178
  .name("akemon")
52
179
  .description("Agent work marketplace — train your agent, let it work for others")
@@ -171,12 +298,14 @@ program
171
298
  .argument("<goal...>", "Task goal to send to the software agent")
172
299
  .option("-p, --port <port>", "Local akemon serve port", "3000")
173
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")
174
302
  .option("--role-scope <scope>", "Role scope: owner|public|order|agent|system", "owner")
175
303
  .option("--memory-scope <scope>", "Memory scope: none|public|task|owner", "owner")
176
304
  .option("--risk <level>", "Risk level: low|medium|high", "medium")
177
305
  .option("--memory-summary <text>", "Pre-filtered memory/context text to include")
178
306
  .option("--deliverable <text>", "Expected output shape")
179
307
  .option("--timeout-ms <ms>", "Task timeout in milliseconds")
308
+ .option("--no-stream", "Disable local streaming and wait for the final response")
180
309
  .action(async (goalParts, opts) => {
181
310
  const body = {
182
311
  goal: goalParts.join(" "),
@@ -186,6 +315,8 @@ program
186
315
  };
187
316
  if (opts.workdir)
188
317
  body.workdir = opts.workdir;
318
+ if (opts.allowOutsideWorkdir)
319
+ body.allowOutsideWorkdir = true;
189
320
  if (opts.memorySummary)
190
321
  body.memorySummary = opts.memorySummary;
191
322
  if (opts.deliverable)
@@ -198,6 +329,10 @@ program
198
329
  }
199
330
  body.timeoutMs = timeoutMs;
200
331
  }
332
+ if (opts.stream !== false) {
333
+ await streamLocalOwnerEndpoint("/self/software-agent/run-stream", opts, body);
334
+ return;
335
+ }
201
336
  const data = await callLocalOwnerEndpoint("/self/software-agent/run", opts, {
202
337
  method: "POST",
203
338
  body: JSON.stringify(body),
@@ -217,6 +352,26 @@ program
217
352
  });
218
353
  console.log(JSON.stringify(data, null, 2));
219
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
+ });
220
375
  program
221
376
  .command("software-agent-reset")
222
377
  .description("Reset the owner-only local software-agent peripheral session")
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,109 @@ 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
+ }
161
270
  export async function handleSoftwareAgentStatusHttp(req, res, deps) {
162
271
  if (!isOwnerRequest(req, deps.options)) {
163
272
  res.writeHead(401, { "Content-Type": "application/json" })
@@ -172,6 +281,58 @@ export async function handleSoftwareAgentStatusHttp(req, res, deps) {
172
281
  res.writeHead(200, { "Content-Type": "application/json" })
173
282
  .end(JSON.stringify(deps.softwareAgent.getState(), null, 2));
174
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
+ }
175
336
  export async function handleSoftwareAgentResetHttp(req, res, deps) {
176
337
  if (!isOwnerRequest(req, deps.options)) {
177
338
  res.writeHead(401, { "Content-Type": "application/json" })
@@ -205,7 +366,8 @@ import { LongTermModule } from "./longterm-module.js";
205
366
  import { ReflectionModule } from "./reflection-module.js";
206
367
  import { ScriptModule } from "./script-module.js";
207
368
  import { FileEventLog, PersistentEventBus } from "./event-bus.js";
208
- 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";
209
371
  import { SIG, sig } from "./types.js";
210
372
  import { loadConversation, listConversations, buildLLMContext } from "./context.js";
211
373
  import { createMcpServer, initMcpProxy, createMcpProxyServer } from "./mcp-server.js";
@@ -288,10 +450,20 @@ export async function serve(options) {
288
450
  if (!isQuiet)
289
451
  console.log(`[http] ${req.method} ${req.url} session=${req.headers["mcp-session-id"] || "none"}`);
290
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
+ }
291
462
  if (req.url === "/self/software-agent/run" && req.method === "POST") {
292
463
  await handleSoftwareAgentRunHttp(req, res, {
293
464
  options,
294
465
  workdir,
466
+ agentName: options.agentName,
295
467
  softwareAgent: codexSoftwareAgent,
296
468
  });
297
469
  return;
@@ -303,6 +475,16 @@ export async function serve(options) {
303
475
  });
304
476
  return;
305
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
+ }
306
488
  if (req.url === "/self/software-agent/reset" && req.method === "POST") {
307
489
  await handleSoftwareAgentResetHttp(req, res, {
308
490
  options,
@@ -498,10 +680,13 @@ export async function serve(options) {
498
680
  workdir,
499
681
  model: process.env.AKEMON_CODEX_MODEL,
500
682
  sandbox: "workspace-write",
683
+ taskLedgerDir: softwareAgentTaskLedgerDir(workdir, options.agentName),
501
684
  });
502
- await codexSoftwareAgent.start(bus);
503
685
  // Peripheral registry — Core routes by capability
504
686
  const peripherals = [relay, engineP, codexSoftwareAgent];
687
+ for (const peripheral of peripherals) {
688
+ await peripheral.start(bus);
689
+ }
505
690
  // requestCompute: acquire the engine slot (priority-aware), execute with a
506
691
  // hard timeout, and release. The slot release and subprocess kill are both
507
692
  // driven by the same AbortController so a stuck engine can't hold the lock.
@@ -0,0 +1,141 @@
1
+ import { buildLLMContext, loadConversation } from "./context.js";
2
+ import { buildRoleContext, loadRoles, resolveRoles } from "./role-module.js";
3
+ const DEFAULT_CONTEXT_BUDGET = 6000;
4
+ export async function buildSoftwareAgentMemorySummary(opts) {
5
+ const budget = opts.contextBudget ?? DEFAULT_CONTEXT_BUDGET;
6
+ const parts = [
7
+ "[Akemon memory boundary]",
8
+ `Role scope: ${opts.envelope.roleScope}`,
9
+ `Memory scope: ${opts.envelope.memoryScope}`,
10
+ boundaryDescription(opts.envelope.roleScope, opts.envelope.memoryScope),
11
+ ];
12
+ if (opts.envelope.memoryScope === "none") {
13
+ parts.push("No Akemon memory/context is included for this task.");
14
+ return parts.join("\n");
15
+ }
16
+ const request = normalizeRequest(opts.request);
17
+ const roleTrigger = readRequestString(request, "roleTrigger") || triggerForRoleScope(opts.envelope.roleScope);
18
+ const productName = readRequestString(request, "productName");
19
+ const productId = readRequestString(request, "productId");
20
+ const rolePolicy = await resolveRoleMemoryPolicy(opts.workdir, opts.agentName, roleTrigger);
21
+ if (rolePolicy.exclude.length) {
22
+ parts.push(`Active role exclusions: ${rolePolicy.exclude.join(", ")}`);
23
+ }
24
+ const roleContext = await buildRoleContext(opts.workdir, opts.agentName, roleTrigger, productName, productId);
25
+ if (roleContext.trim()) {
26
+ parts.push("");
27
+ parts.push("[Role/product context]");
28
+ parts.push(limitText(roleContext.trim(), Math.floor(budget * 0.55)));
29
+ }
30
+ const taskContext = readRequestString(request, "taskContext");
31
+ if (taskContext) {
32
+ parts.push("");
33
+ parts.push("[Task-provided context]");
34
+ parts.push(limitText(taskContext, Math.floor(budget * 0.25)));
35
+ }
36
+ const conversationId = readRequestString(request, "conversationId");
37
+ if (conversationId && canIncludeConversation(opts.envelope.roleScope, opts.envelope.memoryScope)) {
38
+ const conv = await loadConversation(opts.workdir, opts.agentName, conversationId);
39
+ const { text } = buildLLMContext(conv, Math.floor(budget * 0.3));
40
+ if (text.trim()) {
41
+ parts.push("");
42
+ parts.push("[Conversation context]");
43
+ parts.push(text.trim());
44
+ }
45
+ }
46
+ else if (conversationId) {
47
+ parts.push("");
48
+ parts.push("[Excluded conversation context]");
49
+ parts.push("A conversationId was supplied, but conversation memory is only included for owner-scoped software-agent tasks in v1.");
50
+ }
51
+ const ownerMemory = readRequestString(request, "memorySummary");
52
+ if (ownerMemory && canIncludeOwnerMemory(opts.envelope.roleScope, opts.envelope.memoryScope)) {
53
+ if (roleExcludesOwnerMemory(rolePolicy)) {
54
+ parts.push("");
55
+ parts.push("[Role-excluded owner memory]");
56
+ parts.push(`The active role (${rolePolicy.roleName || "unknown"}) excludes ${rolePolicy.exclude.join(", ")}, so owner-provided memory was not included.`);
57
+ }
58
+ else {
59
+ parts.push("");
60
+ parts.push("[Owner-visible memory]");
61
+ parts.push(limitText(ownerMemory, Math.floor(budget * 0.35)));
62
+ }
63
+ }
64
+ else if (ownerMemory) {
65
+ parts.push("");
66
+ parts.push("[Excluded owner memory]");
67
+ parts.push("A memorySummary was supplied, but it was not included because this envelope is not owner/owner scoped.");
68
+ }
69
+ return limitText(parts.join("\n"), budget);
70
+ }
71
+ export function canIncludeOwnerMemory(roleScope, memoryScope) {
72
+ return roleScope === "owner" && memoryScope === "owner";
73
+ }
74
+ function canIncludeConversation(roleScope, memoryScope) {
75
+ return roleScope === "owner" && (memoryScope === "owner" || memoryScope === "task");
76
+ }
77
+ function triggerForRoleScope(roleScope) {
78
+ switch (roleScope) {
79
+ case "owner": return "trigger:chat:owner";
80
+ case "public": return "trigger:chat:public";
81
+ case "order": return "trigger:order";
82
+ case "agent": return "trigger:agent_call";
83
+ case "system": return "trigger:system";
84
+ }
85
+ }
86
+ function boundaryDescription(roleScope, memoryScope) {
87
+ if (roleScope === "owner" && memoryScope === "owner") {
88
+ return "Owner-scoped task: owner-visible memory may be included after Akemon-side selection.";
89
+ }
90
+ if (memoryScope === "none") {
91
+ return "No-memory task: do not use Akemon private memory, conversation history, or subjective state.";
92
+ }
93
+ return "Non-owner task: exclude owner private conversations, personal notes, bio state, diary, subjective impressions, and owner-only memory.";
94
+ }
95
+ async function resolveRoleMemoryPolicy(workdir, agentName, roleTrigger) {
96
+ const roles = await loadRoles(workdir, agentName);
97
+ const { primary } = resolveRoles(roles, roleTrigger);
98
+ return {
99
+ roleName: primary?.name || null,
100
+ exclude: primary?.exclude || [],
101
+ };
102
+ }
103
+ function roleExcludesOwnerMemory(policy) {
104
+ return policy.exclude.some((item) => {
105
+ const normalized = item.toLowerCase();
106
+ return normalized.includes("owner")
107
+ || normalized.includes("private")
108
+ || normalized.includes("personal")
109
+ || normalized.includes("note")
110
+ || normalized.includes("diary")
111
+ || normalized.includes("bio")
112
+ || normalized.includes("全部记忆")
113
+ || normalized.includes("个人")
114
+ || normalized.includes("笔记")
115
+ || normalized.includes("日记")
116
+ || normalized.includes("状态");
117
+ });
118
+ }
119
+ function normalizeRequest(value) {
120
+ if (value === undefined || value === null)
121
+ return {};
122
+ if (typeof value !== "object" || Array.isArray(value)) {
123
+ throw new Error("Invalid request: expected object");
124
+ }
125
+ return value;
126
+ }
127
+ function readRequestString(request, field) {
128
+ const value = request[field];
129
+ if (value === undefined || value === null)
130
+ return "";
131
+ if (typeof value !== "string")
132
+ throw new Error(`Invalid ${field}: expected string`);
133
+ return value.trim();
134
+ }
135
+ function limitText(text, maxChars) {
136
+ if (text.length <= maxChars)
137
+ return text;
138
+ const head = Math.floor(maxChars * 0.45);
139
+ const tail = Math.max(0, maxChars - head - 40);
140
+ return `${text.slice(0, head)}\n[truncated ${text.length - head - tail} chars]\n${text.slice(-tail)}`;
141
+ }
@@ -11,7 +11,9 @@
11
11
  * transport to app-server or a true persistent interactive session.
12
12
  */
13
13
  import { randomUUID } from "crypto";
14
- import { spawn } from "child_process";
14
+ import { spawn, spawnSync } from "child_process";
15
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
16
+ import { isAbsolute, join, relative, resolve as resolvePath } from "path";
15
17
  import { StringDecoder } from "string_decoder";
16
18
  import { SIG, sig } from "./types.js";
17
19
  import { sendTaskEnd, sendTaskStart, sendTaskStream } from "./relay-client.js";
@@ -30,6 +32,9 @@ const DEFAULT_OWNER_FORBIDDEN_ACTIONS = [
30
32
  const ROLE_SCOPES = ["owner", "public", "order", "agent", "system"];
31
33
  const MEMORY_SCOPES = ["none", "public", "task", "owner"];
32
34
  const RISK_LEVELS = ["low", "medium", "high"];
35
+ const MAX_STREAM_SUMMARY_CHARS = 12_000;
36
+ const STREAM_SUMMARY_HEAD_CHARS = 4_000;
37
+ const STREAM_SUMMARY_TAIL_CHARS = 8_000;
33
38
  export class CodexSoftwareAgentPeripheral {
34
39
  id;
35
40
  name;
@@ -39,6 +44,7 @@ export class CodexSoftwareAgentPeripheral {
39
44
  bus = null;
40
45
  activeChild = null;
41
46
  activeTaskId = null;
47
+ activeWorkdir = null;
42
48
  sessionId = randomUUID();
43
49
  constructor(config) {
44
50
  this.config = config;
@@ -73,15 +79,21 @@ export class CodexSoftwareAgentPeripheral {
73
79
  }
74
80
  this.activeChild = null;
75
81
  this.activeTaskId = null;
82
+ this.activeWorkdir = null;
76
83
  this.sessionId = randomUUID();
77
84
  }
78
85
  getState() {
86
+ const currentWorkdir = this.activeWorkdir || resolvePath(this.config.workdir);
79
87
  return {
80
88
  id: this.id,
81
89
  sessionId: this.sessionId,
82
90
  activeTaskId: this.activeTaskId,
91
+ activeWorkdir: this.activeWorkdir,
83
92
  busy: !!this.activeChild,
84
93
  transport: "codex-exec",
94
+ baseWorkdir: resolvePath(this.config.workdir),
95
+ workdirStatus: this.collectWorkdirStatus(currentWorkdir),
96
+ taskLedgerDir: this.config.taskLedgerDir,
85
97
  };
86
98
  }
87
99
  async send(signal) {
@@ -97,13 +109,16 @@ export class CodexSoftwareAgentPeripheral {
97
109
  const result = await this.sendTask(envelope);
98
110
  return sig(SIG.SOFTWARE_AGENT_RESPONSE, { ...result }, this.id);
99
111
  }
100
- async sendTask(envelope, signal) {
112
+ async sendTask(envelope, taskOptions) {
101
113
  if (this.activeChild) {
102
114
  throw new Error(`Software agent busy (task=${this.activeTaskId})`);
103
115
  }
116
+ const { signal, observer } = normalizeSoftwareAgentTaskOptions(taskOptions);
104
117
  const taskId = envelope.taskId || `sw_${Date.now()}_${randomUUID().slice(0, 8)}`;
105
- const workdir = envelope.workdir || this.config.workdir;
106
- const prompt = buildTaskEnvelopePrompt({ ...envelope, taskId, workdir });
118
+ const workdirSafety = resolveWorkdirSafety(this.config.workdir, envelope.workdir || this.config.workdir, envelope.workdirSafety?.allowOutsideWorkdir || false);
119
+ const workdir = workdirSafety.effectiveWorkdir;
120
+ const effectiveEnvelope = { ...envelope, taskId, workdir, workdirSafety };
121
+ const prompt = buildTaskEnvelopePrompt(effectiveEnvelope);
107
122
  const { cmd, args } = buildCodexExecCommand({
108
123
  command: this.config.command || "codex",
109
124
  workdir,
@@ -111,17 +126,38 @@ export class CodexSoftwareAgentPeripheral {
111
126
  sandbox: this.config.sandbox || "workspace-write",
112
127
  });
113
128
  const startedAt = Date.now();
129
+ const startedAtIso = new Date(startedAt).toISOString();
114
130
  const relay = this.config.taskRelay || defaultTaskRelay;
115
131
  const commandLine = [cmd, ...args].join(" ");
116
132
  const spawnImpl = this.config.spawnImpl || spawn;
117
133
  const timeoutMs = envelope.timeoutMs || this.config.defaultTimeoutMs || DEFAULT_TIMEOUT_MS;
134
+ const workdirStatus = this.collectWorkdirStatus(workdir);
135
+ const baseTaskRecord = () => ({
136
+ schemaVersion: 1,
137
+ taskId,
138
+ agentId: this.id,
139
+ sessionId: this.sessionId,
140
+ transport: "codex-exec",
141
+ commandLine,
142
+ envelope: effectiveEnvelope,
143
+ startedAt: startedAtIso,
144
+ workdirStatus,
145
+ });
118
146
  return new Promise((resolve) => {
119
147
  this.activeTaskId = taskId;
120
- relay.sendTaskStart(taskId, "software_agent", commandLine);
148
+ this.activeWorkdir = workdir;
149
+ this.writeTaskRecord({
150
+ ...baseTaskRecord(),
151
+ status: "running",
152
+ updatedAt: startedAtIso,
153
+ });
154
+ const origin = "software_agent";
155
+ relay.sendTaskStart(taskId, origin, commandLine);
156
+ observer?.onStart?.({ taskId, origin, commandLine });
121
157
  this.bus?.emit(SIG.TASK_STARTED, sig(SIG.TASK_STARTED, {
122
158
  taskId,
123
159
  taskType: "software_agent",
124
- description: envelope.goal,
160
+ description: effectiveEnvelope.goal,
125
161
  peripheral: this.id,
126
162
  sessionId: this.sessionId,
127
163
  }, this.id));
@@ -137,8 +173,8 @@ export class CodexSoftwareAgentPeripheral {
137
173
  catch (err) {
138
174
  this.activeChild = null;
139
175
  this.activeTaskId = null;
176
+ this.activeWorkdir = null;
140
177
  const durationMs = Date.now() - startedAt;
141
- relay.sendTaskEnd(taskId, null, durationMs);
142
178
  const result = {
143
179
  success: false,
144
180
  taskId,
@@ -147,6 +183,19 @@ export class CodexSoftwareAgentPeripheral {
147
183
  exitCode: null,
148
184
  durationMs,
149
185
  };
186
+ relay.sendTaskEnd(taskId, null, durationMs);
187
+ observer?.onEnd?.({ taskId, exitCode: null, durationMs, result });
188
+ const completedAt = new Date().toISOString();
189
+ this.writeTaskRecord({
190
+ ...baseTaskRecord(),
191
+ status: "failed",
192
+ updatedAt: completedAt,
193
+ completedAt,
194
+ durationMs,
195
+ result,
196
+ stdoutSummary: summarizeText(""),
197
+ stderrSummary: summarizeText(result.error || ""),
198
+ });
150
199
  this.bus?.emit(SIG.TASK_FAILED, sig(SIG.TASK_FAILED, result, this.id));
151
200
  resolve(result);
152
201
  return;
@@ -169,15 +218,17 @@ export class CodexSoftwareAgentPeripheral {
169
218
  if (tailOut) {
170
219
  stdout += tailOut;
171
220
  relay.sendTaskStream(taskId, "stdout", tailOut);
221
+ observer?.onStream?.({ taskId, stream: "stdout", chunk: tailOut });
172
222
  }
173
223
  if (tailErr) {
174
224
  stderr += tailErr;
175
225
  relay.sendTaskStream(taskId, "stderr", tailErr);
226
+ observer?.onStream?.({ taskId, stream: "stderr", chunk: tailErr });
176
227
  }
177
228
  const durationMs = Date.now() - startedAt;
178
- relay.sendTaskEnd(taskId, exitCode, durationMs);
179
229
  this.activeChild = null;
180
230
  this.activeTaskId = null;
231
+ this.activeWorkdir = null;
181
232
  const output = stdout.trim() || stderr.trim();
182
233
  const success = !error && !aborted && exitCode === 0;
183
234
  const result = {
@@ -188,6 +239,19 @@ export class CodexSoftwareAgentPeripheral {
188
239
  exitCode,
189
240
  durationMs,
190
241
  };
242
+ relay.sendTaskEnd(taskId, exitCode, durationMs);
243
+ observer?.onEnd?.({ taskId, exitCode, durationMs, result });
244
+ const completedAt = new Date().toISOString();
245
+ this.writeTaskRecord({
246
+ ...baseTaskRecord(),
247
+ status: success ? "completed" : "failed",
248
+ updatedAt: completedAt,
249
+ completedAt,
250
+ durationMs,
251
+ result,
252
+ stdoutSummary: summarizeText(stdout),
253
+ stderrSummary: summarizeText(stderr),
254
+ });
191
255
  this.bus?.emit(success ? SIG.TASK_COMPLETED : SIG.TASK_FAILED, sig(success ? SIG.TASK_COMPLETED : SIG.TASK_FAILED, {
192
256
  ...result,
193
257
  taskLabel: `software_agent:${this.id}`,
@@ -239,6 +303,7 @@ export class CodexSoftwareAgentPeripheral {
239
303
  return;
240
304
  stdout += text;
241
305
  relay.sendTaskStream(taskId, "stdout", text);
306
+ observer?.onStream?.({ taskId, stream: "stdout", chunk: text });
242
307
  });
243
308
  child.stderr?.on("data", (chunk) => {
244
309
  const text = errDecoder.write(chunk);
@@ -246,6 +311,7 @@ export class CodexSoftwareAgentPeripheral {
246
311
  return;
247
312
  stderr += text;
248
313
  relay.sendTaskStream(taskId, "stderr", text);
314
+ observer?.onStream?.({ taskId, stream: "stderr", chunk: text });
249
315
  });
250
316
  child.on("close", (code) => {
251
317
  child.unref();
@@ -257,6 +323,48 @@ export class CodexSoftwareAgentPeripheral {
257
323
  });
258
324
  });
259
325
  }
326
+ writeTaskRecord(record) {
327
+ const dir = this.config.taskLedgerDir;
328
+ if (!dir)
329
+ return;
330
+ try {
331
+ mkdirSync(dir, { recursive: true });
332
+ const safeTaskId = safeTaskFilename(record.taskId);
333
+ writeFileSync(join(dir, `${safeTaskId}.json`), `${JSON.stringify(record, null, 2)}\n`);
334
+ }
335
+ catch (err) {
336
+ console.error(`[software-agent] Failed to write task ledger: ${err.message || String(err)}`);
337
+ }
338
+ }
339
+ collectWorkdirStatus(workdir) {
340
+ const impl = this.config.gitStatusImpl || readGitWorktreeStatus;
341
+ return impl(workdir);
342
+ }
343
+ }
344
+ export function summarizeText(text, maxChars = MAX_STREAM_SUMMARY_CHARS) {
345
+ const normalized = text || "";
346
+ const chars = normalized.length;
347
+ const bytes = Buffer.byteLength(normalized, "utf8");
348
+ const lines = normalized ? normalized.split(/\r\n|\r|\n/).length : 0;
349
+ if (chars <= maxChars) {
350
+ return { chars, bytes, lines, text: normalized, truncated: false };
351
+ }
352
+ const headChars = Math.min(STREAM_SUMMARY_HEAD_CHARS, maxChars);
353
+ const tailChars = Math.max(0, maxChars - headChars);
354
+ const omittedChars = chars - headChars - tailChars;
355
+ const tailText = tailChars > 0 ? normalized.slice(-tailChars) : "";
356
+ return {
357
+ chars,
358
+ bytes,
359
+ lines,
360
+ text: [
361
+ normalized.slice(0, headChars),
362
+ `[truncated ${omittedChars} chars]`,
363
+ tailText,
364
+ ].join("\n"),
365
+ truncated: true,
366
+ omittedChars,
367
+ };
260
368
  }
261
369
  export function buildTaskEnvelopePrompt(envelope) {
262
370
  const lines = [
@@ -269,11 +377,15 @@ export function buildTaskEnvelopePrompt(envelope) {
269
377
  `Memory scope: ${envelope.memoryScope}`,
270
378
  `Risk level: ${envelope.riskLevel}`,
271
379
  `Workdir: ${envelope.workdir}`,
272
- "",
273
- "Goal:",
274
- envelope.goal,
275
- "",
276
380
  ];
381
+ if (envelope.workdirSafety) {
382
+ lines.push(`Base workdir: ${envelope.workdirSafety.baseWorkdir}`);
383
+ lines.push(`Requested workdir: ${envelope.workdirSafety.requestedWorkdir}`);
384
+ lines.push(`Effective workdir: ${envelope.workdirSafety.effectiveWorkdir}`);
385
+ lines.push(`Outside base workdir: ${envelope.workdirSafety.outsideBaseWorkdir ? "yes" : "no"}`);
386
+ lines.push(`Outside workdir explicitly allowed: ${envelope.workdirSafety.allowOutsideWorkdir ? "yes" : "no"}`);
387
+ }
388
+ lines.push("", "Goal:", envelope.goal, "");
277
389
  if (envelope.memorySummary?.trim()) {
278
390
  lines.push("Visible Akemon memory/context:");
279
391
  lines.push(envelope.memorySummary.trim());
@@ -307,27 +419,123 @@ export function createOwnerTaskEnvelope(body, defaultWorkdir) {
307
419
  const goal = typeof body?.goal === "string" ? body.goal.trim() : "";
308
420
  if (!goal)
309
421
  throw new Error("Missing required string field: goal");
310
- const callerForbiddenActions = readOptionalStringArray(body.forbiddenActions, "forbiddenActions");
422
+ const callerForbiddenActions = readOptionalStringArray(body?.forbiddenActions, "forbiddenActions");
423
+ const requestedWorkdir = readOptionalString(body?.workdir, "workdir") || defaultWorkdir;
424
+ const workdirSafety = resolveWorkdirSafety(defaultWorkdir, requestedWorkdir, readOptionalBoolean(body?.allowOutsideWorkdir, "allowOutsideWorkdir") || false);
311
425
  return {
312
- taskId: readOptionalString(body.taskId, "taskId"),
426
+ taskId: readOptionalString(body?.taskId, "taskId"),
313
427
  sourceModule: "owner-http",
314
- purpose: readOptionalString(body.purpose, "purpose") || "owner software-agent task",
428
+ purpose: readOptionalString(body?.purpose, "purpose") || "owner software-agent task",
315
429
  goal,
316
- workdir: readOptionalString(body.workdir, "workdir") || defaultWorkdir,
317
- roleScope: readEnum(body.roleScope, "roleScope", ROLE_SCOPES, "owner"),
318
- memoryScope: readEnum(body.memoryScope, "memoryScope", MEMORY_SCOPES, "owner"),
319
- riskLevel: readEnum(body.riskLevel, "riskLevel", RISK_LEVELS, "medium"),
320
- allowedActions: body.allowedActions !== undefined
321
- ? readOptionalStringArray(body.allowedActions, "allowedActions")
430
+ workdir: workdirSafety.effectiveWorkdir,
431
+ workdirSafety,
432
+ roleScope: readEnum(body?.roleScope, "roleScope", ROLE_SCOPES, "owner"),
433
+ memoryScope: readEnum(body?.memoryScope, "memoryScope", MEMORY_SCOPES, "owner"),
434
+ riskLevel: readEnum(body?.riskLevel, "riskLevel", RISK_LEVELS, "medium"),
435
+ allowedActions: body?.allowedActions !== undefined
436
+ ? readOptionalStringArray(body?.allowedActions, "allowedActions")
322
437
  : [...DEFAULT_OWNER_ALLOWED_ACTIONS],
323
438
  forbiddenActions: [...new Set([...DEFAULT_OWNER_FORBIDDEN_ACTIONS, ...callerForbiddenActions])],
324
- memorySummary: typeof body.memorySummary === "string" ? body.memorySummary : "",
325
- deliverable: typeof body.deliverable === "string"
439
+ memorySummary: typeof body?.memorySummary === "string" ? body.memorySummary : "",
440
+ deliverable: typeof body?.deliverable === "string"
326
441
  ? body.deliverable
327
442
  : "Return a concise engineering summary with changes, verification, and remaining risks.",
328
- timeoutMs: readTimeoutMs(body.timeoutMs),
443
+ timeoutMs: readTimeoutMs(body?.timeoutMs),
329
444
  };
330
445
  }
446
+ export function resolveWorkdirSafety(baseWorkdir, requestedWorkdir, allowOutsideWorkdir = false) {
447
+ const base = resolvePath(baseWorkdir);
448
+ const requested = isAbsolute(requestedWorkdir)
449
+ ? resolvePath(requestedWorkdir)
450
+ : resolvePath(base, requestedWorkdir);
451
+ const rel = relative(base, requested);
452
+ const outsideBaseWorkdir = !!rel && (rel.startsWith("..") || isAbsolute(rel));
453
+ if (outsideBaseWorkdir && !allowOutsideWorkdir) {
454
+ throw new Error(`Invalid workdir: ${requested} is outside base workdir ${base}`);
455
+ }
456
+ return {
457
+ baseWorkdir: base,
458
+ requestedWorkdir,
459
+ effectiveWorkdir: requested,
460
+ allowOutsideWorkdir,
461
+ outsideBaseWorkdir,
462
+ };
463
+ }
464
+ export function readGitWorktreeStatus(workdir) {
465
+ const resolvedWorkdir = resolvePath(workdir);
466
+ try {
467
+ const rootResult = spawnSync("git", ["-C", resolvedWorkdir, "rev-parse", "--show-toplevel"], {
468
+ encoding: "utf8",
469
+ timeout: 5000,
470
+ });
471
+ if (rootResult.status !== 0) {
472
+ return {
473
+ workdir: resolvedWorkdir,
474
+ isRepo: false,
475
+ dirty: false,
476
+ changedFiles: [],
477
+ error: summarizeGitError(rootResult.stderr, rootResult.error),
478
+ };
479
+ }
480
+ const root = String(rootResult.stdout || "").trim();
481
+ const statusResult = spawnSync("git", ["-C", resolvedWorkdir, "status", "--short"], {
482
+ encoding: "utf8",
483
+ timeout: 5000,
484
+ });
485
+ if (statusResult.status !== 0) {
486
+ return {
487
+ workdir: resolvedWorkdir,
488
+ isRepo: true,
489
+ dirty: false,
490
+ changedFiles: [],
491
+ root,
492
+ error: summarizeGitError(statusResult.stderr, statusResult.error),
493
+ };
494
+ }
495
+ const changedFiles = String(statusResult.stdout || "")
496
+ .split(/\r?\n/)
497
+ .map((line) => line.trimEnd())
498
+ .filter(Boolean)
499
+ .map((line) => line.slice(3).trim())
500
+ .filter(Boolean);
501
+ return {
502
+ workdir: resolvedWorkdir,
503
+ isRepo: true,
504
+ dirty: changedFiles.length > 0,
505
+ changedFiles,
506
+ root,
507
+ };
508
+ }
509
+ catch (err) {
510
+ return {
511
+ workdir: resolvedWorkdir,
512
+ isRepo: false,
513
+ dirty: false,
514
+ changedFiles: [],
515
+ error: err.message || String(err),
516
+ };
517
+ }
518
+ }
519
+ export function listSoftwareAgentTaskRecords(taskLedgerDir, limit = 20) {
520
+ const safeLimit = normalizeTaskRecordLimit(limit);
521
+ try {
522
+ if (!existsSync(taskLedgerDir))
523
+ return [];
524
+ return readdirSync(taskLedgerDir, { withFileTypes: true })
525
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
526
+ .map((entry) => readSoftwareAgentTaskRecordFile(join(taskLedgerDir, entry.name)))
527
+ .filter((record) => !!record)
528
+ .sort(compareSoftwareAgentTaskRecords)
529
+ .slice(0, safeLimit);
530
+ }
531
+ catch {
532
+ return [];
533
+ }
534
+ }
535
+ export function readSoftwareAgentTaskRecord(taskLedgerDir, taskId) {
536
+ const file = join(taskLedgerDir, `${safeTaskFilename(taskId)}.json`);
537
+ return readSoftwareAgentTaskRecordFile(file);
538
+ }
331
539
  function readOptionalString(value, field) {
332
540
  if (value === undefined || value === null)
333
541
  return undefined;
@@ -336,6 +544,19 @@ function readOptionalString(value, field) {
336
544
  const trimmed = value.trim();
337
545
  return trimmed || undefined;
338
546
  }
547
+ function normalizeSoftwareAgentTaskOptions(options) {
548
+ if (!options)
549
+ return {};
550
+ if (isAbortSignal(options)) {
551
+ return { signal: options };
552
+ }
553
+ return options;
554
+ }
555
+ function isAbortSignal(value) {
556
+ return !!value
557
+ && typeof value.aborted === "boolean"
558
+ && typeof value.addEventListener === "function";
559
+ }
339
560
  function readOptionalStringArray(value, field) {
340
561
  if (value === undefined || value === null)
341
562
  return [];
@@ -348,6 +569,13 @@ function readOptionalStringArray(value, field) {
348
569
  return item.trim();
349
570
  });
350
571
  }
572
+ function readOptionalBoolean(value, field) {
573
+ if (value === undefined || value === null)
574
+ return undefined;
575
+ if (typeof value !== "boolean")
576
+ throw new Error(`Invalid ${field}: expected boolean`);
577
+ return value;
578
+ }
351
579
  function readEnum(value, field, allowed, fallback) {
352
580
  if (value === undefined || value === null || value === "")
353
581
  return fallback;
@@ -367,6 +595,51 @@ function readTimeoutMs(value) {
367
595
  }
368
596
  return value;
369
597
  }
598
+ function safeTaskFilename(taskId) {
599
+ const safe = taskId.replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 200);
600
+ return safe || "task";
601
+ }
602
+ function summarizeGitError(stderr, error) {
603
+ if (error)
604
+ return error.message;
605
+ const text = typeof stderr === "string" ? stderr.trim() : "";
606
+ return text || undefined;
607
+ }
608
+ function normalizeTaskRecordLimit(limit) {
609
+ if (!Number.isInteger(limit) || limit <= 0)
610
+ return 20;
611
+ return Math.min(limit, 100);
612
+ }
613
+ function readSoftwareAgentTaskRecordFile(file) {
614
+ try {
615
+ if (!existsSync(file))
616
+ return null;
617
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
618
+ if (!isSoftwareAgentTaskRecord(parsed))
619
+ return null;
620
+ return parsed;
621
+ }
622
+ catch {
623
+ return null;
624
+ }
625
+ }
626
+ function isSoftwareAgentTaskRecord(value) {
627
+ return value
628
+ && value.schemaVersion === 1
629
+ && typeof value.taskId === "string"
630
+ && (value.status === "running" || value.status === "completed" || value.status === "failed")
631
+ && typeof value.startedAt === "string"
632
+ && typeof value.updatedAt === "string"
633
+ && value.envelope
634
+ && typeof value.envelope.goal === "string";
635
+ }
636
+ function compareSoftwareAgentTaskRecords(a, b) {
637
+ const bTime = Date.parse(b.updatedAt || b.startedAt) || 0;
638
+ const aTime = Date.parse(a.updatedAt || a.startedAt) || 0;
639
+ if (bTime !== aTime)
640
+ return bTime - aTime;
641
+ return b.taskId.localeCompare(a.taskId);
642
+ }
370
643
  function buildCodexExecCommand(opts) {
371
644
  const args = [
372
645
  "exec",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akemon",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "Agent work marketplace — train your agent, let it work for others",
5
5
  "type": "module",
6
6
  "license": "MIT",