Skip to content

Commit 35b2e70

Browse files
committed
refactor(agent-manager): simplify codex matching flow
1 parent 0757cf1 commit 35b2e70

6 files changed

Lines changed: 139 additions & 70 deletions

File tree

docs/ai/design/feature-codex-adapter-agent-manager-package.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ Responsibilities:
9191
- Rationale: preserves cross-agent behavior consistency and avoids adapter-specific drift.
9292
- Decision: Use `codex-<session-id-prefix>` fallback naming when `cwd` is unavailable.
9393
- Rationale: keeps identifiers deterministic and short while remaining user-readable.
94+
- Decision: Keep matching orchestration in explicit phases (`cwd`, `missing-cwd`, `any`) with extracted helper methods and PID/session tracking sets.
95+
- Rationale: preserves behavior while reducing branching complexity and repeated scans in `detectAgents`.
9496

9597
## Non-Functional Requirements
9698

docs/ai/implementation/feature-codex-adapter-agent-manager-package.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ description: Implementation notes for Codex adapter support in package agent man
4040
- Follow `ClaudeCodeAdapter` structure for consistency.
4141
- Keep adapter-specific parsing in adapter module; keep formatting in CLI.
4242
- Fail soft on malformed/partial entries; avoid throwing across adapter boundary.
43+
- Keep `detectAgents` orchestration readable via small private helpers for each matching stage.
4344

4445
## Integration Points
4546

@@ -78,3 +79,13 @@ description: Implementation notes for Codex adapter support in package agent man
7879
- `PROCESS_SESSION_TIME_TOLERANCE_MS`
7980
- `PROCESS_START_DAY_WINDOW_DAYS`
8081
- session-scan bound constants (`MIN/MAX/SCAN_MULTIPLIER`)
82+
- Simplification refactor (no behavior change):
83+
- extracted orchestration helpers:
84+
- `listCodexProcesses`
85+
- `calculateSessionScanLimit`
86+
- `assignSessionsForMode`
87+
- `addMappedSessionAgent`
88+
- `addProcessOnlyAgent`
89+
- `filterCandidateSessions`
90+
- `rankCandidatesByStartTime`
91+
- replaced repeated `agents.some(...)` PID checks with `assignedPids` set tracking

docs/ai/planning/feature-codex-adapter-agent-manager-package.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ description: Task plan for adding Codex adapter support and integrating it into
4040
- [x] Task 3.2: Validate and document
4141
- Run lint/build/tests for affected projects
4242
- Record implementation + testing outcomes in docs/ai
43+
- [x] Task 3.3: Simplify implementation structure without behavior changes
44+
- Extract matching orchestration and ranking helpers for readability
45+
- Replace repeated PID lookup scans with set-based tracking
4346

4447
## Dependencies
4548

@@ -71,4 +74,4 @@ description: Task plan for adding Codex adapter support and integrating it into
7174

7275
## Progress Summary
7376

74-
Implementation scope is complete. `CodexAdapter` was added to `@ai-devkit/agent-manager`, exported through package entry points, and registered in CLI agent command flows. Follow-up fixes addressed three runtime issues: false-positive process matching, missing long-lived session links, and list latency from broad session scans. Matching now uses `etime`-based process start time with configurable tolerance and process-start day-window session inclusion, while keeping a bounded recent-file scan for performance. Focused adapter and CLI agent-command tests pass, and package lint/build/test checks pass for affected areas. Full `cli:test` includes unrelated pre-existing module-resolution failures (`@ai-devkit/memory` and workspace package mocking) and is tracked as external validation noise rather than a feature regression.
77+
Implementation scope is complete. `CodexAdapter` was added to `@ai-devkit/agent-manager`, exported through package entry points, and registered in CLI agent command flows. Follow-up fixes addressed false-positive process matching, missing long-lived session links, and list latency from broad session scans. Matching now uses `etime`-based process start time with configurable tolerance and process-start day-window session inclusion, while keeping a bounded recent-file scan for performance. A final simplification pass extracted helper methods for match phases/ranking and introduced set-based PID assignment tracking; behavior is unchanged with focused tests still passing.

docs/ai/requirements/feature-codex-adapter-agent-manager-package.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Who is affected:
2828
- Reuse shared process/file utilities and adapter contract patterns
2929
- Add tests for Codex adapter discovery, status mapping, and command metadata
3030
- Establish a clean extension path for future adapters (Gemini/others)
31+
- Keep Codex adapter internals maintainable via small helper functions without changing runtime behavior
3132

3233
### Non-Goals
3334
- Reworking overall `ai-devkit agent` UX
@@ -48,6 +49,7 @@ Who is affected:
4849
- `packages/cli/src/commands/agent.ts` registers `CodexAdapter` from package exports
4950
- Unit tests cover Codex adapter happy path, empty/no-session path, and invalid data handling
5051
- Existing agent command tests continue to pass without regressions
52+
- Implementation remains readable enough for future adapter extension work (clear matching phases/helpers)
5153

5254
## Constraints & Assumptions
5355

docs/ai/testing/feature-codex-adapter-agent-manager-package.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ description: Test strategy and coverage plan for Codex adapter integration
5959

6060
Coverage and residual gaps:
6161
- New Codex adapter unit suite (`CodexAdapter.test.ts`) is passing with coverage on detection, filtering, status mapping, fallback naming, and time-based matching.
62+
- Post-simplification verification: focused Codex adapter tests and lint still pass after helper extraction/set-based PID tracking refactor.
6263
- Full `npx nx run cli:test` currently fails due to unrelated pre-existing module-resolution issues in `memory.test.ts` and baseline `agent.test.ts` mocking behavior when running the entire suite without focused filtering.
6364
- Runtime validation confirmed targeted mapping: PID `81442` maps to session `019c7024-89e6-7880-81eb-1417bd2177b5` after time-based matching + process-day window logic.
6465

packages/agent-manager/src/adapters/CodexAdapter.ts

Lines changed: 119 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ interface CodexSession {
4343
lastPayloadType?: string;
4444
}
4545

46+
type SessionMatchMode = 'cwd' | 'missing-cwd' | 'any';
47+
4648
export class CodexAdapter implements AgentAdapter {
4749
readonly type = 'codex' as const;
4850

@@ -69,23 +71,15 @@ export class CodexAdapter implements AgentAdapter {
6971
}
7072

7173
async detectAgents(): Promise<AgentInfo[]> {
72-
const codexProcesses = listProcesses({ namePattern: 'codex' }).filter((processInfo) =>
73-
this.canHandle(processInfo),
74-
);
74+
const codexProcesses = this.listCodexProcesses();
7575

7676
if (codexProcesses.length === 0) {
7777
return [];
7878
}
7979

8080
const processStartByPid = this.getProcessStartTimes(codexProcesses.map((processInfo) => processInfo.pid));
8181

82-
const sessionScanLimit = Math.min(
83-
Math.max(
84-
codexProcesses.length * CodexAdapter.SESSION_SCAN_MULTIPLIER,
85-
CodexAdapter.MIN_SESSION_SCAN,
86-
),
87-
CodexAdapter.MAX_SESSION_SCAN,
88-
);
82+
const sessionScanLimit = this.calculateSessionScanLimit(codexProcesses.length);
8983
const sessions = this.readSessions(sessionScanLimit, processStartByPid);
9084
if (sessions.length === 0) {
9185
return codexProcesses.map((processInfo) =>
@@ -97,72 +91,114 @@ export class CodexAdapter implements AgentAdapter {
9791
(a, b) => b.lastActive.getTime() - a.lastActive.getTime(),
9892
);
9993
const usedSessionIds = new Set<string>();
94+
const assignedPids = new Set<number>();
10095
const agents: AgentInfo[] = [];
10196

102-
// First pass: match sessions by exact cwd, then closest start time.
103-
for (const processInfo of codexProcesses) {
104-
const match = this.selectBestSession(
105-
processInfo,
106-
sortedSessions,
107-
usedSessionIds,
108-
processStartByPid,
109-
'cwd',
110-
);
97+
// Match exact cwd first, then missing-cwd sessions, then any available session.
98+
this.assignSessionsForMode(
99+
'cwd',
100+
codexProcesses,
101+
sortedSessions,
102+
usedSessionIds,
103+
assignedPids,
104+
processStartByPid,
105+
agents,
106+
);
107+
this.assignSessionsForMode(
108+
'missing-cwd',
109+
codexProcesses,
110+
sortedSessions,
111+
usedSessionIds,
112+
assignedPids,
113+
processStartByPid,
114+
agents,
115+
);
116+
this.assignSessionsForMode(
117+
'any',
118+
codexProcesses,
119+
sortedSessions,
120+
usedSessionIds,
121+
assignedPids,
122+
processStartByPid,
123+
agents,
124+
);
111125

112-
if (!match) {
126+
// Every running codex process should still be listed.
127+
for (const processInfo of codexProcesses) {
128+
if (assignedPids.has(processInfo.pid)) {
113129
continue;
114130
}
115131

116-
usedSessionIds.add(match.sessionId);
117-
agents.push(this.mapSessionToAgent(match, processInfo, agents));
132+
this.addProcessOnlyAgent(processInfo, assignedPids, agents);
118133
}
119134

120-
// Second pass: allow missing-cwd sessions to attach to unmatched codex processes.
121-
for (const processInfo of codexProcesses) {
122-
if (agents.some((agent) => agent.pid === processInfo.pid)) {
123-
continue;
124-
}
125-
126-
const match = this.selectBestSession(
127-
processInfo,
128-
sortedSessions,
129-
usedSessionIds,
130-
processStartByPid,
131-
'missing-cwd',
132-
);
135+
return agents;
136+
}
133137

134-
if (!match) {
135-
continue;
136-
}
138+
private listCodexProcesses(): ProcessInfo[] {
139+
return listProcesses({ namePattern: 'codex' }).filter((processInfo) =>
140+
this.canHandle(processInfo),
141+
);
142+
}
137143

138-
usedSessionIds.add(match.sessionId);
139-
agents.push(this.mapSessionToAgent(match, processInfo, agents));
140-
}
144+
private calculateSessionScanLimit(processCount: number): number {
145+
return Math.min(
146+
Math.max(
147+
processCount * CodexAdapter.SESSION_SCAN_MULTIPLIER,
148+
CodexAdapter.MIN_SESSION_SCAN,
149+
),
150+
CodexAdapter.MAX_SESSION_SCAN,
151+
);
152+
}
141153

142-
// Third pass: every running codex process should still be listed.
154+
private assignSessionsForMode(
155+
mode: SessionMatchMode,
156+
codexProcesses: ProcessInfo[],
157+
sessions: CodexSession[],
158+
usedSessionIds: Set<string>,
159+
assignedPids: Set<number>,
160+
processStartByPid: Map<number, Date>,
161+
agents: AgentInfo[],
162+
): void {
143163
for (const processInfo of codexProcesses) {
144-
if (agents.some((agent) => agent.pid === processInfo.pid)) {
164+
if (assignedPids.has(processInfo.pid)) {
145165
continue;
146166
}
147167

148-
const fallbackSession = this.selectBestSession(
168+
const session = this.selectBestSession(
149169
processInfo,
150-
sortedSessions,
170+
sessions,
151171
usedSessionIds,
152172
processStartByPid,
153-
'any',
173+
mode,
154174
);
155-
156-
if (fallbackSession) {
157-
usedSessionIds.add(fallbackSession.sessionId);
158-
agents.push(this.mapSessionToAgent(fallbackSession, processInfo, agents));
175+
if (!session) {
159176
continue;
160177
}
161178

162-
agents.push(this.mapProcessOnlyAgent(processInfo, agents));
179+
this.addMappedSessionAgent(session, processInfo, usedSessionIds, assignedPids, agents);
163180
}
181+
}
164182

165-
return agents;
183+
private addMappedSessionAgent(
184+
session: CodexSession,
185+
processInfo: ProcessInfo,
186+
usedSessionIds: Set<string>,
187+
assignedPids: Set<number>,
188+
agents: AgentInfo[],
189+
): void {
190+
usedSessionIds.add(session.sessionId);
191+
assignedPids.add(processInfo.pid);
192+
agents.push(this.mapSessionToAgent(session, processInfo, agents));
193+
}
194+
195+
private addProcessOnlyAgent(
196+
processInfo: ProcessInfo,
197+
assignedPids: Set<number>,
198+
agents: AgentInfo[],
199+
): void {
200+
assignedPids.add(processInfo.pid);
201+
agents.push(this.mapProcessOnlyAgent(processInfo, agents));
166202
}
167203

168204
private mapSessionToAgent(
@@ -260,14 +296,13 @@ export class CodexAdapter implements AgentAdapter {
260296
}
261297
}
262298

263-
const selectedPaths = new Set(
264-
files
299+
const recentFiles = files
265300
.sort((a, b) => b.mtimeMs - a.mtimeMs)
266301
.slice(0, limit)
267-
.map((file) => file.path),
268-
);
269-
302+
.map((file) => file.path);
270303
const processDayFiles = this.findProcessDaySessionFiles(processStartByPid);
304+
305+
const selectedPaths = new Set(recentFiles);
271306
for (const processDayFile of processDayFiles) {
272307
selectedPaths.add(processDayFile);
273308
}
@@ -385,9 +420,29 @@ export class CodexAdapter implements AgentAdapter {
385420
sessions: CodexSession[],
386421
usedSessionIds: Set<string>,
387422
processStartByPid: Map<number, Date>,
388-
mode: 'cwd' | 'missing-cwd' | 'any',
423+
mode: SessionMatchMode,
389424
): CodexSession | undefined {
390-
const candidates = sessions.filter((session) => {
425+
const candidates = this.filterCandidateSessions(processInfo, sessions, usedSessionIds, mode);
426+
427+
if (candidates.length === 0) {
428+
return undefined;
429+
}
430+
431+
const processStart = processStartByPid.get(processInfo.pid);
432+
if (!processStart) {
433+
return candidates.sort((a, b) => b.lastActive.getTime() - a.lastActive.getTime())[0];
434+
}
435+
436+
return this.rankCandidatesByStartTime(candidates, processStart)[0];
437+
}
438+
439+
private filterCandidateSessions(
440+
processInfo: ProcessInfo,
441+
sessions: CodexSession[],
442+
usedSessionIds: Set<string>,
443+
mode: SessionMatchMode,
444+
): CodexSession[] {
445+
return sessions.filter((session) => {
391446
if (usedSessionIds.has(session.sessionId)) {
392447
return false;
393448
}
@@ -402,17 +457,11 @@ export class CodexAdapter implements AgentAdapter {
402457

403458
return true;
404459
});
460+
}
405461

406-
if (candidates.length === 0) {
407-
return undefined;
408-
}
409-
410-
const processStart = processStartByPid.get(processInfo.pid);
411-
if (!processStart) {
412-
return candidates.sort((a, b) => b.lastActive.getTime() - a.lastActive.getTime())[0];
413-
}
414-
462+
private rankCandidatesByStartTime(candidates: CodexSession[], processStart: Date): CodexSession[] {
415463
const toleranceMs = CodexAdapter.PROCESS_SESSION_TIME_TOLERANCE_MS;
464+
416465
return candidates
417466
.map((session) => {
418467
const diffMs = Math.abs(session.sessionStart.getTime() - processStart.getTime());
@@ -428,7 +477,8 @@ export class CodexAdapter implements AgentAdapter {
428477
if (a.rank !== b.rank) return a.rank - b.rank;
429478
if (a.diffMs !== b.diffMs) return a.diffMs - b.diffMs;
430479
return b.recency - a.recency;
431-
})[0]?.session;
480+
})
481+
.map((ranked) => ranked.session);
432482
}
433483

434484
private getProcessStartTimes(pids: number[]): Map<number, Date> {

0 commit comments

Comments
 (0)