@ai-devkit/agent-manager 0.26.2 → 0.26.4

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 (104) hide show
  1. package/dist/AgentManager.d.ts +5 -2
  2. package/dist/AgentManager.d.ts.map +1 -1
  3. package/dist/AgentManager.js +28 -10
  4. package/dist/AgentManager.js.map +1 -1
  5. package/dist/__tests__/AgentManager.test.js +289 -4
  6. package/dist/__tests__/AgentManager.test.js.map +1 -1
  7. package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js +13 -4
  8. package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js.map +1 -1
  9. package/dist/__tests__/adapters/CodexAdapter.test.js +13 -4
  10. package/dist/__tests__/adapters/CodexAdapter.test.js.map +1 -1
  11. package/dist/__tests__/adapters/CopilotAdapter.test.js +7 -2
  12. package/dist/__tests__/adapters/CopilotAdapter.test.js.map +1 -1
  13. package/dist/__tests__/adapters/GeminiCliAdapter.test.js +18 -5
  14. package/dist/__tests__/adapters/GeminiCliAdapter.test.js.map +1 -1
  15. package/dist/__tests__/adapters/GrokCliAdapter.test.js +7 -2
  16. package/dist/__tests__/adapters/GrokCliAdapter.test.js.map +1 -1
  17. package/dist/__tests__/adapters/OpenCodeAdapter.test.js +13 -4
  18. package/dist/__tests__/adapters/OpenCodeAdapter.test.js.map +1 -1
  19. package/dist/__tests__/adapters/PiAdapter.test.js +32 -4
  20. package/dist/__tests__/adapters/PiAdapter.test.js.map +1 -1
  21. package/dist/__tests__/utils/AgentRegistry.test.js +140 -0
  22. package/dist/__tests__/utils/AgentRegistry.test.js.map +1 -1
  23. package/dist/__tests__/utils/process.test.js +90 -2
  24. package/dist/__tests__/utils/process.test.js.map +1 -1
  25. package/dist/adapters/AgentAdapter.d.ts +9 -3
  26. package/dist/adapters/AgentAdapter.d.ts.map +1 -1
  27. package/dist/adapters/AgentAdapter.js.map +1 -1
  28. package/dist/adapters/ClaudeCodeAdapter.d.ts +5 -4
  29. package/dist/adapters/ClaudeCodeAdapter.d.ts.map +1 -1
  30. package/dist/adapters/ClaudeCodeAdapter.js +11 -7
  31. package/dist/adapters/ClaudeCodeAdapter.js.map +1 -1
  32. package/dist/adapters/CodexAdapter.d.ts +5 -4
  33. package/dist/adapters/CodexAdapter.d.ts.map +1 -1
  34. package/dist/adapters/CodexAdapter.js +11 -7
  35. package/dist/adapters/CodexAdapter.js.map +1 -1
  36. package/dist/adapters/CopilotAdapter.d.ts +5 -4
  37. package/dist/adapters/CopilotAdapter.d.ts.map +1 -1
  38. package/dist/adapters/CopilotAdapter.js +11 -7
  39. package/dist/adapters/CopilotAdapter.js.map +1 -1
  40. package/dist/adapters/GeminiCliAdapter.d.ts +6 -5
  41. package/dist/adapters/GeminiCliAdapter.d.ts.map +1 -1
  42. package/dist/adapters/GeminiCliAdapter.js +12 -8
  43. package/dist/adapters/GeminiCliAdapter.js.map +1 -1
  44. package/dist/adapters/GrokCliAdapter.d.ts +3 -2
  45. package/dist/adapters/GrokCliAdapter.d.ts.map +1 -1
  46. package/dist/adapters/GrokCliAdapter.js +10 -6
  47. package/dist/adapters/GrokCliAdapter.js.map +1 -1
  48. package/dist/adapters/OpenCodeAdapter.d.ts +5 -4
  49. package/dist/adapters/OpenCodeAdapter.d.ts.map +1 -1
  50. package/dist/adapters/OpenCodeAdapter.js +11 -7
  51. package/dist/adapters/OpenCodeAdapter.js.map +1 -1
  52. package/dist/adapters/PiAdapter.d.ts +3 -2
  53. package/dist/adapters/PiAdapter.d.ts.map +1 -1
  54. package/dist/adapters/PiAdapter.js +12 -9
  55. package/dist/adapters/PiAdapter.js.map +1 -1
  56. package/dist/adapters/index.d.ts +1 -1
  57. package/dist/adapters/index.d.ts.map +1 -1
  58. package/dist/adapters/index.js.map +1 -1
  59. package/dist/database/connection.d.ts +1 -1
  60. package/dist/database/connection.d.ts.map +1 -1
  61. package/dist/database/connection.js +1 -1
  62. package/dist/database/connection.js.map +1 -1
  63. package/dist/index.d.ts +3 -2
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +1 -0
  66. package/dist/index.js.map +1 -1
  67. package/dist/utils/AgentRegistry.d.ts +14 -1
  68. package/dist/utils/AgentRegistry.d.ts.map +1 -1
  69. package/dist/utils/AgentRegistry.js +65 -20
  70. package/dist/utils/AgentRegistry.js.map +1 -1
  71. package/dist/utils/index.d.ts +1 -1
  72. package/dist/utils/index.d.ts.map +1 -1
  73. package/dist/utils/index.js +1 -1
  74. package/dist/utils/index.js.map +1 -1
  75. package/dist/utils/process.d.ts +9 -1
  76. package/dist/utils/process.d.ts.map +1 -1
  77. package/dist/utils/process.js +155 -10
  78. package/dist/utils/process.js.map +1 -1
  79. package/package.json +1 -1
  80. package/src/AgentManager.ts +35 -8
  81. package/src/__tests__/AgentManager.test.ts +231 -4
  82. package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +16 -5
  83. package/src/__tests__/adapters/CodexAdapter.test.ts +16 -5
  84. package/src/__tests__/adapters/CopilotAdapter.test.ts +8 -1
  85. package/src/__tests__/adapters/GeminiCliAdapter.test.ts +20 -4
  86. package/src/__tests__/adapters/GrokCliAdapter.test.ts +8 -1
  87. package/src/__tests__/adapters/OpenCodeAdapter.test.ts +16 -5
  88. package/src/__tests__/adapters/PiAdapter.test.ts +28 -5
  89. package/src/__tests__/utils/AgentRegistry.test.ts +119 -0
  90. package/src/__tests__/utils/process.test.ts +72 -1
  91. package/src/adapters/AgentAdapter.ts +11 -3
  92. package/src/adapters/ClaudeCodeAdapter.ts +10 -7
  93. package/src/adapters/CodexAdapter.ts +10 -7
  94. package/src/adapters/CopilotAdapter.ts +16 -7
  95. package/src/adapters/GeminiCliAdapter.ts +17 -8
  96. package/src/adapters/GrokCliAdapter.ts +9 -6
  97. package/src/adapters/OpenCodeAdapter.ts +10 -7
  98. package/src/adapters/PiAdapter.ts +10 -9
  99. package/src/adapters/index.ts +1 -1
  100. package/src/database/connection.ts +4 -2
  101. package/src/index.ts +3 -1
  102. package/src/utils/AgentRegistry.ts +85 -17
  103. package/src/utils/index.ts +1 -1
  104. package/src/utils/process.ts +160 -10
@@ -9,16 +9,21 @@ import * as path from 'path';
9
9
  import { ClaudeCodeAdapter } from '../../adapters/ClaudeCodeAdapter.js';
10
10
  import type { ProcessInfo } from '../../adapters/AgentAdapter.js';
11
11
  import { AgentStatus } from '../../adapters/AgentAdapter.js';
12
- import { listAgentProcesses, enrichProcesses } from '../../utils/process.js';
12
+ import { listAgentProcesses, enrichProcesses, captureProcessSnapshot } from '../../utils/process.js';
13
13
  import { batchGetSessionFileBirthtimes } from '../../utils/session.js';
14
14
  import type { SessionFile } from '../../utils/session.js';
15
15
  import { matchProcessesToSessions, generateAgentName } from '../../utils/matching.js';
16
16
  import type { MatchResult } from '../../utils/matching.js';
17
17
  import * as os from 'os';
18
- vi.mock('../../utils/process.js', () => ({
19
- listAgentProcesses: vi.fn(),
20
- enrichProcesses: vi.fn(),
21
- }));
18
+ vi.mock('../../utils/process.js', async (importOriginal) => {
19
+ const actual = await importOriginal() as typeof import('../../utils/process.js');
20
+ return {
21
+ ...actual,
22
+ listAgentProcesses: vi.fn(),
23
+ enrichProcesses: vi.fn(),
24
+ captureProcessSnapshot: vi.fn(),
25
+ };
26
+ });
22
27
 
23
28
  vi.mock('../../utils/session.js', async () => {
24
29
  const actual = await vi.importActual('../../utils/session') as typeof import('../../utils/session');
@@ -35,6 +40,7 @@ vi.mock('../../utils/matching.js', () => ({
35
40
 
36
41
  const mockedListAgentProcesses = listAgentProcesses as MockedFunction<typeof listAgentProcesses>;
37
42
  const mockedEnrichProcesses = enrichProcesses as MockedFunction<typeof enrichProcesses>;
43
+ const mockedCaptureProcessSnapshot = captureProcessSnapshot as MockedFunction<typeof captureProcessSnapshot>;
38
44
  const mockedBatchGetSessionFileBirthtimes = batchGetSessionFileBirthtimes as MockedFunction<typeof batchGetSessionFileBirthtimes>;
39
45
  const mockedMatchProcessesToSessions = matchProcessesToSessions as MockedFunction<typeof matchProcessesToSessions>;
40
46
  const mockedGenerateAgentName = generateAgentName as MockedFunction<typeof generateAgentName>;
@@ -45,11 +51,16 @@ describe('ClaudeCodeAdapter', () => {
45
51
  adapter = new ClaudeCodeAdapter();
46
52
  mockedListAgentProcesses.mockReset();
47
53
  mockedEnrichProcesses.mockReset();
54
+ mockedCaptureProcessSnapshot.mockReset();
48
55
  mockedBatchGetSessionFileBirthtimes.mockReset();
49
56
  mockedMatchProcessesToSessions.mockReset();
50
57
  mockedGenerateAgentName.mockReset();
51
58
  // Default: enrichProcesses returns what it receives
52
59
  mockedEnrichProcesses.mockImplementation((procs) => procs);
60
+ // Compatibility shim for standalone adapter discovery; the manager captures once and slices by name.
61
+ mockedCaptureProcessSnapshot.mockImplementation(async (names) => (
62
+ enrichProcesses(names.flatMap((name) => listAgentProcesses(name)))
63
+ ));
53
64
  // Default: generateAgentName returns "folder (pid)"
54
65
  mockedGenerateAgentName.mockImplementation((cwd, pid) => {
55
66
  const folder = path.basename(cwd) || 'unknown';
@@ -10,17 +10,22 @@ import { CodexAdapter } from '../../adapters/CodexAdapter.js';
10
10
  import type { ProcessInfo } from '../../adapters/AgentAdapter.js';
11
11
  import { AgentStatus } from '../../adapters/AgentAdapter.js';
12
12
  import { AgentRegistry, type RegistryEntry } from '../../utils/AgentRegistry.js';
13
- import { listAgentProcesses, enrichProcesses } from '../../utils/process.js';
13
+ import { listAgentProcesses, enrichProcesses, captureProcessSnapshot } from '../../utils/process.js';
14
14
  import { batchGetSessionFileBirthtimes } from '../../utils/session.js';
15
15
  import type { SessionFile } from '../../utils/session.js';
16
16
  import { matchProcessesToSessions, generateAgentName } from '../../utils/matching.js';
17
17
  import type { MatchResult } from '../../utils/matching.js';
18
18
  import * as os from 'os';
19
19
 
20
- vi.mock('../../utils/process.js', () => ({
21
- listAgentProcesses: vi.fn(),
22
- enrichProcesses: vi.fn(),
23
- }));
20
+ vi.mock('../../utils/process.js', async (importOriginal) => {
21
+ const actual = await importOriginal() as typeof import('../../utils/process.js');
22
+ return {
23
+ ...actual,
24
+ listAgentProcesses: vi.fn(),
25
+ enrichProcesses: vi.fn(),
26
+ captureProcessSnapshot: vi.fn(),
27
+ };
28
+ });
24
29
 
25
30
  vi.mock('../../utils/session.js', async () => {
26
31
  const actual = await vi.importActual('../../utils/session') as typeof import('../../utils/session');
@@ -37,6 +42,7 @@ vi.mock('../../utils/matching.js', () => ({
37
42
 
38
43
  const mockedListAgentProcesses = listAgentProcesses as MockedFunction<typeof listAgentProcesses>;
39
44
  const mockedEnrichProcesses = enrichProcesses as MockedFunction<typeof enrichProcesses>;
45
+ const mockedCaptureProcessSnapshot = captureProcessSnapshot as MockedFunction<typeof captureProcessSnapshot>;
40
46
  const mockedBatchGetSessionFileBirthtimes = batchGetSessionFileBirthtimes as MockedFunction<typeof batchGetSessionFileBirthtimes>;
41
47
  const mockedMatchProcessesToSessions = matchProcessesToSessions as MockedFunction<typeof matchProcessesToSessions>;
42
48
  const mockedGenerateAgentName = generateAgentName as MockedFunction<typeof generateAgentName>;
@@ -48,11 +54,16 @@ describe('CodexAdapter', () => {
48
54
  adapter = new CodexAdapter();
49
55
  mockedListAgentProcesses.mockReset();
50
56
  mockedEnrichProcesses.mockReset();
57
+ mockedCaptureProcessSnapshot.mockReset();
51
58
  mockedBatchGetSessionFileBirthtimes.mockReset();
52
59
  mockedMatchProcessesToSessions.mockReset();
53
60
  mockedGenerateAgentName.mockReset();
54
61
  // Default: enrichProcesses returns what it receives
55
62
  mockedEnrichProcesses.mockImplementation((procs) => procs);
63
+ // Compatibility shim for standalone adapter discovery; the manager captures once and slices by name.
64
+ mockedCaptureProcessSnapshot.mockImplementation(async (names) => (
65
+ enrichProcesses(names.flatMap((name) => listAgentProcesses(name)))
66
+ ));
56
67
  // Default: generateAgentName returns "folder (pid)"
57
68
  mockedGenerateAgentName.mockImplementation((cwd, pid) => {
58
69
  const folder = path.basename(cwd) || 'unknown';
@@ -10,7 +10,7 @@ import * as path from 'path';
10
10
  import { CopilotAdapter } from '../../adapters/CopilotAdapter.js';
11
11
  import type { ProcessInfo } from '../../adapters/AgentAdapter.js';
12
12
  import { AgentStatus } from '../../adapters/AgentAdapter.js';
13
- import { listAgentProcesses, enrichProcesses } from '../../utils/process.js';
13
+ import { listAgentProcesses, enrichProcesses, captureProcessSnapshot } from '../../utils/process.js';
14
14
  import { generateAgentName } from '../../utils/matching.js';
15
15
  import { AgentRegistry } from '../../utils/AgentRegistry.js';
16
16
 
@@ -20,6 +20,7 @@ vi.mock('../../utils/process.js', async (importOriginal) => {
20
20
  ...actual,
21
21
  listAgentProcesses: vi.fn(),
22
22
  enrichProcesses: vi.fn(),
23
+ captureProcessSnapshot: vi.fn(),
23
24
  };
24
25
  });
25
26
 
@@ -29,6 +30,7 @@ vi.mock('../../utils/matching.js', () => ({
29
30
 
30
31
  const mockedListAgentProcesses = listAgentProcesses as MockedFunction<typeof listAgentProcesses>;
31
32
  const mockedEnrichProcesses = enrichProcesses as MockedFunction<typeof enrichProcesses>;
33
+ const mockedCaptureProcessSnapshot = captureProcessSnapshot as MockedFunction<typeof captureProcessSnapshot>;
32
34
  const mockedGenerateAgentName = generateAgentName as MockedFunction<typeof generateAgentName>;
33
35
 
34
36
  describe('CopilotAdapter', () => {
@@ -45,8 +47,13 @@ describe('CopilotAdapter', () => {
45
47
 
46
48
  mockedListAgentProcesses.mockReset();
47
49
  mockedEnrichProcesses.mockReset();
50
+ mockedCaptureProcessSnapshot.mockReset();
48
51
  mockedGenerateAgentName.mockReset();
49
52
  mockedEnrichProcesses.mockImplementation((procs) => procs);
53
+ // Compatibility shim for standalone adapter discovery; the manager captures once and slices by name.
54
+ mockedCaptureProcessSnapshot.mockImplementation(async (names) => (
55
+ enrichProcesses(names.flatMap((name) => listAgentProcesses(name)))
56
+ ));
50
57
  mockedGenerateAgentName.mockImplementation((cwd, pid) => `${path.basename(cwd) || 'unknown'} (${pid})`);
51
58
  });
52
59
 
@@ -11,7 +11,7 @@ import { GeminiCliAdapter } from '../../adapters/GeminiCliAdapter.js';
11
11
  import type { ProcessInfo } from '../../adapters/AgentAdapter.js';
12
12
  import { AgentStatus } from '../../adapters/AgentAdapter.js';
13
13
  import { AgentRegistry, type RegistryEntry } from '../../utils/AgentRegistry.js';
14
- import { listAgentProcesses, enrichProcesses } from '../../utils/process.js';
14
+ import { listAgentProcesses, enrichProcesses, captureProcessSnapshot } from '../../utils/process.js';
15
15
  import { matchProcessesToSessions, generateAgentName } from '../../utils/matching.js';
16
16
  import * as crypto from 'crypto';
17
17
 
@@ -21,6 +21,7 @@ vi.mock('../../utils/process.js', async (importOriginal) => {
21
21
  ...actual,
22
22
  listAgentProcesses: vi.fn(),
23
23
  enrichProcesses: vi.fn(),
24
+ captureProcessSnapshot: vi.fn(),
24
25
  };
25
26
  });
26
27
 
@@ -31,6 +32,7 @@ vi.mock('../../utils/matching.js', () => ({
31
32
 
32
33
  const mockedListAgentProcesses = listAgentProcesses as MockedFunction<typeof listAgentProcesses>;
33
34
  const mockedEnrichProcesses = enrichProcesses as MockedFunction<typeof enrichProcesses>;
35
+ const mockedCaptureProcessSnapshot = captureProcessSnapshot as MockedFunction<typeof captureProcessSnapshot>;
34
36
  const mockedMatchProcessesToSessions = matchProcessesToSessions as MockedFunction<typeof matchProcessesToSessions>;
35
37
  const mockedGenerateAgentName = generateAgentName as MockedFunction<typeof generateAgentName>;
36
38
 
@@ -45,10 +47,15 @@ describe('GeminiCliAdapter', () => {
45
47
  adapter = new GeminiCliAdapter(new AgentRegistry(path.join(tmpHome, 'agents.json')));
46
48
  mockedListAgentProcesses.mockReset();
47
49
  mockedEnrichProcesses.mockReset();
50
+ mockedCaptureProcessSnapshot.mockReset();
48
51
  mockedMatchProcessesToSessions.mockReset();
49
52
  mockedGenerateAgentName.mockReset();
50
53
 
51
54
  mockedEnrichProcesses.mockImplementation((procs) => procs);
55
+ // Compatibility shim for standalone adapter discovery; the manager captures once and slices by name.
56
+ mockedCaptureProcessSnapshot.mockImplementation(async (names) => (
57
+ enrichProcesses(names.flatMap((name) => listAgentProcesses(name)))
58
+ ));
52
59
  mockedMatchProcessesToSessions.mockReturnValue([]);
53
60
  mockedGenerateAgentName.mockImplementation((cwd: string, pid: number) => {
54
61
  const folder = path.basename(cwd) || 'unknown';
@@ -110,6 +117,15 @@ describe('GeminiCliAdapter', () => {
110
117
  tty: 'ttys006',
111
118
  })).toBe(true);
112
119
  });
120
+
121
+ it('should recognize Windows executable and entrypoint paths', () => {
122
+ expect(adapter.canHandle({
123
+ pid: 7,
124
+ command: 'C:\\tools\\node.exe C:\\lib\\gemini.js',
125
+ cwd: 'C:\\repo',
126
+ tty: '',
127
+ })).toBe(true);
128
+ });
113
129
  });
114
130
 
115
131
  describe('detectAgents', () => {
@@ -144,7 +160,7 @@ describe('GeminiCliAdapter', () => {
144
160
  it('should return process-only agents when no session files exist for the process', async () => {
145
161
  const proc: ProcessInfo = {
146
162
  pid: 1234,
147
- command: 'gemini',
163
+ command: 'node /usr/local/bin/gemini',
148
164
  cwd: '/repo',
149
165
  tty: 'ttys001',
150
166
  startTime: new Date('2026-04-18T00:00:00Z'),
@@ -257,7 +273,7 @@ describe('GeminiCliAdapter', () => {
257
273
 
258
274
  const proc: ProcessInfo = {
259
275
  pid: 42,
260
- command: 'gemini',
276
+ command: 'node /usr/local/bin/gemini',
261
277
  cwd,
262
278
  tty: 'ttys001',
263
279
  startTime: new Date('2026-04-18T00:00:00Z'),
@@ -383,7 +399,7 @@ describe('GeminiCliAdapter', () => {
383
399
 
384
400
  const proc: ProcessInfo = {
385
401
  pid: 7,
386
- command: 'gemini',
402
+ command: 'node /usr/local/bin/gemini',
387
403
  cwd: procCwd,
388
404
  tty: 'ttys001',
389
405
  startTime: new Date(),
@@ -13,7 +13,7 @@ import * as path from 'path';
13
13
  import { GrokCliAdapter } from '../../adapters/GrokCliAdapter.js';
14
14
  import type { ProcessInfo } from '../../adapters/AgentAdapter.js';
15
15
  import { AgentStatus } from '../../adapters/AgentAdapter.js';
16
- import { listAgentProcesses, enrichProcesses } from '../../utils/process.js';
16
+ import { listAgentProcesses, enrichProcesses, captureProcessSnapshot } from '../../utils/process.js';
17
17
  import { generateAgentName } from '../../utils/matching.js';
18
18
 
19
19
  vi.mock('../../utils/process.js', async (importOriginal) => {
@@ -22,6 +22,7 @@ vi.mock('../../utils/process.js', async (importOriginal) => {
22
22
  ...actual,
23
23
  listAgentProcesses: vi.fn(),
24
24
  enrichProcesses: vi.fn(),
25
+ captureProcessSnapshot: vi.fn(),
25
26
  };
26
27
  });
27
28
 
@@ -35,6 +36,7 @@ vi.mock('../../utils/matching.js', async (importOriginal) => {
35
36
 
36
37
  const mockedListAgentProcesses = listAgentProcesses as MockedFunction<typeof listAgentProcesses>;
37
38
  const mockedEnrichProcesses = enrichProcesses as MockedFunction<typeof enrichProcesses>;
39
+ const mockedCaptureProcessSnapshot = captureProcessSnapshot as MockedFunction<typeof captureProcessSnapshot>;
38
40
  const mockedGenerateAgentName = generateAgentName as MockedFunction<typeof generateAgentName>;
39
41
 
40
42
  const SESSION_ID = '019f16c3-5d5d-7dc3-85d1-bc629416ca2d';
@@ -64,9 +66,14 @@ describe('GrokCliAdapter', () => {
64
66
 
65
67
  mockedListAgentProcesses.mockReset();
66
68
  mockedEnrichProcesses.mockReset();
69
+ mockedCaptureProcessSnapshot.mockReset();
67
70
  mockedGenerateAgentName.mockReset();
68
71
 
69
72
  mockedEnrichProcesses.mockImplementation((procs) => procs);
73
+ // Compatibility shim for standalone adapter discovery; the manager captures once and slices by name.
74
+ mockedCaptureProcessSnapshot.mockImplementation(async (names) => (
75
+ enrichProcesses(names.flatMap((name) => listAgentProcesses(name)))
76
+ ));
70
77
  mockedGenerateAgentName.mockImplementation((c: string, pid: number) => `${path.basename(c) || 'unknown'}-${pid}`);
71
78
  });
72
79
 
@@ -9,14 +9,19 @@ import * as path from 'path';
9
9
  import { OpenCodeAdapter } from '../../adapters/OpenCodeAdapter.js';
10
10
  import type { ProcessInfo } from '../../adapters/AgentAdapter.js';
11
11
  import { AgentStatus } from '../../adapters/AgentAdapter.js';
12
- import { listAgentProcesses, enrichProcesses } from '../../utils/process.js';
12
+ import { listAgentProcesses, enrichProcesses, captureProcessSnapshot } from '../../utils/process.js';
13
13
  import { generateAgentName } from '../../utils/matching.js';
14
14
  import * as os from 'os';
15
15
 
16
- vi.mock('../../utils/process.js', () => ({
17
- listAgentProcesses: vi.fn(),
18
- enrichProcesses: vi.fn(),
19
- }));
16
+ vi.mock('../../utils/process.js', async (importOriginal) => {
17
+ const actual = await importOriginal() as typeof import('../../utils/process.js');
18
+ return {
19
+ ...actual,
20
+ listAgentProcesses: vi.fn(),
21
+ enrichProcesses: vi.fn(),
22
+ captureProcessSnapshot: vi.fn(),
23
+ };
24
+ });
20
25
 
21
26
  vi.mock('../../utils/matching.js', () => ({
22
27
  generateAgentName: vi.fn(),
@@ -25,6 +30,7 @@ vi.mock('../../utils/matching.js', () => ({
25
30
 
26
31
  const mockedListAgentProcesses = listAgentProcesses as MockedFunction<typeof listAgentProcesses>;
27
32
  const mockedEnrichProcesses = enrichProcesses as MockedFunction<typeof enrichProcesses>;
33
+ const mockedCaptureProcessSnapshot = captureProcessSnapshot as MockedFunction<typeof captureProcessSnapshot>;
28
34
  const mockedGenerateAgentName = generateAgentName as MockedFunction<typeof generateAgentName>;
29
35
 
30
36
  function makeDb(queries: {
@@ -104,9 +110,14 @@ describe('OpenCodeAdapter', () => {
104
110
 
105
111
  mockedListAgentProcesses.mockReset();
106
112
  mockedEnrichProcesses.mockReset();
113
+ mockedCaptureProcessSnapshot.mockReset();
107
114
  mockedGenerateAgentName.mockReset();
108
115
 
109
116
  mockedEnrichProcesses.mockImplementation((procs) => procs);
117
+ // Compatibility shim for standalone adapter discovery; the manager captures once and slices by name.
118
+ mockedCaptureProcessSnapshot.mockImplementation(async (names) => (
119
+ enrichProcesses(names.flatMap((name) => listAgentProcesses(name)))
120
+ ));
110
121
  mockedGenerateAgentName.mockImplementation((cwd, pid) => {
111
122
  const folder = path.basename(cwd) || 'unknown';
112
123
  return `${folder}-${pid}`;
@@ -11,13 +11,18 @@ import { PiAdapter } from '../../adapters/PiAdapter.js';
11
11
  import type { ProcessInfo } from '../../adapters/AgentAdapter.js';
12
12
  import { AgentStatus } from '../../adapters/AgentAdapter.js';
13
13
  import { AgentRegistry } from '../../utils/AgentRegistry.js';
14
- import { listAgentProcesses, enrichProcesses } from '../../utils/process.js';
14
+ import { listAgentProcesses, enrichProcesses, captureProcessSnapshot } from '../../utils/process.js';
15
15
  import { matchProcessesToSessions, generateAgentName } from '../../utils/matching.js';
16
16
 
17
- vi.mock('../../utils/process.js', () => ({
18
- listAgentProcesses: vi.fn(),
19
- enrichProcesses: vi.fn(),
20
- }));
17
+ vi.mock('../../utils/process.js', async (importOriginal) => {
18
+ const actual = await importOriginal() as typeof import('../../utils/process.js');
19
+ return {
20
+ ...actual,
21
+ listAgentProcesses: vi.fn(),
22
+ enrichProcesses: vi.fn(),
23
+ captureProcessSnapshot: vi.fn(),
24
+ };
25
+ });
21
26
 
22
27
  vi.mock('../../utils/matching.js', () => ({
23
28
  matchProcessesToSessions: vi.fn(),
@@ -26,6 +31,7 @@ vi.mock('../../utils/matching.js', () => ({
26
31
 
27
32
  const mockedListAgentProcesses = listAgentProcesses as MockedFunction<typeof listAgentProcesses>;
28
33
  const mockedEnrichProcesses = enrichProcesses as MockedFunction<typeof enrichProcesses>;
34
+ const mockedCaptureProcessSnapshot = captureProcessSnapshot as MockedFunction<typeof captureProcessSnapshot>;
29
35
  const mockedMatchProcessesToSessions = matchProcessesToSessions as MockedFunction<typeof matchProcessesToSessions>;
30
36
  const mockedGenerateAgentName = generateAgentName as MockedFunction<typeof generateAgentName>;
31
37
 
@@ -43,10 +49,15 @@ describe('PiAdapter', () => {
43
49
  adapter = new PiAdapter(new AgentRegistry(path.join(tmpHome, 'agents.json')));
44
50
  mockedListAgentProcesses.mockReset();
45
51
  mockedEnrichProcesses.mockReset();
52
+ mockedCaptureProcessSnapshot.mockReset();
46
53
  mockedMatchProcessesToSessions.mockReset();
47
54
  mockedGenerateAgentName.mockReset();
48
55
 
49
56
  mockedEnrichProcesses.mockImplementation((procs) => procs);
57
+ // Compatibility shim for standalone adapter discovery; the manager captures once and slices by name.
58
+ mockedCaptureProcessSnapshot.mockImplementation(async (names) => (
59
+ enrichProcesses(names.flatMap((name) => listAgentProcesses(name)))
60
+ ));
50
61
  mockedMatchProcessesToSessions.mockReturnValue([]);
51
62
  mockedGenerateAgentName.mockImplementation((cwd: string, pid: number) => {
52
63
  const folder = path.basename(cwd) || 'unknown';
@@ -67,6 +78,18 @@ describe('PiAdapter', () => {
67
78
  expect(adapter.canHandle({ pid: 2, command: '/usr/local/bin/PI --model x', cwd: '/repo', tty: 'ttys002' })).toBe(true);
68
79
  expect(adapter.canHandle({ pid: 3, command: 'node /opt/pi/bin/pi.js', cwd: '/repo', tty: 'ttys003' })).toBe(true);
69
80
  expect(adapter.canHandle({ pid: 4, command: 'node /repo/feature-pi-adapter/script.js', cwd: '/repo', tty: 'ttys004' })).toBe(false);
81
+ expect(adapter.canHandle({ pid: 5, command: 'C:\\tools\\node.exe C:\\lib\\pi.js', cwd: '/repo', tty: 'ttys005' })).toBe(true);
82
+ });
83
+
84
+ it('rejects foreign executables from a hand-built snapshot context', async () => {
85
+ const foreign: ProcessInfo = {
86
+ pid: 6,
87
+ command: 'claude --resume /Users/x/repos/pi/session.jsonl',
88
+ cwd: '/repo',
89
+ tty: 'ttys006',
90
+ };
91
+
92
+ expect(await adapter.detectAgents({ processes: [foreign] })).toEqual([]);
70
93
  });
71
94
 
72
95
  it('maps a running Pi process to the tracker session for its PID', async () => {
@@ -29,6 +29,7 @@ describe('AgentRegistry', () => {
29
29
  });
30
30
 
31
31
  afterEach(() => {
32
+ vi.restoreAllMocks();
32
33
  fs.rmSync(tmpDir, { recursive: true, force: true });
33
34
  });
34
35
 
@@ -81,6 +82,19 @@ describe('AgentRegistry', () => {
81
82
  expect(registry.lookup(`ai-devkit-${process.pid}`)).toBeNull();
82
83
  expect(registry.list()).toHaveLength(1);
83
84
  });
85
+
86
+ it('preserves an existing name conflict when its probe fails with EPERM', () => {
87
+ registry.register(makeEntry({ name: 'claimed-name', pid: process.pid }));
88
+ vi.spyOn(process, 'kill').mockImplementation(() => {
89
+ throw Object.assign(new Error('operation not permitted'), { code: 'EPERM' });
90
+ });
91
+
92
+ expect(() => registry.register(makeEntry({
93
+ name: 'claimed-name',
94
+ pid: process.pid + 1,
95
+ }))).toThrow();
96
+ expect(registry.lookup('claimed-name')?.pid).toBe(process.pid);
97
+ });
84
98
  });
85
99
 
86
100
  describe('registerBatch', () => {
@@ -118,6 +132,33 @@ describe('AgentRegistry', () => {
118
132
  expect(registry.list()).toHaveLength(1);
119
133
  expect(registry.lookup('custom-name')?.pid).toBe(process.pid);
120
134
  });
135
+
136
+ it('cleans up a cross-type row when its pid has been reused', () => {
137
+ registry.register(makeEntry({ name: 'old-claude', type: 'claude', pid: process.pid }));
138
+
139
+ registry.register(makeEntry({
140
+ name: 'new-codex',
141
+ type: 'codex',
142
+ pid: process.pid,
143
+ tmuxSession: '',
144
+ }));
145
+
146
+ expect(registry.lookup('old-claude')).toBeNull();
147
+ expect(registry.lookup('new-codex')).toMatchObject({ type: 'codex', pid: process.pid });
148
+ expect(registry.list()).toHaveLength(1);
149
+ });
150
+
151
+ it('rolls back the whole batch when a live name conflict rejects one entry', () => {
152
+ registry.register(makeEntry({ name: 'taken', pid: process.pid }));
153
+
154
+ expect(() => registry.registerBatch([
155
+ makeEntry({ name: 'fresh', pid: 999998 }),
156
+ makeEntry({ name: 'taken', type: 'codex', pid: 999997 }),
157
+ ])).toThrow(/UNIQUE constraint failed/);
158
+
159
+ expect(registry.lookup('fresh')).toBeNull();
160
+ expect(registry.lookup('taken')?.pid).toBe(process.pid);
161
+ });
121
162
  });
122
163
 
123
164
  describe('lookup', () => {
@@ -157,6 +198,30 @@ describe('AgentRegistry', () => {
157
198
  it('returns false for a PID that does not exist', () => {
158
199
  expect(registry.isAlive(makeEntry({ pid: 999999 }))).toBe(false);
159
200
  });
201
+
202
+ it('returns true when the process probe is forbidden with EPERM', () => {
203
+ vi.spyOn(process, 'kill').mockImplementation(() => {
204
+ throw Object.assign(new Error('operation not permitted'), { code: 'EPERM' });
205
+ });
206
+
207
+ expect(registry.isAlive(makeEntry())).toBe(true);
208
+ });
209
+
210
+ it('returns false when the process probe reports ESRCH', () => {
211
+ vi.spyOn(process, 'kill').mockImplementation(() => {
212
+ throw Object.assign(new Error('no such process'), { code: 'ESRCH' });
213
+ });
214
+
215
+ expect(registry.isAlive(makeEntry())).toBe(false);
216
+ });
217
+
218
+ it('returns true when the process probe fails without a definitive error code', () => {
219
+ vi.spyOn(process, 'kill').mockImplementation(() => {
220
+ throw new Error('indeterminate probe failure');
221
+ });
222
+
223
+ expect(registry.isAlive(makeEntry())).toBe(true);
224
+ });
160
225
  });
161
226
 
162
227
  describe('prune', () => {
@@ -177,9 +242,52 @@ describe('AgentRegistry', () => {
177
242
  expect(after).toEqual(before);
178
243
  });
179
244
 
245
+ it('preserves entries when liveness probing fails with EPERM', () => {
246
+ registry.register(makeEntry({ name: 'custom-name', tmuxSession: 'tmux-custom' }));
247
+ vi.spyOn(process, 'kill').mockImplementation(() => {
248
+ throw Object.assign(new Error('operation not permitted'), { code: 'EPERM' });
249
+ });
250
+
251
+ registry.prune();
252
+
253
+ expect(registry.lookup('custom-name')).toMatchObject({
254
+ name: 'custom-name',
255
+ tmuxSession: 'tmux-custom',
256
+ });
257
+ });
258
+
259
+ it('removes entries when liveness probing fails with ESRCH', () => {
260
+ registry.register(makeEntry({ name: 'dead' }));
261
+ vi.spyOn(process, 'kill').mockImplementation(() => {
262
+ throw Object.assign(new Error('no such process'), { code: 'ESRCH' });
263
+ });
264
+
265
+ registry.prune();
266
+
267
+ expect(registry.lookup('dead')).toBeNull();
268
+ });
269
+
180
270
  it('does nothing when file is missing', () => {
181
271
  expect(() => registry.prune()).not.toThrow();
182
272
  });
273
+
274
+ it('keeps forced prune available before the passive cadence is due', () => {
275
+ let nowMs = Date.parse('2026-08-14T10:00:00.000Z');
276
+ const clocked = new AgentRegistry(regPath, {
277
+ now: () => new Date(nowMs),
278
+ pruneIntervalMs: 30_000,
279
+ });
280
+ clocked.register(makeEntry({ name: 'forced', pid: process.pid }));
281
+ const alive = vi.spyOn(clocked, 'isAlive').mockReturnValue(true);
282
+ clocked.pruneIfDue();
283
+ alive.mockReturnValue(false);
284
+ nowMs += 1;
285
+
286
+ clocked.prune();
287
+
288
+ expect(alive).toHaveBeenCalledTimes(2);
289
+ expect(clocked.lookup('forced')).toBeNull();
290
+ });
183
291
  });
184
292
 
185
293
  describe('default()', () => {
@@ -215,6 +323,17 @@ describe('AgentRegistry', () => {
215
323
  expect(() => registry.rename('agent-a', 'agent-b')).toThrow(RenameConflictError);
216
324
  });
217
325
 
326
+ it('throws RenameConflictError when the conflicting entry probe fails with EPERM', () => {
327
+ registry.register(makeEntry({ name: 'agent-a', pid: process.pid }));
328
+ registry.register(makeEntry({ name: 'agent-b', pid: process.ppid }));
329
+ vi.spyOn(process, 'kill').mockImplementation(() => {
330
+ throw Object.assign(new Error('operation not permitted'), { code: 'EPERM' });
331
+ });
332
+
333
+ expect(() => registry.rename('agent-a', 'agent-b')).toThrow(RenameConflictError);
334
+ expect(registry.lookup('agent-b')?.pid).toBe(process.ppid);
335
+ });
336
+
218
337
  it('succeeds when new name exists only as a stale (dead) entry', () => {
219
338
  registry.register(makeEntry({ name: 'agent-a', pid: process.pid }));
220
339
  registry.register(makeEntry({ name: 'agent-b', pid: 999999 }));
@@ -4,7 +4,7 @@
4
4
 
5
5
  import type { MockedFunction } from 'vitest';
6
6
 
7
- import { execFileSync } from 'child_process';
7
+ import { execFile, execFileSync } from 'child_process';
8
8
  import {
9
9
  listAgentProcesses,
10
10
  batchGetProcessCwds,
@@ -12,13 +12,84 @@ import {
12
12
  enrichProcesses,
13
13
  findWrapperProcess,
14
14
  findWrapperProcessPids,
15
+ captureProcessSnapshot,
16
+ filterByProcessNames,
15
17
  } from '../../utils/process.js';
16
18
 
17
19
  vi.mock('child_process', () => ({
20
+ execFile: vi.fn(),
18
21
  execFileSync: vi.fn(),
19
22
  }));
20
23
 
21
24
  const mockedExecFileSync = execFileSync as MockedFunction<typeof execFileSync>;
25
+ const mockedExecFile = execFile as unknown as MockedFunction<(
26
+ file: string,
27
+ args: readonly string[],
28
+ options: object,
29
+ callback: (error: Error | null, stdout: string, stderr: string) => void,
30
+ ) => void>;
31
+
32
+ describe('captureProcessSnapshot', () => {
33
+ beforeEach(() => {
34
+ mockedExecFile.mockReset();
35
+ mockedExecFileSync.mockReset();
36
+ });
37
+
38
+ it('captures and enriches relevant processes without synchronous scans', async () => {
39
+ mockedExecFile.mockImplementation((file, args, _options, callback) => {
40
+ queueMicrotask(() => {
41
+ if (file === 'ps' && args.includes('-axo')) {
42
+ callback(null,
43
+ '100 1 s001 /usr/bin/claude --resume abc\n' +
44
+ '200 1 s002 C:\\\\tools\\\\node.exe C:\\\\bin\\\\pi.js\n' +
45
+ '300 1 s003 /usr/bin/unrelated\n',
46
+ '');
47
+ return;
48
+ }
49
+ if (file === 'lsof') {
50
+ callback(null, 'p100\nn/projects/claude\np200\nn/projects/pi\n', '');
51
+ return;
52
+ }
53
+ if (file === 'ps' && args.some((arg) => arg.includes('lstart='))) {
54
+ callback(null,
55
+ '100 Wed Mar 18 23:18:01 2026\n' +
56
+ '200 Thu Mar 19 10:00:00 2026\n',
57
+ '');
58
+ return;
59
+ }
60
+ callback(new Error(`unexpected command: ${file} ${args.join(' ')}`), '', '');
61
+ });
62
+ });
63
+
64
+ const snapshot = await captureProcessSnapshot(['claude', 'node']);
65
+
66
+ expect(snapshot.map((process) => process.pid)).toEqual([100, 200]);
67
+ expect(snapshot.map((process) => process.cwd)).toEqual(['/projects/claude', '/projects/pi']);
68
+ expect(snapshot.map((process) => process.startTime)).toEqual([
69
+ expect.any(Date),
70
+ expect.any(Date),
71
+ ]);
72
+ expect(mockedExecFileSync).not.toHaveBeenCalled();
73
+ expect(mockedExecFile.mock.calls.filter(([, args]) => args.includes('-axo'))).toHaveLength(1);
74
+ for (const [, , options] of mockedExecFile.mock.calls) {
75
+ expect(options).toMatchObject({ encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 });
76
+ expect(options).not.toHaveProperty('stdio');
77
+ }
78
+ });
79
+ });
80
+
81
+ describe('filterByProcessNames', () => {
82
+ it('matches only argv[0] and normalizes Windows paths and executable suffixes', () => {
83
+ const processes: ProcessInfo[] = [
84
+ { pid: 1, command: 'C:\\tools\\node.exe C:\\bin\\pi.js', cwd: '', tty: '' },
85
+ { pid: 2, command: '/usr/local/bin/node /opt/gemini.js', cwd: '', tty: '' },
86
+ { pid: 3, command: 'codex exec --cd C:\\repos\\node', cwd: '', tty: '' },
87
+ ];
88
+
89
+ expect(filterByProcessNames(processes, ['node'])).toEqual([processes[0], processes[1]]);
90
+ expect(filterByProcessNames(processes, ['node.exe'])).toEqual([processes[0], processes[1]]);
91
+ });
92
+ });
22
93
 
23
94
  describe('listAgentProcesses', () => {
24
95
  beforeEach(() => {