akemon 0.3.4 → 0.3.6

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.
@@ -12,11 +12,12 @@
12
12
  */
13
13
  import { randomUUID } from "crypto";
14
14
  import { spawn, spawnSync } from "child_process";
15
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
15
+ import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
16
16
  import { isAbsolute, join, relative, resolve as resolvePath } from "path";
17
17
  import { StringDecoder } from "string_decoder";
18
18
  import { SIG, sig } from "./types.js";
19
19
  import { sendTaskEnd, sendTaskStart, sendTaskStream } from "./relay-client.js";
20
+ import { redactSecrets, StreamingRedactor } from "./redaction.js";
20
21
  const defaultTaskRelay = {
21
22
  sendTaskStart,
22
23
  sendTaskStream,
@@ -34,7 +35,38 @@ const MEMORY_SCOPES = ["none", "public", "task", "owner"];
34
35
  const RISK_LEVELS = ["low", "medium", "high"];
35
36
  const MAX_STREAM_SUMMARY_CHARS = 12_000;
36
37
  const STREAM_SUMMARY_HEAD_CHARS = 4_000;
37
- const STREAM_SUMMARY_TAIL_CHARS = 8_000;
38
+ const DEFAULT_TASK_LEDGER_MAX_RECORDS = 200;
39
+ const CONTEXT_PACKET_FILENAME = "TASK_CONTEXT.md";
40
+ const CONTEXT_SESSION_STATE_FILENAME = "SESSION.json";
41
+ const MAX_CONTEXT_SESSION_ID_LENGTH = 120;
42
+ const MAX_CONTEXT_SESSION_SUMMARY_CHARS = 4_000;
43
+ const DEFAULT_SOFTWARE_AGENT_ENV_POLICY = "inherit";
44
+ const DEFAULT_SOFTWARE_AGENT_ENV_ALLOWLIST = [
45
+ "PATH",
46
+ "HOME",
47
+ "SHELL",
48
+ "USER",
49
+ "LOGNAME",
50
+ "TMPDIR",
51
+ "TEMP",
52
+ "TMP",
53
+ "LANG",
54
+ "LC_ALL",
55
+ "LC_CTYPE",
56
+ "TERM",
57
+ "COLORTERM",
58
+ "NO_COLOR",
59
+ "FORCE_COLOR",
60
+ "OPENAI_API_KEY",
61
+ "OPENAI_BASE_URL",
62
+ "OPENAI_ORG_ID",
63
+ "OPENAI_ORGANIZATION",
64
+ "OPENAI_PROJECT",
65
+ "OPENAI_MODEL",
66
+ "CODEX_HOME",
67
+ "CODEX_MODEL",
68
+ "CODEX_PROFILE",
69
+ ];
38
70
  export class CodexSoftwareAgentPeripheral {
39
71
  id;
40
72
  name;
@@ -47,7 +79,11 @@ export class CodexSoftwareAgentPeripheral {
47
79
  activeWorkdir = null;
48
80
  sessionId = randomUUID();
49
81
  constructor(config) {
50
- this.config = config;
82
+ this.config = {
83
+ ...config,
84
+ envPolicy: normalizeSoftwareAgentEnvPolicy(config.envPolicy),
85
+ envAllowlist: normalizeSoftwareAgentEnvAllowlist(config.envAllowlist),
86
+ };
51
87
  this.id = config.id || "software-agent:codex";
52
88
  this.name = config.name || "Codex CLI Software Agent";
53
89
  }
@@ -65,14 +101,16 @@ export class CodexSoftwareAgentPeripheral {
65
101
  this.sessionId = randomUUID();
66
102
  }
67
103
  async resetSession() {
68
- if (this.activeChild?.pid) {
104
+ const activePid = this.activeChild?.pid;
105
+ if (activePid) {
106
+ const processGroupId = -activePid;
69
107
  try {
70
- process.kill(-this.activeChild.pid, "SIGTERM");
108
+ process.kill(processGroupId, "SIGTERM");
71
109
  }
72
110
  catch { }
73
111
  setTimeout(() => {
74
112
  try {
75
- process.kill(-this.activeChild.pid, "SIGKILL");
113
+ process.kill(processGroupId, "SIGKILL");
76
114
  }
77
115
  catch { }
78
116
  }, 3000).unref();
@@ -94,6 +132,13 @@ export class CodexSoftwareAgentPeripheral {
94
132
  baseWorkdir: resolvePath(this.config.workdir),
95
133
  workdirStatus: this.collectWorkdirStatus(currentWorkdir),
96
134
  taskLedgerDir: this.config.taskLedgerDir,
135
+ contextSessionDir: this.config.contextSessionDir,
136
+ workMemoryDir: this.config.workMemoryDir,
137
+ environment: buildSoftwareAgentChildEnvironment({
138
+ policy: this.config.envPolicy,
139
+ allowlist: this.config.envAllowlist,
140
+ sourceEnv: this.config.sourceEnv,
141
+ }).audit,
97
142
  };
98
143
  }
99
144
  async send(signal) {
@@ -115,10 +160,25 @@ export class CodexSoftwareAgentPeripheral {
115
160
  }
116
161
  const { signal, observer } = normalizeSoftwareAgentTaskOptions(taskOptions);
117
162
  const taskId = envelope.taskId || `sw_${Date.now()}_${randomUUID().slice(0, 8)}`;
163
+ const contextSessionId = normalizeContextSessionId(envelope.contextSessionId) || taskId;
118
164
  const workdirSafety = resolveWorkdirSafety(this.config.workdir, envelope.workdir || this.config.workdir, envelope.workdirSafety?.allowOutsideWorkdir || false);
119
165
  const workdir = workdirSafety.effectiveWorkdir;
120
- const effectiveEnvelope = { ...envelope, taskId, workdir, workdirSafety };
121
- const prompt = buildTaskEnvelopePrompt(effectiveEnvelope);
166
+ let effectiveEnvelope = {
167
+ ...envelope,
168
+ taskId,
169
+ contextSessionId,
170
+ workdir,
171
+ workdirSafety,
172
+ workMemoryDir: envelope.workMemoryDir || this.config.workMemoryDir,
173
+ };
174
+ const contextSession = this.config.contextSessionDir
175
+ ? writeSoftwareAgentContextPacket(this.config.contextSessionDir, effectiveEnvelope)
176
+ : undefined;
177
+ if (contextSession)
178
+ effectiveEnvelope = contextSession.envelope;
179
+ const prompt = contextSession
180
+ ? buildContextPacketLaunchPrompt(effectiveEnvelope, contextSession.audit.packetPath)
181
+ : buildTaskEnvelopePrompt(effectiveEnvelope);
122
182
  const { cmd, args } = buildCodexExecCommand({
123
183
  command: this.config.command || "codex",
124
184
  workdir,
@@ -132,6 +192,16 @@ export class CodexSoftwareAgentPeripheral {
132
192
  const spawnImpl = this.config.spawnImpl || spawn;
133
193
  const timeoutMs = envelope.timeoutMs || this.config.defaultTimeoutMs || DEFAULT_TIMEOUT_MS;
134
194
  const workdirStatus = this.collectWorkdirStatus(workdir);
195
+ const taskMetadata = {
196
+ contextSessionId: effectiveEnvelope.contextSessionId,
197
+ contextPacketPath: effectiveEnvelope.contextPacketPath,
198
+ workMemoryDir: effectiveEnvelope.workMemoryDir,
199
+ };
200
+ const childEnvironment = buildSoftwareAgentChildEnvironment({
201
+ policy: this.config.envPolicy,
202
+ allowlist: this.config.envAllowlist,
203
+ sourceEnv: this.config.sourceEnv,
204
+ });
135
205
  const baseTaskRecord = () => ({
136
206
  schemaVersion: 1,
137
207
  taskId,
@@ -141,6 +211,8 @@ export class CodexSoftwareAgentPeripheral {
141
211
  commandLine,
142
212
  envelope: effectiveEnvelope,
143
213
  startedAt: startedAtIso,
214
+ environment: childEnvironment.audit,
215
+ contextSession: contextSession?.audit,
144
216
  workdirStatus,
145
217
  });
146
218
  return new Promise((resolve) => {
@@ -153,7 +225,7 @@ export class CodexSoftwareAgentPeripheral {
153
225
  });
154
226
  const origin = "software_agent";
155
227
  relay.sendTaskStart(taskId, origin, commandLine);
156
- observer?.onStart?.({ taskId, origin, commandLine });
228
+ observer?.onStart?.({ taskId, origin, commandLine, ...taskMetadata });
157
229
  this.bus?.emit(SIG.TASK_STARTED, sig(SIG.TASK_STARTED, {
158
230
  taskId,
159
231
  taskType: "software_agent",
@@ -165,7 +237,7 @@ export class CodexSoftwareAgentPeripheral {
165
237
  try {
166
238
  child = spawnImpl(cmd, args, {
167
239
  cwd: workdir,
168
- env: process.env,
240
+ env: childEnvironment.env,
169
241
  stdio: ["pipe", "pipe", "pipe"],
170
242
  detached: true,
171
243
  });
@@ -182,9 +254,16 @@ export class CodexSoftwareAgentPeripheral {
182
254
  error: err.message || String(err),
183
255
  exitCode: null,
184
256
  durationMs,
257
+ ...taskMetadata,
185
258
  };
186
259
  relay.sendTaskEnd(taskId, null, durationMs);
187
- observer?.onEnd?.({ taskId, exitCode: null, durationMs, result });
260
+ observer?.onEnd?.({
261
+ taskId,
262
+ exitCode: null,
263
+ durationMs,
264
+ result: redactSecrets(result),
265
+ ...taskMetadata,
266
+ });
188
267
  const completedAt = new Date().toISOString();
189
268
  this.writeTaskRecord({
190
269
  ...baseTaskRecord(),
@@ -196,6 +275,9 @@ export class CodexSoftwareAgentPeripheral {
196
275
  stdoutSummary: summarizeText(""),
197
276
  stderrSummary: summarizeText(result.error || ""),
198
277
  });
278
+ if (contextSession) {
279
+ writeSoftwareAgentContextSessionState(contextSession.audit.statePath, effectiveEnvelope, result, completedAt);
280
+ }
199
281
  this.bus?.emit(SIG.TASK_FAILED, sig(SIG.TASK_FAILED, result, this.id));
200
282
  resolve(result);
201
283
  return;
@@ -207,6 +289,14 @@ export class CodexSoftwareAgentPeripheral {
207
289
  let aborted = false;
208
290
  const outDecoder = new StringDecoder("utf8");
209
291
  const errDecoder = new StringDecoder("utf8");
292
+ const outRedactor = new StreamingRedactor();
293
+ const errRedactor = new StreamingRedactor();
294
+ const emitSafeStream = (stream, text) => {
295
+ if (!text)
296
+ return;
297
+ relay.sendTaskStream(taskId, stream, text);
298
+ observer?.onStream?.({ taskId, stream, chunk: text });
299
+ };
210
300
  const finish = (exitCode, error) => {
211
301
  if (finished)
212
302
  return;
@@ -217,14 +307,14 @@ export class CodexSoftwareAgentPeripheral {
217
307
  const tailErr = errDecoder.end();
218
308
  if (tailOut) {
219
309
  stdout += tailOut;
220
- relay.sendTaskStream(taskId, "stdout", tailOut);
221
- observer?.onStream?.({ taskId, stream: "stdout", chunk: tailOut });
310
+ emitSafeStream("stdout", outRedactor.push(tailOut));
222
311
  }
223
312
  if (tailErr) {
224
313
  stderr += tailErr;
225
- relay.sendTaskStream(taskId, "stderr", tailErr);
226
- observer?.onStream?.({ taskId, stream: "stderr", chunk: tailErr });
314
+ emitSafeStream("stderr", errRedactor.push(tailErr));
227
315
  }
316
+ emitSafeStream("stdout", outRedactor.flush());
317
+ emitSafeStream("stderr", errRedactor.flush());
228
318
  const durationMs = Date.now() - startedAt;
229
319
  this.activeChild = null;
230
320
  this.activeTaskId = null;
@@ -238,9 +328,16 @@ export class CodexSoftwareAgentPeripheral {
238
328
  error: success ? undefined : error || stderr.trim() || `codex exited with code ${exitCode}`,
239
329
  exitCode,
240
330
  durationMs,
331
+ ...taskMetadata,
241
332
  };
242
333
  relay.sendTaskEnd(taskId, exitCode, durationMs);
243
- observer?.onEnd?.({ taskId, exitCode, durationMs, result });
334
+ observer?.onEnd?.({
335
+ taskId,
336
+ exitCode,
337
+ durationMs,
338
+ result: redactSecrets(result),
339
+ ...taskMetadata,
340
+ });
244
341
  const completedAt = new Date().toISOString();
245
342
  this.writeTaskRecord({
246
343
  ...baseTaskRecord(),
@@ -252,6 +349,9 @@ export class CodexSoftwareAgentPeripheral {
252
349
  stdoutSummary: summarizeText(stdout),
253
350
  stderrSummary: summarizeText(stderr),
254
351
  });
352
+ if (contextSession) {
353
+ writeSoftwareAgentContextSessionState(contextSession.audit.statePath, effectiveEnvelope, result, completedAt);
354
+ }
255
355
  this.bus?.emit(success ? SIG.TASK_COMPLETED : SIG.TASK_FAILED, sig(success ? SIG.TASK_COMPLETED : SIG.TASK_FAILED, {
256
356
  ...result,
257
357
  taskLabel: `software_agent:${this.id}`,
@@ -302,16 +402,14 @@ export class CodexSoftwareAgentPeripheral {
302
402
  if (!text)
303
403
  return;
304
404
  stdout += text;
305
- relay.sendTaskStream(taskId, "stdout", text);
306
- observer?.onStream?.({ taskId, stream: "stdout", chunk: text });
405
+ emitSafeStream("stdout", outRedactor.push(text));
307
406
  });
308
407
  child.stderr?.on("data", (chunk) => {
309
408
  const text = errDecoder.write(chunk);
310
409
  if (!text)
311
410
  return;
312
411
  stderr += text;
313
- relay.sendTaskStream(taskId, "stderr", text);
314
- observer?.onStream?.({ taskId, stream: "stderr", chunk: text });
412
+ emitSafeStream("stderr", errRedactor.push(text));
315
413
  });
316
414
  child.on("close", (code) => {
317
415
  child.unref();
@@ -330,7 +428,8 @@ export class CodexSoftwareAgentPeripheral {
330
428
  try {
331
429
  mkdirSync(dir, { recursive: true });
332
430
  const safeTaskId = safeTaskFilename(record.taskId);
333
- writeFileSync(join(dir, `${safeTaskId}.json`), `${JSON.stringify(record, null, 2)}\n`);
431
+ writeFileSync(join(dir, `${safeTaskId}.json`), `${JSON.stringify(redactSecrets(record), null, 2)}\n`);
432
+ pruneSoftwareAgentTaskRecords(dir, this.config.taskLedgerMaxRecords, record.taskId);
334
433
  }
335
434
  catch (err) {
336
435
  console.error(`[software-agent] Failed to write task ledger: ${err.message || String(err)}`);
@@ -371,6 +470,7 @@ export function buildTaskEnvelopePrompt(envelope) {
371
470
  "[Akemon Software Peripheral Task Envelope]",
372
471
  "",
373
472
  `Task ID: ${envelope.taskId || "(unspecified)"}`,
473
+ `Akemon context session: ${envelope.contextSessionId || "(one-shot)"}`,
374
474
  `Source module: ${envelope.sourceModule}`,
375
475
  `Purpose: ${envelope.purpose}`,
376
476
  `Role scope: ${envelope.roleScope}`,
@@ -378,6 +478,12 @@ export function buildTaskEnvelopePrompt(envelope) {
378
478
  `Risk level: ${envelope.riskLevel}`,
379
479
  `Workdir: ${envelope.workdir}`,
380
480
  ];
481
+ if (envelope.workMemoryDir) {
482
+ lines.push(`Work memory directory: ${envelope.workMemoryDir}`);
483
+ }
484
+ if (envelope.contextPacketPath) {
485
+ lines.push(`Context packet path: ${envelope.contextPacketPath}`);
486
+ }
381
487
  if (envelope.workdirSafety) {
382
488
  lines.push(`Base workdir: ${envelope.workdirSafety.baseWorkdir}`);
383
489
  lines.push(`Requested workdir: ${envelope.workdirSafety.requestedWorkdir}`);
@@ -386,11 +492,30 @@ export function buildTaskEnvelopePrompt(envelope) {
386
492
  lines.push(`Outside workdir explicitly allowed: ${envelope.workdirSafety.allowOutsideWorkdir ? "yes" : "no"}`);
387
493
  }
388
494
  lines.push("", "Goal:", envelope.goal, "");
495
+ if (envelope.previousTaskSummary?.trim()) {
496
+ lines.push("Previous task summary for this Akemon context session:");
497
+ lines.push(envelope.previousTaskSummary.trim());
498
+ lines.push("");
499
+ }
389
500
  if (envelope.memorySummary?.trim()) {
390
501
  lines.push("Visible Akemon memory/context:");
391
502
  lines.push(envelope.memorySummary.trim());
392
503
  lines.push("");
393
504
  }
505
+ if (envelope.workMemoryDir) {
506
+ lines.push("Work memory:");
507
+ lines.push("- This is user-owned working context for engineering/task continuity.");
508
+ lines.push("- You may read it with grep, direct file browsing, or semantic review as appropriate.");
509
+ lines.push("- You may update files under this directory when the task or user asks you to maintain work memory.");
510
+ lines.push("- Do not read or edit Akemon self memory as part of this software-agent task.");
511
+ lines.push("- For a quick append, use `akemon work-note \"<durable work memory>\" --source codex --kind note`.");
512
+ lines.push("");
513
+ }
514
+ if (envelope.workMemoryContext?.trim()) {
515
+ lines.push("Included work-memory context:");
516
+ lines.push(envelope.workMemoryContext.trim());
517
+ lines.push("");
518
+ }
394
519
  if (envelope.allowedActions?.length) {
395
520
  lines.push("Allowed actions:");
396
521
  for (const item of envelope.allowedActions)
@@ -411,10 +536,35 @@ export function buildTaskEnvelopePrompt(envelope) {
411
536
  lines.push("Instructions:");
412
537
  lines.push("- Treat this envelope as the complete Akemon-provided context for this task.");
413
538
  lines.push("- Do not attempt to read Akemon private memory outside the visible context above.");
539
+ lines.push("- Do not read or edit Akemon self memory unless the user explicitly names a normal file to inspect.");
414
540
  lines.push("- Work only in the stated workdir unless the envelope explicitly allows otherwise.");
541
+ lines.push("- If you learn durable work memory, update the work memory directory or use `akemon work-note`.");
415
542
  lines.push("- Report what changed, what you verified, and any remaining risk.");
416
543
  return lines.join("\n");
417
544
  }
545
+ export function buildContextPacketLaunchPrompt(envelope, contextPacketPath) {
546
+ return [
547
+ "[Akemon Software Peripheral Task]",
548
+ "",
549
+ `Task ID: ${envelope.taskId || "(unspecified)"}`,
550
+ `Akemon context session: ${envelope.contextSessionId || "(one-shot)"}`,
551
+ `Context packet: ${contextPacketPath}`,
552
+ `Workdir: ${envelope.workdir}`,
553
+ `Work memory directory: ${envelope.workMemoryDir || "(not configured)"}`,
554
+ "",
555
+ "Goal:",
556
+ envelope.goal,
557
+ "",
558
+ "Instructions:",
559
+ "- Read the context packet first before doing repository work.",
560
+ "- Treat that file as the complete Akemon-provided context for this task.",
561
+ "- Do not read Akemon private memory outside the context packet.",
562
+ "- Do not read or edit Akemon self memory unless the user explicitly names a normal file to inspect.",
563
+ "- Work only in the stated workdir unless the packet explicitly allows otherwise.",
564
+ "- If you learn durable work memory, update the work memory directory or use `akemon work-note`.",
565
+ "- Report what changed, what you verified, and any remaining risk.",
566
+ ].join("\n");
567
+ }
418
568
  export function createOwnerTaskEnvelope(body, defaultWorkdir) {
419
569
  const goal = typeof body?.goal === "string" ? body.goal.trim() : "";
420
570
  if (!goal)
@@ -437,6 +587,7 @@ export function createOwnerTaskEnvelope(body, defaultWorkdir) {
437
587
  : [...DEFAULT_OWNER_ALLOWED_ACTIONS],
438
588
  forbiddenActions: [...new Set([...DEFAULT_OWNER_FORBIDDEN_ACTIONS, ...callerForbiddenActions])],
439
589
  memorySummary: typeof body?.memorySummary === "string" ? body.memorySummary : "",
590
+ contextSessionId: readOptionalContextSessionId(body?.contextSessionId ?? body?.sessionId, "contextSessionId"),
440
591
  deliverable: typeof body?.deliverable === "string"
441
592
  ? body.deliverable
442
593
  : "Return a concise engineering summary with changes, verification, and remaining risks.",
@@ -516,8 +667,9 @@ export function readGitWorktreeStatus(workdir) {
516
667
  };
517
668
  }
518
669
  }
519
- export function listSoftwareAgentTaskRecords(taskLedgerDir, limit = 20) {
670
+ export function listSoftwareAgentTaskRecords(taskLedgerDir, limit = 20, opts = {}) {
520
671
  const safeLimit = normalizeTaskRecordLimit(limit);
672
+ const contextSessionId = opts.contextSessionId?.trim();
521
673
  try {
522
674
  if (!existsSync(taskLedgerDir))
523
675
  return [];
@@ -525,6 +677,7 @@ export function listSoftwareAgentTaskRecords(taskLedgerDir, limit = 20) {
525
677
  .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
526
678
  .map((entry) => readSoftwareAgentTaskRecordFile(join(taskLedgerDir, entry.name)))
527
679
  .filter((record) => !!record)
680
+ .filter((record) => !contextSessionId || record.contextSession?.sessionId === contextSessionId || record.envelope.contextSessionId === contextSessionId)
528
681
  .sort(compareSoftwareAgentTaskRecords)
529
682
  .slice(0, safeLimit);
530
683
  }
@@ -536,6 +689,219 @@ export function readSoftwareAgentTaskRecord(taskLedgerDir, taskId) {
536
689
  const file = join(taskLedgerDir, `${safeTaskFilename(taskId)}.json`);
537
690
  return readSoftwareAgentTaskRecordFile(file);
538
691
  }
692
+ export function listSoftwareAgentContextSessions(contextSessionDir, limit = 20) {
693
+ const safeLimit = normalizeTaskRecordLimit(limit);
694
+ try {
695
+ if (!existsSync(contextSessionDir))
696
+ return [];
697
+ return readdirSync(contextSessionDir, { withFileTypes: true })
698
+ .filter((entry) => entry.isDirectory())
699
+ .map((entry) => {
700
+ try {
701
+ return readSoftwareAgentContextSession(contextSessionDir, entry.name);
702
+ }
703
+ catch {
704
+ return null;
705
+ }
706
+ })
707
+ .filter((record) => !!record)
708
+ .sort(compareSoftwareAgentContextSessions)
709
+ .slice(0, safeLimit);
710
+ }
711
+ catch {
712
+ return [];
713
+ }
714
+ }
715
+ export function readSoftwareAgentContextSession(contextSessionDir, sessionId, opts = {}) {
716
+ const safeSessionId = normalizeContextSessionId(sessionId, "sessionId");
717
+ if (!safeSessionId)
718
+ return null;
719
+ const sessionDir = join(contextSessionDir, safeSessionId);
720
+ if (!existsSync(sessionDir))
721
+ return null;
722
+ const packetPath = join(sessionDir, CONTEXT_PACKET_FILENAME);
723
+ const statePath = join(sessionDir, CONTEXT_SESSION_STATE_FILENAME);
724
+ const state = readSoftwareAgentContextSessionState(statePath);
725
+ const record = {
726
+ sessionId: safeSessionId,
727
+ packetPath,
728
+ statePath,
729
+ hasContextPacket: existsSync(packetPath),
730
+ };
731
+ if (state) {
732
+ record.updatedAt = state.updatedAt;
733
+ record.workMemoryDir = state.workMemoryDir;
734
+ record.lastTaskId = state.lastTaskId;
735
+ record.lastGoal = state.lastGoal;
736
+ record.lastResult = state.lastResult;
737
+ }
738
+ if (opts.includeContextPacket && record.hasContextPacket) {
739
+ try {
740
+ record.contextPacket = readFileSync(packetPath, "utf8");
741
+ }
742
+ catch {
743
+ record.contextPacket = "";
744
+ }
745
+ }
746
+ return record;
747
+ }
748
+ export function pruneSoftwareAgentTaskRecords(taskLedgerDir, maxRecords = DEFAULT_TASK_LEDGER_MAX_RECORDS, preserveTaskId) {
749
+ const safeMaxRecords = normalizeTaskLedgerMaxRecords(maxRecords);
750
+ try {
751
+ if (!existsSync(taskLedgerDir))
752
+ return 0;
753
+ const records = readdirSync(taskLedgerDir, { withFileTypes: true })
754
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
755
+ .map((entry) => {
756
+ const file = join(taskLedgerDir, entry.name);
757
+ const record = readSoftwareAgentTaskRecordFile(file);
758
+ return record ? { file, record } : null;
759
+ })
760
+ .filter((entry) => !!entry)
761
+ .sort((a, b) => compareSoftwareAgentTaskRecords(a.record, b.record));
762
+ const keepTaskIds = new Set(records.slice(0, safeMaxRecords).map((entry) => entry.record.taskId));
763
+ if (preserveTaskId)
764
+ keepTaskIds.add(preserveTaskId);
765
+ let deleted = 0;
766
+ for (const entry of records) {
767
+ if (keepTaskIds.has(entry.record.taskId))
768
+ continue;
769
+ try {
770
+ unlinkSync(entry.file);
771
+ deleted++;
772
+ }
773
+ catch {
774
+ // Best effort: retention should not break task completion.
775
+ }
776
+ }
777
+ return deleted;
778
+ }
779
+ catch {
780
+ return 0;
781
+ }
782
+ }
783
+ export function buildSoftwareAgentChildEnvironment(opts = {}) {
784
+ const policy = normalizeSoftwareAgentEnvPolicy(opts.policy);
785
+ const sourceEnv = opts.sourceEnv || process.env;
786
+ if (policy === "inherit") {
787
+ return {
788
+ env: sourceEnv,
789
+ audit: { policy },
790
+ };
791
+ }
792
+ const allowlist = normalizeSoftwareAgentEnvAllowlist([
793
+ ...DEFAULT_SOFTWARE_AGENT_ENV_ALLOWLIST,
794
+ ...(opts.allowlist || []),
795
+ ]);
796
+ const env = {};
797
+ for (const key of allowlist) {
798
+ if (isForbiddenSoftwareAgentEnvKey(key))
799
+ continue;
800
+ const value = sourceEnv[key];
801
+ if (value !== undefined)
802
+ env[key] = value;
803
+ }
804
+ return {
805
+ env,
806
+ audit: {
807
+ policy,
808
+ allowedKeys: Object.keys(env).sort(),
809
+ },
810
+ };
811
+ }
812
+ function writeSoftwareAgentContextPacket(contextSessionDir, envelope) {
813
+ const sessionId = normalizeContextSessionId(envelope.contextSessionId) || envelope.taskId || randomUUID();
814
+ const sessionDir = join(contextSessionDir, sessionId);
815
+ const packetPath = join(sessionDir, CONTEXT_PACKET_FILENAME);
816
+ const statePath = join(sessionDir, CONTEXT_SESSION_STATE_FILENAME);
817
+ const previousTaskSummary = readSoftwareAgentContextSessionSummary(statePath);
818
+ const packetEnvelope = {
819
+ ...envelope,
820
+ contextSessionId: sessionId,
821
+ contextPacketPath: packetPath,
822
+ previousTaskSummary,
823
+ };
824
+ mkdirSync(sessionDir, { recursive: true });
825
+ const content = buildTaskEnvelopePrompt(packetEnvelope);
826
+ writeFileSync(packetPath, `${redactSecrets(content)}\n`);
827
+ return {
828
+ envelope: packetEnvelope,
829
+ audit: { sessionId, packetPath, statePath },
830
+ };
831
+ }
832
+ function writeSoftwareAgentContextSessionState(statePath, envelope, result, updatedAt) {
833
+ try {
834
+ const state = {
835
+ schemaVersion: 1,
836
+ sessionId: envelope.contextSessionId,
837
+ updatedAt,
838
+ workMemoryDir: envelope.workMemoryDir,
839
+ lastTaskId: result.taskId,
840
+ lastGoal: envelope.goal,
841
+ lastResult: {
842
+ success: result.success,
843
+ exitCode: result.exitCode,
844
+ durationMs: result.durationMs,
845
+ outputSummary: summarizeText(result.output || "", MAX_CONTEXT_SESSION_SUMMARY_CHARS),
846
+ errorSummary: result.error ? summarizeText(result.error, MAX_CONTEXT_SESSION_SUMMARY_CHARS) : undefined,
847
+ },
848
+ };
849
+ writeFileSync(statePath, `${JSON.stringify(redactSecrets(state), null, 2)}\n`);
850
+ }
851
+ catch (err) {
852
+ console.error(`[software-agent] Failed to write context session state: ${err.message || String(err)}`);
853
+ }
854
+ }
855
+ function readSoftwareAgentContextSessionSummary(statePath) {
856
+ try {
857
+ const parsed = readSoftwareAgentContextSessionState(statePath);
858
+ if (!parsed?.lastTaskId || !parsed.lastResult)
859
+ return undefined;
860
+ const result = parsed.lastResult;
861
+ const status = result.success === true ? "completed" : "failed";
862
+ const lines = [
863
+ `Previous task: ${parsed.lastTaskId}`,
864
+ parsed.lastGoal ? `Previous goal: ${parsed.lastGoal}` : "",
865
+ `Status: ${status}`,
866
+ Number.isInteger(result.exitCode) ? `Exit code: ${result.exitCode}` : "Exit code: null",
867
+ Number.isInteger(result.durationMs) ? `Duration: ${result.durationMs}ms` : "",
868
+ result.outputSummary?.text ? "Previous output summary:" : "",
869
+ result.outputSummary?.text || "",
870
+ result.errorSummary?.text ? "Previous error summary:" : "",
871
+ result.errorSummary?.text || "",
872
+ ].filter(Boolean);
873
+ const summary = lines.join("\n").trim();
874
+ return summary ? summarizeText(summary, MAX_CONTEXT_SESSION_SUMMARY_CHARS).text : undefined;
875
+ }
876
+ catch {
877
+ return undefined;
878
+ }
879
+ }
880
+ function readSoftwareAgentContextSessionState(statePath) {
881
+ try {
882
+ if (!existsSync(statePath))
883
+ return null;
884
+ const parsed = JSON.parse(readFileSync(statePath, "utf8"));
885
+ if (!isSoftwareAgentContextSessionState(parsed))
886
+ return null;
887
+ return parsed;
888
+ }
889
+ catch {
890
+ return null;
891
+ }
892
+ }
893
+ function isSoftwareAgentContextSessionState(value) {
894
+ return value
895
+ && value.schemaVersion === 1
896
+ && typeof value.sessionId === "string"
897
+ && typeof value.updatedAt === "string"
898
+ && (value.workMemoryDir === undefined || typeof value.workMemoryDir === "string")
899
+ && typeof value.lastTaskId === "string"
900
+ && value.lastResult
901
+ && typeof value.lastResult.success === "boolean"
902
+ && (typeof value.lastResult.exitCode === "number" || value.lastResult.exitCode === null)
903
+ && typeof value.lastResult.durationMs === "number";
904
+ }
539
905
  function readOptionalString(value, field) {
540
906
  if (value === undefined || value === null)
541
907
  return undefined;
@@ -544,6 +910,27 @@ function readOptionalString(value, field) {
544
910
  const trimmed = value.trim();
545
911
  return trimmed || undefined;
546
912
  }
913
+ function readOptionalContextSessionId(value, field) {
914
+ if (value === undefined || value === null || value === "")
915
+ return undefined;
916
+ if (typeof value !== "string")
917
+ throw new Error(`Invalid ${field}: expected string`);
918
+ return normalizeContextSessionId(value, field);
919
+ }
920
+ function normalizeContextSessionId(value, field = "contextSessionId") {
921
+ if (!value)
922
+ return undefined;
923
+ const trimmed = value.trim();
924
+ if (!trimmed)
925
+ return undefined;
926
+ if (trimmed.length > MAX_CONTEXT_SESSION_ID_LENGTH) {
927
+ throw new Error(`Invalid ${field}: expected at most ${MAX_CONTEXT_SESSION_ID_LENGTH} characters`);
928
+ }
929
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(trimmed)) {
930
+ throw new Error(`Invalid ${field}: expected letters, numbers, dot, underscore, or hyphen`);
931
+ }
932
+ return trimmed;
933
+ }
547
934
  function normalizeSoftwareAgentTaskOptions(options) {
548
935
  if (!options)
549
936
  return {};
@@ -552,6 +939,38 @@ function normalizeSoftwareAgentTaskOptions(options) {
552
939
  }
553
940
  return options;
554
941
  }
942
+ function normalizeSoftwareAgentEnvPolicy(value) {
943
+ if (value === undefined || value === null || value === "")
944
+ return DEFAULT_SOFTWARE_AGENT_ENV_POLICY;
945
+ if (value === "inherit" || value === "allowlist")
946
+ return value;
947
+ throw new Error("Invalid software-agent env policy: expected inherit or allowlist");
948
+ }
949
+ function normalizeSoftwareAgentEnvAllowlist(values) {
950
+ if (!values)
951
+ return [];
952
+ const seen = new Set();
953
+ for (const value of values) {
954
+ if (typeof value !== "string") {
955
+ throw new Error("Invalid software-agent env allowlist entry: expected string");
956
+ }
957
+ const key = value.trim();
958
+ if (!key)
959
+ continue;
960
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
961
+ throw new Error(`Invalid software-agent env allowlist entry: ${key}`);
962
+ }
963
+ seen.add(key);
964
+ }
965
+ return [...seen];
966
+ }
967
+ function isForbiddenSoftwareAgentEnvKey(key) {
968
+ const upper = key.toUpperCase();
969
+ if (upper.startsWith("AKEMON_"))
970
+ return true;
971
+ const looksLikeCredential = /(?:SECRET|TOKEN|ACCESS|KEY|CREDENTIAL)/.test(upper);
972
+ return looksLikeCredential && (upper.includes("RELAY") || upper.includes("OWNER"));
973
+ }
555
974
  function isAbortSignal(value) {
556
975
  return !!value
557
976
  && typeof value.aborted === "boolean"
@@ -610,6 +1029,11 @@ function normalizeTaskRecordLimit(limit) {
610
1029
  return 20;
611
1030
  return Math.min(limit, 100);
612
1031
  }
1032
+ function normalizeTaskLedgerMaxRecords(limit) {
1033
+ if (!Number.isInteger(limit) || limit <= 0)
1034
+ return DEFAULT_TASK_LEDGER_MAX_RECORDS;
1035
+ return Math.min(limit, 10_000);
1036
+ }
613
1037
  function readSoftwareAgentTaskRecordFile(file) {
614
1038
  try {
615
1039
  if (!existsSync(file))
@@ -640,6 +1064,13 @@ function compareSoftwareAgentTaskRecords(a, b) {
640
1064
  return bTime - aTime;
641
1065
  return b.taskId.localeCompare(a.taskId);
642
1066
  }
1067
+ function compareSoftwareAgentContextSessions(a, b) {
1068
+ const bTime = Date.parse(b.updatedAt || "") || 0;
1069
+ const aTime = Date.parse(a.updatedAt || "") || 0;
1070
+ if (bTime !== aTime)
1071
+ return bTime - aTime;
1072
+ return b.sessionId.localeCompare(a.sessionId);
1073
+ }
643
1074
  function buildCodexExecCommand(opts) {
644
1075
  const args = [
645
1076
  "exec",