@rynx-ai/runtime 0.1.11-beta.33 → 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.
Files changed (36) hide show
  1. package/dist/claude/native-bridge.d.ts +16 -1
  2. package/dist/claude/native-bridge.js +91 -17
  3. package/dist/claude/native-hook-main.js +1 -0
  4. package/dist/claude/native-integration.d.ts +10 -0
  5. package/dist/claude/native-integration.js +100 -6
  6. package/dist/codex-app-server/forwarder.js +23 -1
  7. package/dist/codex-app-server/mapping.js +34 -10
  8. package/dist/codex-app-server/process-registry.d.ts +36 -0
  9. package/dist/codex-app-server/process-registry.js +320 -0
  10. package/dist/codex-app-server/ws-channel.d.ts +7 -0
  11. package/dist/codex-app-server/ws-channel.js +85 -9
  12. package/dist/codex-home.d.ts +35 -3
  13. package/dist/codex-home.js +321 -14
  14. package/dist/host.d.ts +11 -25
  15. package/dist/host.js +147 -132
  16. package/dist/index.d.ts +2 -2
  17. package/dist/index.js +1 -1
  18. package/dist/runner/child.d.ts +25 -3
  19. package/dist/runner/child.js +359 -64
  20. package/dist/runner/manager.d.ts +33 -18
  21. package/dist/runner/manager.js +229 -49
  22. package/dist/runner/protocol.d.ts +79 -7
  23. package/dist/runner/transport.d.ts +9 -2
  24. package/dist/runner/transport.js +55 -3
  25. package/dist/runner-main.js +8 -3
  26. package/dist/terminal/codex-tui.d.ts +4 -0
  27. package/dist/terminal/codex-tui.js +5 -0
  28. package/dist/terminal/control-parser.d.ts +39 -0
  29. package/dist/terminal/control-parser.js +172 -0
  30. package/dist/terminal/registry.d.ts +18 -15
  31. package/dist/terminal/registry.js +43 -23
  32. package/dist/terminal/spool.d.ts +47 -0
  33. package/dist/terminal/spool.js +231 -0
  34. package/dist/terminal/tmux.d.ts +93 -84
  35. package/dist/terminal/tmux.js +675 -200
  36. package/package.json +3 -4
package/dist/host.js CHANGED
@@ -8,15 +8,17 @@ 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";
15
15
  import { CodexAppServerClient, buildRuntimeUserInput, } from "./codex-app-server/client.js";
16
16
  import { buildAppServerBaseArgs, CodexTransportError, } from "./codex-app-server/transport.js";
17
17
  import { WsRpcChannel, ExternalWsChannel } from "./codex-app-server/ws-channel.js";
18
+ import { reapRuntimeProcessesForStateDir } from "./codex-app-server/process-registry.js";
18
19
  import { CodexSessionForwarder } from "./codex-app-server/forwarder.js";
19
20
  import { readMcpStartupPlan } from "./codex-app-server/mcp-startup.js";
21
+ import { runtimeSessionStateDir } from "./runtime-state-paths.js";
20
22
  import { buildCodexRemoteArgs } from "./terminal/codex-tui.js";
21
23
  import { buildClaudeTuiArgs } from "./terminal/claude-tui.js";
22
24
  import { providerAdditionalDirs, threadWorkspaceParams, turnWorkspaceParams, } from "./provider-workspace.js";
@@ -273,45 +275,6 @@ async function dismissCodexPlanImplementationPrompt(injector, ownsPrompt) {
273
275
  return "failed";
274
276
  }
275
277
  }
276
- /**
277
- * The `OPENAI_*` retry env for a codex-lineage app-server spawn, or `undefined`
278
- * when the budget declares no retry or the runtime is claude. claude applies
279
- * its retry budget per-run in `buildOptions` (see the claude executor), so it
280
- * never needs a dedicated app-server.
281
- */
282
- export function codexRetryEnv(runtime, budget) {
283
- if (runtime === "claude") {
284
- return undefined;
285
- }
286
- const retry = budget?.retry;
287
- if (!retry) {
288
- return undefined;
289
- }
290
- const env = {};
291
- if (retry.maxRetries != null) {
292
- env.OPENAI_MAX_RETRIES = String(retry.maxRetries);
293
- }
294
- if (retry.timeoutPerRequestSeconds != null) {
295
- env.OPENAI_TIMEOUT = String(Math.round(retry.timeoutPerRequestSeconds));
296
- }
297
- return Object.keys(env).length > 0 ? env : undefined;
298
- }
299
- /**
300
- * The cache key for a runtime backend. A retry budget on a codex-lineage
301
- * runtime forks a dedicated app-server (its own retry env) under a composite
302
- * `${runtime}::${hash}` key; everything else shares the base `${runtime}`
303
- * backend — so budget-less agents keep today's shared-process behaviour.
304
- */
305
- function hashRetryEnv(env) {
306
- return Object.keys(env)
307
- .sort()
308
- .map((key) => `${key}=${env[key]}`)
309
- .join("&");
310
- }
311
- export function codexBackendKey(runtime, budget) {
312
- const env = codexRetryEnv(runtime, budget);
313
- return env ? `${runtime}::${hashRetryEnv(env)}` : runtime;
314
- }
315
278
  export class LocalAgentHost {
316
279
  config;
317
280
  sessionStore;
@@ -322,8 +285,6 @@ export class LocalAgentHost {
322
285
  // auto-create. The command runner override backs the status probe.
323
286
  injectedCommandRunner;
324
287
  injectedAppServerClient;
325
- clock;
326
- backendIdleTtlMs;
327
288
  // Builds the SEPARATE forwarder connection for a live codex session (reference implementation's
328
289
  // multi-connection model). Defaults to an ExternalWsChannel client attached to
329
290
  // the backend's app-server; tests inject a fake.
@@ -335,8 +296,8 @@ export class LocalAgentHost {
335
296
  preloadClientFactory;
336
297
  /** Builds one short-lived message/interrupt client. */
337
298
  injectionClientFactory;
338
- // Keyed by `codexBackendKey` — the bare runtime id for budget-less agents, or
339
- // 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.
340
301
  backends = new Map();
341
302
  // The rynx Session this host serves (set from `RYNX_RUNNER_SESSION`).
342
303
  sessionId;
@@ -361,6 +322,7 @@ export class LocalAgentHost {
361
322
  /** Claude forwarders stopped before their terminal is killed. Runner shutdown
362
323
  * finalizes these synchronously afterwards to scrub raw interaction answers. */
363
324
  pendingClaudeFinalizers = new Set();
325
+ pendingCleanupTasks = new Set();
364
326
  // In-flight `ensureLiveCodexSession` calls, so concurrent triggers (a web
365
327
  // `live.ensure` racing a `term.open`) share ONE forwarder — never two.
366
328
  liveEnsuring = new Map();
@@ -368,7 +330,7 @@ export class LocalAgentHost {
368
330
  /** Short-lived dedupe for managed fork notifications delivered after the
369
331
  * `thread/fork` response. Values are expected source Provider thread ids. */
370
332
  managedForkThreadStarts = new Map();
371
- 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, }) {
372
334
  this.config = config;
373
335
  this.sessionId = sessionId ?? "__default__";
374
336
  this.runtimeHomeSessionId = runtimeHomeSessionId ?? this.sessionId;
@@ -377,62 +339,22 @@ export class LocalAgentHost {
377
339
  this.defaultRuntime = config.DEFAULT_RUNTIME ?? "codex";
378
340
  this.injectedCommandRunner = commandRunner;
379
341
  this.injectedAppServerClient = appServerClient;
380
- this.clock = now;
381
- this.backendIdleTtlMs = backendIdleTtlMs;
382
342
  const defaultAttachedClientFactory = (appServerUrl) => new CodexAppServerClient({ channel: new ExternalWsChannel(appServerUrl) });
383
343
  this.forwarderClientFactory = forwarderClientFactory ?? defaultAttachedClientFactory;
384
344
  this.preloadClientFactory = preloadClientFactory ?? defaultAttachedClientFactory;
385
345
  this.injectionClientFactory = injectionClientFactory ?? defaultAttachedClientFactory;
386
346
  }
387
- getBackend(runtime, budget) {
388
- this.reapIdleBackends();
389
- // In forced exec mode (the default runtime's injected app-server is null) a
390
- // retry budget must NOT fork a composite backend: there is no app-server to
391
- // carry the retry env, and forking one would spawn a real app-server and
392
- // defeat the forced-exec override. Fall back to the base key — codex retry is
393
- // then a no-op in exec mode (consistent with the broader codex-retry caveat).
394
- const execMode = runtime === this.defaultRuntime && this.injectedAppServerClient === null;
395
- const retryEnv = execMode ? undefined : codexRetryEnv(runtime, budget);
396
- const key = retryEnv ? `${runtime}::${hashRetryEnv(retryEnv)}` : runtime;
397
- const existing = this.backends.get(key);
398
- if (existing) {
399
- existing.lastUsedAt = this.clock();
347
+ getBackend(runtime) {
348
+ const existing = this.backends.get(runtime);
349
+ if (existing)
400
350
  return existing;
401
- }
402
- const backend = this.createBackend(runtime, retryEnv);
403
- backend.lastUsedAt = this.clock();
404
- this.backends.set(key, backend);
351
+ const backend = this.createBackend(runtime);
352
+ this.backends.set(runtime, backend);
405
353
  return backend;
406
354
  }
407
- /**
408
- * Stop + drop per-budget composite backends that have been idle longer than
409
- * the TTL and have no in-flight run. Base per-runtime backends (key === the
410
- * bare runtime id) are never reaped — they live for the process lifetime, as
411
- * before. There are no per-conversation close hooks, so this lazy sweep (run
412
- * on every `getBackend`) is how dedicated app-servers get reclaimed.
413
- */
414
- reapIdleBackends() {
415
- const now = this.clock();
416
- for (const [key, backend] of this.backends) {
417
- if (!key.includes("::")) {
418
- continue;
419
- }
420
- if ((backend.inFlight ?? 0) > 0 || backend.lastUsedAt === undefined) {
421
- continue;
422
- }
423
- if (now - backend.lastUsedAt < this.backendIdleTtlMs) {
424
- continue;
425
- }
426
- void backend.appServerClient?.stop();
427
- this.backends.delete(key);
428
- }
429
- }
430
- createBackend(runtime, retryEnv) {
355
+ createBackend(runtime) {
431
356
  const profile = getRuntimeProfile(runtime);
432
- // A composite (per-budget) backend is inherently non-default: it gets a
433
- // fresh app-server with the retry env, never the test-injected default deps.
434
- const isComposite = retryEnv !== undefined;
435
- const isDefault = runtime === this.defaultRuntime && !isComposite;
357
+ const isDefault = runtime === this.defaultRuntime;
436
358
  const commandRunner = (isDefault && this.injectedCommandRunner) ||
437
359
  new SpawnCodexCommandRunner(resolveRuntimeBinary(runtime));
438
360
  // The claude runtime is not codex-lineage: it has no app-server (its live
@@ -447,7 +369,7 @@ export class LocalAgentHost {
447
369
  ? this.injectedAppServerClient === null
448
370
  ? null
449
371
  : this.injectedAppServerClient ?? this.createAppServerClient(runtime)
450
- : this.createAppServerClient(runtime, retryEnv);
372
+ : this.createAppServerClient(runtime);
451
373
  return { runtime, profile, appServerClient, commandRunner };
452
374
  }
453
375
  /** This session's private native home, shared by its app-server + TUI. */
@@ -461,7 +383,7 @@ export class LocalAgentHost {
461
383
  this.runtimeHomes.set(runtime, prepared);
462
384
  return prepared;
463
385
  }
464
- createAppServerClient(runtime, extraEnv) {
386
+ createAppServerClient(runtime) {
465
387
  const cliPath = resolveRuntimeBinary(runtime);
466
388
  // Per-agent knobs (resolved at live-session start) win over the daemon
467
389
  // default — the app-server is private to this session's agent.
@@ -477,8 +399,8 @@ export class LocalAgentHost {
477
399
  const channel = new WsRpcChannel({
478
400
  cliPath,
479
401
  baseArgs: buildAppServerBaseArgs(sandbox),
402
+ stateDir: runtimeSessionStateDir(this.runtimeHomeSessionId),
480
403
  extraEnv: {
481
- ...(extraEnv ?? {}),
482
404
  [profile.homeEnvVar]: this.runtimeHome(runtime),
483
405
  },
484
406
  });
@@ -540,7 +462,7 @@ export class LocalAgentHost {
540
462
  const modelChanged = desiredModel !== live.appliedModel;
541
463
  const effortChanged = desiredReasoningEffort !== live.appliedReasoningEffort;
542
464
  const collaborationMode = mode && live.appliedCollaborationMode !== mode
543
- ? await resolveLiveCollaborationMode(client, live, mode)
465
+ ? await resolveLiveCollaborationMode(client, live, mode, live.runtime === "codex" ? this.runtimeHome(live.runtime) : undefined)
544
466
  : undefined;
545
467
  const settings = {
546
468
  threadId,
@@ -596,7 +518,7 @@ export class LocalAgentHost {
596
518
  throw new CodexRuntimeError("session snapshots are not live", 409, "session_not_live");
597
519
  }
598
520
  const runtime = live.runtime;
599
- const backend = this.getBackend(runtime, live.execution.budget ?? undefined);
521
+ const backend = this.getBackend(runtime);
600
522
  const client = backend.appServerClient;
601
523
  if (!client) {
602
524
  throw new CodexRuntimeError(`no app-server for runtime ${runtime}`, 500, "app_server_unavailable");
@@ -616,6 +538,10 @@ export class LocalAgentHost {
616
538
  const traexYolo = runtime === "traex" &&
617
539
  sandbox === "danger-full-access" &&
618
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;
619
545
  const configOverrides = sandbox === "workspace-write"
620
546
  ? ["sandbox_workspace_write.network_access=true"]
621
547
  : [];
@@ -633,21 +559,26 @@ export class LocalAgentHost {
633
559
  const launchEffort = live.reasoningEffort;
634
560
  if (launchEffort)
635
561
  configOverrides.push(`model_reasoning_effort=${JSON.stringify(launchEffort)}`);
636
- if (live?.instructions) {
562
+ if (runtime === "traex" && live?.instructions) {
637
563
  configOverrides.push(`developer_instructions=${JSON.stringify(live.instructions)}`);
638
564
  }
639
565
  const profile = getRuntimeProfile(runtime);
640
566
  const activeThreadId = live?.threadId ?? record?.codexSessionId;
641
567
  return {
568
+ lifecycle: "auxiliary",
642
569
  command: resolveRuntimeBinary(runtime),
643
570
  args: buildCodexRemoteArgs({
644
571
  remoteUrl,
645
572
  ...(activeThreadId ? { threadId: activeThreadId } : {}),
646
573
  configOverrides,
647
- codexArgs: traexYolo ? ["--dangerously-bypass-hook-trust"] : [],
574
+ codexArgs: bypassHookTrust ? ["--dangerously-bypass-hook-trust"] : [],
648
575
  additionalDirs: providerAdditionalDirs(live.workspace),
649
576
  }),
650
577
  cwd: live.workspace.cwd,
578
+ scrollback: 100_000,
579
+ tmuxAllowPassthrough: true,
580
+ tmuxStartOnAttach: false,
581
+ keepAliveAfterExit: false,
651
582
  ...(runtime === "traex" ? { skipTraexStartupPrompts: true } : {}),
652
583
  // Share the app-server's private CODEX_HOME so the TUI inherits the same
653
584
  // login/settings and skips the real home's update/NUX prompt. RYNX_SESSION_ID
@@ -739,11 +670,25 @@ export class LocalAgentHost {
739
670
  return false;
740
671
  }
741
672
  }
742
- // Persist the owner's skills before the app-server boots. Fork targets reuse
743
- // 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.
744
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
+ }
745
690
  try {
746
- populateCodexSkills(this.runtimeHome(runtime), liveSkills);
691
+ populateCodexSkills(runtimeHome, liveSkills);
747
692
  }
748
693
  catch (err) {
749
694
  void snapshotSkills.skillsCleanup();
@@ -765,12 +710,20 @@ export class LocalAgentHost {
765
710
  void snapshotSkills.skillsCleanup();
766
711
  return false;
767
712
  };
768
- const appServerOwner = this.getBackend(runtime, execution.budget ?? undefined).appServerClient;
713
+ reapRuntimeProcessesForStateDir(runtimeSessionStateDir(this.runtimeHomeSessionId));
714
+ const backend = this.getBackend(runtime);
715
+ const appServerOwner = backend.appServerClient;
769
716
  if (!appServerOwner) {
770
717
  this.liveStartupErrors.set(localThreadId, nativeLiveFailure(runtime, "native_app_server_unavailable", "app-server is unavailable for this Session"));
771
718
  console.error(`[codex-live] session=${localThreadId} runtime=${runtime} no app-server client`);
772
719
  return abandon();
773
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
+ }
774
727
  try {
775
728
  await appServerOwner.ensureInitialized();
776
729
  }
@@ -863,12 +816,15 @@ export class LocalAgentHost {
863
816
  appliedModel: coldResume ? "" : model,
864
817
  appliedReasoningEffort: coldResume ? undefined : reasoningEffort,
865
818
  appliedCollaborationMode: undefined,
866
- ...(execution.instructions ? { instructions: execution.instructions } : {}),
819
+ ...(runtime === "traex" && execution.instructions
820
+ ? { instructions: execution.instructions }
821
+ : {}),
867
822
  threadId: record?.codexSessionId ?? null,
868
823
  ready,
869
824
  markReady,
870
825
  startupFailed,
871
826
  markStartupFailed,
827
+ ...(loginRequired ? { startupError: loginRequired } : {}),
872
828
  terminalReady,
873
829
  markTerminalReady,
874
830
  injectLock: Promise.resolve(),
@@ -1080,7 +1036,7 @@ export class LocalAgentHost {
1080
1036
  const client = this.injectionClientFactory(live.appServerUrl);
1081
1037
  try {
1082
1038
  await client.ensureInitialized();
1083
- const collaborationMode = await resolveLiveCollaborationMode(client, live, "default");
1039
+ const collaborationMode = await resolveLiveCollaborationMode(client, live, "default", live.runtime === "codex" ? this.runtimeHome(live.runtime) : undefined);
1084
1040
  let threadId = prompt.threadId;
1085
1041
  let text = CODEX_PLAN_IMPLEMENTATION_CODING_MESSAGE;
1086
1042
  if (choice === CODEX_PLAN_IMPLEMENTATION_CLEAR_CONTEXT) {
@@ -1418,6 +1374,7 @@ export class LocalAgentHost {
1418
1374
  kind,
1419
1375
  });
1420
1376
  this.liveSessions.set(newSessionId, live);
1377
+ this.liveSessions.delete(previousSessionId);
1421
1378
  retargetMirror?.(newSessionId, {
1422
1379
  kind,
1423
1380
  workspace: structuredClone(live.workspace),
@@ -1447,6 +1404,8 @@ export class LocalAgentHost {
1447
1404
  };
1448
1405
  bindObserverClient(forwarderClient);
1449
1406
  this.liveSessions.set(localThreadId, live);
1407
+ if (loginRequired)
1408
+ this.liveStartupErrors.set(localThreadId, loginRequired);
1450
1409
  forwarder.start();
1451
1410
  // Existing bindings resume through the public app-server API. A fresh thread
1452
1411
  // is created by the native remote TUI instead: its `thread/started`
@@ -1526,12 +1485,19 @@ export class LocalAgentHost {
1526
1485
  console.error(`[codex-live] session=${localThreadId} runtime=${runtime} thread bind failed: ${err instanceof Error ? err.message : String(err)}`);
1527
1486
  return abandon();
1528
1487
  }
1529
- this.liveStartupErrors.delete(localThreadId);
1488
+ if (!loginRequired)
1489
+ this.liveStartupErrors.delete(localThreadId);
1530
1490
  return true;
1531
1491
  }
1532
1492
  /** Bind a session's codex thread id once known (TUI broadcast or store): persist
1533
1493
  * it, unblock injection, and kick off the resume-subscribe loop (once). */
1534
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
+ }
1535
1501
  const previousThreadId = live.threadId;
1536
1502
  if (previousThreadId !== threadId) {
1537
1503
  live.threadActive = false;
@@ -1843,7 +1809,8 @@ export class LocalAgentHost {
1843
1809
  claude.pendingImageInputs.delete(token);
1844
1810
  }
1845
1811
  else {
1846
- pendingInput.responseId = result.responseId;
1812
+ if (result.responseId)
1813
+ pendingInput.responseId = result.responseId;
1847
1814
  if (pendingInput.observed)
1848
1815
  forgetPendingInput();
1849
1816
  else
@@ -1854,6 +1821,8 @@ export class LocalAgentHost {
1854
1821
  const live = this.liveSessions.get(localThreadId);
1855
1822
  if (!live)
1856
1823
  return { outcome: "notLive" };
1824
+ if (live.startupError?.code === "login_required")
1825
+ return { outcome: "notReady" };
1857
1826
  const run = live.injectLock.then(async () => {
1858
1827
  if (live.rotationPending || live.stopped)
1859
1828
  return { outcome: "failed" };
@@ -1923,7 +1892,7 @@ export class LocalAgentHost {
1923
1892
  const desiredCollaborationMode = options?.collaborationMode ?? live.execution.collaborationMode;
1924
1893
  const collaborationMode = desiredCollaborationMode &&
1925
1894
  desiredCollaborationMode !== live.appliedCollaborationMode
1926
- ? await resolveLiveCollaborationMode(injectionClient, live, desiredCollaborationMode)
1895
+ ? await resolveLiveCollaborationMode(injectionClient, live, desiredCollaborationMode, live.runtime === "codex" ? this.runtimeHome(live.runtime) : undefined)
1927
1896
  : undefined;
1928
1897
  const settings = {
1929
1898
  threadId,
@@ -1978,7 +1947,8 @@ export class LocalAgentHost {
1978
1947
  catch (error) {
1979
1948
  forgetPendingInput();
1980
1949
  if (error instanceof CodexRuntimeError &&
1981
- error.code === "collaboration_mode_model_unknown") {
1950
+ (error.code === "collaboration_mode_model_unknown" ||
1951
+ error.code === "collaboration_mode_instructions_unknown")) {
1982
1952
  throw error;
1983
1953
  }
1984
1954
  // Preserve the app-server's error instead of collapsing every failure
@@ -2088,6 +2058,7 @@ export class LocalAgentHost {
2088
2058
  this.liveClaudeSessions.delete(localThreadId);
2089
2059
  claude.stopped = true;
2090
2060
  claude.forwarder.stop();
2061
+ this.trackCleanup(claude.forwarder.waitForStop());
2091
2062
  if (opts.deferClaudeInteractionCleanup) {
2092
2063
  this.pendingClaudeFinalizers.add(claude.forwarder);
2093
2064
  }
@@ -2118,9 +2089,10 @@ export class LocalAgentHost {
2118
2089
  live.canonicalInteractions.clear();
2119
2090
  live.settledInteractions.clear();
2120
2091
  live.syntheticInteractions.clear();
2121
- void live.forwarderClient.stop().catch(() => undefined);
2092
+ this.trackCleanup(live.forwarderClient.stop().catch(() => undefined));
2122
2093
  // Remove the session-scoped skills dir — the machine keeps zero task residue.
2123
- void live.skillsCleanup?.();
2094
+ if (live.skillsCleanup)
2095
+ this.trackCleanup(live.skillsCleanup());
2124
2096
  }
2125
2097
  /** Tear down one codex-lineage native runtime without deleting its durable
2126
2098
  * session-store binding. reference implementation couples its auxiliary Terminal, observer,
@@ -2131,21 +2103,26 @@ export class LocalAgentHost {
2131
2103
  if (!live)
2132
2104
  return false;
2133
2105
  const turnFailed = error ? live.forwarder.failOpenTurn(error) : false;
2134
- const backendKey = codexBackendKey(live.runtime, live.execution.budget ?? undefined);
2106
+ const backendKey = live.runtime;
2135
2107
  const backend = this.backends.get(backendKey);
2136
2108
  const appServerOwner = live.appServerOwner;
2137
2109
  this.stopLiveCodexSession(localThreadId);
2138
2110
  if (backend?.appServerClient === appServerOwner) {
2139
2111
  this.backends.delete(backendKey);
2140
2112
  }
2141
- void appServerOwner.stop().catch((stopError) => {
2113
+ this.trackCleanup(appServerOwner.stop().catch((stopError) => {
2142
2114
  console.warn(`[codex-live] session=${localThreadId} runtime=${live.runtime} app-server teardown failed: ${stopError instanceof Error ? stopError.message : String(stopError)}`);
2143
- });
2115
+ }));
2144
2116
  return turnFailed;
2145
2117
  }
2118
+ async teardownAuxiliaryTerminalRuntime(localThreadId) {
2119
+ const owner = this.liveSessions.get(localThreadId)?.appServerOwner;
2120
+ this.teardownLiveCodexSession(localThreadId);
2121
+ await owner?.stop().catch(() => undefined);
2122
+ }
2146
2123
  /** Complete the second shutdown phase after the runner has killed all native
2147
2124
  * terminals and hook subprocesses. Must run before the runner process exits. */
2148
- finalizeStoppedLiveSessions() {
2125
+ async finalizeStoppedLiveSessions() {
2149
2126
  for (const forwarder of this.pendingClaudeFinalizers)
2150
2127
  forwarder.finalizeStop();
2151
2128
  this.pendingClaudeFinalizers.clear();
@@ -2154,10 +2131,20 @@ export class LocalAgentHost {
2154
2131
  // that can only reconnect to a dead session. `stop()` sends SIGTERM before
2155
2132
  // its returned promise yields, so this remains safe in the synchronous
2156
2133
  // runner shutdown path.
2157
- for (const backend of this.backends.values()) {
2158
- void backend.appServerClient?.stop().catch(() => undefined);
2159
- }
2134
+ const backendStops = [...this.backends.values()].map((backend) => backend.appServerClient?.stop().catch(() => undefined));
2160
2135
  this.backends.clear();
2136
+ await Promise.all([
2137
+ ...backendStops,
2138
+ ...this.pendingCleanupTasks,
2139
+ ]);
2140
+ this.pendingCleanupTasks.clear();
2141
+ }
2142
+ trackCleanup(task) {
2143
+ const guarded = task.catch((error) => {
2144
+ console.warn(`[native-runtime] cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
2145
+ });
2146
+ this.pendingCleanupTasks.add(guarded);
2147
+ void guarded.then(() => this.pendingCleanupTasks.delete(guarded));
2161
2148
  }
2162
2149
  // --- claude-native live session (parallel to the codex methods above) -----
2163
2150
  /** The interactive `claude` TUI spec for a claude-native live session: the real
@@ -2200,8 +2187,8 @@ export class LocalAgentHost {
2200
2187
  ...(live.execution.reasoningEffort
2201
2188
  ? { reasoningEffort: live.execution.reasoningEffort }
2202
2189
  : {}),
2203
- ...(live.execution.instructions
2204
- ? { appendSystemPrompt: live.execution.instructions }
2190
+ ...(nativeAuthoredInstructions(live.execution)
2191
+ ? { appendSystemPrompt: nativeAuthoredInstructions(live.execution) }
2205
2192
  : {}),
2206
2193
  additionalDirs: providerAdditionalDirs(live.workspace),
2207
2194
  ...(live.forkIntent
@@ -2216,9 +2203,14 @@ export class LocalAgentHost {
2216
2203
  ...(live.launchExtraArgs.length ? { extraArgs: live.launchExtraArgs } : {}),
2217
2204
  });
2218
2205
  return {
2206
+ lifecycle: "required",
2219
2207
  command: resolveRuntimeBinary("claude"),
2220
2208
  args,
2221
2209
  cwd,
2210
+ scrollback: 50_000,
2211
+ tmuxAllowPassthrough: false,
2212
+ tmuxStartOnAttach: false,
2213
+ keepAliveAfterExit: true,
2222
2214
  // Same env the pane would inherit by default, plus the session scope for
2223
2215
  // agent-run CLIs (rynx-emulator resolves device-less commands via it).
2224
2216
  env: { ...process.env, RYNX_SESSION_ID: localThreadId },
@@ -2598,6 +2590,7 @@ export class LocalAgentHost {
2598
2590
  live.contextWarned = false;
2599
2591
  pendingRotationEvents = [];
2600
2592
  this.liveClaudeSessions.set(newSessionId, live);
2593
+ this.liveClaudeSessions.delete(previousSessionId);
2601
2594
  void this.sessionStore
2602
2595
  .set({
2603
2596
  localThreadId: newSessionId,
@@ -2703,11 +2696,10 @@ export class LocalAgentHost {
2703
2696
  if (!ok)
2704
2697
  return { outcome: "failed" };
2705
2698
  const responseId = pendingInput.responseId ?? live.currentResponseId();
2706
- if (!responseId) {
2707
- live.error = "Claude accepted the message but did not publish its native Turn identity";
2708
- return { outcome: "failed" };
2709
- }
2710
- return { outcome: steered ? "steered" : "injected", responseId };
2699
+ return {
2700
+ outcome: steered ? "steered" : "injected",
2701
+ ...(responseId ? { responseId } : {}),
2702
+ };
2711
2703
  }
2712
2704
  catch (error) {
2713
2705
  live.error = error instanceof Error ? error.message : String(error);
@@ -2860,7 +2852,7 @@ export class LocalAgentHost {
2860
2852
  }
2861
2853
  const runtime = execution.provider;
2862
2854
  const client = live?.appServerOwner ??
2863
- this.getBackend(runtime, execution.budget ?? undefined).appServerClient;
2855
+ this.getBackend(runtime).appServerClient;
2864
2856
  if (!client) {
2865
2857
  return { ok: false, reason: "unsupported" };
2866
2858
  }
@@ -2995,7 +2987,7 @@ function adoptResumedThreadSettings(live, settings) {
2995
2987
  /** Resolve the model required by a full collaboration-mode snapshot. Only a
2996
2988
  * fresh thread with no explicit/config model may use the catalog default; a
2997
2989
  * resumed thread must report its own authoritative model. */
2998
- async function resolveLiveCollaborationMode(client, live, mode) {
2990
+ async function resolveLiveCollaborationMode(client, live, mode, codexHome) {
2999
2991
  let model = live.model.trim();
3000
2992
  if (!model && live.mayUseProviderDefaultModel) {
3001
2993
  let catalog;
@@ -3014,11 +3006,21 @@ async function resolveLiveCollaborationMode(client, live, mode) {
3014
3006
  }
3015
3007
  model = defaults[0];
3016
3008
  }
3017
- 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);
3018
3020
  }
3019
3021
  /** Expand the public mode enum into the App Server's required settings
3020
3022
  * snapshot using the Session's tracked effective model. */
3021
- function buildCollaborationMode(mode, desiredModel, desiredReasoningEffort) {
3023
+ function buildCollaborationMode(mode, desiredModel, desiredReasoningEffort, developerInstructions) {
3022
3024
  const model = desiredModel.trim();
3023
3025
  if (!model) {
3024
3026
  throw new CodexRuntimeError("collaboration mode requires the current model, but the Provider did not report one", 503, "collaboration_mode_model_unknown");
@@ -3028,12 +3030,19 @@ function buildCollaborationMode(mode, desiredModel, desiredReasoningEffort) {
3028
3030
  settings: {
3029
3031
  model,
3030
3032
  reasoning_effort: desiredReasoningEffort ?? null,
3031
- // `null` asks the App Server to install the built-in instructions for
3032
- // the selected mode; thread-level Agent instructions remain separate.
3033
- developer_instructions: null,
3033
+ developer_instructions: developerInstructions,
3034
3034
  },
3035
3035
  };
3036
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
+ }
3037
3046
  function sameSessionSnapshots(workspace, execution, opts) {
3038
3047
  return JSON.stringify(stableValue({
3039
3048
  workspace,
@@ -3090,6 +3099,12 @@ function applyClaudePermissionModeSnapshot(settings, permissionMode) {
3090
3099
  permissions.defaultMode = permissionMode;
3091
3100
  else
3092
3101
  delete permissions.defaultMode;
3102
+ if (permissionMode === "bypassPermissions") {
3103
+ next.skipDangerousModePermissionPrompt = true;
3104
+ }
3105
+ else {
3106
+ delete next.skipDangerousModePermissionPrompt;
3107
+ }
3093
3108
  if (Object.keys(permissions).length > 0)
3094
3109
  next.permissions = permissions;
3095
3110
  else
package/dist/index.d.ts CHANGED
@@ -13,6 +13,6 @@ export type { InjectOutcome, InjectResult, TerminalOpenErrorCode, TerminalRole }
13
13
  export type { ResolveInteractionResult, RuntimeInteractionEvent, RuntimeInteractionListener, } from "./interactions.js";
14
14
  export { probeRuntimeStatus } from "./runtime-status.js";
15
15
  export { listRuntimeModels } from "./models-catalog.js";
16
- export { TmuxTerminal, isTmuxAvailable, terminateTmuxServer, tmuxSocketPath, } from "./terminal/tmux.js";
17
- export type { TerminalAttachment, TmuxTerminalOptions } from "./terminal/tmux.js";
16
+ export { TmuxTerminal, isTmuxAvailable, resolveTmuxBin, terminateTmuxServer, tmuxSocketPath, } from "./terminal/tmux.js";
17
+ export type { PreparedTerminalAttachment, PullTerminalAttachment, TmuxTerminalOptions, } from "./terminal/tmux.js";
18
18
  export { TerminalRegistry } from "./terminal/registry.js";
package/dist/index.js CHANGED
@@ -12,5 +12,5 @@ export { RunnerManager, TerminalOpenError } from "./runner/manager.js";
12
12
  export { probeRuntimeStatus } from "./runtime-status.js";
13
13
  export { listRuntimeModels } from "./models-catalog.js";
14
14
  // Live-terminal subsystem (Phase C): tmux-backed terminals + per-runner registry.
15
- export { TmuxTerminal, isTmuxAvailable, terminateTmuxServer, tmuxSocketPath, } from "./terminal/tmux.js";
15
+ export { TmuxTerminal, isTmuxAvailable, resolveTmuxBin, terminateTmuxServer, tmuxSocketPath, } from "./terminal/tmux.js";
16
16
  export { TerminalRegistry } from "./terminal/registry.js";