@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.
- package/dist/claude/native-bridge.d.ts +16 -1
- package/dist/claude/native-bridge.js +91 -17
- package/dist/claude/native-hook-main.js +1 -0
- package/dist/claude/native-integration.d.ts +10 -0
- package/dist/claude/native-integration.js +100 -6
- package/dist/codex-app-server/forwarder.js +23 -1
- package/dist/codex-app-server/mapping.js +34 -10
- package/dist/codex-app-server/process-registry.d.ts +36 -0
- package/dist/codex-app-server/process-registry.js +320 -0
- package/dist/codex-app-server/ws-channel.d.ts +7 -0
- package/dist/codex-app-server/ws-channel.js +85 -9
- package/dist/codex-home.d.ts +35 -3
- package/dist/codex-home.js +321 -14
- package/dist/host.d.ts +11 -25
- package/dist/host.js +147 -132
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/runner/child.d.ts +25 -3
- package/dist/runner/child.js +359 -64
- package/dist/runner/manager.d.ts +33 -18
- package/dist/runner/manager.js +229 -49
- package/dist/runner/protocol.d.ts +79 -7
- package/dist/runner/transport.d.ts +9 -2
- package/dist/runner/transport.js +55 -3
- package/dist/runner-main.js +8 -3
- package/dist/terminal/codex-tui.d.ts +4 -0
- package/dist/terminal/codex-tui.js +5 -0
- package/dist/terminal/control-parser.d.ts +39 -0
- package/dist/terminal/control-parser.js +172 -0
- package/dist/terminal/registry.d.ts +18 -15
- package/dist/terminal/registry.js +43 -23
- package/dist/terminal/spool.d.ts +47 -0
- package/dist/terminal/spool.js +231 -0
- package/dist/terminal/tmux.d.ts +93 -84
- package/dist/terminal/tmux.js +675 -200
- 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
|
-
//
|
|
339
|
-
//
|
|
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,
|
|
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
|
|
388
|
-
this.
|
|
389
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
743
|
-
// the owner's immutable
|
|
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(
|
|
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
|
-
|
|
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
|
-
...(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2092
|
+
this.trackCleanup(live.forwarderClient.stop().catch(() => undefined));
|
|
2122
2093
|
// Remove the session-scoped skills dir — the machine keeps zero task residue.
|
|
2123
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2204
|
-
? { appendSystemPrompt: live.execution
|
|
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
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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";
|