@ai-devkit/agent-manager 0.19.0 → 0.20.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 (37) hide show
  1. package/dist/__tests__/adapters/CodexAdapter.test.js +344 -0
  2. package/dist/__tests__/adapters/CodexAdapter.test.js.map +1 -1
  3. package/dist/__tests__/adapters/CopilotAdapter.test.js +127 -2
  4. package/dist/__tests__/adapters/CopilotAdapter.test.js.map +1 -1
  5. package/dist/__tests__/adapters/GeminiCliAdapter.test.js +144 -2
  6. package/dist/__tests__/adapters/GeminiCliAdapter.test.js.map +1 -1
  7. package/dist/__tests__/utils/process.test.js +54 -5
  8. package/dist/__tests__/utils/process.test.js.map +1 -1
  9. package/dist/adapters/AgentAdapter.d.ts +2 -0
  10. package/dist/adapters/AgentAdapter.d.ts.map +1 -1
  11. package/dist/adapters/AgentAdapter.js.map +1 -1
  12. package/dist/adapters/CodexAdapter.d.ts +1 -0
  13. package/dist/adapters/CodexAdapter.d.ts.map +1 -1
  14. package/dist/adapters/CodexAdapter.js +14 -2
  15. package/dist/adapters/CodexAdapter.js.map +1 -1
  16. package/dist/adapters/CopilotAdapter.d.ts +4 -2
  17. package/dist/adapters/CopilotAdapter.d.ts.map +1 -1
  18. package/dist/adapters/CopilotAdapter.js +23 -12
  19. package/dist/adapters/CopilotAdapter.js.map +1 -1
  20. package/dist/adapters/GeminiCliAdapter.d.ts +1 -0
  21. package/dist/adapters/GeminiCliAdapter.d.ts.map +1 -1
  22. package/dist/adapters/GeminiCliAdapter.js +36 -11
  23. package/dist/adapters/GeminiCliAdapter.js.map +1 -1
  24. package/dist/utils/process.d.ts +11 -2
  25. package/dist/utils/process.d.ts.map +1 -1
  26. package/dist/utils/process.js +44 -9
  27. package/dist/utils/process.js.map +1 -1
  28. package/package.json +1 -1
  29. package/src/__tests__/adapters/CodexAdapter.test.ts +289 -0
  30. package/src/__tests__/adapters/CopilotAdapter.test.ts +97 -5
  31. package/src/__tests__/adapters/GeminiCliAdapter.test.ts +139 -4
  32. package/src/__tests__/utils/process.test.ts +31 -7
  33. package/src/adapters/AgentAdapter.ts +3 -0
  34. package/src/adapters/CodexAdapter.ts +17 -2
  35. package/src/adapters/CopilotAdapter.ts +25 -14
  36. package/src/adapters/GeminiCliAdapter.ts +42 -11
  37. package/src/utils/process.ts +64 -9
@@ -215,13 +215,14 @@ export class CodexAdapter implements AgentAdapter {
215
215
 
216
216
  const stat = safeStat(filePath);
217
217
  if (!stat) continue;
218
+ const metaTimestampMs = this.parseMetaTimestampMs(parsed.payload?.timestamp);
218
219
 
219
220
  return {
220
221
  sessionId,
221
222
  filePath,
222
223
  projectDir: path.dirname(filePath),
223
- birthtimeMs: stat.birthtimeMs,
224
- resolvedCwd: parsed.payload.cwd || '',
224
+ birthtimeMs: metaTimestampMs ?? stat.birthtimeMs,
225
+ resolvedCwd: parsed.payload?.cwd || '',
225
226
  };
226
227
  } catch {
227
228
  continue;
@@ -290,6 +291,10 @@ export class CodexAdapter implements AgentAdapter {
290
291
  const parsed = JSON.parse(firstLine);
291
292
  if (parsed.type === 'session_meta') {
292
293
  file.resolvedCwd = parsed.payload?.cwd || '';
294
+ const metaTimestampMs = this.parseMetaTimestampMs(parsed.payload?.timestamp);
295
+ if (metaTimestampMs !== null) {
296
+ file.birthtimeMs = metaTimestampMs;
297
+ }
293
298
  }
294
299
  }
295
300
  } catch {
@@ -468,6 +473,16 @@ export class CodexAdapter implements AgentAdapter {
468
473
  return Number.isNaN(timestamp.getTime()) ? null : timestamp;
469
474
  }
470
475
 
476
+ private parseMetaTimestampMs(value?: string): number | null {
477
+ if (typeof value !== 'string') return null;
478
+
479
+ const timestamp = this.parseTimestamp(value);
480
+ if (!timestamp) return null;
481
+
482
+ const timestampMs = timestamp.getTime();
483
+ return Number.isFinite(timestampMs) ? timestampMs : null;
484
+ }
485
+
471
486
  private determineStatus(session: CodexSession): AgentStatus {
472
487
  const diffMs = Date.now() - session.lastActive.getTime();
473
488
  const diffMinutes = diffMs / 60000;
@@ -19,9 +19,10 @@ import type {
19
19
  SessionSummary,
20
20
  } from './AgentAdapter.js';
21
21
  import { AgentStatus } from './AgentAdapter.js';
22
- import { enrichProcesses, listAgentProcesses } from '../utils/process.js';
22
+ import { enrichProcesses, findWrapperProcess, findWrapperProcessPids, listAgentProcesses } from '../utils/process.js';
23
23
  import { generateAgentName } from '../utils/matching.js';
24
24
  import { isDirectory, safeReadFile, safeReaddir, safeStat } from '../utils/session.js';
25
+ import { AgentRegistry, type RegistryEntry } from '../utils/AgentRegistry.js';
25
26
 
26
27
  interface CopilotEventEntry {
27
28
  type?: string;
@@ -107,10 +108,12 @@ export class CopilotAdapter implements AgentAdapter {
107
108
  ]);
108
109
 
109
110
  private sessionStateDir: string;
111
+ private registry: AgentRegistry;
110
112
 
111
- constructor() {
113
+ constructor(registry: AgentRegistry = AgentRegistry.default()) {
112
114
  const homeDir = process.env.HOME || process.env.USERPROFILE || '';
113
115
  this.sessionStateDir = path.join(homeDir, '.copilot', 'session-state');
116
+ this.registry = registry;
114
117
  }
115
118
 
116
119
  canHandle(processInfo: ProcessInfo): boolean {
@@ -122,6 +125,7 @@ export class CopilotAdapter implements AgentAdapter {
122
125
  if (processes.length === 0) return [];
123
126
 
124
127
  const processByPid = new Map(processes.map((proc) => [proc.pid, proc]));
128
+ const registryEntriesByPid = new Map(this.registry.list().map((entry) => [entry.pid, entry]));
125
129
  const matchedPids = new Set<number>();
126
130
  const matchedProcesses: ProcessInfo[] = [];
127
131
  const agents: AgentInfo[] = [];
@@ -133,29 +137,36 @@ export class CopilotAdapter implements AgentAdapter {
133
137
  const session = this.readSessionDir(lock.sessionDir, lock.sessionId);
134
138
  if (!session) continue;
135
139
 
136
- agents.push(this.mapSessionToAgent(session, proc));
140
+ const agent = this.mapSessionToAgent(session, proc);
141
+ this.applyWrapperRegistryName(agent, proc, processes, registryEntriesByPid);
142
+ agents.push(agent);
137
143
  matchedPids.add(proc.pid);
138
144
  matchedProcesses.push(proc);
139
145
  }
140
146
 
147
+ const wrapperPids = findWrapperProcessPids(processes, matchedProcesses);
141
148
  for (const proc of processes) {
142
- if (!matchedPids.has(proc.pid) && !this.isDuplicateProcess(proc, matchedProcesses)) {
143
- agents.push(this.mapProcessOnlyAgent(proc));
149
+ if (!matchedPids.has(proc.pid) && !wrapperPids.has(proc.pid)) {
150
+ const agent = this.mapProcessOnlyAgent(proc);
151
+ this.applyWrapperRegistryName(agent, proc, processes, registryEntriesByPid);
152
+ agents.push(agent);
144
153
  }
145
154
  }
146
155
 
147
156
  return agents;
148
157
  }
149
158
 
150
- private isDuplicateProcess(proc: ProcessInfo, matchedProcesses: ProcessInfo[]): boolean {
151
- return matchedProcesses.some((matched) => {
152
- if (proc.pid === matched.pid) return true;
153
-
154
- const sameTty = proc.tty !== '' && proc.tty !== '?' && proc.tty === matched.tty;
155
- const sameCwd = proc.cwd !== '' && proc.cwd === matched.cwd;
156
-
157
- return sameTty && (sameCwd || proc.cwd === '' || matched.cwd === '');
158
- });
159
+ private applyWrapperRegistryName(
160
+ agent: AgentInfo,
161
+ processInfo: ProcessInfo,
162
+ processes: ProcessInfo[],
163
+ registryEntriesByPid: Map<number, RegistryEntry>,
164
+ ): void {
165
+ const wrapper = findWrapperProcess(processes, processInfo);
166
+ const wrapperEntry = wrapper ? registryEntriesByPid.get(wrapper.pid) : undefined;
167
+ if (wrapperEntry?.type === this.type) {
168
+ agent.name = wrapperEntry.name;
169
+ }
159
170
  }
160
171
 
161
172
  getConversation(sessionFilePath: string, options?: { verbose?: boolean }): ConversationMessage[] {
@@ -22,11 +22,11 @@ import type {
22
22
  ListSessionsOptions,
23
23
  } from './AgentAdapter.js';
24
24
  import { AgentStatus } from './AgentAdapter.js';
25
- import { listAgentProcesses, enrichProcesses } from '../utils/process.js';
25
+ import { listAgentProcesses, enrichProcesses, findWrapperProcess, findWrapperProcessPids } from '../utils/process.js';
26
26
  import { isDirectory, safeReadFile, safeReaddir, safeStat } from '../utils/session.js';
27
27
  import type { SessionFile } from '../utils/session.js';
28
28
  import { matchProcessesToSessions, generateAgentName } from '../utils/matching.js';
29
- import { AgentRegistry } from '../utils/AgentRegistry.js';
29
+ import { AgentRegistry, type RegistryEntry } from '../utils/AgentRegistry.js';
30
30
 
31
31
  /**
32
32
  * A single Gemini CLI message content part. Mirrors the `{text?: string}`
@@ -117,40 +117,70 @@ export class GeminiCliAdapter implements AgentAdapter {
117
117
  const processes = nodeProcesses.filter((proc) => this.isGeminiExecutable(proc.command));
118
118
  if (processes.length === 0) return [];
119
119
 
120
- const { cachedAgents, remaining } = this.tryRegistryCache(processes);
120
+ const wrapperPids = findWrapperProcessPids(processes);
121
+ const { cachedAgents, remaining } = this.tryRegistryCache(processes, wrapperPids);
121
122
  if (remaining.length === 0) return this.deduplicateSessionAgents(cachedAgents);
122
123
 
123
- const { sessions, contentCache } = this.discoverSessions(remaining);
124
+ const candidateProcesses = remaining.filter((proc) => !wrapperPids.has(proc.pid));
125
+ const registryEntriesByPid = new Map(this.registry.list().map((entry) => [entry.pid, entry]));
126
+
127
+ const { sessions, contentCache } = this.discoverSessions(candidateProcesses);
124
128
  if (sessions.length === 0) {
129
+ const processOnlyAgents = candidateProcesses.map((proc) => {
130
+ const agent = this.mapProcessOnlyAgent(proc);
131
+ this.applyWrapperRegistryName(agent, proc, processes, registryEntriesByPid);
132
+ return agent;
133
+ });
134
+
125
135
  return this.deduplicateSessionAgents([
126
136
  ...cachedAgents,
127
- ...remaining.map((p) => this.mapProcessOnlyAgent(p)),
137
+ ...processOnlyAgents,
128
138
  ]);
129
139
  }
130
140
 
131
- const matches = matchProcessesToSessions(remaining, sessions);
141
+ const matches = matchProcessesToSessions(candidateProcesses, sessions);
132
142
  const matchedPids = new Set(matches.map((m) => m.process.pid));
143
+ const matchedProcesses: ProcessInfo[] = [];
133
144
  const agents: AgentInfo[] = [];
134
145
 
135
146
  for (const match of matches) {
136
147
  const cachedContent = contentCache.get(match.session.filePath);
137
148
  const sessionData = this.parseSession(cachedContent, match.session.filePath);
138
149
  if (sessionData) {
139
- agents.push(this.mapSessionToAgent(sessionData, match.process, match.session.filePath));
150
+ const agent = this.mapSessionToAgent(sessionData, match.process, match.session.filePath);
151
+ this.applyWrapperRegistryName(agent, match.process, processes, registryEntriesByPid);
152
+ agents.push(agent);
153
+ matchedProcesses.push(match.process);
140
154
  } else {
141
155
  matchedPids.delete(match.process.pid);
142
156
  }
143
157
  }
144
158
 
145
- for (const proc of remaining) {
146
- if (!matchedPids.has(proc.pid)) {
147
- agents.push(this.mapProcessOnlyAgent(proc));
159
+ const matchedWrapperPids = findWrapperProcessPids(candidateProcesses, matchedProcesses);
160
+ for (const proc of candidateProcesses) {
161
+ if (!matchedPids.has(proc.pid) && !matchedWrapperPids.has(proc.pid)) {
162
+ const agent = this.mapProcessOnlyAgent(proc);
163
+ this.applyWrapperRegistryName(agent, proc, processes, registryEntriesByPid);
164
+ agents.push(agent);
148
165
  }
149
166
  }
150
167
 
151
168
  return this.deduplicateSessionAgents([...cachedAgents, ...agents]);
152
169
  }
153
170
 
171
+ private applyWrapperRegistryName(
172
+ agent: AgentInfo,
173
+ processInfo: ProcessInfo,
174
+ processes: ProcessInfo[],
175
+ registryEntriesByPid: Map<number, RegistryEntry>,
176
+ ): void {
177
+ const wrapper = findWrapperProcess(processes, processInfo);
178
+ const wrapperEntry = wrapper ? registryEntriesByPid.get(wrapper.pid) : undefined;
179
+ if (wrapperEntry?.type === this.type) {
180
+ agent.name = wrapperEntry.name;
181
+ }
182
+ }
183
+
154
184
  private deduplicateSessionAgents(agents: AgentInfo[]): AgentInfo[] {
155
185
  const bySession = new Map<string, AgentInfo>();
156
186
  const result: AgentInfo[] = [];
@@ -187,7 +217,7 @@ export class GeminiCliAdapter implements AgentAdapter {
187
217
  return null;
188
218
  }
189
219
 
190
- private tryRegistryCache(processes: ProcessInfo[]): {
220
+ private tryRegistryCache(processes: ProcessInfo[], wrapperPids: Set<number>): {
191
221
  cachedAgents: AgentInfo[];
192
222
  remaining: ProcessInfo[];
193
223
  } {
@@ -198,6 +228,7 @@ export class GeminiCliAdapter implements AgentAdapter {
198
228
  for (const proc of processes) {
199
229
  const entry = byPid.get(proc.pid);
200
230
  if (
231
+ wrapperPids.has(proc.pid) ||
201
232
  !entry ||
202
233
  entry.type !== this.type ||
203
234
  !entry.sessionFilePath ||
@@ -12,10 +12,10 @@ import type { ProcessInfo } from '../adapters/AgentAdapter.js';
12
12
  /**
13
13
  * List running processes matching an agent executable name.
14
14
  *
15
- * Uses `ps aux` then filters in JS for exact executable basename match.
15
+ * Uses `ps -axo` then filters in JS for exact executable basename match.
16
16
  * This avoids shell pipelines and string interpolation.
17
17
  *
18
- * Returned ProcessInfo has pid, command, tty populated.
18
+ * Returned ProcessInfo has pid, ppid, command, tty populated.
19
19
  * cwd and startTime are NOT populated — call enrichProcesses() to fill them.
20
20
  */
21
21
  export function listAgentProcesses(namePattern: string): ProcessInfo[] {
@@ -25,7 +25,7 @@ export function listAgentProcesses(namePattern: string): ProcessInfo[] {
25
25
  }
26
26
 
27
27
  try {
28
- const output = execFileSync('ps', ['aux'], { encoding: 'utf-8' });
28
+ const output = execFileSync('ps', ['-axo', 'pid=,ppid=,tty=,command='], { encoding: 'utf-8' });
29
29
 
30
30
  const lowerPattern = namePattern.toLowerCase();
31
31
  const processes: ProcessInfo[] = [];
@@ -33,14 +33,15 @@ export function listAgentProcesses(namePattern: string): ProcessInfo[] {
33
33
  for (const line of output.trim().split('\n')) {
34
34
  if (!line.trim()) continue;
35
35
 
36
- const parts = line.trim().split(/\s+/);
37
- if (parts.length < 11) continue;
36
+ const match = line.match(/^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.+)$/);
37
+ if (!match) continue;
38
38
 
39
- const pid = parseInt(parts[1], 10);
40
- if (Number.isNaN(pid)) continue;
39
+ const pid = parseInt(match[1], 10);
40
+ const ppid = parseInt(match[2], 10);
41
+ if (Number.isNaN(pid) || Number.isNaN(ppid)) continue;
41
42
 
42
- const tty = parts[6];
43
- const command = parts.slice(10).join(' ');
43
+ const tty = match[3];
44
+ const command = match[4];
44
45
 
45
46
  // Check that the executable basename matches exactly
46
47
  const executable = command.trim().split(/\s+/)[0] || '';
@@ -53,6 +54,7 @@ export function listAgentProcesses(namePattern: string): ProcessInfo[] {
53
54
 
54
55
  processes.push({
55
56
  pid,
57
+ ppid,
56
58
  command,
57
59
  cwd: '',
58
60
  tty: ttyShort,
@@ -177,6 +179,59 @@ export function enrichProcesses(processes: ProcessInfo[]): ProcessInfo[] {
177
179
  return processes;
178
180
  }
179
181
 
182
+ function isSameTerminalProcess(proc: ProcessInfo, matched: ProcessInfo): boolean {
183
+ const sameTty = proc.tty !== '' && proc.tty !== '?' && proc.tty === matched.tty;
184
+ const sameCwd = proc.cwd !== '' && proc.cwd === matched.cwd;
185
+
186
+ return sameTty && (sameCwd || proc.cwd === '' || matched.cwd === '');
187
+ }
188
+
189
+ function matchesProcessIdentity(proc: ProcessInfo, matched: ProcessInfo): boolean {
190
+ return proc.pid === matched.pid || isSameTerminalProcess(proc, matched);
191
+ }
192
+
193
+ export function findWrapperProcess(
194
+ processes: ProcessInfo[],
195
+ child: ProcessInfo,
196
+ ): ProcessInfo | undefined {
197
+ return processes.find((proc) => (
198
+ proc.pid !== child.pid &&
199
+ child.ppid === proc.pid &&
200
+ matchesProcessIdentity(proc, child)
201
+ ));
202
+ }
203
+
204
+ /**
205
+ * Find parent wrapper processes that should not be reported as separate agents.
206
+ *
207
+ * A process is considered a wrapper when it is the parent of another candidate
208
+ * agent process in the same terminal/worktree, or when it points at the same
209
+ * terminal/worktree as an already session-matched process.
210
+ */
211
+ export function findWrapperProcessPids(
212
+ processes: ProcessInfo[],
213
+ matchedProcesses: ProcessInfo[] = [],
214
+ ): Set<number> {
215
+ const wrappers = new Set<number>();
216
+
217
+ for (const child of processes) {
218
+ const wrapper = findWrapperProcess(processes, child);
219
+ if (wrapper) {
220
+ wrappers.add(wrapper.pid);
221
+ }
222
+ }
223
+
224
+ for (const proc of processes) {
225
+ if (matchedProcesses.some((matched) => (
226
+ proc.pid !== matched.pid && isSameTerminalProcess(proc, matched)
227
+ ))) {
228
+ wrappers.add(proc.pid);
229
+ }
230
+ }
231
+
232
+ return wrappers;
233
+ }
234
+
180
235
  /**
181
236
  * Get the TTY device for a specific process
182
237
  */