@rynx-ai/runtime 0.1.11-beta.34 → 0.1.11-beta.35

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/dist/host.js CHANGED
@@ -8,7 +8,7 @@ import { buildAgentSkillEnvironment, hashPluginPackageTree, scanSkillsDir, Sessi
8
8
  import { getRuntimeProfile, } from "@rynx-ai/core";
9
9
  import { resolveRuntimeBinary, } from "@rynx-ai/core";
10
10
  import { createCodexChildEnv } from "./codex-child-env.js";
11
- import { prepareRuntimeHome, populateCodexSkills, runtimeHomePath, } from "./codex-home.js";
11
+ import { applyCodexProviderRetryPolicy, codexLaunchRequiresLogin, prepareRuntimeHome, populateCodexSkills, readCodexDeveloperInstructionsState, runtimeHomePath, syncCodexDeveloperInstructions, trustCodexProject, } from "./codex-home.js";
12
12
  import { materializeClaudePlugin, reuseClaudePlugin, } from "./claude/executor.js";
13
13
  import { listClaudeModels } from "./claude/models.js";
14
14
  import { validateInteractionResolution } from "./interactions.js";
@@ -275,45 +275,6 @@ async function dismissCodexPlanImplementationPrompt(injector, ownsPrompt) {
275
275
  return "failed";
276
276
  }
277
277
  }
278
- /**
279
- * The `OPENAI_*` retry env for a codex-lineage app-server spawn, or `undefined`
280
- * when the budget declares no retry or the runtime is claude. claude applies
281
- * its retry budget per-run in `buildOptions` (see the claude executor), so it
282
- * never needs a dedicated app-server.
283
- */
284
- export function codexRetryEnv(runtime, budget) {
285
- if (runtime === "claude") {
286
- return undefined;
287
- }
288
- const retry = budget?.retry;
289
- if (!retry) {
290
- return undefined;
291
- }
292
- const env = {};
293
- if (retry.maxRetries != null) {
294
- env.OPENAI_MAX_RETRIES = String(retry.maxRetries);
295
- }
296
- if (retry.timeoutPerRequestSeconds != null) {
297
- env.OPENAI_TIMEOUT = String(Math.round(retry.timeoutPerRequestSeconds));
298
- }
299
- return Object.keys(env).length > 0 ? env : undefined;
300
- }
301
- /**
302
- * The cache key for a runtime backend. A retry budget on a codex-lineage
303
- * runtime forks a dedicated app-server (its own retry env) under a composite
304
- * `${runtime}::${hash}` key; everything else shares the base `${runtime}`
305
- * backend — so budget-less agents keep today's shared-process behaviour.
306
- */
307
- function hashRetryEnv(env) {
308
- return Object.keys(env)
309
- .sort()
310
- .map((key) => `${key}=${env[key]}`)
311
- .join("&");
312
- }
313
- export function codexBackendKey(runtime, budget) {
314
- const env = codexRetryEnv(runtime, budget);
315
- return env ? `${runtime}::${hashRetryEnv(env)}` : runtime;
316
- }
317
278
  export class LocalAgentHost {
318
279
  config;
319
280
  sessionStore;
@@ -324,8 +285,6 @@ export class LocalAgentHost {
324
285
  // auto-create. The command runner override backs the status probe.
325
286
  injectedCommandRunner;
326
287
  injectedAppServerClient;
327
- clock;
328
- backendIdleTtlMs;
329
288
  // Builds the SEPARATE forwarder connection for a live codex session (reference implementation's
330
289
  // multi-connection model). Defaults to an ExternalWsChannel client attached to
331
290
  // the backend's app-server; tests inject a fake.
@@ -337,8 +296,8 @@ export class LocalAgentHost {
337
296
  preloadClientFactory;
338
297
  /** Builds one short-lived message/interrupt client. */
339
298
  injectionClientFactory;
340
- // Keyed by `codexBackendKey` — the bare runtime id for budget-less agents, or
341
- // a `${runtime}::${retryHash}` composite for a per-agent retry budget.
299
+ // One backend per runtime. The host belongs to one Session runner, and Codex
300
+ // retry settings live in that Session's private config rather than process env.
342
301
  backends = new Map();
343
302
  // The rynx Session this host serves (set from `RYNX_RUNNER_SESSION`).
344
303
  sessionId;
@@ -371,7 +330,7 @@ export class LocalAgentHost {
371
330
  /** Short-lived dedupe for managed fork notifications delivered after the
372
331
  * `thread/fork` response. Values are expected source Provider thread ids. */
373
332
  managedForkThreadStarts = new Map();
374
- constructor({ config, commandRunner, sessionStore = new FileCodexSessionStore(resolveCodexSessionStorePath(config)), allowedRoots = resolveAllowedRoots(config), appServerClient, now = () => Date.now(), backendIdleTtlMs = 300_000, forwarderClientFactory, preloadClientFactory, injectionClientFactory, sessionId, runtimeHomeSessionId, }) {
333
+ constructor({ config, commandRunner, sessionStore = new FileCodexSessionStore(resolveCodexSessionStorePath(config)), allowedRoots = resolveAllowedRoots(config), appServerClient, forwarderClientFactory, preloadClientFactory, injectionClientFactory, sessionId, runtimeHomeSessionId, }) {
375
334
  this.config = config;
376
335
  this.sessionId = sessionId ?? "__default__";
377
336
  this.runtimeHomeSessionId = runtimeHomeSessionId ?? this.sessionId;
@@ -380,62 +339,22 @@ export class LocalAgentHost {
380
339
  this.defaultRuntime = config.DEFAULT_RUNTIME ?? "codex";
381
340
  this.injectedCommandRunner = commandRunner;
382
341
  this.injectedAppServerClient = appServerClient;
383
- this.clock = now;
384
- this.backendIdleTtlMs = backendIdleTtlMs;
385
342
  const defaultAttachedClientFactory = (appServerUrl) => new CodexAppServerClient({ channel: new ExternalWsChannel(appServerUrl) });
386
343
  this.forwarderClientFactory = forwarderClientFactory ?? defaultAttachedClientFactory;
387
344
  this.preloadClientFactory = preloadClientFactory ?? defaultAttachedClientFactory;
388
345
  this.injectionClientFactory = injectionClientFactory ?? defaultAttachedClientFactory;
389
346
  }
390
- getBackend(runtime, budget) {
391
- this.reapIdleBackends();
392
- // In forced exec mode (the default runtime's injected app-server is null) a
393
- // retry budget must NOT fork a composite backend: there is no app-server to
394
- // carry the retry env, and forking one would spawn a real app-server and
395
- // defeat the forced-exec override. Fall back to the base key — codex retry is
396
- // then a no-op in exec mode (consistent with the broader codex-retry caveat).
397
- const execMode = runtime === this.defaultRuntime && this.injectedAppServerClient === null;
398
- const retryEnv = execMode ? undefined : codexRetryEnv(runtime, budget);
399
- const key = retryEnv ? `${runtime}::${hashRetryEnv(retryEnv)}` : runtime;
400
- const existing = this.backends.get(key);
401
- if (existing) {
402
- existing.lastUsedAt = this.clock();
347
+ getBackend(runtime) {
348
+ const existing = this.backends.get(runtime);
349
+ if (existing)
403
350
  return existing;
404
- }
405
- const backend = this.createBackend(runtime, retryEnv);
406
- backend.lastUsedAt = this.clock();
407
- this.backends.set(key, backend);
351
+ const backend = this.createBackend(runtime);
352
+ this.backends.set(runtime, backend);
408
353
  return backend;
409
354
  }
410
- /**
411
- * Stop + drop per-budget composite backends that have been idle longer than
412
- * the TTL and have no in-flight run. Base per-runtime backends (key === the
413
- * bare runtime id) are never reaped — they live for the process lifetime, as
414
- * before. There are no per-conversation close hooks, so this lazy sweep (run
415
- * on every `getBackend`) is how dedicated app-servers get reclaimed.
416
- */
417
- reapIdleBackends() {
418
- const now = this.clock();
419
- for (const [key, backend] of this.backends) {
420
- if (!key.includes("::")) {
421
- continue;
422
- }
423
- if ((backend.inFlight ?? 0) > 0 || backend.lastUsedAt === undefined) {
424
- continue;
425
- }
426
- if (now - backend.lastUsedAt < this.backendIdleTtlMs) {
427
- continue;
428
- }
429
- void backend.appServerClient?.stop();
430
- this.backends.delete(key);
431
- }
432
- }
433
- createBackend(runtime, retryEnv) {
355
+ createBackend(runtime) {
434
356
  const profile = getRuntimeProfile(runtime);
435
- // A composite (per-budget) backend is inherently non-default: it gets a
436
- // fresh app-server with the retry env, never the test-injected default deps.
437
- const isComposite = retryEnv !== undefined;
438
- const isDefault = runtime === this.defaultRuntime && !isComposite;
357
+ const isDefault = runtime === this.defaultRuntime;
439
358
  const commandRunner = (isDefault && this.injectedCommandRunner) ||
440
359
  new SpawnCodexCommandRunner(resolveRuntimeBinary(runtime));
441
360
  // The claude runtime is not codex-lineage: it has no app-server (its live
@@ -450,7 +369,7 @@ export class LocalAgentHost {
450
369
  ? this.injectedAppServerClient === null
451
370
  ? null
452
371
  : this.injectedAppServerClient ?? this.createAppServerClient(runtime)
453
- : this.createAppServerClient(runtime, retryEnv);
372
+ : this.createAppServerClient(runtime);
454
373
  return { runtime, profile, appServerClient, commandRunner };
455
374
  }
456
375
  /** This session's private native home, shared by its app-server + TUI. */
@@ -464,7 +383,7 @@ export class LocalAgentHost {
464
383
  this.runtimeHomes.set(runtime, prepared);
465
384
  return prepared;
466
385
  }
467
- createAppServerClient(runtime, extraEnv) {
386
+ createAppServerClient(runtime) {
468
387
  const cliPath = resolveRuntimeBinary(runtime);
469
388
  // Per-agent knobs (resolved at live-session start) win over the daemon
470
389
  // default — the app-server is private to this session's agent.
@@ -482,7 +401,6 @@ export class LocalAgentHost {
482
401
  baseArgs: buildAppServerBaseArgs(sandbox),
483
402
  stateDir: runtimeSessionStateDir(this.runtimeHomeSessionId),
484
403
  extraEnv: {
485
- ...(extraEnv ?? {}),
486
404
  [profile.homeEnvVar]: this.runtimeHome(runtime),
487
405
  },
488
406
  });
@@ -544,7 +462,7 @@ export class LocalAgentHost {
544
462
  const modelChanged = desiredModel !== live.appliedModel;
545
463
  const effortChanged = desiredReasoningEffort !== live.appliedReasoningEffort;
546
464
  const collaborationMode = mode && live.appliedCollaborationMode !== mode
547
- ? await resolveLiveCollaborationMode(client, live, mode)
465
+ ? await resolveLiveCollaborationMode(client, live, mode, live.runtime === "codex" ? this.runtimeHome(live.runtime) : undefined)
548
466
  : undefined;
549
467
  const settings = {
550
468
  threadId,
@@ -600,7 +518,7 @@ export class LocalAgentHost {
600
518
  throw new CodexRuntimeError("session snapshots are not live", 409, "session_not_live");
601
519
  }
602
520
  const runtime = live.runtime;
603
- const backend = this.getBackend(runtime, live.execution.budget ?? undefined);
521
+ const backend = this.getBackend(runtime);
604
522
  const client = backend.appServerClient;
605
523
  if (!client) {
606
524
  throw new CodexRuntimeError(`no app-server for runtime ${runtime}`, 500, "app_server_unavailable");
@@ -620,6 +538,10 @@ export class LocalAgentHost {
620
538
  const traexYolo = runtime === "traex" &&
621
539
  sandbox === "danger-full-access" &&
622
540
  approvalPolicy === "never";
541
+ // Runner-owned Codex terminals start detached, so nobody can answer the
542
+ // interactive hook-review prompt. The private CODEX_HOME is provisioned
543
+ // before launch; use Codex's native bypass flag for this managed TUI.
544
+ const bypassHookTrust = runtime === "codex" || traexYolo;
623
545
  const configOverrides = sandbox === "workspace-write"
624
546
  ? ["sandbox_workspace_write.network_access=true"]
625
547
  : [];
@@ -637,7 +559,7 @@ export class LocalAgentHost {
637
559
  const launchEffort = live.reasoningEffort;
638
560
  if (launchEffort)
639
561
  configOverrides.push(`model_reasoning_effort=${JSON.stringify(launchEffort)}`);
640
- if (live?.instructions) {
562
+ if (runtime === "traex" && live?.instructions) {
641
563
  configOverrides.push(`developer_instructions=${JSON.stringify(live.instructions)}`);
642
564
  }
643
565
  const profile = getRuntimeProfile(runtime);
@@ -649,7 +571,7 @@ export class LocalAgentHost {
649
571
  remoteUrl,
650
572
  ...(activeThreadId ? { threadId: activeThreadId } : {}),
651
573
  configOverrides,
652
- codexArgs: traexYolo ? ["--dangerously-bypass-hook-trust"] : [],
574
+ codexArgs: bypassHookTrust ? ["--dangerously-bypass-hook-trust"] : [],
653
575
  additionalDirs: providerAdditionalDirs(live.workspace),
654
576
  }),
655
577
  cwd: live.workspace.cwd,
@@ -748,11 +670,25 @@ export class LocalAgentHost {
748
670
  return false;
749
671
  }
750
672
  }
751
- // Persist the owner's skills before the app-server boots. Fork targets reuse
752
- // the owner's immutable copies and never mutate the shared Provider home.
673
+ // Persist headless project trust before the app-server boots. Fork targets
674
+ // reuse the owner's immutable Provider home.
753
675
  const liveSkills = snapshotSkills.selectedSkills.map((s) => ({ name: s.name, dir: s.dir }));
676
+ const runtimeHome = this.runtimeHome(runtime);
677
+ if (runtime === "codex") {
678
+ try {
679
+ trustCodexProject(runtimeHome, workspace.cwd);
680
+ syncCodexDeveloperInstructions(runtimeHome, nativeAuthoredInstructions(execution));
681
+ applyCodexProviderRetryPolicy(runtimeHome, execution.budget?.retry);
682
+ }
683
+ catch (err) {
684
+ void snapshotSkills.skillsCleanup();
685
+ this.liveStartupErrors.set(localThreadId, nativeLiveFailure(runtime, "native_project_trust_failed", "project trust preparation failed", err));
686
+ console.error(`[session-snapshot] session=${localThreadId} project trust preparation failed: ${err instanceof Error ? err.message : String(err)}`);
687
+ return false;
688
+ }
689
+ }
754
690
  try {
755
- populateCodexSkills(this.runtimeHome(runtime), liveSkills);
691
+ populateCodexSkills(runtimeHome, liveSkills);
756
692
  }
757
693
  catch (err) {
758
694
  void snapshotSkills.skillsCleanup();
@@ -775,12 +711,19 @@ export class LocalAgentHost {
775
711
  return false;
776
712
  };
777
713
  reapRuntimeProcessesForStateDir(runtimeSessionStateDir(this.runtimeHomeSessionId));
778
- const appServerOwner = this.getBackend(runtime, execution.budget ?? undefined).appServerClient;
714
+ const backend = this.getBackend(runtime);
715
+ const appServerOwner = backend.appServerClient;
779
716
  if (!appServerOwner) {
780
717
  this.liveStartupErrors.set(localThreadId, nativeLiveFailure(runtime, "native_app_server_unavailable", "app-server is unavailable for this Session"));
781
718
  console.error(`[codex-live] session=${localThreadId} runtime=${runtime} no app-server client`);
782
719
  return abandon();
783
720
  }
721
+ let loginRequired;
722
+ if (!coldResume &&
723
+ runtime === "codex" &&
724
+ codexLaunchRequiresLogin(runtimeHome, createCodexChildEnv({ ...process.env, CODEX_HOME: runtimeHome }))) {
725
+ loginRequired = new CodexRuntimeError("Codex is not signed in and no configured Provider can run this Session, so the Terminal is waiting at the sign-in screen. Sign in from the Session Terminal, then send the message again.", 401, "login_required");
726
+ }
784
727
  try {
785
728
  await appServerOwner.ensureInitialized();
786
729
  }
@@ -873,12 +816,15 @@ export class LocalAgentHost {
873
816
  appliedModel: coldResume ? "" : model,
874
817
  appliedReasoningEffort: coldResume ? undefined : reasoningEffort,
875
818
  appliedCollaborationMode: undefined,
876
- ...(execution.instructions ? { instructions: execution.instructions } : {}),
819
+ ...(runtime === "traex" && execution.instructions
820
+ ? { instructions: execution.instructions }
821
+ : {}),
877
822
  threadId: record?.codexSessionId ?? null,
878
823
  ready,
879
824
  markReady,
880
825
  startupFailed,
881
826
  markStartupFailed,
827
+ ...(loginRequired ? { startupError: loginRequired } : {}),
882
828
  terminalReady,
883
829
  markTerminalReady,
884
830
  injectLock: Promise.resolve(),
@@ -1090,7 +1036,7 @@ export class LocalAgentHost {
1090
1036
  const client = this.injectionClientFactory(live.appServerUrl);
1091
1037
  try {
1092
1038
  await client.ensureInitialized();
1093
- const collaborationMode = await resolveLiveCollaborationMode(client, live, "default");
1039
+ const collaborationMode = await resolveLiveCollaborationMode(client, live, "default", live.runtime === "codex" ? this.runtimeHome(live.runtime) : undefined);
1094
1040
  let threadId = prompt.threadId;
1095
1041
  let text = CODEX_PLAN_IMPLEMENTATION_CODING_MESSAGE;
1096
1042
  if (choice === CODEX_PLAN_IMPLEMENTATION_CLEAR_CONTEXT) {
@@ -1458,6 +1404,8 @@ export class LocalAgentHost {
1458
1404
  };
1459
1405
  bindObserverClient(forwarderClient);
1460
1406
  this.liveSessions.set(localThreadId, live);
1407
+ if (loginRequired)
1408
+ this.liveStartupErrors.set(localThreadId, loginRequired);
1461
1409
  forwarder.start();
1462
1410
  // Existing bindings resume through the public app-server API. A fresh thread
1463
1411
  // is created by the native remote TUI instead: its `thread/started`
@@ -1537,12 +1485,19 @@ export class LocalAgentHost {
1537
1485
  console.error(`[codex-live] session=${localThreadId} runtime=${runtime} thread bind failed: ${err instanceof Error ? err.message : String(err)}`);
1538
1486
  return abandon();
1539
1487
  }
1540
- this.liveStartupErrors.delete(localThreadId);
1488
+ if (!loginRequired)
1489
+ this.liveStartupErrors.delete(localThreadId);
1541
1490
  return true;
1542
1491
  }
1543
1492
  /** Bind a session's codex thread id once known (TUI broadcast or store): persist
1544
1493
  * it, unblock injection, and kick off the resume-subscribe loop (once). */
1545
1494
  onLiveThreadStarted(live, localThreadId, threadId, forkedFromId) {
1495
+ if (live.startupError?.code === "login_required") {
1496
+ delete live.startupError;
1497
+ if (this.liveStartupErrors.get(localThreadId)?.code === "login_required") {
1498
+ this.liveStartupErrors.delete(localThreadId);
1499
+ }
1500
+ }
1546
1501
  const previousThreadId = live.threadId;
1547
1502
  if (previousThreadId !== threadId) {
1548
1503
  live.threadActive = false;
@@ -1866,6 +1821,8 @@ export class LocalAgentHost {
1866
1821
  const live = this.liveSessions.get(localThreadId);
1867
1822
  if (!live)
1868
1823
  return { outcome: "notLive" };
1824
+ if (live.startupError?.code === "login_required")
1825
+ return { outcome: "notReady" };
1869
1826
  const run = live.injectLock.then(async () => {
1870
1827
  if (live.rotationPending || live.stopped)
1871
1828
  return { outcome: "failed" };
@@ -1935,7 +1892,7 @@ export class LocalAgentHost {
1935
1892
  const desiredCollaborationMode = options?.collaborationMode ?? live.execution.collaborationMode;
1936
1893
  const collaborationMode = desiredCollaborationMode &&
1937
1894
  desiredCollaborationMode !== live.appliedCollaborationMode
1938
- ? await resolveLiveCollaborationMode(injectionClient, live, desiredCollaborationMode)
1895
+ ? await resolveLiveCollaborationMode(injectionClient, live, desiredCollaborationMode, live.runtime === "codex" ? this.runtimeHome(live.runtime) : undefined)
1939
1896
  : undefined;
1940
1897
  const settings = {
1941
1898
  threadId,
@@ -1990,7 +1947,8 @@ export class LocalAgentHost {
1990
1947
  catch (error) {
1991
1948
  forgetPendingInput();
1992
1949
  if (error instanceof CodexRuntimeError &&
1993
- error.code === "collaboration_mode_model_unknown") {
1950
+ (error.code === "collaboration_mode_model_unknown" ||
1951
+ error.code === "collaboration_mode_instructions_unknown")) {
1994
1952
  throw error;
1995
1953
  }
1996
1954
  // Preserve the app-server's error instead of collapsing every failure
@@ -2145,7 +2103,7 @@ export class LocalAgentHost {
2145
2103
  if (!live)
2146
2104
  return false;
2147
2105
  const turnFailed = error ? live.forwarder.failOpenTurn(error) : false;
2148
- const backendKey = codexBackendKey(live.runtime, live.execution.budget ?? undefined);
2106
+ const backendKey = live.runtime;
2149
2107
  const backend = this.backends.get(backendKey);
2150
2108
  const appServerOwner = live.appServerOwner;
2151
2109
  this.stopLiveCodexSession(localThreadId);
@@ -2229,8 +2187,8 @@ export class LocalAgentHost {
2229
2187
  ...(live.execution.reasoningEffort
2230
2188
  ? { reasoningEffort: live.execution.reasoningEffort }
2231
2189
  : {}),
2232
- ...(live.execution.instructions
2233
- ? { appendSystemPrompt: live.execution.instructions }
2190
+ ...(nativeAuthoredInstructions(live.execution)
2191
+ ? { appendSystemPrompt: nativeAuthoredInstructions(live.execution) }
2234
2192
  : {}),
2235
2193
  additionalDirs: providerAdditionalDirs(live.workspace),
2236
2194
  ...(live.forkIntent
@@ -2894,7 +2852,7 @@ export class LocalAgentHost {
2894
2852
  }
2895
2853
  const runtime = execution.provider;
2896
2854
  const client = live?.appServerOwner ??
2897
- this.getBackend(runtime, execution.budget ?? undefined).appServerClient;
2855
+ this.getBackend(runtime).appServerClient;
2898
2856
  if (!client) {
2899
2857
  return { ok: false, reason: "unsupported" };
2900
2858
  }
@@ -3029,7 +2987,7 @@ function adoptResumedThreadSettings(live, settings) {
3029
2987
  /** Resolve the model required by a full collaboration-mode snapshot. Only a
3030
2988
  * fresh thread with no explicit/config model may use the catalog default; a
3031
2989
  * resumed thread must report its own authoritative model. */
3032
- async function resolveLiveCollaborationMode(client, live, mode) {
2990
+ async function resolveLiveCollaborationMode(client, live, mode, codexHome) {
3033
2991
  let model = live.model.trim();
3034
2992
  if (!model && live.mayUseProviderDefaultModel) {
3035
2993
  let catalog;
@@ -3048,11 +3006,21 @@ async function resolveLiveCollaborationMode(client, live, mode) {
3048
3006
  }
3049
3007
  model = defaults[0];
3050
3008
  }
3051
- return buildCollaborationMode(mode, model, live.reasoningEffort);
3009
+ let developerInstructions = null;
3010
+ if (live.runtime === "codex") {
3011
+ const state = codexHome
3012
+ ? readCodexDeveloperInstructionsState(codexHome)
3013
+ : { state: "unreadable" };
3014
+ if (state.state === "unreadable") {
3015
+ throw new CodexRuntimeError("collaboration mode requires the current developer instructions, but the private Codex config could not be read", 503, "collaboration_mode_instructions_unknown");
3016
+ }
3017
+ developerInstructions = state.state === "present" ? state.value : null;
3018
+ }
3019
+ return buildCollaborationMode(mode, model, live.reasoningEffort, developerInstructions);
3052
3020
  }
3053
3021
  /** Expand the public mode enum into the App Server's required settings
3054
3022
  * snapshot using the Session's tracked effective model. */
3055
- function buildCollaborationMode(mode, desiredModel, desiredReasoningEffort) {
3023
+ function buildCollaborationMode(mode, desiredModel, desiredReasoningEffort, developerInstructions) {
3056
3024
  const model = desiredModel.trim();
3057
3025
  if (!model) {
3058
3026
  throw new CodexRuntimeError("collaboration mode requires the current model, but the Provider did not report one", 503, "collaboration_mode_model_unknown");
@@ -3062,12 +3030,19 @@ function buildCollaborationMode(mode, desiredModel, desiredReasoningEffort) {
3062
3030
  settings: {
3063
3031
  model,
3064
3032
  reasoning_effort: desiredReasoningEffort ?? null,
3065
- // `null` asks the App Server to install the built-in instructions for
3066
- // the selected mode; thread-level Agent instructions remain separate.
3067
- developer_instructions: null,
3033
+ developer_instructions: developerInstructions,
3068
3034
  },
3069
3035
  };
3070
3036
  }
3037
+ /** New snapshots distinguish authored Agent text from framework-rendered
3038
+ * instructions. Older persisted snapshots fall back to their only available
3039
+ * field so existing sessions preserve their launch behavior. */
3040
+ function nativeAuthoredInstructions(execution) {
3041
+ const value = execution.authoredInstructions === undefined
3042
+ ? execution.instructions
3043
+ : execution.authoredInstructions;
3044
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
3045
+ }
3071
3046
  function sameSessionSnapshots(workspace, execution, opts) {
3072
3047
  return JSON.stringify(stableValue({
3073
3048
  workspace,
@@ -3124,6 +3099,12 @@ function applyClaudePermissionModeSnapshot(settings, permissionMode) {
3124
3099
  permissions.defaultMode = permissionMode;
3125
3100
  else
3126
3101
  delete permissions.defaultMode;
3102
+ if (permissionMode === "bypassPermissions") {
3103
+ next.skipDangerousModePermissionPrompt = true;
3104
+ }
3105
+ else {
3106
+ delete next.skipDangerousModePermissionPrompt;
3107
+ }
3127
3108
  if (Object.keys(permissions).length > 0)
3128
3109
  next.permissions = permissions;
3129
3110
  else
@@ -92,6 +92,7 @@ export declare class RunnerSession {
92
92
  private readonly attachmentRoles;
93
93
  private readonly traexStartupWatchers;
94
94
  private readonly traexStartupGates;
95
+ private readonly codexStartupMonitors;
95
96
  private readonly terminalWatchers;
96
97
  /** Opens are async; a close received before attach resolves tombstones the id. */
97
98
  private readonly pendingTerminalOpens;
@@ -126,6 +126,7 @@ export class RunnerSession {
126
126
  attachmentRoles = new Map();
127
127
  traexStartupWatchers = new Map();
128
128
  traexStartupGates = new Map();
129
+ codexStartupMonitors = new Map();
129
130
  terminalWatchers = new Map();
130
131
  /** Opens are async; a close received before attach resolves tombstones the id. */
131
132
  pendingTerminalOpens = new Set();
@@ -528,10 +529,31 @@ export class RunnerSession {
528
529
  }
529
530
  }
530
531
  monitorCodexThreadStartup(localThreadId, runtime, provider) {
532
+ // Starting (or declining to start) a new monitor invalidates every callback
533
+ // owned by the previous launch before it can touch the replacement state.
534
+ this.codexStartupMonitors.delete(localThreadId);
531
535
  if (!provider.waitLiveReady)
532
536
  return;
537
+ if (provider.liveSessionFailure?.(localThreadId)?.code === "login_required")
538
+ return;
539
+ const expectedTerminal = this.terminals.get(`${localThreadId}-main`);
540
+ const monitor = Symbol(localThreadId);
541
+ this.codexStartupMonitors.set(localThreadId, monitor);
542
+ const claimCurrentLaunch = () => {
543
+ if (this.codexStartupMonitors.get(localThreadId) !== monitor ||
544
+ this.terminals.get(`${localThreadId}-main`) !== expectedTerminal)
545
+ return false;
546
+ this.codexStartupMonitors.delete(localThreadId);
547
+ return true;
548
+ };
533
549
  void provider.waitLiveReady(localThreadId, CODEX_THREAD_START_TIMEOUT_MS).then((ready) => {
534
- if (ready)
550
+ if (ready) {
551
+ if (this.codexStartupMonitors.get(localThreadId) === monitor) {
552
+ this.codexStartupMonitors.delete(localThreadId);
553
+ }
554
+ return;
555
+ }
556
+ if (!claimCurrentLaunch())
535
557
  return;
536
558
  const terminal = this.terminals.get(`${localThreadId}-main`);
537
559
  const paneFailure = terminal && !terminal.isAlive()
@@ -550,27 +572,30 @@ export class RunnerSession {
550
572
  : `${runtime === "traex" ? "Traex" : "Codex"} TUI did not publish thread/started within ${CODEX_THREAD_START_TIMEOUT_MS / 1_000}s after app-server and observer readiness`);
551
573
  this.failCodexThreadStartup(localThreadId, provider, new AgentRuntimeError(detail, 503, paneFailure
552
574
  ? "native_terminal_exited_before_session"
553
- : "native_thread_discovery_timeout"));
575
+ : "native_thread_discovery_timeout"), expectedTerminal);
554
576
  }).catch((error) => {
577
+ if (!claimCurrentLaunch())
578
+ return;
555
579
  this.failCodexThreadStartup(localThreadId, provider, error instanceof AgentRuntimeError
556
580
  ? error
557
- : nativePhaseError(runtime, "native_thread_discovery_failed", "native session discovery failed", error));
581
+ : nativePhaseError(runtime, "native_thread_discovery_failed", "native session discovery failed", error), expectedTerminal);
558
582
  });
559
583
  }
560
584
  /** Discovery failure is terminal for this partial native launch. Publish the
561
585
  * exact cause first so an in-flight injection wakes, then drop the observer
562
586
  * and TUI so a later task retry creates a clean launch instead of reusing an
563
587
  * already-rejected readiness promise. */
564
- failCodexThreadStartup(localThreadId, provider, error) {
588
+ failCodexThreadStartup(localThreadId, provider, error, expectedTerminal) {
565
589
  if (!provider.failLiveStartup?.(localThreadId, error))
566
590
  return false;
591
+ this.codexStartupMonitors.delete(localThreadId);
567
592
  const watcher = this.terminalWatchers.get(localThreadId);
568
593
  if (watcher)
569
594
  clearInterval(watcher);
570
595
  this.terminalWatchers.delete(localThreadId);
571
596
  this.cancelTraexStartupWatcher(localThreadId);
572
597
  try {
573
- this.terminals.close(`${localThreadId}-main`);
598
+ this.terminals.close(`${localThreadId}-main`, expectedTerminal);
574
599
  }
575
600
  catch (closeError) {
576
601
  console.warn(`[runner] session=${localThreadId} failed to close native Terminal after startup failure: ${closeError instanceof Error ? closeError.message : String(closeError)}`);
@@ -772,11 +797,11 @@ export class RunnerSession {
772
797
  const runtime = this.liveRuntimes.get(localThreadId);
773
798
  const lifecycle = this.terminals.lifecycle(terminalId);
774
799
  const paneFailure = terminalFailureDetail(terminal.capturePane?.() ?? "");
775
- const startupFailed = this.failCodexThreadStartup(localThreadId, this.liveProvider, nativePhaseError(runtime, "native_terminal_exited_before_session", "Terminal exited before native session discovery completed", paneFailure || undefined));
800
+ const startupFailed = this.failCodexThreadStartup(localThreadId, this.liveProvider, nativePhaseError(runtime, "native_terminal_exited_before_session", "Terminal exited before native session discovery completed", paneFailure || undefined), terminal);
776
801
  if (startupFailed || lifecycle === undefined)
777
802
  return;
778
803
  try {
779
- this.terminals.close(terminalId);
804
+ this.terminals.close(terminalId, terminal);
780
805
  }
781
806
  catch (closeError) {
782
807
  console.warn(`[runner] session=${localThreadId} failed to close exited native Terminal: ${closeError instanceof Error ? closeError.message : String(closeError)}`);
@@ -943,6 +968,7 @@ export class RunnerSession {
943
968
  for (const timer of this.terminalWatchers.values())
944
969
  clearInterval(timer);
945
970
  this.terminalWatchers.clear();
971
+ this.codexStartupMonitors.clear();
946
972
  for (const id of this.liveIds) {
947
973
  this.liveProvider.stopLiveCodexSession?.(id, {
948
974
  deferClaudeInteractionCleanup: true,
@@ -37,8 +37,9 @@ export declare class TerminalRegistry {
37
37
  cols?: number;
38
38
  rows?: number;
39
39
  }): Promise<PrepareResult>;
40
- /** Kill a terminal's tmux server and drop it. */
41
- close(id: string): void;
40
+ /** Kill a terminal's tmux server and drop it. When `expected` is supplied,
41
+ * close only if that exact instance still owns the id. */
42
+ close(id: string, expected?: TmuxTerminal): boolean;
42
43
  /** Kill every terminal (runner shutdown). */
43
44
  closeAll(): void;
44
45
  }
@@ -71,15 +71,17 @@ export class TerminalRegistry {
71
71
  const preparation = await terminal.terminal.prepare(requestedRole, dims);
72
72
  return { preparation, role: requestedRole };
73
73
  }
74
- /** Kill a terminal's tmux server and drop it. */
75
- close(id) {
74
+ /** Kill a terminal's tmux server and drop it. When `expected` is supplied,
75
+ * close only if that exact instance still owns the id. */
76
+ close(id, expected) {
76
77
  const entry = this.terminals.get(id);
77
- if (!entry)
78
- return;
78
+ if (!entry || (expected && entry.terminal !== expected))
79
+ return false;
79
80
  // Remove the resource before best-effort close so a failed kill cannot
80
81
  // leave a dead registry entry that later attaches will reuse.
81
82
  this.terminals.delete(id);
82
83
  entry.terminal.kill();
84
+ return true;
83
85
  }
84
86
  /** Kill every terminal (runner shutdown). */
85
87
  closeAll() {