@yu_robotics/remote-cli 1.6.170 → 1.6.176
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/README.md +25 -0
- package/dist/client/GoalCommand.d.ts +15 -0
- package/dist/client/GoalCommand.d.ts.map +1 -0
- package/dist/client/GoalCommand.js +59 -0
- package/dist/client/GoalCommand.js.map +1 -0
- package/dist/client/MessageHandler.d.ts +4 -0
- package/dist/client/MessageHandler.d.ts.map +1 -1
- package/dist/client/MessageHandler.js +186 -46
- package/dist/client/MessageHandler.js.map +1 -1
- package/dist/client/TaskRecovery.d.ts +3 -1
- package/dist/client/TaskRecovery.d.ts.map +1 -1
- package/dist/client/TaskRecovery.js +9 -4
- package/dist/client/TaskRecovery.js.map +1 -1
- package/dist/client/WebSocketClient.d.ts +2 -1
- package/dist/client/WebSocketClient.d.ts.map +1 -1
- package/dist/client/WebSocketClient.js +9 -1
- package/dist/client/WebSocketClient.js.map +1 -1
- package/dist/delegation/DelegationManager.js +1 -1
- package/dist/delegation/DelegationManager.js.map +1 -1
- package/dist/executor/AcpExecutor.d.ts +6 -0
- package/dist/executor/AcpExecutor.d.ts.map +1 -1
- package/dist/executor/AcpExecutor.js +81 -10
- package/dist/executor/AcpExecutor.js.map +1 -1
- package/dist/executor/CodexAppServerClient.d.ts +1 -0
- package/dist/executor/CodexAppServerClient.d.ts.map +1 -1
- package/dist/executor/CodexAppServerClient.js +12 -4
- package/dist/executor/CodexAppServerClient.js.map +1 -1
- package/dist/executor/CodexAppServerExecutor.d.ts +39 -2
- package/dist/executor/CodexAppServerExecutor.d.ts.map +1 -1
- package/dist/executor/CodexAppServerExecutor.js +712 -32
- package/dist/executor/CodexAppServerExecutor.js.map +1 -1
- package/dist/executor/IExecutor.d.ts +43 -0
- package/dist/executor/IExecutor.d.ts.map +1 -1
- package/dist/executor/KimiExecutor.d.ts +9 -0
- package/dist/executor/KimiExecutor.d.ts.map +1 -1
- package/dist/executor/KimiExecutor.js +45 -0
- package/dist/executor/KimiExecutor.js.map +1 -1
- package/dist/executor/codex/CodexGoals.d.ts +6 -0
- package/dist/executor/codex/CodexGoals.d.ts.map +1 -0
- package/dist/executor/codex/CodexGoals.js +49 -0
- package/dist/executor/codex/CodexGoals.js.map +1 -0
- package/dist/executor/codex/CodexImages.d.ts +22 -0
- package/dist/executor/codex/CodexImages.d.ts.map +1 -0
- package/dist/executor/codex/CodexImages.js +201 -0
- package/dist/executor/codex/CodexImages.js.map +1 -0
- package/dist/executor/index.d.ts +2 -1
- package/dist/executor/index.d.ts.map +1 -1
- package/dist/executor/index.js +1 -0
- package/dist/executor/index.js.map +1 -1
- package/dist/executor/kimi/KimiAccountUsage.d.ts +2 -6
- package/dist/executor/kimi/KimiAccountUsage.d.ts.map +1 -1
- package/dist/executor/kimi/KimiAccountUsage.js +11 -71
- package/dist/executor/kimi/KimiAccountUsage.js.map +1 -1
- package/dist/executor/kimi/KimiLocalServer.d.ts +28 -0
- package/dist/executor/kimi/KimiLocalServer.d.ts.map +1 -0
- package/dist/executor/kimi/KimiLocalServer.js +192 -0
- package/dist/executor/kimi/KimiLocalServer.js.map +1 -0
- package/dist/executor/kimi/KimiModelCatalog.d.ts +6 -0
- package/dist/executor/kimi/KimiModelCatalog.d.ts.map +1 -0
- package/dist/executor/kimi/KimiModelCatalog.js +86 -0
- package/dist/executor/kimi/KimiModelCatalog.js.map +1 -0
- package/dist/maintenance/release-notes.json +1 -1
- package/dist/settings/SettingsCatalog.d.ts +1 -1
- package/dist/settings/SettingsCatalog.d.ts.map +1 -1
- package/dist/settings/SettingsCatalog.js +6 -2
- package/dist/settings/SettingsCatalog.js.map +1 -1
- package/dist/settings/SettingsService.d.ts +3 -1
- package/dist/settings/SettingsService.d.ts.map +1 -1
- package/dist/settings/SettingsService.js +17 -5
- package/dist/settings/SettingsService.js.map +1 -1
- package/dist/thread/ThreadExecutorPool.d.ts +3 -1
- package/dist/thread/ThreadExecutorPool.d.ts.map +1 -1
- package/dist/thread/ThreadExecutorPool.js +13 -1
- package/dist/thread/ThreadExecutorPool.js.map +1 -1
- package/dist/thread/ThreadManager.d.ts +1 -1
- package/dist/thread/ThreadManager.d.ts.map +1 -1
- package/dist/thread/ThreadManager.js.map +1 -1
- package/dist/thread/types.d.ts +2 -0
- package/dist/thread/types.d.ts.map +1 -1
- package/dist/types/Settings.d.ts +8 -0
- package/dist/types/Settings.d.ts.map +1 -1
- package/dist/utils/ImageDelivery.d.ts +67 -0
- package/dist/utils/ImageDelivery.d.ts.map +1 -0
- package/dist/utils/ImageDelivery.js +270 -0
- package/dist/utils/ImageDelivery.js.map +1 -0
- package/dist/utils/ImageValidation.d.ts +11 -0
- package/dist/utils/ImageValidation.d.ts.map +1 -0
- package/dist/utils/ImageValidation.js +62 -0
- package/dist/utils/ImageValidation.js.map +1 -0
- package/dist/utils/LocalImageDetector.d.ts +12 -3
- package/dist/utils/LocalImageDetector.d.ts.map +1 -1
- package/dist/utils/LocalImageDetector.js +153 -55
- package/dist/utils/LocalImageDetector.js.map +1 -1
- package/package.json +1 -1
|
@@ -38,6 +38,8 @@ const fs = __importStar(require("fs"));
|
|
|
38
38
|
const os = __importStar(require("os"));
|
|
39
39
|
const path = __importStar(require("path"));
|
|
40
40
|
const crypto_1 = require("crypto");
|
|
41
|
+
const CodexGoals_1 = require("./codex/CodexGoals");
|
|
42
|
+
const CodexImages_1 = require("./codex/CodexImages");
|
|
41
43
|
const ExecutionMetadata_1 = require("./ExecutionMetadata");
|
|
42
44
|
const CodexAppServerClient_1 = require("./CodexAppServerClient");
|
|
43
45
|
const CodexTaskNotifications_1 = require("./CodexTaskNotifications");
|
|
@@ -50,6 +52,8 @@ const CodexWebSearch_1 = require("./CodexWebSearch");
|
|
|
50
52
|
const DEFAULT_INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000;
|
|
51
53
|
const DEFAULT_COMPACT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
52
54
|
const ACCOUNT_USAGE_TIMEOUT_MS = 10_000;
|
|
55
|
+
const GOAL_CONTROL_TIMEOUT_MS = 10_000;
|
|
56
|
+
const GOAL_RECOVERY_GUIDANCE = 'Use /goal resume to continue the saved native goal, or /goal pause or /goal clear before sending another task.';
|
|
53
57
|
function formatWindowDuration(minutes) {
|
|
54
58
|
if (!Number.isFinite(minutes) || !minutes || minutes <= 0)
|
|
55
59
|
return 'usage window';
|
|
@@ -119,6 +123,9 @@ class CodexAppServerExecutor {
|
|
|
119
123
|
currentWorkingDirectory;
|
|
120
124
|
model;
|
|
121
125
|
effort;
|
|
126
|
+
speed;
|
|
127
|
+
/** Only a fresh legacy thread is known not to carry an older remote speed override. */
|
|
128
|
+
legacyFollowReady = false;
|
|
122
129
|
reportedModel;
|
|
123
130
|
reportedEffort;
|
|
124
131
|
autoApprove;
|
|
@@ -142,6 +149,13 @@ class CodexAppServerExecutor {
|
|
|
142
149
|
delegationConfigured = false;
|
|
143
150
|
delegationWorker;
|
|
144
151
|
sessionResumeFailure = false;
|
|
152
|
+
goalManaged = false;
|
|
153
|
+
goalNeedsResume = false;
|
|
154
|
+
goalRun = null;
|
|
155
|
+
goalControl = null;
|
|
156
|
+
goalControlClear = false;
|
|
157
|
+
goalTransportStops = 0;
|
|
158
|
+
threadLoads = 0;
|
|
145
159
|
async configureDelegation(connection) {
|
|
146
160
|
if (this.delegationConfigured && (0, contract_1.sameConnection)(this.delegation, connection))
|
|
147
161
|
return;
|
|
@@ -155,6 +169,7 @@ class CodexAppServerExecutor {
|
|
|
155
169
|
constructor(directoryGuard, options = {}) {
|
|
156
170
|
this.directoryGuard = directoryGuard;
|
|
157
171
|
this.model = options.model;
|
|
172
|
+
this.speed = options.speed;
|
|
158
173
|
this.effort = options.effort;
|
|
159
174
|
this.autoApprove = options.autoApprove ?? true;
|
|
160
175
|
this.remoteThreadId = options.threadId;
|
|
@@ -196,11 +211,13 @@ class CodexAppServerExecutor {
|
|
|
196
211
|
this.sessionResumeFailure = false;
|
|
197
212
|
if (this.destroyed)
|
|
198
213
|
throw new Error('Executor has been destroyed');
|
|
199
|
-
if (this.
|
|
214
|
+
if (this.isBusy()) {
|
|
200
215
|
return { success: false, error: 'Codex thread is already running a command' };
|
|
201
216
|
}
|
|
202
217
|
try {
|
|
203
218
|
await this.ensureThread();
|
|
219
|
+
if (this.isBusy())
|
|
220
|
+
return { success: false, error: 'Codex thread is already running a command' };
|
|
204
221
|
}
|
|
205
222
|
catch (error) {
|
|
206
223
|
return { success: false, error: this.errorMessage(error) };
|
|
@@ -211,6 +228,11 @@ class CodexAppServerExecutor {
|
|
|
211
228
|
// leave the executor busy without an actual app-server turn.
|
|
212
229
|
const sandboxOptions = this.sandbox.turnOptions(this.currentWorkingDirectory);
|
|
213
230
|
const input = await this.buildInput(prompt, options.attachments, temporaryFiles);
|
|
231
|
+
const speedOptions = await this.speedOptions();
|
|
232
|
+
if (this.isBusy()) {
|
|
233
|
+
await this.cleanupTemporaryFiles(temporaryFiles);
|
|
234
|
+
return { success: false, error: 'Codex thread is already running a command' };
|
|
235
|
+
}
|
|
214
236
|
return await new Promise((resolve) => {
|
|
215
237
|
const active = {
|
|
216
238
|
options,
|
|
@@ -221,6 +243,7 @@ class CodexAppServerExecutor {
|
|
|
221
243
|
emittedTools: new Map(),
|
|
222
244
|
itemPhases: new Map(),
|
|
223
245
|
temporaryFiles,
|
|
246
|
+
images: new CodexImages_1.CodexImages(),
|
|
224
247
|
inactivityTimeoutMs: options.inactivityTimeout,
|
|
225
248
|
};
|
|
226
249
|
this.activityTracker.setCallback(options.onActivity);
|
|
@@ -238,6 +261,7 @@ class CodexAppServerExecutor {
|
|
|
238
261
|
cwd: this.currentWorkingDirectory,
|
|
239
262
|
...(this.model ? { model: this.model } : {}),
|
|
240
263
|
...(this.effort ? { effort: this.effort } : {}),
|
|
264
|
+
...speedOptions,
|
|
241
265
|
...(this.autoApprove ? {
|
|
242
266
|
approvalPolicy: 'never',
|
|
243
267
|
sandboxPolicy: { type: 'dangerFullAccess' },
|
|
@@ -269,6 +293,337 @@ class CodexAppServerExecutor {
|
|
|
269
293
|
getCurrentWorkingDirectory() {
|
|
270
294
|
return this.currentWorkingDirectory;
|
|
271
295
|
}
|
|
296
|
+
isGoalRunning() {
|
|
297
|
+
return this.goalRun !== null;
|
|
298
|
+
}
|
|
299
|
+
/** Reading a saved goal must never materialize or resume its native thread. */
|
|
300
|
+
async getGoal() {
|
|
301
|
+
if (this.destroyed)
|
|
302
|
+
throw new Error('Executor has been destroyed');
|
|
303
|
+
if (!this.codexThreadId)
|
|
304
|
+
return null;
|
|
305
|
+
const threadId = this.codexThreadId;
|
|
306
|
+
try {
|
|
307
|
+
await this.goalDeadline(this.client.start());
|
|
308
|
+
(0, CodexGoals_1.assertNativeGoalsVersion)(this.client.getServerVersion?.());
|
|
309
|
+
const response = await this.goalRequest('thread/goal/get', { threadId });
|
|
310
|
+
if (this.codexThreadId !== threadId || this.destroyed)
|
|
311
|
+
throw new Error('The native goal session changed while reading it.');
|
|
312
|
+
const goal = (0, CodexGoals_1.parseNativeGoal)(response?.goal, threadId);
|
|
313
|
+
if (goal && !this.goalManaged)
|
|
314
|
+
this.rememberGoalManagement();
|
|
315
|
+
return goal;
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
throw new Error((0, CodexGoals_1.nativeGoalError)(error));
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
async executeGoal(request, options) {
|
|
322
|
+
try {
|
|
323
|
+
(0, CodexGoals_1.validateGoalRequest)(request);
|
|
324
|
+
if (this.destroyed)
|
|
325
|
+
throw new Error('Executor has been destroyed');
|
|
326
|
+
if (this.isBusy())
|
|
327
|
+
throw new Error('Codex thread is already running a command');
|
|
328
|
+
if (this.delegationWorker || this.delegation)
|
|
329
|
+
throw new Error('Native goals require managed delegation to be off and cannot run in a delegated worker.');
|
|
330
|
+
if (request.objective === undefined && !this.codexThreadId)
|
|
331
|
+
throw new Error('There is no saved native goal to resume.');
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
return { success: false, error: (0, CodexGoals_1.nativeGoalError)(error) };
|
|
335
|
+
}
|
|
336
|
+
let resolve;
|
|
337
|
+
const done = new Promise(settle => { resolve = settle; });
|
|
338
|
+
const active = { options, resolve, turnId: null, output: [], sideEffectsStarted: false,
|
|
339
|
+
emittedTools: new Map(), itemPhases: new Map(), temporaryFiles: [], images: new CodexImages_1.CodexImages(), inactivityTimeoutMs: options.inactivityTimeout };
|
|
340
|
+
const run = { active, done, objective: request.objective, ready: false, inFlight: new Set(), retired: new Set(),
|
|
341
|
+
revision: 0, checking: false, controlling: false, stopping: false };
|
|
342
|
+
// Install ownership before any native loading or mutation can start a turn.
|
|
343
|
+
this.goalRun = run;
|
|
344
|
+
this.activeTurn = active;
|
|
345
|
+
this.activityTracker.setCallback(options.onActivity);
|
|
346
|
+
this.activityTracker.reset();
|
|
347
|
+
this.armInactivityTimer();
|
|
348
|
+
if (options.timeout && options.timeout > 0)
|
|
349
|
+
active.timeoutTimer = setTimeout(() => {
|
|
350
|
+
void this.failGoal(run, `Native goal timed out after ${options.timeout}ms. ${GOAL_RECOVERY_GUIDANCE}`);
|
|
351
|
+
}, options.timeout);
|
|
352
|
+
run.startup = this.startGoal(run, request).catch(error => this.failGoal(run, (0, CodexGoals_1.nativeGoalError)(error)));
|
|
353
|
+
return done;
|
|
354
|
+
}
|
|
355
|
+
pauseGoal() {
|
|
356
|
+
return this.controlGoal(false);
|
|
357
|
+
}
|
|
358
|
+
clearGoal() {
|
|
359
|
+
return this.controlGoal(true);
|
|
360
|
+
}
|
|
361
|
+
async startGoal(run, request) {
|
|
362
|
+
await this.goalDeadline(this.client.start());
|
|
363
|
+
this.assertGoalOwner(run);
|
|
364
|
+
(0, CodexGoals_1.assertNativeGoalsVersion)(this.client.getServerVersion?.());
|
|
365
|
+
let previous = null;
|
|
366
|
+
if (this.codexThreadId) {
|
|
367
|
+
previous = await this.getGoal();
|
|
368
|
+
this.assertGoalOwner(run);
|
|
369
|
+
}
|
|
370
|
+
if (request.objective === undefined && (!previous || previous.status === 'complete')) {
|
|
371
|
+
throw new Error('There is no unfinished native goal to resume.');
|
|
372
|
+
}
|
|
373
|
+
// Replacing an active saved goal must not briefly execute its old objective on load.
|
|
374
|
+
if (request.objective !== undefined && previous?.status === 'active' && !this.threadReady) {
|
|
375
|
+
throw new Error('Pause or clear the saved active goal before replacing its objective.');
|
|
376
|
+
}
|
|
377
|
+
run.goal = previous;
|
|
378
|
+
run.objective = request.objective ?? previous?.objective;
|
|
379
|
+
this.rememberGoalManagement();
|
|
380
|
+
if (previous?.status === 'active' && !this.threadReady) {
|
|
381
|
+
// Loading an active goal can dispatch before settings/update. Suspend it
|
|
382
|
+
// without loading, then restore activation only after native policy sync.
|
|
383
|
+
const response = await this.goalRequest('thread/goal/set', {
|
|
384
|
+
threadId: this.codexThreadId, status: 'paused', origin: 'user',
|
|
385
|
+
});
|
|
386
|
+
this.assertGoalOwner(run);
|
|
387
|
+
const paused = (0, CodexGoals_1.parseNativeGoal)(response?.goal, this.codexThreadId);
|
|
388
|
+
if (!paused || paused.status !== 'paused')
|
|
389
|
+
throw new Error('Codex did not pause the saved goal before loading its policies.');
|
|
390
|
+
run.goal = paused;
|
|
391
|
+
if (run.controlling)
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
await this.ensureThread();
|
|
395
|
+
this.assertGoalOwner(run);
|
|
396
|
+
this.rememberGoalManagement();
|
|
397
|
+
if (run.controlling)
|
|
398
|
+
return;
|
|
399
|
+
// Native continuations bypass turn/start, so their settings must be sticky.
|
|
400
|
+
const goalPolicy = this.sandbox.turnOptions(this.currentWorkingDirectory);
|
|
401
|
+
if (this.model || this.effort || this.speed !== undefined || Object.keys(goalPolicy).length) {
|
|
402
|
+
const speedOptions = await this.speedOptions();
|
|
403
|
+
this.assertGoalOwner(run);
|
|
404
|
+
if (run.controlling)
|
|
405
|
+
return;
|
|
406
|
+
await this.goalRequest('thread/settings/update', { threadId: this.codexThreadId,
|
|
407
|
+
...(this.model ? { model: this.model } : {}), ...(this.effort ? { effort: this.effort } : {}),
|
|
408
|
+
...speedOptions, ...goalPolicy });
|
|
409
|
+
this.assertGoalOwner(run);
|
|
410
|
+
if (run.controlling)
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
// A resumed active goal may already have completed while the load request ran.
|
|
414
|
+
if (request.objective !== undefined || run.goal?.status !== 'complete') {
|
|
415
|
+
const revision = run.revision;
|
|
416
|
+
const response = await this.goalRequest('thread/goal/set', {
|
|
417
|
+
threadId: this.codexThreadId, origin: 'user',
|
|
418
|
+
...(request.objective !== undefined ? { objective: request.objective } : {}),
|
|
419
|
+
status: 'active', ...(request.tokenBudget !== undefined ? { tokenBudget: request.tokenBudget } : {}),
|
|
420
|
+
});
|
|
421
|
+
this.assertGoalOwner(run);
|
|
422
|
+
const goal = (0, CodexGoals_1.parseNativeGoal)(response?.goal, this.codexThreadId);
|
|
423
|
+
if (!goal)
|
|
424
|
+
throw new Error('Codex did not return the activated native goal.');
|
|
425
|
+
if (run.revision === revision)
|
|
426
|
+
run.goal = goal;
|
|
427
|
+
}
|
|
428
|
+
run.ready = true;
|
|
429
|
+
this.goalNeedsResume = false;
|
|
430
|
+
if (run.goal?.status === 'active') {
|
|
431
|
+
this.goalNotice(run, run.inFlight.size ? 'Native goal running' : 'Native goal active; waiting for native continuation');
|
|
432
|
+
}
|
|
433
|
+
this.maybeCompleteGoal(run);
|
|
434
|
+
if (this.goalRun === run && !run.inFlight.size)
|
|
435
|
+
void this.checkGoalAfterTurn(run);
|
|
436
|
+
}
|
|
437
|
+
assertGoalOwner(run) {
|
|
438
|
+
if (this.goalRun !== run || this.destroyed || run.stopping)
|
|
439
|
+
throw new Error('Native goal operation was cancelled.');
|
|
440
|
+
}
|
|
441
|
+
goalNotice(run, text) {
|
|
442
|
+
const bounded = Array.from(text).slice(0, 400).join('');
|
|
443
|
+
if (this.goalRun !== run || run.lastNotice === bounded)
|
|
444
|
+
return;
|
|
445
|
+
run.lastNotice = bounded;
|
|
446
|
+
try {
|
|
447
|
+
if (run.active.options.onActivity)
|
|
448
|
+
run.active.options.onActivity({ source: 'state', text: bounded });
|
|
449
|
+
else
|
|
450
|
+
run.active.options.onStream?.(`\n[${bounded}]\n`);
|
|
451
|
+
}
|
|
452
|
+
catch { /* A display failure cannot change native goal ownership. */ }
|
|
453
|
+
}
|
|
454
|
+
maybeCompleteGoal(run) {
|
|
455
|
+
if (this.goalRun !== run || !run.ready || run.controlling || run.stopping || run.inFlight.size
|
|
456
|
+
|| run.goal === undefined || run.goal?.status === 'active')
|
|
457
|
+
return;
|
|
458
|
+
const status = run.goal?.status ?? 'cleared';
|
|
459
|
+
this.goalNotice(run, `Native goal ${status}`);
|
|
460
|
+
const summary = `\n\nNative goal ${status}.\n`;
|
|
461
|
+
run.active.output.push(summary);
|
|
462
|
+
try {
|
|
463
|
+
run.active.options.onStream?.(summary);
|
|
464
|
+
}
|
|
465
|
+
catch { /* Native completion still releases ownership. */ }
|
|
466
|
+
try {
|
|
467
|
+
run.active.options.onDisplayText?.(summary);
|
|
468
|
+
}
|
|
469
|
+
catch { /* Native completion still releases ownership. */ }
|
|
470
|
+
this.completeActive({ success: status === 'complete' || status === 'paused' || status === 'cleared',
|
|
471
|
+
output: run.active.output.join(''), sessionAbbr: this.codexThreadId?.slice(0, 8),
|
|
472
|
+
...(['blocked', 'usageLimited', 'budgetLimited'].includes(status) ? { error: `Native goal stopped: ${status}.` } : {}) });
|
|
473
|
+
}
|
|
474
|
+
async checkGoalAfterTurn(run) {
|
|
475
|
+
if (this.goalRun !== run || run.checking || run.controlling || run.stopping)
|
|
476
|
+
return;
|
|
477
|
+
run.checking = true;
|
|
478
|
+
const revision = run.revision;
|
|
479
|
+
try {
|
|
480
|
+
const goal = await this.getGoal();
|
|
481
|
+
if (this.goalRun !== run || run.controlling || run.stopping)
|
|
482
|
+
return;
|
|
483
|
+
if (revision === run.revision)
|
|
484
|
+
run.goal = goal;
|
|
485
|
+
this.maybeCompleteGoal(run);
|
|
486
|
+
}
|
|
487
|
+
catch (error) {
|
|
488
|
+
if (this.goalRun === run)
|
|
489
|
+
await this.failGoal(run, `Could not reconcile native goal: ${(0, CodexGoals_1.nativeGoalError)(error)}`);
|
|
490
|
+
}
|
|
491
|
+
finally {
|
|
492
|
+
run.checking = false;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
goalDeadline(promise) {
|
|
496
|
+
return new Promise((resolve, reject) => {
|
|
497
|
+
const timer = setTimeout(() => reject(new Error('Native goal control timed out.')), GOAL_CONTROL_TIMEOUT_MS);
|
|
498
|
+
promise.then(value => { clearTimeout(timer); resolve(value); }, error => { clearTimeout(timer); reject(error); });
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
goalRequest(method, params) {
|
|
502
|
+
if (this.destroyed)
|
|
503
|
+
return Promise.reject(new Error('Executor has been destroyed'));
|
|
504
|
+
return this.goalDeadline(this.client.request(method, params, GOAL_CONTROL_TIMEOUT_MS));
|
|
505
|
+
}
|
|
506
|
+
async stopGoalTransport() {
|
|
507
|
+
this.threadReady = false;
|
|
508
|
+
this.goalNeedsResume = this.goalManaged;
|
|
509
|
+
this.goalTransportStops++;
|
|
510
|
+
try {
|
|
511
|
+
await this.goalDeadline(this.client.stop());
|
|
512
|
+
if (this.client.waitForExit)
|
|
513
|
+
await this.goalDeadline(this.client.waitForExit());
|
|
514
|
+
else if (this.client.isRunning())
|
|
515
|
+
throw new Error('Codex goal process exit could not be confirmed.');
|
|
516
|
+
}
|
|
517
|
+
finally {
|
|
518
|
+
this.goalTransportStops--;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
async failGoal(run, error) {
|
|
522
|
+
if (this.goalRun !== run || run.stopping)
|
|
523
|
+
return;
|
|
524
|
+
run.stopping = true;
|
|
525
|
+
this.clearInactivityTimer();
|
|
526
|
+
if (run.active.timeoutTimer)
|
|
527
|
+
clearTimeout(run.active.timeoutTimer);
|
|
528
|
+
try {
|
|
529
|
+
await this.stopGoalTransport();
|
|
530
|
+
if (this.goalRun === run)
|
|
531
|
+
this.completeActive({ success: false, error });
|
|
532
|
+
}
|
|
533
|
+
catch (stopError) {
|
|
534
|
+
run.stopping = false;
|
|
535
|
+
this.goalNotice(run, `Native goal stop unconfirmed: ${(0, CodexGoals_1.nativeGoalError)(stopError)}. Retry /abort.`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
controlGoal(clear) {
|
|
539
|
+
if (this.goalControl) {
|
|
540
|
+
if (this.goalControlClear === clear)
|
|
541
|
+
return this.goalControl;
|
|
542
|
+
return this.goalControl.then(result => result.success ? this.controlGoal(clear) : result);
|
|
543
|
+
}
|
|
544
|
+
const control = this.stopGoalControl(this.goalRun, clear);
|
|
545
|
+
this.goalControl = control;
|
|
546
|
+
this.goalControlClear = clear;
|
|
547
|
+
void control.then(() => { if (this.goalControl === control)
|
|
548
|
+
this.goalControl = null; });
|
|
549
|
+
return control;
|
|
550
|
+
}
|
|
551
|
+
async stopGoalControl(run, clear) {
|
|
552
|
+
if (this.destroyed)
|
|
553
|
+
return { success: false, error: 'Executor has been destroyed' };
|
|
554
|
+
if (!run && (this.activeTurn || this.compactWaiter || this.threadLoads)) {
|
|
555
|
+
return { success: false, error: 'Wait for the current ordinary task before changing its goal.' };
|
|
556
|
+
}
|
|
557
|
+
if (!this.codexThreadId && !run)
|
|
558
|
+
return { success: true, output: 'No native goal is set.' };
|
|
559
|
+
if (run)
|
|
560
|
+
run.controlling = true;
|
|
561
|
+
try {
|
|
562
|
+
if (run?.startup)
|
|
563
|
+
await this.goalDeadline(run.startup);
|
|
564
|
+
if (run && this.goalRun !== run)
|
|
565
|
+
return { success: false, error: 'The native goal run already stopped.' };
|
|
566
|
+
await this.goalDeadline(this.client.start());
|
|
567
|
+
(0, CodexGoals_1.assertNativeGoalsVersion)(this.client.getServerVersion?.());
|
|
568
|
+
const threadId = this.codexThreadId;
|
|
569
|
+
if (!threadId)
|
|
570
|
+
throw new Error('No native goal session was created.');
|
|
571
|
+
this.rememberGoalManagement();
|
|
572
|
+
const existing = await this.getGoal();
|
|
573
|
+
let goal = existing;
|
|
574
|
+
if (existing && existing.status !== 'complete') {
|
|
575
|
+
const paused = await this.goalRequest('thread/goal/set', { threadId, status: 'paused', origin: 'user' });
|
|
576
|
+
goal = (0, CodexGoals_1.parseNativeGoal)(paused?.goal, threadId);
|
|
577
|
+
if (!goal || goal.status === 'active')
|
|
578
|
+
throw new Error('Codex did not confirm that the native goal is paused.');
|
|
579
|
+
}
|
|
580
|
+
if (clear && existing) {
|
|
581
|
+
const response = await this.goalRequest('thread/goal/clear', { threadId, origin: 'user' });
|
|
582
|
+
if (typeof response?.cleared !== 'boolean')
|
|
583
|
+
throw new Error('Codex did not confirm clearing the native goal.');
|
|
584
|
+
goal = null;
|
|
585
|
+
}
|
|
586
|
+
if (run) {
|
|
587
|
+
run.goal = goal;
|
|
588
|
+
for (const pending of this.pendingUserInputs.splice(0)) {
|
|
589
|
+
this.finishApproval(pending, 'expired');
|
|
590
|
+
this.client.respond(pending.id, pending.method === 'item/tool/requestUserInput' ? { answers: {} }
|
|
591
|
+
: pending.method === 'item/permissions/requestApproval' ? { permissions: {}, scope: 'turn' } : { decision: 'cancel' });
|
|
592
|
+
}
|
|
593
|
+
for (const turnId of [...run.inFlight])
|
|
594
|
+
await this.goalRequest('turn/interrupt', { threadId, turnId });
|
|
595
|
+
if (run.inFlight.size)
|
|
596
|
+
await this.goalDeadline(new Promise(resolve => { run.onIdle = resolve; }));
|
|
597
|
+
if (this.goalRun !== run)
|
|
598
|
+
throw new Error('The native goal stopped before its control operation finished.');
|
|
599
|
+
}
|
|
600
|
+
// A late dispatched continuation must not outlive a successful stop ACK.
|
|
601
|
+
// Reopening this process is safe because native state is now paused/cleared.
|
|
602
|
+
await this.stopGoalTransport();
|
|
603
|
+
this.goalNeedsResume = false;
|
|
604
|
+
if (run) {
|
|
605
|
+
if (this.goalRun !== run)
|
|
606
|
+
throw new Error('The native goal stopped before its control operation finished.');
|
|
607
|
+
run.inFlight.clear();
|
|
608
|
+
run.ready = true;
|
|
609
|
+
run.controlling = false;
|
|
610
|
+
this.maybeCompleteGoal(run);
|
|
611
|
+
const result = await this.goalDeadline(run.done);
|
|
612
|
+
if (!result.success)
|
|
613
|
+
throw new Error(result.error ?? 'Native goal did not stop successfully.');
|
|
614
|
+
}
|
|
615
|
+
return { success: true, output: clear ? 'Native goal cleared; execution stopped.'
|
|
616
|
+
: goal?.status === 'complete' ? 'Native goal was already complete; execution stopped.'
|
|
617
|
+
: goal ? 'Native goal paused; execution stopped.' : 'No native goal is set; execution stopped.' };
|
|
618
|
+
}
|
|
619
|
+
catch (error) {
|
|
620
|
+
if (run && this.goalRun === run) {
|
|
621
|
+
run.controlling = false;
|
|
622
|
+
await this.failGoal(run, `Native goal control failed: ${(0, CodexGoals_1.nativeGoalError)(error)}. ${GOAL_RECOVERY_GUIDANCE}`);
|
|
623
|
+
}
|
|
624
|
+
return { success: false, error: (0, CodexGoals_1.nativeGoalError)(error) };
|
|
625
|
+
}
|
|
626
|
+
}
|
|
272
627
|
getExecutionMetadata() {
|
|
273
628
|
const data = (0, ExecutionMetadata_1.configuredExecutionMetadata)(this.model, this.effort);
|
|
274
629
|
if (this.threadReady && this.client.isRunning() && this.reportedModel) {
|
|
@@ -288,7 +643,7 @@ class CodexAppServerExecutor {
|
|
|
288
643
|
this.reportedEffort = response.reasoningEffort;
|
|
289
644
|
}
|
|
290
645
|
isBusy() {
|
|
291
|
-
return this.activeTurn !== null || this.compactWaiter !== null;
|
|
646
|
+
return this.activeTurn !== null || this.compactWaiter !== null || this.goalControl !== null || this.threadLoads > 0;
|
|
292
647
|
}
|
|
293
648
|
async setWorkingDirectory(targetPath) {
|
|
294
649
|
const resolved = this.directoryGuard.resolveWorkingDirectory(targetPath, this.currentWorkingDirectory);
|
|
@@ -341,6 +696,8 @@ class CodexAppServerExecutor {
|
|
|
341
696
|
}
|
|
342
697
|
}
|
|
343
698
|
resetContext() {
|
|
699
|
+
if (this.goalRun)
|
|
700
|
+
throw new Error('Stop the native goal before clearing its conversation.');
|
|
344
701
|
this.taskNotifications.clear();
|
|
345
702
|
const previousId = this.codexThreadId;
|
|
346
703
|
if (this.activeTurn || this.compactWaiter) {
|
|
@@ -349,6 +706,8 @@ class CodexAppServerExecutor {
|
|
|
349
706
|
void this.client.stop();
|
|
350
707
|
}
|
|
351
708
|
this.codexThreadId = null;
|
|
709
|
+
this.goalManaged = false;
|
|
710
|
+
this.goalNeedsResume = false;
|
|
352
711
|
this.threadReady = false;
|
|
353
712
|
this.pendingUserInputs = [];
|
|
354
713
|
this.clearThreadId();
|
|
@@ -357,6 +716,8 @@ class CodexAppServerExecutor {
|
|
|
357
716
|
}
|
|
358
717
|
}
|
|
359
718
|
async abort() {
|
|
719
|
+
if (this.goalRun)
|
|
720
|
+
return (await this.pauseGoal()).success;
|
|
360
721
|
const active = this.activeTurn;
|
|
361
722
|
if (!active || !this.codexThreadId)
|
|
362
723
|
return false;
|
|
@@ -384,12 +745,17 @@ class CodexAppServerExecutor {
|
|
|
384
745
|
return true;
|
|
385
746
|
}
|
|
386
747
|
async destroy() {
|
|
387
|
-
if (this.destroyed)
|
|
748
|
+
if (this.destroyed && !this.goalRun)
|
|
388
749
|
return;
|
|
389
750
|
this.destroyed = true;
|
|
390
751
|
this.taskNotifications.clear();
|
|
391
752
|
this.unsubscribeClient?.();
|
|
392
753
|
this.unsubscribeClient = null;
|
|
754
|
+
if (this.goalRun) {
|
|
755
|
+
this.goalRun.stopping = true;
|
|
756
|
+
this.clearInactivityTimer();
|
|
757
|
+
await this.stopGoalTransport();
|
|
758
|
+
}
|
|
393
759
|
if (this.activeTurn)
|
|
394
760
|
this.completeActive({ success: false, error: 'Executor destroyed' });
|
|
395
761
|
this.completeCompact({ success: false, error: 'Executor destroyed' });
|
|
@@ -505,15 +871,22 @@ class CodexAppServerExecutor {
|
|
|
505
871
|
}
|
|
506
872
|
async listModels() {
|
|
507
873
|
await this.refreshModelCatalogAfterCommandUpgrade();
|
|
874
|
+
return this.readModelCatalog();
|
|
875
|
+
}
|
|
876
|
+
async readModelCatalog() {
|
|
508
877
|
const models = [];
|
|
509
878
|
let cursor = null;
|
|
510
879
|
const currentModelId = this.reportedModel ?? this.model;
|
|
511
880
|
do {
|
|
512
|
-
const
|
|
881
|
+
const owner = this.goalRun;
|
|
882
|
+
const params = {
|
|
513
883
|
cursor,
|
|
514
884
|
limit: 100,
|
|
515
885
|
includeHidden: false,
|
|
516
|
-
}
|
|
886
|
+
};
|
|
887
|
+
const response = await (owner ? this.goalRequest('model/list', params) : this.client.request('model/list', params));
|
|
888
|
+
if (owner)
|
|
889
|
+
this.assertGoalOwner(owner);
|
|
517
890
|
for (const model of Array.isArray(response?.data) ? response.data : []) {
|
|
518
891
|
if (typeof model?.id !== 'string')
|
|
519
892
|
continue;
|
|
@@ -530,6 +903,11 @@ class CodexAppServerExecutor {
|
|
|
530
903
|
? model.defaultReasoningEffort
|
|
531
904
|
: undefined,
|
|
532
905
|
inputModalities: Array.isArray(model.inputModalities) ? model.inputModalities : undefined,
|
|
906
|
+
...(Array.isArray(model.serviceTiers) && model.serviceTiers.length <= 32
|
|
907
|
+
&& model.serviceTiers.every((tier) => tier && this.validSpeedValue(tier.id) && this.validSpeedValue(tier.name)
|
|
908
|
+
&& (tier.description === undefined || (typeof tier.description === 'string' && tier.description.length <= 4096)))
|
|
909
|
+
? { serviceTiers: model.serviceTiers.map((tier) => ({ id: tier.id, name: tier.name, description: tier.description })),
|
|
910
|
+
...(model.defaultServiceTier === null || this.validSpeedValue(model.defaultServiceTier) ? { defaultServiceTier: model.defaultServiceTier } : {}) } : {}),
|
|
533
911
|
});
|
|
534
912
|
}
|
|
535
913
|
cursor = typeof response?.nextCursor === 'string' ? response.nextCursor : null;
|
|
@@ -582,6 +960,10 @@ class CodexAppServerExecutor {
|
|
|
582
960
|
if (!selected) {
|
|
583
961
|
return { success: false, error: `Unknown Codex model: ${model}. Use /model to list available models.` };
|
|
584
962
|
}
|
|
963
|
+
if (typeof this.speed === 'string' && (!selected.serviceTiers?.length
|
|
964
|
+
|| (this.speed !== 'default' && !selected.serviceTiers.some(tier => tier.id === this.speed)))) {
|
|
965
|
+
return { success: false, error: 'The saved speed is incompatible with this model. Follow native config in the Speed section before changing models.' };
|
|
966
|
+
}
|
|
585
967
|
if (this.effort && !selected.supportedReasoningEfforts?.includes(this.effort)) {
|
|
586
968
|
return { success: false, error: 'The saved reasoning effort is incompatible with this model. Reset /effort auto before changing models.' };
|
|
587
969
|
}
|
|
@@ -595,10 +977,157 @@ class CodexAppServerExecutor {
|
|
|
595
977
|
}
|
|
596
978
|
}
|
|
597
979
|
clearModel() {
|
|
980
|
+
if (typeof this.speed === 'string')
|
|
981
|
+
throw new Error('Follow native config in the Speed section before clearing the model override.');
|
|
598
982
|
this.model = undefined;
|
|
599
983
|
this.reportedModel = undefined;
|
|
600
984
|
this.reportedEffort = undefined;
|
|
601
985
|
}
|
|
986
|
+
validSpeedValue(value) {
|
|
987
|
+
return typeof value === 'string' && value.length > 0 && value.length <= 512 && !/[\x00-\x1f\x7f]/.test(value);
|
|
988
|
+
}
|
|
989
|
+
async listSpeeds(models) {
|
|
990
|
+
const catalog = models ?? await this.listModels();
|
|
991
|
+
const recovery = this.speed !== undefined ? { choices: [], current: this.speed ?? undefined } : undefined;
|
|
992
|
+
// Do not add config RPCs to legacy catalogs or to models with no tier choices.
|
|
993
|
+
if (!catalog.some(model => model.serviceTiers?.length))
|
|
994
|
+
return recovery;
|
|
995
|
+
let id = this.reportedModel ?? this.model;
|
|
996
|
+
if (!id) {
|
|
997
|
+
try {
|
|
998
|
+
id = (await this.readNativeSpeedConfig()).model;
|
|
999
|
+
}
|
|
1000
|
+
catch (error) {
|
|
1001
|
+
// A failed native query must not turn a catalog default into an offer of
|
|
1002
|
+
// paid speed. A saved override can still be cleared without discovery.
|
|
1003
|
+
if (recovery)
|
|
1004
|
+
return recovery;
|
|
1005
|
+
throw error;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
const selected = id ? catalog.find(model => model.id === id) : catalog.find(model => model.isDefault);
|
|
1009
|
+
if (!selected?.serviceTiers?.length)
|
|
1010
|
+
return recovery;
|
|
1011
|
+
const choices = [{ value: 'default', displayName: 'Standard', description: 'Use standard processing.' },
|
|
1012
|
+
...selected.serviceTiers.filter(tier => tier.id !== 'default').map(tier => ({ value: tier.id, displayName: tier.name, description: tier.description }))];
|
|
1013
|
+
return { choices, current: this.speed ?? undefined, default: selected.defaultServiceTier };
|
|
1014
|
+
}
|
|
1015
|
+
async assertReportedSpeed(speed) {
|
|
1016
|
+
if (!this.reportedModel)
|
|
1017
|
+
return;
|
|
1018
|
+
const owner = this.goalRun;
|
|
1019
|
+
const catalog = await (owner ? this.goalDeadline(this.readModelCatalog()) : this.readModelCatalog());
|
|
1020
|
+
if (owner)
|
|
1021
|
+
this.assertGoalOwner(owner);
|
|
1022
|
+
const selected = catalog.find(model => model.id === this.reportedModel);
|
|
1023
|
+
if (!selected?.serviceTiers?.length || (speed !== 'default' && !selected.serviceTiers.some(tier => tier.id === speed))) {
|
|
1024
|
+
throw new Error('This speed is unavailable for the model reported by Codex. Follow native config in /model before starting work.');
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
async speedOptions(speed = this.speed) {
|
|
1028
|
+
if (speed === undefined)
|
|
1029
|
+
return {};
|
|
1030
|
+
if (speed !== null)
|
|
1031
|
+
return { serviceTier: speed };
|
|
1032
|
+
// Avoid the public upgrade check while preparing a turn: replacing the
|
|
1033
|
+
// transport here would discard the native thread just loaded by ensureThread.
|
|
1034
|
+
const catalog = await this.readModelCatalog();
|
|
1035
|
+
if (!catalog.some(model => model.serviceTiers !== undefined)) {
|
|
1036
|
+
if (this.codexThreadId && !(this.threadReady && this.legacyFollowReady)) {
|
|
1037
|
+
throw new Error('This Codex version cannot safely clear the speed of a saved conversation. Upgrade Codex or use /clear before resuming.');
|
|
1038
|
+
}
|
|
1039
|
+
// Old protocols must never receive unsupported serviceTier keys. A fresh
|
|
1040
|
+
// thread can safely use native startup configuration without an override.
|
|
1041
|
+
return {};
|
|
1042
|
+
}
|
|
1043
|
+
const config = await this.readNativeSpeedConfig();
|
|
1044
|
+
const id = this.reportedModel ?? this.model ?? config.model;
|
|
1045
|
+
const selected = id ? catalog.find(model => model.id === id) : catalog.find(model => model.isDefault);
|
|
1046
|
+
if (config.tier === 'fast' || config.tier === 'priority') {
|
|
1047
|
+
const fast = selected?.serviceTiers?.find(tier => tier.id === 'priority')
|
|
1048
|
+
?? selected?.serviceTiers?.find(tier => tier.id === 'fast');
|
|
1049
|
+
return { serviceTier: config.fastMode && fast ? fast.id : null };
|
|
1050
|
+
}
|
|
1051
|
+
if (config.tier !== null && config.tier !== 'default' && config.tier !== 'flex'
|
|
1052
|
+
&& !selected?.serviceTiers?.some(tier => tier.id === config.tier)) {
|
|
1053
|
+
throw new Error('Codex configured speed is unavailable for the selected model. Check native configuration before resuming.');
|
|
1054
|
+
}
|
|
1055
|
+
return { serviceTier: config.tier };
|
|
1056
|
+
}
|
|
1057
|
+
async readNativeSpeedConfig() {
|
|
1058
|
+
const owner = this.goalRun;
|
|
1059
|
+
const params = { includeLayers: false, cwd: this.currentWorkingDirectory };
|
|
1060
|
+
const response = await (owner ? this.goalRequest('config/read', params) : this.client.request('config/read', params));
|
|
1061
|
+
if (owner)
|
|
1062
|
+
this.assertGoalOwner(owner);
|
|
1063
|
+
if (!response?.config || typeof response.config !== 'object')
|
|
1064
|
+
throw new Error('Codex did not provide its native speed configuration.');
|
|
1065
|
+
const native = response.config.service_tier;
|
|
1066
|
+
if (native !== undefined && native !== null && !this.validSpeedValue(native))
|
|
1067
|
+
throw new Error('Codex returned an invalid native speed setting.');
|
|
1068
|
+
const model = response.config.model;
|
|
1069
|
+
if (model !== undefined && model !== null && !this.validSpeedValue(model))
|
|
1070
|
+
throw new Error('Codex returned an invalid configured model.');
|
|
1071
|
+
const features = response.config.features;
|
|
1072
|
+
if (features !== undefined && (features === null || typeof features !== 'object' || Array.isArray(features)))
|
|
1073
|
+
throw new Error('Codex returned invalid native speed features.');
|
|
1074
|
+
const fastMode = features?.fast_mode;
|
|
1075
|
+
if (fastMode !== undefined && typeof fastMode !== 'boolean')
|
|
1076
|
+
throw new Error('Codex returned an invalid native Fast mode setting.');
|
|
1077
|
+
return { model: model ?? undefined, tier: native ?? null, fastMode: fastMode !== false };
|
|
1078
|
+
}
|
|
1079
|
+
async setSpeed(speed) {
|
|
1080
|
+
try {
|
|
1081
|
+
const cwd = this.currentWorkingDirectory, threadId = this.codexThreadId;
|
|
1082
|
+
const assertCurrent = () => {
|
|
1083
|
+
if (this.isBusy() || this.destroyed || cwd !== this.currentWorkingDirectory || threadId !== this.codexThreadId)
|
|
1084
|
+
throw new Error('The Codex thread changed during speed selection.');
|
|
1085
|
+
};
|
|
1086
|
+
if (this.isBusy() || this.destroyed)
|
|
1087
|
+
throw new Error('The Codex thread must be idle to change speed.');
|
|
1088
|
+
if (speed === undefined) {
|
|
1089
|
+
// Reset is recovery, not another selection. Never load a saved thread
|
|
1090
|
+
// through its obsolete tier, nor resume suspended native goals here.
|
|
1091
|
+
this.threadReady = false;
|
|
1092
|
+
this.legacyFollowReady = false;
|
|
1093
|
+
this.threadLoads++;
|
|
1094
|
+
try {
|
|
1095
|
+
await this.client.stop();
|
|
1096
|
+
await this.client.waitForExit?.();
|
|
1097
|
+
}
|
|
1098
|
+
finally {
|
|
1099
|
+
this.threadLoads--;
|
|
1100
|
+
}
|
|
1101
|
+
assertCurrent();
|
|
1102
|
+
this.speed = null;
|
|
1103
|
+
return { success: true, output: 'Speed follows native configuration on the next safe resume. Older Codex versions may require an upgrade or /clear.' };
|
|
1104
|
+
}
|
|
1105
|
+
const choices = await this.listSpeeds();
|
|
1106
|
+
assertCurrent();
|
|
1107
|
+
if (!choices || !choices.choices.some(choice => choice.value === speed)) {
|
|
1108
|
+
throw new Error('This speed is unavailable for the current Codex model. Refresh /model.');
|
|
1109
|
+
}
|
|
1110
|
+
if (this.isBusy() || this.destroyed)
|
|
1111
|
+
throw new Error('The Codex thread changed during speed selection.');
|
|
1112
|
+
if (this.codexThreadId && !(this.goalManaged && this.goalNeedsResume)) {
|
|
1113
|
+
await this.ensureThread();
|
|
1114
|
+
assertCurrent();
|
|
1115
|
+
// Restored conversations can report a different model than the preview.
|
|
1116
|
+
await this.assertReportedSpeed(speed);
|
|
1117
|
+
assertCurrent();
|
|
1118
|
+
// Explicit selections may update a live thread. Follow never loads it.
|
|
1119
|
+
const { serviceTier } = await this.speedOptions(speed);
|
|
1120
|
+
assertCurrent();
|
|
1121
|
+
await this.client.request('thread/settings/update', { threadId: this.codexThreadId, serviceTier });
|
|
1122
|
+
assertCurrent();
|
|
1123
|
+
}
|
|
1124
|
+
this.speed = speed;
|
|
1125
|
+
return { success: true, output: 'Speed preference applied.' };
|
|
1126
|
+
}
|
|
1127
|
+
catch (error) {
|
|
1128
|
+
return { success: false, error: this.errorMessage(error) };
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
602
1131
|
async listEfforts() {
|
|
603
1132
|
const models = await this.listModels();
|
|
604
1133
|
const currentModelId = this.reportedModel ?? this.model;
|
|
@@ -709,14 +1238,45 @@ class CodexAppServerExecutor {
|
|
|
709
1238
|
this.sandbox.deleteData();
|
|
710
1239
|
}
|
|
711
1240
|
async ensureThread() {
|
|
1241
|
+
this.threadLoads++;
|
|
1242
|
+
try {
|
|
1243
|
+
await this.loadThread();
|
|
1244
|
+
}
|
|
1245
|
+
finally {
|
|
1246
|
+
this.threadLoads--;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
async loadThread() {
|
|
712
1250
|
(0, WorkingDirectory_1.assertWorkingDirectoryExists)(this.currentWorkingDirectory);
|
|
1251
|
+
if (this.goalManaged && this.codexThreadId && !this.goalRun) {
|
|
1252
|
+
const goal = await this.getGoal();
|
|
1253
|
+
if (this.goalNeedsResume || goal?.status === 'active')
|
|
1254
|
+
throw new Error(GOAL_RECOVERY_GUIDANCE);
|
|
1255
|
+
}
|
|
713
1256
|
if (this.threadReady && this.client.isRunning())
|
|
714
1257
|
return;
|
|
1258
|
+
this.threadReady = false;
|
|
1259
|
+
this.legacyFollowReady = false;
|
|
1260
|
+
const goalOwner = this.goalRun;
|
|
715
1261
|
this.reportedModel = undefined;
|
|
716
1262
|
this.reportedEffort = undefined;
|
|
717
1263
|
await this.client.start();
|
|
1264
|
+
if (goalOwner)
|
|
1265
|
+
this.assertGoalOwner(goalOwner);
|
|
1266
|
+
if (typeof this.speed === 'string') {
|
|
1267
|
+
const available = await (goalOwner ? this.goalDeadline(this.listSpeeds()) : this.listSpeeds());
|
|
1268
|
+
if (goalOwner)
|
|
1269
|
+
this.assertGoalOwner(goalOwner);
|
|
1270
|
+
if (!available?.choices.some(choice => choice.value === this.speed)) {
|
|
1271
|
+
throw new Error('The saved speed is unavailable for this Codex model. Follow native config in /model before starting work.');
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
const speedOptions = await this.speedOptions();
|
|
1275
|
+
if (goalOwner)
|
|
1276
|
+
this.assertGoalOwner(goalOwner);
|
|
718
1277
|
const common = {
|
|
719
1278
|
...(this.model ? { model: this.model } : {}),
|
|
1279
|
+
...speedOptions,
|
|
720
1280
|
cwd: this.currentWorkingDirectory,
|
|
721
1281
|
...(this.autoApprove ? {
|
|
722
1282
|
approvalPolicy: 'never',
|
|
@@ -724,6 +1284,9 @@ class CodexAppServerExecutor {
|
|
|
724
1284
|
} : {}),
|
|
725
1285
|
...this.sandbox.threadOptions(this.currentWorkingDirectory, this.delegationWorker || !!this.delegation),
|
|
726
1286
|
};
|
|
1287
|
+
// A saved active goal can dispatch during resume, before settings/update.
|
|
1288
|
+
if (goalOwner && this.effort)
|
|
1289
|
+
common.config = { ...common.config, model_reasoning_effort: this.effort };
|
|
727
1290
|
if (this.delegationConfigured) {
|
|
728
1291
|
common.config = { ...common.config,
|
|
729
1292
|
'mcp_servers.remote_cli_delegation': this.delegation
|
|
@@ -744,6 +1307,8 @@ class CodexAppServerExecutor {
|
|
|
744
1307
|
if (response?.thread?.id !== this.codexThreadId) {
|
|
745
1308
|
throw new Error('Codex app-server resumed an unexpected thread');
|
|
746
1309
|
}
|
|
1310
|
+
if (goalOwner)
|
|
1311
|
+
this.assertGoalOwner(goalOwner);
|
|
747
1312
|
this.captureReportedSettings(response);
|
|
748
1313
|
}
|
|
749
1314
|
catch (error) {
|
|
@@ -761,14 +1326,23 @@ class CodexAppServerExecutor {
|
|
|
761
1326
|
serviceName: 'remote_cli',
|
|
762
1327
|
});
|
|
763
1328
|
const id = response?.thread?.id;
|
|
1329
|
+
if (goalOwner)
|
|
1330
|
+
this.assertGoalOwner(goalOwner);
|
|
764
1331
|
if (typeof id !== 'string' || !id)
|
|
765
1332
|
throw new Error('Codex app-server returned no thread ID');
|
|
766
1333
|
this.captureReportedSettings(response);
|
|
767
1334
|
this.setThreadId(id);
|
|
1335
|
+
this.legacyFollowReady = this.speed === null && !Object.prototype.hasOwnProperty.call(common, 'serviceTier');
|
|
768
1336
|
}
|
|
1337
|
+
if (typeof this.speed === 'string')
|
|
1338
|
+
await this.assertReportedSpeed(this.speed);
|
|
1339
|
+
if (goalOwner)
|
|
1340
|
+
this.assertGoalOwner(goalOwner);
|
|
769
1341
|
this.threadReady = true;
|
|
770
1342
|
}
|
|
771
1343
|
handleMessage(message) {
|
|
1344
|
+
if (this.destroyed)
|
|
1345
|
+
return;
|
|
772
1346
|
const method = message.method;
|
|
773
1347
|
if (!method)
|
|
774
1348
|
return;
|
|
@@ -776,7 +1350,12 @@ class CodexAppServerExecutor {
|
|
|
776
1350
|
if (method === 'client/disconnected') {
|
|
777
1351
|
this.taskNotifications.clear();
|
|
778
1352
|
this.threadReady = false;
|
|
779
|
-
if (this.
|
|
1353
|
+
if (this.goalRun) {
|
|
1354
|
+
if (!this.goalTransportStops) {
|
|
1355
|
+
void this.failGoal(this.goalRun, `${params.error ?? 'Codex app-server disconnected'}. ${GOAL_RECOVERY_GUIDANCE}`);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
else if (this.activeTurn) {
|
|
780
1359
|
this.completeActive({ success: false, error: params.error ?? 'Codex app-server disconnected' });
|
|
781
1360
|
}
|
|
782
1361
|
this.completeCompact({ success: false, error: params.error ?? 'Codex app-server disconnected' });
|
|
@@ -788,6 +1367,29 @@ class CodexAppServerExecutor {
|
|
|
788
1367
|
}
|
|
789
1368
|
if (params.threadId && params.threadId !== this.codexThreadId)
|
|
790
1369
|
return;
|
|
1370
|
+
const goalRun = this.goalRun;
|
|
1371
|
+
if (goalRun?.stopping)
|
|
1372
|
+
return;
|
|
1373
|
+
// Goal changes may arrive after their turn completed, or without any turn ID.
|
|
1374
|
+
if (goalRun && (method === 'thread/goal/updated' || method === 'thread/goal/cleared')) {
|
|
1375
|
+
if (params.threadId !== this.codexThreadId)
|
|
1376
|
+
return;
|
|
1377
|
+
try {
|
|
1378
|
+
const goal = method === 'thread/goal/cleared' ? null : (0, CodexGoals_1.parseNativeGoal)(params.goal, this.codexThreadId);
|
|
1379
|
+
if (method === 'thread/goal/updated' && !goal)
|
|
1380
|
+
throw new Error('Codex returned an empty native goal update.');
|
|
1381
|
+
if (goal && (goalRun.objective && goal.objective !== goalRun.objective
|
|
1382
|
+
|| goalRun.goal && goal.updatedAt < goalRun.goal.updatedAt))
|
|
1383
|
+
return;
|
|
1384
|
+
goalRun.goal = goal;
|
|
1385
|
+
goalRun.revision++;
|
|
1386
|
+
this.maybeCompleteGoal(goalRun);
|
|
1387
|
+
}
|
|
1388
|
+
catch (error) {
|
|
1389
|
+
void this.failGoal(goalRun, (0, CodexGoals_1.nativeGoalError)(error));
|
|
1390
|
+
}
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
791
1393
|
if (method === 'serverRequest/resolved') {
|
|
792
1394
|
this.pendingUserInputs = this.pendingUserInputs.filter(pending => {
|
|
793
1395
|
if (pending.id !== params.requestId)
|
|
@@ -804,18 +1406,46 @@ class CodexAppServerExecutor {
|
|
|
804
1406
|
return;
|
|
805
1407
|
// Late background output must not become part of a subsequent turn.
|
|
806
1408
|
if (eventTurnId && !matchesActiveTurn && method !== 'turn/started'
|
|
1409
|
+
&& !(method === 'turn/completed' && goalRun?.inFlight.has(eventTurnId))
|
|
807
1410
|
&& params.item?.type !== 'contextCompaction')
|
|
808
1411
|
return;
|
|
809
1412
|
if (this.activeTurn)
|
|
810
1413
|
this.armInactivityTimer();
|
|
811
1414
|
switch (method) {
|
|
812
1415
|
case 'turn/started':
|
|
1416
|
+
if (goalRun) {
|
|
1417
|
+
const id = params.turn?.id;
|
|
1418
|
+
if (params.threadId !== this.codexThreadId || typeof id !== 'string' || !id || goalRun.retired.has(id))
|
|
1419
|
+
break;
|
|
1420
|
+
goalRun.inFlight.add(id);
|
|
1421
|
+
if (this.activeTurn?.turnId !== id) {
|
|
1422
|
+
if (goalRun.retired.size && goalRun.active.output.length) {
|
|
1423
|
+
goalRun.active.output.push('\n\n');
|
|
1424
|
+
goalRun.active.options.onStream?.('\n\n');
|
|
1425
|
+
goalRun.active.options.onDisplayText?.('\n\n');
|
|
1426
|
+
}
|
|
1427
|
+
goalRun.active.emittedTools.clear();
|
|
1428
|
+
goalRun.active.itemPhases.clear();
|
|
1429
|
+
goalRun.active.error = undefined;
|
|
1430
|
+
goalRun.active.errorCode = undefined;
|
|
1431
|
+
}
|
|
1432
|
+
this.goalNotice(goalRun, 'Native goal running');
|
|
1433
|
+
}
|
|
813
1434
|
if (this.activeTurn && typeof params.turn?.id === 'string')
|
|
814
1435
|
this.activeTurn.turnId = params.turn.id;
|
|
815
1436
|
break;
|
|
1437
|
+
case 'thread/status/changed':
|
|
1438
|
+
if (goalRun && params.status?.type === 'idle' && goalRun.goal?.status === 'active') {
|
|
1439
|
+
this.goalNotice(goalRun, 'Native goal active; waiting for native continuation or user control');
|
|
1440
|
+
}
|
|
1441
|
+
break;
|
|
816
1442
|
case 'item/agentMessage/delta':
|
|
817
1443
|
if (this.activeTurn && typeof params.delta === 'string') {
|
|
818
1444
|
this.activeTurn.output.push(params.delta);
|
|
1445
|
+
if (goalRun && this.activeTurn.output.reduce((size, part) => size + part.length, 0) > 65_536) {
|
|
1446
|
+
this.activeTurn.output = ['[Earlier native goal output omitted from this diagnostic buffer]\n',
|
|
1447
|
+
this.activeTurn.output.join('').slice(-65_536)];
|
|
1448
|
+
}
|
|
819
1449
|
this.activeTurn.options.onStream?.(params.delta);
|
|
820
1450
|
this.activeTurn.options.onDisplayText?.(params.delta);
|
|
821
1451
|
const phase = params.itemId ? this.activeTurn.itemPhases.get(params.itemId) : undefined;
|
|
@@ -933,6 +1563,11 @@ class CodexAppServerExecutor {
|
|
|
933
1563
|
catch { /* Rendering failures must not make an accepted request executable again. */ }
|
|
934
1564
|
}
|
|
935
1565
|
handleServerRequest(id, method, params) {
|
|
1566
|
+
if (this.goalRun && (this.goalRun.stopping || this.goalRun.controlling || !this.goalRun.inFlight.size
|
|
1567
|
+
|| params.turnId && !this.goalRun.inFlight.has(params.turnId))) {
|
|
1568
|
+
this.client.respondError(id, 'Request does not belong to a running native goal turn');
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
936
1571
|
if (params.threadId && params.threadId !== this.codexThreadId) {
|
|
937
1572
|
this.client.respondError(id, 'Request does not belong to this remote-cli thread');
|
|
938
1573
|
return;
|
|
@@ -1055,9 +1690,10 @@ class CodexAppServerExecutor {
|
|
|
1055
1690
|
case 'mcpToolCall':
|
|
1056
1691
|
case 'dynamicToolCall':
|
|
1057
1692
|
this.emitToolUse(active, id, item.tool ?? 'MCP', item.arguments ?? {});
|
|
1693
|
+
this.handleGeneratedImage(active, item);
|
|
1058
1694
|
active.options.onToolResult?.({
|
|
1059
1695
|
tool_use_id: id,
|
|
1060
|
-
content:
|
|
1696
|
+
content: (0, CodexImages_1.codexToolResultText)(item.result ?? item.contentItems ?? item.error ?? ''),
|
|
1061
1697
|
is_error: item.status !== 'completed' || item.success === false || Boolean(item.error),
|
|
1062
1698
|
});
|
|
1063
1699
|
break;
|
|
@@ -1081,6 +1717,35 @@ class CodexAppServerExecutor {
|
|
|
1081
1717
|
const active = this.activeTurn;
|
|
1082
1718
|
if (!active || !turn)
|
|
1083
1719
|
return;
|
|
1720
|
+
const run = this.goalRun;
|
|
1721
|
+
if (run) {
|
|
1722
|
+
if (!run.inFlight.has(turn.id) || run.retired.has(turn.id))
|
|
1723
|
+
return;
|
|
1724
|
+
run.inFlight.delete(turn.id);
|
|
1725
|
+
if (!run.inFlight.size)
|
|
1726
|
+
run.onIdle?.();
|
|
1727
|
+
run.retired.add(turn.id);
|
|
1728
|
+
if (run.retired.size > 256)
|
|
1729
|
+
run.retired.delete(run.retired.values().next().value);
|
|
1730
|
+
if (active.turnId === turn.id)
|
|
1731
|
+
active.turnId = null;
|
|
1732
|
+
this.pendingUserInputs = this.pendingUserInputs.filter(pending => {
|
|
1733
|
+
if (pending.params.turnId && pending.params.turnId !== turn.id)
|
|
1734
|
+
return true;
|
|
1735
|
+
this.finishApproval(pending, 'expired');
|
|
1736
|
+
return false;
|
|
1737
|
+
});
|
|
1738
|
+
if (turn.status !== 'completed' && !run.controlling) {
|
|
1739
|
+
void this.failGoal(run, turn.status === 'interrupted' ? 'Native goal interrupted. Resume explicitly to continue.'
|
|
1740
|
+
: turn.error?.message ?? active.error ?? 'Native goal turn failed.');
|
|
1741
|
+
}
|
|
1742
|
+
else {
|
|
1743
|
+
this.maybeCompleteGoal(run);
|
|
1744
|
+
if (run.ready && this.goalRun === run)
|
|
1745
|
+
void this.checkGoalAfterTurn(run);
|
|
1746
|
+
}
|
|
1747
|
+
return;
|
|
1748
|
+
}
|
|
1084
1749
|
if (active.turnId && turn.id && active.turnId !== turn.id)
|
|
1085
1750
|
return;
|
|
1086
1751
|
if (turn.status === 'completed') {
|
|
@@ -1126,27 +1791,27 @@ class CodexAppServerExecutor {
|
|
|
1126
1791
|
this.activityTracker.emitTool(name, input, description ?? title);
|
|
1127
1792
|
}
|
|
1128
1793
|
handleGeneratedImage(active, item) {
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
console.warn('[CodexAppServer] Ignoring imageGeneration result with unsupported format');
|
|
1140
|
-
return;
|
|
1794
|
+
const parsed = active.images.extract(item, active.turnId);
|
|
1795
|
+
for (const artifact of parsed.artifacts) {
|
|
1796
|
+
if ('image' in artifact)
|
|
1797
|
+
active.options.onImage?.(artifact.image);
|
|
1798
|
+
else
|
|
1799
|
+
active.options.onImageFile?.(artifact.path);
|
|
1800
|
+
}
|
|
1801
|
+
if (parsed.notice) {
|
|
1802
|
+
active.output.push(parsed.notice);
|
|
1803
|
+
active.options.onStream?.(parsed.notice);
|
|
1141
1804
|
}
|
|
1142
|
-
const image = { type: 'image', data, mimeType };
|
|
1143
|
-
active.options.onImage?.(image);
|
|
1144
1805
|
}
|
|
1145
1806
|
completeActive(result) {
|
|
1146
1807
|
const active = this.activeTurn;
|
|
1147
1808
|
if (!active)
|
|
1148
1809
|
return;
|
|
1149
1810
|
this.activeTurn = null;
|
|
1811
|
+
if (this.goalRun?.active === active) {
|
|
1812
|
+
this.goalRun.onIdle?.();
|
|
1813
|
+
this.goalRun = null;
|
|
1814
|
+
}
|
|
1150
1815
|
this.activityTracker.reset();
|
|
1151
1816
|
this.activityTracker.setCallback(undefined);
|
|
1152
1817
|
for (const pending of this.pendingUserInputs.splice(0))
|
|
@@ -1166,6 +1831,8 @@ class CodexAppServerExecutor {
|
|
|
1166
1831
|
waiter.resolve(result);
|
|
1167
1832
|
}
|
|
1168
1833
|
async failAndRestart(error) {
|
|
1834
|
+
if (this.goalRun)
|
|
1835
|
+
return this.failGoal(this.goalRun, error);
|
|
1169
1836
|
this.taskNotifications.clear();
|
|
1170
1837
|
this.threadReady = false;
|
|
1171
1838
|
this.completeActive({ success: false, error });
|
|
@@ -1222,15 +1889,19 @@ class CodexAppServerExecutor {
|
|
|
1222
1889
|
const session = JSON.parse(fs.readFileSync(this.sessionFilePath, 'utf8'));
|
|
1223
1890
|
if (typeof session.id === 'string' && session.id)
|
|
1224
1891
|
this.codexThreadId = session.id;
|
|
1892
|
+
this.goalManaged = !!this.codexThreadId && session.goalManaged === true;
|
|
1893
|
+
this.goalNeedsResume = this.goalManaged;
|
|
1225
1894
|
}
|
|
1226
1895
|
catch {
|
|
1227
1896
|
this.codexThreadId = null;
|
|
1897
|
+
this.goalManaged = false;
|
|
1898
|
+
this.goalNeedsResume = false;
|
|
1228
1899
|
}
|
|
1229
1900
|
}
|
|
1230
1901
|
setThreadId(id) {
|
|
1231
1902
|
this.codexThreadId = id;
|
|
1232
1903
|
const temporaryPath = `${this.sessionFilePath}.${process.pid}.tmp`;
|
|
1233
|
-
const data = JSON.stringify({ id, savedAt: new Date().toISOString() });
|
|
1904
|
+
const data = JSON.stringify({ id, savedAt: new Date().toISOString(), ...(this.goalManaged ? { goalManaged: true } : {}) });
|
|
1234
1905
|
try {
|
|
1235
1906
|
fs.writeFileSync(temporaryPath, data, { encoding: 'utf8', mode: 0o600 });
|
|
1236
1907
|
fs.renameSync(temporaryPath, this.sessionFilePath);
|
|
@@ -1252,6 +1923,25 @@ class CodexAppServerExecutor {
|
|
|
1252
1923
|
console.error('[CodexAppServerExecutor] Failed to delete session file:', error);
|
|
1253
1924
|
}
|
|
1254
1925
|
}
|
|
1926
|
+
/** Persist opt-in before a native mutation; a failed write must prevent activation. */
|
|
1927
|
+
rememberGoalManagement() {
|
|
1928
|
+
this.goalManaged = true;
|
|
1929
|
+
if (!this.codexThreadId)
|
|
1930
|
+
return;
|
|
1931
|
+
const temporaryPath = `${this.sessionFilePath}.${process.pid}.goal.tmp`;
|
|
1932
|
+
try {
|
|
1933
|
+
fs.writeFileSync(temporaryPath, JSON.stringify({ id: this.codexThreadId, goalManaged: true,
|
|
1934
|
+
savedAt: new Date().toISOString() }), { encoding: 'utf8', mode: 0o600 });
|
|
1935
|
+
fs.renameSync(temporaryPath, this.sessionFilePath);
|
|
1936
|
+
}
|
|
1937
|
+
catch {
|
|
1938
|
+
try {
|
|
1939
|
+
fs.unlinkSync(temporaryPath);
|
|
1940
|
+
}
|
|
1941
|
+
catch { /* Keep the previous binding intact. */ }
|
|
1942
|
+
throw new Error('Could not persist native goal ownership. No goal activation was requested.');
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1255
1945
|
extractErrorCode(info) {
|
|
1256
1946
|
if (typeof info === 'string')
|
|
1257
1947
|
return info;
|
|
@@ -1261,16 +1951,6 @@ class CodexAppServerExecutor {
|
|
|
1261
1951
|
return info.type;
|
|
1262
1952
|
return Object.keys(info)[0];
|
|
1263
1953
|
}
|
|
1264
|
-
stringifyResult(value) {
|
|
1265
|
-
if (typeof value === 'string')
|
|
1266
|
-
return value;
|
|
1267
|
-
try {
|
|
1268
|
-
return JSON.stringify(value).slice(0, 2_000);
|
|
1269
|
-
}
|
|
1270
|
-
catch {
|
|
1271
|
-
return String(value);
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
1954
|
errorMessage(error) {
|
|
1275
1955
|
return error instanceof Error ? error.message : String(error);
|
|
1276
1956
|
}
|