@yu_robotics/remote-cli 1.6.170 → 1.6.173

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 (49) hide show
  1. package/README.md +21 -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 +3 -0
  7. package/dist/client/MessageHandler.d.ts.map +1 -1
  8. package/dist/client/MessageHandler.js +167 -43
  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 +8 -1
  17. package/dist/client/WebSocketClient.js.map +1 -1
  18. package/dist/executor/CodexAppServerClient.d.ts +1 -0
  19. package/dist/executor/CodexAppServerClient.d.ts.map +1 -1
  20. package/dist/executor/CodexAppServerClient.js +12 -4
  21. package/dist/executor/CodexAppServerClient.js.map +1 -1
  22. package/dist/executor/CodexAppServerExecutor.d.ts +28 -2
  23. package/dist/executor/CodexAppServerExecutor.d.ts.map +1 -1
  24. package/dist/executor/CodexAppServerExecutor.js +517 -30
  25. package/dist/executor/CodexAppServerExecutor.js.map +1 -1
  26. package/dist/executor/IExecutor.d.ts +24 -0
  27. package/dist/executor/IExecutor.d.ts.map +1 -1
  28. package/dist/executor/codex/CodexGoals.d.ts +6 -0
  29. package/dist/executor/codex/CodexGoals.d.ts.map +1 -0
  30. package/dist/executor/codex/CodexGoals.js +49 -0
  31. package/dist/executor/codex/CodexGoals.js.map +1 -0
  32. package/dist/executor/codex/CodexImages.d.ts +22 -0
  33. package/dist/executor/codex/CodexImages.d.ts.map +1 -0
  34. package/dist/executor/codex/CodexImages.js +201 -0
  35. package/dist/executor/codex/CodexImages.js.map +1 -0
  36. package/dist/maintenance/release-notes.json +1 -1
  37. package/dist/utils/ImageDelivery.d.ts +67 -0
  38. package/dist/utils/ImageDelivery.d.ts.map +1 -0
  39. package/dist/utils/ImageDelivery.js +270 -0
  40. package/dist/utils/ImageDelivery.js.map +1 -0
  41. package/dist/utils/ImageValidation.d.ts +11 -0
  42. package/dist/utils/ImageValidation.d.ts.map +1 -0
  43. package/dist/utils/ImageValidation.js +62 -0
  44. package/dist/utils/ImageValidation.js.map +1 -0
  45. package/dist/utils/LocalImageDetector.d.ts +12 -3
  46. package/dist/utils/LocalImageDetector.d.ts.map +1 -1
  47. package/dist/utils/LocalImageDetector.js +153 -55
  48. package/dist/utils/LocalImageDetector.js.map +1 -1
  49. 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';
@@ -142,6 +146,13 @@ class CodexAppServerExecutor {
142
146
  delegationConfigured = false;
143
147
  delegationWorker;
144
148
  sessionResumeFailure = false;
149
+ goalManaged = false;
150
+ goalNeedsResume = false;
151
+ goalRun = null;
152
+ goalControl = null;
153
+ goalControlClear = false;
154
+ goalTransportStops = 0;
155
+ threadLoads = 0;
145
156
  async configureDelegation(connection) {
146
157
  if (this.delegationConfigured && (0, contract_1.sameConnection)(this.delegation, connection))
147
158
  return;
@@ -196,11 +207,13 @@ class CodexAppServerExecutor {
196
207
  this.sessionResumeFailure = false;
197
208
  if (this.destroyed)
198
209
  throw new Error('Executor has been destroyed');
199
- if (this.activeTurn || this.compactWaiter) {
210
+ if (this.isBusy()) {
200
211
  return { success: false, error: 'Codex thread is already running a command' };
201
212
  }
202
213
  try {
203
214
  await this.ensureThread();
215
+ if (this.isBusy())
216
+ return { success: false, error: 'Codex thread is already running a command' };
204
217
  }
205
218
  catch (error) {
206
219
  return { success: false, error: this.errorMessage(error) };
@@ -211,6 +224,10 @@ class CodexAppServerExecutor {
211
224
  // leave the executor busy without an actual app-server turn.
212
225
  const sandboxOptions = this.sandbox.turnOptions(this.currentWorkingDirectory);
213
226
  const input = await this.buildInput(prompt, options.attachments, temporaryFiles);
227
+ if (this.isBusy()) {
228
+ await this.cleanupTemporaryFiles(temporaryFiles);
229
+ return { success: false, error: 'Codex thread is already running a command' };
230
+ }
214
231
  return await new Promise((resolve) => {
215
232
  const active = {
216
233
  options,
@@ -221,6 +238,7 @@ class CodexAppServerExecutor {
221
238
  emittedTools: new Map(),
222
239
  itemPhases: new Map(),
223
240
  temporaryFiles,
241
+ images: new CodexImages_1.CodexImages(),
224
242
  inactivityTimeoutMs: options.inactivityTimeout,
225
243
  };
226
244
  this.activityTracker.setCallback(options.onActivity);
@@ -269,6 +287,332 @@ class CodexAppServerExecutor {
269
287
  getCurrentWorkingDirectory() {
270
288
  return this.currentWorkingDirectory;
271
289
  }
290
+ isGoalRunning() {
291
+ return this.goalRun !== null;
292
+ }
293
+ /** Reading a saved goal must never materialize or resume its native thread. */
294
+ async getGoal() {
295
+ if (this.destroyed)
296
+ throw new Error('Executor has been destroyed');
297
+ if (!this.codexThreadId)
298
+ return null;
299
+ const threadId = this.codexThreadId;
300
+ try {
301
+ await this.goalDeadline(this.client.start());
302
+ (0, CodexGoals_1.assertNativeGoalsVersion)(this.client.getServerVersion?.());
303
+ const response = await this.goalRequest('thread/goal/get', { threadId });
304
+ if (this.codexThreadId !== threadId || this.destroyed)
305
+ throw new Error('The native goal session changed while reading it.');
306
+ const goal = (0, CodexGoals_1.parseNativeGoal)(response?.goal, threadId);
307
+ if (goal && !this.goalManaged)
308
+ this.rememberGoalManagement();
309
+ return goal;
310
+ }
311
+ catch (error) {
312
+ throw new Error((0, CodexGoals_1.nativeGoalError)(error));
313
+ }
314
+ }
315
+ async executeGoal(request, options) {
316
+ try {
317
+ (0, CodexGoals_1.validateGoalRequest)(request);
318
+ if (this.destroyed)
319
+ throw new Error('Executor has been destroyed');
320
+ if (this.isBusy())
321
+ throw new Error('Codex thread is already running a command');
322
+ if (this.delegationWorker || this.delegation)
323
+ throw new Error('Native goals require managed delegation to be off and cannot run in a delegated worker.');
324
+ if (request.objective === undefined && !this.codexThreadId)
325
+ throw new Error('There is no saved native goal to resume.');
326
+ }
327
+ catch (error) {
328
+ return { success: false, error: (0, CodexGoals_1.nativeGoalError)(error) };
329
+ }
330
+ let resolve;
331
+ const done = new Promise(settle => { resolve = settle; });
332
+ const active = { options, resolve, turnId: null, output: [], sideEffectsStarted: false,
333
+ emittedTools: new Map(), itemPhases: new Map(), temporaryFiles: [], images: new CodexImages_1.CodexImages(), inactivityTimeoutMs: options.inactivityTimeout };
334
+ const run = { active, done, objective: request.objective, ready: false, inFlight: new Set(), retired: new Set(),
335
+ revision: 0, checking: false, controlling: false, stopping: false };
336
+ // Install ownership before any native loading or mutation can start a turn.
337
+ this.goalRun = run;
338
+ this.activeTurn = active;
339
+ this.activityTracker.setCallback(options.onActivity);
340
+ this.activityTracker.reset();
341
+ this.armInactivityTimer();
342
+ if (options.timeout && options.timeout > 0)
343
+ active.timeoutTimer = setTimeout(() => {
344
+ void this.failGoal(run, `Native goal timed out after ${options.timeout}ms. ${GOAL_RECOVERY_GUIDANCE}`);
345
+ }, options.timeout);
346
+ run.startup = this.startGoal(run, request).catch(error => this.failGoal(run, (0, CodexGoals_1.nativeGoalError)(error)));
347
+ return done;
348
+ }
349
+ pauseGoal() {
350
+ return this.controlGoal(false);
351
+ }
352
+ clearGoal() {
353
+ return this.controlGoal(true);
354
+ }
355
+ async startGoal(run, request) {
356
+ await this.goalDeadline(this.client.start());
357
+ this.assertGoalOwner(run);
358
+ (0, CodexGoals_1.assertNativeGoalsVersion)(this.client.getServerVersion?.());
359
+ let previous = null;
360
+ if (this.codexThreadId) {
361
+ previous = await this.getGoal();
362
+ this.assertGoalOwner(run);
363
+ }
364
+ if (request.objective === undefined && (!previous || previous.status === 'complete')) {
365
+ throw new Error('There is no unfinished native goal to resume.');
366
+ }
367
+ // Replacing an active saved goal must not briefly execute its old objective on load.
368
+ if (request.objective !== undefined && previous?.status === 'active' && !this.threadReady) {
369
+ throw new Error('Pause or clear the saved active goal before replacing its objective.');
370
+ }
371
+ run.goal = previous;
372
+ run.objective = request.objective ?? previous?.objective;
373
+ this.rememberGoalManagement();
374
+ if (previous?.status === 'active' && !this.threadReady) {
375
+ // Loading an active goal can dispatch before settings/update. Suspend it
376
+ // without loading, then restore activation only after native policy sync.
377
+ const response = await this.goalRequest('thread/goal/set', {
378
+ threadId: this.codexThreadId, status: 'paused', origin: 'user',
379
+ });
380
+ this.assertGoalOwner(run);
381
+ const paused = (0, CodexGoals_1.parseNativeGoal)(response?.goal, this.codexThreadId);
382
+ if (!paused || paused.status !== 'paused')
383
+ throw new Error('Codex did not pause the saved goal before loading its policies.');
384
+ run.goal = paused;
385
+ if (run.controlling)
386
+ return;
387
+ }
388
+ await this.ensureThread();
389
+ this.assertGoalOwner(run);
390
+ this.rememberGoalManagement();
391
+ if (run.controlling)
392
+ return;
393
+ // Native continuations bypass turn/start, so their settings must be sticky.
394
+ const goalPolicy = this.sandbox.turnOptions(this.currentWorkingDirectory);
395
+ if (this.model || this.effort || Object.keys(goalPolicy).length) {
396
+ await this.goalRequest('thread/settings/update', { threadId: this.codexThreadId,
397
+ ...(this.model ? { model: this.model } : {}), ...(this.effort ? { effort: this.effort } : {}), ...goalPolicy });
398
+ this.assertGoalOwner(run);
399
+ if (run.controlling)
400
+ return;
401
+ }
402
+ // A resumed active goal may already have completed while the load request ran.
403
+ if (request.objective !== undefined || run.goal?.status !== 'complete') {
404
+ const revision = run.revision;
405
+ const response = await this.goalRequest('thread/goal/set', {
406
+ threadId: this.codexThreadId, origin: 'user',
407
+ ...(request.objective !== undefined ? { objective: request.objective } : {}),
408
+ status: 'active', ...(request.tokenBudget !== undefined ? { tokenBudget: request.tokenBudget } : {}),
409
+ });
410
+ this.assertGoalOwner(run);
411
+ const goal = (0, CodexGoals_1.parseNativeGoal)(response?.goal, this.codexThreadId);
412
+ if (!goal)
413
+ throw new Error('Codex did not return the activated native goal.');
414
+ if (run.revision === revision)
415
+ run.goal = goal;
416
+ }
417
+ run.ready = true;
418
+ this.goalNeedsResume = false;
419
+ if (run.goal?.status === 'active') {
420
+ this.goalNotice(run, run.inFlight.size ? 'Native goal running' : 'Native goal active; waiting for native continuation');
421
+ }
422
+ this.maybeCompleteGoal(run);
423
+ if (this.goalRun === run && !run.inFlight.size)
424
+ void this.checkGoalAfterTurn(run);
425
+ }
426
+ assertGoalOwner(run) {
427
+ if (this.goalRun !== run || this.destroyed || run.stopping)
428
+ throw new Error('Native goal operation was cancelled.');
429
+ }
430
+ goalNotice(run, text) {
431
+ const bounded = Array.from(text).slice(0, 400).join('');
432
+ if (this.goalRun !== run || run.lastNotice === bounded)
433
+ return;
434
+ run.lastNotice = bounded;
435
+ try {
436
+ if (run.active.options.onActivity)
437
+ run.active.options.onActivity({ source: 'state', text: bounded });
438
+ else
439
+ run.active.options.onStream?.(`\n[${bounded}]\n`);
440
+ }
441
+ catch { /* A display failure cannot change native goal ownership. */ }
442
+ }
443
+ maybeCompleteGoal(run) {
444
+ if (this.goalRun !== run || !run.ready || run.controlling || run.stopping || run.inFlight.size
445
+ || run.goal === undefined || run.goal?.status === 'active')
446
+ return;
447
+ const status = run.goal?.status ?? 'cleared';
448
+ this.goalNotice(run, `Native goal ${status}`);
449
+ const summary = `\n\nNative goal ${status}.\n`;
450
+ run.active.output.push(summary);
451
+ try {
452
+ run.active.options.onStream?.(summary);
453
+ }
454
+ catch { /* Native completion still releases ownership. */ }
455
+ try {
456
+ run.active.options.onDisplayText?.(summary);
457
+ }
458
+ catch { /* Native completion still releases ownership. */ }
459
+ this.completeActive({ success: status === 'complete' || status === 'paused' || status === 'cleared',
460
+ output: run.active.output.join(''), sessionAbbr: this.codexThreadId?.slice(0, 8),
461
+ ...(['blocked', 'usageLimited', 'budgetLimited'].includes(status) ? { error: `Native goal stopped: ${status}.` } : {}) });
462
+ }
463
+ async checkGoalAfterTurn(run) {
464
+ if (this.goalRun !== run || run.checking || run.controlling || run.stopping)
465
+ return;
466
+ run.checking = true;
467
+ const revision = run.revision;
468
+ try {
469
+ const goal = await this.getGoal();
470
+ if (this.goalRun !== run || run.controlling || run.stopping)
471
+ return;
472
+ if (revision === run.revision)
473
+ run.goal = goal;
474
+ this.maybeCompleteGoal(run);
475
+ }
476
+ catch (error) {
477
+ if (this.goalRun === run)
478
+ await this.failGoal(run, `Could not reconcile native goal: ${(0, CodexGoals_1.nativeGoalError)(error)}`);
479
+ }
480
+ finally {
481
+ run.checking = false;
482
+ }
483
+ }
484
+ goalDeadline(promise) {
485
+ return new Promise((resolve, reject) => {
486
+ const timer = setTimeout(() => reject(new Error('Native goal control timed out.')), GOAL_CONTROL_TIMEOUT_MS);
487
+ promise.then(value => { clearTimeout(timer); resolve(value); }, error => { clearTimeout(timer); reject(error); });
488
+ });
489
+ }
490
+ goalRequest(method, params) {
491
+ if (this.destroyed)
492
+ return Promise.reject(new Error('Executor has been destroyed'));
493
+ return this.goalDeadline(this.client.request(method, params, GOAL_CONTROL_TIMEOUT_MS));
494
+ }
495
+ async stopGoalTransport() {
496
+ this.threadReady = false;
497
+ this.goalNeedsResume = this.goalManaged;
498
+ this.goalTransportStops++;
499
+ try {
500
+ await this.goalDeadline(this.client.stop());
501
+ if (this.client.waitForExit)
502
+ await this.goalDeadline(this.client.waitForExit());
503
+ else if (this.client.isRunning())
504
+ throw new Error('Codex goal process exit could not be confirmed.');
505
+ }
506
+ finally {
507
+ this.goalTransportStops--;
508
+ }
509
+ }
510
+ async failGoal(run, error) {
511
+ if (this.goalRun !== run || run.stopping)
512
+ return;
513
+ run.stopping = true;
514
+ this.clearInactivityTimer();
515
+ if (run.active.timeoutTimer)
516
+ clearTimeout(run.active.timeoutTimer);
517
+ try {
518
+ await this.stopGoalTransport();
519
+ if (this.goalRun === run)
520
+ this.completeActive({ success: false, error });
521
+ }
522
+ catch (stopError) {
523
+ run.stopping = false;
524
+ this.goalNotice(run, `Native goal stop unconfirmed: ${(0, CodexGoals_1.nativeGoalError)(stopError)}. Retry /abort.`);
525
+ }
526
+ }
527
+ controlGoal(clear) {
528
+ if (this.goalControl) {
529
+ if (this.goalControlClear === clear)
530
+ return this.goalControl;
531
+ return this.goalControl.then(result => result.success ? this.controlGoal(clear) : result);
532
+ }
533
+ const control = this.stopGoalControl(this.goalRun, clear);
534
+ this.goalControl = control;
535
+ this.goalControlClear = clear;
536
+ void control.then(() => { if (this.goalControl === control)
537
+ this.goalControl = null; });
538
+ return control;
539
+ }
540
+ async stopGoalControl(run, clear) {
541
+ if (this.destroyed)
542
+ return { success: false, error: 'Executor has been destroyed' };
543
+ if (!run && (this.activeTurn || this.compactWaiter || this.threadLoads)) {
544
+ return { success: false, error: 'Wait for the current ordinary task before changing its goal.' };
545
+ }
546
+ if (!this.codexThreadId && !run)
547
+ return { success: true, output: 'No native goal is set.' };
548
+ if (run)
549
+ run.controlling = true;
550
+ try {
551
+ if (run?.startup)
552
+ await this.goalDeadline(run.startup);
553
+ if (run && this.goalRun !== run)
554
+ return { success: false, error: 'The native goal run already stopped.' };
555
+ await this.goalDeadline(this.client.start());
556
+ (0, CodexGoals_1.assertNativeGoalsVersion)(this.client.getServerVersion?.());
557
+ const threadId = this.codexThreadId;
558
+ if (!threadId)
559
+ throw new Error('No native goal session was created.');
560
+ this.rememberGoalManagement();
561
+ const existing = await this.getGoal();
562
+ let goal = existing;
563
+ if (existing && existing.status !== 'complete') {
564
+ const paused = await this.goalRequest('thread/goal/set', { threadId, status: 'paused', origin: 'user' });
565
+ goal = (0, CodexGoals_1.parseNativeGoal)(paused?.goal, threadId);
566
+ if (!goal || goal.status === 'active')
567
+ throw new Error('Codex did not confirm that the native goal is paused.');
568
+ }
569
+ if (clear && existing) {
570
+ const response = await this.goalRequest('thread/goal/clear', { threadId, origin: 'user' });
571
+ if (typeof response?.cleared !== 'boolean')
572
+ throw new Error('Codex did not confirm clearing the native goal.');
573
+ goal = null;
574
+ }
575
+ if (run) {
576
+ run.goal = goal;
577
+ for (const pending of this.pendingUserInputs.splice(0)) {
578
+ this.finishApproval(pending, 'expired');
579
+ this.client.respond(pending.id, pending.method === 'item/tool/requestUserInput' ? { answers: {} }
580
+ : pending.method === 'item/permissions/requestApproval' ? { permissions: {}, scope: 'turn' } : { decision: 'cancel' });
581
+ }
582
+ for (const turnId of [...run.inFlight])
583
+ await this.goalRequest('turn/interrupt', { threadId, turnId });
584
+ if (run.inFlight.size)
585
+ await this.goalDeadline(new Promise(resolve => { run.onIdle = resolve; }));
586
+ if (this.goalRun !== run)
587
+ throw new Error('The native goal stopped before its control operation finished.');
588
+ }
589
+ // A late dispatched continuation must not outlive a successful stop ACK.
590
+ // Reopening this process is safe because native state is now paused/cleared.
591
+ await this.stopGoalTransport();
592
+ this.goalNeedsResume = false;
593
+ if (run) {
594
+ if (this.goalRun !== run)
595
+ throw new Error('The native goal stopped before its control operation finished.');
596
+ run.inFlight.clear();
597
+ run.ready = true;
598
+ run.controlling = false;
599
+ this.maybeCompleteGoal(run);
600
+ const result = await this.goalDeadline(run.done);
601
+ if (!result.success)
602
+ throw new Error(result.error ?? 'Native goal did not stop successfully.');
603
+ }
604
+ return { success: true, output: clear ? 'Native goal cleared; execution stopped.'
605
+ : goal?.status === 'complete' ? 'Native goal was already complete; execution stopped.'
606
+ : goal ? 'Native goal paused; execution stopped.' : 'No native goal is set; execution stopped.' };
607
+ }
608
+ catch (error) {
609
+ if (run && this.goalRun === run) {
610
+ run.controlling = false;
611
+ await this.failGoal(run, `Native goal control failed: ${(0, CodexGoals_1.nativeGoalError)(error)}. ${GOAL_RECOVERY_GUIDANCE}`);
612
+ }
613
+ return { success: false, error: (0, CodexGoals_1.nativeGoalError)(error) };
614
+ }
615
+ }
272
616
  getExecutionMetadata() {
273
617
  const data = (0, ExecutionMetadata_1.configuredExecutionMetadata)(this.model, this.effort);
274
618
  if (this.threadReady && this.client.isRunning() && this.reportedModel) {
@@ -288,7 +632,7 @@ class CodexAppServerExecutor {
288
632
  this.reportedEffort = response.reasoningEffort;
289
633
  }
290
634
  isBusy() {
291
- return this.activeTurn !== null || this.compactWaiter !== null;
635
+ return this.activeTurn !== null || this.compactWaiter !== null || this.goalControl !== null || this.threadLoads > 0;
292
636
  }
293
637
  async setWorkingDirectory(targetPath) {
294
638
  const resolved = this.directoryGuard.resolveWorkingDirectory(targetPath, this.currentWorkingDirectory);
@@ -341,6 +685,8 @@ class CodexAppServerExecutor {
341
685
  }
342
686
  }
343
687
  resetContext() {
688
+ if (this.goalRun)
689
+ throw new Error('Stop the native goal before clearing its conversation.');
344
690
  this.taskNotifications.clear();
345
691
  const previousId = this.codexThreadId;
346
692
  if (this.activeTurn || this.compactWaiter) {
@@ -349,6 +695,8 @@ class CodexAppServerExecutor {
349
695
  void this.client.stop();
350
696
  }
351
697
  this.codexThreadId = null;
698
+ this.goalManaged = false;
699
+ this.goalNeedsResume = false;
352
700
  this.threadReady = false;
353
701
  this.pendingUserInputs = [];
354
702
  this.clearThreadId();
@@ -357,6 +705,8 @@ class CodexAppServerExecutor {
357
705
  }
358
706
  }
359
707
  async abort() {
708
+ if (this.goalRun)
709
+ return (await this.pauseGoal()).success;
360
710
  const active = this.activeTurn;
361
711
  if (!active || !this.codexThreadId)
362
712
  return false;
@@ -384,12 +734,17 @@ class CodexAppServerExecutor {
384
734
  return true;
385
735
  }
386
736
  async destroy() {
387
- if (this.destroyed)
737
+ if (this.destroyed && !this.goalRun)
388
738
  return;
389
739
  this.destroyed = true;
390
740
  this.taskNotifications.clear();
391
741
  this.unsubscribeClient?.();
392
742
  this.unsubscribeClient = null;
743
+ if (this.goalRun) {
744
+ this.goalRun.stopping = true;
745
+ this.clearInactivityTimer();
746
+ await this.stopGoalTransport();
747
+ }
393
748
  if (this.activeTurn)
394
749
  this.completeActive({ success: false, error: 'Executor destroyed' });
395
750
  this.completeCompact({ success: false, error: 'Executor destroyed' });
@@ -709,12 +1064,29 @@ class CodexAppServerExecutor {
709
1064
  this.sandbox.deleteData();
710
1065
  }
711
1066
  async ensureThread() {
1067
+ this.threadLoads++;
1068
+ try {
1069
+ await this.loadThread();
1070
+ }
1071
+ finally {
1072
+ this.threadLoads--;
1073
+ }
1074
+ }
1075
+ async loadThread() {
712
1076
  (0, WorkingDirectory_1.assertWorkingDirectoryExists)(this.currentWorkingDirectory);
1077
+ if (this.goalManaged && this.codexThreadId && !this.goalRun) {
1078
+ const goal = await this.getGoal();
1079
+ if (this.goalNeedsResume || goal?.status === 'active')
1080
+ throw new Error(GOAL_RECOVERY_GUIDANCE);
1081
+ }
713
1082
  if (this.threadReady && this.client.isRunning())
714
1083
  return;
1084
+ const goalOwner = this.goalRun;
715
1085
  this.reportedModel = undefined;
716
1086
  this.reportedEffort = undefined;
717
1087
  await this.client.start();
1088
+ if (goalOwner)
1089
+ this.assertGoalOwner(goalOwner);
718
1090
  const common = {
719
1091
  ...(this.model ? { model: this.model } : {}),
720
1092
  cwd: this.currentWorkingDirectory,
@@ -724,6 +1096,9 @@ class CodexAppServerExecutor {
724
1096
  } : {}),
725
1097
  ...this.sandbox.threadOptions(this.currentWorkingDirectory, this.delegationWorker || !!this.delegation),
726
1098
  };
1099
+ // A saved active goal can dispatch during resume, before settings/update.
1100
+ if (goalOwner && this.effort)
1101
+ common.config = { ...common.config, model_reasoning_effort: this.effort };
727
1102
  if (this.delegationConfigured) {
728
1103
  common.config = { ...common.config,
729
1104
  'mcp_servers.remote_cli_delegation': this.delegation
@@ -744,6 +1119,8 @@ class CodexAppServerExecutor {
744
1119
  if (response?.thread?.id !== this.codexThreadId) {
745
1120
  throw new Error('Codex app-server resumed an unexpected thread');
746
1121
  }
1122
+ if (goalOwner)
1123
+ this.assertGoalOwner(goalOwner);
747
1124
  this.captureReportedSettings(response);
748
1125
  }
749
1126
  catch (error) {
@@ -761,6 +1138,8 @@ class CodexAppServerExecutor {
761
1138
  serviceName: 'remote_cli',
762
1139
  });
763
1140
  const id = response?.thread?.id;
1141
+ if (goalOwner)
1142
+ this.assertGoalOwner(goalOwner);
764
1143
  if (typeof id !== 'string' || !id)
765
1144
  throw new Error('Codex app-server returned no thread ID');
766
1145
  this.captureReportedSettings(response);
@@ -769,6 +1148,8 @@ class CodexAppServerExecutor {
769
1148
  this.threadReady = true;
770
1149
  }
771
1150
  handleMessage(message) {
1151
+ if (this.destroyed)
1152
+ return;
772
1153
  const method = message.method;
773
1154
  if (!method)
774
1155
  return;
@@ -776,7 +1157,12 @@ class CodexAppServerExecutor {
776
1157
  if (method === 'client/disconnected') {
777
1158
  this.taskNotifications.clear();
778
1159
  this.threadReady = false;
779
- if (this.activeTurn) {
1160
+ if (this.goalRun) {
1161
+ if (!this.goalTransportStops) {
1162
+ void this.failGoal(this.goalRun, `${params.error ?? 'Codex app-server disconnected'}. ${GOAL_RECOVERY_GUIDANCE}`);
1163
+ }
1164
+ }
1165
+ else if (this.activeTurn) {
780
1166
  this.completeActive({ success: false, error: params.error ?? 'Codex app-server disconnected' });
781
1167
  }
782
1168
  this.completeCompact({ success: false, error: params.error ?? 'Codex app-server disconnected' });
@@ -788,6 +1174,29 @@ class CodexAppServerExecutor {
788
1174
  }
789
1175
  if (params.threadId && params.threadId !== this.codexThreadId)
790
1176
  return;
1177
+ const goalRun = this.goalRun;
1178
+ if (goalRun?.stopping)
1179
+ return;
1180
+ // Goal changes may arrive after their turn completed, or without any turn ID.
1181
+ if (goalRun && (method === 'thread/goal/updated' || method === 'thread/goal/cleared')) {
1182
+ if (params.threadId !== this.codexThreadId)
1183
+ return;
1184
+ try {
1185
+ const goal = method === 'thread/goal/cleared' ? null : (0, CodexGoals_1.parseNativeGoal)(params.goal, this.codexThreadId);
1186
+ if (method === 'thread/goal/updated' && !goal)
1187
+ throw new Error('Codex returned an empty native goal update.');
1188
+ if (goal && (goalRun.objective && goal.objective !== goalRun.objective
1189
+ || goalRun.goal && goal.updatedAt < goalRun.goal.updatedAt))
1190
+ return;
1191
+ goalRun.goal = goal;
1192
+ goalRun.revision++;
1193
+ this.maybeCompleteGoal(goalRun);
1194
+ }
1195
+ catch (error) {
1196
+ void this.failGoal(goalRun, (0, CodexGoals_1.nativeGoalError)(error));
1197
+ }
1198
+ return;
1199
+ }
791
1200
  if (method === 'serverRequest/resolved') {
792
1201
  this.pendingUserInputs = this.pendingUserInputs.filter(pending => {
793
1202
  if (pending.id !== params.requestId)
@@ -804,18 +1213,46 @@ class CodexAppServerExecutor {
804
1213
  return;
805
1214
  // Late background output must not become part of a subsequent turn.
806
1215
  if (eventTurnId && !matchesActiveTurn && method !== 'turn/started'
1216
+ && !(method === 'turn/completed' && goalRun?.inFlight.has(eventTurnId))
807
1217
  && params.item?.type !== 'contextCompaction')
808
1218
  return;
809
1219
  if (this.activeTurn)
810
1220
  this.armInactivityTimer();
811
1221
  switch (method) {
812
1222
  case 'turn/started':
1223
+ if (goalRun) {
1224
+ const id = params.turn?.id;
1225
+ if (params.threadId !== this.codexThreadId || typeof id !== 'string' || !id || goalRun.retired.has(id))
1226
+ break;
1227
+ goalRun.inFlight.add(id);
1228
+ if (this.activeTurn?.turnId !== id) {
1229
+ if (goalRun.retired.size && goalRun.active.output.length) {
1230
+ goalRun.active.output.push('\n\n');
1231
+ goalRun.active.options.onStream?.('\n\n');
1232
+ goalRun.active.options.onDisplayText?.('\n\n');
1233
+ }
1234
+ goalRun.active.emittedTools.clear();
1235
+ goalRun.active.itemPhases.clear();
1236
+ goalRun.active.error = undefined;
1237
+ goalRun.active.errorCode = undefined;
1238
+ }
1239
+ this.goalNotice(goalRun, 'Native goal running');
1240
+ }
813
1241
  if (this.activeTurn && typeof params.turn?.id === 'string')
814
1242
  this.activeTurn.turnId = params.turn.id;
815
1243
  break;
1244
+ case 'thread/status/changed':
1245
+ if (goalRun && params.status?.type === 'idle' && goalRun.goal?.status === 'active') {
1246
+ this.goalNotice(goalRun, 'Native goal active; waiting for native continuation or user control');
1247
+ }
1248
+ break;
816
1249
  case 'item/agentMessage/delta':
817
1250
  if (this.activeTurn && typeof params.delta === 'string') {
818
1251
  this.activeTurn.output.push(params.delta);
1252
+ if (goalRun && this.activeTurn.output.reduce((size, part) => size + part.length, 0) > 65_536) {
1253
+ this.activeTurn.output = ['[Earlier native goal output omitted from this diagnostic buffer]\n',
1254
+ this.activeTurn.output.join('').slice(-65_536)];
1255
+ }
819
1256
  this.activeTurn.options.onStream?.(params.delta);
820
1257
  this.activeTurn.options.onDisplayText?.(params.delta);
821
1258
  const phase = params.itemId ? this.activeTurn.itemPhases.get(params.itemId) : undefined;
@@ -933,6 +1370,11 @@ class CodexAppServerExecutor {
933
1370
  catch { /* Rendering failures must not make an accepted request executable again. */ }
934
1371
  }
935
1372
  handleServerRequest(id, method, params) {
1373
+ if (this.goalRun && (this.goalRun.stopping || this.goalRun.controlling || !this.goalRun.inFlight.size
1374
+ || params.turnId && !this.goalRun.inFlight.has(params.turnId))) {
1375
+ this.client.respondError(id, 'Request does not belong to a running native goal turn');
1376
+ return;
1377
+ }
936
1378
  if (params.threadId && params.threadId !== this.codexThreadId) {
937
1379
  this.client.respondError(id, 'Request does not belong to this remote-cli thread');
938
1380
  return;
@@ -1055,9 +1497,10 @@ class CodexAppServerExecutor {
1055
1497
  case 'mcpToolCall':
1056
1498
  case 'dynamicToolCall':
1057
1499
  this.emitToolUse(active, id, item.tool ?? 'MCP', item.arguments ?? {});
1500
+ this.handleGeneratedImage(active, item);
1058
1501
  active.options.onToolResult?.({
1059
1502
  tool_use_id: id,
1060
- content: this.stringifyResult(item.result ?? item.contentItems ?? item.error ?? ''),
1503
+ content: (0, CodexImages_1.codexToolResultText)(item.result ?? item.contentItems ?? item.error ?? ''),
1061
1504
  is_error: item.status !== 'completed' || item.success === false || Boolean(item.error),
1062
1505
  });
1063
1506
  break;
@@ -1081,6 +1524,35 @@ class CodexAppServerExecutor {
1081
1524
  const active = this.activeTurn;
1082
1525
  if (!active || !turn)
1083
1526
  return;
1527
+ const run = this.goalRun;
1528
+ if (run) {
1529
+ if (!run.inFlight.has(turn.id) || run.retired.has(turn.id))
1530
+ return;
1531
+ run.inFlight.delete(turn.id);
1532
+ if (!run.inFlight.size)
1533
+ run.onIdle?.();
1534
+ run.retired.add(turn.id);
1535
+ if (run.retired.size > 256)
1536
+ run.retired.delete(run.retired.values().next().value);
1537
+ if (active.turnId === turn.id)
1538
+ active.turnId = null;
1539
+ this.pendingUserInputs = this.pendingUserInputs.filter(pending => {
1540
+ if (pending.params.turnId && pending.params.turnId !== turn.id)
1541
+ return true;
1542
+ this.finishApproval(pending, 'expired');
1543
+ return false;
1544
+ });
1545
+ if (turn.status !== 'completed' && !run.controlling) {
1546
+ void this.failGoal(run, turn.status === 'interrupted' ? 'Native goal interrupted. Resume explicitly to continue.'
1547
+ : turn.error?.message ?? active.error ?? 'Native goal turn failed.');
1548
+ }
1549
+ else {
1550
+ this.maybeCompleteGoal(run);
1551
+ if (run.ready && this.goalRun === run)
1552
+ void this.checkGoalAfterTurn(run);
1553
+ }
1554
+ return;
1555
+ }
1084
1556
  if (active.turnId && turn.id && active.turnId !== turn.id)
1085
1557
  return;
1086
1558
  if (turn.status === 'completed') {
@@ -1126,27 +1598,27 @@ class CodexAppServerExecutor {
1126
1598
  this.activityTracker.emitTool(name, input, description ?? title);
1127
1599
  }
1128
1600
  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;
1601
+ const parsed = active.images.extract(item, active.turnId);
1602
+ for (const artifact of parsed.artifacts) {
1603
+ if ('image' in artifact)
1604
+ active.options.onImage?.(artifact.image);
1605
+ else
1606
+ active.options.onImageFile?.(artifact.path);
1607
+ }
1608
+ if (parsed.notice) {
1609
+ active.output.push(parsed.notice);
1610
+ active.options.onStream?.(parsed.notice);
1141
1611
  }
1142
- const image = { type: 'image', data, mimeType };
1143
- active.options.onImage?.(image);
1144
1612
  }
1145
1613
  completeActive(result) {
1146
1614
  const active = this.activeTurn;
1147
1615
  if (!active)
1148
1616
  return;
1149
1617
  this.activeTurn = null;
1618
+ if (this.goalRun?.active === active) {
1619
+ this.goalRun.onIdle?.();
1620
+ this.goalRun = null;
1621
+ }
1150
1622
  this.activityTracker.reset();
1151
1623
  this.activityTracker.setCallback(undefined);
1152
1624
  for (const pending of this.pendingUserInputs.splice(0))
@@ -1166,6 +1638,8 @@ class CodexAppServerExecutor {
1166
1638
  waiter.resolve(result);
1167
1639
  }
1168
1640
  async failAndRestart(error) {
1641
+ if (this.goalRun)
1642
+ return this.failGoal(this.goalRun, error);
1169
1643
  this.taskNotifications.clear();
1170
1644
  this.threadReady = false;
1171
1645
  this.completeActive({ success: false, error });
@@ -1222,15 +1696,19 @@ class CodexAppServerExecutor {
1222
1696
  const session = JSON.parse(fs.readFileSync(this.sessionFilePath, 'utf8'));
1223
1697
  if (typeof session.id === 'string' && session.id)
1224
1698
  this.codexThreadId = session.id;
1699
+ this.goalManaged = !!this.codexThreadId && session.goalManaged === true;
1700
+ this.goalNeedsResume = this.goalManaged;
1225
1701
  }
1226
1702
  catch {
1227
1703
  this.codexThreadId = null;
1704
+ this.goalManaged = false;
1705
+ this.goalNeedsResume = false;
1228
1706
  }
1229
1707
  }
1230
1708
  setThreadId(id) {
1231
1709
  this.codexThreadId = id;
1232
1710
  const temporaryPath = `${this.sessionFilePath}.${process.pid}.tmp`;
1233
- const data = JSON.stringify({ id, savedAt: new Date().toISOString() });
1711
+ const data = JSON.stringify({ id, savedAt: new Date().toISOString(), ...(this.goalManaged ? { goalManaged: true } : {}) });
1234
1712
  try {
1235
1713
  fs.writeFileSync(temporaryPath, data, { encoding: 'utf8', mode: 0o600 });
1236
1714
  fs.renameSync(temporaryPath, this.sessionFilePath);
@@ -1252,6 +1730,25 @@ class CodexAppServerExecutor {
1252
1730
  console.error('[CodexAppServerExecutor] Failed to delete session file:', error);
1253
1731
  }
1254
1732
  }
1733
+ /** Persist opt-in before a native mutation; a failed write must prevent activation. */
1734
+ rememberGoalManagement() {
1735
+ this.goalManaged = true;
1736
+ if (!this.codexThreadId)
1737
+ return;
1738
+ const temporaryPath = `${this.sessionFilePath}.${process.pid}.goal.tmp`;
1739
+ try {
1740
+ fs.writeFileSync(temporaryPath, JSON.stringify({ id: this.codexThreadId, goalManaged: true,
1741
+ savedAt: new Date().toISOString() }), { encoding: 'utf8', mode: 0o600 });
1742
+ fs.renameSync(temporaryPath, this.sessionFilePath);
1743
+ }
1744
+ catch {
1745
+ try {
1746
+ fs.unlinkSync(temporaryPath);
1747
+ }
1748
+ catch { /* Keep the previous binding intact. */ }
1749
+ throw new Error('Could not persist native goal ownership. No goal activation was requested.');
1750
+ }
1751
+ }
1255
1752
  extractErrorCode(info) {
1256
1753
  if (typeof info === 'string')
1257
1754
  return info;
@@ -1261,16 +1758,6 @@ class CodexAppServerExecutor {
1261
1758
  return info.type;
1262
1759
  return Object.keys(info)[0];
1263
1760
  }
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
1761
  errorMessage(error) {
1275
1762
  return error instanceof Error ? error.message : String(error);
1276
1763
  }