@@ -510,6 +510,101 @@ def selective_stat(self, *args, **kwargs):
510510 assert "SKIP: unreadable.txt" in err
511511 assert "stat error" in err
512512
513+ def test_scan_skips_subagent_dirs_by_default (self , tmp_path ):
514+ # Mimic Claude Code layout: ~/.claude/projects/<slug>/<session>/subagents/agent-*.jsonl
515+ session_dir = tmp_path / "session-abc"
516+ session_dir .mkdir ()
517+ (session_dir / "main.jsonl" ).write_text ('{"type":"user"}\n ' , encoding = "utf-8" )
518+ subagents_dir = session_dir / "subagents"
519+ subagents_dir .mkdir ()
520+ (subagents_dir / "agent-abc.jsonl" ).write_text ('{"type":"user"}\n ' , encoding = "utf-8" )
521+ (subagents_dir / "agent-def.jsonl" ).write_text ('{"type":"user"}\n ' , encoding = "utf-8" )
522+
523+ files = scan_convos (str (tmp_path ))
524+ names = [f .name for f in files ]
525+
526+ assert "main.jsonl" in names
527+ assert "agent-abc.jsonl" not in names
528+ assert "agent-def.jsonl" not in names
529+
530+ def test_scan_includes_subagent_dirs_when_opted_in (self , tmp_path ):
531+ session_dir = tmp_path / "session-abc"
532+ session_dir .mkdir ()
533+ (session_dir / "main.jsonl" ).write_text ('{"type":"user"}\n ' , encoding = "utf-8" )
534+ subagents_dir = session_dir / "subagents"
535+ subagents_dir .mkdir ()
536+ (subagents_dir / "agent-abc.jsonl" ).write_text ('{"type":"user"}\n ' , encoding = "utf-8" )
537+
538+ files = scan_convos (str (tmp_path ), include_subagents = True )
539+ names = [f .name for f in files ]
540+
541+ assert "main.jsonl" in names
542+ assert "agent-abc.jsonl" in names
543+
544+ def test_scan_skips_subagent_dirs_at_any_depth (self , tmp_path ):
545+ # The "subagents" name match is by directory name, not by depth: verify
546+ # both shallow (top-level) and nested subagents/ get skipped.
547+ (tmp_path / "subagents" ).mkdir ()
548+ (tmp_path / "subagents" / "agent-top.jsonl" ).write_text ("{}" , encoding = "utf-8" )
549+ nested = tmp_path / "session" / "subagents"
550+ nested .mkdir (parents = True )
551+ (nested / "agent-deep.jsonl" ).write_text ("{}" , encoding = "utf-8" )
552+ (tmp_path / "session" / "main.jsonl" ).write_text ("{}" , encoding = "utf-8" )
553+
554+ files = scan_convos (str (tmp_path ))
555+ names = [f .name for f in files ]
556+
557+ assert "main.jsonl" in names
558+ assert "agent-top.jsonl" not in names
559+ assert "agent-deep.jsonl" not in names
560+
561+ def test_scan_does_not_skip_suffix_named_dirs (self , tmp_path ):
562+ # Exact name match only: 'mysubagents' or 'subagentsbackup' must still
563+ # be mined. Guards against future regression to substring/regex match.
564+ for dir_name in ("mysubagents" , "subagentsbackup" , "subagent" ):
565+ d = tmp_path / dir_name
566+ d .mkdir ()
567+ (d / f"{ dir_name } .jsonl" ).write_text ("{}" , encoding = "utf-8" )
568+
569+ files = scan_convos (str (tmp_path ))
570+ names = {f .name for f in files }
571+
572+ assert "mysubagents.jsonl" in names
573+ assert "subagentsbackup.jsonl" in names
574+ assert "subagent.jsonl" in names
575+
576+ def test_scan_skips_subagents_case_insensitive (self , tmp_path ):
577+ # On Windows + macOS APFS the filesystem is case-preserving; if Claude
578+ # Code or a plugin ever emits 'Subagents' (capitalized), the filter
579+ # must still match. Only one variant per tmp_path because case-
580+ # insensitive filesystems collapse 'Subagents' and 'SUBAGENTS'.
581+ d = tmp_path / "Subagents"
582+ d .mkdir ()
583+ (d / "agent.jsonl" ).write_text ("{}" , encoding = "utf-8" )
584+ (tmp_path / "main.jsonl" ).write_text ("{}" , encoding = "utf-8" )
585+
586+ files = scan_convos (str (tmp_path ))
587+ names = {f .name for f in files }
588+
589+ assert "main.jsonl" in names
590+ assert "agent.jsonl" not in names
591+
592+ def test_scan_mines_an_explicitly_named_file_inside_subagents (self , tmp_path ):
593+ # The skip is directory pruning, so it cannot reach a caller who names
594+ # one file: that path feeds a single synthetic entry with no directories
595+ # to prune. The split is deliberate -- --include-subagents governs what a
596+ # directory walk sweeps up, while naming a path is an explicit request
597+ # and stays honored. Pinned because the two behaviours were written
598+ # independently and nothing else exercises them together.
599+ subagents_dir = tmp_path / "session-abc" / "subagents"
600+ subagents_dir .mkdir (parents = True )
601+ target = subagents_dir / "agent-abc.jsonl"
602+ target .write_text ('{"type":"user"}\n ' , encoding = "utf-8" )
603+
604+ files = scan_convos (str (target ))
605+
606+ assert [f .name for f in files ] == ["agent-abc.jsonl" ]
607+
513608
514609class TestFileChunksLocked :
515610 def test_uses_bounded_upsert_batches (self , monkeypatch ):
0 commit comments