akemon 0.3.3 → 0.3.5
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 +24 -1
- package/dist/cli.js +218 -14
- package/dist/engine-peripheral.js +5 -4
- package/dist/engine-routing.js +99 -0
- package/dist/event-bus.js +63 -17
- package/dist/privacy-filter.js +269 -0
- package/dist/redaction.js +159 -0
- package/dist/relay-client.js +39 -2
- package/dist/server.js +222 -52
- package/dist/software-agent-memory.js +139 -0
- package/dist/software-agent-peripheral.js +599 -33
- package/dist/software-agent-stream-cli.js +101 -0
- package/package.json +1 -1
|
@@ -11,10 +11,13 @@
|
|
|
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, unlinkSync, 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";
|
|
20
|
+
import { redactSecrets, StreamingRedactor } from "./redaction.js";
|
|
18
21
|
const defaultTaskRelay = {
|
|
19
22
|
sendTaskStart,
|
|
20
23
|
sendTaskStream,
|
|
@@ -30,6 +33,40 @@ const DEFAULT_OWNER_FORBIDDEN_ACTIONS = [
|
|
|
30
33
|
const ROLE_SCOPES = ["owner", "public", "order", "agent", "system"];
|
|
31
34
|
const MEMORY_SCOPES = ["none", "public", "task", "owner"];
|
|
32
35
|
const RISK_LEVELS = ["low", "medium", "high"];
|
|
36
|
+
const MAX_STREAM_SUMMARY_CHARS = 12_000;
|
|
37
|
+
const STREAM_SUMMARY_HEAD_CHARS = 4_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
|
+
];
|
|
33
70
|
export class CodexSoftwareAgentPeripheral {
|
|
34
71
|
id;
|
|
35
72
|
name;
|
|
@@ -39,9 +76,14 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
39
76
|
bus = null;
|
|
40
77
|
activeChild = null;
|
|
41
78
|
activeTaskId = null;
|
|
79
|
+
activeWorkdir = null;
|
|
42
80
|
sessionId = randomUUID();
|
|
43
81
|
constructor(config) {
|
|
44
|
-
this.config =
|
|
82
|
+
this.config = {
|
|
83
|
+
...config,
|
|
84
|
+
envPolicy: normalizeSoftwareAgentEnvPolicy(config.envPolicy),
|
|
85
|
+
envAllowlist: normalizeSoftwareAgentEnvAllowlist(config.envAllowlist),
|
|
86
|
+
};
|
|
45
87
|
this.id = config.id || "software-agent:codex";
|
|
46
88
|
this.name = config.name || "Codex CLI Software Agent";
|
|
47
89
|
}
|
|
@@ -59,29 +101,43 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
59
101
|
this.sessionId = randomUUID();
|
|
60
102
|
}
|
|
61
103
|
async resetSession() {
|
|
62
|
-
|
|
104
|
+
const activePid = this.activeChild?.pid;
|
|
105
|
+
if (activePid) {
|
|
106
|
+
const processGroupId = -activePid;
|
|
63
107
|
try {
|
|
64
|
-
process.kill(
|
|
108
|
+
process.kill(processGroupId, "SIGTERM");
|
|
65
109
|
}
|
|
66
110
|
catch { }
|
|
67
111
|
setTimeout(() => {
|
|
68
112
|
try {
|
|
69
|
-
process.kill(
|
|
113
|
+
process.kill(processGroupId, "SIGKILL");
|
|
70
114
|
}
|
|
71
115
|
catch { }
|
|
72
116
|
}, 3000).unref();
|
|
73
117
|
}
|
|
74
118
|
this.activeChild = null;
|
|
75
119
|
this.activeTaskId = null;
|
|
120
|
+
this.activeWorkdir = null;
|
|
76
121
|
this.sessionId = randomUUID();
|
|
77
122
|
}
|
|
78
123
|
getState() {
|
|
124
|
+
const currentWorkdir = this.activeWorkdir || resolvePath(this.config.workdir);
|
|
79
125
|
return {
|
|
80
126
|
id: this.id,
|
|
81
127
|
sessionId: this.sessionId,
|
|
82
128
|
activeTaskId: this.activeTaskId,
|
|
129
|
+
activeWorkdir: this.activeWorkdir,
|
|
83
130
|
busy: !!this.activeChild,
|
|
84
131
|
transport: "codex-exec",
|
|
132
|
+
baseWorkdir: resolvePath(this.config.workdir),
|
|
133
|
+
workdirStatus: this.collectWorkdirStatus(currentWorkdir),
|
|
134
|
+
taskLedgerDir: this.config.taskLedgerDir,
|
|
135
|
+
contextSessionDir: this.config.contextSessionDir,
|
|
136
|
+
environment: buildSoftwareAgentChildEnvironment({
|
|
137
|
+
policy: this.config.envPolicy,
|
|
138
|
+
allowlist: this.config.envAllowlist,
|
|
139
|
+
sourceEnv: this.config.sourceEnv,
|
|
140
|
+
}).audit,
|
|
85
141
|
};
|
|
86
142
|
}
|
|
87
143
|
async send(signal) {
|
|
@@ -97,13 +153,24 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
97
153
|
const result = await this.sendTask(envelope);
|
|
98
154
|
return sig(SIG.SOFTWARE_AGENT_RESPONSE, { ...result }, this.id);
|
|
99
155
|
}
|
|
100
|
-
async sendTask(envelope,
|
|
156
|
+
async sendTask(envelope, taskOptions) {
|
|
101
157
|
if (this.activeChild) {
|
|
102
158
|
throw new Error(`Software agent busy (task=${this.activeTaskId})`);
|
|
103
159
|
}
|
|
160
|
+
const { signal, observer } = normalizeSoftwareAgentTaskOptions(taskOptions);
|
|
104
161
|
const taskId = envelope.taskId || `sw_${Date.now()}_${randomUUID().slice(0, 8)}`;
|
|
105
|
-
const
|
|
106
|
-
const
|
|
162
|
+
const contextSessionId = normalizeContextSessionId(envelope.contextSessionId) || taskId;
|
|
163
|
+
const workdirSafety = resolveWorkdirSafety(this.config.workdir, envelope.workdir || this.config.workdir, envelope.workdirSafety?.allowOutsideWorkdir || false);
|
|
164
|
+
const workdir = workdirSafety.effectiveWorkdir;
|
|
165
|
+
let effectiveEnvelope = { ...envelope, taskId, contextSessionId, workdir, workdirSafety };
|
|
166
|
+
const contextSession = this.config.contextSessionDir
|
|
167
|
+
? writeSoftwareAgentContextPacket(this.config.contextSessionDir, effectiveEnvelope)
|
|
168
|
+
: undefined;
|
|
169
|
+
if (contextSession)
|
|
170
|
+
effectiveEnvelope = contextSession.envelope;
|
|
171
|
+
const prompt = contextSession
|
|
172
|
+
? buildContextPacketLaunchPrompt(effectiveEnvelope, contextSession.audit.packetPath)
|
|
173
|
+
: buildTaskEnvelopePrompt(effectiveEnvelope);
|
|
107
174
|
const { cmd, args } = buildCodexExecCommand({
|
|
108
175
|
command: this.config.command || "codex",
|
|
109
176
|
workdir,
|
|
@@ -111,17 +178,45 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
111
178
|
sandbox: this.config.sandbox || "workspace-write",
|
|
112
179
|
});
|
|
113
180
|
const startedAt = Date.now();
|
|
181
|
+
const startedAtIso = new Date(startedAt).toISOString();
|
|
114
182
|
const relay = this.config.taskRelay || defaultTaskRelay;
|
|
115
183
|
const commandLine = [cmd, ...args].join(" ");
|
|
116
184
|
const spawnImpl = this.config.spawnImpl || spawn;
|
|
117
185
|
const timeoutMs = envelope.timeoutMs || this.config.defaultTimeoutMs || DEFAULT_TIMEOUT_MS;
|
|
186
|
+
const workdirStatus = this.collectWorkdirStatus(workdir);
|
|
187
|
+
const childEnvironment = buildSoftwareAgentChildEnvironment({
|
|
188
|
+
policy: this.config.envPolicy,
|
|
189
|
+
allowlist: this.config.envAllowlist,
|
|
190
|
+
sourceEnv: this.config.sourceEnv,
|
|
191
|
+
});
|
|
192
|
+
const baseTaskRecord = () => ({
|
|
193
|
+
schemaVersion: 1,
|
|
194
|
+
taskId,
|
|
195
|
+
agentId: this.id,
|
|
196
|
+
sessionId: this.sessionId,
|
|
197
|
+
transport: "codex-exec",
|
|
198
|
+
commandLine,
|
|
199
|
+
envelope: effectiveEnvelope,
|
|
200
|
+
startedAt: startedAtIso,
|
|
201
|
+
environment: childEnvironment.audit,
|
|
202
|
+
contextSession: contextSession?.audit,
|
|
203
|
+
workdirStatus,
|
|
204
|
+
});
|
|
118
205
|
return new Promise((resolve) => {
|
|
119
206
|
this.activeTaskId = taskId;
|
|
120
|
-
|
|
207
|
+
this.activeWorkdir = workdir;
|
|
208
|
+
this.writeTaskRecord({
|
|
209
|
+
...baseTaskRecord(),
|
|
210
|
+
status: "running",
|
|
211
|
+
updatedAt: startedAtIso,
|
|
212
|
+
});
|
|
213
|
+
const origin = "software_agent";
|
|
214
|
+
relay.sendTaskStart(taskId, origin, commandLine);
|
|
215
|
+
observer?.onStart?.({ taskId, origin, commandLine });
|
|
121
216
|
this.bus?.emit(SIG.TASK_STARTED, sig(SIG.TASK_STARTED, {
|
|
122
217
|
taskId,
|
|
123
218
|
taskType: "software_agent",
|
|
124
|
-
description:
|
|
219
|
+
description: effectiveEnvelope.goal,
|
|
125
220
|
peripheral: this.id,
|
|
126
221
|
sessionId: this.sessionId,
|
|
127
222
|
}, this.id));
|
|
@@ -129,7 +224,7 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
129
224
|
try {
|
|
130
225
|
child = spawnImpl(cmd, args, {
|
|
131
226
|
cwd: workdir,
|
|
132
|
-
env:
|
|
227
|
+
env: childEnvironment.env,
|
|
133
228
|
stdio: ["pipe", "pipe", "pipe"],
|
|
134
229
|
detached: true,
|
|
135
230
|
});
|
|
@@ -137,8 +232,8 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
137
232
|
catch (err) {
|
|
138
233
|
this.activeChild = null;
|
|
139
234
|
this.activeTaskId = null;
|
|
235
|
+
this.activeWorkdir = null;
|
|
140
236
|
const durationMs = Date.now() - startedAt;
|
|
141
|
-
relay.sendTaskEnd(taskId, null, durationMs);
|
|
142
237
|
const result = {
|
|
143
238
|
success: false,
|
|
144
239
|
taskId,
|
|
@@ -147,6 +242,22 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
147
242
|
exitCode: null,
|
|
148
243
|
durationMs,
|
|
149
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
|
+
}
|
|
150
261
|
this.bus?.emit(SIG.TASK_FAILED, sig(SIG.TASK_FAILED, result, this.id));
|
|
151
262
|
resolve(result);
|
|
152
263
|
return;
|
|
@@ -158,6 +269,14 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
158
269
|
let aborted = false;
|
|
159
270
|
const outDecoder = new StringDecoder("utf8");
|
|
160
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
|
+
};
|
|
161
280
|
const finish = (exitCode, error) => {
|
|
162
281
|
if (finished)
|
|
163
282
|
return;
|
|
@@ -168,16 +287,18 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
168
287
|
const tailErr = errDecoder.end();
|
|
169
288
|
if (tailOut) {
|
|
170
289
|
stdout += tailOut;
|
|
171
|
-
|
|
290
|
+
emitSafeStream("stdout", outRedactor.push(tailOut));
|
|
172
291
|
}
|
|
173
292
|
if (tailErr) {
|
|
174
293
|
stderr += tailErr;
|
|
175
|
-
|
|
294
|
+
emitSafeStream("stderr", errRedactor.push(tailErr));
|
|
176
295
|
}
|
|
296
|
+
emitSafeStream("stdout", outRedactor.flush());
|
|
297
|
+
emitSafeStream("stderr", errRedactor.flush());
|
|
177
298
|
const durationMs = Date.now() - startedAt;
|
|
178
|
-
relay.sendTaskEnd(taskId, exitCode, durationMs);
|
|
179
299
|
this.activeChild = null;
|
|
180
300
|
this.activeTaskId = null;
|
|
301
|
+
this.activeWorkdir = null;
|
|
181
302
|
const output = stdout.trim() || stderr.trim();
|
|
182
303
|
const success = !error && !aborted && exitCode === 0;
|
|
183
304
|
const result = {
|
|
@@ -188,6 +309,22 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
188
309
|
exitCode,
|
|
189
310
|
durationMs,
|
|
190
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
|
+
}
|
|
191
328
|
this.bus?.emit(success ? SIG.TASK_COMPLETED : SIG.TASK_FAILED, sig(success ? SIG.TASK_COMPLETED : SIG.TASK_FAILED, {
|
|
192
329
|
...result,
|
|
193
330
|
taskLabel: `software_agent:${this.id}`,
|
|
@@ -238,14 +375,14 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
238
375
|
if (!text)
|
|
239
376
|
return;
|
|
240
377
|
stdout += text;
|
|
241
|
-
|
|
378
|
+
emitSafeStream("stdout", outRedactor.push(text));
|
|
242
379
|
});
|
|
243
380
|
child.stderr?.on("data", (chunk) => {
|
|
244
381
|
const text = errDecoder.write(chunk);
|
|
245
382
|
if (!text)
|
|
246
383
|
return;
|
|
247
384
|
stderr += text;
|
|
248
|
-
|
|
385
|
+
emitSafeStream("stderr", errRedactor.push(text));
|
|
249
386
|
});
|
|
250
387
|
child.on("close", (code) => {
|
|
251
388
|
child.unref();
|
|
@@ -257,23 +394,79 @@ export class CodexSoftwareAgentPeripheral {
|
|
|
257
394
|
});
|
|
258
395
|
});
|
|
259
396
|
}
|
|
397
|
+
writeTaskRecord(record) {
|
|
398
|
+
const dir = this.config.taskLedgerDir;
|
|
399
|
+
if (!dir)
|
|
400
|
+
return;
|
|
401
|
+
try {
|
|
402
|
+
mkdirSync(dir, { recursive: true });
|
|
403
|
+
const safeTaskId = safeTaskFilename(record.taskId);
|
|
404
|
+
writeFileSync(join(dir, `${safeTaskId}.json`), `${JSON.stringify(redactSecrets(record), null, 2)}\n`);
|
|
405
|
+
pruneSoftwareAgentTaskRecords(dir, this.config.taskLedgerMaxRecords, record.taskId);
|
|
406
|
+
}
|
|
407
|
+
catch (err) {
|
|
408
|
+
console.error(`[software-agent] Failed to write task ledger: ${err.message || String(err)}`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
collectWorkdirStatus(workdir) {
|
|
412
|
+
const impl = this.config.gitStatusImpl || readGitWorktreeStatus;
|
|
413
|
+
return impl(workdir);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
export function summarizeText(text, maxChars = MAX_STREAM_SUMMARY_CHARS) {
|
|
417
|
+
const normalized = text || "";
|
|
418
|
+
const chars = normalized.length;
|
|
419
|
+
const bytes = Buffer.byteLength(normalized, "utf8");
|
|
420
|
+
const lines = normalized ? normalized.split(/\r\n|\r|\n/).length : 0;
|
|
421
|
+
if (chars <= maxChars) {
|
|
422
|
+
return { chars, bytes, lines, text: normalized, truncated: false };
|
|
423
|
+
}
|
|
424
|
+
const headChars = Math.min(STREAM_SUMMARY_HEAD_CHARS, maxChars);
|
|
425
|
+
const tailChars = Math.max(0, maxChars - headChars);
|
|
426
|
+
const omittedChars = chars - headChars - tailChars;
|
|
427
|
+
const tailText = tailChars > 0 ? normalized.slice(-tailChars) : "";
|
|
428
|
+
return {
|
|
429
|
+
chars,
|
|
430
|
+
bytes,
|
|
431
|
+
lines,
|
|
432
|
+
text: [
|
|
433
|
+
normalized.slice(0, headChars),
|
|
434
|
+
`[truncated ${omittedChars} chars]`,
|
|
435
|
+
tailText,
|
|
436
|
+
].join("\n"),
|
|
437
|
+
truncated: true,
|
|
438
|
+
omittedChars,
|
|
439
|
+
};
|
|
260
440
|
}
|
|
261
441
|
export function buildTaskEnvelopePrompt(envelope) {
|
|
262
442
|
const lines = [
|
|
263
443
|
"[Akemon Software Peripheral Task Envelope]",
|
|
264
444
|
"",
|
|
265
445
|
`Task ID: ${envelope.taskId || "(unspecified)"}`,
|
|
446
|
+
`Akemon context session: ${envelope.contextSessionId || "(one-shot)"}`,
|
|
266
447
|
`Source module: ${envelope.sourceModule}`,
|
|
267
448
|
`Purpose: ${envelope.purpose}`,
|
|
268
449
|
`Role scope: ${envelope.roleScope}`,
|
|
269
450
|
`Memory scope: ${envelope.memoryScope}`,
|
|
270
451
|
`Risk level: ${envelope.riskLevel}`,
|
|
271
452
|
`Workdir: ${envelope.workdir}`,
|
|
272
|
-
"",
|
|
273
|
-
"Goal:",
|
|
274
|
-
envelope.goal,
|
|
275
|
-
"",
|
|
276
453
|
];
|
|
454
|
+
if (envelope.contextPacketPath) {
|
|
455
|
+
lines.push(`Context packet path: ${envelope.contextPacketPath}`);
|
|
456
|
+
}
|
|
457
|
+
if (envelope.workdirSafety) {
|
|
458
|
+
lines.push(`Base workdir: ${envelope.workdirSafety.baseWorkdir}`);
|
|
459
|
+
lines.push(`Requested workdir: ${envelope.workdirSafety.requestedWorkdir}`);
|
|
460
|
+
lines.push(`Effective workdir: ${envelope.workdirSafety.effectiveWorkdir}`);
|
|
461
|
+
lines.push(`Outside base workdir: ${envelope.workdirSafety.outsideBaseWorkdir ? "yes" : "no"}`);
|
|
462
|
+
lines.push(`Outside workdir explicitly allowed: ${envelope.workdirSafety.allowOutsideWorkdir ? "yes" : "no"}`);
|
|
463
|
+
}
|
|
464
|
+
lines.push("", "Goal:", envelope.goal, "");
|
|
465
|
+
if (envelope.previousTaskSummary?.trim()) {
|
|
466
|
+
lines.push("Previous task summary for this Akemon context session:");
|
|
467
|
+
lines.push(envelope.previousTaskSummary.trim());
|
|
468
|
+
lines.push("");
|
|
469
|
+
}
|
|
277
470
|
if (envelope.memorySummary?.trim()) {
|
|
278
471
|
lines.push("Visible Akemon memory/context:");
|
|
279
472
|
lines.push(envelope.memorySummary.trim());
|
|
@@ -303,31 +496,281 @@ export function buildTaskEnvelopePrompt(envelope) {
|
|
|
303
496
|
lines.push("- Report what changed, what you verified, and any remaining risk.");
|
|
304
497
|
return lines.join("\n");
|
|
305
498
|
}
|
|
499
|
+
export function buildContextPacketLaunchPrompt(envelope, contextPacketPath) {
|
|
500
|
+
return [
|
|
501
|
+
"[Akemon Software Peripheral Task]",
|
|
502
|
+
"",
|
|
503
|
+
`Task ID: ${envelope.taskId || "(unspecified)"}`,
|
|
504
|
+
`Akemon context session: ${envelope.contextSessionId || "(one-shot)"}`,
|
|
505
|
+
`Context packet: ${contextPacketPath}`,
|
|
506
|
+
`Workdir: ${envelope.workdir}`,
|
|
507
|
+
"",
|
|
508
|
+
"Goal:",
|
|
509
|
+
envelope.goal,
|
|
510
|
+
"",
|
|
511
|
+
"Instructions:",
|
|
512
|
+
"- Read the context packet first before doing repository work.",
|
|
513
|
+
"- Treat that file as the complete Akemon-provided context for this task.",
|
|
514
|
+
"- Do not read Akemon private memory outside the context packet.",
|
|
515
|
+
"- Work only in the stated workdir unless the packet explicitly allows otherwise.",
|
|
516
|
+
"- Report what changed, what you verified, and any remaining risk.",
|
|
517
|
+
].join("\n");
|
|
518
|
+
}
|
|
306
519
|
export function createOwnerTaskEnvelope(body, defaultWorkdir) {
|
|
307
520
|
const goal = typeof body?.goal === "string" ? body.goal.trim() : "";
|
|
308
521
|
if (!goal)
|
|
309
522
|
throw new Error("Missing required string field: goal");
|
|
310
|
-
const callerForbiddenActions = readOptionalStringArray(body
|
|
523
|
+
const callerForbiddenActions = readOptionalStringArray(body?.forbiddenActions, "forbiddenActions");
|
|
524
|
+
const requestedWorkdir = readOptionalString(body?.workdir, "workdir") || defaultWorkdir;
|
|
525
|
+
const workdirSafety = resolveWorkdirSafety(defaultWorkdir, requestedWorkdir, readOptionalBoolean(body?.allowOutsideWorkdir, "allowOutsideWorkdir") || false);
|
|
311
526
|
return {
|
|
312
|
-
taskId: readOptionalString(body
|
|
527
|
+
taskId: readOptionalString(body?.taskId, "taskId"),
|
|
313
528
|
sourceModule: "owner-http",
|
|
314
|
-
purpose: readOptionalString(body
|
|
529
|
+
purpose: readOptionalString(body?.purpose, "purpose") || "owner software-agent task",
|
|
315
530
|
goal,
|
|
316
|
-
workdir:
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
531
|
+
workdir: workdirSafety.effectiveWorkdir,
|
|
532
|
+
workdirSafety,
|
|
533
|
+
roleScope: readEnum(body?.roleScope, "roleScope", ROLE_SCOPES, "owner"),
|
|
534
|
+
memoryScope: readEnum(body?.memoryScope, "memoryScope", MEMORY_SCOPES, "owner"),
|
|
535
|
+
riskLevel: readEnum(body?.riskLevel, "riskLevel", RISK_LEVELS, "medium"),
|
|
536
|
+
allowedActions: body?.allowedActions !== undefined
|
|
537
|
+
? readOptionalStringArray(body?.allowedActions, "allowedActions")
|
|
322
538
|
: [...DEFAULT_OWNER_ALLOWED_ACTIONS],
|
|
323
539
|
forbiddenActions: [...new Set([...DEFAULT_OWNER_FORBIDDEN_ACTIONS, ...callerForbiddenActions])],
|
|
324
|
-
memorySummary: typeof body
|
|
325
|
-
|
|
540
|
+
memorySummary: typeof body?.memorySummary === "string" ? body.memorySummary : "",
|
|
541
|
+
contextSessionId: readOptionalContextSessionId(body?.contextSessionId ?? body?.sessionId, "contextSessionId"),
|
|
542
|
+
deliverable: typeof body?.deliverable === "string"
|
|
326
543
|
? body.deliverable
|
|
327
544
|
: "Return a concise engineering summary with changes, verification, and remaining risks.",
|
|
328
|
-
timeoutMs: readTimeoutMs(body
|
|
545
|
+
timeoutMs: readTimeoutMs(body?.timeoutMs),
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
export function resolveWorkdirSafety(baseWorkdir, requestedWorkdir, allowOutsideWorkdir = false) {
|
|
549
|
+
const base = resolvePath(baseWorkdir);
|
|
550
|
+
const requested = isAbsolute(requestedWorkdir)
|
|
551
|
+
? resolvePath(requestedWorkdir)
|
|
552
|
+
: resolvePath(base, requestedWorkdir);
|
|
553
|
+
const rel = relative(base, requested);
|
|
554
|
+
const outsideBaseWorkdir = !!rel && (rel.startsWith("..") || isAbsolute(rel));
|
|
555
|
+
if (outsideBaseWorkdir && !allowOutsideWorkdir) {
|
|
556
|
+
throw new Error(`Invalid workdir: ${requested} is outside base workdir ${base}`);
|
|
557
|
+
}
|
|
558
|
+
return {
|
|
559
|
+
baseWorkdir: base,
|
|
560
|
+
requestedWorkdir,
|
|
561
|
+
effectiveWorkdir: requested,
|
|
562
|
+
allowOutsideWorkdir,
|
|
563
|
+
outsideBaseWorkdir,
|
|
329
564
|
};
|
|
330
565
|
}
|
|
566
|
+
export function readGitWorktreeStatus(workdir) {
|
|
567
|
+
const resolvedWorkdir = resolvePath(workdir);
|
|
568
|
+
try {
|
|
569
|
+
const rootResult = spawnSync("git", ["-C", resolvedWorkdir, "rev-parse", "--show-toplevel"], {
|
|
570
|
+
encoding: "utf8",
|
|
571
|
+
timeout: 5000,
|
|
572
|
+
});
|
|
573
|
+
if (rootResult.status !== 0) {
|
|
574
|
+
return {
|
|
575
|
+
workdir: resolvedWorkdir,
|
|
576
|
+
isRepo: false,
|
|
577
|
+
dirty: false,
|
|
578
|
+
changedFiles: [],
|
|
579
|
+
error: summarizeGitError(rootResult.stderr, rootResult.error),
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
const root = String(rootResult.stdout || "").trim();
|
|
583
|
+
const statusResult = spawnSync("git", ["-C", resolvedWorkdir, "status", "--short"], {
|
|
584
|
+
encoding: "utf8",
|
|
585
|
+
timeout: 5000,
|
|
586
|
+
});
|
|
587
|
+
if (statusResult.status !== 0) {
|
|
588
|
+
return {
|
|
589
|
+
workdir: resolvedWorkdir,
|
|
590
|
+
isRepo: true,
|
|
591
|
+
dirty: false,
|
|
592
|
+
changedFiles: [],
|
|
593
|
+
root,
|
|
594
|
+
error: summarizeGitError(statusResult.stderr, statusResult.error),
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
const changedFiles = String(statusResult.stdout || "")
|
|
598
|
+
.split(/\r?\n/)
|
|
599
|
+
.map((line) => line.trimEnd())
|
|
600
|
+
.filter(Boolean)
|
|
601
|
+
.map((line) => line.slice(3).trim())
|
|
602
|
+
.filter(Boolean);
|
|
603
|
+
return {
|
|
604
|
+
workdir: resolvedWorkdir,
|
|
605
|
+
isRepo: true,
|
|
606
|
+
dirty: changedFiles.length > 0,
|
|
607
|
+
changedFiles,
|
|
608
|
+
root,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
catch (err) {
|
|
612
|
+
return {
|
|
613
|
+
workdir: resolvedWorkdir,
|
|
614
|
+
isRepo: false,
|
|
615
|
+
dirty: false,
|
|
616
|
+
changedFiles: [],
|
|
617
|
+
error: err.message || String(err),
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
export function listSoftwareAgentTaskRecords(taskLedgerDir, limit = 20) {
|
|
622
|
+
const safeLimit = normalizeTaskRecordLimit(limit);
|
|
623
|
+
try {
|
|
624
|
+
if (!existsSync(taskLedgerDir))
|
|
625
|
+
return [];
|
|
626
|
+
return readdirSync(taskLedgerDir, { withFileTypes: true })
|
|
627
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
|
|
628
|
+
.map((entry) => readSoftwareAgentTaskRecordFile(join(taskLedgerDir, entry.name)))
|
|
629
|
+
.filter((record) => !!record)
|
|
630
|
+
.sort(compareSoftwareAgentTaskRecords)
|
|
631
|
+
.slice(0, safeLimit);
|
|
632
|
+
}
|
|
633
|
+
catch {
|
|
634
|
+
return [];
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
export function readSoftwareAgentTaskRecord(taskLedgerDir, taskId) {
|
|
638
|
+
const file = join(taskLedgerDir, `${safeTaskFilename(taskId)}.json`);
|
|
639
|
+
return readSoftwareAgentTaskRecordFile(file);
|
|
640
|
+
}
|
|
641
|
+
export function pruneSoftwareAgentTaskRecords(taskLedgerDir, maxRecords = DEFAULT_TASK_LEDGER_MAX_RECORDS, preserveTaskId) {
|
|
642
|
+
const safeMaxRecords = normalizeTaskLedgerMaxRecords(maxRecords);
|
|
643
|
+
try {
|
|
644
|
+
if (!existsSync(taskLedgerDir))
|
|
645
|
+
return 0;
|
|
646
|
+
const records = readdirSync(taskLedgerDir, { withFileTypes: true })
|
|
647
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
|
|
648
|
+
.map((entry) => {
|
|
649
|
+
const file = join(taskLedgerDir, entry.name);
|
|
650
|
+
const record = readSoftwareAgentTaskRecordFile(file);
|
|
651
|
+
return record ? { file, record } : null;
|
|
652
|
+
})
|
|
653
|
+
.filter((entry) => !!entry)
|
|
654
|
+
.sort((a, b) => compareSoftwareAgentTaskRecords(a.record, b.record));
|
|
655
|
+
const keepTaskIds = new Set(records.slice(0, safeMaxRecords).map((entry) => entry.record.taskId));
|
|
656
|
+
if (preserveTaskId)
|
|
657
|
+
keepTaskIds.add(preserveTaskId);
|
|
658
|
+
let deleted = 0;
|
|
659
|
+
for (const entry of records) {
|
|
660
|
+
if (keepTaskIds.has(entry.record.taskId))
|
|
661
|
+
continue;
|
|
662
|
+
try {
|
|
663
|
+
unlinkSync(entry.file);
|
|
664
|
+
deleted++;
|
|
665
|
+
}
|
|
666
|
+
catch {
|
|
667
|
+
// Best effort: retention should not break task completion.
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return deleted;
|
|
671
|
+
}
|
|
672
|
+
catch {
|
|
673
|
+
return 0;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
export function buildSoftwareAgentChildEnvironment(opts = {}) {
|
|
677
|
+
const policy = normalizeSoftwareAgentEnvPolicy(opts.policy);
|
|
678
|
+
const sourceEnv = opts.sourceEnv || process.env;
|
|
679
|
+
if (policy === "inherit") {
|
|
680
|
+
return {
|
|
681
|
+
env: sourceEnv,
|
|
682
|
+
audit: { policy },
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
const allowlist = normalizeSoftwareAgentEnvAllowlist([
|
|
686
|
+
...DEFAULT_SOFTWARE_AGENT_ENV_ALLOWLIST,
|
|
687
|
+
...(opts.allowlist || []),
|
|
688
|
+
]);
|
|
689
|
+
const env = {};
|
|
690
|
+
for (const key of allowlist) {
|
|
691
|
+
if (isForbiddenSoftwareAgentEnvKey(key))
|
|
692
|
+
continue;
|
|
693
|
+
const value = sourceEnv[key];
|
|
694
|
+
if (value !== undefined)
|
|
695
|
+
env[key] = value;
|
|
696
|
+
}
|
|
697
|
+
return {
|
|
698
|
+
env,
|
|
699
|
+
audit: {
|
|
700
|
+
policy,
|
|
701
|
+
allowedKeys: Object.keys(env).sort(),
|
|
702
|
+
},
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
function writeSoftwareAgentContextPacket(contextSessionDir, envelope) {
|
|
706
|
+
const sessionId = normalizeContextSessionId(envelope.contextSessionId) || envelope.taskId || randomUUID();
|
|
707
|
+
const sessionDir = join(contextSessionDir, sessionId);
|
|
708
|
+
const packetPath = join(sessionDir, CONTEXT_PACKET_FILENAME);
|
|
709
|
+
const statePath = join(sessionDir, CONTEXT_SESSION_STATE_FILENAME);
|
|
710
|
+
const previousTaskSummary = readSoftwareAgentContextSessionSummary(statePath);
|
|
711
|
+
const packetEnvelope = {
|
|
712
|
+
...envelope,
|
|
713
|
+
contextSessionId: sessionId,
|
|
714
|
+
contextPacketPath: packetPath,
|
|
715
|
+
previousTaskSummary,
|
|
716
|
+
};
|
|
717
|
+
mkdirSync(sessionDir, { recursive: true });
|
|
718
|
+
const content = buildTaskEnvelopePrompt(packetEnvelope);
|
|
719
|
+
writeFileSync(packetPath, `${redactSecrets(content)}\n`);
|
|
720
|
+
return {
|
|
721
|
+
envelope: packetEnvelope,
|
|
722
|
+
audit: { sessionId, packetPath, statePath },
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
function writeSoftwareAgentContextSessionState(statePath, envelope, result, updatedAt) {
|
|
726
|
+
try {
|
|
727
|
+
const state = {
|
|
728
|
+
schemaVersion: 1,
|
|
729
|
+
sessionId: envelope.contextSessionId,
|
|
730
|
+
updatedAt,
|
|
731
|
+
lastTaskId: result.taskId,
|
|
732
|
+
lastGoal: envelope.goal,
|
|
733
|
+
lastResult: {
|
|
734
|
+
success: result.success,
|
|
735
|
+
exitCode: result.exitCode,
|
|
736
|
+
durationMs: result.durationMs,
|
|
737
|
+
outputSummary: summarizeText(result.output || "", MAX_CONTEXT_SESSION_SUMMARY_CHARS),
|
|
738
|
+
errorSummary: result.error ? summarizeText(result.error, MAX_CONTEXT_SESSION_SUMMARY_CHARS) : undefined,
|
|
739
|
+
},
|
|
740
|
+
};
|
|
741
|
+
writeFileSync(statePath, `${JSON.stringify(redactSecrets(state), null, 2)}\n`);
|
|
742
|
+
}
|
|
743
|
+
catch (err) {
|
|
744
|
+
console.error(`[software-agent] Failed to write context session state: ${err.message || String(err)}`);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
function readSoftwareAgentContextSessionSummary(statePath) {
|
|
748
|
+
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)
|
|
753
|
+
return undefined;
|
|
754
|
+
const result = parsed.lastResult;
|
|
755
|
+
const status = result.success === true ? "completed" : "failed";
|
|
756
|
+
const lines = [
|
|
757
|
+
`Previous task: ${parsed.lastTaskId}`,
|
|
758
|
+
parsed.lastGoal ? `Previous goal: ${parsed.lastGoal}` : "",
|
|
759
|
+
`Status: ${status}`,
|
|
760
|
+
Number.isInteger(result.exitCode) ? `Exit code: ${result.exitCode}` : "Exit code: null",
|
|
761
|
+
Number.isInteger(result.durationMs) ? `Duration: ${result.durationMs}ms` : "",
|
|
762
|
+
result.outputSummary?.text ? "Previous output summary:" : "",
|
|
763
|
+
result.outputSummary?.text || "",
|
|
764
|
+
result.errorSummary?.text ? "Previous error summary:" : "",
|
|
765
|
+
result.errorSummary?.text || "",
|
|
766
|
+
].filter(Boolean);
|
|
767
|
+
const summary = lines.join("\n").trim();
|
|
768
|
+
return summary ? summarizeText(summary, MAX_CONTEXT_SESSION_SUMMARY_CHARS).text : undefined;
|
|
769
|
+
}
|
|
770
|
+
catch {
|
|
771
|
+
return undefined;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
331
774
|
function readOptionalString(value, field) {
|
|
332
775
|
if (value === undefined || value === null)
|
|
333
776
|
return undefined;
|
|
@@ -336,6 +779,72 @@ function readOptionalString(value, field) {
|
|
|
336
779
|
const trimmed = value.trim();
|
|
337
780
|
return trimmed || undefined;
|
|
338
781
|
}
|
|
782
|
+
function readOptionalContextSessionId(value, field) {
|
|
783
|
+
if (value === undefined || value === null || value === "")
|
|
784
|
+
return undefined;
|
|
785
|
+
if (typeof value !== "string")
|
|
786
|
+
throw new Error(`Invalid ${field}: expected string`);
|
|
787
|
+
return normalizeContextSessionId(value, field);
|
|
788
|
+
}
|
|
789
|
+
function normalizeContextSessionId(value, field = "contextSessionId") {
|
|
790
|
+
if (!value)
|
|
791
|
+
return undefined;
|
|
792
|
+
const trimmed = value.trim();
|
|
793
|
+
if (!trimmed)
|
|
794
|
+
return undefined;
|
|
795
|
+
if (trimmed.length > MAX_CONTEXT_SESSION_ID_LENGTH) {
|
|
796
|
+
throw new Error(`Invalid ${field}: expected at most ${MAX_CONTEXT_SESSION_ID_LENGTH} characters`);
|
|
797
|
+
}
|
|
798
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(trimmed)) {
|
|
799
|
+
throw new Error(`Invalid ${field}: expected letters, numbers, dot, underscore, or hyphen`);
|
|
800
|
+
}
|
|
801
|
+
return trimmed;
|
|
802
|
+
}
|
|
803
|
+
function normalizeSoftwareAgentTaskOptions(options) {
|
|
804
|
+
if (!options)
|
|
805
|
+
return {};
|
|
806
|
+
if (isAbortSignal(options)) {
|
|
807
|
+
return { signal: options };
|
|
808
|
+
}
|
|
809
|
+
return options;
|
|
810
|
+
}
|
|
811
|
+
function normalizeSoftwareAgentEnvPolicy(value) {
|
|
812
|
+
if (value === undefined || value === null || value === "")
|
|
813
|
+
return DEFAULT_SOFTWARE_AGENT_ENV_POLICY;
|
|
814
|
+
if (value === "inherit" || value === "allowlist")
|
|
815
|
+
return value;
|
|
816
|
+
throw new Error("Invalid software-agent env policy: expected inherit or allowlist");
|
|
817
|
+
}
|
|
818
|
+
function normalizeSoftwareAgentEnvAllowlist(values) {
|
|
819
|
+
if (!values)
|
|
820
|
+
return [];
|
|
821
|
+
const seen = new Set();
|
|
822
|
+
for (const value of values) {
|
|
823
|
+
if (typeof value !== "string") {
|
|
824
|
+
throw new Error("Invalid software-agent env allowlist entry: expected string");
|
|
825
|
+
}
|
|
826
|
+
const key = value.trim();
|
|
827
|
+
if (!key)
|
|
828
|
+
continue;
|
|
829
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
830
|
+
throw new Error(`Invalid software-agent env allowlist entry: ${key}`);
|
|
831
|
+
}
|
|
832
|
+
seen.add(key);
|
|
833
|
+
}
|
|
834
|
+
return [...seen];
|
|
835
|
+
}
|
|
836
|
+
function isForbiddenSoftwareAgentEnvKey(key) {
|
|
837
|
+
const upper = key.toUpperCase();
|
|
838
|
+
if (upper.startsWith("AKEMON_"))
|
|
839
|
+
return true;
|
|
840
|
+
const looksLikeCredential = /(?:SECRET|TOKEN|ACCESS|KEY|CREDENTIAL)/.test(upper);
|
|
841
|
+
return looksLikeCredential && (upper.includes("RELAY") || upper.includes("OWNER"));
|
|
842
|
+
}
|
|
843
|
+
function isAbortSignal(value) {
|
|
844
|
+
return !!value
|
|
845
|
+
&& typeof value.aborted === "boolean"
|
|
846
|
+
&& typeof value.addEventListener === "function";
|
|
847
|
+
}
|
|
339
848
|
function readOptionalStringArray(value, field) {
|
|
340
849
|
if (value === undefined || value === null)
|
|
341
850
|
return [];
|
|
@@ -348,6 +857,13 @@ function readOptionalStringArray(value, field) {
|
|
|
348
857
|
return item.trim();
|
|
349
858
|
});
|
|
350
859
|
}
|
|
860
|
+
function readOptionalBoolean(value, field) {
|
|
861
|
+
if (value === undefined || value === null)
|
|
862
|
+
return undefined;
|
|
863
|
+
if (typeof value !== "boolean")
|
|
864
|
+
throw new Error(`Invalid ${field}: expected boolean`);
|
|
865
|
+
return value;
|
|
866
|
+
}
|
|
351
867
|
function readEnum(value, field, allowed, fallback) {
|
|
352
868
|
if (value === undefined || value === null || value === "")
|
|
353
869
|
return fallback;
|
|
@@ -367,6 +883,56 @@ function readTimeoutMs(value) {
|
|
|
367
883
|
}
|
|
368
884
|
return value;
|
|
369
885
|
}
|
|
886
|
+
function safeTaskFilename(taskId) {
|
|
887
|
+
const safe = taskId.replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 200);
|
|
888
|
+
return safe || "task";
|
|
889
|
+
}
|
|
890
|
+
function summarizeGitError(stderr, error) {
|
|
891
|
+
if (error)
|
|
892
|
+
return error.message;
|
|
893
|
+
const text = typeof stderr === "string" ? stderr.trim() : "";
|
|
894
|
+
return text || undefined;
|
|
895
|
+
}
|
|
896
|
+
function normalizeTaskRecordLimit(limit) {
|
|
897
|
+
if (!Number.isInteger(limit) || limit <= 0)
|
|
898
|
+
return 20;
|
|
899
|
+
return Math.min(limit, 100);
|
|
900
|
+
}
|
|
901
|
+
function normalizeTaskLedgerMaxRecords(limit) {
|
|
902
|
+
if (!Number.isInteger(limit) || limit <= 0)
|
|
903
|
+
return DEFAULT_TASK_LEDGER_MAX_RECORDS;
|
|
904
|
+
return Math.min(limit, 10_000);
|
|
905
|
+
}
|
|
906
|
+
function readSoftwareAgentTaskRecordFile(file) {
|
|
907
|
+
try {
|
|
908
|
+
if (!existsSync(file))
|
|
909
|
+
return null;
|
|
910
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
911
|
+
if (!isSoftwareAgentTaskRecord(parsed))
|
|
912
|
+
return null;
|
|
913
|
+
return parsed;
|
|
914
|
+
}
|
|
915
|
+
catch {
|
|
916
|
+
return null;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
function isSoftwareAgentTaskRecord(value) {
|
|
920
|
+
return value
|
|
921
|
+
&& value.schemaVersion === 1
|
|
922
|
+
&& typeof value.taskId === "string"
|
|
923
|
+
&& (value.status === "running" || value.status === "completed" || value.status === "failed")
|
|
924
|
+
&& typeof value.startedAt === "string"
|
|
925
|
+
&& typeof value.updatedAt === "string"
|
|
926
|
+
&& value.envelope
|
|
927
|
+
&& typeof value.envelope.goal === "string";
|
|
928
|
+
}
|
|
929
|
+
function compareSoftwareAgentTaskRecords(a, b) {
|
|
930
|
+
const bTime = Date.parse(b.updatedAt || b.startedAt) || 0;
|
|
931
|
+
const aTime = Date.parse(a.updatedAt || a.startedAt) || 0;
|
|
932
|
+
if (bTime !== aTime)
|
|
933
|
+
return bTime - aTime;
|
|
934
|
+
return b.taskId.localeCompare(a.taskId);
|
|
935
|
+
}
|
|
370
936
|
function buildCodexExecCommand(opts) {
|
|
371
937
|
const args = [
|
|
372
938
|
"exec",
|