@nyssance/codex-acp-v2 0.3.3 → 0.3.5
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 +41 -0
- package/dist/index.js +1302 -395
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -21721,7 +21721,7 @@ var package_default = {
|
|
|
21721
21721
|
publishConfig: {
|
|
21722
21722
|
access: "public"
|
|
21723
21723
|
},
|
|
21724
|
-
version: "0.3.
|
|
21724
|
+
version: "0.3.5",
|
|
21725
21725
|
description: "Native ACP v2 agent for the OpenAI Codex app-server",
|
|
21726
21726
|
packageManager: "bun@1.4.0",
|
|
21727
21727
|
engines: {
|
|
@@ -21733,6 +21733,7 @@ var package_default = {
|
|
|
21733
21733
|
files: [
|
|
21734
21734
|
"dist/index.js",
|
|
21735
21735
|
"README.md",
|
|
21736
|
+
"CHANGELOG.md",
|
|
21736
21737
|
"LICENSE",
|
|
21737
21738
|
"package.json"
|
|
21738
21739
|
],
|
|
@@ -21758,7 +21759,9 @@ var package_default = {
|
|
|
21758
21759
|
test: "vitest run --exclude src/__tests__/e2e.test.ts",
|
|
21759
21760
|
"test:watch": "vitest",
|
|
21760
21761
|
typecheck: "tsc --noEmit",
|
|
21761
|
-
"test:e2e": "RUN_E2E_TESTS=true vitest run src/__tests__/e2e.test.ts"
|
|
21762
|
+
"test:e2e": "RUN_E2E_TESTS=true vitest run src/__tests__/e2e.test.ts",
|
|
21763
|
+
"test:benchmark": "RUN_BENCHMARKS=true vitest run src/__tests__/benchmark.test.ts",
|
|
21764
|
+
"test:soak": "bun --expose-gc src/benchmarks/bridge-soak.ts"
|
|
21762
21765
|
},
|
|
21763
21766
|
homepage: "https://github.com/nyssance/codex-acp-v2#readme",
|
|
21764
21767
|
bugs: {
|
|
@@ -21794,6 +21797,46 @@ var package_default = {
|
|
|
21794
21797
|
}
|
|
21795
21798
|
};
|
|
21796
21799
|
|
|
21800
|
+
// src/agent/turnErrors.ts
|
|
21801
|
+
function classifyTurnError(info) {
|
|
21802
|
+
const failure = (category, retryable = false, stopReason = "_error") => ({ category, retryable, stopReason });
|
|
21803
|
+
if (info === null)
|
|
21804
|
+
return failure("unknown");
|
|
21805
|
+
if (typeof info === "object") {
|
|
21806
|
+
if ("activeTurnNotSteerable" in info)
|
|
21807
|
+
return failure("turn_not_steerable");
|
|
21808
|
+
const status = "httpConnectionFailed" in info ? info.httpConnectionFailed.httpStatusCode : ("responseStreamConnectionFailed" in info) ? info.responseStreamConnectionFailed.httpStatusCode : ("responseStreamDisconnected" in info) ? info.responseStreamDisconnected.httpStatusCode : info.responseTooManyFailedAttempts.httpStatusCode;
|
|
21809
|
+
return failure("connection", status === null || status === 408 || status === 429 || status >= 500);
|
|
21810
|
+
}
|
|
21811
|
+
switch (info) {
|
|
21812
|
+
case "contextWindowExceeded":
|
|
21813
|
+
return failure("context_window", false, "max_tokens");
|
|
21814
|
+
case "cyberPolicy":
|
|
21815
|
+
case "misalignmentPolicyViolation":
|
|
21816
|
+
return failure("policy", false, "refusal");
|
|
21817
|
+
case "sessionBudgetExceeded":
|
|
21818
|
+
return failure("budget");
|
|
21819
|
+
case "usageLimitExceeded":
|
|
21820
|
+
return failure("usage_limit");
|
|
21821
|
+
case "rateLimitExceeded":
|
|
21822
|
+
return failure("rate_limit", true);
|
|
21823
|
+
case "serverOverloaded":
|
|
21824
|
+
return failure("overloaded", true);
|
|
21825
|
+
case "internalServerError":
|
|
21826
|
+
return failure("internal", true);
|
|
21827
|
+
case "unauthorized":
|
|
21828
|
+
return failure("authentication");
|
|
21829
|
+
case "badRequest":
|
|
21830
|
+
return failure("invalid_request");
|
|
21831
|
+
case "threadRollbackFailed":
|
|
21832
|
+
return failure("rollback");
|
|
21833
|
+
case "sandboxError":
|
|
21834
|
+
return failure("sandbox");
|
|
21835
|
+
case "other":
|
|
21836
|
+
return failure("unknown");
|
|
21837
|
+
}
|
|
21838
|
+
}
|
|
21839
|
+
|
|
21797
21840
|
// src/agent/CodexAgent.ts
|
|
21798
21841
|
import path6 from "path";
|
|
21799
21842
|
|
|
@@ -22756,9 +22799,6 @@ function toAcpStatus(status) {
|
|
|
22756
22799
|
return "cancelled";
|
|
22757
22800
|
}
|
|
22758
22801
|
}
|
|
22759
|
-
function toolCallCancelled(toolCallId) {
|
|
22760
|
-
return frame({ toolCallId, status: "cancelled" });
|
|
22761
|
-
}
|
|
22762
22802
|
function frame(fields) {
|
|
22763
22803
|
return { sessionUpdate: "tool_call_update", ...fields };
|
|
22764
22804
|
}
|
|
@@ -22847,14 +22887,17 @@ function commandStarted(item) {
|
|
|
22847
22887
|
});
|
|
22848
22888
|
}
|
|
22849
22889
|
function commandCompleted(item) {
|
|
22890
|
+
const output = item.aggregatedOutput ?? "";
|
|
22891
|
+
const truncated = output.length > 16384;
|
|
22850
22892
|
return frame({
|
|
22851
22893
|
toolCallId: item.id,
|
|
22852
22894
|
status: item.status === "completed" ? "completed" : "failed",
|
|
22853
22895
|
rawOutput: {
|
|
22854
|
-
output:
|
|
22896
|
+
output: truncated ? output.slice(0, 16384).replace(/[\uD800-\uDBFF]$/, "") : output,
|
|
22855
22897
|
exitCode: item.exitCode,
|
|
22856
22898
|
durationMs: item.durationMs
|
|
22857
22899
|
},
|
|
22900
|
+
...truncated ? { _meta: { codex: { outputTruncated: true, outputCharacters: output.length, ...usesTerminal(item) ? { terminalId: item.id } : {} } } } : {},
|
|
22858
22901
|
...usesTerminal(item) || !item.aggregatedOutput ? {} : { content: [textContent(item.aggregatedOutput)] }
|
|
22859
22902
|
});
|
|
22860
22903
|
}
|
|
@@ -23071,8 +23114,8 @@ function fuzzySearchUpdated(event, create) {
|
|
|
23071
23114
|
rawInput: { query: event.query }
|
|
23072
23115
|
});
|
|
23073
23116
|
}
|
|
23074
|
-
function fuzzySearchCompleted(event) {
|
|
23075
|
-
return frame({ toolCallId: fuzzySearchToolCallId(event.sessionId), status: "completed" });
|
|
23117
|
+
function fuzzySearchCompleted(event, create = false) {
|
|
23118
|
+
return frame({ toolCallId: fuzzySearchToolCallId(event.sessionId), ...create ? { name: ToolName.FuzzySearch, title: "File search", kind: "search" } : {}, status: "completed" });
|
|
23076
23119
|
}
|
|
23077
23120
|
function guardianReviewToolCallId(reviewId) {
|
|
23078
23121
|
return `guardian-review:${reviewId}`;
|
|
@@ -23143,6 +23186,7 @@ function mcpStartupFailed(serverName, message) {
|
|
|
23143
23186
|
}
|
|
23144
23187
|
|
|
23145
23188
|
// src/codex/sessionConfig.ts
|
|
23189
|
+
import { pathToFileURL } from "url";
|
|
23146
23190
|
import path3 from "path";
|
|
23147
23191
|
var MCP_SERVER_NAME_WHITESPACE = /\p{White_Space}/gu;
|
|
23148
23192
|
function sanitizeMcpServerName(name) {
|
|
@@ -23263,7 +23307,7 @@ function fromUserInput(input) {
|
|
|
23263
23307
|
}
|
|
23264
23308
|
}
|
|
23265
23309
|
function toFileUri(value) {
|
|
23266
|
-
return value.startsWith("file://") ? value :
|
|
23310
|
+
return value.startsWith("file://") ? value : pathToFileURL(value).href;
|
|
23267
23311
|
}
|
|
23268
23312
|
function isSupportedImageUrl(uri) {
|
|
23269
23313
|
if (!uri)
|
|
@@ -23295,6 +23339,65 @@ function promptTitle(prompt) {
|
|
|
23295
23339
|
return null;
|
|
23296
23340
|
}
|
|
23297
23341
|
|
|
23342
|
+
// src/bridge/itemSnapshot.ts
|
|
23343
|
+
function itemSnapshot(item) {
|
|
23344
|
+
switch (item.type) {
|
|
23345
|
+
case "userMessage": {
|
|
23346
|
+
const content = item.content.flatMap(fromUserInput);
|
|
23347
|
+
return content.length > 0 ? [{ sessionUpdate: "user_message", messageId: item.id, content }] : [];
|
|
23348
|
+
}
|
|
23349
|
+
case "agentMessage":
|
|
23350
|
+
return item.text.length > 0 ? [{
|
|
23351
|
+
sessionUpdate: "agent_message",
|
|
23352
|
+
messageId: item.id,
|
|
23353
|
+
content: [{ type: "text", text: item.text }],
|
|
23354
|
+
...item.phase ? { _meta: { codex: { phase: item.phase } } } : {}
|
|
23355
|
+
}] : [];
|
|
23356
|
+
case "reasoning": {
|
|
23357
|
+
const parts = (item.summary.length > 0 ? item.summary : item.content).filter((part) => part.length > 0);
|
|
23358
|
+
return parts.length > 0 ? [{ sessionUpdate: "agent_thought", messageId: item.id, content: parts.map((text) => ({ type: "text", text })) }] : [];
|
|
23359
|
+
}
|
|
23360
|
+
case "plan":
|
|
23361
|
+
return item.text.length > 0 ? [{ sessionUpdate: "plan_update", plan: { type: "markdown", planId: item.id, content: item.text } }] : [];
|
|
23362
|
+
case "fileChange":
|
|
23363
|
+
return [fileChangeStarted(item)];
|
|
23364
|
+
case "commandExecution": {
|
|
23365
|
+
const updates = [commandStarted(item)];
|
|
23366
|
+
if (usesTerminal(item))
|
|
23367
|
+
updates.unshift(terminalSnapshot(item));
|
|
23368
|
+
if (item.status !== "inProgress")
|
|
23369
|
+
updates.push(commandCompleted(item));
|
|
23370
|
+
return updates;
|
|
23371
|
+
}
|
|
23372
|
+
case "mcpToolCall":
|
|
23373
|
+
return [mcpToolCallStarted(item)];
|
|
23374
|
+
case "dynamicToolCall":
|
|
23375
|
+
return [{ ...dynamicToolCallStarted(item), ...dynamicToolCallCompleted(item) }];
|
|
23376
|
+
case "webSearch":
|
|
23377
|
+
return [webSearchSnapshot(item)];
|
|
23378
|
+
case "imageView":
|
|
23379
|
+
return [imageViewed(item)];
|
|
23380
|
+
case "imageGeneration":
|
|
23381
|
+
return [imageGenerationSnapshot(item)];
|
|
23382
|
+
case "collabAgentToolCall":
|
|
23383
|
+
return [collabToolCall(item, true)];
|
|
23384
|
+
case "subAgentActivity":
|
|
23385
|
+
return [subAgentActivity(item, "completed", true)];
|
|
23386
|
+
case "contextCompaction":
|
|
23387
|
+
return [compactionUpdate(item.id, "completed")];
|
|
23388
|
+
case "enteredReviewMode":
|
|
23389
|
+
return [];
|
|
23390
|
+
case "exitedReviewMode": {
|
|
23391
|
+
const text = item.review.trim();
|
|
23392
|
+
return text.length > 0 ? [{ sessionUpdate: "agent_message", messageId: item.id, content: [{ type: "text", text }] }] : [];
|
|
23393
|
+
}
|
|
23394
|
+
case "hookPrompt":
|
|
23395
|
+
case "functionCallOutput":
|
|
23396
|
+
case "sleep":
|
|
23397
|
+
return [];
|
|
23398
|
+
}
|
|
23399
|
+
}
|
|
23400
|
+
|
|
23298
23401
|
// src/bridge/EventBridge.ts
|
|
23299
23402
|
var TURN_PLAN_ID = "codex-turn-plan";
|
|
23300
23403
|
var PLAN_STREAM_INTERVAL_MS = 150;
|
|
@@ -23307,6 +23410,7 @@ class EventBridge {
|
|
|
23307
23410
|
completedPlan = null;
|
|
23308
23411
|
noticeSequence = 0;
|
|
23309
23412
|
openToolCalls = new Set;
|
|
23413
|
+
announcedTools = new Set;
|
|
23310
23414
|
messagePhases = new Map;
|
|
23311
23415
|
reasoningWithDeltas = new Set;
|
|
23312
23416
|
terminalItems = new Set;
|
|
@@ -23327,6 +23431,14 @@ class EventBridge {
|
|
|
23327
23431
|
this.lastError = null;
|
|
23328
23432
|
this.completedPlan = null;
|
|
23329
23433
|
this.openToolCalls.clear();
|
|
23434
|
+
this.announcedTools.clear();
|
|
23435
|
+
this.messagePhases.clear();
|
|
23436
|
+
this.reasoningWithDeltas.clear();
|
|
23437
|
+
this.terminalItems.clear();
|
|
23438
|
+
this.imageGenerations.clear();
|
|
23439
|
+
this.subAgentActivities.clear();
|
|
23440
|
+
this.fuzzySearches.clear();
|
|
23441
|
+
this.guardianReviews.clear();
|
|
23330
23442
|
this.clearPlanState();
|
|
23331
23443
|
}
|
|
23332
23444
|
takeError() {
|
|
@@ -23339,24 +23451,47 @@ class EventBridge {
|
|
|
23339
23451
|
this.completedPlan = null;
|
|
23340
23452
|
return plan;
|
|
23341
23453
|
}
|
|
23454
|
+
async restore(items) {
|
|
23455
|
+
for (const item of items) {
|
|
23456
|
+
if (item.type === "commandExecution" && item.status === "inProgress" && usesTerminal(item))
|
|
23457
|
+
this.terminalItems.add(item.id);
|
|
23458
|
+
for (const update of itemSnapshot(item)) {
|
|
23459
|
+
this.trackToolCall(update);
|
|
23460
|
+
await this.client.update(update);
|
|
23461
|
+
}
|
|
23462
|
+
}
|
|
23463
|
+
}
|
|
23342
23464
|
async handle(notification) {
|
|
23343
|
-
|
|
23465
|
+
let updates = await this.translate(notification);
|
|
23466
|
+
if (notification.method === "item/completed") {
|
|
23467
|
+
const snapshots = itemSnapshot(notification.params.item);
|
|
23468
|
+
const first = snapshots.find((update) => update.sessionUpdate === "tool_call_update");
|
|
23469
|
+
if (first?.sessionUpdate === "tool_call_update" && !this.announcedTools.has(first.toolCallId)) {
|
|
23470
|
+
updates = snapshots;
|
|
23471
|
+
}
|
|
23472
|
+
}
|
|
23344
23473
|
for (const update of updates) {
|
|
23345
23474
|
this.trackToolCall(update);
|
|
23346
23475
|
await this.client.update(update);
|
|
23347
23476
|
}
|
|
23348
23477
|
}
|
|
23349
|
-
async
|
|
23478
|
+
async finishOpenToolCalls(status) {
|
|
23350
23479
|
const ids = [...this.openToolCalls];
|
|
23480
|
+
const terminals = [...this.terminalItems];
|
|
23351
23481
|
this.openToolCalls.clear();
|
|
23482
|
+
this.terminalItems.clear();
|
|
23483
|
+
for (const terminalId of terminals) {
|
|
23484
|
+
await this.client.update({ sessionUpdate: "terminal_update", terminalId, exitStatus: { exitCode: null, signal: null }, _meta: { codex: { reconciled: true, turnOutcome: status } } });
|
|
23485
|
+
}
|
|
23352
23486
|
for (const toolCallId of ids) {
|
|
23353
|
-
await this.client.update(
|
|
23487
|
+
await this.client.update({ sessionUpdate: "tool_call_update", toolCallId, status, _meta: { codex: { reconciled: true } } });
|
|
23354
23488
|
}
|
|
23355
23489
|
}
|
|
23356
23490
|
trackToolCall(update) {
|
|
23357
23491
|
if (update.sessionUpdate !== "tool_call_update")
|
|
23358
23492
|
return;
|
|
23359
23493
|
const { toolCallId, status } = update;
|
|
23494
|
+
this.announcedTools.add(toolCallId);
|
|
23360
23495
|
if (status === "pending" || status === "in_progress")
|
|
23361
23496
|
this.openToolCalls.add(toolCallId);
|
|
23362
23497
|
else if (status !== undefined && status !== null)
|
|
@@ -23366,7 +23501,7 @@ class EventBridge {
|
|
|
23366
23501
|
await this.flushPlans();
|
|
23367
23502
|
}
|
|
23368
23503
|
dispose() {
|
|
23369
|
-
this.
|
|
23504
|
+
this.beginTurn();
|
|
23370
23505
|
}
|
|
23371
23506
|
async translate(notification) {
|
|
23372
23507
|
switch (notification.method) {
|
|
@@ -23398,14 +23533,17 @@ ${stdin}
|
|
|
23398
23533
|
`)] : [];
|
|
23399
23534
|
}
|
|
23400
23535
|
case "item/fileChange/patchUpdated":
|
|
23401
|
-
return [fileChangePatched({
|
|
23536
|
+
return [(this.announcedTools.has(notification.params.itemId) ? fileChangePatched : fileChangeStarted)({
|
|
23402
23537
|
type: "fileChange",
|
|
23403
23538
|
id: notification.params.itemId,
|
|
23404
23539
|
changes: notification.params.changes,
|
|
23405
23540
|
status: "inProgress"
|
|
23406
23541
|
})];
|
|
23407
23542
|
case "item/mcpToolCall/progress":
|
|
23408
|
-
return [
|
|
23543
|
+
return [{
|
|
23544
|
+
...!this.announcedTools.has(notification.params.itemId) ? { name: ToolName.Mcp, title: "MCP tool", kind: "execute", status: "in_progress" } : {},
|
|
23545
|
+
...mcpToolCallProgress(notification.params.itemId, notification.params.message)
|
|
23546
|
+
}];
|
|
23409
23547
|
case "item/plan/delta":
|
|
23410
23548
|
this.planDelta(notification.params.itemId, notification.params.delta);
|
|
23411
23549
|
return [];
|
|
@@ -23427,9 +23565,11 @@ ${stdin}
|
|
|
23427
23565
|
this.fuzzySearches.add(id);
|
|
23428
23566
|
return [fuzzySearchUpdated(notification.params, create)];
|
|
23429
23567
|
}
|
|
23430
|
-
case "fuzzyFileSearch/sessionCompleted":
|
|
23568
|
+
case "fuzzyFileSearch/sessionCompleted": {
|
|
23569
|
+
const create = !this.fuzzySearches.has(notification.params.sessionId);
|
|
23431
23570
|
this.fuzzySearches.delete(notification.params.sessionId);
|
|
23432
|
-
return [fuzzySearchCompleted(notification.params)];
|
|
23571
|
+
return [fuzzySearchCompleted(notification.params, create)];
|
|
23572
|
+
}
|
|
23433
23573
|
case "thread/tokenUsage/updated": {
|
|
23434
23574
|
const usage = notification.params.tokenUsage;
|
|
23435
23575
|
this.session.lastUsage = toTokenCount(usage.last);
|
|
@@ -23538,7 +23678,7 @@ ${details}` : summary)];
|
|
|
23538
23678
|
case "commandExecution":
|
|
23539
23679
|
if (usesTerminal(item)) {
|
|
23540
23680
|
this.terminalItems.add(item.id);
|
|
23541
|
-
return [
|
|
23681
|
+
return [terminalStarted(item), commandStarted(item)];
|
|
23542
23682
|
}
|
|
23543
23683
|
return [commandStarted(item)];
|
|
23544
23684
|
case "mcpToolCall":
|
|
@@ -23577,7 +23717,7 @@ ${details}` : summary)];
|
|
|
23577
23717
|
switch (item.type) {
|
|
23578
23718
|
case "agentMessage":
|
|
23579
23719
|
this.messagePhases.delete(item.id);
|
|
23580
|
-
return
|
|
23720
|
+
return itemSnapshot(item);
|
|
23581
23721
|
case "reasoning": {
|
|
23582
23722
|
if (this.reasoningWithDeltas.delete(item.id))
|
|
23583
23723
|
return [];
|
|
@@ -24749,6 +24889,29 @@ class TurnContext {
|
|
|
24749
24889
|
}
|
|
24750
24890
|
}
|
|
24751
24891
|
|
|
24892
|
+
// src/util/abort.ts
|
|
24893
|
+
function abortable(operation, signal) {
|
|
24894
|
+
if (signal.aborted) {
|
|
24895
|
+
operation.catch(() => {});
|
|
24896
|
+
return Promise.reject(signal.reason);
|
|
24897
|
+
}
|
|
24898
|
+
return new Promise((resolve, reject) => {
|
|
24899
|
+
const abort = () => {
|
|
24900
|
+
cleanup();
|
|
24901
|
+
reject(signal.reason);
|
|
24902
|
+
};
|
|
24903
|
+
const cleanup = () => signal.removeEventListener("abort", abort);
|
|
24904
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
24905
|
+
operation.then((value) => {
|
|
24906
|
+
cleanup();
|
|
24907
|
+
resolve(value);
|
|
24908
|
+
}, (error40) => {
|
|
24909
|
+
cleanup();
|
|
24910
|
+
reject(error40);
|
|
24911
|
+
});
|
|
24912
|
+
});
|
|
24913
|
+
}
|
|
24914
|
+
|
|
24752
24915
|
// node_modules/open/index.js
|
|
24753
24916
|
import process10 from "process";
|
|
24754
24917
|
import path5 from "path";
|
|
@@ -25470,63 +25633,63 @@ async function loginWithApiKey(codex, request, env) {
|
|
|
25470
25633
|
if (!apiKey) {
|
|
25471
25634
|
throw RequestError.invalidParams({ envVars: [CODEX_API_KEY_ENV, OPENAI_API_KEY_ENV] }, `No API key: set ${CODEX_API_KEY_ENV} or ${OPENAI_API_KEY_ENV}, or pass _meta["api-key"].apiKey`);
|
|
25472
25635
|
}
|
|
25473
|
-
|
|
25474
|
-
|
|
25475
|
-
|
|
25476
|
-
|
|
25477
|
-
|
|
25636
|
+
await codex.withNotification("account/login/completed", async (completed) => {
|
|
25637
|
+
await codex.accountLogin({ type: "apiKey", apiKey });
|
|
25638
|
+
const result = await completed;
|
|
25639
|
+
if (!result.success)
|
|
25640
|
+
throw RequestError.authRequired({ error: result.error }, result.error ?? "API key login failed");
|
|
25641
|
+
});
|
|
25478
25642
|
}
|
|
25479
25643
|
async function loginWithChatGpt(codex) {
|
|
25480
25644
|
const account = await codex.accountRead({ refreshToken: true });
|
|
25481
25645
|
if (account.account?.type === "chatgpt")
|
|
25482
25646
|
return;
|
|
25483
|
-
|
|
25484
|
-
|
|
25485
|
-
|
|
25486
|
-
|
|
25487
|
-
|
|
25488
|
-
|
|
25489
|
-
|
|
25490
|
-
|
|
25491
|
-
|
|
25647
|
+
await codex.withNotification("account/login/completed", async (completed) => {
|
|
25648
|
+
const started = await codex.accountLogin({ type: "chatgpt" });
|
|
25649
|
+
if (started.type !== "chatgpt")
|
|
25650
|
+
throw RequestError.internalError({ type: started.type }, "Unexpected login response");
|
|
25651
|
+
logger.log("opening browser for ChatGPT login", { loginId: started.loginId });
|
|
25652
|
+
await open_default(started.authUrl);
|
|
25653
|
+
const result = await completed;
|
|
25654
|
+
if (!result.success)
|
|
25655
|
+
throw RequestError.authRequired({ error: result.error }, result.error ?? "ChatGPT login failed");
|
|
25656
|
+
});
|
|
25492
25657
|
}
|
|
25493
25658
|
async function loginWithDeviceCode(codex, link, requestId) {
|
|
25494
25659
|
const account = await codex.accountRead({ refreshToken: true });
|
|
25495
25660
|
if (account.account?.type === "chatgpt")
|
|
25496
25661
|
return;
|
|
25497
|
-
|
|
25498
|
-
|
|
25499
|
-
|
|
25500
|
-
|
|
25501
|
-
|
|
25502
|
-
|
|
25503
|
-
|
|
25504
|
-
|
|
25505
|
-
|
|
25506
|
-
|
|
25507
|
-
|
|
25508
|
-
|
|
25509
|
-
|
|
25510
|
-
|
|
25511
|
-
|
|
25512
|
-
|
|
25513
|
-
|
|
25514
|
-
|
|
25515
|
-
|
|
25516
|
-
|
|
25517
|
-
|
|
25518
|
-
|
|
25519
|
-
|
|
25520
|
-
|
|
25662
|
+
await codex.withNotification("account/login/completed", async (completed) => {
|
|
25663
|
+
const started = await codex.accountLogin({ type: "chatgptDeviceCode" });
|
|
25664
|
+
if (started.type !== "chatgptDeviceCode")
|
|
25665
|
+
throw RequestError.internalError({ type: started.type }, "Unexpected login response");
|
|
25666
|
+
const elicitation = link.request(methods.client.elicitation.create, {
|
|
25667
|
+
mode: "url",
|
|
25668
|
+
requestId,
|
|
25669
|
+
elicitationId: started.loginId,
|
|
25670
|
+
url: started.verificationUrl,
|
|
25671
|
+
message: `Sign in to ChatGPT and enter this code: ${started.userCode}`
|
|
25672
|
+
});
|
|
25673
|
+
elicitation.catch(() => {});
|
|
25674
|
+
const first = await Promise.race([
|
|
25675
|
+
completed.then((result2) => ({ kind: "completed", result: result2 })),
|
|
25676
|
+
elicitation.then((response) => ({ kind: "elicitation", response }))
|
|
25677
|
+
]);
|
|
25678
|
+
if (first.kind === "elicitation" && !CreateElicitationResponse.isAccept(first.response)) {
|
|
25679
|
+
await codex.accountLoginCancel({ loginId: started.loginId });
|
|
25680
|
+
throw RequestError.authRequired(undefined, "ChatGPT device code login was cancelled");
|
|
25681
|
+
}
|
|
25682
|
+
const result = first.kind === "completed" ? first.result : await completed;
|
|
25683
|
+
await link.notify(methods.client.elicitation.complete, { elicitationId: started.loginId });
|
|
25684
|
+
if (!result.success)
|
|
25685
|
+
throw RequestError.authRequired({ error: result.error }, result.error ?? "ChatGPT login failed");
|
|
25686
|
+
});
|
|
25521
25687
|
}
|
|
25522
25688
|
async function logout(codex) {
|
|
25523
|
-
|
|
25524
|
-
|
|
25525
|
-
|
|
25526
|
-
}
|
|
25527
|
-
async function authRequired(codex) {
|
|
25528
|
-
const account = await codex.accountRead({ refreshToken: false });
|
|
25529
|
-
return account.requiresOpenaiAuth && account.account === null;
|
|
25689
|
+
await codex.withNotification("account/updated", async (updated) => {
|
|
25690
|
+
await codex.accountLogout();
|
|
25691
|
+
await updated;
|
|
25692
|
+
});
|
|
25530
25693
|
}
|
|
25531
25694
|
|
|
25532
25695
|
// src/agent/clientSession.ts
|
|
@@ -25536,33 +25699,61 @@ class ClientSession {
|
|
|
25536
25699
|
capabilities;
|
|
25537
25700
|
waiting = 0;
|
|
25538
25701
|
turnActive = false;
|
|
25702
|
+
turnGeneration = 0;
|
|
25703
|
+
disposed = false;
|
|
25704
|
+
output = new AbortController;
|
|
25705
|
+
terminalAttempted = false;
|
|
25706
|
+
get signal() {
|
|
25707
|
+
return this.output.signal;
|
|
25708
|
+
}
|
|
25539
25709
|
constructor(sessionId, link, capabilities) {
|
|
25540
25710
|
this.sessionId = sessionId;
|
|
25541
25711
|
this.link = link;
|
|
25542
25712
|
this.capabilities = capabilities;
|
|
25543
25713
|
}
|
|
25544
25714
|
async update(update) {
|
|
25545
|
-
|
|
25715
|
+
if (this.disposed)
|
|
25716
|
+
return;
|
|
25717
|
+
await abortable(this.link.notify(methods.client.session.update, { sessionId: this.sessionId, update }), this.output.signal);
|
|
25546
25718
|
}
|
|
25547
25719
|
async updateAll(updates) {
|
|
25548
25720
|
for (const update of updates)
|
|
25549
25721
|
await this.update(update);
|
|
25550
25722
|
}
|
|
25551
25723
|
async requestPermission(request, signal) {
|
|
25552
|
-
|
|
25724
|
+
signal = signal ? AbortSignal.any([signal, this.output.signal]) : this.output.signal;
|
|
25725
|
+
signal.throwIfAborted();
|
|
25726
|
+
return await this.waitingOnClient(() => {
|
|
25727
|
+
signal?.throwIfAborted();
|
|
25728
|
+
const pending = this.link.request(methods.client.session.requestPermission, { sessionId: this.sessionId, ...request }, signal ? { cancellationSignal: signal } : undefined);
|
|
25729
|
+
return signal ? abortable(pending, signal) : pending;
|
|
25730
|
+
}, signal);
|
|
25553
25731
|
}
|
|
25554
25732
|
async createElicitation(request, signal) {
|
|
25555
|
-
|
|
25733
|
+
signal = signal ? AbortSignal.any([signal, this.output.signal]) : this.output.signal;
|
|
25734
|
+
signal.throwIfAborted();
|
|
25735
|
+
return await this.waitingOnClient(() => {
|
|
25736
|
+
signal?.throwIfAborted();
|
|
25737
|
+
const pending = this.link.request(methods.client.elicitation.create, request, signal ? { cancellationSignal: signal } : undefined);
|
|
25738
|
+
return signal ? abortable(pending, signal) : pending;
|
|
25739
|
+
}, signal);
|
|
25556
25740
|
}
|
|
25557
25741
|
async completeElicitation(elicitationId) {
|
|
25558
|
-
|
|
25742
|
+
if (this.disposed)
|
|
25743
|
+
return;
|
|
25744
|
+
await abortable(this.link.notify(methods.client.elicitation.complete, { elicitationId }), this.output.signal);
|
|
25559
25745
|
}
|
|
25560
25746
|
reportRunning() {
|
|
25747
|
+
this.terminalAttempted = false;
|
|
25748
|
+
this.turnGeneration += 1;
|
|
25561
25749
|
this.turnActive = true;
|
|
25562
25750
|
this.waiting = 0;
|
|
25563
25751
|
this.state({ sessionUpdate: "state_update", state: "running" });
|
|
25564
25752
|
}
|
|
25565
25753
|
async reportIdle(stopReason, extra) {
|
|
25754
|
+
if (this.terminalAttempted)
|
|
25755
|
+
return;
|
|
25756
|
+
this.terminalAttempted = true;
|
|
25566
25757
|
this.turnActive = false;
|
|
25567
25758
|
this.waiting = 0;
|
|
25568
25759
|
await this.update({
|
|
@@ -25573,18 +25764,21 @@ class ClientSession {
|
|
|
25573
25764
|
...extra?._meta === undefined ? {} : { _meta: extra._meta }
|
|
25574
25765
|
});
|
|
25575
25766
|
}
|
|
25576
|
-
async waitingOnClient(operation) {
|
|
25577
|
-
if (this.
|
|
25578
|
-
|
|
25579
|
-
|
|
25767
|
+
async waitingOnClient(operation, signal) {
|
|
25768
|
+
if (this.disposed)
|
|
25769
|
+
throw new Error("Session is closed");
|
|
25770
|
+
const generation = this.turnActive ? this.turnGeneration : null;
|
|
25580
25771
|
try {
|
|
25772
|
+
if (generation !== null && this.waiting++ === 0) {
|
|
25773
|
+
const pending = this.state({ sessionUpdate: "state_update", state: "requires_action" });
|
|
25774
|
+
await (signal ? abortable(pending, signal) : pending);
|
|
25775
|
+
}
|
|
25581
25776
|
return await operation();
|
|
25582
25777
|
} finally {
|
|
25583
|
-
if (this.turnActive && --this.waiting === 0) {
|
|
25584
|
-
|
|
25778
|
+
if (this.turnActive && generation === this.turnGeneration && --this.waiting === 0) {
|
|
25779
|
+
const pending = this.state({ sessionUpdate: "state_update", state: "running" });
|
|
25780
|
+
await (signal ? abortable(pending, signal).catch(() => {}) : pending);
|
|
25585
25781
|
}
|
|
25586
|
-
if (this.waiting < 0)
|
|
25587
|
-
this.waiting = 0;
|
|
25588
25782
|
}
|
|
25589
25783
|
}
|
|
25590
25784
|
async state(update) {
|
|
@@ -25594,6 +25788,12 @@ class ClientSession {
|
|
|
25594
25788
|
logger.error("Failed to publish session state", error40, { sessionId: this.sessionId });
|
|
25595
25789
|
}
|
|
25596
25790
|
}
|
|
25791
|
+
dispose() {
|
|
25792
|
+
this.disposed = true;
|
|
25793
|
+
this.output.abort();
|
|
25794
|
+
this.turnActive = false;
|
|
25795
|
+
this.waiting = 0;
|
|
25796
|
+
}
|
|
25597
25797
|
}
|
|
25598
25798
|
|
|
25599
25799
|
// src/agent/commands.ts
|
|
@@ -25830,7 +26030,7 @@ function historyUpdates(turns) {
|
|
|
25830
26030
|
const updates = [];
|
|
25831
26031
|
for (const turn of turns) {
|
|
25832
26032
|
for (const item of turn.items) {
|
|
25833
|
-
updates.push(...
|
|
26033
|
+
updates.push(...itemSnapshot(item));
|
|
25834
26034
|
}
|
|
25835
26035
|
}
|
|
25836
26036
|
return updates;
|
|
@@ -25855,63 +26055,6 @@ function firstLine(text2) {
|
|
|
25855
26055
|
const line = text2.split(/\r?\n/).map((part) => part.trim()).find((part) => part.length > 0) ?? text2;
|
|
25856
26056
|
return line.length > 120 ? `${line.slice(0, 117)}...` : line;
|
|
25857
26057
|
}
|
|
25858
|
-
function itemHistory(item) {
|
|
25859
|
-
switch (item.type) {
|
|
25860
|
-
case "userMessage": {
|
|
25861
|
-
const content = item.content.flatMap(fromUserInput);
|
|
25862
|
-
return content.length > 0 ? [{ sessionUpdate: "user_message", messageId: item.id, content }] : [];
|
|
25863
|
-
}
|
|
25864
|
-
case "agentMessage":
|
|
25865
|
-
return item.text.length > 0 ? [{
|
|
25866
|
-
sessionUpdate: "agent_message",
|
|
25867
|
-
messageId: item.id,
|
|
25868
|
-
content: [{ type: "text", text: item.text }],
|
|
25869
|
-
...item.phase ? { _meta: { codex: { phase: item.phase } } } : {}
|
|
25870
|
-
}] : [];
|
|
25871
|
-
case "reasoning": {
|
|
25872
|
-
const parts = (item.summary.length > 0 ? item.summary : item.content).filter((part) => part.length > 0);
|
|
25873
|
-
return parts.length > 0 ? [{ sessionUpdate: "agent_thought", messageId: item.id, content: parts.map((text2) => ({ type: "text", text: text2 })) }] : [];
|
|
25874
|
-
}
|
|
25875
|
-
case "plan":
|
|
25876
|
-
return item.text.length > 0 ? [{ sessionUpdate: "plan_update", plan: { type: "markdown", planId: item.id, content: item.text } }] : [];
|
|
25877
|
-
case "fileChange":
|
|
25878
|
-
return [fileChangeStarted(item)];
|
|
25879
|
-
case "commandExecution": {
|
|
25880
|
-
const updates = [commandStarted(item)];
|
|
25881
|
-
if (usesTerminal(item))
|
|
25882
|
-
updates.push(terminalSnapshot(item));
|
|
25883
|
-
if (item.status !== "inProgress")
|
|
25884
|
-
updates.push(commandCompleted(item));
|
|
25885
|
-
return updates;
|
|
25886
|
-
}
|
|
25887
|
-
case "mcpToolCall":
|
|
25888
|
-
return [mcpToolCallStarted(item)];
|
|
25889
|
-
case "dynamicToolCall":
|
|
25890
|
-
return [{ ...dynamicToolCallStarted(item), ...dynamicToolCallCompleted(item) }];
|
|
25891
|
-
case "webSearch":
|
|
25892
|
-
return [webSearchSnapshot(item)];
|
|
25893
|
-
case "imageView":
|
|
25894
|
-
return [imageViewed(item)];
|
|
25895
|
-
case "imageGeneration":
|
|
25896
|
-
return [imageGenerationSnapshot(item)];
|
|
25897
|
-
case "collabAgentToolCall":
|
|
25898
|
-
return [collabToolCall(item, true)];
|
|
25899
|
-
case "subAgentActivity":
|
|
25900
|
-
return [subAgentActivity(item, "completed", true)];
|
|
25901
|
-
case "contextCompaction":
|
|
25902
|
-
return [compactionUpdate(item.id, "completed")];
|
|
25903
|
-
case "enteredReviewMode":
|
|
25904
|
-
return [];
|
|
25905
|
-
case "exitedReviewMode": {
|
|
25906
|
-
const text2 = item.review.trim();
|
|
25907
|
-
return text2.length > 0 ? [{ sessionUpdate: "agent_message", messageId: item.id, content: [{ type: "text", text: text2 }] }] : [];
|
|
25908
|
-
}
|
|
25909
|
-
case "hookPrompt":
|
|
25910
|
-
case "functionCallOutput":
|
|
25911
|
-
case "sleep":
|
|
25912
|
-
return [];
|
|
25913
|
-
}
|
|
25914
|
-
}
|
|
25915
26058
|
|
|
25916
26059
|
// src/agent/providers.ts
|
|
25917
26060
|
var OPENAI_PROVIDER_ID = "openai";
|
|
@@ -25926,6 +26069,11 @@ class ProviderRouting {
|
|
|
25926
26069
|
this.baseConfig = baseConfig;
|
|
25927
26070
|
this.configuredProvider = configuredProvider;
|
|
25928
26071
|
}
|
|
26072
|
+
copy() {
|
|
26073
|
+
const copy = new ProviderRouting(this.baseConfig, this.configuredProvider);
|
|
26074
|
+
copy.gateway = this.gateway;
|
|
26075
|
+
return copy;
|
|
26076
|
+
}
|
|
25929
26077
|
get active() {
|
|
25930
26078
|
return this.gateway;
|
|
25931
26079
|
}
|
|
@@ -26046,7 +26194,7 @@ function gatewayModel(model, isDefault, template) {
|
|
|
26046
26194
|
function createActiveTurn(threadId) {
|
|
26047
26195
|
let resolveStarted = () => {};
|
|
26048
26196
|
let resolveFinished = () => {};
|
|
26049
|
-
|
|
26197
|
+
let started = new Promise((resolve) => {
|
|
26050
26198
|
resolveStarted = resolve;
|
|
26051
26199
|
});
|
|
26052
26200
|
const finished = new Promise((resolve) => {
|
|
@@ -26058,7 +26206,18 @@ function createActiveTurn(threadId) {
|
|
|
26058
26206
|
turnId: null,
|
|
26059
26207
|
threadId,
|
|
26060
26208
|
abort: new AbortController,
|
|
26061
|
-
|
|
26209
|
+
stop: new AbortController,
|
|
26210
|
+
interrupts: new Map,
|
|
26211
|
+
get started() {
|
|
26212
|
+
return started;
|
|
26213
|
+
},
|
|
26214
|
+
resetStarted() {
|
|
26215
|
+
this.turnId = null;
|
|
26216
|
+
startedSettled = false;
|
|
26217
|
+
started = new Promise((resolve) => {
|
|
26218
|
+
resolveStarted = resolve;
|
|
26219
|
+
});
|
|
26220
|
+
},
|
|
26062
26221
|
resolveStarted(turnId) {
|
|
26063
26222
|
if (startedSettled)
|
|
26064
26223
|
return;
|
|
@@ -26078,7 +26237,6 @@ function createActiveTurn(threadId) {
|
|
|
26078
26237
|
// src/agent/CodexAgent.ts
|
|
26079
26238
|
var CLOSE_TURN_GRACE_MS = 5000;
|
|
26080
26239
|
var HISTORY_PAGE_SIZE = 50;
|
|
26081
|
-
var ERROR_STOP_REASON = "_error";
|
|
26082
26240
|
var IMPLEMENT_PLAN_OPTION = "implement_plan";
|
|
26083
26241
|
var REVISE_PLAN_OPTION = "revise_plan";
|
|
26084
26242
|
|
|
@@ -26087,13 +26245,27 @@ class CodexAgent {
|
|
|
26087
26245
|
codex;
|
|
26088
26246
|
process;
|
|
26089
26247
|
providers;
|
|
26248
|
+
switching = false;
|
|
26249
|
+
admissions = 0;
|
|
26250
|
+
pendingUnsubscribes = new Map;
|
|
26251
|
+
sessionMutations = new Set;
|
|
26252
|
+
providerQueue = Promise.resolve();
|
|
26090
26253
|
info;
|
|
26091
26254
|
env;
|
|
26092
26255
|
closeGraceMs;
|
|
26093
26256
|
sessions = new Map;
|
|
26094
26257
|
capabilities = null;
|
|
26095
26258
|
codexInitialized = false;
|
|
26259
|
+
initializing = null;
|
|
26260
|
+
terminalTurns = new WeakSet;
|
|
26261
|
+
completedTurns = new WeakMap;
|
|
26096
26262
|
skillRoots = [];
|
|
26263
|
+
skillsGeneration = 0;
|
|
26264
|
+
changingSkillRoots = false;
|
|
26265
|
+
publishedSkills = new WeakMap;
|
|
26266
|
+
skillSnapshots = new Map;
|
|
26267
|
+
skillsQueue = Promise.resolve();
|
|
26268
|
+
accountGeneration = 0;
|
|
26097
26269
|
constructor(link, options) {
|
|
26098
26270
|
this.link = link;
|
|
26099
26271
|
this.codex = options.codex;
|
|
@@ -26104,22 +26276,40 @@ class CodexAgent {
|
|
|
26104
26276
|
this.closeGraceMs = options.closeGraceMs ?? CLOSE_TURN_GRACE_MS;
|
|
26105
26277
|
this.process?.exited.then(() => this.handleCodexExit());
|
|
26106
26278
|
this.codex.connection.onClose(() => this.handleCodexExit());
|
|
26279
|
+
const stopObserving = this.codex.observeNotifications((notification) => {
|
|
26280
|
+
if (notification.method === "skills/changed") {
|
|
26281
|
+
this.skillsGeneration += 1;
|
|
26282
|
+
this.skillSnapshots.clear();
|
|
26283
|
+
if (!this.changingSkillRoots)
|
|
26284
|
+
for (const runtime of this.sessions.values())
|
|
26285
|
+
this.refreshAvailableCommands(runtime);
|
|
26286
|
+
}
|
|
26287
|
+
if (notification.method === "account/updated")
|
|
26288
|
+
this.refreshAccounts().catch((error40) => logger.error("refreshing account failed", error40));
|
|
26289
|
+
});
|
|
26290
|
+
this.codex.disconnectSignal.addEventListener("abort", stopObserving, { once: true });
|
|
26107
26291
|
}
|
|
26108
26292
|
async initialize(params) {
|
|
26109
26293
|
if (params.protocolVersion !== PROTOCOL_VERSION) {
|
|
26110
26294
|
throw RequestError.invalidParams({ protocolVersion: params.protocolVersion }, `Unsupported protocol version ${params.protocolVersion}; this agent speaks ACP v${PROTOCOL_VERSION}`);
|
|
26111
26295
|
}
|
|
26112
|
-
|
|
26296
|
+
const capabilities = {
|
|
26113
26297
|
formElicitation: params.capabilities?.elicitation?.form != null,
|
|
26114
26298
|
urlElicitation: params.capabilities?.elicitation?.url != null
|
|
26115
26299
|
};
|
|
26116
26300
|
if (!this.codexInitialized) {
|
|
26117
|
-
|
|
26118
|
-
|
|
26119
|
-
|
|
26120
|
-
|
|
26121
|
-
|
|
26301
|
+
this.initializing ??= this.withCodex(async () => {
|
|
26302
|
+
await this.codex.initialize({
|
|
26303
|
+
clientInfo: { name: params.info.name, title: params.info.title ?? null, version: params.info.version },
|
|
26304
|
+
capabilities: { experimentalApi: true, requestAttestation: false }
|
|
26305
|
+
});
|
|
26306
|
+
this.codexInitialized = true;
|
|
26307
|
+
}).finally(() => {
|
|
26308
|
+
this.initializing = null;
|
|
26309
|
+
});
|
|
26310
|
+
await this.initializing;
|
|
26122
26311
|
}
|
|
26312
|
+
this.capabilities = capabilities;
|
|
26123
26313
|
return {
|
|
26124
26314
|
protocolVersion: PROTOCOL_VERSION,
|
|
26125
26315
|
info: this.info,
|
|
@@ -26156,7 +26346,11 @@ class CodexAgent {
|
|
|
26156
26346
|
return {};
|
|
26157
26347
|
}
|
|
26158
26348
|
async refreshAccounts() {
|
|
26349
|
+
this.codex.invalidateModels();
|
|
26350
|
+
const generation = ++this.accountGeneration;
|
|
26159
26351
|
const account = (await this.codex.accountRead({ refreshToken: false })).account;
|
|
26352
|
+
if (generation !== this.accountGeneration)
|
|
26353
|
+
return;
|
|
26160
26354
|
for (const runtime of this.sessions.values())
|
|
26161
26355
|
runtime.session.account = account;
|
|
26162
26356
|
}
|
|
@@ -26166,16 +26360,14 @@ class CodexAgent {
|
|
|
26166
26360
|
}
|
|
26167
26361
|
async setProvider(params) {
|
|
26168
26362
|
this.requireInitialized("providers/set");
|
|
26169
|
-
this.
|
|
26170
|
-
this.providers.set(params);
|
|
26171
|
-
await this.rebindSessions();
|
|
26363
|
+
await this.changeProvider((candidate) => candidate.set(params));
|
|
26172
26364
|
return {};
|
|
26173
26365
|
}
|
|
26174
26366
|
async disableProvider(params) {
|
|
26175
26367
|
this.requireInitialized("providers/disable");
|
|
26176
|
-
|
|
26177
|
-
|
|
26178
|
-
await this.
|
|
26368
|
+
if (params.providerId !== OPENAI_PROVIDER_ID)
|
|
26369
|
+
return {};
|
|
26370
|
+
await this.changeProvider((candidate) => candidate.disable(params));
|
|
26179
26371
|
return {};
|
|
26180
26372
|
}
|
|
26181
26373
|
assertNoActiveTurns(method) {
|
|
@@ -26184,57 +26376,173 @@ class CodexAgent {
|
|
|
26184
26376
|
throw RequestError.invalidRequest({ sessions: busy }, `${method} cannot change routing while a turn is running; cancel it or wait for idle`);
|
|
26185
26377
|
}
|
|
26186
26378
|
}
|
|
26187
|
-
|
|
26188
|
-
|
|
26189
|
-
|
|
26190
|
-
|
|
26191
|
-
|
|
26192
|
-
|
|
26193
|
-
|
|
26194
|
-
|
|
26195
|
-
|
|
26196
|
-
|
|
26197
|
-
|
|
26198
|
-
|
|
26199
|
-
|
|
26200
|
-
|
|
26201
|
-
|
|
26202
|
-
|
|
26203
|
-
|
|
26379
|
+
assertRoutingAvailable() {
|
|
26380
|
+
if (this.switching)
|
|
26381
|
+
throw RequestError.invalidRequest(undefined, "Provider routing is changing; retry after providers/set or providers/disable finishes");
|
|
26382
|
+
}
|
|
26383
|
+
async admission(operation, sessionId) {
|
|
26384
|
+
this.assertRoutingAvailable();
|
|
26385
|
+
if (sessionId && this.sessionMutations.has(sessionId))
|
|
26386
|
+
throw RequestError.invalidRequest({ sessionId }, "Session lifecycle or configuration work is in progress; retry when it finishes");
|
|
26387
|
+
if (sessionId)
|
|
26388
|
+
this.sessionMutations.add(sessionId);
|
|
26389
|
+
this.admissions += 1;
|
|
26390
|
+
try {
|
|
26391
|
+
return await operation();
|
|
26392
|
+
} finally {
|
|
26393
|
+
this.admissions -= 1;
|
|
26394
|
+
if (sessionId)
|
|
26395
|
+
this.sessionMutations.delete(sessionId);
|
|
26396
|
+
}
|
|
26397
|
+
}
|
|
26398
|
+
async changeProvider(change) {
|
|
26399
|
+
const pending = this.providerQueue.then(async () => {
|
|
26400
|
+
this.assertNoActiveTurns("providers/set or providers/disable");
|
|
26401
|
+
if (this.admissions > 0)
|
|
26402
|
+
throw RequestError.invalidRequest(undefined, "Session configuration or lifecycle work is in progress; retry the provider change when it finishes");
|
|
26403
|
+
this.switching = true;
|
|
26404
|
+
const candidate = this.providers.copy();
|
|
26405
|
+
const attempted = [];
|
|
26406
|
+
const staged = new Map;
|
|
26407
|
+
try {
|
|
26408
|
+
change(candidate);
|
|
26409
|
+
const catalog = await this.withCodex(() => this.codex.allModels(true));
|
|
26410
|
+
const modelProvider = await this.resolveModelProvider(candidate);
|
|
26411
|
+
for (const runtime of this.sessions.values()) {
|
|
26412
|
+
try {
|
|
26413
|
+
await this.codex.threadTurnsList({ threadId: runtime.session.id, limit: 1, itemsView: "notLoaded" });
|
|
26414
|
+
} catch (error40) {
|
|
26415
|
+
throw RequestError.invalidRequest({ sessionId: runtime.session.id, details: errorMessage(error40) }, "Cannot reload this session's history for a provider change; close empty sessions and configure the provider before creating them, or retry after history is available");
|
|
26416
|
+
}
|
|
26417
|
+
}
|
|
26418
|
+
for (const runtime of this.sessions.values()) {
|
|
26419
|
+
const config2 = { ...runtime.config };
|
|
26420
|
+
const providers = candidate.threadConfig()["model_providers"];
|
|
26421
|
+
if (providers === undefined)
|
|
26422
|
+
delete config2["model_providers"];
|
|
26423
|
+
else
|
|
26424
|
+
config2["model_providers"] = providers;
|
|
26425
|
+
const nextCatalog = candidate.catalog(catalog);
|
|
26426
|
+
const requestedModel = candidate.active?.model ?? (findModel(nextCatalog, runtime.session.model.model) ? runtime.session.model.model : null);
|
|
26427
|
+
const selection = resolveModelSelection(nextCatalog, requestedModel, requestedModel === runtime.session.model.model ? runtime.session.model.effort : null);
|
|
26428
|
+
attempted.push(runtime);
|
|
26429
|
+
await this.codex.threadUnsubscribe({ threadId: runtime.session.id });
|
|
26430
|
+
const thread = await this.withCodex(() => this.codex.threadResume({
|
|
26431
|
+
threadId: runtime.session.id,
|
|
26432
|
+
cwd: runtime.session.cwd,
|
|
26433
|
+
config: config2,
|
|
26434
|
+
modelProvider,
|
|
26435
|
+
model: selection.model,
|
|
26436
|
+
excludeTurns: true
|
|
26437
|
+
}));
|
|
26438
|
+
staged.set(runtime, { config: config2, catalog: nextCatalog, model: resolveModelSelection(nextCatalog, candidate.active?.model ?? thread.model, thread.reasoningEffort) });
|
|
26439
|
+
}
|
|
26440
|
+
this.providers = candidate;
|
|
26441
|
+
for (const [runtime, next] of staged) {
|
|
26442
|
+
runtime.config = next.config;
|
|
26443
|
+
runtime.modelProvider = modelProvider;
|
|
26444
|
+
runtime.session.catalog = next.catalog;
|
|
26445
|
+
runtime.session.model = next.model;
|
|
26446
|
+
runtime.stale = false;
|
|
26447
|
+
runtime.client.update({ sessionUpdate: "config_option_update", configOptions: sessionConfigOptions(runtime.session), _meta: { codex: { routing: { stale: false } } } }).catch((error40) => logger.error("Provider config notification failed", error40));
|
|
26448
|
+
}
|
|
26449
|
+
} catch (error40) {
|
|
26450
|
+
if (attempted.length === 0 && error40 instanceof RequestError)
|
|
26451
|
+
throw error40;
|
|
26452
|
+
const staleSessions = [];
|
|
26453
|
+
for (const runtime of attempted) {
|
|
26454
|
+
try {
|
|
26455
|
+
await this.codex.threadUnsubscribe({ threadId: runtime.session.id });
|
|
26456
|
+
await this.codex.threadResume({
|
|
26457
|
+
threadId: runtime.session.id,
|
|
26458
|
+
cwd: runtime.session.cwd,
|
|
26459
|
+
config: runtime.config,
|
|
26460
|
+
modelProvider: runtime.modelProvider,
|
|
26461
|
+
model: runtime.session.model.model,
|
|
26462
|
+
excludeTurns: true
|
|
26463
|
+
});
|
|
26464
|
+
} catch (rollbackError) {
|
|
26465
|
+
runtime.stale = true;
|
|
26466
|
+
staleSessions.push(runtime.session.id);
|
|
26467
|
+
logger.error("Provider rollback failed", rollbackError, { sessionId: runtime.session.id });
|
|
26468
|
+
runtime.client.update({ sessionUpdate: "config_option_update", configOptions: sessionConfigOptions(runtime.session), _meta: { codex: { routing: { stale: true } } } }).catch(() => {});
|
|
26469
|
+
}
|
|
26470
|
+
}
|
|
26471
|
+
throw RequestError.internalError({ staleSessions, attemptedSessions: attempted.map((runtime) => runtime.session.id) }, `Provider change failed: ${errorMessage(error40)}${staleSessions.length ? "; resume affected sessions or retry the provider change before prompting" : "; previous routing restored"}`);
|
|
26472
|
+
} finally {
|
|
26473
|
+
this.switching = false;
|
|
26474
|
+
}
|
|
26475
|
+
});
|
|
26476
|
+
this.providerQueue = pending.catch(() => {});
|
|
26477
|
+
await pending;
|
|
26204
26478
|
}
|
|
26205
|
-
async newSession(params) {
|
|
26206
|
-
const runtime = await this.openSession({ kind: "new", request: params });
|
|
26479
|
+
async newSession(params, signal) {
|
|
26480
|
+
const runtime = await this.openSession({ kind: "new", request: params }, signal);
|
|
26207
26481
|
return { sessionId: runtime.session.id, configOptions: sessionConfigOptions(runtime.session) };
|
|
26208
26482
|
}
|
|
26209
|
-
async resumeSession(params) {
|
|
26210
|
-
const runtime = await this.openSession({ kind: "resume", request: params });
|
|
26483
|
+
async resumeSession(params, signal) {
|
|
26484
|
+
const runtime = await this.openSession({ kind: "resume", request: params }, signal);
|
|
26211
26485
|
return { configOptions: sessionConfigOptions(runtime.session) };
|
|
26212
26486
|
}
|
|
26213
|
-
async forkSession(params) {
|
|
26214
|
-
const runtime = await this.openSession({ kind: "fork", request: params });
|
|
26487
|
+
async forkSession(params, signal) {
|
|
26488
|
+
const runtime = await this.openSession({ kind: "fork", request: params }, signal);
|
|
26215
26489
|
return { sessionId: runtime.session.id, configOptions: sessionConfigOptions(runtime.session) };
|
|
26216
26490
|
}
|
|
26217
|
-
async openSession(open2) {
|
|
26491
|
+
async openSession(open2, signal) {
|
|
26492
|
+
if (signal?.aborted)
|
|
26493
|
+
throw RequestError.requestCancelled(undefined, "Session opening was cancelled");
|
|
26494
|
+
const pending = this.admission(async () => {
|
|
26495
|
+
const runtime = await this.openSessionInternal(open2, signal);
|
|
26496
|
+
if (signal?.aborted) {
|
|
26497
|
+
await this.closeRuntime(runtime.session.id);
|
|
26498
|
+
throw RequestError.requestCancelled(undefined, "Session opening was cancelled");
|
|
26499
|
+
}
|
|
26500
|
+
return runtime;
|
|
26501
|
+
}, open2.kind === "new" ? undefined : open2.request.sessionId);
|
|
26502
|
+
try {
|
|
26503
|
+
return signal ? await abortable(pending, signal) : await pending;
|
|
26504
|
+
} catch (error40) {
|
|
26505
|
+
if (signal?.aborted)
|
|
26506
|
+
throw RequestError.requestCancelled(undefined, "Session opening was cancelled; any thread returned later will be unsubscribed");
|
|
26507
|
+
throw error40;
|
|
26508
|
+
}
|
|
26509
|
+
}
|
|
26510
|
+
assertUnsubscribeSettled(sessionId) {
|
|
26511
|
+
if (this.pendingUnsubscribes.has(sessionId)) {
|
|
26512
|
+
throw RequestError.invalidRequest({ sessionId }, "A previous unsubscribe is still pending; retry after Codex responds or reconnect the agent");
|
|
26513
|
+
}
|
|
26514
|
+
}
|
|
26515
|
+
async openSessionInternal(open2, signal) {
|
|
26218
26516
|
const method = { new: "session/new", resume: "session/resume", fork: "session/fork" }[open2.kind];
|
|
26219
26517
|
const capabilities = this.requireInitialized(method);
|
|
26518
|
+
if (open2.kind !== "new")
|
|
26519
|
+
this.assertUnsubscribeSettled(open2.request.sessionId);
|
|
26220
26520
|
const { request } = open2;
|
|
26221
26521
|
if (typeof request.cwd !== "string" || !path6.isAbsolute(request.cwd)) {
|
|
26222
26522
|
throw RequestError.invalidParams({ cwd: request.cwd }, "cwd must be an absolute path");
|
|
26223
26523
|
}
|
|
26224
|
-
if (open2.kind
|
|
26225
|
-
|
|
26524
|
+
if (open2.kind === "resume" && open2.request.replayFrom != null && open2.request.replayFrom.type !== "start") {
|
|
26525
|
+
throw RequestError.invalidParams({ replayFrom: open2.request.replayFrom }, 'Only replayFrom {type: "start"} is supported');
|
|
26226
26526
|
}
|
|
26527
|
+
const seed = open2.kind === "new" ? seedHistoryOf(open2.request._meta) : [];
|
|
26227
26528
|
const additionalDirectories = readAdditionalDirectories(request.cwd, request.additionalDirectories);
|
|
26529
|
+
if (open2.kind === "resume" && this.sessions.has(open2.request.sessionId)) {
|
|
26530
|
+
await this.closeRuntime(open2.request.sessionId);
|
|
26531
|
+
this.assertUnsubscribeSettled(open2.request.sessionId);
|
|
26532
|
+
}
|
|
26228
26533
|
const mcpServers = request.mcpServers ?? [];
|
|
26229
|
-
|
|
26534
|
+
let accountVersion = this.accountGeneration;
|
|
26535
|
+
const account = await this.withCodex(() => this.codex.accountRead({ refreshToken: false }));
|
|
26536
|
+
if (this.providers.active === null && account.requiresOpenaiAuth && account.account === null) {
|
|
26230
26537
|
throw RequestError.authRequired(undefined, "Log in to Codex first (auth/login)");
|
|
26231
26538
|
}
|
|
26232
|
-
await this.withCodex(() => this.refreshSkills(request.cwd, additionalDirectories));
|
|
26233
26539
|
const existingMcp = mcpServers.length > 0 ? await this.withCodex(() => this.configuredMcpServerNames(request.cwd)) : new Set;
|
|
26234
26540
|
const config2 = buildThreadConfig(this.providers.threadConfig(), request.cwd, additionalDirectories, mcpServers, existingMcp);
|
|
26235
26541
|
const mcpStartupGeneration = this.codex.mcpStartupGeneration;
|
|
26236
26542
|
const modelProvider = await this.withCodex(() => this.resolveModelProvider());
|
|
26237
|
-
const thread = await this.withCodex(async () => {
|
|
26543
|
+
const { thread, skills } = await this.withSkillsContext(request.cwd, additionalDirectories, async (skills2) => ({ skills: skills2, thread: await this.withCodex(async () => {
|
|
26544
|
+
if (signal?.aborted)
|
|
26545
|
+
throw RequestError.requestCancelled(undefined, "Session opening was cancelled");
|
|
26238
26546
|
switch (open2.kind) {
|
|
26239
26547
|
case "new":
|
|
26240
26548
|
return await this.codex.threadStart({ config: config2, cwd: request.cwd, modelProvider });
|
|
@@ -26243,13 +26551,32 @@ class CodexAgent {
|
|
|
26243
26551
|
case "fork":
|
|
26244
26552
|
return await this.codex.threadFork({ threadId: open2.request.sessionId, config: config2, cwd: request.cwd, modelProvider, excludeTurns: true });
|
|
26245
26553
|
}
|
|
26246
|
-
});
|
|
26554
|
+
}) }));
|
|
26247
26555
|
const sessionId = thread.thread.id;
|
|
26556
|
+
let removeCancellation = () => {};
|
|
26557
|
+
const openingCompletions = new Map;
|
|
26558
|
+
const openingStarts = new Set;
|
|
26559
|
+
const stopObservingOpen = this.codex.observeNotifications((notification) => {
|
|
26560
|
+
if (notification.method === "turn/completed" && notification.params.threadId === sessionId) {
|
|
26561
|
+
if (openingCompletions.size >= 32)
|
|
26562
|
+
openingCompletions.delete(openingCompletions.keys().next().value);
|
|
26563
|
+
openingCompletions.set(notification.params.turn.id, notification.params);
|
|
26564
|
+
}
|
|
26565
|
+
if (notification.method === "turn/started" && notification.params.threadId === sessionId && this.sessions.has(sessionId)) {
|
|
26566
|
+
if (openingStarts.size >= 32)
|
|
26567
|
+
openingStarts.delete(openingStarts.values().next().value);
|
|
26568
|
+
openingStarts.add(notification.params.turn.id);
|
|
26569
|
+
}
|
|
26570
|
+
});
|
|
26248
26571
|
try {
|
|
26249
|
-
|
|
26250
|
-
|
|
26251
|
-
|
|
26252
|
-
|
|
26572
|
+
if (signal?.aborted)
|
|
26573
|
+
throw RequestError.requestCancelled(undefined, "Session opening was cancelled");
|
|
26574
|
+
const codexCatalog = await this.withCodex(() => this.codex.allModels());
|
|
26575
|
+
let openedAccount = account.account;
|
|
26576
|
+
while (accountVersion !== this.accountGeneration) {
|
|
26577
|
+
accountVersion = this.accountGeneration;
|
|
26578
|
+
openedAccount = (await this.withCodex(() => this.codex.accountRead({ refreshToken: false }))).account;
|
|
26579
|
+
}
|
|
26253
26580
|
const gateway = this.providers.active;
|
|
26254
26581
|
const catalog = this.providers.catalog(codexCatalog);
|
|
26255
26582
|
const model = resolveModelSelection(catalog, gateway?.model ?? thread.model, thread.reasoningEffort);
|
|
@@ -26257,13 +26584,13 @@ class CodexAgent {
|
|
|
26257
26584
|
id: sessionId,
|
|
26258
26585
|
cwd: request.cwd,
|
|
26259
26586
|
additionalDirectories,
|
|
26260
|
-
mcpServerNames: mcpServerNames(mcpServers),
|
|
26587
|
+
mcpServerNames: mcpServerNames(mcpServers).filter((name) => !existingMcp.has(name)),
|
|
26261
26588
|
catalog,
|
|
26262
26589
|
model,
|
|
26263
26590
|
mode: initialAgentMode(this.env),
|
|
26264
26591
|
collaborationMode: DEFAULT_COLLABORATION_MODE,
|
|
26265
26592
|
fastMode: thread.serviceTier === FAST_SERVICE_TIER,
|
|
26266
|
-
account:
|
|
26593
|
+
account: openedAccount,
|
|
26267
26594
|
title: null,
|
|
26268
26595
|
titleIsExplicit: false,
|
|
26269
26596
|
activeTurn: null,
|
|
@@ -26271,40 +26598,66 @@ class CodexAgent {
|
|
|
26271
26598
|
contextWindow: null,
|
|
26272
26599
|
closed: false
|
|
26273
26600
|
};
|
|
26274
|
-
const runtime = this.installRuntime(session, capabilities);
|
|
26601
|
+
const runtime = this.installRuntime(session, capabilities, config2, thread.modelProvider);
|
|
26602
|
+
const cancelOpening = () => {
|
|
26603
|
+
runtime.session.closed = true;
|
|
26604
|
+
runtime.lifetime.abort();
|
|
26605
|
+
runtime.client.dispose();
|
|
26606
|
+
};
|
|
26607
|
+
signal?.addEventListener("abort", cancelOpening, { once: true });
|
|
26608
|
+
removeCancellation = () => signal?.removeEventListener("abort", cancelOpening);
|
|
26609
|
+
if (signal?.aborted)
|
|
26610
|
+
cancelOpening();
|
|
26275
26611
|
const replay = open2.kind === "fork" || open2.kind === "resume" && open2.request.replayFrom?.type === "start";
|
|
26276
|
-
if (open2.kind === "resume" && open2.request.replayFrom != null && open2.request.replayFrom.type !== "start") {
|
|
26277
|
-
throw RequestError.invalidParams({ replayFrom: open2.request.replayFrom }, 'Only replayFrom {type: "start"} is supported');
|
|
26278
|
-
}
|
|
26279
26612
|
if (open2.kind !== "new") {
|
|
26280
26613
|
await this.replayHistory(runtime, replay, thread.thread);
|
|
26281
26614
|
} else {
|
|
26282
|
-
const seed = seedHistoryOf(open2.request._meta);
|
|
26283
26615
|
if (seed.length > 0) {
|
|
26284
26616
|
await this.withCodex(() => this.codex.threadInjectItems({ threadId: sessionId, items: seed.map(seedItem) }));
|
|
26285
26617
|
}
|
|
26286
26618
|
}
|
|
26619
|
+
if (thread.thread.status.type === "active" && runtime.session.activeTurn === null) {
|
|
26620
|
+
const latest = await this.withCodex(() => this.codex.threadTurnsList({ threadId: session.id, limit: 1, sortDirection: "desc", itemsView: "full" }));
|
|
26621
|
+
const active = latest.data.find((turn) => turn.status === "inProgress");
|
|
26622
|
+
if (active && !(openingStarts.has(active.id) && openingCompletions.has(active.id))) {
|
|
26623
|
+
const completed = openingCompletions.get(active.id);
|
|
26624
|
+
this.observeForeignTurn(runtime, active.id, completed ? Promise.resolve(completed) : undefined, active.items);
|
|
26625
|
+
}
|
|
26626
|
+
}
|
|
26287
26627
|
if (mcpServers.length > 0)
|
|
26288
26628
|
this.reportMcpStartup(runtime, mcpStartupGeneration);
|
|
26289
|
-
this.publishAvailableCommands(runtime);
|
|
26629
|
+
this.publishAvailableCommands(runtime, skills);
|
|
26290
26630
|
return runtime;
|
|
26291
26631
|
} catch (error40) {
|
|
26632
|
+
const runtime = this.sessions.get(sessionId);
|
|
26633
|
+
if (runtime) {
|
|
26634
|
+
runtime.session.closed = true;
|
|
26635
|
+
runtime.lifetime.abort();
|
|
26636
|
+
runtime.bridge.dispose();
|
|
26637
|
+
runtime.client.dispose();
|
|
26638
|
+
}
|
|
26292
26639
|
this.sessions.delete(sessionId);
|
|
26293
26640
|
this.codex.detachThread(sessionId);
|
|
26294
|
-
await this.
|
|
26641
|
+
await within(this.unsubscribeThread(sessionId), this.closeGraceMs).catch(() => {});
|
|
26295
26642
|
throw error40;
|
|
26643
|
+
} finally {
|
|
26644
|
+
removeCancellation();
|
|
26645
|
+
stopObservingOpen();
|
|
26296
26646
|
}
|
|
26297
26647
|
}
|
|
26298
|
-
installRuntime(session, capabilities) {
|
|
26648
|
+
installRuntime(session, capabilities, config2, modelProvider) {
|
|
26299
26649
|
const client = new ClientSession(session.id, this.link, capabilities);
|
|
26300
26650
|
const bridge = new EventBridge(client, session);
|
|
26301
26651
|
const turnContext = new TurnContext(session.id);
|
|
26302
|
-
const signal = () =>
|
|
26652
|
+
const signal = () => {
|
|
26653
|
+
const turn = session.activeTurn;
|
|
26654
|
+
return AbortSignal.any([this.codex.disconnectSignal, ...turn ? [turn.abort.signal, turn.stop.signal] : []]);
|
|
26655
|
+
};
|
|
26303
26656
|
const approval = new CodexApprovalHandler(client, turnContext, signal);
|
|
26304
26657
|
const elicitation = new CodexElicitationHandler(client, turnContext, signal);
|
|
26305
|
-
const runtime = { session, client, bridge, turnContext, elicitation, queue: Promise.resolve() };
|
|
26658
|
+
const runtime = { config: config2, modelProvider, stale: false, session, client, bridge, turnContext, elicitation, lifetime: new AbortController, queue: Promise.resolve() };
|
|
26306
26659
|
const drained = (operation) => async (params) => {
|
|
26307
|
-
await this.drain(runtime);
|
|
26660
|
+
await abortable(this.drain(runtime), this.codex.disconnectSignal);
|
|
26308
26661
|
return await operation(params);
|
|
26309
26662
|
};
|
|
26310
26663
|
this.codex.attachThread(session.id, {
|
|
@@ -26323,7 +26676,23 @@ class CodexAgent {
|
|
|
26323
26676
|
return runtime;
|
|
26324
26677
|
}
|
|
26325
26678
|
enqueue(runtime, notification) {
|
|
26679
|
+
if (runtime.lifetime.signal.aborted)
|
|
26680
|
+
return;
|
|
26681
|
+
if (notification.method === "turn/started" && runtime.session.activeTurn === null) {
|
|
26682
|
+
this.observeForeignTurn(runtime, notification.params.turn.id);
|
|
26683
|
+
}
|
|
26684
|
+
const turn = runtime.session.activeTurn;
|
|
26685
|
+
if (notification.method.startsWith("item/") && "turnId" in notification.params && typeof notification.params.turnId === "string") {
|
|
26686
|
+
if (!turn || turn.turnId !== null && notification.params.turnId !== turn.turnId)
|
|
26687
|
+
return;
|
|
26688
|
+
}
|
|
26689
|
+
if (turn && notification.method === "turn/completed")
|
|
26690
|
+
this.completedTurns.set(turn, notification.params.turn.id);
|
|
26691
|
+
if (runtime.lifetime.signal.aborted)
|
|
26692
|
+
return;
|
|
26326
26693
|
const run = async () => {
|
|
26694
|
+
if (runtime.lifetime.signal.aborted)
|
|
26695
|
+
return;
|
|
26327
26696
|
try {
|
|
26328
26697
|
runtime.turnContext.observe(notification);
|
|
26329
26698
|
await runtime.elicitation.observe(notification);
|
|
@@ -26334,11 +26703,21 @@ class CodexAgent {
|
|
|
26334
26703
|
};
|
|
26335
26704
|
runtime.queue = runtime.queue.then(run, run);
|
|
26336
26705
|
}
|
|
26706
|
+
observeForeignTurn(runtime, turnId, completion, items = []) {
|
|
26707
|
+
if (runtime.session.activeTurn || runtime.session.closed)
|
|
26708
|
+
return;
|
|
26709
|
+
const turn = createActiveTurn(runtime.session.id);
|
|
26710
|
+
runtime.session.activeTurn = turn;
|
|
26711
|
+
this.turnStarted(runtime, turn, turnId, runtime.session.id);
|
|
26712
|
+
runtime.client.reportRunning();
|
|
26713
|
+
const completed = completion ?? this.codex.awaitTurnCompleted(runtime.session.id, turnId, turn.stop.signal);
|
|
26714
|
+
this.runPrompt(runtime, turn, { sessionId: runtime.session.id, prompt: [] }, completed, items);
|
|
26715
|
+
}
|
|
26337
26716
|
async drain(runtime) {
|
|
26338
26717
|
let current;
|
|
26339
26718
|
do {
|
|
26340
26719
|
current = runtime.queue;
|
|
26341
|
-
await current;
|
|
26720
|
+
await abortable(current, runtime.client.signal);
|
|
26342
26721
|
} while (runtime.queue !== current);
|
|
26343
26722
|
}
|
|
26344
26723
|
async replayHistory(runtime, replay, thread) {
|
|
@@ -26361,12 +26740,18 @@ class CodexAgent {
|
|
|
26361
26740
|
await publishTitle(firstPage.data);
|
|
26362
26741
|
return;
|
|
26363
26742
|
}
|
|
26743
|
+
const cursors = new Set;
|
|
26364
26744
|
let cursor = null;
|
|
26365
26745
|
do {
|
|
26366
26746
|
const page = await this.turnPage(session.id, cursor);
|
|
26367
26747
|
await publishTitle(page.data);
|
|
26368
26748
|
await client.updateAll(historyUpdates(page.data));
|
|
26369
26749
|
cursor = page.nextCursor;
|
|
26750
|
+
if (cursor !== null) {
|
|
26751
|
+
if (cursors.has(cursor))
|
|
26752
|
+
throw RequestError.internalError({ sessionId: session.id }, "Codex history pagination repeated a cursor; retry after restarting Codex");
|
|
26753
|
+
cursors.add(cursor);
|
|
26754
|
+
}
|
|
26370
26755
|
} while (cursor !== null && !session.closed);
|
|
26371
26756
|
await publishTitle([]);
|
|
26372
26757
|
}
|
|
@@ -26376,7 +26761,7 @@ class CodexAgent {
|
|
|
26376
26761
|
}
|
|
26377
26762
|
async reportMcpStartup(runtime, afterGeneration) {
|
|
26378
26763
|
try {
|
|
26379
|
-
const result = await this.codex.awaitMcpStartup(runtime.session.mcpServerNames, afterGeneration);
|
|
26764
|
+
const result = await this.codex.awaitMcpStartup(runtime.session.mcpServerNames, afterGeneration, runtime.lifetime.signal, runtime.session.id);
|
|
26380
26765
|
if (runtime.session.closed)
|
|
26381
26766
|
return;
|
|
26382
26767
|
for (const failure of result.failed) {
|
|
@@ -26386,15 +26771,18 @@ class CodexAgent {
|
|
|
26386
26771
|
await runtime.client.update(mcpStartupFailed(server, `MCP server "${server}" startup was cancelled.`));
|
|
26387
26772
|
}
|
|
26388
26773
|
} catch (error40) {
|
|
26774
|
+
if (runtime.lifetime.signal.aborted)
|
|
26775
|
+
return;
|
|
26389
26776
|
logger.error("MCP startup reporting failed", error40, { sessionId: runtime.session.id });
|
|
26390
26777
|
}
|
|
26391
26778
|
}
|
|
26392
|
-
async publishAvailableCommands(runtime) {
|
|
26779
|
+
async publishAvailableCommands(runtime, snapshot) {
|
|
26393
26780
|
try {
|
|
26394
|
-
const skills = await this.codex.skillsList({ cwds: [runtime.session.cwd, ...runtime.session.additionalDirectories] });
|
|
26781
|
+
const skills = snapshot ?? await this.codex.skillsList({ cwds: [runtime.session.cwd, ...runtime.session.additionalDirectories] });
|
|
26395
26782
|
if (runtime.session.closed)
|
|
26396
26783
|
return;
|
|
26397
26784
|
await runtime.client.update({ sessionUpdate: "available_commands_update", availableCommands: availableCommands(skills.data) });
|
|
26785
|
+
this.publishedSkills.set(runtime, skills);
|
|
26398
26786
|
} catch (error40) {
|
|
26399
26787
|
logger.error("publishing available commands failed", error40, { sessionId: runtime.session.id });
|
|
26400
26788
|
}
|
|
@@ -26421,49 +26809,92 @@ class CodexAgent {
|
|
|
26421
26809
|
};
|
|
26422
26810
|
}
|
|
26423
26811
|
async closeSession(params) {
|
|
26812
|
+
return await this.admission(() => this.closeSessionInternal(params), params.sessionId);
|
|
26813
|
+
}
|
|
26814
|
+
async closeSessionInternal(params) {
|
|
26424
26815
|
this.requireInitialized("session/close");
|
|
26425
|
-
await this.closeRuntime(params.sessionId);
|
|
26426
|
-
return {};
|
|
26816
|
+
return await this.closeRuntime(params.sessionId);
|
|
26427
26817
|
}
|
|
26428
26818
|
async deleteSession(params) {
|
|
26819
|
+
return await this.admission(() => this.deleteSessionInternal(params), params.sessionId);
|
|
26820
|
+
}
|
|
26821
|
+
async deleteSessionInternal(params) {
|
|
26429
26822
|
this.requireInitialized("session/delete");
|
|
26430
26823
|
await this.closeRuntime(params.sessionId);
|
|
26431
26824
|
await this.withCodex(() => this.codex.threadDelete({ threadId: params.sessionId }));
|
|
26432
26825
|
return {};
|
|
26433
26826
|
}
|
|
26434
26827
|
async archiveSession(params) {
|
|
26828
|
+
return await this.admission(() => this.archiveSessionInternal(params), params.sessionId);
|
|
26829
|
+
}
|
|
26830
|
+
async archiveSessionInternal(params) {
|
|
26435
26831
|
this.requireInitialized("_codex/session_archive");
|
|
26436
26832
|
await this.closeRuntime(params.sessionId);
|
|
26437
26833
|
await this.withCodex(() => this.codex.threadArchive({ threadId: params.sessionId }));
|
|
26438
26834
|
return {};
|
|
26439
26835
|
}
|
|
26440
26836
|
async unarchiveSession(params) {
|
|
26837
|
+
return await this.admission(() => this.unarchiveSessionInternal(params), params.sessionId);
|
|
26838
|
+
}
|
|
26839
|
+
async unarchiveSessionInternal(params) {
|
|
26441
26840
|
this.requireInitialized("_codex/session_unarchive");
|
|
26442
26841
|
await this.withCodex(() => this.codex.threadUnarchive({ threadId: params.sessionId }));
|
|
26443
26842
|
return {};
|
|
26444
26843
|
}
|
|
26445
26844
|
async closeRuntime(sessionId) {
|
|
26845
|
+
const deadline = performance.now() + this.closeGraceMs;
|
|
26446
26846
|
const runtime = this.sessions.get(sessionId);
|
|
26447
26847
|
if (!runtime)
|
|
26448
|
-
return;
|
|
26848
|
+
return {};
|
|
26449
26849
|
runtime.session.closed = true;
|
|
26850
|
+
runtime.lifetime.abort();
|
|
26450
26851
|
const turn = runtime.session.activeTurn;
|
|
26451
26852
|
if (turn) {
|
|
26452
|
-
|
|
26453
|
-
|
|
26454
|
-
if (
|
|
26455
|
-
this.
|
|
26456
|
-
await turn.finished;
|
|
26853
|
+
this.interruptTurn(runtime, turn);
|
|
26854
|
+
await within(turn.finished, Math.max(0, this.closeGraceMs / 2));
|
|
26855
|
+
if (runtime.session.activeTurn === turn) {
|
|
26856
|
+
await within(this.reportIdle(runtime, turn, "cancelled", { usage: usageOf(runtime.session) }), Math.max(0, (deadline - performance.now()) / 2)).catch(() => {});
|
|
26457
26857
|
}
|
|
26858
|
+
turn.stop.abort();
|
|
26458
26859
|
}
|
|
26459
|
-
this.sessions.
|
|
26860
|
+
if (this.sessions.get(sessionId) === runtime)
|
|
26861
|
+
this.sessions.delete(sessionId);
|
|
26460
26862
|
this.codex.detachThread(sessionId);
|
|
26863
|
+
runtime.client.dispose();
|
|
26461
26864
|
runtime.bridge.dispose();
|
|
26462
|
-
|
|
26865
|
+
if (turn) {
|
|
26866
|
+
turn.resolveStarted(null);
|
|
26867
|
+
runtime.session.activeTurn = null;
|
|
26868
|
+
turn.resolveFinished();
|
|
26869
|
+
}
|
|
26870
|
+
const unsubscribe = this.unsubscribeThread(sessionId);
|
|
26871
|
+
let remoteUnsubscribe = "confirmed";
|
|
26872
|
+
try {
|
|
26873
|
+
if (!await within(unsubscribe, Math.max(0, deadline - performance.now()))) {
|
|
26874
|
+
remoteUnsubscribe = "timed_out";
|
|
26875
|
+
}
|
|
26876
|
+
} catch (error40) {
|
|
26877
|
+
remoteUnsubscribe = "failed";
|
|
26463
26878
|
logger.error("thread/unsubscribe failed", error40, { sessionId });
|
|
26464
|
-
}
|
|
26879
|
+
}
|
|
26880
|
+
if (remoteUnsubscribe !== "confirmed")
|
|
26881
|
+
logger.log("session close detached locally", { sessionId, remoteUnsubscribe });
|
|
26882
|
+
return { _meta: { codex: { close: { attempted: true, localDetached: true, remoteUnsubscribe } } } };
|
|
26883
|
+
}
|
|
26884
|
+
unsubscribeThread(sessionId) {
|
|
26885
|
+
const unsubscribe = this.codex.threadUnsubscribe({ threadId: sessionId });
|
|
26886
|
+
this.pendingUnsubscribes.set(sessionId, unsubscribe);
|
|
26887
|
+
const release = () => {
|
|
26888
|
+
if (this.pendingUnsubscribes.get(sessionId) === unsubscribe)
|
|
26889
|
+
this.pendingUnsubscribes.delete(sessionId);
|
|
26890
|
+
};
|
|
26891
|
+
unsubscribe.then(release, release);
|
|
26892
|
+
return unsubscribe;
|
|
26465
26893
|
}
|
|
26466
26894
|
async setSessionConfigOption(params) {
|
|
26895
|
+
return await this.admission(() => this.setSessionConfigOptionInternal(params), params.sessionId);
|
|
26896
|
+
}
|
|
26897
|
+
async setSessionConfigOptionInternal(params) {
|
|
26467
26898
|
const runtime = this.runtime(params.sessionId, "session/set_config_option");
|
|
26468
26899
|
await this.withCodex(() => applyConfigOption(runtime.session, this.codex, params));
|
|
26469
26900
|
const configOptions = sessionConfigOptions(runtime.session);
|
|
@@ -26473,23 +26904,28 @@ class CodexAgent {
|
|
|
26473
26904
|
runtime(sessionId, method) {
|
|
26474
26905
|
this.requireInitialized(method);
|
|
26475
26906
|
const runtime = this.sessions.get(sessionId);
|
|
26476
|
-
if (!runtime)
|
|
26477
|
-
throw RequestError.invalidParams({ sessionId }, `Unknown session "${sessionId}"`);
|
|
26907
|
+
if (!runtime || runtime.session.closed)
|
|
26908
|
+
throw RequestError.invalidParams({ sessionId }, `Unknown session "${sessionId}" (missing or closed); create or resume it first`);
|
|
26478
26909
|
return runtime;
|
|
26479
26910
|
}
|
|
26480
26911
|
async prompt(params) {
|
|
26912
|
+
this.assertRoutingAvailable();
|
|
26913
|
+
if (this.sessionMutations.has(params.sessionId))
|
|
26914
|
+
throw RequestError.invalidRequest({ sessionId: params.sessionId }, "Session lifecycle or configuration work is in progress; retry when it finishes");
|
|
26481
26915
|
const runtime = this.runtime(params.sessionId, "session/prompt");
|
|
26916
|
+
if (runtime.stale)
|
|
26917
|
+
throw RequestError.invalidRequest({ sessionId: params.sessionId, _meta: { codex: { routing: { stale: true } } } }, "Session routing is stale; resume the session or successfully change providers before prompting");
|
|
26482
26918
|
const { session } = runtime;
|
|
26483
26919
|
if (!Array.isArray(params.prompt) || params.prompt.length === 0) {
|
|
26484
26920
|
throw RequestError.invalidParams(undefined, "prompt must contain at least one content block");
|
|
26485
26921
|
}
|
|
26486
|
-
if (session.activeTurn) {
|
|
26487
|
-
return await this.steerActiveTurn(runtime, session.activeTurn, params);
|
|
26488
|
-
}
|
|
26489
26922
|
const model = findModel(session.catalog, session.model.model);
|
|
26490
26923
|
if (!modelSupportsImages(model) && params.prompt.some((block) => block.type === "image")) {
|
|
26491
26924
|
throw RequestError.invalidParams({ model: session.model.model }, "The current model does not support image input");
|
|
26492
26925
|
}
|
|
26926
|
+
if (session.activeTurn) {
|
|
26927
|
+
return await this.steerActiveTurn(runtime, session.activeTurn, params);
|
|
26928
|
+
}
|
|
26493
26929
|
const turn = createActiveTurn(session.id);
|
|
26494
26930
|
session.activeTurn = turn;
|
|
26495
26931
|
runtime.client.reportRunning();
|
|
@@ -26505,71 +26941,55 @@ class CodexAgent {
|
|
|
26505
26941
|
await this.codex.turnSteer({ threadId: runtime.session.id, expectedTurnId: turnId, input: toUserInput(params.prompt) });
|
|
26506
26942
|
return { _meta: { codex: { steered: turnId } } };
|
|
26507
26943
|
} catch (error40) {
|
|
26944
|
+
if (this.completedTurns.get(turn) === turnId || runtime.session.activeTurn !== turn) {
|
|
26945
|
+
await turn.finished;
|
|
26946
|
+
return await this.prompt(params);
|
|
26947
|
+
}
|
|
26508
26948
|
throw RequestError.invalidRequest({ sessionId: params.sessionId, turnId }, `Could not steer the running turn: ${errorMessage(error40)}`);
|
|
26509
26949
|
}
|
|
26510
26950
|
}
|
|
26511
|
-
async runPrompt(runtime, turn, params) {
|
|
26512
|
-
const { session,
|
|
26951
|
+
async runPrompt(runtime, turn, params, observed, items = []) {
|
|
26952
|
+
const { session, bridge } = runtime;
|
|
26513
26953
|
bridge.beginTurn();
|
|
26954
|
+
if (items.length)
|
|
26955
|
+
runtime.queue = runtime.queue.then(() => bridge.restore(items));
|
|
26514
26956
|
try {
|
|
26515
26957
|
const command = parseCommand(params.prompt);
|
|
26516
26958
|
const outcome = command ? resolveCommand(command, session) : { kind: "prompt" };
|
|
26517
|
-
let completed =
|
|
26518
|
-
switch (outcome.kind) {
|
|
26519
|
-
case "prompt":
|
|
26520
|
-
completed = await this.runCodexTurn(runtime, turn, params.prompt);
|
|
26521
|
-
break;
|
|
26522
|
-
case "message":
|
|
26523
|
-
await client.update(agentMessage(`command:${command?.name}:${Date.now()}`, await this.commandText(runtime, command?.name ?? "", outcome.text)));
|
|
26524
|
-
break;
|
|
26525
|
-
case "config":
|
|
26526
|
-
await this.setSessionConfigOption({ sessionId: session.id, configId: outcome.configId, type: "id", value: outcome.value });
|
|
26527
|
-
break;
|
|
26528
|
-
case "compact":
|
|
26529
|
-
await this.runCompaction(runtime, turn);
|
|
26530
|
-
break;
|
|
26531
|
-
case "review":
|
|
26532
|
-
completed = await this.withCodex(() => this.codex.runReview({ threadId: session.id, target: outcome.target, delivery: "inline" }, (turnId, threadId) => {
|
|
26533
|
-
turn.turnId = turnId;
|
|
26534
|
-
turn.threadId = threadId;
|
|
26535
|
-
turn.resolveStarted(turnId);
|
|
26536
|
-
}));
|
|
26537
|
-
break;
|
|
26538
|
-
case "logout":
|
|
26539
|
-
await this.withCodex(() => logout(this.codex));
|
|
26540
|
-
await this.refreshAccounts();
|
|
26541
|
-
await client.update(agentMessage(`command:logout:${Date.now()}`, "Logged out of the Codex account."));
|
|
26542
|
-
break;
|
|
26543
|
-
}
|
|
26959
|
+
let completed = await abortable(observed ?? this.executePrompt(runtime, turn, command, outcome, params), turn.stop.signal);
|
|
26544
26960
|
await this.drain(runtime);
|
|
26545
26961
|
await bridge.flush();
|
|
26546
|
-
if (completed?.turn.status === "completed" && !turn.abort.signal.aborted) {
|
|
26547
|
-
completed = await this.maybeImplementPlan(runtime, turn, completed);
|
|
26962
|
+
if (completed?.turn.status === "completed" && !turn.abort.signal.aborted && !this.exitHandled) {
|
|
26963
|
+
completed = await abortable(this.maybeImplementPlan(runtime, turn, completed), turn.stop.signal);
|
|
26548
26964
|
}
|
|
26549
26965
|
if (completed?.turn.status === "interrupted" || turn.abort.signal.aborted) {
|
|
26550
|
-
await
|
|
26551
|
-
await client.reportIdle("cancelled", { usage: usageOf(session) });
|
|
26966
|
+
await this.reportIdle(runtime, turn, "cancelled", { usage: usageOf(session) });
|
|
26552
26967
|
return;
|
|
26553
26968
|
}
|
|
26554
26969
|
if (completed?.turn.status === "failed") {
|
|
26555
|
-
await this.reportTurnFailure(runtime, completed.turn.error ?? bridge.takeError() ?? { message: "Turn failed", codexErrorInfo: null, additionalDetails: null, misalignment: null });
|
|
26970
|
+
await this.reportTurnFailure(runtime, turn, completed.turn.error ?? bridge.takeError() ?? { message: "Turn failed", codexErrorInfo: null, additionalDetails: null, misalignment: null });
|
|
26556
26971
|
return;
|
|
26557
26972
|
}
|
|
26558
26973
|
const pendingError = bridge.takeError();
|
|
26559
26974
|
if (pendingError) {
|
|
26560
|
-
await this.reportTurnFailure(runtime, pendingError);
|
|
26975
|
+
await this.reportTurnFailure(runtime, turn, pendingError);
|
|
26561
26976
|
return;
|
|
26562
26977
|
}
|
|
26563
26978
|
await this.publishFallbackTitle(runtime, promptTitle(params.prompt));
|
|
26564
|
-
await
|
|
26979
|
+
await this.reportIdle(runtime, turn, "end_turn", { usage: usageOf(session) });
|
|
26565
26980
|
} catch (error40) {
|
|
26981
|
+
if (this.terminalTurns.has(turn)) {
|
|
26982
|
+
logger.error("Terminal update failed", error40, { sessionId: session.id });
|
|
26983
|
+
return;
|
|
26984
|
+
}
|
|
26985
|
+
if (turn.stop.signal.aborted && session.closed)
|
|
26986
|
+
return;
|
|
26566
26987
|
if (turn.abort.signal.aborted || session.closed) {
|
|
26567
|
-
await
|
|
26568
|
-
await client.reportIdle("cancelled", { usage: usageOf(session) }).catch(() => {});
|
|
26988
|
+
await this.reportIdle(runtime, turn, "cancelled", { usage: usageOf(session) }).catch(() => {});
|
|
26569
26989
|
return;
|
|
26570
26990
|
}
|
|
26571
26991
|
logger.error("prompt failed", error40, { sessionId: session.id });
|
|
26572
|
-
await this.reportTurnFailure(runtime,
|
|
26992
|
+
await this.reportTurnFailure(runtime, turn, this.failureOf(error40)).catch(() => {});
|
|
26573
26993
|
} finally {
|
|
26574
26994
|
turn.resolveStarted(null);
|
|
26575
26995
|
if (session.activeTurn === turn)
|
|
@@ -26577,49 +26997,106 @@ class CodexAgent {
|
|
|
26577
26997
|
turn.resolveFinished();
|
|
26578
26998
|
}
|
|
26579
26999
|
}
|
|
27000
|
+
async executePrompt(runtime, turn, command, outcome, params) {
|
|
27001
|
+
const { session, client } = runtime;
|
|
27002
|
+
let completed = null;
|
|
27003
|
+
switch (outcome.kind) {
|
|
27004
|
+
case "prompt":
|
|
27005
|
+
completed = await this.runCodexTurn(runtime, turn, params.prompt);
|
|
27006
|
+
break;
|
|
27007
|
+
case "message":
|
|
27008
|
+
await client.update(agentMessage(`command:${command?.name}:${Date.now()}`, await this.commandText(runtime, command?.name ?? "", outcome.text)));
|
|
27009
|
+
break;
|
|
27010
|
+
case "config":
|
|
27011
|
+
await this.setSessionConfigOption({ sessionId: session.id, configId: outcome.configId, type: "id", value: outcome.value });
|
|
27012
|
+
break;
|
|
27013
|
+
case "compact":
|
|
27014
|
+
await this.runCompaction(runtime, turn);
|
|
27015
|
+
break;
|
|
27016
|
+
case "review":
|
|
27017
|
+
completed = await this.withCodex(() => this.codex.runReview({ threadId: session.id, target: outcome.target, delivery: "inline" }, (turnId, threadId) => {
|
|
27018
|
+
this.turnStarted(runtime, turn, turnId, threadId);
|
|
27019
|
+
}, turn.stop.signal));
|
|
27020
|
+
break;
|
|
27021
|
+
case "logout":
|
|
27022
|
+
await this.withCodex(() => logout(this.codex));
|
|
27023
|
+
await this.refreshAccounts();
|
|
27024
|
+
await client.update(agentMessage(`command:logout:${Date.now()}`, "Logged out of the Codex account."));
|
|
27025
|
+
break;
|
|
27026
|
+
}
|
|
27027
|
+
return completed;
|
|
27028
|
+
}
|
|
26580
27029
|
async runCodexTurn(runtime, turn, prompt) {
|
|
26581
27030
|
const { session } = runtime;
|
|
26582
27031
|
const model = findModel(session.catalog, session.model.model);
|
|
26583
27032
|
const disableSummary = session.account?.type === "apiKey" || modelLacksReasoning(model);
|
|
26584
|
-
|
|
26585
|
-
|
|
26586
|
-
|
|
26587
|
-
|
|
26588
|
-
|
|
26589
|
-
|
|
26590
|
-
|
|
26591
|
-
|
|
26592
|
-
|
|
26593
|
-
|
|
26594
|
-
|
|
26595
|
-
|
|
26596
|
-
|
|
26597
|
-
|
|
26598
|
-
|
|
26599
|
-
|
|
26600
|
-
|
|
26601
|
-
|
|
27033
|
+
let startSent = false;
|
|
27034
|
+
const beforeStart = new AbortController;
|
|
27035
|
+
const cancelBeforeStart = () => {
|
|
27036
|
+
if (!startSent)
|
|
27037
|
+
beforeStart.abort(turn.abort.signal.reason);
|
|
27038
|
+
};
|
|
27039
|
+
turn.abort.signal.addEventListener("abort", cancelBeforeStart, { once: true });
|
|
27040
|
+
if (turn.abort.signal.aborted)
|
|
27041
|
+
cancelBeforeStart();
|
|
27042
|
+
try {
|
|
27043
|
+
const result = await abortable(this.withSkillsContext(session.cwd, session.additionalDirectories, async (skills) => {
|
|
27044
|
+
if (this.publishedSkills.get(runtime) !== skills)
|
|
27045
|
+
this.publishAvailableCommands(runtime, skills);
|
|
27046
|
+
if (turn.abort.signal.aborted || turn.stop.signal.aborted)
|
|
27047
|
+
return { completed: Promise.resolve(interruptedTurn(session.id)) };
|
|
27048
|
+
startSent = true;
|
|
27049
|
+
const completed = this.withCodex(() => this.codex.runTurn({
|
|
27050
|
+
threadId: session.id,
|
|
27051
|
+
input: toUserInput(prompt),
|
|
27052
|
+
approvalPolicy: session.mode.approvalPolicy,
|
|
27053
|
+
approvalsReviewer: session.mode.approvalsReviewer,
|
|
27054
|
+
sandboxPolicy: withWritableRoots(session.mode.sandboxPolicy, session.additionalDirectories),
|
|
27055
|
+
model: session.model.model,
|
|
27056
|
+
effort: session.model.effort,
|
|
27057
|
+
summary: disableSummary ? "none" : "auto",
|
|
27058
|
+
serviceTier: session.fastMode ? FAST_SERVICE_TIER : null
|
|
27059
|
+
}, (turnId) => {
|
|
27060
|
+
this.turnStarted(runtime, turn, turnId, session.id);
|
|
27061
|
+
}, turn.stop.signal));
|
|
27062
|
+
await Promise.race([turn.started, completed]);
|
|
27063
|
+
return { completed };
|
|
27064
|
+
}), beforeStart.signal);
|
|
27065
|
+
return await result.completed;
|
|
27066
|
+
} finally {
|
|
27067
|
+
turn.abort.signal.removeEventListener("abort", cancelBeforeStart);
|
|
27068
|
+
}
|
|
26602
27069
|
}
|
|
26603
27070
|
async runCompaction(runtime, turn) {
|
|
26604
27071
|
const threadId = runtime.session.id;
|
|
27072
|
+
turn.resolveStarted(null);
|
|
27073
|
+
const pending = new AbortController;
|
|
27074
|
+
const signal = AbortSignal.any([pending.signal, turn.abort.signal]);
|
|
26605
27075
|
const completed = Promise.race([
|
|
26606
|
-
this.codex.awaitNotification("item/completed", (params) => params.threadId === threadId && params.item.type === "contextCompaction"),
|
|
26607
|
-
this.codex.awaitNotification("thread/compacted", (params) => params.threadId === threadId)
|
|
27076
|
+
this.codex.awaitNotification("item/completed", (params) => params.threadId === threadId && params.item.type === "contextCompaction", signal),
|
|
27077
|
+
this.codex.awaitNotification("thread/compacted", (params) => params.threadId === threadId, signal)
|
|
26608
27078
|
]);
|
|
26609
|
-
|
|
26610
|
-
|
|
26611
|
-
|
|
27079
|
+
completed.catch(() => {});
|
|
27080
|
+
try {
|
|
27081
|
+
signal.throwIfAborted();
|
|
27082
|
+
await abortable(this.withCodex(() => this.codex.threadCompactStart({ threadId })), signal);
|
|
27083
|
+
await completed;
|
|
27084
|
+
} finally {
|
|
27085
|
+
pending.abort();
|
|
27086
|
+
}
|
|
26612
27087
|
}
|
|
26613
27088
|
async maybeImplementPlan(runtime, turn, completed) {
|
|
26614
27089
|
const plan = runtime.bridge.takeCompletedPlan();
|
|
26615
27090
|
if (!plan || runtime.session.collaborationMode !== PLAN_COLLABORATION_MODE)
|
|
26616
27091
|
return completed;
|
|
26617
|
-
|
|
26618
|
-
|
|
27092
|
+
this.completedTurns.delete(turn);
|
|
27093
|
+
const signal = AbortSignal.any([turn.abort.signal, turn.stop.signal]);
|
|
27094
|
+
const approved = await this.requestPlanApproval(runtime, plan, signal);
|
|
27095
|
+
if (!approved || signal.aborted)
|
|
26619
27096
|
return completed;
|
|
26620
27097
|
await this.setSessionConfigOption({ sessionId: runtime.session.id, configId: "collaboration_mode", type: "id", value: DEFAULT_COLLABORATION_MODE });
|
|
26621
27098
|
runtime.bridge.beginTurn();
|
|
26622
|
-
turn.
|
|
27099
|
+
turn.resetStarted();
|
|
26623
27100
|
const implementation = await this.runCodexTurn(runtime, turn, [{ type: "text", text: "Implement the approved plan." }]);
|
|
26624
27101
|
await this.drain(runtime);
|
|
26625
27102
|
await runtime.bridge.flush();
|
|
@@ -26627,13 +27104,12 @@ class CodexAgent {
|
|
|
26627
27104
|
}
|
|
26628
27105
|
async requestPlanApproval(runtime, plan, signal) {
|
|
26629
27106
|
const toolCallId = `plan-review:${plan.itemId}`;
|
|
27107
|
+
const toolCall = { toolCallId, name: ToolName.PlanReview, title: "Implement this plan?", kind: "switch_mode", status: "pending", rawInput: { plan: plan.text } };
|
|
26630
27108
|
try {
|
|
27109
|
+
await runtime.client.update({ sessionUpdate: "tool_call_update", ...toolCall });
|
|
26631
27110
|
const response = await runtime.client.requestPermission({
|
|
26632
27111
|
title: "Implement this plan?",
|
|
26633
|
-
subject: {
|
|
26634
|
-
type: "tool_call",
|
|
26635
|
-
toolCall: { toolCallId, name: ToolName.PlanReview, title: "Implement this plan?", kind: "switch_mode", status: "pending", rawInput: { plan: plan.text } }
|
|
26636
|
-
},
|
|
27112
|
+
subject: { type: "tool_call", toolCall },
|
|
26637
27113
|
options: [
|
|
26638
27114
|
{ optionId: IMPLEMENT_PLAN_OPTION, name: "Yes, implement this plan", kind: "allow_once" },
|
|
26639
27115
|
{ optionId: REVISE_PLAN_OPTION, name: "No, and tell Codex what to do differently", kind: "reject_once" }
|
|
@@ -26650,6 +27126,7 @@ class CodexAgent {
|
|
|
26650
27126
|
return approved;
|
|
26651
27127
|
} catch (error40) {
|
|
26652
27128
|
logger.error("plan approval failed", error40, { sessionId: runtime.session.id });
|
|
27129
|
+
await runtime.client.update({ sessionUpdate: "tool_call_update", toolCallId, status: signal.aborted ? "cancelled" : "failed" }).catch(() => {});
|
|
26653
27130
|
return false;
|
|
26654
27131
|
}
|
|
26655
27132
|
}
|
|
@@ -26667,7 +27144,17 @@ class CodexAgent {
|
|
|
26667
27144
|
return fallback;
|
|
26668
27145
|
}
|
|
26669
27146
|
}
|
|
26670
|
-
async
|
|
27147
|
+
async reportIdle(runtime, turn, reason, extra) {
|
|
27148
|
+
if (this.terminalTurns.has(turn))
|
|
27149
|
+
return;
|
|
27150
|
+
this.terminalTurns.add(turn);
|
|
27151
|
+
await runtime.bridge.finishOpenToolCalls(reason === "cancelled" ? "cancelled" : reason === "end_turn" ? "completed" : "failed");
|
|
27152
|
+
if (runtime.session.activeTurn === turn)
|
|
27153
|
+
runtime.session.activeTurn = null;
|
|
27154
|
+
await runtime.client.reportIdle(reason, extra);
|
|
27155
|
+
}
|
|
27156
|
+
async reportTurnFailure(runtime, turn, error40) {
|
|
27157
|
+
const { stopReason, ...classification } = classifyTurnError(error40.codexErrorInfo);
|
|
26671
27158
|
const message = error40.additionalDetails ? `${error40.message}
|
|
26672
27159
|
|
|
26673
27160
|
${error40.additionalDetails}` : error40.message;
|
|
@@ -26675,11 +27162,11 @@ ${error40.additionalDetails}` : error40.message;
|
|
|
26675
27162
|
sessionUpdate: "agent_message_chunk",
|
|
26676
27163
|
messageId: `codex-error:${runtime.session.id}:${Date.now()}`,
|
|
26677
27164
|
content: { type: "text", text: message },
|
|
26678
|
-
_meta: { codex: { error: { message: error40.message, codexErrorInfo: error40.codexErrorInfo, additionalDetails: error40.additionalDetails } } }
|
|
27165
|
+
_meta: { codex: { error: { ...classification, message: error40.message, codexErrorInfo: error40.codexErrorInfo, additionalDetails: error40.additionalDetails } } }
|
|
26679
27166
|
});
|
|
26680
|
-
await
|
|
27167
|
+
await this.reportIdle(runtime, turn, stopReason, {
|
|
26681
27168
|
usage: usageOf(runtime.session),
|
|
26682
|
-
_meta: { codex: { error: { message: error40.message, codexErrorInfo: error40.codexErrorInfo } } }
|
|
27169
|
+
_meta: { codex: { error: { ...classification, message: error40.message, codexErrorInfo: error40.codexErrorInfo } } }
|
|
26683
27170
|
});
|
|
26684
27171
|
}
|
|
26685
27172
|
async publishFallbackTitle(runtime, title) {
|
|
@@ -26701,19 +27188,77 @@ ${error40.additionalDetails}` : error40.message;
|
|
|
26701
27188
|
const turnId = turn.turnId ?? await turn.started;
|
|
26702
27189
|
if (turnId === null)
|
|
26703
27190
|
return;
|
|
26704
|
-
|
|
26705
|
-
|
|
26706
|
-
|
|
26707
|
-
|
|
27191
|
+
await this.sendInterrupt(runtime, turn, turnId);
|
|
27192
|
+
}
|
|
27193
|
+
sendInterrupt(runtime, turn, turnId) {
|
|
27194
|
+
const key = `${turn.threadId}:${turnId}`;
|
|
27195
|
+
let pending = turn.interrupts.get(key);
|
|
27196
|
+
if (!pending) {
|
|
27197
|
+
pending = this.codex.turnInterrupt({ threadId: turn.threadId, turnId }).then(() => {}, (error40) => {
|
|
27198
|
+
turn.interrupts.delete(key);
|
|
27199
|
+
logger.error("turn/interrupt failed", error40, { sessionId: runtime.session.id, turnId });
|
|
27200
|
+
});
|
|
27201
|
+
turn.interrupts.set(key, pending);
|
|
27202
|
+
}
|
|
27203
|
+
return pending;
|
|
27204
|
+
}
|
|
27205
|
+
turnStarted(runtime, turn, turnId, threadId) {
|
|
27206
|
+
turn.turnId = turnId;
|
|
27207
|
+
turn.threadId = threadId;
|
|
27208
|
+
turn.resolveStarted(turnId);
|
|
27209
|
+
if (turn.abort.signal.aborted)
|
|
27210
|
+
this.sendInterrupt(runtime, turn, turnId);
|
|
27211
|
+
}
|
|
27212
|
+
withSkillsContext(cwd, additionalDirectories, operation) {
|
|
27213
|
+
const run = async () => {
|
|
27214
|
+
const roots = additionalDirectories.map((root) => path6.join(root, ".agents", "skills"));
|
|
27215
|
+
if (roots.length !== this.skillRoots.length || roots.some((root, index) => root !== this.skillRoots[index])) {
|
|
27216
|
+
this.changingSkillRoots = true;
|
|
27217
|
+
try {
|
|
27218
|
+
await this.codex.skillsExtraRootsSet({ extraRoots: roots });
|
|
27219
|
+
} finally {
|
|
27220
|
+
this.changingSkillRoots = false;
|
|
27221
|
+
}
|
|
27222
|
+
this.skillRoots = roots;
|
|
27223
|
+
this.skillSnapshots.clear();
|
|
27224
|
+
}
|
|
27225
|
+
const skills = await this.loadSkills(cwd, additionalDirectories);
|
|
27226
|
+
return await operation(skills);
|
|
27227
|
+
};
|
|
27228
|
+
const result = this.skillsQueue.then(run, run);
|
|
27229
|
+
this.skillsQueue = result.then(() => {}, () => {});
|
|
27230
|
+
return result;
|
|
27231
|
+
}
|
|
27232
|
+
async loadSkills(cwd, additionalDirectories) {
|
|
27233
|
+
const key = JSON.stringify([cwd, ...additionalDirectories]);
|
|
27234
|
+
let skills = this.skillSnapshots.get(key);
|
|
27235
|
+
if (!skills) {
|
|
27236
|
+
const generation = this.skillsGeneration;
|
|
27237
|
+
skills = await this.withCodex(() => this.codex.skillsList({ cwds: [cwd, ...additionalDirectories], forceReload: true }));
|
|
27238
|
+
if (generation === this.skillsGeneration) {
|
|
27239
|
+
if (this.skillSnapshots.size >= 64)
|
|
27240
|
+
this.skillSnapshots.delete(this.skillSnapshots.keys().next().value);
|
|
27241
|
+
this.skillSnapshots.set(key, skills);
|
|
27242
|
+
}
|
|
26708
27243
|
}
|
|
27244
|
+
return skills;
|
|
26709
27245
|
}
|
|
26710
|
-
async
|
|
26711
|
-
const
|
|
26712
|
-
|
|
26713
|
-
|
|
26714
|
-
|
|
27246
|
+
async refreshAvailableCommands(runtime) {
|
|
27247
|
+
const run = async () => {
|
|
27248
|
+
const roots = runtime.session.additionalDirectories.map((root) => path6.join(root, ".agents", "skills"));
|
|
27249
|
+
if (runtime.session.closed || roots.length !== this.skillRoots.length || roots.some((root, i) => root !== this.skillRoots[i]))
|
|
27250
|
+
return;
|
|
27251
|
+
return await this.loadSkills(runtime.session.cwd, runtime.session.additionalDirectories);
|
|
27252
|
+
};
|
|
27253
|
+
const pending = this.skillsQueue.then(run, run);
|
|
27254
|
+
this.skillsQueue = pending.then(() => {}, () => {});
|
|
27255
|
+
try {
|
|
27256
|
+
const skills = await pending;
|
|
27257
|
+
if (skills && !runtime.session.closed)
|
|
27258
|
+
await this.publishAvailableCommands(runtime, skills);
|
|
27259
|
+
} catch (error40) {
|
|
27260
|
+
logger.error("refreshing available commands failed", error40, { sessionId: runtime.session.id });
|
|
26715
27261
|
}
|
|
26716
|
-
await this.codex.skillsList({ cwds: [cwd, ...additionalDirectories], forceReload: true });
|
|
26717
27262
|
}
|
|
26718
27263
|
async configuredMcpServerNames(cwd) {
|
|
26719
27264
|
const response = await this.codex.configRead({ includeLayers: true, cwd });
|
|
@@ -26726,13 +27271,13 @@ ${error40.additionalDetails}` : error40.message;
|
|
|
26726
27271
|
}
|
|
26727
27272
|
return names;
|
|
26728
27273
|
}
|
|
26729
|
-
async resolveModelProvider() {
|
|
26730
|
-
const routed =
|
|
27274
|
+
async resolveModelProvider(routing = this.providers) {
|
|
27275
|
+
const routed = routing.modelProvider();
|
|
26731
27276
|
if (routed)
|
|
26732
27277
|
return routed;
|
|
26733
27278
|
const config2 = await this.codex.configRead({ includeLayers: false });
|
|
26734
27279
|
const provider = config2.config["model_provider"];
|
|
26735
|
-
return typeof provider === "string" && provider.length > 0 ? provider :
|
|
27280
|
+
return typeof provider === "string" && provider.length > 0 ? provider : OPENAI_PROVIDER_ID;
|
|
26736
27281
|
}
|
|
26737
27282
|
async withCodex(operation) {
|
|
26738
27283
|
try {
|
|
@@ -26749,6 +27294,15 @@ ${stderr}` : ""}`);
|
|
|
26749
27294
|
throw RequestError.internalError({ details: errorMessage(error40) }, errorMessage(error40));
|
|
26750
27295
|
}
|
|
26751
27296
|
}
|
|
27297
|
+
failureOf(error40) {
|
|
27298
|
+
const details = error40 instanceof Error ? error40.additionalDetails : undefined;
|
|
27299
|
+
return {
|
|
27300
|
+
message: this.describeFailure(error40),
|
|
27301
|
+
codexErrorInfo: null,
|
|
27302
|
+
additionalDetails: typeof details === "string" && details.length > 0 ? details : null,
|
|
27303
|
+
misalignment: null
|
|
27304
|
+
};
|
|
27305
|
+
}
|
|
26752
27306
|
describeFailure(error40) {
|
|
26753
27307
|
if (error40 instanceof RequestError) {
|
|
26754
27308
|
const details = error40.data?.details;
|
|
@@ -26763,14 +27317,16 @@ ${stderr}` : ""}`);
|
|
|
26763
27317
|
this.exitHandled = true;
|
|
26764
27318
|
for (const runtime of this.sessions.values()) {
|
|
26765
27319
|
const turn = runtime.session.activeTurn;
|
|
26766
|
-
if (!turn)
|
|
27320
|
+
if (!turn || this.completedTurns.get(turn) === turn.turnId)
|
|
26767
27321
|
continue;
|
|
27322
|
+
const stderr = this.process?.recentStderr() || null;
|
|
27323
|
+
turn.stop.abort(Object.assign(new Error("Connection to Codex was lost"), { additionalDetails: stderr }));
|
|
26768
27324
|
turn.resolveStarted(null);
|
|
26769
27325
|
if (turn.turnId) {
|
|
26770
27326
|
this.codex.failTurn(turn.threadId, turn.turnId, {
|
|
26771
27327
|
message: "Connection to Codex was lost",
|
|
26772
27328
|
codexErrorInfo: null,
|
|
26773
|
-
additionalDetails:
|
|
27329
|
+
additionalDetails: stderr,
|
|
26774
27330
|
misalignment: null
|
|
26775
27331
|
});
|
|
26776
27332
|
}
|
|
@@ -26825,6 +27381,19 @@ function seedItem(message) {
|
|
|
26825
27381
|
content: [{ type: message.role === "user" ? "input_text" : "output_text", text: message.text }]
|
|
26826
27382
|
};
|
|
26827
27383
|
}
|
|
27384
|
+
async function within(operation, ms) {
|
|
27385
|
+
let timer;
|
|
27386
|
+
try {
|
|
27387
|
+
return await Promise.race([
|
|
27388
|
+
operation.then(() => true),
|
|
27389
|
+
new Promise((resolve) => {
|
|
27390
|
+
timer = setTimeout(() => resolve(false), ms);
|
|
27391
|
+
})
|
|
27392
|
+
]);
|
|
27393
|
+
} finally {
|
|
27394
|
+
clearTimeout(timer);
|
|
27395
|
+
}
|
|
27396
|
+
}
|
|
26828
27397
|
|
|
26829
27398
|
// src/agent/createAgent.ts
|
|
26830
27399
|
function createAgentApp(options) {
|
|
@@ -26836,7 +27405,7 @@ function createAgentApp(options) {
|
|
|
26836
27405
|
};
|
|
26837
27406
|
return agent({ name: options.info.name }).onConnect((connection) => {
|
|
26838
27407
|
agent2 = new CodexAgent(connection.client, options);
|
|
26839
|
-
}).onRequest(methods.agent.initialize, (ctx) => current().initialize(ctx.params)).onRequest(methods.agent.auth.login, (ctx) => current().login(ctx.params, ctx.requestId)).onRequest(methods.agent.auth.logout, (ctx) => current().logout(ctx.params)).onRequest(methods.agent.providers.list, (ctx) => current().listProviders(ctx.params)).onRequest(methods.agent.providers.set, (ctx) => current().setProvider(ctx.params)).onRequest(methods.agent.providers.disable, (ctx) => current().disableProvider(ctx.params)).onRequest(methods.agent.session.new, (ctx) => current().newSession(ctx.params)).onRequest(methods.agent.session.resume, (ctx) => current().resumeSession(ctx.params)).onRequest(methods.agent.session.fork, (ctx) => current().forkSession(ctx.params)).onRequest(methods.agent.session.list, (ctx) => current().listSessions(ctx.params)).onRequest(methods.agent.session.close, (ctx) => current().closeSession(ctx.params)).onRequest(methods.agent.session.delete, (ctx) => current().deleteSession(ctx.params)).onRequest("_codex/session_archive", { parse: parseSessionIdParams }, (ctx) => current().archiveSession(ctx.params)).onRequest("_codex/session_unarchive", { parse: parseSessionIdParams }, (ctx) => current().unarchiveSession(ctx.params)).onRequest(methods.agent.session.setConfigOption, (ctx) => current().setSessionConfigOption(ctx.params)).onRequest(methods.agent.session.prompt, (ctx) => current().prompt(ctx.params)).onNotification(methods.agent.session.cancel, (ctx) => current().cancel(ctx.params));
|
|
27408
|
+
}).onRequest(methods.agent.initialize, (ctx) => current().initialize(ctx.params)).onRequest(methods.agent.auth.login, (ctx) => current().login(ctx.params, ctx.requestId)).onRequest(methods.agent.auth.logout, (ctx) => current().logout(ctx.params)).onRequest(methods.agent.providers.list, (ctx) => current().listProviders(ctx.params)).onRequest(methods.agent.providers.set, (ctx) => current().setProvider(ctx.params)).onRequest(methods.agent.providers.disable, (ctx) => current().disableProvider(ctx.params)).onRequest(methods.agent.session.new, (ctx) => current().newSession(ctx.params, ctx.signal)).onRequest(methods.agent.session.resume, (ctx) => current().resumeSession(ctx.params, ctx.signal)).onRequest(methods.agent.session.fork, (ctx) => current().forkSession(ctx.params, ctx.signal)).onRequest(methods.agent.session.list, (ctx) => current().listSessions(ctx.params)).onRequest(methods.agent.session.close, (ctx) => current().closeSession(ctx.params)).onRequest(methods.agent.session.delete, (ctx) => current().deleteSession(ctx.params)).onRequest("_codex/session_archive", { parse: parseSessionIdParams }, (ctx) => current().archiveSession(ctx.params)).onRequest("_codex/session_unarchive", { parse: parseSessionIdParams }, (ctx) => current().unarchiveSession(ctx.params)).onRequest(methods.agent.session.setConfigOption, (ctx) => current().setSessionConfigOption(ctx.params)).onRequest(methods.agent.session.prompt, (ctx) => current().prompt(ctx.params)).onNotification(methods.agent.session.cancel, (ctx) => current().cancel(ctx.params));
|
|
26840
27409
|
}
|
|
26841
27410
|
|
|
26842
27411
|
// src/codex/AppServerClient.ts
|
|
@@ -26854,12 +27423,18 @@ class AppServerClient {
|
|
|
26854
27423
|
elicitationHandlers = new Map;
|
|
26855
27424
|
turnCompletionWaiters = new Map;
|
|
26856
27425
|
earlyTurnCompletions = new Map;
|
|
27426
|
+
startingThreads = new Map;
|
|
27427
|
+
startingReviews = 0;
|
|
27428
|
+
modelCatalog = null;
|
|
26857
27429
|
mcpStartupStates = new Map;
|
|
26858
27430
|
mcpStartupWaiters = [];
|
|
26859
27431
|
notificationWaiters = [];
|
|
26860
27432
|
mcpStartupVersion = 0;
|
|
27433
|
+
disconnected = new AbortController;
|
|
26861
27434
|
constructor(connection) {
|
|
26862
27435
|
this.connection = connection;
|
|
27436
|
+
connection.onClose(() => this.disconnected.abort(new Error("Connection to Codex was lost")));
|
|
27437
|
+
connection.onDispose(() => this.disconnected.abort(new Error("Connection to Codex was lost")));
|
|
26863
27438
|
connection.onUnhandledNotification((message) => {
|
|
26864
27439
|
this.dispatchNotification(message);
|
|
26865
27440
|
});
|
|
@@ -26884,6 +27459,9 @@ class AppServerClient {
|
|
|
26884
27459
|
return handler ? await handler.handleUserInput(params) : { answers: {} };
|
|
26885
27460
|
});
|
|
26886
27461
|
}
|
|
27462
|
+
get disconnectSignal() {
|
|
27463
|
+
return this.disconnected.signal;
|
|
27464
|
+
}
|
|
26887
27465
|
attachThread(threadId, handlers) {
|
|
26888
27466
|
this.threadHandlers.set(threadId, handlers.notification);
|
|
26889
27467
|
this.approvalHandlers.set(threadId, handlers.approval);
|
|
@@ -26893,18 +27471,49 @@ class AppServerClient {
|
|
|
26893
27471
|
this.threadHandlers.delete(threadId);
|
|
26894
27472
|
this.approvalHandlers.delete(threadId);
|
|
26895
27473
|
this.elicitationHandlers.delete(threadId);
|
|
27474
|
+
for (const key of this.mcpStartupStates.keys()) {
|
|
27475
|
+
if (key.startsWith(`${threadId}\x00`))
|
|
27476
|
+
this.mcpStartupStates.delete(key);
|
|
27477
|
+
}
|
|
26896
27478
|
}
|
|
26897
|
-
awaitNotification(method, predicate = () => true) {
|
|
27479
|
+
awaitNotification(method, predicate = () => true, signal) {
|
|
27480
|
+
let waiter;
|
|
26898
27481
|
const settled = new Promise((resolve) => {
|
|
26899
|
-
|
|
27482
|
+
waiter = {
|
|
26900
27483
|
method,
|
|
26901
27484
|
matches: (params) => predicate(params),
|
|
26902
27485
|
resolve
|
|
26903
|
-
}
|
|
27486
|
+
};
|
|
27487
|
+
this.notificationWaiters.push(waiter);
|
|
26904
27488
|
});
|
|
26905
|
-
|
|
27489
|
+
const result = abortable(settled, signal ? AbortSignal.any([signal, this.disconnected.signal]) : this.disconnected.signal).finally(() => {
|
|
27490
|
+
const index = this.notificationWaiters.indexOf(waiter);
|
|
27491
|
+
if (index >= 0)
|
|
27492
|
+
this.notificationWaiters.splice(index, 1);
|
|
27493
|
+
});
|
|
27494
|
+
result.catch(() => {});
|
|
27495
|
+
return result;
|
|
27496
|
+
}
|
|
27497
|
+
async withNotification(method, operation) {
|
|
27498
|
+
const scope = new AbortController;
|
|
27499
|
+
try {
|
|
27500
|
+
return await operation(this.awaitNotification(method, () => true, scope.signal));
|
|
27501
|
+
} finally {
|
|
27502
|
+
scope.abort();
|
|
27503
|
+
}
|
|
27504
|
+
}
|
|
27505
|
+
observers = new Set;
|
|
27506
|
+
observeNotifications(handler) {
|
|
27507
|
+
this.observers.add(handler);
|
|
27508
|
+
return () => {
|
|
27509
|
+
this.observers.delete(handler);
|
|
27510
|
+
};
|
|
26906
27511
|
}
|
|
26907
27512
|
dispatchNotification(notification) {
|
|
27513
|
+
if (notification.method === "account/updated")
|
|
27514
|
+
this.modelCatalog = null;
|
|
27515
|
+
for (const observer of this.observers)
|
|
27516
|
+
observer(notification);
|
|
26908
27517
|
if (this.notificationWaiters.length > 0) {
|
|
26909
27518
|
const remaining = [];
|
|
26910
27519
|
for (const waiter of this.notificationWaiters) {
|
|
@@ -26917,7 +27526,7 @@ class AppServerClient {
|
|
|
26917
27526
|
}
|
|
26918
27527
|
if (notification.method === "mcpServer/startupStatus/updated") {
|
|
26919
27528
|
this.mcpStartupVersion += 1;
|
|
26920
|
-
this.mcpStartupStates.set(notification.params.name, {
|
|
27529
|
+
this.mcpStartupStates.set(mcpKey(notification.params.threadId, notification.params.name), {
|
|
26921
27530
|
status: notification.params.status,
|
|
26922
27531
|
error: notification.params.error,
|
|
26923
27532
|
version: this.mcpStartupVersion
|
|
@@ -26936,25 +27545,62 @@ class AppServerClient {
|
|
|
26936
27545
|
handler(notification);
|
|
26937
27546
|
}
|
|
26938
27547
|
}
|
|
26939
|
-
async runTurn(params, onTurnStarted) {
|
|
26940
|
-
|
|
26941
|
-
|
|
26942
|
-
|
|
27548
|
+
async runTurn(params, onTurnStarted, signal) {
|
|
27549
|
+
this.startingThreads.set(params.threadId, (this.startingThreads.get(params.threadId) ?? 0) + 1);
|
|
27550
|
+
let completed;
|
|
27551
|
+
try {
|
|
27552
|
+
const started = await this.turnStart(params);
|
|
27553
|
+
onTurnStarted?.(started.turn.id);
|
|
27554
|
+
completed = this.awaitTurnCompleted(params.threadId, started.turn.id, signal);
|
|
27555
|
+
} finally {
|
|
27556
|
+
const remaining = (this.startingThreads.get(params.threadId) ?? 1) - 1;
|
|
27557
|
+
if (remaining === 0)
|
|
27558
|
+
this.startingThreads.delete(params.threadId);
|
|
27559
|
+
else
|
|
27560
|
+
this.startingThreads.set(params.threadId, remaining);
|
|
27561
|
+
this.pruneEarlyCompletions();
|
|
27562
|
+
}
|
|
27563
|
+
return await completed;
|
|
26943
27564
|
}
|
|
26944
|
-
async runReview(params, onTurnStarted) {
|
|
26945
|
-
|
|
26946
|
-
|
|
26947
|
-
|
|
27565
|
+
async runReview(params, onTurnStarted, signal) {
|
|
27566
|
+
this.startingReviews += 1;
|
|
27567
|
+
let completed;
|
|
27568
|
+
try {
|
|
27569
|
+
const started = await this.reviewStart(params);
|
|
27570
|
+
onTurnStarted?.(started.turn.id, started.reviewThreadId);
|
|
27571
|
+
completed = this.awaitTurnCompleted(started.reviewThreadId, started.turn.id, signal);
|
|
27572
|
+
} finally {
|
|
27573
|
+
this.startingReviews -= 1;
|
|
27574
|
+
this.pruneEarlyCompletions();
|
|
27575
|
+
}
|
|
27576
|
+
return await completed;
|
|
26948
27577
|
}
|
|
26949
|
-
awaitTurnCompleted(threadId, turnId) {
|
|
27578
|
+
awaitTurnCompleted(threadId, turnId, signal) {
|
|
26950
27579
|
const key = turnKey(threadId, turnId);
|
|
26951
27580
|
const early = this.earlyTurnCompletions.get(key);
|
|
26952
27581
|
if (early) {
|
|
26953
27582
|
this.earlyTurnCompletions.delete(key);
|
|
26954
27583
|
return Promise.resolve(early);
|
|
26955
27584
|
}
|
|
26956
|
-
|
|
26957
|
-
|
|
27585
|
+
const lifetime = signal ? AbortSignal.any([signal, this.disconnected.signal]) : this.disconnected.signal;
|
|
27586
|
+
return new Promise((resolve, reject) => {
|
|
27587
|
+
const cleanup = () => {
|
|
27588
|
+
lifetime.removeEventListener("abort", aborted2);
|
|
27589
|
+
this.turnCompletionWaiters.delete(key);
|
|
27590
|
+
};
|
|
27591
|
+
const aborted2 = () => {
|
|
27592
|
+
cleanup();
|
|
27593
|
+
reject(lifetime.reason);
|
|
27594
|
+
};
|
|
27595
|
+
if (lifetime.aborted) {
|
|
27596
|
+
reject(lifetime.reason);
|
|
27597
|
+
return;
|
|
27598
|
+
}
|
|
27599
|
+
lifetime.addEventListener("abort", aborted2, { once: true });
|
|
27600
|
+
this.turnCompletionWaiters.set(key, (event) => {
|
|
27601
|
+
cleanup();
|
|
27602
|
+
resolve(event);
|
|
27603
|
+
});
|
|
26958
27604
|
});
|
|
26959
27605
|
}
|
|
26960
27606
|
resolveTurnInterrupted(threadId, turnId) {
|
|
@@ -26986,26 +27632,42 @@ class AppServerClient {
|
|
|
26986
27632
|
waiter(event);
|
|
26987
27633
|
return;
|
|
26988
27634
|
}
|
|
26989
|
-
this.
|
|
27635
|
+
if (this.startingThreads.has(event.threadId) || this.startingReviews > 0)
|
|
27636
|
+
this.earlyTurnCompletions.set(key, event);
|
|
27637
|
+
}
|
|
27638
|
+
pruneEarlyCompletions() {
|
|
27639
|
+
if (this.startingReviews > 0)
|
|
27640
|
+
return;
|
|
27641
|
+
for (const [key, event] of this.earlyTurnCompletions) {
|
|
27642
|
+
if (!this.startingThreads.has(event.threadId))
|
|
27643
|
+
this.earlyTurnCompletions.delete(key);
|
|
27644
|
+
}
|
|
26990
27645
|
}
|
|
26991
27646
|
get mcpStartupGeneration() {
|
|
26992
27647
|
return this.mcpStartupVersion;
|
|
26993
27648
|
}
|
|
26994
|
-
awaitMcpStartup(serverNames, afterVersion) {
|
|
27649
|
+
awaitMcpStartup(serverNames, afterVersion, signal, threadId = null) {
|
|
26995
27650
|
const names = [...new Set(serverNames.map((name) => name.trim()).filter((name) => name.length > 0))];
|
|
26996
27651
|
if (names.length === 0)
|
|
26997
27652
|
return Promise.resolve({ ready: [], failed: [], cancelled: [] });
|
|
26998
|
-
const immediate = this.buildMcpStartupResult(names, afterVersion);
|
|
27653
|
+
const immediate = this.buildMcpStartupResult(names, afterVersion, threadId);
|
|
26999
27654
|
if (immediate)
|
|
27000
27655
|
return Promise.resolve(immediate);
|
|
27001
|
-
|
|
27002
|
-
|
|
27656
|
+
let waiter;
|
|
27657
|
+
const completed = new Promise((resolve) => {
|
|
27658
|
+
waiter = { serverNames: names, afterVersion, threadId, resolve };
|
|
27659
|
+
this.mcpStartupWaiters.push(waiter);
|
|
27660
|
+
});
|
|
27661
|
+
return abortable(completed, signal ? AbortSignal.any([signal, this.disconnected.signal]) : this.disconnected.signal).finally(() => {
|
|
27662
|
+
const index = this.mcpStartupWaiters.indexOf(waiter);
|
|
27663
|
+
if (index >= 0)
|
|
27664
|
+
this.mcpStartupWaiters.splice(index, 1);
|
|
27003
27665
|
});
|
|
27004
27666
|
}
|
|
27005
27667
|
settleMcpStartupWaiters() {
|
|
27006
27668
|
const pending = [];
|
|
27007
27669
|
for (const waiter of this.mcpStartupWaiters) {
|
|
27008
|
-
const result = this.buildMcpStartupResult(waiter.serverNames, waiter.afterVersion);
|
|
27670
|
+
const result = this.buildMcpStartupResult(waiter.serverNames, waiter.afterVersion, waiter.threadId);
|
|
27009
27671
|
if (result)
|
|
27010
27672
|
waiter.resolve(result);
|
|
27011
27673
|
else
|
|
@@ -27013,10 +27675,12 @@ class AppServerClient {
|
|
|
27013
27675
|
}
|
|
27014
27676
|
this.mcpStartupWaiters.splice(0, this.mcpStartupWaiters.length, ...pending);
|
|
27015
27677
|
}
|
|
27016
|
-
buildMcpStartupResult(serverNames, afterVersion) {
|
|
27678
|
+
buildMcpStartupResult(serverNames, afterVersion, threadId) {
|
|
27017
27679
|
const result = { ready: [], failed: [], cancelled: [] };
|
|
27018
27680
|
for (const name of serverNames) {
|
|
27019
|
-
const
|
|
27681
|
+
const scoped = this.mcpStartupStates.get(mcpKey(threadId, name));
|
|
27682
|
+
const global = this.mcpStartupStates.get(mcpKey(null, name));
|
|
27683
|
+
const state = scoped && scoped.version > afterVersion ? scoped : global;
|
|
27020
27684
|
if (!state || state.version <= afterVersion || state.status === "starting")
|
|
27021
27685
|
return null;
|
|
27022
27686
|
if (state.status === "ready")
|
|
@@ -27068,8 +27732,9 @@ class AppServerClient {
|
|
|
27068
27732
|
return this.send({ method: "thread/compact/start", params });
|
|
27069
27733
|
}
|
|
27070
27734
|
async threadSettingsUpdate(params) {
|
|
27735
|
+
this.disconnected.signal.throwIfAborted();
|
|
27071
27736
|
logger.log("[codex request]", { method: "thread/settings/update" });
|
|
27072
|
-
await this.connection.sendRequest("thread/settings/update", params);
|
|
27737
|
+
await abortable(this.connection.sendRequest("thread/settings/update", params), this.disconnected.signal);
|
|
27073
27738
|
}
|
|
27074
27739
|
turnStart(params) {
|
|
27075
27740
|
return this.send({ method: "turn/start", params });
|
|
@@ -27086,13 +27751,34 @@ class AppServerClient {
|
|
|
27086
27751
|
modelList(params) {
|
|
27087
27752
|
return this.send({ method: "model/list", params });
|
|
27088
27753
|
}
|
|
27089
|
-
|
|
27754
|
+
invalidateModels() {
|
|
27755
|
+
this.modelCatalog = null;
|
|
27756
|
+
}
|
|
27757
|
+
allModels(refresh = false) {
|
|
27758
|
+
if (!refresh && this.modelCatalog && this.modelCatalog.expires > performance.now())
|
|
27759
|
+
return this.modelCatalog.pending;
|
|
27760
|
+
const pending = this.loadModels();
|
|
27761
|
+
const entry = { expires: performance.now() + 30000, pending };
|
|
27762
|
+
this.modelCatalog = entry;
|
|
27763
|
+
pending.catch(() => {
|
|
27764
|
+
if (this.modelCatalog === entry)
|
|
27765
|
+
this.modelCatalog = null;
|
|
27766
|
+
});
|
|
27767
|
+
return pending;
|
|
27768
|
+
}
|
|
27769
|
+
async loadModels() {
|
|
27090
27770
|
const models = [];
|
|
27771
|
+
const cursors = new Set;
|
|
27091
27772
|
let cursor = null;
|
|
27092
27773
|
do {
|
|
27093
27774
|
const page = await this.modelList({ cursor, limit: null });
|
|
27094
27775
|
models.push(...page.data);
|
|
27095
27776
|
cursor = page.nextCursor;
|
|
27777
|
+
if (cursor) {
|
|
27778
|
+
if (cursors.has(cursor))
|
|
27779
|
+
throw new Error("Codex model/list returned a repeated pagination cursor; retry after restarting Codex");
|
|
27780
|
+
cursors.add(cursor);
|
|
27781
|
+
}
|
|
27096
27782
|
} while (cursor);
|
|
27097
27783
|
return models;
|
|
27098
27784
|
}
|
|
@@ -27121,23 +27807,279 @@ class AppServerClient {
|
|
|
27121
27807
|
return this.send({ method: "mcpServerStatus/list", params });
|
|
27122
27808
|
}
|
|
27123
27809
|
async send(request) {
|
|
27810
|
+
this.disconnected.signal.throwIfAborted();
|
|
27124
27811
|
logger.log("[codex request]", { method: request.method });
|
|
27125
27812
|
const params = "params" in request ? request.params : undefined;
|
|
27126
|
-
return params === undefined ?
|
|
27813
|
+
return await abortable(params === undefined ? this.connection.sendRequest(request.method) : this.connection.sendRequest(request.method, params), this.disconnected.signal);
|
|
27127
27814
|
}
|
|
27128
27815
|
}
|
|
27129
27816
|
function turnKey(threadId, turnId) {
|
|
27130
27817
|
return `${threadId}\x00${turnId}`;
|
|
27131
27818
|
}
|
|
27819
|
+
function mcpKey(threadId, name) {
|
|
27820
|
+
return `${threadId ?? ""}\x00${name}`;
|
|
27821
|
+
}
|
|
27132
27822
|
function threadIdOf(notification) {
|
|
27133
27823
|
const params = notification.params;
|
|
27134
27824
|
return params && typeof params.threadId === "string" ? params.threadId : null;
|
|
27135
27825
|
}
|
|
27136
27826
|
|
|
27137
27827
|
// src/codex/process.ts
|
|
27828
|
+
import { existsSync } from "fs";
|
|
27829
|
+
import path7 from "path";
|
|
27138
27830
|
import { spawn, spawnSync } from "child_process";
|
|
27139
27831
|
import { createRequire } from "module";
|
|
27140
|
-
|
|
27832
|
+
|
|
27833
|
+
// src/codex/transport.ts
|
|
27834
|
+
var import_node2 = __toESM(require_main(), 1);
|
|
27835
|
+
import { StringDecoder } from "string_decoder";
|
|
27836
|
+
function wireLog(value) {
|
|
27837
|
+
return JSON.stringify(value, (key, nested) => {
|
|
27838
|
+
const normalized = key.replace(/[_-]/g, "").toLowerCase();
|
|
27839
|
+
if (/^(apikey|accesskey|accesstoken|refreshtoken|idtoken|secretaccesskey|sessiontoken|authorization|proxyauthorization|password|secret|token|cookie|setcookie|headers|httpheaders|env|queryparams)$/.test(normalized))
|
|
27840
|
+
return "***";
|
|
27841
|
+
return nested;
|
|
27842
|
+
});
|
|
27843
|
+
}
|
|
27844
|
+
function createWriter(writable) {
|
|
27845
|
+
const errors3 = new import_node2.Emitter;
|
|
27846
|
+
const closed = new import_node2.Emitter;
|
|
27847
|
+
const pending = new Set;
|
|
27848
|
+
let ended = false;
|
|
27849
|
+
const onError = (error40) => {
|
|
27850
|
+
for (const reject of [...pending])
|
|
27851
|
+
reject(error40);
|
|
27852
|
+
errors3.fire([error40, undefined, undefined]);
|
|
27853
|
+
};
|
|
27854
|
+
const onClose = () => {
|
|
27855
|
+
if (ended)
|
|
27856
|
+
return;
|
|
27857
|
+
ended = true;
|
|
27858
|
+
for (const reject of [...pending])
|
|
27859
|
+
reject(new Error("Codex input stream closed"));
|
|
27860
|
+
closed.fire();
|
|
27861
|
+
};
|
|
27862
|
+
writable.on("error", onError);
|
|
27863
|
+
writable.on("close", onClose);
|
|
27864
|
+
writable.on("finish", onClose);
|
|
27865
|
+
return {
|
|
27866
|
+
write(message) {
|
|
27867
|
+
if (ended || writable.destroyed || writable.writableEnded)
|
|
27868
|
+
return Promise.reject(new Error("Codex input stream is closed"));
|
|
27869
|
+
const { jsonrpc: _jsonrpc, ...frame2 } = message;
|
|
27870
|
+
if (logger.enabled)
|
|
27871
|
+
logger.log("[codex <-]", { line: wireLog(frame2) });
|
|
27872
|
+
return new Promise((resolve, reject) => {
|
|
27873
|
+
const fail = (error40) => {
|
|
27874
|
+
pending.delete(fail);
|
|
27875
|
+
reject(error40);
|
|
27876
|
+
};
|
|
27877
|
+
pending.add(fail);
|
|
27878
|
+
try {
|
|
27879
|
+
writable.write(`${JSON.stringify(frame2)}
|
|
27880
|
+
`, (error40) => {
|
|
27881
|
+
pending.delete(fail);
|
|
27882
|
+
if (error40)
|
|
27883
|
+
reject(error40);
|
|
27884
|
+
else
|
|
27885
|
+
resolve();
|
|
27886
|
+
});
|
|
27887
|
+
} catch (error40) {
|
|
27888
|
+
fail(error40 instanceof Error ? error40 : new Error(String(error40)));
|
|
27889
|
+
}
|
|
27890
|
+
});
|
|
27891
|
+
},
|
|
27892
|
+
end() {
|
|
27893
|
+
writable.end();
|
|
27894
|
+
},
|
|
27895
|
+
onError: errors3.event,
|
|
27896
|
+
onClose: closed.event,
|
|
27897
|
+
dispose() {
|
|
27898
|
+
onClose();
|
|
27899
|
+
writable.off("error", onError);
|
|
27900
|
+
writable.off("close", onClose);
|
|
27901
|
+
writable.off("finish", onClose);
|
|
27902
|
+
errors3.dispose();
|
|
27903
|
+
closed.dispose();
|
|
27904
|
+
}
|
|
27905
|
+
};
|
|
27906
|
+
}
|
|
27907
|
+
function createReader(readable) {
|
|
27908
|
+
const errors3 = new import_node2.Emitter;
|
|
27909
|
+
const closed = new import_node2.Emitter;
|
|
27910
|
+
const partial2 = new import_node2.Emitter;
|
|
27911
|
+
const decoder = new StringDecoder("utf8");
|
|
27912
|
+
let fragments = [];
|
|
27913
|
+
let ended = false;
|
|
27914
|
+
let callback = null;
|
|
27915
|
+
const deliver = (line) => {
|
|
27916
|
+
if (!line.trim())
|
|
27917
|
+
return;
|
|
27918
|
+
let message;
|
|
27919
|
+
try {
|
|
27920
|
+
const parsed = JSON.parse(line);
|
|
27921
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
27922
|
+
throw new Error("Expected an object");
|
|
27923
|
+
const frame2 = parsed;
|
|
27924
|
+
if (frame2["jsonrpc"] === undefined)
|
|
27925
|
+
frame2["jsonrpc"] = "2.0";
|
|
27926
|
+
if (logger.enabled)
|
|
27927
|
+
logger.log("[codex ->]", { line: wireLog(frame2) });
|
|
27928
|
+
message = frame2;
|
|
27929
|
+
} catch {
|
|
27930
|
+
const error40 = new Error("Malformed JSON-RPC frame from Codex");
|
|
27931
|
+
logger.error(error40.message, error40);
|
|
27932
|
+
errors3.fire(error40);
|
|
27933
|
+
return;
|
|
27934
|
+
}
|
|
27935
|
+
callback?.(message);
|
|
27936
|
+
};
|
|
27937
|
+
const onData = (chunk) => {
|
|
27938
|
+
const text2 = typeof chunk === "string" ? chunk : decoder.write(chunk);
|
|
27939
|
+
let offset = 0;
|
|
27940
|
+
for (;; ) {
|
|
27941
|
+
const newline2 = text2.indexOf(`
|
|
27942
|
+
`, offset);
|
|
27943
|
+
if (newline2 < 0)
|
|
27944
|
+
break;
|
|
27945
|
+
const part = text2.slice(offset, newline2);
|
|
27946
|
+
const line = fragments.length ? fragments.join("") + part : part;
|
|
27947
|
+
fragments = [];
|
|
27948
|
+
deliver(line);
|
|
27949
|
+
offset = newline2 + 1;
|
|
27950
|
+
}
|
|
27951
|
+
if (offset < text2.length)
|
|
27952
|
+
fragments.push(text2.slice(offset));
|
|
27953
|
+
};
|
|
27954
|
+
const onClose = () => {
|
|
27955
|
+
if (ended)
|
|
27956
|
+
return;
|
|
27957
|
+
ended = true;
|
|
27958
|
+
closed.fire();
|
|
27959
|
+
};
|
|
27960
|
+
const onEnd = () => {
|
|
27961
|
+
fragments.push(decoder.end());
|
|
27962
|
+
deliver(fragments.join(""));
|
|
27963
|
+
fragments = [];
|
|
27964
|
+
onClose();
|
|
27965
|
+
};
|
|
27966
|
+
const onError = (error40) => errors3.fire(error40);
|
|
27967
|
+
const stop = () => {
|
|
27968
|
+
readable.off("data", onData);
|
|
27969
|
+
readable.off("end", onEnd);
|
|
27970
|
+
readable.off("close", onClose);
|
|
27971
|
+
readable.off("error", onError);
|
|
27972
|
+
callback = null;
|
|
27973
|
+
fragments = [];
|
|
27974
|
+
};
|
|
27975
|
+
return {
|
|
27976
|
+
listen(receive) {
|
|
27977
|
+
callback = receive;
|
|
27978
|
+
readable.on("data", onData);
|
|
27979
|
+
readable.on("end", onEnd);
|
|
27980
|
+
readable.on("close", onClose);
|
|
27981
|
+
readable.on("error", onError);
|
|
27982
|
+
return { dispose: stop };
|
|
27983
|
+
},
|
|
27984
|
+
onError: errors3.event,
|
|
27985
|
+
onClose: closed.event,
|
|
27986
|
+
onPartialMessage: partial2.event,
|
|
27987
|
+
dispose() {
|
|
27988
|
+
stop();
|
|
27989
|
+
errors3.dispose();
|
|
27990
|
+
closed.dispose();
|
|
27991
|
+
partial2.dispose();
|
|
27992
|
+
}
|
|
27993
|
+
};
|
|
27994
|
+
}
|
|
27995
|
+
function createCodexConnection(readable, writable) {
|
|
27996
|
+
const reader = createReader(readable);
|
|
27997
|
+
const writer = createWriter(writable);
|
|
27998
|
+
const closed = new import_node2.Emitter;
|
|
27999
|
+
let queued = 0;
|
|
28000
|
+
let eof = false;
|
|
28001
|
+
let finished = false;
|
|
28002
|
+
let timer;
|
|
28003
|
+
let resolveDrained;
|
|
28004
|
+
const drained = new Promise((resolve) => {
|
|
28005
|
+
resolveDrained = resolve;
|
|
28006
|
+
});
|
|
28007
|
+
const finish = () => {
|
|
28008
|
+
if (finished)
|
|
28009
|
+
return;
|
|
28010
|
+
finished = true;
|
|
28011
|
+
clearTimeout(timer);
|
|
28012
|
+
closed.fire();
|
|
28013
|
+
resolveDrained();
|
|
28014
|
+
};
|
|
28015
|
+
const check2 = () => {
|
|
28016
|
+
if (eof && queued === 0)
|
|
28017
|
+
setImmediate(() => {
|
|
28018
|
+
if (queued === 0)
|
|
28019
|
+
finish();
|
|
28020
|
+
});
|
|
28021
|
+
};
|
|
28022
|
+
reader.onClose(() => {
|
|
28023
|
+
eof = true;
|
|
28024
|
+
timer ??= setTimeout(() => {
|
|
28025
|
+
logger.error("Codex dispatch drain deadline exceeded", new Error(`Undispatched frames: ${queued}`));
|
|
28026
|
+
finish();
|
|
28027
|
+
}, 5000);
|
|
28028
|
+
check2();
|
|
28029
|
+
});
|
|
28030
|
+
const inputFailed = () => {
|
|
28031
|
+
if (finished || timer)
|
|
28032
|
+
return;
|
|
28033
|
+
timer = setTimeout(() => {
|
|
28034
|
+
logger.error("Codex stdout did not close after stdin", new Error("Transport shutdown deadline exceeded"));
|
|
28035
|
+
finish();
|
|
28036
|
+
}, 5000);
|
|
28037
|
+
};
|
|
28038
|
+
writer.onClose(inputFailed);
|
|
28039
|
+
const counted = (message) => !(("method" in message) && message.method === "$/cancelRequest");
|
|
28040
|
+
const connection = import_node2.createMessageConnection({
|
|
28041
|
+
...reader,
|
|
28042
|
+
onClose: closed.event,
|
|
28043
|
+
listen(receive) {
|
|
28044
|
+
return reader.listen((message) => {
|
|
28045
|
+
if (counted(message))
|
|
28046
|
+
queued += 1;
|
|
28047
|
+
receive(message);
|
|
28048
|
+
});
|
|
28049
|
+
}
|
|
28050
|
+
}, {
|
|
28051
|
+
...writer,
|
|
28052
|
+
onClose: () => ({ dispose() {} }),
|
|
28053
|
+
async write(message) {
|
|
28054
|
+
try {
|
|
28055
|
+
await writer.write(message);
|
|
28056
|
+
} catch (error40) {
|
|
28057
|
+
logger.error("Codex write failed; draining stdout before disconnect", error40);
|
|
28058
|
+
inputFailed();
|
|
28059
|
+
await drained;
|
|
28060
|
+
}
|
|
28061
|
+
}
|
|
28062
|
+
}, undefined, {
|
|
28063
|
+
messageStrategy: { handleMessage(message, next) {
|
|
28064
|
+
try {
|
|
28065
|
+
return next(message);
|
|
28066
|
+
} finally {
|
|
28067
|
+
if (counted(message))
|
|
28068
|
+
queued -= 1;
|
|
28069
|
+
check2();
|
|
28070
|
+
}
|
|
28071
|
+
} }
|
|
28072
|
+
});
|
|
28073
|
+
connection.onDispose(() => {
|
|
28074
|
+
finished = true;
|
|
28075
|
+
clearTimeout(timer);
|
|
28076
|
+
resolveDrained();
|
|
28077
|
+
closed.dispose();
|
|
28078
|
+
});
|
|
28079
|
+
return { connection, drained };
|
|
28080
|
+
}
|
|
28081
|
+
|
|
28082
|
+
// src/codex/process.ts
|
|
27141
28083
|
var STDERR_RETAIN_BYTES = 8 * 1024;
|
|
27142
28084
|
var MINIMUM_CODEX_VERSION = package_default.optionalDependencies["@openai/codex"].replace(/^[\^~]/, "");
|
|
27143
28085
|
function parseCodexVersion(output) {
|
|
@@ -27162,7 +28104,29 @@ function resolveCodexLauncher(codexPath) {
|
|
|
27162
28104
|
return process.platform === "win32" ? { command: `"${codexPath}"`, prefixArgs: [], shell: true } : { command: codexPath, prefixArgs: [], shell: false };
|
|
27163
28105
|
}
|
|
27164
28106
|
try {
|
|
27165
|
-
|
|
28107
|
+
const require2 = createRequire(import.meta.url);
|
|
28108
|
+
const launcher = require2.resolve("@openai/codex/bin/codex.js");
|
|
28109
|
+
const platformRequire = createRequire(launcher);
|
|
28110
|
+
const targets = {
|
|
28111
|
+
"linux-x64": "x86_64-unknown-linux-musl",
|
|
28112
|
+
"linux-arm64": "aarch64-unknown-linux-musl",
|
|
28113
|
+
"darwin-x64": "x86_64-apple-darwin",
|
|
28114
|
+
"darwin-arm64": "aarch64-apple-darwin",
|
|
28115
|
+
"win32-x64": "x86_64-pc-windows-msvc",
|
|
28116
|
+
"win32-arm64": "aarch64-pc-windows-msvc"
|
|
28117
|
+
};
|
|
28118
|
+
const platform2 = `${process.platform}-${process.arch}`;
|
|
28119
|
+
const target = targets[platform2];
|
|
28120
|
+
if (!target)
|
|
28121
|
+
throw new Error(`Unsupported Codex platform: ${platform2}`);
|
|
28122
|
+
let vendor = path7.resolve(path7.dirname(launcher), "..", "vendor");
|
|
28123
|
+
try {
|
|
28124
|
+
vendor = path7.join(path7.dirname(platformRequire.resolve(`@openai/codex-${platform2}/package.json`)), "vendor");
|
|
28125
|
+
} catch {}
|
|
28126
|
+
const command = path7.join(vendor, target, "bin", process.platform === "win32" ? "codex.exe" : "codex");
|
|
28127
|
+
if (!existsSync(command))
|
|
28128
|
+
throw new Error(`Missing Codex platform binary: ${command}`);
|
|
28129
|
+
return { command, prefixArgs: [], shell: false };
|
|
27166
28130
|
} catch (error40) {
|
|
27167
28131
|
throw new Error("No Codex CLI: set CODEX_PATH to an installed Codex executable, or install the optional " + `@openai/codex dependency (${String(error40)})`);
|
|
27168
28132
|
}
|
|
@@ -27179,11 +28143,13 @@ function startCodexProcess(codexPath, env = process.env) {
|
|
|
27179
28143
|
stderr = (stderr + data.toString()).slice(-STDERR_RETAIN_BYTES);
|
|
27180
28144
|
logger.log("[codex stderr]", { data: data.toString() });
|
|
27181
28145
|
});
|
|
27182
|
-
const connection =
|
|
28146
|
+
const { connection, drained } = createCodexConnection(child.stdout, child.stdin);
|
|
27183
28147
|
connection.listen();
|
|
28148
|
+
drained.then(() => connection.dispose());
|
|
27184
28149
|
const exited = new Promise((resolve) => {
|
|
27185
|
-
child.on("
|
|
28150
|
+
child.on("close", async (code, signal) => {
|
|
27186
28151
|
logger.log("codex exited", { code, signal });
|
|
28152
|
+
await drained;
|
|
27187
28153
|
connection.dispose();
|
|
27188
28154
|
resolve(code);
|
|
27189
28155
|
});
|
|
@@ -27196,65 +28162,6 @@ function startCodexProcess(codexPath, env = process.env) {
|
|
|
27196
28162
|
exitCode: () => child.exitCode
|
|
27197
28163
|
};
|
|
27198
28164
|
}
|
|
27199
|
-
function redactSecrets(line) {
|
|
27200
|
-
return line.replace(/"(apiKey|accessToken|secretAccessKey|sessionToken)":"[^"]*"/g, '"$1":"***"');
|
|
27201
|
-
}
|
|
27202
|
-
function createWriter(writable) {
|
|
27203
|
-
return {
|
|
27204
|
-
async write(message) {
|
|
27205
|
-
const { jsonrpc: _jsonrpc, ...frame2 } = message;
|
|
27206
|
-
const line = JSON.stringify(frame2);
|
|
27207
|
-
logger.log("[codex <-]", { line: redactSecrets(line) });
|
|
27208
|
-
writable.write(`${line}
|
|
27209
|
-
`);
|
|
27210
|
-
},
|
|
27211
|
-
end() {
|
|
27212
|
-
writable.end();
|
|
27213
|
-
},
|
|
27214
|
-
onError: new rpc.Emitter().event,
|
|
27215
|
-
onClose: new rpc.Emitter().event,
|
|
27216
|
-
dispose() {}
|
|
27217
|
-
};
|
|
27218
|
-
}
|
|
27219
|
-
function createReader(readable) {
|
|
27220
|
-
return {
|
|
27221
|
-
listen(callback) {
|
|
27222
|
-
let buffer = "";
|
|
27223
|
-
const onData = (chunk) => {
|
|
27224
|
-
buffer += chunk.toString();
|
|
27225
|
-
for (;; ) {
|
|
27226
|
-
const newline2 = buffer.indexOf(`
|
|
27227
|
-
`);
|
|
27228
|
-
if (newline2 < 0)
|
|
27229
|
-
break;
|
|
27230
|
-
const line = buffer.slice(0, newline2).trim();
|
|
27231
|
-
buffer = buffer.slice(newline2 + 1);
|
|
27232
|
-
if (line.length === 0)
|
|
27233
|
-
continue;
|
|
27234
|
-
logger.log("[codex ->]", { line: redactSecrets(line) });
|
|
27235
|
-
try {
|
|
27236
|
-
const message = JSON.parse(line);
|
|
27237
|
-
if (message["jsonrpc"] === undefined)
|
|
27238
|
-
message["jsonrpc"] = "2.0";
|
|
27239
|
-
callback(message);
|
|
27240
|
-
} catch (error40) {
|
|
27241
|
-
logger.error("Malformed JSON-RPC frame from codex", error40, { line });
|
|
27242
|
-
}
|
|
27243
|
-
}
|
|
27244
|
-
};
|
|
27245
|
-
readable.on("data", onData);
|
|
27246
|
-
return {
|
|
27247
|
-
dispose() {
|
|
27248
|
-
readable.off("data", onData);
|
|
27249
|
-
}
|
|
27250
|
-
};
|
|
27251
|
-
},
|
|
27252
|
-
onError: new rpc.Emitter().event,
|
|
27253
|
-
onClose: new rpc.Emitter().event,
|
|
27254
|
-
onPartialMessage: new rpc.Emitter().event,
|
|
27255
|
-
dispose() {}
|
|
27256
|
-
};
|
|
27257
|
-
}
|
|
27258
28165
|
|
|
27259
28166
|
// src/index.ts
|
|
27260
28167
|
var CODEX_EXIT_GRACE_MS = 2000;
|