@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/claude/native-hook-main.js +1 -0
- package/dist/codex-app-server/forwarder.js +7 -1
- package/dist/codex-app-server/mapping.js +34 -10
- package/dist/codex-home.d.ts +35 -3
- package/dist/codex-home.js +321 -14
- package/dist/host.d.ts +2 -24
- package/dist/host.js +99 -118
- package/dist/runner/child.d.ts +1 -0
- package/dist/runner/child.js +33 -7
- package/dist/terminal/registry.d.ts +3 -2
- package/dist/terminal/registry.js +6 -4
- package/dist/terminal/tmux.js +14 -6
- package/package.json +2 -2
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
|
-
//
|
|
341
|
-
//
|
|
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,
|
|
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
|
|
391
|
-
this.
|
|
392
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
752
|
-
// the owner's immutable
|
|
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(
|
|
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
|
|
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
|
-
...(
|
|
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
|
-
|
|
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 =
|
|
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
|
|
2233
|
-
? { appendSystemPrompt: live.execution
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/dist/runner/child.d.ts
CHANGED
|
@@ -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;
|
package/dist/runner/child.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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() {
|