@ai-devkit/agent-manager 0.4.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 +29 -34
  4. package/dist/adapters/ClaudeCodeAdapter.d.ts.map +1 -1
  5. package/dist/adapters/ClaudeCodeAdapter.js +138 -294
  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 -282
  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 +1 -1
  32. package/src/__tests__/AgentManager.test.ts +0 -25
  33. package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +598 -845
  34. package/src/__tests__/adapters/CodexAdapter.test.ts +467 -274
  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 +177 -425
  40. package/src/adapters/CodexAdapter.ts +155 -409
  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';
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,264 +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.
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
-
117
- // Every running codex process should still be listed.
118
- for (const processInfo of codexProcesses) {
119
- if (assignedPids.has(processInfo.pid)) {
120
- 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);
121
84
  }
122
-
123
- this.addProcessOnlyAgent(processInfo, assignedPids, agents);
124
85
  }
125
86
 
126
- return agents;
127
- }
128
-
129
- private listCodexProcesses(): ProcessInfo[] {
130
- return listProcesses({ namePattern: 'codex' }).filter((processInfo) =>
131
- this.canHandle(processInfo),
132
- );
133
- }
134
-
135
- private calculateSessionScanLimit(processCount: number): number {
136
- return Math.min(
137
- Math.max(
138
- processCount * CodexAdapter.SESSION_SCAN_MULTIPLIER,
139
- CodexAdapter.MIN_SESSION_SCAN,
140
- ),
141
- CodexAdapter.MAX_SESSION_SCAN,
142
- );
143
- }
144
-
145
- private assignSessionsForMode(
146
- mode: SessionMatchMode,
147
- codexProcesses: ProcessInfo[],
148
- sessions: CodexSession[],
149
- usedSessionIds: Set<string>,
150
- assignedPids: Set<number>,
151
- processStartByPid: Map<number, Date>,
152
- agents: AgentInfo[],
153
- ): void {
154
- for (const processInfo of codexProcesses) {
155
- if (assignedPids.has(processInfo.pid)) {
156
- continue;
157
- }
158
-
159
- const session = this.selectBestSession(
160
- processInfo,
161
- sessions,
162
- usedSessionIds,
163
- processStartByPid,
164
- mode,
165
- );
166
- if (!session) {
167
- continue;
87
+ for (const proc of processes) {
88
+ if (!matchedPids.has(proc.pid)) {
89
+ agents.push(this.mapProcessOnlyAgent(proc));
168
90
  }
169
-
170
- this.addMappedSessionAgent(session, processInfo, usedSessionIds, assignedPids, agents);
171
91
  }
172
- }
173
-
174
- private addMappedSessionAgent(
175
- session: CodexSession,
176
- processInfo: ProcessInfo,
177
- usedSessionIds: Set<string>,
178
- assignedPids: Set<number>,
179
- agents: AgentInfo[],
180
- ): void {
181
- usedSessionIds.add(session.sessionId);
182
- assignedPids.add(processInfo.pid);
183
- agents.push(this.mapSessionToAgent(session, processInfo, agents));
184
- }
185
-
186
- private addProcessOnlyAgent(
187
- processInfo: ProcessInfo,
188
- assignedPids: Set<number>,
189
- agents: AgentInfo[],
190
- ): void {
191
- assignedPids.add(processInfo.pid);
192
- agents.push(this.mapProcessOnlyAgent(processInfo, agents));
193
- }
194
-
195
- private mapSessionToAgent(
196
- session: CodexSession,
197
- processInfo: ProcessInfo,
198
- existingAgents: AgentInfo[],
199
- ): AgentInfo {
200
- return {
201
- name: this.generateAgentName(session, existingAgents),
202
- type: this.type,
203
- status: this.determineStatus(session),
204
- summary: session.summary || 'Codex session active',
205
- pid: processInfo.pid,
206
- projectPath: session.projectPath || processInfo.cwd || '',
207
- sessionId: session.sessionId,
208
- lastActive: session.lastActive,
209
- };
210
- }
211
-
212
- private mapProcessOnlyAgent(
213
- processInfo: ProcessInfo,
214
- existingAgents: AgentInfo[],
215
- summary: string = 'Codex process running',
216
- ): AgentInfo {
217
- const syntheticSession: CodexSession = {
218
- sessionId: `pid-${processInfo.pid}`,
219
- projectPath: processInfo.cwd || '',
220
- summary,
221
- sessionStart: new Date(),
222
- lastActive: new Date(),
223
- lastPayloadType: 'process_only',
224
- };
225
92
 
226
- return {
227
- name: this.generateAgentName(syntheticSession, existingAgents),
228
- type: this.type,
229
- status: AgentStatus.RUNNING,
230
- summary,
231
- pid: processInfo.pid,
232
- projectPath: processInfo.cwd || '',
233
- sessionId: syntheticSession.sessionId,
234
- lastActive: syntheticSession.lastActive,
235
- };
93
+ return agents;
236
94
  }
237
95
 
238
- private readSessions(limit: number, processStartByPid: Map<number, Date>): CodexSession[] {
239
- const sessionFiles = this.findSessionFiles(limit, processStartByPid);
240
- const sessions: CodexSession[] = [];
241
-
242
- 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) {
243
119
  try {
244
- const session = this.readSession(sessionFile);
245
- if (session) {
246
- sessions.push(session);
247
- }
248
- } catch (error) {
249
- console.error(`Failed to parse Codex session ${sessionFile}:`, error);
250
- }
251
- }
252
-
253
- return sessions;
254
- }
255
-
256
- private findSessionFiles(limit: number, processStartByPid: Map<number, Date>): string[] {
257
- if (!fs.existsSync(this.codexSessionsDir)) {
258
- return [];
259
- }
260
-
261
- const files: Array<{ path: string; mtimeMs: number }> = [];
262
- const stack: string[] = [this.codexSessionsDir];
263
-
264
- while (stack.length > 0) {
265
- const currentDir = stack.pop();
266
- if (!currentDir || !fs.existsSync(currentDir)) {
267
- continue;
268
- }
269
-
270
- for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
271
- const fullPath = path.join(currentDir, entry.name);
272
- if (entry.isDirectory()) {
273
- stack.push(fullPath);
274
- continue;
275
- }
276
-
277
- if (entry.isFile() && entry.name.endsWith('.jsonl')) {
278
- try {
279
- files.push({
280
- path: fullPath,
281
- mtimeMs: fs.statSync(fullPath).mtimeMs,
282
- });
283
- } catch {
284
- 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 || '';
285
128
  }
286
129
  }
130
+ } catch {
131
+ // Skip unreadable files
287
132
  }
288
133
  }
289
134
 
290
- const recentFiles = files
291
- .sort((a, b) => b.mtimeMs - a.mtimeMs)
292
- .slice(0, limit)
293
- .map((file) => file.path);
294
- const processDayFiles = this.findProcessDaySessionFiles(processStartByPid);
295
-
296
- const selectedPaths = new Set(recentFiles);
297
- for (const processDayFile of processDayFiles) {
298
- selectedPaths.add(processDayFile);
299
- }
300
-
301
- return Array.from(selectedPaths);
135
+ return { sessions: files, contentCache };
302
136
  }
303
137
 
304
- private findProcessDaySessionFiles(processStartByPid: Map<number, Date>): string[] {
305
- 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[] {
306
143
  const dayKeys = new Set<string>();
307
- const dayWindow = CodexAdapter.PROCESS_START_DAY_WINDOW_DAYS;
144
+ const window = CodexAdapter.PROCESS_START_DAY_WINDOW_DAYS;
308
145
 
309
- for (const processStart of processStartByPid.values()) {
310
- for (let offset = -dayWindow; offset <= dayWindow; offset++) {
311
- 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());
312
150
  day.setDate(day.getDate() + offset);
313
151
  dayKeys.add(this.toSessionDayKey(day));
314
152
  }
315
153
  }
316
154
 
155
+ const dirs: string[] = [];
317
156
  for (const dayKey of dayKeys) {
318
157
  const dayDir = path.join(this.codexSessionsDir, dayKey);
319
- if (!fs.existsSync(dayDir)) {
320
- continue;
321
- }
322
-
323
- for (const entry of fs.readdirSync(dayDir, { withFileTypes: true })) {
324
- if (entry.isFile() && entry.name.endsWith('.jsonl')) {
325
- files.push(path.join(dayDir, entry.name));
158
+ try {
159
+ if (fs.statSync(dayDir).isDirectory()) {
160
+ dirs.push(dayDir);
326
161
  }
162
+ } catch {
163
+ continue;
327
164
  }
328
165
  }
329
166
 
330
- return files;
167
+ return dirs;
331
168
  }
332
169
 
333
170
  private toSessionDayKey(date: Date): string {
@@ -337,18 +174,45 @@ export class CodexAdapter implements AgentAdapter {
337
174
  return path.join(yyyy, mm, dd);
338
175
  }
339
176
 
340
- private readSession(filePath: string): CodexSession | null {
341
- const firstLine = this.readFirstLine(filePath);
342
- 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 {
343
200
  return null;
344
201
  }
345
202
 
346
- const metaEntry = this.parseSessionMeta(firstLine);
347
- if (!metaEntry?.payload?.id) {
203
+ if (metaEntry.type !== 'session_meta' || !metaEntry.payload?.id) {
348
204
  return null;
349
205
  }
350
206
 
351
- 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
+
352
216
  const lastEntry = this.findLastEventEntry(entries);
353
217
  const lastPayloadType = lastEntry?.payload?.type;
354
218
 
@@ -370,21 +234,30 @@ export class CodexAdapter implements AgentAdapter {
370
234
  };
371
235
  }
372
236
 
373
- private readFirstLine(filePath: string): string {
374
- const content = fs.readFileSync(filePath, 'utf-8');
375
- 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
+ };
376
248
  }
377
249
 
378
- private parseSessionMeta(line: string): CodexSessionMetaEntry | null {
379
- try {
380
- const parsed = JSON.parse(line) as CodexSessionMetaEntry;
381
- if (parsed.type !== 'session_meta') {
382
- return null;
383
- }
384
- return parsed;
385
- } catch {
386
- return null;
387
- }
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
+ };
388
261
  }
389
262
 
390
263
  private findLastEventEntry(entries: CodexEventEntry[]): CodexEventEntry | undefined {
@@ -398,122 +271,28 @@ export class CodexAdapter implements AgentAdapter {
398
271
  }
399
272
 
400
273
  private parseTimestamp(value?: string): Date | null {
401
- if (!value) {
402
- return null;
403
- }
404
-
274
+ if (!value) return null;
405
275
  const timestamp = new Date(value);
406
276
  return Number.isNaN(timestamp.getTime()) ? null : timestamp;
407
277
  }
408
278
 
409
- private selectBestSession(
410
- processInfo: ProcessInfo,
411
- sessions: CodexSession[],
412
- usedSessionIds: Set<string>,
413
- processStartByPid: Map<number, Date>,
414
- mode: SessionMatchMode,
415
- ): CodexSession | undefined {
416
- const candidates = this.filterCandidateSessions(processInfo, sessions, usedSessionIds, mode);
417
-
418
- if (candidates.length === 0) {
419
- return undefined;
420
- }
421
-
422
- const processStart = processStartByPid.get(processInfo.pid);
423
- if (!processStart) {
424
- return candidates.sort((a, b) => b.lastActive.getTime() - a.lastActive.getTime())[0];
425
- }
426
-
427
- return this.rankCandidatesByStartTime(candidates, processStart)[0];
428
- }
429
-
430
- private filterCandidateSessions(
431
- processInfo: ProcessInfo,
432
- sessions: CodexSession[],
433
- usedSessionIds: Set<string>,
434
- mode: SessionMatchMode,
435
- ): CodexSession[] {
436
- return sessions.filter((session) => {
437
- if (usedSessionIds.has(session.sessionId)) {
438
- return false;
439
- }
440
-
441
- if (mode === 'cwd') {
442
- return session.projectPath === processInfo.cwd;
443
- }
444
-
445
- if (mode === 'missing-cwd') {
446
- return !session.projectPath;
447
- }
448
- });
449
- }
450
-
451
- private rankCandidatesByStartTime(candidates: CodexSession[], processStart: Date): CodexSession[] {
452
- const toleranceMs = CodexAdapter.PROCESS_SESSION_TIME_TOLERANCE_MS;
453
-
454
- return candidates
455
- .map((session) => {
456
- const diffMs = Math.abs(session.sessionStart.getTime() - processStart.getTime());
457
- const outsideTolerance = diffMs > toleranceMs ? 1 : 0;
458
- return {
459
- session,
460
- rank: outsideTolerance,
461
- diffMs,
462
- recency: session.lastActive.getTime(),
463
- };
464
- })
465
- .sort((a, b) => {
466
- if (a.rank !== b.rank) return a.rank - b.rank;
467
- if (a.diffMs !== b.diffMs) return a.diffMs - b.diffMs;
468
- return b.recency - a.recency;
469
- })
470
- .map((ranked) => ranked.session);
471
- }
472
-
473
- private getProcessStartTimes(pids: number[]): Map<number, Date> {
474
- if (pids.length === 0 || process.env.JEST_WORKER_ID) {
475
- return new Map();
476
- }
477
-
478
- try {
479
- const output = execSync(`ps -o pid=,etime= -p ${pids.join(',')}`, {
480
- encoding: 'utf-8',
481
- });
482
- const nowMs = Date.now();
483
- const startTimes = new Map<number, Date>();
484
-
485
- for (const rawLine of output.split('\n')) {
486
- const line = rawLine.trim();
487
- if (!line) continue;
488
-
489
- const parts = line.split(/\s+/);
490
- if (parts.length < 2) continue;
491
-
492
- const pid = Number.parseInt(parts[0], 10);
493
- const elapsedSeconds = this.parseElapsedSeconds(parts[1]);
494
- if (!Number.isFinite(pid) || elapsedSeconds === null) continue;
495
-
496
- startTimes.set(pid, new Date(nowMs - elapsedSeconds * 1000));
497
- }
279
+ private determineStatus(session: CodexSession): AgentStatus {
280
+ const diffMs = Date.now() - session.lastActive.getTime();
281
+ const diffMinutes = diffMs / 60000;
498
282
 
499
- return startTimes;
500
- } catch {
501
- return new Map();
283
+ if (diffMinutes > CodexAdapter.IDLE_THRESHOLD_MINUTES) {
284
+ return AgentStatus.IDLE;
502
285
  }
503
- }
504
286
 
505
- private parseElapsedSeconds(etime: string): number | null {
506
- const match = etime.trim().match(/^(?:(\d+)-)?(?:(\d{1,2}):)?(\d{1,2}):(\d{2})$/);
507
- if (!match) {
508
- return null;
287
+ if (
288
+ session.lastPayloadType === 'agent_message' ||
289
+ session.lastPayloadType === 'task_complete' ||
290
+ session.lastPayloadType === 'turn_aborted'
291
+ ) {
292
+ return AgentStatus.WAITING;
509
293
  }
510
294
 
511
- const days = Number.parseInt(match[1] || '0', 10);
512
- const hours = Number.parseInt(match[2] || '0', 10);
513
- const minutes = Number.parseInt(match[3] || '0', 10);
514
- const seconds = Number.parseInt(match[4] || '0', 10);
515
-
516
- return (((days * 24 + hours) * 60 + minutes) * 60) + seconds;
295
+ return AgentStatus.RUNNING;
517
296
  }
518
297
 
519
298
  private extractSummary(entries: CodexEventEntry[]): string {
@@ -528,9 +307,7 @@ export class CodexAdapter implements AgentAdapter {
528
307
  }
529
308
 
530
309
  private truncate(value: string, maxLength: number): string {
531
- if (value.length <= maxLength) {
532
- return value;
533
- }
310
+ if (value.length <= maxLength) return value;
534
311
  return `${value.slice(0, maxLength - 3)}...`;
535
312
  }
536
313
 
@@ -539,35 +316,4 @@ export class CodexAdapter implements AgentAdapter {
539
316
  const base = path.basename(executable).toLowerCase();
540
317
  return base === 'codex' || base === 'codex.exe';
541
318
  }
542
-
543
- private determineStatus(session: CodexSession): AgentStatus {
544
- const diffMs = Date.now() - session.lastActive.getTime();
545
- const diffMinutes = diffMs / 60000;
546
-
547
- if (diffMinutes > CodexAdapter.IDLE_THRESHOLD_MINUTES) {
548
- return AgentStatus.IDLE;
549
- }
550
-
551
- if (
552
- session.lastPayloadType === 'agent_message' ||
553
- session.lastPayloadType === 'task_complete' ||
554
- session.lastPayloadType === 'turn_aborted'
555
- ) {
556
- return AgentStatus.WAITING;
557
- }
558
-
559
- return AgentStatus.RUNNING;
560
- }
561
-
562
- private generateAgentName(session: CodexSession, existingAgents: AgentInfo[]): string {
563
- const fallback = `codex-${session.sessionId.slice(0, 8)}`;
564
- const baseName = session.projectPath ? path.basename(path.normalize(session.projectPath)) : fallback;
565
-
566
- const conflict = existingAgents.some((agent) => agent.name === baseName);
567
- if (!conflict) {
568
- return baseName || fallback;
569
- }
570
-
571
- return `${baseName || fallback} (${session.sessionId.slice(0, 8)})`;
572
- }
573
319
  }