agent-dump 1.2.2__tar.gz → 1.2.3__tar.gz

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 (118) hide show
  1. {agent_dump-1.2.2 → agent_dump-1.2.3}/Cargo.lock +7 -7
  2. {agent_dump-1.2.2 → agent_dump-1.2.3}/Cargo.toml +1 -1
  3. {agent_dump-1.2.2 → agent_dump-1.2.3}/PKG-INFO +3 -3
  4. {agent_dump-1.2.2 → agent_dump-1.2.3}/README.md +2 -2
  5. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/compat/json/diagnostic.rs +22 -3
  6. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/compat/json.rs +12 -0
  7. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/output/export.rs +9 -38
  8. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/providers/claude/mod.rs +19 -20
  9. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/providers/claude/transcript.rs +5 -1
  10. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/providers/codex/enrichment.rs +4 -1
  11. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/providers/codex/mod.rs +10 -4
  12. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/providers/codex/transcript.rs +11 -3
  13. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/providers/contract.rs +1 -0
  14. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/providers/cursor/mod.rs +5 -0
  15. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/providers/desktop/mod.rs +4 -0
  16. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/providers/desktop/tests.rs +33 -0
  17. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/providers/files.rs +64 -22
  18. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/providers/jsonl.rs +60 -15
  19. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/providers/kimi/mod.rs +10 -4
  20. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/providers/kimi/transcript.rs +13 -1
  21. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/providers/kimi/wire.rs +17 -14
  22. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/providers/pi/mod.rs +10 -4
  23. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/providers/registry.rs +21 -0
  24. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/providers/sqlite/mod.rs +43 -0
  25. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/query/filter.rs +10 -6
  26. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/query/index/tests.rs +17 -0
  27. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/query/index.rs +33 -18
  28. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/query/scanner.rs +5 -4
  29. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/session/cache/tests.rs +5 -0
  30. {agent_dump-1.2.2 → agent_dump-1.2.3}/pyproject.toml +1 -1
  31. {agent_dump-1.2.2 → agent_dump-1.2.3}/resources/locales/en.json +1 -0
  32. {agent_dump-1.2.2 → agent_dump-1.2.3}/resources/locales/zh.json +1 -0
  33. {agent_dump-1.2.2 → agent_dump-1.2.3}/resources/prompts/insight-handoff.txt +1 -1
  34. {agent_dump-1.2.2 → agent_dump-1.2.3}/resources/prompts/pm-handoff.txt +1 -1
  35. {agent_dump-1.2.2 → agent_dump-1.2.3}/src/collect/llm.rs +45 -7
  36. {agent_dump-1.2.2 → agent_dump-1.2.3}/src/workflows/collect.rs +9 -20
  37. {agent_dump-1.2.2 → agent_dump-1.2.3}/src/workflows/interactive.rs +23 -20
  38. {agent_dump-1.2.2 → agent_dump-1.2.3}/src/workflows/list.rs +19 -27
  39. {agent_dump-1.2.2 → agent_dump-1.2.3}/src/workflows/machine.rs +17 -29
  40. {agent_dump-1.2.2 → agent_dump-1.2.3}/src/workflows/maintenance.rs +13 -8
  41. {agent_dump-1.2.2 → agent_dump-1.2.3}/src/workflows/reader.rs +1 -0
  42. {agent_dump-1.2.2 → agent_dump-1.2.3}/src/workflows/uri.rs +3 -4
  43. {agent_dump-1.2.2 → agent_dump-1.2.3}/LICENSE +0 -0
  44. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/Cargo.toml +0 -0
  45. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/compat/json/encoding.rs +0 -0
  46. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/compat/mod.rs +0 -0
  47. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/compat/value.rs +0 -0
  48. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/config.rs +0 -0
  49. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/lib.rs +0 -0
  50. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/output/diagnostics.rs +0 -0
  51. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/output/formats.rs +0 -0
  52. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/output/i18n.rs +0 -0
  53. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/output/mod.rs +0 -0
  54. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/output/render.rs +0 -0
  55. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/parallel.rs +0 -0
  56. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/providers/codex/patch.rs +0 -0
  57. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/providers/cursor/transcript.rs +0 -0
  58. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/providers/desktop/cherry.rs +0 -0
  59. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/providers/desktop/deepchat.rs +0 -0
  60. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/providers/desktop/minimax.rs +6 -6
  61. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/providers/error.rs +0 -0
  62. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/providers/mod.rs +0 -0
  63. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/providers/pi/transcript.rs +0 -0
  64. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/providers/source_tests.rs +0 -0
  65. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/providers/sqlite/connection.rs +0 -0
  66. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/providers/sqlite/legacy.rs +0 -0
  67. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/providers/sqlite/v2.rs +0 -0
  68. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/providers/title.rs +0 -0
  69. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/query/context.rs +0 -0
  70. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/query/mod.rs +0 -0
  71. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/query/read.rs +0 -0
  72. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/query/text.rs +0 -0
  73. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/query/transcript.rs +0 -0
  74. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/session/assembly.rs +0 -0
  75. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/session/cache.rs +0 -0
  76. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/session/mod.rs +0 -0
  77. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/session/timestamp.rs +0 -0
  78. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/storage/mod.rs +0 -0
  79. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/storage/private_files.rs +0 -0
  80. {agent_dump-1.2.2 → agent_dump-1.2.3}/crates/agent-dump-core/src/storage/source_io.rs +0 -0
  81. {agent_dump-1.2.2 → agent_dump-1.2.3}/resources/prompts/.gitattributes +0 -0
  82. {agent_dump-1.2.2 → agent_dump-1.2.3}/resources/prompts/README.md +0 -0
  83. {agent_dump-1.2.2 → agent_dump-1.2.3}/resources/prompts/insight-chunk.txt +0 -0
  84. {agent_dump-1.2.2 → agent_dump-1.2.3}/resources/prompts/insight-merge.txt +0 -0
  85. {agent_dump-1.2.2 → agent_dump-1.2.3}/resources/prompts/insight-report.txt +0 -0
  86. {agent_dump-1.2.2 → agent_dump-1.2.3}/resources/prompts/insight-retry.txt +0 -0
  87. {agent_dump-1.2.2 → agent_dump-1.2.3}/resources/prompts/pm-chunk.txt +0 -0
  88. {agent_dump-1.2.2 → agent_dump-1.2.3}/resources/prompts/pm-merge.txt +0 -0
  89. {agent_dump-1.2.2 → agent_dump-1.2.3}/resources/prompts/pm-report.txt +0 -0
  90. {agent_dump-1.2.2 → agent_dump-1.2.3}/resources/prompts/pm-retry.txt +0 -0
  91. {agent_dump-1.2.2 → agent_dump-1.2.3}/resources/prompts/read-session-en.txt +0 -0
  92. {agent_dump-1.2.2 → agent_dump-1.2.3}/resources/prompts/read-session-zh.txt +0 -0
  93. {agent_dump-1.2.2 → agent_dump-1.2.3}/resources/prompts/system.txt +0 -0
  94. {agent_dump-1.2.2 → agent_dump-1.2.3}/resources/prompts/uri.txt +0 -0
  95. {agent_dump-1.2.2 → agent_dump-1.2.3}/rust-toolchain.toml +0 -0
  96. {agent_dump-1.2.2 → agent_dump-1.2.3}/rustfmt.toml +0 -0
  97. {agent_dump-1.2.2 → agent_dump-1.2.3}/src/cli_args.rs +0 -0
  98. {agent_dump-1.2.2 → agent_dump-1.2.3}/src/collect/events.rs +0 -0
  99. {agent_dump-1.2.2 → agent_dump-1.2.3}/src/collect/handoff.rs +0 -0
  100. {agent_dump-1.2.2 → agent_dump-1.2.3}/src/collect/log.rs +0 -0
  101. {agent_dump-1.2.2 → agent_dump-1.2.3}/src/collect/mod.rs +0 -0
  102. {agent_dump-1.2.2 → agent_dump-1.2.3}/src/collect/model.rs +0 -0
  103. {agent_dump-1.2.2 → agent_dump-1.2.3}/src/collect/progress.rs +0 -0
  104. {agent_dump-1.2.2 → agent_dump-1.2.3}/src/collect/prompts.rs +0 -0
  105. {agent_dump-1.2.2 → agent_dump-1.2.3}/src/collect/reduction.rs +0 -0
  106. {agent_dump-1.2.2 → agent_dump-1.2.3}/src/collect/sessions.rs +0 -0
  107. {agent_dump-1.2.2 → agent_dump-1.2.3}/src/collect/summary.rs +0 -0
  108. {agent_dump-1.2.2 → agent_dump-1.2.3}/src/command.rs +0 -0
  109. {agent_dump-1.2.2 → agent_dump-1.2.3}/src/date_input.rs +0 -0
  110. {agent_dump-1.2.2 → agent_dump-1.2.3}/src/main.rs +0 -0
  111. {agent_dump-1.2.2 → agent_dump-1.2.3}/src/shortcut.rs +0 -0
  112. {agent_dump-1.2.2 → agent_dump-1.2.3}/src/terminal/mod.rs +0 -0
  113. {agent_dump-1.2.2 → agent_dump-1.2.3}/src/terminal/reader.rs +0 -0
  114. {agent_dump-1.2.2 → agent_dump-1.2.3}/src/terminal/selector.rs +0 -0
  115. {agent_dump-1.2.2 → agent_dump-1.2.3}/src/terminal/tui.rs +0 -0
  116. {agent_dump-1.2.2 → agent_dump-1.2.3}/src/workflows/config.rs +0 -0
  117. {agent_dump-1.2.2 → agent_dump-1.2.3}/src/workflows/mod.rs +0 -0
  118. {agent_dump-1.2.2 → agent_dump-1.2.3}/src/workflows/read.rs +0 -0
@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
10
10
 
11
11
  [[package]]
12
12
  name = "agent-dump"
13
- version = "1.2.2"
13
+ version = "1.2.3"
14
14
  dependencies = [
15
15
  "agent-dump-core",
16
16
  "caseless",
@@ -837,9 +837,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
837
837
 
838
838
  [[package]]
839
839
  name = "jiff"
840
- version = "0.2.37"
840
+ version = "0.2.38"
841
841
  source = "registry+https://github.com/rust-lang/crates.io-index"
842
- checksum = "0ab1baf72f08796de0260609515130699b890ac25f30e610ad894bc5856cafdb"
842
+ checksum = "b2b005715dcbeb0089a3c0dab99f2ff1cc3b2525323552703d648585d342a383"
843
843
  dependencies = [
844
844
  "defmt",
845
845
  "jiff-core",
@@ -864,9 +864,9 @@ dependencies = [
864
864
 
865
865
  [[package]]
866
866
  name = "jiff-static"
867
- version = "0.2.37"
867
+ version = "0.2.38"
868
868
  source = "registry+https://github.com/rust-lang/crates.io-index"
869
- checksum = "378268a1116ad67ae6228701118ac9f491d78fda38a40a1f1a9e1348de6f7212"
869
+ checksum = "2cc9817253cf7c7ee4684451bd327e88d6f3658014e54a29198625590650695c"
870
870
  dependencies = [
871
871
  "jiff-core",
872
872
  "proc-macro2",
@@ -1859,9 +1859,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
1859
1859
 
1860
1860
  [[package]]
1861
1861
  name = "uuid"
1862
- version = "1.26.1"
1862
+ version = "1.28.0"
1863
1863
  source = "registry+https://github.com/rust-lang/crates.io-index"
1864
- checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce"
1864
+ checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606"
1865
1865
  dependencies = [
1866
1866
  "getrandom 0.4.3",
1867
1867
  "js-sys",
@@ -46,7 +46,7 @@ nix = { version = "0.31", default-features = false, features = ["fs", "user"] }
46
46
 
47
47
  [package]
48
48
  name = "agent-dump"
49
- version = "1.2.2"
49
+ version = "1.2.3"
50
50
  edition = "2024"
51
51
  rust-version = "1.90"
52
52
  publish = false
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-dump
3
- Version: 1.2.2
3
+ Version: 1.2.3
4
4
  Classifier: Environment :: Console
5
5
  Classifier: License :: OSI Approved :: MIT License
6
6
  Classifier: Programming Language :: Rust
@@ -462,7 +462,7 @@ File discovery failures include partially unreadable providers. Valid sessions r
462
462
 
463
463
  When some session reads or summaries fail, collection continues with successful sessions. The saved Markdown includes a fixed incomplete-report notice with failure and included-session counts; an entirely failed run still fails.
464
464
 
465
- Collect discovers candidate sessions without a creation-time cutoff and processes user/assistant visible text only within the requested local dates. A session spanning several days is split into separate daily units before summarization; session counts remain unique URI counts. Text without a reliable timestamp (including undated Kimi context and inferred Cursor times) belongs to no date and is excluded without being reported as a coverage gap. No session creation/update time is substituted. Query filters select candidates; limits apply to unique sessions after text-date filtering. Events, including long individual messages, are split into chunks of at most 3,200 Unicode characters including labels. Chunk summaries are merged in groups of at most eight; large final reports are rendered in separate source-preserving groups. Model inputs are limited to 64,000 characters per request. Oversized metadata or derived summaries fail explicitly; source text is never silently truncated. Partial reports list omitted session URIs. Full input coverage does not mean a summary preserves every detail.
465
+ Collect discovers candidate sessions without a creation-time cutoff and processes user/assistant visible text only within the requested local dates. A session spanning several days is split into separate daily units before summarization; session counts remain unique URI counts. Text without a reliable timestamp (including undated Kimi context and inferred Cursor times) belongs to no date and is excluded without being reported as a coverage gap. No session creation/update time is substituted. Query filters select candidates; limits apply to unique sessions after text-date filtering. Events, including long individual messages, are split into chunks of at most 3,200 Unicode characters including labels. Chunk summaries are merged in groups of at most eight; large final reports are rendered in separate source-preserving groups. Model inputs are limited to 64,000 characters per request. Oversized metadata or derived summaries fail explicitly; source text is never silently truncated. A final report or `--summary` reply that stops at the model output token limit is treated as a failed request, not saved as complete. Partial reports list omitted session URIs. Full input coverage does not mean a summary preserves every detail.
466
466
 
467
467
  PM summaries merge sessions only within the same date and known working directory. Sessions without a working directory retain separate attribution.
468
468
 
@@ -559,7 +559,7 @@ Pass `locations[].locator` from search unchanged to `--message`. Positions are o
559
559
 
560
560
  `--locate` requires `--search`. It returns messages containing any search term, respecting role filters, while all terms must still match the session. Title-only matches have empty locations; failed location reads have null locations and partial status. Search without --locate is unchanged.
561
561
 
562
- `--message` cannot combine with `--head` or `--summary`. Without `--format`, it prints context; `--json` writes a context envelope to stdout with kind=context and data containing uri, locator, total_messages, start/end and messages (position plus normalized message). Invalid or stale locators exit nonzero; JSON diagnostics go only to stderr.
562
+ `--message` cannot combine with `--head` or `--summary`. Without `--format`, it prints context; `--json` writes a context envelope to stdout with kind=context, status (partial when recoverable source diagnostics occur) and data containing uri, locator, total_messages, start/end and messages (position plus normalized message). Invalid or stale locators exit nonzero; JSON diagnostics go only to stderr.
563
563
 
564
564
  Use `--format json,markdown` to export the selected context. Files are named `<id>.messages-<start>-<end>.json` / `.md` under the Provider's output directory, separate from full-session exports. Both formats retain the canonical Session URI, the locator (revision plus target position), the absolute message range, and status. JSON retains normalized message records; Markdown renders their searchable text. Recoverable source diagnostics mark the file `partial` and are reported on stderr. Stale locators are rejected before file creation. Provider format restrictions still apply; raw/print file formats and `--json` with file-export options are rejected. `--output` requires an explicit `--format`; output-directory defaults follow full-session export rules.
565
565
 
@@ -438,7 +438,7 @@ File discovery failures include partially unreadable providers. Valid sessions r
438
438
 
439
439
  When some session reads or summaries fail, collection continues with successful sessions. The saved Markdown includes a fixed incomplete-report notice with failure and included-session counts; an entirely failed run still fails.
440
440
 
441
- Collect discovers candidate sessions without a creation-time cutoff and processes user/assistant visible text only within the requested local dates. A session spanning several days is split into separate daily units before summarization; session counts remain unique URI counts. Text without a reliable timestamp (including undated Kimi context and inferred Cursor times) belongs to no date and is excluded without being reported as a coverage gap. No session creation/update time is substituted. Query filters select candidates; limits apply to unique sessions after text-date filtering. Events, including long individual messages, are split into chunks of at most 3,200 Unicode characters including labels. Chunk summaries are merged in groups of at most eight; large final reports are rendered in separate source-preserving groups. Model inputs are limited to 64,000 characters per request. Oversized metadata or derived summaries fail explicitly; source text is never silently truncated. Partial reports list omitted session URIs. Full input coverage does not mean a summary preserves every detail.
441
+ Collect discovers candidate sessions without a creation-time cutoff and processes user/assistant visible text only within the requested local dates. A session spanning several days is split into separate daily units before summarization; session counts remain unique URI counts. Text without a reliable timestamp (including undated Kimi context and inferred Cursor times) belongs to no date and is excluded without being reported as a coverage gap. No session creation/update time is substituted. Query filters select candidates; limits apply to unique sessions after text-date filtering. Events, including long individual messages, are split into chunks of at most 3,200 Unicode characters including labels. Chunk summaries are merged in groups of at most eight; large final reports are rendered in separate source-preserving groups. Model inputs are limited to 64,000 characters per request. Oversized metadata or derived summaries fail explicitly; source text is never silently truncated. A final report or `--summary` reply that stops at the model output token limit is treated as a failed request, not saved as complete. Partial reports list omitted session URIs. Full input coverage does not mean a summary preserves every detail.
442
442
 
443
443
  PM summaries merge sessions only within the same date and known working directory. Sessions without a working directory retain separate attribution.
444
444
 
@@ -535,7 +535,7 @@ Pass `locations[].locator` from search unchanged to `--message`. Positions are o
535
535
 
536
536
  `--locate` requires `--search`. It returns messages containing any search term, respecting role filters, while all terms must still match the session. Title-only matches have empty locations; failed location reads have null locations and partial status. Search without --locate is unchanged.
537
537
 
538
- `--message` cannot combine with `--head` or `--summary`. Without `--format`, it prints context; `--json` writes a context envelope to stdout with kind=context and data containing uri, locator, total_messages, start/end and messages (position plus normalized message). Invalid or stale locators exit nonzero; JSON diagnostics go only to stderr.
538
+ `--message` cannot combine with `--head` or `--summary`. Without `--format`, it prints context; `--json` writes a context envelope to stdout with kind=context, status (partial when recoverable source diagnostics occur) and data containing uri, locator, total_messages, start/end and messages (position plus normalized message). Invalid or stale locators exit nonzero; JSON diagnostics go only to stderr.
539
539
 
540
540
  Use `--format json,markdown` to export the selected context. Files are named `<id>.messages-<start>-<end>.json` / `.md` under the Provider's output directory, separate from full-session exports. Both formats retain the canonical Session URI, the locator (revision plus target position), the absolute message range, and status. JSON retains normalized message records; Markdown renders their searchable text. Recoverable source diagnostics mark the file `partial` and are reported on stderr. Stale locators are rejected before file creation. Provider format restrictions still apply; raw/print file formats and `--json` with file-export options are rejected. `--output` requires an explicit `--format`; output-directory defaults follow full-session export rules.
541
541
 
@@ -39,6 +39,7 @@ pub fn syntax(text: &str, original: &serde_json::Error) -> Error {
39
39
  let mut parser = Parser {
40
40
  chars: text.chars().collect(),
41
41
  index: 0,
42
+ depth: 0,
42
43
  };
43
44
  let failure = if text.starts_with('\u{feff}') {
44
45
  Some(("Unexpected UTF-8 BOM (decode using utf-8-sig)", 0))
@@ -49,7 +50,9 @@ pub fn syntax(text: &str, original: &serde_json::Error) -> Error {
49
50
  .then_some(("Extra data", parser.index))
50
51
  })
51
52
  };
52
- let message = if let Some((message, position)) = failure {
53
+ let message = if let Some((message, position)) =
54
+ failure.filter(|(message, _)| *message != TOO_DEEP)
55
+ {
53
56
  let before = &parser.chars[..position];
54
57
  let line = before.iter().filter(|c| **c == '\n').count() + 1;
55
58
  let column = position
@@ -70,9 +73,14 @@ pub fn syntax(text: &str, original: &serde_json::Error) -> Error {
70
73
 
71
74
  type Failure = (&'static str, usize);
72
75
 
76
+ // Matches serde_json's recursion limit so deep input cannot overflow the stack.
77
+ const MAX_DEPTH: usize = 128;
78
+ const TOO_DEEP: &str = "recursion limit exceeded";
79
+
73
80
  struct Parser {
74
81
  chars: Vec<char>,
75
82
  index: usize,
83
+ depth: usize,
76
84
  }
77
85
 
78
86
  impl Parser {
@@ -107,8 +115,19 @@ impl Parser {
107
115
  self.whitespace();
108
116
  match self.chars.get(self.index).copied() {
109
117
  Some('"') => self.string(),
110
- Some('{') => self.object(),
111
- Some('[') => self.array(),
118
+ Some('{' | '[') if self.depth == MAX_DEPTH => {
119
+ Err((TOO_DEEP, self.index))
120
+ }
121
+ Some(open @ ('{' | '[')) => {
122
+ self.depth += 1;
123
+ let result = if open == '{' {
124
+ self.object()
125
+ } else {
126
+ self.array()
127
+ };
128
+ self.depth -= 1;
129
+ result
130
+ }
112
131
  _ => {
113
132
  for word in
114
133
  ["null", "true", "false", "NaN", "Infinity", "-Infinity"]
@@ -156,3 +156,15 @@ impl serde_json::ser::Formatter for Formatter {
156
156
  writer.write_all(output.as_bytes())
157
157
  }
158
158
  }
159
+
160
+ #[cfg(test)]
161
+ mod tests {
162
+ use super::*;
163
+
164
+ #[test]
165
+ fn deep_nesting_reports_error_without_overflow() {
166
+ let error = from_str(&"[".repeat(100_000)).unwrap_err();
167
+
168
+ assert!(error.to_string().starts_with("recursion limit exceeded"));
169
+ }
170
+ }
@@ -59,25 +59,14 @@ fn filename(id: &str) -> crate::Result<String> {
59
59
  }
60
60
  }
61
61
 
62
- fn absolute_existing_ancestor(path: &Path) -> crate::Result<PathBuf> {
63
- if path.exists() {
64
- return Ok(path.canonicalize()?);
65
- }
66
- let parent = path.parent().ok_or("Cannot resolve output path")?;
67
- Ok(absolute_existing_ancestor(parent)?
68
- .join(path.file_name().ok_or("Invalid output path")?))
69
- }
70
-
71
62
  fn write(
72
63
  output: &Path,
73
64
  session_id: &str,
74
65
  suffix: &str,
75
- source_root: &Path,
76
66
  contents: impl FnOnce(&mut fs::File) -> crate::Result<()>,
77
67
  ) -> crate::Result<PathBuf> {
78
68
  let directory = std::path::absolute(output)?;
79
- let resolved = absolute_existing_ancestor(&directory)?;
80
- if resolved.starts_with(source_root.canonicalize()?) {
69
+ if crate::providers::registry::inside_source(&directory)? {
81
70
  return Err(
82
71
  "Export output must be outside the Provider source directory"
83
72
  .into(),
@@ -114,10 +103,9 @@ pub fn json(
114
103
  session_id: &str,
115
104
  data: &impl serde::Serialize,
116
105
  output: &Path,
117
- source_root: &Path,
118
106
  suffix: &str,
119
107
  ) -> crate::Result<PathBuf> {
120
- write(output, session_id, suffix, source_root, |file| {
108
+ write(output, session_id, suffix, |file| {
121
109
  let mut writer = io::BufWriter::new(file);
122
110
  data.serialize(&mut serde_json::Serializer::with_formatter(
123
111
  &mut writer,
@@ -132,19 +120,17 @@ pub fn markdown(
132
120
  session_id: &str,
133
121
  text: &str,
134
122
  output: &Path,
135
- source_root: &Path,
136
123
  ) -> crate::Result<PathBuf> {
137
- markdown_with_suffix(session_id, text, output, source_root, ".md")
124
+ markdown_with_suffix(session_id, text, output, ".md")
138
125
  }
139
126
 
140
127
  fn markdown_with_suffix(
141
128
  session_id: &str,
142
129
  text: &str,
143
130
  output: &Path,
144
- source_root: &Path,
145
131
  suffix: &str,
146
132
  ) -> crate::Result<PathBuf> {
147
- write(output, session_id, suffix, source_root, |file| {
133
+ write(output, session_id, suffix, |file| {
148
134
  if cfg!(windows) {
149
135
  file.write_all(text.replace('\n', "\r\n").as_bytes())?;
150
136
  } else {
@@ -158,9 +144,8 @@ pub fn raw(
158
144
  session_id: &str,
159
145
  source: &Path,
160
146
  output: &Path,
161
- source_root: &Path,
162
147
  ) -> crate::Result<PathBuf> {
163
- write(output, session_id, ".raw.jsonl", source_root, |file| {
148
+ write(output, session_id, ".raw.jsonl", |file| {
164
149
  io::copy(&mut fs::File::open(source)?, file)?;
165
150
  Ok(())
166
151
  })
@@ -179,7 +164,6 @@ impl Excerpt<'_> {
179
164
  &self,
180
165
  format: crate::output::formats::OutputFormat,
181
166
  output: &Path,
182
- source_root: &Path,
183
167
  ) -> crate::Result<PathBuf> {
184
168
  use crate::output::formats::OutputFormat;
185
169
  let suffix =
@@ -197,7 +181,6 @@ impl Excerpt<'_> {
197
181
  &self.data.id,
198
182
  &document,
199
183
  output,
200
- source_root,
201
184
  &format!("{suffix}.json"),
202
185
  )
203
186
  }
@@ -211,7 +194,6 @@ impl Excerpt<'_> {
211
194
  self.incomplete,
212
195
  ),
213
196
  output,
214
- source_root,
215
197
  &format!("{suffix}.md"),
216
198
  ),
217
199
  OutputFormat::Raw | OutputFormat::Print => {
@@ -239,17 +221,13 @@ impl SessionExport<'_> {
239
221
  use crate::providers::contract::RawExport;
240
222
  match format {
241
223
  OutputFormat::Raw => match self.raw.as_ref().unwrap() {
242
- RawExport::File(source) => raw(
243
- &self.session.id,
244
- source,
245
- output,
246
- self.provider.source_root(),
247
- ),
224
+ RawExport::File(source) => {
225
+ raw(&self.session.id, source, output)
226
+ }
248
227
  RawExport::Session => json(
249
228
  &self.session.id,
250
229
  self.data.unwrap(),
251
230
  output,
252
- self.provider.source_root(),
253
231
  ".raw.json",
254
232
  ),
255
233
  },
@@ -261,13 +239,7 @@ impl SessionExport<'_> {
261
239
  {
262
240
  object.insert("summary".into(), summary.into());
263
241
  }
264
- json(
265
- &self.session.id,
266
- &payload,
267
- output,
268
- self.provider.source_root(),
269
- ".json",
270
- )
242
+ json(&self.session.id, &payload, output, ".json")
271
243
  }
272
244
  OutputFormat::Markdown => markdown(
273
245
  &self.session.id,
@@ -276,7 +248,6 @@ impl SessionExport<'_> {
276
248
  self.data.unwrap(),
277
249
  ),
278
250
  output,
279
- self.provider.source_root(),
280
251
  ),
281
252
  OutputFormat::Print => unreachable!(),
282
253
  }
@@ -1,10 +1,12 @@
1
1
  pub(super) mod transcript;
2
2
 
3
- use crate::compat::value::{field, text, truthy};
3
+ use crate::compat::value::{field, text};
4
4
  use crate::providers::contract::{
5
5
  DiagnosticSink, Provider, RecoverableDiagnostic,
6
6
  };
7
- use crate::providers::files::{self as file_sessions, SourceRoots};
7
+ use crate::providers::files::{
8
+ self as file_sessions, SourceFiles, SourceRoots,
9
+ };
8
10
  use crate::providers::jsonl;
9
11
  use crate::providers::title::{basename, normalize_title};
10
12
  use crate::session::timestamp::Timestamp;
@@ -51,7 +53,7 @@ impl Claude {
51
53
  })
52
54
  }
53
55
 
54
- fn files(&mut self) -> crate::Result<Vec<PathBuf>> {
56
+ fn files(&mut self) -> crate::Result<SourceFiles> {
55
57
  let files = self.roots.files(Some(2), |path, base| {
56
58
  path.extension().is_some_and(|ext| ext == "jsonl")
57
59
  && path.parent() != Some(base)
@@ -112,11 +114,14 @@ impl Claude {
112
114
  let mut skipped = 0;
113
115
  for entry in value["entries"].as_array().into_iter().flatten() {
114
116
  let id = text(&entry["sessionId"]);
115
- if id.trim().is_empty() {
117
+ let summary = &entry["summary"];
118
+ if id.trim().is_empty()
119
+ || !(summary.is_null() || summary.is_string())
120
+ {
116
121
  skipped += 1;
117
122
  continue;
118
123
  }
119
- titles.insert(id.to_owned(), entry["summary"].clone());
124
+ titles.insert(id.to_owned(), summary.clone());
120
125
  }
121
126
  if skipped > 0 {
122
127
  diagnostics(RecoverableDiagnostic::TitleCacheEntriesSkipped {
@@ -149,21 +154,11 @@ impl Claude {
149
154
  .into_owned();
150
155
  let created_at = parse_timestamp(text(&header["timestamp"]))
151
156
  .unwrap_or(Timestamp::try_from(path.metadata()?.modified()?)?);
152
- let explicit = match self
157
+ let explicit = self
153
158
  .titles(project, diagnostics)?
154
159
  .get(&id)
155
- .filter(|value| truthy(value))
156
- {
157
- Some(Value::String(title)) => normalize_title(title),
158
- Some(value) => {
159
- let kind = crate::compat::value::type_name(value);
160
- return Err(format!(
161
- "expected string or bytes-like object, got '{kind}'"
162
- )
163
- .into());
164
- }
165
- None => None,
166
- };
160
+ .and_then(Value::as_str)
161
+ .and_then(normalize_title);
167
162
  let first_user = scan
168
163
  .records
169
164
  .iter()
@@ -244,7 +239,7 @@ impl Provider for Claude {
244
239
  ) -> crate::Result<crate::providers::contract::Discovery> {
245
240
  self.titles.clear();
246
241
  file_sessions::discover(
247
- &self.files()?,
242
+ self.files()?,
248
243
  days,
249
244
  true,
250
245
  Self::scan,
@@ -261,7 +256,7 @@ impl Provider for Claude {
261
256
  let files = self.files()?;
262
257
  file_sessions::find(
263
258
  self.roots.base.clone().as_deref(),
264
- &files,
259
+ files,
265
260
  id,
266
261
  |path| path.file_stem().is_some_and(|name| name == id),
267
262
  |path| self.parse(path, Self::scan(path)?, diagnostics),
@@ -296,6 +291,10 @@ impl Provider for Claude {
296
291
  fn source_root(&self) -> &Path {
297
292
  self.roots.owned()
298
293
  }
294
+
295
+ fn select_source(&mut self) -> crate::Result<Option<&Path>> {
296
+ self.roots.select()
297
+ }
299
298
  }
300
299
 
301
300
  #[cfg(test)]
@@ -255,12 +255,16 @@ impl Decoder {
255
255
  continue;
256
256
  }
257
257
  let parts = output_parts(&item["content"], timestamp);
258
+ let mut updates = updates.clone();
259
+ if item["is_error"] == true {
260
+ updates.insert("status".into(), "error".into());
261
+ }
258
262
  if let Some(tool) = backfill(
259
263
  &mut self.messages,
260
264
  &self.pending,
261
265
  &call_id,
262
266
  &parts,
263
- Some(updates.clone()),
267
+ Some(updates),
264
268
  )? {
265
269
  if !parts.is_empty() {
266
270
  tool.state
@@ -79,7 +79,10 @@ pub fn subagent_notification(
79
79
  if !payload.is_object() {
80
80
  return None;
81
81
  }
82
- let agent_id = field(&payload, "agent_id").trim().to_owned();
82
+ let agent_id = match field(&payload, "agent_id").trim() {
83
+ "" => field(&payload, "agent_path").trim().to_owned(),
84
+ agent_id => agent_id.to_owned(),
85
+ };
83
86
  let nickname = field(&payload, "nickname").trim().to_owned();
84
87
  let completed = if payload["status"].is_object() {
85
88
  field(&payload["status"], "completed").trim().to_owned()
@@ -6,7 +6,9 @@ use crate::compat::value::text;
6
6
  use crate::providers::contract::{
7
7
  DiagnosticSink, Provider, RecoverableDiagnostic,
8
8
  };
9
- use crate::providers::files::{self as file_sessions, SourceRoots};
9
+ use crate::providers::files::{
10
+ self as file_sessions, SourceFiles, SourceRoots,
11
+ };
10
12
  use crate::providers::jsonl;
11
13
  use crate::providers::title::{basename, normalize_title};
12
14
  use crate::session::timestamp::Timestamp;
@@ -87,7 +89,7 @@ impl Codex {
87
89
  .cloned())
88
90
  }
89
91
 
90
- fn files(&mut self) -> crate::Result<Vec<PathBuf>> {
92
+ fn files(&mut self) -> crate::Result<SourceFiles> {
91
93
  self.roots.files(None, |path, _| {
92
94
  path.extension().is_some_and(|ext| ext == "jsonl")
93
95
  })
@@ -219,7 +221,7 @@ impl Provider for Codex {
219
221
  ) -> crate::Result<crate::providers::contract::Discovery> {
220
222
  self.titles = None;
221
223
  file_sessions::discover(
222
- &self.files()?,
224
+ self.files()?,
223
225
  days,
224
226
  true,
225
227
  Self::scan,
@@ -237,7 +239,7 @@ impl Provider for Codex {
237
239
  let files = self.files()?;
238
240
  file_sessions::find(
239
241
  self.roots.base.clone().as_deref(),
240
- &files,
242
+ files,
241
243
  id,
242
244
  |path| {
243
245
  path.file_name().is_some_and(|name| {
@@ -280,6 +282,10 @@ impl Provider for Codex {
280
282
  fn source_root(&self) -> &Path {
281
283
  self.roots.owned()
282
284
  }
285
+
286
+ fn select_source(&mut self) -> crate::Result<Option<&Path>> {
287
+ self.roots.select()
288
+ }
283
289
  }
284
290
 
285
291
  #[cfg(test)]
@@ -154,13 +154,17 @@ impl Decoder {
154
154
  })
155
155
  .collect::<Vec<_>>()
156
156
  .join("\n\n");
157
+ let is_notification = user_text.starts_with("<subagent_notification>")
158
+ && user_text.ends_with("</subagent_notification>");
157
159
  if self.pending_plan.is_some()
158
160
  && role == "user"
159
161
  && !user_text.is_empty()
162
+ && !is_notification
160
163
  {
161
164
  if user_text
162
165
  .trim_start()
163
166
  .starts_with("PLEASE IMPLEMENT THIS PLAN")
167
+ || user_text == "Implement the plan."
164
168
  {
165
169
  self.finish_plan("success", None);
166
170
  } else {
@@ -343,10 +347,13 @@ pub fn read(
343
347
  ) -> crate::Result<SessionData> {
344
348
  let mut decoder = Decoder::default();
345
349
  let mut stats = Stats::default();
350
+ let mut usage = Value::Null;
346
351
  jsonl::scan(&session.source_path, diagnostics, |record, diagnostics| {
347
- let result = decoder.record(&record, zh).and_then(|()| {
348
- let usage = &record["payload"]["info"]["total_token_usage"];
349
- stats.add_tokens(&usage["input_tokens"], &usage["output_tokens"])
352
+ let result = decoder.record(&record, zh).map(|()| {
353
+ let snapshot = &record["payload"]["info"]["total_token_usage"];
354
+ if snapshot.is_object() {
355
+ usage = snapshot.clone();
356
+ }
350
357
  });
351
358
  if let Err(error) = result {
352
359
  diagnostics(RecoverableDiagnostic::MessageConvertFailed(
@@ -356,6 +363,7 @@ pub fn read(
356
363
  Ok(())
357
364
  })?;
358
365
  decoder.finish_plan("fail", None);
366
+ stats.add_tokens(&usage["input_tokens"], &usage["output_tokens"])?;
359
367
  stats.message_count = decoder.messages.len();
360
368
  let mut data = session.payload(decoder.messages, stats);
361
369
  data.directory = session.source_metadata["cwd"].clone();
@@ -98,6 +98,7 @@ pub trait Provider: Send + Sync {
98
98
  diagnostics: &mut DiagnosticSink<'_>,
99
99
  ) -> crate::Result<SessionData>;
100
100
  fn source_root(&self) -> &Path;
101
+ fn select_source(&mut self) -> crate::Result<Option<&Path>>;
101
102
  fn search_roots(&self) -> crate::Result<Vec<(&'static str, PathBuf)>>;
102
103
 
103
104
  fn change_sources(&self, session: &Session) -> Vec<PathBuf> {
@@ -163,6 +163,11 @@ impl Provider for Cursor {
163
163
  fn source_root(&self) -> &Path {
164
164
  self.database.parent().unwrap()
165
165
  }
166
+
167
+ fn select_source(&mut self) -> crate::Result<Option<&Path>> {
168
+ self.database = (self.resolve_database)()?;
169
+ Ok(self.database.exists().then(|| self.source_root()))
170
+ }
166
171
  fn supports_format(&self, format: OutputFormat) -> bool {
167
172
  matches!(format, OutputFormat::Json | OutputFormat::Print)
168
173
  }
@@ -220,6 +220,10 @@ impl Provider for Desktop {
220
220
  self.database.parent().unwrap()
221
221
  }
222
222
 
223
+ fn select_source(&mut self) -> crate::Result<Option<&Path>> {
224
+ Ok(self.select_database()?.then(|| self.source_root()))
225
+ }
226
+
223
227
  fn supports_format(&self, format: OutputFormat) -> bool {
224
228
  format != OutputFormat::Raw
225
229
  }
@@ -408,3 +408,36 @@ fn migration_required_after_lookup_uses_localized_read_failure() {
408
408
  }
409
409
  assert_eq!(std::fs::read(&session.source_path).unwrap(), before);
410
410
  }
411
+
412
+ #[test]
413
+ fn minimax_out_of_scope_sessions_skip_storage_checks() {
414
+ let (_directory, mut provider, session, _) = fixture(Kind::MiniMax);
415
+ Connection::open(&session.source_path)
416
+ .unwrap()
417
+ .execute_batch(
418
+ "INSERT INTO local_runtime_sessions VALUES ('hidden-v2', 2, 'pi-agent', 'hidden', 'conversation', 0, NULL, NULL, NULL, 1768478400000, 1768478400000, '{}');
419
+ INSERT INTO local_runtime_sessions VALUES ('cron-pending', 3, 'pi-agent', 'visible', 'cron', 0, NULL, NULL, NULL, 1768478400000, 1768478400000, '{}');
420
+ INSERT INTO local_runtime_messages VALUES ('cron-pending', '[{}]');
421
+ INSERT INTO local_runtime_sessions VALUES ('opencode-v1', 1, 'opencode', 'visible', 'conversation', 0, NULL, NULL, NULL, 1768478400000, 1768478400000, '{}');",
422
+ )
423
+ .unwrap();
424
+ let found = provider.discover(None, &mut |_| Ok(())).unwrap();
425
+ assert!(found.failures.is_empty());
426
+ assert_eq!(
427
+ found
428
+ .sessions
429
+ .iter()
430
+ .map(|s| s.id.as_str())
431
+ .collect::<Vec<_>>(),
432
+ ["kept"]
433
+ );
434
+ for id in ["hidden-v2", "cron-pending", "opencode-v1"] {
435
+ assert!(
436
+ provider
437
+ .find(id, &mut |_| Ok(()))
438
+ .unwrap()
439
+ .session
440
+ .is_none()
441
+ );
442
+ }
443
+ }