diff --git a/.gitignore b/.gitignore index 7c00804..910a619 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ tmp/ backups/ dist-backups/ .deploy-backups/ +recovery/ cache/ runners/*/node_modules/ runners/codex-runner-backups/ diff --git a/src/agent-runner.test.ts b/src/agent-runner.test.ts index 9ab63c3..8dc1178 100644 --- a/src/agent-runner.test.ts +++ b/src/agent-runner.test.ts @@ -474,6 +474,55 @@ describe('agent-runner timeout behavior', () => { expect(spawnEnv?.CODEX_HOME).toBe('/tmp/host-reviewer-session/.codex'); }); + it('releases the initial Codex auth lease when read-only session preparation fails', async () => { + vi.useRealTimers(); + fakeProc = createFakeProcess(); + + const releaseLease = vi.fn(); + const prepareGroupEnvironmentSpy = mockCodexLeaseEnvironment(releaseLease); + const readonlyError = new Error('Codex rotation pool unavailable'); + const prepareReadonlySessionEnvironmentSpy = vi + .spyOn(agentRunnerEnvironment, 'prepareReadonlySessionEnvironment') + .mockImplementationOnce(() => { + throw readonlyError; + }); + const codexGroup: RegisteredGroup = { + ...testGroup, + agentType: 'codex', + }; + + try { + await expect( + runAgentProcess( + codexGroup, + { + ...testInput, + roomRoleContext: { + serviceId: 'codex-review', + role: 'reviewer', + ownerServiceId: 'codex-main', + reviewerServiceId: 'codex-review', + failoverOwner: false, + }, + }, + () => {}, + async () => {}, + { + EJCLAW_UNSAFE_HOST_PAIRED_MODE: '1', + CLAUDE_CONFIG_DIR: '/tmp/host-reviewer-session', + EJCLAW_WORK_DIR: '/tmp/paired/task-1/reviewer', + }, + ), + ).rejects.toThrow(readonlyError); + + expect(prepareReadonlySessionEnvironmentSpy).toHaveBeenCalledTimes(1); + expect(releaseLease).toHaveBeenCalledTimes(1); + } finally { + prepareReadonlySessionEnvironmentSpy.mockRestore(); + prepareGroupEnvironmentSpy.mockRestore(); + } + }); + it('serializes roomRoleContext into the runner stdin payload', async () => { vi.useRealTimers(); fakeProc = createFakeProcess(); diff --git a/src/agent-runner.ts b/src/agent-runner.ts index 3b7a7dd..0bf1cb0 100644 --- a/src/agent-runner.ts +++ b/src/agent-runner.ts @@ -157,21 +157,31 @@ export async function runAgentProcess( (input.roomRoleContext?.role === 'reviewer' || input.roomRoleContext?.role === 'arbiter') ) { - const readonlySession = prepareReadonlySessionEnvironment({ - sessionDir: envOverrides.CLAUDE_CONFIG_DIR, - chatJid: input.chatJid, - isMain: input.isMain, - groupFolder: group.folder, - agentType: group.agentType || 'claude-code', - memoryBriefing: input.memoryBriefing, - role: input.roomRoleContext.role, - ipcDir: env[EJCLAW_ENV.ipcDir], - hostIpcDir: env[EJCLAW_ENV.hostIpcDir], - workDir: envOverrides[EJCLAW_ENV.workDir] || env[EJCLAW_ENV.workDir], - skillOverrides, - }); - if ((group.agentType || 'claude-code') === 'codex') { - releaseCodexAuthSession(codexSessionAuth); + const isCodexGroup = (group.agentType || 'claude-code') === 'codex'; + const previousCodexSessionAuth = codexSessionAuth; + let readonlySession: ReturnType; + try { + readonlySession = prepareReadonlySessionEnvironment({ + sessionDir: envOverrides.CLAUDE_CONFIG_DIR, + chatJid: input.chatJid, + isMain: input.isMain, + groupFolder: group.folder, + agentType: group.agentType || 'claude-code', + memoryBriefing: input.memoryBriefing, + role: input.roomRoleContext.role, + ipcDir: env[EJCLAW_ENV.ipcDir], + hostIpcDir: env[EJCLAW_ENV.hostIpcDir], + workDir: envOverrides[EJCLAW_ENV.workDir] || env[EJCLAW_ENV.workDir], + skillOverrides, + }); + } catch (err) { + if (isCodexGroup) { + releaseCodexAuthSession(previousCodexSessionAuth); + } + throw err; + } + if (isCodexGroup) { + releaseCodexAuthSession(previousCodexSessionAuth); codexSessionAuth = readonlySession.codexSessionAuth; env.CODEX_HOME = path.join(envOverrides.CLAUDE_CONFIG_DIR, '.codex'); if (readonlySession.codexHomeDir) {