Skip to content

Commit 03c351c

Browse files
committed
fix(omp): discover sub-agent sessions nested inside session subdirectories
OMP stores sub-agent JSONL files one level deeper than the project dir: ~/.omp/agent/sessions/<project>/<session-id>/<agent-name>.jsonl The previous flat readdir scan only found files directly inside the project dir, missing all sub-agent sessions entirely. - Extract collectJsonlFromDir helper that recurses one level into subdirectories within each project dir - Extend collectDiscoverySnapshot to also record mtime of sub-session subdirectories (adding files inside a subdir does not bump the parent dir mtime, so cache invalidation would otherwise never trigger) - Add test covering sub-agent session discovery
1 parent dcb872f commit 03c351c

2 files changed

Lines changed: 96 additions & 31 deletions

File tree

src/providers/pi.ts

Lines changed: 65 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,23 @@ async function collectDiscoverySnapshot(sessionsDir: string): Promise<DiscoveryS
8888
const dirStat = await stat(dirPath).catch(() => null)
8989
if (!dirStat?.isDirectory()) continue
9090
snapshot.push({ path: dirPath, mtimeMs: dirStat.mtimeMs })
91+
92+
// Sub-agent sessions land in <project-dir>/<session-id>/ subdirectories.
93+
// Their mtimes must be tracked separately — adding files inside a subdir
94+
// does not bump the parent project dir's mtime.
95+
let subEntries: string[]
96+
try {
97+
subEntries = await readdir(dirPath)
98+
} catch {
99+
continue
100+
}
101+
for (const subName of subEntries) {
102+
const subPath = join(dirPath, subName)
103+
const subStat = await stat(subPath).catch(() => null)
104+
if (subStat?.isDirectory()) {
105+
snapshot.push({ path: subPath, mtimeMs: subStat.mtimeMs })
106+
}
107+
}
91108
}
92109

93110
return snapshot
@@ -112,39 +129,58 @@ async function discoverSessionsInDir(sessionsDir: string, providerName: string):
112129
const dirStat = await stat(dirPath).catch(() => null)
113130
if (!dirStat?.isDirectory()) continue
114131

115-
let files: string[]
116-
try {
117-
files = await readdir(dirPath)
118-
} catch {
119-
continue
120-
}
121-
122-
for (const file of files) {
123-
if (!file.endsWith('.jsonl')) continue
124-
const filePath = join(dirPath, file)
125-
const fileStat = await stat(filePath).catch(() => null)
126-
if (!fileStat?.isFile()) continue
127-
128-
const first = await readFirstEntry(filePath)
129-
if (!first || first.type !== 'session') continue
130-
131-
const cwd = first.cwd ?? dirName
132-
sources.push({
133-
path: filePath,
134-
project: basename(cwd),
135-
provider: providerName,
136-
fingerprintPath: filePath,
137-
cacheStrategy: 'append-jsonl',
138-
progressLabel: basename(filePath),
139-
parserVersion: `${providerName}:v1`,
140-
})
141-
}
132+
await collectJsonlFromDir(dirPath, dirName, providerName, sources)
142133
}
143134

144135
await saveDiscoveryCache(providerName, sessionsDir, snapshot, sources)
145136
return sources
146137
}
147138

139+
// Collects session sources from dirPath and one level of subdirectories.
140+
// Sub-agent sessions land in <project-dir>/<parent-session-id>/<agent-name>.jsonl,
141+
// so we must recurse one level deeper than the project directory.
142+
async function collectJsonlFromDir(
143+
dirPath: string,
144+
projectDirName: string,
145+
providerName: string,
146+
sources: SessionSource[],
147+
): Promise<void> {
148+
let entries: string[]
149+
try {
150+
entries = await readdir(dirPath)
151+
} catch {
152+
return
153+
}
154+
155+
for (const entry of entries) {
156+
const entryPath = join(dirPath, entry)
157+
const entryStat = await stat(entryPath).catch(() => null)
158+
if (!entryStat) continue
159+
160+
if (entryStat.isDirectory()) {
161+
// Sub-agent session dir: recurse one level, but don't go deeper
162+
await collectJsonlFromDir(entryPath, projectDirName, providerName, sources)
163+
continue
164+
}
165+
166+
if (!entryStat.isFile() || !entry.endsWith('.jsonl')) continue
167+
168+
const first = await readFirstEntry(entryPath)
169+
if (!first || first.type !== 'session') continue
170+
171+
const cwd = first.cwd ?? projectDirName
172+
sources.push({
173+
path: entryPath,
174+
project: basename(cwd),
175+
provider: providerName,
176+
fingerprintPath: entryPath,
177+
cacheStrategy: 'append-jsonl',
178+
progressLabel: basename(entryPath),
179+
parserVersion: `${providerName}:v1`,
180+
})
181+
}
182+
}
183+
148184
function createParser(source: SessionSource, seenKeys: Set<string>): SessionParser {
149185
return {
150186
async *parse(): AsyncGenerator<ParsedProviderCall> {
@@ -188,7 +224,7 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
188224

189225
const model = msg.model ?? 'gpt-5'
190226
const responseId = msg.responseId ?? ''
191-
const dedupKey = `pi:${source.path}:${responseId || entry.id || entry.timestamp || String(lineIdx)}`
227+
const dedupKey = `${source.provider}:${source.path}:${responseId || entry.id || entry.timestamp || String(lineIdx)}`
192228

193229
if (seenKeys.has(dedupKey)) continue
194230
seenKeys.add(dedupKey)
@@ -289,4 +325,4 @@ export function createOmpProvider(sessionsDir?: string): Provider {
289325
}
290326
}
291327

292-
export const omp = createOmpProvider()
328+
export const omp = createOmpProvider()

tests/providers/omp.test.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,35 @@ describe('omp provider - session discovery', () => {
126126
const sessions = await provider.discoverSessions()
127127
expect(sessions).toEqual([])
128128
})
129+
it('discovers sub-agent sessions nested inside session subdirectory', async () => {
130+
const projectDir = join(tmpDir, '--Users-test-myproject--')
131+
const parentFile = '2026-04-21T10-00-00-000Z_abc123'
132+
133+
// Main session at project-dir level
134+
await writeSession(projectDir, `${parentFile}.jsonl`, [
135+
sessionMeta({ id: 'main-session', cwd: '/Users/test/myproject' }),
136+
assistantMessage({}),
137+
])
138+
139+
// Sub-agent sessions inside <session-id>/ subdirectory
140+
const subDir = join(projectDir, parentFile)
141+
await writeSession(subDir, '0-SubAgent.jsonl', [
142+
sessionMeta({ id: 'sub-agent-0', cwd: '/Users/test/myproject' }),
143+
assistantMessage({ input: 500, output: 100 }),
144+
])
145+
await writeSession(subDir, '1-SubAgent.jsonl', [
146+
sessionMeta({ id: 'sub-agent-1', cwd: '/Users/test/myproject' }),
147+
assistantMessage({ input: 600, output: 120 }),
148+
])
149+
150+
const provider = createOmpProvider(tmpDir)
151+
const sessions = await provider.discoverSessions()
152+
153+
expect(sessions).toHaveLength(3)
154+
expect(sessions.every(s => s.provider === 'omp')).toBe(true)
155+
expect(sessions.every(s => s.project === 'myproject')).toBe(true)
156+
})
157+
129158
})
130159

131160
describe('omp provider - JSONL parsing', () => {
@@ -164,7 +193,7 @@ describe('omp provider - JSONL parsing', () => {
164193
expect(call.sessionId).toBe('sess-omp-1')
165194
expect(call.userMessage).toBe('write a test')
166195
expect(call.timestamp).toBe('2026-04-14T10:00:30.000Z')
167-
expect(call.deduplicationKey).toContain('pi:')
196+
expect(call.deduplicationKey).toContain('omp:')
168197
expect(call.deduplicationKey).toContain('resp-omp-1')
169198
})
170199

@@ -183,7 +212,7 @@ describe('omp provider - JSONL parsing', () => {
183212
}
184213

185214
// cost must be calculated by codeburn, not taken from usage.cost (which is zeroed in fixture)
186-
expect(calls[0]!.costUSD).toBeGreaterThanOrEqual(0)
215+
expect(calls[0]!.costUSD).toBeGreaterThan(0)
187216
})
188217

189218
it('collects tool names from toolCall content items', async () => {

0 commit comments

Comments
 (0)