@ai-devkit/agent-manager 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/adapters/AgentAdapter.d.ts +2 -0
  2. package/dist/adapters/AgentAdapter.d.ts.map +1 -1
  3. package/dist/adapters/ClaudeCodeAdapter.d.ts +49 -38
  4. package/dist/adapters/ClaudeCodeAdapter.d.ts.map +1 -1
  5. package/dist/adapters/ClaudeCodeAdapter.js +286 -293
  6. package/dist/adapters/ClaudeCodeAdapter.js.map +1 -1
  7. package/dist/adapters/CodexAdapter.d.ts +32 -30
  8. package/dist/adapters/CodexAdapter.d.ts.map +1 -1
  9. package/dist/adapters/CodexAdapter.js +148 -284
  10. package/dist/adapters/CodexAdapter.js.map +1 -1
  11. package/dist/index.d.ts +1 -3
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +1 -10
  14. package/dist/index.js.map +1 -1
  15. package/dist/utils/index.d.ts +6 -3
  16. package/dist/utils/index.d.ts.map +1 -1
  17. package/dist/utils/index.js +12 -11
  18. package/dist/utils/index.js.map +1 -1
  19. package/dist/utils/matching.d.ts +39 -0
  20. package/dist/utils/matching.d.ts.map +1 -0
  21. package/dist/utils/matching.js +103 -0
  22. package/dist/utils/matching.js.map +1 -0
  23. package/dist/utils/process.d.ts +25 -40
  24. package/dist/utils/process.d.ts.map +1 -1
  25. package/dist/utils/process.js +151 -105
  26. package/dist/utils/process.js.map +1 -1
  27. package/dist/utils/session.d.ts +30 -0
  28. package/dist/utils/session.d.ts.map +1 -0
  29. package/dist/utils/session.js +101 -0
  30. package/dist/utils/session.js.map +1 -0
  31. package/package.json +2 -2
  32. package/src/__tests__/AgentManager.test.ts +0 -25
  33. package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +921 -205
  34. package/src/__tests__/adapters/CodexAdapter.test.ts +468 -269
  35. package/src/__tests__/utils/matching.test.ts +191 -0
  36. package/src/__tests__/utils/process.test.ts +202 -0
  37. package/src/__tests__/utils/session.test.ts +117 -0
  38. package/src/adapters/AgentAdapter.ts +3 -0
  39. package/src/adapters/ClaudeCodeAdapter.ts +341 -418
  40. package/src/adapters/CodexAdapter.ts +155 -420
  41. package/src/index.ts +1 -3
  42. package/src/utils/index.ts +6 -3
  43. package/src/utils/matching.ts +92 -0
  44. package/src/utils/process.ts +133 -119
  45. package/src/utils/session.ts +92 -0
  46. package/dist/utils/file.d.ts +0 -52
  47. package/dist/utils/file.d.ts.map +0 -1
  48. package/dist/utils/file.js +0 -135
  49. package/dist/utils/file.js.map +0 -1
  50. package/src/utils/file.ts +0 -100
@@ -1,29 +1,23 @@
1
1
  /**
2
2
  * Codex Adapter
3
3
  *
4
- * Detects running Codex agents by combining:
5
- * 1. Running `codex` processes
6
- * 2. Session metadata under ~/.codex/sessions
4
+ * Detects running Codex agents by:
5
+ * 1. Finding running codex processes via shared listAgentProcesses()
6
+ * 2. Enriching with CWD and start times via shared enrichProcesses()
7
+ * 3. Discovering session files from ~/.codex/sessions/YYYY/MM/DD/ via shared batchGetSessionFileBirthtimes()
8
+ * 4. Setting resolvedCwd from session_meta first line
9
+ * 5. Matching sessions to processes via shared matchProcessesToSessions()
10
+ * 6. Extracting summary from last event entry in session JSONL
7
11
  */
8
12
 
9
13
  import * as fs from 'fs';
10
14
  import * as path from 'path';
11
- import { execSync } from 'child_process';
12
15
  import type { AgentAdapter, AgentInfo, ProcessInfo } from './AgentAdapter';
13
16
  import { AgentStatus } from './AgentAdapter';
14
- import { listProcesses } from '../utils/process';
15
- import { readJsonLines } from '../utils/file';
16
-
17
- interface CodexSessionMetaPayload {
18
- id?: string;
19
- timestamp?: string;
20
- cwd?: string;
21
- }
22
-
23
- interface CodexSessionMetaEntry {
24
- type?: string;
25
- payload?: CodexSessionMetaPayload;
26
- }
17
+ import { listAgentProcesses, enrichProcesses } from '../utils/process';
18
+ import { batchGetSessionFileBirthtimes } from '../utils/session';
19
+ import type { SessionFile } from '../utils/session';
20
+ import { matchProcessesToSessions, generateAgentName } from '../utils/matching';
27
21
 
28
22
  interface CodexEventEntry {
29
23
  timestamp?: string;
@@ -31,6 +25,9 @@ interface CodexEventEntry {
31
25
  payload?: {
32
26
  type?: string;
33
27
  message?: string;
28
+ id?: string;
29
+ cwd?: string;
30
+ timestamp?: string;
34
31
  };
35
32
  }
36
33
 
@@ -43,21 +40,12 @@ interface CodexSession {
43
40
  lastPayloadType?: string;
44
41
  }
45
42
 
46
- type SessionMatchMode = 'cwd' | 'missing-cwd' | 'any';
47
-
48
43
  export class CodexAdapter implements AgentAdapter {
49
44
  readonly type = 'codex' as const;
50
45
 
51
- /** Keep status thresholds aligned across adapters. */
52
46
  private static readonly IDLE_THRESHOLD_MINUTES = 5;
53
- /** Limit session parsing per run to keep list latency bounded. */
54
- private static readonly MIN_SESSION_SCAN = 12;
55
- private static readonly MAX_SESSION_SCAN = 40;
56
- private static readonly SESSION_SCAN_MULTIPLIER = 4;
57
- /** Also include session files around process start day to recover long-lived processes. */
47
+ /** Include session files around process start day to recover long-lived processes. */
58
48
  private static readonly PROCESS_START_DAY_WINDOW_DAYS = 1;
59
- /** Matching tolerance between process start time and session start time. */
60
- private static readonly PROCESS_SESSION_TIME_TOLERANCE_MS = 2 * 60 * 1000;
61
49
 
62
50
  private codexSessionsDir: string;
63
51
 
@@ -70,273 +58,113 @@ export class CodexAdapter implements AgentAdapter {
70
58
  return this.isCodexExecutable(processInfo.command);
71
59
  }
72
60
 
61
+ /**
62
+ * Detect running Codex agents
63
+ */
73
64
  async detectAgents(): Promise<AgentInfo[]> {
74
- const codexProcesses = this.listCodexProcesses();
75
-
76
- if (codexProcesses.length === 0) {
77
- return [];
78
- }
65
+ const processes = enrichProcesses(listAgentProcesses('codex'));
66
+ if (processes.length === 0) return [];
79
67
 
80
- const processStartByPid = this.getProcessStartTimes(codexProcesses.map((processInfo) => processInfo.pid));
81
-
82
- const sessionScanLimit = this.calculateSessionScanLimit(codexProcesses.length);
83
- const sessions = this.readSessions(sessionScanLimit, processStartByPid);
68
+ const { sessions, contentCache } = this.discoverSessions(processes);
84
69
  if (sessions.length === 0) {
85
- return codexProcesses.map((processInfo) =>
86
- this.mapProcessOnlyAgent(processInfo, [], 'No Codex session metadata found'),
87
- );
70
+ return processes.map((p) => this.mapProcessOnlyAgent(p));
88
71
  }
89
72
 
90
- const sortedSessions = [...sessions].sort(
91
- (a, b) => b.lastActive.getTime() - a.lastActive.getTime(),
92
- );
93
- const usedSessionIds = new Set<string>();
94
- const assignedPids = new Set<number>();
73
+ const matches = matchProcessesToSessions(processes, sessions);
74
+ const matchedPids = new Set(matches.map((m) => m.process.pid));
95
75
  const agents: AgentInfo[] = [];
96
76
 
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
- );
125
-
126
- // Every running codex process should still be listed.
127
- for (const processInfo of codexProcesses) {
128
- if (assignedPids.has(processInfo.pid)) {
129
- continue;
77
+ for (const match of matches) {
78
+ const cachedContent = contentCache.get(match.session.filePath);
79
+ const sessionData = this.parseSession(cachedContent, match.session.filePath);
80
+ if (sessionData) {
81
+ agents.push(this.mapSessionToAgent(sessionData, match.process));
82
+ } else {
83
+ matchedPids.delete(match.process.pid);
130
84
  }
131
-
132
- this.addProcessOnlyAgent(processInfo, assignedPids, agents);
133
85
  }
134
86
 
135
- return agents;
136
- }
137
-
138
- private listCodexProcesses(): ProcessInfo[] {
139
- return listProcesses({ namePattern: 'codex' }).filter((processInfo) =>
140
- this.canHandle(processInfo),
141
- );
142
- }
143
-
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
- }
153
-
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 {
163
- for (const processInfo of codexProcesses) {
164
- if (assignedPids.has(processInfo.pid)) {
165
- continue;
166
- }
167
-
168
- const session = this.selectBestSession(
169
- processInfo,
170
- sessions,
171
- usedSessionIds,
172
- processStartByPid,
173
- mode,
174
- );
175
- if (!session) {
176
- continue;
87
+ for (const proc of processes) {
88
+ if (!matchedPids.has(proc.pid)) {
89
+ agents.push(this.mapProcessOnlyAgent(proc));
177
90
  }
178
-
179
- this.addMappedSessionAgent(session, processInfo, usedSessionIds, assignedPids, agents);
180
91
  }
181
- }
182
-
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));
202
- }
203
-
204
- private mapSessionToAgent(
205
- session: CodexSession,
206
- processInfo: ProcessInfo,
207
- existingAgents: AgentInfo[],
208
- ): AgentInfo {
209
- return {
210
- name: this.generateAgentName(session, existingAgents),
211
- type: this.type,
212
- status: this.determineStatus(session),
213
- summary: session.summary || 'Codex session active',
214
- pid: processInfo.pid,
215
- projectPath: session.projectPath || processInfo.cwd || '',
216
- sessionId: session.sessionId,
217
- lastActive: session.lastActive,
218
- };
219
- }
220
-
221
- private mapProcessOnlyAgent(
222
- processInfo: ProcessInfo,
223
- existingAgents: AgentInfo[],
224
- summary: string = 'Codex process running',
225
- ): AgentInfo {
226
- const syntheticSession: CodexSession = {
227
- sessionId: `pid-${processInfo.pid}`,
228
- projectPath: processInfo.cwd || '',
229
- summary,
230
- sessionStart: new Date(),
231
- lastActive: new Date(),
232
- lastPayloadType: 'process_only',
233
- };
234
92
 
235
- return {
236
- name: this.generateAgentName(syntheticSession, existingAgents),
237
- type: this.type,
238
- status: AgentStatus.RUNNING,
239
- summary,
240
- pid: processInfo.pid,
241
- projectPath: processInfo.cwd || '',
242
- sessionId: syntheticSession.sessionId,
243
- lastActive: syntheticSession.lastActive,
244
- };
93
+ return agents;
245
94
  }
246
95
 
247
- private readSessions(limit: number, processStartByPid: Map<number, Date>): CodexSession[] {
248
- const sessionFiles = this.findSessionFiles(limit, processStartByPid);
249
- const sessions: CodexSession[] = [];
250
-
251
- for (const sessionFile of sessionFiles) {
96
+ /**
97
+ * Discover session files for the given processes.
98
+ *
99
+ * Uses process start times to determine which YYYY/MM/DD date directories
100
+ * to scan (±1 day window), then batches stat calls across all directories.
101
+ * Reads each file once and caches content for later parsing by parseSession().
102
+ * Sets resolvedCwd from session_meta first line.
103
+ */
104
+ private discoverSessions(processes: ProcessInfo[]): {
105
+ sessions: SessionFile[];
106
+ contentCache: Map<string, string>;
107
+ } {
108
+ const empty = { sessions: [], contentCache: new Map<string, string>() };
109
+ if (!fs.existsSync(this.codexSessionsDir)) return empty;
110
+
111
+ const dateDirs = this.getDateDirs(processes);
112
+ if (dateDirs.length === 0) return empty;
113
+
114
+ const files = batchGetSessionFileBirthtimes(dateDirs);
115
+ const contentCache = new Map<string, string>();
116
+
117
+ // Read each file once: extract CWD for matching, cache content for later parsing
118
+ for (const file of files) {
252
119
  try {
253
- const session = this.readSession(sessionFile);
254
- if (session) {
255
- sessions.push(session);
256
- }
257
- } catch (error) {
258
- console.error(`Failed to parse Codex session ${sessionFile}:`, error);
259
- }
260
- }
261
-
262
- return sessions;
263
- }
264
-
265
- private findSessionFiles(limit: number, processStartByPid: Map<number, Date>): string[] {
266
- if (!fs.existsSync(this.codexSessionsDir)) {
267
- return [];
268
- }
269
-
270
- const files: Array<{ path: string; mtimeMs: number }> = [];
271
- const stack: string[] = [this.codexSessionsDir];
272
-
273
- while (stack.length > 0) {
274
- const currentDir = stack.pop();
275
- if (!currentDir || !fs.existsSync(currentDir)) {
276
- continue;
277
- }
278
-
279
- for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
280
- const fullPath = path.join(currentDir, entry.name);
281
- if (entry.isDirectory()) {
282
- stack.push(fullPath);
283
- continue;
284
- }
285
-
286
- if (entry.isFile() && entry.name.endsWith('.jsonl')) {
287
- try {
288
- files.push({
289
- path: fullPath,
290
- mtimeMs: fs.statSync(fullPath).mtimeMs,
291
- });
292
- } catch {
293
- continue;
120
+ const content = fs.readFileSync(file.filePath, 'utf-8');
121
+ contentCache.set(file.filePath, content);
122
+
123
+ const firstLine = content.split('\n')[0]?.trim();
124
+ if (firstLine) {
125
+ const parsed = JSON.parse(firstLine);
126
+ if (parsed.type === 'session_meta') {
127
+ file.resolvedCwd = parsed.payload?.cwd || '';
294
128
  }
295
129
  }
130
+ } catch {
131
+ // Skip unreadable files
296
132
  }
297
133
  }
298
134
 
299
- const recentFiles = files
300
- .sort((a, b) => b.mtimeMs - a.mtimeMs)
301
- .slice(0, limit)
302
- .map((file) => file.path);
303
- const processDayFiles = this.findProcessDaySessionFiles(processStartByPid);
304
-
305
- const selectedPaths = new Set(recentFiles);
306
- for (const processDayFile of processDayFiles) {
307
- selectedPaths.add(processDayFile);
308
- }
309
-
310
- return Array.from(selectedPaths);
135
+ return { sessions: files, contentCache };
311
136
  }
312
137
 
313
- private findProcessDaySessionFiles(processStartByPid: Map<number, Date>): string[] {
314
- const files: string[] = [];
138
+ /**
139
+ * Determine which date directories to scan based on process start times.
140
+ * Returns only directories that actually exist.
141
+ */
142
+ private getDateDirs(processes: ProcessInfo[]): string[] {
315
143
  const dayKeys = new Set<string>();
316
- const dayWindow = CodexAdapter.PROCESS_START_DAY_WINDOW_DAYS;
144
+ const window = CodexAdapter.PROCESS_START_DAY_WINDOW_DAYS;
317
145
 
318
- for (const processStart of processStartByPid.values()) {
319
- for (let offset = -dayWindow; offset <= dayWindow; offset++) {
320
- const day = new Date(processStart.getTime());
146
+ for (const proc of processes) {
147
+ const startTime = proc.startTime || new Date();
148
+ for (let offset = -window; offset <= window; offset++) {
149
+ const day = new Date(startTime.getTime());
321
150
  day.setDate(day.getDate() + offset);
322
151
  dayKeys.add(this.toSessionDayKey(day));
323
152
  }
324
153
  }
325
154
 
155
+ const dirs: string[] = [];
326
156
  for (const dayKey of dayKeys) {
327
157
  const dayDir = path.join(this.codexSessionsDir, dayKey);
328
- if (!fs.existsSync(dayDir)) {
329
- continue;
330
- }
331
-
332
- for (const entry of fs.readdirSync(dayDir, { withFileTypes: true })) {
333
- if (entry.isFile() && entry.name.endsWith('.jsonl')) {
334
- files.push(path.join(dayDir, entry.name));
158
+ try {
159
+ if (fs.statSync(dayDir).isDirectory()) {
160
+ dirs.push(dayDir);
335
161
  }
162
+ } catch {
163
+ continue;
336
164
  }
337
165
  }
338
166
 
339
- return files;
167
+ return dirs;
340
168
  }
341
169
 
342
170
  private toSessionDayKey(date: Date): string {
@@ -346,18 +174,45 @@ export class CodexAdapter implements AgentAdapter {
346
174
  return path.join(yyyy, mm, dd);
347
175
  }
348
176
 
349
- private readSession(filePath: string): CodexSession | null {
350
- const firstLine = this.readFirstLine(filePath);
351
- if (!firstLine) {
177
+ /**
178
+ * Parse session file content into CodexSession.
179
+ * Uses cached content if available, otherwise reads from disk.
180
+ */
181
+ private parseSession(cachedContent: string | undefined, filePath: string): CodexSession | null {
182
+ let content: string;
183
+ if (cachedContent !== undefined) {
184
+ content = cachedContent;
185
+ } else {
186
+ try {
187
+ content = fs.readFileSync(filePath, 'utf-8');
188
+ } catch {
189
+ return null;
190
+ }
191
+ }
192
+
193
+ const allLines = content.trim().split('\n');
194
+ if (!allLines[0]) return null;
195
+
196
+ let metaEntry: CodexEventEntry;
197
+ try {
198
+ metaEntry = JSON.parse(allLines[0]);
199
+ } catch {
352
200
  return null;
353
201
  }
354
202
 
355
- const metaEntry = this.parseSessionMeta(firstLine);
356
- if (!metaEntry?.payload?.id) {
203
+ if (metaEntry.type !== 'session_meta' || !metaEntry.payload?.id) {
357
204
  return null;
358
205
  }
359
206
 
360
- const entries = readJsonLines<CodexEventEntry>(filePath, 300);
207
+ const entries: CodexEventEntry[] = [];
208
+ for (const line of allLines) {
209
+ try {
210
+ entries.push(JSON.parse(line));
211
+ } catch {
212
+ continue;
213
+ }
214
+ }
215
+
361
216
  const lastEntry = this.findLastEventEntry(entries);
362
217
  const lastPayloadType = lastEntry?.payload?.type;
363
218
 
@@ -379,21 +234,30 @@ export class CodexAdapter implements AgentAdapter {
379
234
  };
380
235
  }
381
236
 
382
- private readFirstLine(filePath: string): string {
383
- const content = fs.readFileSync(filePath, 'utf-8');
384
- return content.split('\n')[0]?.trim() || '';
237
+ private mapSessionToAgent(session: CodexSession, processInfo: ProcessInfo): AgentInfo {
238
+ return {
239
+ name: generateAgentName(session.projectPath || processInfo.cwd || '', processInfo.pid),
240
+ type: this.type,
241
+ status: this.determineStatus(session),
242
+ summary: session.summary || 'Codex session active',
243
+ pid: processInfo.pid,
244
+ projectPath: session.projectPath || processInfo.cwd || '',
245
+ sessionId: session.sessionId,
246
+ lastActive: session.lastActive,
247
+ };
385
248
  }
386
249
 
387
- private parseSessionMeta(line: string): CodexSessionMetaEntry | null {
388
- try {
389
- const parsed = JSON.parse(line) as CodexSessionMetaEntry;
390
- if (parsed.type !== 'session_meta') {
391
- return null;
392
- }
393
- return parsed;
394
- } catch {
395
- return null;
396
- }
250
+ private mapProcessOnlyAgent(processInfo: ProcessInfo): AgentInfo {
251
+ return {
252
+ name: generateAgentName(processInfo.cwd || '', processInfo.pid),
253
+ type: this.type,
254
+ status: AgentStatus.RUNNING,
255
+ summary: 'Codex process running',
256
+ pid: processInfo.pid,
257
+ projectPath: processInfo.cwd || '',
258
+ sessionId: `pid-${processInfo.pid}`,
259
+ lastActive: new Date(),
260
+ };
397
261
  }
398
262
 
399
263
  private findLastEventEntry(entries: CodexEventEntry[]): CodexEventEntry | undefined {
@@ -407,124 +271,28 @@ export class CodexAdapter implements AgentAdapter {
407
271
  }
408
272
 
409
273
  private parseTimestamp(value?: string): Date | null {
410
- if (!value) {
411
- return null;
412
- }
413
-
274
+ if (!value) return null;
414
275
  const timestamp = new Date(value);
415
276
  return Number.isNaN(timestamp.getTime()) ? null : timestamp;
416
277
  }
417
278
 
418
- private selectBestSession(
419
- processInfo: ProcessInfo,
420
- sessions: CodexSession[],
421
- usedSessionIds: Set<string>,
422
- processStartByPid: Map<number, Date>,
423
- mode: SessionMatchMode,
424
- ): CodexSession | undefined {
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) => {
446
- if (usedSessionIds.has(session.sessionId)) {
447
- return false;
448
- }
449
-
450
- if (mode === 'cwd') {
451
- return session.projectPath === processInfo.cwd;
452
- }
453
-
454
- if (mode === 'missing-cwd') {
455
- return !session.projectPath;
456
- }
457
-
458
- return true;
459
- });
460
- }
461
-
462
- private rankCandidatesByStartTime(candidates: CodexSession[], processStart: Date): CodexSession[] {
463
- const toleranceMs = CodexAdapter.PROCESS_SESSION_TIME_TOLERANCE_MS;
464
-
465
- return candidates
466
- .map((session) => {
467
- const diffMs = Math.abs(session.sessionStart.getTime() - processStart.getTime());
468
- const outsideTolerance = diffMs > toleranceMs ? 1 : 0;
469
- return {
470
- session,
471
- rank: outsideTolerance,
472
- diffMs,
473
- recency: session.lastActive.getTime(),
474
- };
475
- })
476
- .sort((a, b) => {
477
- if (a.rank !== b.rank) return a.rank - b.rank;
478
- if (a.diffMs !== b.diffMs) return a.diffMs - b.diffMs;
479
- return b.recency - a.recency;
480
- })
481
- .map((ranked) => ranked.session);
482
- }
483
-
484
- private getProcessStartTimes(pids: number[]): Map<number, Date> {
485
- if (pids.length === 0 || process.env.JEST_WORKER_ID) {
486
- return new Map();
487
- }
488
-
489
- try {
490
- const output = execSync(`ps -o pid=,etime= -p ${pids.join(',')}`, {
491
- encoding: 'utf-8',
492
- });
493
- const nowMs = Date.now();
494
- const startTimes = new Map<number, Date>();
495
-
496
- for (const rawLine of output.split('\n')) {
497
- const line = rawLine.trim();
498
- if (!line) continue;
499
-
500
- const parts = line.split(/\s+/);
501
- if (parts.length < 2) continue;
502
-
503
- const pid = Number.parseInt(parts[0], 10);
504
- const elapsedSeconds = this.parseElapsedSeconds(parts[1]);
505
- if (!Number.isFinite(pid) || elapsedSeconds === null) continue;
506
-
507
- startTimes.set(pid, new Date(nowMs - elapsedSeconds * 1000));
508
- }
279
+ private determineStatus(session: CodexSession): AgentStatus {
280
+ const diffMs = Date.now() - session.lastActive.getTime();
281
+ const diffMinutes = diffMs / 60000;
509
282
 
510
- return startTimes;
511
- } catch {
512
- return new Map();
283
+ if (diffMinutes > CodexAdapter.IDLE_THRESHOLD_MINUTES) {
284
+ return AgentStatus.IDLE;
513
285
  }
514
- }
515
286
 
516
- private parseElapsedSeconds(etime: string): number | null {
517
- const match = etime.trim().match(/^(?:(\d+)-)?(?:(\d{1,2}):)?(\d{1,2}):(\d{2})$/);
518
- if (!match) {
519
- return null;
287
+ if (
288
+ session.lastPayloadType === 'agent_message' ||
289
+ session.lastPayloadType === 'task_complete' ||
290
+ session.lastPayloadType === 'turn_aborted'
291
+ ) {
292
+ return AgentStatus.WAITING;
520
293
  }
521
294
 
522
- const days = Number.parseInt(match[1] || '0', 10);
523
- const hours = Number.parseInt(match[2] || '0', 10);
524
- const minutes = Number.parseInt(match[3] || '0', 10);
525
- const seconds = Number.parseInt(match[4] || '0', 10);
526
-
527
- return (((days * 24 + hours) * 60 + minutes) * 60) + seconds;
295
+ return AgentStatus.RUNNING;
528
296
  }
529
297
 
530
298
  private extractSummary(entries: CodexEventEntry[]): string {
@@ -539,9 +307,7 @@ export class CodexAdapter implements AgentAdapter {
539
307
  }
540
308
 
541
309
  private truncate(value: string, maxLength: number): string {
542
- if (value.length <= maxLength) {
543
- return value;
544
- }
310
+ if (value.length <= maxLength) return value;
545
311
  return `${value.slice(0, maxLength - 3)}...`;
546
312
  }
547
313
 
@@ -550,35 +316,4 @@ export class CodexAdapter implements AgentAdapter {
550
316
  const base = path.basename(executable).toLowerCase();
551
317
  return base === 'codex' || base === 'codex.exe';
552
318
  }
553
-
554
- private determineStatus(session: CodexSession): AgentStatus {
555
- const diffMs = Date.now() - session.lastActive.getTime();
556
- const diffMinutes = diffMs / 60000;
557
-
558
- if (diffMinutes > CodexAdapter.IDLE_THRESHOLD_MINUTES) {
559
- return AgentStatus.IDLE;
560
- }
561
-
562
- if (
563
- session.lastPayloadType === 'agent_message' ||
564
- session.lastPayloadType === 'task_complete' ||
565
- session.lastPayloadType === 'turn_aborted'
566
- ) {
567
- return AgentStatus.WAITING;
568
- }
569
-
570
- return AgentStatus.RUNNING;
571
- }
572
-
573
- private generateAgentName(session: CodexSession, existingAgents: AgentInfo[]): string {
574
- const fallback = `codex-${session.sessionId.slice(0, 8)}`;
575
- const baseName = session.projectPath ? path.basename(path.normalize(session.projectPath)) : fallback;
576
-
577
- const conflict = existingAgents.some((agent) => agent.name === baseName);
578
- if (!conflict) {
579
- return baseName || fallback;
580
- }
581
-
582
- return `${baseName || fallback} (${session.sessionId.slice(0, 8)})`;
583
- }
584
319
  }