@rynx-ai/runtime 0.1.11-beta.23 → 0.1.11-beta.25
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/executor.d.ts +19 -5
- package/dist/claude/executor.js +56 -12
- package/dist/claude/native-hook-main.js +62 -0
- package/dist/claude/native-integration.js +1 -1
- package/dist/codex-app-server/forwarder.d.ts +16 -1
- package/dist/codex-app-server/forwarder.js +61 -17
- package/dist/codex-app-server/mapping.js +16 -1
- package/dist/codex-app-server/protocol.d.ts +25 -1
- package/dist/host.d.ts +15 -7
- package/dist/host.js +535 -44
- package/dist/runner/child.d.ts +7 -4
- package/dist/runner/child.js +29 -3
- package/dist/runner/manager.d.ts +10 -3
- package/dist/runner/manager.js +66 -7
- package/dist/runner/protocol.d.ts +16 -1
- package/dist/terminal/registry.js +1 -1
- package/dist/terminal/tmux.d.ts +5 -5
- package/dist/terminal/tmux.js +6 -6
- package/package.json +2 -2
package/dist/host.js
CHANGED
|
@@ -9,8 +9,9 @@ import { getRuntimeProfile, } from "@rynx-ai/core";
|
|
|
9
9
|
import { resolveRuntimeBinary, } from "@rynx-ai/core";
|
|
10
10
|
import { createCodexChildEnv } from "./codex-child-env.js";
|
|
11
11
|
import { prepareRuntimeHome, populateCodexSkills, runtimeHomePath, } from "./codex-home.js";
|
|
12
|
-
import {
|
|
12
|
+
import { materializeClaudePlugin, reuseClaudePlugin, } from "./claude/executor.js";
|
|
13
13
|
import { listClaudeModels } from "./claude/models.js";
|
|
14
|
+
import { validateInteractionResolution } from "./interactions.js";
|
|
14
15
|
import { CodexAppServerClient, buildRuntimeUserInput, } from "./codex-app-server/client.js";
|
|
15
16
|
import { buildAppServerBaseArgs, CodexTransportError, } from "./codex-app-server/transport.js";
|
|
16
17
|
import { WsRpcChannel, ExternalWsChannel } from "./codex-app-server/ws-channel.js";
|
|
@@ -192,6 +193,85 @@ function providerPluginSkillName(namespace, name) {
|
|
|
192
193
|
return `${namespace.slice(0, 48)}-${name.slice(0, 65)}-${digest}`;
|
|
193
194
|
}
|
|
194
195
|
const MAX_CODEX_SETTLED_INTERACTIONS = 512;
|
|
196
|
+
const CODEX_PLAN_IMPLEMENTATION_YES = "Yes, implement this plan";
|
|
197
|
+
const CODEX_PLAN_IMPLEMENTATION_CLEAR_CONTEXT = "Yes, clear context and implement";
|
|
198
|
+
const CODEX_PLAN_IMPLEMENTATION_NO = "No, stay in Plan mode";
|
|
199
|
+
const CODEX_PLAN_IMPLEMENTATION_CODING_MESSAGE = "Implement the plan.";
|
|
200
|
+
const CODEX_PLAN_PROMPT_DISMISS_TIMEOUT_MS = 1_500;
|
|
201
|
+
const CODEX_PLAN_PROMPT_DISMISS_POLL_MS = 50;
|
|
202
|
+
const CODEX_PLAN_IMPLEMENTATION_CLEAR_CONTEXT_PREFIX = "A previous agent produced the plan below to accomplish the user's task. " +
|
|
203
|
+
"Implement the plan in a fresh context. Treat the plan as the source of " +
|
|
204
|
+
"user intent, re-read files as needed, and carry the work through " +
|
|
205
|
+
"implementation and verification.";
|
|
206
|
+
function codexPlanImplementationRequest(interactionId) {
|
|
207
|
+
return {
|
|
208
|
+
interactionId,
|
|
209
|
+
kind: "question",
|
|
210
|
+
title: "Plan",
|
|
211
|
+
fields: [{
|
|
212
|
+
id: "plan_implementation",
|
|
213
|
+
type: "select",
|
|
214
|
+
label: "Implement this plan?",
|
|
215
|
+
required: true,
|
|
216
|
+
options: [
|
|
217
|
+
{
|
|
218
|
+
value: CODEX_PLAN_IMPLEMENTATION_YES,
|
|
219
|
+
label: CODEX_PLAN_IMPLEMENTATION_YES,
|
|
220
|
+
description: "Switch to Default and start coding.",
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
value: CODEX_PLAN_IMPLEMENTATION_CLEAR_CONTEXT,
|
|
224
|
+
label: CODEX_PLAN_IMPLEMENTATION_CLEAR_CONTEXT,
|
|
225
|
+
description: "Fresh thread with this plan.",
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
value: CODEX_PLAN_IMPLEMENTATION_NO,
|
|
229
|
+
label: CODEX_PLAN_IMPLEMENTATION_NO,
|
|
230
|
+
description: "Continue planning with the model.",
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
}],
|
|
234
|
+
actions: [{ id: "submit", label: "Submit", style: "primary", requiresAnswers: true }],
|
|
235
|
+
createdAt: Date.now(),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function codexPlanImplementationPromptVisible(pane) {
|
|
239
|
+
return pane.includes("Implement this plan?") &&
|
|
240
|
+
pane.includes(CODEX_PLAN_IMPLEMENTATION_YES) &&
|
|
241
|
+
pane.includes(CODEX_PLAN_IMPLEMENTATION_NO);
|
|
242
|
+
}
|
|
243
|
+
/** Codex's final Plan picker is owned by the TUI and is not an app-server
|
|
244
|
+
* request that Web can resolve. Once the synthetic Web form takes ownership,
|
|
245
|
+
* close only that exact picker with its documented Escape action. The owner
|
|
246
|
+
* predicate is checked both before and after capture: app-server notifications
|
|
247
|
+
* cannot interleave between the two synchronous tmux calls, so a newly observed
|
|
248
|
+
* Turn cannot receive a blind Escape. This is deliberately best-effort — a
|
|
249
|
+
* cosmetic Terminal redraw failure must not make an otherwise valid Web Plan
|
|
250
|
+
* decision fail when the structured app-server path can still proceed. */
|
|
251
|
+
async function dismissCodexPlanImplementationPrompt(injector, ownsPrompt) {
|
|
252
|
+
if (!injector || !ownsPrompt())
|
|
253
|
+
return "not_visible";
|
|
254
|
+
try {
|
|
255
|
+
const pane = injector.capturePane();
|
|
256
|
+
if (!ownsPrompt())
|
|
257
|
+
return "superseded";
|
|
258
|
+
if (!codexPlanImplementationPromptVisible(pane))
|
|
259
|
+
return "not_visible";
|
|
260
|
+
injector.interrupt();
|
|
261
|
+
const deadline = Date.now() + CODEX_PLAN_PROMPT_DISMISS_TIMEOUT_MS;
|
|
262
|
+
while (ownsPrompt()) {
|
|
263
|
+
if (!codexPlanImplementationPromptVisible(injector.capturePane()))
|
|
264
|
+
return "dismissed";
|
|
265
|
+
if (Date.now() >= deadline)
|
|
266
|
+
return "failed";
|
|
267
|
+
await new Promise((resolve) => setTimeout(resolve, CODEX_PLAN_PROMPT_DISMISS_POLL_MS));
|
|
268
|
+
}
|
|
269
|
+
return "superseded";
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
return "failed";
|
|
273
|
+
}
|
|
274
|
+
}
|
|
195
275
|
/**
|
|
196
276
|
* The `OPENAI_*` retry env for a codex-lineage app-server spawn, or `undefined`
|
|
197
277
|
* when the budget declares no retry or the runtime is claude. claude applies
|
|
@@ -247,7 +327,7 @@ export class LocalAgentHost {
|
|
|
247
327
|
// multi-connection model). Defaults to an ExternalWsChannel client attached to
|
|
248
328
|
// the backend's app-server; tests inject a fake.
|
|
249
329
|
forwarderClientFactory;
|
|
250
|
-
// Builds
|
|
330
|
+
// Builds reference implementation's short-lived cold-resume preload connection. This must be
|
|
251
331
|
// distinct from both the app-server owner/injector and the background
|
|
252
332
|
// observer: the owner's initialize handshake is the readiness probe, then a
|
|
253
333
|
// freshly initialized connection performs the one resume and closes.
|
|
@@ -418,8 +498,51 @@ export class LocalAgentHost {
|
|
|
418
498
|
if (live.settledInteractions.has(interactionId)) {
|
|
419
499
|
return { disposition: "already_resolved" };
|
|
420
500
|
}
|
|
501
|
+
const synthetic = live.syntheticInteractions.get(interactionId);
|
|
502
|
+
if (synthetic)
|
|
503
|
+
return synthetic.resolve(resolution);
|
|
421
504
|
return live.forwarderClient.resolveInteraction(interactionId, resolution);
|
|
422
505
|
}
|
|
506
|
+
/** Apply collaboration mode to an already-loaded Codex-lineage thread. The
|
|
507
|
+
* caller persists only after this native RPC succeeds. */
|
|
508
|
+
async updateCollaborationMode(localThreadId, mode) {
|
|
509
|
+
const live = this.liveSessions.get(localThreadId);
|
|
510
|
+
if (!live || live.stopped || live.rotationPending) {
|
|
511
|
+
throw new CodexRuntimeError("Native Session is not live", 409, "native_session_not_live");
|
|
512
|
+
}
|
|
513
|
+
const run = live.injectLock.then(async () => {
|
|
514
|
+
const bound = await this.waitLiveReady(localThreadId, CODEX_BRIDGE_READY_TIMEOUT_MS);
|
|
515
|
+
const threadId = live.threadId ?? live.forwarder.threadId();
|
|
516
|
+
if (!bound || !threadId) {
|
|
517
|
+
throw new CodexRuntimeError("Native thread is not ready", 503, "native_thread_not_ready");
|
|
518
|
+
}
|
|
519
|
+
await this.applyLiveCollaborationMode(live, threadId, mode);
|
|
520
|
+
});
|
|
521
|
+
live.injectLock = run.then(() => undefined, () => undefined);
|
|
522
|
+
await run;
|
|
523
|
+
}
|
|
524
|
+
/** Apply one explicit collaboration mode to the current native thread and
|
|
525
|
+
* remember only a successful RPC. A supplied client is already initialized
|
|
526
|
+
* and remains owned by its caller (used by cold-resume preload). */
|
|
527
|
+
async applyLiveCollaborationMode(live, threadId, mode, suppliedClient) {
|
|
528
|
+
if (live.appliedCollaborationMode === mode)
|
|
529
|
+
return;
|
|
530
|
+
const client = suppliedClient ?? this.injectionClientFactory(live.appServerUrl);
|
|
531
|
+
try {
|
|
532
|
+
if (!suppliedClient)
|
|
533
|
+
await client.ensureInitialized();
|
|
534
|
+
await client.threadSettingsUpdate({
|
|
535
|
+
threadId,
|
|
536
|
+
collaborationMode: await buildCollaborationMode(client, mode, live.model, live.reasoningEffort),
|
|
537
|
+
});
|
|
538
|
+
live.appliedCollaborationMode = mode;
|
|
539
|
+
live.execution = { ...live.execution, collaborationMode: mode };
|
|
540
|
+
}
|
|
541
|
+
finally {
|
|
542
|
+
if (!suppliedClient)
|
|
543
|
+
await client.stop().catch(() => undefined);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
423
546
|
/**
|
|
424
547
|
* The command to run in a session's live terminal so it co-drives the codex
|
|
425
548
|
* app-server thread (Phase D). Returns `null` when live-terminal is off, the
|
|
@@ -529,16 +652,21 @@ export class LocalAgentHost {
|
|
|
529
652
|
return sameSessionSnapshots(existingClaude.workspace, existingClaude.execution, snapshotOpts);
|
|
530
653
|
}
|
|
531
654
|
// Dedupe concurrent triggers so only one forwarder is ever created per
|
|
532
|
-
// session (two would double-mirror every turn).
|
|
533
|
-
//
|
|
534
|
-
//
|
|
535
|
-
// the thread id instead of launching a bare `codex --remote` pane.
|
|
655
|
+
// session (two would double-mirror every turn). startLiveCodexSession
|
|
656
|
+
// publishes its partially initialized session before thread/start completes,
|
|
657
|
+
// so wait for that startup and then adopt any newer mutable Turn settings.
|
|
536
658
|
const inflight = this.liveEnsuring.get(localThreadId);
|
|
537
|
-
if (inflight)
|
|
538
|
-
|
|
659
|
+
if (inflight) {
|
|
660
|
+
if (!await inflight)
|
|
661
|
+
return false;
|
|
662
|
+
const startedLive = this.liveSessions.get(localThreadId);
|
|
663
|
+
return startedLive
|
|
664
|
+
? updateLiveCodexTurnSettings(startedLive, snapshotOpts)
|
|
665
|
+
: false;
|
|
666
|
+
}
|
|
539
667
|
const live = this.liveSessions.get(localThreadId);
|
|
540
668
|
if (live)
|
|
541
|
-
return
|
|
669
|
+
return updateLiveCodexTurnSettings(live, snapshotOpts);
|
|
542
670
|
const started = this.startLiveCodexSession(localThreadId, emit, snapshotOpts);
|
|
543
671
|
this.liveEnsuring.set(localThreadId, started);
|
|
544
672
|
try {
|
|
@@ -629,7 +757,7 @@ export class LocalAgentHost {
|
|
|
629
757
|
// notifications (the backend client injects; this one only observes).
|
|
630
758
|
// Fresh sessions must connect this listener before the TUI launches so its
|
|
631
759
|
// one-shot `thread/started` cannot race discovery. A known-thread cold resume
|
|
632
|
-
// already has its durable id: like
|
|
760
|
+
// already has its durable id: like reference implementation's `_codex_forward_known_thread`,
|
|
633
761
|
// its observer starts in the background only after the replacement TUI has
|
|
634
762
|
// launched and is never part of the resume admission boundary.
|
|
635
763
|
const forwarderClient = this.forwarderClientFactory(appServerUrl);
|
|
@@ -670,6 +798,9 @@ export class LocalAgentHost {
|
|
|
670
798
|
approvalPolicy,
|
|
671
799
|
model,
|
|
672
800
|
reasoningEffort,
|
|
801
|
+
appliedModel: model,
|
|
802
|
+
appliedReasoningEffort: reasoningEffort,
|
|
803
|
+
appliedCollaborationMode: undefined,
|
|
673
804
|
...(execution.instructions ? { instructions: execution.instructions } : {}),
|
|
674
805
|
threadId: record?.codexSessionId ?? null,
|
|
675
806
|
ready,
|
|
@@ -681,6 +812,8 @@ export class LocalAgentHost {
|
|
|
681
812
|
injectLock: Promise.resolve(),
|
|
682
813
|
pendingInjectedInputs: [],
|
|
683
814
|
publishInjectedInput: () => undefined,
|
|
815
|
+
publishTurnAdmission: () => undefined,
|
|
816
|
+
publishTurnAdmissionFailure: () => undefined,
|
|
684
817
|
publishInterrupted: () => undefined,
|
|
685
818
|
interruptedResponseId: null,
|
|
686
819
|
subscribing: false,
|
|
@@ -690,6 +823,8 @@ export class LocalAgentHost {
|
|
|
690
823
|
skillsCleanup: snapshotSkills.skillsCleanup,
|
|
691
824
|
canonicalInteractions: new Set(),
|
|
692
825
|
settledInteractions: new Set(),
|
|
826
|
+
syntheticInteractions: new Map(),
|
|
827
|
+
cancelSyntheticInteractions: () => undefined,
|
|
693
828
|
};
|
|
694
829
|
let currentSessionId = localThreadId;
|
|
695
830
|
let pendingRotationEvents = null;
|
|
@@ -737,6 +872,21 @@ export class LocalAgentHost {
|
|
|
737
872
|
for (const se of n.userInput(content))
|
|
738
873
|
emitCurrent(se);
|
|
739
874
|
};
|
|
875
|
+
live.publishTurnAdmission = () => {
|
|
876
|
+
emitCurrent({
|
|
877
|
+
type: "session.status",
|
|
878
|
+
sessionId: currentSessionId,
|
|
879
|
+
status: "running",
|
|
880
|
+
});
|
|
881
|
+
};
|
|
882
|
+
live.publishTurnAdmissionFailure = (message) => {
|
|
883
|
+
emitCurrent({
|
|
884
|
+
type: "session.status",
|
|
885
|
+
sessionId: currentSessionId,
|
|
886
|
+
status: "failed",
|
|
887
|
+
note: message,
|
|
888
|
+
});
|
|
889
|
+
};
|
|
740
890
|
const clearPendingInputsForResponse = (responseId) => {
|
|
741
891
|
if (!responseId)
|
|
742
892
|
return;
|
|
@@ -750,11 +900,168 @@ export class LocalAgentHost {
|
|
|
750
900
|
if (oldest)
|
|
751
901
|
live.settledInteractions.delete(oldest);
|
|
752
902
|
};
|
|
903
|
+
const cancelSyntheticInteractions = (reason) => {
|
|
904
|
+
for (const interaction of live.syntheticInteractions.values())
|
|
905
|
+
interaction.cancel(reason);
|
|
906
|
+
};
|
|
907
|
+
live.cancelSyntheticInteractions = cancelSyntheticInteractions;
|
|
908
|
+
const publishPlanImplementationPrompt = (prompt) => {
|
|
909
|
+
const suffix = createHash("sha256")
|
|
910
|
+
.update(`${prompt.threadId}\0${prompt.turnId}`)
|
|
911
|
+
.digest("hex")
|
|
912
|
+
.slice(0, 24);
|
|
913
|
+
const interactionId = `codex_plan_implementation_${suffix}`;
|
|
914
|
+
if (live.syntheticInteractions.has(interactionId) ||
|
|
915
|
+
live.settledInteractions.has(interactionId))
|
|
916
|
+
return;
|
|
917
|
+
const responseId = `resp_codex_plan_approval_${suffix}`;
|
|
918
|
+
const request = codexPlanImplementationRequest(interactionId);
|
|
919
|
+
const interactionNormalizer = new SessionNormalizer({
|
|
920
|
+
sessionId: currentSessionId,
|
|
921
|
+
responseId,
|
|
922
|
+
model: live.model || live.runtime,
|
|
923
|
+
});
|
|
924
|
+
let submitted = false;
|
|
925
|
+
const finish = (kind, resolution, reason) => {
|
|
926
|
+
if (!live.syntheticInteractions.delete(interactionId))
|
|
927
|
+
return;
|
|
928
|
+
rememberSettledInteraction(interactionId);
|
|
929
|
+
const event = kind === "resolved"
|
|
930
|
+
? {
|
|
931
|
+
type: "interaction_resolved",
|
|
932
|
+
interactionId,
|
|
933
|
+
resolution: resolution,
|
|
934
|
+
}
|
|
935
|
+
: {
|
|
936
|
+
type: "interaction_cancelled",
|
|
937
|
+
interactionId,
|
|
938
|
+
...(reason ? { reason } : {}),
|
|
939
|
+
};
|
|
940
|
+
// This interaction belongs to the Session that produced the Plan. A
|
|
941
|
+
// clear-context choice can rotate the native thread before turn/start
|
|
942
|
+
// returns, so never retarget its resolution to the new Session.
|
|
943
|
+
for (const sessionEvent of interactionNormalizer.next(event))
|
|
944
|
+
emit(sessionEvent);
|
|
945
|
+
for (const sessionEvent of interactionNormalizer.next({ type: "done" })) {
|
|
946
|
+
emit(sessionEvent);
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
live.syntheticInteractions.set(interactionId, {
|
|
950
|
+
cancel: (reason) => {
|
|
951
|
+
// The selected implementation itself starts a native Turn and the
|
|
952
|
+
// clear-context variant also starts a new thread. Their observer
|
|
953
|
+
// notifications can beat the short-lived RPC response; they are not
|
|
954
|
+
// competing user actions and must not cancel the card being resolved.
|
|
955
|
+
if (submitted &&
|
|
956
|
+
(reason === "superseded_by_native_turn" || reason === "native_thread_rotated"))
|
|
957
|
+
return;
|
|
958
|
+
finish("cancelled", undefined, reason);
|
|
959
|
+
},
|
|
960
|
+
resolve: async (resolution) => {
|
|
961
|
+
if (submitted)
|
|
962
|
+
return { disposition: "already_resolved" };
|
|
963
|
+
const invalid = validateInteractionResolution(request, resolution);
|
|
964
|
+
if (invalid)
|
|
965
|
+
return { disposition: "invalid", message: invalid };
|
|
966
|
+
const choice = resolution.answers?.plan_implementation;
|
|
967
|
+
if (typeof choice !== "string") {
|
|
968
|
+
return { disposition: "invalid", message: "plan implementation choice is required" };
|
|
969
|
+
}
|
|
970
|
+
submitted = true;
|
|
971
|
+
const run = live.injectLock.then(async () => {
|
|
972
|
+
if (live.stopped || live.rotationPending) {
|
|
973
|
+
throw new CodexRuntimeError("Native Session is not available for plan implementation", 409, "native_session_not_live");
|
|
974
|
+
}
|
|
975
|
+
const promptDismissal = await dismissCodexPlanImplementationPrompt(live.injector, () => !live.stopped &&
|
|
976
|
+
!live.rotationPending &&
|
|
977
|
+
live.syntheticInteractions.has(interactionId) &&
|
|
978
|
+
live.forwarder.threadId() === prompt.threadId &&
|
|
979
|
+
!live.forwarder.isTurnOpen());
|
|
980
|
+
if (promptDismissal === "failed") {
|
|
981
|
+
console.warn(`[codex-live] session=${currentSessionId} native Plan picker remained visible after the Web decision; continuing through the app-server`);
|
|
982
|
+
}
|
|
983
|
+
if (choice === CODEX_PLAN_IMPLEMENTATION_NO) {
|
|
984
|
+
finish("resolved", resolution);
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
const client = this.injectionClientFactory(live.appServerUrl);
|
|
988
|
+
try {
|
|
989
|
+
await client.ensureInitialized();
|
|
990
|
+
const collaborationMode = await buildCollaborationMode(client, "default", live.model, live.reasoningEffort);
|
|
991
|
+
let threadId = prompt.threadId;
|
|
992
|
+
let text = CODEX_PLAN_IMPLEMENTATION_CODING_MESSAGE;
|
|
993
|
+
if (choice === CODEX_PLAN_IMPLEMENTATION_CLEAR_CONTEXT) {
|
|
994
|
+
live.nextThreadCollaborationMode = "default";
|
|
995
|
+
let startedThread;
|
|
996
|
+
try {
|
|
997
|
+
startedThread = await client.threadStart({
|
|
998
|
+
...(live.model ? { model: live.model } : {}),
|
|
999
|
+
...threadWorkspaceParams(live.runtime, live.workspace, live.sandbox),
|
|
1000
|
+
approvalPolicy: live.approvalPolicy,
|
|
1001
|
+
...(live.instructions ? { developerInstructions: live.instructions } : {}),
|
|
1002
|
+
sessionStartSource: "clear",
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
catch (error) {
|
|
1006
|
+
live.nextThreadCollaborationMode = undefined;
|
|
1007
|
+
throw error;
|
|
1008
|
+
}
|
|
1009
|
+
threadId = startedThread.threadId;
|
|
1010
|
+
text = `${CODEX_PLAN_IMPLEMENTATION_CLEAR_CONTEXT_PREFIX}\n\n${prompt.text}`;
|
|
1011
|
+
}
|
|
1012
|
+
else if (choice !== CODEX_PLAN_IMPLEMENTATION_YES) {
|
|
1013
|
+
throw new CodexRuntimeError("Unknown plan implementation choice", 400, "invalid_interaction_resolution");
|
|
1014
|
+
}
|
|
1015
|
+
const started = await client.turnStart({
|
|
1016
|
+
threadId,
|
|
1017
|
+
input: [{ type: "text", text }],
|
|
1018
|
+
...turnWorkspaceParams(live.runtime, live.workspace, live.sandbox),
|
|
1019
|
+
approvalPolicy: live.approvalPolicy,
|
|
1020
|
+
collaborationMode,
|
|
1021
|
+
});
|
|
1022
|
+
live.execution = { ...live.execution, collaborationMode: "default" };
|
|
1023
|
+
live.appliedCollaborationMode = "default";
|
|
1024
|
+
emitCurrent({
|
|
1025
|
+
type: "session.collaboration_mode",
|
|
1026
|
+
sessionId: currentSessionId,
|
|
1027
|
+
mode: "default",
|
|
1028
|
+
});
|
|
1029
|
+
live.forwarder.noteTurnAccepted(started.turnId);
|
|
1030
|
+
finish("resolved", resolution);
|
|
1031
|
+
}
|
|
1032
|
+
finally {
|
|
1033
|
+
await client.stop().catch(() => undefined);
|
|
1034
|
+
}
|
|
1035
|
+
});
|
|
1036
|
+
live.injectLock = run.then(() => undefined, () => undefined);
|
|
1037
|
+
try {
|
|
1038
|
+
await run;
|
|
1039
|
+
return { disposition: "applied" };
|
|
1040
|
+
}
|
|
1041
|
+
catch (error) {
|
|
1042
|
+
submitted = false;
|
|
1043
|
+
return {
|
|
1044
|
+
disposition: "invalid",
|
|
1045
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
},
|
|
1049
|
+
});
|
|
1050
|
+
for (const sessionEvent of interactionNormalizer.next({
|
|
1051
|
+
type: "interaction_requested",
|
|
1052
|
+
interaction: request,
|
|
1053
|
+
}))
|
|
1054
|
+
emit(sessionEvent);
|
|
1055
|
+
};
|
|
753
1056
|
const forwardInteraction = (event) => {
|
|
754
1057
|
const interactionId = event.type === "requested"
|
|
755
1058
|
? event.request.interactionId
|
|
756
1059
|
: event.interactionId;
|
|
757
1060
|
if (event.type === "requested") {
|
|
1061
|
+
if (event.request.fields.some((field) => field.id === "plan_implementation")) {
|
|
1062
|
+
live.forwarder.noteNativePlanImplementationPrompt(event.turnId);
|
|
1063
|
+
cancelSyntheticInteractions("superseded_by_native_plan_prompt");
|
|
1064
|
+
}
|
|
758
1065
|
if (live.canonicalInteractions.has(interactionId) ||
|
|
759
1066
|
live.settledInteractions.has(interactionId))
|
|
760
1067
|
return;
|
|
@@ -872,6 +1179,7 @@ export class LocalAgentHost {
|
|
|
872
1179
|
const sink = {
|
|
873
1180
|
onTurnStart: (turnId) => startNormalizer(turnId),
|
|
874
1181
|
onTurnObserved: (turnId) => {
|
|
1182
|
+
cancelSyntheticInteractions("superseded_by_native_turn");
|
|
875
1183
|
const n = startNormalizer(turnId);
|
|
876
1184
|
for (const event of n.next({ type: "turn_started", ...(turnId ? { turnId } : {}) })) {
|
|
877
1185
|
emitCurrent(event);
|
|
@@ -937,6 +1245,16 @@ export class LocalAgentHost {
|
|
|
937
1245
|
shouldIgnoreThreadStarted: (threadId, forkedFromId) => this.shouldIgnoreManagedForkThreadStarted(live, threadId, forkedFromId),
|
|
938
1246
|
onThreadStarted: (threadId, forkedFromId) => this.onLiveThreadStarted(live, localThreadId, threadId, forkedFromId),
|
|
939
1247
|
onThreadActive: () => live.releaseActive?.(),
|
|
1248
|
+
onCollaborationModeChanged: (mode) => {
|
|
1249
|
+
live.appliedCollaborationMode = mode;
|
|
1250
|
+
live.execution = { ...live.execution, collaborationMode: mode };
|
|
1251
|
+
emitCurrent({
|
|
1252
|
+
type: "session.collaboration_mode",
|
|
1253
|
+
sessionId: currentSessionId,
|
|
1254
|
+
mode,
|
|
1255
|
+
});
|
|
1256
|
+
},
|
|
1257
|
+
onPlanImplementationPrompt: publishPlanImplementationPrompt,
|
|
940
1258
|
};
|
|
941
1259
|
const forwarder = new CodexSessionForwarder(forwarderClient, sink, {
|
|
942
1260
|
// Traex 0.200 can publish a final reasoning item immediately after
|
|
@@ -955,8 +1273,15 @@ export class LocalAgentHost {
|
|
|
955
1273
|
live.stopped = true;
|
|
956
1274
|
return;
|
|
957
1275
|
}
|
|
1276
|
+
cancelSyntheticInteractions("native_thread_rotated");
|
|
958
1277
|
const previousSessionId = currentSessionId;
|
|
959
1278
|
const newSessionId = makeSessionId();
|
|
1279
|
+
const rotationMode = live.nextThreadCollaborationMode;
|
|
1280
|
+
live.nextThreadCollaborationMode = undefined;
|
|
1281
|
+
live.appliedCollaborationMode = undefined;
|
|
1282
|
+
if (rotationMode) {
|
|
1283
|
+
live.execution = { ...live.execution, collaborationMode: rotationMode };
|
|
1284
|
+
}
|
|
960
1285
|
live.rotationPending = true;
|
|
961
1286
|
currentSessionId = newSessionId;
|
|
962
1287
|
normalizer = null;
|
|
@@ -993,6 +1318,14 @@ export class LocalAgentHost {
|
|
|
993
1318
|
live.rotationPending = false;
|
|
994
1319
|
for (const event of queued)
|
|
995
1320
|
emit(event);
|
|
1321
|
+
const desiredMode = live.execution.collaborationMode;
|
|
1322
|
+
if (desiredMode) {
|
|
1323
|
+
const restore = live.injectLock.then(() => this.applyLiveCollaborationMode(live, threadId, desiredMode));
|
|
1324
|
+
live.injectLock = restore.then(() => undefined, () => undefined);
|
|
1325
|
+
void restore.catch((error) => {
|
|
1326
|
+
this.failObserverLifecycle(newSessionId, live, nativeLiveFailure(live.runtime, "native_collaboration_mode_restore_failed", "rotated native thread could not restore the Session collaboration mode", error));
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
996
1329
|
void this.subscribeUntilReady(newSessionId, live, threadId);
|
|
997
1330
|
})
|
|
998
1331
|
.catch(() => {
|
|
@@ -1011,7 +1344,7 @@ export class LocalAgentHost {
|
|
|
1011
1344
|
// turn-less app-server thread resumable by the TUI.
|
|
1012
1345
|
try {
|
|
1013
1346
|
if (record?.codexSessionId) {
|
|
1014
|
-
// Match
|
|
1347
|
+
// Match reference implementation's cold-resume preload exactly: one history-free
|
|
1015
1348
|
// `thread/resume` on a short-lived, separately initialized connection.
|
|
1016
1349
|
// The backend owner's handshake above is the app-server readiness
|
|
1017
1350
|
// probe; reusing that first connection here races Codex's startup state
|
|
@@ -1028,6 +1361,9 @@ export class LocalAgentHost {
|
|
|
1028
1361
|
approvalPolicy,
|
|
1029
1362
|
excludeTurns: true,
|
|
1030
1363
|
});
|
|
1364
|
+
if (execution.collaborationMode) {
|
|
1365
|
+
await this.applyLiveCollaborationMode(live, resumed.threadId, execution.collaborationMode, preloadClient);
|
|
1366
|
+
}
|
|
1031
1367
|
}
|
|
1032
1368
|
finally {
|
|
1033
1369
|
await preloadClient.stop().catch((error) => {
|
|
@@ -1114,16 +1450,38 @@ export class LocalAgentHost {
|
|
|
1114
1450
|
});
|
|
1115
1451
|
})
|
|
1116
1452
|
.catch(() => undefined);
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1453
|
+
const finishBinding = () => {
|
|
1454
|
+
if (live.stopped)
|
|
1455
|
+
return;
|
|
1456
|
+
live.markReady(); // thread id + explicit Session mode are ready for turns
|
|
1457
|
+
if (live.observerAvailable) {
|
|
1458
|
+
void this.subscribeUntilReady(localThreadId, live, threadId).then(live.markTerminalReady);
|
|
1459
|
+
}
|
|
1460
|
+
else {
|
|
1461
|
+
// Known-thread cold resume has already completed the dedicated preload. Its
|
|
1462
|
+
// observer is deliberately post-TUI background work, not Terminal
|
|
1463
|
+
// readiness. RunnerSession calls startLiveCodexObserver after launch.
|
|
1464
|
+
live.markTerminalReady(true);
|
|
1465
|
+
}
|
|
1466
|
+
};
|
|
1467
|
+
const desiredMode = live.execution.collaborationMode;
|
|
1468
|
+
if (!desiredMode || live.appliedCollaborationMode === desiredMode) {
|
|
1469
|
+
finishBinding();
|
|
1470
|
+
return;
|
|
1126
1471
|
}
|
|
1472
|
+
// A fresh thread does not exist until the TUI broadcasts thread/started.
|
|
1473
|
+
// Apply the Session-owned mode immediately at that boundary, before Web
|
|
1474
|
+
// injection is released. This also narrows the window in which a user can
|
|
1475
|
+
// type into the native pane before its explicit startup mode is installed.
|
|
1476
|
+
const restore = live.injectLock.then(() => this.applyLiveCollaborationMode(live, threadId, desiredMode));
|
|
1477
|
+
live.injectLock = restore.then(() => undefined, () => undefined);
|
|
1478
|
+
void restore.then(finishBinding).catch((error) => {
|
|
1479
|
+
const failure = nativeLiveFailure(live.runtime, "native_collaboration_mode_restore_failed", "native thread could not apply the Session collaboration mode", error);
|
|
1480
|
+
live.startupError = failure;
|
|
1481
|
+
live.markStartupFailed();
|
|
1482
|
+
this.liveStartupErrors.set(localThreadId, failure);
|
|
1483
|
+
this.teardownLiveCodexSession(localThreadId, failure);
|
|
1484
|
+
});
|
|
1127
1485
|
}
|
|
1128
1486
|
/** Start the known-thread observer after the replacement TUI has launched.
|
|
1129
1487
|
* Fresh sessions already connected their discovery listener before launch;
|
|
@@ -1141,7 +1499,7 @@ export class LocalAgentHost {
|
|
|
1141
1499
|
if (live.stopped)
|
|
1142
1500
|
return;
|
|
1143
1501
|
live.observerAvailable = true;
|
|
1144
|
-
//
|
|
1502
|
+
// reference implementation starts subscription as a sibling task after the transport
|
|
1145
1503
|
// connects. A no/empty rollout remains event-driven retryable; any
|
|
1146
1504
|
// other subscription rejection is diagnostic only and must not tear
|
|
1147
1505
|
// down the otherwise usable Terminal/injection lifecycle.
|
|
@@ -1152,7 +1510,7 @@ export class LocalAgentHost {
|
|
|
1152
1510
|
}
|
|
1153
1511
|
})();
|
|
1154
1512
|
}
|
|
1155
|
-
/**
|
|
1513
|
+
/** reference implementation treats the forwarder as a required component of one native
|
|
1156
1514
|
* lifecycle: if its transport dies, it closes the app-server instead of
|
|
1157
1515
|
* accepting turns that can no longer reach the canonical mirror. */
|
|
1158
1516
|
failObserverLifecycle(localThreadId, live, error) {
|
|
@@ -1184,7 +1542,7 @@ export class LocalAgentHost {
|
|
|
1184
1542
|
* Subscribe the forwarder connection to a thread (reference implementation's
|
|
1185
1543
|
* `_subscribe_until_ready`). A fresh TUI thread has no rollout until its first
|
|
1186
1544
|
* turn, so `thread/resume` is retried: park until the forwarder observes the
|
|
1187
|
-
* thread active, then retry. Like
|
|
1545
|
+
* thread active, then retry. Like reference implementation, the first attempt always uses
|
|
1188
1546
|
* `excludeTurns`: a known-session cold resume therefore never reconstructs
|
|
1189
1547
|
* historical running/completed state. Only a fresh thread whose first attempt
|
|
1190
1548
|
* failed as not-ready retries without `excludeTurns`, backfilling the newly
|
|
@@ -1321,7 +1679,7 @@ export class LocalAgentHost {
|
|
|
1321
1679
|
* forwarder). Park-until-ready (~60s), aligning reference implementation's executor waiting for
|
|
1322
1680
|
* the bridge instead of a short race that falls back to a second output path.
|
|
1323
1681
|
*/
|
|
1324
|
-
async injectMessage(localThreadId, input) {
|
|
1682
|
+
async injectMessage(localThreadId, input, options) {
|
|
1325
1683
|
const runtimeInput = typeof input === "string"
|
|
1326
1684
|
? { content: [{ type: "text", text: input }] }
|
|
1327
1685
|
: input;
|
|
@@ -1389,6 +1747,13 @@ export class LocalAgentHost {
|
|
|
1389
1747
|
};
|
|
1390
1748
|
live.pendingInjectedInputs.push(pendingInput);
|
|
1391
1749
|
let injectionMethod = "turn/start";
|
|
1750
|
+
let admissionPublished = false;
|
|
1751
|
+
const publishAdmission = () => {
|
|
1752
|
+
if (admissionPublished)
|
|
1753
|
+
return;
|
|
1754
|
+
admissionPublished = true;
|
|
1755
|
+
live.publishTurnAdmission();
|
|
1756
|
+
};
|
|
1392
1757
|
const forgetPendingInput = () => {
|
|
1393
1758
|
const index = live.pendingInjectedInputs.indexOf(pendingInput);
|
|
1394
1759
|
if (index >= 0)
|
|
@@ -1396,8 +1761,14 @@ export class LocalAgentHost {
|
|
|
1396
1761
|
};
|
|
1397
1762
|
const injectionClient = this.injectionClientFactory(live.appServerUrl);
|
|
1398
1763
|
try {
|
|
1764
|
+
// reference implementation publishes codex-native running only after the native Terminal
|
|
1765
|
+
// is ready and the runner has accepted the message, but before the
|
|
1766
|
+
// short-lived app-server client initializes and starts the Turn. Claude
|
|
1767
|
+
// deliberately has no matching synthesized edge.
|
|
1768
|
+
if (!live.forwarder.isTurnOpen())
|
|
1769
|
+
publishAdmission();
|
|
1399
1770
|
await injectionClient.ensureInitialized();
|
|
1400
|
-
// Match
|
|
1771
|
+
// Match reference implementation's executor: each message uses one initialized client
|
|
1401
1772
|
// that closes as soon as turn/start or turn/steer is acknowledged.
|
|
1402
1773
|
if (live.forwarder.isTurnOpen()) {
|
|
1403
1774
|
const turnId = live.forwarder.currentTurnId();
|
|
@@ -1419,15 +1790,51 @@ export class LocalAgentHost {
|
|
|
1419
1790
|
return { outcome: "steered", responseId: `resp_codex_${steered.turnId}` };
|
|
1420
1791
|
}
|
|
1421
1792
|
}
|
|
1422
|
-
//
|
|
1423
|
-
//
|
|
1793
|
+
// Match reference implementation's turn boundary: change the native thread settings
|
|
1794
|
+
// under the same lock immediately before starting the next Turn. Never
|
|
1795
|
+
// put settings on turn/start or mutate a Turn that is already open.
|
|
1796
|
+
const desiredModel = live.model;
|
|
1797
|
+
const desiredReasoningEffort = live.reasoningEffort;
|
|
1798
|
+
// A Session-owned mode survives runner restarts. Normal Web messages do
|
|
1799
|
+
// not carry per-Turn options, so a cold-resumed native thread must fall
|
|
1800
|
+
// back to the persisted execution snapshot before its first new Turn.
|
|
1801
|
+
// An explicit session.run option still wins for that invocation.
|
|
1802
|
+
const desiredCollaborationMode = options?.collaborationMode ?? live.execution.collaborationMode;
|
|
1803
|
+
const collaborationMode = desiredCollaborationMode &&
|
|
1804
|
+
desiredCollaborationMode !== live.appliedCollaborationMode
|
|
1805
|
+
? await buildCollaborationMode(injectionClient, desiredCollaborationMode, desiredModel, desiredReasoningEffort)
|
|
1806
|
+
: undefined;
|
|
1807
|
+
const settings = {
|
|
1808
|
+
threadId,
|
|
1809
|
+
...(desiredModel !== live.appliedModel
|
|
1810
|
+
? { model: desiredModel || null }
|
|
1811
|
+
: {}),
|
|
1812
|
+
...(desiredReasoningEffort !== live.appliedReasoningEffort
|
|
1813
|
+
? { effort: desiredReasoningEffort ?? null }
|
|
1814
|
+
: {}),
|
|
1815
|
+
...(collaborationMode ? { collaborationMode } : {}),
|
|
1816
|
+
};
|
|
1817
|
+
if (Object.keys(settings).length > 1) {
|
|
1818
|
+
await injectionClient.threadSettingsUpdate(settings);
|
|
1819
|
+
// Record exactly what this RPC carried. A concurrent settings save may
|
|
1820
|
+
// already have advanced the desired fields and must remain pending for
|
|
1821
|
+
// the following new Turn.
|
|
1822
|
+
live.appliedModel = desiredModel;
|
|
1823
|
+
live.appliedReasoningEffort = desiredReasoningEffort;
|
|
1824
|
+
if (desiredCollaborationMode) {
|
|
1825
|
+
live.appliedCollaborationMode = desiredCollaborationMode;
|
|
1826
|
+
live.execution = {
|
|
1827
|
+
...live.execution,
|
|
1828
|
+
collaborationMode: desiredCollaborationMode,
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
publishAdmission();
|
|
1424
1833
|
const started = await injectionClient.turnStart({
|
|
1425
1834
|
threadId,
|
|
1426
1835
|
input: nativeInput,
|
|
1427
1836
|
...turnWorkspaceParams(live.runtime, live.workspace, live.sandbox),
|
|
1428
1837
|
approvalPolicy: live.approvalPolicy,
|
|
1429
|
-
...(live.model ? { model: live.model } : {}),
|
|
1430
|
-
...(live.reasoningEffort ? { effort: live.reasoningEffort } : {}),
|
|
1431
1838
|
});
|
|
1432
1839
|
if (pendingInput.observed) {
|
|
1433
1840
|
forgetPendingInput();
|
|
@@ -1444,11 +1851,22 @@ export class LocalAgentHost {
|
|
|
1444
1851
|
}
|
|
1445
1852
|
catch (error) {
|
|
1446
1853
|
forgetPendingInput();
|
|
1854
|
+
if (error instanceof CodexRuntimeError &&
|
|
1855
|
+
error.code === "collaboration_mode_model_unknown") {
|
|
1856
|
+
throw error;
|
|
1857
|
+
}
|
|
1447
1858
|
// Preserve the app-server's error instead of collapsing every failure
|
|
1448
1859
|
// into the unactionable `live injection failed` string.
|
|
1449
1860
|
const baseDetail = codexRpcError(error, injectionMethod);
|
|
1450
1861
|
const startupDetail = live.forwarder.mcpStartupDetail();
|
|
1451
1862
|
const detail = startupDetail ? `${baseDetail} (${startupDetail})` : baseDetail;
|
|
1863
|
+
// A turn may open on the observer while this short-lived client is
|
|
1864
|
+
// initializing, converting the submission to steer. A rejected steer
|
|
1865
|
+
// does not fail that already-running Turn; only a new-Turn admission
|
|
1866
|
+
// owns the terminal failed edge.
|
|
1867
|
+
if (admissionPublished && injectionMethod === "turn/start") {
|
|
1868
|
+
live.publishTurnAdmissionFailure(detail);
|
|
1869
|
+
}
|
|
1452
1870
|
console.error(`[codex-live] session=${localThreadId} runtime=${live.runtime} injection failed: ${detail}`);
|
|
1453
1871
|
throw nativeLiveFailure(live.runtime, "native_message_injection_failed", `message injection via ${injectionMethod} failed`, detail);
|
|
1454
1872
|
}
|
|
@@ -1551,8 +1969,6 @@ export class LocalAgentHost {
|
|
|
1551
1969
|
claude.forwarder.finalizeStop();
|
|
1552
1970
|
}
|
|
1553
1971
|
removeManagedClaudeSettings(claude.bridgeDir);
|
|
1554
|
-
// Remove the throwaway skills --plugin-dir temp so it doesn't leak per launch.
|
|
1555
|
-
void claude.skillCleanup?.();
|
|
1556
1972
|
return;
|
|
1557
1973
|
}
|
|
1558
1974
|
const live = this.liveSessions.get(localThreadId);
|
|
@@ -1565,6 +1981,7 @@ export class LocalAgentHost {
|
|
|
1565
1981
|
live.markStartupFailed();
|
|
1566
1982
|
}
|
|
1567
1983
|
live.releaseActive?.();
|
|
1984
|
+
live.cancelSyntheticInteractions("session_stopped");
|
|
1568
1985
|
live.appServerOwner.cancelInteractions("session_stopped");
|
|
1569
1986
|
live.forwarderClient.cancelInteractions("session_stopped");
|
|
1570
1987
|
live.appServerOwner.setInteractionListener(null);
|
|
@@ -1574,12 +1991,13 @@ export class LocalAgentHost {
|
|
|
1574
1991
|
live.forwarder.stop();
|
|
1575
1992
|
live.canonicalInteractions.clear();
|
|
1576
1993
|
live.settledInteractions.clear();
|
|
1994
|
+
live.syntheticInteractions.clear();
|
|
1577
1995
|
void live.forwarderClient.stop().catch(() => undefined);
|
|
1578
1996
|
// Remove the session-scoped skills dir — the machine keeps zero task residue.
|
|
1579
1997
|
void live.skillsCleanup?.();
|
|
1580
1998
|
}
|
|
1581
1999
|
/** Tear down one codex-lineage native runtime without deleting its durable
|
|
1582
|
-
* session-store binding.
|
|
2000
|
+
* session-store binding. reference implementation couples its auxiliary Terminal, observer,
|
|
1583
2001
|
* forwarder and per-session app-server as one disposable runtime envelope;
|
|
1584
2002
|
* the next message recreates that envelope and cold-resumes the native id. */
|
|
1585
2003
|
teardownLiveCodexSession(localThreadId, error) {
|
|
@@ -1789,17 +2207,27 @@ export class LocalAgentHost {
|
|
|
1789
2207
|
throw new CodexRuntimeError(`cannot launch Claude from ${execution.provider} execution snapshot`, 422, "invalid_execution_snapshot");
|
|
1790
2208
|
}
|
|
1791
2209
|
const cwd = workspace.cwd;
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
2210
|
+
const hasSkillSnapshot = execution.skills.length > 0 || execution.pluginSkills.length > 0;
|
|
2211
|
+
let skillPlugin = hasSkillSnapshot
|
|
2212
|
+
? await reuseClaudePlugin(localThreadId)
|
|
2213
|
+
: null;
|
|
2214
|
+
if (!skillPlugin && hasSkillSnapshot) {
|
|
2215
|
+
let snapshotSkills;
|
|
2216
|
+
try {
|
|
2217
|
+
snapshotSkills = await this.prepareExecutionSkills(execution);
|
|
2218
|
+
}
|
|
2219
|
+
catch (err) {
|
|
2220
|
+
console.error(`[session-snapshot] session=${localThreadId} skill materialization failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2221
|
+
return false;
|
|
2222
|
+
}
|
|
2223
|
+
try {
|
|
2224
|
+
skillPlugin = await materializeClaudePlugin(localThreadId, snapshotSkills.selectedSkills);
|
|
2225
|
+
}
|
|
2226
|
+
finally {
|
|
2227
|
+
await snapshotSkills.skillsCleanup();
|
|
2228
|
+
}
|
|
1799
2229
|
}
|
|
1800
2230
|
const model = execution.model ?? "";
|
|
1801
|
-
const skillPlugin = await materializeSkillPlugin(snapshotSkills.selectedSkills);
|
|
1802
|
-
void snapshotSkills.skillsCleanup();
|
|
1803
2231
|
const permissionMode = execution.permissionMode;
|
|
1804
2232
|
const launchExtraArgs = [
|
|
1805
2233
|
...(skillPlugin?.pluginDir ? ["--plugin-dir", skillPlugin.pluginDir] : []),
|
|
@@ -1876,7 +2304,6 @@ export class LocalAgentHost {
|
|
|
1876
2304
|
launchExtraArgs,
|
|
1877
2305
|
permissionMode,
|
|
1878
2306
|
...(forkIntent ? { forkIntent } : {}),
|
|
1879
|
-
...(skillPlugin?.cleanup ? { skillCleanup: skillPlugin.cleanup } : {}),
|
|
1880
2307
|
};
|
|
1881
2308
|
live.publishInterrupted = (responseId) => {
|
|
1882
2309
|
if (live.interruptedResponseId === responseId)
|
|
@@ -2166,8 +2593,9 @@ export class LocalAgentHost {
|
|
|
2166
2593
|
return live.injector ?? null;
|
|
2167
2594
|
}
|
|
2168
2595
|
/** Attach a session's tmux pane injector (from the runner-child, which owns the
|
|
2169
|
-
* terminal registry — the host does not hold tmux).
|
|
2170
|
-
*
|
|
2596
|
+
* terminal registry — the host does not hold tmux). Claude uses it for normal
|
|
2597
|
+
* message injection; Codex uses it only to dismiss the exact TUI-local Plan
|
|
2598
|
+
* picker after its synthetic Web form is submitted. Idempotent. */
|
|
2171
2599
|
attachTerminalInjector(localThreadId, injector) {
|
|
2172
2600
|
const live = this.liveClaudeSessions.get(localThreadId);
|
|
2173
2601
|
if (live) {
|
|
@@ -2175,7 +2603,11 @@ export class LocalAgentHost {
|
|
|
2175
2603
|
return;
|
|
2176
2604
|
live.injector = injector;
|
|
2177
2605
|
live.forwarder.attachStatusSource({ panePid: () => injector.panePid?.() });
|
|
2606
|
+
return;
|
|
2178
2607
|
}
|
|
2608
|
+
const codex = this.liveSessions.get(localThreadId);
|
|
2609
|
+
if (codex)
|
|
2610
|
+
codex.injector = injector;
|
|
2179
2611
|
}
|
|
2180
2612
|
/** Close an active native response when its terminal or runner disappears. */
|
|
2181
2613
|
failLiveSession(localThreadId, error) {
|
|
@@ -2402,6 +2834,40 @@ function raceReady(ready, timeoutMs, failed) {
|
|
|
2402
2834
|
clearTimeout(timer);
|
|
2403
2835
|
});
|
|
2404
2836
|
}
|
|
2837
|
+
/** Expand the public mode enum into the App Server's required settings
|
|
2838
|
+
* snapshot. This mirrors Omnigent: an explicit Session model wins; otherwise
|
|
2839
|
+
* the Provider's advertised native default is required, and an unknown model
|
|
2840
|
+
* fails the update instead of silently dropping collaboration mode. */
|
|
2841
|
+
async function buildCollaborationMode(client, mode, desiredModel, desiredReasoningEffort) {
|
|
2842
|
+
let model = desiredModel.trim();
|
|
2843
|
+
if (!model) {
|
|
2844
|
+
let catalog;
|
|
2845
|
+
try {
|
|
2846
|
+
catalog = await client.modelList();
|
|
2847
|
+
}
|
|
2848
|
+
catch (error) {
|
|
2849
|
+
throw new CodexRuntimeError(`collaboration mode requires the current model: ${error instanceof Error ? error.message : String(error)}`, 503, "collaboration_mode_model_unknown");
|
|
2850
|
+
}
|
|
2851
|
+
const defaults = [...new Set(catalog.data
|
|
2852
|
+
.filter((candidate) => candidate.isDefault === true)
|
|
2853
|
+
.map((candidate) => (candidate.model || candidate.id).trim())
|
|
2854
|
+
.filter(Boolean))];
|
|
2855
|
+
if (defaults.length !== 1) {
|
|
2856
|
+
throw new CodexRuntimeError(`collaboration mode requires exactly one current default model; Provider advertised ${defaults.length}`, 503, "collaboration_mode_model_unknown");
|
|
2857
|
+
}
|
|
2858
|
+
model = defaults[0];
|
|
2859
|
+
}
|
|
2860
|
+
return {
|
|
2861
|
+
mode,
|
|
2862
|
+
settings: {
|
|
2863
|
+
model,
|
|
2864
|
+
reasoning_effort: desiredReasoningEffort ?? null,
|
|
2865
|
+
// `null` asks the App Server to install the built-in instructions for
|
|
2866
|
+
// the selected mode; thread-level Agent instructions remain separate.
|
|
2867
|
+
developer_instructions: null,
|
|
2868
|
+
},
|
|
2869
|
+
};
|
|
2870
|
+
}
|
|
2405
2871
|
function sameSessionSnapshots(workspace, execution, opts) {
|
|
2406
2872
|
return JSON.stringify(stableValue({
|
|
2407
2873
|
workspace,
|
|
@@ -2411,6 +2877,31 @@ function sameSessionSnapshots(workspace, execution, opts) {
|
|
|
2411
2877
|
execution: opts.execution,
|
|
2412
2878
|
}));
|
|
2413
2879
|
}
|
|
2880
|
+
function sameImmutableSessionSnapshots(workspace, execution, opts) {
|
|
2881
|
+
const current = {
|
|
2882
|
+
...execution,
|
|
2883
|
+
model: null,
|
|
2884
|
+
reasoningEffort: null,
|
|
2885
|
+
collaborationMode: undefined,
|
|
2886
|
+
};
|
|
2887
|
+
const requested = {
|
|
2888
|
+
...opts.execution,
|
|
2889
|
+
model: null,
|
|
2890
|
+
reasoningEffort: null,
|
|
2891
|
+
collaborationMode: undefined,
|
|
2892
|
+
};
|
|
2893
|
+
return JSON.stringify(stableValue({ workspace, execution: current })) ===
|
|
2894
|
+
JSON.stringify(stableValue({ workspace: opts.workspace, execution: requested }));
|
|
2895
|
+
}
|
|
2896
|
+
function updateLiveCodexTurnSettings(live, opts) {
|
|
2897
|
+
if (!sameImmutableSessionSnapshots(live.workspace, live.execution, opts)) {
|
|
2898
|
+
return false;
|
|
2899
|
+
}
|
|
2900
|
+
live.execution = structuredClone(opts.execution);
|
|
2901
|
+
live.model = opts.execution.model ?? "";
|
|
2902
|
+
live.reasoningEffort = opts.execution.reasoningEffort ?? undefined;
|
|
2903
|
+
return true;
|
|
2904
|
+
}
|
|
2414
2905
|
/** Freeze the Session's permission choice into the explicit managed settings.
|
|
2415
2906
|
* Host/project settings may still contribute allow/deny rules, but changing
|
|
2416
2907
|
* their defaultMode cannot silently mutate an existing Session on resume. */
|