@henryqw/pi-session-recall 2.0.0 → 2.1.1

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.
package/README.md CHANGED
@@ -4,7 +4,7 @@ Find decisions and context in past Pi sessions through a local FTS5 index.
4
4
 
5
5
  Saved transcripts are not injected on every turn. The active tool registration still adds standing prompt cost through its schema, descriptions, and guideline. Returned content enters active model context.
6
6
 
7
- The bundled `pi-session-pattern-miner` skill finds repeated work that may deserve automation.
7
+ The bundled `pi-session-pattern-miner` skill prepares one bounded sample. The model then finds repeated work that may deserve automation.
8
8
 
9
9
  ## Install
10
10
 
@@ -22,7 +22,19 @@ Start discovery with a distinctive query:
22
22
 
23
23
  `session_search` returns ranked sessions. The top result includes nearby messages and session bookends.
24
24
 
25
- Use IDs from that result to ask for more context:
25
+ Prepare a repository-scoped pattern-mining sample with one call:
26
+
27
+ ```json
28
+ {
29
+ "operation": "prepare-pattern-miner",
30
+ "scope": "repository",
31
+ "limit": 10
32
+ }
33
+ ```
34
+
35
+ Use `scope:"all"` for cross-repository work. It still returns the corpus when repository inventory is unavailable.
36
+
37
+ Use IDs from a discovery result to ask for more context:
26
38
 
27
39
  ```json
28
40
  {
@@ -36,13 +48,14 @@ The follow-up returns up to ten messages before and after that anchor on the sel
36
48
 
37
49
  | Surface | Type | Purpose |
38
50
  | --- | --- | --- |
39
- | `session_search` | tool | Search past sessions or inspect one. |
51
+ | `session_search` | tool | Search, inspect, or prepare a bounded mining corpus from past sessions. |
40
52
  | `pi-session-pattern-miner` | skill | Find repeated work and choose the smallest useful automation. |
41
53
 
42
54
  BM25 is a text-ranking method. Hydrated results include messages read from saved session files.
43
55
 
44
56
  | Mode | Call | Result |
45
57
  | --- | --- | --- |
58
+ | Pattern preparation | `operation:"prepare-pattern-miner"` + `scope:"repository"` or `scope:"all"` | Up to ten recent lineage-unique sessions plus repository inventory. The default limit is 10. Repository scope includes exact and descendant `cwd` values, filters before the limit, and excludes the current session file. |
46
59
  | Discovery | `query` | BM25-ranked top sessions. Adaptive retrieval uses user and assistant text for windows, bookends, anchors, and counts. It omits tool-result messages and sets `toolResultsOmitted:true` when it removes one. Lower hits still include their indexed anchor. Use `detail:"full"` to hydrate every hit with tool-result messages included. |
47
60
  | Scroll | `sessionId` + `aroundMessageId` | Raw message roles, including tool results, within ±`window` ([1,20]) of the anchor. Re-anchor on the last or first message ID to scroll. Across forks, pass the previous response's `branchTip`; `aroundMessageId` only centers the window and must lie on that branch. |
48
61
  | Read | `sessionId` | Raw message roles, including tool results, from the session. Large sessions return head 20 + tail 10. Oversized content is bounded to 50k characters and marked with `contentTruncated`. |
@@ -52,10 +65,41 @@ In the interactive TUI, the collapsed tool block shows the last five visual line
52
65
 
53
66
  ### Skills
54
67
 
55
- Run `/skill:pi-session-pattern-miner` to find repeated workflows in past sessions. It requires evidence from two independent sessions and checks for existing automation. It prefers a fixed script when model judgment is not needed.
68
+ Run `/skill:pi-session-pattern-miner` to find repeated workflows in past sessions. The skill makes one preparation call before interpretation.
69
+
70
+ It treats one lineage as one source. It requires two independent examples before recommending automation. A requested topic gets a focused confirmation search even when the prepared sample does not contain it.
71
+
72
+ After clustering, the skill always checks current candidate-relevant package manifests, scripts, skills, and instructions. It abstains if it cannot check them safely.
73
+
74
+ ### Repository inventory
75
+
76
+ Repository inventory contains discovery hints. It is never proof that a file owns a workflow. It does not verify the current worktree.
77
+
78
+ Package scripts, executable paths, and instruction paths come from stage-0 entries in the Git index. Package content comes from indexed blobs. Git reads the objects locally in one check batch and one content batch. Lazy object fetching and replacement refs are disabled. Inventory never opens working-tree package paths.
79
+
80
+ Executables need index mode `100755`. Instructions need a recognized name and a regular-file index mode.
81
+
82
+ Skills come from Pi's effective command registry. Their canonical source paths must stay inside the repository.
83
+
84
+ Available inventory includes this provenance:
85
+
86
+ ```json
87
+ {
88
+ "packageScripts": "git-index",
89
+ "executableScripts": "git-index",
90
+ "agentInstructions": "git-index",
91
+ "skills": "pi-effective-registry"
92
+ }
93
+ ```
94
+
95
+ It also sets `worktreeVerified:false`. Staged adds, changes, and deletes affect the snapshot. Unstaged changes, deletions, mode changes, symlinks, and untracked files do not.
96
+
97
+ Inspect the current candidate files before assigning ownership. Abstain if targeted current-file checks cannot be done safely.
56
98
 
57
99
  ## Flow
58
100
 
101
+ ![Flowchart showing session_search routing: preparation, discovery, and browse use the index; scroll and read retrieve saved transcripts directly.](./docs/session-search-routing.svg)
102
+
59
103
  Search makes no model calls.
60
104
 
61
105
  ### Query and index
@@ -70,12 +114,16 @@ Search makes no model calls.
70
114
 
71
115
  Hits inside the current session's live context are suppressed. Compacted-away or inactive-branch history stays discoverable. Forked sessions collapse into their parent when both match.
72
116
 
73
- Before browse or discovery, the extension lazily syncs the index from the session tree.
117
+ Before browse, discovery, or pattern preparation, the extension lazily syncs the index from the session tree.
118
+
119
+ Pattern preparation reports `sync.walkComplete`, `sync.backlogRemaining`, and `sync.complete`. `sync.complete` is true only after a complete walk with no backlog.
74
120
 
75
121
  ### Retrieval safety
76
122
 
77
123
  Adaptive discovery leaves tool-result messages out of returned context. Use `detail:"full"`, READ, or SCROLL when you explicitly need them.
78
124
 
125
+ Pattern preparation returns only non-empty user and assistant text. It never returns thinking blocks or tool-result content. Each session keeps citation and lineage metadata even when its hydration fails.
126
+
79
127
  Historical tool output may contain secrets or other sensitive data. Raw retrieval places that output in active model context.
80
128
 
81
129
  ## State and storage
@@ -91,7 +139,7 @@ The index and transcript reads stay local. Transcripts are read in place. Return
91
139
  Pin the previous release:
92
140
 
93
141
  ```bash
94
- pi install npm:@henryqw/pi-session-recall@1.0.3
142
+ pi install npm:@henryqw/pi-session-recall@2.0.0
95
143
  ```
96
144
 
97
145
  No index migration or cleanup is needed.
@@ -109,3 +157,19 @@ Session directories whose encoded path starts with `--tmp-` or `--private-tmp-`
109
157
  Session files over 32 MiB are excluded from indexing and hydration. Discovery cannot newly find them.
110
158
 
111
159
  READ and SCROLL return an explicit size error. A stale discovery hit from before a file grew returns metadata with empty messages and that error.
160
+
161
+ Pattern preparation runs one sync pass. A positive backlog or incomplete walk limits the sample and sets `sync.complete:false`. A total sync or required repository-inventory failure returns an explicit tool error.
162
+
163
+ Repository scope fails outside Git or when repository inventory fails. All scope still returns its corpus in both cases.
164
+
165
+ Outside Git, all scope sets `inventory.available:false` with `reason:"not-a-git-repository"`. On a repository inventory error, it uses `reason:"inventory-failed"`. Cancellation always aborts the call instead of returning unavailable inventory.
166
+
167
+ Preparation rejects `query`, session cursors, `window`, or `detail` in the same call. It also rejects `scope` without the operation.
168
+
169
+ Preparation output stays within 50,000 serialized characters. Inventory uses at most 10,000 characters. Its `omittedCounts` report only omitted collection entries, and `inventory.truncated` reports those omissions.
170
+
171
+ Inventory fails if the Git root output exceeds 4 KiB or the raw index listing exceeds 8 MiB. It also fails on malformed index data, invalid UTF-8, conflict entries, unsupported package modes, Git errors, unexpected Git stderr, or bounded stream overflow.
172
+
173
+ Inventory accepts at most 512 package manifests. Each indexed manifest can be at most 1 MiB, and their declared sizes can total at most 16 MiB. One bounded batch checks all blob sizes before one bounded batch reads their content. Blob order, type, size, UTF-8, and exact output framing are checked before manifest data is parsed.
174
+
175
+ Session and top-level `contentTruncated` report transcript budget trimming only. Session `truncated` reports omitted middle messages.
@@ -0,0 +1,138 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>session_search routing</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&amp;family=Geist:wght@400;500;600&amp;family=Geist+Mono:wght@400;500;600&amp;display=swap" rel="stylesheet">
8
+ <style>
9
+ html, body { min-height: 100%; margin: 0; background: #f0eee9; }
10
+ body { display: grid; place-items: center; }
11
+ figure { width: min(100%, 1280px); margin: 0; }
12
+ svg { display: block; width: 100%; height: auto; }
13
+ </style>
14
+ </head>
15
+ <body>
16
+ <figure>
17
+ <svg width="1280" height="720" viewBox="0 0 1280 720" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="session-search-routing-title session-search-routing-desc">
18
+ <title id="session-search-routing-title">session_search routing</title>
19
+ <desc id="session-search-routing-desc">Flowchart routing session_search requests between indexed preparation, discovery, and browse calls and direct transcript scroll and read calls.</desc>
20
+ <defs>
21
+ <style>
22
+ .eyebrow { fill: #6a7282; font: 500 8px 'Geist Mono', monospace; letter-spacing: .16em; }
23
+ .diagram-title { fill: #101828; font: 400 28px 'Instrument Serif', serif; }
24
+ .zone-label { fill: #4c5665; font: 500 8px 'Geist Mono', monospace; letter-spacing: .12em; }
25
+ .zone-note { fill: #6a7282; font: 400 8px 'Geist Mono', monospace; letter-spacing: .04em; }
26
+ .node-name { fill: #101828; font: 600 12px 'Geist', sans-serif; text-anchor: middle; }
27
+ .technical-name { fill: #101828; font: 500 12px 'Geist Mono', monospace; text-anchor: middle; }
28
+ .node-sub { fill: #4c5665; font: 400 8px 'Geist Mono', monospace; text-anchor: middle; }
29
+ .edge { fill: none; stroke: #4c5665; stroke-width: 1.2; marker-end: url(#session-search-routing-arrow); }
30
+ .edge-label { fill: #4c5665; font: 500 8px 'Geist Mono', monospace; text-anchor: middle; letter-spacing: .08em; }
31
+ </style>
32
+ <marker id="session-search-routing-arrow" markerWidth="8" markerHeight="8" refX="8" refY="4" orient="auto">
33
+ <polygon points="0 0,8 4,0 8" fill="#4c5665"/>
34
+ </marker>
35
+ <marker id="session-search-routing-arrow-accent" markerWidth="8" markerHeight="8" refX="8" refY="4" orient="auto">
36
+ <polygon points="0 0,8 4,0 8" fill="#1d4ed8"/>
37
+ </marker>
38
+ <marker id="session-search-routing-arrow-link" markerWidth="8" markerHeight="8" refX="8" refY="4" orient="auto">
39
+ <polygon points="0 0,8 4,0 8" fill="#1d4ed8"/>
40
+ </marker>
41
+ </defs>
42
+
43
+ <rect width="1280" height="720" fill="#f0eee9"/>
44
+
45
+ <text class="eyebrow" x="40" y="48">PI-SESSION-RECALL · FLOWCHART</text>
46
+ <text class="diagram-title" x="40" y="88">Five routes through session_search</text>
47
+
48
+ <!-- Routing zones are backgrounds, not primary nodes. -->
49
+ <rect x="40" y="352" width="752" height="320" rx="8" fill="#e6e4de" stroke="rgba(16,24,40,0.14)" stroke-width="1"/>
50
+ <text class="zone-label" x="56" y="376">INDEX-BACKED CALLS · LAZY SYNC</text>
51
+ <text class="zone-note" x="56" y="392">PREPARE · DISCOVERY · BROWSE</text>
52
+
53
+ <rect x="816" y="352" width="424" height="320" rx="8" fill="#e6e4de" stroke="rgba(16,24,40,0.14)" stroke-width="1"/>
54
+ <text class="zone-label" x="832" y="376">DIRECT TRANSCRIPT READS</text>
55
+ <text class="zone-note" x="832" y="392">RAW ROLES · TOOL RESULTS</text>
56
+
57
+ <!-- Connectors: all routes are painted before their nodes. -->
58
+ <path class="edge" d="M640 180 V212"/>
59
+ <path class="edge" d="M532 264 H320 Q312 264 312 272 V388 Q312 396 304 396 H184 Q176 396 176 404 V412"/>
60
+ <path class="edge" d="M640 316 V336 Q640 344 632 344 H432 Q424 344 424 352 V392"/>
61
+ <path class="edge" d="M748 264 H792 Q800 264 800 272 V328 Q800 336 808 336 H1016 Q1024 336 1024 344 V392"/>
62
+ <path class="edge" d="M536 448 H576"/>
63
+ <path class="edge" d="M424 504 V544"/>
64
+ <path class="edge" d="M1136 448 V536"/>
65
+ <path class="edge" d="M912 448 H896 Q888 448 888 456 V520 Q888 528 896 528 H912 Q920 528 920 536"/>
66
+
67
+ <!-- Branch labels stay 8px clear of their connectors. -->
68
+ <rect x="384" y="244" width="72" height="12" rx="2" fill="#f0eee9"/>
69
+ <text class="edge-label" x="420" y="252">PREPARE</text>
70
+
71
+ <rect x="488" y="324" width="112" height="12" rx="2" fill="#f0eee9"/>
72
+ <text class="edge-label" x="544" y="332">SEARCH / BROWSE</text>
73
+
74
+ <rect x="808" y="288" width="56" height="12" rx="2" fill="#f0eee9"/>
75
+ <text class="edge-label" x="836" y="296">INSPECT</text>
76
+
77
+ <rect x="544" y="428" width="24" height="12" rx="2" fill="#f0eee9"/>
78
+ <text class="edge-label" x="556" y="436">YES</text>
79
+
80
+ <rect x="432" y="516" width="24" height="12" rx="2" fill="#f0eee9"/>
81
+ <text class="edge-label" x="444" y="524">NO</text>
82
+
83
+ <rect x="1144" y="480" width="24" height="12" rx="2" fill="#f0eee9"/>
84
+ <text class="edge-label" x="1156" y="488">YES</text>
85
+
86
+ <rect x="896" y="480" width="24" height="12" rx="2" fill="#f0eee9"/>
87
+ <text class="edge-label" x="908" y="488">NO</text>
88
+
89
+ <!-- Primary nodes: one start, three decisions, and five terminal modes. -->
90
+ <rect x="540" y="132" width="200" height="48" rx="20" fill="rgba(16,24,40,0.03)" stroke="#4c5665" stroke-width="1"/>
91
+ <text class="technical-name" x="640" y="156">session_search</text>
92
+ <text class="node-sub" x="640" y="172">ONE LOCAL TOOL</text>
93
+
94
+ <polygon points="640,212 748,264 640,316 532,264" fill="rgba(29,78,216,0.08)" stroke="#1d4ed8" stroke-width="1.2"/>
95
+ <text class="node-name" x="640" y="260">Call family?</text>
96
+ <text class="node-sub" x="640" y="280">ROUTE BY ARGUMENTS</text>
97
+
98
+ <rect x="48" y="412" width="240" height="128" rx="20" fill="#f0eee9" stroke="#101828" stroke-width="1"/>
99
+ <text class="node-name" x="168" y="444">Pattern preparation</text>
100
+ <text class="node-sub" x="168" y="472">operation + scope · lazy sync</text>
101
+ <text class="node-sub" x="168" y="488">bounded user/assistant corpus</text>
102
+ <text class="node-sub" x="168" y="504">+ repository inventory · never tool results</text>
103
+
104
+ <polygon points="424,392 536,448 424,504 312,448" fill="#f0eee9" stroke="#101828" stroke-width="1"/>
105
+ <text class="technical-name" x="424" y="444">query?</text>
106
+ <text class="node-sub" x="424" y="464">PRESENT</text>
107
+
108
+ <rect x="576" y="400" width="200" height="96" rx="20" fill="#f0eee9" stroke="#101828" stroke-width="1"/>
109
+ <text class="node-name" x="676" y="428">Discovery</text>
110
+ <text class="node-sub" x="676" y="452">query · lazy sync + FTS5</text>
111
+ <text class="node-sub" x="676" y="468">adaptive output omits tool results</text>
112
+ <text class="node-sub" x="676" y="484">detail:&quot;full&quot; includes tool results</text>
113
+
114
+ <rect x="328" y="544" width="192" height="96" rx="20" fill="#f0eee9" stroke="#101828" stroke-width="1"/>
115
+ <text class="node-name" x="424" y="572">Browse</text>
116
+ <text class="node-sub" x="424" y="596">no args · lazy sync</text>
117
+ <text class="node-sub" x="424" y="612">recent session metadata</text>
118
+
119
+ <polygon points="1024,392 1136,448 1024,504 912,448" fill="#f0eee9" stroke="#101828" stroke-width="1"/>
120
+ <text class="technical-name" x="1024" y="444">aroundMessageId?</text>
121
+ <text class="node-sub" x="1024" y="464">WITH sessionId</text>
122
+
123
+ <rect x="1040" y="536" width="192" height="112" rx="20" fill="#f0eee9" stroke="#101828" stroke-width="1"/>
124
+ <text class="node-name" x="1136" y="564">Scroll</text>
125
+ <text class="node-sub" x="1136" y="588">sessionId + aroundMessageId</text>
126
+ <text class="node-sub" x="1136" y="604">direct saved JSONL</text>
127
+ <text class="node-sub" x="1136" y="620">raw roles including tool results</text>
128
+ <text class="node-sub" x="1136" y="636">±window with branchTip</text>
129
+
130
+ <rect x="824" y="536" width="192" height="112" rx="20" fill="#f0eee9" stroke="#101828" stroke-width="1"/>
131
+ <text class="node-name" x="920" y="564">Read</text>
132
+ <text class="node-sub" x="920" y="588">sessionId · direct saved JSONL</text>
133
+ <text class="node-sub" x="920" y="604">raw roles including tool results</text>
134
+ <text class="node-sub" x="920" y="620">head 20 + tail 10</text>
135
+ </svg>
136
+ </figure>
137
+ </body>
138
+ </html>
@@ -0,0 +1,121 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg width="1280" height="720" viewBox="0 0 1280 720" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="session-search-routing-title session-search-routing-desc">
3
+ <title id="session-search-routing-title">session_search routing</title>
4
+ <desc id="session-search-routing-desc">Flowchart routing session_search requests between indexed preparation, discovery, and browse calls and direct transcript scroll and read calls.</desc>
5
+ <defs>
6
+ <style>@import url('https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&amp;family=Geist:wght@400;500;600&amp;family=Geist+Mono:wght@400;500;600&amp;display=swap');</style>
7
+ <style>
8
+ .eyebrow { fill: #6a7282; font: 500 8px 'Geist Mono', monospace; letter-spacing: .16em; }
9
+ .diagram-title { fill: #101828; font: 400 28px 'Instrument Serif', serif; }
10
+ .zone-label { fill: #4c5665; font: 500 8px 'Geist Mono', monospace; letter-spacing: .12em; }
11
+ .zone-note { fill: #6a7282; font: 400 8px 'Geist Mono', monospace; letter-spacing: .04em; }
12
+ .node-name { fill: #101828; font: 600 12px 'Geist', sans-serif; text-anchor: middle; }
13
+ .technical-name { fill: #101828; font: 500 12px 'Geist Mono', monospace; text-anchor: middle; }
14
+ .node-sub { fill: #4c5665; font: 400 8px 'Geist Mono', monospace; text-anchor: middle; }
15
+ .edge { fill: none; stroke: #4c5665; stroke-width: 1.2; marker-end: url(#session-search-routing-arrow); }
16
+ .edge-label { fill: #4c5665; font: 500 8px 'Geist Mono', monospace; text-anchor: middle; letter-spacing: .08em; }
17
+ </style>
18
+ <marker id="session-search-routing-arrow" markerWidth="8" markerHeight="8" refX="8" refY="4" orient="auto">
19
+ <polygon points="0 0,8 4,0 8" fill="#4c5665"/>
20
+ </marker>
21
+ <marker id="session-search-routing-arrow-accent" markerWidth="8" markerHeight="8" refX="8" refY="4" orient="auto">
22
+ <polygon points="0 0,8 4,0 8" fill="#1d4ed8"/>
23
+ </marker>
24
+ <marker id="session-search-routing-arrow-link" markerWidth="8" markerHeight="8" refX="8" refY="4" orient="auto">
25
+ <polygon points="0 0,8 4,0 8" fill="#1d4ed8"/>
26
+ </marker>
27
+ </defs>
28
+
29
+ <rect width="1280" height="720" fill="#f0eee9"/>
30
+
31
+ <text class="eyebrow" x="40" y="48">PI-SESSION-RECALL · FLOWCHART</text>
32
+ <text class="diagram-title" x="40" y="88">Five routes through session_search</text>
33
+
34
+ <!-- Routing zones are backgrounds, not primary nodes. -->
35
+ <rect x="40" y="352" width="752" height="320" rx="8" fill="#e6e4de" stroke="#101828" stroke-opacity="0.14" stroke-width="1"/>
36
+ <text class="zone-label" x="56" y="376">INDEX-BACKED CALLS · LAZY SYNC</text>
37
+ <text class="zone-note" x="56" y="392">PREPARE · DISCOVERY · BROWSE</text>
38
+
39
+ <rect x="816" y="352" width="424" height="320" rx="8" fill="#e6e4de" stroke="#101828" stroke-opacity="0.14" stroke-width="1"/>
40
+ <text class="zone-label" x="832" y="376">DIRECT TRANSCRIPT READS</text>
41
+ <text class="zone-note" x="832" y="392">RAW ROLES · TOOL RESULTS</text>
42
+
43
+ <!-- Connectors: all routes are painted before their nodes. -->
44
+ <path class="edge" d="M640 180 V212"/>
45
+ <path class="edge" d="M532 264 H320 Q312 264 312 272 V388 Q312 396 304 396 H184 Q176 396 176 404 V412"/>
46
+ <path class="edge" d="M640 316 V336 Q640 344 632 344 H432 Q424 344 424 352 V392"/>
47
+ <path class="edge" d="M748 264 H792 Q800 264 800 272 V328 Q800 336 808 336 H1016 Q1024 336 1024 344 V392"/>
48
+ <path class="edge" d="M536 448 H576"/>
49
+ <path class="edge" d="M424 504 V544"/>
50
+ <path class="edge" d="M1136 448 V536"/>
51
+ <path class="edge" d="M912 448 H896 Q888 448 888 456 V520 Q888 528 896 528 H912 Q920 528 920 536"/>
52
+
53
+ <!-- Branch labels stay 8px clear of their connectors. -->
54
+ <rect x="384" y="244" width="72" height="12" rx="2" fill="#f0eee9"/>
55
+ <text class="edge-label" x="420" y="252">PREPARE</text>
56
+
57
+ <rect x="488" y="324" width="112" height="12" rx="2" fill="#f0eee9"/>
58
+ <text class="edge-label" x="544" y="332">SEARCH / BROWSE</text>
59
+
60
+ <rect x="808" y="288" width="56" height="12" rx="2" fill="#f0eee9"/>
61
+ <text class="edge-label" x="836" y="296">INSPECT</text>
62
+
63
+ <rect x="544" y="428" width="24" height="12" rx="2" fill="#f0eee9"/>
64
+ <text class="edge-label" x="556" y="436">YES</text>
65
+
66
+ <rect x="432" y="516" width="24" height="12" rx="2" fill="#f0eee9"/>
67
+ <text class="edge-label" x="444" y="524">NO</text>
68
+
69
+ <rect x="1144" y="480" width="24" height="12" rx="2" fill="#f0eee9"/>
70
+ <text class="edge-label" x="1156" y="488">YES</text>
71
+
72
+ <rect x="896" y="480" width="24" height="12" rx="2" fill="#f0eee9"/>
73
+ <text class="edge-label" x="908" y="488">NO</text>
74
+
75
+ <!-- Primary nodes: one start, three decisions, and five terminal modes. -->
76
+ <rect x="540" y="132" width="200" height="48" rx="20" fill="#101828" fill-opacity="0.03" stroke="#4c5665" stroke-width="1"/>
77
+ <text class="technical-name" x="640" y="156">session_search</text>
78
+ <text class="node-sub" x="640" y="172">ONE LOCAL TOOL</text>
79
+
80
+ <polygon points="640,212 748,264 640,316 532,264" fill="#1d4ed8" fill-opacity="0.08" stroke="#1d4ed8" stroke-width="1.2"/>
81
+ <text class="node-name" x="640" y="260">Call family?</text>
82
+ <text class="node-sub" x="640" y="280">ROUTE BY ARGUMENTS</text>
83
+
84
+ <rect x="48" y="412" width="240" height="128" rx="20" fill="#f0eee9" stroke="#101828" stroke-width="1"/>
85
+ <text class="node-name" x="168" y="444">Pattern preparation</text>
86
+ <text class="node-sub" x="168" y="472">operation + scope · lazy sync</text>
87
+ <text class="node-sub" x="168" y="488">bounded user/assistant corpus</text>
88
+ <text class="node-sub" x="168" y="504">+ repository inventory · never tool results</text>
89
+
90
+ <polygon points="424,392 536,448 424,504 312,448" fill="#f0eee9" stroke="#101828" stroke-width="1"/>
91
+ <text class="technical-name" x="424" y="444">query?</text>
92
+ <text class="node-sub" x="424" y="464">PRESENT</text>
93
+
94
+ <rect x="576" y="400" width="200" height="96" rx="20" fill="#f0eee9" stroke="#101828" stroke-width="1"/>
95
+ <text class="node-name" x="676" y="428">Discovery</text>
96
+ <text class="node-sub" x="676" y="452">query · lazy sync + FTS5</text>
97
+ <text class="node-sub" x="676" y="468">adaptive output omits tool results</text>
98
+ <text class="node-sub" x="676" y="484">detail:&quot;full&quot; includes tool results</text>
99
+
100
+ <rect x="328" y="544" width="192" height="96" rx="20" fill="#f0eee9" stroke="#101828" stroke-width="1"/>
101
+ <text class="node-name" x="424" y="572">Browse</text>
102
+ <text class="node-sub" x="424" y="596">no args · lazy sync</text>
103
+ <text class="node-sub" x="424" y="612">recent session metadata</text>
104
+
105
+ <polygon points="1024,392 1136,448 1024,504 912,448" fill="#f0eee9" stroke="#101828" stroke-width="1"/>
106
+ <text class="technical-name" x="1024" y="444">aroundMessageId?</text>
107
+ <text class="node-sub" x="1024" y="464">WITH sessionId</text>
108
+
109
+ <rect x="1040" y="536" width="192" height="112" rx="20" fill="#f0eee9" stroke="#101828" stroke-width="1"/>
110
+ <text class="node-name" x="1136" y="564">Scroll</text>
111
+ <text class="node-sub" x="1136" y="588">sessionId + aroundMessageId</text>
112
+ <text class="node-sub" x="1136" y="604">direct saved JSONL</text>
113
+ <text class="node-sub" x="1136" y="620">raw roles including tool results</text>
114
+ <text class="node-sub" x="1136" y="636">±window with branchTip</text>
115
+
116
+ <rect x="824" y="536" width="192" height="112" rx="20" fill="#f0eee9" stroke="#101828" stroke-width="1"/>
117
+ <text class="node-name" x="920" y="564">Read</text>
118
+ <text class="node-sub" x="920" y="588">sessionId · direct saved JSONL</text>
119
+ <text class="node-sub" x="920" y="604">raw roles including tool results</text>
120
+ <text class="node-sub" x="920" y="620">head 20 + tail 10</text>
121
+ </svg>
@@ -25,6 +25,13 @@ export interface ReadResult {
25
25
  messages: WindowMessage[];
26
26
  totalMessages: number;
27
27
  truncated: boolean;
28
+ /** Resolved leaf branch, or null when no selected message can be hydrated. */
29
+ branchTip: string | null;
30
+ }
31
+
32
+ export interface ReadOptions {
33
+ /** Preparation view: retain only non-empty user/assistant text. */
34
+ userAssistantTextOnly?: boolean;
28
35
  }
29
36
 
30
37
  interface Entry {
@@ -249,18 +256,24 @@ export function readSession(
249
256
  sessionPath: string,
250
257
  head = 20,
251
258
  tail = 10,
259
+ opts?: ReadOptions,
252
260
  ): ReadResult {
253
261
  const entries = parseSessionEntries(sessionPath);
254
262
  const entriesById = new Map(entries.map((e) => [e.id, e]));
255
263
  const leaf = leafId(entriesById, entries);
256
- if (!leaf) return { messages: [], totalMessages: 0, truncated: false };
257
- const msgs = branchMessages(entriesById, leaf);
264
+ if (!leaf) return { messages: [], totalMessages: 0, truncated: false, branchTip: null };
265
+ const rawMessages = branchMessages(entriesById, leaf);
266
+ const msgs = opts?.userAssistantTextOnly
267
+ ? rawMessages.filter((m) => (m.role === "user" || m.role === "assistant") && m.content.trim().length > 0)
268
+ : rawMessages;
269
+ const branchTip = msgs.length > 0 ? leaf : null;
258
270
  if (msgs.length > head + tail) {
259
271
  return {
260
272
  messages: [...msgs.slice(0, head), ...msgs.slice(-tail)],
261
273
  totalMessages: msgs.length,
262
274
  truncated: true,
275
+ branchTip,
263
276
  };
264
277
  }
265
- return { messages: msgs, totalMessages: msgs.length, truncated: false };
278
+ return { messages: msgs, totalMessages: msgs.length, truncated: false, branchTip };
266
279
  }