doer-agent 0.4.1 → 0.4.3

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,120 @@
1
+ import { AckPolicy, connect, DeliverPolicy, JSONCodec, RetentionPolicy, StorageType, } from "nats";
2
+ export function normalizeNatsServers(value) {
3
+ if (!Array.isArray(value)) {
4
+ return [];
5
+ }
6
+ return value.filter((item) => typeof item === "string").map((v) => v.trim()).filter((v) => v.length > 0);
7
+ }
8
+ export function normalizeNatsToken(value) {
9
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
10
+ return null;
11
+ }
12
+ const auth = value;
13
+ const token = typeof auth.token === "string" ? auth.token.trim() : "";
14
+ return token.length > 0 ? token : null;
15
+ }
16
+ function formatNatsStatusData(value) {
17
+ if (value === null || value === undefined) {
18
+ return "null";
19
+ }
20
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
21
+ return String(value);
22
+ }
23
+ try {
24
+ return JSON.stringify(value);
25
+ }
26
+ catch {
27
+ return String(value);
28
+ }
29
+ }
30
+ async function ensureJetStreamInfra(args) {
31
+ const streamInfo = await args.jsm.streams.info(args.stream).catch(() => null);
32
+ if (!streamInfo) {
33
+ await args.jsm.streams.add({
34
+ name: args.stream,
35
+ subjects: [args.subject],
36
+ storage: StorageType.File,
37
+ retention: RetentionPolicy.Limits,
38
+ });
39
+ }
40
+ if (args.durable) {
41
+ const consumerInfo = await args.jsm.consumers.info(args.stream, args.durable).catch(() => null);
42
+ if (!consumerInfo) {
43
+ await args.jsm.consumers.add(args.stream, {
44
+ durable_name: args.durable,
45
+ ack_policy: AckPolicy.Explicit,
46
+ deliver_policy: DeliverPolicy.All,
47
+ filter_subject: args.subject,
48
+ ack_wait: 30_000_000_000,
49
+ });
50
+ }
51
+ }
52
+ }
53
+ async function initJetStreamContext(args) {
54
+ const sanitized = args.sanitizeUserId(args.userId);
55
+ const stream = `DOER_AGENT_EVENTS_${sanitized}`;
56
+ const subject = `doer.agent.events.${sanitized}`;
57
+ const durable = `doer-agent-uploader-${sanitized}`;
58
+ const nc = await connect(args.token ? { servers: args.servers, token: args.token } : { servers: args.servers });
59
+ const jsm = await nc.jetstreamManager();
60
+ await ensureJetStreamInfra({ jsm, stream, subject, durable });
61
+ void (async () => {
62
+ try {
63
+ for await (const status of nc.status()) {
64
+ const statusType = typeof status.type === "string" ? status.type : "unknown";
65
+ if (statusType === "pingTimer") {
66
+ continue;
67
+ }
68
+ const statusData = formatNatsStatusData(status.data);
69
+ args.onInfraError("nats status type=" + statusType + " data=" + statusData);
70
+ }
71
+ }
72
+ catch (error) {
73
+ const message = error instanceof Error ? error.message : String(error);
74
+ args.onInfraError(`nats status loop ended: ${message}`);
75
+ }
76
+ })();
77
+ return {
78
+ nc,
79
+ js: nc.jetstream(),
80
+ jsm,
81
+ codec: JSONCodec(),
82
+ subject,
83
+ stream,
84
+ durable,
85
+ servers: args.servers,
86
+ };
87
+ }
88
+ export async function connectBootstrapWithRetry(args) {
89
+ let attempt = 0;
90
+ while (true) {
91
+ attempt += 1;
92
+ try {
93
+ const natsBootstrap = await args.postJson(`${args.serverBaseUrl}/api/agent/nats`, {
94
+ userId: args.userId,
95
+ agentToken: args.agentToken,
96
+ });
97
+ const bootstrapRecord = natsBootstrap;
98
+ const natsServers = normalizeNatsServers(bootstrapRecord.servers);
99
+ if (natsServers.length === 0) {
100
+ throw new Error("No NATS servers configured by server");
101
+ }
102
+ const natsToken = normalizeNatsToken(bootstrapRecord.auth);
103
+ const jetstream = await initJetStreamContext({
104
+ userId: args.userId,
105
+ servers: natsServers,
106
+ token: natsToken,
107
+ sanitizeUserId: args.sanitizeUserId,
108
+ onInfraError: args.onInfraError,
109
+ });
110
+ args.onInfraError(`bootstrap ok servers=${natsServers.length} eventStream=${jetstream.stream} eventSubject=${jetstream.subject}`);
111
+ return { natsBootstrap, jetstream };
112
+ }
113
+ catch (error) {
114
+ const message = error instanceof Error ? error.message : String(error);
115
+ const retryMs = Math.min(30_000, 1000 * Math.max(1, attempt));
116
+ args.onError(`bootstrap failed: ${message} (retry in ${Math.floor(retryMs / 1000)}s, attempt=${attempt})`);
117
+ await args.sleep(retryMs);
118
+ }
119
+ }
120
+ }
@@ -0,0 +1,39 @@
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
+ }
@@ -0,0 +1,67 @@
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
+ }
@@ -0,0 +1,93 @@
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
+ }
@@ -0,0 +1,229 @@
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
+ export async function resetRunsDir(workspaceRoot) {
76
+ const dir = await resolveRunsDir(workspaceRoot);
77
+ await rm(dir, { recursive: true, force: true }).catch(() => undefined);
78
+ await mkdir(dir, { recursive: true });
79
+ }
80
+ export async function persistRunTask(workspaceRoot, task) {
81
+ const dir = await resolveRunsDir(workspaceRoot);
82
+ const payload = {
83
+ runId: task.id,
84
+ agentId: task.agentId,
85
+ userId: task.userId,
86
+ processPid: task.processPid,
87
+ sessionId: task.sessionId,
88
+ sessionFilePath: task.sessionFilePath,
89
+ status: task.status,
90
+ cancelRequested: task.cancelRequested,
91
+ resultExitCode: task.resultExitCode,
92
+ resultSignal: task.resultSignal,
93
+ createdAt: task.createdAt,
94
+ updatedAt: task.updatedAt,
95
+ startedAt: task.startedAt,
96
+ finishedAt: task.finishedAt,
97
+ error: task.error,
98
+ };
99
+ await writeFile(path.join(dir, `${task.id}.json`), `${JSON.stringify(payload, null, 2)}\n`, "utf8");
100
+ }
101
+ export async function removeRunTask(workspaceRoot, runId) {
102
+ const dir = await resolveRunsDir(workspaceRoot);
103
+ await unlink(path.join(dir, `${runId}.json`)).catch(() => undefined);
104
+ }
105
+ export async function claimRunStartSlot(args) {
106
+ const lockPath = await resolveRunStartLockPath(args);
107
+ try {
108
+ const handle = await open(lockPath, "wx");
109
+ try {
110
+ const payload = {
111
+ runId: args.runId,
112
+ sessionId: typeof args.sessionId === "string" && args.sessionId.trim() ? args.sessionId.trim() : null,
113
+ pid: process.pid,
114
+ createdAt: args.formatTimestamp(),
115
+ };
116
+ await handle.writeFile(`${JSON.stringify(payload, null, 2)}\n`, "utf8");
117
+ }
118
+ finally {
119
+ await handle.close().catch(() => undefined);
120
+ }
121
+ }
122
+ catch (error) {
123
+ if (error?.code === "EEXIST") {
124
+ const lockContents = await readFile(lockPath, "utf8").catch(() => "");
125
+ const existingRunId = (() => {
126
+ try {
127
+ const parsed = JSON.parse(lockContents);
128
+ return typeof parsed.runId === "string" && parsed.runId.trim() ? parsed.runId.trim() : null;
129
+ }
130
+ catch {
131
+ return null;
132
+ }
133
+ })();
134
+ throw new Error(existingRunId ? `Another run is already active: ${existingRunId}` : "Another run is already active");
135
+ }
136
+ throw error;
137
+ }
138
+ }
139
+ export async function updateRunStartSlotSession(args) {
140
+ const nextSessionId = args.sessionId.trim();
141
+ if (!nextSessionId) {
142
+ return;
143
+ }
144
+ const previousSessionId = typeof args.previousSessionId === "string" && args.previousSessionId.trim() ? args.previousSessionId.trim() : null;
145
+ if (previousSessionId === nextSessionId) {
146
+ return;
147
+ }
148
+ const currentPath = await resolveRunStartLockPath({
149
+ workspaceRoot: args.workspaceRoot,
150
+ runId: args.runId,
151
+ sessionId: previousSessionId,
152
+ });
153
+ const nextPath = await resolveRunStartLockPath({
154
+ workspaceRoot: args.workspaceRoot,
155
+ runId: args.runId,
156
+ sessionId: nextSessionId,
157
+ });
158
+ if (currentPath === nextPath) {
159
+ return;
160
+ }
161
+ try {
162
+ await rename(currentPath, nextPath);
163
+ }
164
+ catch (error) {
165
+ const code = error?.code;
166
+ if (code === "ENOENT") {
167
+ return;
168
+ }
169
+ if (code === "EEXIST") {
170
+ throw new Error(`Another run is already active for session: ${nextSessionId}`);
171
+ }
172
+ throw error;
173
+ }
174
+ const payload = {
175
+ runId: args.runId,
176
+ sessionId: nextSessionId,
177
+ pid: process.pid,
178
+ createdAt: args.formatTimestamp(),
179
+ };
180
+ await writeFile(nextPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
181
+ }
182
+ export async function releaseRunStartSlot(args) {
183
+ const paths = new Set();
184
+ paths.add(await resolveRunStartLockPath({ workspaceRoot: args.workspaceRoot, runId: args.runId, sessionId: args.sessionId ?? null }));
185
+ paths.add(await resolveRunStartLockPath({ workspaceRoot: args.workspaceRoot, runId: args.runId, sessionId: null }));
186
+ for (const lockPath of paths) {
187
+ await unlink(lockPath).catch(() => undefined);
188
+ }
189
+ }
190
+ export function cloneRunTask(task, _sinceSeq) {
191
+ return { ...task };
192
+ }
193
+ export function publishImmediateRunEvent(args) {
194
+ args.nc.publish(args.buildRunEventsSubject(args.userId, args.task.agentId), runEventsCodec.encode(JSON.stringify(buildImmediateRunEvent(args.task, args.type ?? "run.changed"))));
195
+ }
196
+ export async function listPersistedRunTasks(workspaceRoot) {
197
+ const dir = await resolveRunsDir(workspaceRoot);
198
+ const names = await readdir(dir).catch(() => []);
199
+ const tasks = await Promise.all(names
200
+ .filter((name) => name.endsWith(".json"))
201
+ .map(async (name) => {
202
+ const raw = await readFile(path.join(dir, name), "utf8").catch(() => null);
203
+ if (!raw) {
204
+ return null;
205
+ }
206
+ try {
207
+ return normalizePersistedRunTask(JSON.parse(raw));
208
+ }
209
+ catch {
210
+ return null;
211
+ }
212
+ }));
213
+ return tasks.filter((task) => task !== null);
214
+ }
215
+ export async function getStoredRun(workspaceRoot, runId) {
216
+ const persisted = await readFile(path.join(await resolveRunsDir(workspaceRoot), `${runId}.json`), "utf8").catch(() => null);
217
+ if (persisted) {
218
+ try {
219
+ const parsed = normalizePersistedRunTask(JSON.parse(persisted));
220
+ if (parsed) {
221
+ return parsed;
222
+ }
223
+ }
224
+ catch {
225
+ // Ignore malformed persisted state.
226
+ }
227
+ }
228
+ return null;
229
+ }