Skip to content
Closed
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
e5dd4d7
fix(desktop): relink Codex thread across providers
zqchris Aug 27, 2026
239d279
fix(desktop): close Codex relink races
zqchris Aug 28, 2026
3cb26b1
fix(desktop): roll back stale Codex relinks
zqchris Aug 28, 2026
4c9b5c3
Merge remote-tracking branch 'upstream/main' into codex-provider-thre…
zqchris Aug 28, 2026
fe2aa3b
fix(desktop): relink persisted Codex threads without live handles
zqchris Aug 28, 2026
7319ce0
fix(desktop): recalculate deferred Codex thread relink
zqchris Aug 28, 2026
8fc9608
fix(codex): preserve committed relink recovery state
zqchris Aug 28, 2026
ad849fa
fix(codex): roll back relink across owner changes
zqchris Aug 28, 2026
7b74e31
fix(codex): fence relink during app teardown
zqchris Aug 28, 2026
88dcd72
fix(codex): discard stale owner pending switches
zqchris Aug 28, 2026
c0d129a
fix(desktop): roll back persisted Codex route atomically
zqchris Aug 28, 2026
7337ac6
fix(desktop): compensate stale pending Codex routes
zqchris Aug 28, 2026
31b9c02
fix(desktop): restore live runtime after route rollback
zqchris Aug 28, 2026
45e8e96
fix(desktop): preserve IM switch source route
zqchris Aug 28, 2026
9f5c3a9
fix(desktop): compensate resolved pending route
zqchris Aug 28, 2026
1c3763d
fix(desktop): reconcile retained runtime after close failure
zqchris Aug 28, 2026
9e4a695
fix(desktop): compensate stale pending registration
zqchris Aug 28, 2026
feccf41
fix(desktop): gate deferred relink on close failure
zqchris Aug 28, 2026
0f75b86
fix(desktop): keep deferred relink route restart-safe
zqchris Aug 28, 2026
ee44907
fix(desktop): compensate unpersisted relink failures
zqchris Aug 28, 2026
7bb48a6
test(desktop): guard deferred switch registration
zqchris Aug 28, 2026
8f4d246
test(desktop): preload Codex proxy host suite
zqchris Aug 28, 2026
821566a
fix(desktop): complete deferred remote selections
zqchris Aug 28, 2026
6f3ef30
fix(desktop): compensate superseded deferred routes
zqchris Aug 28, 2026
53f5125
fix(desktop): restore cancelled deferred runtime route
zqchris Aug 28, 2026
78876ec
fix(desktop): await deferred switch restoration
zqchris Aug 28, 2026
de01489
fix(desktop): retry stale owner route compensation
zqchris Aug 28, 2026
12b3750
fix(desktop): await hot switch pending recovery
zqchris Aug 28, 2026
04663bf
fix(desktop): atomically persist IM Codex relinks
zqchris Aug 28, 2026
a1bdaa9
fix(desktop): atomically restore stale Codex routes
zqchris Aug 28, 2026
0fc8d8e
fix(desktop): cancel failed deferred runtime switches
zqchris Aug 28, 2026
06f8b79
fix(desktop): atomically persist immediate Codex relinks
zqchris Aug 28, 2026
013cc17
fix(desktop): retain post-commit relink compensation
zqchris Aug 28, 2026
75ef786
fix(desktop): isolate relink compensation retries
zqchris Aug 28, 2026
2792222
fix(desktop): persist non-user Codex relink routes
zqchris Aug 28, 2026
2324134
fix(desktop): atomically persist deferred Codex relinks
zqchris Aug 28, 2026
7f47950
fix(desktop): stabilize Windows startup barrier
zqchris Aug 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,10 @@ describe('sendToSession ordering', () => {
);
expect(setModelBlock).toContain('patch.effort = atomicSelection.effort;');
expect(setModelBlock).toContain('patch.fastMode = atomicSelection.fastMode;');
expect(setModelBlock).toContain(
"result.status === 'deferred' && result.preservePersistedRoute === true",
);
expect(setModelBlock).toContain('if (!preservePersistedRoute) {');
expect(setModelBlock).toContain('await persistSessionFields(sessionId, patch);');
expect(setModelBlock).toContain('markRemoteSettingPersistedInsideHandler(response);');
expectOrder(
Expand Down Expand Up @@ -548,6 +552,8 @@ describe('sendToSession ordering', () => {
);
expect(setModelBlock).toContain('pendingCredentialSwitchHolder?.clear(sessionId);');
expect(setModelBlock).toContain('restoreControlStores();');
expect(setModelBlock).toContain('const hadRelink = appliedCodexThreadRelink !== undefined;');
expect(setModelBlock).toContain('await recoverRuntimeAfterSelectionRollback({');
expect(setModelBlock).toContain('previousRuntime.pendingCredentialSwitch');
expect(setModelBlock).toContain('withRehydrateCloseSuppressed(sessionId');
expect(setModelBlock).toContain('recordRecoveredSessionRuntimeMutation(sessionId');
Expand Down
285 changes: 285 additions & 0 deletions apps/desktop/src/main/__tests__/sessionRuntimeControlWiring.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ const registerSource = readFileSync(resolve(mainRoot, 'maker-ipc/register.ts'),
/\r\n?/g,
'\n',
);
const pendingCredentialSwitchSource = readFileSync(
resolve(mainRoot, 'maker-ipc/pendingCredentialSwitch.ts'),
'utf8',
).replace(/\r\n?/g, '\n');

function handlerBody(source: string, channel: string, nextChannel: string): string {
const start = source.indexOf(channel);
Expand Down Expand Up @@ -186,6 +190,287 @@ describe('session runtime control wiring', () => {
expect(axisValidation).toBeLessThan(setModel.indexOf('persistSessionFields(sessionId'));
});

it('rolls back a committed Codex thread relink when atomic route persistence fails', () => {
const setModel = handlerBody(
registerSource,
'const handleSetModel = async (',
'const recoverRemoteRuntimeAxisPersistence',
);
const persist = setModel.indexOf('await persistSessionFields(sessionId, patch)');
const rollback = setModel.indexOf(
'const restored = await rollbackAppliedCodexThreadRelink()',
persist,
);

expect(persist).toBeGreaterThan(-1);
expect(rollback).toBeGreaterThan(persist);
expect(setModel).toContain("throw new Error('Codex thread relink rollback was superseded')");
});

it('rolls back the complete persisted route before an owner-boundary request exits', () => {
const setModel = handlerBody(
registerSource,
'const handleSetModel = async (',
'const recoverRemoteRuntimeAxisPersistence',
);
const guard = setModel.indexOf(
'const rollbackRuntimeSelectionForSupersededOwner = async (): Promise<boolean> => {',
);
const rollback = setModel.indexOf('await rollbackAppliedRuntimeSelection();', guard);
const ownerFailure = setModel.indexOf('assertRuntimeOwnerCurrent();', guard);

expect(setModel).toContain('!isAppSessionBoundaryPending()');
expect(setModel).toContain('const rollbackAppliedRuntimeSelection = async ()');
expect(setModel).toContain('const runtimeDbSnapshot = getCurrentDbClientSnapshot();');
expect(setModel).toContain('effort: sessions.effort');
expect(setModel).toContain('fastMode: sessions.fastMode');
expect(setModel).toContain('rollbackPersistedCodexRuntimeSelection({');
expect(setModel).toContain('sdkSessionId: runtimeStatus.sdkSessionId');
expect(setModel).toContain('model: runtimeStatus.model');
expect(setModel).toContain('providerId: runtimeStatus.providerId');
expect(setModel).toContain('effort: runtimeStatus.effort');
expect(setModel).toContain('fastMode: runtimeStatus.fastMode');
expect(setModel).toContain('eq(sessions.model, expected.model)');
expect(setModel).toContain('eq(sessions.sdkSessionId, expected.sdkSessionId)');
expect(setModel).toContain('eq(sessions.providerId, expected.providerId)');
expect(setModel).toContain('eq(sessions.effort, expected.effort');
expect(setModel).toContain('eq(sessions.fastMode, expected.fastMode)');
expect(guard).toBeGreaterThan(-1);
expect(rollback).toBeGreaterThan(guard);
expect(ownerFailure).toBeGreaterThan(rollback);
expect(setModel.match(/await rollbackRuntimeSelectionForSupersededOwner\(\)/g)).toHaveLength(3);
const outerCatch = setModel.lastIndexOf('} catch (err) {');
expect(setModel.indexOf('await rollbackAppliedRuntimeSelection();', outerCatch)).toBeGreaterThan(
outerCatch,
);
});

it('restores host stores and retires the live session after a post-selection failure', () => {
const setModel = handlerBody(
registerSource,
'const handleSetModel = async (',
'const recoverRemoteRuntimeAxisPersistence',
);
const runtimeRecovery = setModel.indexOf(
'const recoverRuntimeAfterSelectionRollback = async (',
);
const ownerGuard = setModel.indexOf('const runtimeRecoveryCurrent =', runtimeRecovery);
const restoreStores = setModel.indexOf('restoreControlStores();', ownerGuard);
const retireLiveSession = setModel.indexOf(
'await withRehydrateCloseSuppressed(sessionId, () => maker.closeSession(sessionId));',
restoreStores,
);
const restoredRouteBroadcast = setModel.indexOf(
'broadcastSessionPatched(\n sessionId,\n {\n ...restoredState,',
retireLiveSession,
);
const reconcileRetainedLive = setModel.indexOf(
'await reconcileRetainedLiveProfile();',
restoredRouteBroadcast,
);
const rollbackFailure = setModel.indexOf(
"'runtime selection rollback could not retire the live session'",
reconcileRetainedLive,
);
const rollback = setModel.indexOf(
'const rollbackAppliedRuntimeSelection = async (): Promise<boolean> => {',
rollbackFailure,
);
const sqliteRestore = setModel.indexOf('rollbackPersistedCodexRuntimeSelection({', rollback);
const projectionRead = setModel.indexOf('const projectionMeta = await maker.getSessionMeta(sessionId);');
const outerCatch = setModel.lastIndexOf('} catch (err) {');

expect(ownerGuard).toBeGreaterThan(runtimeRecovery);
expect(restoreStores).toBeGreaterThan(ownerGuard);
expect(retireLiveSession).toBeGreaterThan(restoreStores);
expect(restoredRouteBroadcast).toBeGreaterThan(retireLiveSession);
expect(reconcileRetainedLive).toBeGreaterThan(restoredRouteBroadcast);
expect(rollbackFailure).toBeGreaterThan(reconcileRetainedLive);
expect(rollback).toBeGreaterThan(rollbackFailure);
expect(sqliteRestore).toBeGreaterThan(rollback);
expect(setModel.slice(retireLiveSession, restoredRouteBroadcast)).toContain(
'if (recoveryErrors.length === 0)',
);
expect(setModel.slice(ownerGuard, restoreStores)).toContain('runtimeOwnerBoundaryCurrent()');
expect(setModel.slice(ownerGuard, restoreStores)).toContain(
'getCurrentDbClientSnapshot()?.clientEpoch === runtimeDbSnapshot.clientEpoch',
);
expect(projectionRead).toBeGreaterThan(retireLiveSession);
expect(outerCatch).toBeGreaterThan(projectionRead);
expect(setModel.indexOf('await rollbackAppliedRuntimeSelection();', outerCatch)).toBeGreaterThan(
outerCatch,
);
});

it('compensates ordinary renderer runtime state when a post-relink metadata read fails', () => {
const setModel = handlerBody(
registerSource,
'const handleSetModel = async (',
'const recoverRemoteRuntimeAxisPersistence',
);
const rollback = setModel.indexOf(
'const rollbackAppliedRuntimeSelection = async (): Promise<boolean> => {',
);
const noPersistedRoute = setModel.indexOf('if (!persistedRoute) {', rollback);
const captureRelink = setModel.indexOf(
'const hadRelink = appliedCodexThreadRelink !== undefined;',
noPersistedRoute,
);
const rollbackRelink = setModel.indexOf(
'const restored = await rollbackAppliedCodexThreadRelink();',
captureRelink,
);
const runtimeRecovery = setModel.indexOf(
'await recoverRuntimeAfterSelectionRollback({',
rollbackRelink,
);
const projectionRead = setModel.indexOf(
'const projectionMeta = await maker.getSessionMeta(sessionId);',
);
const outerCatch = setModel.lastIndexOf('} catch (err) {');
const outerRollback = setModel.indexOf(
'await rollbackAppliedRuntimeSelection();',
outerCatch,
);

expect(noPersistedRoute).toBeGreaterThan(rollback);
expect(captureRelink).toBeGreaterThan(noPersistedRoute);
expect(rollbackRelink).toBeGreaterThan(captureRelink);
expect(runtimeRecovery).toBeGreaterThan(rollbackRelink);
expect(setModel.slice(runtimeRecovery, runtimeRecovery + 500)).toContain(
'sdkSessionId: runtimeStatus.sdkSessionId',
);
expect(setModel.slice(runtimeRecovery, runtimeRecovery + 500)).toContain(
'providerId: runtimeStatus.providerId',
);
expect(projectionRead).toBeGreaterThan(runtimeRecovery);
expect(outerRollback).toBeGreaterThan(outerCatch);
});

it('treats teardown pending as stale at the final relink commit boundary', () => {
const relink = handlerBody(
registerSource,
'export async function relinkCodexThreadForCredentialSwitch(',
'export function clearPendingCredentialSwitchForSession(',
);
const guardedCommit = relink.indexOf(
'commitCodexProviderThreadRelinkWithBoundaryGuard({',
);
const pendingGuard = relink.lastIndexOf('!isAppSessionBoundaryPending()', guardedCommit);
const broadcast = relink.indexOf('broadcastSessionPatched(', guardedCommit);

expect(pendingGuard).toBeGreaterThan(-1);
expect(pendingGuard).toBeLessThan(guardedCommit);
expect(broadcast).toBeGreaterThan(guardedCommit);
expect(relink).toContain('rollbackCapturedRelink({');
});

it('passes the locked persistent thread identity into runtime model switching', () => {
const setModel = handlerBody(
registerSource,
'const handleSetModel = async (',
'const recoverRemoteRuntimeAxisPersistence',
);

expect(setModel).toContain('agentKind: sessions.agentKind');
expect(setModel).toContain('sdkSessionId: sessions.sdkSessionId');
expect(setModel).toContain('persistedSession: {');
expect(setModel).toContain('model: runtimeStatus.model');
expect(setModel).toContain('providerId: runtimeStatus.providerId ?? null');
});

it('keeps the Codex rebuild marker in the production pending-switch projection', () => {
const getter = handlerBody(
registerSource,
'export function getPendingCredentialSwitchTarget(',
'// ── Scheduler 撞忙排队桥',
);

expect(getter).toContain('pending.rebuildCodexThread');
expect(getter).toContain('{ rebuildCodexThread: true }');
expect(getter).toContain('pending.codexThreadRelinkCommitted');
expect(getter).toContain('{ codexThreadRelinkCommitted: true }');
expect(getter).toContain('pending.ownerScope');
expect(getter).toContain('pending.sourceCodexThreadModelProviderId');
expect(getter).toContain('pending.previousRoute');
expect(getter).toContain('pending.restoreStaleOwnerRoute');
});

it('restores the source route before an in-memory relink pending can be lost on restart', () => {
const registration = handlerBody(
registerSource,
'export async function registerPendingCredentialSwitchForSession(',
'/**\n * 跨订阅远端压缩身份边界时',
);
const restartSafeRestore = registration.indexOf(
'const restored = await restoreStaleOwnerRoute({',
);
const registrationAttempt = registration.indexOf('const registered = await service.register(');

expect(registration).toContain('model: capturedPrevRow.model');
expect(registration).toContain('providerId: capturedPrevRow.providerId');
expect(registration).toContain('effort: capturedPrevRow.effort');
expect(registration).toContain('fastMode: capturedPrevRow.fastMode');
expect(registration).toContain(
'Codex pending credential switch could not preserve the source route',
);
expect(restartSafeRestore).toBeGreaterThan(-1);
expect(registrationAttempt).toBeGreaterThan(restartSafeRestore);
});

it('binds deferred credential switches to the captured owner scope and runtime epoch', () => {
const registration = handlerBody(
registerSource,
'export async function registerPendingCredentialSwitchForSession(',
'/**\n * 跨订阅远端压缩身份边界时',
);
const service = handlerBody(
registerSource,
'const pendingCredentialSwitchService = new PendingCredentialSwitchService({',
'pendingCredentialSwitchHolder = pendingCredentialSwitchService;',
);

expect(registration).toContain('ownerScopeKey: activeOwnerScopeKey()');
expect(registration).toContain(
'runtimeOwnerEpoch: captureSessionRuntimeControlOwnerEpoch()',
);
expect(registration).toContain('target.ownerScope ??');
expect(registration).toContain('const dbSnapshot = getCurrentDbClientSnapshot();');
expect(registration).toContain('const sourcePersistedSession = target.sourcePersistedSession;');
expect(registration).toContain('sourcePersistedSession.sdkSessionId');
expect(registration).toContain('sourcePersistedSession.fastMode');
expect(registration).toContain('restoreCapturedProfileRoute');
expect(registration).toContain('const appliedRoute = persistedRoute ??');
expect(registration).toContain('eq(sessions.model, appliedRoute.model)');
expect(registration).toContain('sdkSessionId: capturedPrevRow.sdkSessionId');
expect(registration).toContain('model: capturedPrevRow.model');
expect(registration).toContain('providerId: capturedPrevRow.providerId');
expect(registration).toContain('effort: capturedPrevRow.effort');
expect(registration).toContain('fastMode: capturedPrevRow.fastMode');
expect(registration).toContain(
'Codex pending credential switch requires an old-profile rollback snapshot',
);
const registrationAttempt = registration.indexOf('const registered = await service.register(');
const registrationFailure = registration.indexOf('if (!registered) {', registrationAttempt);
const failureReturn = registration.indexOf(
"throw new Error('Pending credential switch owner changed during registration')",
registrationFailure,
);
expect(registrationAttempt).toBeGreaterThan(-1);
expect(registrationFailure).toBeGreaterThan(registrationAttempt);
expect(failureReturn).toBeGreaterThan(registrationFailure);
expect(pendingCredentialSwitchSource).toContain('restoreStaleOwnerRoute');
expect(pendingCredentialSwitchSource).toContain(
'await this.compensateStaleOwnerRoute(sessionId, pending);',
);
expect(pendingCredentialSwitchSource).toContain('this.staleDiscards.has(target)');
expect(service).toContain('!isAppSessionBoundaryPending()');
expect(service).toContain('activeOwnerScopeKey() === scope.ownerScopeKey');
expect(service).toContain(
'sessionRuntimeControlOwnerEpochMatches(scope.runtimeOwnerEpoch)',
);
});

it('commits user effort and Fast state only after the live runtime call succeeds', () => {
const effort = handlerBody(
registerSource,
Expand Down
Loading
Loading