akemon 0.3.5 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/DATA_POLICY.md +128 -0
  2. package/README.md +156 -19
  3. package/TRADEMARK.md +74 -0
  4. package/dist/akemon-home.js +56 -0
  5. package/dist/akemon-message.js +107 -0
  6. package/dist/best-effort.js +8 -0
  7. package/dist/cli.js +1411 -132
  8. package/dist/cognitive-artifact-store.js +101 -0
  9. package/dist/cognitive-event-log.js +47 -0
  10. package/dist/config.js +45 -9
  11. package/dist/context.js +27 -6
  12. package/dist/core/contracts/layers.js +1 -0
  13. package/dist/core/contracts/permission.js +1 -0
  14. package/dist/core/contracts/workspace.js +1 -0
  15. package/dist/core-cognitive-module.js +768 -0
  16. package/dist/engine-peripheral.js +127 -26
  17. package/dist/engine-routing.js +58 -17
  18. package/dist/interactive-session.js +361 -0
  19. package/dist/local-interconnect.js +156 -0
  20. package/dist/local-registry.js +178 -0
  21. package/dist/mcp-server.js +4 -1
  22. package/dist/memory-proposal.js +379 -0
  23. package/dist/memory-recorder.js +368 -0
  24. package/dist/orphan-scan.js +36 -24
  25. package/dist/passive-reflection-cognitive-module.js +172 -0
  26. package/dist/peripheral-registry.js +235 -0
  27. package/dist/permission-audit.js +132 -0
  28. package/dist/relay-client.js +68 -9
  29. package/dist/relay-mode.js +34 -0
  30. package/dist/relay-peripheral.js +139 -49
  31. package/dist/runtime-platform.js +122 -0
  32. package/dist/secretariat/client.js +87 -0
  33. package/dist/self.js +15 -6
  34. package/dist/server.js +3695 -439
  35. package/dist/social-discovery.js +231 -0
  36. package/dist/software-agent-peripheral.js +314 -235
  37. package/dist/software-agent-result-cli.js +69 -0
  38. package/dist/software-agent-stream-cli.js +23 -0
  39. package/dist/software-agent-transport.js +177 -0
  40. package/dist/task-module.js +243 -0
  41. package/dist/task-registry.js +756 -0
  42. package/dist/vendor/xterm/addon-fit.js +2 -0
  43. package/dist/vendor/xterm/addon-search.js +2 -0
  44. package/dist/vendor/xterm/addon-web-links.js +2 -0
  45. package/dist/vendor/xterm/xterm.css +285 -0
  46. package/dist/vendor/xterm/xterm.js +2 -0
  47. package/dist/work-memory.js +339 -0
  48. package/dist/workbench-peripheral-guide.js +79 -0
  49. package/dist/workbench-session.js +1074 -0
  50. package/dist/workbench.html +4011 -0
  51. package/package.json +11 -4
  52. package/scripts/build.cjs +24 -0
  53. package/scripts/check-architecture-baseline.cjs +68 -0
  54. package/scripts/test.cjs +38 -0
@@ -11,13 +11,17 @@
11
11
  * transport to app-server or a true persistent interactive session.
12
12
  */
13
13
  import { randomUUID } from "crypto";
14
- import { spawn, spawnSync } from "child_process";
14
+ import { spawnSync } from "child_process";
15
15
  import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
16
16
  import { isAbsolute, join, relative, resolve as resolvePath } from "path";
17
- import { StringDecoder } from "string_decoder";
18
17
  import { SIG, sig } from "./types.js";
19
18
  import { sendTaskEnd, sendTaskStart, sendTaskStream } from "./relay-client.js";
20
- import { redactSecrets, StreamingRedactor } from "./redaction.js";
19
+ import { redactSecrets } from "./redaction.js";
20
+ import { terminateProcessTree } from "./runtime-platform.js";
21
+ import { actorRef } from "./akemon-message.js";
22
+ import { appendPermissionAuditRecordSync } from "./permission-audit.js";
23
+ import { logBestEffortError } from "./best-effort.js";
24
+ import { CodexExecTransport, } from "./software-agent-transport.js";
21
25
  const defaultTaskRelay = {
22
26
  sendTaskStart,
23
27
  sendTaskStream,
@@ -78,12 +82,19 @@ export class CodexSoftwareAgentPeripheral {
78
82
  activeTaskId = null;
79
83
  activeWorkdir = null;
80
84
  sessionId = randomUUID();
85
+ transport;
81
86
  constructor(config) {
82
87
  this.config = {
83
88
  ...config,
84
89
  envPolicy: normalizeSoftwareAgentEnvPolicy(config.envPolicy),
85
90
  envAllowlist: normalizeSoftwareAgentEnvAllowlist(config.envAllowlist),
86
91
  };
92
+ this.transport = config.transport || new CodexExecTransport({
93
+ command: config.command || "codex",
94
+ model: config.model,
95
+ sandbox: config.sandbox || "workspace-write",
96
+ spawnImpl: config.spawnImpl,
97
+ });
87
98
  this.id = config.id || "software-agent:codex";
88
99
  this.name = config.name || "Codex CLI Software Agent";
89
100
  }
@@ -103,18 +114,9 @@ export class CodexSoftwareAgentPeripheral {
103
114
  async resetSession() {
104
115
  const activePid = this.activeChild?.pid;
105
116
  if (activePid) {
106
- const processGroupId = -activePid;
107
- try {
108
- process.kill(processGroupId, "SIGTERM");
109
- }
110
- catch { }
111
- setTimeout(() => {
112
- try {
113
- process.kill(processGroupId, "SIGKILL");
114
- }
115
- catch { }
116
- }, 3000).unref();
117
+ terminateProcessTree(activePid, { signal: "SIGTERM", forceAfterMs: 3000 });
117
118
  }
119
+ await this.transport.reset?.();
118
120
  this.activeChild = null;
119
121
  this.activeTaskId = null;
120
122
  this.activeWorkdir = null;
@@ -127,12 +129,14 @@ export class CodexSoftwareAgentPeripheral {
127
129
  sessionId: this.sessionId,
128
130
  activeTaskId: this.activeTaskId,
129
131
  activeWorkdir: this.activeWorkdir,
130
- busy: !!this.activeChild,
131
- transport: "codex-exec",
132
+ busy: !!this.activeTaskId || !!this.activeChild,
133
+ transport: this.transport.kind,
134
+ transportContract: this.transport.describe(currentWorkdir),
132
135
  baseWorkdir: resolvePath(this.config.workdir),
133
136
  workdirStatus: this.collectWorkdirStatus(currentWorkdir),
134
137
  taskLedgerDir: this.config.taskLedgerDir,
135
138
  contextSessionDir: this.config.contextSessionDir,
139
+ workMemoryDir: this.config.workMemoryDir,
136
140
  environment: buildSoftwareAgentChildEnvironment({
137
141
  policy: this.config.envPolicy,
138
142
  allowlist: this.config.envAllowlist,
@@ -154,7 +158,17 @@ export class CodexSoftwareAgentPeripheral {
154
158
  return sig(SIG.SOFTWARE_AGENT_RESPONSE, { ...result }, this.id);
155
159
  }
156
160
  async sendTask(envelope, taskOptions) {
157
- if (this.activeChild) {
161
+ if (this.activeTaskId || this.activeChild) {
162
+ this.writePermissionAudit({
163
+ envelope,
164
+ taskId: envelope.taskId,
165
+ workdir: envelope.workdir || this.config.workdir,
166
+ decision: {
167
+ result: "denied",
168
+ mode: "automatic",
169
+ reason: `Software agent busy (task=${this.activeTaskId})`,
170
+ },
171
+ });
158
172
  throw new Error(`Software agent busy (task=${this.activeTaskId})`);
159
173
  }
160
174
  const { signal, observer } = normalizeSoftwareAgentTaskOptions(taskOptions);
@@ -162,7 +176,14 @@ export class CodexSoftwareAgentPeripheral {
162
176
  const contextSessionId = normalizeContextSessionId(envelope.contextSessionId) || taskId;
163
177
  const workdirSafety = resolveWorkdirSafety(this.config.workdir, envelope.workdir || this.config.workdir, envelope.workdirSafety?.allowOutsideWorkdir || false);
164
178
  const workdir = workdirSafety.effectiveWorkdir;
165
- let effectiveEnvelope = { ...envelope, taskId, contextSessionId, workdir, workdirSafety };
179
+ let effectiveEnvelope = {
180
+ ...envelope,
181
+ taskId,
182
+ contextSessionId,
183
+ workdir,
184
+ workdirSafety,
185
+ workMemoryDir: envelope.workMemoryDir || this.config.workMemoryDir,
186
+ };
166
187
  const contextSession = this.config.contextSessionDir
167
188
  ? writeSoftwareAgentContextPacket(this.config.contextSessionDir, effectiveEnvelope)
168
189
  : undefined;
@@ -171,30 +192,42 @@ export class CodexSoftwareAgentPeripheral {
171
192
  const prompt = contextSession
172
193
  ? buildContextPacketLaunchPrompt(effectiveEnvelope, contextSession.audit.packetPath)
173
194
  : buildTaskEnvelopePrompt(effectiveEnvelope);
174
- const { cmd, args } = buildCodexExecCommand({
175
- command: this.config.command || "codex",
176
- workdir,
177
- model: this.config.model,
178
- sandbox: this.config.sandbox || "workspace-write",
179
- });
195
+ const transportDescriptor = this.transport.describe(workdir);
180
196
  const startedAt = Date.now();
181
197
  const startedAtIso = new Date(startedAt).toISOString();
182
198
  const relay = this.config.taskRelay || defaultTaskRelay;
183
- const commandLine = [cmd, ...args].join(" ");
184
- const spawnImpl = this.config.spawnImpl || spawn;
199
+ const commandLine = transportDescriptor.commandLine;
185
200
  const timeoutMs = envelope.timeoutMs || this.config.defaultTimeoutMs || DEFAULT_TIMEOUT_MS;
186
201
  const workdirStatus = this.collectWorkdirStatus(workdir);
202
+ const taskMetadata = {
203
+ contextSessionId: effectiveEnvelope.contextSessionId,
204
+ contextPacketPath: effectiveEnvelope.contextPacketPath,
205
+ workMemoryDir: effectiveEnvelope.workMemoryDir,
206
+ };
187
207
  const childEnvironment = buildSoftwareAgentChildEnvironment({
188
208
  policy: this.config.envPolicy,
189
209
  allowlist: this.config.envAllowlist,
190
210
  sourceEnv: this.config.sourceEnv,
191
211
  });
212
+ this.writePermissionAudit({
213
+ envelope: effectiveEnvelope,
214
+ taskId,
215
+ workdir,
216
+ workdirSafety,
217
+ commandLine,
218
+ environment: childEnvironment.audit,
219
+ decision: {
220
+ result: "allowed",
221
+ mode: "owner-approved",
222
+ reason: "Owner local endpoint accepted the software-agent task envelope.",
223
+ },
224
+ });
192
225
  const baseTaskRecord = () => ({
193
226
  schemaVersion: 1,
194
227
  taskId,
195
228
  agentId: this.id,
196
229
  sessionId: this.sessionId,
197
- transport: "codex-exec",
230
+ transport: this.transport.kind,
198
231
  commandLine,
199
232
  envelope: effectiveEnvelope,
200
233
  startedAt: startedAtIso,
@@ -202,197 +235,103 @@ export class CodexSoftwareAgentPeripheral {
202
235
  contextSession: contextSession?.audit,
203
236
  workdirStatus,
204
237
  });
205
- return new Promise((resolve) => {
206
- this.activeTaskId = taskId;
207
- this.activeWorkdir = workdir;
208
- this.writeTaskRecord({
209
- ...baseTaskRecord(),
210
- status: "running",
211
- updatedAt: startedAtIso,
238
+ this.activeTaskId = taskId;
239
+ this.activeWorkdir = workdir;
240
+ this.writeTaskRecord({
241
+ ...baseTaskRecord(),
242
+ status: "running",
243
+ updatedAt: startedAtIso,
244
+ });
245
+ const origin = "software_agent";
246
+ relay.sendTaskStart(taskId, origin, commandLine);
247
+ observer?.onStart?.({ taskId, origin, commandLine, ...taskMetadata });
248
+ this.bus?.emit(SIG.TASK_STARTED, sig(SIG.TASK_STARTED, {
249
+ taskId,
250
+ taskType: "software_agent",
251
+ description: effectiveEnvelope.goal,
252
+ peripheral: this.id,
253
+ sessionId: this.sessionId,
254
+ }, this.id));
255
+ let transportResult;
256
+ try {
257
+ transportResult = await this.transport.run({
258
+ taskId,
259
+ prompt,
260
+ workdir,
261
+ env: childEnvironment.env,
262
+ timeoutMs,
263
+ signal,
264
+ startedAt,
265
+ onStream: (event) => {
266
+ relay.sendTaskStream(event.taskId, event.stream, event.chunk);
267
+ observer?.onStream?.(event);
268
+ },
269
+ onProcessStart: (child) => {
270
+ this.activeChild = child;
271
+ },
272
+ onProcessEnd: (child) => {
273
+ if (this.activeChild === child)
274
+ this.activeChild = null;
275
+ },
212
276
  });
213
- const origin = "software_agent";
214
- relay.sendTaskStart(taskId, origin, commandLine);
215
- observer?.onStart?.({ taskId, origin, commandLine });
216
- this.bus?.emit(SIG.TASK_STARTED, sig(SIG.TASK_STARTED, {
277
+ }
278
+ catch (err) {
279
+ transportResult = {
217
280
  taskId,
218
- taskType: "software_agent",
219
- description: effectiveEnvelope.goal,
220
- peripheral: this.id,
221
- sessionId: this.sessionId,
222
- }, this.id));
223
- let child;
224
- try {
225
- child = spawnImpl(cmd, args, {
226
- cwd: workdir,
227
- env: childEnvironment.env,
228
- stdio: ["pipe", "pipe", "pipe"],
229
- detached: true,
230
- });
231
- }
232
- catch (err) {
233
- this.activeChild = null;
234
- this.activeTaskId = null;
235
- this.activeWorkdir = null;
236
- const durationMs = Date.now() - startedAt;
237
- const result = {
238
- success: false,
239
- taskId,
240
- output: "",
241
- error: err.message || String(err),
242
- exitCode: null,
243
- durationMs,
244
- };
245
- relay.sendTaskEnd(taskId, null, durationMs);
246
- observer?.onEnd?.({ taskId, exitCode: null, durationMs, result: redactSecrets(result) });
247
- const completedAt = new Date().toISOString();
248
- this.writeTaskRecord({
249
- ...baseTaskRecord(),
250
- status: "failed",
251
- updatedAt: completedAt,
252
- completedAt,
253
- durationMs,
254
- result,
255
- stdoutSummary: summarizeText(""),
256
- stderrSummary: summarizeText(result.error || ""),
257
- });
258
- if (contextSession) {
259
- writeSoftwareAgentContextSessionState(contextSession.audit.statePath, effectiveEnvelope, result, completedAt);
260
- }
261
- this.bus?.emit(SIG.TASK_FAILED, sig(SIG.TASK_FAILED, result, this.id));
262
- resolve(result);
263
- return;
264
- }
265
- this.activeChild = child;
266
- let stdout = "";
267
- let stderr = "";
268
- let finished = false;
269
- let aborted = false;
270
- const outDecoder = new StringDecoder("utf8");
271
- const errDecoder = new StringDecoder("utf8");
272
- const outRedactor = new StreamingRedactor();
273
- const errRedactor = new StreamingRedactor();
274
- const emitSafeStream = (stream, text) => {
275
- if (!text)
276
- return;
277
- relay.sendTaskStream(taskId, stream, text);
278
- observer?.onStream?.({ taskId, stream, chunk: text });
279
- };
280
- const finish = (exitCode, error) => {
281
- if (finished)
282
- return;
283
- finished = true;
284
- signal?.removeEventListener("abort", onAbort);
285
- clearTimeout(timer);
286
- const tailOut = outDecoder.end();
287
- const tailErr = errDecoder.end();
288
- if (tailOut) {
289
- stdout += tailOut;
290
- emitSafeStream("stdout", outRedactor.push(tailOut));
291
- }
292
- if (tailErr) {
293
- stderr += tailErr;
294
- emitSafeStream("stderr", errRedactor.push(tailErr));
295
- }
296
- emitSafeStream("stdout", outRedactor.flush());
297
- emitSafeStream("stderr", errRedactor.flush());
298
- const durationMs = Date.now() - startedAt;
299
- this.activeChild = null;
300
- this.activeTaskId = null;
301
- this.activeWorkdir = null;
302
- const output = stdout.trim() || stderr.trim();
303
- const success = !error && !aborted && exitCode === 0;
304
- const result = {
305
- success,
306
- taskId,
307
- output,
308
- error: success ? undefined : error || stderr.trim() || `codex exited with code ${exitCode}`,
309
- exitCode,
310
- durationMs,
311
- };
312
- relay.sendTaskEnd(taskId, exitCode, durationMs);
313
- observer?.onEnd?.({ taskId, exitCode, durationMs, result: redactSecrets(result) });
314
- const completedAt = new Date().toISOString();
315
- this.writeTaskRecord({
316
- ...baseTaskRecord(),
317
- status: success ? "completed" : "failed",
318
- updatedAt: completedAt,
319
- completedAt,
320
- durationMs,
321
- result,
322
- stdoutSummary: summarizeText(stdout),
323
- stderrSummary: summarizeText(stderr),
324
- });
325
- if (contextSession) {
326
- writeSoftwareAgentContextSessionState(contextSession.audit.statePath, effectiveEnvelope, result, completedAt);
327
- }
328
- this.bus?.emit(success ? SIG.TASK_COMPLETED : SIG.TASK_FAILED, sig(success ? SIG.TASK_COMPLETED : SIG.TASK_FAILED, {
329
- ...result,
330
- taskLabel: `software_agent:${this.id}`,
331
- }, this.id));
332
- resolve(result);
333
- };
334
- const onAbort = () => {
335
- if (aborted || !child.pid)
336
- return;
337
- aborted = true;
338
- try {
339
- process.kill(-child.pid, "SIGTERM");
340
- }
341
- catch { }
342
- setTimeout(() => {
343
- try {
344
- process.kill(-child.pid, "SIGKILL");
345
- }
346
- catch { }
347
- }, 3000).unref();
281
+ stdout: "",
282
+ stderr: "",
283
+ error: err.message || String(err),
284
+ exitCode: null,
285
+ durationMs: Date.now() - startedAt,
286
+ aborted: false,
287
+ success: false,
348
288
  };
349
- const timer = setTimeout(() => {
350
- if (!child.pid)
351
- return;
352
- aborted = true;
353
- try {
354
- process.kill(-child.pid, "SIGTERM");
355
- }
356
- catch { }
357
- setTimeout(() => {
358
- try {
359
- process.kill(-child.pid, "SIGKILL");
360
- }
361
- catch { }
362
- }, 3000).unref();
363
- }, timeoutMs);
364
- if (signal) {
365
- if (signal.aborted)
366
- onAbort();
367
- else
368
- signal.addEventListener("abort", onAbort, { once: true });
369
- }
370
- child.stdin?.on("error", () => { });
371
- child.stdin?.write(prompt);
372
- child.stdin?.end();
373
- child.stdout?.on("data", (chunk) => {
374
- const text = outDecoder.write(chunk);
375
- if (!text)
376
- return;
377
- stdout += text;
378
- emitSafeStream("stdout", outRedactor.push(text));
379
- });
380
- child.stderr?.on("data", (chunk) => {
381
- const text = errDecoder.write(chunk);
382
- if (!text)
383
- return;
384
- stderr += text;
385
- emitSafeStream("stderr", errRedactor.push(text));
386
- });
387
- child.on("close", (code) => {
388
- child.unref();
389
- finish(code);
390
- });
391
- child.on("error", (err) => {
392
- child.unref();
393
- finish(null, err.message);
394
- });
289
+ }
290
+ finally {
291
+ this.activeChild = null;
292
+ this.activeTaskId = null;
293
+ this.activeWorkdir = null;
294
+ }
295
+ const output = transportResult.stdout.trim() || transportResult.stderr.trim();
296
+ const transportLabel = this.transport.kind === "codex-exec" ? "codex" : this.transport.kind;
297
+ const result = {
298
+ success: transportResult.success,
299
+ taskId,
300
+ output,
301
+ error: transportResult.success
302
+ ? undefined
303
+ : transportResult.error || transportResult.stderr.trim() || `${transportLabel} exited with code ${transportResult.exitCode}`,
304
+ exitCode: transportResult.exitCode,
305
+ durationMs: transportResult.durationMs,
306
+ ...taskMetadata,
307
+ };
308
+ relay.sendTaskEnd(taskId, transportResult.exitCode, transportResult.durationMs);
309
+ observer?.onEnd?.({
310
+ taskId,
311
+ exitCode: transportResult.exitCode,
312
+ durationMs: transportResult.durationMs,
313
+ result: redactSecrets(result),
314
+ ...taskMetadata,
315
+ });
316
+ const completedAt = new Date().toISOString();
317
+ this.writeTaskRecord({
318
+ ...baseTaskRecord(),
319
+ status: result.success ? "completed" : "failed",
320
+ updatedAt: completedAt,
321
+ completedAt,
322
+ durationMs: transportResult.durationMs,
323
+ result,
324
+ stdoutSummary: summarizeText(transportResult.stdout),
325
+ stderrSummary: summarizeText(transportResult.stderr || result.error || ""),
395
326
  });
327
+ if (contextSession) {
328
+ writeSoftwareAgentContextSessionState(contextSession.audit.statePath, effectiveEnvelope, result, completedAt);
329
+ }
330
+ this.bus?.emit(result.success ? SIG.TASK_COMPLETED : SIG.TASK_FAILED, sig(result.success ? SIG.TASK_COMPLETED : SIG.TASK_FAILED, {
331
+ ...result,
332
+ taskLabel: `software_agent:${this.id}`,
333
+ }, this.id));
334
+ return result;
396
335
  }
397
336
  writeTaskRecord(record) {
398
337
  const dir = this.config.taskLedgerDir;
@@ -400,14 +339,54 @@ export class CodexSoftwareAgentPeripheral {
400
339
  return;
401
340
  try {
402
341
  mkdirSync(dir, { recursive: true });
403
- const safeTaskId = safeTaskFilename(record.taskId);
404
- writeFileSync(join(dir, `${safeTaskId}.json`), `${JSON.stringify(redactSecrets(record), null, 2)}\n`);
342
+ writeFileSync(softwareAgentTaskRecordPath(dir, record.taskId), `${JSON.stringify(redactSecrets(record), null, 2)}\n`);
405
343
  pruneSoftwareAgentTaskRecords(dir, this.config.taskLedgerMaxRecords, record.taskId);
406
344
  }
407
345
  catch (err) {
408
346
  console.error(`[software-agent] Failed to write task ledger: ${err.message || String(err)}`);
409
347
  }
410
348
  }
349
+ writePermissionAudit(input) {
350
+ if (!this.config.agentName)
351
+ return;
352
+ try {
353
+ const requesterKind = input.envelope.sourceModule === "owner-http" ? "owner" : "system";
354
+ appendPermissionAuditRecordSync(this.config.agentName, {
355
+ actionKind: "software-agent-task",
356
+ action: "software-agent.run",
357
+ requestedBy: actorRef(requesterKind, input.envelope.sourceModule || "owner", "local"),
358
+ performedBy: actorRef("agent", this.config.agentName, "software-agent"),
359
+ target: actorRef("external", this.id, "software-agent"),
360
+ sourceModule: input.envelope.sourceModule,
361
+ peripheralId: this.id,
362
+ riskLevel: input.envelope.riskLevel,
363
+ roleScope: input.envelope.roleScope,
364
+ memoryScope: input.envelope.memoryScope,
365
+ workdir: input.workdir || input.envelope.workdir,
366
+ projectScope: input.workdirSafety
367
+ ? (input.workdirSafety.outsideBaseWorkdir ? "outside-workdir" : "inside-workdir")
368
+ : "unknown",
369
+ transport: "software-agent",
370
+ decision: input.decision,
371
+ allowedActions: input.envelope.allowedActions,
372
+ forbiddenActions: input.envelope.forbiddenActions,
373
+ references: {
374
+ taskId: input.taskId || input.envelope.taskId,
375
+ contextSessionId: input.envelope.contextSessionId,
376
+ contextPacketPath: input.envelope.contextPacketPath,
377
+ },
378
+ metadata: {
379
+ purpose: input.envelope.purpose,
380
+ commandLine: input.commandLine,
381
+ environment: input.environment,
382
+ workMemoryDir: input.envelope.workMemoryDir,
383
+ },
384
+ });
385
+ }
386
+ catch (error) {
387
+ logBestEffortError("software-agent audit append", error);
388
+ }
389
+ }
411
390
  collectWorkdirStatus(workdir) {
412
391
  const impl = this.config.gitStatusImpl || readGitWorktreeStatus;
413
392
  return impl(workdir);
@@ -451,6 +430,9 @@ export function buildTaskEnvelopePrompt(envelope) {
451
430
  `Risk level: ${envelope.riskLevel}`,
452
431
  `Workdir: ${envelope.workdir}`,
453
432
  ];
433
+ if (envelope.workMemoryDir) {
434
+ lines.push(`Work memory directory: ${envelope.workMemoryDir}`);
435
+ }
454
436
  if (envelope.contextPacketPath) {
455
437
  lines.push(`Context packet path: ${envelope.contextPacketPath}`);
456
438
  }
@@ -472,6 +454,20 @@ export function buildTaskEnvelopePrompt(envelope) {
472
454
  lines.push(envelope.memorySummary.trim());
473
455
  lines.push("");
474
456
  }
457
+ if (envelope.workMemoryDir) {
458
+ lines.push("Work memory:");
459
+ lines.push("- This is user-owned working context for engineering/task continuity.");
460
+ lines.push("- You may read it with grep, direct file browsing, or semantic review as appropriate.");
461
+ lines.push("- You may update files under this directory when the task or user asks you to maintain work memory.");
462
+ lines.push("- Do not read or edit Akemon self memory as part of this software-agent task.");
463
+ lines.push("- For a quick append, use `akemon work-note \"<durable work memory>\" --source codex --kind note`.");
464
+ lines.push("");
465
+ }
466
+ if (envelope.workMemoryContext?.trim()) {
467
+ lines.push("Included work-memory context:");
468
+ lines.push(envelope.workMemoryContext.trim());
469
+ lines.push("");
470
+ }
475
471
  if (envelope.allowedActions?.length) {
476
472
  lines.push("Allowed actions:");
477
473
  for (const item of envelope.allowedActions)
@@ -492,7 +488,9 @@ export function buildTaskEnvelopePrompt(envelope) {
492
488
  lines.push("Instructions:");
493
489
  lines.push("- Treat this envelope as the complete Akemon-provided context for this task.");
494
490
  lines.push("- Do not attempt to read Akemon private memory outside the visible context above.");
491
+ lines.push("- Do not read or edit Akemon self memory unless the user explicitly names a normal file to inspect.");
495
492
  lines.push("- Work only in the stated workdir unless the envelope explicitly allows otherwise.");
493
+ lines.push("- If you learn durable work memory, update the work memory directory or use `akemon work-note`.");
496
494
  lines.push("- Report what changed, what you verified, and any remaining risk.");
497
495
  return lines.join("\n");
498
496
  }
@@ -504,6 +502,7 @@ export function buildContextPacketLaunchPrompt(envelope, contextPacketPath) {
504
502
  `Akemon context session: ${envelope.contextSessionId || "(one-shot)"}`,
505
503
  `Context packet: ${contextPacketPath}`,
506
504
  `Workdir: ${envelope.workdir}`,
505
+ `Work memory directory: ${envelope.workMemoryDir || "(not configured)"}`,
507
506
  "",
508
507
  "Goal:",
509
508
  envelope.goal,
@@ -512,7 +511,9 @@ export function buildContextPacketLaunchPrompt(envelope, contextPacketPath) {
512
511
  "- Read the context packet first before doing repository work.",
513
512
  "- Treat that file as the complete Akemon-provided context for this task.",
514
513
  "- Do not read Akemon private memory outside the context packet.",
514
+ "- Do not read or edit Akemon self memory unless the user explicitly names a normal file to inspect.",
515
515
  "- Work only in the stated workdir unless the packet explicitly allows otherwise.",
516
+ "- If you learn durable work memory, update the work memory directory or use `akemon work-note`.",
516
517
  "- Report what changed, what you verified, and any remaining risk.",
517
518
  ].join("\n");
518
519
  }
@@ -618,8 +619,9 @@ export function readGitWorktreeStatus(workdir) {
618
619
  };
619
620
  }
620
621
  }
621
- export function listSoftwareAgentTaskRecords(taskLedgerDir, limit = 20) {
622
+ export function listSoftwareAgentTaskRecords(taskLedgerDir, limit = 20, opts = {}) {
622
623
  const safeLimit = normalizeTaskRecordLimit(limit);
624
+ const contextSessionId = opts.contextSessionId?.trim();
623
625
  try {
624
626
  if (!existsSync(taskLedgerDir))
625
627
  return [];
@@ -627,6 +629,7 @@ export function listSoftwareAgentTaskRecords(taskLedgerDir, limit = 20) {
627
629
  .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
628
630
  .map((entry) => readSoftwareAgentTaskRecordFile(join(taskLedgerDir, entry.name)))
629
631
  .filter((record) => !!record)
632
+ .filter((record) => !contextSessionId || record.contextSession?.sessionId === contextSessionId || record.envelope.contextSessionId === contextSessionId)
630
633
  .sort(compareSoftwareAgentTaskRecords)
631
634
  .slice(0, safeLimit);
632
635
  }
@@ -635,8 +638,66 @@ export function listSoftwareAgentTaskRecords(taskLedgerDir, limit = 20) {
635
638
  }
636
639
  }
637
640
  export function readSoftwareAgentTaskRecord(taskLedgerDir, taskId) {
638
- const file = join(taskLedgerDir, `${safeTaskFilename(taskId)}.json`);
639
- return readSoftwareAgentTaskRecordFile(file);
641
+ return readSoftwareAgentTaskRecordFile(softwareAgentTaskRecordPath(taskLedgerDir, taskId));
642
+ }
643
+ export function softwareAgentTaskRecordPath(taskLedgerDir, taskId) {
644
+ return join(taskLedgerDir, `${safeTaskFilename(taskId)}.json`);
645
+ }
646
+ export function listSoftwareAgentContextSessions(contextSessionDir, limit = 20) {
647
+ const safeLimit = normalizeTaskRecordLimit(limit);
648
+ try {
649
+ if (!existsSync(contextSessionDir))
650
+ return [];
651
+ return readdirSync(contextSessionDir, { withFileTypes: true })
652
+ .filter((entry) => entry.isDirectory())
653
+ .map((entry) => {
654
+ try {
655
+ return readSoftwareAgentContextSession(contextSessionDir, entry.name);
656
+ }
657
+ catch {
658
+ return null;
659
+ }
660
+ })
661
+ .filter((record) => !!record)
662
+ .sort(compareSoftwareAgentContextSessions)
663
+ .slice(0, safeLimit);
664
+ }
665
+ catch {
666
+ return [];
667
+ }
668
+ }
669
+ export function readSoftwareAgentContextSession(contextSessionDir, sessionId, opts = {}) {
670
+ const safeSessionId = normalizeContextSessionId(sessionId, "sessionId");
671
+ if (!safeSessionId)
672
+ return null;
673
+ const sessionDir = join(contextSessionDir, safeSessionId);
674
+ if (!existsSync(sessionDir))
675
+ return null;
676
+ const packetPath = join(sessionDir, CONTEXT_PACKET_FILENAME);
677
+ const statePath = join(sessionDir, CONTEXT_SESSION_STATE_FILENAME);
678
+ const state = readSoftwareAgentContextSessionState(statePath);
679
+ const record = {
680
+ sessionId: safeSessionId,
681
+ packetPath,
682
+ statePath,
683
+ hasContextPacket: existsSync(packetPath),
684
+ };
685
+ if (state) {
686
+ record.updatedAt = state.updatedAt;
687
+ record.workMemoryDir = state.workMemoryDir;
688
+ record.lastTaskId = state.lastTaskId;
689
+ record.lastGoal = state.lastGoal;
690
+ record.lastResult = state.lastResult;
691
+ }
692
+ if (opts.includeContextPacket && record.hasContextPacket) {
693
+ try {
694
+ record.contextPacket = readFileSync(packetPath, "utf8");
695
+ }
696
+ catch {
697
+ record.contextPacket = "";
698
+ }
699
+ }
700
+ return record;
640
701
  }
641
702
  export function pruneSoftwareAgentTaskRecords(taskLedgerDir, maxRecords = DEFAULT_TASK_LEDGER_MAX_RECORDS, preserveTaskId) {
642
703
  const safeMaxRecords = normalizeTaskLedgerMaxRecords(maxRecords);
@@ -728,6 +789,7 @@ function writeSoftwareAgentContextSessionState(statePath, envelope, result, upda
728
789
  schemaVersion: 1,
729
790
  sessionId: envelope.contextSessionId,
730
791
  updatedAt,
792
+ workMemoryDir: envelope.workMemoryDir,
731
793
  lastTaskId: result.taskId,
732
794
  lastGoal: envelope.goal,
733
795
  lastResult: {
@@ -746,10 +808,8 @@ function writeSoftwareAgentContextSessionState(statePath, envelope, result, upda
746
808
  }
747
809
  function readSoftwareAgentContextSessionSummary(statePath) {
748
810
  try {
749
- if (!existsSync(statePath))
750
- return undefined;
751
- const parsed = JSON.parse(readFileSync(statePath, "utf8"));
752
- if (!parsed || parsed.schemaVersion !== 1 || !parsed.lastTaskId || !parsed.lastResult)
811
+ const parsed = readSoftwareAgentContextSessionState(statePath);
812
+ if (!parsed?.lastTaskId || !parsed.lastResult)
753
813
  return undefined;
754
814
  const result = parsed.lastResult;
755
815
  const status = result.success === true ? "completed" : "failed";
@@ -771,6 +831,31 @@ function readSoftwareAgentContextSessionSummary(statePath) {
771
831
  return undefined;
772
832
  }
773
833
  }
834
+ function readSoftwareAgentContextSessionState(statePath) {
835
+ try {
836
+ if (!existsSync(statePath))
837
+ return null;
838
+ const parsed = JSON.parse(readFileSync(statePath, "utf8"));
839
+ if (!isSoftwareAgentContextSessionState(parsed))
840
+ return null;
841
+ return parsed;
842
+ }
843
+ catch {
844
+ return null;
845
+ }
846
+ }
847
+ function isSoftwareAgentContextSessionState(value) {
848
+ return value
849
+ && value.schemaVersion === 1
850
+ && typeof value.sessionId === "string"
851
+ && typeof value.updatedAt === "string"
852
+ && (value.workMemoryDir === undefined || typeof value.workMemoryDir === "string")
853
+ && typeof value.lastTaskId === "string"
854
+ && value.lastResult
855
+ && typeof value.lastResult.success === "boolean"
856
+ && (typeof value.lastResult.exitCode === "number" || value.lastResult.exitCode === null)
857
+ && typeof value.lastResult.durationMs === "number";
858
+ }
774
859
  function readOptionalString(value, field) {
775
860
  if (value === undefined || value === null)
776
861
  return undefined;
@@ -933,16 +1018,10 @@ function compareSoftwareAgentTaskRecords(a, b) {
933
1018
  return bTime - aTime;
934
1019
  return b.taskId.localeCompare(a.taskId);
935
1020
  }
936
- function buildCodexExecCommand(opts) {
937
- const args = [
938
- "exec",
939
- "--skip-git-repo-check",
940
- "--color", "never",
941
- "-s", opts.sandbox,
942
- "-C", opts.workdir,
943
- ];
944
- if (opts.model)
945
- args.push("-m", opts.model);
946
- args.push("-");
947
- return { cmd: opts.command, args };
1021
+ function compareSoftwareAgentContextSessions(a, b) {
1022
+ const bTime = Date.parse(b.updatedAt || "") || 0;
1023
+ const aTime = Date.parse(a.updatedAt || "") || 0;
1024
+ if (bTime !== aTime)
1025
+ return bTime - aTime;
1026
+ return b.sessionId.localeCompare(a.sessionId);
948
1027
  }