doer-agent 0.3.4 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/agent.js +317 -80
  2. package/package.json +1 -1
package/dist/agent.js CHANGED
@@ -18,6 +18,7 @@ const codexAuthRpcCodec = StringCodec();
18
18
  const settingsRpcCodec = StringCodec();
19
19
  const gitRpcCodec = StringCodec();
20
20
  const skillRpcCodec = StringCodec();
21
+ const runEventsCodec = StringCodec();
21
22
  const retainedRuns = new Map();
22
23
  const activeSessionWatchers = new Map();
23
24
  const sessionLineIndexCache = new Map();
@@ -30,6 +31,9 @@ function sanitizeUserId(userId) {
30
31
  function buildAgentRunRpcSubject(userId, agentId) {
31
32
  return `doer.agent.run.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
32
33
  }
34
+ function buildAgentRunEventsSubject(userId, agentId) {
35
+ return `doer.agent.run.events.${sanitizeUserId(userId)}.${agentId.trim()}`;
36
+ }
33
37
  function buildAgentSessionRpcSubject(userId, agentId) {
34
38
  return `doer.agent.session.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
35
39
  }
@@ -514,7 +518,7 @@ async function resolveRunStartLockPath(args) {
514
518
  if (typeof args.sessionId === "string" && args.sessionId.trim()) {
515
519
  return path.join(dir, `session__${sanitizeRunLockSegment(args.sessionId)}.lock`);
516
520
  }
517
- return path.join(dir, `run__${sanitizeRunLockSegment(args.runId)}.lock`);
521
+ return path.join(dir, "pending_new_session.lock");
518
522
  }
519
523
  async function claimRunStartSlot(args) {
520
524
  const lockPath = await resolveRunStartLockPath(args);
@@ -609,7 +613,6 @@ function createDefaultAgentSettingsConfig() {
609
613
  codex: {
610
614
  model: "gpt-5.4",
611
615
  authMode: "api_key",
612
- apiKey: null,
613
616
  },
614
617
  realtime: {
615
618
  model: process.env.OPENAI_REALTIME_MODEL?.trim() || "gpt-realtime",
@@ -687,7 +690,6 @@ function normalizeAgentSettingsConfig(value, fallback) {
687
690
  codex: {
688
691
  model: typeof codex.model === "string" && codex.model.trim() ? codex.model.trim() : base.codex.model,
689
692
  authMode: codex.authMode === "oauth" ? "oauth" : codex.authMode === "api_key" ? "api_key" : base.codex.authMode,
690
- apiKey: codex.apiKey === null ? null : normalizeNullableString(codex.apiKey) ?? base.codex.apiKey,
691
693
  },
692
694
  realtime: {
693
695
  model: typeof realtime.model === "string" && realtime.model.trim() ? realtime.model.trim() : base.realtime.model,
@@ -768,7 +770,6 @@ function toMaskedSecret(value) {
768
770
  return { has: true, masked: maskSecretPreview(value), length: value.length };
769
771
  }
770
772
  function toAgentSettingsPublic(config) {
771
- const codexKey = toMaskedSecret(config.codex.apiKey);
772
773
  const realtimeKey = toMaskedSecret(config.realtime.apiKey);
773
774
  const gitOauth = toMaskedSecret(config.git.oauthToken);
774
775
  const awsSecret = toMaskedSecret(config.aws.secretAccessKey);
@@ -784,9 +785,9 @@ function toAgentSettingsPublic(config) {
784
785
  codex: {
785
786
  model: config.codex.model,
786
787
  authMode: config.codex.authMode,
787
- hasApiKey: codexKey.has,
788
- apiKeyMasked: codexKey.masked,
789
- apiKeyLength: codexKey.length,
788
+ hasApiKey: false,
789
+ apiKeyMasked: null,
790
+ apiKeyLength: null,
790
791
  },
791
792
  realtime: {
792
793
  model: config.realtime.model,
@@ -874,7 +875,6 @@ function normalizeAgentSettingsPatch(value) {
874
875
  move("firstTurnPrompt", "general", "firstTurnPrompt");
875
876
  move("codexModel", "codex", "model");
876
877
  move("codexAuthMode", "codex", "authMode");
877
- move("codexApiKey", "codex", "apiKey");
878
878
  move("realtimeModel", "realtime", "model");
879
879
  move("realtimeVoice", "realtime", "voice");
880
880
  move("realtimeWakeName", "realtime", "wakeName");
@@ -915,9 +915,6 @@ async function resolveAgentSettingsConfig(args) {
915
915
  }
916
916
  function buildAgentSettingsEnvPatch(config) {
917
917
  const envPatch = {};
918
- if (config.codex.authMode === "api_key" && config.codex.apiKey) {
919
- envPatch.OPENAI_API_KEY = config.codex.apiKey;
920
- }
921
918
  if (config.git.enabled) {
922
919
  if (config.git.name)
923
920
  envPatch.GIT_AUTHOR_NAME = config.git.name;
@@ -983,34 +980,136 @@ function cloneRunTask(task, _sinceSeq) {
983
980
  ...task,
984
981
  };
985
982
  }
986
- function extractCodexSessionMetadata(value) {
983
+ function buildImmediateRunChangedEvent(task) {
984
+ return {
985
+ type: "run.changed",
986
+ agentId: task.agentId,
987
+ sessionId: task.sessionId,
988
+ filePath: task.sessionFilePath,
989
+ runId: task.id,
990
+ updatedAt: task.updatedAt,
991
+ status: task.status,
992
+ cancelRequested: task.cancelRequested,
993
+ resultExitCode: task.resultExitCode,
994
+ resultSignal: task.resultSignal,
995
+ error: task.error,
996
+ finishedAt: task.finishedAt,
997
+ };
998
+ }
999
+ function publishImmediateRunChangedEvent(args) {
1000
+ args.nc.publish(buildAgentRunEventsSubject(args.userId, args.task.agentId), runEventsCodec.encode(JSON.stringify(buildImmediateRunChangedEvent(args.task))));
1001
+ }
1002
+ async function findSessionFilePathBySessionId(sessionId) {
1003
+ const targetSessionId = sessionId.trim();
1004
+ if (!targetSessionId) {
1005
+ return null;
1006
+ }
1007
+ let sessionsRootStat;
987
1008
  try {
988
- const parsed = JSON.parse(value);
989
- const lineType = typeof parsed.type === "string" ? parsed.type : "";
990
- if (!parsed.payload || typeof parsed.payload !== "object" || Array.isArray(parsed.payload)) {
991
- return { sessionId: null, sessionFilePath: null };
992
- }
993
- const payload = parsed.payload;
994
- const sessionIdCandidate = typeof payload.sessionId === "string" && payload.sessionId.trim()
995
- ? payload.sessionId.trim()
996
- : typeof payload.session_id === "string" && payload.session_id.trim()
997
- ? payload.session_id.trim()
998
- : typeof payload.id === "string" && payload.id.trim() && (lineType === "session_meta" || lineType === "session.started")
999
- ? payload.id.trim()
1000
- : null;
1001
- const filePathCandidate = typeof payload.rollout_path === "string" && payload.rollout_path.trim()
1002
- ? payload.rollout_path.trim()
1003
- : typeof payload.filePath === "string" && payload.filePath.trim()
1004
- ? payload.filePath.trim()
1005
- : null;
1006
- return {
1007
- sessionId: sessionIdCandidate,
1008
- sessionFilePath: filePathCandidate,
1009
- };
1009
+ sessionsRootStat = await stat(getSessionsRootPath());
1010
1010
  }
1011
1011
  catch {
1012
- return { sessionId: null, sessionFilePath: null };
1012
+ return null;
1013
+ }
1014
+ if (!sessionsRootStat.isDirectory()) {
1015
+ return null;
1016
+ }
1017
+ const files = await collectSessionJsonlFiles(getSessionsRootPath());
1018
+ files.sort((a, b) => b.mtimeMs - a.mtimeMs || a.filePath.localeCompare(b.filePath));
1019
+ for (const file of files) {
1020
+ let fileHandle = null;
1021
+ try {
1022
+ fileHandle = await open(file.filePath, "r");
1023
+ const entryStat = await fileHandle.stat();
1024
+ const firstLine = await readFirstLine(fileHandle, entryStat.size);
1025
+ if (!firstLine) {
1026
+ continue;
1027
+ }
1028
+ const parsed = JSON.parse(firstLine);
1029
+ const candidateMeta = parsed && parsed.type === "session_meta" && isObjectRecord(parsed.payload)
1030
+ ? parsed.payload
1031
+ : isObjectRecord(parsed.session_meta)
1032
+ ? parsed.session_meta
1033
+ : isObjectRecord(parsed.sessionMeta)
1034
+ ? parsed.sessionMeta
1035
+ : isObjectRecord(parsed.meta)
1036
+ ? parsed.meta
1037
+ : isObjectRecord(parsed.payload)
1038
+ ? parsed.payload
1039
+ : parsed;
1040
+ const candidateId = pickSessionString(candidateMeta.sessionId, candidateMeta.session_id, candidateMeta.id);
1041
+ if (candidateId === targetSessionId) {
1042
+ return file.filePath;
1043
+ }
1044
+ }
1045
+ catch {
1046
+ // ignore malformed session files
1047
+ }
1048
+ finally {
1049
+ await fileHandle?.close().catch(() => undefined);
1050
+ }
1013
1051
  }
1052
+ return null;
1053
+ }
1054
+ async function readSessionIdFromSessionFile(filePath) {
1055
+ let fileHandle = null;
1056
+ try {
1057
+ fileHandle = await open(filePath, "r");
1058
+ const entryStat = await fileHandle.stat();
1059
+ const firstLine = await readFirstLine(fileHandle, entryStat.size);
1060
+ if (!firstLine) {
1061
+ return null;
1062
+ }
1063
+ const parsed = JSON.parse(firstLine);
1064
+ const candidateMeta = parsed && parsed.type === "session_meta" && isObjectRecord(parsed.payload)
1065
+ ? parsed.payload
1066
+ : isObjectRecord(parsed.session_meta)
1067
+ ? parsed.session_meta
1068
+ : isObjectRecord(parsed.sessionMeta)
1069
+ ? parsed.sessionMeta
1070
+ : isObjectRecord(parsed.meta)
1071
+ ? parsed.meta
1072
+ : isObjectRecord(parsed.payload)
1073
+ ? parsed.payload
1074
+ : parsed;
1075
+ return pickSessionString(candidateMeta.sessionId, candidateMeta.session_id, candidateMeta.id);
1076
+ }
1077
+ catch {
1078
+ return null;
1079
+ }
1080
+ finally {
1081
+ await fileHandle?.close().catch(() => undefined);
1082
+ }
1083
+ }
1084
+ async function detectPendingRunSession(knownFilePaths) {
1085
+ const sessionsRoot = getSessionsRootPath();
1086
+ let sessionsRootStat;
1087
+ try {
1088
+ sessionsRootStat = await stat(sessionsRoot);
1089
+ }
1090
+ catch {
1091
+ return null;
1092
+ }
1093
+ if (!sessionsRootStat.isDirectory()) {
1094
+ return null;
1095
+ }
1096
+ const files = await collectSessionJsonlFiles(sessionsRoot);
1097
+ files.sort((a, b) => a.mtimeMs - b.mtimeMs || a.filePath.localeCompare(b.filePath));
1098
+ for (const file of files) {
1099
+ if (knownFilePaths.has(file.filePath)) {
1100
+ continue;
1101
+ }
1102
+ const sessionId = await readSessionIdFromSessionFile(file.filePath);
1103
+ if (!sessionId) {
1104
+ continue;
1105
+ }
1106
+ knownFilePaths.add(file.filePath);
1107
+ return {
1108
+ sessionId,
1109
+ sessionFilePath: file.filePath,
1110
+ };
1111
+ }
1112
+ return null;
1014
1113
  }
1015
1114
  async function updateRunSessionMetadata(task, metadata) {
1016
1115
  let changed = false;
@@ -1023,11 +1122,25 @@ async function updateRunSessionMetadata(task, metadata) {
1023
1122
  task.sessionFilePath = metadata.sessionFilePath.trim();
1024
1123
  changed = true;
1025
1124
  }
1125
+ if (!task.sessionFilePath && task.sessionId) {
1126
+ const resolvedSessionFilePath = await findSessionFilePathBySessionId(task.sessionId).catch(() => null);
1127
+ if (resolvedSessionFilePath) {
1128
+ task.sessionFilePath = resolvedSessionFilePath;
1129
+ changed = true;
1130
+ }
1131
+ }
1026
1132
  if (!changed) {
1027
1133
  return;
1028
1134
  }
1029
1135
  task.updatedAt = formatLocalTimestamp();
1030
1136
  await persistRunTask(task).catch(() => undefined);
1137
+ if (metadata.nc) {
1138
+ publishImmediateRunChangedEvent({
1139
+ nc: metadata.nc,
1140
+ userId: task.userId,
1141
+ task,
1142
+ });
1143
+ }
1031
1144
  if (!previousSessionId && task.sessionId) {
1032
1145
  await updateRunStartSlotSession({
1033
1146
  runId: task.id,
@@ -1113,6 +1226,7 @@ async function startManagedRun(args) {
1113
1226
  userId: args.userId,
1114
1227
  taskId: args.runId,
1115
1228
  codexAuthBundle: args.codexAuthBundle,
1229
+ runtimeEnvPatch: args.runtimeEnvPatch,
1116
1230
  });
1117
1231
  const child = spawnManagedCodexCommand({
1118
1232
  codexArgs: args.codexArgs,
@@ -1138,46 +1252,60 @@ async function startManagedRun(args) {
1138
1252
  startedAt: now,
1139
1253
  finishedAt: null,
1140
1254
  };
1141
- let stdoutBuffer = "";
1142
- const recordChunk = (stream, chunk) => {
1143
- writeRunStream(task.id, stream, chunk);
1144
- if (stream !== "stdout" || (task.sessionId && task.sessionFilePath)) {
1255
+ let pendingSessionPollClosed = false;
1256
+ let pendingSessionPollTimer = null;
1257
+ const knownPendingSessionFiles = new Set();
1258
+ const stopPendingSessionPoll = () => {
1259
+ pendingSessionPollClosed = true;
1260
+ if (pendingSessionPollTimer) {
1261
+ clearInterval(pendingSessionPollTimer);
1262
+ pendingSessionPollTimer = null;
1263
+ }
1264
+ };
1265
+ const pollPendingSession = async () => {
1266
+ if (pendingSessionPollClosed || task.sessionId) {
1267
+ stopPendingSessionPoll();
1145
1268
  return;
1146
1269
  }
1147
- stdoutBuffer += chunk;
1148
- const lines = stdoutBuffer.split(/\r?\n/);
1149
- stdoutBuffer = lines.pop() ?? "";
1150
- for (const line of lines) {
1151
- const trimmed = line.trim();
1152
- if (!trimmed) {
1153
- continue;
1154
- }
1155
- const metadata = extractCodexSessionMetadata(trimmed);
1156
- if (metadata.sessionId || metadata.sessionFilePath) {
1157
- void updateRunSessionMetadata(task, metadata);
1158
- }
1270
+ const detected = await detectPendingRunSession(knownPendingSessionFiles).catch(() => null);
1271
+ if (!detected) {
1272
+ return;
1273
+ }
1274
+ await updateRunSessionMetadata(task, {
1275
+ sessionId: detected.sessionId,
1276
+ sessionFilePath: detected.sessionFilePath,
1277
+ nc: args.nc,
1278
+ }).catch(() => undefined);
1279
+ if (task.sessionId) {
1280
+ stopPendingSessionPoll();
1159
1281
  }
1160
1282
  };
1161
- child.stdout.on("data", (chunk) => recordChunk("stdout", chunk));
1162
- child.stderr.on("data", (chunk) => recordChunk("stderr", chunk));
1283
+ if (!task.sessionId) {
1284
+ const existingFiles = await collectSessionJsonlFiles(getSessionsRootPath()).catch(() => []);
1285
+ for (const file of existingFiles) {
1286
+ knownPendingSessionFiles.add(file.filePath);
1287
+ }
1288
+ pendingSessionPollTimer = setInterval(() => {
1289
+ void pollPendingSession();
1290
+ }, 1000);
1291
+ }
1292
+ child.stdout.on("data", (chunk) => writeRunStream(task.id, "stdout", chunk));
1293
+ child.stderr.on("data", (chunk) => writeRunStream(task.id, "stderr", chunk));
1163
1294
  child.once("error", (error) => {
1295
+ stopPendingSessionPoll();
1164
1296
  const message = error instanceof Error ? error.message : String(error);
1165
1297
  task.status = "failed";
1166
1298
  task.error = message;
1167
1299
  task.finishedAt = formatLocalTimestamp();
1168
1300
  persistRetainedRun(task);
1301
+ publishImmediateRunChangedEvent({ nc: args.nc, userId: task.userId, task });
1169
1302
  void removeRunTask(task.id).catch(() => undefined);
1170
1303
  void releaseRunStartSlot({ runId: task.id, sessionId: task.sessionId }).catch(() => undefined);
1171
1304
  void prepared.codexAuthCleanup().catch(() => undefined);
1172
1305
  writeRunStatus(task.id, `failed error=${message}`);
1173
1306
  });
1174
1307
  child.once("close", async (code, signal) => {
1175
- if (stdoutBuffer.trim() && (!task.sessionId || !task.sessionFilePath)) {
1176
- const metadata = extractCodexSessionMetadata(stdoutBuffer.trim());
1177
- if (metadata.sessionId || metadata.sessionFilePath) {
1178
- void updateRunSessionMetadata(task, metadata);
1179
- }
1180
- }
1308
+ stopPendingSessionPoll();
1181
1309
  const latest = await getStoredRun(task.id).catch(() => null);
1182
1310
  if (latest?.cancelRequested) {
1183
1311
  task.cancelRequested = true;
@@ -1188,6 +1316,7 @@ async function startManagedRun(args) {
1188
1316
  task.status = task.cancelRequested ? "canceled" : (task.resultExitCode ?? 1) === 0 ? "completed" : "failed";
1189
1317
  task.error = task.status === "failed" ? `Command exited with code ${task.resultExitCode ?? "null"}` : null;
1190
1318
  persistRetainedRun(task);
1319
+ publishImmediateRunChangedEvent({ nc: args.nc, userId: task.userId, task });
1191
1320
  void removeRunTask(task.id).catch(() => undefined);
1192
1321
  void releaseRunStartSlot({ runId: task.id, sessionId: task.sessionId }).catch(() => undefined);
1193
1322
  void prepared.codexAuthCleanup().catch(() => undefined);
@@ -1195,7 +1324,11 @@ async function startManagedRun(args) {
1195
1324
  });
1196
1325
  persistRetainedRun(task);
1197
1326
  void persistRunTask(task).catch(() => undefined);
1327
+ publishImmediateRunChangedEvent({ nc: args.nc, userId: task.userId, task });
1198
1328
  writeRunStatus(task.id, `started requestId=${args.requestId} cwd=${prepared.taskWorkspace}`);
1329
+ if (!task.sessionId) {
1330
+ void pollPendingSession();
1331
+ }
1199
1332
  return cloneRunTask(task);
1200
1333
  }
1201
1334
  function shellSingleQuote(value) {
@@ -1312,6 +1445,62 @@ async function runLocalCodexCli(args, timeoutMs, envPatch) {
1312
1445
  });
1313
1446
  });
1314
1447
  }
1448
+ async function runLocalCodexCliWithInput(args, input, timeoutMs, envPatch) {
1449
+ const command = buildLocalCodexCliCommand(args);
1450
+ const workspaceRoot = workspaceRootOverride ?? (process.env.WORKSPACE?.trim() || process.cwd());
1451
+ const env = {
1452
+ ...process.env,
1453
+ ...(envPatch ?? {}),
1454
+ WORKSPACE: workspaceRoot,
1455
+ CODEX_HOME: resolveCodexHomePath(),
1456
+ };
1457
+ return await new Promise((resolve, reject) => {
1458
+ const child = spawn(command, {
1459
+ cwd: workspaceRoot,
1460
+ shell: resolveShellPath(),
1461
+ env,
1462
+ stdio: ["pipe", "pipe", "pipe"],
1463
+ });
1464
+ let stdout = "";
1465
+ let stderr = "";
1466
+ let done = false;
1467
+ let timedOut = false;
1468
+ child.stdout.setEncoding("utf8");
1469
+ child.stderr.setEncoding("utf8");
1470
+ child.stdout.on("data", (chunk) => {
1471
+ stdout += chunk;
1472
+ });
1473
+ child.stderr.on("data", (chunk) => {
1474
+ stderr += chunk;
1475
+ });
1476
+ child.stdin?.write(input);
1477
+ if (!input.endsWith("\n")) {
1478
+ child.stdin?.write("\n");
1479
+ }
1480
+ child.stdin?.end();
1481
+ const timer = setTimeout(() => {
1482
+ timedOut = true;
1483
+ sendSignalToTaskProcess(child, "SIGTERM");
1484
+ setTimeout(() => sendSignalToTaskProcess(child, "SIGKILL"), 1000);
1485
+ }, Math.max(500, timeoutMs));
1486
+ child.once("error", (error) => {
1487
+ if (done) {
1488
+ return;
1489
+ }
1490
+ done = true;
1491
+ clearTimeout(timer);
1492
+ reject(error);
1493
+ });
1494
+ child.once("exit", (code) => {
1495
+ if (done) {
1496
+ return;
1497
+ }
1498
+ done = true;
1499
+ clearTimeout(timer);
1500
+ resolve({ code, stdout, stderr, timedOut });
1501
+ });
1502
+ });
1503
+ }
1315
1504
  function buildSkillGeneratorPrompt(userPrompt) {
1316
1505
  return [
1317
1506
  "Create a Codex skill from the user's description.",
@@ -1663,6 +1852,20 @@ async function startLocalCodexLogin() {
1663
1852
  }
1664
1853
  throw new Error(normalized || `Codex login failed with code ${result.code ?? "null"}`);
1665
1854
  }
1855
+ async function loginLocalCodexWithApiKey(apiKey) {
1856
+ const result = await runLocalCodexCliWithInput(["login", "--with-api-key"], apiKey, 15000);
1857
+ const normalized = stripAnsi([result.stdout, result.stderr].filter(Boolean).join("\n")).trim();
1858
+ if ((result.code ?? 1) !== 0) {
1859
+ throw new Error(normalized || `Codex API key login failed with code ${result.code ?? "null"}`);
1860
+ }
1861
+ const status = await getLocalCodexLoginStatus().catch(() => null);
1862
+ return {
1863
+ loggedIn: status?.loggedIn === true,
1864
+ output: status?.output || normalized || "Logged in",
1865
+ verificationUri: null,
1866
+ userCode: null,
1867
+ };
1868
+ }
1666
1869
  async function logoutLocalCodexAuth() {
1667
1870
  if (pendingCodexDeviceAuth && pendingCodexDeviceAuth.child.exitCode === null) {
1668
1871
  sendSignalToTaskProcess(pendingCodexDeviceAuth.child, "SIGTERM");
@@ -1694,11 +1897,15 @@ function normalizeCodexAuthRpcRequest(args) {
1694
1897
  const responseSubject = typeof args.request.responseSubject === "string" ? args.request.responseSubject.trim() : "";
1695
1898
  const requestAgentId = typeof args.request.agentId === "string" ? args.request.agentId.trim() : "";
1696
1899
  const actionRaw = typeof args.request.action === "string" ? args.request.action.trim() : "";
1697
- const action = actionRaw === "start" || actionRaw === "logout" ? actionRaw : "status";
1900
+ const action = actionRaw === "start" || actionRaw === "logout" || actionRaw === "login_api_key" ? actionRaw : "status";
1901
+ const apiKey = typeof args.request.apiKey === "string" && args.request.apiKey.trim() ? args.request.apiKey.trim() : null;
1698
1902
  if (!requestId || !responseSubject || !requestAgentId || requestAgentId !== args.agentId) {
1699
1903
  throw new Error("invalid codex auth rpc request");
1700
1904
  }
1701
- return { requestId, responseSubject, action };
1905
+ if (action === "login_api_key" && !apiKey) {
1906
+ throw new Error("api key is required");
1907
+ }
1908
+ return { requestId, responseSubject, action, apiKey };
1702
1909
  }
1703
1910
  function publishCodexAuthRpcResponse(args) {
1704
1911
  args.nc.publish(args.responseSubject, codexAuthRpcCodec.encode(JSON.stringify(args.payload)));
@@ -1712,7 +1919,10 @@ async function handleCodexAuthRpcMessage(args) {
1712
1919
  requestId = request.requestId;
1713
1920
  responseSubject = request.responseSubject;
1714
1921
  let result = null;
1715
- if (request.action === "start") {
1922
+ if (request.action === "login_api_key") {
1923
+ result = await loginLocalCodexWithApiKey(request.apiKey ?? "");
1924
+ }
1925
+ else if (request.action === "start") {
1716
1926
  const status = await getLocalCodexLoginStatus();
1717
1927
  if (status.loggedIn) {
1718
1928
  result = { loggedIn: true, output: status.output };
@@ -2133,6 +2343,7 @@ async function handleRunRpcMessage(args) {
2133
2343
  serverBaseUrl: args.serverBaseUrl,
2134
2344
  userId: args.userId,
2135
2345
  agentId: args.agentId,
2346
+ nc: args.jetstream.nc,
2136
2347
  sessionId: request.sessionId,
2137
2348
  codexArgs: buildManagedCodexArgs({
2138
2349
  prompt: request.prompt ?? "",
@@ -2182,6 +2393,7 @@ async function handleRunRpcMessage(args) {
2182
2393
  target.cancelRequested = true;
2183
2394
  target.updatedAt = formatLocalTimestamp();
2184
2395
  await persistRunTask(target);
2396
+ publishImmediateRunChangedEvent({ nc: args.jetstream.nc, userId: target.userId, task: target });
2185
2397
  writeRunStatus(target.id, `cancel requested pid=${target.processPid}`);
2186
2398
  sendSignalToPid(target.processPid, "SIGINT");
2187
2399
  const task = cloneRunTask(target);
@@ -3560,16 +3772,18 @@ function normalizeShellRpcCodexAuthBundle(value) {
3560
3772
  }
3561
3773
  const row = value;
3562
3774
  const authJson = typeof row.authJson === "string" ? row.authJson : null;
3563
- if (!authJson) {
3775
+ const authMode = row.authMode === "oauth" ? "oauth" : row.authMode === "api_key" ? "api_key" : undefined;
3776
+ const apiKey = typeof row.apiKey === "string" || row.apiKey === null ? row.apiKey : undefined;
3777
+ if (!authJson && authMode !== "api_key" && apiKey === undefined) {
3564
3778
  return null;
3565
3779
  }
3566
3780
  return {
3567
3781
  taskId: typeof row.taskId === "string" ? row.taskId : undefined,
3568
- authMode: row.authMode === "oauth" ? "oauth" : row.authMode === "api_key" ? "api_key" : undefined,
3782
+ authMode,
3569
3783
  issuedAt: typeof row.issuedAt === "string" ? row.issuedAt : undefined,
3570
3784
  expiresAt: typeof row.expiresAt === "string" ? row.expiresAt : undefined,
3571
- authJson,
3572
- apiKey: typeof row.apiKey === "string" || row.apiKey === null ? row.apiKey : undefined,
3785
+ authJson: authJson ?? undefined,
3786
+ apiKey,
3573
3787
  };
3574
3788
  }
3575
3789
  async function postJson(url, body) {
@@ -3734,27 +3948,49 @@ async function checkCancelRequested(args) {
3734
3948
  const response = await getJson(`${args.serverBaseUrl}/api/agent/tasks/${encodeURIComponent(args.taskId)}/events?${query.toString()}`);
3735
3949
  return Boolean(response.task?.cancelRequested);
3736
3950
  }
3737
- async function prepareTaskCodexAuth(args) {
3738
- void args;
3951
+ async function syncCodexAuthState(args) {
3952
+ const envPatch = {};
3953
+ const synced = false;
3954
+ if (args.authMode === "api_key") {
3955
+ if (args.apiKey) {
3956
+ envPatch.OPENAI_API_KEY = args.apiKey;
3957
+ }
3958
+ }
3739
3959
  return {
3740
- envPatch: {},
3960
+ envPatch,
3741
3961
  cleanup: async () => { },
3742
3962
  meta: {
3743
- codexAuthSource: "agent_local",
3744
- codexAuthSynced: false,
3963
+ codexAuthSource: args.source,
3964
+ codexAuthMode: args.authMode ?? null,
3965
+ codexAuthHasApiKey: Boolean(args.apiKey),
3966
+ codexAuthHasAuthJson: Boolean(args.authJson),
3967
+ codexAuthIssuedAt: args.issuedAt ?? null,
3968
+ codexAuthExpiresAt: args.expiresAt ?? null,
3969
+ codexAuthSynced: synced,
3745
3970
  },
3746
3971
  };
3747
3972
  }
3973
+ async function prepareTaskCodexAuth(args) {
3974
+ void args;
3975
+ return await syncCodexAuthState({
3976
+ source: "agent_local",
3977
+ authJson: null,
3978
+ issuedAt: null,
3979
+ expiresAt: null,
3980
+ });
3981
+ }
3748
3982
  async function prepareCodexAuthBundle(bundle) {
3749
- void bundle;
3750
- return {
3751
- envPatch: {},
3752
- cleanup: async () => { },
3753
- meta: {
3754
- codexAuthSource: "agent_local",
3755
- codexAuthSynced: false,
3756
- },
3757
- };
3983
+ if (!bundle) {
3984
+ return null;
3985
+ }
3986
+ return await syncCodexAuthState({
3987
+ source: "server_bundle",
3988
+ authMode: bundle.authMode,
3989
+ apiKey: bundle.apiKey,
3990
+ authJson: bundle.authJson ?? null,
3991
+ issuedAt: bundle.issuedAt ?? null,
3992
+ expiresAt: bundle.expiresAt ?? null,
3993
+ });
3758
3994
  }
3759
3995
  async function prepareCommandExecution(args) {
3760
3996
  const shellPath = resolveShellPath();
@@ -3768,6 +4004,7 @@ async function prepareCommandExecution(args) {
3768
4004
  DOER_USER_ID: args.userId,
3769
4005
  DOER_AGENT_TASK_ID: args.taskId,
3770
4006
  ...buildAgentSettingsEnvPatch(localAgentSettings),
4007
+ ...args.runtimeEnvPatch,
3771
4008
  ...(codexAuth?.envPatch ?? {}),
3772
4009
  WORKSPACE: taskWorkspace,
3773
4010
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doer-agent",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "Reverse-polling agent runtime for doer",
5
5
  "type": "module",
6
6
  "main": "dist/agent.js",