@ai-devkit/agent-manager 0.4.0 → 0.6.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 +21 -2
- package/dist/adapters/AgentAdapter.d.ts.map +1 -1
- package/dist/adapters/ClaudeCodeAdapter.d.ts +44 -35
- package/dist/adapters/ClaudeCodeAdapter.d.ts.map +1 -1
- package/dist/adapters/ClaudeCodeAdapter.js +230 -298
- package/dist/adapters/ClaudeCodeAdapter.js.map +1 -1
- package/dist/adapters/CodexAdapter.d.ts +41 -31
- package/dist/adapters/CodexAdapter.d.ts.map +1 -1
- package/dist/adapters/CodexAdapter.js +198 -278
- package/dist/adapters/CodexAdapter.js.map +1 -1
- package/dist/index.d.ts +2 -4
- 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 +107 -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 +1 -1
- package/src/__tests__/AgentManager.test.ts +5 -27
- package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +754 -830
- package/src/__tests__/adapters/CodexAdapter.test.ts +581 -273
- package/src/__tests__/utils/matching.test.ts +199 -0
- package/src/__tests__/utils/process.test.ts +202 -0
- package/src/__tests__/utils/session.test.ts +117 -0
- package/src/adapters/AgentAdapter.ts +23 -4
- package/src/adapters/ClaudeCodeAdapter.ts +285 -437
- package/src/adapters/CodexAdapter.ts +202 -400
- package/src/index.ts +2 -4
- package/src/utils/index.ts +6 -3
- package/src/utils/matching.ts +96 -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 {
|
|
12
|
-
import type { AgentAdapter, AgentInfo, ProcessInfo } from './AgentAdapter';
|
|
15
|
+
import type { AgentAdapter, AgentInfo, ProcessInfo, ConversationMessage } 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';
|
|
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,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
|
|
75
|
-
|
|
76
|
-
if (codexProcesses.length === 0) {
|
|
77
|
-
return [];
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const processStartByPid = this.getProcessStartTimes(codexProcesses.map((processInfo) => processInfo.pid));
|
|
65
|
+
const processes = enrichProcesses(listAgentProcesses('codex'));
|
|
66
|
+
if (processes.length === 0) return [];
|
|
81
67
|
|
|
82
|
-
const
|
|
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
|
-
|
|
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, match.session.filePath));
|
|
82
|
+
} else {
|
|
83
|
+
matchedPids.delete(match.process.pid);
|
|
121
84
|
}
|
|
122
|
-
|
|
123
|
-
this.addProcessOnlyAgent(processInfo, assignedPids, agents);
|
|
124
85
|
}
|
|
125
86
|
|
|
126
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
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
|
-
|
|
305
|
-
|
|
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
|
|
144
|
+
const window = CodexAdapter.PROCESS_START_DAY_WINDOW_DAYS;
|
|
308
145
|
|
|
309
|
-
for (const
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
320
|
-
|
|
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
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
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 =
|
|
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,31 @@ export class CodexAdapter implements AgentAdapter {
|
|
|
370
234
|
};
|
|
371
235
|
}
|
|
372
236
|
|
|
373
|
-
private
|
|
374
|
-
|
|
375
|
-
|
|
237
|
+
private mapSessionToAgent(session: CodexSession, processInfo: ProcessInfo, filePath: string): 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
|
+
sessionFilePath: filePath,
|
|
248
|
+
};
|
|
376
249
|
}
|
|
377
250
|
|
|
378
|
-
private
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
251
|
+
private mapProcessOnlyAgent(processInfo: ProcessInfo): AgentInfo {
|
|
252
|
+
return {
|
|
253
|
+
name: generateAgentName(processInfo.cwd || '', processInfo.pid),
|
|
254
|
+
type: this.type,
|
|
255
|
+
status: AgentStatus.RUNNING,
|
|
256
|
+
summary: 'Codex process running',
|
|
257
|
+
pid: processInfo.pid,
|
|
258
|
+
projectPath: processInfo.cwd || '',
|
|
259
|
+
sessionId: `pid-${processInfo.pid}`,
|
|
260
|
+
lastActive: new Date(),
|
|
261
|
+
};
|
|
388
262
|
}
|
|
389
263
|
|
|
390
264
|
private findLastEventEntry(entries: CodexEventEntry[]): CodexEventEntry | undefined {
|
|
@@ -398,122 +272,28 @@ export class CodexAdapter implements AgentAdapter {
|
|
|
398
272
|
}
|
|
399
273
|
|
|
400
274
|
private parseTimestamp(value?: string): Date | null {
|
|
401
|
-
if (!value)
|
|
402
|
-
return null;
|
|
403
|
-
}
|
|
404
|
-
|
|
275
|
+
if (!value) return null;
|
|
405
276
|
const timestamp = new Date(value);
|
|
406
277
|
return Number.isNaN(timestamp.getTime()) ? null : timestamp;
|
|
407
278
|
}
|
|
408
279
|
|
|
409
|
-
private
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
}
|
|
280
|
+
private determineStatus(session: CodexSession): AgentStatus {
|
|
281
|
+
const diffMs = Date.now() - session.lastActive.getTime();
|
|
282
|
+
const diffMinutes = diffMs / 60000;
|
|
498
283
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
return new Map();
|
|
284
|
+
if (diffMinutes > CodexAdapter.IDLE_THRESHOLD_MINUTES) {
|
|
285
|
+
return AgentStatus.IDLE;
|
|
502
286
|
}
|
|
503
|
-
}
|
|
504
287
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
288
|
+
if (
|
|
289
|
+
session.lastPayloadType === 'agent_message' ||
|
|
290
|
+
session.lastPayloadType === 'task_complete' ||
|
|
291
|
+
session.lastPayloadType === 'turn_aborted'
|
|
292
|
+
) {
|
|
293
|
+
return AgentStatus.WAITING;
|
|
509
294
|
}
|
|
510
295
|
|
|
511
|
-
|
|
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;
|
|
296
|
+
return AgentStatus.RUNNING;
|
|
517
297
|
}
|
|
518
298
|
|
|
519
299
|
private extractSummary(entries: CodexEventEntry[]): string {
|
|
@@ -528,9 +308,7 @@ export class CodexAdapter implements AgentAdapter {
|
|
|
528
308
|
}
|
|
529
309
|
|
|
530
310
|
private truncate(value: string, maxLength: number): string {
|
|
531
|
-
if (value.length <= maxLength)
|
|
532
|
-
return value;
|
|
533
|
-
}
|
|
311
|
+
if (value.length <= maxLength) return value;
|
|
534
312
|
return `${value.slice(0, maxLength - 3)}...`;
|
|
535
313
|
}
|
|
536
314
|
|
|
@@ -540,34 +318,58 @@ export class CodexAdapter implements AgentAdapter {
|
|
|
540
318
|
return base === 'codex' || base === 'codex.exe';
|
|
541
319
|
}
|
|
542
320
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
321
|
+
/**
|
|
322
|
+
* Read the full conversation from a Codex session JSONL file.
|
|
323
|
+
*
|
|
324
|
+
* Codex entries use payload.type to indicate message role and payload.message for content.
|
|
325
|
+
*/
|
|
326
|
+
getConversation(sessionFilePath: string, options?: { verbose?: boolean }): ConversationMessage[] {
|
|
327
|
+
const verbose = options?.verbose ?? false;
|
|
546
328
|
|
|
547
|
-
|
|
548
|
-
|
|
329
|
+
let content: string;
|
|
330
|
+
try {
|
|
331
|
+
content = fs.readFileSync(sessionFilePath, 'utf-8');
|
|
332
|
+
} catch {
|
|
333
|
+
return [];
|
|
549
334
|
}
|
|
550
335
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
session.lastPayloadType === 'task_complete' ||
|
|
554
|
-
session.lastPayloadType === 'turn_aborted'
|
|
555
|
-
) {
|
|
556
|
-
return AgentStatus.WAITING;
|
|
557
|
-
}
|
|
336
|
+
const lines = content.trim().split('\n');
|
|
337
|
+
const messages: ConversationMessage[] = [];
|
|
558
338
|
|
|
559
|
-
|
|
560
|
-
|
|
339
|
+
for (const line of lines) {
|
|
340
|
+
let entry: CodexEventEntry;
|
|
341
|
+
try {
|
|
342
|
+
entry = JSON.parse(line);
|
|
343
|
+
} catch {
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (entry.type === 'session_meta') continue;
|
|
561
348
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
const baseName = session.projectPath ? path.basename(path.normalize(session.projectPath)) : fallback;
|
|
349
|
+
const payloadType = entry.payload?.type;
|
|
350
|
+
if (!payloadType) continue;
|
|
565
351
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
352
|
+
let role: ConversationMessage['role'];
|
|
353
|
+
if (payloadType === 'user_message') {
|
|
354
|
+
role = 'user';
|
|
355
|
+
} else if (payloadType === 'agent_message' || payloadType === 'task_complete') {
|
|
356
|
+
role = 'assistant';
|
|
357
|
+
} else if (verbose) {
|
|
358
|
+
role = 'system';
|
|
359
|
+
} else {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const text = entry.payload?.message?.trim();
|
|
364
|
+
if (!text) continue;
|
|
365
|
+
|
|
366
|
+
messages.push({
|
|
367
|
+
role,
|
|
368
|
+
content: text,
|
|
369
|
+
timestamp: entry.timestamp,
|
|
370
|
+
});
|
|
569
371
|
}
|
|
570
372
|
|
|
571
|
-
return
|
|
373
|
+
return messages;
|
|
572
374
|
}
|
|
573
375
|
}
|