doer-agent 0.3.5 → 0.4.0
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 +274 -64
- package/package.json +1 -1
package/dist/agent.js
CHANGED
|
@@ -9,6 +9,8 @@ const DEFAULT_SERVER_BASE_URL = "https://doer.cranix.net";
|
|
|
9
9
|
const AGENT_MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
10
10
|
const AGENT_PROJECT_DIR = path.join(AGENT_MODULE_DIR, "..");
|
|
11
11
|
const AGENT_PACKAGE_JSON_PATH = path.join(AGENT_PROJECT_DIR, "package.json");
|
|
12
|
+
const HEARTBEAT_INTERVAL_MS = 5_000;
|
|
13
|
+
const HEARTBEAT_FAILURE_THRESHOLD = 3;
|
|
12
14
|
let activeTaskLogContext = null;
|
|
13
15
|
let workspaceRootOverride = null;
|
|
14
16
|
const fsRpcCodec = StringCodec();
|
|
@@ -18,6 +20,7 @@ const codexAuthRpcCodec = StringCodec();
|
|
|
18
20
|
const settingsRpcCodec = StringCodec();
|
|
19
21
|
const gitRpcCodec = StringCodec();
|
|
20
22
|
const skillRpcCodec = StringCodec();
|
|
23
|
+
const runEventsCodec = StringCodec();
|
|
21
24
|
const retainedRuns = new Map();
|
|
22
25
|
const activeSessionWatchers = new Map();
|
|
23
26
|
const sessionLineIndexCache = new Map();
|
|
@@ -30,6 +33,9 @@ function sanitizeUserId(userId) {
|
|
|
30
33
|
function buildAgentRunRpcSubject(userId, agentId) {
|
|
31
34
|
return `doer.agent.run.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
|
|
32
35
|
}
|
|
36
|
+
function buildAgentRunEventsSubject(userId, agentId) {
|
|
37
|
+
return `doer.agent.run.events.${sanitizeUserId(userId)}.${agentId.trim()}`;
|
|
38
|
+
}
|
|
33
39
|
function buildAgentSessionRpcSubject(userId, agentId) {
|
|
34
40
|
return `doer.agent.session.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
|
|
35
41
|
}
|
|
@@ -514,7 +520,7 @@ async function resolveRunStartLockPath(args) {
|
|
|
514
520
|
if (typeof args.sessionId === "string" && args.sessionId.trim()) {
|
|
515
521
|
return path.join(dir, `session__${sanitizeRunLockSegment(args.sessionId)}.lock`);
|
|
516
522
|
}
|
|
517
|
-
return path.join(dir,
|
|
523
|
+
return path.join(dir, "pending_new_session.lock");
|
|
518
524
|
}
|
|
519
525
|
async function claimRunStartSlot(args) {
|
|
520
526
|
const lockPath = await resolveRunStartLockPath(args);
|
|
@@ -601,10 +607,13 @@ function resolveAgentSettingsDir() {
|
|
|
601
607
|
function resolveAgentSettingsFilePath() {
|
|
602
608
|
return path.join(resolveAgentSettingsDir(), "config.json");
|
|
603
609
|
}
|
|
610
|
+
function resolveAgentModelInstructionsFilePath() {
|
|
611
|
+
return path.join(resolveAgentSettingsDir(), "model-instructions.md");
|
|
612
|
+
}
|
|
604
613
|
function createDefaultAgentSettingsConfig() {
|
|
605
614
|
return {
|
|
606
615
|
general: {
|
|
607
|
-
|
|
616
|
+
personality: "pragmatic",
|
|
608
617
|
},
|
|
609
618
|
codex: {
|
|
610
619
|
model: "gpt-5.4",
|
|
@@ -667,6 +676,9 @@ function normalizeNullableString(value) {
|
|
|
667
676
|
const trimmed = value.trim();
|
|
668
677
|
return trimmed ? trimmed : null;
|
|
669
678
|
}
|
|
679
|
+
function normalizeCodexPersonality(value, fallback) {
|
|
680
|
+
return value === "friendly" || value === "pragmatic" ? value : fallback;
|
|
681
|
+
}
|
|
670
682
|
function normalizeAgentSettingsConfig(value, fallback) {
|
|
671
683
|
const base = fallback ?? createDefaultAgentSettingsConfig();
|
|
672
684
|
const raw = value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
@@ -681,7 +693,7 @@ function normalizeAgentSettingsConfig(value, fallback) {
|
|
|
681
693
|
const figma = raw.figma && typeof raw.figma === "object" ? raw.figma : {};
|
|
682
694
|
return {
|
|
683
695
|
general: {
|
|
684
|
-
|
|
696
|
+
personality: normalizeCodexPersonality(general.personality, base.general.personality),
|
|
685
697
|
},
|
|
686
698
|
codex: {
|
|
687
699
|
model: typeof codex.model === "string" && codex.model.trim() ? codex.model.trim() : base.codex.model,
|
|
@@ -753,6 +765,20 @@ async function writeAgentSettingsConfig(config) {
|
|
|
753
765
|
await mkdir(dir, { recursive: true });
|
|
754
766
|
await writeFile(resolveAgentSettingsFilePath(), `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
755
767
|
}
|
|
768
|
+
async function readAgentModelInstructions() {
|
|
769
|
+
const raw = await readFile(resolveAgentModelInstructionsFilePath(), "utf8").catch(() => "");
|
|
770
|
+
return raw.trim() ? raw : null;
|
|
771
|
+
}
|
|
772
|
+
async function writeAgentModelInstructions(value) {
|
|
773
|
+
const filePath = resolveAgentModelInstructionsFilePath();
|
|
774
|
+
const nextValue = typeof value === "string" ? value.trim() : "";
|
|
775
|
+
if (!nextValue) {
|
|
776
|
+
await unlink(filePath).catch(() => undefined);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
await mkdir(resolveAgentSettingsDir(), { recursive: true });
|
|
780
|
+
await writeFile(filePath, value ?? "", "utf8");
|
|
781
|
+
}
|
|
756
782
|
function maskSecretPreview(secret) {
|
|
757
783
|
if (secret.length <= 6) {
|
|
758
784
|
return `${secret.slice(0, 1)}***${secret.slice(-1)}`;
|
|
@@ -765,7 +791,7 @@ function toMaskedSecret(value) {
|
|
|
765
791
|
}
|
|
766
792
|
return { has: true, masked: maskSecretPreview(value), length: value.length };
|
|
767
793
|
}
|
|
768
|
-
function toAgentSettingsPublic(config) {
|
|
794
|
+
async function toAgentSettingsPublic(config) {
|
|
769
795
|
const realtimeKey = toMaskedSecret(config.realtime.apiKey);
|
|
770
796
|
const gitOauth = toMaskedSecret(config.git.oauthToken);
|
|
771
797
|
const awsSecret = toMaskedSecret(config.aws.secretAccessKey);
|
|
@@ -774,9 +800,11 @@ function toAgentSettingsPublic(config) {
|
|
|
774
800
|
const notionToken = toMaskedSecret(config.notion.apiToken);
|
|
775
801
|
const slackToken = toMaskedSecret(config.slack.botToken);
|
|
776
802
|
const figmaToken = toMaskedSecret(config.figma.apiToken);
|
|
803
|
+
const customInstructions = await readAgentModelInstructions();
|
|
777
804
|
return {
|
|
778
805
|
general: {
|
|
779
|
-
|
|
806
|
+
personality: config.general.personality,
|
|
807
|
+
customInstructions,
|
|
780
808
|
},
|
|
781
809
|
codex: {
|
|
782
810
|
model: config.codex.model,
|
|
@@ -868,7 +896,7 @@ function normalizeAgentSettingsPatch(value) {
|
|
|
868
896
|
assignNested(section, key, raw[flatKey]);
|
|
869
897
|
delete patch[flatKey];
|
|
870
898
|
};
|
|
871
|
-
move("
|
|
899
|
+
move("personality", "general", "personality");
|
|
872
900
|
move("codexModel", "codex", "model");
|
|
873
901
|
move("codexAuthMode", "codex", "authMode");
|
|
874
902
|
move("realtimeModel", "realtime", "model");
|
|
@@ -976,34 +1004,136 @@ function cloneRunTask(task, _sinceSeq) {
|
|
|
976
1004
|
...task,
|
|
977
1005
|
};
|
|
978
1006
|
}
|
|
979
|
-
function
|
|
1007
|
+
function buildImmediateRunChangedEvent(task) {
|
|
1008
|
+
return {
|
|
1009
|
+
type: "run.changed",
|
|
1010
|
+
agentId: task.agentId,
|
|
1011
|
+
sessionId: task.sessionId,
|
|
1012
|
+
filePath: task.sessionFilePath,
|
|
1013
|
+
runId: task.id,
|
|
1014
|
+
updatedAt: task.updatedAt,
|
|
1015
|
+
status: task.status,
|
|
1016
|
+
cancelRequested: task.cancelRequested,
|
|
1017
|
+
resultExitCode: task.resultExitCode,
|
|
1018
|
+
resultSignal: task.resultSignal,
|
|
1019
|
+
error: task.error,
|
|
1020
|
+
finishedAt: task.finishedAt,
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
function publishImmediateRunChangedEvent(args) {
|
|
1024
|
+
args.nc.publish(buildAgentRunEventsSubject(args.userId, args.task.agentId), runEventsCodec.encode(JSON.stringify(buildImmediateRunChangedEvent(args.task))));
|
|
1025
|
+
}
|
|
1026
|
+
async function findSessionFilePathBySessionId(sessionId) {
|
|
1027
|
+
const targetSessionId = sessionId.trim();
|
|
1028
|
+
if (!targetSessionId) {
|
|
1029
|
+
return null;
|
|
1030
|
+
}
|
|
1031
|
+
let sessionsRootStat;
|
|
980
1032
|
try {
|
|
981
|
-
|
|
982
|
-
const lineType = typeof parsed.type === "string" ? parsed.type : "";
|
|
983
|
-
if (!parsed.payload || typeof parsed.payload !== "object" || Array.isArray(parsed.payload)) {
|
|
984
|
-
return { sessionId: null, sessionFilePath: null };
|
|
985
|
-
}
|
|
986
|
-
const payload = parsed.payload;
|
|
987
|
-
const sessionIdCandidate = typeof payload.sessionId === "string" && payload.sessionId.trim()
|
|
988
|
-
? payload.sessionId.trim()
|
|
989
|
-
: typeof payload.session_id === "string" && payload.session_id.trim()
|
|
990
|
-
? payload.session_id.trim()
|
|
991
|
-
: typeof payload.id === "string" && payload.id.trim() && (lineType === "session_meta" || lineType === "session.started")
|
|
992
|
-
? payload.id.trim()
|
|
993
|
-
: null;
|
|
994
|
-
const filePathCandidate = typeof payload.rollout_path === "string" && payload.rollout_path.trim()
|
|
995
|
-
? payload.rollout_path.trim()
|
|
996
|
-
: typeof payload.filePath === "string" && payload.filePath.trim()
|
|
997
|
-
? payload.filePath.trim()
|
|
998
|
-
: null;
|
|
999
|
-
return {
|
|
1000
|
-
sessionId: sessionIdCandidate,
|
|
1001
|
-
sessionFilePath: filePathCandidate,
|
|
1002
|
-
};
|
|
1033
|
+
sessionsRootStat = await stat(getSessionsRootPath());
|
|
1003
1034
|
}
|
|
1004
1035
|
catch {
|
|
1005
|
-
return
|
|
1036
|
+
return null;
|
|
1006
1037
|
}
|
|
1038
|
+
if (!sessionsRootStat.isDirectory()) {
|
|
1039
|
+
return null;
|
|
1040
|
+
}
|
|
1041
|
+
const files = await collectSessionJsonlFiles(getSessionsRootPath());
|
|
1042
|
+
files.sort((a, b) => b.mtimeMs - a.mtimeMs || a.filePath.localeCompare(b.filePath));
|
|
1043
|
+
for (const file of files) {
|
|
1044
|
+
let fileHandle = null;
|
|
1045
|
+
try {
|
|
1046
|
+
fileHandle = await open(file.filePath, "r");
|
|
1047
|
+
const entryStat = await fileHandle.stat();
|
|
1048
|
+
const firstLine = await readFirstLine(fileHandle, entryStat.size);
|
|
1049
|
+
if (!firstLine) {
|
|
1050
|
+
continue;
|
|
1051
|
+
}
|
|
1052
|
+
const parsed = JSON.parse(firstLine);
|
|
1053
|
+
const candidateMeta = parsed && parsed.type === "session_meta" && isObjectRecord(parsed.payload)
|
|
1054
|
+
? parsed.payload
|
|
1055
|
+
: isObjectRecord(parsed.session_meta)
|
|
1056
|
+
? parsed.session_meta
|
|
1057
|
+
: isObjectRecord(parsed.sessionMeta)
|
|
1058
|
+
? parsed.sessionMeta
|
|
1059
|
+
: isObjectRecord(parsed.meta)
|
|
1060
|
+
? parsed.meta
|
|
1061
|
+
: isObjectRecord(parsed.payload)
|
|
1062
|
+
? parsed.payload
|
|
1063
|
+
: parsed;
|
|
1064
|
+
const candidateId = pickSessionString(candidateMeta.sessionId, candidateMeta.session_id, candidateMeta.id);
|
|
1065
|
+
if (candidateId === targetSessionId) {
|
|
1066
|
+
return file.filePath;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
catch {
|
|
1070
|
+
// ignore malformed session files
|
|
1071
|
+
}
|
|
1072
|
+
finally {
|
|
1073
|
+
await fileHandle?.close().catch(() => undefined);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
return null;
|
|
1077
|
+
}
|
|
1078
|
+
async function readSessionIdFromSessionFile(filePath) {
|
|
1079
|
+
let fileHandle = null;
|
|
1080
|
+
try {
|
|
1081
|
+
fileHandle = await open(filePath, "r");
|
|
1082
|
+
const entryStat = await fileHandle.stat();
|
|
1083
|
+
const firstLine = await readFirstLine(fileHandle, entryStat.size);
|
|
1084
|
+
if (!firstLine) {
|
|
1085
|
+
return null;
|
|
1086
|
+
}
|
|
1087
|
+
const parsed = JSON.parse(firstLine);
|
|
1088
|
+
const candidateMeta = parsed && parsed.type === "session_meta" && isObjectRecord(parsed.payload)
|
|
1089
|
+
? parsed.payload
|
|
1090
|
+
: isObjectRecord(parsed.session_meta)
|
|
1091
|
+
? parsed.session_meta
|
|
1092
|
+
: isObjectRecord(parsed.sessionMeta)
|
|
1093
|
+
? parsed.sessionMeta
|
|
1094
|
+
: isObjectRecord(parsed.meta)
|
|
1095
|
+
? parsed.meta
|
|
1096
|
+
: isObjectRecord(parsed.payload)
|
|
1097
|
+
? parsed.payload
|
|
1098
|
+
: parsed;
|
|
1099
|
+
return pickSessionString(candidateMeta.sessionId, candidateMeta.session_id, candidateMeta.id);
|
|
1100
|
+
}
|
|
1101
|
+
catch {
|
|
1102
|
+
return null;
|
|
1103
|
+
}
|
|
1104
|
+
finally {
|
|
1105
|
+
await fileHandle?.close().catch(() => undefined);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
async function detectPendingRunSession(knownFilePaths) {
|
|
1109
|
+
const sessionsRoot = getSessionsRootPath();
|
|
1110
|
+
let sessionsRootStat;
|
|
1111
|
+
try {
|
|
1112
|
+
sessionsRootStat = await stat(sessionsRoot);
|
|
1113
|
+
}
|
|
1114
|
+
catch {
|
|
1115
|
+
return null;
|
|
1116
|
+
}
|
|
1117
|
+
if (!sessionsRootStat.isDirectory()) {
|
|
1118
|
+
return null;
|
|
1119
|
+
}
|
|
1120
|
+
const files = await collectSessionJsonlFiles(sessionsRoot);
|
|
1121
|
+
files.sort((a, b) => a.mtimeMs - b.mtimeMs || a.filePath.localeCompare(b.filePath));
|
|
1122
|
+
for (const file of files) {
|
|
1123
|
+
if (knownFilePaths.has(file.filePath)) {
|
|
1124
|
+
continue;
|
|
1125
|
+
}
|
|
1126
|
+
const sessionId = await readSessionIdFromSessionFile(file.filePath);
|
|
1127
|
+
if (!sessionId) {
|
|
1128
|
+
continue;
|
|
1129
|
+
}
|
|
1130
|
+
knownFilePaths.add(file.filePath);
|
|
1131
|
+
return {
|
|
1132
|
+
sessionId,
|
|
1133
|
+
sessionFilePath: file.filePath,
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
return null;
|
|
1007
1137
|
}
|
|
1008
1138
|
async function updateRunSessionMetadata(task, metadata) {
|
|
1009
1139
|
let changed = false;
|
|
@@ -1016,11 +1146,25 @@ async function updateRunSessionMetadata(task, metadata) {
|
|
|
1016
1146
|
task.sessionFilePath = metadata.sessionFilePath.trim();
|
|
1017
1147
|
changed = true;
|
|
1018
1148
|
}
|
|
1149
|
+
if (!task.sessionFilePath && task.sessionId) {
|
|
1150
|
+
const resolvedSessionFilePath = await findSessionFilePathBySessionId(task.sessionId).catch(() => null);
|
|
1151
|
+
if (resolvedSessionFilePath) {
|
|
1152
|
+
task.sessionFilePath = resolvedSessionFilePath;
|
|
1153
|
+
changed = true;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1019
1156
|
if (!changed) {
|
|
1020
1157
|
return;
|
|
1021
1158
|
}
|
|
1022
1159
|
task.updatedAt = formatLocalTimestamp();
|
|
1023
1160
|
await persistRunTask(task).catch(() => undefined);
|
|
1161
|
+
if (metadata.nc) {
|
|
1162
|
+
publishImmediateRunChangedEvent({
|
|
1163
|
+
nc: metadata.nc,
|
|
1164
|
+
userId: task.userId,
|
|
1165
|
+
task,
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1024
1168
|
if (!previousSessionId && task.sessionId) {
|
|
1025
1169
|
await updateRunStartSlotSession({
|
|
1026
1170
|
runId: task.id,
|
|
@@ -1132,46 +1276,60 @@ async function startManagedRun(args) {
|
|
|
1132
1276
|
startedAt: now,
|
|
1133
1277
|
finishedAt: null,
|
|
1134
1278
|
};
|
|
1135
|
-
let
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1279
|
+
let pendingSessionPollClosed = false;
|
|
1280
|
+
let pendingSessionPollTimer = null;
|
|
1281
|
+
const knownPendingSessionFiles = new Set();
|
|
1282
|
+
const stopPendingSessionPoll = () => {
|
|
1283
|
+
pendingSessionPollClosed = true;
|
|
1284
|
+
if (pendingSessionPollTimer) {
|
|
1285
|
+
clearInterval(pendingSessionPollTimer);
|
|
1286
|
+
pendingSessionPollTimer = null;
|
|
1287
|
+
}
|
|
1288
|
+
};
|
|
1289
|
+
const pollPendingSession = async () => {
|
|
1290
|
+
if (pendingSessionPollClosed || task.sessionId) {
|
|
1291
|
+
stopPendingSessionPoll();
|
|
1139
1292
|
return;
|
|
1140
1293
|
}
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
}
|
|
1294
|
+
const detected = await detectPendingRunSession(knownPendingSessionFiles).catch(() => null);
|
|
1295
|
+
if (!detected) {
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
await updateRunSessionMetadata(task, {
|
|
1299
|
+
sessionId: detected.sessionId,
|
|
1300
|
+
sessionFilePath: detected.sessionFilePath,
|
|
1301
|
+
nc: args.nc,
|
|
1302
|
+
}).catch(() => undefined);
|
|
1303
|
+
if (task.sessionId) {
|
|
1304
|
+
stopPendingSessionPoll();
|
|
1153
1305
|
}
|
|
1154
1306
|
};
|
|
1155
|
-
|
|
1156
|
-
|
|
1307
|
+
if (!task.sessionId) {
|
|
1308
|
+
const existingFiles = await collectSessionJsonlFiles(getSessionsRootPath()).catch(() => []);
|
|
1309
|
+
for (const file of existingFiles) {
|
|
1310
|
+
knownPendingSessionFiles.add(file.filePath);
|
|
1311
|
+
}
|
|
1312
|
+
pendingSessionPollTimer = setInterval(() => {
|
|
1313
|
+
void pollPendingSession();
|
|
1314
|
+
}, 1000);
|
|
1315
|
+
}
|
|
1316
|
+
child.stdout.on("data", (chunk) => writeRunStream(task.id, "stdout", chunk));
|
|
1317
|
+
child.stderr.on("data", (chunk) => writeRunStream(task.id, "stderr", chunk));
|
|
1157
1318
|
child.once("error", (error) => {
|
|
1319
|
+
stopPendingSessionPoll();
|
|
1158
1320
|
const message = error instanceof Error ? error.message : String(error);
|
|
1159
1321
|
task.status = "failed";
|
|
1160
1322
|
task.error = message;
|
|
1161
1323
|
task.finishedAt = formatLocalTimestamp();
|
|
1162
1324
|
persistRetainedRun(task);
|
|
1325
|
+
publishImmediateRunChangedEvent({ nc: args.nc, userId: task.userId, task });
|
|
1163
1326
|
void removeRunTask(task.id).catch(() => undefined);
|
|
1164
1327
|
void releaseRunStartSlot({ runId: task.id, sessionId: task.sessionId }).catch(() => undefined);
|
|
1165
1328
|
void prepared.codexAuthCleanup().catch(() => undefined);
|
|
1166
1329
|
writeRunStatus(task.id, `failed error=${message}`);
|
|
1167
1330
|
});
|
|
1168
1331
|
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
|
-
}
|
|
1332
|
+
stopPendingSessionPoll();
|
|
1175
1333
|
const latest = await getStoredRun(task.id).catch(() => null);
|
|
1176
1334
|
if (latest?.cancelRequested) {
|
|
1177
1335
|
task.cancelRequested = true;
|
|
@@ -1182,6 +1340,7 @@ async function startManagedRun(args) {
|
|
|
1182
1340
|
task.status = task.cancelRequested ? "canceled" : (task.resultExitCode ?? 1) === 0 ? "completed" : "failed";
|
|
1183
1341
|
task.error = task.status === "failed" ? `Command exited with code ${task.resultExitCode ?? "null"}` : null;
|
|
1184
1342
|
persistRetainedRun(task);
|
|
1343
|
+
publishImmediateRunChangedEvent({ nc: args.nc, userId: task.userId, task });
|
|
1185
1344
|
void removeRunTask(task.id).catch(() => undefined);
|
|
1186
1345
|
void releaseRunStartSlot({ runId: task.id, sessionId: task.sessionId }).catch(() => undefined);
|
|
1187
1346
|
void prepared.codexAuthCleanup().catch(() => undefined);
|
|
@@ -1189,7 +1348,11 @@ async function startManagedRun(args) {
|
|
|
1189
1348
|
});
|
|
1190
1349
|
persistRetainedRun(task);
|
|
1191
1350
|
void persistRunTask(task).catch(() => undefined);
|
|
1351
|
+
publishImmediateRunChangedEvent({ nc: args.nc, userId: task.userId, task });
|
|
1192
1352
|
writeRunStatus(task.id, `started requestId=${args.requestId} cwd=${prepared.taskWorkspace}`);
|
|
1353
|
+
if (!task.sessionId) {
|
|
1354
|
+
void pollPendingSession();
|
|
1355
|
+
}
|
|
1193
1356
|
return cloneRunTask(task);
|
|
1194
1357
|
}
|
|
1195
1358
|
function shellSingleQuote(value) {
|
|
@@ -1202,12 +1365,22 @@ function normalizeCodexModel(value) {
|
|
|
1202
1365
|
const normalized = typeof value === "string" ? value.trim() : "";
|
|
1203
1366
|
return normalized || "gpt-5.4";
|
|
1204
1367
|
}
|
|
1368
|
+
function toTomlStringLiteral(value) {
|
|
1369
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
1370
|
+
}
|
|
1205
1371
|
function buildManagedCodexArgs(args) {
|
|
1206
1372
|
const promptArgs = ["--", args.prompt];
|
|
1207
1373
|
const fixedArgs = ["--dangerously-bypass-approvals-and-sandbox"];
|
|
1374
|
+
const configArgs = [
|
|
1375
|
+
...(args.personality ? ["--config", `personality=${toTomlStringLiteral(args.personality)}`] : []),
|
|
1376
|
+
...(args.modelInstructionsFile
|
|
1377
|
+
? ["--config", `model_instructions_file=${toTomlStringLiteral(args.modelInstructionsFile)}`]
|
|
1378
|
+
: []),
|
|
1379
|
+
];
|
|
1208
1380
|
const imageArgs = args.imagePaths.flatMap((imagePath) => ["--image", imagePath]);
|
|
1209
1381
|
return [
|
|
1210
1382
|
...fixedArgs,
|
|
1383
|
+
...configArgs,
|
|
1211
1384
|
"--model",
|
|
1212
1385
|
args.model,
|
|
1213
1386
|
...(args.sessionId
|
|
@@ -1578,6 +1751,14 @@ async function handleSettingsRpcMessage(args) {
|
|
|
1578
1751
|
const next = request.action === "update" ? normalizeAgentSettingsConfig(request.patch, existing) : existing;
|
|
1579
1752
|
if (request.action === "update") {
|
|
1580
1753
|
await writeAgentSettingsConfig(next);
|
|
1754
|
+
const customInstructions = typeof request.patch.customInstructions === "string"
|
|
1755
|
+
? request.patch.customInstructions
|
|
1756
|
+
: request.patch.customInstructions === null
|
|
1757
|
+
? null
|
|
1758
|
+
: undefined;
|
|
1759
|
+
if (customInstructions !== undefined) {
|
|
1760
|
+
await writeAgentModelInstructions(customInstructions);
|
|
1761
|
+
}
|
|
1581
1762
|
}
|
|
1582
1763
|
else if (request.defaults) {
|
|
1583
1764
|
const filePath = resolveAgentSettingsFilePath();
|
|
@@ -1592,7 +1773,7 @@ async function handleSettingsRpcMessage(args) {
|
|
|
1592
1773
|
payload: {
|
|
1593
1774
|
requestId,
|
|
1594
1775
|
ok: true,
|
|
1595
|
-
settings: toAgentSettingsPublic(next),
|
|
1776
|
+
settings: await toAgentSettingsPublic(next),
|
|
1596
1777
|
},
|
|
1597
1778
|
});
|
|
1598
1779
|
}
|
|
@@ -2198,18 +2379,23 @@ async function handleRunRpcMessage(args) {
|
|
|
2198
2379
|
const runId = request.runId ?? requestId;
|
|
2199
2380
|
await claimRunStartSlot({ runId, sessionId: request.sessionId });
|
|
2200
2381
|
try {
|
|
2382
|
+
const localAgentSettings = await readAgentSettingsConfig(null);
|
|
2383
|
+
const customInstructions = await readAgentModelInstructions();
|
|
2201
2384
|
const task = await startManagedRun({
|
|
2202
2385
|
requestId,
|
|
2203
2386
|
runId,
|
|
2204
2387
|
serverBaseUrl: args.serverBaseUrl,
|
|
2205
2388
|
userId: args.userId,
|
|
2206
2389
|
agentId: args.agentId,
|
|
2390
|
+
nc: args.jetstream.nc,
|
|
2207
2391
|
sessionId: request.sessionId,
|
|
2208
2392
|
codexArgs: buildManagedCodexArgs({
|
|
2209
2393
|
prompt: request.prompt ?? "",
|
|
2210
2394
|
imagePaths: request.imagePaths,
|
|
2211
2395
|
sessionId: request.sessionId,
|
|
2212
2396
|
model: request.model,
|
|
2397
|
+
personality: localAgentSettings.general.personality,
|
|
2398
|
+
modelInstructionsFile: customInstructions ? resolveAgentModelInstructionsFilePath() : null,
|
|
2213
2399
|
}),
|
|
2214
2400
|
cwd: request.cwd,
|
|
2215
2401
|
runtimeEnvPatch: request.runtimeEnvPatch,
|
|
@@ -2253,6 +2439,7 @@ async function handleRunRpcMessage(args) {
|
|
|
2253
2439
|
target.cancelRequested = true;
|
|
2254
2440
|
target.updatedAt = formatLocalTimestamp();
|
|
2255
2441
|
await persistRunTask(target);
|
|
2442
|
+
publishImmediateRunChangedEvent({ nc: args.jetstream.nc, userId: target.userId, task: target });
|
|
2256
2443
|
writeRunStatus(target.id, `cancel requested pid=${target.processPid}`);
|
|
2257
2444
|
sendSignalToPid(target.processPid, "SIGINT");
|
|
2258
2445
|
const task = cloneRunTask(target);
|
|
@@ -3749,7 +3936,8 @@ function persistEventOrFatal(args) {
|
|
|
3749
3936
|
}
|
|
3750
3937
|
})();
|
|
3751
3938
|
}
|
|
3752
|
-
async function
|
|
3939
|
+
async function heartbeatAgentSession(args) {
|
|
3940
|
+
await args.nc.flush();
|
|
3753
3941
|
await postJson(`${args.serverBaseUrl}/api/agent/heartbeat`, {
|
|
3754
3942
|
userId: args.userId,
|
|
3755
3943
|
agentToken: args.agentToken,
|
|
@@ -4189,23 +4377,45 @@ async function main() {
|
|
|
4189
4377
|
else {
|
|
4190
4378
|
writeAgentInfraError(`nats session restored agentId=${initialAgentId} servers=${jetstream.servers.join(",")} at=${formatLocalTimestamp()}`);
|
|
4191
4379
|
}
|
|
4192
|
-
let
|
|
4380
|
+
let heartbeatFailures = 0;
|
|
4381
|
+
let heartbeatInFlight = false;
|
|
4382
|
+
let sessionInvalidated = false;
|
|
4383
|
+
const invalidateAgentSession = (reason) => {
|
|
4384
|
+
if (sessionInvalidated) {
|
|
4385
|
+
return;
|
|
4386
|
+
}
|
|
4387
|
+
sessionInvalidated = true;
|
|
4388
|
+
writeAgentInfraError(`closing nats session: ${reason}`);
|
|
4389
|
+
void jetstream.nc.close().catch((error) => {
|
|
4390
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4391
|
+
writeAgentInfraError(`failed to close nats session: ${message}`);
|
|
4392
|
+
});
|
|
4393
|
+
};
|
|
4193
4394
|
const heartbeatTimer = setInterval(() => {
|
|
4194
|
-
|
|
4395
|
+
if (heartbeatInFlight || sessionInvalidated) {
|
|
4396
|
+
return;
|
|
4397
|
+
}
|
|
4398
|
+
heartbeatInFlight = true;
|
|
4399
|
+
void heartbeatAgentSession({ nc: jetstream.nc, serverBaseUrl, userId, agentToken })
|
|
4195
4400
|
.then(() => {
|
|
4196
|
-
|
|
4401
|
+
heartbeatInFlight = false;
|
|
4402
|
+
if (heartbeatFailures > 0) {
|
|
4197
4403
|
writeAgentInfraError(`heartbeat reconnected at=${formatLocalTimestamp()}`);
|
|
4198
4404
|
}
|
|
4199
|
-
|
|
4405
|
+
heartbeatFailures = 0;
|
|
4200
4406
|
})
|
|
4201
4407
|
.catch((error) => {
|
|
4408
|
+
heartbeatInFlight = false;
|
|
4202
4409
|
const message = error instanceof Error ? error.message : String(error);
|
|
4203
|
-
|
|
4204
|
-
|
|
4410
|
+
heartbeatFailures += 1;
|
|
4411
|
+
if (heartbeatFailures > 1) {
|
|
4412
|
+
writeAgentInfraError(`heartbeat failed: ${message} (count=${heartbeatFailures}/${HEARTBEAT_FAILURE_THRESHOLD})`);
|
|
4413
|
+
}
|
|
4414
|
+
if (heartbeatFailures >= HEARTBEAT_FAILURE_THRESHOLD) {
|
|
4415
|
+
invalidateAgentSession(`heartbeat failure threshold reached at=${formatLocalTimestamp()}`);
|
|
4205
4416
|
}
|
|
4206
|
-
heartbeatHealthy = false;
|
|
4207
4417
|
});
|
|
4208
|
-
},
|
|
4418
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
4209
4419
|
subscribeToFsRpc({
|
|
4210
4420
|
jetstream,
|
|
4211
4421
|
serverBaseUrl,
|