@ai-devkit/agent-manager 0.19.1 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -86,6 +86,10 @@ describe('CodexAdapter', ()=>{
86
86
  });
87
87
  });
88
88
  describe('detectAgents', ()=>{
89
+ async function useRealSessionMatcher() {
90
+ const actualMatching = await vi.importActual('../../utils/matching.js');
91
+ mockedMatchProcessesToSessions.mockImplementation(actualMatching.matchProcessesToSessions);
92
+ }
89
93
  it('should return empty list when no codex process is running', async ()=>{
90
94
  mockedListAgentProcesses.mockReturnValue([]);
91
95
  const agents = await adapter.detectAgents();
@@ -194,6 +198,203 @@ describe('CodexAdapter', ()=>{
194
198
  force: true
195
199
  });
196
200
  });
201
+ it('should match when session_meta timestamp and file birthtime both align with process start', async ()=>{
202
+ await useRealSessionMatcher();
203
+ const processStart = new Date('2026-03-18T15:00:00.000Z');
204
+ const sessionTimestamp = '2026-03-18T15:00:05.000Z';
205
+ const processes = [
206
+ {
207
+ pid: 101,
208
+ command: 'codex',
209
+ cwd: '/repo-a',
210
+ tty: 'ttys001',
211
+ startTime: processStart
212
+ }
213
+ ];
214
+ mockedListAgentProcesses.mockReturnValue(processes);
215
+ mockedEnrichProcesses.mockReturnValue(processes);
216
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-test-'));
217
+ const sessionsDir = path.join(tmpDir, 'sessions');
218
+ const dateDir = path.join(sessionsDir, '2026', '03', '18');
219
+ fs.mkdirSync(dateDir, {
220
+ recursive: true
221
+ });
222
+ const sessionFile = path.join(dateDir, 'sess-aligned.jsonl');
223
+ fs.writeFileSync(sessionFile, [
224
+ JSON.stringify({
225
+ type: 'session_meta',
226
+ payload: {
227
+ id: 'sess-aligned',
228
+ timestamp: sessionTimestamp,
229
+ cwd: '/repo-a'
230
+ }
231
+ }),
232
+ JSON.stringify({
233
+ type: 'event',
234
+ timestamp: sessionTimestamp,
235
+ payload: {
236
+ type: 'token_count',
237
+ message: 'Aligned session'
238
+ }
239
+ })
240
+ ].join('\n'));
241
+ adapter.codexSessionsDir = sessionsDir;
242
+ mockedBatchGetSessionFileBirthtimes.mockReturnValue([
243
+ {
244
+ sessionId: 'sess-aligned',
245
+ filePath: sessionFile,
246
+ projectDir: dateDir,
247
+ birthtimeMs: new Date(sessionTimestamp).getTime(),
248
+ resolvedCwd: ''
249
+ }
250
+ ]);
251
+ const agents = await adapter.detectAgents();
252
+ expect(agents).toHaveLength(1);
253
+ expect(agents[0]).toMatchObject({
254
+ pid: 101,
255
+ sessionId: 'sess-aligned',
256
+ summary: 'Aligned session'
257
+ });
258
+ fs.rmSync(tmpDir, {
259
+ recursive: true,
260
+ force: true
261
+ });
262
+ });
263
+ it('should match late-created session files using session_meta timestamp', async ()=>{
264
+ await useRealSessionMatcher();
265
+ const processStart = new Date('2026-03-18T15:00:00.000Z');
266
+ const sessionTimestamp = '2026-03-18T15:00:10.000Z';
267
+ const lateBirthtime = new Date('2026-03-18T15:05:30.000Z').getTime();
268
+ const processes = [
269
+ {
270
+ pid: 102,
271
+ command: 'codex',
272
+ cwd: '/repo-a',
273
+ tty: 'ttys001',
274
+ startTime: processStart
275
+ }
276
+ ];
277
+ mockedListAgentProcesses.mockReturnValue(processes);
278
+ mockedEnrichProcesses.mockReturnValue(processes);
279
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-test-'));
280
+ const sessionsDir = path.join(tmpDir, 'sessions');
281
+ const dateDir = path.join(sessionsDir, '2026', '03', '18');
282
+ fs.mkdirSync(dateDir, {
283
+ recursive: true
284
+ });
285
+ const sessionFile = path.join(dateDir, 'sess-late.jsonl');
286
+ fs.writeFileSync(sessionFile, [
287
+ JSON.stringify({
288
+ type: 'session_meta',
289
+ payload: {
290
+ id: 'sess-late',
291
+ timestamp: sessionTimestamp,
292
+ cwd: '/repo-a'
293
+ }
294
+ }),
295
+ JSON.stringify({
296
+ type: 'event',
297
+ timestamp: sessionTimestamp,
298
+ payload: {
299
+ type: 'token_count',
300
+ message: 'Late file session'
301
+ }
302
+ })
303
+ ].join('\n'));
304
+ adapter.codexSessionsDir = sessionsDir;
305
+ mockedBatchGetSessionFileBirthtimes.mockReturnValue([
306
+ {
307
+ sessionId: 'sess-late',
308
+ filePath: sessionFile,
309
+ projectDir: dateDir,
310
+ birthtimeMs: lateBirthtime,
311
+ resolvedCwd: ''
312
+ }
313
+ ]);
314
+ const agents = await adapter.detectAgents();
315
+ expect(agents).toHaveLength(1);
316
+ expect(agents[0]).toMatchObject({
317
+ pid: 102,
318
+ sessionId: 'sess-late',
319
+ summary: 'Late file session'
320
+ });
321
+ expect(mockedMatchProcessesToSessions.mock.calls[0][1][0].birthtimeMs).toBe(new Date(sessionTimestamp).getTime());
322
+ fs.rmSync(tmpDir, {
323
+ recursive: true,
324
+ force: true
325
+ });
326
+ });
327
+ it.each([
328
+ [
329
+ 'missing',
330
+ undefined
331
+ ],
332
+ [
333
+ 'invalid',
334
+ 'not-a-date'
335
+ ]
336
+ ])('should fall back to file birthtime when session_meta timestamp is %s', async (_label, metaTimestamp)=>{
337
+ await useRealSessionMatcher();
338
+ const processStart = new Date('2026-03-18T15:00:00.000Z');
339
+ const fileBirthtime = new Date('2026-03-18T15:00:20.000Z').getTime();
340
+ const processes = [
341
+ {
342
+ pid: 103,
343
+ command: 'codex',
344
+ cwd: '/repo-a',
345
+ tty: 'ttys001',
346
+ startTime: processStart
347
+ }
348
+ ];
349
+ mockedListAgentProcesses.mockReturnValue(processes);
350
+ mockedEnrichProcesses.mockReturnValue(processes);
351
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-test-'));
352
+ const sessionsDir = path.join(tmpDir, 'sessions');
353
+ const dateDir = path.join(sessionsDir, '2026', '03', '18');
354
+ fs.mkdirSync(dateDir, {
355
+ recursive: true
356
+ });
357
+ const payload = {
358
+ id: `sess-${_label}`,
359
+ cwd: '/repo-a'
360
+ };
361
+ if (metaTimestamp !== undefined) {
362
+ payload.timestamp = metaTimestamp;
363
+ }
364
+ const sessionFile = path.join(dateDir, `sess-${_label}.jsonl`);
365
+ fs.writeFileSync(sessionFile, [
366
+ JSON.stringify({
367
+ type: 'session_meta',
368
+ payload
369
+ }),
370
+ JSON.stringify({
371
+ type: 'event',
372
+ timestamp: '2026-03-18T15:00:30.000Z',
373
+ payload: {
374
+ type: 'token_count',
375
+ message: `${_label} timestamp session`
376
+ }
377
+ })
378
+ ].join('\n'));
379
+ adapter.codexSessionsDir = sessionsDir;
380
+ mockedBatchGetSessionFileBirthtimes.mockReturnValue([
381
+ {
382
+ sessionId: `sess-${_label}`,
383
+ filePath: sessionFile,
384
+ projectDir: dateDir,
385
+ birthtimeMs: fileBirthtime,
386
+ resolvedCwd: ''
387
+ }
388
+ ]);
389
+ const agents = await adapter.detectAgents();
390
+ expect(agents).toHaveLength(1);
391
+ expect(agents[0].sessionId).toBe(`sess-${_label}`);
392
+ expect(mockedMatchProcessesToSessions.mock.calls[0][1][0].birthtimeMs).toBe(fileBirthtime);
393
+ fs.rmSync(tmpDir, {
394
+ recursive: true,
395
+ force: true
396
+ });
397
+ });
197
398
  it('should fall back to process-only for unmatched processes', async ()=>{
198
399
  const processes = [
199
400
  {
@@ -699,6 +900,149 @@ describe('CodexAdapter', ()=>{
699
900
  const { sessions } = discoverSessions(processes);
700
901
  expect(sessions[0].resolvedCwd).toBe('');
701
902
  });
903
+ it('should use session_meta timestamp as the matching birthtime when valid', ()=>{
904
+ const sessionsDir = path.join(tmpDir, 'sessions');
905
+ adapter.codexSessionsDir = sessionsDir;
906
+ const discoverSessions = adapter.discoverSessions.bind(adapter);
907
+ const dateDir = path.join(sessionsDir, '2026', '03', '18');
908
+ fs.mkdirSync(dateDir, {
909
+ recursive: true
910
+ });
911
+ const sessionFile = path.join(dateDir, 'sess-meta-time.jsonl');
912
+ const metaTimestamp = '2026-03-18T15:00:05.000Z';
913
+ fs.writeFileSync(sessionFile, JSON.stringify({
914
+ type: 'session_meta',
915
+ payload: {
916
+ id: 'sess-meta-time',
917
+ timestamp: metaTimestamp,
918
+ cwd: '/repo-a'
919
+ }
920
+ }));
921
+ mockedBatchGetSessionFileBirthtimes.mockReturnValue([
922
+ {
923
+ sessionId: 'sess-meta-time',
924
+ filePath: sessionFile,
925
+ projectDir: dateDir,
926
+ birthtimeMs: new Date('2026-03-18T15:05:30.000Z').getTime(),
927
+ resolvedCwd: ''
928
+ }
929
+ ]);
930
+ const { sessions } = discoverSessions([
931
+ {
932
+ pid: 1,
933
+ command: 'codex',
934
+ cwd: '/repo-a',
935
+ tty: '',
936
+ startTime: new Date('2026-03-18T15:00:00Z')
937
+ }
938
+ ]);
939
+ expect(sessions[0].resolvedCwd).toBe('/repo-a');
940
+ expect(sessions[0].birthtimeMs).toBe(new Date(metaTimestamp).getTime());
941
+ });
942
+ it('should tolerate malformed session_meta and unreadable files', ()=>{
943
+ const sessionsDir = path.join(tmpDir, 'sessions');
944
+ adapter.codexSessionsDir = sessionsDir;
945
+ const discoverSessions = adapter.discoverSessions.bind(adapter);
946
+ const dateDir = path.join(sessionsDir, '2026', '03', '18');
947
+ fs.mkdirSync(dateDir, {
948
+ recursive: true
949
+ });
950
+ const malformedFile = path.join(dateDir, 'malformed.jsonl');
951
+ const missingFile = path.join(dateDir, 'missing.jsonl');
952
+ fs.writeFileSync(malformedFile, '{not valid json');
953
+ mockedBatchGetSessionFileBirthtimes.mockReturnValue([
954
+ {
955
+ sessionId: 'malformed',
956
+ filePath: malformedFile,
957
+ projectDir: dateDir,
958
+ birthtimeMs: 1710800324000,
959
+ resolvedCwd: ''
960
+ },
961
+ {
962
+ sessionId: 'missing',
963
+ filePath: missingFile,
964
+ projectDir: dateDir,
965
+ birthtimeMs: 1710800325000,
966
+ resolvedCwd: ''
967
+ }
968
+ ]);
969
+ const result = discoverSessions([
970
+ {
971
+ pid: 1,
972
+ command: 'codex',
973
+ cwd: '/repo',
974
+ tty: '',
975
+ startTime: new Date('2026-03-18T15:00:00Z')
976
+ }
977
+ ]);
978
+ expect(result.sessions).toHaveLength(2);
979
+ expect(result.sessions[0].resolvedCwd).toBe('');
980
+ expect(result.sessions[1].resolvedCwd).toBe('');
981
+ expect(result.contentCache.has(malformedFile)).toBe(true);
982
+ expect(result.contentCache.has(missingFile)).toBe(false);
983
+ });
984
+ });
985
+ describe('findSessionFileById', ()=>{
986
+ let tmpDir;
987
+ let sessionsDir;
988
+ const sessionId = 'aaaaaaaa-bbbb-4ccc-dddd-eeeeeeeeeeee';
989
+ beforeEach(()=>{
990
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-resume-find-'));
991
+ sessionsDir = path.join(tmpDir, 'sessions');
992
+ fs.mkdirSync(path.join(sessionsDir, '2026', '03', '18'), {
993
+ recursive: true
994
+ });
995
+ adapter.codexSessionsDir = sessionsDir;
996
+ });
997
+ afterEach(()=>{
998
+ fs.rmSync(tmpDir, {
999
+ recursive: true,
1000
+ force: true
1001
+ });
1002
+ });
1003
+ function writeResumeSession(timestamp) {
1004
+ const filePath = path.join(sessionsDir, '2026', '03', '18', `${sessionId}.jsonl`);
1005
+ const payload = {
1006
+ id: sessionId,
1007
+ cwd: '/repo-a'
1008
+ };
1009
+ if (timestamp !== undefined) {
1010
+ payload.timestamp = timestamp;
1011
+ }
1012
+ fs.writeFileSync(filePath, JSON.stringify({
1013
+ type: 'session_meta',
1014
+ payload
1015
+ }));
1016
+ return filePath;
1017
+ }
1018
+ it('should return session_meta timestamp as birthtimeMs when valid', ()=>{
1019
+ const metaTimestamp = '2026-03-18T15:00:05.000Z';
1020
+ writeResumeSession(metaTimestamp);
1021
+ const findSessionFileById = adapter.findSessionFileById.bind(adapter);
1022
+ const session = findSessionFileById(sessionId);
1023
+ expect(session).toMatchObject({
1024
+ sessionId,
1025
+ resolvedCwd: '/repo-a',
1026
+ birthtimeMs: new Date(metaTimestamp).getTime()
1027
+ });
1028
+ });
1029
+ it.each([
1030
+ [
1031
+ 'missing',
1032
+ undefined
1033
+ ],
1034
+ [
1035
+ 'invalid',
1036
+ 'not-a-date'
1037
+ ]
1038
+ ])('should fall back to stat birthtimeMs when session_meta timestamp is %s', (_label, metaTimestamp)=>{
1039
+ const sessionFile = writeResumeSession(metaTimestamp);
1040
+ const stat = fs.statSync(sessionFile);
1041
+ const findSessionFileById = adapter.findSessionFileById.bind(adapter);
1042
+ const session = findSessionFileById(sessionId);
1043
+ expect(session).not.toBeNull();
1044
+ expect(session.birthtimeMs).toBe(stat.birthtimeMs);
1045
+ });
702
1046
  });
703
1047
  describe('helper methods', ()=>{
704
1048
  describe('determineStatus', ()=>{