@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.
Files changed (94) hide show
  1. package/README.md +25 -0
  2. package/dist/client/GoalCommand.d.ts +15 -0
  3. package/dist/client/GoalCommand.d.ts.map +1 -0
  4. package/dist/client/GoalCommand.js +59 -0
  5. package/dist/client/GoalCommand.js.map +1 -0
  6. package/dist/client/MessageHandler.d.ts +4 -0
  7. package/dist/client/MessageHandler.d.ts.map +1 -1
  8. package/dist/client/MessageHandler.js +186 -46
  9. package/dist/client/MessageHandler.js.map +1 -1
  10. package/dist/client/TaskRecovery.d.ts +3 -1
  11. package/dist/client/TaskRecovery.d.ts.map +1 -1
  12. package/dist/client/TaskRecovery.js +9 -4
  13. package/dist/client/TaskRecovery.js.map +1 -1
  14. package/dist/client/WebSocketClient.d.ts +2 -1
  15. package/dist/client/WebSocketClient.d.ts.map +1 -1
  16. package/dist/client/WebSocketClient.js +9 -1
  17. package/dist/client/WebSocketClient.js.map +1 -1
  18. package/dist/delegation/DelegationManager.js +1 -1
  19. package/dist/delegation/DelegationManager.js.map +1 -1
  20. package/dist/executor/AcpExecutor.d.ts +6 -0
  21. package/dist/executor/AcpExecutor.d.ts.map +1 -1
  22. package/dist/executor/AcpExecutor.js +81 -10
  23. package/dist/executor/AcpExecutor.js.map +1 -1
  24. package/dist/executor/CodexAppServerClient.d.ts +1 -0
  25. package/dist/executor/CodexAppServerClient.d.ts.map +1 -1
  26. package/dist/executor/CodexAppServerClient.js +12 -4
  27. package/dist/executor/CodexAppServerClient.js.map +1 -1
  28. package/dist/executor/CodexAppServerExecutor.d.ts +39 -2
  29. package/dist/executor/CodexAppServerExecutor.d.ts.map +1 -1
  30. package/dist/executor/CodexAppServerExecutor.js +712 -32
  31. package/dist/executor/CodexAppServerExecutor.js.map +1 -1
  32. package/dist/executor/IExecutor.d.ts +43 -0
  33. package/dist/executor/IExecutor.d.ts.map +1 -1
  34. package/dist/executor/KimiExecutor.d.ts +9 -0
  35. package/dist/executor/KimiExecutor.d.ts.map +1 -1
  36. package/dist/executor/KimiExecutor.js +45 -0
  37. package/dist/executor/KimiExecutor.js.map +1 -1
  38. package/dist/executor/codex/CodexGoals.d.ts +6 -0
  39. package/dist/executor/codex/CodexGoals.d.ts.map +1 -0
  40. package/dist/executor/codex/CodexGoals.js +49 -0
  41. package/dist/executor/codex/CodexGoals.js.map +1 -0
  42. package/dist/executor/codex/CodexImages.d.ts +22 -0
  43. package/dist/executor/codex/CodexImages.d.ts.map +1 -0
  44. package/dist/executor/codex/CodexImages.js +201 -0
  45. package/dist/executor/codex/CodexImages.js.map +1 -0
  46. package/dist/executor/index.d.ts +2 -1
  47. package/dist/executor/index.d.ts.map +1 -1
  48. package/dist/executor/index.js +1 -0
  49. package/dist/executor/index.js.map +1 -1
  50. package/dist/executor/kimi/KimiAccountUsage.d.ts +2 -6
  51. package/dist/executor/kimi/KimiAccountUsage.d.ts.map +1 -1
  52. package/dist/executor/kimi/KimiAccountUsage.js +11 -71
  53. package/dist/executor/kimi/KimiAccountUsage.js.map +1 -1
  54. package/dist/executor/kimi/KimiLocalServer.d.ts +28 -0
  55. package/dist/executor/kimi/KimiLocalServer.d.ts.map +1 -0
  56. package/dist/executor/kimi/KimiLocalServer.js +192 -0
  57. package/dist/executor/kimi/KimiLocalServer.js.map +1 -0
  58. package/dist/executor/kimi/KimiModelCatalog.d.ts +6 -0
  59. package/dist/executor/kimi/KimiModelCatalog.d.ts.map +1 -0
  60. package/dist/executor/kimi/KimiModelCatalog.js +86 -0
  61. package/dist/executor/kimi/KimiModelCatalog.js.map +1 -0
  62. package/dist/maintenance/release-notes.json +1 -1
  63. package/dist/settings/SettingsCatalog.d.ts +1 -1
  64. package/dist/settings/SettingsCatalog.d.ts.map +1 -1
  65. package/dist/settings/SettingsCatalog.js +6 -2
  66. package/dist/settings/SettingsCatalog.js.map +1 -1
  67. package/dist/settings/SettingsService.d.ts +3 -1
  68. package/dist/settings/SettingsService.d.ts.map +1 -1
  69. package/dist/settings/SettingsService.js +17 -5
  70. package/dist/settings/SettingsService.js.map +1 -1
  71. package/dist/thread/ThreadExecutorPool.d.ts +3 -1
  72. package/dist/thread/ThreadExecutorPool.d.ts.map +1 -1
  73. package/dist/thread/ThreadExecutorPool.js +13 -1
  74. package/dist/thread/ThreadExecutorPool.js.map +1 -1
  75. package/dist/thread/ThreadManager.d.ts +1 -1
  76. package/dist/thread/ThreadManager.d.ts.map +1 -1
  77. package/dist/thread/ThreadManager.js.map +1 -1
  78. package/dist/thread/types.d.ts +2 -0
  79. package/dist/thread/types.d.ts.map +1 -1
  80. package/dist/types/Settings.d.ts +8 -0
  81. package/dist/types/Settings.d.ts.map +1 -1
  82. package/dist/utils/ImageDelivery.d.ts +67 -0
  83. package/dist/utils/ImageDelivery.d.ts.map +1 -0
  84. package/dist/utils/ImageDelivery.js +270 -0
  85. package/dist/utils/ImageDelivery.js.map +1 -0
  86. package/dist/utils/ImageValidation.d.ts +11 -0
  87. package/dist/utils/ImageValidation.d.ts.map +1 -0
  88. package/dist/utils/ImageValidation.js +62 -0
  89. package/dist/utils/ImageValidation.js.map +1 -0
  90. package/dist/utils/LocalImageDetector.d.ts +12 -3
  91. package/dist/utils/LocalImageDetector.d.ts.map +1 -1
  92. package/dist/utils/LocalImageDetector.js +153 -55
  93. package/dist/utils/LocalImageDetector.js.map +1 -1
  94. 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.activeTurn || this.compactWaiter) {
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 response = await this.client.request('model/list', {
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.activeTurn) {
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: this.stringifyResult(item.result ?? item.contentItems ?? item.error ?? ''),
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
- if (item.status !== 'completed' || typeof item.result !== 'string' || !item.result)
1130
- return;
1131
- let data = item.result;
1132
- let mimeType = typeof item.mimeType === 'string' ? item.mimeType : 'image/png';
1133
- const dataUrlMatch = data.match(/^data:(image\/[a-z0-9.+-]+);base64,(.+)$/i);
1134
- if (dataUrlMatch) {
1135
- mimeType = dataUrlMatch[1];
1136
- data = dataUrlMatch[2];
1137
- }
1138
- if (!/^[a-z0-9+/]+={0,2}$/i.test(data) || data.length % 4 !== 0) {
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
  }