@henryqw/pi-session-recall 2.1.0 → 4.0.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 +15 -13
- package/docs/session-search-routing.html +138 -0
- package/docs/session-search-routing.svg +121 -0
- package/extensions/hydrate.ts +7 -24
- package/extensions/search-core.ts +12 -33
- package/extensions/session-recall.ts +21 -119
- package/extensions/types.ts +2 -2
- package/package.json +60 -54
package/README.md
CHANGED
|
@@ -14,13 +14,13 @@ pi install npm:@henryqw/pi-session-recall
|
|
|
14
14
|
|
|
15
15
|
## Use
|
|
16
16
|
|
|
17
|
-
Start
|
|
17
|
+
Start a progressive search with a distinctive keyword query:
|
|
18
18
|
|
|
19
19
|
```json
|
|
20
20
|
{ "query": "database migration rollback" }
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Discovery returns ranked metadata and snippets. Use a result's `path` and `matchMessageId` to scroll for the relevant messages.
|
|
24
24
|
|
|
25
25
|
Prepare a repository-scoped pattern-mining sample with one call:
|
|
26
26
|
|
|
@@ -34,12 +34,12 @@ Prepare a repository-scoped pattern-mining sample with one call:
|
|
|
34
34
|
|
|
35
35
|
Use `scope:"all"` for cross-repository work. It still returns the corpus when repository inventory is unavailable.
|
|
36
36
|
|
|
37
|
-
Use
|
|
37
|
+
Use the discovery result's `path` and `matchMessageId` for a follow-up scroll:
|
|
38
38
|
|
|
39
39
|
```json
|
|
40
40
|
{
|
|
41
|
-
"sessionId": "<
|
|
42
|
-
"aroundMessageId": "<
|
|
41
|
+
"sessionId": "<result path>",
|
|
42
|
+
"aroundMessageId": "<result matchMessageId>",
|
|
43
43
|
"window": 10
|
|
44
44
|
}
|
|
45
45
|
```
|
|
@@ -51,12 +51,12 @@ The follow-up returns up to ten messages before and after that anchor on the sel
|
|
|
51
51
|
| `session_search` | tool | Search, inspect, or prepare a bounded mining corpus from past sessions. |
|
|
52
52
|
| `pi-session-pattern-miner` | skill | Find repeated work and choose the smallest useful automation. |
|
|
53
53
|
|
|
54
|
-
BM25 is a text-ranking method.
|
|
54
|
+
BM25 is a text-ranking method. Discovery returns index metadata; READ and SCROLL retrieve messages from saved session files.
|
|
55
55
|
|
|
56
56
|
| Mode | Call | Result |
|
|
57
57
|
| --- | --- | --- |
|
|
58
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. |
|
|
59
|
-
| Discovery | `query` | BM25-ranked
|
|
59
|
+
| Discovery | `query` | BM25-ranked metadata and snippets. Each result includes `path` and `matchMessageId` for a follow-up SCROLL. Start with keywords, then narrow the query before scrolling. |
|
|
60
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. |
|
|
61
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`. |
|
|
62
62
|
| Browse | no args | Recent sessions with path, name, cwd, started date, and preview. |
|
|
@@ -98,6 +98,8 @@ Inspect the current candidate files before assigning ownership. Abstain if targe
|
|
|
98
98
|
|
|
99
99
|
## Flow
|
|
100
100
|
|
|
101
|
+

|
|
102
|
+
|
|
101
103
|
Search makes no model calls.
|
|
102
104
|
|
|
103
105
|
### Query and index
|
|
@@ -110,7 +112,7 @@ Search makes no model calls.
|
|
|
110
112
|
|
|
111
113
|
### Context and sync
|
|
112
114
|
|
|
113
|
-
|
|
115
|
+
Discovery excludes the current session file when its path is available. Forked sessions collapse into their parent when both match.
|
|
114
116
|
|
|
115
117
|
Before browse, discovery, or pattern preparation, the extension lazily syncs the index from the session tree.
|
|
116
118
|
|
|
@@ -118,9 +120,9 @@ Pattern preparation reports `sync.walkComplete`, `sync.backlogRemaining`, and `s
|
|
|
118
120
|
|
|
119
121
|
### Retrieval safety
|
|
120
122
|
|
|
121
|
-
|
|
123
|
+
Discovery returns short snippets from indexed user and assistant transcript text. Use a result's `path` and `matchMessageId` with SCROLL for targeted context; READ and SCROLL retrieve raw messages, including tool-result messages when present.
|
|
122
124
|
|
|
123
|
-
Pattern preparation returns only non-empty user and assistant text. It never returns thinking blocks or tool-result content.
|
|
125
|
+
Pattern preparation returns only non-empty user and assistant text. It never returns thinking blocks or tool-result content. Preparation keeps citation and lineage metadata when a source cannot be read.
|
|
124
126
|
|
|
125
127
|
Historical tool output may contain secrets or other sensitive data. Raw retrieval places that output in active model context.
|
|
126
128
|
|
|
@@ -137,7 +139,7 @@ The index and transcript reads stay local. Transcripts are read in place. Return
|
|
|
137
139
|
Pin the previous release:
|
|
138
140
|
|
|
139
141
|
```bash
|
|
140
|
-
pi install npm:@henryqw/pi-session-recall@2.
|
|
142
|
+
pi install npm:@henryqw/pi-session-recall@2.1.1
|
|
141
143
|
```
|
|
142
144
|
|
|
143
145
|
No index migration or cleanup is needed.
|
|
@@ -154,7 +156,7 @@ Session directories whose encoded path starts with `--tmp-` or `--private-tmp-`
|
|
|
154
156
|
|
|
155
157
|
Session files over 32 MiB are excluded from indexing and hydration. Discovery cannot newly find them.
|
|
156
158
|
|
|
157
|
-
READ and SCROLL return an explicit size error.
|
|
159
|
+
READ and SCROLL return an explicit size error. Discovery returns the indexed metadata even when a source file changes afterward.
|
|
158
160
|
|
|
159
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.
|
|
160
162
|
|
|
@@ -162,7 +164,7 @@ Repository scope fails outside Git or when repository inventory fails. All scope
|
|
|
162
164
|
|
|
163
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.
|
|
164
166
|
|
|
165
|
-
Preparation rejects `query`, session cursors,
|
|
167
|
+
Preparation rejects `query`, session cursors, or `window` in the same call. It also rejects `scope` without the operation.
|
|
166
168
|
|
|
167
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.
|
|
168
170
|
|
|
@@ -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&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&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 progressive search and scroll</title>
|
|
19
|
+
<desc id="session-search-routing-desc">Flowchart routing progressive keyword discovery to metadata results, then using each result path and matchMessageId for direct transcript scroll.</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">keyword query · lazy sync + FTS5</text>
|
|
111
|
+
<text class="node-sub" x="676" y="468">metadata + snippets only</text>
|
|
112
|
+
<text class="node-sub" x="676" y="484">path + matchMessageId → SCROLL</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 progressive search and scroll</title>
|
|
4
|
+
<desc id="session-search-routing-desc">Flowchart routing progressive keyword discovery to metadata results, then using each result path and matchMessageId for direct transcript scroll.</desc>
|
|
5
|
+
<defs>
|
|
6
|
+
<style>@import url('https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&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">keyword query · lazy sync + FTS5</text>
|
|
97
|
+
<text class="node-sub" x="676" y="468">metadata + snippets only</text>
|
|
98
|
+
<text class="node-sub" x="676" y="484">path + matchMessageId → SCROLL</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>
|
package/extensions/hydrate.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Hydration: direct JSONL parsing of Pi session files (never SessionManager.open —
|
|
3
|
-
* it rewrites legacy files), branch-aware windows
|
|
3
|
+
* it rewrites legacy files), branch-aware windows, READ head/tail.
|
|
4
4
|
* Pure functions over file paths; no SQLite access.
|
|
5
5
|
*/
|
|
6
6
|
/// <reference types="node" />
|
|
@@ -9,13 +9,8 @@ import type { WindowMessage } from "./types.ts";
|
|
|
9
9
|
|
|
10
10
|
export interface WindowResult {
|
|
11
11
|
messages: WindowMessage[];
|
|
12
|
-
/** Every message on the resolved branch, root→tip chronological. Callers
|
|
13
|
-
* derive bookends from this instead of re-parsing the transcript. */
|
|
14
|
-
branchMessages: WindowMessage[];
|
|
15
12
|
messagesBefore: number;
|
|
16
13
|
messagesAfter: number;
|
|
17
|
-
/** Present only when discovery filtering removed at least one tool result. */
|
|
18
|
-
toolResultsOmitted?: true;
|
|
19
14
|
/** Tip of the branch the window was resolved on — pass back as branchTip to
|
|
20
15
|
* keep scrolling on this branch across forks. May be a non-message entry id. */
|
|
21
16
|
branchTip: string;
|
|
@@ -139,11 +134,7 @@ function branch(entriesById: Map<string, Entry>, entryId: string): Entry[] {
|
|
|
139
134
|
}
|
|
140
135
|
|
|
141
136
|
/** Deepest entry in file order whose ancestry contains tipId (tipId itself if none). */
|
|
142
|
-
function deepestDescendant(
|
|
143
|
-
entriesById: Map<string, Entry>,
|
|
144
|
-
entries: Entry[],
|
|
145
|
-
anchorId: string,
|
|
146
|
-
): string {
|
|
137
|
+
function deepestDescendant(entries: Entry[], anchorId: string): string {
|
|
147
138
|
const onBranch = new Set([anchorId]);
|
|
148
139
|
let tip = anchorId;
|
|
149
140
|
for (const e of entries) {
|
|
@@ -207,7 +198,7 @@ export function getWindow(
|
|
|
207
198
|
sessionPath: string,
|
|
208
199
|
anchorEntryId: string,
|
|
209
200
|
windowN: number,
|
|
210
|
-
opts?: { branchTip?: string
|
|
201
|
+
opts?: { branchTip?: string },
|
|
211
202
|
): WindowResult {
|
|
212
203
|
const n = Math.max(0, Math.min(50, windowN));
|
|
213
204
|
const entries = parseSessionEntries(sessionPath);
|
|
@@ -222,32 +213,24 @@ export function getWindow(
|
|
|
222
213
|
// Explicit branch selection: resolve the branch from its tip (deepest
|
|
223
214
|
// descendant), independent of where the window centers.
|
|
224
215
|
resolveMessageCursor(entriesById, opts.branchTip); // unknown tip → throw
|
|
225
|
-
tip = deepestDescendant(
|
|
216
|
+
tip = deepestDescendant(entries, opts.branchTip);
|
|
226
217
|
if (!branch(entriesById, tip).some((e) => e.id === messageAnchor.id)) {
|
|
227
218
|
throw new Error(`anchor entry ${anchorEntryId} is not on branch ${opts.branchTip}`);
|
|
228
219
|
}
|
|
229
220
|
} else {
|
|
230
|
-
tip = deepestDescendant(
|
|
221
|
+
tip = deepestDescendant(entries, anchorEntryId);
|
|
231
222
|
}
|
|
232
|
-
const
|
|
233
|
-
const toolResultsOmitted = opts?.userAssistantTextOnly && rawMessages.some((m) => m.role === "toolResult")
|
|
234
|
-
? true
|
|
235
|
-
: undefined;
|
|
236
|
-
const msgs = opts?.userAssistantTextOnly
|
|
237
|
-
? rawMessages.filter((m) => (m.role === "user" || m.role === "assistant") && m.content.length > 0)
|
|
238
|
-
: rawMessages;
|
|
223
|
+
const msgs = branchMessages(entriesById, tip);
|
|
239
224
|
const idx = msgs.findIndex((m) => m.entryId === anchorEntryIdMsg);
|
|
240
|
-
if (idx < 0) throw new Error(`anchor entry ${anchorEntryId} is not a
|
|
225
|
+
if (idx < 0) throw new Error(`anchor entry ${anchorEntryId} is not a message`);
|
|
241
226
|
const start = Math.max(0, idx - n);
|
|
242
227
|
const end = Math.min(msgs.length - 1, idx + n);
|
|
243
228
|
return {
|
|
244
|
-
branchMessages: msgs,
|
|
245
229
|
messages: msgs.slice(start, end + 1).map((m) =>
|
|
246
230
|
m.entryId === anchorEntryIdMsg ? { ...m, anchor: true } : m,
|
|
247
231
|
),
|
|
248
232
|
messagesBefore: idx,
|
|
249
233
|
messagesAfter: msgs.length - 1 - idx,
|
|
250
|
-
...(toolResultsOmitted ? { toolResultsOmitted } : {}),
|
|
251
234
|
branchTip: tip,
|
|
252
235
|
};
|
|
253
236
|
}
|
|
@@ -15,7 +15,7 @@ export const DEFAULT_SYNC_CAP = 50;
|
|
|
15
15
|
const MAX_SYNC_CAP = DEFAULT_SYNC_CAP * 10;
|
|
16
16
|
const MAX_TEXT_CHARS = 20000;
|
|
17
17
|
const SCAN_LIMIT = 300;
|
|
18
|
-
/** Best-ranked candidate retained per session after
|
|
18
|
+
/** Best-ranked candidate retained per session after SQL prefiltering. */
|
|
19
19
|
const ROWS_PER_FILE = 1;
|
|
20
20
|
|
|
21
21
|
const SCHEMA_SQL = `
|
|
@@ -519,19 +519,13 @@ function getBacklog(db: DatabaseSync): number {
|
|
|
519
519
|
|
|
520
520
|
export interface SearchOptions {
|
|
521
521
|
limit?: number;
|
|
522
|
-
/**
|
|
523
|
-
currentLiveEntryIds?: Set<string>;
|
|
524
|
-
/** Path of the current session file. */
|
|
522
|
+
/** Path of the current session file — the whole file is skipped. */
|
|
525
523
|
currentSessionPath?: string;
|
|
526
524
|
}
|
|
527
525
|
|
|
528
|
-
//
|
|
529
|
-
//
|
|
530
|
-
|
|
531
|
-
// live_filter is a per-connection TEMP table populated from SearchOptions.
|
|
532
|
-
const LIVE_FILTER_SQL = `NOT EXISTS (
|
|
533
|
-
SELECT 1 FROM live_filter lf WHERE lf.path = m.path AND lf.entry_id = m.entry_id
|
|
534
|
-
)`;
|
|
526
|
+
// Current-session exclusion happens INSIDE SQL (before ROW_NUMBER/caps), so
|
|
527
|
+
// the excluded file cannot consume the per-file rank, lineage, or scan limit.
|
|
528
|
+
const CURRENT_SESSION_FILTER_SQL = "(? IS NULL OR m.path <> ?)";
|
|
535
529
|
const LINEAGE_FILTER_SQL = `NOT EXISTS (
|
|
536
530
|
SELECT 1 FROM ranked parent
|
|
537
531
|
WHERE parent.path = r.parent_session
|
|
@@ -549,7 +543,7 @@ WITH matches AS (
|
|
|
549
543
|
bm25(session_fts) AS score
|
|
550
544
|
FROM session_fts JOIN messages m ON m.rowid = session_fts.rowid
|
|
551
545
|
LEFT JOIN sessions s ON s.path = m.path
|
|
552
|
-
WHERE session_fts MATCH ? AND ${
|
|
546
|
+
WHERE session_fts MATCH ? AND ${CURRENT_SESSION_FILTER_SQL}
|
|
553
547
|
), ranked AS (
|
|
554
548
|
SELECT *, ROW_NUMBER() OVER (PARTITION BY path ORDER BY score, rid) AS rn
|
|
555
549
|
FROM matches
|
|
@@ -587,22 +581,7 @@ export function searchIndex(
|
|
|
587
581
|
const limit = opts?.limit ?? 3;
|
|
588
582
|
const db = openDb(dbPath);
|
|
589
583
|
try {
|
|
590
|
-
|
|
591
|
-
// parameterized, immune to SQLite variable limits. Primary key indexes
|
|
592
|
-
// the correlated NOT EXISTS probe and dedupes inserts.
|
|
593
|
-
db.exec("CREATE TEMP TABLE IF NOT EXISTS live_filter (path TEXT NOT NULL, entry_id TEXT NOT NULL, PRIMARY KEY (path, entry_id))");
|
|
594
|
-
if (opts?.currentSessionPath && opts?.currentLiveEntryIds?.size) {
|
|
595
|
-
const ins = db.prepare("INSERT INTO live_filter(path, entry_id) VALUES (?, ?)");
|
|
596
|
-
db.exec("BEGIN");
|
|
597
|
-
try {
|
|
598
|
-
for (const id of opts.currentLiveEntryIds!) ins.run(opts.currentSessionPath!, id);
|
|
599
|
-
db.exec("COMMIT");
|
|
600
|
-
} catch (err) {
|
|
601
|
-
db.exec("ROLLBACK");
|
|
602
|
-
throw err;
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
|
|
584
|
+
const currentSessionPath = opts?.currentSessionPath ?? null;
|
|
606
585
|
const plan = buildFtsQueryPlan(query);
|
|
607
586
|
if (plan.ftsCandidates.length === 0 && !plan.forceLike) {
|
|
608
587
|
return { hits: [], backlogRemaining: getBacklog(db) };
|
|
@@ -628,7 +607,7 @@ export function searchIndex(
|
|
|
628
607
|
let parseFailed = false;
|
|
629
608
|
for (const cand of plan.ftsCandidates) {
|
|
630
609
|
try {
|
|
631
|
-
rows = db.prepare(BASE_SELECT).all(cand) as any;
|
|
610
|
+
rows = db.prepare(BASE_SELECT).all(cand, currentSessionPath, currentSessionPath) as any;
|
|
632
611
|
// First/raw success defines the result even with zero rows. After a
|
|
633
612
|
// parse error, an empty recovery candidate keeps trying later ones.
|
|
634
613
|
if (!parseFailed || rows.length > 0) {
|
|
@@ -652,21 +631,21 @@ export function searchIndex(
|
|
|
652
631
|
ROW_NUMBER() OVER (PARTITION BY m.path ORDER BY m.rowid DESC) AS rn,
|
|
653
632
|
COUNT(*) OVER (PARTITION BY m.path) AS matches
|
|
654
633
|
FROM messages m LEFT JOIN sessions s ON s.path = m.path
|
|
655
|
-
WHERE ${likePlan.where} AND ${
|
|
634
|
+
WHERE ${likePlan.where} AND ${CURRENT_SESSION_FILTER_SQL}
|
|
656
635
|
)
|
|
657
636
|
SELECT path, entry_id, role, timestamp, head, tail, cwd, name, started_at
|
|
658
637
|
FROM ranked r
|
|
659
638
|
WHERE rn <= ${ROWS_PER_FILE}
|
|
660
639
|
AND ${LINEAGE_FILTER_SQL}
|
|
661
640
|
ORDER BY matches DESC, started_at DESC, path
|
|
662
|
-
LIMIT ${SCAN_LIMIT}`).all(...likePlan.params) as any;
|
|
641
|
+
LIMIT ${SCAN_LIMIT}`).all(...likePlan.params, currentSessionPath, currentSessionPath) as any;
|
|
663
642
|
// Snippets anchor on operand terms only — operator words like OR would
|
|
664
643
|
// otherwise match common substrings and hide the real match.
|
|
665
644
|
for (const r of rows as any[]) r.snip = likeSnippet((r as any).head ?? "", (r as any).tail ?? "", likePlan.terms);
|
|
666
645
|
}
|
|
667
646
|
|
|
668
|
-
//
|
|
669
|
-
// before the scan limit, so fork rows cannot starve
|
|
647
|
+
// Current-file and one-hop lineage suppression already happened in SQL,
|
|
648
|
+
// before the scan limit, so excluded or fork rows cannot starve matches.
|
|
670
649
|
const seenFiles = new Set<string>();
|
|
671
650
|
const hits: SearchHit[] = [];
|
|
672
651
|
let rankCounter = 0;
|
|
@@ -266,16 +266,15 @@ interface ToolParams {
|
|
|
266
266
|
branchTip?: string;
|
|
267
267
|
window?: number;
|
|
268
268
|
limit?: number;
|
|
269
|
-
detail?: "adaptive" | "full";
|
|
270
269
|
}
|
|
271
270
|
|
|
272
|
-
const DESCRIPTION = `Search past Pi sessions locally with FTS5; returns
|
|
271
|
+
const DESCRIPTION = `Search past Pi sessions locally with FTS5; returns indexed metadata and snippets.
|
|
273
272
|
|
|
274
273
|
- \`operation: "prepare-pattern-miner"\` + \`scope\`: prepare one bounded corpus and repository inventory.
|
|
275
274
|
- \`query\`: discover matches. Prefer distinctive identifiers or uncommon terms; multi-word queries are AND. Use \`OR\`/\`NOT\` for Boolean queries and quotes only when exact wording is known.
|
|
276
275
|
- \`sessionId\` + \`aroundMessageId\`: scroll ±\`window\`; retain \`branchTip\` across forks.
|
|
277
276
|
- \`sessionId\` alone: read; no args: browse recent sessions.
|
|
278
|
-
- Discovery
|
|
277
|
+
- Discovery returns metadata and snippets. Use a result's \`path\` and \`matchMessageId\` for a follow-up scroll.`;
|
|
279
278
|
|
|
280
279
|
export default function (pi: ExtensionAPI): void {
|
|
281
280
|
// Best-effort sync at startup, deferred so the synchronous walk + SQLite
|
|
@@ -307,8 +306,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
307
306
|
branchTip: Type.Optional(Type.String({ description: "Branch tip entry id from a previous response — selects which branch of a forked session to scroll; aroundMessageId must lie on it." })),
|
|
308
307
|
window: Type.Optional(Type.Number({ description: "Scroll window radius, [1,20], default 5." })),
|
|
309
308
|
limit: Type.Optional(Type.Number({ description: "Max results, [1,10]. Defaults to 10 for preparation and 3 otherwise." })),
|
|
310
|
-
|
|
311
|
-
}),
|
|
309
|
+
}, { additionalProperties: false }),
|
|
312
310
|
renderResult(result, { expanded }, theme) {
|
|
313
311
|
const output = result.content.find((part) => part.type === "text")?.text ?? "";
|
|
314
312
|
const styledOutput = theme.fg("toolOutput", output);
|
|
@@ -333,7 +331,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
333
331
|
throw new Error("scope requires operation: prepare-pattern-miner.");
|
|
334
332
|
}
|
|
335
333
|
if (rawParams.operation === "prepare-pattern-miner") {
|
|
336
|
-
const incompatible = (["query", "sessionId", "aroundMessageId", "branchTip", "window"
|
|
334
|
+
const incompatible = (["query", "sessionId", "aroundMessageId", "branchTip", "window"] as const)
|
|
337
335
|
.filter((key) => rawParams[key] !== undefined);
|
|
338
336
|
if (incompatible.length > 0) {
|
|
339
337
|
throw new Error(`prepare-pattern-miner does not accept: ${incompatible.join(", ")}.`);
|
|
@@ -365,7 +363,6 @@ export default function (pi: ExtensionAPI): void {
|
|
|
365
363
|
branchTip: rawParams.branchTip != null ? String(rawParams.branchTip) : undefined,
|
|
366
364
|
window: rawParams.window,
|
|
367
365
|
limit: rawParams.limit,
|
|
368
|
-
detail: rawParams.detail,
|
|
369
366
|
};
|
|
370
367
|
let sessionId = params.sessionId?.trim() || undefined;
|
|
371
368
|
const anchor = params.aroundMessageId?.trim() || undefined;
|
|
@@ -445,135 +442,40 @@ export default function (pi: ExtensionAPI): void {
|
|
|
445
442
|
|
|
446
443
|
// --- DISCOVERY ---
|
|
447
444
|
const limit = clamp(params.limit, 1, 10, 3);
|
|
448
|
-
const full = params.detail === "full";
|
|
449
445
|
|
|
450
|
-
//
|
|
451
|
-
|
|
446
|
+
// Exclude the whole current file when the session manager provides it.
|
|
447
|
+
// A missing or failing manager leaves discovery usable without exclusion.
|
|
452
448
|
let currentSessionPath: string | undefined;
|
|
453
449
|
try {
|
|
454
|
-
currentSessionPath = ctx.sessionManager.getSessionFile();
|
|
455
|
-
liveIds = new Set(
|
|
456
|
-
ctx.sessionManager
|
|
457
|
-
.buildContextEntries()
|
|
458
|
-
.filter((e) => e.type === "message")
|
|
459
|
-
.map((e) => e.id),
|
|
460
|
-
);
|
|
450
|
+
currentSessionPath = ctx.sessionManager.getSessionFile() ?? undefined;
|
|
461
451
|
} catch {
|
|
462
|
-
// Guard unavailable →
|
|
452
|
+
// Guard unavailable → continue without exclusion.
|
|
463
453
|
}
|
|
464
454
|
|
|
465
455
|
const { hits, backlogRemaining } = searchIndex(dbPath(), params.query, {
|
|
466
456
|
limit,
|
|
467
|
-
currentLiveEntryIds: liveIds,
|
|
468
457
|
currentSessionPath,
|
|
469
458
|
});
|
|
470
459
|
|
|
471
460
|
const resultQuery = params.query!.trim().slice(0, MAX_QUERY_CHARS);
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
461
|
+
const results = hits.map((hit) => ({
|
|
462
|
+
path: hit.path,
|
|
463
|
+
snippet: hit.snippet,
|
|
464
|
+
rank: hit.rank,
|
|
465
|
+
matchMessageId: hit.entryId,
|
|
466
|
+
role: hit.role,
|
|
467
|
+
timestamp: hit.timestamp,
|
|
468
|
+
cwd: hit.cwd,
|
|
469
|
+
name: hit.name,
|
|
470
|
+
startedAt: hit.startedAt,
|
|
471
|
+
}));
|
|
472
|
+
return textResult({
|
|
478
473
|
mode: "discovery",
|
|
479
474
|
query: resultQuery,
|
|
480
|
-
results
|
|
475
|
+
results,
|
|
481
476
|
backlogRemaining,
|
|
482
477
|
...(syncWarning ? { syncWarning } : {}),
|
|
483
|
-
};
|
|
484
|
-
let used = JSON.stringify(envelope).length + Math.max(0, hits.length - 1);
|
|
485
|
-
const results = hits.map((hit, index) => {
|
|
486
|
-
const remaining = Math.floor((OUTPUT_CHAR_BUDGET - used) / (hits.length - index));
|
|
487
|
-
const meta = {
|
|
488
|
-
path: hit.path,
|
|
489
|
-
snippet: hit.snippet,
|
|
490
|
-
rank: hit.rank,
|
|
491
|
-
matchMessageId: hit.entryId,
|
|
492
|
-
role: hit.role,
|
|
493
|
-
timestamp: hit.timestamp,
|
|
494
|
-
cwd: hit.cwd,
|
|
495
|
-
name: hit.name,
|
|
496
|
-
startedAt: hit.startedAt,
|
|
497
|
-
};
|
|
498
|
-
const hydrateFull = full || hit.rank === 0;
|
|
499
|
-
// Every hit is sized against the cumulative remaining budget: keep
|
|
500
|
-
// as-is when it fits, else truncate to the largest uniform cap that
|
|
501
|
-
// fits across messages and bookends, else metadata-only.
|
|
502
|
-
// contentTruncated signals either case.
|
|
503
|
-
const fitOrTruncate = (
|
|
504
|
-
hitObj: Record<string, unknown>,
|
|
505
|
-
messages: WindowMessage[],
|
|
506
|
-
bookends?: { start: WindowMessage[]; end: WindowMessage[] },
|
|
507
|
-
): Record<string, unknown> => {
|
|
508
|
-
const out: Record<string, unknown> = { ...hitObj };
|
|
509
|
-
if (JSON.stringify(out).length > remaining) {
|
|
510
|
-
const pools = bookends ? [messages, bookends.start, bookends.end] : [messages];
|
|
511
|
-
const maxLen = Math.max(...pools.flatMap((a) => a.map((m) => m.content.length)), 0);
|
|
512
|
-
Object.assign(
|
|
513
|
-
out,
|
|
514
|
-
boundContent(
|
|
515
|
-
(cap) => ({
|
|
516
|
-
...hitObj,
|
|
517
|
-
messages: cap === null ? [] : truncateContent(messages, cap),
|
|
518
|
-
...(bookends && cap !== null
|
|
519
|
-
? { bookends: { start: truncateContent(bookends.start, cap), end: truncateContent(bookends.end, cap) } }
|
|
520
|
-
: bookends
|
|
521
|
-
? { bookends: { start: [], end: [] } }
|
|
522
|
-
: {}),
|
|
523
|
-
contentTruncated: true,
|
|
524
|
-
}),
|
|
525
|
-
maxLen,
|
|
526
|
-
remaining,
|
|
527
|
-
),
|
|
528
|
-
);
|
|
529
|
-
}
|
|
530
|
-
used += JSON.stringify(out).length;
|
|
531
|
-
return out;
|
|
532
|
-
};
|
|
533
|
-
const hydrationFallback = (error: unknown) =>
|
|
534
|
-
fitOrTruncate(
|
|
535
|
-
{ ...meta, detail: hydrateFull ? "full" : "compact", messages: [], bookends: { start: [], end: [] }, messagesBefore: 0, messagesAfter: 0, error: (error instanceof Error ? error.message : String(error)).slice(0, 512) },
|
|
536
|
-
[],
|
|
537
|
-
);
|
|
538
|
-
if (!hydrateFull) {
|
|
539
|
-
// Compact hits still carry the matched anchor message.
|
|
540
|
-
try {
|
|
541
|
-
const win = getWindow(hit.path, hit.entryId, 0, { userAssistantTextOnly: true });
|
|
542
|
-
// Mark when the fixed compact cap already removed content, so a
|
|
543
|
-
// hit that still fits the budget isn't mistaken for complete.
|
|
544
|
-
const overCompactCap = win.messages.some((m) => m.content.length > 2000);
|
|
545
|
-
return fitOrTruncate({ ...meta, detail: "compact", ...(overCompactCap ? { contentTruncated: true } : {}), ...(win.toolResultsOmitted ? { toolResultsOmitted: true } : {}), messages: truncateContent(win.messages, 2000), bookends: { start: [], end: [] }, messagesBefore: win.messagesBefore, messagesAfter: win.messagesAfter }, win.messages);
|
|
546
|
-
} catch (error) {
|
|
547
|
-
return hydrationFallback(error);
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
try {
|
|
551
|
-
// One bounded snapshot feeds both window and branch bookends.
|
|
552
|
-
const win = getWindow(hit.path, hit.entryId, 5, full ? undefined : { userAssistantTextOnly: true });
|
|
553
|
-
// Same branch as the anchor — following the file's final leaf
|
|
554
|
-
// would attach unrelated sibling messages.
|
|
555
|
-
const bookends = { start: win.branchMessages.slice(0, 3), end: win.branchMessages.slice(-3) };
|
|
556
|
-
return fitOrTruncate(
|
|
557
|
-
{
|
|
558
|
-
...meta,
|
|
559
|
-
detail: "full" as const,
|
|
560
|
-
...(win.toolResultsOmitted ? { toolResultsOmitted: true } : {}),
|
|
561
|
-
messages: win.messages,
|
|
562
|
-
bookends,
|
|
563
|
-
messagesBefore: win.messagesBefore,
|
|
564
|
-
messagesAfter: win.messagesAfter,
|
|
565
|
-
},
|
|
566
|
-
win.messages,
|
|
567
|
-
bookends,
|
|
568
|
-
);
|
|
569
|
-
} catch (error) {
|
|
570
|
-
// Session file unreadable/moved since indexing → anchor-only.
|
|
571
|
-
return hydrationFallback(error);
|
|
572
|
-
}
|
|
573
478
|
});
|
|
574
|
-
|
|
575
|
-
const result: Record<string, unknown> = { ...envelope, results };
|
|
576
|
-
return textResult(result);
|
|
577
479
|
} catch (error) {
|
|
578
480
|
const message = error instanceof Error ? error.message : String(error);
|
|
579
481
|
return textResult({ success: false, error: message });
|
package/extensions/types.ts
CHANGED
|
@@ -9,7 +9,7 @@ export interface WindowMessage {
|
|
|
9
9
|
role: string;
|
|
10
10
|
content: string;
|
|
11
11
|
timestamp: string;
|
|
12
|
-
/** True when this message is the anchor of a scroll
|
|
12
|
+
/** True when this message is the anchor of a scroll window. */
|
|
13
13
|
anchor?: boolean;
|
|
14
14
|
}
|
|
15
15
|
|
|
@@ -28,7 +28,7 @@ export interface PreparationSessionRow extends SessionRow {
|
|
|
28
28
|
lineageId: string;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
/** One FTS discovery hit
|
|
31
|
+
/** One metadata-only FTS discovery hit. */
|
|
32
32
|
export interface SearchHit {
|
|
33
33
|
path: string;
|
|
34
34
|
entryId: string;
|
package/package.json
CHANGED
|
@@ -1,56 +1,62 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
2
|
+
"name": "@henryqw/pi-session-recall",
|
|
3
|
+
"version": "4.0.1",
|
|
4
|
+
"description": "Local FTS5 search over past Pi sessions plus a skill for turning recurring work into deterministic automation.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"pi",
|
|
8
|
+
"session-recall",
|
|
9
|
+
"fts5",
|
|
10
|
+
"recall"
|
|
11
|
+
],
|
|
12
|
+
"type": "module",
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=22.19.0"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"files": [
|
|
18
|
+
"LICENSE",
|
|
19
|
+
"extensions",
|
|
20
|
+
"skills",
|
|
21
|
+
"docs",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"test": "node --test test/*.test.ts",
|
|
26
|
+
"typecheck": "tsc --noEmit --allowImportingTsExtensions --target ES2022 --module NodeNext --moduleResolution NodeNext --skipLibCheck extensions/*.ts test/*.test.ts",
|
|
27
|
+
"pack:check": "npm pack --dry-run"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"@earendil-works/pi-ai": ">=0.86.0 <0.88.0",
|
|
31
|
+
"@earendil-works/pi-coding-agent": ">=0.86.0 <0.88.0",
|
|
32
|
+
"@earendil-works/pi-tui": ">=0.86.0 <0.88.0",
|
|
33
|
+
"typebox": "^1.3.15"
|
|
34
|
+
},
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/HenryQW/pi-harness.git",
|
|
38
|
+
"directory": "extensions/pi-session-recall"
|
|
39
|
+
},
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/HenryQW/pi-harness/issues"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"pi": {
|
|
47
|
+
"extensions": [
|
|
48
|
+
"./extensions/session-recall.ts"
|
|
49
|
+
],
|
|
50
|
+
"skills": [
|
|
51
|
+
"./skills"
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@henryqw/pi-config-store": "^2.0.0"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@earendil-works/pi-ai": "0.87.0",
|
|
59
|
+
"@earendil-works/pi-coding-agent": "0.87.0",
|
|
60
|
+
"@earendil-works/pi-tui": "0.87.0"
|
|
61
|
+
}
|
|
56
62
|
}
|