doer-agent 0.4.2 → 0.4.4
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/dist/agent-codex-auth-rpc.js +322 -0
- package/dist/agent-codex-cli.js +210 -0
- package/dist/agent-fs-rpc.js +405 -0
- package/dist/agent-git-rpc.js +299 -0
- package/dist/agent-jetstream.js +120 -0
- package/dist/agent-run-execution.js +39 -0
- package/dist/agent-run-lifecycle.js +67 -0
- package/dist/agent-run-rpc.js +93 -0
- package/dist/agent-run-state.js +229 -0
- package/dist/agent-runtime-env.js +147 -0
- package/dist/agent-runtime-io.js +112 -0
- package/dist/agent-runtime-utils.js +253 -0
- package/dist/agent-session-loop.js +53 -0
- package/dist/agent-session-rpc.js +867 -0
- package/dist/agent-settings-rpc.js +75 -0
- package/dist/agent-settings.js +397 -0
- package/dist/agent-skill-rpc.js +164 -0
- package/dist/agent-task-execution.js +275 -0
- package/dist/agent.js +376 -4275
- package/package.json +1 -1
package/dist/agent.js
CHANGED
|
@@ -1,10 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { existsSync, statSync, watch } from "node:fs";
|
|
3
|
-
import { chmod, mkdir, open, readFile, readdir, realpath, rename, rm, rmdir, stat, unlink, writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
4
2
|
import path from "node:path";
|
|
5
3
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
4
|
+
import { StringCodec } from "nats";
|
|
5
|
+
import { buildAgentSettingsEnvPatch, readAgentModelInstructions, readAgentSettingsConfig, resolveAgentModelInstructionsFilePath, } from "./agent-settings.js";
|
|
6
|
+
import { handleFsRpcMessage } from "./agent-fs-rpc.js";
|
|
7
|
+
import { handleGitRpcMessage } from "./agent-git-rpc.js";
|
|
8
|
+
import { subscribeToCodexAuthRpc } from "./agent-codex-auth-rpc.js";
|
|
9
|
+
import { buildManagedCodexArgs, createLocalCodexCliTools, normalizeCodexModel, normalizeShellRpcCodexAuthBundle, spawnManagedCodexCommand, } from "./agent-codex-cli.js";
|
|
10
|
+
import { connectBootstrapWithRetry } from "./agent-jetstream.js";
|
|
11
|
+
import { prepareCommandExecution } from "./agent-run-execution.js";
|
|
12
|
+
import { attachManagedRunProcessLifecycle, createPendingRunSessionTracker } from "./agent-run-lifecycle.js";
|
|
13
|
+
import { claimRunStartSlot, cloneRunTask, getStoredRun, listPersistedRunTasks, persistRunTask, publishImmediateRunEvent, releaseRunStartSlot, resetRunsDir, removeRunTask, updateRunStartSlotSession, } from "./agent-run-state.js";
|
|
14
|
+
import { runConnectedAgentSession } from "./agent-session-loop.js";
|
|
15
|
+
import { subscribeToSkillRpc } from "./agent-skill-rpc.js";
|
|
16
|
+
import { prepareCodexAuthBundle, sendSignalToPid, sendSignalToTaskProcess, } from "./agent-task-execution.js";
|
|
17
|
+
import { collectSessionJsonlFiles, detectPendingRunSession, findSessionFilePathBySessionId, stopAllSessionWatchers, subscribeToSessionRpc, } from "./agent-session-rpc.js";
|
|
18
|
+
import { handleNonStartRunRpc, normalizeRunRpcRequest, publishRunRpcResponse, } from "./agent-run-rpc.js";
|
|
19
|
+
import { buildAgentCodexAuthRpcSubject, buildAgentFsRpcSubject, buildAgentGitRpcSubject, buildAgentRunEventsSubject, buildAgentRunRpcSubject, buildAgentSessionRpcSubject, buildAgentSettingsRpcSubject, buildAgentSkillRpcSubject, formatLocalTimestamp, normalizeEnvPatch, normalizeRunImagePaths, parseArgs, resolveAgentVersion, resolveArgOrEnv, resolveContainerReachableServerBaseUrl, sanitizeUserId, sleep, writeRunStatus, writeRunStream, } from "./agent-runtime-utils.js";
|
|
20
|
+
import { createRuntimeEnvHelpers } from "./agent-runtime-env.js";
|
|
21
|
+
import { createEventPersistenceHelpers, heartbeatAgentSession, postJson, } from "./agent-runtime-io.js";
|
|
22
|
+
import { handleSettingsRpcMessage } from "./agent-settings-rpc.js";
|
|
8
23
|
const DEFAULT_SERVER_BASE_URL = "https://doer.cranix.net";
|
|
9
24
|
const AGENT_MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
10
25
|
const AGENT_PROJECT_DIR = path.join(AGENT_MODULE_DIR, "..");
|
|
@@ -13,308 +28,10 @@ const HEARTBEAT_INTERVAL_MS = 5_000;
|
|
|
13
28
|
const HEARTBEAT_FAILURE_THRESHOLD = 3;
|
|
14
29
|
let activeTaskLogContext = null;
|
|
15
30
|
let workspaceRootOverride = null;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const sessionRpcCodec = StringCodec();
|
|
19
|
-
const codexAuthRpcCodec = StringCodec();
|
|
20
|
-
const settingsRpcCodec = StringCodec();
|
|
21
|
-
const gitRpcCodec = StringCodec();
|
|
22
|
-
const skillRpcCodec = StringCodec();
|
|
23
|
-
const runEventsCodec = StringCodec();
|
|
24
|
-
const activeSessionWatchers = new Map();
|
|
25
|
-
const sessionLineIndexCache = new Map();
|
|
26
|
-
const ANSI_RE = /\u001b\[[0-9;]*m/g;
|
|
27
|
-
let pendingCodexDeviceAuth = null;
|
|
28
|
-
function sanitizeUserId(userId) {
|
|
29
|
-
const normalized = userId.trim().replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
30
|
-
return normalized.length > 0 ? normalized : "anonymous";
|
|
31
|
-
}
|
|
32
|
-
function buildAgentRunRpcSubject(userId, agentId) {
|
|
33
|
-
return `doer.agent.run.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
|
|
34
|
-
}
|
|
35
|
-
function buildAgentRunEventsSubject(userId, agentId) {
|
|
36
|
-
return `doer.agent.run.events.${sanitizeUserId(userId)}.${agentId.trim()}`;
|
|
37
|
-
}
|
|
38
|
-
function buildAgentSessionRpcSubject(userId, agentId) {
|
|
39
|
-
return `doer.agent.session.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
|
|
40
|
-
}
|
|
41
|
-
function buildAgentCodexAuthRpcSubject(userId, agentId) {
|
|
42
|
-
return `doer.agent.codex.auth.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
|
|
43
|
-
}
|
|
44
|
-
function buildAgentSettingsRpcSubject(userId, agentId) {
|
|
45
|
-
return `doer.agent.settings.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
|
|
46
|
-
}
|
|
47
|
-
function buildAgentGitRpcSubject(userId, agentId) {
|
|
48
|
-
return `doer.agent.git.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
|
|
49
|
-
}
|
|
50
|
-
function buildAgentSkillRpcSubject(userId, agentId) {
|
|
51
|
-
return `doer.agent.skill.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
|
|
52
|
-
}
|
|
53
|
-
function normalizeNatsServers(value) {
|
|
54
|
-
if (!Array.isArray(value)) {
|
|
55
|
-
return [];
|
|
56
|
-
}
|
|
57
|
-
return value.filter((item) => typeof item === "string").map((v) => v.trim()).filter((v) => v.length > 0);
|
|
58
|
-
}
|
|
59
|
-
function parseBootstrapTaskConfig(value) {
|
|
60
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
const task = value;
|
|
64
|
-
const stream = typeof task.stream === "string" ? task.stream.trim() : "";
|
|
65
|
-
const subject = typeof task.subject === "string" ? task.subject.trim() : "";
|
|
66
|
-
const durable = typeof task.durable === "string" ? task.durable.trim() : "";
|
|
67
|
-
if (!stream || !subject || !durable) {
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
return { stream, subject, durable };
|
|
71
|
-
}
|
|
72
|
-
function normalizeTaskIds(value) {
|
|
73
|
-
if (!Array.isArray(value)) {
|
|
74
|
-
return [];
|
|
75
|
-
}
|
|
76
|
-
const out = [];
|
|
77
|
-
for (const item of value) {
|
|
78
|
-
if (typeof item !== "string") {
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
const id = item.trim();
|
|
82
|
-
if (!id) {
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
out.push(id);
|
|
86
|
-
}
|
|
87
|
-
return out;
|
|
88
|
-
}
|
|
89
|
-
function normalizeNatsToken(value) {
|
|
90
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
const auth = value;
|
|
94
|
-
const token = typeof auth.token === "string" ? auth.token.trim() : "";
|
|
95
|
-
return token.length > 0 ? token : null;
|
|
96
|
-
}
|
|
97
|
-
async function ensureJetStreamInfra(args) {
|
|
98
|
-
const streamInfo = await args.jsm.streams.info(args.stream).catch(() => null);
|
|
99
|
-
if (!streamInfo) {
|
|
100
|
-
await args.jsm.streams.add({
|
|
101
|
-
name: args.stream,
|
|
102
|
-
subjects: [args.subject],
|
|
103
|
-
storage: StorageType.File,
|
|
104
|
-
retention: RetentionPolicy.Limits,
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
if (args.durable) {
|
|
108
|
-
const consumerInfo = await args.jsm.consumers.info(args.stream, args.durable).catch(() => null);
|
|
109
|
-
if (!consumerInfo) {
|
|
110
|
-
await args.jsm.consumers.add(args.stream, {
|
|
111
|
-
durable_name: args.durable,
|
|
112
|
-
ack_policy: AckPolicy.Explicit,
|
|
113
|
-
deliver_policy: DeliverPolicy.All,
|
|
114
|
-
filter_subject: args.subject,
|
|
115
|
-
ack_wait: 30_000_000_000,
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
async function initJetStreamContext(args) {
|
|
121
|
-
const sanitized = sanitizeUserId(args.userId);
|
|
122
|
-
const stream = `DOER_AGENT_EVENTS_${sanitized}`;
|
|
123
|
-
const subject = `doer.agent.events.${sanitized}`;
|
|
124
|
-
const durable = `doer-agent-uploader-${sanitized}`;
|
|
125
|
-
const nc = await connect(args.token ? { servers: args.servers, token: args.token } : { servers: args.servers });
|
|
126
|
-
const jsm = await nc.jetstreamManager();
|
|
127
|
-
await ensureJetStreamInfra({ jsm, stream, subject, durable });
|
|
128
|
-
void (async () => {
|
|
129
|
-
try {
|
|
130
|
-
for await (const status of nc.status()) {
|
|
131
|
-
const statusType = typeof status.type === "string" ? status.type : "unknown";
|
|
132
|
-
if (statusType === "pingTimer") {
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
const statusData = formatNatsStatusData(status.data);
|
|
136
|
-
writeAgentInfraError("nats status type=" + statusType + " data=" + statusData);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
catch (error) {
|
|
140
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
141
|
-
writeAgentInfraError(`nats status loop ended: ${message}`);
|
|
142
|
-
}
|
|
143
|
-
})();
|
|
144
|
-
return {
|
|
145
|
-
nc,
|
|
146
|
-
js: nc.jetstream(),
|
|
147
|
-
jsm,
|
|
148
|
-
codec: JSONCodec(),
|
|
149
|
-
subject,
|
|
150
|
-
stream,
|
|
151
|
-
durable,
|
|
152
|
-
servers: args.servers,
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
function resolveCodexHomePath() {
|
|
156
|
-
const workspaceRoot = workspaceRootOverride ?? (process.env.WORKSPACE?.trim() || process.cwd());
|
|
157
|
-
return path.join(workspaceRoot, ".codex");
|
|
158
|
-
}
|
|
159
|
-
function parseEnvBoolean(value) {
|
|
160
|
-
return value?.trim().toLowerCase() === "true";
|
|
161
|
-
}
|
|
162
|
-
function parseEnvInteger(value, fallback) {
|
|
163
|
-
const normalized = value?.trim();
|
|
164
|
-
if (!normalized) {
|
|
165
|
-
return fallback;
|
|
166
|
-
}
|
|
167
|
-
const parsed = Number.parseInt(normalized, 10);
|
|
168
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
169
|
-
}
|
|
170
|
-
function resolveContainerReachableServerBaseUrl(serverBaseUrl) {
|
|
171
|
-
return serverBaseUrl;
|
|
172
|
-
}
|
|
173
|
-
async function resolveAgentVersion() {
|
|
174
|
-
const raw = await readFile(AGENT_PACKAGE_JSON_PATH, "utf8").catch(() => "");
|
|
175
|
-
if (!raw) {
|
|
176
|
-
return "unknown";
|
|
177
|
-
}
|
|
178
|
-
try {
|
|
179
|
-
const parsed = JSON.parse(raw);
|
|
180
|
-
return typeof parsed.version === "string" && parsed.version.trim() ? parsed.version.trim() : "unknown";
|
|
181
|
-
}
|
|
182
|
-
catch {
|
|
183
|
-
return "unknown";
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
function pickFirstNonEmpty(values) {
|
|
187
|
-
for (const value of values) {
|
|
188
|
-
if (typeof value !== "string") {
|
|
189
|
-
continue;
|
|
190
|
-
}
|
|
191
|
-
const normalized = value.trim();
|
|
192
|
-
if (normalized) {
|
|
193
|
-
return normalized;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
return "";
|
|
197
|
-
}
|
|
198
|
-
async function ensureGitAskpassScript() {
|
|
199
|
-
const binDir = path.join(AGENT_PROJECT_DIR, "runtime/bin");
|
|
200
|
-
const scriptPath = path.join(binDir, "git-askpass.sh");
|
|
201
|
-
const scriptBody = `#!/bin/sh
|
|
202
|
-
case "$1" in
|
|
203
|
-
*Username*) printf "%s\\n" "x-access-token" ;;
|
|
204
|
-
*Password*) printf "%s\\n" "\${GITHUB_TOKEN:-\${GH_TOKEN:-}}" ;;
|
|
205
|
-
*) printf "\\n" ;;
|
|
206
|
-
esac
|
|
207
|
-
`;
|
|
208
|
-
await mkdir(binDir, { recursive: true });
|
|
209
|
-
await writeFile(scriptPath, scriptBody, "utf8");
|
|
210
|
-
await chmod(scriptPath, 0o700).catch(() => undefined);
|
|
211
|
-
return scriptPath;
|
|
212
|
-
}
|
|
213
|
-
function applyGitIdentityIfPossible(args) {
|
|
214
|
-
if (!args.cwd) {
|
|
215
|
-
return false;
|
|
216
|
-
}
|
|
217
|
-
const inRepo = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
218
|
-
cwd: args.cwd,
|
|
219
|
-
stdio: "ignore",
|
|
220
|
-
});
|
|
221
|
-
if (inRepo.status !== 0) {
|
|
222
|
-
return false;
|
|
223
|
-
}
|
|
224
|
-
const setName = spawnSync("git", ["config", "--local", "user.name", args.userName], {
|
|
225
|
-
cwd: args.cwd,
|
|
226
|
-
stdio: "ignore",
|
|
227
|
-
});
|
|
228
|
-
if (setName.status !== 0) {
|
|
229
|
-
return false;
|
|
230
|
-
}
|
|
231
|
-
const setEmail = spawnSync("git", ["config", "--local", "user.email", args.userEmail], {
|
|
232
|
-
cwd: args.cwd,
|
|
233
|
-
stdio: "ignore",
|
|
234
|
-
});
|
|
235
|
-
return setEmail.status === 0;
|
|
236
|
-
}
|
|
237
|
-
async function prepareTaskGitEnv(args) {
|
|
238
|
-
const envPatch = {
|
|
239
|
-
GIT_TERMINAL_PROMPT: "0",
|
|
240
|
-
GCM_INTERACTIVE: "Never",
|
|
241
|
-
};
|
|
242
|
-
const githubToken = pickFirstNonEmpty([
|
|
243
|
-
args.baseEnvPatch.GITHUB_TOKEN,
|
|
244
|
-
args.baseEnvPatch.GH_TOKEN,
|
|
245
|
-
process.env.GITHUB_TOKEN,
|
|
246
|
-
process.env.GH_TOKEN,
|
|
247
|
-
]);
|
|
248
|
-
if (githubToken) {
|
|
249
|
-
envPatch.GITHUB_TOKEN = githubToken;
|
|
250
|
-
envPatch.GH_TOKEN = githubToken;
|
|
251
|
-
envPatch.GIT_ASKPASS_REQUIRE = "force";
|
|
252
|
-
envPatch.GIT_ASKPASS = await ensureGitAskpassScript();
|
|
253
|
-
}
|
|
254
|
-
const userName = pickFirstNonEmpty([
|
|
255
|
-
args.baseEnvPatch.DOER_GIT_USER_NAME,
|
|
256
|
-
args.baseEnvPatch.GIT_USER_NAME,
|
|
257
|
-
args.baseEnvPatch.GIT_AUTHOR_NAME,
|
|
258
|
-
args.baseEnvPatch.GIT_COMMITTER_NAME,
|
|
259
|
-
]);
|
|
260
|
-
const userEmail = pickFirstNonEmpty([
|
|
261
|
-
args.baseEnvPatch.DOER_GIT_USER_EMAIL,
|
|
262
|
-
args.baseEnvPatch.GIT_USER_EMAIL,
|
|
263
|
-
args.baseEnvPatch.GIT_AUTHOR_EMAIL,
|
|
264
|
-
args.baseEnvPatch.GIT_COMMITTER_EMAIL,
|
|
265
|
-
]);
|
|
266
|
-
const gitIdentityApplied = userName && userEmail
|
|
267
|
-
? applyGitIdentityIfPossible({
|
|
268
|
-
cwd: args.cwd,
|
|
269
|
-
userName,
|
|
270
|
-
userEmail,
|
|
271
|
-
})
|
|
272
|
-
: false;
|
|
273
|
-
return {
|
|
274
|
-
envPatch,
|
|
275
|
-
meta: {
|
|
276
|
-
gitAskpassEnabled: Boolean(envPatch.GIT_ASKPASS),
|
|
277
|
-
gitIdentityApplied,
|
|
278
|
-
gitIdentityProvided: Boolean(userName && userEmail),
|
|
279
|
-
},
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
function normalizeEnvPatch(value) {
|
|
283
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
284
|
-
return {};
|
|
285
|
-
}
|
|
286
|
-
const out = {};
|
|
287
|
-
for (const [key, raw] of Object.entries(value)) {
|
|
288
|
-
if (typeof raw !== "string") {
|
|
289
|
-
continue;
|
|
290
|
-
}
|
|
291
|
-
const normalizedKey = key.trim();
|
|
292
|
-
if (!normalizedKey) {
|
|
293
|
-
continue;
|
|
294
|
-
}
|
|
295
|
-
out[normalizedKey] = raw;
|
|
296
|
-
}
|
|
297
|
-
return out;
|
|
298
|
-
}
|
|
299
|
-
function normalizeRunImagePaths(value) {
|
|
300
|
-
if (!Array.isArray(value)) {
|
|
301
|
-
return [];
|
|
302
|
-
}
|
|
303
|
-
const seen = new Set();
|
|
304
|
-
const out = [];
|
|
305
|
-
for (const item of value) {
|
|
306
|
-
if (typeof item !== "string") {
|
|
307
|
-
continue;
|
|
308
|
-
}
|
|
309
|
-
const normalized = item.trim();
|
|
310
|
-
if (!normalized || seen.has(normalized)) {
|
|
311
|
-
continue;
|
|
312
|
-
}
|
|
313
|
-
seen.add(normalized);
|
|
314
|
-
out.push(normalized);
|
|
315
|
-
}
|
|
316
|
-
return out;
|
|
31
|
+
function resolveWorkspaceRoot() {
|
|
32
|
+
return workspaceRootOverride ?? (process.env.WORKSPACE?.trim() || process.cwd());
|
|
317
33
|
}
|
|
34
|
+
const runRpcCodec = StringCodec();
|
|
318
35
|
async function prepareTaskRuntimeConfig(args) {
|
|
319
36
|
const bundle = await postJson(`${args.serverBaseUrl}/api/agent/tasks/${encodeURIComponent(args.taskId)}/runtime-config`, {
|
|
320
37
|
userId: args.userId,
|
|
@@ -339,22 +56,13 @@ async function prepareTaskRuntimeConfig(args) {
|
|
|
339
56
|
},
|
|
340
57
|
};
|
|
341
58
|
}
|
|
342
|
-
function fatalExit(message, error) {
|
|
343
|
-
const detail = error instanceof Error ? error.message : typeof error === "string" ? error : error ? String(error) : "";
|
|
344
|
-
const full = detail ? `${message}: ${detail}` : message;
|
|
345
|
-
writeAgentError(`fatal: ${full}`);
|
|
346
|
-
process.exit(1);
|
|
347
|
-
}
|
|
348
|
-
function sleep(ms) {
|
|
349
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
350
|
-
}
|
|
351
59
|
function writeAgentInfo(message) {
|
|
352
60
|
process.stdout.write(`[doer-agent] ${message}\n`);
|
|
353
|
-
emitAgentMetaLog("info", message);
|
|
61
|
+
eventPersistenceHelpers.emitAgentMetaLog("info", message);
|
|
354
62
|
}
|
|
355
63
|
function writeAgentError(message) {
|
|
356
64
|
process.stderr.write(`[doer-agent] ${message}\n`);
|
|
357
|
-
emitAgentMetaLog("error", message);
|
|
65
|
+
eventPersistenceHelpers.emitAgentMetaLog("error", message);
|
|
358
66
|
}
|
|
359
67
|
function writeAgentInfraError(message) {
|
|
360
68
|
try {
|
|
@@ -364,3419 +72,294 @@ function writeAgentInfraError(message) {
|
|
|
364
72
|
// Keep heartbeat/connectivity failures non-fatal.
|
|
365
73
|
}
|
|
366
74
|
}
|
|
367
|
-
function
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
}
|
|
374
|
-
try {
|
|
375
|
-
return JSON.stringify(value);
|
|
376
|
-
}
|
|
377
|
-
catch {
|
|
378
|
-
return String(value);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
function writeTaskStream(taskId, stream, chunk) {
|
|
382
|
-
const target = stream === "stdout" ? process.stdout : process.stderr;
|
|
383
|
-
const lines = chunk.replace(/\r/g, "\n").split("\n");
|
|
384
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
385
|
-
const line = lines[i];
|
|
386
|
-
if (line.length === 0 && i === lines.length - 1) {
|
|
387
|
-
continue;
|
|
388
|
-
}
|
|
389
|
-
target.write(`[doer-agent][task=${taskId}][${stream}] ${line}\n`);
|
|
75
|
+
async function updateRunSessionMetadata(task, metadata) {
|
|
76
|
+
let changed = false;
|
|
77
|
+
const previousSessionId = task.sessionId;
|
|
78
|
+
if (!task.sessionId && typeof metadata.sessionId === "string" && metadata.sessionId.trim()) {
|
|
79
|
+
task.sessionId = metadata.sessionId.trim();
|
|
80
|
+
changed = true;
|
|
390
81
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
}
|
|
395
|
-
function writeRpcStream(requestId, stream, chunk) {
|
|
396
|
-
const target = stream === "stdout" ? process.stdout : process.stderr;
|
|
397
|
-
const lines = chunk.replace(/\r/g, "\n").split("\n");
|
|
398
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
399
|
-
const line = lines[i];
|
|
400
|
-
if (line.length === 0 && i === lines.length - 1) {
|
|
401
|
-
continue;
|
|
402
|
-
}
|
|
403
|
-
target.write(`[doer-agent][rpc=${requestId}][${stream}] ${line}\n`);
|
|
82
|
+
if (!task.sessionFilePath && typeof metadata.sessionFilePath === "string" && metadata.sessionFilePath.trim()) {
|
|
83
|
+
task.sessionFilePath = metadata.sessionFilePath.trim();
|
|
84
|
+
changed = true;
|
|
404
85
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
process.stdout.write(`[doer-agent][run=${runId}][status] ${message}\n`);
|
|
411
|
-
}
|
|
412
|
-
function writeRunStream(runId, stream, chunk) {
|
|
413
|
-
const target = stream === "stdout" ? process.stdout : process.stderr;
|
|
414
|
-
const lines = chunk.split(/\r?\n/);
|
|
415
|
-
for (let index = 0; index < lines.length; index += 1) {
|
|
416
|
-
const line = lines[index];
|
|
417
|
-
if (!line && index === lines.length - 1) {
|
|
418
|
-
continue;
|
|
86
|
+
if (!task.sessionFilePath && task.sessionId) {
|
|
87
|
+
const resolvedSessionFilePath = await findSessionFilePathBySessionId(resolveWorkspaceRoot(), task.sessionId).catch(() => null);
|
|
88
|
+
if (resolvedSessionFilePath) {
|
|
89
|
+
task.sessionFilePath = resolvedSessionFilePath;
|
|
90
|
+
changed = true;
|
|
419
91
|
}
|
|
420
|
-
target.write(`[doer-agent][run=${runId}][${stream}] ${line}\n`);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
function normalizeRunRpcRequest(args) {
|
|
424
|
-
const requestId = typeof args.request.requestId === "string" ? args.request.requestId.trim() : "";
|
|
425
|
-
if (!requestId) {
|
|
426
|
-
throw new Error("missing requestId");
|
|
427
|
-
}
|
|
428
|
-
const requestAgentId = typeof args.request.agentId === "string" ? args.request.agentId.trim() : "";
|
|
429
|
-
if (!requestAgentId || requestAgentId !== args.agentId) {
|
|
430
|
-
throw new Error("agent id mismatch");
|
|
431
92
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
const responseSubject = typeof args.request.responseSubject === "string" ? args.request.responseSubject.trim() : "";
|
|
435
|
-
if (!responseSubject) {
|
|
436
|
-
throw new Error("missing responseSubject");
|
|
93
|
+
if (!changed) {
|
|
94
|
+
return;
|
|
437
95
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
96
|
+
task.updatedAt = formatLocalTimestamp();
|
|
97
|
+
await persistRunTask(resolveWorkspaceRoot(), task).catch(() => undefined);
|
|
98
|
+
if (metadata.nc) {
|
|
99
|
+
publishImmediateRunEvent({
|
|
100
|
+
nc: metadata.nc,
|
|
101
|
+
userId: task.userId,
|
|
102
|
+
task,
|
|
103
|
+
buildRunEventsSubject: buildAgentRunEventsSubject,
|
|
104
|
+
});
|
|
445
105
|
}
|
|
446
|
-
if (
|
|
447
|
-
|
|
106
|
+
if (!previousSessionId && task.sessionId) {
|
|
107
|
+
await updateRunStartSlotSession({
|
|
108
|
+
workspaceRoot: resolveWorkspaceRoot(),
|
|
109
|
+
runId: task.id,
|
|
110
|
+
previousSessionId,
|
|
111
|
+
sessionId: task.sessionId,
|
|
112
|
+
formatTimestamp: formatLocalTimestamp,
|
|
113
|
+
}).catch(() => undefined);
|
|
448
114
|
}
|
|
449
|
-
const cwd = typeof args.request.cwd === "string" && args.request.cwd.trim() ? args.request.cwd.trim() : null;
|
|
450
|
-
const sinceSeqRaw = Number(args.request.sinceSeq);
|
|
451
|
-
const sinceSeq = Number.isInteger(sinceSeqRaw) && sinceSeqRaw >= 0 ? sinceSeqRaw : null;
|
|
452
|
-
const limitRaw = Number(args.request.limit);
|
|
453
|
-
const limit = Number.isFinite(limitRaw) ? Math.max(1, Math.min(Math.floor(limitRaw), 200)) : 50;
|
|
454
|
-
return {
|
|
455
|
-
requestId,
|
|
456
|
-
action,
|
|
457
|
-
runId,
|
|
458
|
-
prompt,
|
|
459
|
-
imagePaths,
|
|
460
|
-
sessionId,
|
|
461
|
-
model,
|
|
462
|
-
cwd,
|
|
463
|
-
responseSubject,
|
|
464
|
-
sinceSeq,
|
|
465
|
-
limit,
|
|
466
|
-
runtimeEnvPatch: normalizeEnvPatch(args.request.runtimeEnvPatch),
|
|
467
|
-
codexAuthBundle: normalizeShellRpcCodexAuthBundle(args.request.codexAuth),
|
|
468
|
-
};
|
|
469
|
-
}
|
|
470
|
-
function publishRunRpcResponse(args) {
|
|
471
|
-
args.nc.publish(args.responseSubject, runRpcCodec.encode(JSON.stringify(args.payload)));
|
|
472
|
-
}
|
|
473
|
-
async function resolveRunsDir() {
|
|
474
|
-
const workspaceRoot = workspaceRootOverride ?? (process.env.WORKSPACE?.trim() || process.cwd());
|
|
475
|
-
const dir = path.join(workspaceRoot, ".doer-agent", "runs");
|
|
476
|
-
await mkdir(dir, { recursive: true });
|
|
477
|
-
return dir;
|
|
478
|
-
}
|
|
479
|
-
async function resetRunsDir() {
|
|
480
|
-
const dir = await resolveRunsDir();
|
|
481
|
-
await rm(dir, { recursive: true, force: true }).catch(() => undefined);
|
|
482
|
-
await mkdir(dir, { recursive: true });
|
|
483
115
|
}
|
|
484
|
-
async function
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
116
|
+
async function startManagedRun(args) {
|
|
117
|
+
const prepared = await prepareCommandExecution({
|
|
118
|
+
cwd: args.cwd,
|
|
119
|
+
userId: args.userId,
|
|
120
|
+
taskId: args.runId,
|
|
121
|
+
codexAuthBundle: args.codexAuthBundle,
|
|
122
|
+
runtimeEnvPatch: args.runtimeEnvPatch,
|
|
123
|
+
agentProjectDir: AGENT_PROJECT_DIR,
|
|
124
|
+
resolveShellPath: runtimeEnvHelpers.resolveShellPath,
|
|
125
|
+
resolveTaskWorkspace: runtimeEnvHelpers.resolveTaskWorkspace,
|
|
126
|
+
resolveCodexHomePath: runtimeEnvHelpers.resolveCodexHomePath,
|
|
127
|
+
prepareCodexAuthBundle,
|
|
128
|
+
readAgentSettingsConfig,
|
|
129
|
+
resolveWorkspaceRoot,
|
|
130
|
+
buildAgentSettingsEnvPatch,
|
|
131
|
+
prepareTaskGitEnv: runtimeEnvHelpers.prepareTaskGitEnv,
|
|
132
|
+
});
|
|
133
|
+
const child = spawnManagedCodexCommand({
|
|
134
|
+
codexArgs: args.codexArgs,
|
|
135
|
+
taskWorkspace: prepared.taskWorkspace,
|
|
136
|
+
env: prepared.env,
|
|
137
|
+
agentToken: args.agentToken,
|
|
138
|
+
});
|
|
139
|
+
const now = formatLocalTimestamp();
|
|
140
|
+
const task = {
|
|
141
|
+
id: args.runId,
|
|
142
|
+
userId: args.userId,
|
|
143
|
+
agentId: args.agentId,
|
|
144
|
+
processPid: typeof child.pid === "number" ? child.pid : null,
|
|
145
|
+
sessionId: typeof args.sessionId === "string" && args.sessionId.trim() ? args.sessionId.trim() : null,
|
|
146
|
+
sessionFilePath: null,
|
|
147
|
+
status: "running",
|
|
148
|
+
cancelRequested: false,
|
|
149
|
+
resultExitCode: null,
|
|
150
|
+
resultSignal: null,
|
|
151
|
+
error: null,
|
|
152
|
+
createdAt: now,
|
|
153
|
+
updatedAt: now,
|
|
154
|
+
startedAt: now,
|
|
155
|
+
finishedAt: null,
|
|
502
156
|
};
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
async function resolveRunStartLockPath(args) {
|
|
518
|
-
const dir = await resolveRunLocksDir();
|
|
519
|
-
if (typeof args.sessionId === "string" && args.sessionId.trim()) {
|
|
520
|
-
return path.join(dir, `session__${sanitizeRunLockSegment(args.sessionId)}.lock`);
|
|
521
|
-
}
|
|
522
|
-
return path.join(dir, "pending_new_session.lock");
|
|
523
|
-
}
|
|
524
|
-
async function claimRunStartSlot(args) {
|
|
525
|
-
const lockPath = await resolveRunStartLockPath(args);
|
|
526
|
-
try {
|
|
527
|
-
const handle = await open(lockPath, "wx");
|
|
528
|
-
try {
|
|
529
|
-
const payload = {
|
|
530
|
-
runId: args.runId,
|
|
531
|
-
sessionId: typeof args.sessionId === "string" && args.sessionId.trim() ? args.sessionId.trim() : null,
|
|
532
|
-
pid: process.pid,
|
|
533
|
-
createdAt: formatLocalTimestamp(),
|
|
534
|
-
};
|
|
535
|
-
await handle.writeFile(`${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
536
|
-
}
|
|
537
|
-
finally {
|
|
538
|
-
await handle.close().catch(() => undefined);
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
catch (error) {
|
|
542
|
-
if (error?.code === "EEXIST") {
|
|
543
|
-
const lockContents = await readFile(lockPath, "utf8").catch(() => "");
|
|
544
|
-
const existingRunId = (() => {
|
|
545
|
-
try {
|
|
546
|
-
const parsed = JSON.parse(lockContents);
|
|
547
|
-
return typeof parsed.runId === "string" && parsed.runId.trim() ? parsed.runId.trim() : null;
|
|
548
|
-
}
|
|
549
|
-
catch {
|
|
550
|
-
return null;
|
|
551
|
-
}
|
|
552
|
-
})();
|
|
553
|
-
throw new Error(existingRunId ? `Another run is already active: ${existingRunId}` : "Another run is already active");
|
|
554
|
-
}
|
|
555
|
-
throw error;
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
async function updateRunStartSlotSession(args) {
|
|
559
|
-
const nextSessionId = args.sessionId.trim();
|
|
560
|
-
if (!nextSessionId) {
|
|
561
|
-
return;
|
|
562
|
-
}
|
|
563
|
-
const previousSessionId = typeof args.previousSessionId === "string" && args.previousSessionId.trim() ? args.previousSessionId.trim() : null;
|
|
564
|
-
if (previousSessionId === nextSessionId) {
|
|
565
|
-
return;
|
|
566
|
-
}
|
|
567
|
-
const currentPath = await resolveRunStartLockPath({ runId: args.runId, sessionId: previousSessionId });
|
|
568
|
-
const nextPath = await resolveRunStartLockPath({ runId: args.runId, sessionId: nextSessionId });
|
|
569
|
-
if (currentPath === nextPath) {
|
|
570
|
-
return;
|
|
571
|
-
}
|
|
572
|
-
try {
|
|
573
|
-
await rename(currentPath, nextPath);
|
|
574
|
-
}
|
|
575
|
-
catch (error) {
|
|
576
|
-
const code = error?.code;
|
|
577
|
-
if (code === "ENOENT") {
|
|
578
|
-
// Lock may already be released; nothing to migrate.
|
|
157
|
+
let pendingSessionPollClosed = false;
|
|
158
|
+
const knownPendingSessionFiles = new Set();
|
|
159
|
+
const pendingSessionTracker = createPendingRunSessionTracker({
|
|
160
|
+
task,
|
|
161
|
+
detectPendingRunSession: async () => detectPendingRunSession(resolveWorkspaceRoot(), knownPendingSessionFiles),
|
|
162
|
+
updateRunSessionMetadata: async (metadata) => updateRunSessionMetadata(task, { ...metadata, nc: args.nc }),
|
|
163
|
+
});
|
|
164
|
+
const stopPendingSessionPoll = () => {
|
|
165
|
+
pendingSessionPollClosed = true;
|
|
166
|
+
pendingSessionTracker.stop();
|
|
167
|
+
};
|
|
168
|
+
const pollPendingSession = async () => {
|
|
169
|
+
if (pendingSessionPollClosed) {
|
|
170
|
+
stopPendingSessionPoll();
|
|
579
171
|
return;
|
|
580
172
|
}
|
|
581
|
-
|
|
582
|
-
throw new Error(`Another run is already active for session: ${nextSessionId}`);
|
|
583
|
-
}
|
|
584
|
-
throw error;
|
|
585
|
-
}
|
|
586
|
-
const payload = {
|
|
587
|
-
runId: args.runId,
|
|
588
|
-
sessionId: nextSessionId,
|
|
589
|
-
pid: process.pid,
|
|
590
|
-
createdAt: formatLocalTimestamp(),
|
|
591
|
-
};
|
|
592
|
-
await writeFile(nextPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
593
|
-
}
|
|
594
|
-
async function releaseRunStartSlot(args) {
|
|
595
|
-
const paths = new Set();
|
|
596
|
-
paths.add(await resolveRunStartLockPath({ runId: args.runId, sessionId: args.sessionId ?? null }));
|
|
597
|
-
paths.add(await resolveRunStartLockPath({ runId: args.runId, sessionId: null }));
|
|
598
|
-
for (const lockPath of paths) {
|
|
599
|
-
await unlink(lockPath).catch(() => undefined);
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
function resolveAgentSettingsDir() {
|
|
603
|
-
const workspaceRoot = workspaceRootOverride ?? (process.env.WORKSPACE?.trim() || process.cwd());
|
|
604
|
-
return path.join(workspaceRoot, ".doer-agent");
|
|
605
|
-
}
|
|
606
|
-
function resolveAgentSettingsFilePath() {
|
|
607
|
-
return path.join(resolveAgentSettingsDir(), "config.json");
|
|
608
|
-
}
|
|
609
|
-
function resolveAgentModelInstructionsFilePath() {
|
|
610
|
-
return path.join(resolveAgentSettingsDir(), "model-instructions.md");
|
|
611
|
-
}
|
|
612
|
-
function createDefaultAgentSettingsConfig() {
|
|
613
|
-
return {
|
|
614
|
-
general: {
|
|
615
|
-
personality: "pragmatic",
|
|
616
|
-
},
|
|
617
|
-
codex: {
|
|
618
|
-
model: "gpt-5.4",
|
|
619
|
-
authMode: "api_key",
|
|
620
|
-
},
|
|
621
|
-
realtime: {
|
|
622
|
-
model: process.env.OPENAI_REALTIME_MODEL?.trim() || "gpt-realtime",
|
|
623
|
-
voice: process.env.OPENAI_REALTIME_VOICE?.trim() || "alloy",
|
|
624
|
-
wakeName: null,
|
|
625
|
-
requireWakeName: true,
|
|
626
|
-
apiKey: null,
|
|
627
|
-
},
|
|
628
|
-
git: {
|
|
629
|
-
enabled: true,
|
|
630
|
-
name: null,
|
|
631
|
-
email: null,
|
|
632
|
-
authMode: "none",
|
|
633
|
-
oauthToken: null,
|
|
634
|
-
oauthLogin: null,
|
|
635
|
-
oauthScope: null,
|
|
636
|
-
},
|
|
637
|
-
aws: {
|
|
638
|
-
enabled: true,
|
|
639
|
-
accessKeyId: null,
|
|
640
|
-
defaultRegion: null,
|
|
641
|
-
secretAccessKey: null,
|
|
642
|
-
sessionToken: null,
|
|
643
|
-
},
|
|
644
|
-
jira: {
|
|
645
|
-
baseUrl: null,
|
|
646
|
-
email: null,
|
|
647
|
-
enabled: false,
|
|
648
|
-
apiToken: null,
|
|
649
|
-
},
|
|
650
|
-
notion: {
|
|
651
|
-
baseUrl: "https://api.notion.com",
|
|
652
|
-
version: "2022-06-28",
|
|
653
|
-
enabled: false,
|
|
654
|
-
apiToken: null,
|
|
655
|
-
},
|
|
656
|
-
slack: {
|
|
657
|
-
baseUrl: "https://slack.com/api",
|
|
658
|
-
enabled: false,
|
|
659
|
-
botToken: null,
|
|
660
|
-
},
|
|
661
|
-
figma: {
|
|
662
|
-
baseUrl: "https://api.figma.com",
|
|
663
|
-
enabled: false,
|
|
664
|
-
apiToken: null,
|
|
665
|
-
},
|
|
173
|
+
await pendingSessionTracker.poll();
|
|
666
174
|
};
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
175
|
+
if (!task.sessionId) {
|
|
176
|
+
const existingFiles = await collectSessionJsonlFiles(resolveWorkspaceRoot()).catch(() => []);
|
|
177
|
+
for (const file of existingFiles) {
|
|
178
|
+
knownPendingSessionFiles.add(file.filePath);
|
|
179
|
+
}
|
|
180
|
+
pendingSessionTracker.start();
|
|
671
181
|
}
|
|
672
|
-
|
|
673
|
-
|
|
182
|
+
child.stdout.on("data", (chunk) => writeRunStream(task.id, "stdout", chunk));
|
|
183
|
+
child.stderr.on("data", (chunk) => writeRunStream(task.id, "stderr", chunk));
|
|
184
|
+
attachManagedRunProcessLifecycle({
|
|
185
|
+
child,
|
|
186
|
+
task,
|
|
187
|
+
nc: args.nc,
|
|
188
|
+
stopPendingSessionPoll,
|
|
189
|
+
getStoredRun: (runId) => getStoredRun(resolveWorkspaceRoot(), runId),
|
|
190
|
+
publishImmediateRunEvent: (eventArgs) => publishImmediateRunEvent({
|
|
191
|
+
...eventArgs,
|
|
192
|
+
buildRunEventsSubject: buildAgentRunEventsSubject,
|
|
193
|
+
}),
|
|
194
|
+
removeRunTask: (runId) => removeRunTask(resolveWorkspaceRoot(), runId),
|
|
195
|
+
releaseRunStartSlot: ({ runId, sessionId }) => releaseRunStartSlot({
|
|
196
|
+
workspaceRoot: resolveWorkspaceRoot(),
|
|
197
|
+
runId,
|
|
198
|
+
sessionId,
|
|
199
|
+
}),
|
|
200
|
+
codexAuthCleanup: prepared.codexAuthCleanup,
|
|
201
|
+
writeRunStatus,
|
|
202
|
+
formatTimestamp: formatLocalTimestamp,
|
|
203
|
+
});
|
|
204
|
+
void persistRunTask(resolveWorkspaceRoot(), task).catch(() => undefined);
|
|
205
|
+
publishImmediateRunEvent({ nc: args.nc, userId: task.userId, task, buildRunEventsSubject: buildAgentRunEventsSubject });
|
|
206
|
+
writeRunStatus(task.id, `started requestId=${args.requestId} cwd=${prepared.taskWorkspace}`);
|
|
207
|
+
if (!task.sessionId) {
|
|
208
|
+
void pollPendingSession();
|
|
674
209
|
}
|
|
675
|
-
|
|
676
|
-
return trimmed ? trimmed : null;
|
|
677
|
-
}
|
|
678
|
-
function normalizeCodexPersonality(value, fallback) {
|
|
679
|
-
return value === "friendly" || value === "pragmatic" ? value : fallback;
|
|
210
|
+
return cloneRunTask(task);
|
|
680
211
|
}
|
|
681
|
-
function
|
|
682
|
-
const
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
codex: {
|
|
698
|
-
model: typeof codex.model === "string" && codex.model.trim() ? codex.model.trim() : base.codex.model,
|
|
699
|
-
authMode: codex.authMode === "oauth" ? "oauth" : codex.authMode === "api_key" ? "api_key" : base.codex.authMode,
|
|
700
|
-
},
|
|
701
|
-
realtime: {
|
|
702
|
-
model: typeof realtime.model === "string" && realtime.model.trim() ? realtime.model.trim() : base.realtime.model,
|
|
703
|
-
voice: typeof realtime.voice === "string" && realtime.voice.trim() ? realtime.voice.trim() : base.realtime.voice,
|
|
704
|
-
wakeName: realtime.wakeName === null ? null : normalizeNullableString(realtime.wakeName) ?? base.realtime.wakeName,
|
|
705
|
-
requireWakeName: typeof realtime.requireWakeName === "boolean" ? realtime.requireWakeName : base.realtime.requireWakeName,
|
|
706
|
-
apiKey: realtime.apiKey === null ? null : normalizeNullableString(realtime.apiKey) ?? base.realtime.apiKey,
|
|
707
|
-
},
|
|
708
|
-
git: {
|
|
709
|
-
enabled: typeof git.enabled === "boolean" ? git.enabled : base.git.enabled,
|
|
710
|
-
name: git.name === null ? null : normalizeNullableString(git.name) ?? base.git.name,
|
|
711
|
-
email: git.email === null ? null : normalizeNullableString(git.email) ?? base.git.email,
|
|
712
|
-
authMode: git.authMode === "oauth_app" ? "oauth_app" : git.authMode === "none" ? "none" : base.git.authMode,
|
|
713
|
-
oauthToken: git.oauthToken === null ? null : normalizeNullableString(git.oauthToken) ?? base.git.oauthToken,
|
|
714
|
-
oauthLogin: git.oauthLogin === null ? null : normalizeNullableString(git.oauthLogin) ?? base.git.oauthLogin,
|
|
715
|
-
oauthScope: git.oauthScope === null ? null : normalizeNullableString(git.oauthScope) ?? base.git.oauthScope,
|
|
716
|
-
},
|
|
717
|
-
aws: {
|
|
718
|
-
enabled: typeof aws.enabled === "boolean" ? aws.enabled : base.aws.enabled,
|
|
719
|
-
accessKeyId: aws.accessKeyId === null ? null : normalizeNullableString(aws.accessKeyId) ?? base.aws.accessKeyId,
|
|
720
|
-
defaultRegion: aws.defaultRegion === null ? null : normalizeNullableString(aws.defaultRegion) ?? base.aws.defaultRegion,
|
|
721
|
-
secretAccessKey: aws.secretAccessKey === null ? null : normalizeNullableString(aws.secretAccessKey) ?? base.aws.secretAccessKey,
|
|
722
|
-
sessionToken: aws.sessionToken === null ? null : normalizeNullableString(aws.sessionToken) ?? base.aws.sessionToken,
|
|
723
|
-
},
|
|
724
|
-
jira: {
|
|
725
|
-
baseUrl: jira.baseUrl === null ? null : normalizeNullableString(jira.baseUrl) ?? base.jira.baseUrl,
|
|
726
|
-
email: jira.email === null ? null : normalizeNullableString(jira.email) ?? base.jira.email,
|
|
727
|
-
enabled: typeof jira.enabled === "boolean" ? jira.enabled : base.jira.enabled,
|
|
728
|
-
apiToken: jira.apiToken === null ? null : normalizeNullableString(jira.apiToken) ?? base.jira.apiToken,
|
|
729
|
-
},
|
|
730
|
-
notion: {
|
|
731
|
-
baseUrl: notion.baseUrl === null ? null : normalizeNullableString(notion.baseUrl) ?? base.notion.baseUrl,
|
|
732
|
-
version: notion.version === null ? null : normalizeNullableString(notion.version) ?? base.notion.version,
|
|
733
|
-
enabled: typeof notion.enabled === "boolean" ? notion.enabled : base.notion.enabled,
|
|
734
|
-
apiToken: notion.apiToken === null ? null : normalizeNullableString(notion.apiToken) ?? base.notion.apiToken,
|
|
735
|
-
},
|
|
736
|
-
slack: {
|
|
737
|
-
baseUrl: slack.baseUrl === null ? null : normalizeNullableString(slack.baseUrl) ?? base.slack.baseUrl,
|
|
738
|
-
enabled: typeof slack.enabled === "boolean" ? slack.enabled : base.slack.enabled,
|
|
739
|
-
botToken: slack.botToken === null ? null : normalizeNullableString(slack.botToken) ?? base.slack.botToken,
|
|
740
|
-
},
|
|
741
|
-
figma: {
|
|
742
|
-
baseUrl: figma.baseUrl === null ? null : normalizeNullableString(figma.baseUrl) ?? base.figma.baseUrl,
|
|
743
|
-
enabled: typeof figma.enabled === "boolean" ? figma.enabled : base.figma.enabled,
|
|
744
|
-
apiToken: figma.apiToken === null ? null : normalizeNullableString(figma.apiToken) ?? base.figma.apiToken,
|
|
212
|
+
function subscribeToSettingsRpc(args) {
|
|
213
|
+
const subject = buildAgentSettingsRpcSubject(args.userId, args.agentId);
|
|
214
|
+
args.jetstream.nc.subscribe(subject, {
|
|
215
|
+
callback: (error, msg) => {
|
|
216
|
+
if (error) {
|
|
217
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
218
|
+
writeAgentError(`settings rpc subscription error: ${message}`);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
void handleSettingsRpcMessage({
|
|
222
|
+
msg,
|
|
223
|
+
nc: args.jetstream.nc,
|
|
224
|
+
agentId: args.agentId,
|
|
225
|
+
workspaceRoot: resolveWorkspaceRoot(),
|
|
226
|
+
onError: writeAgentError,
|
|
227
|
+
});
|
|
745
228
|
},
|
|
746
|
-
};
|
|
229
|
+
});
|
|
230
|
+
writeAgentInfo(`settings rpc subscribed subject=${subject}`);
|
|
747
231
|
}
|
|
748
|
-
|
|
749
|
-
const
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
return fallback;
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
async function writeAgentSettingsConfig(config) {
|
|
763
|
-
const dir = resolveAgentSettingsDir();
|
|
764
|
-
await mkdir(dir, { recursive: true });
|
|
765
|
-
await writeFile(resolveAgentSettingsFilePath(), `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
766
|
-
}
|
|
767
|
-
async function readAgentModelInstructions() {
|
|
768
|
-
const raw = await readFile(resolveAgentModelInstructionsFilePath(), "utf8").catch(() => "");
|
|
769
|
-
return raw.trim() ? raw : null;
|
|
770
|
-
}
|
|
771
|
-
async function writeAgentModelInstructions(value) {
|
|
772
|
-
const filePath = resolveAgentModelInstructionsFilePath();
|
|
773
|
-
const nextValue = typeof value === "string" ? value.trim() : "";
|
|
774
|
-
if (!nextValue) {
|
|
775
|
-
await unlink(filePath).catch(() => undefined);
|
|
776
|
-
return;
|
|
777
|
-
}
|
|
778
|
-
await mkdir(resolveAgentSettingsDir(), { recursive: true });
|
|
779
|
-
await writeFile(filePath, value ?? "", "utf8");
|
|
780
|
-
}
|
|
781
|
-
function maskSecretPreview(secret) {
|
|
782
|
-
if (secret.length <= 6) {
|
|
783
|
-
return `${secret.slice(0, 1)}***${secret.slice(-1)}`;
|
|
784
|
-
}
|
|
785
|
-
return `${secret.slice(0, 4)}...${secret.slice(-4)}`;
|
|
786
|
-
}
|
|
787
|
-
function toMaskedSecret(value) {
|
|
788
|
-
if (!value) {
|
|
789
|
-
return { has: false, masked: null, length: null };
|
|
790
|
-
}
|
|
791
|
-
return { has: true, masked: maskSecretPreview(value), length: value.length };
|
|
792
|
-
}
|
|
793
|
-
async function toAgentSettingsPublic(config) {
|
|
794
|
-
const realtimeKey = toMaskedSecret(config.realtime.apiKey);
|
|
795
|
-
const gitOauth = toMaskedSecret(config.git.oauthToken);
|
|
796
|
-
const awsSecret = toMaskedSecret(config.aws.secretAccessKey);
|
|
797
|
-
const awsSession = toMaskedSecret(config.aws.sessionToken);
|
|
798
|
-
const jiraToken = toMaskedSecret(config.jira.apiToken);
|
|
799
|
-
const notionToken = toMaskedSecret(config.notion.apiToken);
|
|
800
|
-
const slackToken = toMaskedSecret(config.slack.botToken);
|
|
801
|
-
const figmaToken = toMaskedSecret(config.figma.apiToken);
|
|
802
|
-
const customInstructions = await readAgentModelInstructions();
|
|
803
|
-
return {
|
|
804
|
-
general: {
|
|
805
|
-
personality: config.general.personality,
|
|
806
|
-
customInstructions,
|
|
807
|
-
},
|
|
808
|
-
codex: {
|
|
809
|
-
model: config.codex.model,
|
|
810
|
-
authMode: config.codex.authMode,
|
|
811
|
-
hasApiKey: false,
|
|
812
|
-
apiKeyMasked: null,
|
|
813
|
-
apiKeyLength: null,
|
|
814
|
-
},
|
|
815
|
-
realtime: {
|
|
816
|
-
model: config.realtime.model,
|
|
817
|
-
voice: config.realtime.voice,
|
|
818
|
-
wakeName: config.realtime.wakeName,
|
|
819
|
-
requireWakeName: config.realtime.requireWakeName,
|
|
820
|
-
hasApiKey: realtimeKey.has,
|
|
821
|
-
apiKeyMasked: realtimeKey.masked,
|
|
822
|
-
apiKeyLength: realtimeKey.length,
|
|
823
|
-
},
|
|
824
|
-
git: {
|
|
825
|
-
enabled: config.git.enabled,
|
|
826
|
-
name: config.git.name,
|
|
827
|
-
email: config.git.email,
|
|
828
|
-
authMode: config.git.authMode,
|
|
829
|
-
hasOauthToken: gitOauth.has,
|
|
830
|
-
oauthTokenMasked: gitOauth.masked,
|
|
831
|
-
oauthTokenLength: gitOauth.length,
|
|
832
|
-
oauthLogin: config.git.oauthLogin,
|
|
833
|
-
oauthScope: config.git.oauthScope,
|
|
834
|
-
},
|
|
835
|
-
aws: {
|
|
836
|
-
enabled: config.aws.enabled,
|
|
837
|
-
accessKeyId: config.aws.accessKeyId,
|
|
838
|
-
defaultRegion: config.aws.defaultRegion,
|
|
839
|
-
hasSecretAccessKey: awsSecret.has,
|
|
840
|
-
secretAccessKeyMasked: awsSecret.masked,
|
|
841
|
-
secretAccessKeyLength: awsSecret.length,
|
|
842
|
-
hasSessionToken: awsSession.has,
|
|
843
|
-
sessionTokenMasked: awsSession.masked,
|
|
844
|
-
sessionTokenLength: awsSession.length,
|
|
845
|
-
},
|
|
846
|
-
jira: {
|
|
847
|
-
baseUrl: config.jira.baseUrl,
|
|
848
|
-
email: config.jira.email,
|
|
849
|
-
enabled: config.jira.enabled,
|
|
850
|
-
hasApiToken: jiraToken.has,
|
|
851
|
-
apiTokenMasked: jiraToken.masked,
|
|
852
|
-
apiTokenLength: jiraToken.length,
|
|
853
|
-
},
|
|
854
|
-
notion: {
|
|
855
|
-
baseUrl: config.notion.baseUrl,
|
|
856
|
-
version: config.notion.version,
|
|
857
|
-
enabled: config.notion.enabled,
|
|
858
|
-
hasApiToken: notionToken.has,
|
|
859
|
-
apiTokenMasked: notionToken.masked,
|
|
860
|
-
apiTokenLength: notionToken.length,
|
|
861
|
-
},
|
|
862
|
-
slack: {
|
|
863
|
-
baseUrl: config.slack.baseUrl,
|
|
864
|
-
enabled: config.slack.enabled,
|
|
865
|
-
hasBotToken: slackToken.has,
|
|
866
|
-
botTokenMasked: slackToken.masked,
|
|
867
|
-
botTokenLength: slackToken.length,
|
|
868
|
-
},
|
|
869
|
-
figma: {
|
|
870
|
-
baseUrl: config.figma.baseUrl,
|
|
871
|
-
enabled: config.figma.enabled,
|
|
872
|
-
hasApiToken: figmaToken.has,
|
|
873
|
-
apiTokenMasked: figmaToken.masked,
|
|
874
|
-
apiTokenLength: figmaToken.length,
|
|
875
|
-
},
|
|
876
|
-
};
|
|
877
|
-
}
|
|
878
|
-
function normalizeAgentSettingsPatch(value) {
|
|
879
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
880
|
-
return {};
|
|
881
|
-
}
|
|
882
|
-
const raw = value;
|
|
883
|
-
const patch = { ...raw };
|
|
884
|
-
const assignNested = (section, key, value) => {
|
|
885
|
-
const current = patch[section] && typeof patch[section] === "object" && !Array.isArray(patch[section])
|
|
886
|
-
? ({ ...patch[section] })
|
|
887
|
-
: {};
|
|
888
|
-
current[key] = value;
|
|
889
|
-
patch[section] = current;
|
|
890
|
-
};
|
|
891
|
-
const move = (flatKey, section, key) => {
|
|
892
|
-
if (!(flatKey in raw)) {
|
|
893
|
-
return;
|
|
894
|
-
}
|
|
895
|
-
assignNested(section, key, raw[flatKey]);
|
|
896
|
-
delete patch[flatKey];
|
|
897
|
-
};
|
|
898
|
-
move("personality", "general", "personality");
|
|
899
|
-
move("codexModel", "codex", "model");
|
|
900
|
-
move("codexAuthMode", "codex", "authMode");
|
|
901
|
-
move("realtimeModel", "realtime", "model");
|
|
902
|
-
move("realtimeVoice", "realtime", "voice");
|
|
903
|
-
move("realtimeWakeName", "realtime", "wakeName");
|
|
904
|
-
move("realtimeRequireWakeName", "realtime", "requireWakeName");
|
|
905
|
-
move("realtimeApiKey", "realtime", "apiKey");
|
|
906
|
-
move("gitEnabled", "git", "enabled");
|
|
907
|
-
move("gitName", "git", "name");
|
|
908
|
-
move("gitEmail", "git", "email");
|
|
909
|
-
move("gitAuthMode", "git", "authMode");
|
|
910
|
-
move("gitOauthToken", "git", "oauthToken");
|
|
911
|
-
move("gitOauthLogin", "git", "oauthLogin");
|
|
912
|
-
move("gitOauthScope", "git", "oauthScope");
|
|
913
|
-
move("awsEnabled", "aws", "enabled");
|
|
914
|
-
move("awsAccessKeyId", "aws", "accessKeyId");
|
|
915
|
-
move("awsDefaultRegion", "aws", "defaultRegion");
|
|
916
|
-
move("awsSecretAccessKey", "aws", "secretAccessKey");
|
|
917
|
-
move("awsSessionToken", "aws", "sessionToken");
|
|
918
|
-
move("jiraBaseUrl", "jira", "baseUrl");
|
|
919
|
-
move("jiraEmail", "jira", "email");
|
|
920
|
-
move("jiraEnabled", "jira", "enabled");
|
|
921
|
-
move("jiraApiToken", "jira", "apiToken");
|
|
922
|
-
move("notionBaseUrl", "notion", "baseUrl");
|
|
923
|
-
move("notionVersion", "notion", "version");
|
|
924
|
-
move("notionEnabled", "notion", "enabled");
|
|
925
|
-
move("notionApiToken", "notion", "apiToken");
|
|
926
|
-
move("slackBaseUrl", "slack", "baseUrl");
|
|
927
|
-
move("slackEnabled", "slack", "enabled");
|
|
928
|
-
move("slackBotToken", "slack", "botToken");
|
|
929
|
-
move("figmaBaseUrl", "figma", "baseUrl");
|
|
930
|
-
move("figmaEnabled", "figma", "enabled");
|
|
931
|
-
move("figmaApiToken", "figma", "apiToken");
|
|
932
|
-
return patch;
|
|
933
|
-
}
|
|
934
|
-
async function resolveAgentSettingsConfig(args) {
|
|
935
|
-
const existing = await readAgentSettingsConfig(args.defaults ?? null);
|
|
936
|
-
const next = normalizeAgentSettingsConfig(args.patch ?? null, existing);
|
|
937
|
-
return next;
|
|
938
|
-
}
|
|
939
|
-
function buildAgentSettingsEnvPatch(config) {
|
|
940
|
-
const envPatch = {};
|
|
941
|
-
if (config.git.enabled) {
|
|
942
|
-
if (config.git.name)
|
|
943
|
-
envPatch.GIT_AUTHOR_NAME = config.git.name;
|
|
944
|
-
if (config.git.name)
|
|
945
|
-
envPatch.GIT_COMMITTER_NAME = config.git.name;
|
|
946
|
-
if (config.git.email)
|
|
947
|
-
envPatch.GIT_AUTHOR_EMAIL = config.git.email;
|
|
948
|
-
if (config.git.email)
|
|
949
|
-
envPatch.GIT_COMMITTER_EMAIL = config.git.email;
|
|
950
|
-
if (config.git.oauthToken)
|
|
951
|
-
envPatch.GITHUB_TOKEN = config.git.oauthToken;
|
|
952
|
-
if (config.git.oauthToken)
|
|
953
|
-
envPatch.GH_TOKEN = config.git.oauthToken;
|
|
954
|
-
if (config.git.oauthLogin)
|
|
955
|
-
envPatch.DOER_GIT_OAUTH_LOGIN = config.git.oauthLogin;
|
|
956
|
-
if (config.git.oauthScope)
|
|
957
|
-
envPatch.DOER_GIT_OAUTH_SCOPE = config.git.oauthScope;
|
|
958
|
-
}
|
|
959
|
-
if (config.aws.enabled) {
|
|
960
|
-
if (config.aws.accessKeyId)
|
|
961
|
-
envPatch.AWS_ACCESS_KEY_ID = config.aws.accessKeyId;
|
|
962
|
-
if (config.aws.defaultRegion)
|
|
963
|
-
envPatch.AWS_DEFAULT_REGION = config.aws.defaultRegion;
|
|
964
|
-
if (config.aws.defaultRegion)
|
|
965
|
-
envPatch.AWS_REGION = config.aws.defaultRegion;
|
|
966
|
-
if (config.aws.secretAccessKey)
|
|
967
|
-
envPatch.AWS_SECRET_ACCESS_KEY = config.aws.secretAccessKey;
|
|
968
|
-
if (config.aws.sessionToken)
|
|
969
|
-
envPatch.AWS_SESSION_TOKEN = config.aws.sessionToken;
|
|
970
|
-
}
|
|
971
|
-
if (config.jira.enabled) {
|
|
972
|
-
if (config.jira.baseUrl)
|
|
973
|
-
envPatch.JIRA_BASE_URL = config.jira.baseUrl;
|
|
974
|
-
if (config.jira.email)
|
|
975
|
-
envPatch.JIRA_EMAIL = config.jira.email;
|
|
976
|
-
if (config.jira.apiToken)
|
|
977
|
-
envPatch.JIRA_API_TOKEN = config.jira.apiToken;
|
|
978
|
-
}
|
|
979
|
-
if (config.notion.enabled) {
|
|
980
|
-
if (config.notion.baseUrl)
|
|
981
|
-
envPatch.NOTION_BASE_URL = config.notion.baseUrl;
|
|
982
|
-
if (config.notion.version)
|
|
983
|
-
envPatch.NOTION_VERSION = config.notion.version;
|
|
984
|
-
if (config.notion.apiToken)
|
|
985
|
-
envPatch.NOTION_API_TOKEN = config.notion.apiToken;
|
|
986
|
-
}
|
|
987
|
-
if (config.slack.enabled) {
|
|
988
|
-
if (config.slack.baseUrl)
|
|
989
|
-
envPatch.SLACK_BASE_URL = config.slack.baseUrl;
|
|
990
|
-
if (config.slack.botToken)
|
|
991
|
-
envPatch.SLACK_BOT_TOKEN = config.slack.botToken;
|
|
992
|
-
}
|
|
993
|
-
if (config.figma.enabled) {
|
|
994
|
-
if (config.figma.baseUrl)
|
|
995
|
-
envPatch.FIGMA_BASE_URL = config.figma.baseUrl;
|
|
996
|
-
if (config.figma.apiToken)
|
|
997
|
-
envPatch.FIGMA_API_TOKEN = config.figma.apiToken;
|
|
998
|
-
}
|
|
999
|
-
return envPatch;
|
|
1000
|
-
}
|
|
1001
|
-
function cloneRunTask(task, _sinceSeq) {
|
|
1002
|
-
return {
|
|
1003
|
-
...task,
|
|
1004
|
-
};
|
|
1005
|
-
}
|
|
1006
|
-
function buildImmediateRunEvent(task, type) {
|
|
1007
|
-
return {
|
|
1008
|
-
type,
|
|
1009
|
-
agentId: task.agentId,
|
|
1010
|
-
sessionId: task.sessionId,
|
|
1011
|
-
filePath: task.sessionFilePath,
|
|
1012
|
-
runId: task.id,
|
|
1013
|
-
updatedAt: task.updatedAt,
|
|
1014
|
-
status: task.status,
|
|
1015
|
-
cancelRequested: task.cancelRequested,
|
|
1016
|
-
resultExitCode: task.resultExitCode,
|
|
1017
|
-
resultSignal: task.resultSignal,
|
|
1018
|
-
error: task.error,
|
|
1019
|
-
finishedAt: task.finishedAt,
|
|
1020
|
-
};
|
|
1021
|
-
}
|
|
1022
|
-
function publishImmediateRunEvent(args) {
|
|
1023
|
-
args.nc.publish(buildAgentRunEventsSubject(args.userId, args.task.agentId), runEventsCodec.encode(JSON.stringify(buildImmediateRunEvent(args.task, args.type ?? "run.changed"))));
|
|
1024
|
-
}
|
|
1025
|
-
async function findSessionFilePathBySessionId(sessionId) {
|
|
1026
|
-
const targetSessionId = sessionId.trim();
|
|
1027
|
-
if (!targetSessionId) {
|
|
1028
|
-
return null;
|
|
1029
|
-
}
|
|
1030
|
-
let sessionsRootStat;
|
|
1031
|
-
try {
|
|
1032
|
-
sessionsRootStat = await stat(getSessionsRootPath());
|
|
1033
|
-
}
|
|
1034
|
-
catch {
|
|
1035
|
-
return null;
|
|
1036
|
-
}
|
|
1037
|
-
if (!sessionsRootStat.isDirectory()) {
|
|
1038
|
-
return null;
|
|
1039
|
-
}
|
|
1040
|
-
const files = await collectSessionJsonlFiles(getSessionsRootPath());
|
|
1041
|
-
files.sort((a, b) => b.mtimeMs - a.mtimeMs || a.filePath.localeCompare(b.filePath));
|
|
1042
|
-
for (const file of files) {
|
|
1043
|
-
let fileHandle = null;
|
|
1044
|
-
try {
|
|
1045
|
-
fileHandle = await open(file.filePath, "r");
|
|
1046
|
-
const entryStat = await fileHandle.stat();
|
|
1047
|
-
const firstLine = await readFirstLine(fileHandle, entryStat.size);
|
|
1048
|
-
if (!firstLine) {
|
|
1049
|
-
continue;
|
|
1050
|
-
}
|
|
1051
|
-
const parsed = JSON.parse(firstLine);
|
|
1052
|
-
const candidateMeta = parsed && parsed.type === "session_meta" && isObjectRecord(parsed.payload)
|
|
1053
|
-
? parsed.payload
|
|
1054
|
-
: isObjectRecord(parsed.session_meta)
|
|
1055
|
-
? parsed.session_meta
|
|
1056
|
-
: isObjectRecord(parsed.sessionMeta)
|
|
1057
|
-
? parsed.sessionMeta
|
|
1058
|
-
: isObjectRecord(parsed.meta)
|
|
1059
|
-
? parsed.meta
|
|
1060
|
-
: isObjectRecord(parsed.payload)
|
|
1061
|
-
? parsed.payload
|
|
1062
|
-
: parsed;
|
|
1063
|
-
const candidateId = pickSessionString(candidateMeta.sessionId, candidateMeta.session_id, candidateMeta.id);
|
|
1064
|
-
if (candidateId === targetSessionId) {
|
|
1065
|
-
return file.filePath;
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
catch {
|
|
1069
|
-
// ignore malformed session files
|
|
1070
|
-
}
|
|
1071
|
-
finally {
|
|
1072
|
-
await fileHandle?.close().catch(() => undefined);
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
return null;
|
|
1076
|
-
}
|
|
1077
|
-
async function readSessionIdFromSessionFile(filePath) {
|
|
1078
|
-
let fileHandle = null;
|
|
1079
|
-
try {
|
|
1080
|
-
fileHandle = await open(filePath, "r");
|
|
1081
|
-
const entryStat = await fileHandle.stat();
|
|
1082
|
-
const firstLine = await readFirstLine(fileHandle, entryStat.size);
|
|
1083
|
-
if (!firstLine) {
|
|
1084
|
-
return null;
|
|
1085
|
-
}
|
|
1086
|
-
const parsed = JSON.parse(firstLine);
|
|
1087
|
-
const candidateMeta = parsed && parsed.type === "session_meta" && isObjectRecord(parsed.payload)
|
|
1088
|
-
? parsed.payload
|
|
1089
|
-
: isObjectRecord(parsed.session_meta)
|
|
1090
|
-
? parsed.session_meta
|
|
1091
|
-
: isObjectRecord(parsed.sessionMeta)
|
|
1092
|
-
? parsed.sessionMeta
|
|
1093
|
-
: isObjectRecord(parsed.meta)
|
|
1094
|
-
? parsed.meta
|
|
1095
|
-
: isObjectRecord(parsed.payload)
|
|
1096
|
-
? parsed.payload
|
|
1097
|
-
: parsed;
|
|
1098
|
-
return pickSessionString(candidateMeta.sessionId, candidateMeta.session_id, candidateMeta.id);
|
|
1099
|
-
}
|
|
1100
|
-
catch {
|
|
1101
|
-
return null;
|
|
1102
|
-
}
|
|
1103
|
-
finally {
|
|
1104
|
-
await fileHandle?.close().catch(() => undefined);
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
async function detectPendingRunSession(knownFilePaths) {
|
|
1108
|
-
const sessionsRoot = getSessionsRootPath();
|
|
1109
|
-
let sessionsRootStat;
|
|
1110
|
-
try {
|
|
1111
|
-
sessionsRootStat = await stat(sessionsRoot);
|
|
1112
|
-
}
|
|
1113
|
-
catch {
|
|
1114
|
-
return null;
|
|
1115
|
-
}
|
|
1116
|
-
if (!sessionsRootStat.isDirectory()) {
|
|
1117
|
-
return null;
|
|
1118
|
-
}
|
|
1119
|
-
const files = await collectSessionJsonlFiles(sessionsRoot);
|
|
1120
|
-
files.sort((a, b) => a.mtimeMs - b.mtimeMs || a.filePath.localeCompare(b.filePath));
|
|
1121
|
-
for (const file of files) {
|
|
1122
|
-
if (knownFilePaths.has(file.filePath)) {
|
|
1123
|
-
continue;
|
|
1124
|
-
}
|
|
1125
|
-
const sessionId = await readSessionIdFromSessionFile(file.filePath);
|
|
1126
|
-
if (!sessionId) {
|
|
1127
|
-
continue;
|
|
1128
|
-
}
|
|
1129
|
-
knownFilePaths.add(file.filePath);
|
|
1130
|
-
return {
|
|
1131
|
-
sessionId,
|
|
1132
|
-
sessionFilePath: file.filePath,
|
|
1133
|
-
};
|
|
1134
|
-
}
|
|
1135
|
-
return null;
|
|
1136
|
-
}
|
|
1137
|
-
async function updateRunSessionMetadata(task, metadata) {
|
|
1138
|
-
let changed = false;
|
|
1139
|
-
const previousSessionId = task.sessionId;
|
|
1140
|
-
if (!task.sessionId && typeof metadata.sessionId === "string" && metadata.sessionId.trim()) {
|
|
1141
|
-
task.sessionId = metadata.sessionId.trim();
|
|
1142
|
-
changed = true;
|
|
1143
|
-
}
|
|
1144
|
-
if (!task.sessionFilePath && typeof metadata.sessionFilePath === "string" && metadata.sessionFilePath.trim()) {
|
|
1145
|
-
task.sessionFilePath = metadata.sessionFilePath.trim();
|
|
1146
|
-
changed = true;
|
|
1147
|
-
}
|
|
1148
|
-
if (!task.sessionFilePath && task.sessionId) {
|
|
1149
|
-
const resolvedSessionFilePath = await findSessionFilePathBySessionId(task.sessionId).catch(() => null);
|
|
1150
|
-
if (resolvedSessionFilePath) {
|
|
1151
|
-
task.sessionFilePath = resolvedSessionFilePath;
|
|
1152
|
-
changed = true;
|
|
1153
|
-
}
|
|
1154
|
-
}
|
|
1155
|
-
if (!changed) {
|
|
1156
|
-
return;
|
|
1157
|
-
}
|
|
1158
|
-
task.updatedAt = formatLocalTimestamp();
|
|
1159
|
-
await persistRunTask(task).catch(() => undefined);
|
|
1160
|
-
if (metadata.nc) {
|
|
1161
|
-
publishImmediateRunEvent({
|
|
1162
|
-
nc: metadata.nc,
|
|
1163
|
-
userId: task.userId,
|
|
1164
|
-
task,
|
|
1165
|
-
});
|
|
1166
|
-
}
|
|
1167
|
-
if (!previousSessionId && task.sessionId) {
|
|
1168
|
-
await updateRunStartSlotSession({
|
|
1169
|
-
runId: task.id,
|
|
1170
|
-
previousSessionId,
|
|
1171
|
-
sessionId: task.sessionId,
|
|
1172
|
-
}).catch(() => undefined);
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
function normalizePersistedRunTask(value) {
|
|
1176
|
-
if (!value || typeof value !== "object") {
|
|
1177
|
-
return null;
|
|
1178
|
-
}
|
|
1179
|
-
const record = value;
|
|
1180
|
-
const id = typeof record.runId === "string" && record.runId.trim()
|
|
1181
|
-
? record.runId.trim()
|
|
1182
|
-
: typeof record.id === "string" && record.id.trim()
|
|
1183
|
-
? record.id.trim()
|
|
1184
|
-
: "";
|
|
1185
|
-
const userId = typeof record.userId === "string" ? record.userId : "";
|
|
1186
|
-
const agentId = typeof record.agentId === "string" ? record.agentId : "";
|
|
1187
|
-
const status = record.status;
|
|
1188
|
-
if (!id || !userId || !agentId || !["queued", "running", "completed", "failed", "canceled"].includes(String(status))) {
|
|
1189
|
-
return null;
|
|
1190
|
-
}
|
|
1191
|
-
return {
|
|
1192
|
-
id,
|
|
1193
|
-
userId,
|
|
1194
|
-
agentId,
|
|
1195
|
-
processPid: typeof record.processPid === "number" ? record.processPid : null,
|
|
1196
|
-
sessionId: typeof record.sessionId === "string" && record.sessionId.trim() ? record.sessionId.trim() : null,
|
|
1197
|
-
sessionFilePath: typeof record.sessionFilePath === "string" && record.sessionFilePath.trim() ? record.sessionFilePath.trim() : null,
|
|
1198
|
-
status: status,
|
|
1199
|
-
cancelRequested: Boolean(record.cancelRequested),
|
|
1200
|
-
resultExitCode: typeof record.resultExitCode === "number" ? record.resultExitCode : null,
|
|
1201
|
-
resultSignal: typeof record.resultSignal === "string" && record.resultSignal.trim() ? record.resultSignal.trim() : null,
|
|
1202
|
-
error: typeof record.error === "string" && record.error.trim() ? record.error : null,
|
|
1203
|
-
createdAt: typeof record.createdAt === "string" ? record.createdAt : "",
|
|
1204
|
-
updatedAt: typeof record.updatedAt === "string" ? record.updatedAt : "",
|
|
1205
|
-
startedAt: typeof record.startedAt === "string" && record.startedAt.trim() ? record.startedAt : null,
|
|
1206
|
-
finishedAt: typeof record.finishedAt === "string" && record.finishedAt.trim() ? record.finishedAt : null,
|
|
1207
|
-
};
|
|
1208
|
-
}
|
|
1209
|
-
async function listPersistedRunTasks() {
|
|
1210
|
-
const dir = await resolveRunsDir();
|
|
1211
|
-
const names = await readdir(dir).catch(() => []);
|
|
1212
|
-
const tasks = await Promise.all(names
|
|
1213
|
-
.filter((name) => name.endsWith(".json"))
|
|
1214
|
-
.map(async (name) => {
|
|
1215
|
-
const raw = await readFile(path.join(dir, name), "utf8").catch(() => null);
|
|
1216
|
-
if (!raw) {
|
|
1217
|
-
return null;
|
|
1218
|
-
}
|
|
1219
|
-
try {
|
|
1220
|
-
return normalizePersistedRunTask(JSON.parse(raw));
|
|
1221
|
-
}
|
|
1222
|
-
catch {
|
|
1223
|
-
return null;
|
|
1224
|
-
}
|
|
1225
|
-
}));
|
|
1226
|
-
return tasks.filter((task) => task !== null);
|
|
1227
|
-
}
|
|
1228
|
-
async function getStoredRun(runId) {
|
|
1229
|
-
const persisted = await readFile(path.join(await resolveRunsDir(), `${runId}.json`), "utf8").catch(() => null);
|
|
1230
|
-
if (persisted) {
|
|
1231
|
-
try {
|
|
1232
|
-
const parsed = normalizePersistedRunTask(JSON.parse(persisted));
|
|
1233
|
-
if (parsed) {
|
|
1234
|
-
return parsed;
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
catch {
|
|
1238
|
-
// Ignore malformed persisted state.
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
1241
|
-
return null;
|
|
1242
|
-
}
|
|
1243
|
-
async function startManagedRun(args) {
|
|
1244
|
-
const prepared = await prepareCommandExecution({
|
|
1245
|
-
cwd: args.cwd,
|
|
1246
|
-
userId: args.userId,
|
|
1247
|
-
taskId: args.runId,
|
|
1248
|
-
codexAuthBundle: args.codexAuthBundle,
|
|
1249
|
-
runtimeEnvPatch: args.runtimeEnvPatch,
|
|
1250
|
-
});
|
|
1251
|
-
const child = spawnManagedCodexCommand({
|
|
1252
|
-
codexArgs: args.codexArgs,
|
|
1253
|
-
taskWorkspace: prepared.taskWorkspace,
|
|
1254
|
-
env: prepared.env,
|
|
1255
|
-
agentToken: args.agentToken,
|
|
1256
|
-
});
|
|
1257
|
-
const now = formatLocalTimestamp();
|
|
1258
|
-
const task = {
|
|
1259
|
-
id: args.runId,
|
|
1260
|
-
userId: args.userId,
|
|
1261
|
-
agentId: args.agentId,
|
|
1262
|
-
processPid: typeof child.pid === "number" ? child.pid : null,
|
|
1263
|
-
sessionId: typeof args.sessionId === "string" && args.sessionId.trim() ? args.sessionId.trim() : null,
|
|
1264
|
-
sessionFilePath: null,
|
|
1265
|
-
status: "running",
|
|
1266
|
-
cancelRequested: false,
|
|
1267
|
-
resultExitCode: null,
|
|
1268
|
-
resultSignal: null,
|
|
1269
|
-
error: null,
|
|
1270
|
-
createdAt: now,
|
|
1271
|
-
updatedAt: now,
|
|
1272
|
-
startedAt: now,
|
|
1273
|
-
finishedAt: null,
|
|
1274
|
-
};
|
|
1275
|
-
let pendingSessionPollClosed = false;
|
|
1276
|
-
let pendingSessionPollTimer = null;
|
|
1277
|
-
const knownPendingSessionFiles = new Set();
|
|
1278
|
-
const stopPendingSessionPoll = () => {
|
|
1279
|
-
pendingSessionPollClosed = true;
|
|
1280
|
-
if (pendingSessionPollTimer) {
|
|
1281
|
-
clearInterval(pendingSessionPollTimer);
|
|
1282
|
-
pendingSessionPollTimer = null;
|
|
1283
|
-
}
|
|
1284
|
-
};
|
|
1285
|
-
const pollPendingSession = async () => {
|
|
1286
|
-
if (pendingSessionPollClosed || task.sessionId) {
|
|
1287
|
-
stopPendingSessionPoll();
|
|
1288
|
-
return;
|
|
1289
|
-
}
|
|
1290
|
-
const detected = await detectPendingRunSession(knownPendingSessionFiles).catch(() => null);
|
|
1291
|
-
if (!detected) {
|
|
1292
|
-
return;
|
|
1293
|
-
}
|
|
1294
|
-
await updateRunSessionMetadata(task, {
|
|
1295
|
-
sessionId: detected.sessionId,
|
|
1296
|
-
sessionFilePath: detected.sessionFilePath,
|
|
1297
|
-
nc: args.nc,
|
|
1298
|
-
}).catch(() => undefined);
|
|
1299
|
-
if (task.sessionId) {
|
|
1300
|
-
stopPendingSessionPoll();
|
|
1301
|
-
}
|
|
1302
|
-
};
|
|
1303
|
-
if (!task.sessionId) {
|
|
1304
|
-
const existingFiles = await collectSessionJsonlFiles(getSessionsRootPath()).catch(() => []);
|
|
1305
|
-
for (const file of existingFiles) {
|
|
1306
|
-
knownPendingSessionFiles.add(file.filePath);
|
|
1307
|
-
}
|
|
1308
|
-
pendingSessionPollTimer = setInterval(() => {
|
|
1309
|
-
void pollPendingSession();
|
|
1310
|
-
}, 1000);
|
|
1311
|
-
}
|
|
1312
|
-
child.stdout.on("data", (chunk) => writeRunStream(task.id, "stdout", chunk));
|
|
1313
|
-
child.stderr.on("data", (chunk) => writeRunStream(task.id, "stderr", chunk));
|
|
1314
|
-
child.once("error", (error) => {
|
|
1315
|
-
stopPendingSessionPoll();
|
|
1316
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1317
|
-
task.status = "failed";
|
|
1318
|
-
task.error = message;
|
|
1319
|
-
task.finishedAt = formatLocalTimestamp();
|
|
1320
|
-
publishImmediateRunEvent({ nc: args.nc, userId: task.userId, task });
|
|
1321
|
-
publishImmediateRunEvent({ nc: args.nc, userId: task.userId, task, type: "run.finished" });
|
|
1322
|
-
void removeRunTask(task.id).catch(() => undefined);
|
|
1323
|
-
void releaseRunStartSlot({ runId: task.id, sessionId: task.sessionId }).catch(() => undefined);
|
|
1324
|
-
void prepared.codexAuthCleanup().catch(() => undefined);
|
|
1325
|
-
writeRunStatus(task.id, `failed error=${message}`);
|
|
1326
|
-
});
|
|
1327
|
-
child.once("close", async (code, signal) => {
|
|
1328
|
-
stopPendingSessionPoll();
|
|
1329
|
-
const latest = await getStoredRun(task.id).catch(() => null);
|
|
1330
|
-
if (latest?.cancelRequested) {
|
|
1331
|
-
task.cancelRequested = true;
|
|
1332
|
-
}
|
|
1333
|
-
task.resultExitCode = typeof code === "number" ? code : null;
|
|
1334
|
-
task.resultSignal = signal;
|
|
1335
|
-
task.finishedAt = formatLocalTimestamp();
|
|
1336
|
-
task.status = task.cancelRequested ? "canceled" : (task.resultExitCode ?? 1) === 0 ? "completed" : "failed";
|
|
1337
|
-
task.error = task.status === "failed" ? `Command exited with code ${task.resultExitCode ?? "null"}` : null;
|
|
1338
|
-
publishImmediateRunEvent({ nc: args.nc, userId: task.userId, task });
|
|
1339
|
-
publishImmediateRunEvent({ nc: args.nc, userId: task.userId, task, type: "run.finished" });
|
|
1340
|
-
void removeRunTask(task.id).catch(() => undefined);
|
|
1341
|
-
void releaseRunStartSlot({ runId: task.id, sessionId: task.sessionId }).catch(() => undefined);
|
|
1342
|
-
void prepared.codexAuthCleanup().catch(() => undefined);
|
|
1343
|
-
writeRunStatus(task.id, `completed status=${task.status} exitCode=${task.resultExitCode ?? "null"} signal=${task.resultSignal ?? "null"}`);
|
|
1344
|
-
});
|
|
1345
|
-
void persistRunTask(task).catch(() => undefined);
|
|
1346
|
-
publishImmediateRunEvent({ nc: args.nc, userId: task.userId, task });
|
|
1347
|
-
writeRunStatus(task.id, `started requestId=${args.requestId} cwd=${prepared.taskWorkspace}`);
|
|
1348
|
-
if (!task.sessionId) {
|
|
1349
|
-
void pollPendingSession();
|
|
1350
|
-
}
|
|
1351
|
-
return cloneRunTask(task);
|
|
1352
|
-
}
|
|
1353
|
-
function shellSingleQuote(value) {
|
|
1354
|
-
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
1355
|
-
}
|
|
1356
|
-
function stripAnsi(value) {
|
|
1357
|
-
return value.replace(ANSI_RE, "");
|
|
1358
|
-
}
|
|
1359
|
-
function normalizeCodexModel(value) {
|
|
1360
|
-
const normalized = typeof value === "string" ? value.trim() : "";
|
|
1361
|
-
return normalized || "gpt-5.4";
|
|
1362
|
-
}
|
|
1363
|
-
function toTomlStringLiteral(value) {
|
|
1364
|
-
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
1365
|
-
}
|
|
1366
|
-
function buildManagedCodexArgs(args) {
|
|
1367
|
-
const promptArgs = ["--", args.prompt];
|
|
1368
|
-
const fixedArgs = ["--dangerously-bypass-approvals-and-sandbox"];
|
|
1369
|
-
const configArgs = [
|
|
1370
|
-
...(args.personality ? ["--config", `personality=${toTomlStringLiteral(args.personality)}`] : []),
|
|
1371
|
-
...(args.modelInstructionsFile
|
|
1372
|
-
? ["--config", `model_instructions_file=${toTomlStringLiteral(args.modelInstructionsFile)}`]
|
|
1373
|
-
: []),
|
|
1374
|
-
];
|
|
1375
|
-
const imageArgs = args.imagePaths.flatMap((imagePath) => ["--image", imagePath]);
|
|
1376
|
-
return [
|
|
1377
|
-
...fixedArgs,
|
|
1378
|
-
...configArgs,
|
|
1379
|
-
"--model",
|
|
1380
|
-
args.model,
|
|
1381
|
-
...(args.sessionId
|
|
1382
|
-
? ["exec", "resume", ...imageArgs, args.sessionId, ...promptArgs]
|
|
1383
|
-
: ["exec", ...imageArgs, ...promptArgs]),
|
|
1384
|
-
];
|
|
1385
|
-
}
|
|
1386
|
-
function buildLocalCodexCliCommand(args) {
|
|
1387
|
-
const quotedArgs = args.map(shellSingleQuote).join(" ");
|
|
1388
|
-
const direct = `exec codex ${quotedArgs}`;
|
|
1389
|
-
const fallback = `exec npm exec --yes --package doer-agent -- codex ${quotedArgs}`;
|
|
1390
|
-
const script = [
|
|
1391
|
-
"if command -v codex >/dev/null 2>&1; then",
|
|
1392
|
-
` ${direct}`,
|
|
1393
|
-
"fi",
|
|
1394
|
-
fallback,
|
|
1395
|
-
].join("\n");
|
|
1396
|
-
return `bash -lc ${shellSingleQuote(script)}`;
|
|
1397
|
-
}
|
|
1398
|
-
function hasDirectCodexBinary() {
|
|
1399
|
-
const result = spawnSync("bash", ["-lc", "command -v codex >/dev/null 2>&1"], {
|
|
1400
|
-
stdio: "ignore",
|
|
1401
|
-
});
|
|
1402
|
-
return result.status === 0;
|
|
1403
|
-
}
|
|
1404
|
-
function spawnManagedCodexCommand(args) {
|
|
1405
|
-
const env = {
|
|
1406
|
-
...args.env,
|
|
1407
|
-
DOER_AGENT_TOKEN: args.agentToken,
|
|
1408
|
-
};
|
|
1409
|
-
const child = hasDirectCodexBinary()
|
|
1410
|
-
? spawn("codex", args.codexArgs, {
|
|
1411
|
-
cwd: args.taskWorkspace,
|
|
1412
|
-
detached: process.platform !== "win32",
|
|
1413
|
-
env,
|
|
1414
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1415
|
-
})
|
|
1416
|
-
: spawn("npm", ["exec", "--yes", "--package", "doer-agent", "--", "codex", ...args.codexArgs], {
|
|
1417
|
-
cwd: args.taskWorkspace,
|
|
1418
|
-
detached: process.platform !== "win32",
|
|
1419
|
-
env,
|
|
1420
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1421
|
-
});
|
|
1422
|
-
child.stdout?.setEncoding("utf8");
|
|
1423
|
-
child.stderr?.setEncoding("utf8");
|
|
1424
|
-
return child;
|
|
1425
|
-
}
|
|
1426
|
-
async function runLocalCodexCli(args, timeoutMs, envPatch) {
|
|
1427
|
-
const command = buildLocalCodexCliCommand(args);
|
|
1428
|
-
const workspaceRoot = workspaceRootOverride ?? (process.env.WORKSPACE?.trim() || process.cwd());
|
|
1429
|
-
const env = {
|
|
1430
|
-
...process.env,
|
|
1431
|
-
...(envPatch ?? {}),
|
|
1432
|
-
WORKSPACE: workspaceRoot,
|
|
1433
|
-
CODEX_HOME: resolveCodexHomePath(),
|
|
1434
|
-
};
|
|
1435
|
-
return await new Promise((resolve, reject) => {
|
|
1436
|
-
const child = spawn(command, {
|
|
1437
|
-
cwd: workspaceRoot,
|
|
1438
|
-
shell: resolveShellPath(),
|
|
1439
|
-
env,
|
|
1440
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1441
|
-
});
|
|
1442
|
-
let stdout = "";
|
|
1443
|
-
let stderr = "";
|
|
1444
|
-
let done = false;
|
|
1445
|
-
let timedOut = false;
|
|
1446
|
-
child.stdout.setEncoding("utf8");
|
|
1447
|
-
child.stderr.setEncoding("utf8");
|
|
1448
|
-
child.stdout.on("data", (chunk) => {
|
|
1449
|
-
stdout += chunk;
|
|
1450
|
-
});
|
|
1451
|
-
child.stderr.on("data", (chunk) => {
|
|
1452
|
-
stderr += chunk;
|
|
1453
|
-
});
|
|
1454
|
-
const timer = setTimeout(() => {
|
|
1455
|
-
timedOut = true;
|
|
1456
|
-
sendSignalToTaskProcess(child, "SIGTERM");
|
|
1457
|
-
setTimeout(() => sendSignalToTaskProcess(child, "SIGKILL"), 1000);
|
|
1458
|
-
}, Math.max(500, timeoutMs));
|
|
1459
|
-
child.once("error", (error) => {
|
|
1460
|
-
if (done) {
|
|
1461
|
-
return;
|
|
1462
|
-
}
|
|
1463
|
-
done = true;
|
|
1464
|
-
clearTimeout(timer);
|
|
1465
|
-
reject(error);
|
|
1466
|
-
});
|
|
1467
|
-
child.once("exit", (code) => {
|
|
1468
|
-
if (done) {
|
|
1469
|
-
return;
|
|
1470
|
-
}
|
|
1471
|
-
done = true;
|
|
1472
|
-
clearTimeout(timer);
|
|
1473
|
-
resolve({ code, stdout, stderr, timedOut });
|
|
1474
|
-
});
|
|
1475
|
-
});
|
|
1476
|
-
}
|
|
1477
|
-
async function runLocalCodexCliWithInput(args, input, timeoutMs, envPatch) {
|
|
1478
|
-
const command = buildLocalCodexCliCommand(args);
|
|
1479
|
-
const workspaceRoot = workspaceRootOverride ?? (process.env.WORKSPACE?.trim() || process.cwd());
|
|
1480
|
-
const env = {
|
|
1481
|
-
...process.env,
|
|
1482
|
-
...(envPatch ?? {}),
|
|
1483
|
-
WORKSPACE: workspaceRoot,
|
|
1484
|
-
CODEX_HOME: resolveCodexHomePath(),
|
|
1485
|
-
};
|
|
1486
|
-
return await new Promise((resolve, reject) => {
|
|
1487
|
-
const child = spawn(command, {
|
|
1488
|
-
cwd: workspaceRoot,
|
|
1489
|
-
shell: resolveShellPath(),
|
|
1490
|
-
env,
|
|
1491
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
1492
|
-
});
|
|
1493
|
-
let stdout = "";
|
|
1494
|
-
let stderr = "";
|
|
1495
|
-
let done = false;
|
|
1496
|
-
let timedOut = false;
|
|
1497
|
-
child.stdout.setEncoding("utf8");
|
|
1498
|
-
child.stderr.setEncoding("utf8");
|
|
1499
|
-
child.stdout.on("data", (chunk) => {
|
|
1500
|
-
stdout += chunk;
|
|
1501
|
-
});
|
|
1502
|
-
child.stderr.on("data", (chunk) => {
|
|
1503
|
-
stderr += chunk;
|
|
1504
|
-
});
|
|
1505
|
-
child.stdin?.write(input);
|
|
1506
|
-
if (!input.endsWith("\n")) {
|
|
1507
|
-
child.stdin?.write("\n");
|
|
1508
|
-
}
|
|
1509
|
-
child.stdin?.end();
|
|
1510
|
-
const timer = setTimeout(() => {
|
|
1511
|
-
timedOut = true;
|
|
1512
|
-
sendSignalToTaskProcess(child, "SIGTERM");
|
|
1513
|
-
setTimeout(() => sendSignalToTaskProcess(child, "SIGKILL"), 1000);
|
|
1514
|
-
}, Math.max(500, timeoutMs));
|
|
1515
|
-
child.once("error", (error) => {
|
|
1516
|
-
if (done) {
|
|
1517
|
-
return;
|
|
1518
|
-
}
|
|
1519
|
-
done = true;
|
|
1520
|
-
clearTimeout(timer);
|
|
1521
|
-
reject(error);
|
|
1522
|
-
});
|
|
1523
|
-
child.once("exit", (code) => {
|
|
1524
|
-
if (done) {
|
|
1525
|
-
return;
|
|
1526
|
-
}
|
|
1527
|
-
done = true;
|
|
1528
|
-
clearTimeout(timer);
|
|
1529
|
-
resolve({ code, stdout, stderr, timedOut });
|
|
1530
|
-
});
|
|
1531
|
-
});
|
|
1532
|
-
}
|
|
1533
|
-
function buildSkillGeneratorPrompt(userPrompt) {
|
|
1534
|
-
return [
|
|
1535
|
-
"Create a Codex skill from the user's description.",
|
|
1536
|
-
"",
|
|
1537
|
-
"Return JSON only with this shape:",
|
|
1538
|
-
'{ "skillName": "kebab-or-dot-or-underscore-name", "skillMd": "full SKILL.md content" }',
|
|
1539
|
-
"",
|
|
1540
|
-
"Requirements:",
|
|
1541
|
-
"- skillName must be lowercase ASCII and use only letters, numbers, dot, underscore, or dash.",
|
|
1542
|
-
"- skillName must not start with a dot and must not be .system.",
|
|
1543
|
-
"- skillMd must be a complete SKILL.md file.",
|
|
1544
|
-
"- skillMd must start with YAML frontmatter containing name and description.",
|
|
1545
|
-
"- Keep the skill concise and action-oriented.",
|
|
1546
|
-
"- Include when to use the skill, the core workflow, and any important constraints.",
|
|
1547
|
-
"- Do not add README, changelog, or any extra files.",
|
|
1548
|
-
"",
|
|
1549
|
-
"User request:",
|
|
1550
|
-
userPrompt.trim(),
|
|
1551
|
-
].join("\n");
|
|
1552
|
-
}
|
|
1553
|
-
function extractJsonObject(value) {
|
|
1554
|
-
const trimmed = value.trim();
|
|
1555
|
-
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
1556
|
-
return trimmed;
|
|
1557
|
-
}
|
|
1558
|
-
const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
1559
|
-
if (fenced?.[1]) {
|
|
1560
|
-
return fenced[1].trim();
|
|
1561
|
-
}
|
|
1562
|
-
const start = trimmed.indexOf("{");
|
|
1563
|
-
const end = trimmed.lastIndexOf("}");
|
|
1564
|
-
if (start >= 0 && end > start) {
|
|
1565
|
-
return trimmed.slice(start, end + 1);
|
|
1566
|
-
}
|
|
1567
|
-
throw new Error("Codex did not return JSON");
|
|
1568
|
-
}
|
|
1569
|
-
function slugifySkillName(value) {
|
|
1570
|
-
return value
|
|
1571
|
-
.trim()
|
|
1572
|
-
.toLowerCase()
|
|
1573
|
-
.replace(/[^a-z0-9._-]+/g, "-")
|
|
1574
|
-
.replace(/^-+|-+$/g, "")
|
|
1575
|
-
.replace(/-{2,}/g, "-");
|
|
1576
|
-
}
|
|
1577
|
-
function normalizeGeneratedSkill(value) {
|
|
1578
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1579
|
-
throw new Error("Invalid generated skill payload");
|
|
1580
|
-
}
|
|
1581
|
-
const row = value;
|
|
1582
|
-
const skillName = slugifySkillName(typeof row.skillName === "string" ? row.skillName : "");
|
|
1583
|
-
const skillMd = typeof row.skillMd === "string" ? row.skillMd.trim() : "";
|
|
1584
|
-
if (!skillName || skillName.startsWith(".") || skillName === ".system") {
|
|
1585
|
-
throw new Error("Codex returned an invalid skill name");
|
|
1586
|
-
}
|
|
1587
|
-
if (!skillMd) {
|
|
1588
|
-
throw new Error("Codex returned an empty SKILL.md");
|
|
1589
|
-
}
|
|
1590
|
-
if (!/^---\s*\n[\s\S]*?\n---\s*\n/m.test(skillMd)) {
|
|
1591
|
-
throw new Error("Generated SKILL.md is missing YAML frontmatter");
|
|
1592
|
-
}
|
|
1593
|
-
if (!/\nname:\s*[^\n]+/i.test(skillMd) || !/\ndescription:\s*[^\n]+/i.test(skillMd)) {
|
|
1594
|
-
throw new Error("Generated SKILL.md frontmatter is incomplete");
|
|
1595
|
-
}
|
|
1596
|
-
return { skillName, skillMd };
|
|
1597
|
-
}
|
|
1598
|
-
function buildSkillGeneratorCodexArgs(prompt, model) {
|
|
1599
|
-
return ["--dangerously-bypass-approvals-and-sandbox", "--model", model, "exec", "--", prompt];
|
|
1600
|
-
}
|
|
1601
|
-
async function generateSkillViaCodex(userPrompt) {
|
|
1602
|
-
const localAgentSettings = await readAgentSettingsConfig(null);
|
|
1603
|
-
const envPatch = buildAgentSettingsEnvPatch(localAgentSettings);
|
|
1604
|
-
const prompt = buildSkillGeneratorPrompt(userPrompt);
|
|
1605
|
-
const result = await runLocalCodexCli(buildSkillGeneratorCodexArgs(prompt, localAgentSettings.codex.model || "gpt-5.4"), 120_000, envPatch);
|
|
1606
|
-
if (result.timedOut) {
|
|
1607
|
-
throw new Error("Codex timed out while generating the skill");
|
|
1608
|
-
}
|
|
1609
|
-
if ((result.code ?? 1) !== 0) {
|
|
1610
|
-
const details = stripAnsi(result.stderr || result.stdout).trim();
|
|
1611
|
-
throw new Error(details || `Codex exited with code ${result.code ?? "null"}`);
|
|
1612
|
-
}
|
|
1613
|
-
const payload = JSON.parse(extractJsonObject(stripAnsi(result.stdout)));
|
|
1614
|
-
const generated = normalizeGeneratedSkill(payload);
|
|
1615
|
-
const skillPath = path.join(resolveCodexHomePath(), "skills", generated.skillName);
|
|
1616
|
-
const skillFilePath = path.join(skillPath, "SKILL.md");
|
|
1617
|
-
try {
|
|
1618
|
-
await stat(skillPath);
|
|
1619
|
-
throw new Error("A skill with that name already exists");
|
|
1620
|
-
}
|
|
1621
|
-
catch (error) {
|
|
1622
|
-
if (!(error instanceof Error) || !/ENOENT/i.test(error.message)) {
|
|
1623
|
-
throw error;
|
|
1624
|
-
}
|
|
1625
|
-
}
|
|
1626
|
-
await mkdir(skillPath, { recursive: true });
|
|
1627
|
-
await writeFile(skillFilePath, `${generated.skillMd}\n`, "utf8");
|
|
1628
|
-
return {
|
|
1629
|
-
skillName: generated.skillName,
|
|
1630
|
-
skillPath: `.codex/skills/${generated.skillName}`,
|
|
1631
|
-
skillFilePath: `.codex/skills/${generated.skillName}/SKILL.md`,
|
|
1632
|
-
};
|
|
1633
|
-
}
|
|
1634
|
-
async function handleSkillRpcMessage(args) {
|
|
1635
|
-
let payload = {};
|
|
1636
|
-
try {
|
|
1637
|
-
payload = JSON.parse(skillRpcCodec.decode(args.msg.data));
|
|
1638
|
-
if (typeof payload.agentId === "string" && payload.agentId.trim() && payload.agentId !== args.agentId) {
|
|
1639
|
-
throw new Error("agent id mismatch");
|
|
1640
|
-
}
|
|
1641
|
-
const prompt = typeof payload.prompt === "string" ? payload.prompt.trim() : "";
|
|
1642
|
-
if (!prompt) {
|
|
1643
|
-
throw new Error("prompt is required");
|
|
1644
|
-
}
|
|
1645
|
-
const result = await generateSkillViaCodex(prompt);
|
|
1646
|
-
args.msg.respond(skillRpcCodec.encode(JSON.stringify({
|
|
1647
|
-
ok: true,
|
|
1648
|
-
skillName: result.skillName,
|
|
1649
|
-
skillPath: result.skillPath,
|
|
1650
|
-
skillFilePath: result.skillFilePath,
|
|
1651
|
-
})));
|
|
1652
|
-
}
|
|
1653
|
-
catch (error) {
|
|
1654
|
-
const message = error instanceof Error ? error.message : "unknown error";
|
|
1655
|
-
args.msg.respond(skillRpcCodec.encode(JSON.stringify({
|
|
1656
|
-
ok: false,
|
|
1657
|
-
error: message,
|
|
1658
|
-
})));
|
|
1659
|
-
writeAgentError(`skill rpc failed error=${message}`);
|
|
1660
|
-
}
|
|
1661
|
-
}
|
|
1662
|
-
function subscribeToSkillRpc(args) {
|
|
1663
|
-
const subject = buildAgentSkillRpcSubject(args.userId, args.agentId);
|
|
1664
|
-
args.jetstream.nc.subscribe(subject, {
|
|
1665
|
-
callback: (error, msg) => {
|
|
1666
|
-
if (error) {
|
|
1667
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1668
|
-
writeAgentError(`skill rpc subscription error: ${message}`);
|
|
1669
|
-
return;
|
|
1670
|
-
}
|
|
1671
|
-
void handleSkillRpcMessage({
|
|
1672
|
-
msg,
|
|
1673
|
-
agentId: args.agentId,
|
|
1674
|
-
});
|
|
1675
|
-
},
|
|
1676
|
-
});
|
|
1677
|
-
writeAgentInfo(`skill rpc subscribed subject=${subject}`);
|
|
1678
|
-
}
|
|
1679
|
-
function parseCodexDeviceAuthOutput(raw) {
|
|
1680
|
-
const text = stripAnsi(raw);
|
|
1681
|
-
const urlMatch = text.match(/https?:\/\/[^\s]+/i);
|
|
1682
|
-
const codeMatch = text.match(/\b[A-Z0-9]{4,}(?:-[A-Z0-9]{4,})+\b/);
|
|
1683
|
-
return {
|
|
1684
|
-
verificationUri: urlMatch?.[0] ?? null,
|
|
1685
|
-
userCode: codeMatch?.[0] ?? null,
|
|
1686
|
-
};
|
|
1687
|
-
}
|
|
1688
|
-
function pendingCodexDeviceAuthMessage(state) {
|
|
1689
|
-
const parsed = parseCodexDeviceAuthOutput(state.output);
|
|
1690
|
-
if (parsed.verificationUri && parsed.userCode) {
|
|
1691
|
-
return `Waiting for approval. Enter code ${parsed.userCode} at ${parsed.verificationUri}`;
|
|
1692
|
-
}
|
|
1693
|
-
return stripAnsi(state.output).trim() || "Waiting for approval";
|
|
1694
|
-
}
|
|
1695
|
-
async function getLocalCodexLoginStatus() {
|
|
1696
|
-
const result = await runLocalCodexCli(["login", "status"], 5000);
|
|
1697
|
-
const merged = stripAnsi([result.stdout, result.stderr].filter(Boolean).join("\n")).trim();
|
|
1698
|
-
return {
|
|
1699
|
-
loggedIn: (result.code ?? 1) === 0,
|
|
1700
|
-
output: merged || ((result.code ?? 1) === 0 ? "Logged in" : "Not logged in"),
|
|
1701
|
-
};
|
|
1702
|
-
}
|
|
1703
|
-
async function waitForCodexDeviceCode(state, timeoutMs) {
|
|
1704
|
-
const startedAt = Date.now();
|
|
1705
|
-
while (Date.now() - startedAt < timeoutMs) {
|
|
1706
|
-
const parsed = parseCodexDeviceAuthOutput(state.output);
|
|
1707
|
-
if (parsed.verificationUri && parsed.userCode) {
|
|
1708
|
-
return;
|
|
1709
|
-
}
|
|
1710
|
-
if (state.child.exitCode !== null) {
|
|
1711
|
-
return;
|
|
1712
|
-
}
|
|
1713
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1714
|
-
}
|
|
1715
|
-
}
|
|
1716
|
-
function normalizeSettingsRpcRequest(args) {
|
|
1717
|
-
const requestId = typeof args.request.requestId === "string" ? args.request.requestId.trim() : "";
|
|
1718
|
-
const responseSubject = typeof args.request.responseSubject === "string" ? args.request.responseSubject.trim() : "";
|
|
1719
|
-
const requestAgentId = typeof args.request.agentId === "string" ? args.request.agentId.trim() : "";
|
|
1720
|
-
const action = args.request.action === "update" ? "update" : "get";
|
|
1721
|
-
if (!requestId || !responseSubject || !requestAgentId || requestAgentId !== args.agentId) {
|
|
1722
|
-
throw new Error("invalid settings rpc request");
|
|
1723
|
-
}
|
|
1724
|
-
return {
|
|
1725
|
-
requestId,
|
|
1726
|
-
responseSubject,
|
|
1727
|
-
action,
|
|
1728
|
-
patch: normalizeAgentSettingsPatch(args.request.patch),
|
|
1729
|
-
defaults: args.request.defaults && typeof args.request.defaults === "object" && !Array.isArray(args.request.defaults)
|
|
1730
|
-
? normalizeAgentSettingsConfig(args.request.defaults)
|
|
1731
|
-
: null,
|
|
1732
|
-
};
|
|
1733
|
-
}
|
|
1734
|
-
function publishSettingsRpcResponse(args) {
|
|
1735
|
-
args.nc.publish(args.responseSubject, settingsRpcCodec.encode(JSON.stringify(args.payload)));
|
|
1736
|
-
}
|
|
1737
|
-
async function handleSettingsRpcMessage(args) {
|
|
1738
|
-
let requestId = "unknown";
|
|
1739
|
-
let responseSubject = "";
|
|
1740
|
-
try {
|
|
1741
|
-
const payload = JSON.parse(settingsRpcCodec.decode(args.msg.data));
|
|
1742
|
-
const request = normalizeSettingsRpcRequest({ request: payload, agentId: args.agentId });
|
|
1743
|
-
requestId = request.requestId;
|
|
1744
|
-
responseSubject = request.responseSubject;
|
|
1745
|
-
const existing = await readAgentSettingsConfig(request.defaults);
|
|
1746
|
-
const next = request.action === "update" ? normalizeAgentSettingsConfig(request.patch, existing) : existing;
|
|
1747
|
-
if (request.action === "update") {
|
|
1748
|
-
await writeAgentSettingsConfig(next);
|
|
1749
|
-
const customInstructions = typeof request.patch.customInstructions === "string"
|
|
1750
|
-
? request.patch.customInstructions
|
|
1751
|
-
: request.patch.customInstructions === null
|
|
1752
|
-
? null
|
|
1753
|
-
: undefined;
|
|
1754
|
-
if (customInstructions !== undefined) {
|
|
1755
|
-
await writeAgentModelInstructions(customInstructions);
|
|
1756
|
-
}
|
|
1757
|
-
}
|
|
1758
|
-
else if (request.defaults) {
|
|
1759
|
-
const filePath = resolveAgentSettingsFilePath();
|
|
1760
|
-
const raw = await readFile(filePath, "utf8").catch(() => "");
|
|
1761
|
-
if (!raw.trim()) {
|
|
1762
|
-
await writeAgentSettingsConfig(next);
|
|
1763
|
-
}
|
|
1764
|
-
}
|
|
1765
|
-
publishSettingsRpcResponse({
|
|
1766
|
-
nc: args.jetstream.nc,
|
|
1767
|
-
responseSubject,
|
|
1768
|
-
payload: {
|
|
1769
|
-
requestId,
|
|
1770
|
-
ok: true,
|
|
1771
|
-
settings: await toAgentSettingsPublic(next),
|
|
1772
|
-
},
|
|
1773
|
-
});
|
|
1774
|
-
}
|
|
1775
|
-
catch (error) {
|
|
1776
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1777
|
-
if (responseSubject) {
|
|
1778
|
-
publishSettingsRpcResponse({
|
|
1779
|
-
nc: args.jetstream.nc,
|
|
1780
|
-
responseSubject,
|
|
1781
|
-
payload: { requestId, ok: false, error: message },
|
|
1782
|
-
});
|
|
1783
|
-
}
|
|
1784
|
-
writeAgentError(`settings rpc failed requestId=${requestId} error=${message}`);
|
|
1785
|
-
}
|
|
1786
|
-
}
|
|
1787
|
-
function subscribeToSettingsRpc(args) {
|
|
1788
|
-
const subject = buildAgentSettingsRpcSubject(args.userId, args.agentId);
|
|
1789
|
-
args.jetstream.nc.subscribe(subject, {
|
|
1790
|
-
callback: (error, msg) => {
|
|
1791
|
-
if (error) {
|
|
1792
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1793
|
-
writeAgentError(`settings rpc subscription error: ${message}`);
|
|
1794
|
-
return;
|
|
1795
|
-
}
|
|
1796
|
-
void handleSettingsRpcMessage({
|
|
1797
|
-
msg,
|
|
1798
|
-
jetstream: args.jetstream,
|
|
1799
|
-
agentId: args.agentId,
|
|
1800
|
-
});
|
|
1801
|
-
},
|
|
1802
|
-
});
|
|
1803
|
-
writeAgentInfo(`settings rpc subscribed subject=${subject}`);
|
|
1804
|
-
}
|
|
1805
|
-
async function startLocalCodexDeviceAuth() {
|
|
1806
|
-
if (pendingCodexDeviceAuth && pendingCodexDeviceAuth.child.exitCode === null) {
|
|
1807
|
-
const parsed = parseCodexDeviceAuthOutput(pendingCodexDeviceAuth.output);
|
|
1808
|
-
return {
|
|
1809
|
-
loggedIn: false,
|
|
1810
|
-
output: pendingCodexDeviceAuthMessage(pendingCodexDeviceAuth),
|
|
1811
|
-
verificationUri: parsed.verificationUri,
|
|
1812
|
-
userCode: parsed.userCode,
|
|
1813
|
-
};
|
|
1814
|
-
}
|
|
1815
|
-
const workspaceRoot = workspaceRootOverride ?? (process.env.WORKSPACE?.trim() || process.cwd());
|
|
1816
|
-
const child = spawn(buildLocalCodexCliCommand(["login", "--device-auth"]), {
|
|
1817
|
-
cwd: workspaceRoot,
|
|
1818
|
-
shell: resolveShellPath(),
|
|
1819
|
-
detached: process.platform !== "win32",
|
|
1820
|
-
env: {
|
|
1821
|
-
...process.env,
|
|
1822
|
-
WORKSPACE: workspaceRoot,
|
|
1823
|
-
CODEX_HOME: resolveCodexHomePath(),
|
|
1824
|
-
},
|
|
1825
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1826
|
-
});
|
|
1827
|
-
child.stdout.setEncoding("utf8");
|
|
1828
|
-
child.stderr.setEncoding("utf8");
|
|
1829
|
-
const state = { child, output: "" };
|
|
1830
|
-
pendingCodexDeviceAuth = state;
|
|
1831
|
-
const appendOutput = (chunk) => {
|
|
1832
|
-
state.output += chunk;
|
|
1833
|
-
};
|
|
1834
|
-
child.stdout.on("data", appendOutput);
|
|
1835
|
-
child.stderr.on("data", appendOutput);
|
|
1836
|
-
child.once("exit", () => {
|
|
1837
|
-
if (pendingCodexDeviceAuth === state) {
|
|
1838
|
-
pendingCodexDeviceAuth = null;
|
|
1839
|
-
}
|
|
1840
|
-
});
|
|
1841
|
-
await waitForCodexDeviceCode(state, 8000);
|
|
1842
|
-
const parsed = parseCodexDeviceAuthOutput(state.output);
|
|
1843
|
-
if ((!parsed.verificationUri || !parsed.userCode) && state.child.exitCode !== null) {
|
|
1844
|
-
throw new Error("Failed to read device code from Codex CLI");
|
|
1845
|
-
}
|
|
1846
|
-
return {
|
|
1847
|
-
loggedIn: false,
|
|
1848
|
-
output: pendingCodexDeviceAuthMessage(state),
|
|
1849
|
-
verificationUri: parsed.verificationUri,
|
|
1850
|
-
userCode: parsed.userCode,
|
|
1851
|
-
};
|
|
1852
|
-
}
|
|
1853
|
-
async function startLocalCodexLogin() {
|
|
1854
|
-
const workspaceRoot = workspaceRootOverride ?? (process.env.WORKSPACE?.trim() || process.cwd());
|
|
1855
|
-
const child = spawn(buildLocalCodexCliCommand(["login"]), {
|
|
1856
|
-
cwd: workspaceRoot,
|
|
1857
|
-
shell: resolveShellPath(),
|
|
1858
|
-
detached: process.platform !== "win32",
|
|
1859
|
-
env: {
|
|
1860
|
-
...process.env,
|
|
1861
|
-
WORKSPACE: workspaceRoot,
|
|
1862
|
-
CODEX_HOME: resolveCodexHomePath(),
|
|
1863
|
-
},
|
|
1864
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1865
|
-
});
|
|
1866
|
-
let output = "";
|
|
1867
|
-
child.stdout.setEncoding("utf8");
|
|
1868
|
-
child.stderr.setEncoding("utf8");
|
|
1869
|
-
child.stdout.on("data", (chunk) => {
|
|
1870
|
-
output += chunk;
|
|
1871
|
-
});
|
|
1872
|
-
child.stderr.on("data", (chunk) => {
|
|
1873
|
-
output += chunk;
|
|
1874
|
-
});
|
|
1875
|
-
const result = await new Promise((resolve, reject) => {
|
|
1876
|
-
child.once("error", reject);
|
|
1877
|
-
child.once("exit", (code) => resolve({ code, output }));
|
|
1878
|
-
});
|
|
1879
|
-
const normalized = stripAnsi(result.output).trim();
|
|
1880
|
-
const parsed = parseCodexDeviceAuthOutput(result.output);
|
|
1881
|
-
if ((result.code ?? 1) === 0) {
|
|
1882
|
-
const status = await getLocalCodexLoginStatus().catch(() => null);
|
|
1883
|
-
return {
|
|
1884
|
-
loggedIn: status?.loggedIn === true,
|
|
1885
|
-
output: status?.output || normalized || "Login started",
|
|
1886
|
-
verificationUri: parsed.verificationUri,
|
|
1887
|
-
userCode: parsed.userCode,
|
|
1888
|
-
};
|
|
1889
|
-
}
|
|
1890
|
-
throw new Error(normalized || `Codex login failed with code ${result.code ?? "null"}`);
|
|
1891
|
-
}
|
|
1892
|
-
async function loginLocalCodexWithApiKey(apiKey) {
|
|
1893
|
-
const result = await runLocalCodexCliWithInput(["login", "--with-api-key"], apiKey, 15000);
|
|
1894
|
-
const normalized = stripAnsi([result.stdout, result.stderr].filter(Boolean).join("\n")).trim();
|
|
1895
|
-
if ((result.code ?? 1) !== 0) {
|
|
1896
|
-
throw new Error(normalized || `Codex API key login failed with code ${result.code ?? "null"}`);
|
|
1897
|
-
}
|
|
1898
|
-
const status = await getLocalCodexLoginStatus().catch(() => null);
|
|
1899
|
-
return {
|
|
1900
|
-
loggedIn: status?.loggedIn === true,
|
|
1901
|
-
output: status?.output || normalized || "Logged in",
|
|
1902
|
-
verificationUri: null,
|
|
1903
|
-
userCode: null,
|
|
1904
|
-
};
|
|
1905
|
-
}
|
|
1906
|
-
async function logoutLocalCodexAuth() {
|
|
1907
|
-
if (pendingCodexDeviceAuth && pendingCodexDeviceAuth.child.exitCode === null) {
|
|
1908
|
-
sendSignalToTaskProcess(pendingCodexDeviceAuth.child, "SIGTERM");
|
|
1909
|
-
setTimeout(() => {
|
|
1910
|
-
if (pendingCodexDeviceAuth?.child.exitCode === null) {
|
|
1911
|
-
sendSignalToTaskProcess(pendingCodexDeviceAuth.child, "SIGKILL");
|
|
1912
|
-
}
|
|
1913
|
-
}, 1000);
|
|
1914
|
-
pendingCodexDeviceAuth = null;
|
|
1915
|
-
}
|
|
1916
|
-
const result = await runLocalCodexCli(["logout"], 5000);
|
|
1917
|
-
let merged = stripAnsi([result.stdout, result.stderr].filter(Boolean).join("\n")).trim();
|
|
1918
|
-
const statusAfterLogout = await getLocalCodexLoginStatus().catch(() => null);
|
|
1919
|
-
if (statusAfterLogout?.loggedIn) {
|
|
1920
|
-
const authFile = path.join(resolveCodexHomePath(), "auth.json");
|
|
1921
|
-
await unlink(authFile).catch(() => undefined);
|
|
1922
|
-
const statusAfterDelete = await getLocalCodexLoginStatus().catch(() => null);
|
|
1923
|
-
if (statusAfterDelete?.output) {
|
|
1924
|
-
merged = [merged, statusAfterDelete.output].filter(Boolean).join("\n");
|
|
1925
|
-
}
|
|
1926
|
-
}
|
|
1927
|
-
return {
|
|
1928
|
-
loggedIn: false,
|
|
1929
|
-
output: merged || "Logged out",
|
|
1930
|
-
};
|
|
1931
|
-
}
|
|
1932
|
-
function normalizeCodexAuthRpcRequest(args) {
|
|
1933
|
-
const requestId = typeof args.request.requestId === "string" ? args.request.requestId.trim() : "";
|
|
1934
|
-
const responseSubject = typeof args.request.responseSubject === "string" ? args.request.responseSubject.trim() : "";
|
|
1935
|
-
const requestAgentId = typeof args.request.agentId === "string" ? args.request.agentId.trim() : "";
|
|
1936
|
-
const actionRaw = typeof args.request.action === "string" ? args.request.action.trim() : "";
|
|
1937
|
-
const action = actionRaw === "start" || actionRaw === "logout" || actionRaw === "login_api_key" ? actionRaw : "status";
|
|
1938
|
-
const apiKey = typeof args.request.apiKey === "string" && args.request.apiKey.trim() ? args.request.apiKey.trim() : null;
|
|
1939
|
-
if (!requestId || !responseSubject || !requestAgentId || requestAgentId !== args.agentId) {
|
|
1940
|
-
throw new Error("invalid codex auth rpc request");
|
|
1941
|
-
}
|
|
1942
|
-
if (action === "login_api_key" && !apiKey) {
|
|
1943
|
-
throw new Error("api key is required");
|
|
1944
|
-
}
|
|
1945
|
-
return { requestId, responseSubject, action, apiKey };
|
|
1946
|
-
}
|
|
1947
|
-
function publishCodexAuthRpcResponse(args) {
|
|
1948
|
-
args.nc.publish(args.responseSubject, codexAuthRpcCodec.encode(JSON.stringify(args.payload)));
|
|
1949
|
-
}
|
|
1950
|
-
async function handleCodexAuthRpcMessage(args) {
|
|
1951
|
-
let requestId = "unknown";
|
|
1952
|
-
let responseSubject = "";
|
|
1953
|
-
try {
|
|
1954
|
-
const payload = JSON.parse(codexAuthRpcCodec.decode(args.msg.data));
|
|
1955
|
-
const request = normalizeCodexAuthRpcRequest({ request: payload, agentId: args.agentId });
|
|
1956
|
-
requestId = request.requestId;
|
|
1957
|
-
responseSubject = request.responseSubject;
|
|
1958
|
-
let result = null;
|
|
1959
|
-
if (request.action === "login_api_key") {
|
|
1960
|
-
result = await loginLocalCodexWithApiKey(request.apiKey ?? "");
|
|
1961
|
-
}
|
|
1962
|
-
else if (request.action === "start") {
|
|
1963
|
-
const status = await getLocalCodexLoginStatus();
|
|
1964
|
-
if (status.loggedIn) {
|
|
1965
|
-
result = { loggedIn: true, output: status.output };
|
|
1966
|
-
}
|
|
1967
|
-
else {
|
|
1968
|
-
try {
|
|
1969
|
-
result = await startLocalCodexDeviceAuth();
|
|
1970
|
-
}
|
|
1971
|
-
catch (error) {
|
|
1972
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1973
|
-
const normalized = message.toLowerCase();
|
|
1974
|
-
if (normalized.includes("operation not permitted") ||
|
|
1975
|
-
normalized.includes("failed to read device code") ||
|
|
1976
|
-
normalized.includes("panic") ||
|
|
1977
|
-
normalized.includes("null object")) {
|
|
1978
|
-
result = await startLocalCodexLogin();
|
|
1979
|
-
}
|
|
1980
|
-
else {
|
|
1981
|
-
throw error;
|
|
1982
|
-
}
|
|
1983
|
-
}
|
|
1984
|
-
}
|
|
1985
|
-
}
|
|
1986
|
-
else if (request.action === "logout") {
|
|
1987
|
-
result = await logoutLocalCodexAuth();
|
|
1988
|
-
}
|
|
1989
|
-
else {
|
|
1990
|
-
const status = await getLocalCodexLoginStatus();
|
|
1991
|
-
if (status.loggedIn) {
|
|
1992
|
-
result = { loggedIn: true, output: status.output };
|
|
1993
|
-
}
|
|
1994
|
-
else if (pendingCodexDeviceAuth && pendingCodexDeviceAuth.child.exitCode === null) {
|
|
1995
|
-
const parsed = parseCodexDeviceAuthOutput(pendingCodexDeviceAuth.output);
|
|
1996
|
-
result = {
|
|
1997
|
-
loggedIn: false,
|
|
1998
|
-
output: pendingCodexDeviceAuthMessage(pendingCodexDeviceAuth),
|
|
1999
|
-
verificationUri: parsed.verificationUri,
|
|
2000
|
-
userCode: parsed.userCode,
|
|
2001
|
-
};
|
|
2002
|
-
}
|
|
2003
|
-
else {
|
|
2004
|
-
result = { loggedIn: false, output: status.output || "Not logged in" };
|
|
2005
|
-
}
|
|
2006
|
-
}
|
|
2007
|
-
publishCodexAuthRpcResponse({
|
|
2008
|
-
nc: args.jetstream.nc,
|
|
2009
|
-
responseSubject,
|
|
2010
|
-
payload: {
|
|
2011
|
-
requestId,
|
|
2012
|
-
ok: true,
|
|
2013
|
-
loggedIn: result.loggedIn,
|
|
2014
|
-
output: result.output,
|
|
2015
|
-
verificationUri: result.verificationUri ?? null,
|
|
2016
|
-
userCode: result.userCode ?? null,
|
|
2017
|
-
},
|
|
2018
|
-
});
|
|
2019
|
-
}
|
|
2020
|
-
catch (error) {
|
|
2021
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2022
|
-
if (responseSubject) {
|
|
2023
|
-
publishCodexAuthRpcResponse({
|
|
2024
|
-
nc: args.jetstream.nc,
|
|
2025
|
-
responseSubject,
|
|
2026
|
-
payload: { requestId, ok: false, error: message },
|
|
2027
|
-
});
|
|
2028
|
-
}
|
|
2029
|
-
writeAgentError(`codex auth rpc failed requestId=${requestId} error=${message}`);
|
|
2030
|
-
}
|
|
2031
|
-
}
|
|
2032
|
-
function subscribeToCodexAuthRpc(args) {
|
|
2033
|
-
const subject = buildAgentCodexAuthRpcSubject(args.userId, args.agentId);
|
|
2034
|
-
args.jetstream.nc.subscribe(subject, {
|
|
2035
|
-
callback: (error, msg) => {
|
|
2036
|
-
if (error) {
|
|
2037
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2038
|
-
writeAgentError(`codex auth rpc subscription error: ${message}`);
|
|
2039
|
-
return;
|
|
2040
|
-
}
|
|
2041
|
-
void handleCodexAuthRpcMessage({
|
|
2042
|
-
msg,
|
|
2043
|
-
jetstream: args.jetstream,
|
|
2044
|
-
agentId: args.agentId,
|
|
2045
|
-
});
|
|
2046
|
-
},
|
|
2047
|
-
});
|
|
2048
|
-
writeAgentInfo(`codex auth rpc subscribed subject=${subject}`);
|
|
2049
|
-
}
|
|
2050
|
-
function runLocalCommand(command, args, cwd) {
|
|
2051
|
-
return new Promise((resolve, reject) => {
|
|
2052
|
-
const child = spawn(command, args, {
|
|
2053
|
-
cwd,
|
|
2054
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
2055
|
-
});
|
|
2056
|
-
let stdout = "";
|
|
2057
|
-
let stderr = "";
|
|
2058
|
-
child.stdout.setEncoding("utf8");
|
|
2059
|
-
child.stderr.setEncoding("utf8");
|
|
2060
|
-
child.stdout.on("data", (chunk) => {
|
|
2061
|
-
stdout += chunk;
|
|
2062
|
-
});
|
|
2063
|
-
child.stderr.on("data", (chunk) => {
|
|
2064
|
-
stderr += chunk;
|
|
2065
|
-
});
|
|
2066
|
-
child.once("error", reject);
|
|
2067
|
-
child.once("close", (code) => {
|
|
2068
|
-
resolve({ code: code ?? 1, stdout, stderr });
|
|
2069
|
-
});
|
|
2070
|
-
});
|
|
2071
|
-
}
|
|
2072
|
-
function sanitizeGitRef(value) {
|
|
2073
|
-
if (typeof value !== "string" || !value.trim()) {
|
|
2074
|
-
return null;
|
|
2075
|
-
}
|
|
2076
|
-
const trimmed = value.trim();
|
|
2077
|
-
if (trimmed.startsWith("-") || /\s/.test(trimmed) || trimmed.includes("..") || trimmed.includes(":")) {
|
|
2078
|
-
throw new Error(`Invalid git ref: ${trimmed}`);
|
|
2079
|
-
}
|
|
2080
|
-
return trimmed;
|
|
2081
|
-
}
|
|
2082
|
-
function sanitizeGitPathspec(value) {
|
|
2083
|
-
if (typeof value !== "string") {
|
|
2084
|
-
throw new Error("Invalid pathspec");
|
|
2085
|
-
}
|
|
2086
|
-
const trimmed = value.trim().replace(/\\/g, "/");
|
|
2087
|
-
if (!trimmed || trimmed.startsWith("-") || trimmed.includes("\0")) {
|
|
2088
|
-
throw new Error(`Invalid pathspec: ${trimmed}`);
|
|
2089
|
-
}
|
|
2090
|
-
return trimmed;
|
|
2091
|
-
}
|
|
2092
|
-
function normalizeGitRpcRequest(args) {
|
|
2093
|
-
const requestId = typeof args.request.requestId === "string" ? args.request.requestId.trim() : "";
|
|
2094
|
-
const responseSubject = typeof args.request.responseSubject === "string" ? args.request.responseSubject.trim() : "";
|
|
2095
|
-
const requestAgentId = typeof args.request.agentId === "string" ? args.request.agentId.trim() : "";
|
|
2096
|
-
const targetPath = typeof args.request.targetPath === "string" ? args.request.targetPath.trim() : "";
|
|
2097
|
-
if (!requestId || !responseSubject || !requestAgentId || requestAgentId !== args.agentId || !targetPath) {
|
|
2098
|
-
throw new Error("invalid git rpc request");
|
|
2099
|
-
}
|
|
2100
|
-
const format = args.request.format === "name-only" ||
|
|
2101
|
-
args.request.format === "name-status" ||
|
|
2102
|
-
args.request.format === "stat" ||
|
|
2103
|
-
args.request.format === "numstat" ||
|
|
2104
|
-
args.request.format === "raw"
|
|
2105
|
-
? args.request.format
|
|
2106
|
-
: "patch";
|
|
2107
|
-
const ignoreWhitespace = args.request.ignoreWhitespace === "at-eol" ||
|
|
2108
|
-
args.request.ignoreWhitespace === "change" ||
|
|
2109
|
-
args.request.ignoreWhitespace === "all"
|
|
2110
|
-
? args.request.ignoreWhitespace
|
|
2111
|
-
: "none";
|
|
2112
|
-
const diffAlgorithm = args.request.diffAlgorithm === "minimal" ||
|
|
2113
|
-
args.request.diffAlgorithm === "patience" ||
|
|
2114
|
-
args.request.diffAlgorithm === "histogram"
|
|
2115
|
-
? args.request.diffAlgorithm
|
|
2116
|
-
: "default";
|
|
2117
|
-
const contextRaw = Number(args.request.contextLines);
|
|
2118
|
-
const contextLines = Number.isFinite(contextRaw) ? Math.max(0, Math.min(200, Math.trunc(contextRaw))) : null;
|
|
2119
|
-
const pathspecs = Array.isArray(args.request.pathspecs) ? args.request.pathspecs.map((item) => sanitizeGitPathspec(item)) : [];
|
|
2120
|
-
return {
|
|
2121
|
-
requestId,
|
|
2122
|
-
responseSubject,
|
|
2123
|
-
targetPath,
|
|
2124
|
-
base: sanitizeGitRef(args.request.base),
|
|
2125
|
-
target: sanitizeGitRef(args.request.target),
|
|
2126
|
-
mergeBase: args.request.mergeBase === true,
|
|
2127
|
-
staged: args.request.staged === true,
|
|
2128
|
-
format,
|
|
2129
|
-
contextLines,
|
|
2130
|
-
ignoreWhitespace,
|
|
2131
|
-
diffAlgorithm,
|
|
2132
|
-
findRenames: args.request.findRenames === true,
|
|
2133
|
-
pathspecs,
|
|
2134
|
-
};
|
|
2135
|
-
}
|
|
2136
|
-
function buildAgentGitDiffArgs(repoRootAbs, request) {
|
|
2137
|
-
const args = ["-C", repoRootAbs, "diff", "--no-color"];
|
|
2138
|
-
const displayParts = ["git", "diff", "--no-color"];
|
|
2139
|
-
if (request.staged) {
|
|
2140
|
-
args.push("--cached");
|
|
2141
|
-
displayParts.push("--cached");
|
|
2142
|
-
}
|
|
2143
|
-
if (typeof request.contextLines === "number") {
|
|
2144
|
-
args.push(`-U${request.contextLines}`);
|
|
2145
|
-
displayParts.push(`-U${request.contextLines}`);
|
|
2146
|
-
}
|
|
2147
|
-
if (request.ignoreWhitespace === "at-eol") {
|
|
2148
|
-
args.push("--ignore-space-at-eol");
|
|
2149
|
-
displayParts.push("--ignore-space-at-eol");
|
|
2150
|
-
}
|
|
2151
|
-
else if (request.ignoreWhitespace === "change") {
|
|
2152
|
-
args.push("--ignore-space-change");
|
|
2153
|
-
displayParts.push("--ignore-space-change");
|
|
2154
|
-
}
|
|
2155
|
-
else if (request.ignoreWhitespace === "all") {
|
|
2156
|
-
args.push("--ignore-all-space");
|
|
2157
|
-
displayParts.push("--ignore-all-space");
|
|
2158
|
-
}
|
|
2159
|
-
if (request.diffAlgorithm !== "default") {
|
|
2160
|
-
args.push(`--diff-algorithm=${request.diffAlgorithm}`);
|
|
2161
|
-
displayParts.push(`--diff-algorithm=${request.diffAlgorithm}`);
|
|
2162
|
-
}
|
|
2163
|
-
if (request.findRenames) {
|
|
2164
|
-
args.push("--find-renames");
|
|
2165
|
-
displayParts.push("--find-renames");
|
|
2166
|
-
}
|
|
2167
|
-
if (request.format === "name-only") {
|
|
2168
|
-
args.push("--name-only");
|
|
2169
|
-
displayParts.push("--name-only");
|
|
2170
|
-
}
|
|
2171
|
-
else if (request.format === "name-status") {
|
|
2172
|
-
args.push("--name-status");
|
|
2173
|
-
displayParts.push("--name-status");
|
|
2174
|
-
}
|
|
2175
|
-
else if (request.format === "stat") {
|
|
2176
|
-
args.push("--stat");
|
|
2177
|
-
displayParts.push("--stat");
|
|
2178
|
-
}
|
|
2179
|
-
else if (request.format === "numstat") {
|
|
2180
|
-
args.push("--numstat");
|
|
2181
|
-
displayParts.push("--numstat");
|
|
2182
|
-
}
|
|
2183
|
-
else if (request.format === "raw") {
|
|
2184
|
-
args.push("--raw");
|
|
2185
|
-
displayParts.push("--raw");
|
|
2186
|
-
}
|
|
2187
|
-
if (request.mergeBase) {
|
|
2188
|
-
if (!request.base || !request.target) {
|
|
2189
|
-
throw new Error("mergeBase mode requires both base and target");
|
|
2190
|
-
}
|
|
2191
|
-
const merged = `${request.base}...${request.target}`;
|
|
2192
|
-
args.push(merged);
|
|
2193
|
-
displayParts.push(merged);
|
|
2194
|
-
}
|
|
2195
|
-
else {
|
|
2196
|
-
if (request.base) {
|
|
2197
|
-
args.push(request.base);
|
|
2198
|
-
displayParts.push(request.base);
|
|
2199
|
-
}
|
|
2200
|
-
if (request.target) {
|
|
2201
|
-
args.push(request.target);
|
|
2202
|
-
displayParts.push(request.target);
|
|
2203
|
-
}
|
|
2204
|
-
}
|
|
2205
|
-
if (request.pathspecs.length > 0) {
|
|
2206
|
-
args.push("--", ...request.pathspecs);
|
|
2207
|
-
displayParts.push("--", ...request.pathspecs);
|
|
2208
|
-
}
|
|
2209
|
-
return { args, display: displayParts.join(" ") };
|
|
2210
|
-
}
|
|
2211
|
-
function buildUntrackedText(format, untrackedPaths) {
|
|
2212
|
-
if (untrackedPaths.length === 0) {
|
|
2213
|
-
return "";
|
|
2214
|
-
}
|
|
2215
|
-
if (format === "name-status" || format === "raw") {
|
|
2216
|
-
return `${untrackedPaths.map((item) => `??\t${item}`).join("\n")}\n`;
|
|
2217
|
-
}
|
|
2218
|
-
if (format === "name-only") {
|
|
2219
|
-
return `${untrackedPaths.join("\n")}\n`;
|
|
2220
|
-
}
|
|
2221
|
-
return `\n# Untracked files\n${untrackedPaths.join("\n")}\n`;
|
|
2222
|
-
}
|
|
2223
|
-
async function appendAgentLocalUntrackedDiff(repoRootAbs, request, baseOutput) {
|
|
2224
|
-
const listArgs = ["-C", repoRootAbs, "ls-files", "--others", "--exclude-standard"];
|
|
2225
|
-
if (request.pathspecs.length > 0) {
|
|
2226
|
-
listArgs.push("--", ...request.pathspecs);
|
|
2227
|
-
}
|
|
2228
|
-
const listResult = await runLocalCommand("git", listArgs, repoRootAbs);
|
|
2229
|
-
if (listResult.code !== 0) {
|
|
2230
|
-
return { output: baseOutput, hasUntracked: false };
|
|
2231
|
-
}
|
|
2232
|
-
const untrackedPaths = listResult.stdout.split(/\r?\n/).map((item) => item.trim()).filter(Boolean);
|
|
2233
|
-
if (untrackedPaths.length === 0) {
|
|
2234
|
-
return { output: baseOutput, hasUntracked: false };
|
|
2235
|
-
}
|
|
2236
|
-
if (request.format !== "patch") {
|
|
2237
|
-
return { output: `${baseOutput}${buildUntrackedText(request.format, untrackedPaths)}`, hasUntracked: true };
|
|
2238
|
-
}
|
|
2239
|
-
let output = baseOutput;
|
|
2240
|
-
for (const relPath of untrackedPaths) {
|
|
2241
|
-
const diffResult = await runLocalCommand("git", ["-C", repoRootAbs, "diff", "--no-color", "--no-index", "--", "/dev/null", relPath], repoRootAbs);
|
|
2242
|
-
if (diffResult.code !== 0 && diffResult.code !== 1) {
|
|
2243
|
-
throw new Error(diffResult.stderr.trim() || `Failed to render agent untracked diff: ${relPath}`);
|
|
2244
|
-
}
|
|
2245
|
-
if (diffResult.stdout) {
|
|
2246
|
-
output += diffResult.stdout;
|
|
2247
|
-
if (!output.endsWith("\n")) {
|
|
2248
|
-
output += "\n";
|
|
2249
|
-
}
|
|
2250
|
-
}
|
|
2251
|
-
}
|
|
2252
|
-
return { output, hasUntracked: true };
|
|
2253
|
-
}
|
|
2254
|
-
function publishGitRpcResponse(args) {
|
|
2255
|
-
args.nc.publish(args.responseSubject, gitRpcCodec.encode(JSON.stringify(args.payload)));
|
|
2256
|
-
}
|
|
2257
|
-
async function handleGitRpcMessage(args) {
|
|
2258
|
-
let requestId = "unknown";
|
|
2259
|
-
let responseSubject = "";
|
|
2260
|
-
try {
|
|
2261
|
-
const payload = JSON.parse(gitRpcCodec.decode(args.msg.data));
|
|
2262
|
-
const request = normalizeGitRpcRequest({ request: payload, agentId: args.agentId });
|
|
2263
|
-
requestId = request.requestId;
|
|
2264
|
-
responseSubject = request.responseSubject;
|
|
2265
|
-
if (!request.targetPath.startsWith("/")) {
|
|
2266
|
-
throw new Error("agent source requires an absolute directory path");
|
|
2267
|
-
}
|
|
2268
|
-
const topLevelResult = await runLocalCommand("git", ["-C", request.targetPath, "rev-parse", "--show-toplevel"], request.targetPath);
|
|
2269
|
-
if (topLevelResult.code !== 0) {
|
|
2270
|
-
publishGitRpcResponse({
|
|
2271
|
-
nc: args.jetstream.nc,
|
|
2272
|
-
responseSubject,
|
|
2273
|
-
payload: {
|
|
2274
|
-
requestId,
|
|
2275
|
-
ok: true,
|
|
2276
|
-
payload: {
|
|
2277
|
-
isGitRepo: false,
|
|
2278
|
-
mode: "git_diff",
|
|
2279
|
-
source: "agent",
|
|
2280
|
-
agent: { id: args.agentId, name: null },
|
|
2281
|
-
currentPath: request.targetPath,
|
|
2282
|
-
repoRoot: null,
|
|
2283
|
-
repoRelativePath: null,
|
|
2284
|
-
branch: null,
|
|
2285
|
-
gitDiff: {
|
|
2286
|
-
command: "git diff --no-color",
|
|
2287
|
-
format: "patch",
|
|
2288
|
-
output: "",
|
|
2289
|
-
outputTruncated: false,
|
|
2290
|
-
},
|
|
2291
|
-
message: "현재 경로가 Git 저장소가 아닙니다.",
|
|
2292
|
-
},
|
|
2293
|
-
},
|
|
2294
|
-
});
|
|
2295
|
-
return;
|
|
2296
|
-
}
|
|
2297
|
-
const repoRootAbs = topLevelResult.stdout.trim();
|
|
2298
|
-
const prefixResult = await runLocalCommand("git", ["-C", request.targetPath, "rev-parse", "--show-prefix"], request.targetPath);
|
|
2299
|
-
const repoRelativePath = prefixResult.code === 0 ? (prefixResult.stdout.trim().replace(/\/$/, "") || ".") : ".";
|
|
2300
|
-
const branchResult = await runLocalCommand("git", ["-C", repoRootAbs, "symbolic-ref", "--quiet", "--short", "HEAD"], repoRootAbs);
|
|
2301
|
-
const detachedResult = branchResult.code === 0 ? null : await runLocalCommand("git", ["-C", repoRootAbs, "rev-parse", "--short", "HEAD"], repoRootAbs);
|
|
2302
|
-
const branch = branchResult.code === 0 ? branchResult.stdout.trim() || null : detachedResult && detachedResult.code === 0 ? detachedResult.stdout.trim() || null : null;
|
|
2303
|
-
const gitDiffArgs = buildAgentGitDiffArgs(repoRootAbs, request);
|
|
2304
|
-
const gitDiffResult = await runLocalCommand("git", gitDiffArgs.args, repoRootAbs);
|
|
2305
|
-
if (gitDiffResult.code !== 0) {
|
|
2306
|
-
throw new Error(gitDiffResult.stderr.trim() || "Failed to run agent git diff");
|
|
2307
|
-
}
|
|
2308
|
-
const withUntracked = await appendAgentLocalUntrackedDiff(repoRootAbs, request, gitDiffResult.stdout);
|
|
2309
|
-
publishGitRpcResponse({
|
|
2310
|
-
nc: args.jetstream.nc,
|
|
2311
|
-
responseSubject,
|
|
2312
|
-
payload: {
|
|
2313
|
-
requestId,
|
|
2314
|
-
ok: true,
|
|
2315
|
-
payload: {
|
|
2316
|
-
isGitRepo: true,
|
|
2317
|
-
mode: "git_diff",
|
|
2318
|
-
source: "agent",
|
|
2319
|
-
agent: { id: args.agentId, name: null },
|
|
2320
|
-
currentPath: request.targetPath,
|
|
2321
|
-
repoRoot: repoRootAbs,
|
|
2322
|
-
repoRelativePath,
|
|
2323
|
-
branch,
|
|
2324
|
-
gitDiff: {
|
|
2325
|
-
command: withUntracked.hasUntracked ? `${gitDiffArgs.display} (+ untracked)` : gitDiffArgs.display,
|
|
2326
|
-
format: request.format,
|
|
2327
|
-
output: withUntracked.output,
|
|
2328
|
-
outputTruncated: false,
|
|
2329
|
-
},
|
|
2330
|
-
},
|
|
2331
|
-
},
|
|
2332
|
-
});
|
|
2333
|
-
}
|
|
2334
|
-
catch (error) {
|
|
2335
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2336
|
-
if (responseSubject) {
|
|
2337
|
-
publishGitRpcResponse({
|
|
2338
|
-
nc: args.jetstream.nc,
|
|
2339
|
-
responseSubject,
|
|
2340
|
-
payload: { requestId, ok: false, error: message },
|
|
2341
|
-
});
|
|
2342
|
-
}
|
|
2343
|
-
writeAgentError(`git rpc failed requestId=${requestId} error=${message}`);
|
|
2344
|
-
}
|
|
2345
|
-
}
|
|
2346
|
-
function subscribeToGitRpc(args) {
|
|
2347
|
-
const subject = buildAgentGitRpcSubject(args.userId, args.agentId);
|
|
2348
|
-
args.jetstream.nc.subscribe(subject, {
|
|
2349
|
-
callback: (error, msg) => {
|
|
2350
|
-
if (error) {
|
|
2351
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2352
|
-
writeAgentError(`git rpc subscription error: ${message}`);
|
|
2353
|
-
return;
|
|
2354
|
-
}
|
|
2355
|
-
void handleGitRpcMessage({
|
|
2356
|
-
msg,
|
|
2357
|
-
jetstream: args.jetstream,
|
|
2358
|
-
userId: args.userId,
|
|
2359
|
-
agentId: args.agentId,
|
|
2360
|
-
});
|
|
2361
|
-
},
|
|
2362
|
-
});
|
|
2363
|
-
writeAgentInfo(`git rpc subscribed subject=${subject}`);
|
|
2364
|
-
}
|
|
2365
|
-
async function handleRunRpcMessage(args) {
|
|
2366
|
-
let requestId = "unknown";
|
|
2367
|
-
let responseSubject = "";
|
|
2368
|
-
try {
|
|
2369
|
-
const payload = JSON.parse(runRpcCodec.decode(args.msg.data));
|
|
2370
|
-
const request = normalizeRunRpcRequest({ request: payload, agentId: args.agentId });
|
|
2371
|
-
requestId = request.requestId;
|
|
2372
|
-
responseSubject = request.responseSubject;
|
|
2373
|
-
if (request.action === "start") {
|
|
2374
|
-
const runId = request.runId ?? requestId;
|
|
2375
|
-
await claimRunStartSlot({ runId, sessionId: request.sessionId });
|
|
2376
|
-
try {
|
|
2377
|
-
const localAgentSettings = await readAgentSettingsConfig(null);
|
|
2378
|
-
const customInstructions = await readAgentModelInstructions();
|
|
2379
|
-
const task = await startManagedRun({
|
|
2380
|
-
requestId,
|
|
2381
|
-
runId,
|
|
2382
|
-
serverBaseUrl: args.serverBaseUrl,
|
|
2383
|
-
userId: args.userId,
|
|
2384
|
-
agentId: args.agentId,
|
|
2385
|
-
nc: args.jetstream.nc,
|
|
2386
|
-
sessionId: request.sessionId,
|
|
2387
|
-
codexArgs: buildManagedCodexArgs({
|
|
2388
|
-
prompt: request.prompt ?? "",
|
|
2389
|
-
imagePaths: request.imagePaths,
|
|
2390
|
-
sessionId: request.sessionId,
|
|
2391
|
-
model: request.model,
|
|
2392
|
-
personality: localAgentSettings.general.personality,
|
|
2393
|
-
modelInstructionsFile: customInstructions ? resolveAgentModelInstructionsFilePath() : null,
|
|
2394
|
-
}),
|
|
2395
|
-
cwd: request.cwd,
|
|
2396
|
-
runtimeEnvPatch: request.runtimeEnvPatch,
|
|
2397
|
-
codexAuthBundle: request.codexAuthBundle,
|
|
2398
|
-
agentToken: args.agentToken,
|
|
2399
|
-
});
|
|
2400
|
-
publishRunRpcResponse({ nc: args.jetstream.nc, responseSubject, payload: { requestId, ok: true, task } });
|
|
2401
|
-
}
|
|
2402
|
-
catch (error) {
|
|
2403
|
-
await releaseRunStartSlot({ runId, sessionId: request.sessionId }).catch(() => undefined);
|
|
2404
|
-
throw error;
|
|
2405
|
-
}
|
|
2406
|
-
return;
|
|
2407
|
-
}
|
|
2408
|
-
if (request.action === "list") {
|
|
2409
|
-
const merged = (await listPersistedRunTasks())
|
|
2410
|
-
.map((task) => cloneRunTask(task))
|
|
2411
|
-
.sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt))
|
|
2412
|
-
.slice(0, request.limit);
|
|
2413
|
-
publishRunRpcResponse({ nc: args.jetstream.nc, responseSubject, payload: { requestId, ok: true, tasks: merged } });
|
|
2414
|
-
return;
|
|
2415
|
-
}
|
|
2416
|
-
const stored = request.runId ? await getStoredRun(request.runId) : null;
|
|
2417
|
-
if (!stored || stored.agentId !== args.agentId || stored.userId !== args.userId) {
|
|
2418
|
-
throw new Error("Run not found");
|
|
2419
|
-
}
|
|
2420
|
-
if (request.action === "cancel") {
|
|
2421
|
-
const target = stored;
|
|
2422
|
-
if (target.processPid === null) {
|
|
2423
|
-
throw new Error("Run pid not found");
|
|
2424
|
-
}
|
|
2425
|
-
target.cancelRequested = true;
|
|
2426
|
-
target.updatedAt = formatLocalTimestamp();
|
|
2427
|
-
await persistRunTask(target);
|
|
2428
|
-
publishImmediateRunEvent({ nc: args.jetstream.nc, userId: target.userId, task: target });
|
|
2429
|
-
writeRunStatus(target.id, `cancel requested pid=${target.processPid}`);
|
|
2430
|
-
sendSignalToPid(target.processPid, "SIGINT");
|
|
2431
|
-
const task = cloneRunTask(target);
|
|
2432
|
-
publishRunRpcResponse({ nc: args.jetstream.nc, responseSubject, payload: { requestId, ok: true, task } });
|
|
2433
|
-
return;
|
|
2434
|
-
}
|
|
2435
|
-
const task = cloneRunTask(stored, request.sinceSeq);
|
|
2436
|
-
publishRunRpcResponse({ nc: args.jetstream.nc, responseSubject, payload: { requestId, ok: true, task } });
|
|
2437
|
-
}
|
|
2438
|
-
catch (error) {
|
|
2439
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2440
|
-
if (responseSubject) {
|
|
2441
|
-
publishRunRpcResponse({
|
|
2442
|
-
nc: args.jetstream.nc,
|
|
2443
|
-
responseSubject,
|
|
2444
|
-
payload: { requestId, ok: false, error: message },
|
|
2445
|
-
});
|
|
2446
|
-
}
|
|
2447
|
-
writeAgentError(`run rpc failed requestId=${requestId} error=${message}`);
|
|
2448
|
-
}
|
|
2449
|
-
}
|
|
2450
|
-
function subscribeToRunRpc(args) {
|
|
2451
|
-
const subject = buildAgentRunRpcSubject(args.userId, args.agentId);
|
|
2452
|
-
args.jetstream.nc.subscribe(subject, {
|
|
2453
|
-
callback: (error, msg) => {
|
|
2454
|
-
if (error) {
|
|
2455
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2456
|
-
writeAgentError(`run rpc subscription error: ${message}`);
|
|
2457
|
-
return;
|
|
2458
|
-
}
|
|
2459
|
-
void handleRunRpcMessage({
|
|
2460
|
-
msg,
|
|
2461
|
-
jetstream: args.jetstream,
|
|
2462
|
-
serverBaseUrl: args.serverBaseUrl,
|
|
2463
|
-
userId: args.userId,
|
|
2464
|
-
agentId: args.agentId,
|
|
2465
|
-
agentToken: args.agentToken,
|
|
2466
|
-
});
|
|
2467
|
-
},
|
|
2468
|
-
});
|
|
2469
|
-
writeAgentInfo(`run rpc subscribed subject=${subject}`);
|
|
2470
|
-
}
|
|
2471
|
-
function isLikelyNatsAuthError(error) {
|
|
2472
|
-
const message = (error instanceof Error ? error.message : String(error)).toLowerCase();
|
|
2473
|
-
return (message.includes("auth")
|
|
2474
|
-
|| message.includes("authorization")
|
|
2475
|
-
|| message.includes("authentication")
|
|
2476
|
-
|| message.includes("permission")
|
|
2477
|
-
|| message.includes("jwt")
|
|
2478
|
-
|| message.includes("token"));
|
|
2479
|
-
}
|
|
2480
|
-
function isLikelyNatsReconnectError(error) {
|
|
2481
|
-
const message = (error instanceof Error ? error.message : String(error)).toLowerCase();
|
|
2482
|
-
return (message.includes("connection_closed")
|
|
2483
|
-
|| message.includes("connection closed")
|
|
2484
|
-
|| message.includes("closed connection")
|
|
2485
|
-
|| message.includes("disconnected")
|
|
2486
|
-
|| message.includes("timeout")
|
|
2487
|
-
|| message.includes("no responders"));
|
|
2488
|
-
}
|
|
2489
|
-
function sendSignalToTaskProcess(child, signal) {
|
|
2490
|
-
if (process.platform !== "win32" && typeof child.pid === "number") {
|
|
2491
|
-
try {
|
|
2492
|
-
// Detached child owns a process group; signal the whole group first.
|
|
2493
|
-
process.kill(-child.pid, signal);
|
|
2494
|
-
return;
|
|
2495
|
-
}
|
|
2496
|
-
catch {
|
|
2497
|
-
// Fall back to direct child signaling.
|
|
2498
|
-
}
|
|
2499
|
-
}
|
|
2500
|
-
try {
|
|
2501
|
-
child.kill(signal);
|
|
2502
|
-
}
|
|
2503
|
-
catch {
|
|
2504
|
-
// noop
|
|
2505
|
-
}
|
|
2506
|
-
}
|
|
2507
|
-
function sendSignalToPid(pid, signal) {
|
|
2508
|
-
if (process.platform !== "win32") {
|
|
2509
|
-
try {
|
|
2510
|
-
process.kill(-pid, signal);
|
|
2511
|
-
return;
|
|
2512
|
-
}
|
|
2513
|
-
catch {
|
|
2514
|
-
// Fall back to direct pid signaling.
|
|
2515
|
-
}
|
|
2516
|
-
}
|
|
2517
|
-
process.kill(pid, signal);
|
|
2518
|
-
}
|
|
2519
|
-
function resolveLogTimeZone() {
|
|
2520
|
-
const configured = process.env.DOER_AGENT_LOG_TIMEZONE?.trim() || process.env.TZ?.trim();
|
|
2521
|
-
return configured && configured.length > 0 ? configured : "Asia/Seoul";
|
|
2522
|
-
}
|
|
2523
|
-
function resolveTimeZoneOffsetString(date, timeZone) {
|
|
2524
|
-
try {
|
|
2525
|
-
const parts = new Intl.DateTimeFormat("en-US", {
|
|
2526
|
-
timeZone,
|
|
2527
|
-
timeZoneName: "shortOffset",
|
|
2528
|
-
hour: "2-digit",
|
|
2529
|
-
minute: "2-digit",
|
|
2530
|
-
hour12: false,
|
|
2531
|
-
}).formatToParts(date);
|
|
2532
|
-
const token = parts.find((part) => part.type === "timeZoneName")?.value || "GMT+0";
|
|
2533
|
-
const matched = token.match(/GMT([+-]\d{1,2})(?::?(\d{2}))?/i);
|
|
2534
|
-
if (!matched) {
|
|
2535
|
-
return "+00:00";
|
|
2536
|
-
}
|
|
2537
|
-
const hourRaw = matched[1] || "+0";
|
|
2538
|
-
const minuteRaw = matched[2] || "00";
|
|
2539
|
-
const sign = hourRaw.startsWith("-") ? "-" : "+";
|
|
2540
|
-
const absHour = String(Math.abs(Number.parseInt(hourRaw, 10))).padStart(2, "0");
|
|
2541
|
-
const absMinute = String(Math.abs(Number.parseInt(minuteRaw, 10))).padStart(2, "0");
|
|
2542
|
-
return `${sign}${absHour}:${absMinute}`;
|
|
2543
|
-
}
|
|
2544
|
-
catch {
|
|
2545
|
-
return "+00:00";
|
|
2546
|
-
}
|
|
2547
|
-
}
|
|
2548
|
-
function formatLocalTimestamp(date = new Date()) {
|
|
2549
|
-
const timeZone = resolveLogTimeZone();
|
|
2550
|
-
try {
|
|
2551
|
-
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
2552
|
-
timeZone,
|
|
2553
|
-
year: "numeric",
|
|
2554
|
-
month: "2-digit",
|
|
2555
|
-
day: "2-digit",
|
|
2556
|
-
hour: "2-digit",
|
|
2557
|
-
minute: "2-digit",
|
|
2558
|
-
second: "2-digit",
|
|
2559
|
-
hour12: false,
|
|
2560
|
-
}).formatToParts(date);
|
|
2561
|
-
const pick = (type) => {
|
|
2562
|
-
return parts.find((part) => part.type === type)?.value || "00";
|
|
2563
|
-
};
|
|
2564
|
-
const year = pick("year");
|
|
2565
|
-
const month = pick("month");
|
|
2566
|
-
const day = pick("day");
|
|
2567
|
-
const hours = pick("hour");
|
|
2568
|
-
const minutes = pick("minute");
|
|
2569
|
-
const seconds = pick("second");
|
|
2570
|
-
const ms = String(date.getMilliseconds()).padStart(3, "0");
|
|
2571
|
-
const offset = resolveTimeZoneOffsetString(date, timeZone);
|
|
2572
|
-
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.${ms}${offset}`;
|
|
2573
|
-
}
|
|
2574
|
-
catch {
|
|
2575
|
-
return date.toISOString();
|
|
2576
|
-
}
|
|
2577
|
-
}
|
|
2578
|
-
function parseArgs(argv) {
|
|
2579
|
-
const out = {};
|
|
2580
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
2581
|
-
const key = argv[i];
|
|
2582
|
-
if (!key.startsWith("--")) {
|
|
2583
|
-
continue;
|
|
2584
|
-
}
|
|
2585
|
-
const value = argv[i + 1];
|
|
2586
|
-
if (typeof value === "string" && !value.startsWith("--")) {
|
|
2587
|
-
out[key.slice(2)] = value;
|
|
2588
|
-
i += 1;
|
|
2589
|
-
continue;
|
|
2590
|
-
}
|
|
2591
|
-
out[key.slice(2)] = "true";
|
|
2592
|
-
}
|
|
2593
|
-
return out;
|
|
2594
|
-
}
|
|
2595
|
-
function resolveArgOrEnv(args, argKeys, envKeys, fallback = "") {
|
|
2596
|
-
for (const key of argKeys) {
|
|
2597
|
-
const value = args[key]?.trim();
|
|
2598
|
-
if (value) {
|
|
2599
|
-
return value;
|
|
2600
|
-
}
|
|
2601
|
-
}
|
|
2602
|
-
for (const key of envKeys) {
|
|
2603
|
-
const value = process.env[key]?.trim();
|
|
2604
|
-
if (value) {
|
|
2605
|
-
return value;
|
|
2606
|
-
}
|
|
2607
|
-
}
|
|
2608
|
-
return fallback;
|
|
2609
|
-
}
|
|
2610
|
-
function resolveShellPath() {
|
|
2611
|
-
if (process.platform === "win32") {
|
|
2612
|
-
return process.env.ComSpec || "cmd.exe";
|
|
2613
|
-
}
|
|
2614
|
-
const candidates = [process.env.SHELL, "/bin/bash", "/usr/bin/bash", "/bin/sh", "/usr/bin/sh"].filter((value) => typeof value === "string" && value.trim().length > 0);
|
|
2615
|
-
for (const candidate of candidates) {
|
|
2616
|
-
if (existsSync(candidate)) {
|
|
2617
|
-
return candidate;
|
|
2618
|
-
}
|
|
2619
|
-
}
|
|
2620
|
-
throw new Error("No shell executable found. Set SHELL env or install /bin/sh (or bash).");
|
|
2621
|
-
}
|
|
2622
|
-
function resolveTaskWorkspace(rawCwd) {
|
|
2623
|
-
const workspaceRoot = workspaceRootOverride ?? (process.env.WORKSPACE?.trim() || process.cwd());
|
|
2624
|
-
const requestedCwd = rawCwd?.trim() || "";
|
|
2625
|
-
const resolvedCwd = requestedCwd
|
|
2626
|
-
? path.isAbsolute(requestedCwd)
|
|
2627
|
-
? path.resolve(requestedCwd)
|
|
2628
|
-
: path.resolve(workspaceRoot, requestedCwd)
|
|
2629
|
-
: workspaceRoot;
|
|
2630
|
-
if (!existsSync(resolvedCwd)) {
|
|
2631
|
-
throw new Error(`Invalid cwd: ${requestedCwd || "(empty)"} resolved to ${resolvedCwd} (path does not exist)`);
|
|
2632
|
-
}
|
|
2633
|
-
let stats;
|
|
2634
|
-
try {
|
|
2635
|
-
stats = statSync(resolvedCwd);
|
|
2636
|
-
}
|
|
2637
|
-
catch (error) {
|
|
2638
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2639
|
-
throw new Error(`Invalid cwd: ${requestedCwd || "(empty)"} resolved to ${resolvedCwd} (${message})`);
|
|
2640
|
-
}
|
|
2641
|
-
if (!stats.isDirectory()) {
|
|
2642
|
-
throw new Error(`Invalid cwd: ${requestedCwd || "(empty)"} resolved to ${resolvedCwd} (not a directory)`);
|
|
2643
|
-
}
|
|
2644
|
-
return resolvedCwd;
|
|
2645
|
-
}
|
|
2646
|
-
function buildAgentFsRpcSubject(userId, agentId) {
|
|
2647
|
-
return `doer.agent.fs.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
|
|
2648
|
-
}
|
|
2649
|
-
function normalizeFsRpcPath(rawPath) {
|
|
2650
|
-
const root = workspaceRootOverride ?? (process.env.WORKSPACE?.trim() || process.cwd());
|
|
2651
|
-
const raw = typeof rawPath === "string" && rawPath.trim() ? rawPath.trim() : ".";
|
|
2652
|
-
const normalizedRaw = raw.replace(/\\/g, "/");
|
|
2653
|
-
const useAbsolute = path.isAbsolute(normalizedRaw);
|
|
2654
|
-
const rel = normalizedRaw.replace(/^\/+/, "") || ".";
|
|
2655
|
-
const abs = useAbsolute ? path.resolve(normalizedRaw) : path.resolve(root, rel);
|
|
2656
|
-
if (!useAbsolute && abs !== root && !abs.startsWith(root + path.sep)) {
|
|
2657
|
-
throw new Error("path escapes workspace root");
|
|
2658
|
-
}
|
|
2659
|
-
const formatPath = (target) => {
|
|
2660
|
-
if (useAbsolute) {
|
|
2661
|
-
return target.split(path.sep).join("/") || "/";
|
|
2662
|
-
}
|
|
2663
|
-
return path.relative(root, target).split(path.sep).join("/") || ".";
|
|
2664
|
-
};
|
|
2665
|
-
return { abs, formatPath };
|
|
2666
|
-
}
|
|
2667
|
-
function parseFsRpcAction(value) {
|
|
2668
|
-
if (value === "list" ||
|
|
2669
|
-
value === "stat" ||
|
|
2670
|
-
value === "fetch_file" ||
|
|
2671
|
-
value === "read_text" ||
|
|
2672
|
-
value === "read_file" ||
|
|
2673
|
-
value === "write_file" ||
|
|
2674
|
-
value === "download_file" ||
|
|
2675
|
-
value === "delete_path" ||
|
|
2676
|
-
value === "archive_dir" ||
|
|
2677
|
-
value === "extract_archive") {
|
|
2678
|
-
return value;
|
|
2679
|
-
}
|
|
2680
|
-
throw new Error("unsupported action");
|
|
2681
|
-
}
|
|
2682
|
-
function normalizeFsRpcNumber(value, fallback) {
|
|
2683
|
-
const n = Number(value);
|
|
2684
|
-
if (!Number.isFinite(n)) {
|
|
2685
|
-
return fallback;
|
|
2686
|
-
}
|
|
2687
|
-
return Math.floor(n);
|
|
2688
|
-
}
|
|
2689
|
-
function inferMimeType(filePath) {
|
|
2690
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
2691
|
-
if (ext === ".txt" || ext === ".md" || ext === ".log") {
|
|
2692
|
-
return "text/plain";
|
|
2693
|
-
}
|
|
2694
|
-
if (ext === ".json") {
|
|
2695
|
-
return "application/json";
|
|
2696
|
-
}
|
|
2697
|
-
if (ext === ".js" || ext === ".mjs" || ext === ".cjs") {
|
|
2698
|
-
return "text/javascript";
|
|
2699
|
-
}
|
|
2700
|
-
if (ext === ".ts" || ext === ".tsx") {
|
|
2701
|
-
return "text/typescript";
|
|
2702
|
-
}
|
|
2703
|
-
if (ext === ".jsx") {
|
|
2704
|
-
return "text/jsx";
|
|
2705
|
-
}
|
|
2706
|
-
if (ext === ".css") {
|
|
2707
|
-
return "text/css";
|
|
2708
|
-
}
|
|
2709
|
-
if (ext === ".html" || ext === ".htm") {
|
|
2710
|
-
return "text/html";
|
|
2711
|
-
}
|
|
2712
|
-
if (ext === ".xml") {
|
|
2713
|
-
return "application/xml";
|
|
2714
|
-
}
|
|
2715
|
-
if (ext === ".svg") {
|
|
2716
|
-
return "image/svg+xml";
|
|
2717
|
-
}
|
|
2718
|
-
if (ext === ".png") {
|
|
2719
|
-
return "image/png";
|
|
2720
|
-
}
|
|
2721
|
-
if (ext === ".jpg" || ext === ".jpeg") {
|
|
2722
|
-
return "image/jpeg";
|
|
2723
|
-
}
|
|
2724
|
-
if (ext === ".gif") {
|
|
2725
|
-
return "image/gif";
|
|
2726
|
-
}
|
|
2727
|
-
if (ext === ".webp") {
|
|
2728
|
-
return "image/webp";
|
|
2729
|
-
}
|
|
2730
|
-
if (ext === ".pdf") {
|
|
2731
|
-
return "application/pdf";
|
|
2732
|
-
}
|
|
2733
|
-
return "application/octet-stream";
|
|
2734
|
-
}
|
|
2735
|
-
function normalizeArchiveRelativePath(value) {
|
|
2736
|
-
const normalized = value.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
|
|
2737
|
-
if (!normalized || normalized.includes("..")) {
|
|
2738
|
-
throw new Error("invalid archive entry path");
|
|
2739
|
-
}
|
|
2740
|
-
return normalized;
|
|
2741
|
-
}
|
|
2742
|
-
async function collectDirectoryFiles(absDir, rootDir = absDir) {
|
|
2743
|
-
const rows = await readdir(absDir, { withFileTypes: true });
|
|
2744
|
-
const files = [];
|
|
2745
|
-
for (const row of rows.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
2746
|
-
const child = path.join(absDir, row.name);
|
|
2747
|
-
if (row.isDirectory()) {
|
|
2748
|
-
files.push(...await collectDirectoryFiles(child, rootDir));
|
|
2749
|
-
continue;
|
|
2750
|
-
}
|
|
2751
|
-
if (!row.isFile()) {
|
|
2752
|
-
continue;
|
|
2753
|
-
}
|
|
2754
|
-
const bytes = await readFile(child);
|
|
2755
|
-
files.push({
|
|
2756
|
-
relPath: normalizeArchiveRelativePath(path.relative(rootDir, child)),
|
|
2757
|
-
contentBase64: Buffer.from(bytes).toString("base64"),
|
|
2758
|
-
sizeBytes: bytes.byteLength,
|
|
2759
|
-
});
|
|
2760
|
-
}
|
|
2761
|
-
return files;
|
|
2762
|
-
}
|
|
2763
|
-
async function executeFsRpc(args) {
|
|
2764
|
-
const action = parseFsRpcAction(args.request.action);
|
|
2765
|
-
const { abs, formatPath } = normalizeFsRpcPath(args.request.path);
|
|
2766
|
-
if (action === "stat") {
|
|
2767
|
-
const entry = await stat(abs);
|
|
2768
|
-
return {
|
|
2769
|
-
ok: true,
|
|
2770
|
-
action,
|
|
2771
|
-
path: formatPath(abs),
|
|
2772
|
-
kind: entry.isDirectory() ? "dir" : "file",
|
|
2773
|
-
size: entry.size,
|
|
2774
|
-
mtimeMs: entry.mtimeMs,
|
|
2775
|
-
};
|
|
2776
|
-
}
|
|
2777
|
-
if (action === "list") {
|
|
2778
|
-
const entry = await stat(abs);
|
|
2779
|
-
if (!entry.isDirectory()) {
|
|
2780
|
-
throw new Error("path is not a directory");
|
|
2781
|
-
}
|
|
2782
|
-
const limit = Math.max(1, Math.min(normalizeFsRpcNumber(args.request.limit, 200), 1000));
|
|
2783
|
-
const rows = await readdir(abs, { withFileTypes: true });
|
|
2784
|
-
const items = await Promise.all(rows.map(async (row) => {
|
|
2785
|
-
const child = path.join(abs, row.name);
|
|
2786
|
-
const childStat = await stat(child);
|
|
2787
|
-
return {
|
|
2788
|
-
name: row.name,
|
|
2789
|
-
path: formatPath(child),
|
|
2790
|
-
kind: row.isDirectory() ? "dir" : "file",
|
|
2791
|
-
size: childStat.size,
|
|
2792
|
-
mtimeMs: childStat.mtimeMs,
|
|
2793
|
-
};
|
|
2794
|
-
}));
|
|
2795
|
-
items.sort((a, b) => (a.kind === b.kind ? a.name.localeCompare(b.name) : a.kind === "dir" ? -1 : 1));
|
|
2796
|
-
return {
|
|
2797
|
-
ok: true,
|
|
2798
|
-
action,
|
|
2799
|
-
path: formatPath(abs),
|
|
2800
|
-
items: items.slice(0, limit),
|
|
2801
|
-
truncated: items.length > limit,
|
|
2802
|
-
total: items.length,
|
|
2803
|
-
};
|
|
2804
|
-
}
|
|
2805
|
-
if (action === "archive_dir") {
|
|
2806
|
-
const entry = await stat(abs);
|
|
2807
|
-
if (!entry.isDirectory()) {
|
|
2808
|
-
throw new Error("path is not a directory");
|
|
2809
|
-
}
|
|
2810
|
-
const rawArchivePath = typeof args.request.archivePath === "string" ? args.request.archivePath : "";
|
|
2811
|
-
if (!rawArchivePath) {
|
|
2812
|
-
throw new Error("archivePath is required");
|
|
2813
|
-
}
|
|
2814
|
-
const archiveTarget = normalizeFsRpcPath(rawArchivePath);
|
|
2815
|
-
const files = await collectDirectoryFiles(abs);
|
|
2816
|
-
if (!files.some((file) => file.relPath === "SKILL.md")) {
|
|
2817
|
-
throw new Error("Selected skill directory must contain SKILL.md");
|
|
2818
|
-
}
|
|
2819
|
-
const payload = gzipSync(Buffer.from(JSON.stringify({
|
|
2820
|
-
files,
|
|
2821
|
-
}), "utf8"));
|
|
2822
|
-
await mkdir(path.dirname(archiveTarget.abs), { recursive: true });
|
|
2823
|
-
await writeFile(archiveTarget.abs, payload);
|
|
2824
|
-
const archiveStat = await stat(archiveTarget.abs);
|
|
2825
|
-
return {
|
|
2826
|
-
ok: true,
|
|
2827
|
-
action,
|
|
2828
|
-
path: formatPath(abs),
|
|
2829
|
-
archivePath: archiveTarget.formatPath(archiveTarget.abs),
|
|
2830
|
-
size: archiveStat.size,
|
|
2831
|
-
};
|
|
2832
|
-
}
|
|
2833
|
-
if (action === "fetch_file") {
|
|
2834
|
-
const entry = await stat(abs);
|
|
2835
|
-
if (!entry.isFile()) {
|
|
2836
|
-
throw new Error("path is not a file");
|
|
2837
|
-
}
|
|
2838
|
-
const uploadUrl = typeof args.request.uploadUrl === "string" ? args.request.uploadUrl : "";
|
|
2839
|
-
const agentId = typeof args.request.agentId === "string" ? args.request.agentId : "";
|
|
2840
|
-
if (!uploadUrl || !agentId) {
|
|
2841
|
-
throw new Error("missing upload parameters");
|
|
2842
|
-
}
|
|
2843
|
-
const resolvedUploadUrl = new URL(uploadUrl, `${args.serverBaseUrl}/`).toString();
|
|
2844
|
-
const data = await readFile(abs);
|
|
2845
|
-
const fileName = path.basename(abs) || "file";
|
|
2846
|
-
const form = new FormData();
|
|
2847
|
-
form.append("file", new File([data], fileName));
|
|
2848
|
-
form.append("agentId", agentId);
|
|
2849
|
-
const response = await fetch(resolvedUploadUrl, {
|
|
2850
|
-
method: "POST",
|
|
2851
|
-
headers: { Authorization: `Bearer ${args.agentToken}` },
|
|
2852
|
-
body: form,
|
|
2853
|
-
});
|
|
2854
|
-
const text = await response.text();
|
|
2855
|
-
let upload = {};
|
|
2856
|
-
try {
|
|
2857
|
-
upload = JSON.parse(text || "{}");
|
|
2858
|
-
}
|
|
2859
|
-
catch {
|
|
2860
|
-
upload = {};
|
|
2861
|
-
}
|
|
2862
|
-
if (!response.ok) {
|
|
2863
|
-
const message = typeof upload.error === "string" ? upload.error : `upload failed: ${response.status}`;
|
|
2864
|
-
throw new Error(message);
|
|
2865
|
-
}
|
|
2866
|
-
return {
|
|
2867
|
-
ok: true,
|
|
2868
|
-
action,
|
|
2869
|
-
path: formatPath(abs),
|
|
2870
|
-
size: entry.size,
|
|
2871
|
-
upload,
|
|
2872
|
-
};
|
|
2873
|
-
}
|
|
2874
|
-
if (action === "write_file") {
|
|
2875
|
-
const contentBase64 = typeof args.request.contentBase64 === "string" ? args.request.contentBase64 : "";
|
|
2876
|
-
if (!contentBase64) {
|
|
2877
|
-
throw new Error("contentBase64 is required");
|
|
2878
|
-
}
|
|
2879
|
-
const parentDir = path.dirname(abs);
|
|
2880
|
-
await mkdir(parentDir, { recursive: true });
|
|
2881
|
-
const bytes = Buffer.from(contentBase64, "base64");
|
|
2882
|
-
await writeFile(abs, bytes);
|
|
2883
|
-
const entry = await stat(abs);
|
|
2884
|
-
return {
|
|
2885
|
-
ok: true,
|
|
2886
|
-
action,
|
|
2887
|
-
path: formatPath(abs),
|
|
2888
|
-
absolutePath: abs.split(path.sep).join("/"),
|
|
2889
|
-
size: entry.size,
|
|
2890
|
-
mimeType: inferMimeType(abs),
|
|
2891
|
-
mtimeMs: entry.mtimeMs,
|
|
2892
|
-
};
|
|
2893
|
-
}
|
|
2894
|
-
if (action === "delete_path") {
|
|
2895
|
-
await rm(abs, { recursive: true, force: true });
|
|
2896
|
-
return {
|
|
2897
|
-
ok: true,
|
|
2898
|
-
action,
|
|
2899
|
-
path: formatPath(abs),
|
|
2900
|
-
absolutePath: abs.split(path.sep).join("/"),
|
|
2901
|
-
};
|
|
2902
|
-
}
|
|
2903
|
-
if (action === "download_file") {
|
|
2904
|
-
const downloadPath = typeof args.request.downloadPath === "string" ? args.request.downloadPath.trim() : "";
|
|
2905
|
-
if (!downloadPath) {
|
|
2906
|
-
throw new Error("downloadPath is required");
|
|
2907
|
-
}
|
|
2908
|
-
const downloadUrl = new URL(downloadPath, `${args.serverBaseUrl}/`).toString();
|
|
2909
|
-
const response = await fetch(downloadUrl, {
|
|
2910
|
-
method: "GET",
|
|
2911
|
-
headers: {
|
|
2912
|
-
Authorization: `Bearer ${args.agentToken}`,
|
|
2913
|
-
},
|
|
2914
|
-
});
|
|
2915
|
-
if (!response.ok) {
|
|
2916
|
-
const text = await response.text().catch(() => "");
|
|
2917
|
-
throw new Error(text || `download failed: ${response.status}`);
|
|
2918
|
-
}
|
|
2919
|
-
const bytes = Buffer.from(await response.arrayBuffer());
|
|
2920
|
-
const parentDir = path.dirname(abs);
|
|
2921
|
-
await mkdir(parentDir, { recursive: true });
|
|
2922
|
-
await writeFile(abs, bytes);
|
|
2923
|
-
const entry = await stat(abs);
|
|
2924
|
-
return {
|
|
2925
|
-
ok: true,
|
|
2926
|
-
action,
|
|
2927
|
-
path: formatPath(abs),
|
|
2928
|
-
absolutePath: abs.split(path.sep).join("/"),
|
|
2929
|
-
size: entry.size,
|
|
2930
|
-
mimeType: inferMimeType(abs),
|
|
2931
|
-
mtimeMs: entry.mtimeMs,
|
|
2932
|
-
};
|
|
2933
|
-
}
|
|
2934
|
-
if (action === "extract_archive") {
|
|
2935
|
-
const archiveEntry = await stat(abs);
|
|
2936
|
-
if (!archiveEntry.isFile()) {
|
|
2937
|
-
throw new Error("path is not a file");
|
|
2938
|
-
}
|
|
2939
|
-
const rawDestinationPath = typeof args.request.destinationPath === "string" ? args.request.destinationPath : "";
|
|
2940
|
-
if (!rawDestinationPath) {
|
|
2941
|
-
throw new Error("destinationPath is required");
|
|
2942
|
-
}
|
|
2943
|
-
const destinationTarget = normalizeFsRpcPath(rawDestinationPath);
|
|
2944
|
-
const archiveBytes = await readFile(abs);
|
|
2945
|
-
const decoded = JSON.parse(gunzipSync(archiveBytes).toString("utf8"));
|
|
2946
|
-
const files = Array.isArray(decoded.files) ? decoded.files : [];
|
|
2947
|
-
await mkdir(destinationTarget.abs, { recursive: true });
|
|
2948
|
-
for (const file of files) {
|
|
2949
|
-
const relPath = typeof file.relPath === "string" ? normalizeArchiveRelativePath(file.relPath) : "";
|
|
2950
|
-
const contentBase64 = typeof file.contentBase64 === "string" ? file.contentBase64 : "";
|
|
2951
|
-
if (!relPath || !contentBase64) {
|
|
2952
|
-
throw new Error("archive contains an invalid file entry");
|
|
2953
|
-
}
|
|
2954
|
-
const targetPath = path.join(destinationTarget.abs, relPath);
|
|
2955
|
-
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
2956
|
-
await writeFile(targetPath, Buffer.from(contentBase64, "base64"));
|
|
2957
|
-
}
|
|
2958
|
-
return {
|
|
2959
|
-
ok: true,
|
|
2960
|
-
action,
|
|
2961
|
-
path: formatPath(abs),
|
|
2962
|
-
absolutePath: destinationTarget.formatPath(destinationTarget.abs),
|
|
2963
|
-
};
|
|
2964
|
-
}
|
|
2965
|
-
const entry = await stat(abs);
|
|
2966
|
-
if (!entry.isFile()) {
|
|
2967
|
-
throw new Error("path is not a file");
|
|
2968
|
-
}
|
|
2969
|
-
if (action === "read_file") {
|
|
2970
|
-
const maxBytes = Math.max(1, Math.min(normalizeFsRpcNumber(args.request.maxBytes, 2_000_000), 5_000_000));
|
|
2971
|
-
const data = await readFile(abs);
|
|
2972
|
-
const truncated = data.byteLength > maxBytes;
|
|
2973
|
-
const bytes = truncated ? data.subarray(0, maxBytes) : data;
|
|
2974
|
-
return {
|
|
2975
|
-
ok: true,
|
|
2976
|
-
action,
|
|
2977
|
-
path: formatPath(abs),
|
|
2978
|
-
mimeType: inferMimeType(abs),
|
|
2979
|
-
size: entry.size,
|
|
2980
|
-
truncated,
|
|
2981
|
-
contentBase64: bytes.toString("base64"),
|
|
2982
|
-
};
|
|
2983
|
-
}
|
|
2984
|
-
const offset = Math.max(0, normalizeFsRpcNumber(args.request.offset, 0));
|
|
2985
|
-
const length = Math.max(1, Math.min(normalizeFsRpcNumber(args.request.length, 65536), 262144));
|
|
2986
|
-
const encoding = typeof args.request.encoding === "string" && args.request.encoding ? args.request.encoding : "utf8";
|
|
2987
|
-
const fd = await open(abs, "r");
|
|
2988
|
-
try {
|
|
2989
|
-
const buffer = Buffer.alloc(length);
|
|
2990
|
-
const readResult = await fd.read(buffer, 0, length, offset);
|
|
2991
|
-
const slice = buffer.subarray(0, readResult.bytesRead);
|
|
2992
|
-
try {
|
|
2993
|
-
const text = slice.toString(encoding);
|
|
2994
|
-
return {
|
|
2995
|
-
ok: true,
|
|
2996
|
-
action,
|
|
2997
|
-
path: formatPath(abs),
|
|
2998
|
-
offset,
|
|
2999
|
-
length: readResult.bytesRead,
|
|
3000
|
-
totalSize: entry.size,
|
|
3001
|
-
eof: offset + readResult.bytesRead >= entry.size,
|
|
3002
|
-
encoding,
|
|
3003
|
-
text,
|
|
3004
|
-
bytesRead: readResult.bytesRead,
|
|
3005
|
-
};
|
|
3006
|
-
}
|
|
3007
|
-
catch (error) {
|
|
3008
|
-
const message = error instanceof Error ? error.message : "failed to decode text";
|
|
3009
|
-
return {
|
|
3010
|
-
ok: false,
|
|
3011
|
-
action,
|
|
3012
|
-
path: formatPath(abs),
|
|
3013
|
-
error: message,
|
|
3014
|
-
};
|
|
3015
|
-
}
|
|
3016
|
-
}
|
|
3017
|
-
finally {
|
|
3018
|
-
await fd.close();
|
|
3019
|
-
}
|
|
3020
|
-
}
|
|
3021
|
-
function normalizeSessionRpcRequest(args) {
|
|
3022
|
-
const requestId = typeof args.request.requestId === "string" ? args.request.requestId.trim() : "";
|
|
3023
|
-
if (!requestId) {
|
|
3024
|
-
throw new Error("missing requestId");
|
|
3025
|
-
}
|
|
3026
|
-
const requestAgentId = typeof args.request.agentId === "string" ? args.request.agentId.trim() : "";
|
|
3027
|
-
if (!requestAgentId || requestAgentId !== args.agentId) {
|
|
3028
|
-
throw new Error("agent id mismatch");
|
|
3029
|
-
}
|
|
3030
|
-
const actionRaw = typeof args.request.action === "string" ? args.request.action.trim() : "";
|
|
3031
|
-
const action = actionRaw === "messages" || actionRaw === "delete" || actionRaw === "watch" || actionRaw === "stop_watch"
|
|
3032
|
-
? actionRaw
|
|
3033
|
-
: "list";
|
|
3034
|
-
const responseSubject = typeof args.request.responseSubject === "string" ? args.request.responseSubject.trim() : "";
|
|
3035
|
-
if (!responseSubject) {
|
|
3036
|
-
throw new Error("missing responseSubject");
|
|
3037
|
-
}
|
|
3038
|
-
const filePath = typeof args.request.filePath === "string" && args.request.filePath.trim() ? args.request.filePath.trim() : null;
|
|
3039
|
-
if ((action === "messages" || action === "delete" || action === "watch") && !filePath) {
|
|
3040
|
-
throw new Error("missing filePath");
|
|
3041
|
-
}
|
|
3042
|
-
const sinceLineRaw = Number(args.request.sinceLine);
|
|
3043
|
-
const sinceLine = Number.isInteger(sinceLineRaw) && sinceLineRaw > 0 ? sinceLineRaw : 0;
|
|
3044
|
-
const beforeRowIdRaw = Number(args.request.beforeRowId);
|
|
3045
|
-
const beforeRowId = Number.isInteger(beforeRowIdRaw) && beforeRowIdRaw > 0 ? beforeRowIdRaw : null;
|
|
3046
|
-
const pageSizeRaw = Number(args.request.pageSize);
|
|
3047
|
-
const pageSize = Number.isFinite(pageSizeRaw) ? Math.max(1, Math.min(Math.floor(pageSizeRaw), 100)) : 100;
|
|
3048
|
-
const watchId = typeof args.request.watchId === "string" && args.request.watchId.trim() ? args.request.watchId.trim() : null;
|
|
3049
|
-
if (action === "stop_watch" && !watchId) {
|
|
3050
|
-
throw new Error("missing watchId");
|
|
3051
|
-
}
|
|
3052
|
-
return {
|
|
3053
|
-
requestId,
|
|
3054
|
-
action,
|
|
3055
|
-
agentId: requestAgentId,
|
|
3056
|
-
filePath,
|
|
3057
|
-
sessionId: typeof args.request.sessionId === "string" && args.request.sessionId.trim() ? args.request.sessionId.trim() : null,
|
|
3058
|
-
sinceLine,
|
|
3059
|
-
beforeRowId,
|
|
3060
|
-
pageSize,
|
|
3061
|
-
responseSubject,
|
|
3062
|
-
watchId,
|
|
3063
|
-
};
|
|
3064
|
-
}
|
|
3065
|
-
function getSessionsRootPath() {
|
|
3066
|
-
const workspaceRoot = workspaceRootOverride ?? (process.env.WORKSPACE?.trim() || process.cwd());
|
|
3067
|
-
return path.join(workspaceRoot, ".codex", "sessions");
|
|
3068
|
-
}
|
|
3069
|
-
function resolveSessionFilePath(filePath) {
|
|
3070
|
-
const root = path.resolve(getSessionsRootPath());
|
|
3071
|
-
const resolved = path.resolve(filePath);
|
|
3072
|
-
if (!(resolved === root || resolved.startsWith(root + path.sep))) {
|
|
3073
|
-
throw new Error("filePath is outside sessions root");
|
|
3074
|
-
}
|
|
3075
|
-
return resolved;
|
|
3076
|
-
}
|
|
3077
|
-
function isObjectRecord(value) {
|
|
3078
|
-
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
3079
|
-
}
|
|
3080
|
-
const SESSION_RPC_BLOB_KEYS = new Set([
|
|
3081
|
-
"image_url",
|
|
3082
|
-
"image_base64",
|
|
3083
|
-
"content_base64",
|
|
3084
|
-
"file_data",
|
|
3085
|
-
"bytes",
|
|
3086
|
-
"data",
|
|
3087
|
-
]);
|
|
3088
|
-
function isInlineBlobString(value) {
|
|
3089
|
-
const trimmed = value.trim();
|
|
3090
|
-
if (!trimmed) {
|
|
3091
|
-
return false;
|
|
3092
|
-
}
|
|
3093
|
-
return trimmed.startsWith("data:") || trimmed.includes(";base64,");
|
|
3094
|
-
}
|
|
3095
|
-
function buildInlineBlobMarker(value) {
|
|
3096
|
-
const trimmed = value.trim();
|
|
3097
|
-
if (trimmed.startsWith("data:")) {
|
|
3098
|
-
const mimeEnd = trimmed.indexOf(";");
|
|
3099
|
-
const mimeType = mimeEnd > 5 ? trimmed.slice(5, mimeEnd) : "";
|
|
3100
|
-
if (mimeType) {
|
|
3101
|
-
return `[inline blob omitted: ${mimeType}]`;
|
|
3102
|
-
}
|
|
3103
|
-
}
|
|
3104
|
-
return "[inline blob omitted]";
|
|
3105
|
-
}
|
|
3106
|
-
function sanitizeSessionRpcPayload(value) {
|
|
3107
|
-
if (typeof value === "string") {
|
|
3108
|
-
return value;
|
|
3109
|
-
}
|
|
3110
|
-
if (Array.isArray(value)) {
|
|
3111
|
-
return value.map((entry) => sanitizeSessionRpcPayload(entry));
|
|
3112
|
-
}
|
|
3113
|
-
if (!isObjectRecord(value)) {
|
|
3114
|
-
return value;
|
|
3115
|
-
}
|
|
3116
|
-
const sanitized = {};
|
|
3117
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
3118
|
-
if (SESSION_RPC_BLOB_KEYS.has(key) && typeof entry === "string" && isInlineBlobString(entry)) {
|
|
3119
|
-
sanitized[key] = buildInlineBlobMarker(entry);
|
|
3120
|
-
continue;
|
|
3121
|
-
}
|
|
3122
|
-
sanitized[key] = sanitizeSessionRpcPayload(entry);
|
|
3123
|
-
}
|
|
3124
|
-
return sanitized;
|
|
3125
|
-
}
|
|
3126
|
-
function sanitizeSessionRpcRawLine(line) {
|
|
3127
|
-
const trimmed = line.trim();
|
|
3128
|
-
if (!trimmed.startsWith("{")) {
|
|
3129
|
-
return line;
|
|
3130
|
-
}
|
|
3131
|
-
try {
|
|
3132
|
-
const parsed = JSON.parse(line);
|
|
3133
|
-
if (!isObjectRecord(parsed)) {
|
|
3134
|
-
return line;
|
|
3135
|
-
}
|
|
3136
|
-
if (parsed.type === "compacted" || parsed.type === "turn_context" || parsed.type === "session_meta") {
|
|
3137
|
-
return null;
|
|
3138
|
-
}
|
|
3139
|
-
if (!isObjectRecord(parsed.payload) || parsed.type !== "response_item") {
|
|
3140
|
-
return line;
|
|
3141
|
-
}
|
|
3142
|
-
const payloadType = typeof parsed.payload.type === "string" ? parsed.payload.type : "";
|
|
3143
|
-
if (payloadType === "message" || payloadType === "reasoning") {
|
|
3144
|
-
return null;
|
|
3145
|
-
}
|
|
3146
|
-
return JSON.stringify({
|
|
3147
|
-
...parsed,
|
|
3148
|
-
payload: sanitizeSessionRpcPayload(parsed.payload),
|
|
3149
|
-
});
|
|
3150
|
-
}
|
|
3151
|
-
catch {
|
|
3152
|
-
return line;
|
|
3153
|
-
}
|
|
3154
|
-
}
|
|
3155
|
-
function toTrimmedStringOrNull(value) {
|
|
3156
|
-
if (typeof value !== "string") {
|
|
3157
|
-
return null;
|
|
3158
|
-
}
|
|
3159
|
-
const trimmed = value.trim();
|
|
3160
|
-
return trimmed || null;
|
|
3161
|
-
}
|
|
3162
|
-
function pickSessionString(...values) {
|
|
3163
|
-
for (const value of values) {
|
|
3164
|
-
const picked = toTrimmedStringOrNull(value);
|
|
3165
|
-
if (picked) {
|
|
3166
|
-
return picked;
|
|
3167
|
-
}
|
|
3168
|
-
}
|
|
3169
|
-
return null;
|
|
3170
|
-
}
|
|
3171
|
-
async function collectSessionJsonlFiles(rootDir) {
|
|
3172
|
-
const out = [];
|
|
3173
|
-
const stack = [rootDir];
|
|
3174
|
-
while (stack.length > 0) {
|
|
3175
|
-
const current = stack.pop();
|
|
3176
|
-
if (!current) {
|
|
3177
|
-
continue;
|
|
3178
|
-
}
|
|
3179
|
-
let entries = [];
|
|
3180
|
-
try {
|
|
3181
|
-
entries = await readdir(current, { withFileTypes: true });
|
|
3182
|
-
}
|
|
3183
|
-
catch {
|
|
3184
|
-
continue;
|
|
3185
|
-
}
|
|
3186
|
-
for (const entry of entries) {
|
|
3187
|
-
const fullPath = path.join(current, entry.name);
|
|
3188
|
-
if (entry.isDirectory()) {
|
|
3189
|
-
stack.push(fullPath);
|
|
3190
|
-
continue;
|
|
3191
|
-
}
|
|
3192
|
-
if (!entry.isFile() || !entry.name.toLowerCase().endsWith(".jsonl")) {
|
|
3193
|
-
continue;
|
|
3194
|
-
}
|
|
3195
|
-
try {
|
|
3196
|
-
const entryStat = await stat(fullPath);
|
|
3197
|
-
out.push({ filePath: fullPath, mtimeMs: entryStat.mtimeMs });
|
|
3198
|
-
}
|
|
3199
|
-
catch {
|
|
3200
|
-
// ignore removed files
|
|
3201
|
-
}
|
|
3202
|
-
}
|
|
3203
|
-
}
|
|
3204
|
-
return out;
|
|
3205
|
-
}
|
|
3206
|
-
async function readFirstLine(fileHandle, fileSize) {
|
|
3207
|
-
const chunkBytes = 16_384;
|
|
3208
|
-
const maxScanBytes = 262_144;
|
|
3209
|
-
let position = 0;
|
|
3210
|
-
let scanned = 0;
|
|
3211
|
-
let raw = "";
|
|
3212
|
-
while (position < fileSize && scanned < maxScanBytes) {
|
|
3213
|
-
const readSize = Math.min(chunkBytes, fileSize - position, maxScanBytes - scanned);
|
|
3214
|
-
const buffer = Buffer.alloc(readSize);
|
|
3215
|
-
const { bytesRead } = await fileHandle.read(buffer, 0, readSize, position);
|
|
3216
|
-
if (bytesRead <= 0) {
|
|
3217
|
-
break;
|
|
3218
|
-
}
|
|
3219
|
-
raw += buffer.toString("utf8", 0, bytesRead);
|
|
3220
|
-
scanned += bytesRead;
|
|
3221
|
-
position += bytesRead;
|
|
3222
|
-
const newlineIndex = raw.search(/\r?\n/);
|
|
3223
|
-
if (newlineIndex >= 0) {
|
|
3224
|
-
return raw.slice(0, newlineIndex).trim();
|
|
3225
|
-
}
|
|
3226
|
-
}
|
|
3227
|
-
return raw.trim();
|
|
3228
|
-
}
|
|
3229
|
-
function extractLastAgentMessage(candidateLines) {
|
|
3230
|
-
let fallback = null;
|
|
3231
|
-
for (const line of candidateLines) {
|
|
3232
|
-
const trimmed = line.trim();
|
|
3233
|
-
if (!trimmed) {
|
|
3234
|
-
continue;
|
|
3235
|
-
}
|
|
3236
|
-
try {
|
|
3237
|
-
const parsed = JSON.parse(trimmed);
|
|
3238
|
-
if (parsed.type !== "event_msg" || !isObjectRecord(parsed.payload)) {
|
|
3239
|
-
continue;
|
|
3240
|
-
}
|
|
3241
|
-
if (parsed.payload.type === "agent_message" && typeof parsed.payload.message === "string" && parsed.payload.message.trim()) {
|
|
3242
|
-
return {
|
|
3243
|
-
message: parsed.payload.message.trim(),
|
|
3244
|
-
updatedAt: toTrimmedStringOrNull(parsed.timestamp),
|
|
3245
|
-
};
|
|
3246
|
-
}
|
|
3247
|
-
if (!fallback &&
|
|
3248
|
-
parsed.payload.type === "task_complete" &&
|
|
3249
|
-
typeof parsed.payload.last_agent_message === "string" &&
|
|
3250
|
-
parsed.payload.last_agent_message.trim()) {
|
|
3251
|
-
fallback = {
|
|
3252
|
-
message: parsed.payload.last_agent_message.trim(),
|
|
3253
|
-
updatedAt: toTrimmedStringOrNull(parsed.timestamp),
|
|
3254
|
-
};
|
|
3255
|
-
}
|
|
3256
|
-
}
|
|
3257
|
-
catch {
|
|
3258
|
-
// ignore malformed lines
|
|
3259
|
-
}
|
|
3260
|
-
}
|
|
3261
|
-
return fallback;
|
|
3262
|
-
}
|
|
3263
|
-
async function readLastAgentMessage(fileHandle, fileSize) {
|
|
3264
|
-
const chunkBytes = 16_384;
|
|
3265
|
-
const maxScanBytes = 131_072;
|
|
3266
|
-
if (fileSize <= 0) {
|
|
3267
|
-
return null;
|
|
3268
|
-
}
|
|
3269
|
-
let position = fileSize;
|
|
3270
|
-
let scanned = 0;
|
|
3271
|
-
let carry = "";
|
|
3272
|
-
while (position > 0 && scanned < maxScanBytes) {
|
|
3273
|
-
const readSize = Math.min(chunkBytes, position, maxScanBytes - scanned);
|
|
3274
|
-
position -= readSize;
|
|
3275
|
-
scanned += readSize;
|
|
3276
|
-
const buffer = Buffer.alloc(readSize);
|
|
3277
|
-
const { bytesRead } = await fileHandle.read(buffer, 0, readSize, position);
|
|
3278
|
-
if (bytesRead <= 0) {
|
|
3279
|
-
break;
|
|
3280
|
-
}
|
|
3281
|
-
const merged = buffer.toString("utf8", 0, bytesRead) + carry;
|
|
3282
|
-
const lines = merged.split(/\r?\n/);
|
|
3283
|
-
carry = lines.shift() || "";
|
|
3284
|
-
const found = extractLastAgentMessage(lines.reverse());
|
|
3285
|
-
if (found) {
|
|
3286
|
-
return found;
|
|
3287
|
-
}
|
|
3288
|
-
}
|
|
3289
|
-
return extractLastAgentMessage([carry]);
|
|
3290
|
-
}
|
|
3291
|
-
function normalizeSessionMeta(rawMeta, filePath, mtimeMs) {
|
|
3292
|
-
const baseName = path.basename(filePath, path.extname(filePath));
|
|
3293
|
-
const meta = isObjectRecord(rawMeta) ? rawMeta : {};
|
|
3294
|
-
const updatedAtCandidate = pickSessionString(meta.updatedAt, meta.updated_at, meta.timestamp);
|
|
3295
|
-
return {
|
|
3296
|
-
id: pickSessionString(meta.sessionId, meta.session_id, meta.id) || baseName,
|
|
3297
|
-
label: pickSessionString(meta.label, meta.title, meta.name, meta.sessionLabel, meta.session_label) || baseName,
|
|
3298
|
-
updatedAt: updatedAtCandidate || new Date(mtimeMs).toISOString(),
|
|
3299
|
-
cwd: pickSessionString(meta.cwd, meta.workingDirectory, meta.working_directory),
|
|
3300
|
-
source: pickSessionString(meta.source, meta.sessionSource, meta.session_source) || "codex",
|
|
3301
|
-
originator: pickSessionString(meta.originator, meta.author, meta.user, meta.username) || "unknown",
|
|
3302
|
-
filePath,
|
|
3303
|
-
};
|
|
3304
|
-
}
|
|
3305
|
-
async function readSessionSummary(filePath, mtimeMs) {
|
|
3306
|
-
let fileHandle = null;
|
|
3307
|
-
try {
|
|
3308
|
-
fileHandle = await open(filePath, "r");
|
|
3309
|
-
const entryStat = await fileHandle.stat();
|
|
3310
|
-
const firstLine = await readFirstLine(fileHandle, entryStat.size);
|
|
3311
|
-
const tailSummary = await readLastAgentMessage(fileHandle, entryStat.size);
|
|
3312
|
-
let normalized = normalizeSessionMeta({}, filePath, mtimeMs);
|
|
3313
|
-
if (firstLine) {
|
|
3314
|
-
try {
|
|
3315
|
-
const parsed = JSON.parse(firstLine);
|
|
3316
|
-
const candidateMeta = parsed && parsed.type === "session_meta" && isObjectRecord(parsed.payload)
|
|
3317
|
-
? parsed.payload
|
|
3318
|
-
: isObjectRecord(parsed.session_meta)
|
|
3319
|
-
? parsed.session_meta
|
|
3320
|
-
: isObjectRecord(parsed.sessionMeta)
|
|
3321
|
-
? parsed.sessionMeta
|
|
3322
|
-
: isObjectRecord(parsed.meta)
|
|
3323
|
-
? parsed.meta
|
|
3324
|
-
: isObjectRecord(parsed.payload)
|
|
3325
|
-
? parsed.payload
|
|
3326
|
-
: parsed;
|
|
3327
|
-
normalized = normalizeSessionMeta(candidateMeta, filePath, mtimeMs);
|
|
3328
|
-
}
|
|
3329
|
-
catch {
|
|
3330
|
-
normalized = normalizeSessionMeta({}, filePath, mtimeMs);
|
|
3331
|
-
}
|
|
3332
|
-
}
|
|
3333
|
-
return {
|
|
3334
|
-
...normalized,
|
|
3335
|
-
label: tailSummary?.message || "(no agent message)",
|
|
3336
|
-
updatedAt: tailSummary?.updatedAt || normalized.updatedAt,
|
|
3337
|
-
};
|
|
3338
|
-
}
|
|
3339
|
-
catch {
|
|
3340
|
-
return normalizeSessionMeta({}, filePath, mtimeMs);
|
|
3341
|
-
}
|
|
3342
|
-
finally {
|
|
3343
|
-
await fileHandle?.close().catch(() => undefined);
|
|
3344
|
-
}
|
|
3345
|
-
}
|
|
3346
|
-
async function listAgentSessions() {
|
|
3347
|
-
const sessionsRoot = getSessionsRootPath();
|
|
3348
|
-
let sessionsRootStat;
|
|
3349
|
-
try {
|
|
3350
|
-
sessionsRootStat = await stat(sessionsRoot);
|
|
3351
|
-
}
|
|
3352
|
-
catch {
|
|
3353
|
-
return [];
|
|
3354
|
-
}
|
|
3355
|
-
if (!sessionsRootStat.isDirectory()) {
|
|
3356
|
-
return [];
|
|
3357
|
-
}
|
|
3358
|
-
const files = await collectSessionJsonlFiles(sessionsRoot);
|
|
3359
|
-
files.sort((a, b) => b.mtimeMs - a.mtimeMs || a.filePath.localeCompare(b.filePath));
|
|
3360
|
-
const sessions = await Promise.all(files.slice(0, 10).map((file) => readSessionSummary(file.filePath, file.mtimeMs)));
|
|
3361
|
-
return sessions.sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt) || b.filePath.localeCompare(a.filePath));
|
|
3362
|
-
}
|
|
3363
|
-
async function readSessionLineIndex(filePath) {
|
|
3364
|
-
const resolvedFile = resolveSessionFilePath(filePath);
|
|
3365
|
-
const entryStat = await stat(resolvedFile);
|
|
3366
|
-
const nextSize = entryStat.size;
|
|
3367
|
-
const cached = sessionLineIndexCache.get(resolvedFile) ?? null;
|
|
3368
|
-
if (cached && cached.size === nextSize) {
|
|
3369
|
-
return cached;
|
|
3370
|
-
}
|
|
3371
|
-
const fileHandle = await open(resolvedFile, "r");
|
|
3372
|
-
try {
|
|
3373
|
-
let lineStartOffsets = cached?.lineStartOffsets.slice() ?? [];
|
|
3374
|
-
let scanStart = cached?.size ?? 0;
|
|
3375
|
-
let endsWithNewline = cached?.endsWithNewline ?? false;
|
|
3376
|
-
if (!cached || nextSize < cached.size) {
|
|
3377
|
-
lineStartOffsets = nextSize > 0 ? [0] : [];
|
|
3378
|
-
scanStart = 0;
|
|
3379
|
-
endsWithNewline = false;
|
|
3380
|
-
}
|
|
3381
|
-
else if (cached.endsWithNewline && nextSize > cached.size) {
|
|
3382
|
-
lineStartOffsets.push(cached.size);
|
|
3383
|
-
}
|
|
3384
|
-
let position = scanStart;
|
|
3385
|
-
const chunkBytes = 65_536;
|
|
3386
|
-
while (position < nextSize) {
|
|
3387
|
-
const readSize = Math.min(chunkBytes, nextSize - position);
|
|
3388
|
-
const buffer = Buffer.alloc(readSize);
|
|
3389
|
-
const { bytesRead } = await fileHandle.read(buffer, 0, readSize, position);
|
|
3390
|
-
if (bytesRead <= 0) {
|
|
3391
|
-
break;
|
|
3392
|
-
}
|
|
3393
|
-
for (let index = 0; index < bytesRead; index += 1) {
|
|
3394
|
-
if (buffer[index] !== 0x0a) {
|
|
3395
|
-
continue;
|
|
3396
|
-
}
|
|
3397
|
-
const nextLineStart = position + index + 1;
|
|
3398
|
-
if (nextLineStart < nextSize) {
|
|
3399
|
-
lineStartOffsets.push(nextLineStart);
|
|
3400
|
-
}
|
|
3401
|
-
}
|
|
3402
|
-
position += bytesRead;
|
|
3403
|
-
}
|
|
3404
|
-
if (nextSize > 0) {
|
|
3405
|
-
const tail = Buffer.alloc(1);
|
|
3406
|
-
const { bytesRead } = await fileHandle.read(tail, 0, 1, nextSize - 1);
|
|
3407
|
-
endsWithNewline = bytesRead > 0 && tail[0] === 0x0a;
|
|
3408
|
-
}
|
|
3409
|
-
else {
|
|
3410
|
-
endsWithNewline = false;
|
|
3411
|
-
}
|
|
3412
|
-
const nextEntry = {
|
|
3413
|
-
size: nextSize,
|
|
3414
|
-
lineStartOffsets,
|
|
3415
|
-
endsWithNewline,
|
|
3416
|
-
};
|
|
3417
|
-
sessionLineIndexCache.set(resolvedFile, nextEntry);
|
|
3418
|
-
return nextEntry;
|
|
3419
|
-
}
|
|
3420
|
-
finally {
|
|
3421
|
-
await fileHandle.close().catch(() => undefined);
|
|
3422
|
-
}
|
|
3423
|
-
}
|
|
3424
|
-
async function getAgentSessionRawRows(args) {
|
|
3425
|
-
const resolvedFile = resolveSessionFilePath(args.filePath);
|
|
3426
|
-
const index = await readSessionLineIndex(resolvedFile);
|
|
3427
|
-
const totalLines = index.lineStartOffsets.length;
|
|
3428
|
-
const sinceLine = Math.max(0, Math.floor(args.sinceLine));
|
|
3429
|
-
const beforeRowId = args.beforeRowId && args.beforeRowId > 0 ? Math.floor(args.beforeRowId) : null;
|
|
3430
|
-
const maxRawRows = 200;
|
|
3431
|
-
const maxSelectionBytes = 120_000;
|
|
3432
|
-
const maxLineSelectionBytes = 4_096;
|
|
3433
|
-
const maxReadBytes = 2_000_000;
|
|
3434
|
-
if (totalLines === 0) {
|
|
3435
|
-
return {
|
|
3436
|
-
rawRows: [],
|
|
3437
|
-
nextCursor: 0,
|
|
3438
|
-
};
|
|
3439
|
-
}
|
|
3440
|
-
let startLineIndex = 0;
|
|
3441
|
-
let endLineIndex = totalLines;
|
|
3442
|
-
const getLineSpanBytes = (lineIndex) => {
|
|
3443
|
-
const start = index.lineStartOffsets[lineIndex] ?? index.size;
|
|
3444
|
-
const end = lineIndex + 1 < totalLines ? (index.lineStartOffsets[lineIndex + 1] ?? index.size) : index.size;
|
|
3445
|
-
return Math.max(0, end - start);
|
|
3446
|
-
};
|
|
3447
|
-
if (beforeRowId !== null) {
|
|
3448
|
-
endLineIndex = Math.max(0, Math.min(totalLines, beforeRowId - 1));
|
|
3449
|
-
startLineIndex = endLineIndex;
|
|
3450
|
-
let collectedRows = 0;
|
|
3451
|
-
let collectedSelectionBytes = 0;
|
|
3452
|
-
let collectedReadBytes = 0;
|
|
3453
|
-
while (startLineIndex > 0 && collectedRows < maxRawRows) {
|
|
3454
|
-
const nextIndex = startLineIndex - 1;
|
|
3455
|
-
const nextReadBytes = getLineSpanBytes(nextIndex);
|
|
3456
|
-
const nextSelectionBytes = Math.min(nextReadBytes, maxLineSelectionBytes);
|
|
3457
|
-
if (collectedRows > 0 && collectedSelectionBytes + nextSelectionBytes > maxSelectionBytes) {
|
|
3458
|
-
break;
|
|
3459
|
-
}
|
|
3460
|
-
if (collectedRows > 0 && collectedReadBytes + nextReadBytes > maxReadBytes) {
|
|
3461
|
-
break;
|
|
3462
|
-
}
|
|
3463
|
-
startLineIndex = nextIndex;
|
|
3464
|
-
collectedRows += 1;
|
|
3465
|
-
collectedSelectionBytes += nextSelectionBytes;
|
|
3466
|
-
collectedReadBytes += nextReadBytes;
|
|
3467
|
-
}
|
|
3468
|
-
}
|
|
3469
|
-
else if (sinceLine > 0) {
|
|
3470
|
-
startLineIndex = Math.min(totalLines, sinceLine);
|
|
3471
|
-
endLineIndex = startLineIndex;
|
|
3472
|
-
let collectedRows = 0;
|
|
3473
|
-
let collectedSelectionBytes = 0;
|
|
3474
|
-
let collectedReadBytes = 0;
|
|
3475
|
-
while (endLineIndex < totalLines && collectedRows < maxRawRows) {
|
|
3476
|
-
const nextReadBytes = getLineSpanBytes(endLineIndex);
|
|
3477
|
-
const nextSelectionBytes = Math.min(nextReadBytes, maxLineSelectionBytes);
|
|
3478
|
-
if (collectedRows > 0 && collectedSelectionBytes + nextSelectionBytes > maxSelectionBytes) {
|
|
3479
|
-
break;
|
|
3480
|
-
}
|
|
3481
|
-
if (collectedRows > 0 && collectedReadBytes + nextReadBytes > maxReadBytes) {
|
|
3482
|
-
break;
|
|
3483
|
-
}
|
|
3484
|
-
endLineIndex += 1;
|
|
3485
|
-
collectedRows += 1;
|
|
3486
|
-
collectedSelectionBytes += nextSelectionBytes;
|
|
3487
|
-
collectedReadBytes += nextReadBytes;
|
|
3488
|
-
}
|
|
3489
|
-
}
|
|
3490
|
-
else {
|
|
3491
|
-
startLineIndex = totalLines;
|
|
3492
|
-
let collectedRows = 0;
|
|
3493
|
-
let collectedSelectionBytes = 0;
|
|
3494
|
-
let collectedReadBytes = 0;
|
|
3495
|
-
while (startLineIndex > 0 && collectedRows < maxRawRows) {
|
|
3496
|
-
const nextIndex = startLineIndex - 1;
|
|
3497
|
-
const nextReadBytes = getLineSpanBytes(nextIndex);
|
|
3498
|
-
const nextSelectionBytes = Math.min(nextReadBytes, maxLineSelectionBytes);
|
|
3499
|
-
if (collectedRows > 0 && collectedSelectionBytes + nextSelectionBytes > maxSelectionBytes) {
|
|
3500
|
-
break;
|
|
3501
|
-
}
|
|
3502
|
-
if (collectedRows > 0 && collectedReadBytes + nextReadBytes > maxReadBytes) {
|
|
3503
|
-
break;
|
|
3504
|
-
}
|
|
3505
|
-
startLineIndex = nextIndex;
|
|
3506
|
-
collectedRows += 1;
|
|
3507
|
-
collectedSelectionBytes += nextSelectionBytes;
|
|
3508
|
-
collectedReadBytes += nextReadBytes;
|
|
3509
|
-
}
|
|
3510
|
-
}
|
|
3511
|
-
if (startLineIndex >= endLineIndex) {
|
|
3512
|
-
return {
|
|
3513
|
-
rawRows: [],
|
|
3514
|
-
nextCursor: endLineIndex,
|
|
3515
|
-
};
|
|
3516
|
-
}
|
|
3517
|
-
const startOffset = index.lineStartOffsets[startLineIndex] ?? index.size;
|
|
3518
|
-
const endOffset = endLineIndex < totalLines ? (index.lineStartOffsets[endLineIndex] ?? index.size) : index.size;
|
|
3519
|
-
if (startOffset >= endOffset) {
|
|
3520
|
-
return {
|
|
3521
|
-
rawRows: [],
|
|
3522
|
-
nextCursor: endLineIndex,
|
|
3523
|
-
};
|
|
3524
|
-
}
|
|
3525
|
-
const fileHandle = await open(resolvedFile, "r");
|
|
3526
|
-
try {
|
|
3527
|
-
const readSize = endOffset - startOffset;
|
|
3528
|
-
const buffer = Buffer.alloc(readSize);
|
|
3529
|
-
const { bytesRead } = await fileHandle.read(buffer, 0, readSize, startOffset);
|
|
3530
|
-
const raw = buffer.toString("utf8", 0, bytesRead);
|
|
3531
|
-
const lines = raw.split(/\r?\n/);
|
|
3532
|
-
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
3533
|
-
lines.pop();
|
|
3534
|
-
}
|
|
3535
|
-
const rawRows = [];
|
|
3536
|
-
let lineNumber = startLineIndex + 1;
|
|
3537
|
-
for (const line of lines) {
|
|
3538
|
-
if (line.trim()) {
|
|
3539
|
-
const sanitized = sanitizeSessionRpcRawLine(line);
|
|
3540
|
-
if (!sanitized) {
|
|
3541
|
-
lineNumber += 1;
|
|
3542
|
-
continue;
|
|
3543
|
-
}
|
|
3544
|
-
rawRows.push({
|
|
3545
|
-
id: lineNumber,
|
|
3546
|
-
raw: sanitized,
|
|
3547
|
-
});
|
|
3548
|
-
}
|
|
3549
|
-
lineNumber += 1;
|
|
3550
|
-
}
|
|
3551
|
-
return {
|
|
3552
|
-
rawRows,
|
|
3553
|
-
nextCursor: endLineIndex,
|
|
3554
|
-
};
|
|
3555
|
-
}
|
|
3556
|
-
finally {
|
|
3557
|
-
await fileHandle.close().catch(() => undefined);
|
|
3558
|
-
}
|
|
3559
|
-
}
|
|
3560
|
-
function resolveSessionUploadsDir(sessionId) {
|
|
3561
|
-
const workspaceRoot = workspaceRootOverride ?? (process.env.WORKSPACE?.trim() || process.cwd());
|
|
3562
|
-
const safeSessionId = sessionId.trim().replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 160) || "session";
|
|
3563
|
-
return path.join(workspaceRoot, ".doer-agent", "sessions", safeSessionId);
|
|
3564
|
-
}
|
|
3565
|
-
async function deleteAgentSession(filePath, sessionId) {
|
|
3566
|
-
const resolvedFile = resolveSessionFilePath(filePath);
|
|
3567
|
-
sessionLineIndexCache.delete(resolvedFile);
|
|
3568
|
-
await unlink(resolvedFile);
|
|
3569
|
-
if (sessionId) {
|
|
3570
|
-
await rm(resolveSessionUploadsDir(sessionId), { recursive: true, force: true }).catch(() => undefined);
|
|
3571
|
-
}
|
|
3572
|
-
const sessionsRoot = path.resolve(getSessionsRootPath());
|
|
3573
|
-
let currentDir = path.dirname(resolvedFile);
|
|
3574
|
-
while (currentDir.startsWith(sessionsRoot + path.sep)) {
|
|
3575
|
-
try {
|
|
3576
|
-
const entries = await readdir(currentDir);
|
|
3577
|
-
if (entries.length > 0) {
|
|
3578
|
-
break;
|
|
3579
|
-
}
|
|
3580
|
-
await rmdir(currentDir);
|
|
3581
|
-
}
|
|
3582
|
-
catch {
|
|
3583
|
-
break;
|
|
3584
|
-
}
|
|
3585
|
-
currentDir = path.dirname(currentDir);
|
|
3586
|
-
}
|
|
3587
|
-
}
|
|
3588
|
-
function publishSessionRpcResponse(args) {
|
|
3589
|
-
try {
|
|
3590
|
-
args.nc.publish(args.responseSubject, sessionRpcCodec.encode(JSON.stringify(args.payload)));
|
|
3591
|
-
}
|
|
3592
|
-
catch (error) {
|
|
3593
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3594
|
-
writeAgentError(`session rpc publish failed responseSubject=${args.responseSubject}: ${message}`);
|
|
3595
|
-
}
|
|
3596
|
-
}
|
|
3597
|
-
function stopAllSessionWatchers() {
|
|
3598
|
-
const stops = [...activeSessionWatchers.values()];
|
|
3599
|
-
for (const stop of stops) {
|
|
3600
|
-
try {
|
|
3601
|
-
stop();
|
|
3602
|
-
}
|
|
3603
|
-
catch (error) {
|
|
3604
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3605
|
-
writeAgentError(`session watcher cleanup failed: ${message}`);
|
|
3606
|
-
}
|
|
3607
|
-
}
|
|
3608
|
-
}
|
|
3609
|
-
async function startSessionWatch(args) {
|
|
3610
|
-
const resolvedFile = resolveSessionFilePath(args.filePath);
|
|
3611
|
-
const canonicalFile = await realpath(resolvedFile).catch(() => resolvedFile);
|
|
3612
|
-
const watchId = `watch_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
3613
|
-
let watcher = null;
|
|
3614
|
-
let active = true;
|
|
3615
|
-
const emitEvent = (event) => {
|
|
3616
|
-
if (!active) {
|
|
3617
|
-
return;
|
|
3618
|
-
}
|
|
3619
|
-
publishSessionRpcResponse({
|
|
3620
|
-
nc: args.nc,
|
|
3621
|
-
responseSubject: args.responseSubject,
|
|
3622
|
-
payload: {
|
|
3623
|
-
requestId: args.requestId,
|
|
3624
|
-
ok: true,
|
|
3625
|
-
action: "watch",
|
|
3626
|
-
watchId,
|
|
3627
|
-
event,
|
|
3628
|
-
},
|
|
3629
|
-
});
|
|
3630
|
-
};
|
|
3631
|
-
const cleanup = () => {
|
|
3632
|
-
if (!active) {
|
|
3633
|
-
return;
|
|
3634
|
-
}
|
|
3635
|
-
active = false;
|
|
3636
|
-
activeSessionWatchers.delete(watchId);
|
|
3637
|
-
watcher?.close();
|
|
3638
|
-
watcher = null;
|
|
3639
|
-
};
|
|
3640
|
-
const notifyFromContent = () => {
|
|
3641
|
-
emitEvent({
|
|
3642
|
-
type: "messages.changed",
|
|
3643
|
-
at: formatLocalTimestamp(),
|
|
3644
|
-
});
|
|
3645
|
-
};
|
|
3646
|
-
watcher = watch(canonicalFile, { persistent: false }, (eventType) => {
|
|
3647
|
-
if (!active) {
|
|
3648
|
-
return;
|
|
3649
|
-
}
|
|
3650
|
-
if (eventType === "change" || eventType === "rename") {
|
|
3651
|
-
notifyFromContent();
|
|
3652
|
-
}
|
|
3653
|
-
});
|
|
3654
|
-
activeSessionWatchers.set(watchId, cleanup);
|
|
3655
|
-
emitEvent({ type: "stream.started", watchId, at: formatLocalTimestamp() });
|
|
3656
|
-
return watchId;
|
|
3657
|
-
}
|
|
3658
|
-
async function handleSessionRpcMessage(args) {
|
|
3659
|
-
let requestId = "unknown";
|
|
3660
|
-
let responseSubject = "";
|
|
3661
|
-
try {
|
|
3662
|
-
const payload = JSON.parse(sessionRpcCodec.decode(args.msg.data));
|
|
3663
|
-
const request = normalizeSessionRpcRequest({ request: payload, agentId: args.agentId });
|
|
3664
|
-
requestId = request.requestId;
|
|
3665
|
-
responseSubject = request.responseSubject;
|
|
3666
|
-
if (request.action === "list") {
|
|
3667
|
-
const sessions = await listAgentSessions();
|
|
3668
|
-
publishSessionRpcResponse({
|
|
3669
|
-
nc: args.jetstream.nc,
|
|
3670
|
-
responseSubject,
|
|
3671
|
-
payload: { requestId, ok: true, action: "list", sessions },
|
|
3672
|
-
});
|
|
3673
|
-
return;
|
|
3674
|
-
}
|
|
3675
|
-
if (request.action === "messages") {
|
|
3676
|
-
const result = await getAgentSessionRawRows({
|
|
3677
|
-
filePath: request.filePath ?? "",
|
|
3678
|
-
sinceLine: request.sinceLine,
|
|
3679
|
-
beforeRowId: request.beforeRowId,
|
|
3680
|
-
pageSize: request.pageSize,
|
|
3681
|
-
});
|
|
3682
|
-
publishSessionRpcResponse({
|
|
3683
|
-
nc: args.jetstream.nc,
|
|
3684
|
-
responseSubject,
|
|
3685
|
-
payload: { requestId, ok: true, action: "messages", rawRows: result.rawRows, nextCursor: result.nextCursor },
|
|
3686
|
-
});
|
|
3687
|
-
return;
|
|
3688
|
-
}
|
|
3689
|
-
if (request.action === "delete") {
|
|
3690
|
-
await deleteAgentSession(request.filePath ?? "", request.sessionId);
|
|
3691
|
-
publishSessionRpcResponse({
|
|
3692
|
-
nc: args.jetstream.nc,
|
|
3693
|
-
responseSubject,
|
|
3694
|
-
payload: { requestId, ok: true, action: "delete" },
|
|
3695
|
-
});
|
|
3696
|
-
return;
|
|
3697
|
-
}
|
|
3698
|
-
if (request.action === "watch") {
|
|
3699
|
-
const watchId = await startSessionWatch({
|
|
232
|
+
function subscribeToGitRpc(args) {
|
|
233
|
+
const subject = buildAgentGitRpcSubject(args.userId, args.agentId);
|
|
234
|
+
args.jetstream.nc.subscribe(subject, {
|
|
235
|
+
callback: (error, msg) => {
|
|
236
|
+
if (error) {
|
|
237
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
238
|
+
writeAgentError(`git rpc subscription error: ${message}`);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
void handleGitRpcMessage({
|
|
242
|
+
msg,
|
|
3700
243
|
nc: args.jetstream.nc,
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
filePath: request.filePath ?? "",
|
|
244
|
+
agentId: args.agentId,
|
|
245
|
+
onError: writeAgentError,
|
|
3704
246
|
});
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
writeAgentInfo(`git rpc subscribed subject=${subject}`);
|
|
250
|
+
}
|
|
251
|
+
async function handleRunRpcMessage(args) {
|
|
252
|
+
let requestId = "unknown";
|
|
253
|
+
let responseSubject = "";
|
|
254
|
+
try {
|
|
255
|
+
const payload = JSON.parse(runRpcCodec.decode(args.msg.data));
|
|
256
|
+
const request = normalizeRunRpcRequest({
|
|
257
|
+
request: payload,
|
|
258
|
+
agentId: args.agentId,
|
|
259
|
+
normalizeModel: normalizeCodexModel,
|
|
260
|
+
normalizeImagePaths: normalizeRunImagePaths,
|
|
261
|
+
normalizeEnvPatch,
|
|
262
|
+
normalizeCodexAuthBundle: normalizeShellRpcCodexAuthBundle,
|
|
263
|
+
});
|
|
264
|
+
requestId = request.requestId;
|
|
265
|
+
responseSubject = request.responseSubject;
|
|
266
|
+
if (request.action === "start") {
|
|
267
|
+
const runId = request.runId ?? requestId;
|
|
268
|
+
await claimRunStartSlot({
|
|
269
|
+
workspaceRoot: resolveWorkspaceRoot(),
|
|
270
|
+
runId,
|
|
271
|
+
sessionId: request.sessionId,
|
|
272
|
+
formatTimestamp: formatLocalTimestamp,
|
|
3709
273
|
});
|
|
274
|
+
try {
|
|
275
|
+
const workspaceRoot = resolveWorkspaceRoot();
|
|
276
|
+
const localAgentSettings = await readAgentSettingsConfig({ workspaceRoot });
|
|
277
|
+
const customInstructions = await readAgentModelInstructions(workspaceRoot);
|
|
278
|
+
const task = await startManagedRun({
|
|
279
|
+
requestId,
|
|
280
|
+
runId,
|
|
281
|
+
serverBaseUrl: args.serverBaseUrl,
|
|
282
|
+
userId: args.userId,
|
|
283
|
+
agentId: args.agentId,
|
|
284
|
+
nc: args.jetstream.nc,
|
|
285
|
+
sessionId: request.sessionId,
|
|
286
|
+
codexArgs: buildManagedCodexArgs({
|
|
287
|
+
prompt: request.prompt ?? "",
|
|
288
|
+
imagePaths: request.imagePaths,
|
|
289
|
+
sessionId: request.sessionId,
|
|
290
|
+
model: request.model,
|
|
291
|
+
personality: localAgentSettings.general.personality,
|
|
292
|
+
modelInstructionsFile: customInstructions ? resolveAgentModelInstructionsFilePath(workspaceRoot) : null,
|
|
293
|
+
}),
|
|
294
|
+
cwd: request.cwd,
|
|
295
|
+
runtimeEnvPatch: request.runtimeEnvPatch,
|
|
296
|
+
codexAuthBundle: request.codexAuthBundle,
|
|
297
|
+
agentToken: args.agentToken,
|
|
298
|
+
});
|
|
299
|
+
publishRunRpcResponse({ nc: args.jetstream.nc, responseSubject, payload: { requestId, ok: true, task } });
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
await releaseRunStartSlot({
|
|
303
|
+
workspaceRoot: resolveWorkspaceRoot(),
|
|
304
|
+
runId,
|
|
305
|
+
sessionId: request.sessionId,
|
|
306
|
+
}).catch(() => undefined);
|
|
307
|
+
throw error;
|
|
308
|
+
}
|
|
3710
309
|
return;
|
|
3711
310
|
}
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
publishSessionRpcResponse({
|
|
311
|
+
await handleNonStartRunRpc({
|
|
312
|
+
request,
|
|
3715
313
|
nc: args.jetstream.nc,
|
|
3716
|
-
|
|
3717
|
-
|
|
314
|
+
userId: args.userId,
|
|
315
|
+
agentId: args.agentId,
|
|
316
|
+
listPersistedRunTasks: async () => listPersistedRunTasks(resolveWorkspaceRoot()),
|
|
317
|
+
cloneRunTask,
|
|
318
|
+
getStoredRun: async (runId) => getStoredRun(resolveWorkspaceRoot(), runId),
|
|
319
|
+
persistRunTask: async (task) => persistRunTask(resolveWorkspaceRoot(), task),
|
|
320
|
+
publishImmediateRunEvent: (task) => publishImmediateRunEvent({
|
|
321
|
+
nc: args.jetstream.nc,
|
|
322
|
+
userId: task.userId,
|
|
323
|
+
task,
|
|
324
|
+
buildRunEventsSubject: buildAgentRunEventsSubject,
|
|
325
|
+
}),
|
|
326
|
+
writeRunStatus,
|
|
327
|
+
sendSignalToPid,
|
|
328
|
+
formatTimestamp: formatLocalTimestamp,
|
|
3718
329
|
});
|
|
3719
330
|
}
|
|
3720
331
|
catch (error) {
|
|
3721
332
|
const message = error instanceof Error ? error.message : String(error);
|
|
3722
333
|
if (responseSubject) {
|
|
3723
|
-
|
|
334
|
+
publishRunRpcResponse({
|
|
3724
335
|
nc: args.jetstream.nc,
|
|
3725
336
|
responseSubject,
|
|
3726
|
-
payload: {
|
|
3727
|
-
requestId,
|
|
3728
|
-
ok: false,
|
|
3729
|
-
error: message,
|
|
3730
|
-
},
|
|
337
|
+
payload: { requestId, ok: false, error: message },
|
|
3731
338
|
});
|
|
3732
339
|
}
|
|
3733
|
-
writeAgentError(`
|
|
340
|
+
writeAgentError(`run rpc failed requestId=${requestId} error=${message}`);
|
|
3734
341
|
}
|
|
3735
342
|
}
|
|
3736
|
-
function
|
|
3737
|
-
const subject =
|
|
343
|
+
function subscribeToRunRpc(args) {
|
|
344
|
+
const subject = buildAgentRunRpcSubject(args.userId, args.agentId);
|
|
3738
345
|
args.jetstream.nc.subscribe(subject, {
|
|
3739
346
|
callback: (error, msg) => {
|
|
3740
347
|
if (error) {
|
|
3741
348
|
const message = error instanceof Error ? error.message : String(error);
|
|
3742
|
-
writeAgentError(`
|
|
349
|
+
writeAgentError(`run rpc subscription error: ${message}`);
|
|
3743
350
|
return;
|
|
3744
351
|
}
|
|
3745
|
-
void
|
|
352
|
+
void handleRunRpcMessage({
|
|
3746
353
|
msg,
|
|
3747
354
|
jetstream: args.jetstream,
|
|
355
|
+
serverBaseUrl: args.serverBaseUrl,
|
|
356
|
+
userId: args.userId,
|
|
3748
357
|
agentId: args.agentId,
|
|
358
|
+
agentToken: args.agentToken,
|
|
3749
359
|
});
|
|
3750
360
|
},
|
|
3751
361
|
});
|
|
3752
|
-
writeAgentInfo(`
|
|
3753
|
-
}
|
|
3754
|
-
async function handleFsRpcMessage(args) {
|
|
3755
|
-
let payload = {};
|
|
3756
|
-
try {
|
|
3757
|
-
payload = JSON.parse(fsRpcCodec.decode(args.msg.data));
|
|
3758
|
-
if (typeof payload.agentId === "string" && payload.agentId.trim() && payload.agentId !== args.agentId) {
|
|
3759
|
-
throw new Error("agent id mismatch");
|
|
3760
|
-
}
|
|
3761
|
-
const result = await executeFsRpc({
|
|
3762
|
-
request: payload,
|
|
3763
|
-
serverBaseUrl: args.serverBaseUrl,
|
|
3764
|
-
agentToken: args.agentToken,
|
|
3765
|
-
});
|
|
3766
|
-
args.msg.respond(fsRpcCodec.encode(JSON.stringify(result)));
|
|
3767
|
-
}
|
|
3768
|
-
catch (error) {
|
|
3769
|
-
const message = error instanceof Error ? error.message : "unknown error";
|
|
3770
|
-
const action = typeof payload.action === "string" ? payload.action : "";
|
|
3771
|
-
const response = {
|
|
3772
|
-
ok: false,
|
|
3773
|
-
action,
|
|
3774
|
-
path: typeof payload.path === "string" ? payload.path : ".",
|
|
3775
|
-
error: message,
|
|
3776
|
-
};
|
|
3777
|
-
args.msg.respond(fsRpcCodec.encode(JSON.stringify(response)));
|
|
3778
|
-
writeAgentError(`fs rpc failed action=${action || "unknown"} error=${message}`);
|
|
3779
|
-
}
|
|
362
|
+
writeAgentInfo(`run rpc subscribed subject=${subject}`);
|
|
3780
363
|
}
|
|
3781
364
|
function subscribeToFsRpc(args) {
|
|
3782
365
|
const subject = buildAgentFsRpcSubject(args.userId, args.agentId);
|
|
@@ -3789,527 +372,48 @@ function subscribeToFsRpc(args) {
|
|
|
3789
372
|
}
|
|
3790
373
|
void handleFsRpcMessage({
|
|
3791
374
|
msg,
|
|
375
|
+
workspaceRoot: resolveWorkspaceRoot(),
|
|
3792
376
|
serverBaseUrl: args.serverBaseUrl,
|
|
3793
|
-
userId: args.userId,
|
|
3794
377
|
agentId: args.agentId,
|
|
3795
378
|
agentToken: args.agentToken,
|
|
379
|
+
onError: writeAgentError,
|
|
3796
380
|
});
|
|
3797
381
|
},
|
|
3798
382
|
});
|
|
3799
383
|
writeAgentInfo(`fs rpc subscribed subject=${subject}`);
|
|
3800
384
|
}
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
issuedAt: typeof row.issuedAt === "string" ? row.issuedAt : undefined,
|
|
3816
|
-
expiresAt: typeof row.expiresAt === "string" ? row.expiresAt : undefined,
|
|
3817
|
-
authJson: authJson ?? undefined,
|
|
3818
|
-
apiKey,
|
|
3819
|
-
};
|
|
3820
|
-
}
|
|
3821
|
-
async function postJson(url, body) {
|
|
3822
|
-
const res = await fetch(url, {
|
|
3823
|
-
method: "POST",
|
|
3824
|
-
headers: { "Content-Type": "application/json" },
|
|
3825
|
-
body: JSON.stringify(body),
|
|
3826
|
-
});
|
|
3827
|
-
const text = await res.text();
|
|
3828
|
-
let data = {};
|
|
3829
|
-
if (text) {
|
|
3830
|
-
try {
|
|
3831
|
-
data = JSON.parse(text);
|
|
3832
|
-
}
|
|
3833
|
-
catch {
|
|
3834
|
-
data = {};
|
|
3835
|
-
}
|
|
3836
|
-
}
|
|
3837
|
-
if (!res.ok) {
|
|
3838
|
-
const errObj = (data && typeof data === "object" ? data : {});
|
|
3839
|
-
const message = typeof errObj.error === "string" ? errObj.error : `HTTP ${res.status}`;
|
|
3840
|
-
throw new Error(message);
|
|
3841
|
-
}
|
|
3842
|
-
return data;
|
|
3843
|
-
}
|
|
3844
|
-
async function getJson(url) {
|
|
3845
|
-
const res = await fetch(url);
|
|
3846
|
-
const text = await res.text();
|
|
3847
|
-
let data = {};
|
|
3848
|
-
if (text) {
|
|
3849
|
-
try {
|
|
3850
|
-
data = JSON.parse(text);
|
|
3851
|
-
}
|
|
3852
|
-
catch {
|
|
3853
|
-
data = {};
|
|
3854
|
-
}
|
|
3855
|
-
}
|
|
3856
|
-
if (!res.ok) {
|
|
3857
|
-
const errObj = (data && typeof data === "object" ? data : {});
|
|
3858
|
-
const message = typeof errObj.error === "string" ? errObj.error : `HTTP ${res.status}`;
|
|
3859
|
-
throw new Error(message);
|
|
3860
|
-
}
|
|
3861
|
-
return data;
|
|
3862
|
-
}
|
|
3863
|
-
const nextEventSeqByTask = new Map();
|
|
3864
|
-
function reserveNextEventSeq(taskId) {
|
|
3865
|
-
const current = nextEventSeqByTask.get(taskId) ?? 1;
|
|
3866
|
-
nextEventSeqByTask.set(taskId, current + 1);
|
|
3867
|
-
return current;
|
|
3868
|
-
}
|
|
3869
|
-
function emitAgentMetaLog(level, message) {
|
|
3870
|
-
const ctx = activeTaskLogContext;
|
|
3871
|
-
if (!ctx) {
|
|
3872
|
-
return;
|
|
3873
|
-
}
|
|
3874
|
-
const seq = reserveNextEventSeq(ctx.taskId);
|
|
3875
|
-
void recordAgentEvent({
|
|
3876
|
-
jetstream: ctx.jetstream,
|
|
3877
|
-
serverBaseUrl: ctx.serverBaseUrl,
|
|
3878
|
-
taskId: ctx.taskId,
|
|
3879
|
-
userId: ctx.userId,
|
|
3880
|
-
type: "meta",
|
|
3881
|
-
seq,
|
|
3882
|
-
payload: {
|
|
3883
|
-
channel: "agent",
|
|
3884
|
-
level,
|
|
3885
|
-
message,
|
|
3886
|
-
at: formatLocalTimestamp(),
|
|
3887
|
-
},
|
|
3888
|
-
}).catch((error) => {
|
|
3889
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
3890
|
-
process.stderr.write(`[doer-agent] meta log persist failed task=${ctx.taskId}: ${detail}\n`);
|
|
3891
|
-
});
|
|
3892
|
-
}
|
|
3893
|
-
async function recordAgentEvent(args) {
|
|
3894
|
-
await args.jetstream.js.publish(args.jetstream.subject, args.jetstream.codec.encode({
|
|
3895
|
-
serverBaseUrl: args.serverBaseUrl,
|
|
3896
|
-
userId: args.userId,
|
|
3897
|
-
taskId: args.taskId,
|
|
3898
|
-
type: args.type,
|
|
3899
|
-
seq: args.seq,
|
|
3900
|
-
payload: args.payload,
|
|
3901
|
-
}));
|
|
3902
|
-
}
|
|
3903
|
-
function persistEventOrFatal(args) {
|
|
3904
|
-
void (async () => {
|
|
3905
|
-
let attempt = 0;
|
|
3906
|
-
let delayMs = 150;
|
|
3907
|
-
while (attempt < 3) {
|
|
3908
|
-
attempt += 1;
|
|
3909
|
-
try {
|
|
3910
|
-
await recordAgentEvent(args);
|
|
3911
|
-
return;
|
|
3912
|
-
}
|
|
3913
|
-
catch (error) {
|
|
3914
|
-
if (attempt >= 3) {
|
|
3915
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3916
|
-
writeAgentError(`task=${args.taskId} ${args.context}: ${message} (dropped after ${attempt} attempts)`);
|
|
3917
|
-
return;
|
|
3918
|
-
}
|
|
3919
|
-
await sleep(delayMs);
|
|
3920
|
-
delayMs *= 2;
|
|
3921
|
-
}
|
|
3922
|
-
}
|
|
3923
|
-
})();
|
|
3924
|
-
}
|
|
3925
|
-
async function heartbeatAgentSession(args) {
|
|
3926
|
-
await args.nc.flush();
|
|
3927
|
-
await postJson(`${args.serverBaseUrl}/api/agent/heartbeat`, {
|
|
3928
|
-
userId: args.userId,
|
|
3929
|
-
agentToken: args.agentToken,
|
|
3930
|
-
});
|
|
3931
|
-
}
|
|
3932
|
-
async function claimTaskById(args) {
|
|
3933
|
-
const response = await postJson(`${args.serverBaseUrl}/api/agent/tasks/claim`, {
|
|
3934
|
-
userId: args.userId,
|
|
3935
|
-
agentToken: args.agentToken,
|
|
3936
|
-
taskId: args.taskId,
|
|
3937
|
-
});
|
|
3938
|
-
return response.task ?? null;
|
|
3939
|
-
}
|
|
3940
|
-
async function runClaimedTask(args) {
|
|
3941
|
-
try {
|
|
3942
|
-
writeAgentInfo(`run task=${args.task.id} command=${args.task.command}`);
|
|
3943
|
-
await runTask({
|
|
3944
|
-
serverBaseUrl: args.serverBaseUrl,
|
|
3945
|
-
taskId: args.task.id,
|
|
3946
|
-
command: args.task.command,
|
|
3947
|
-
cwd: args.task.cwd,
|
|
3948
|
-
userId: args.userId,
|
|
3949
|
-
agentToken: args.agentToken,
|
|
3950
|
-
jetstream: args.jetstream,
|
|
3951
|
-
}).catch(async (error) => {
|
|
3952
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3953
|
-
writeAgentError(`task=${args.task.id} run failed: ${message}`);
|
|
3954
|
-
const failPayload = {
|
|
3955
|
-
status: "failed",
|
|
3956
|
-
error: message,
|
|
3957
|
-
finishedAt: formatLocalTimestamp(),
|
|
3958
|
-
};
|
|
3959
|
-
await recordAgentEvent({
|
|
3960
|
-
jetstream: args.jetstream,
|
|
3961
|
-
serverBaseUrl: args.serverBaseUrl,
|
|
3962
|
-
taskId: args.task.id,
|
|
3963
|
-
userId: args.userId,
|
|
3964
|
-
type: "status",
|
|
3965
|
-
seq: reserveNextEventSeq(args.task.id),
|
|
3966
|
-
payload: failPayload,
|
|
3967
|
-
});
|
|
3968
|
-
});
|
|
3969
|
-
}
|
|
3970
|
-
finally {
|
|
3971
|
-
if (activeTaskLogContext?.taskId === args.task.id) {
|
|
3972
|
-
activeTaskLogContext = null;
|
|
3973
|
-
}
|
|
3974
|
-
}
|
|
3975
|
-
}
|
|
3976
|
-
async function checkCancelRequested(args) {
|
|
3977
|
-
const query = new URLSearchParams({
|
|
3978
|
-
userId: args.userId,
|
|
3979
|
-
agentToken: args.agentToken,
|
|
3980
|
-
});
|
|
3981
|
-
const response = await getJson(`${args.serverBaseUrl}/api/agent/tasks/${encodeURIComponent(args.taskId)}/events?${query.toString()}`);
|
|
3982
|
-
return Boolean(response.task?.cancelRequested);
|
|
3983
|
-
}
|
|
3984
|
-
async function syncCodexAuthState(args) {
|
|
3985
|
-
const envPatch = {};
|
|
3986
|
-
const synced = false;
|
|
3987
|
-
if (args.authMode === "api_key") {
|
|
3988
|
-
if (args.apiKey) {
|
|
3989
|
-
envPatch.OPENAI_API_KEY = args.apiKey;
|
|
3990
|
-
}
|
|
3991
|
-
}
|
|
3992
|
-
return {
|
|
3993
|
-
envPatch,
|
|
3994
|
-
cleanup: async () => { },
|
|
3995
|
-
meta: {
|
|
3996
|
-
codexAuthSource: args.source,
|
|
3997
|
-
codexAuthMode: args.authMode ?? null,
|
|
3998
|
-
codexAuthHasApiKey: Boolean(args.apiKey),
|
|
3999
|
-
codexAuthHasAuthJson: Boolean(args.authJson),
|
|
4000
|
-
codexAuthIssuedAt: args.issuedAt ?? null,
|
|
4001
|
-
codexAuthExpiresAt: args.expiresAt ?? null,
|
|
4002
|
-
codexAuthSynced: synced,
|
|
4003
|
-
},
|
|
4004
|
-
};
|
|
4005
|
-
}
|
|
4006
|
-
async function prepareTaskCodexAuth(args) {
|
|
4007
|
-
void args;
|
|
4008
|
-
return await syncCodexAuthState({
|
|
4009
|
-
source: "agent_local",
|
|
4010
|
-
authJson: null,
|
|
4011
|
-
issuedAt: null,
|
|
4012
|
-
expiresAt: null,
|
|
4013
|
-
});
|
|
4014
|
-
}
|
|
4015
|
-
async function prepareCodexAuthBundle(bundle) {
|
|
4016
|
-
if (!bundle) {
|
|
4017
|
-
return null;
|
|
4018
|
-
}
|
|
4019
|
-
return await syncCodexAuthState({
|
|
4020
|
-
source: "server_bundle",
|
|
4021
|
-
authMode: bundle.authMode,
|
|
4022
|
-
apiKey: bundle.apiKey,
|
|
4023
|
-
authJson: bundle.authJson ?? null,
|
|
4024
|
-
issuedAt: bundle.issuedAt ?? null,
|
|
4025
|
-
expiresAt: bundle.expiresAt ?? null,
|
|
4026
|
-
});
|
|
4027
|
-
}
|
|
4028
|
-
async function prepareCommandExecution(args) {
|
|
4029
|
-
const shellPath = resolveShellPath();
|
|
4030
|
-
const taskWorkspace = resolveTaskWorkspace(args.cwd);
|
|
4031
|
-
const codexHome = resolveCodexHomePath();
|
|
4032
|
-
await mkdir(codexHome, { recursive: true });
|
|
4033
|
-
const codexAuth = await prepareCodexAuthBundle(args.codexAuthBundle);
|
|
4034
|
-
const localAgentSettings = await readAgentSettingsConfig(null);
|
|
4035
|
-
const baseTaskEnvPatch = {
|
|
4036
|
-
CODEX_HOME: codexHome,
|
|
4037
|
-
DOER_USER_ID: args.userId,
|
|
4038
|
-
DOER_AGENT_TASK_ID: args.taskId,
|
|
4039
|
-
...buildAgentSettingsEnvPatch(localAgentSettings),
|
|
4040
|
-
...args.runtimeEnvPatch,
|
|
4041
|
-
...(codexAuth?.envPatch ?? {}),
|
|
4042
|
-
WORKSPACE: taskWorkspace,
|
|
4043
|
-
};
|
|
4044
|
-
const taskGitEnv = await prepareTaskGitEnv({
|
|
4045
|
-
cwd: taskWorkspace,
|
|
4046
|
-
baseEnvPatch: baseTaskEnvPatch,
|
|
4047
|
-
});
|
|
4048
|
-
const runtimeBinPath = path.join(AGENT_PROJECT_DIR, "runtime/bin");
|
|
4049
|
-
const taskPath = [runtimeBinPath, process.env.PATH || ""].filter(Boolean).join(path.delimiter);
|
|
4050
|
-
return {
|
|
4051
|
-
shellPath,
|
|
4052
|
-
taskWorkspace,
|
|
4053
|
-
taskPath,
|
|
4054
|
-
env: {
|
|
4055
|
-
...process.env,
|
|
4056
|
-
...baseTaskEnvPatch,
|
|
4057
|
-
...taskGitEnv.envPatch,
|
|
4058
|
-
PATH: taskPath,
|
|
4059
|
-
},
|
|
4060
|
-
taskGitMeta: taskGitEnv.meta ?? {},
|
|
4061
|
-
codexAuthMeta: codexAuth?.meta ?? { codexAuthSynced: false },
|
|
4062
|
-
codexAuthCleanup: codexAuth?.cleanup ?? (async () => { }),
|
|
4063
|
-
};
|
|
4064
|
-
}
|
|
4065
|
-
function spawnPreparedCommand(args) {
|
|
4066
|
-
const env = {
|
|
4067
|
-
...args.env,
|
|
4068
|
-
DOER_AGENT_TOKEN: args.agentToken,
|
|
4069
|
-
};
|
|
4070
|
-
const child = args.kind === "apply_patch"
|
|
4071
|
-
? spawn("apply_patch", {
|
|
4072
|
-
cwd: args.taskWorkspace,
|
|
4073
|
-
detached: process.platform !== "win32",
|
|
4074
|
-
env,
|
|
4075
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
4076
|
-
})
|
|
4077
|
-
: spawn(args.command ?? "", {
|
|
4078
|
-
cwd: args.taskWorkspace,
|
|
4079
|
-
shell: args.shellPath,
|
|
4080
|
-
detached: process.platform !== "win32",
|
|
4081
|
-
env,
|
|
4082
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
4083
|
-
});
|
|
4084
|
-
if (args.kind === "apply_patch") {
|
|
4085
|
-
child.stdin?.write(args.patch ?? "");
|
|
4086
|
-
child.stdin?.end();
|
|
4087
|
-
}
|
|
4088
|
-
child.stdout.setEncoding("utf8");
|
|
4089
|
-
child.stderr.setEncoding("utf8");
|
|
4090
|
-
return child;
|
|
4091
|
-
}
|
|
4092
|
-
async function runTask(args) {
|
|
4093
|
-
activeTaskLogContext = {
|
|
4094
|
-
jetstream: args.jetstream,
|
|
4095
|
-
serverBaseUrl: args.serverBaseUrl,
|
|
4096
|
-
taskId: args.taskId,
|
|
4097
|
-
userId: args.userId,
|
|
4098
|
-
};
|
|
4099
|
-
const shellPath = resolveShellPath();
|
|
4100
|
-
const taskWorkspace = resolveTaskWorkspace(args.cwd);
|
|
4101
|
-
const codexHome = resolveCodexHomePath();
|
|
4102
|
-
await mkdir(codexHome, { recursive: true });
|
|
4103
|
-
const runtimeConfig = await prepareTaskRuntimeConfig({
|
|
4104
|
-
serverBaseUrl: args.serverBaseUrl,
|
|
4105
|
-
taskId: args.taskId,
|
|
4106
|
-
userId: args.userId,
|
|
4107
|
-
agentToken: args.agentToken,
|
|
4108
|
-
});
|
|
4109
|
-
const codexAuth = await prepareTaskCodexAuth({
|
|
4110
|
-
serverBaseUrl: args.serverBaseUrl,
|
|
4111
|
-
taskId: args.taskId,
|
|
4112
|
-
userId: args.userId,
|
|
4113
|
-
agentToken: args.agentToken,
|
|
4114
|
-
});
|
|
4115
|
-
const localAgentSettings = await readAgentSettingsConfig(null);
|
|
4116
|
-
const baseTaskEnvPatch = {
|
|
4117
|
-
CODEX_HOME: codexHome,
|
|
4118
|
-
...buildAgentSettingsEnvPatch(localAgentSettings),
|
|
4119
|
-
...(runtimeConfig?.envPatch ?? {}),
|
|
4120
|
-
...(codexAuth?.envPatch ?? {}),
|
|
4121
|
-
WORKSPACE: taskWorkspace,
|
|
4122
|
-
};
|
|
4123
|
-
const taskGitEnv = await prepareTaskGitEnv({
|
|
4124
|
-
cwd: taskWorkspace,
|
|
4125
|
-
baseEnvPatch: baseTaskEnvPatch,
|
|
4126
|
-
});
|
|
4127
|
-
await recordAgentEvent({ jetstream: args.jetstream,
|
|
4128
|
-
serverBaseUrl: args.serverBaseUrl,
|
|
4129
|
-
taskId: args.taskId,
|
|
4130
|
-
userId: args.userId,
|
|
4131
|
-
type: "meta",
|
|
4132
|
-
seq: reserveNextEventSeq(args.taskId),
|
|
4133
|
-
payload: {
|
|
4134
|
-
host: process.platform,
|
|
4135
|
-
pid: process.pid,
|
|
4136
|
-
startedAt: formatLocalTimestamp(),
|
|
4137
|
-
command: args.command,
|
|
4138
|
-
cwd: taskWorkspace,
|
|
4139
|
-
requestedCwd: args.cwd,
|
|
4140
|
-
shell: shellPath,
|
|
4141
|
-
...(runtimeConfig?.meta ?? { runtimeConfigSynced: false }),
|
|
4142
|
-
...(codexAuth?.meta ?? { codexAuthSynced: false }),
|
|
4143
|
-
...(taskGitEnv.meta ?? {}),
|
|
4144
|
-
},
|
|
4145
|
-
});
|
|
4146
|
-
try {
|
|
4147
|
-
let terminationReason = null;
|
|
4148
|
-
let cancelStage1Timer = null;
|
|
4149
|
-
let cancelStage2Timer = null;
|
|
4150
|
-
let stopCancelPolling = false;
|
|
4151
|
-
let cancelSignalSent = false;
|
|
4152
|
-
const runtimeBinPath = path.join(AGENT_PROJECT_DIR, "runtime/bin");
|
|
4153
|
-
const taskPath = [runtimeBinPath, process.env.PATH || ""].filter(Boolean).join(path.delimiter);
|
|
4154
|
-
const child = spawn(args.command, {
|
|
4155
|
-
cwd: taskWorkspace,
|
|
4156
|
-
shell: shellPath,
|
|
4157
|
-
detached: process.platform !== "win32",
|
|
4158
|
-
env: {
|
|
4159
|
-
...process.env,
|
|
4160
|
-
...baseTaskEnvPatch,
|
|
4161
|
-
...taskGitEnv.envPatch,
|
|
4162
|
-
PATH: taskPath,
|
|
4163
|
-
DOER_AGENT_TOKEN: args.agentToken,
|
|
4164
|
-
},
|
|
4165
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
4166
|
-
});
|
|
4167
|
-
child.stdout.setEncoding("utf8");
|
|
4168
|
-
child.stderr.setEncoding("utf8");
|
|
4169
|
-
const requestCancel = () => {
|
|
4170
|
-
if (cancelSignalSent || terminationReason === "cancel") {
|
|
4171
|
-
return;
|
|
4172
|
-
}
|
|
4173
|
-
cancelSignalSent = true;
|
|
4174
|
-
terminationReason = "cancel";
|
|
4175
|
-
sendSignalToTaskProcess(child, "SIGINT");
|
|
4176
|
-
cancelStage1Timer = setTimeout(() => {
|
|
4177
|
-
sendSignalToTaskProcess(child, "SIGTERM");
|
|
4178
|
-
}, 1200);
|
|
4179
|
-
cancelStage1Timer.unref?.();
|
|
4180
|
-
cancelStage2Timer = setTimeout(() => {
|
|
4181
|
-
sendSignalToTaskProcess(child, "SIGKILL");
|
|
4182
|
-
}, 3500);
|
|
4183
|
-
cancelStage2Timer.unref?.();
|
|
4184
|
-
};
|
|
4185
|
-
child.stdout.on("data", (chunk) => {
|
|
4186
|
-
writeTaskStream(args.taskId, "stdout", chunk);
|
|
4187
|
-
const seq = reserveNextEventSeq(args.taskId);
|
|
4188
|
-
persistEventOrFatal({
|
|
4189
|
-
jetstream: args.jetstream,
|
|
4190
|
-
serverBaseUrl: args.serverBaseUrl,
|
|
4191
|
-
taskId: args.taskId,
|
|
4192
|
-
userId: args.userId,
|
|
4193
|
-
type: "stdout",
|
|
4194
|
-
seq,
|
|
4195
|
-
payload: { chunk, at: formatLocalTimestamp() },
|
|
4196
|
-
context: "stdout persist failed",
|
|
4197
|
-
});
|
|
4198
|
-
});
|
|
4199
|
-
child.stderr.on("data", (chunk) => {
|
|
4200
|
-
writeTaskStream(args.taskId, "stderr", chunk);
|
|
4201
|
-
const seq = reserveNextEventSeq(args.taskId);
|
|
4202
|
-
persistEventOrFatal({
|
|
4203
|
-
jetstream: args.jetstream,
|
|
4204
|
-
serverBaseUrl: args.serverBaseUrl,
|
|
4205
|
-
taskId: args.taskId,
|
|
4206
|
-
userId: args.userId,
|
|
4207
|
-
type: "stderr",
|
|
4208
|
-
seq,
|
|
4209
|
-
payload: { chunk, at: formatLocalTimestamp() },
|
|
4210
|
-
context: "stderr persist failed",
|
|
4211
|
-
});
|
|
4212
|
-
});
|
|
4213
|
-
const cancelPoller = (async () => {
|
|
4214
|
-
while (!stopCancelPolling) {
|
|
4215
|
-
await sleep(5000);
|
|
4216
|
-
if (stopCancelPolling || terminationReason === "cancel") {
|
|
4217
|
-
continue;
|
|
4218
|
-
}
|
|
4219
|
-
const cancelRequested = await checkCancelRequested({
|
|
4220
|
-
serverBaseUrl: args.serverBaseUrl,
|
|
4221
|
-
taskId: args.taskId,
|
|
4222
|
-
userId: args.userId,
|
|
4223
|
-
agentToken: args.agentToken,
|
|
4224
|
-
}).catch(() => false);
|
|
4225
|
-
if (!cancelRequested) {
|
|
4226
|
-
continue;
|
|
4227
|
-
}
|
|
4228
|
-
requestCancel();
|
|
4229
|
-
}
|
|
4230
|
-
})();
|
|
4231
|
-
const result = await new Promise((resolve, reject) => {
|
|
4232
|
-
child.once("error", (error) => {
|
|
4233
|
-
reject(error);
|
|
4234
|
-
});
|
|
4235
|
-
child.once("close", (code, signal) => {
|
|
4236
|
-
resolve({ code, signal });
|
|
4237
|
-
});
|
|
4238
|
-
}).finally(() => {
|
|
4239
|
-
stopCancelPolling = true;
|
|
4240
|
-
if (cancelStage1Timer) {
|
|
4241
|
-
clearTimeout(cancelStage1Timer);
|
|
4242
|
-
}
|
|
4243
|
-
if (cancelStage2Timer) {
|
|
4244
|
-
clearTimeout(cancelStage2Timer);
|
|
4245
|
-
}
|
|
4246
|
-
});
|
|
4247
|
-
await cancelPoller.catch(() => undefined);
|
|
4248
|
-
const canceled = await checkCancelRequested({
|
|
385
|
+
const runtimeEnvHelpers = createRuntimeEnvHelpers({
|
|
386
|
+
resolveWorkspaceRoot,
|
|
387
|
+
agentProjectDir: AGENT_PROJECT_DIR,
|
|
388
|
+
});
|
|
389
|
+
const localCodexCliTools = createLocalCodexCliTools({
|
|
390
|
+
resolveWorkspaceRoot,
|
|
391
|
+
resolveCodexHomePath: runtimeEnvHelpers.resolveCodexHomePath,
|
|
392
|
+
resolveShellPath: runtimeEnvHelpers.resolveShellPath,
|
|
393
|
+
sendSignalToTaskProcess,
|
|
394
|
+
});
|
|
395
|
+
const eventPersistenceHelpers = createEventPersistenceHelpers({
|
|
396
|
+
getActiveTaskLogContext: () => activeTaskLogContext,
|
|
397
|
+
publishEvent: async (args) => {
|
|
398
|
+
await args.jetstream.js.publish(args.jetstream.subject, args.jetstream.codec.encode({
|
|
4249
399
|
serverBaseUrl: args.serverBaseUrl,
|
|
4250
|
-
taskId: args.taskId,
|
|
4251
400
|
userId: args.userId,
|
|
4252
|
-
agentToken: args.agentToken,
|
|
4253
|
-
}).catch(() => false);
|
|
4254
|
-
const status = canceled || terminationReason === "cancel"
|
|
4255
|
-
? "canceled"
|
|
4256
|
-
: (result.code ?? 1) === 0
|
|
4257
|
-
? "completed"
|
|
4258
|
-
: "failed";
|
|
4259
|
-
const statusPayload = {
|
|
4260
|
-
status,
|
|
4261
|
-
exitCode: typeof result.code === "number" ? result.code : null,
|
|
4262
|
-
signal: result.signal,
|
|
4263
|
-
finishedAt: formatLocalTimestamp(),
|
|
4264
|
-
error: status === "failed"
|
|
4265
|
-
? `Command exited with code ${result.code ?? "null"}`
|
|
4266
|
-
: null,
|
|
4267
|
-
};
|
|
4268
|
-
await recordAgentEvent({ jetstream: args.jetstream,
|
|
4269
|
-
serverBaseUrl: args.serverBaseUrl,
|
|
4270
401
|
taskId: args.taskId,
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
attempt += 1;
|
|
4287
|
-
try {
|
|
4288
|
-
const natsBootstrap = await postJson(`${args.serverBaseUrl}/api/agent/nats`, {
|
|
4289
|
-
userId: args.userId,
|
|
4290
|
-
agentToken: args.agentToken,
|
|
4291
|
-
});
|
|
4292
|
-
const natsServers = normalizeNatsServers(natsBootstrap.servers);
|
|
4293
|
-
if (natsServers.length === 0) {
|
|
4294
|
-
throw new Error("No NATS servers configured by server");
|
|
4295
|
-
}
|
|
4296
|
-
const natsToken = normalizeNatsToken(natsBootstrap.auth);
|
|
4297
|
-
const jetstream = await initJetStreamContext({
|
|
4298
|
-
userId: args.userId,
|
|
4299
|
-
servers: natsServers,
|
|
4300
|
-
token: natsToken,
|
|
4301
|
-
});
|
|
4302
|
-
writeAgentInfraError(`bootstrap ok servers=${natsServers.length} eventStream=${jetstream.stream} eventSubject=${jetstream.subject}`);
|
|
4303
|
-
return { natsBootstrap, jetstream };
|
|
4304
|
-
}
|
|
4305
|
-
catch (error) {
|
|
4306
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
4307
|
-
const retryMs = Math.min(30_000, 1000 * Math.max(1, attempt));
|
|
4308
|
-
writeAgentError(`bootstrap failed: ${message} (retry in ${Math.floor(retryMs / 1000)}s, attempt=${attempt})`);
|
|
4309
|
-
await sleep(retryMs);
|
|
4310
|
-
}
|
|
4311
|
-
}
|
|
4312
|
-
}
|
|
402
|
+
type: args.type,
|
|
403
|
+
seq: args.seq,
|
|
404
|
+
payload: args.payload,
|
|
405
|
+
}));
|
|
406
|
+
},
|
|
407
|
+
formatTimestamp: formatLocalTimestamp,
|
|
408
|
+
onError: writeAgentError,
|
|
409
|
+
sleep,
|
|
410
|
+
});
|
|
411
|
+
const heartbeatSession = async (args) => {
|
|
412
|
+
await heartbeatAgentSession({
|
|
413
|
+
...args,
|
|
414
|
+
postJson,
|
|
415
|
+
});
|
|
416
|
+
};
|
|
4313
417
|
async function main() {
|
|
4314
418
|
const args = parseArgs(process.argv.slice(2));
|
|
4315
419
|
const workspaceDir = resolveArgOrEnv(args, ["workspace-dir", "workspaceDir"], ["WORKSPACE"]);
|
|
@@ -4319,7 +423,7 @@ async function main() {
|
|
|
4319
423
|
process.env.WORKSPACE = startupWorkspaceRoot;
|
|
4320
424
|
process.env.CODEX_HOME = path.join(startupWorkspaceRoot, ".codex");
|
|
4321
425
|
await mkdir(process.env.CODEX_HOME, { recursive: true }).catch(() => undefined);
|
|
4322
|
-
await resetRunsDir();
|
|
426
|
+
await resetRunsDir(resolveWorkspaceRoot());
|
|
4323
427
|
const serverBaseUrlRaw = resolveArgOrEnv(args, ["server", "url"], ["DOER_AGENT_SERVER"], DEFAULT_SERVER_BASE_URL);
|
|
4324
428
|
const requestedServerBaseUrl = serverBaseUrlRaw.replace(/\/$/, "");
|
|
4325
429
|
const serverBaseUrl = resolveContainerReachableServerBaseUrl(requestedServerBaseUrl);
|
|
@@ -4330,13 +434,18 @@ async function main() {
|
|
|
4330
434
|
throw new Error("user-id and agent-secret are required");
|
|
4331
435
|
}
|
|
4332
436
|
const agentToken = agentSecret;
|
|
4333
|
-
const agentVersion = await resolveAgentVersion();
|
|
437
|
+
const agentVersion = await resolveAgentVersion(AGENT_PACKAGE_JSON_PATH);
|
|
4334
438
|
let bannerShown = false;
|
|
4335
439
|
while (true) {
|
|
4336
440
|
const { natsBootstrap, jetstream } = await connectBootstrapWithRetry({
|
|
4337
441
|
serverBaseUrl,
|
|
4338
442
|
userId,
|
|
4339
443
|
agentToken,
|
|
444
|
+
postJson,
|
|
445
|
+
sanitizeUserId,
|
|
446
|
+
onInfraError: writeAgentInfraError,
|
|
447
|
+
onError: writeAgentError,
|
|
448
|
+
sleep,
|
|
4340
449
|
});
|
|
4341
450
|
const initialAgentId = typeof natsBootstrap.agentId === "string" ? natsBootstrap.agentId : "";
|
|
4342
451
|
if (!initialAgentId) {
|
|
@@ -4363,90 +472,82 @@ async function main() {
|
|
|
4363
472
|
else {
|
|
4364
473
|
writeAgentInfraError(`nats session restored agentId=${initialAgentId} servers=${jetstream.servers.join(",")} at=${formatLocalTimestamp()}`);
|
|
4365
474
|
}
|
|
4366
|
-
|
|
4367
|
-
let heartbeatInFlight = false;
|
|
4368
|
-
let sessionInvalidated = false;
|
|
4369
|
-
const invalidateAgentSession = (reason) => {
|
|
4370
|
-
if (sessionInvalidated) {
|
|
4371
|
-
return;
|
|
4372
|
-
}
|
|
4373
|
-
sessionInvalidated = true;
|
|
4374
|
-
writeAgentInfraError(`closing nats session: ${reason}`);
|
|
4375
|
-
void jetstream.nc.close().catch((error) => {
|
|
4376
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
4377
|
-
writeAgentInfraError(`failed to close nats session: ${message}`);
|
|
4378
|
-
});
|
|
4379
|
-
};
|
|
4380
|
-
const heartbeatTimer = setInterval(() => {
|
|
4381
|
-
if (heartbeatInFlight || sessionInvalidated) {
|
|
4382
|
-
return;
|
|
4383
|
-
}
|
|
4384
|
-
heartbeatInFlight = true;
|
|
4385
|
-
void heartbeatAgentSession({ nc: jetstream.nc, serverBaseUrl, userId, agentToken })
|
|
4386
|
-
.then(() => {
|
|
4387
|
-
heartbeatInFlight = false;
|
|
4388
|
-
if (heartbeatFailures > 0) {
|
|
4389
|
-
writeAgentInfraError(`heartbeat reconnected at=${formatLocalTimestamp()}`);
|
|
4390
|
-
}
|
|
4391
|
-
heartbeatFailures = 0;
|
|
4392
|
-
})
|
|
4393
|
-
.catch((error) => {
|
|
4394
|
-
heartbeatInFlight = false;
|
|
4395
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
4396
|
-
heartbeatFailures += 1;
|
|
4397
|
-
if (heartbeatFailures > 1) {
|
|
4398
|
-
writeAgentInfraError(`heartbeat failed: ${message} (count=${heartbeatFailures}/${HEARTBEAT_FAILURE_THRESHOLD})`);
|
|
4399
|
-
}
|
|
4400
|
-
if (heartbeatFailures >= HEARTBEAT_FAILURE_THRESHOLD) {
|
|
4401
|
-
invalidateAgentSession(`heartbeat failure threshold reached at=${formatLocalTimestamp()}`);
|
|
4402
|
-
}
|
|
4403
|
-
});
|
|
4404
|
-
}, HEARTBEAT_INTERVAL_MS);
|
|
4405
|
-
subscribeToFsRpc({
|
|
4406
|
-
jetstream,
|
|
4407
|
-
serverBaseUrl,
|
|
4408
|
-
userId,
|
|
4409
|
-
agentId: initialAgentId,
|
|
4410
|
-
agentToken,
|
|
4411
|
-
});
|
|
4412
|
-
subscribeToSessionRpc({
|
|
4413
|
-
jetstream,
|
|
4414
|
-
userId,
|
|
4415
|
-
agentId: initialAgentId,
|
|
4416
|
-
});
|
|
4417
|
-
subscribeToCodexAuthRpc({
|
|
4418
|
-
jetstream,
|
|
4419
|
-
userId,
|
|
4420
|
-
agentId: initialAgentId,
|
|
4421
|
-
});
|
|
4422
|
-
subscribeToSettingsRpc({
|
|
4423
|
-
jetstream,
|
|
4424
|
-
userId,
|
|
4425
|
-
agentId: initialAgentId,
|
|
4426
|
-
});
|
|
4427
|
-
subscribeToGitRpc({
|
|
4428
|
-
jetstream,
|
|
4429
|
-
userId,
|
|
4430
|
-
agentId: initialAgentId,
|
|
4431
|
-
});
|
|
4432
|
-
subscribeToSkillRpc({
|
|
4433
|
-
jetstream,
|
|
4434
|
-
userId,
|
|
4435
|
-
agentId: initialAgentId,
|
|
4436
|
-
});
|
|
4437
|
-
subscribeToRunRpc({
|
|
475
|
+
await runConnectedAgentSession({
|
|
4438
476
|
jetstream,
|
|
4439
477
|
serverBaseUrl,
|
|
4440
478
|
userId,
|
|
4441
|
-
agentId: initialAgentId,
|
|
4442
479
|
agentToken,
|
|
480
|
+
heartbeatIntervalMs: HEARTBEAT_INTERVAL_MS,
|
|
481
|
+
heartbeatFailureThreshold: HEARTBEAT_FAILURE_THRESHOLD,
|
|
482
|
+
formatTimestamp: formatLocalTimestamp,
|
|
483
|
+
heartbeatAgentSession: heartbeatSession,
|
|
484
|
+
subscribeAll: () => {
|
|
485
|
+
subscribeToFsRpc({
|
|
486
|
+
jetstream,
|
|
487
|
+
serverBaseUrl,
|
|
488
|
+
userId,
|
|
489
|
+
agentId: initialAgentId,
|
|
490
|
+
agentToken,
|
|
491
|
+
});
|
|
492
|
+
subscribeToSessionRpc({
|
|
493
|
+
nc: jetstream.nc,
|
|
494
|
+
subject: buildAgentSessionRpcSubject(userId, initialAgentId),
|
|
495
|
+
agentId: initialAgentId,
|
|
496
|
+
workspaceRoot: resolveWorkspaceRoot(),
|
|
497
|
+
onInfo: writeAgentInfo,
|
|
498
|
+
onError: writeAgentError,
|
|
499
|
+
formatTimestamp: formatLocalTimestamp,
|
|
500
|
+
});
|
|
501
|
+
subscribeToCodexAuthRpc({
|
|
502
|
+
nc: jetstream.nc,
|
|
503
|
+
subject: buildAgentCodexAuthRpcSubject(userId, initialAgentId),
|
|
504
|
+
agentId: initialAgentId,
|
|
505
|
+
workspaceRoot: resolveWorkspaceRoot(),
|
|
506
|
+
buildLocalCodexCliCommand: localCodexCliTools.buildLocalCodexCliCommand,
|
|
507
|
+
resolveShellPath: runtimeEnvHelpers.resolveShellPath,
|
|
508
|
+
resolveCodexHomePath: runtimeEnvHelpers.resolveCodexHomePath,
|
|
509
|
+
runLocalCodexCli: localCodexCliTools.runLocalCodexCli,
|
|
510
|
+
runLocalCodexCliWithInput: localCodexCliTools.runLocalCodexCliWithInput,
|
|
511
|
+
sendSignalToTaskProcess,
|
|
512
|
+
stripAnsi: localCodexCliTools.stripAnsi,
|
|
513
|
+
onInfo: writeAgentInfo,
|
|
514
|
+
onError: writeAgentError,
|
|
515
|
+
});
|
|
516
|
+
subscribeToSettingsRpc({
|
|
517
|
+
jetstream,
|
|
518
|
+
userId,
|
|
519
|
+
agentId: initialAgentId,
|
|
520
|
+
});
|
|
521
|
+
subscribeToGitRpc({
|
|
522
|
+
jetstream,
|
|
523
|
+
userId,
|
|
524
|
+
agentId: initialAgentId,
|
|
525
|
+
});
|
|
526
|
+
subscribeToSkillRpc({
|
|
527
|
+
nc: jetstream.nc,
|
|
528
|
+
subject: buildAgentSkillRpcSubject(userId, initialAgentId),
|
|
529
|
+
agentId: initialAgentId,
|
|
530
|
+
workspaceRoot: resolveWorkspaceRoot(),
|
|
531
|
+
resolveCodexHomePath: runtimeEnvHelpers.resolveCodexHomePath,
|
|
532
|
+
readAgentSettingsConfig,
|
|
533
|
+
buildAgentSettingsEnvPatch,
|
|
534
|
+
runLocalCodexCli: localCodexCliTools.runLocalCodexCli,
|
|
535
|
+
stripAnsi: localCodexCliTools.stripAnsi,
|
|
536
|
+
onInfo: writeAgentInfo,
|
|
537
|
+
onError: writeAgentError,
|
|
538
|
+
});
|
|
539
|
+
subscribeToRunRpc({
|
|
540
|
+
jetstream,
|
|
541
|
+
serverBaseUrl,
|
|
542
|
+
userId,
|
|
543
|
+
agentId: initialAgentId,
|
|
544
|
+
agentToken,
|
|
545
|
+
});
|
|
546
|
+
},
|
|
547
|
+
stopAllSessionWatchers: () => stopAllSessionWatchers({ onError: writeAgentError }),
|
|
548
|
+
onInfraError: writeAgentInfraError,
|
|
549
|
+
sleep,
|
|
4443
550
|
});
|
|
4444
|
-
const closeError = await jetstream.nc.closed();
|
|
4445
|
-
clearInterval(heartbeatTimer);
|
|
4446
|
-
stopAllSessionWatchers();
|
|
4447
|
-
const detail = closeError instanceof Error ? closeError.message : "clean close";
|
|
4448
|
-
writeAgentInfraError(`nats session ended: ${detail}; reconnecting`);
|
|
4449
|
-
await sleep(1000);
|
|
4450
551
|
}
|
|
4451
552
|
}
|
|
4452
553
|
main().catch((error) => {
|