@agentex/agent 0.0.34 → 0.0.36
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/CHANGELOG.md +227 -0
- package/README.md +2 -1
- package/dist/providers/claude/discovery.d.ts +64 -0
- package/dist/providers/claude/discovery.d.ts.map +1 -0
- package/dist/providers/claude/discovery.js +147 -0
- package/dist/providers/claude/discovery.js.map +1 -0
- package/dist/providers/claude/effort.d.ts +20 -0
- package/dist/providers/claude/effort.d.ts.map +1 -0
- package/dist/providers/claude/effort.js +32 -0
- package/dist/providers/claude/effort.js.map +1 -0
- package/dist/providers/claude/execute.d.ts.map +1 -1
- package/dist/providers/claude/execute.js +2 -1
- package/dist/providers/claude/execute.js.map +1 -1
- package/dist/providers/claude/index.d.ts.map +1 -1
- package/dist/providers/claude/index.js +2 -1
- package/dist/providers/claude/index.js.map +1 -1
- package/dist/providers/claude/parse.d.ts.map +1 -1
- package/dist/providers/claude/parse.js +26 -4
- package/dist/providers/claude/parse.js.map +1 -1
- package/dist/providers/claude/session.d.ts.map +1 -1
- package/dist/providers/claude/session.js +2 -1
- package/dist/providers/claude/session.js.map +1 -1
- package/dist/providers/codex/discovery.d.ts +20 -0
- package/dist/providers/codex/discovery.d.ts.map +1 -0
- package/dist/providers/codex/discovery.js +76 -0
- package/dist/providers/codex/discovery.js.map +1 -0
- package/dist/providers/codex/execute.d.ts.map +1 -1
- package/dist/providers/codex/execute.js +45 -3
- package/dist/providers/codex/execute.js.map +1 -1
- package/dist/providers/codex/index.d.ts.map +1 -1
- package/dist/providers/codex/index.js +2 -1
- package/dist/providers/codex/index.js.map +1 -1
- package/dist/providers/codex/parse.d.ts +14 -6
- package/dist/providers/codex/parse.d.ts.map +1 -1
- package/dist/providers/codex/parse.js +51 -16
- package/dist/providers/codex/parse.js.map +1 -1
- package/dist/providers/codex/session.d.ts +29 -1
- package/dist/providers/codex/session.d.ts.map +1 -1
- package/dist/providers/codex/session.js +255 -57
- package/dist/providers/codex/session.js.map +1 -1
- package/dist/providers/cursor/discovery.d.ts.map +1 -1
- package/dist/providers/cursor/discovery.js +4 -0
- package/dist/providers/cursor/discovery.js.map +1 -1
- package/dist/providers/opencode/discovery.d.ts.map +1 -1
- package/dist/providers/opencode/discovery.js +11 -8
- package/dist/providers/opencode/discovery.js.map +1 -1
- package/dist/providers/opencode/event-parse.d.ts +29 -0
- package/dist/providers/opencode/event-parse.d.ts.map +1 -1
- package/dist/providers/opencode/event-parse.js +55 -0
- package/dist/providers/opencode/event-parse.js.map +1 -1
- package/dist/providers/opencode/history.d.ts.map +1 -1
- package/dist/providers/opencode/history.js +25 -4
- package/dist/providers/opencode/history.js.map +1 -1
- package/dist/providers/opencode/http-session.d.ts.map +1 -1
- package/dist/providers/opencode/http-session.js +72 -8
- package/dist/providers/opencode/http-session.js.map +1 -1
- package/dist/types.d.ts +19 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/model-cache.d.ts +33 -0
- package/dist/utils/model-cache.d.ts.map +1 -0
- package/dist/utils/model-cache.js +57 -0
- package/dist/utils/model-cache.js.map +1 -0
- package/package.json +1 -1
- package/src/providers/claude/discovery.ts +215 -0
- package/src/providers/claude/effort.ts +34 -0
- package/src/providers/claude/execute.ts +2 -1
- package/src/providers/claude/index.ts +2 -1
- package/src/providers/claude/parse.ts +27 -5
- package/src/providers/claude/session.ts +2 -1
- package/src/providers/codex/discovery.ts +91 -0
- package/src/providers/codex/execute.ts +42 -3
- package/src/providers/codex/index.ts +2 -1
- package/src/providers/codex/parse.ts +60 -18
- package/src/providers/codex/session.ts +285 -59
- package/src/providers/cursor/discovery.ts +5 -0
- package/src/providers/opencode/discovery.ts +10 -7
- package/src/providers/opencode/event-parse.ts +77 -0
- package/src/providers/opencode/history.ts +32 -4
- package/src/providers/opencode/http-session.ts +72 -8
- package/src/types.ts +19 -5
- package/src/utils/model-cache.ts +67 -0
|
@@ -9,7 +9,7 @@ import { translateEndpoint } from "../../utils/endpoint.js";
|
|
|
9
9
|
import { injectWorkspaceSkills } from "../../utils/skills.js";
|
|
10
10
|
import { resolveInstructions } from "../../utils/instructions.js";
|
|
11
11
|
import { createToolNameTracker } from "../../utils/tool-names.js";
|
|
12
|
-
import {
|
|
12
|
+
import { parseCodexStreamLines } from "./parse.js";
|
|
13
13
|
import { withPlanModePreamble } from "./plan-mode.js";
|
|
14
14
|
import { scanCodexSessionUsage } from "./usage-scanner.js";
|
|
15
15
|
import { codexSessionCodec } from "./codec.js";
|
|
@@ -305,6 +305,8 @@ export class CodexSessionImpl {
|
|
|
305
305
|
*/
|
|
306
306
|
_activeTurnId = null;
|
|
307
307
|
_activeTurnReady = null;
|
|
308
|
+
/** Incremented per turn latch. Never reused, so staleness is decidable. */
|
|
309
|
+
_turnGeneration = 0;
|
|
308
310
|
/** Successful repeated interrupts coalesce until the terminal notification. */
|
|
309
311
|
_interruptPromise = null;
|
|
310
312
|
/** Prevents a late interrupt request after the terminal frame was observed. */
|
|
@@ -439,6 +441,21 @@ export class CodexSessionImpl {
|
|
|
439
441
|
}));
|
|
440
442
|
}
|
|
441
443
|
}
|
|
444
|
+
/**
|
|
445
|
+
* Remove and return the pending sends belonging to one turn generation.
|
|
446
|
+
*
|
|
447
|
+
* `undefined` matches entries with no generation, which is how tests that
|
|
448
|
+
* drive the session without `send()` enqueue resolvers.
|
|
449
|
+
*/
|
|
450
|
+
takePendingResults(generation) {
|
|
451
|
+
const mine = [];
|
|
452
|
+
const others = [];
|
|
453
|
+
for (const entry of this._pendingResults) {
|
|
454
|
+
(entry.generation === generation ? mine : others).push(entry);
|
|
455
|
+
}
|
|
456
|
+
this._pendingResults = others;
|
|
457
|
+
return mine;
|
|
458
|
+
}
|
|
442
459
|
/** Reject every pending send() Promise and outgoing JSON-RPC call. */
|
|
443
460
|
rejectAllPending(err) {
|
|
444
461
|
const pending = this._pendingResults.splice(0);
|
|
@@ -472,6 +489,7 @@ export class CodexSessionImpl {
|
|
|
472
489
|
resolve: resolveFn,
|
|
473
490
|
reject: rejectFn,
|
|
474
491
|
settled: false,
|
|
492
|
+
generation: ++this._turnGeneration,
|
|
475
493
|
};
|
|
476
494
|
this._activeTurnId = null;
|
|
477
495
|
this._activeTurnReady = ready;
|
|
@@ -497,9 +515,17 @@ export class CodexSessionImpl {
|
|
|
497
515
|
expected.settled = true;
|
|
498
516
|
expected.reject(err);
|
|
499
517
|
}
|
|
500
|
-
/**
|
|
501
|
-
|
|
518
|
+
/**
|
|
519
|
+
* Clear the current turn and release an interrupt waiting for its id.
|
|
520
|
+
*
|
|
521
|
+
* `expected` fences the clear against a turn that started after the one being
|
|
522
|
+
* torn down. Without it, a late teardown nulls the latch a newer, running
|
|
523
|
+
* turn is relying on, which is the same silent-Stop failure by another route.
|
|
524
|
+
*/
|
|
525
|
+
clearActiveTurn(err, expected) {
|
|
502
526
|
const ready = this._activeTurnReady;
|
|
527
|
+
if (expected && ready !== expected)
|
|
528
|
+
return;
|
|
503
529
|
if (ready && !ready.settled) {
|
|
504
530
|
ready.settled = true;
|
|
505
531
|
if (err)
|
|
@@ -704,9 +730,17 @@ export class CodexSessionImpl {
|
|
|
704
730
|
// and pass through. If the second `turn/start` lands during the first
|
|
705
731
|
// turn, the per-turn accumulators continue collecting until the result
|
|
706
732
|
// event fires; the result then drains all pending resolvers.
|
|
733
|
+
// Joining the active turn requires that it still be active. The latch
|
|
734
|
+
// survives the terminal frame until deliverTurnResult() clears it, and that
|
|
735
|
+
// gap is real time: it spans `await this._eventChain` (a host persisting
|
|
736
|
+
// events) and, when turn/completed carried no usage, an on-disk usage scan.
|
|
737
|
+
// A send landing in that gap belongs to a NEW turn — joining the dead one
|
|
738
|
+
// would leave it with no interrupt latch and no turn/start capture, so Stop
|
|
739
|
+
// would find nothing to interrupt and return as if it had succeeded.
|
|
707
740
|
const existingTurnReady = this._activeTurnReady;
|
|
708
|
-
const
|
|
709
|
-
const
|
|
741
|
+
const canJoinActiveTurn = existingTurnReady !== null && !this._turnTerminalObserved;
|
|
742
|
+
const isTurnLeader = !canJoinActiveTurn;
|
|
743
|
+
const turnReady = canJoinActiveTurn ? existingTurnReady : this.beginActiveTurn();
|
|
710
744
|
if (this._state === "idle") {
|
|
711
745
|
this._state = "thinking";
|
|
712
746
|
this._turnStartedAt = new Date();
|
|
@@ -736,7 +770,11 @@ export class CodexSessionImpl {
|
|
|
736
770
|
resolveFn = resolve;
|
|
737
771
|
rejectFn = reject;
|
|
738
772
|
});
|
|
739
|
-
const entry = {
|
|
773
|
+
const entry = {
|
|
774
|
+
resolve: resolveFn,
|
|
775
|
+
reject: rejectFn,
|
|
776
|
+
generation: turnReady.generation,
|
|
777
|
+
};
|
|
740
778
|
this._pendingResults.push(entry);
|
|
741
779
|
// Track the in-flight turn so drain() can await it; drop it on settle.
|
|
742
780
|
this._inFlight.add(result);
|
|
@@ -935,6 +973,10 @@ export class CodexSessionImpl {
|
|
|
935
973
|
method: "session/closed",
|
|
936
974
|
});
|
|
937
975
|
this.cancelBackgroundTaskPollTimers();
|
|
976
|
+
// finalizeActiveBackgroundTasks above has already emitted the terminal
|
|
977
|
+
// edges, so nothing downstream needs these records any more.
|
|
978
|
+
this._backgroundTasks.clear();
|
|
979
|
+
this._backgroundTaskIdsByPath.clear();
|
|
938
980
|
this.rejectAllPending(new Error("Codex session closed"));
|
|
939
981
|
this.proc.stdin.end();
|
|
940
982
|
// Grace window before SIGKILL is configurable via ProviderConfig.graceSec
|
|
@@ -1017,13 +1059,22 @@ export class CodexSessionImpl {
|
|
|
1017
1059
|
method === "item/fileChange/requestApproval") {
|
|
1018
1060
|
void this.handleApproval(id, method, params);
|
|
1019
1061
|
}
|
|
1062
|
+
else if (method === "item/permissions/requestApproval") {
|
|
1063
|
+
void this.handlePermissionsApproval(id, params);
|
|
1064
|
+
}
|
|
1020
1065
|
else if (method === "item/tool/requestUserInput" ||
|
|
1021
1066
|
method === "tool/requestUserInput") {
|
|
1022
1067
|
// `tool/requestUserInput` is the legacy method name on older codex builds.
|
|
1023
1068
|
void this.handleUserInputRequest(id, params);
|
|
1024
1069
|
}
|
|
1025
1070
|
else {
|
|
1026
|
-
// Unknown server request
|
|
1071
|
+
// Unknown server request. `{}` is a best-effort reply, NOT a guarantee
|
|
1072
|
+
// that the turn proceeds: every server request the protocol defines but
|
|
1073
|
+
// this file does not handle declares required response fields, so an
|
|
1074
|
+
// empty result is schema-invalid for all of them. A JSON-RPC error reply
|
|
1075
|
+
// would be the honest answer, but `rpcResponse` has no error path and
|
|
1076
|
+
// introducing one changes behavior (an error can abort a turn where this
|
|
1077
|
+
// limps along), so that is a deliberate follow-up rather than a fix here.
|
|
1027
1078
|
this.rpcResponse(id, {});
|
|
1028
1079
|
}
|
|
1029
1080
|
}
|
|
@@ -1039,13 +1090,24 @@ export class CodexSessionImpl {
|
|
|
1039
1090
|
this._state = this._pendingResults.length > 0 ? "thinking" : "idle";
|
|
1040
1091
|
}
|
|
1041
1092
|
async handleApproval(id, method, params) {
|
|
1042
|
-
|
|
1093
|
+
// A child thread's approval still has to be answered — somebody must decide
|
|
1094
|
+
// whether that subagent may run its command — but it is not the root turn's
|
|
1095
|
+
// business. Driving root state from it would show the session as blocked on
|
|
1096
|
+
// approval when the root turn is running fine, and `restoreStateAfter` would
|
|
1097
|
+
// then recompute root state from a child's lifecycle. The protocol makes
|
|
1098
|
+
// `threadId` mandatory on every approval, so the attribution is free.
|
|
1099
|
+
const requestThreadId = str(params, "threadId") || null;
|
|
1100
|
+
const foreign = this.isForeignThread(requestThreadId);
|
|
1101
|
+
if (!foreign)
|
|
1102
|
+
this._state = "waiting_for_approval";
|
|
1103
|
+
const restore = () => { if (!foreign)
|
|
1104
|
+
this.restoreStateAfter("waiting_for_approval"); };
|
|
1043
1105
|
// Codex's app-server expects `{ decision: "accept" | "decline" | "cancel" }`
|
|
1044
1106
|
// (NOT `{ approved: boolean }`). agentex's UserInputResponse has no interrupt
|
|
1045
1107
|
// concept, so allow → accept and deny → decline.
|
|
1046
1108
|
if (!this.ctx.onUserInputRequest) {
|
|
1047
1109
|
this.rpcResponse(id, { decision: "accept" });
|
|
1048
|
-
|
|
1110
|
+
restore();
|
|
1049
1111
|
return;
|
|
1050
1112
|
}
|
|
1051
1113
|
const toolName = method === "item/commandExecution/requestApproval"
|
|
@@ -1055,15 +1117,66 @@ export class CodexSessionImpl {
|
|
|
1055
1117
|
const resp = await this.ctx.onUserInputRequest({
|
|
1056
1118
|
toolName,
|
|
1057
1119
|
input: params,
|
|
1058
|
-
|
|
1059
|
-
|
|
1120
|
+
// `itemId`, not `id` — the protocol has no `id` here, so this was
|
|
1121
|
+
// always "". It is the field that lets a host line an approval up with
|
|
1122
|
+
// the tool_call event carrying the same item id.
|
|
1123
|
+
toolUseId: str(params, "itemId"),
|
|
1124
|
+
// FileChangeRequestApprovalParams carries `grantRoot`, never `path`,
|
|
1125
|
+
// so file-change approvals reached hosts with no description at all.
|
|
1126
|
+
description: str(params, "command")
|
|
1127
|
+
|| str(params, "grantRoot")
|
|
1128
|
+
|| str(params, "reason")
|
|
1129
|
+
|| undefined,
|
|
1130
|
+
// Which subagent is asking. The child thread id is also its
|
|
1131
|
+
// `background_task.taskId`, so a host can line the approval up with
|
|
1132
|
+
// the task it already renders. Absent for the root turn.
|
|
1133
|
+
...(foreign && requestThreadId ? { agentId: requestThreadId } : {}),
|
|
1060
1134
|
});
|
|
1061
1135
|
this.rpcResponse(id, { decision: resp.allow ? "accept" : "decline" });
|
|
1062
1136
|
}
|
|
1063
1137
|
catch {
|
|
1064
1138
|
this.rpcResponse(id, { decision: "decline" });
|
|
1065
1139
|
}
|
|
1066
|
-
|
|
1140
|
+
restore();
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* Handle `item/permissions/requestApproval`.
|
|
1144
|
+
*
|
|
1145
|
+
* Unlike the accept/decline approvals, the response here IS the grant: the
|
|
1146
|
+
* schema requires a `permissions` profile, so the generic "ack with `{}` to
|
|
1147
|
+
* unblock the turn" fallback this used to land in was schema-invalid.
|
|
1148
|
+
* Granting means echoing back the profile the agent asked for; refusing means
|
|
1149
|
+
* an empty profile, which is valid and grants nothing beyond what it already
|
|
1150
|
+
* has. With no host handler we refuse, because silently widening a sandbox is
|
|
1151
|
+
* not a safe default for an unattended session.
|
|
1152
|
+
*/
|
|
1153
|
+
async handlePermissionsApproval(id, params) {
|
|
1154
|
+
const requestThreadId = str(params, "threadId") || null;
|
|
1155
|
+
const foreign = this.isForeignThread(requestThreadId);
|
|
1156
|
+
if (!foreign)
|
|
1157
|
+
this._state = "waiting_for_approval";
|
|
1158
|
+
const restore = () => { if (!foreign)
|
|
1159
|
+
this.restoreStateAfter("waiting_for_approval"); };
|
|
1160
|
+
const requested = asObj(params, "permissions");
|
|
1161
|
+
if (!this.ctx.onUserInputRequest) {
|
|
1162
|
+
this.rpcResponse(id, { permissions: {} });
|
|
1163
|
+
restore();
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
try {
|
|
1167
|
+
const resp = await this.ctx.onUserInputRequest({
|
|
1168
|
+
toolName: "permissions",
|
|
1169
|
+
input: params,
|
|
1170
|
+
toolUseId: str(params, "itemId"),
|
|
1171
|
+
description: str(params, "reason") || undefined,
|
|
1172
|
+
...(foreign && requestThreadId ? { agentId: requestThreadId } : {}),
|
|
1173
|
+
});
|
|
1174
|
+
this.rpcResponse(id, { permissions: resp.allow ? requested : {} });
|
|
1175
|
+
}
|
|
1176
|
+
catch {
|
|
1177
|
+
this.rpcResponse(id, { permissions: {} });
|
|
1178
|
+
}
|
|
1179
|
+
restore();
|
|
1067
1180
|
}
|
|
1068
1181
|
/**
|
|
1069
1182
|
* Handle a Codex `requestUserInput` server→client request: the agent is asking
|
|
@@ -1072,29 +1185,41 @@ export class CodexSessionImpl {
|
|
|
1072
1185
|
* back in Codex's `{ answers: { [questionId]: { answers: string[] } } }` shape.
|
|
1073
1186
|
*/
|
|
1074
1187
|
async handleUserInputRequest(id, params) {
|
|
1188
|
+
// Same scoping as the approval handlers: a child subagent's question still
|
|
1189
|
+
// has to be answered, but it is not the root turn's business. Blocking root
|
|
1190
|
+
// state on it shows the session as waiting for input when the root turn is
|
|
1191
|
+
// running fine. `ToolRequestUserInputParams` requires `threadId`, so the
|
|
1192
|
+
// attribution is read from the wire rather than inferred.
|
|
1193
|
+
const requestThreadId = str(params, "threadId") || null;
|
|
1194
|
+
const foreign = this.isForeignThread(requestThreadId);
|
|
1075
1195
|
// Questions are user *input*, not a tool-permission gate — distinct state so a
|
|
1076
1196
|
// host UI can render a question form vs an approval prompt.
|
|
1077
|
-
|
|
1197
|
+
if (!foreign)
|
|
1198
|
+
this._state = "waiting_for_input";
|
|
1199
|
+
const restore = () => { if (!foreign)
|
|
1200
|
+
this.restoreStateAfter("waiting_for_input"); };
|
|
1078
1201
|
const questions = parseCodexQuestions(params);
|
|
1079
1202
|
// No host handler, or nothing answerable → return empty answers so the
|
|
1080
1203
|
// agent proceeds without hanging.
|
|
1081
1204
|
if (!this.ctx.onUserInputRequest || questions.length === 0) {
|
|
1082
1205
|
this.rpcResponse(id, { answers: {} });
|
|
1083
|
-
|
|
1206
|
+
restore();
|
|
1084
1207
|
return;
|
|
1085
1208
|
}
|
|
1086
1209
|
try {
|
|
1087
1210
|
const resp = await this.ctx.onUserInputRequest({
|
|
1088
1211
|
toolName: "AskUserQuestion",
|
|
1089
1212
|
input: { questions },
|
|
1090
|
-
toolUseId: str(params, "
|
|
1213
|
+
toolUseId: str(params, "itemId") || "codex-user-input",
|
|
1214
|
+
// Which subagent is asking, matching its `background_task.taskId`.
|
|
1215
|
+
...(foreign && requestThreadId ? { agentId: requestThreadId } : {}),
|
|
1091
1216
|
});
|
|
1092
1217
|
this.rpcResponse(id, { answers: buildCodexUserInputAnswers(questions, resp) });
|
|
1093
1218
|
}
|
|
1094
1219
|
catch {
|
|
1095
1220
|
this.rpcResponse(id, { answers: {} });
|
|
1096
1221
|
}
|
|
1097
|
-
|
|
1222
|
+
restore();
|
|
1098
1223
|
}
|
|
1099
1224
|
// -------------------------------------------------------------------------
|
|
1100
1225
|
// Notification handling (v2 format)
|
|
@@ -1175,9 +1300,11 @@ export class CodexSessionImpl {
|
|
|
1175
1300
|
* A completed spawn call only means the child was created. Register every
|
|
1176
1301
|
* receiver and then follow the child thread itself for the real terminus.
|
|
1177
1302
|
*/
|
|
1303
|
+
/** @returns whether the item produced at least one background-task edge. */
|
|
1178
1304
|
handleCollabAgentToolCall(item, raw, parentTaskId) {
|
|
1179
1305
|
if (this._state === "closed" || !this.ctx.onEvent)
|
|
1180
|
-
return;
|
|
1306
|
+
return false;
|
|
1307
|
+
let dispatched = false;
|
|
1181
1308
|
const tool = str(item, "tool");
|
|
1182
1309
|
const states = asObj(item, "agentsStates");
|
|
1183
1310
|
const receiverIds = stringArray(item["receiverThreadIds"]);
|
|
@@ -1186,7 +1313,7 @@ export class CodexSessionImpl {
|
|
|
1186
1313
|
const metadataOnly = tool === "resumeAgent" || tool === "sendInput";
|
|
1187
1314
|
const canStopWithoutState = tool === "closeAgent" && str(item, "status") === "completed";
|
|
1188
1315
|
for (const taskId of taskIds) {
|
|
1189
|
-
|
|
1316
|
+
const previous = this._backgroundTasks.get(taskId);
|
|
1190
1317
|
const description = str(item, "prompt") || previous?.description || null;
|
|
1191
1318
|
const resolvedParentTaskId = previous?.parentTaskId ?? parentTaskId;
|
|
1192
1319
|
// The child thread's own turn/started notification is the only
|
|
@@ -1205,6 +1332,7 @@ export class CodexSessionImpl {
|
|
|
1205
1332
|
: null,
|
|
1206
1333
|
raw,
|
|
1207
1334
|
}));
|
|
1335
|
+
dispatched = true;
|
|
1208
1336
|
}
|
|
1209
1337
|
continue;
|
|
1210
1338
|
}
|
|
@@ -1213,7 +1341,7 @@ export class CodexSessionImpl {
|
|
|
1213
1341
|
if (!canStart && !canStopWithoutState && !(taskId in states))
|
|
1214
1342
|
continue;
|
|
1215
1343
|
const hasState = taskId in states;
|
|
1216
|
-
const state = !hasState &&
|
|
1344
|
+
const state = !hasState && canStopWithoutState
|
|
1217
1345
|
? { terminal: true, status: "stopped", summary: null }
|
|
1218
1346
|
: codexAgentState(states[taskId]);
|
|
1219
1347
|
if (!previous) {
|
|
@@ -1226,7 +1354,7 @@ export class CodexSessionImpl {
|
|
|
1226
1354
|
: null,
|
|
1227
1355
|
raw,
|
|
1228
1356
|
}));
|
|
1229
|
-
|
|
1357
|
+
dispatched = true;
|
|
1230
1358
|
}
|
|
1231
1359
|
else if (previous.terminal) {
|
|
1232
1360
|
continue;
|
|
@@ -1243,6 +1371,7 @@ export class CodexSessionImpl {
|
|
|
1243
1371
|
: null,
|
|
1244
1372
|
raw,
|
|
1245
1373
|
}));
|
|
1374
|
+
dispatched = true;
|
|
1246
1375
|
}
|
|
1247
1376
|
if (state.terminal) {
|
|
1248
1377
|
this.dispatchEvent(this.backgroundTaskEvent(taskId, "completed", state.status, {
|
|
@@ -1254,11 +1383,10 @@ export class CodexSessionImpl {
|
|
|
1254
1383
|
: null,
|
|
1255
1384
|
raw,
|
|
1256
1385
|
}));
|
|
1257
|
-
|
|
1258
|
-
else {
|
|
1259
|
-
this.startBackgroundTaskPoller(taskId);
|
|
1386
|
+
dispatched = true;
|
|
1260
1387
|
}
|
|
1261
1388
|
}
|
|
1389
|
+
return dispatched;
|
|
1262
1390
|
}
|
|
1263
1391
|
waitForBackgroundTaskPoll(pollKey) {
|
|
1264
1392
|
if (this._state === "closed")
|
|
@@ -1408,19 +1536,40 @@ export class CodexSessionImpl {
|
|
|
1408
1536
|
event.description = description;
|
|
1409
1537
|
event.summary = summary;
|
|
1410
1538
|
event.parentTaskId = parentTaskId;
|
|
1539
|
+
const terminal = event.phase === "completed";
|
|
1411
1540
|
this._backgroundTasks.set(event.taskId, {
|
|
1412
1541
|
taskId: event.taskId,
|
|
1542
|
+
// `description` survives termination: the reactivation path in
|
|
1543
|
+
// handleForeignNotification reads it precisely BECAUSE the record is
|
|
1544
|
+
// terminal, and dropping it lost a reactivated child's label forever.
|
|
1545
|
+
// It is one bounded string per child. `summary` is the unbounded one —
|
|
1546
|
+
// full agent messages, appended per item — and has no reader that
|
|
1547
|
+
// outlives the task, so it is released.
|
|
1413
1548
|
description,
|
|
1414
|
-
summary,
|
|
1549
|
+
summary: terminal ? null : summary,
|
|
1415
1550
|
parentTaskId,
|
|
1416
|
-
terminal
|
|
1551
|
+
terminal,
|
|
1417
1552
|
generation: previous?.generation ?? 0,
|
|
1418
|
-
activeTurnId: event.
|
|
1419
|
-
? null
|
|
1420
|
-
: event.turnId ?? previous?.activeTurnId ?? null,
|
|
1553
|
+
activeTurnId: terminal ? null : event.turnId ?? previous?.activeTurnId ?? null,
|
|
1421
1554
|
});
|
|
1555
|
+
// Kept past termination too: a grandchild can spawn after its parent has
|
|
1556
|
+
// finished, and lineage resolves through this index.
|
|
1422
1557
|
if (description)
|
|
1423
1558
|
this._backgroundTaskIdsByPath.set(description, event.taskId);
|
|
1559
|
+
// Every registration route converges here, so this is the one place that
|
|
1560
|
+
// can guarantee a reconciliation poller exists. Registering by way of the
|
|
1561
|
+
// parser's subAgentActivity branch previously produced a task with no
|
|
1562
|
+
// safety net at all, wholly dependent on the child notification arriving.
|
|
1563
|
+
//
|
|
1564
|
+
// Correctness rests on `startBackgroundTaskPoller` and the poll loop both
|
|
1565
|
+
// re-reading the record and bailing when it is terminal — NOT on the
|
|
1566
|
+
// microtask. The deferral is an optimization: one wire item can emit
|
|
1567
|
+
// several edges in a tick (a collab spawn whose child is already finished
|
|
1568
|
+
// dispatches `started` then `completed`), and yielding first means the
|
|
1569
|
+
// common case declines before spawning a `thread/read` rather than after.
|
|
1570
|
+
// Do not remove the terminal checks on the grounds that this defers.
|
|
1571
|
+
if (!terminal)
|
|
1572
|
+
queueMicrotask(() => this.startBackgroundTaskPoller(event.taskId));
|
|
1424
1573
|
return true;
|
|
1425
1574
|
}
|
|
1426
1575
|
/**
|
|
@@ -1437,8 +1586,21 @@ export class CodexSessionImpl {
|
|
|
1437
1586
|
// App-server can publish child thread/started before the corresponding
|
|
1438
1587
|
// root subAgentActivity item. Register only descendants of this session,
|
|
1439
1588
|
// not unrelated foreign threads multiplexed by a future server version.
|
|
1440
|
-
if (parentThreadId !== rootThreadId && !parentTask)
|
|
1589
|
+
if (parentThreadId !== rootThreadId && !parentTask) {
|
|
1590
|
+
// Say so. A wire shape that omits parentThreadId takes this branch for
|
|
1591
|
+
// every child, which silently disables the whole registration and
|
|
1592
|
+
// reconciliation path — the safety net that exists precisely because
|
|
1593
|
+
// child notifications cannot be relied on. Degrading quietly here
|
|
1594
|
+
// hides the one failure this machinery was built to survive.
|
|
1595
|
+
if (!parentThreadId && this.ctx.onOutput) {
|
|
1596
|
+
try {
|
|
1597
|
+
void this.ctx.onOutput("stderr", `agentex: codex child thread ${childThreadId} announced no parent thread id; `
|
|
1598
|
+
+ "background-task tracking and reconciliation are disabled for it\n");
|
|
1599
|
+
}
|
|
1600
|
+
catch { /* swallow */ }
|
|
1601
|
+
}
|
|
1441
1602
|
return;
|
|
1603
|
+
}
|
|
1442
1604
|
const source = asObj(thread, "source");
|
|
1443
1605
|
const subAgent = Object.keys(asObj(source, "subAgent")).length > 0
|
|
1444
1606
|
? asObj(source, "subAgent")
|
|
@@ -1472,7 +1634,6 @@ export class CodexSessionImpl {
|
|
|
1472
1634
|
parentToolCallId: null,
|
|
1473
1635
|
raw: parseJson(rawLine) ?? params,
|
|
1474
1636
|
});
|
|
1475
|
-
this.startBackgroundTaskPoller(childThreadId);
|
|
1476
1637
|
return;
|
|
1477
1638
|
}
|
|
1478
1639
|
const task = this._backgroundTasks.get(childThreadId);
|
|
@@ -1507,7 +1668,6 @@ export class CodexSessionImpl {
|
|
|
1507
1668
|
parentToolCallId: null,
|
|
1508
1669
|
raw: parseJson(rawLine) ?? params,
|
|
1509
1670
|
});
|
|
1510
|
-
this.startBackgroundTaskPoller(childThreadId);
|
|
1511
1671
|
return;
|
|
1512
1672
|
}
|
|
1513
1673
|
if (method === "item/completed") {
|
|
@@ -1613,7 +1773,13 @@ export class CodexSessionImpl {
|
|
|
1613
1773
|
this.extractSummaryFromItem(params);
|
|
1614
1774
|
const item = asObj(params, "item");
|
|
1615
1775
|
if (str(item, "type") === "collabAgentToolCall") {
|
|
1616
|
-
|
|
1776
|
+
// Shapes we model become background_task edges. Anything that produced
|
|
1777
|
+
// no edge still reaches the host as a raw `unknown` event rather than
|
|
1778
|
+
// vanishing — that escape hatch is how unmodeled wire shapes stay
|
|
1779
|
+
// visible, and collab is exactly where new shapes keep appearing.
|
|
1780
|
+
if (!this.handleCollabAgentToolCall(item, parseJson(rawLine) ?? params, null)) {
|
|
1781
|
+
this.emitStreamEvent(rawLine);
|
|
1782
|
+
}
|
|
1617
1783
|
return;
|
|
1618
1784
|
}
|
|
1619
1785
|
this.emitStreamEvent(rawLine);
|
|
@@ -1785,10 +1951,35 @@ export class CodexSessionImpl {
|
|
|
1785
1951
|
this.resolveTurn();
|
|
1786
1952
|
}
|
|
1787
1953
|
resolveTurn() {
|
|
1954
|
+
// Bind delivery to the turn that just terminated. Everything below this
|
|
1955
|
+
// point can yield (usage scan, event chain), and a send() arriving in the
|
|
1956
|
+
// meantime starts a new turn that must not be settled by this result.
|
|
1957
|
+
const delivering = this._activeTurnReady;
|
|
1788
1958
|
const resolvedModel = this._turnModel ?? this.model;
|
|
1789
1959
|
let usage = this._turnUsage && resolvedModel
|
|
1790
1960
|
? { [resolvedModel]: { inputTokens: this._turnUsage.inputTokens, outputTokens: this._turnUsage.outputTokens } }
|
|
1791
1961
|
: undefined;
|
|
1962
|
+
// Freeze this turn's outcome and clear the accumulators NOW, while we are
|
|
1963
|
+
// still synchronous with the terminal frame. Delivery reads the snapshot,
|
|
1964
|
+
// so a turn that starts during the async gap below neither inherits these
|
|
1965
|
+
// values nor has its own overwritten. Resetting inside beginActiveTurn()
|
|
1966
|
+
// instead would not work: the usage-scan path re-reads state after an
|
|
1967
|
+
// await, so the new turn's reset would eat the old turn's summary.
|
|
1968
|
+
const outcome = {
|
|
1969
|
+
summary: this._turnSummary,
|
|
1970
|
+
isError: this._turnIsError,
|
|
1971
|
+
errorMessage: this._turnErrorMessage,
|
|
1972
|
+
wasInterrupted: this._turnWasInterrupted,
|
|
1973
|
+
};
|
|
1974
|
+
this._turnSummary = null;
|
|
1975
|
+
this._turnUsage = null;
|
|
1976
|
+
this._turnModel = null;
|
|
1977
|
+
this._turnIsError = false;
|
|
1978
|
+
this._turnWasInterrupted = false;
|
|
1979
|
+
this._turnErrorMessage = null;
|
|
1980
|
+
// Captured before clearing: the usage-scan fallback below still needs it.
|
|
1981
|
+
const turnStartedAt = this._turnStartedAt;
|
|
1982
|
+
this._turnStartedAt = null;
|
|
1792
1983
|
// Usage precedence: the `turn.completed` payload is authoritative when
|
|
1793
1984
|
// present (captured above into _turnUsage). Only when the stream carried no
|
|
1794
1985
|
// usage do we fall back to scanning Codex's on-disk session logs.
|
|
@@ -1798,8 +1989,8 @@ export class CodexSessionImpl {
|
|
|
1798
1989
|
// latest turn or read partially-written totals. We therefore scan ONLY when
|
|
1799
1990
|
// there is no in-band usage, and never let a scan failure fail the turn
|
|
1800
1991
|
// (usage simply stays undefined). Prefer the in-band payload always.
|
|
1801
|
-
if (!usage &&
|
|
1802
|
-
const startedAt =
|
|
1992
|
+
if (!usage && turnStartedAt) {
|
|
1993
|
+
const startedAt = turnStartedAt;
|
|
1803
1994
|
const threadId = this._threadId ?? undefined;
|
|
1804
1995
|
// Fire-and-forget: scan logs then deliver result
|
|
1805
1996
|
void scanCodexSessionUsage({ startedAfter: startedAt, threadId }).then((scanned) => {
|
|
@@ -1807,20 +1998,20 @@ export class CodexSessionImpl {
|
|
|
1807
1998
|
}).catch(() => {
|
|
1808
1999
|
// Non-fatal — usage stays undefined
|
|
1809
2000
|
}).finally(() => {
|
|
1810
|
-
void this.deliverTurnResult(usage);
|
|
2001
|
+
void this.deliverTurnResult(usage, delivering, outcome);
|
|
1811
2002
|
});
|
|
1812
2003
|
return;
|
|
1813
2004
|
}
|
|
1814
|
-
void this.deliverTurnResult(usage);
|
|
2005
|
+
void this.deliverTurnResult(usage, delivering, outcome);
|
|
1815
2006
|
}
|
|
1816
|
-
async deliverTurnResult(usage) {
|
|
2007
|
+
async deliverTurnResult(usage, delivering, outcome) {
|
|
1817
2008
|
const result = {
|
|
1818
|
-
summary:
|
|
2009
|
+
summary: outcome.summary,
|
|
1819
2010
|
usage,
|
|
1820
2011
|
costUsd: null,
|
|
1821
|
-
status:
|
|
1822
|
-
errorCode:
|
|
1823
|
-
errorMessage:
|
|
2012
|
+
status: outcome.wasInterrupted ? "aborted" : outcome.isError ? "failed" : "completed",
|
|
2013
|
+
errorCode: outcome.wasInterrupted ? "aborted" : outcome.isError ? "execution_error" : null,
|
|
2014
|
+
errorMessage: outcome.wasInterrupted ? "Turn was interrupted" : outcome.errorMessage,
|
|
1824
2015
|
};
|
|
1825
2016
|
// Drain pending onEvent handlers so callers awaiting send() see a settled
|
|
1826
2017
|
// DB / log / UI state by the time TurnResult resolves. The chain snapshot
|
|
@@ -1834,20 +2025,27 @@ export class CodexSessionImpl {
|
|
|
1834
2025
|
// session whose stdin is dead.
|
|
1835
2026
|
if (this._state === "closed")
|
|
1836
2027
|
return;
|
|
1837
|
-
this
|
|
1838
|
-
//
|
|
1839
|
-
//
|
|
1840
|
-
|
|
1841
|
-
//
|
|
1842
|
-
//
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
this.
|
|
1849
|
-
this.
|
|
1850
|
-
|
|
2028
|
+
// A send() that landed while this delivery was suspended has already begun
|
|
2029
|
+
// a new turn and owns the session. Its result is still owed to it, so this
|
|
2030
|
+
// delivery settles only its OWN generation and touches neither the state
|
|
2031
|
+
// nor the interrupt latch the new turn needs to stay stoppable.
|
|
2032
|
+
//
|
|
2033
|
+
// `delivering === null` must take this branch too. A duplicate terminal
|
|
2034
|
+
// frame for an already-delivered turn (codex can report failure through
|
|
2035
|
+
// both `turn/completed` with a failed status and a separate `turn/failed`)
|
|
2036
|
+
// reaches here with a null latch, and exempting it restored every symptom
|
|
2037
|
+
// the fence exists to prevent: silent Stop, idle-while-running, and a
|
|
2038
|
+
// stale settle.
|
|
2039
|
+
const superseded = this._activeTurnReady !== delivering;
|
|
2040
|
+
// Drain the pending send() resolvers belonging to this turn. Several
|
|
2041
|
+
// concurrent sends coalesced into one turn share the same TurnResult; a
|
|
2042
|
+
// send from a later turn is left in place for its own delivery.
|
|
2043
|
+
const pending = this.takePendingResults(delivering?.generation);
|
|
2044
|
+
if (!superseded) {
|
|
2045
|
+
this._state = "idle";
|
|
2046
|
+
// Accumulators were already snapshotted and cleared in resolveTurn().
|
|
2047
|
+
this.clearActiveTurn(undefined, delivering ?? undefined);
|
|
2048
|
+
}
|
|
1851
2049
|
for (const p of pending) {
|
|
1852
2050
|
// Skip sends already settled early by timeout / abort.
|
|
1853
2051
|
if (p.settled)
|
|
@@ -1867,9 +2065,9 @@ export class CodexSessionImpl {
|
|
|
1867
2065
|
// Pass current threadId so NDJSON-shaped events (via codex/event wrapper)
|
|
1868
2066
|
// carry sessionId. v2 notifications parse threadId from params directly
|
|
1869
2067
|
// and ignore this arg.
|
|
1870
|
-
const event
|
|
1871
|
-
if (event)
|
|
2068
|
+
for (const event of parseCodexStreamLines(rawLine, this._threadId)) {
|
|
1872
2069
|
this.dispatchEvent(event);
|
|
2070
|
+
}
|
|
1873
2071
|
}
|
|
1874
2072
|
/**
|
|
1875
2073
|
* Queue an event for in-order delivery to `onEvent`. Each call appends a
|