diff --git a/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts b/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts index 530e4328..9320a2fe 100644 --- a/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +++ b/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts @@ -25,6 +25,21 @@ export class ClaudeSessionSynchronizer implements IProviderSessionSynchronizer { private readonly provider = 'claude' as const; private readonly claudeHome = path.join(os.homedir(), '.claude'); + /** + * Returns true when a JSONL file is a subagent transcript rather than a + * top-level session. + * + * Claude stores subagent transcripts under a `subagents/` directory, e.g. + * `~/.claude/projects///subagents/agent-.jsonl`. + * Those files repeat the parent session's `sessionId`, so indexing them as + * standalone sessions overwrites the parent row's `jsonl_path` and corrupts + * the main session record. The recursive scan in `synchronize()` reaches + * them, so both entry points must skip them. + */ + private isSubagentTranscript(filePath: string): boolean { + return path.normalize(filePath).split(path.sep).includes('subagents'); + } + /** * Scans ~/.claude/projects and upserts discovered sessions into DB. */ @@ -38,6 +53,10 @@ export class ClaudeSessionSynchronizer implements IProviderSessionSynchronizer { let processed = 0; for (const filePath of files) { + if (this.isSubagentTranscript(filePath)) { + continue; + } + const parsed = await this.processSessionFile(filePath, nameMap); if (!parsed) { continue; @@ -66,6 +85,9 @@ export class ClaudeSessionSynchronizer implements IProviderSessionSynchronizer { if (!filePath.endsWith('.jsonl')) { return null; } + if (this.isSubagentTranscript(filePath)) { + return null; + } const nameMap = await buildLookupMap(path.join(this.claudeHome, 'history.jsonl'), 'sessionId', 'display'); const parsed = await this.processSessionFile(filePath, nameMap);