@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.
- package/dist/adapters/AgentAdapter.d.ts +2 -0
- package/dist/adapters/AgentAdapter.d.ts.map +1 -1
- package/dist/adapters/ClaudeCodeAdapter.d.ts +49 -38
- package/dist/adapters/ClaudeCodeAdapter.d.ts.map +1 -1
- package/dist/adapters/ClaudeCodeAdapter.js +286 -293
- package/dist/adapters/ClaudeCodeAdapter.js.map +1 -1
- package/dist/adapters/CodexAdapter.d.ts +32 -30
- package/dist/adapters/CodexAdapter.d.ts.map +1 -1
- package/dist/adapters/CodexAdapter.js +148 -284
- package/dist/adapters/CodexAdapter.js.map +1 -1
- package/dist/index.d.ts +1 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -10
- package/dist/index.js.map +1 -1
- package/dist/utils/index.d.ts +6 -3
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +12 -11
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/matching.d.ts +39 -0
- package/dist/utils/matching.d.ts.map +1 -0
- package/dist/utils/matching.js +103 -0
- package/dist/utils/matching.js.map +1 -0
- package/dist/utils/process.d.ts +25 -40
- package/dist/utils/process.d.ts.map +1 -1
- package/dist/utils/process.js +151 -105
- package/dist/utils/process.js.map +1 -1
- package/dist/utils/session.d.ts +30 -0
- package/dist/utils/session.d.ts.map +1 -0
- package/dist/utils/session.js +101 -0
- package/dist/utils/session.js.map +1 -0
- package/package.json +2 -2
- package/src/__tests__/AgentManager.test.ts +0 -25
- package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +921 -205
- package/src/__tests__/adapters/CodexAdapter.test.ts +468 -269
- package/src/__tests__/utils/matching.test.ts +191 -0
- package/src/__tests__/utils/process.test.ts +202 -0
- package/src/__tests__/utils/session.test.ts +117 -0
- package/src/adapters/AgentAdapter.ts +3 -0
- package/src/adapters/ClaudeCodeAdapter.ts +341 -418
- package/src/adapters/CodexAdapter.ts +155 -420
- package/src/index.ts +1 -3
- package/src/utils/index.ts +6 -3
- package/src/utils/matching.ts +92 -0
- package/src/utils/process.ts +133 -119
- package/src/utils/session.ts +92 -0
- package/dist/utils/file.d.ts +0 -52
- package/dist/utils/file.d.ts.map +0 -1
- package/dist/utils/file.js +0 -135
- package/dist/utils/file.js.map +0 -1
- 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
|
|
5
|
-
* 1.
|
|
6
|
-
* 2.
|
|
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 {
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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
|
|
86
|
-
this.mapProcessOnlyAgent(processInfo, [], 'No Codex session metadata found'),
|
|
87
|
-
);
|
|
70
|
+
return processes.map((p) => this.mapProcessOnlyAgent(p));
|
|
88
71
|
}
|
|
89
72
|
|
|
90
|
-
const
|
|
91
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
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
|
-
|
|
314
|
-
|
|
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
|
|
144
|
+
const window = CodexAdapter.PROCESS_START_DAY_WINDOW_DAYS;
|
|
317
145
|
|
|
318
|
-
for (const
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
329
|
-
|
|
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
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
383
|
-
|
|
384
|
-
|
|
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
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
|
419
|
-
|
|
420
|
-
|
|
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
|
-
|
|
511
|
-
|
|
512
|
-
return new Map();
|
|
283
|
+
if (diffMinutes > CodexAdapter.IDLE_THRESHOLD_MINUTES) {
|
|
284
|
+
return AgentStatus.IDLE;
|
|
513
285
|
}
|
|
514
|
-
}
|
|
515
286
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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
|
-
|
|
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
|
}
|