@ai-devkit/agent-manager 0.25.0 → 0.26.1

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 (67) hide show
  1. package/README.md +14 -0
  2. package/dist/__tests__/adapters/CodexAdapter.test.js +249 -0
  3. package/dist/__tests__/adapters/CodexAdapter.test.js.map +1 -1
  4. package/dist/__tests__/print/ClaudeCliProbe.test.js +53 -0
  5. package/dist/__tests__/print/ClaudeCliProbe.test.js.map +1 -0
  6. package/dist/__tests__/print/ClaudePrintAgent.integration.test.js +69 -0
  7. package/dist/__tests__/print/ClaudePrintAgent.integration.test.js.map +1 -0
  8. package/dist/__tests__/print/ClaudePrintAgentService.test.js +108 -0
  9. package/dist/__tests__/print/ClaudePrintAgentService.test.js.map +1 -0
  10. package/dist/__tests__/print/ClaudePrintRunner.test.js +187 -0
  11. package/dist/__tests__/print/ClaudePrintRunner.test.js.map +1 -0
  12. package/dist/__tests__/print/PrintAgent.test.js +17 -0
  13. package/dist/__tests__/print/PrintAgent.test.js.map +1 -0
  14. package/dist/__tests__/print/PrintAgentStore.test.js +307 -0
  15. package/dist/__tests__/print/PrintAgentStore.test.js.map +1 -0
  16. package/dist/__tests__/terminal/TmuxManager.test.js +9 -0
  17. package/dist/__tests__/terminal/TmuxManager.test.js.map +1 -1
  18. package/dist/adapters/CodexAdapter.d.ts +9 -1
  19. package/dist/adapters/CodexAdapter.d.ts.map +1 -1
  20. package/dist/adapters/CodexAdapter.js +106 -24
  21. package/dist/adapters/CodexAdapter.js.map +1 -1
  22. package/dist/index.d.ts +11 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +6 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/print/ClaudeCliProbe.d.ts +20 -0
  27. package/dist/print/ClaudeCliProbe.d.ts.map +1 -0
  28. package/dist/print/ClaudeCliProbe.js +57 -0
  29. package/dist/print/ClaudeCliProbe.js.map +1 -0
  30. package/dist/print/ClaudePrintAgentService.d.ts +44 -0
  31. package/dist/print/ClaudePrintAgentService.d.ts.map +1 -0
  32. package/dist/print/ClaudePrintAgentService.js +66 -0
  33. package/dist/print/ClaudePrintAgentService.js.map +1 -0
  34. package/dist/print/ClaudePrintRunner.d.ts +32 -0
  35. package/dist/print/ClaudePrintRunner.d.ts.map +1 -0
  36. package/dist/print/ClaudePrintRunner.js +128 -0
  37. package/dist/print/ClaudePrintRunner.js.map +1 -0
  38. package/dist/print/PrintAgent.d.ts +57 -0
  39. package/dist/print/PrintAgent.d.ts.map +1 -0
  40. package/dist/print/PrintAgent.js +42 -0
  41. package/dist/print/PrintAgent.js.map +1 -0
  42. package/dist/print/PrintAgentStore.d.ts +69 -0
  43. package/dist/print/PrintAgentStore.d.ts.map +1 -0
  44. package/dist/print/PrintAgentStore.js +484 -0
  45. package/dist/print/PrintAgentStore.js.map +1 -0
  46. package/dist/terminal/TmuxManager.d.ts +2 -2
  47. package/dist/terminal/TmuxManager.d.ts.map +1 -1
  48. package/dist/terminal/TmuxManager.js +5 -7
  49. package/dist/terminal/TmuxManager.js.map +1 -1
  50. package/package.json +1 -1
  51. package/src/__tests__/adapters/CodexAdapter.test.ts +155 -0
  52. package/src/__tests__/fixtures/fake-claude.cjs +24 -0
  53. package/src/__tests__/print/ClaudeCliProbe.test.ts +32 -0
  54. package/src/__tests__/print/ClaudePrintAgent.integration.test.ts +56 -0
  55. package/src/__tests__/print/ClaudePrintAgentService.test.ts +46 -0
  56. package/src/__tests__/print/ClaudePrintRunner.test.ts +105 -0
  57. package/src/__tests__/print/PrintAgent.test.ts +21 -0
  58. package/src/__tests__/print/PrintAgentStore.test.ts +192 -0
  59. package/src/__tests__/terminal/TmuxManager.test.ts +10 -0
  60. package/src/adapters/CodexAdapter.ts +147 -27
  61. package/src/index.ts +39 -0
  62. package/src/print/ClaudeCliProbe.ts +58 -0
  63. package/src/print/ClaudePrintAgentService.ts +94 -0
  64. package/src/print/ClaudePrintRunner.ts +139 -0
  65. package/src/print/PrintAgent.ts +86 -0
  66. package/src/print/PrintAgentStore.ts +503 -0
  67. package/src/terminal/TmuxManager.ts +5 -7
@@ -0,0 +1,307 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { afterEach, describe, expect, it } from 'vitest';
5
+ const tempDirs = [];
6
+ afterEach(()=>{
7
+ for (const dir of tempDirs.splice(0))fs.rmSync(dir, {
8
+ recursive: true,
9
+ force: true
10
+ });
11
+ });
12
+ async function loadStore() {
13
+ const api = await import('../../index.js');
14
+ expect(api).toHaveProperty('PrintAgentStore');
15
+ return api.PrintAgentStore;
16
+ }
17
+ function fixture() {
18
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'print-agent-store-'));
19
+ tempDirs.push(root);
20
+ const cwd = path.join(root, 'project');
21
+ fs.mkdirSync(cwd);
22
+ return {
23
+ root,
24
+ cwd,
25
+ filePath: path.join(root, 'state', 'print-agents.json')
26
+ };
27
+ }
28
+ describe('PrintAgentStore create/list/resolve', ()=>{
29
+ it('creates distinct durable identities with a canonical cwd and lists them', async ()=>{
30
+ const PrintAgentStore = await loadStore();
31
+ const { cwd, filePath } = fixture();
32
+ const store = new PrintAgentStore({
33
+ filePath,
34
+ now: ()=>new Date('2026-08-07T09:00:00Z')
35
+ });
36
+ const agent = await store.create({
37
+ name: 'reviewer',
38
+ cwd
39
+ });
40
+ expect(agent).toMatchObject({
41
+ name: 'reviewer',
42
+ provider: 'claude',
43
+ mode: 'print',
44
+ cwd: fs.realpathSync(cwd),
45
+ state: 'ready',
46
+ sessionHealth: 'uninitialized',
47
+ activeRun: null
48
+ });
49
+ expect(agent.id).toMatch(/^[0-9a-f-]{36}$/);
50
+ expect(agent.providerSessionId).toMatch(/^[0-9a-f-]{36}$/);
51
+ expect(agent.id).not.toBe(agent.providerSessionId);
52
+ expect(await store.list()).toEqual([
53
+ agent
54
+ ]);
55
+ expect(fs.statSync(filePath).mode & 0o777).toBe(0o600);
56
+ });
57
+ it('resolves exact ids and names and rejects duplicate names', async ()=>{
58
+ const PrintAgentStore = await loadStore();
59
+ const { cwd, filePath } = fixture();
60
+ const store = new PrintAgentStore({
61
+ filePath
62
+ });
63
+ const agent = await store.create({
64
+ name: 'Reviewer',
65
+ cwd
66
+ });
67
+ expect(await store.resolve(agent.id)).toMatchObject({
68
+ id: agent.id
69
+ });
70
+ expect(await store.resolve('reviewer')).toMatchObject({
71
+ id: agent.id
72
+ });
73
+ expect(await store.resolve('view')).toBeNull();
74
+ await expect(store.create({
75
+ name: 'reviewer',
76
+ cwd
77
+ })).rejects.toMatchObject({
78
+ code: 'PRINT_AGENT_NAME_CONFLICT'
79
+ });
80
+ });
81
+ it('rejects missing cwd, malformed storage, and symlinked store targets', async ()=>{
82
+ const PrintAgentStore = await loadStore();
83
+ const { root, cwd, filePath } = fixture();
84
+ const store = new PrintAgentStore({
85
+ filePath
86
+ });
87
+ await expect(store.create({
88
+ name: 'missing',
89
+ cwd: path.join(root, 'missing')
90
+ })).rejects.toMatchObject({
91
+ code: 'PRINT_AGENT_STORE'
92
+ });
93
+ fs.mkdirSync(path.dirname(filePath), {
94
+ recursive: true
95
+ });
96
+ fs.writeFileSync(filePath, '{bad json', {
97
+ mode: 0o600
98
+ });
99
+ await expect(store.list()).rejects.toMatchObject({
100
+ code: 'PRINT_AGENT_STORE'
101
+ });
102
+ fs.rmSync(filePath);
103
+ const target = path.join(root, 'target.json');
104
+ fs.writeFileSync(target, JSON.stringify({
105
+ version: 1,
106
+ agents: []
107
+ }));
108
+ fs.symlinkSync(target, filePath);
109
+ await expect(store.create({
110
+ name: 'unsafe',
111
+ cwd
112
+ })).rejects.toMatchObject({
113
+ code: 'PRINT_AGENT_STORE'
114
+ });
115
+ });
116
+ it('recovers an abandoned old mutation lock after a crash', async ()=>{
117
+ const PrintAgentStore = await loadStore();
118
+ const { cwd, filePath } = fixture();
119
+ fs.mkdirSync(path.dirname(filePath), {
120
+ recursive: true
121
+ });
122
+ const lockPath = `${filePath}.lock`;
123
+ fs.mkdirSync(lockPath);
124
+ const old = new Date(Date.now() - 60_000);
125
+ fs.utimesSync(lockPath, old, old);
126
+ const store = new PrintAgentStore({
127
+ filePath,
128
+ mutationLockStaleMs: 10
129
+ });
130
+ await expect(store.create({
131
+ name: 'recovered',
132
+ cwd
133
+ })).resolves.toMatchObject({
134
+ name: 'recovered'
135
+ });
136
+ });
137
+ });
138
+ describe('PrintAgentStore run ownership', ()=>{
139
+ it('fails fast when another exact owner is live and completes only for its token', async ()=>{
140
+ const PrintAgentStore = await loadStore();
141
+ const { cwd, filePath } = fixture();
142
+ const live = new Map([
143
+ [
144
+ process.pid,
145
+ 'owner-start'
146
+ ]
147
+ ]);
148
+ const processInspector = {
149
+ getIdentity: (pid)=>{
150
+ const startedAt = live.get(pid);
151
+ return startedAt ? {
152
+ pid,
153
+ startedAt
154
+ } : null;
155
+ }
156
+ };
157
+ const store = new PrintAgentStore({
158
+ filePath,
159
+ processInspector
160
+ });
161
+ const agent = await store.create({
162
+ name: 'runner',
163
+ cwd
164
+ });
165
+ const acquired = await store.acquireRun(agent.id);
166
+ await expect(store.acquireRun(agent.id)).rejects.toMatchObject({
167
+ code: 'PRINT_AGENT_BUSY'
168
+ });
169
+ await expect(store.completeRun(agent.id, 'wrong-token', {
170
+ status: 'succeeded',
171
+ exitCode: 0,
172
+ summary: 'done',
173
+ sessionHealth: 'healthy'
174
+ })).rejects.toMatchObject({
175
+ code: 'PRINT_AGENT_STORE'
176
+ });
177
+ const completed = await store.completeRun(agent.id, acquired.token, {
178
+ status: 'succeeded',
179
+ exitCode: 0,
180
+ summary: 'done',
181
+ sessionHealth: 'healthy'
182
+ });
183
+ expect(completed).toMatchObject({
184
+ state: 'ready',
185
+ sessionHealth: 'healthy',
186
+ activeRun: null
187
+ });
188
+ });
189
+ it('retains busy for a live provider then recovers a dead run without signaling it', async ()=>{
190
+ const PrintAgentStore = await loadStore();
191
+ const { cwd, filePath } = fixture();
192
+ const live = new Map([
193
+ [
194
+ process.pid,
195
+ 'owner-start'
196
+ ],
197
+ [
198
+ 4242,
199
+ 'provider-start'
200
+ ]
201
+ ]);
202
+ const processInspector = {
203
+ getIdentity: (pid)=>{
204
+ const startedAt = live.get(pid);
205
+ return startedAt ? {
206
+ pid,
207
+ startedAt
208
+ } : null;
209
+ }
210
+ };
211
+ const first = new PrintAgentStore({
212
+ filePath,
213
+ processInspector
214
+ });
215
+ const agent = await first.create({
216
+ name: 'recoverable',
217
+ cwd
218
+ });
219
+ const run = await first.acquireRun(agent.id);
220
+ await first.recordProviderProcess(agent.id, run.token, {
221
+ pid: 4242,
222
+ startedAt: 'provider-start'
223
+ });
224
+ live.delete(process.pid);
225
+ await expect(first.acquireRun(agent.id)).rejects.toMatchObject({
226
+ code: 'PRINT_AGENT_BUSY'
227
+ });
228
+ live.delete(4242);
229
+ live.set(process.pid, 'replacement-owner-start');
230
+ const recovered = await first.acquireRun(agent.id);
231
+ expect(recovered.agent).toMatchObject({
232
+ state: 'running',
233
+ lastResult: {
234
+ status: 'interrupted'
235
+ }
236
+ });
237
+ await first.completeRun(agent.id, recovered.token, {
238
+ status: 'failed',
239
+ exitCode: 1,
240
+ summary: 'failed',
241
+ sessionHealth: 'unknown'
242
+ });
243
+ });
244
+ it('reconciles an old incomplete lock to degraded during list', async ()=>{
245
+ const PrintAgentStore = await loadStore();
246
+ const { root, cwd, filePath } = fixture();
247
+ const live = new Map([
248
+ [
249
+ process.pid,
250
+ 'owner-start'
251
+ ]
252
+ ]);
253
+ const store = new PrintAgentStore({
254
+ filePath,
255
+ incompleteLockGraceMs: 10,
256
+ processInspector: {
257
+ getIdentity: (pid)=>{
258
+ const startedAt = live.get(pid);
259
+ return startedAt ? {
260
+ pid,
261
+ startedAt
262
+ } : null;
263
+ }
264
+ }
265
+ });
266
+ const agent = await store.create({
267
+ name: 'crashed',
268
+ cwd
269
+ });
270
+ await store.acquireRun(agent.id);
271
+ const lockPath = path.join(root, 'state', 'print-agent-locks', `${agent.id}.lock`);
272
+ fs.unlinkSync(path.join(lockPath, 'owner.json'));
273
+ const old = new Date(Date.now() - 1000);
274
+ fs.utimesSync(lockPath, old, old);
275
+ live.clear();
276
+ const listed = await store.list();
277
+ expect(listed[0]).toMatchObject({
278
+ state: 'degraded',
279
+ sessionHealth: 'unknown',
280
+ activeRun: null,
281
+ lastResult: {
282
+ status: 'interrupted'
283
+ }
284
+ });
285
+ });
286
+ it('rejects send acquisition when the bound cwd is replaced by a symlink', async ()=>{
287
+ const PrintAgentStore = await loadStore();
288
+ const { root, cwd, filePath } = fixture();
289
+ const store = new PrintAgentStore({
290
+ filePath
291
+ });
292
+ const agent = await store.create({
293
+ name: 'bound',
294
+ cwd
295
+ });
296
+ const moved = path.join(root, 'moved-project');
297
+ const other = path.join(root, 'other-project');
298
+ fs.renameSync(cwd, moved);
299
+ fs.mkdirSync(other);
300
+ fs.symlinkSync(other, cwd);
301
+ await expect(store.acquireRun(agent.id)).rejects.toMatchObject({
302
+ code: 'PRINT_AGENT_STORE'
303
+ });
304
+ });
305
+ });
306
+
307
+ //# sourceMappingURL=PrintAgentStore.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/__tests__/print/PrintAgentStore.test.ts"],"sourcesContent":["import fs from 'fs';\nimport os from 'os';\nimport path from 'path';\nimport { afterEach, describe, expect, it } from 'vitest';\n\nconst tempDirs: string[] = [];\n\nafterEach(() => {\n for (const dir of tempDirs.splice(0)) fs.rmSync(dir, { recursive: true, force: true });\n});\n\nasync function loadStore(): Promise<any> {\n const api = await import('../../index.js') as Record<string, unknown>;\n expect(api).toHaveProperty('PrintAgentStore');\n return api.PrintAgentStore;\n}\n\nfunction fixture(): { root: string; cwd: string; filePath: string } {\n const root = fs.mkdtempSync(path.join(os.tmpdir(), 'print-agent-store-'));\n tempDirs.push(root);\n const cwd = path.join(root, 'project');\n fs.mkdirSync(cwd);\n return { root, cwd, filePath: path.join(root, 'state', 'print-agents.json') };\n}\n\ndescribe('PrintAgentStore create/list/resolve', () => {\n it('creates distinct durable identities with a canonical cwd and lists them', async () => {\n const PrintAgentStore = await loadStore();\n const { cwd, filePath } = fixture();\n const store = new PrintAgentStore({ filePath, now: () => new Date('2026-08-07T09:00:00Z') });\n\n const agent = await store.create({ name: 'reviewer', cwd });\n\n expect(agent).toMatchObject({\n name: 'reviewer',\n provider: 'claude',\n mode: 'print',\n cwd: fs.realpathSync(cwd),\n state: 'ready',\n sessionHealth: 'uninitialized',\n activeRun: null,\n });\n expect(agent.id).toMatch(/^[0-9a-f-]{36}$/);\n expect(agent.providerSessionId).toMatch(/^[0-9a-f-]{36}$/);\n expect(agent.id).not.toBe(agent.providerSessionId);\n expect(await store.list()).toEqual([agent]);\n expect(fs.statSync(filePath).mode & 0o777).toBe(0o600);\n });\n\n it('resolves exact ids and names and rejects duplicate names', async () => {\n const PrintAgentStore = await loadStore();\n const { cwd, filePath } = fixture();\n const store = new PrintAgentStore({ filePath });\n const agent = await store.create({ name: 'Reviewer', cwd });\n\n expect(await store.resolve(agent.id)).toMatchObject({ id: agent.id });\n expect(await store.resolve('reviewer')).toMatchObject({ id: agent.id });\n expect(await store.resolve('view')).toBeNull();\n await expect(store.create({ name: 'reviewer', cwd })).rejects.toMatchObject({\n code: 'PRINT_AGENT_NAME_CONFLICT',\n });\n });\n\n it('rejects missing cwd, malformed storage, and symlinked store targets', async () => {\n const PrintAgentStore = await loadStore();\n const { root, cwd, filePath } = fixture();\n const store = new PrintAgentStore({ filePath });\n\n await expect(store.create({ name: 'missing', cwd: path.join(root, 'missing') }))\n .rejects.toMatchObject({ code: 'PRINT_AGENT_STORE' });\n\n fs.mkdirSync(path.dirname(filePath), { recursive: true });\n fs.writeFileSync(filePath, '{bad json', { mode: 0o600 });\n await expect(store.list()).rejects.toMatchObject({ code: 'PRINT_AGENT_STORE' });\n\n fs.rmSync(filePath);\n const target = path.join(root, 'target.json');\n fs.writeFileSync(target, JSON.stringify({ version: 1, agents: [] }));\n fs.symlinkSync(target, filePath);\n await expect(store.create({ name: 'unsafe', cwd })).rejects.toMatchObject({\n code: 'PRINT_AGENT_STORE',\n });\n });\n\n it('recovers an abandoned old mutation lock after a crash', async () => {\n const PrintAgentStore = await loadStore();\n const { cwd, filePath } = fixture();\n fs.mkdirSync(path.dirname(filePath), { recursive: true });\n const lockPath = `${filePath}.lock`;\n fs.mkdirSync(lockPath);\n const old = new Date(Date.now() - 60_000);\n fs.utimesSync(lockPath, old, old);\n const store = new PrintAgentStore({ filePath, mutationLockStaleMs: 10 });\n\n await expect(store.create({ name: 'recovered', cwd })).resolves.toMatchObject({ name: 'recovered' });\n });\n});\n\ndescribe('PrintAgentStore run ownership', () => {\n it('fails fast when another exact owner is live and completes only for its token', async () => {\n const PrintAgentStore = await loadStore();\n const { cwd, filePath } = fixture();\n const live = new Map<number, string>([[process.pid, 'owner-start']]);\n const processInspector = { getIdentity: (pid: number) => {\n const startedAt = live.get(pid);\n return startedAt ? { pid, startedAt } : null;\n } };\n const store = new PrintAgentStore({ filePath, processInspector });\n const agent = await store.create({ name: 'runner', cwd });\n\n const acquired = await store.acquireRun(agent.id);\n await expect(store.acquireRun(agent.id)).rejects.toMatchObject({ code: 'PRINT_AGENT_BUSY' });\n await expect(store.completeRun(agent.id, 'wrong-token', {\n status: 'succeeded', exitCode: 0, summary: 'done', sessionHealth: 'healthy',\n })).rejects.toMatchObject({ code: 'PRINT_AGENT_STORE' });\n\n const completed = await store.completeRun(agent.id, acquired.token, {\n status: 'succeeded', exitCode: 0, summary: 'done', sessionHealth: 'healthy',\n });\n expect(completed).toMatchObject({ state: 'ready', sessionHealth: 'healthy', activeRun: null });\n });\n\n it('retains busy for a live provider then recovers a dead run without signaling it', async () => {\n const PrintAgentStore = await loadStore();\n const { cwd, filePath } = fixture();\n const live = new Map<number, string>([[process.pid, 'owner-start'], [4242, 'provider-start']]);\n const processInspector = { getIdentity: (pid: number) => {\n const startedAt = live.get(pid);\n return startedAt ? { pid, startedAt } : null;\n } };\n const first = new PrintAgentStore({ filePath, processInspector });\n const agent = await first.create({ name: 'recoverable', cwd });\n const run = await first.acquireRun(agent.id);\n await first.recordProviderProcess(agent.id, run.token, { pid: 4242, startedAt: 'provider-start' });\n\n live.delete(process.pid);\n await expect(first.acquireRun(agent.id)).rejects.toMatchObject({ code: 'PRINT_AGENT_BUSY' });\n\n live.delete(4242);\n live.set(process.pid, 'replacement-owner-start');\n const recovered = await first.acquireRun(agent.id);\n expect(recovered.agent).toMatchObject({\n state: 'running',\n lastResult: { status: 'interrupted' },\n });\n await first.completeRun(agent.id, recovered.token, {\n status: 'failed', exitCode: 1, summary: 'failed', sessionHealth: 'unknown',\n });\n });\n\n it('reconciles an old incomplete lock to degraded during list', async () => {\n const PrintAgentStore = await loadStore();\n const { root, cwd, filePath } = fixture();\n const live = new Map<number, string>([[process.pid, 'owner-start']]);\n const store = new PrintAgentStore({ filePath, incompleteLockGraceMs: 10, processInspector: {\n getIdentity: (pid: number) => {\n const startedAt = live.get(pid);\n return startedAt ? { pid, startedAt } : null;\n },\n } });\n const agent = await store.create({ name: 'crashed', cwd });\n await store.acquireRun(agent.id);\n const lockPath = path.join(root, 'state', 'print-agent-locks', `${agent.id}.lock`);\n fs.unlinkSync(path.join(lockPath, 'owner.json'));\n const old = new Date(Date.now() - 1000);\n fs.utimesSync(lockPath, old, old);\n live.clear();\n\n const listed = await store.list();\n\n expect(listed[0]).toMatchObject({\n state: 'degraded',\n sessionHealth: 'unknown',\n activeRun: null,\n lastResult: { status: 'interrupted' },\n });\n });\n\n it('rejects send acquisition when the bound cwd is replaced by a symlink', async () => {\n const PrintAgentStore = await loadStore();\n const { root, cwd, filePath } = fixture();\n const store = new PrintAgentStore({ filePath });\n const agent = await store.create({ name: 'bound', cwd });\n const moved = path.join(root, 'moved-project');\n const other = path.join(root, 'other-project');\n fs.renameSync(cwd, moved);\n fs.mkdirSync(other);\n fs.symlinkSync(other, cwd);\n\n await expect(store.acquireRun(agent.id)).rejects.toMatchObject({ code: 'PRINT_AGENT_STORE' });\n });\n});\n"],"names":["fs","os","path","afterEach","describe","expect","it","tempDirs","dir","splice","rmSync","recursive","force","loadStore","api","toHaveProperty","PrintAgentStore","fixture","root","mkdtempSync","join","tmpdir","push","cwd","mkdirSync","filePath","store","now","Date","agent","create","name","toMatchObject","provider","mode","realpathSync","state","sessionHealth","activeRun","id","toMatch","providerSessionId","not","toBe","list","toEqual","statSync","resolve","toBeNull","rejects","code","dirname","writeFileSync","target","JSON","stringify","version","agents","symlinkSync","lockPath","old","utimesSync","mutationLockStaleMs","resolves","live","Map","process","pid","processInspector","getIdentity","startedAt","get","acquired","acquireRun","completeRun","status","exitCode","summary","completed","token","first","run","recordProviderProcess","delete","set","recovered","lastResult","incompleteLockGraceMs","unlinkSync","clear","listed","moved","other","renameSync"],"mappings":"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"}
@@ -93,6 +93,15 @@ describe('TmuxManager', ()=>{
93
93
  });
94
94
  expect(await tmux.findAgentPid('foo', matchesClaude)).toBeNull();
95
95
  });
96
+ it('returns the matching pane PID when the agent replaces the shell', async ()=>{
97
+ setExecFileHandler((cmd, args)=>{
98
+ if (cmd === 'tmux' && args[0] === 'list-panes') return '100\n';
99
+ if (cmd === 'pgrep') return new Error('no children');
100
+ if (cmd === 'ps' && args[1] === '100') return '/usr/local/bin/claude';
101
+ return '';
102
+ });
103
+ expect(await tmux.findAgentPid('foo', matchesClaude)).toBe(100);
104
+ });
96
105
  it('returns the matching descendant when found', async ()=>{
97
106
  // pane 100 → child 200 (claude) — no grandchildren
98
107
  setExecFileHandler((cmd, args)=>{
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/__tests__/terminal/TmuxManager.test.ts"],"sourcesContent":["import { execFile } from 'child_process';\nimport type { MockedFunction } from 'vitest';\nimport { TmuxManager } from '../../terminal/TmuxManager.js';\n\nvi.mock('child_process', () => ({\n execFile: vi.fn(),\n}));\n\ntype ExecFileCb = (err: Error | null, result?: { stdout: string; stderr: string }) => void;\nconst mockedExecFile = execFile as unknown as MockedFunction<\n (cmd: string, args: string[], cb: ExecFileCb) => void\n>;\n\n/** Drive the promisified execFile mock with a per-call handler. */\nfunction setExecFileHandler(handler: (cmd: string, args: string[]) => string | Error) {\n mockedExecFile.mockImplementation((_cmd, _args, cb) => {\n const result = handler(_cmd, _args);\n if (result instanceof Error) cb(result);\n else cb(null, { stdout: result, stderr: '' });\n });\n}\n\ndescribe('TmuxManager', () => {\n let tmux: TmuxManager;\n\n beforeEach(() => {\n tmux = new TmuxManager();\n mockedExecFile.mockReset();\n });\n\n describe('isAvailable', () => {\n it('returns true when `tmux -V` succeeds', async () => {\n setExecFileHandler(() => 'tmux 3.4');\n expect(await tmux.isAvailable()).toBe(true);\n });\n\n it('returns false when tmux is missing', async () => {\n setExecFileHandler(() => new Error('ENOENT'));\n expect(await tmux.isAvailable()).toBe(false);\n });\n });\n\n describe('sessionExists', () => {\n it('returns true when has-session succeeds', async () => {\n setExecFileHandler(() => '');\n expect(await tmux.sessionExists('foo')).toBe(true);\n });\n\n it('returns false when has-session fails', async () => {\n setExecFileHandler(() => new Error('no session'));\n expect(await tmux.sessionExists('foo')).toBe(false);\n });\n });\n\n describe('createSession', () => {\n it('issues `tmux new-session -d -s <name> -c <cwd>`', async () => {\n setExecFileHandler(() => '');\n await tmux.createSession('foo', '/work');\n expect(mockedExecFile).toHaveBeenCalledWith(\n 'tmux',\n ['new-session', '-d', '-s', 'foo', '-c', '/work'],\n expect.any(Function),\n );\n });\n });\n\n describe('sendKeys', () => {\n it('appends Enter so the command runs', async () => {\n setExecFileHandler(() => '');\n await tmux.sendKeys('foo', 'claude');\n expect(mockedExecFile).toHaveBeenCalledWith(\n 'tmux',\n ['send-keys', '-t', 'foo', 'claude', 'Enter'],\n expect.any(Function),\n );\n });\n });\n\n describe('killSession', () => {\n it('swallows errors when session is already gone', async () => {\n setExecFileHandler(() => new Error(\"can't find session\"));\n await expect(tmux.killSession('foo')).resolves.toBeUndefined();\n });\n });\n\n describe('findAgentPid', () => {\n const matchesClaude = (cmd: string) => cmd.split(/\\s+/)[0]?.endsWith('claude') ?? false;\n\n it('returns null when the session has no pane', async () => {\n setExecFileHandler((cmd, args) => {\n if (args[0] === 'list-panes') return new Error('no session');\n return '';\n });\n expect(await tmux.findAgentPid('foo', matchesClaude)).toBeNull();\n });\n\n it('returns null when no descendant matches', async () => {\n // pane PID 100; child 200 is \"node /unrelated\" — no match\n setExecFileHandler((cmd, args) => {\n if (cmd === 'tmux' && args[0] === 'list-panes') return '100\\n';\n if (cmd === 'pgrep' && args[1] === '100') return '200\\n';\n if (cmd === 'pgrep') return new Error('no children');\n if (cmd === 'ps' && args[1] === '200') return 'node /unrelated';\n return '';\n });\n expect(await tmux.findAgentPid('foo', matchesClaude)).toBeNull();\n });\n\n it('returns the matching descendant when found', async () => {\n // pane 100 → child 200 (claude) — no grandchildren\n setExecFileHandler((cmd, args) => {\n if (cmd === 'tmux') return '100\\n';\n if (cmd === 'pgrep' && args[1] === '100') return '200\\n';\n if (cmd === 'pgrep') return new Error('no children');\n if (cmd === 'ps' && args[1] === '200') return 'claude';\n return '';\n });\n expect(await tmux.findAgentPid('foo', matchesClaude)).toBe(200);\n });\n\n it('prefers the deepest match (wrapper case)', async () => {\n // pane 100 → 200 (claude wrapper, matches) → 300 (claude, matches, deeper)\n setExecFileHandler((cmd, args) => {\n if (cmd === 'tmux') return '100\\n';\n if (cmd === 'pgrep' && args[1] === '100') return '200\\n';\n if (cmd === 'pgrep' && args[1] === '200') return '300\\n';\n if (cmd === 'pgrep') return new Error('no children');\n if (cmd === 'ps' && args[1] === '200') return '/usr/local/bin/claude';\n if (cmd === 'ps' && args[1] === '300') return '/usr/local/lib/claude';\n return '';\n });\n expect(await tmux.findAgentPid('foo', matchesClaude)).toBe(300);\n });\n\n it('skips non-matching subprocesses (MCP child case)', async () => {\n // pane 100 → 200 (claude, matches) → 300 (mcp-server, no match)\n setExecFileHandler((cmd, args) => {\n if (cmd === 'tmux') return '100\\n';\n if (cmd === 'pgrep' && args[1] === '100') return '200\\n';\n if (cmd === 'pgrep' && args[1] === '200') return '300\\n';\n if (cmd === 'pgrep') return new Error('no children');\n if (cmd === 'ps' && args[1] === '200') return 'claude';\n if (cmd === 'ps' && args[1] === '300') return 'node mcp-server.js';\n return '';\n });\n expect(await tmux.findAgentPid('foo', matchesClaude)).toBe(200);\n });\n\n it('handles the gemini Node-script shape via a token-scan matcher', async () => {\n const matchesGemini = (cmd: string) =>\n cmd.split(/\\s+/).some((t) => t.endsWith('/gemini') || t === 'gemini');\n setExecFileHandler((cmd, args) => {\n if (cmd === 'tmux') return '100\\n';\n if (cmd === 'pgrep' && args[1] === '100') return '200\\n';\n if (cmd === 'pgrep') return new Error('no children');\n if (cmd === 'ps' && args[1] === '200') return 'node /opt/homebrew/bin/gemini --foo';\n return '';\n });\n expect(await tmux.findAgentPid('foo', matchesGemini)).toBe(200);\n });\n\n it('handles multiple children at the same level', async () => {\n // pane 100 → [200 (nope), 201 (claude)]\n setExecFileHandler((cmd, args) => {\n if (cmd === 'tmux') return '100\\n';\n if (cmd === 'pgrep' && args[1] === '100') return '200\\n201\\n';\n if (cmd === 'pgrep') return new Error('no children');\n if (cmd === 'ps' && args[1] === '200') return 'bash';\n if (cmd === 'ps' && args[1] === '201') return 'claude';\n return '';\n });\n expect(await tmux.findAgentPid('foo', matchesClaude)).toBe(201);\n });\n });\n});\n"],"names":["execFile","TmuxManager","vi","mock","fn","mockedExecFile","setExecFileHandler","handler","mockImplementation","_cmd","_args","cb","result","Error","stdout","stderr","describe","tmux","beforeEach","mockReset","it","expect","isAvailable","toBe","sessionExists","createSession","toHaveBeenCalledWith","any","Function","sendKeys","killSession","resolves","toBeUndefined","matchesClaude","cmd","split","endsWith","args","findAgentPid","toBeNull","matchesGemini","some","t"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/__tests__/terminal/TmuxManager.test.ts"],"sourcesContent":["import { execFile } from 'child_process';\nimport type { MockedFunction } from 'vitest';\nimport { TmuxManager } from '../../terminal/TmuxManager.js';\n\nvi.mock('child_process', () => ({\n execFile: vi.fn(),\n}));\n\ntype ExecFileCb = (err: Error | null, result?: { stdout: string; stderr: string }) => void;\nconst mockedExecFile = execFile as unknown as MockedFunction<\n (cmd: string, args: string[], cb: ExecFileCb) => void\n>;\n\n/** Drive the promisified execFile mock with a per-call handler. */\nfunction setExecFileHandler(handler: (cmd: string, args: string[]) => string | Error) {\n mockedExecFile.mockImplementation((_cmd, _args, cb) => {\n const result = handler(_cmd, _args);\n if (result instanceof Error) cb(result);\n else cb(null, { stdout: result, stderr: '' });\n });\n}\n\ndescribe('TmuxManager', () => {\n let tmux: TmuxManager;\n\n beforeEach(() => {\n tmux = new TmuxManager();\n mockedExecFile.mockReset();\n });\n\n describe('isAvailable', () => {\n it('returns true when `tmux -V` succeeds', async () => {\n setExecFileHandler(() => 'tmux 3.4');\n expect(await tmux.isAvailable()).toBe(true);\n });\n\n it('returns false when tmux is missing', async () => {\n setExecFileHandler(() => new Error('ENOENT'));\n expect(await tmux.isAvailable()).toBe(false);\n });\n });\n\n describe('sessionExists', () => {\n it('returns true when has-session succeeds', async () => {\n setExecFileHandler(() => '');\n expect(await tmux.sessionExists('foo')).toBe(true);\n });\n\n it('returns false when has-session fails', async () => {\n setExecFileHandler(() => new Error('no session'));\n expect(await tmux.sessionExists('foo')).toBe(false);\n });\n });\n\n describe('createSession', () => {\n it('issues `tmux new-session -d -s <name> -c <cwd>`', async () => {\n setExecFileHandler(() => '');\n await tmux.createSession('foo', '/work');\n expect(mockedExecFile).toHaveBeenCalledWith(\n 'tmux',\n ['new-session', '-d', '-s', 'foo', '-c', '/work'],\n expect.any(Function),\n );\n });\n });\n\n describe('sendKeys', () => {\n it('appends Enter so the command runs', async () => {\n setExecFileHandler(() => '');\n await tmux.sendKeys('foo', 'claude');\n expect(mockedExecFile).toHaveBeenCalledWith(\n 'tmux',\n ['send-keys', '-t', 'foo', 'claude', 'Enter'],\n expect.any(Function),\n );\n });\n });\n\n describe('killSession', () => {\n it('swallows errors when session is already gone', async () => {\n setExecFileHandler(() => new Error(\"can't find session\"));\n await expect(tmux.killSession('foo')).resolves.toBeUndefined();\n });\n });\n\n describe('findAgentPid', () => {\n const matchesClaude = (cmd: string) => cmd.split(/\\s+/)[0]?.endsWith('claude') ?? false;\n\n it('returns null when the session has no pane', async () => {\n setExecFileHandler((cmd, args) => {\n if (args[0] === 'list-panes') return new Error('no session');\n return '';\n });\n expect(await tmux.findAgentPid('foo', matchesClaude)).toBeNull();\n });\n\n it('returns null when no descendant matches', async () => {\n // pane PID 100; child 200 is \"node /unrelated\" — no match\n setExecFileHandler((cmd, args) => {\n if (cmd === 'tmux' && args[0] === 'list-panes') return '100\\n';\n if (cmd === 'pgrep' && args[1] === '100') return '200\\n';\n if (cmd === 'pgrep') return new Error('no children');\n if (cmd === 'ps' && args[1] === '200') return 'node /unrelated';\n return '';\n });\n expect(await tmux.findAgentPid('foo', matchesClaude)).toBeNull();\n });\n\n it('returns the matching pane PID when the agent replaces the shell', async () => {\n setExecFileHandler((cmd, args) => {\n if (cmd === 'tmux' && args[0] === 'list-panes') return '100\\n';\n if (cmd === 'pgrep') return new Error('no children');\n if (cmd === 'ps' && args[1] === '100') return '/usr/local/bin/claude';\n return '';\n });\n expect(await tmux.findAgentPid('foo', matchesClaude)).toBe(100);\n });\n\n it('returns the matching descendant when found', async () => {\n // pane 100 → child 200 (claude) — no grandchildren\n setExecFileHandler((cmd, args) => {\n if (cmd === 'tmux') return '100\\n';\n if (cmd === 'pgrep' && args[1] === '100') return '200\\n';\n if (cmd === 'pgrep') return new Error('no children');\n if (cmd === 'ps' && args[1] === '200') return 'claude';\n return '';\n });\n expect(await tmux.findAgentPid('foo', matchesClaude)).toBe(200);\n });\n\n it('prefers the deepest match (wrapper case)', async () => {\n // pane 100 → 200 (claude wrapper, matches) → 300 (claude, matches, deeper)\n setExecFileHandler((cmd, args) => {\n if (cmd === 'tmux') return '100\\n';\n if (cmd === 'pgrep' && args[1] === '100') return '200\\n';\n if (cmd === 'pgrep' && args[1] === '200') return '300\\n';\n if (cmd === 'pgrep') return new Error('no children');\n if (cmd === 'ps' && args[1] === '200') return '/usr/local/bin/claude';\n if (cmd === 'ps' && args[1] === '300') return '/usr/local/lib/claude';\n return '';\n });\n expect(await tmux.findAgentPid('foo', matchesClaude)).toBe(300);\n });\n\n it('skips non-matching subprocesses (MCP child case)', async () => {\n // pane 100 → 200 (claude, matches) → 300 (mcp-server, no match)\n setExecFileHandler((cmd, args) => {\n if (cmd === 'tmux') return '100\\n';\n if (cmd === 'pgrep' && args[1] === '100') return '200\\n';\n if (cmd === 'pgrep' && args[1] === '200') return '300\\n';\n if (cmd === 'pgrep') return new Error('no children');\n if (cmd === 'ps' && args[1] === '200') return 'claude';\n if (cmd === 'ps' && args[1] === '300') return 'node mcp-server.js';\n return '';\n });\n expect(await tmux.findAgentPid('foo', matchesClaude)).toBe(200);\n });\n\n it('handles the gemini Node-script shape via a token-scan matcher', async () => {\n const matchesGemini = (cmd: string) =>\n cmd.split(/\\s+/).some((t) => t.endsWith('/gemini') || t === 'gemini');\n setExecFileHandler((cmd, args) => {\n if (cmd === 'tmux') return '100\\n';\n if (cmd === 'pgrep' && args[1] === '100') return '200\\n';\n if (cmd === 'pgrep') return new Error('no children');\n if (cmd === 'ps' && args[1] === '200') return 'node /opt/homebrew/bin/gemini --foo';\n return '';\n });\n expect(await tmux.findAgentPid('foo', matchesGemini)).toBe(200);\n });\n\n it('handles multiple children at the same level', async () => {\n // pane 100 → [200 (nope), 201 (claude)]\n setExecFileHandler((cmd, args) => {\n if (cmd === 'tmux') return '100\\n';\n if (cmd === 'pgrep' && args[1] === '100') return '200\\n201\\n';\n if (cmd === 'pgrep') return new Error('no children');\n if (cmd === 'ps' && args[1] === '200') return 'bash';\n if (cmd === 'ps' && args[1] === '201') return 'claude';\n return '';\n });\n expect(await tmux.findAgentPid('foo', matchesClaude)).toBe(201);\n });\n });\n});\n"],"names":["execFile","TmuxManager","vi","mock","fn","mockedExecFile","setExecFileHandler","handler","mockImplementation","_cmd","_args","cb","result","Error","stdout","stderr","describe","tmux","beforeEach","mockReset","it","expect","isAvailable","toBe","sessionExists","createSession","toHaveBeenCalledWith","any","Function","sendKeys","killSession","resolves","toBeUndefined","matchesClaude","cmd","split","endsWith","args","findAgentPid","toBeNull","matchesGemini","some","t"],"mappings":"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"}
@@ -71,16 +71,24 @@ export declare class CodexAdapter implements AgentAdapter {
71
71
  private parseMetaTimestampMs;
72
72
  private determineStatus;
73
73
  private extractSummary;
74
+ private normalizedPayloadType;
75
+ private extractEntryText;
74
76
  private truncate;
75
77
  private isCodexExecutable;
76
78
  /**
77
79
  * Read the full conversation from a Codex session JSONL file.
78
80
  *
79
- * Codex entries use payload.type to indicate message role and payload.message for content.
81
+ * Codex entries use either legacy payload.message fields or current
82
+ * response_item/event_msg content arrays.
80
83
  */
81
84
  getConversation(sessionFilePath: string, options?: {
82
85
  verbose?: boolean;
83
86
  }): ConversationMessage[];
87
+ private toConversationMessage;
88
+ private mapCodexRole;
89
+ private mapCodexItemRole;
90
+ private mirroredMessageKey;
91
+ private extractContentText;
84
92
  listSessions(opts?: ListSessionsOptions): Promise<SessionSummary[]>;
85
93
  /**
86
94
  * Walk every YYYY/MM/DD directory under `codexSessionsDir` and return
@@ -1 +1 @@
1
- {"version":3,"file":"CodexAdapter.d.ts","sourceRoot":"","sources":["../../src/adapters/CodexAdapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,KAAK,EACR,YAAY,EACZ,SAAS,EACT,WAAW,EACX,mBAAmB,EACnB,cAAc,EACd,mBAAmB,EACtB,MAAM,mBAAmB,CAAC;AAM3B,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AA2C1D,qBAAa,YAAa,YAAW,YAAY;IAC7C,QAAQ,CAAC,IAAI,EAAG,OAAO,CAAU;IAEjC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,sBAAsB,CAAK;IACnD,sFAAsF;IACtF,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,6BAA6B,CAAK;IAE1D,OAAO,CAAC,gBAAgB,CAAS;IACjC,OAAO,CAAC,kBAAkB,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAgB;gBAEpB,QAAQ,GAAE,aAAuC;IAO7D,SAAS,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO;IAI5C;;OAEG;IACG,YAAY,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;IAmD1C,OAAO,CAAC,wBAAwB;IAgBhC,OAAO,CAAC,uBAAuB;IAuB/B,OAAO,CAAC,kBAAkB;IAwB1B,OAAO,CAAC,KAAK;IAQb,OAAO,CAAC,oBAAoB;IAM5B,OAAO,CAAC,gBAAgB;IAiCxB;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IA0BzB,OAAO,CAAC,sBAAsB;IAK9B,OAAO,CAAC,mBAAmB;IAiC3B,OAAO,CAAC,wBAAwB;IAWhC,OAAO,CAAC,gBAAgB;IAgBxB;;;;;;;OAOG;IACH,OAAO,CAAC,gBAAgB;IAsCxB;;;OAGG;IACH,OAAO,CAAC,WAAW;IA4BnB,OAAO,CAAC,qBAAqB;IAe7B,OAAO,CAAC,eAAe;IAOvB,OAAO,CAAC,kBAAkB;IAW1B;;;OAGG;IACH,OAAO,CAAC,YAAY;IAwDpB,OAAO,CAAC,iBAAiB;IAczB,OAAO,CAAC,mBAAmB;IAa3B,OAAO,CAAC,kBAAkB;IAU1B,OAAO,CAAC,cAAc;IAMtB,OAAO,CAAC,oBAAoB;IAU5B,OAAO,CAAC,eAAe;IAmBvB,OAAO,CAAC,cAAc;IAWtB,OAAO,CAAC,QAAQ;IAKhB,OAAO,CAAC,iBAAiB;IAMzB;;;;OAIG;IACH,eAAe,CAAC,eAAe,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,mBAAmB,EAAE;IA8C1F,YAAY,CAAC,IAAI,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAgBzE;;;;OAIG;IACH,OAAO,CAAC,sBAAsB;IA0B9B,OAAO,CAAC,6BAA6B;IAarC;;;;OAIG;IACH,OAAO,CAAC,oBAAoB;CA8D/B"}
1
+ {"version":3,"file":"CodexAdapter.d.ts","sourceRoot":"","sources":["../../src/adapters/CodexAdapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,KAAK,EACR,YAAY,EACZ,SAAS,EACT,WAAW,EACX,mBAAmB,EACnB,cAAc,EACd,mBAAmB,EACtB,MAAM,mBAAmB,CAAC;AAM3B,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AA4D1D,qBAAa,YAAa,YAAW,YAAY;IAC7C,QAAQ,CAAC,IAAI,EAAG,OAAO,CAAU;IAEjC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,sBAAsB,CAAK;IACnD,sFAAsF;IACtF,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,6BAA6B,CAAK;IAE1D,OAAO,CAAC,gBAAgB,CAAS;IACjC,OAAO,CAAC,kBAAkB,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAgB;gBAEpB,QAAQ,GAAE,aAAuC;IAO7D,SAAS,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO;IAI5C;;OAEG;IACG,YAAY,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;IAmD1C,OAAO,CAAC,wBAAwB;IAgBhC,OAAO,CAAC,uBAAuB;IAuB/B,OAAO,CAAC,kBAAkB;IAwB1B,OAAO,CAAC,KAAK;IAQb,OAAO,CAAC,oBAAoB;IAM5B,OAAO,CAAC,gBAAgB;IAiCxB;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IA0BzB,OAAO,CAAC,sBAAsB;IAK9B,OAAO,CAAC,mBAAmB;IAiC3B,OAAO,CAAC,wBAAwB;IAWhC,OAAO,CAAC,gBAAgB;IAgBxB;;;;;;;OAOG;IACH,OAAO,CAAC,gBAAgB;IAsCxB;;;OAGG;IACH,OAAO,CAAC,WAAW;IA4BnB,OAAO,CAAC,qBAAqB;IAe7B,OAAO,CAAC,eAAe;IAOvB,OAAO,CAAC,kBAAkB;IAW1B;;;OAGG;IACH,OAAO,CAAC,YAAY;IAwDpB,OAAO,CAAC,iBAAiB;IAczB,OAAO,CAAC,mBAAmB;IAa3B,OAAO,CAAC,kBAAkB;IAU1B,OAAO,CAAC,cAAc;IAMtB,OAAO,CAAC,oBAAoB;IAU5B,OAAO,CAAC,eAAe;IAmBvB,OAAO,CAAC,cAAc;IAStB,OAAO,CAAC,qBAAqB;IAmB7B,OAAO,CAAC,gBAAgB;IAYxB,OAAO,CAAC,QAAQ;IAKhB,OAAO,CAAC,iBAAiB;IAMzB;;;;;OAKG;IACH,eAAe,CAAC,eAAe,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,mBAAmB,EAAE;IA8ChG,OAAO,CAAC,qBAAqB;IAwC7B,OAAO,CAAC,YAAY;IAMpB,OAAO,CAAC,gBAAgB;IAMxB,OAAO,CAAC,kBAAkB;IAS1B,OAAO,CAAC,kBAAkB;IAYpB,YAAY,CAAC,IAAI,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAgBzE;;;;OAIG;IACH,OAAO,CAAC,sBAAsB;IA0B9B,OAAO,CAAC,6BAA6B;IAarC;;;;OAIG;IACH,OAAO,CAAC,oBAAoB;CA8D/B"}
@@ -390,7 +390,7 @@ export class CodexAdapter {
390
390
  }
391
391
  }
392
392
  const lastEntry = this.findLastEventEntry(entries);
393
- const lastPayloadType = lastEntry?.payload?.type;
393
+ const lastPayloadType = lastEntry ? this.normalizedPayloadType(lastEntry) : undefined;
394
394
  const lastActive = this.parseTimestamp(lastEntry?.timestamp) || this.parseTimestamp(metaEntry.payload.timestamp) || fs.statSync(filePath).mtime;
395
395
  const sessionStart = this.parseTimestamp(metaEntry.payload.timestamp) || lastActive;
396
396
  return {
@@ -461,13 +461,35 @@ export class CodexAdapter {
461
461
  }
462
462
  extractSummary(entries) {
463
463
  for(let i = entries.length - 1; i >= 0; i--){
464
- const message = entries[i]?.payload?.message;
465
- if (typeof message === 'string' && message.trim().length > 0) {
466
- return this.truncate(message.trim(), 120);
467
- }
464
+ const message = this.extractEntryText(entries[i]);
465
+ if (message) return this.truncate(message, 120);
468
466
  }
469
467
  return 'Codex session active';
470
468
  }
469
+ normalizedPayloadType(entry) {
470
+ const payloadType = entry.payload?.type;
471
+ if (entry.type === 'response_item' && payloadType === 'message') {
472
+ if (entry.payload?.role === 'assistant') return 'agent_message';
473
+ if (entry.payload?.role === 'user') return 'user_message';
474
+ return payloadType;
475
+ }
476
+ if (entry.type === 'event_msg' && payloadType === 'item_completed') {
477
+ const itemType = entry.payload?.item?.type;
478
+ if (itemType === 'AgentMessage') return 'agent_message';
479
+ if (itemType === 'UserMessage') return 'user_message';
480
+ return itemType ?? payloadType;
481
+ }
482
+ return payloadType;
483
+ }
484
+ extractEntryText(entry) {
485
+ if (!entry) return '';
486
+ const legacyMessage = entry.payload?.message;
487
+ if (typeof legacyMessage === 'string' && legacyMessage.trim().length > 0) {
488
+ return legacyMessage.trim();
489
+ }
490
+ const conversationMessage = this.toConversationMessage(entry, false);
491
+ return conversationMessage?.content.trim() ?? '';
492
+ }
471
493
  truncate(value, maxLength) {
472
494
  if (value.length <= maxLength) return value;
473
495
  return `${value.slice(0, maxLength - 3)}...`;
@@ -480,42 +502,102 @@ export class CodexAdapter {
480
502
  /**
481
503
  * Read the full conversation from a Codex session JSONL file.
482
504
  *
483
- * Codex entries use payload.type to indicate message role and payload.message for content.
505
+ * Codex entries use either legacy payload.message fields or current
506
+ * response_item/event_msg content arrays.
484
507
  */ getConversation(sessionFilePath, options) {
485
508
  const verbose = options?.verbose ?? false;
486
509
  const content = safeReadFile(sessionFilePath);
487
510
  if (content === undefined) return [];
488
511
  const lines = content.trim().split('\n');
512
+ const entries = [];
489
513
  const messages = [];
490
514
  for (const line of lines){
491
- let entry;
492
515
  try {
493
- entry = JSON.parse(line);
516
+ entries.push(JSON.parse(line));
494
517
  } catch {
495
518
  continue;
496
519
  }
497
- if (entry.type === 'session_meta') continue;
498
- const payloadType = entry.payload?.type;
499
- if (!payloadType) continue;
500
- let role;
501
- if (payloadType === 'user_message') {
502
- role = 'user';
503
- } else if (payloadType === 'agent_message' || payloadType === 'task_complete') {
504
- role = 'assistant';
505
- } else if (verbose) {
506
- role = 'system';
507
- } else {
520
+ }
521
+ const responseItemMirrorKeys = new Set();
522
+ for (const entry of entries){
523
+ if (entry.type !== 'response_item') continue;
524
+ const message = this.toConversationMessage(entry, verbose);
525
+ const mirrorKey = message ? this.mirroredMessageKey(entry, message) : null;
526
+ if (mirrorKey) responseItemMirrorKeys.add(mirrorKey);
527
+ }
528
+ for (const entry of entries){
529
+ const message = this.toConversationMessage(entry, verbose);
530
+ if (!message) continue;
531
+ const mirrorKey = this.mirroredMessageKey(entry, message);
532
+ if (entry.type === 'event_msg' && mirrorKey && responseItemMirrorKeys.has(mirrorKey)) {
508
533
  continue;
509
534
  }
510
- const text = entry.payload?.message?.trim();
511
- if (!text) continue;
512
- messages.push({
535
+ messages.push(message);
536
+ }
537
+ return messages;
538
+ }
539
+ toConversationMessage(entry, verbose) {
540
+ if (entry.type === 'session_meta') return null;
541
+ const payloadType = entry.payload?.type;
542
+ if (entry.type === 'response_item' && payloadType === 'message') {
543
+ const role = this.mapCodexRole(entry.payload?.role, verbose);
544
+ const text = this.extractContentText(entry.payload?.content);
545
+ if (!role || !text) return null;
546
+ return {
513
547
  role,
514
548
  content: text,
515
549
  timestamp: entry.timestamp
516
- });
550
+ };
517
551
  }
518
- return messages;
552
+ if (entry.type === 'event_msg' && payloadType === 'item_completed') {
553
+ const item = entry.payload?.item;
554
+ const role = this.mapCodexItemRole(item?.type, verbose);
555
+ const text = this.extractContentText(item?.content);
556
+ if (!role || !text) return null;
557
+ return {
558
+ role,
559
+ content: text,
560
+ timestamp: entry.timestamp
561
+ };
562
+ }
563
+ if (!payloadType) return null;
564
+ let role;
565
+ if (payloadType === 'user_message') {
566
+ role = 'user';
567
+ } else if (payloadType === 'agent_message' || payloadType === 'task_complete') {
568
+ role = 'assistant';
569
+ } else if (verbose) {
570
+ role = 'system';
571
+ } else {
572
+ return null;
573
+ }
574
+ const text = entry.payload?.message?.trim();
575
+ if (!text) return null;
576
+ return {
577
+ role,
578
+ content: text,
579
+ timestamp: entry.timestamp
580
+ };
581
+ }
582
+ mapCodexRole(role, verbose) {
583
+ if (role === 'user') return 'user';
584
+ if (role === 'assistant') return 'assistant';
585
+ return verbose ? 'system' : null;
586
+ }
587
+ mapCodexItemRole(itemType, verbose) {
588
+ if (itemType === 'AgentMessage') return 'assistant';
589
+ if (itemType === 'UserMessage') return 'user';
590
+ return verbose ? 'system' : null;
591
+ }
592
+ mirroredMessageKey(entry, message) {
593
+ const turnId = entry.payload?.turn_id || entry.payload?.internal_chat_message_metadata_passthrough?.turn_id;
594
+ if (!turnId) return null;
595
+ return `${turnId}\0${message.role}\0${message.content}`;
596
+ }
597
+ extractContentText(content) {
598
+ if (typeof content === 'string') return content.trim();
599
+ if (!Array.isArray(content)) return '';
600
+ return content.map((part)=>part.text).filter((text)=>typeof text === 'string' && text.trim().length > 0).map((text)=>text.trim()).join('\n').trim();
519
601
  }
520
602
  async listSessions(opts) {
521
603
  if (!isDirectory(this.codexSessionsDir)) return [];