@ai-devkit/agent-manager 0.26.0 → 0.26.2

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__/AgentManager.test.js +37 -0
  2. package/dist/__tests__/AgentManager.test.js.map +1 -1
  3. package/dist/__tests__/adapters/CodexAdapter.test.js +249 -0
  4. package/dist/__tests__/adapters/CodexAdapter.test.js.map +1 -1
  5. package/dist/__tests__/utils/AgentRegistry.test.js +68 -46
  6. package/dist/__tests__/utils/AgentRegistry.test.js.map +1 -1
  7. package/dist/adapters/CodexAdapter.d.ts +9 -1
  8. package/dist/adapters/CodexAdapter.d.ts.map +1 -1
  9. package/dist/adapters/CodexAdapter.js +106 -24
  10. package/dist/adapters/CodexAdapter.js.map +1 -1
  11. package/dist/database/connection.d.ts +22 -0
  12. package/dist/database/connection.d.ts.map +1 -0
  13. package/dist/database/connection.js +58 -0
  14. package/dist/database/connection.js.map +1 -0
  15. package/dist/database/index.d.ts +4 -0
  16. package/dist/database/index.d.ts.map +1 -0
  17. package/dist/database/index.js +4 -0
  18. package/dist/database/index.js.map +1 -0
  19. package/dist/database/migrations/001_initial.sql +12 -0
  20. package/dist/database/schema.d.ts +4 -0
  21. package/dist/database/schema.d.ts.map +1 -0
  22. package/dist/database/schema.js +47 -0
  23. package/dist/database/schema.js.map +1 -0
  24. package/dist/utils/AgentRegistry.d.ts +7 -3
  25. package/dist/utils/AgentRegistry.d.ts.map +1 -1
  26. package/dist/utils/AgentRegistry.js +108 -57
  27. package/dist/utils/AgentRegistry.js.map +1 -1
  28. package/package.json +2 -2
  29. package/src/__tests__/AgentManager.test.ts +37 -0
  30. package/src/__tests__/adapters/CodexAdapter.test.ts +155 -0
  31. package/src/__tests__/utils/AgentRegistry.test.ts +48 -40
  32. package/src/adapters/CodexAdapter.ts +147 -27
  33. package/src/database/connection.ts +74 -0
  34. package/src/database/index.ts +7 -0
  35. package/src/database/migrations/001_initial.sql +12 -0
  36. package/src/database/schema.ts +62 -0
  37. package/src/utils/AgentRegistry.ts +109 -49
@@ -1,6 +1,6 @@
1
- import fs from 'fs';
2
1
  import os from 'os';
3
2
  import path from 'path';
3
+ import { DatabaseConnection, resolveAgentRegistryDbPath } from '../database/index.js';
4
4
  export class RenameNotFoundError extends Error {
5
5
  agentName;
6
6
  constructor(agentName){
@@ -18,9 +18,11 @@ export class RenameConflictError extends Error {
18
18
  const DEFAULT_REGISTRY_PATH = path.join(os.homedir(), '.ai-devkit', 'agents.json');
19
19
  let defaultInstance = null;
20
20
  export class AgentRegistry {
21
- filePath;
21
+ db;
22
22
  constructor(filePath = DEFAULT_REGISTRY_PATH){
23
- this.filePath = filePath;
23
+ this.db = new DatabaseConnection({
24
+ dbPath: resolveAgentRegistryDbPath(filePath)
25
+ });
24
26
  }
25
27
  static default() {
26
28
  if (!defaultInstance) {
@@ -28,35 +30,80 @@ export class AgentRegistry {
28
30
  }
29
31
  return defaultInstance;
30
32
  }
31
- readFile() {
32
- try {
33
- const raw = fs.readFileSync(this.filePath, 'utf8');
34
- const parsed = JSON.parse(raw);
35
- return {
36
- entries: Array.isArray(parsed.entries) ? parsed.entries : []
37
- };
38
- } catch {
39
- return {
40
- entries: []
41
- };
42
- }
43
- }
44
- writeFile(data) {
45
- const dir = path.dirname(this.filePath);
46
- fs.mkdirSync(dir, {
47
- recursive: true
48
- });
49
- const tmp = `${this.filePath}.tmp`;
50
- fs.writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf8');
51
- fs.renameSync(tmp, this.filePath);
33
+ rowToEntry(row) {
34
+ return {
35
+ name: row.name,
36
+ type: row.type,
37
+ pid: row.pid,
38
+ tmuxSession: row.tmux_session,
39
+ cwd: row.cwd,
40
+ startedAt: row.started_at,
41
+ sessionId: row.session_id,
42
+ sessionFilePath: row.session_file_path
43
+ };
52
44
  }
53
45
  mergeEntry(incoming, existing) {
54
46
  if (!existing) return incoming;
47
+ const incomingIsManaged = Boolean(incoming.tmuxSession);
55
48
  return {
56
- ...incoming,
57
- tmuxSession: incoming.tmuxSession || existing.tmuxSession
49
+ ...existing,
50
+ name: incomingIsManaged ? incoming.name : existing.name,
51
+ tmuxSession: incoming.tmuxSession || existing.tmuxSession,
52
+ cwd: incoming.cwd || existing.cwd,
53
+ startedAt: existing.startedAt || incoming.startedAt,
54
+ sessionId: incoming.sessionId || existing.sessionId,
55
+ sessionFilePath: incoming.sessionFilePath || existing.sessionFilePath
58
56
  };
59
57
  }
58
+ findByIdentity(type, pid) {
59
+ const row = this.db.queryOne('SELECT * FROM agents WHERE type = ? AND pid = ?', [
60
+ type,
61
+ pid
62
+ ]);
63
+ return row ? this.rowToEntry(row) : undefined;
64
+ }
65
+ findByName(name) {
66
+ const row = this.db.queryOne('SELECT * FROM agents WHERE name = ?', [
67
+ name
68
+ ]);
69
+ return row ? this.rowToEntry(row) : undefined;
70
+ }
71
+ deleteNameConflict(name, type, pid) {
72
+ const conflict = this.findByName(name);
73
+ if (!conflict) return;
74
+ if (conflict.type === type && conflict.pid === pid) return;
75
+ if (!this.isAlive(conflict)) {
76
+ this.db.execute('DELETE FROM agents WHERE type = ? AND pid = ?', [
77
+ conflict.type,
78
+ conflict.pid
79
+ ]);
80
+ }
81
+ }
82
+ insertOrUpdate(entry) {
83
+ this.db.instance.prepare(`
84
+ INSERT INTO agents (
85
+ type, pid, name, tmux_session, cwd, started_at, session_id, session_file_path, updated_at
86
+ )
87
+ VALUES (
88
+ @type, @pid, @name, @tmuxSession, @cwd, @startedAt, @sessionId, @sessionFilePath, @updatedAt
89
+ )
90
+ ON CONFLICT(type, pid) DO UPDATE SET
91
+ name = excluded.name,
92
+ tmux_session = excluded.tmux_session,
93
+ cwd = excluded.cwd,
94
+ started_at = agents.started_at,
95
+ session_id = excluded.session_id,
96
+ session_file_path = excluded.session_file_path,
97
+ updated_at = excluded.updated_at
98
+ `).run({
99
+ ...entry,
100
+ updatedAt: new Date().toISOString()
101
+ });
102
+ }
103
+ save(entry) {
104
+ this.deleteNameConflict(entry.name, entry.type, entry.pid);
105
+ this.insertOrUpdate(entry);
106
+ }
60
107
  isAlive(entry) {
61
108
  try {
62
109
  process.kill(entry.pid, 0);
@@ -66,13 +113,16 @@ export class AgentRegistry {
66
113
  }
67
114
  }
68
115
  prune() {
69
- const data = this.readFile();
70
- const live = data.entries.filter((e)=>this.isAlive(e));
71
- if (live.length !== data.entries.length) {
72
- this.writeFile({
73
- entries: live
74
- });
75
- }
116
+ const entries = this.list();
117
+ const stale = entries.filter((e)=>!this.isAlive(e));
118
+ this.db.transaction(()=>{
119
+ for (const entry of stale){
120
+ this.db.execute('DELETE FROM agents WHERE type = ? AND pid = ?', [
121
+ entry.type,
122
+ entry.pid
123
+ ]);
124
+ }
125
+ });
76
126
  }
77
127
  register(entry) {
78
128
  this.registerBatch([
@@ -81,42 +131,43 @@ export class AgentRegistry {
81
131
  }
82
132
  registerBatch(entries) {
83
133
  if (entries.length === 0) return;
84
- const data = this.readFile();
85
- for (const incoming of entries){
86
- const idx = data.entries.findIndex((e)=>e.name === incoming.name);
87
- if (idx >= 0) {
88
- data.entries[idx] = this.mergeEntry(incoming, data.entries[idx]);
89
- } else {
90
- data.entries.push(incoming);
134
+ this.db.transaction(()=>{
135
+ for (const incoming of entries){
136
+ const existing = this.findByIdentity(incoming.type, incoming.pid);
137
+ this.save(this.mergeEntry(incoming, existing));
91
138
  }
92
- }
93
- this.writeFile(data);
139
+ });
94
140
  }
95
141
  rename(currentName, newName) {
96
- const data = this.readFile();
97
- const idx = data.entries.findIndex((e)=>e.name === currentName);
98
- if (idx < 0) {
142
+ const existing = this.findByName(currentName);
143
+ if (!existing) {
99
144
  throw new RenameNotFoundError(currentName);
100
145
  }
101
- const liveEntries = data.entries.filter((e)=>this.isAlive(e));
102
- const conflict = liveEntries.find((e)=>e.name === newName);
103
- if (conflict) {
146
+ const conflict = this.findByName(newName);
147
+ if (conflict && this.isAlive(conflict)) {
104
148
  throw new RenameConflictError(newName);
105
149
  }
106
- const pruned = liveEntries.map((e)=>e.name === currentName ? {
107
- ...e,
108
- name: newName
109
- } : e);
110
- this.writeFile({
111
- entries: pruned
150
+ this.db.transaction(()=>{
151
+ if (conflict) {
152
+ this.db.execute('DELETE FROM agents WHERE type = ? AND pid = ?', [
153
+ conflict.type,
154
+ conflict.pid
155
+ ]);
156
+ }
157
+ this.db.execute('UPDATE agents SET name = ?, updated_at = ? WHERE type = ? AND pid = ?', [
158
+ newName,
159
+ new Date().toISOString(),
160
+ existing.type,
161
+ existing.pid
162
+ ]);
112
163
  });
113
164
  }
114
165
  lookup(name) {
115
- const data = this.readFile();
116
- return data.entries.find((e)=>e.name === name) ?? null;
166
+ return this.findByName(name) ?? null;
117
167
  }
118
168
  list() {
119
- return this.readFile().entries;
169
+ const rows = this.db.query('SELECT * FROM agents ORDER BY started_at ASC, name ASC');
170
+ return rows.map((row)=>this.rowToEntry(row));
120
171
  }
121
172
  }
122
173
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/AgentRegistry.ts"],"sourcesContent":["import fs from 'fs';\nimport os from 'os';\nimport path from 'path';\nimport type { AgentType } from '../adapters/AgentAdapter.js';\n\nexport class RenameNotFoundError extends Error {\n constructor(public agentName: string) {\n super(`Agent \"${agentName}\" not found in registry.`);\n this.name = 'RenameNotFoundError';\n }\n}\n\nexport class RenameConflictError extends Error {\n constructor(public agentName: string) {\n super(`Agent \"${agentName}\" is already in use.`);\n this.name = 'RenameConflictError';\n }\n}\n\nexport interface RegistryEntry {\n name: string;\n type: AgentType;\n pid: number;\n tmuxSession: string;\n cwd: string;\n startedAt: string; // ISO 8601\n sessionId: string;\n sessionFilePath: string;\n}\n\ninterface RegistryFile {\n entries: RegistryEntry[];\n}\n\nconst DEFAULT_REGISTRY_PATH = path.join(os.homedir(), '.ai-devkit', 'agents.json');\n\nlet defaultInstance: AgentRegistry | null = null;\n\nexport class AgentRegistry {\n private filePath: string;\n\n constructor(filePath: string = DEFAULT_REGISTRY_PATH) {\n this.filePath = filePath;\n }\n\n static default(): AgentRegistry {\n if (!defaultInstance) {\n defaultInstance = new AgentRegistry();\n }\n return defaultInstance;\n }\n\n private readFile(): RegistryFile {\n try {\n const raw = fs.readFileSync(this.filePath, 'utf8');\n const parsed = JSON.parse(raw) as RegistryFile;\n return { entries: Array.isArray(parsed.entries) ? parsed.entries : [] };\n } catch {\n return { entries: [] };\n }\n }\n\n private writeFile(data: RegistryFile): void {\n const dir = path.dirname(this.filePath);\n fs.mkdirSync(dir, { recursive: true });\n const tmp = `${this.filePath}.tmp`;\n fs.writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf8');\n fs.renameSync(tmp, this.filePath);\n }\n\n private mergeEntry(incoming: RegistryEntry, existing: RegistryEntry | undefined): RegistryEntry {\n if (!existing) return incoming;\n return {\n ...incoming,\n tmuxSession: incoming.tmuxSession || existing.tmuxSession,\n };\n }\n\n isAlive(entry: RegistryEntry): boolean {\n try {\n process.kill(entry.pid, 0);\n return true;\n } catch {\n return false;\n }\n }\n\n prune(): void {\n const data = this.readFile();\n const live = data.entries.filter((e) => this.isAlive(e));\n if (live.length !== data.entries.length) {\n this.writeFile({ entries: live });\n }\n }\n\n register(entry: RegistryEntry): void {\n this.registerBatch([entry]);\n }\n\n registerBatch(entries: RegistryEntry[]): void {\n if (entries.length === 0) return;\n const data = this.readFile();\n for (const incoming of entries) {\n const idx = data.entries.findIndex((e) => e.name === incoming.name);\n if (idx >= 0) {\n data.entries[idx] = this.mergeEntry(incoming, data.entries[idx]);\n } else {\n data.entries.push(incoming);\n }\n }\n this.writeFile(data);\n }\n\n rename(currentName: string, newName: string): void {\n const data = this.readFile();\n const idx = data.entries.findIndex((e) => e.name === currentName);\n if (idx < 0) {\n throw new RenameNotFoundError(currentName);\n }\n const liveEntries = data.entries.filter((e) => this.isAlive(e));\n const conflict = liveEntries.find((e) => e.name === newName);\n if (conflict) {\n throw new RenameConflictError(newName);\n }\n const pruned = liveEntries.map((e) =>\n e.name === currentName ? { ...e, name: newName } : e,\n );\n this.writeFile({ entries: pruned });\n }\n\n lookup(name: string): RegistryEntry | null {\n const data = this.readFile();\n return data.entries.find((e) => e.name === name) ?? null;\n }\n\n list(): RegistryEntry[] {\n return this.readFile().entries;\n }\n}\n"],"names":["fs","os","path","RenameNotFoundError","Error","agentName","name","RenameConflictError","DEFAULT_REGISTRY_PATH","join","homedir","defaultInstance","AgentRegistry","filePath","default","readFile","raw","readFileSync","parsed","JSON","parse","entries","Array","isArray","writeFile","data","dir","dirname","mkdirSync","recursive","tmp","writeFileSync","stringify","renameSync","mergeEntry","incoming","existing","tmuxSession","isAlive","entry","process","kill","pid","prune","live","filter","e","length","register","registerBatch","idx","findIndex","push","rename","currentName","newName","liveEntries","conflict","find","pruned","map","lookup","list"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/utils/AgentRegistry.ts"],"sourcesContent":["import os from 'os';\nimport path from 'path';\nimport type { AgentType } from '../adapters/AgentAdapter.js';\nimport {\n DatabaseConnection,\n resolveAgentRegistryDbPath,\n} from '../database/index.js';\n\nexport class RenameNotFoundError extends Error {\n constructor(public agentName: string) {\n super(`Agent \"${agentName}\" not found in registry.`);\n this.name = 'RenameNotFoundError';\n }\n}\n\nexport class RenameConflictError extends Error {\n constructor(public agentName: string) {\n super(`Agent \"${agentName}\" is already in use.`);\n this.name = 'RenameConflictError';\n }\n}\n\nexport interface RegistryEntry {\n name: string;\n type: AgentType;\n pid: number;\n tmuxSession: string;\n cwd: string;\n startedAt: string; // ISO 8601\n sessionId: string;\n sessionFilePath: string;\n}\n\ninterface RegistryRow {\n name: string;\n type: AgentType;\n pid: number;\n tmux_session: string;\n cwd: string;\n started_at: string;\n session_id: string;\n session_file_path: string;\n updated_at: string;\n}\n\nconst DEFAULT_REGISTRY_PATH = path.join(os.homedir(), '.ai-devkit', 'agents.json');\n\nlet defaultInstance: AgentRegistry | null = null;\n\nexport class AgentRegistry {\n private db: DatabaseConnection;\n\n constructor(filePath: string = DEFAULT_REGISTRY_PATH) {\n this.db = new DatabaseConnection({ dbPath: resolveAgentRegistryDbPath(filePath) });\n }\n\n static default(): AgentRegistry {\n if (!defaultInstance) {\n defaultInstance = new AgentRegistry();\n }\n return defaultInstance;\n }\n\n private rowToEntry(row: RegistryRow): RegistryEntry {\n return {\n name: row.name,\n type: row.type,\n pid: row.pid,\n tmuxSession: row.tmux_session,\n cwd: row.cwd,\n startedAt: row.started_at,\n sessionId: row.session_id,\n sessionFilePath: row.session_file_path,\n };\n }\n\n private mergeEntry(incoming: RegistryEntry, existing: RegistryEntry | undefined): RegistryEntry {\n if (!existing) return incoming;\n const incomingIsManaged = Boolean(incoming.tmuxSession);\n return {\n ...existing,\n name: incomingIsManaged ? incoming.name : existing.name,\n tmuxSession: incoming.tmuxSession || existing.tmuxSession,\n cwd: incoming.cwd || existing.cwd,\n startedAt: existing.startedAt || incoming.startedAt,\n sessionId: incoming.sessionId || existing.sessionId,\n sessionFilePath: incoming.sessionFilePath || existing.sessionFilePath,\n };\n }\n\n private findByIdentity(type: AgentType, pid: number): RegistryEntry | undefined {\n const row = this.db.queryOne<RegistryRow>(\n 'SELECT * FROM agents WHERE type = ? AND pid = ?',\n [type, pid],\n );\n return row ? this.rowToEntry(row) : undefined;\n }\n\n private findByName(name: string): RegistryEntry | undefined {\n const row = this.db.queryOne<RegistryRow>('SELECT * FROM agents WHERE name = ?', [name]);\n return row ? this.rowToEntry(row) : undefined;\n }\n\n private deleteNameConflict(name: string, type: AgentType, pid: number): void {\n const conflict = this.findByName(name);\n if (!conflict) return;\n if (conflict.type === type && conflict.pid === pid) return;\n if (!this.isAlive(conflict)) {\n this.db.execute('DELETE FROM agents WHERE type = ? AND pid = ?', [conflict.type, conflict.pid]);\n }\n }\n\n private insertOrUpdate(entry: RegistryEntry): void {\n this.db.instance.prepare(`\n INSERT INTO agents (\n type, pid, name, tmux_session, cwd, started_at, session_id, session_file_path, updated_at\n )\n VALUES (\n @type, @pid, @name, @tmuxSession, @cwd, @startedAt, @sessionId, @sessionFilePath, @updatedAt\n )\n ON CONFLICT(type, pid) DO UPDATE SET\n name = excluded.name,\n tmux_session = excluded.tmux_session,\n cwd = excluded.cwd,\n started_at = agents.started_at,\n session_id = excluded.session_id,\n session_file_path = excluded.session_file_path,\n updated_at = excluded.updated_at\n `).run({ ...entry, updatedAt: new Date().toISOString() });\n }\n\n private save(entry: RegistryEntry): void {\n this.deleteNameConflict(entry.name, entry.type, entry.pid);\n this.insertOrUpdate(entry);\n }\n\n isAlive(entry: RegistryEntry): boolean {\n try {\n process.kill(entry.pid, 0);\n return true;\n } catch {\n return false;\n }\n }\n\n prune(): void {\n const entries = this.list();\n const stale = entries.filter((e) => !this.isAlive(e));\n this.db.transaction(() => {\n for (const entry of stale) {\n this.db.execute('DELETE FROM agents WHERE type = ? AND pid = ?', [entry.type, entry.pid]);\n }\n });\n }\n\n register(entry: RegistryEntry): void {\n this.registerBatch([entry]);\n }\n\n registerBatch(entries: RegistryEntry[]): void {\n if (entries.length === 0) return;\n this.db.transaction(() => {\n for (const incoming of entries) {\n const existing = this.findByIdentity(incoming.type, incoming.pid);\n this.save(this.mergeEntry(incoming, existing));\n }\n });\n }\n\n rename(currentName: string, newName: string): void {\n const existing = this.findByName(currentName);\n if (!existing) {\n throw new RenameNotFoundError(currentName);\n }\n const conflict = this.findByName(newName);\n if (conflict && this.isAlive(conflict)) {\n throw new RenameConflictError(newName);\n }\n\n this.db.transaction(() => {\n if (conflict) {\n this.db.execute('DELETE FROM agents WHERE type = ? AND pid = ?', [conflict.type, conflict.pid]);\n }\n this.db.execute(\n 'UPDATE agents SET name = ?, updated_at = ? WHERE type = ? AND pid = ?',\n [newName, new Date().toISOString(), existing.type, existing.pid],\n );\n });\n }\n\n lookup(name: string): RegistryEntry | null {\n return this.findByName(name) ?? null;\n }\n\n list(): RegistryEntry[] {\n const rows = this.db.query<RegistryRow>('SELECT * FROM agents ORDER BY started_at ASC, name ASC');\n return rows.map((row) => this.rowToEntry(row));\n }\n}\n"],"names":["os","path","DatabaseConnection","resolveAgentRegistryDbPath","RenameNotFoundError","Error","agentName","name","RenameConflictError","DEFAULT_REGISTRY_PATH","join","homedir","defaultInstance","AgentRegistry","db","filePath","dbPath","default","rowToEntry","row","type","pid","tmuxSession","tmux_session","cwd","startedAt","started_at","sessionId","session_id","sessionFilePath","session_file_path","mergeEntry","incoming","existing","incomingIsManaged","Boolean","findByIdentity","queryOne","undefined","findByName","deleteNameConflict","conflict","isAlive","execute","insertOrUpdate","entry","instance","prepare","run","updatedAt","Date","toISOString","save","process","kill","prune","entries","list","stale","filter","e","transaction","register","registerBatch","length","rename","currentName","newName","lookup","rows","query","map"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-devkit/agent-manager",
3
- "version": "0.26.0",
3
+ "version": "0.26.2",
4
4
  "type": "module",
5
5
  "description": "Standalone agent detection and management utilities for AI DevKit",
6
6
  "main": "dist/index.js",
@@ -12,7 +12,7 @@
12
12
  }
13
13
  },
14
14
  "scripts": {
15
- "build": "swc src -d dist --strip-leading-paths && tsc --emitDeclarationOnly",
15
+ "build": "swc src -d dist --strip-leading-paths && tsc --emitDeclarationOnly && cp -r src/database/migrations dist/database/",
16
16
  "dev": "swc src -d dist --strip-leading-paths --watch",
17
17
  "test": "vitest run",
18
18
  "test:watch": "vitest",
@@ -340,6 +340,43 @@ describe('AgentManager', () => {
340
340
  expect(registry.list()[0].startedAt).toBe('2026-05-30T00:00:00.000Z');
341
341
  });
342
342
 
343
+ it('preserves a user-managed name when a fallback row was written later for the same pid', async () => {
344
+ registry.register({
345
+ name: 'agent-list-debug',
346
+ type: 'codex',
347
+ pid: process.pid,
348
+ tmuxSession: 'agent-list-debug',
349
+ cwd: '/cwd/debug',
350
+ startedAt: '2026-05-30T00:00:00.000Z',
351
+ sessionId: 'pid-debug',
352
+ sessionFilePath: '',
353
+ });
354
+ registry.register({
355
+ name: `ai-devkit-${process.pid}`,
356
+ type: 'codex',
357
+ pid: process.pid,
358
+ tmuxSession: '',
359
+ cwd: '/cwd/debug',
360
+ startedAt: '2026-05-31T00:00:00.000Z',
361
+ sessionId: 'pid-debug',
362
+ sessionFilePath: '',
363
+ });
364
+
365
+ scopedManager.registerAdapter(new MockAdapter('codex', [
366
+ createMockAgent({ name: `ai-devkit-${process.pid}`, type: 'codex', pid: process.pid }),
367
+ ]));
368
+
369
+ const agents = await scopedManager.listAgents();
370
+
371
+ expect(agents[0].name).toBe('agent-list-debug');
372
+ expect(registry.list()).toHaveLength(1);
373
+ expect(registry.list()[0]).toMatchObject({
374
+ name: 'agent-list-debug',
375
+ pid: process.pid,
376
+ tmuxSession: 'agent-list-debug',
377
+ });
378
+ });
379
+
343
380
  it('writes a fresh startedAt for new entries', async () => {
344
381
  const before = new Date().toISOString();
345
382
  scopedManager.registerAdapter(new MockAdapter('claude', [
@@ -1133,6 +1133,52 @@ describe('CodexAdapter', () => {
1133
1133
  expect(session.summary).toBe('Last message');
1134
1134
  });
1135
1135
 
1136
+ it('should extract summary from current Codex response_item messages', () => {
1137
+ const parseSession = (adapter as any).parseSession.bind(adapter);
1138
+ const filePath = path.join(tmpDir, 'response-item-summary.jsonl');
1139
+ fs.writeFileSync(filePath, [
1140
+ JSON.stringify({ type: 'session_meta', payload: { id: 'sess-ri', timestamp: '2026-03-18T15:00:00Z', cwd: '/repo' } }),
1141
+ JSON.stringify({
1142
+ type: 'response_item',
1143
+ timestamp: '2026-03-18T15:01:00Z',
1144
+ payload: {
1145
+ type: 'message',
1146
+ role: 'assistant',
1147
+ content: [{ type: 'output_text', text: 'Parsed from current schema' }],
1148
+ },
1149
+ }),
1150
+ ].join('\n'));
1151
+
1152
+ const session = parseSession(undefined, filePath);
1153
+ expect(session.summary).toBe('Parsed from current schema');
1154
+ expect(session.lastPayloadType).toBe('agent_message');
1155
+ });
1156
+
1157
+ it('should treat completed Codex AgentMessage events as waiting for list status', () => {
1158
+ const parseSession = (adapter as any).parseSession.bind(adapter);
1159
+ const determineStatus = (adapter as any).determineStatus.bind(adapter);
1160
+ const filePath = path.join(tmpDir, 'agent-message-status.jsonl');
1161
+ fs.writeFileSync(filePath, [
1162
+ JSON.stringify({ type: 'session_meta', payload: { id: 'sess-am', timestamp: '2026-03-18T15:00:00Z', cwd: '/repo' } }),
1163
+ JSON.stringify({
1164
+ type: 'event_msg',
1165
+ timestamp: new Date().toISOString(),
1166
+ payload: {
1167
+ type: 'item_completed',
1168
+ item: {
1169
+ type: 'AgentMessage',
1170
+ content: [{ type: 'Text', text: 'Waiting for the user now' }],
1171
+ },
1172
+ },
1173
+ }),
1174
+ ].join('\n'));
1175
+
1176
+ const session = parseSession(undefined, filePath);
1177
+ expect(session.summary).toBe('Waiting for the user now');
1178
+ expect(session.lastPayloadType).toBe('agent_message');
1179
+ expect(determineStatus(session)).toBe(AgentStatus.WAITING);
1180
+ });
1181
+
1136
1182
  it('should handle malformed JSON lines gracefully', () => {
1137
1183
  const parseSession = (adapter as any).parseSession.bind(adapter);
1138
1184
  const filePath = path.join(tmpDir, 'malformed.jsonl');
@@ -1212,6 +1258,115 @@ describe('CodexAdapter', () => {
1212
1258
  expect(messages[1]).toEqual({ role: 'assistant', content: 'I found the issue', timestamp: '2026-03-27T10:00:05Z' });
1213
1259
  });
1214
1260
 
1261
+ it('should parse Codex response_item message records', () => {
1262
+ const filePath = writeJsonl([
1263
+ { type: 'session_meta', payload: { id: 'sess-1', cwd: '/repo', timestamp: '2026-03-27T10:00:00Z' } },
1264
+ {
1265
+ type: 'response_item',
1266
+ timestamp: '2026-03-27T10:00:01Z',
1267
+ payload: {
1268
+ type: 'message',
1269
+ role: 'user',
1270
+ content: [{ type: 'input_text', text: 'Fix the bug' }],
1271
+ },
1272
+ },
1273
+ {
1274
+ type: 'response_item',
1275
+ timestamp: '2026-03-27T10:00:05Z',
1276
+ payload: {
1277
+ type: 'message',
1278
+ role: 'assistant',
1279
+ content: [{ type: 'output_text', text: 'I found the issue' }],
1280
+ },
1281
+ },
1282
+ ]);
1283
+
1284
+ const messages = adapter.getConversation(filePath);
1285
+ expect(messages).toHaveLength(2);
1286
+ expect(messages[0]).toEqual({ role: 'user', content: 'Fix the bug', timestamp: '2026-03-27T10:00:01Z' });
1287
+ expect(messages[1]).toEqual({ role: 'assistant', content: 'I found the issue', timestamp: '2026-03-27T10:00:05Z' });
1288
+ });
1289
+
1290
+ it('should parse completed Codex AgentMessage event records', () => {
1291
+ const filePath = writeJsonl([
1292
+ { type: 'session_meta', payload: { id: 'sess-1', cwd: '/repo', timestamp: '2026-03-27T10:00:00Z' } },
1293
+ {
1294
+ type: 'event_msg',
1295
+ timestamp: '2026-03-27T10:00:05Z',
1296
+ payload: {
1297
+ type: 'item_completed',
1298
+ item: {
1299
+ type: 'AgentMessage',
1300
+ content: [{ type: 'Text', text: 'I found the issue' }],
1301
+ },
1302
+ },
1303
+ },
1304
+ ]);
1305
+
1306
+ const messages = adapter.getConversation(filePath);
1307
+ expect(messages).toEqual([
1308
+ { role: 'assistant', content: 'I found the issue', timestamp: '2026-03-27T10:00:05Z' },
1309
+ ]);
1310
+ });
1311
+
1312
+ it('should not duplicate mirrored current Codex message records', () => {
1313
+ const filePath = writeJsonl([
1314
+ { type: 'session_meta', payload: { id: 'sess-1', cwd: '/repo', timestamp: '2026-03-27T10:00:00Z' } },
1315
+ {
1316
+ type: 'response_item',
1317
+ timestamp: '2026-03-27T10:00:01Z',
1318
+ payload: {
1319
+ type: 'message',
1320
+ role: 'user',
1321
+ content: [{ type: 'input_text', text: 'Fix the bug' }],
1322
+ internal_chat_message_metadata_passthrough: { turn_id: 'turn-1' },
1323
+ },
1324
+ },
1325
+ {
1326
+ type: 'event_msg',
1327
+ timestamp: '2026-03-27T10:00:01.001Z',
1328
+ payload: {
1329
+ type: 'item_completed',
1330
+ turn_id: 'turn-1',
1331
+ item: {
1332
+ type: 'UserMessage',
1333
+ content: [{ type: 'text', text: 'Fix the bug' }],
1334
+ },
1335
+ },
1336
+ },
1337
+ {
1338
+ type: 'event_msg',
1339
+ timestamp: '2026-03-27T10:00:05Z',
1340
+ payload: {
1341
+ type: 'item_completed',
1342
+ turn_id: 'turn-1',
1343
+ item: {
1344
+ type: 'AgentMessage',
1345
+ id: 'msg-1',
1346
+ content: [{ type: 'Text', text: 'I found the issue' }],
1347
+ },
1348
+ },
1349
+ },
1350
+ {
1351
+ type: 'response_item',
1352
+ timestamp: '2026-03-27T10:00:05.005Z',
1353
+ payload: {
1354
+ type: 'message',
1355
+ id: 'msg-1',
1356
+ role: 'assistant',
1357
+ content: [{ type: 'output_text', text: 'I found the issue' }],
1358
+ internal_chat_message_metadata_passthrough: { turn_id: 'turn-1' },
1359
+ },
1360
+ },
1361
+ ]);
1362
+
1363
+ const messages = adapter.getConversation(filePath);
1364
+ expect(messages).toEqual([
1365
+ { role: 'user', content: 'Fix the bug', timestamp: '2026-03-27T10:00:01Z' },
1366
+ { role: 'assistant', content: 'I found the issue', timestamp: '2026-03-27T10:00:05.005Z' },
1367
+ ]);
1368
+ });
1369
+
1215
1370
  it('should skip session_meta entry', () => {
1216
1371
  const filePath = writeJsonl([
1217
1372
  { type: 'session_meta', payload: { id: 'sess-1', cwd: '/repo', timestamp: '2026-03-27T10:00:00Z' } },