doer-agent 0.3.5 → 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.
- package/dist/agent.js +190 -49
- 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,
|
|
521
|
+
return path.join(dir, "pending_new_session.lock");
|
|
518
522
|
}
|
|
519
523
|
async function claimRunStartSlot(args) {
|
|
520
524
|
const lockPath = await resolveRunStartLockPath(args);
|
|
@@ -976,34 +980,136 @@ function cloneRunTask(task, _sinceSeq) {
|
|
|
976
980
|
...task,
|
|
977
981
|
};
|
|
978
982
|
}
|
|
979
|
-
function
|
|
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;
|
|
980
1008
|
try {
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1009
|
+
sessionsRootStat = await stat(getSessionsRootPath());
|
|
1010
|
+
}
|
|
1011
|
+
catch {
|
|
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
|
+
}
|
|
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);
|
|
1003
1076
|
}
|
|
1004
1077
|
catch {
|
|
1005
|
-
return
|
|
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
|
+
};
|
|
1006
1111
|
}
|
|
1112
|
+
return null;
|
|
1007
1113
|
}
|
|
1008
1114
|
async function updateRunSessionMetadata(task, metadata) {
|
|
1009
1115
|
let changed = false;
|
|
@@ -1016,11 +1122,25 @@ async function updateRunSessionMetadata(task, metadata) {
|
|
|
1016
1122
|
task.sessionFilePath = metadata.sessionFilePath.trim();
|
|
1017
1123
|
changed = true;
|
|
1018
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
|
+
}
|
|
1019
1132
|
if (!changed) {
|
|
1020
1133
|
return;
|
|
1021
1134
|
}
|
|
1022
1135
|
task.updatedAt = formatLocalTimestamp();
|
|
1023
1136
|
await persistRunTask(task).catch(() => undefined);
|
|
1137
|
+
if (metadata.nc) {
|
|
1138
|
+
publishImmediateRunChangedEvent({
|
|
1139
|
+
nc: metadata.nc,
|
|
1140
|
+
userId: task.userId,
|
|
1141
|
+
task,
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1024
1144
|
if (!previousSessionId && task.sessionId) {
|
|
1025
1145
|
await updateRunStartSlotSession({
|
|
1026
1146
|
runId: task.id,
|
|
@@ -1132,46 +1252,60 @@ async function startManagedRun(args) {
|
|
|
1132
1252
|
startedAt: now,
|
|
1133
1253
|
finishedAt: null,
|
|
1134
1254
|
};
|
|
1135
|
-
let
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
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();
|
|
1139
1268
|
return;
|
|
1140
1269
|
}
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
}
|
|
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();
|
|
1153
1281
|
}
|
|
1154
1282
|
};
|
|
1155
|
-
|
|
1156
|
-
|
|
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));
|
|
1157
1294
|
child.once("error", (error) => {
|
|
1295
|
+
stopPendingSessionPoll();
|
|
1158
1296
|
const message = error instanceof Error ? error.message : String(error);
|
|
1159
1297
|
task.status = "failed";
|
|
1160
1298
|
task.error = message;
|
|
1161
1299
|
task.finishedAt = formatLocalTimestamp();
|
|
1162
1300
|
persistRetainedRun(task);
|
|
1301
|
+
publishImmediateRunChangedEvent({ nc: args.nc, userId: task.userId, task });
|
|
1163
1302
|
void removeRunTask(task.id).catch(() => undefined);
|
|
1164
1303
|
void releaseRunStartSlot({ runId: task.id, sessionId: task.sessionId }).catch(() => undefined);
|
|
1165
1304
|
void prepared.codexAuthCleanup().catch(() => undefined);
|
|
1166
1305
|
writeRunStatus(task.id, `failed error=${message}`);
|
|
1167
1306
|
});
|
|
1168
1307
|
child.once("close", async (code, signal) => {
|
|
1169
|
-
|
|
1170
|
-
const metadata = extractCodexSessionMetadata(stdoutBuffer.trim());
|
|
1171
|
-
if (metadata.sessionId || metadata.sessionFilePath) {
|
|
1172
|
-
void updateRunSessionMetadata(task, metadata);
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1308
|
+
stopPendingSessionPoll();
|
|
1175
1309
|
const latest = await getStoredRun(task.id).catch(() => null);
|
|
1176
1310
|
if (latest?.cancelRequested) {
|
|
1177
1311
|
task.cancelRequested = true;
|
|
@@ -1182,6 +1316,7 @@ async function startManagedRun(args) {
|
|
|
1182
1316
|
task.status = task.cancelRequested ? "canceled" : (task.resultExitCode ?? 1) === 0 ? "completed" : "failed";
|
|
1183
1317
|
task.error = task.status === "failed" ? `Command exited with code ${task.resultExitCode ?? "null"}` : null;
|
|
1184
1318
|
persistRetainedRun(task);
|
|
1319
|
+
publishImmediateRunChangedEvent({ nc: args.nc, userId: task.userId, task });
|
|
1185
1320
|
void removeRunTask(task.id).catch(() => undefined);
|
|
1186
1321
|
void releaseRunStartSlot({ runId: task.id, sessionId: task.sessionId }).catch(() => undefined);
|
|
1187
1322
|
void prepared.codexAuthCleanup().catch(() => undefined);
|
|
@@ -1189,7 +1324,11 @@ async function startManagedRun(args) {
|
|
|
1189
1324
|
});
|
|
1190
1325
|
persistRetainedRun(task);
|
|
1191
1326
|
void persistRunTask(task).catch(() => undefined);
|
|
1327
|
+
publishImmediateRunChangedEvent({ nc: args.nc, userId: task.userId, task });
|
|
1192
1328
|
writeRunStatus(task.id, `started requestId=${args.requestId} cwd=${prepared.taskWorkspace}`);
|
|
1329
|
+
if (!task.sessionId) {
|
|
1330
|
+
void pollPendingSession();
|
|
1331
|
+
}
|
|
1193
1332
|
return cloneRunTask(task);
|
|
1194
1333
|
}
|
|
1195
1334
|
function shellSingleQuote(value) {
|
|
@@ -2204,6 +2343,7 @@ async function handleRunRpcMessage(args) {
|
|
|
2204
2343
|
serverBaseUrl: args.serverBaseUrl,
|
|
2205
2344
|
userId: args.userId,
|
|
2206
2345
|
agentId: args.agentId,
|
|
2346
|
+
nc: args.jetstream.nc,
|
|
2207
2347
|
sessionId: request.sessionId,
|
|
2208
2348
|
codexArgs: buildManagedCodexArgs({
|
|
2209
2349
|
prompt: request.prompt ?? "",
|
|
@@ -2253,6 +2393,7 @@ async function handleRunRpcMessage(args) {
|
|
|
2253
2393
|
target.cancelRequested = true;
|
|
2254
2394
|
target.updatedAt = formatLocalTimestamp();
|
|
2255
2395
|
await persistRunTask(target);
|
|
2396
|
+
publishImmediateRunChangedEvent({ nc: args.jetstream.nc, userId: target.userId, task: target });
|
|
2256
2397
|
writeRunStatus(target.id, `cancel requested pid=${target.processPid}`);
|
|
2257
2398
|
sendSignalToPid(target.processPid, "SIGINT");
|
|
2258
2399
|
const task = cloneRunTask(target);
|