Skip to content

Commit 70bb669

Browse files
refactor(codex): split app-server root turn detection
1 parent ce88d52 commit 70bb669

4 files changed

Lines changed: 70 additions & 37 deletions

File tree

docs/intentional-architecture-rewrite-2026-06-27/worklog.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -659,3 +659,9 @@ Ninety-seventh production slice:
659659
- Split sync workflow contracts into `src/services/sync/types.ts`.
660660
- Moved `SyncWorkflowOptions`, `SyncWorkflowSummary`, item shapes, and result union out of `src/services/sync/sync.ts`.
661661
- Kept `src/services/sync/sync.ts` focused on source parsing, sweep orchestration, provider selection, and Absorb handoff.
662+
663+
Ninety-eighth production slice:
664+
665+
- Split Codex app-server root turn detection into `src/harness/providers/codex/app-server-root-turn.ts`.
666+
- Moved root turn/thread completion checks and notification turn ID extraction out of the app-server process run loop.
667+
- Kept `src/harness/providers/codex/app-server.ts` focused on process lifecycle, RPC wiring, notification mapping, timeout handling, and result finalization.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import {
2+
asRecord,
3+
stringField,
4+
} from "./fields.js";
5+
import type { CodexRunState, JsonRpcNotification } from "./types.js";
6+
7+
export function codexNotificationTurnId(
8+
message: JsonRpcNotification,
9+
): string | undefined {
10+
return stringField(asRecord(message.params), "turnId");
11+
}
12+
13+
export function isCodexRootTurnCompletion(args: {
14+
message: JsonRpcNotification;
15+
state: CodexRunState;
16+
activeTurnId: string | undefined;
17+
}): boolean | undefined {
18+
if (args.message.method !== "turn/completed") return undefined;
19+
20+
const params = asRecord(args.message.params);
21+
const completedTurnId = stringField(params, "turnId");
22+
const completedThreadId = stringField(params, "threadId");
23+
const isRootTurn =
24+
(args.state.rootTurnId !== undefined &&
25+
completedTurnId === args.state.rootTurnId) ||
26+
(args.state.rootTurnId === undefined &&
27+
args.activeTurnId !== undefined &&
28+
completedTurnId === args.activeTurnId);
29+
const isRootThread =
30+
args.state.rootThreadId !== undefined &&
31+
completedThreadId === args.state.rootThreadId;
32+
return isRootTurn || isRootThread;
33+
}
34+
35+
export function isCodexRootThreadNotification(args: {
36+
message: JsonRpcNotification;
37+
state: CodexRunState;
38+
}): boolean {
39+
const threadId = stringField(asRecord(args.message.params), "threadId");
40+
return (
41+
args.state.rootThreadId !== undefined &&
42+
threadId === args.state.rootThreadId
43+
);
44+
}

src/harness/providers/codex/app-server.ts

Lines changed: 12 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@ import {
1111
type CodexAppServerSandboxMode,
1212
} from "./app-server-config.js";
1313
import { createCodexAppServerRpcTransport } from "./app-server-rpc.js";
14+
import {
15+
codexNotificationTurnId,
16+
isCodexRootThreadNotification,
17+
isCodexRootTurnCompletion,
18+
} from "./app-server-root-turn.js";
1419
import { startCodexAppServerTurn } from "./app-server-session.js";
1520
import { classifyCodexFailure } from "./failures.js";
16-
import {
17-
asRecord,
18-
stringField,
19-
} from "./fields.js";
2021
import { toHarnessResult } from "./result.js";
2122
import { respondToCodexServerRequest } from "./server-requests.js";
2223
import type { CodexRunState, JsonRpcNotification } from "./types.js";
@@ -104,15 +105,19 @@ export async function runCodexAppServer(
104105
};
105106

106107
const handleNotification = (message: JsonRpcNotification): void => {
107-
const turnId = stringField(asRecord(message.params), "turnId");
108+
const turnId = codexNotificationTurnId(message);
108109
if (
109110
activeTurnId === undefined &&
110111
turnId !== undefined &&
111-
isRootThreadNotification(message, state)
112+
isCodexRootThreadNotification({ message, state })
112113
) {
113114
activeTurnId = turnId;
114115
}
115-
const isRootCompletion = isRootTurnCompletion(message, state, activeTurnId);
116+
const isRootCompletion = isCodexRootTurnCompletion({
117+
message,
118+
state,
119+
activeTurnId,
120+
});
116121
const events = mapCodexAppServerNotification(message, state, {
117122
activeTurnId,
118123
rootThreadId: state.rootThreadId,
@@ -239,33 +244,3 @@ async function terminateManagedChildSafely(
239244
return `Provider process cleanup failed: ${err instanceof Error ? err.message : String(err)}`;
240245
}
241246
}
242-
243-
function isRootTurnCompletion(
244-
message: JsonRpcNotification,
245-
state: CodexRunState,
246-
activeTurnId: string | undefined,
247-
): boolean | undefined {
248-
if (message.method !== "turn/completed") return undefined;
249-
const params = asRecord(message.params);
250-
const completedTurnId = stringField(params, "turnId");
251-
const completedThreadId = stringField(params, "threadId");
252-
const isRootTurn =
253-
(state.rootTurnId !== undefined && completedTurnId === state.rootTurnId) ||
254-
(state.rootTurnId === undefined &&
255-
activeTurnId !== undefined &&
256-
completedTurnId === activeTurnId);
257-
const isRootThread =
258-
state.rootThreadId !== undefined && completedThreadId === state.rootThreadId;
259-
return isRootTurn || isRootThread;
260-
}
261-
262-
function isRootThreadNotification(
263-
message: JsonRpcNotification,
264-
state: CodexRunState,
265-
): boolean {
266-
const completedThreadId = stringField(asRecord(message.params), "threadId");
267-
return (
268-
state.rootThreadId !== undefined &&
269-
completedThreadId === state.rootThreadId
270-
);
271-
}

test/architecture-boundaries.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -852,11 +852,15 @@ describe("architecture boundaries", () => {
852852
const appServerSession = await readSource(
853853
"src/harness/providers/codex/app-server-session.ts",
854854
);
855+
const appServerRootTurn = await readSource(
856+
"src/harness/providers/codex/app-server-root-turn.ts",
857+
);
855858

856859
expect(existsSync(join(ROOT, "src/harness/providers/codex/app-server-config.ts"))).toBe(true);
857860
expect(existsSync(join(ROOT, "src/harness/providers/codex/server-requests.ts"))).toBe(true);
858861
expect(existsSync(join(ROOT, "src/harness/providers/codex/app-server-session.ts"))).toBe(true);
859862
expect(existsSync(join(ROOT, "src/harness/providers/codex/app-server-rpc.ts"))).toBe(true);
863+
expect(existsSync(join(ROOT, "src/harness/providers/codex/app-server-root-turn.ts"))).toBe(true);
860864
expect(appServer).not.toContain("CODEALMANAC_CODEX_APP_SERVER");
861865
expect(appServer).not.toContain("function parsePositiveEnvInt");
862866
expect(appServer).not.toContain("interface PendingRequest");
@@ -871,13 +875,17 @@ describe("architecture boundaries", () => {
871875
expect(appServer).not.toContain("requestRpc(\"initialize\"");
872876
expect(appServer).not.toContain("requestRpc(\"thread/start\"");
873877
expect(appServer).not.toContain("requestRpc(\"turn/start\"");
878+
expect(appServer).not.toContain("function isRootTurnCompletion");
879+
expect(appServer).not.toContain("function isRootThreadNotification");
874880
expect(appServerSession).toContain("startCodexAppServerTurn");
875881
expect(appServerSession).toContain("requestRpc(\"initialize\"");
876882
expect(appServerSession).toContain("requestRpc(\"thread/start\"");
877883
expect(appServerSession).toContain("requestRpc(\"turn/start\"");
878884
expect(appServerRpc).toContain("interface PendingRequest");
879885
expect(appServerRpc).toContain("pending.set");
880886
expect(appServerRpc).toContain("function handleResponse");
887+
expect(appServerRootTurn).toContain("isCodexRootTurnCompletion");
888+
expect(appServerRootTurn).toContain("isCodexRootThreadNotification");
881889
});
882890

883891
it("keeps migrate legacy-sources adapter out of source migration mechanics", async () => {

0 commit comments

Comments
 (0)