Repository navigation
fix(server): switching Codex accounts releases the old account's writer #30647
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
10fee5f
83c8657
3b89ebb
664807c
760c753
bbaa948
ce3e8c6
ba2e51e
68db213
18d1fc3
0c6d246
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1940,6 +1940,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( | |
| string, | ||
| { readonly runtimeMode: RuntimeMode; readonly kinds: Set<ProviderRequestKind> } | ||
| >(); | ||
| const threadCloses = new Map<string, Set<Deferred.Deferred<void>>>(); | ||
| /** Bind answers to the request's approval set so mode changes invalidate late answers. */ | ||
| const endSessionApprovalsOnModeChange = (context: ActiveCodexTurnContext) => { | ||
| const nativeThreadId = context.providerThread.nativeThreadRef?.nativeId; | ||
|
|
@@ -4979,7 +4980,17 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( | |
| }); | ||
| yield* releaseSettledTurnIfIdle(context.nativeTurnId); | ||
| } | ||
| }).pipe(Effect.orDie, turnTerminalizationPermit.withPermits(1)), | ||
| }).pipe( | ||
| Effect.orDie, | ||
| turnTerminalizationPermit.withPermits(1), | ||
| Effect.andThen(() => | ||
| Effect.forEach( | ||
| threadCloses.get(payload.threadId) ?? [], | ||
| (closed) => Deferred.succeed(closed, undefined), | ||
| { discard: true }, | ||
| ), | ||
| ), | ||
| ), | ||
| ); | ||
|
|
||
| yield* client.handleServerNotification("item/started", (payload) => | ||
|
|
@@ -7501,7 +7512,25 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( | |
| function* (unloadInput) { | ||
| const nativeThreadId = yield* getNativeThreadId(unloadInput.providerThread); | ||
| sessionApprovals.delete(nativeThreadId); | ||
| yield* client.request("thread/unsubscribe", { threadId: nativeThreadId }); | ||
| // Codex replies before it closes the thread; the writer lock and a same-process | ||
| // resume ("is closing") both wait for thread/closed. Callers bound the wait. | ||
| const closed = yield* Deferred.make<void>(); | ||
| const waiters = threadCloses.get(nativeThreadId) ?? new Set(); | ||
| waiters.add(closed); | ||
| threadCloses.set(nativeThreadId, waiters); | ||
| yield* client.request("thread/unsubscribe", { threadId: nativeThreadId }).pipe( | ||
| Effect.flatMap(({ status }) => | ||
| status === "unsubscribed" ? Deferred.await(closed) : Effect.void, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| ), | ||
| Effect.ensuring( | ||
| Effect.sync(() => { | ||
| waiters.delete(closed); | ||
| if (waiters.size === 0 && threadCloses.get(nativeThreadId) === waiters) { | ||
| threadCloses.delete(nativeThreadId); | ||
| } | ||
| }), | ||
| ), | ||
| ); | ||
|
Comment on lines
+7515
to
+7533
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| }, | ||
| (effect, unloadInput) => | ||
| effect.pipe( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2726,6 +2726,7 @@ export const layerWithOptions = ( | |
| return [ | ||
| Option.none<{ | ||
| readonly entry: LiveSessionEntry; | ||
| readonly heldProviderThread: OrchestrationV2ProviderThread | undefined; | ||
| readonly idleUnloadFiber: Fiber.Fiber<void, never> | null; | ||
| }>(), | ||
| current, | ||
|
|
@@ -2762,6 +2763,7 @@ export const layerWithOptions = ( | |
| return [ | ||
| Option.some({ | ||
| entry: updatedEntry, | ||
| heldProviderThread: entry.idleThreadUnloads.get(input.threadId)?.providerThread, | ||
| idleUnloadFiber: entry.idleThreadUnloads.get(input.threadId)?.fiber ?? null, | ||
| }), | ||
| updated, | ||
|
|
@@ -2770,17 +2772,17 @@ export const layerWithOptions = ( | |
| if (Option.isSome(detachResult)) { | ||
| yield* cancelIdleFiber(detachResult.value.idleUnloadFiber); | ||
| } | ||
| const detached = Option.map(detachResult, (result) => result.entry); | ||
| // Plain detaches preserve credentials held by live shared providers across workspace handoffs. | ||
| if (input.revokeMcpCredential === true) { | ||
| yield* clearMcpSession(input.threadId); | ||
| } | ||
| if (Option.isNone(detached)) { | ||
| if (Option.isNone(detachResult)) { | ||
| return; | ||
| } | ||
| const { entry: detached, heldProviderThread } = detachResult.value; | ||
| if ( | ||
| detached.value.attachedThreadIds.size === 0 && | ||
| !detached.value.supportsMultipleProviderThreads | ||
| detached.attachedThreadIds.size === 0 && | ||
| !detached.supportsMultipleProviderThreads | ||
| ) { | ||
| yield* releaseEntry({ | ||
| providerSessionId: input.providerSessionId, | ||
|
|
@@ -2790,8 +2792,16 @@ export const layerWithOptions = ( | |
| }); | ||
| return; | ||
| } | ||
| const unloadThread = detached.value.exposedRuntime.unloadThread; | ||
| if (detached.value.supportsMultipleProviderThreads && unloadThread !== undefined) { | ||
| const unloadThread = detached.exposedRuntime.unloadThread; | ||
| if (detached.supportsMultipleProviderThreads && unloadThread !== undefined) { | ||
| // A cross-instance switch repoints the row before this detach, so the projection | ||
| // can miss what this session still holds for the thread. | ||
| if ( | ||
| heldProviderThread !== undefined && | ||
| !detachedProviderThreads.some((thread) => thread.id === heldProviderThread.id) | ||
| ) { | ||
| detachedProviderThreads = [...detachedProviderThreads, heldProviderThread]; | ||
|
Comment on lines
+2800
to
+2803
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
| // The projection includes native children owned by other app threads; | ||
| // lock each owner so an attached child stays loaded and later attaches wait. | ||
| for (const providerThread of detachedProviderThreads) { | ||
|
|
@@ -2805,7 +2815,7 @@ export const layerWithOptions = ( | |
| Effect.gen(function* () { | ||
| const entry = (yield* Ref.get(sessions)).get(key); | ||
| if ( | ||
| entry?.runtime !== detached.value.runtime || | ||
| entry?.runtime !== detached.runtime || | ||
| entry.attachedThreadIds.has(ownerThreadId) | ||
| ) { | ||
| return; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,10 +9,17 @@ export const resolveCodexLaunchArgs = ( | |
|
|
||
| const codexLaunchArgv = (launchArgs?: string): ReadonlyArray<string> => tokenizeCliArgs(launchArgs); | ||
|
|
||
| export const codexAppServerArgs = (launchArgs?: string) => [ | ||
| "app-server", | ||
| ...codexLaunchArgv(launchArgs), | ||
| ]; | ||
| export const codexAppServerArgs = (launchArgs?: string) => { | ||
| const args = codexLaunchArgv(launchArgs); | ||
| return [ | ||
| "app-server", | ||
| // Otherwise Codex keeps an unsubscribed thread, and its writer lock, loaded for a delay. | ||
| // Codex 0.163 ignores this key in per-thread config, so it is set for the app-server. | ||
| // Codex before 0.154 lacks the key, which --strict-config rejects at startup. | ||
| ...(args.includes("--strict-config") ? [] : ["--config", "thread_unload_delay_secs=0"]), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Preserve immediate unload with With Codex 0.167.0 and
|
||
| ...args, | ||
| ]; | ||
| }; | ||
|
|
||
| export const codexExecLaunchArgs = (launchArgs?: string) => { | ||
| const args = codexLaunchArgv(launchArgs); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Script executed:
Repository: pingdotgg/t3code
Length of output: 4561
Script executed:
Repository: pingdotgg/t3code
Length of output: 41498
Web query:
Codex rust-v0.167.0 thread/closed notification thread_lifecycle closure writer shutdown unsubscribe threadResult:
Web query:
Codex rust-v0.0.167.0 thread/closed notification thread_lifecycle closure writer shutdown unsubscribe threadResult:
Script executed:
Repository: pingdotgg/t3code
Length of output: 23876
Web query:
Effect 4.1.2 Effect ensuring finalizer runs after effect succeeds fails interrupted semaphore withPermitsResult:
Script executed:
Repository: pingdotgg/t3code
Length of output: 16403
Script executed:
Repository: pingdotgg/t3code
Length of output: 30061
Script executed:
Repository: pingdotgg/t3code
Length of output: 21777
Script executed:
Repository: pingdotgg/t3code
Length of output: 15408
Script executed:
Repository: pingdotgg/t3code
Length of output: 11029
Script executed:
Repository: pingdotgg/t3code
Length of output: 9779
Script executed:
Repository: pingdotgg/t3code
Length of output: 11212
Script executed:
Repository: pingdotgg/t3code
Length of output: 11517
Script executed:
Repository: pingdotgg/t3code
Length of output: 5267
Script executed:
Repository: pingdotgg/t3code
Length of output: 203
Script executed:
Repository: pingdotgg/t3code
Length of output: 1014
Script executed:
Repository: pingdotgg/t3code
Length of output: 1590
Script executed:
Repository: pingdotgg/t3code
Length of output: 2914
Decouple the close signal from terminalization.
An unrelated interrupt can hold
turnTerminalizationPermitwhile an MCP capture completes uninterruptible cleanup. If that takes more than two seconds,unloadThreadcan time out after Codex has already closed the thread.Complete the close signal independently of that permit while preserving command terminalization. An
Effect.ensuringafterwithPermitsstill waits for the permit.