fix(codex): ignore subagents in transcript fallback

This commit is contained in:
weishu
2026-07-22 09:20:00 +08:00
parent a9115715f8
commit 3208f139b5
5 changed files with 125 additions and 30 deletions
@@ -0,0 +1,6 @@
export function isCodexSubagentSource(value: unknown): boolean {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return false;
}
return Object.prototype.hasOwnProperty.call(value, 'subagent');
}
@@ -129,6 +129,42 @@ describe('codexTranscriptLocator', () => {
expect(new Set(ambiguous[0])).toEqual(new Set([first, second]));
});
it('ignores a review subagent and waits for the top-level review transcript', async () => {
const located: string[] = [];
const ambiguous: string[][] = [];
locator = createCodexTranscriptLocator({
cwd: '/tmp/project',
startupTimestampMs: Date.now(),
intervalMs: 25,
settlementMs: 50,
onLocated: (result) => located.push(result.transcriptPath),
onAmbiguous: (paths) => ambiguous.push(paths)
});
await locator.ready;
const reviewSubagent = await createTranscript(
'thread-review-subagent',
'/tmp/project',
{ subagent: 'review' }
);
const userEvent = (message: string) => `${JSON.stringify({
timestamp: new Date().toISOString(),
type: 'event_msg',
payload: { type: 'user_message', message }
})}\n`;
await appendFile(reviewSubagent, userEvent('review instructions'));
await wait(100);
expect(located).toEqual([]);
expect(ambiguous).toEqual([]);
const primary = await createTranscript('thread-review-primary', '/tmp/project', 'cli');
await appendFile(primary, userEvent('/review'));
await wait(150);
expect(located).toEqual([primary]);
expect(ambiguous).toEqual([]);
});
it('rejects candidates whose activity arrives in adjacent polling cycles', async () => {
const located: string[] = [];
const ambiguous: string[][] = [];
@@ -208,9 +244,13 @@ describe('codexTranscriptLocator', () => {
expect(located).toEqual([]);
});
it('polls only the exact resume transcript once it is found', async () => {
it('polls only the exact resume transcript once it is found, including a subagent', async () => {
const unrelated = await createTranscript('thread-unrelated', '/tmp/project');
const target = await createTranscript('thread-resume', '/tmp/original-project');
const target = await createTranscript(
'thread-resume',
'/tmp/original-project',
{ subagent: 'review' }
);
await appendFile(unrelated, `${JSON.stringify({
timestamp: new Date().toISOString(),
type: 'event_msg',
@@ -239,11 +279,15 @@ describe('codexTranscriptLocator', () => {
expect(ambiguous).toEqual([]);
});
async function createTranscript(sessionId: string, cwd: string): Promise<string> {
async function createTranscript(sessionId: string, cwd: string, source?: unknown): Promise<string> {
const transcriptPath = join(sessionDirectory, `rollout-${sessionId}.jsonl`);
await writeFile(transcriptPath, `${JSON.stringify({
type: 'session_meta',
payload: { id: sessionId, cwd }
payload: {
id: sessionId,
cwd,
...(source === undefined ? {} : { source })
}
})}\n`);
return transcriptPath;
}
+13 -4
View File
@@ -3,6 +3,7 @@ import { join, resolve } from 'node:path';
import { open, readdir, stat } from 'node:fs/promises';
import { logger } from '@/ui/logger';
import { convertCodexEvent, type CodexSessionEvent } from './codexEventConverter';
import { isCodexSubagentSource } from './codexSessionMetadata';
export type LocatedCodexTranscript = {
sessionId: string;
@@ -21,6 +22,7 @@ type TranscriptState = {
ino: number;
sessionId: string | null;
cwd: string | null;
isSubagent: boolean;
};
type CodexTranscriptLocatorOptions = {
@@ -170,7 +172,8 @@ class CodexTranscriptLocatorImpl {
mtimeMs: 0,
ino: fileStats.ino,
sessionId: null,
cwd: null
cwd: null,
isSubagent: false
};
const replaced = previous && previous.ino !== fileStats.ino;
@@ -186,7 +189,8 @@ class CodexTranscriptLocatorImpl {
mtimeMs: 0,
ino: fileStats.ino,
sessionId: null,
cwd: null
cwd: null,
isSubagent: false
};
} else if (previous
&& fileStats.size === previous.size
@@ -238,6 +242,9 @@ class CodexTranscriptLocatorImpl {
state.sessionId = asString(metadata?.id) ?? state.sessionId;
const eventCwd = asString(metadata?.cwd);
state.cwd = eventCwd ? normalizePath(eventCwd) : state.cwd;
if (metadata && Object.prototype.hasOwnProperty.call(metadata, 'source')) {
state.isSubagent = isCodexSubagentSource(metadata.source);
}
}
if (convertCodexEvent(event)?.userActivity) {
@@ -263,8 +270,10 @@ class CodexTranscriptLocatorImpl {
if (state.sessionId !== this.resumeSessionId) {
return null;
}
} else if (state.cwd !== this.targetCwd) {
return null;
} else {
if (state.isSubagent || state.cwd !== this.targetCwd) {
return null;
}
}
return { sessionId: state.sessionId, transcriptPath };