doer-agent 0.5.9 → 0.6.1

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.
@@ -0,0 +1,117 @@
1
+ import { buildAgentSettingsEnvPatch, readAgentModelInstructions, resolveAgentModelInstructionsFilePath, } from "./agent-settings.js";
2
+ import { buildDaemonMcpConfigArgs } from "./agent-codex-cli.js";
3
+ import { CodexAppServerClient } from "./codex-app-server-client.js";
4
+ function toTomlStringLiteral(value) {
5
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
6
+ }
7
+ function buildConfigArg(key, tomlValue) {
8
+ return ["--config", `${key}=${tomlValue}`];
9
+ }
10
+ function buildFeatureArg(enabled, name) {
11
+ return [enabled ? "--enable" : "--disable", name];
12
+ }
13
+ async function buildCodexAppServerArgs(args) {
14
+ const configArgs = [
15
+ ...buildConfigArg("model", toTomlStringLiteral(args.settings.codex.model)),
16
+ ...buildConfigArg("personality", toTomlStringLiteral(args.settings.general.personality)),
17
+ ...buildConfigArg("approval_policy", toTomlStringLiteral("never")),
18
+ ...buildConfigArg("sandbox_mode", toTomlStringLiteral("danger-full-access")),
19
+ ];
20
+ const customInstructions = await readAgentModelInstructions(args.workspaceRoot);
21
+ if (customInstructions) {
22
+ configArgs.push(...buildConfigArg("model_instructions_file", toTomlStringLiteral(resolveAgentModelInstructionsFilePath(args.workspaceRoot))));
23
+ }
24
+ return [
25
+ "app-server",
26
+ ...configArgs,
27
+ ...buildDaemonMcpConfigArgs({
28
+ agentProjectDir: args.agentProjectDir,
29
+ workspaceRoot: args.workspaceRoot,
30
+ }),
31
+ ...buildFeatureArg(args.settings.codex.computerUseEnabled, "computer_use"),
32
+ ...buildFeatureArg(args.settings.codex.browserUseEnabled, "browser_use"),
33
+ "--listen",
34
+ "stdio://",
35
+ ];
36
+ }
37
+ async function buildCodexAppServerEnv(args) {
38
+ return {
39
+ ...process.env,
40
+ ...buildAgentSettingsEnvPatch(args.settings),
41
+ CODEX_HOME: args.resolveCodexHomePath(),
42
+ };
43
+ }
44
+ export function createCodexAppServerManager(args) {
45
+ let client = null;
46
+ let createPromise = null;
47
+ let generation = 0;
48
+ const createClient = async () => {
49
+ const settings = await args.readAgentSettingsConfig({ workspaceRoot: args.workspaceRoot });
50
+ const appServerArgs = await buildCodexAppServerArgs({
51
+ workspaceRoot: args.workspaceRoot,
52
+ agentProjectDir: args.agentProjectDir,
53
+ settings,
54
+ });
55
+ const env = await buildCodexAppServerEnv({
56
+ workspaceRoot: args.workspaceRoot,
57
+ resolveCodexHomePath: args.resolveCodexHomePath,
58
+ settings,
59
+ });
60
+ args.onLog?.(`starting codex app-server model=${settings.codex.model} personality=${settings.general.personality} computerUse=${settings.codex.computerUseEnabled} browserUse=${settings.codex.browserUseEnabled}`);
61
+ return new CodexAppServerClient({
62
+ cwd: args.workspaceRoot,
63
+ args: appServerArgs,
64
+ env,
65
+ onLog: args.onLog,
66
+ onNotification: args.onNotification,
67
+ });
68
+ };
69
+ const getClient = async () => {
70
+ if (client) {
71
+ return client;
72
+ }
73
+ if (!createPromise) {
74
+ createPromise = createClient();
75
+ }
76
+ const activeCreatePromise = createPromise;
77
+ const requestedGeneration = generation;
78
+ try {
79
+ const createdClient = await activeCreatePromise;
80
+ if (requestedGeneration !== generation) {
81
+ await createdClient.stop();
82
+ return await getClient();
83
+ }
84
+ client = createdClient;
85
+ return client;
86
+ }
87
+ finally {
88
+ if (createPromise === activeCreatePromise) {
89
+ createPromise = null;
90
+ }
91
+ }
92
+ };
93
+ return {
94
+ async request(method, params) {
95
+ const activeClient = await getClient();
96
+ return await activeClient.request(method, params);
97
+ },
98
+ async restart(reason) {
99
+ generation += 1;
100
+ const activeClient = client;
101
+ client = null;
102
+ createPromise = null;
103
+ if (!activeClient) {
104
+ args.onLog?.(`codex app-server restart requested before start reason=${reason}`);
105
+ return;
106
+ }
107
+ args.onLog?.(`restarting codex app-server reason=${reason}`);
108
+ await activeClient.stop();
109
+ },
110
+ async stop() {
111
+ const activeClient = client;
112
+ client = null;
113
+ createPromise = null;
114
+ await activeClient?.stop();
115
+ },
116
+ };
117
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doer-agent",
3
- "version": "0.5.9",
3
+ "version": "0.6.1",
4
4
  "description": "Reverse-polling agent runtime for doer",
5
5
  "type": "module",
6
6
  "main": "dist/agent.js",
@@ -26,14 +26,11 @@
26
26
  "dependencies": {
27
27
  "@modelcontextprotocol/sdk": "^1.27.1",
28
28
  "@openai/codex-sdk": "^0.128.0",
29
- "mysql2": "^3.22.2",
30
29
  "nats": "^2.29.3",
31
- "pg": "^8.16.3",
32
30
  "tar": "^7.5.13"
33
31
  },
34
32
  "devDependencies": {
35
33
  "@types/node": "^20",
36
- "@types/pg": "^8.15.5",
37
34
  "tsx": "^4.21.0",
38
35
  "typescript": "^5"
39
36
  }
@@ -1,39 +0,0 @@
1
- import path from "node:path";
2
- import { mkdir } from "node:fs/promises";
3
- export async function prepareCommandExecution(args) {
4
- const shellPath = args.resolveShellPath();
5
- const taskWorkspace = args.resolveTaskWorkspace(args.cwd);
6
- const codexHome = args.resolveCodexHomePath();
7
- await mkdir(codexHome, { recursive: true });
8
- const codexAuth = await args.prepareCodexAuthBundle(args.codexAuthBundle);
9
- const localAgentSettings = await args.readAgentSettingsConfig({ workspaceRoot: args.resolveWorkspaceRoot() });
10
- const baseTaskEnvPatch = {
11
- CODEX_HOME: codexHome,
12
- DOER_USER_ID: args.userId,
13
- DOER_AGENT_TASK_ID: args.taskId,
14
- ...args.buildAgentSettingsEnvPatch(localAgentSettings),
15
- ...args.runtimeEnvPatch,
16
- ...(codexAuth?.envPatch ?? {}),
17
- WORKSPACE: taskWorkspace,
18
- };
19
- const taskGitEnv = await args.prepareTaskGitEnv({
20
- cwd: taskWorkspace,
21
- baseEnvPatch: baseTaskEnvPatch,
22
- });
23
- const runtimeBinPath = path.join(args.agentProjectDir, "runtime/bin");
24
- const taskPath = [runtimeBinPath, process.env.PATH || ""].filter(Boolean).join(path.delimiter);
25
- return {
26
- shellPath,
27
- taskWorkspace,
28
- taskPath,
29
- env: {
30
- ...process.env,
31
- ...baseTaskEnvPatch,
32
- ...taskGitEnv.envPatch,
33
- PATH: taskPath,
34
- },
35
- taskGitMeta: taskGitEnv.meta ?? {},
36
- codexAuthMeta: codexAuth?.meta ?? { codexAuthSynced: false },
37
- codexAuthCleanup: codexAuth?.cleanup ?? (async () => { }),
38
- };
39
- }
@@ -1,67 +0,0 @@
1
- export function createPendingRunSessionTracker(args) {
2
- let closed = false;
3
- let timer = null;
4
- const stop = () => {
5
- closed = true;
6
- if (timer) {
7
- clearInterval(timer);
8
- timer = null;
9
- }
10
- };
11
- const poll = async () => {
12
- if (closed || args.task.sessionId) {
13
- stop();
14
- return;
15
- }
16
- const detected = await args.detectPendingRunSession().catch(() => null);
17
- if (!detected) {
18
- return;
19
- }
20
- await args.updateRunSessionMetadata(detected).catch(() => undefined);
21
- if (args.task.sessionId) {
22
- stop();
23
- }
24
- };
25
- const start = () => {
26
- if (closed || args.task.sessionId || timer) {
27
- return;
28
- }
29
- timer = setInterval(() => {
30
- void poll();
31
- }, args.pollIntervalMs ?? 1000);
32
- };
33
- return { poll, start, stop };
34
- }
35
- export function attachManagedRunProcessLifecycle(args) {
36
- args.child.once("error", (error) => {
37
- args.stopPendingSessionPoll();
38
- const message = error instanceof Error ? error.message : String(error);
39
- args.task.status = "failed";
40
- args.task.error = message;
41
- args.task.finishedAt = args.formatTimestamp();
42
- args.publishImmediateRunEvent({ nc: args.nc, userId: args.task.userId, task: args.task });
43
- args.publishImmediateRunEvent({ nc: args.nc, userId: args.task.userId, task: args.task, type: "run.finished" });
44
- void args.removeRunTask(args.task.id).catch(() => undefined);
45
- void args.releaseRunStartSlot({ runId: args.task.id, sessionId: args.task.sessionId }).catch(() => undefined);
46
- void args.codexAuthCleanup().catch(() => undefined);
47
- args.writeRunStatus(args.task.id, `failed error=${message}`);
48
- });
49
- args.child.once("close", async (code, signal) => {
50
- args.stopPendingSessionPoll();
51
- const latest = await args.getStoredRun(args.task.id).catch(() => null);
52
- if (latest?.cancelRequested) {
53
- args.task.cancelRequested = true;
54
- }
55
- args.task.resultExitCode = typeof code === "number" ? code : null;
56
- args.task.resultSignal = signal;
57
- args.task.finishedAt = args.formatTimestamp();
58
- args.task.status = args.task.cancelRequested ? "canceled" : (args.task.resultExitCode ?? 1) === 0 ? "completed" : "failed";
59
- args.task.error = args.task.status === "failed" ? `Command exited with code ${args.task.resultExitCode ?? "null"}` : null;
60
- args.publishImmediateRunEvent({ nc: args.nc, userId: args.task.userId, task: args.task });
61
- args.publishImmediateRunEvent({ nc: args.nc, userId: args.task.userId, task: args.task, type: "run.finished" });
62
- void args.removeRunTask(args.task.id).catch(() => undefined);
63
- void args.releaseRunStartSlot({ runId: args.task.id, sessionId: args.task.sessionId }).catch(() => undefined);
64
- void args.codexAuthCleanup().catch(() => undefined);
65
- args.writeRunStatus(args.task.id, `completed status=${args.task.status} exitCode=${args.task.resultExitCode ?? "null"} signal=${args.task.resultSignal ?? "null"}`);
66
- });
67
- }
@@ -1,93 +0,0 @@
1
- import { StringCodec } from "nats";
2
- const runRpcCodec = StringCodec();
3
- export function normalizeRunRpcRequest(args) {
4
- const requestId = typeof args.request.requestId === "string" ? args.request.requestId.trim() : "";
5
- if (!requestId) {
6
- throw new Error("missing requestId");
7
- }
8
- const requestAgentId = typeof args.request.agentId === "string" ? args.request.agentId.trim() : "";
9
- if (!requestAgentId || requestAgentId !== args.agentId) {
10
- throw new Error("agent id mismatch");
11
- }
12
- const actionRaw = typeof args.request.action === "string" ? args.request.action.trim() : "";
13
- const action = actionRaw === "cancel" || actionRaw === "get" || actionRaw === "list" ? actionRaw : "start";
14
- const responseSubject = typeof args.request.responseSubject === "string" ? args.request.responseSubject.trim() : "";
15
- if (!responseSubject) {
16
- throw new Error("missing responseSubject");
17
- }
18
- const runId = typeof args.request.runId === "string" && args.request.runId.trim() ? args.request.runId.trim() : null;
19
- const prompt = typeof args.request.prompt === "string" && args.request.prompt.trim() ? args.request.prompt.trim() : null;
20
- const imagePaths = args.normalizeImagePaths(args.request.imagePaths);
21
- const sessionId = typeof args.request.sessionId === "string" && args.request.sessionId.trim() ? args.request.sessionId.trim() : null;
22
- const model = args.normalizeModel(args.request.model);
23
- if (action === "start" && !prompt) {
24
- throw new Error("missing prompt");
25
- }
26
- if ((action === "get" || action === "cancel") && !runId) {
27
- throw new Error("missing runId");
28
- }
29
- const cwd = typeof args.request.cwd === "string" && args.request.cwd.trim() ? args.request.cwd.trim() : null;
30
- const sinceSeqRaw = Number(args.request.sinceSeq);
31
- const sinceSeq = Number.isInteger(sinceSeqRaw) && sinceSeqRaw >= 0 ? sinceSeqRaw : null;
32
- const limitRaw = Number(args.request.limit);
33
- const limit = Number.isFinite(limitRaw) ? Math.max(1, Math.min(Math.floor(limitRaw), 200)) : 50;
34
- return {
35
- requestId,
36
- action,
37
- runId,
38
- prompt,
39
- imagePaths,
40
- sessionId,
41
- model,
42
- cwd,
43
- responseSubject,
44
- sinceSeq,
45
- limit,
46
- runtimeEnvPatch: args.normalizeEnvPatch(args.request.runtimeEnvPatch),
47
- codexAuthBundle: args.normalizeCodexAuthBundle(args.request.codexAuth),
48
- };
49
- }
50
- export function publishRunRpcResponse(args) {
51
- args.nc.publish(args.responseSubject, runRpcCodec.encode(JSON.stringify(args.payload)));
52
- }
53
- export async function handleNonStartRunRpc(args) {
54
- const { request } = args;
55
- if (request.action === "list") {
56
- const merged = (await args.listPersistedRunTasks())
57
- .map((task) => args.cloneRunTask(task))
58
- .sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt))
59
- .slice(0, request.limit);
60
- publishRunRpcResponse({
61
- nc: args.nc,
62
- responseSubject: request.responseSubject,
63
- payload: { requestId: request.requestId, ok: true, tasks: merged },
64
- });
65
- return;
66
- }
67
- const stored = request.runId ? await args.getStoredRun(request.runId) : null;
68
- if (!stored || stored.agentId !== args.agentId || stored.userId !== args.userId) {
69
- throw new Error("Run not found");
70
- }
71
- if (request.action === "cancel") {
72
- if (stored.processPid === null) {
73
- throw new Error("Run pid not found");
74
- }
75
- stored.cancelRequested = true;
76
- stored.updatedAt = args.formatTimestamp();
77
- await args.persistRunTask(stored);
78
- args.publishImmediateRunEvent(stored);
79
- args.writeRunStatus(stored.id, `cancel requested pid=${stored.processPid}`);
80
- args.sendSignalToPid(stored.processPid, "SIGINT");
81
- publishRunRpcResponse({
82
- nc: args.nc,
83
- responseSubject: request.responseSubject,
84
- payload: { requestId: request.requestId, ok: true, task: args.cloneRunTask(stored) },
85
- });
86
- return;
87
- }
88
- publishRunRpcResponse({
89
- nc: args.nc,
90
- responseSubject: request.responseSubject,
91
- payload: { requestId: request.requestId, ok: true, task: args.cloneRunTask(stored, request.sinceSeq) },
92
- });
93
- }
@@ -1,287 +0,0 @@
1
- import { mkdir, open, readFile, readdir, rename, rm, unlink, writeFile } from "node:fs/promises";
2
- import path from "node:path";
3
- import { StringCodec } from "nats";
4
- const runEventsCodec = StringCodec();
5
- async function resolveRunsDir(workspaceRoot) {
6
- const dir = path.join(workspaceRoot, ".doer-agent", "runs");
7
- await mkdir(dir, { recursive: true });
8
- return dir;
9
- }
10
- function sanitizeRunLockSegment(value) {
11
- return value.trim().replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 160) || "lock";
12
- }
13
- async function resolveRunLocksDir(workspaceRoot) {
14
- const dir = path.join(await resolveRunsDir(workspaceRoot), "locks");
15
- await mkdir(dir, { recursive: true });
16
- return dir;
17
- }
18
- async function resolveRunStartLockPath(args) {
19
- const dir = await resolveRunLocksDir(args.workspaceRoot);
20
- if (typeof args.sessionId === "string" && args.sessionId.trim()) {
21
- return path.join(dir, `session__${sanitizeRunLockSegment(args.sessionId)}.lock`);
22
- }
23
- return path.join(dir, "pending_new_session.lock");
24
- }
25
- function normalizePersistedRunTask(value) {
26
- if (!value || typeof value !== "object") {
27
- return null;
28
- }
29
- const record = value;
30
- const id = typeof record.runId === "string" && record.runId.trim()
31
- ? record.runId.trim()
32
- : typeof record.id === "string" && record.id.trim()
33
- ? record.id.trim()
34
- : "";
35
- const userId = typeof record.userId === "string" ? record.userId : "";
36
- const agentId = typeof record.agentId === "string" ? record.agentId : "";
37
- const status = record.status;
38
- if (!id || !userId || !agentId || !["queued", "running", "completed", "failed", "canceled"].includes(String(status))) {
39
- return null;
40
- }
41
- return {
42
- id,
43
- userId,
44
- agentId,
45
- processPid: typeof record.processPid === "number" ? record.processPid : null,
46
- sessionId: typeof record.sessionId === "string" && record.sessionId.trim() ? record.sessionId.trim() : null,
47
- sessionFilePath: typeof record.sessionFilePath === "string" && record.sessionFilePath.trim() ? record.sessionFilePath.trim() : null,
48
- status: status,
49
- cancelRequested: Boolean(record.cancelRequested),
50
- resultExitCode: typeof record.resultExitCode === "number" ? record.resultExitCode : null,
51
- resultSignal: typeof record.resultSignal === "string" && record.resultSignal.trim() ? record.resultSignal.trim() : null,
52
- error: typeof record.error === "string" && record.error.trim() ? record.error : null,
53
- createdAt: typeof record.createdAt === "string" ? record.createdAt : "",
54
- updatedAt: typeof record.updatedAt === "string" ? record.updatedAt : "",
55
- startedAt: typeof record.startedAt === "string" && record.startedAt.trim() ? record.startedAt : null,
56
- finishedAt: typeof record.finishedAt === "string" && record.finishedAt.trim() ? record.finishedAt : null,
57
- };
58
- }
59
- function buildImmediateRunEvent(task, type) {
60
- return {
61
- type,
62
- agentId: task.agentId,
63
- sessionId: task.sessionId,
64
- filePath: task.sessionFilePath,
65
- runId: task.id,
66
- updatedAt: task.updatedAt,
67
- status: task.status,
68
- cancelRequested: task.cancelRequested,
69
- resultExitCode: task.resultExitCode,
70
- resultSignal: task.resultSignal,
71
- error: task.error,
72
- finishedAt: task.finishedAt,
73
- };
74
- }
75
- function isPidAlive(pid) {
76
- if (!Number.isInteger(pid) || (pid ?? 0) <= 0) {
77
- return false;
78
- }
79
- try {
80
- process.kill(pid, 0);
81
- return true;
82
- }
83
- catch (error) {
84
- const code = error?.code;
85
- return code === "EPERM";
86
- }
87
- }
88
- async function removePathIfStale(pathToRemove, pid) {
89
- if (isPidAlive(pid)) {
90
- return;
91
- }
92
- await rm(pathToRemove, { recursive: true, force: true }).catch(() => undefined);
93
- }
94
- export async function pruneStaleRunsDir(workspaceRoot) {
95
- const dir = await resolveRunsDir(workspaceRoot);
96
- const names = await readdir(dir).catch(() => []);
97
- for (const name of names) {
98
- const entryPath = path.join(dir, name);
99
- if (name === "locks") {
100
- const lockNames = await readdir(entryPath).catch(() => []);
101
- for (const lockName of lockNames) {
102
- const lockPath = path.join(entryPath, lockName);
103
- const contents = await readFile(lockPath, "utf8").catch(() => null);
104
- if (!contents) {
105
- await rm(lockPath, { recursive: true, force: true }).catch(() => undefined);
106
- continue;
107
- }
108
- let pid = null;
109
- try {
110
- const parsed = JSON.parse(contents);
111
- pid = typeof parsed.pid === "number" && Number.isInteger(parsed.pid) && parsed.pid > 0 ? parsed.pid : null;
112
- }
113
- catch {
114
- pid = null;
115
- }
116
- await removePathIfStale(lockPath, pid);
117
- }
118
- continue;
119
- }
120
- if (!name.endsWith(".json")) {
121
- continue;
122
- }
123
- const contents = await readFile(entryPath, "utf8").catch(() => null);
124
- if (!contents) {
125
- await rm(entryPath, { recursive: true, force: true }).catch(() => undefined);
126
- continue;
127
- }
128
- let task = null;
129
- try {
130
- task = normalizePersistedRunTask(JSON.parse(contents));
131
- }
132
- catch {
133
- task = null;
134
- }
135
- await removePathIfStale(entryPath, task?.processPid ?? null);
136
- }
137
- }
138
- export async function persistRunTask(workspaceRoot, task) {
139
- const dir = await resolveRunsDir(workspaceRoot);
140
- const payload = {
141
- runId: task.id,
142
- agentId: task.agentId,
143
- userId: task.userId,
144
- processPid: task.processPid,
145
- sessionId: task.sessionId,
146
- sessionFilePath: task.sessionFilePath,
147
- status: task.status,
148
- cancelRequested: task.cancelRequested,
149
- resultExitCode: task.resultExitCode,
150
- resultSignal: task.resultSignal,
151
- createdAt: task.createdAt,
152
- updatedAt: task.updatedAt,
153
- startedAt: task.startedAt,
154
- finishedAt: task.finishedAt,
155
- error: task.error,
156
- };
157
- await writeFile(path.join(dir, `${task.id}.json`), `${JSON.stringify(payload, null, 2)}\n`, "utf8");
158
- }
159
- export async function removeRunTask(workspaceRoot, runId) {
160
- const dir = await resolveRunsDir(workspaceRoot);
161
- await unlink(path.join(dir, `${runId}.json`)).catch(() => undefined);
162
- }
163
- export async function claimRunStartSlot(args) {
164
- const lockPath = await resolveRunStartLockPath(args);
165
- try {
166
- const handle = await open(lockPath, "wx");
167
- try {
168
- const payload = {
169
- runId: args.runId,
170
- sessionId: typeof args.sessionId === "string" && args.sessionId.trim() ? args.sessionId.trim() : null,
171
- pid: process.pid,
172
- createdAt: args.formatTimestamp(),
173
- };
174
- await handle.writeFile(`${JSON.stringify(payload, null, 2)}\n`, "utf8");
175
- }
176
- finally {
177
- await handle.close().catch(() => undefined);
178
- }
179
- }
180
- catch (error) {
181
- if (error?.code === "EEXIST") {
182
- const lockContents = await readFile(lockPath, "utf8").catch(() => "");
183
- const existingRunId = (() => {
184
- try {
185
- const parsed = JSON.parse(lockContents);
186
- return typeof parsed.runId === "string" && parsed.runId.trim() ? parsed.runId.trim() : null;
187
- }
188
- catch {
189
- return null;
190
- }
191
- })();
192
- throw new Error(existingRunId ? `Another run is already active: ${existingRunId}` : "Another run is already active");
193
- }
194
- throw error;
195
- }
196
- }
197
- export async function updateRunStartSlotSession(args) {
198
- const nextSessionId = args.sessionId.trim();
199
- if (!nextSessionId) {
200
- return;
201
- }
202
- const previousSessionId = typeof args.previousSessionId === "string" && args.previousSessionId.trim() ? args.previousSessionId.trim() : null;
203
- if (previousSessionId === nextSessionId) {
204
- return;
205
- }
206
- const currentPath = await resolveRunStartLockPath({
207
- workspaceRoot: args.workspaceRoot,
208
- runId: args.runId,
209
- sessionId: previousSessionId,
210
- });
211
- const nextPath = await resolveRunStartLockPath({
212
- workspaceRoot: args.workspaceRoot,
213
- runId: args.runId,
214
- sessionId: nextSessionId,
215
- });
216
- if (currentPath === nextPath) {
217
- return;
218
- }
219
- try {
220
- await rename(currentPath, nextPath);
221
- }
222
- catch (error) {
223
- const code = error?.code;
224
- if (code === "ENOENT") {
225
- return;
226
- }
227
- if (code === "EEXIST") {
228
- throw new Error(`Another run is already active for session: ${nextSessionId}`);
229
- }
230
- throw error;
231
- }
232
- const payload = {
233
- runId: args.runId,
234
- sessionId: nextSessionId,
235
- pid: process.pid,
236
- createdAt: args.formatTimestamp(),
237
- };
238
- await writeFile(nextPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
239
- }
240
- export async function releaseRunStartSlot(args) {
241
- const paths = new Set();
242
- paths.add(await resolveRunStartLockPath({ workspaceRoot: args.workspaceRoot, runId: args.runId, sessionId: args.sessionId ?? null }));
243
- paths.add(await resolveRunStartLockPath({ workspaceRoot: args.workspaceRoot, runId: args.runId, sessionId: null }));
244
- for (const lockPath of paths) {
245
- await unlink(lockPath).catch(() => undefined);
246
- }
247
- }
248
- export function cloneRunTask(task, _sinceSeq) {
249
- return { ...task };
250
- }
251
- export function publishImmediateRunEvent(args) {
252
- args.nc.publish(args.buildRunEventsSubject(args.userId, args.task.agentId), runEventsCodec.encode(JSON.stringify(buildImmediateRunEvent(args.task, args.type ?? "run.changed"))));
253
- }
254
- export async function listPersistedRunTasks(workspaceRoot) {
255
- const dir = await resolveRunsDir(workspaceRoot);
256
- const names = await readdir(dir).catch(() => []);
257
- const tasks = await Promise.all(names
258
- .filter((name) => name.endsWith(".json"))
259
- .map(async (name) => {
260
- const raw = await readFile(path.join(dir, name), "utf8").catch(() => null);
261
- if (!raw) {
262
- return null;
263
- }
264
- try {
265
- return normalizePersistedRunTask(JSON.parse(raw));
266
- }
267
- catch {
268
- return null;
269
- }
270
- }));
271
- return tasks.filter((task) => task !== null);
272
- }
273
- export async function getStoredRun(workspaceRoot, runId) {
274
- const persisted = await readFile(path.join(await resolveRunsDir(workspaceRoot), `${runId}.json`), "utf8").catch(() => null);
275
- if (persisted) {
276
- try {
277
- const parsed = normalizePersistedRunTask(JSON.parse(persisted));
278
- if (parsed) {
279
- return parsed;
280
- }
281
- }
282
- catch {
283
- // Ignore malformed persisted state.
284
- }
285
- }
286
- return null;
287
- }
@@ -1,38 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import os from "node:os";
4
- import path from "node:path";
5
- import { mkdtemp, rm, writeFile } from "node:fs/promises";
6
- import { filterValidRunImagePaths } from "./agent-runtime-utils.js";
7
- const invalidTinyPngBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aK1cAAAAASUVORK5CYII=";
8
- function buildValidTinyPng() {
9
- const bytes = Buffer.from(invalidTinyPngBase64, "base64");
10
- bytes.writeUInt32BE(0xefa2a75b, 0x34);
11
- return bytes;
12
- }
13
- test("filterValidRunImagePaths drops PNGs with CRC mismatches", async () => {
14
- const workspaceRoot = await mkdtemp(path.join(os.tmpdir(), "doer-agent-image-filter-"));
15
- try {
16
- const invalidPngPath = path.join(workspaceRoot, "bad.png");
17
- const validPngPath = path.join(workspaceRoot, "good.png");
18
- const validJpgPath = path.join(workspaceRoot, "photo.jpg");
19
- await writeFile(invalidPngPath, Buffer.from(invalidTinyPngBase64, "base64"));
20
- await writeFile(validPngPath, buildValidTinyPng());
21
- await writeFile(validJpgPath, Buffer.from([0xff, 0xd8, 0xff, 0xd9]));
22
- const invalid = [];
23
- const result = await filterValidRunImagePaths({
24
- workspaceRoot,
25
- imagePaths: ["bad.png", "good.png", "photo.jpg"],
26
- onInvalidImage: (imagePath, reason) => {
27
- invalid.push({ imagePath, reason });
28
- },
29
- });
30
- assert.deepEqual(result, ["good.png", "photo.jpg"]);
31
- assert.equal(invalid.length, 1);
32
- assert.equal(invalid[0]?.imagePath, "bad.png");
33
- assert.match(invalid[0]?.reason ?? "", /CRC mismatch/i);
34
- }
35
- finally {
36
- await rm(workspaceRoot, { recursive: true, force: true });
37
- }
38
- });