fix: release Codex lease after final output
This commit is contained in:
@@ -22,6 +22,11 @@ interface RunSpawnedAgentProcessArgs {
|
||||
logsDir: string;
|
||||
startTime: number;
|
||||
onOutput?: (output: AgentOutput) => Promise<void>;
|
||||
onTerminalStreamedOutputFlushed?: (output: AgentOutput) => void;
|
||||
}
|
||||
|
||||
function isTerminalStreamedOutput(output: AgentOutput): boolean {
|
||||
return (output.phase ?? 'final') !== 'progress';
|
||||
}
|
||||
|
||||
function parseLegacyAgentOutput(stdout: string): AgentOutput {
|
||||
@@ -195,9 +200,16 @@ export function runSpawnedAgentProcess(
|
||||
'Streamed agent error output',
|
||||
);
|
||||
}
|
||||
outputChain = outputChain
|
||||
.then(() => onOutput(parsed))
|
||||
.catch((err) => logStreamedOutputDeliveryError(err, group, input));
|
||||
outputChain = outputChain.then(async () => {
|
||||
try {
|
||||
await onOutput(parsed);
|
||||
if (isTerminalStreamedOutput(parsed)) {
|
||||
args.onTerminalStreamedOutputFlushed?.(parsed);
|
||||
}
|
||||
} catch (err) {
|
||||
logStreamedOutputDeliveryError(err, group, input);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
{
|
||||
|
||||
@@ -235,6 +235,68 @@ describe('agent-runner timeout behavior', () => {
|
||||
expect(result.newSessionId).toBe('session-456');
|
||||
});
|
||||
|
||||
it('releases Codex auth lease after final streamed output flushes even before process close', async () => {
|
||||
const releaseLease = vi.fn();
|
||||
const prepareGroupEnvironmentSpy = vi
|
||||
.spyOn(agentRunnerEnvironment, 'prepareGroupEnvironment')
|
||||
.mockReturnValueOnce({
|
||||
env: {
|
||||
EJCLAW_IPC_DIR: '/tmp/ejclaw-test-data/ipc/test-group',
|
||||
},
|
||||
groupDir: '/tmp/ejclaw-test-groups/test-group',
|
||||
runnerDir: '/tmp/ejclaw-test-runners/codex-runner',
|
||||
codexSessionAuth: {
|
||||
canonicalAuthPath: '/tmp/codex-account/auth.json',
|
||||
sessionAuthPath: '/tmp/codex-session/auth.json',
|
||||
accountIndex: 0,
|
||||
lease: {
|
||||
accountIndex: 0,
|
||||
authPath: '/tmp/codex-account/auth.json',
|
||||
release: releaseLease,
|
||||
},
|
||||
},
|
||||
});
|
||||
const onOutput = vi.fn(async () => {});
|
||||
const codexGroup: RegisteredGroup = {
|
||||
...testGroup,
|
||||
agentType: 'codex',
|
||||
};
|
||||
let resultPromise: Promise<AgentOutput> | undefined;
|
||||
|
||||
try {
|
||||
resultPromise = runAgentProcess(
|
||||
codexGroup,
|
||||
{
|
||||
...testInput,
|
||||
runId: 'run-codex-lease-final',
|
||||
},
|
||||
() => {},
|
||||
onOutput,
|
||||
);
|
||||
|
||||
emitOutputMarker(fakeProc, {
|
||||
status: 'success',
|
||||
result: 'TASK_DONE\n작업 완료',
|
||||
phase: 'final',
|
||||
newSessionId: 'codex-session-lease-final',
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(10);
|
||||
|
||||
expect(onOutput).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
result: 'TASK_DONE\n작업 완료',
|
||||
phase: 'final',
|
||||
}),
|
||||
);
|
||||
expect(releaseLease).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
fakeProc.emit('close', 0);
|
||||
await vi.advanceTimersByTimeAsync(10);
|
||||
await resultPromise?.catch(() => undefined);
|
||||
prepareGroupEnvironmentSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it('preserves streamed progress phase metadata', async () => {
|
||||
const onOutput = vi.fn(async () => {});
|
||||
const resultPromise = runAgentProcess(
|
||||
|
||||
@@ -108,6 +108,17 @@ function finalizeCodexAuthSession(
|
||||
}
|
||||
}
|
||||
|
||||
function createCodexAuthSessionFinalizer(
|
||||
getAuth: () => PreparedCodexSessionAuth | null | undefined,
|
||||
): () => void {
|
||||
let finalized = false;
|
||||
return () => {
|
||||
if (finalized) return;
|
||||
finalized = true;
|
||||
finalizeCodexAuthSession(getAuth());
|
||||
};
|
||||
}
|
||||
|
||||
export async function runAgentProcess(
|
||||
group: RegisteredGroup,
|
||||
input: AgentInput,
|
||||
@@ -175,6 +186,9 @@ export async function runAgentProcess(
|
||||
const safeName = group.folder.replace(/[^a-zA-Z0-9-]/g, '-');
|
||||
const processSuffix = input.runId || `${Date.now()}`;
|
||||
const processName = `ejclaw-${safeName}-${processSuffix}`;
|
||||
const finalizeCodexAuthSessionOnce = createCodexAuthSessionFinalizer(
|
||||
() => codexSessionAuth,
|
||||
);
|
||||
|
||||
// Check if runner is built
|
||||
const distEntry = path.join(runnerDir, 'dist', 'index.js');
|
||||
@@ -231,13 +245,14 @@ export async function runAgentProcess(
|
||||
logsDir,
|
||||
startTime,
|
||||
onOutput,
|
||||
onTerminalStreamedOutputFlushed: finalizeCodexAuthSessionOnce,
|
||||
})
|
||||
.then((output) => {
|
||||
finalizeCodexAuthSession(codexSessionAuth);
|
||||
finalizeCodexAuthSessionOnce();
|
||||
resolve(output);
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
finalizeCodexAuthSession(codexSessionAuth);
|
||||
finalizeCodexAuthSessionOnce();
|
||||
logger.error(
|
||||
{ err, processName, chatJid: input.chatJid, runId: input.runId },
|
||||
'Spawned agent process runner failed',
|
||||
|
||||
Reference in New Issue
Block a user