opencode-session-recall 0.8.0 → 0.9.0
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 +110 -11
- package/dist/opencode-session-recall.js +111 -90
- package/dist/search.d.ts.map +1 -1
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# opencode-session-recall
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/opencode-session-recall)
|
|
4
|
+
[](https://www.npmjs.com/package/opencode-session-recall)
|
|
5
|
+
[](https://github.com/rmk40/opencode-session-recall/blob/main/LICENSE)
|
|
6
|
+
|
|
3
7
|
**Every conversation your agent has ever had — across every session, every project — is already in the database. It's just not looking.**
|
|
4
8
|
|
|
5
9
|
[OpenCode](https://github.com/opencode-ai/opencode) stores the full conversation history from every session your agent has ever run — messages, tool calls, tool outputs, reasoning traces. All of it. Not just the current session. Not just the current project. Every project on the machine. Even after compaction shrinks what the model can see, the original content stays in the database — just no longer visible to the agent.
|
|
@@ -67,6 +71,47 @@ recall({ query: "chose postgres over", scope: "project", type: "reasoning" })
|
|
|
67
71
|
|
|
68
72
|
Recovers the reasoning behind an architectural decision from three sessions ago. Context that no summary captures.
|
|
69
73
|
|
|
74
|
+
**"Find it even with a typo."**
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
recall({ query: "prefiltr", match: "fuzzy", scope: "session" })
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Fuzzy search finds `prefilter` even when the agent misremembers the exact spelling. Results ranked by relevance, not just recency.
|
|
81
|
+
|
|
82
|
+
**"Which sessions touched this topic?"**
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
recall({ query: "rate limiting", scope: "global", match: "smart", group: "session" })
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
4 sessions across 3 projects, each with `hitCount` and best representative snippet. One call to discover everywhere a topic came up.
|
|
89
|
+
|
|
90
|
+
## Smart and fuzzy search
|
|
91
|
+
|
|
92
|
+
Ranked fuzzy retrieval powered by [Fuse.js](https://www.fusejs.io/). Three matching strategies:
|
|
93
|
+
|
|
94
|
+
| Mode | Behavior | Best for |
|
|
95
|
+
| ------------------- | ----------------------------------- | ----------------------------------------------- |
|
|
96
|
+
| `literal` (default) | Case-insensitive substring match | Exact terms, all scopes |
|
|
97
|
+
| `smart` | Fuzzy ranked search (threshold 0.3) | Uncertain wording, typos, separator differences |
|
|
98
|
+
| `fuzzy` | Looser fuzzy search (threshold 0.5) | Very approximate queries, exploratory search |
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
recall({ query: "rate limit middleware", match: "smart", scope: "project" })
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Smart and fuzzy modes:
|
|
105
|
+
|
|
106
|
+
- **Handle typos** — `prefiltr` finds `prefilter`, `ECONNREFUSD` finds `ECONNREFUSED`
|
|
107
|
+
- **Normalize separators** — `rate-limit` matches `rateLimit` matches `rate_limit`
|
|
108
|
+
- **Rank by relevance** — results scored 0–1 with structural boosts for exact phrases, full token coverage, reasoning traces, and recency
|
|
109
|
+
- **Fall back gracefully** — if smart/fuzzy finds nothing, literal search runs automatically
|
|
110
|
+
- **Time-budget degradation** — if ranking takes too long, returns prefilter-ranked results instead of timing out
|
|
111
|
+
- **Explain mode** — add `explain: true` to see scoring breakdowns via `matchReasons`
|
|
112
|
+
|
|
113
|
+
Available across all scopes — `"session"`, `"project"`, and `"global"`.
|
|
114
|
+
|
|
70
115
|
## Recall is not memory
|
|
71
116
|
|
|
72
117
|
This is not a memory system. Memory is selective and curated. Recall is raw history retrieval — verbatim, exhaustive, on demand.
|
|
@@ -107,19 +152,43 @@ The primary tool. Full-text search across messages, tool outputs, tool inputs, r
|
|
|
107
152
|
recall({ query: "authentication", scope: "project" })
|
|
108
153
|
recall({ query: "error", type: "tool", scope: "session" })
|
|
109
154
|
recall({ query: "JWT", sessionID: "ses_from_another_project" })
|
|
155
|
+
recall({ query: "rate limit", match: "smart", scope: "global", group: "session" })
|
|
156
|
+
recall({ query: "prefiltr", match: "fuzzy", scope: "session", explain: true })
|
|
110
157
|
```
|
|
111
158
|
|
|
112
|
-
| Param | Default
|
|
113
|
-
| ---------------- |
|
|
114
|
-
| `query` | required
|
|
115
|
-
| `scope` | `"global"`
|
|
116
|
-
| `
|
|
117
|
-
| `
|
|
118
|
-
| `
|
|
119
|
-
| `
|
|
120
|
-
| `
|
|
121
|
-
| `
|
|
122
|
-
| `
|
|
159
|
+
| Param | Default | Description |
|
|
160
|
+
| ---------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
161
|
+
| `query` | required | Text to search for |
|
|
162
|
+
| `scope` | `"global"` | `"session"`, `"project"`, or `"global"` |
|
|
163
|
+
| `match` | `"literal"` | `"literal"`, `"smart"`, or `"fuzzy"` |
|
|
164
|
+
| `explain` | `false` | Include scoring metadata in results |
|
|
165
|
+
| `sessionID` | — | Target a specific session (overrides scope) |
|
|
166
|
+
| `type` | `"all"` | `"text"`, `"tool"`, `"reasoning"`, or `"all"` |
|
|
167
|
+
| `role` | `"all"` | `"user"`, `"assistant"`, or `"all"` |
|
|
168
|
+
| `before`/`after` | — | Timestamp filters (ms epoch) |
|
|
169
|
+
| `width` | `200` | Snippet size (50–1000 chars) |
|
|
170
|
+
| `sessions` | `10` | Max sessions to scan |
|
|
171
|
+
| `title` | — | Filter by session title substring (rarely needed) |
|
|
172
|
+
| `group` | `"part"` | `"part"` or `"session"` — when `"session"`, collapses results by session (one entry per session with the best-scoring or most-recent hit as representative, plus `hitCount`) |
|
|
173
|
+
| `results` | `10` | Max results to return |
|
|
174
|
+
|
|
175
|
+
Smart/fuzzy results include additional fields:
|
|
176
|
+
|
|
177
|
+
| Field | Description |
|
|
178
|
+
| -------------- | ------------------------------------------------------------------------ |
|
|
179
|
+
| `score` | Relevance score (0–1, higher is better) |
|
|
180
|
+
| `matchMode` | Which strategy produced this result |
|
|
181
|
+
| `matchedTerms` | Query tokens found in the candidate |
|
|
182
|
+
| `matchReasons` | Scoring breakdown (only when `explain: true`) |
|
|
183
|
+
| `hitCount` | Number of part-level hits in this session (only when `group: "session"`) |
|
|
184
|
+
|
|
185
|
+
Response-level metadata for smart/fuzzy:
|
|
186
|
+
|
|
187
|
+
| Field | Description |
|
|
188
|
+
| ------------- | ---------------------------------------------------------- |
|
|
189
|
+
| `matchMode` | `"smart"`, `"fuzzy"`, or `"literal"` (if fell back) |
|
|
190
|
+
| `degradeKind` | `"none"`, `"time"`, `"budget"`, or `"fallback"` |
|
|
191
|
+
| `group` | `"part"` or `"session"` — echoes back the grouping applied |
|
|
123
192
|
|
|
124
193
|
### `recall_get` — Retrieve
|
|
125
194
|
|
|
@@ -168,6 +237,18 @@ recall_sessions({ scope: "global", search: "deployment" })
|
|
|
168
237
|
| `primary` | `boolean` | `true` | Register tools as primary (available to all agents) |
|
|
169
238
|
| `global` | `boolean` | `true` | Allow cross-project search via `scope: "global"` |
|
|
170
239
|
|
|
240
|
+
Advanced limits (all have sensible defaults):
|
|
241
|
+
|
|
242
|
+
| Option | Default | Description |
|
|
243
|
+
| ---------------- | ------- | ----------------------- |
|
|
244
|
+
| `concurrency` | `3` | Parallel session loads |
|
|
245
|
+
| `maxSessions` | `50` | Max sessions per search |
|
|
246
|
+
| `maxResults` | `50` | Max results per search |
|
|
247
|
+
| `maxSessionList` | `100` | Max sessions in listing |
|
|
248
|
+
| `maxMessages` | `50` | Max messages per browse |
|
|
249
|
+
| `maxWindow` | `10` | Max context window size |
|
|
250
|
+
| `defaultWidth` | `200` | Default snippet width |
|
|
251
|
+
|
|
171
252
|
## How it works
|
|
172
253
|
|
|
173
254
|
When OpenCode compacts a session, it doesn't delete anything. Tool outputs get a `compacted` timestamp and are replaced with placeholder text in the LLM's context — but the original data stays in the database. Messages before a compaction boundary are skipped when building the LLM context — but they're still there.
|
|
@@ -179,6 +260,24 @@ This plugin reads all of it through the OpenCode SDK:
|
|
|
179
260
|
- Sessions scanned newest-first with bounded concurrency
|
|
180
261
|
- Respects abort signals for long-running searches
|
|
181
262
|
- Cross-project search enabled by default (disable with `global: false`)
|
|
263
|
+
- Smart and fuzzy ranking works across all scopes — session, project, and global
|
|
264
|
+
|
|
265
|
+
### Smart/fuzzy pipeline
|
|
266
|
+
|
|
267
|
+
When `match` is `"smart"` or `"fuzzy"`, the search goes through a multi-stage ranking pipeline:
|
|
268
|
+
|
|
269
|
+
1. **Candidate construction** — Messages are scanned newest-first. Each part's searchable text is extracted and tokenized. Per-session and global budgets cap the candidate pool.
|
|
270
|
+
2. **Prefiltering** — Cheap lexical gate using exact substring, quoted phrase, token overlap, and bounded edit-distance (Levenshtein ≤ 1 for tokens ≥ 4 chars). Only candidates with at least one match survive.
|
|
271
|
+
3. **Normalization** — Surviving candidates get full stage-2 normalization (camelCase splitting, separator normalization, whitespace collapse) for Fuse.js field matching.
|
|
272
|
+
4. **Fuse.js ranking** — Weighted search across primary text (0.65), project directory (0.20), session title (0.10), and tool name (0.05). Returns all matches above the mode threshold.
|
|
273
|
+
5. **Structural re-ranking** — Fuse scores are adjusted with deterministic boosts (exact phrase, full token coverage, reasoning traces, error text, user role, recency) and penalties (weak single-token fuzzy, poor coverage).
|
|
274
|
+
6. **Snippet selection** — Token-density sliding window finds the most relevant excerpt from the raw text.
|
|
275
|
+
|
|
276
|
+
The entire pipeline runs within a 2-second post-fetch time budget. If the pre-Fuse stage alone exceeds 1.5 seconds, Fuse.js is skipped and prefilter-ranked results are returned with `degradeKind: "time"`. If the full pipeline completes but exceeds the total budget, Fuse-ranked results are still returned but marked as time-degraded.
|
|
277
|
+
|
|
278
|
+
## Contributing
|
|
279
|
+
|
|
280
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for architecture details, module guide, and development setup.
|
|
182
281
|
|
|
183
282
|
## License
|
|
184
283
|
|
|
@@ -739,9 +739,9 @@ function smartSnippet(rawText, query, width = 200) {
|
|
|
739
739
|
}
|
|
740
740
|
|
|
741
741
|
// src/search.ts
|
|
742
|
-
var PROMOTED_SCOPES = /* @__PURE__ */ new Set(["session"]);
|
|
743
742
|
var TIME_BUDGET_MS = 2e3;
|
|
744
743
|
var PREFUSE_BUDGET_MS = 1500;
|
|
744
|
+
var MAX_GROUPED_LITERAL_RESULTS = 1e3;
|
|
745
745
|
function meta(s) {
|
|
746
746
|
return { id: s.id, title: s.title, directory: s.directory };
|
|
747
747
|
}
|
|
@@ -784,7 +784,7 @@ function scan(messages2, session, query, type, role, limit, before, after, width
|
|
|
784
784
|
}
|
|
785
785
|
return { results, total };
|
|
786
786
|
}
|
|
787
|
-
function smartScan(allMessages, query, type, role,
|
|
787
|
+
function smartScan(allMessages, query, type, role, explain, mode, before, after, width) {
|
|
788
788
|
const pq = parseQuery(query);
|
|
789
789
|
const startTime = performance.now();
|
|
790
790
|
const allCandidates = [];
|
|
@@ -821,16 +821,10 @@ function smartScan(allMessages, query, type, role, limit, explain, mode, before,
|
|
|
821
821
|
const prefuseTime = performance.now() - startTime;
|
|
822
822
|
if (prefuseTime > PREFUSE_BUDGET_MS) {
|
|
823
823
|
const ranked2 = rankDegraded(filtered, pq, explain);
|
|
824
|
-
const
|
|
825
|
-
ranked2.slice(0, limit),
|
|
826
|
-
mode,
|
|
827
|
-
explain,
|
|
828
|
-
pq,
|
|
829
|
-
width
|
|
830
|
-
);
|
|
824
|
+
const results = rankedToSearchResults(ranked2, mode, explain, pq, width);
|
|
831
825
|
return {
|
|
832
|
-
results
|
|
833
|
-
total:
|
|
826
|
+
results,
|
|
827
|
+
total: results.length,
|
|
834
828
|
degradeKind: "time",
|
|
835
829
|
matchMode: mode
|
|
836
830
|
};
|
|
@@ -842,32 +836,18 @@ function smartScan(allMessages, query, type, role, limit, explain, mode, before,
|
|
|
842
836
|
const hits = fuseSearch(fuseCandidates, pq, mode);
|
|
843
837
|
const totalTime = performance.now() - startTime;
|
|
844
838
|
const ranked = rank(hits, pq, explain);
|
|
845
|
-
const
|
|
839
|
+
const allResults = rankedToSearchResults(ranked, mode, explain, pq, width);
|
|
846
840
|
if (totalTime > TIME_BUDGET_MS) {
|
|
847
|
-
const results2 = rankedToSearchResults(
|
|
848
|
-
ranked.slice(0, limit),
|
|
849
|
-
mode,
|
|
850
|
-
explain,
|
|
851
|
-
pq,
|
|
852
|
-
width
|
|
853
|
-
);
|
|
854
841
|
return {
|
|
855
|
-
results:
|
|
856
|
-
total:
|
|
842
|
+
results: allResults,
|
|
843
|
+
total: allResults.length,
|
|
857
844
|
degradeKind: "time",
|
|
858
845
|
matchMode: mode
|
|
859
846
|
};
|
|
860
847
|
}
|
|
861
|
-
const results = rankedToSearchResults(
|
|
862
|
-
ranked.slice(0, limit),
|
|
863
|
-
mode,
|
|
864
|
-
explain,
|
|
865
|
-
pq,
|
|
866
|
-
width
|
|
867
|
-
);
|
|
868
848
|
return {
|
|
869
|
-
results,
|
|
870
|
-
total:
|
|
849
|
+
results: allResults,
|
|
850
|
+
total: allResults.length,
|
|
871
851
|
degradeKind: anyBudgetHit ? "budget" : "none",
|
|
872
852
|
matchMode: mode
|
|
873
853
|
};
|
|
@@ -898,32 +878,62 @@ function rankedToSearchResults(ranked, mode, explain, query, width) {
|
|
|
898
878
|
return result;
|
|
899
879
|
});
|
|
900
880
|
}
|
|
881
|
+
function groupBySession(results) {
|
|
882
|
+
const groups = /* @__PURE__ */ new Map();
|
|
883
|
+
for (const r of results) {
|
|
884
|
+
const existing = groups.get(r.sessionID);
|
|
885
|
+
if (!existing) {
|
|
886
|
+
groups.set(r.sessionID, { best: r, count: 1 });
|
|
887
|
+
} else {
|
|
888
|
+
existing.count++;
|
|
889
|
+
if (r.score != null && existing.best.score != null) {
|
|
890
|
+
if (r.score > existing.best.score) {
|
|
891
|
+
existing.best = r;
|
|
892
|
+
}
|
|
893
|
+
} else if (r.time > existing.best.time) {
|
|
894
|
+
existing.best = r;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
return [...groups.values()].map(({ best, count }) => ({
|
|
899
|
+
...best,
|
|
900
|
+
hitCount: count
|
|
901
|
+
}));
|
|
902
|
+
}
|
|
901
903
|
function search(client, unscoped, global, limits) {
|
|
902
904
|
return tool2({
|
|
903
905
|
description: `Search your conversation history in the opencode database. This is the primary discovery tool \u2014 use it before recall_sessions, which only searches titles. Before debugging an issue or implementing a feature, check whether prior sessions already tackled it \u2014 the history shows whether an approach succeeded or was abandoned. If you have access to a memory system, add useful findings to memory so they're available directly next time without searching history.
|
|
904
906
|
|
|
905
|
-
|
|
907
|
+
Supports three matching strategies via the \`match\` parameter:
|
|
908
|
+
- "literal" (default): case-insensitive substring matching. Works across all scopes. Fast and predictable.
|
|
909
|
+
- "smart": fuzzy ranked search using Fuse.js. Handles typos, separator differences (rate-limit vs rateLimit), and ranks results by relevance. Works across all scopes.
|
|
910
|
+
- "fuzzy": looser fuzzy search with a higher match threshold. Works across all scopes.
|
|
911
|
+
|
|
912
|
+
When using smart or fuzzy, results include a relevance \`score\` (0-1, higher is better) and \`matchedTerms\`. Add \`explain: true\` for detailed scoring breakdowns via \`matchReasons\`. If smart/fuzzy finds no matches, it automatically falls back to literal search.
|
|
906
913
|
|
|
907
|
-
|
|
914
|
+
Use \`group: "session"\` to collapse results by session \u2014 returns one entry per session with the best-scoring hit as representative (or most recent for literal), plus a \`hitCount\` showing how many part-level hits that session had. Useful for cross-project discovery: "which sessions are about this topic?"
|
|
915
|
+
|
|
916
|
+
Searches globally by default \u2014 this is fast and finds results across all projects. Results are ordered by session recency (newest first) for literal, or by relevance score for smart/fuzzy. Try multiple query terms before concluding no prior work exists. Use role "user" to find original requirements.
|
|
908
917
|
|
|
909
918
|
Scope costs: all scopes scan up to \`sessions\` sessions (default 10). "session" scans 1. "project" and "global" scan up to 10 newest. Increase \`sessions\` if nothing found.
|
|
910
919
|
|
|
911
920
|
Returns { ok, results: [{ sessionID, messageID, role, time, partID, partType, pruned, snippet, toolName? }], scanned, total, truncated }. Each result includes a pruned flag \u2014 if true, the content was compacted from your context window and recall_get will return the original full output. Check truncated to know if more matches exist beyond your results limit.
|
|
912
921
|
|
|
913
|
-
This tool's own outputs are excluded from search results to prevent recursive noise; use recall_get or recall_context to retrieve any message directly
|
|
914
|
-
|
|
915
|
-
Use match:"smart" for fuzzy search when exact wording is uncertain \u2014 it handles typos, separator differences (rate-limit vs rateLimit), and ranks results by relevance. Currently available for scope:"session" only.`,
|
|
922
|
+
This tool's own outputs are excluded from search results to prevent recursive noise; use recall_get or recall_context to retrieve any message directly.`,
|
|
916
923
|
args: {
|
|
917
924
|
query: tool2.schema.string().min(1).describe("Text to search for (case-insensitive substring match)"),
|
|
918
925
|
scope: tool2.schema.enum(["session", "project", "global"]).default("global").describe(
|
|
919
926
|
"global = all projects (default), project = current project, session = current only. Searching broadly is fast."
|
|
920
927
|
),
|
|
921
928
|
match: tool2.schema.enum(["literal", "smart", "fuzzy"]).default("literal").describe(
|
|
922
|
-
'Matching strategy: "literal" = exact substring (default), "smart" = fuzzy ranked search
|
|
929
|
+
'Matching strategy: "literal" = exact substring (default), "smart" = fuzzy ranked search, "fuzzy" = looser fuzzy search. All work across all scopes.'
|
|
923
930
|
),
|
|
924
931
|
explain: tool2.schema.boolean().default(false).describe(
|
|
925
932
|
"Return scoring metadata for debugging. Adds matchReasons to each result."
|
|
926
933
|
),
|
|
934
|
+
group: tool2.schema.enum(["part", "session"]).default("part").describe(
|
|
935
|
+
'"part" (default) = one result per matching part. "session" = collapse by session, returning one entry per session with best-scoring (smart/fuzzy) or most-recent (literal) hit and a hitCount.'
|
|
936
|
+
),
|
|
927
937
|
sessionID: tool2.schema.string().optional().describe("Search a specific session (overrides scope)"),
|
|
928
938
|
type: tool2.schema.enum(["text", "tool", "reasoning", "all"]).default("all").describe("Filter by part type"),
|
|
929
939
|
role: tool2.schema.enum(["user", "assistant", "all"]).default("all").describe("Filter by message role"),
|
|
@@ -947,16 +957,6 @@ Use match:"smart" for fuzzy search when exact wording is uncertain \u2014 it han
|
|
|
947
957
|
ctx.metadata({
|
|
948
958
|
title: `Searching ${args.scope} for "${args.query}"${matchMode !== "literal" ? ` (${matchMode})` : ""}`
|
|
949
959
|
});
|
|
950
|
-
if (matchMode !== "literal") {
|
|
951
|
-
const effectiveScope = args.sessionID ? "session" : args.scope;
|
|
952
|
-
if (!PROMOTED_SCOPES.has(effectiveScope)) {
|
|
953
|
-
const err = {
|
|
954
|
-
ok: false,
|
|
955
|
-
error: `match:"${matchMode}" is not yet available for scope:"${effectiveScope}". Try scope:"session" or use match:"literal" for broader searches.`
|
|
956
|
-
};
|
|
957
|
-
return JSON.stringify(err);
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
960
|
if (args.scope === "global" && !args.sessionID && !global) {
|
|
961
961
|
const err = {
|
|
962
962
|
ok: false,
|
|
@@ -1046,16 +1046,18 @@ Use match:"smart" for fuzzy search when exact wording is uncertain \u2014 it han
|
|
|
1046
1046
|
const err = { ok: false, error: "aborted" };
|
|
1047
1047
|
return JSON.stringify(err);
|
|
1048
1048
|
}
|
|
1049
|
-
|
|
1049
|
+
const groupMode = args.group;
|
|
1050
|
+
const isGrouped = groupMode === "session";
|
|
1051
|
+
const literalScan = (scanLimit) => {
|
|
1050
1052
|
const collected = [];
|
|
1051
1053
|
let total = 0;
|
|
1052
1054
|
let early = false;
|
|
1053
1055
|
for (const { session: sess, messages: msgs } of allLoaded) {
|
|
1054
|
-
if (collected.length >=
|
|
1056
|
+
if (collected.length >= scanLimit) {
|
|
1055
1057
|
early = true;
|
|
1056
1058
|
break;
|
|
1057
1059
|
}
|
|
1058
|
-
const remaining =
|
|
1060
|
+
const remaining = scanLimit - collected.length;
|
|
1059
1061
|
const result = scan(
|
|
1060
1062
|
msgs,
|
|
1061
1063
|
sess,
|
|
@@ -1070,16 +1072,44 @@ Use match:"smart" for fuzzy search when exact wording is uncertain \u2014 it han
|
|
|
1070
1072
|
collected.push(...result.results);
|
|
1071
1073
|
total += result.total;
|
|
1072
1074
|
}
|
|
1073
|
-
|
|
1075
|
+
return { collected, total, early };
|
|
1076
|
+
};
|
|
1077
|
+
const applyGroupAndSlice = (results, partTotal, earlyExit) => {
|
|
1078
|
+
if (isGrouped) {
|
|
1079
|
+
const grouped = groupBySession(results);
|
|
1080
|
+
const final3 = grouped.slice(0, args.results);
|
|
1081
|
+
return {
|
|
1082
|
+
final: final3,
|
|
1083
|
+
total: grouped.length,
|
|
1084
|
+
truncated: earlyExit || grouped.length > final3.length
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
const final2 = results.slice(0, args.results);
|
|
1088
|
+
return {
|
|
1089
|
+
final: final2,
|
|
1090
|
+
total: partTotal,
|
|
1091
|
+
truncated: earlyExit || partTotal > final2.length
|
|
1092
|
+
};
|
|
1093
|
+
};
|
|
1094
|
+
if (matchMode === "literal") {
|
|
1095
|
+
const limit = isGrouped ? MAX_GROUPED_LITERAL_RESULTS : args.results;
|
|
1096
|
+
const { collected, total, early } = literalScan(limit);
|
|
1097
|
+
const {
|
|
1098
|
+
final: final2,
|
|
1099
|
+
total: outTotal2,
|
|
1100
|
+
truncated: truncated2
|
|
1101
|
+
} = applyGroupAndSlice(collected, total, early);
|
|
1102
|
+
const unit2 = isGrouped ? "session" : "result";
|
|
1074
1103
|
ctx.metadata({
|
|
1075
|
-
title: `Found ${
|
|
1104
|
+
title: `Found ${final2.length} ${unit2}${final2.length !== 1 ? "s" : ""} for "${args.query}" (${scanned} session${scanned !== 1 ? "s" : ""} searched)`
|
|
1076
1105
|
});
|
|
1077
1106
|
const out2 = {
|
|
1078
1107
|
ok: true,
|
|
1079
|
-
results:
|
|
1108
|
+
results: final2,
|
|
1080
1109
|
scanned,
|
|
1081
|
-
total,
|
|
1082
|
-
truncated:
|
|
1110
|
+
total: outTotal2,
|
|
1111
|
+
truncated: truncated2,
|
|
1112
|
+
group: groupMode
|
|
1083
1113
|
};
|
|
1084
1114
|
return JSON.stringify(out2);
|
|
1085
1115
|
}
|
|
@@ -1088,7 +1118,6 @@ Use match:"smart" for fuzzy search when exact wording is uncertain \u2014 it han
|
|
|
1088
1118
|
args.query,
|
|
1089
1119
|
args.type,
|
|
1090
1120
|
args.role,
|
|
1091
|
-
args.results,
|
|
1092
1121
|
args.explain,
|
|
1093
1122
|
matchMode,
|
|
1094
1123
|
args.before,
|
|
@@ -1096,57 +1125,49 @@ Use match:"smart" for fuzzy search when exact wording is uncertain \u2014 it han
|
|
|
1096
1125
|
args.width
|
|
1097
1126
|
);
|
|
1098
1127
|
if (smartResult.results.length === 0) {
|
|
1099
|
-
const
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
const
|
|
1108
|
-
const result = scan(
|
|
1109
|
-
msgs,
|
|
1110
|
-
sess,
|
|
1111
|
-
args.query,
|
|
1112
|
-
args.type,
|
|
1113
|
-
args.role,
|
|
1114
|
-
remaining,
|
|
1115
|
-
args.before,
|
|
1116
|
-
args.after,
|
|
1117
|
-
args.width
|
|
1118
|
-
);
|
|
1119
|
-
collected.push(...result.results);
|
|
1120
|
-
total += result.total;
|
|
1121
|
-
}
|
|
1122
|
-
const final = collected.slice(0, args.results);
|
|
1123
|
-
if (final.length > 0) {
|
|
1128
|
+
const limit = isGrouped ? MAX_GROUPED_LITERAL_RESULTS : args.results;
|
|
1129
|
+
const { collected, total, early } = literalScan(limit);
|
|
1130
|
+
const {
|
|
1131
|
+
final: final2,
|
|
1132
|
+
total: outTotal2,
|
|
1133
|
+
truncated: truncated2
|
|
1134
|
+
} = applyGroupAndSlice(collected, total, early);
|
|
1135
|
+
if (final2.length > 0) {
|
|
1136
|
+
const unit2 = isGrouped ? "session" : "result";
|
|
1124
1137
|
ctx.metadata({
|
|
1125
|
-
title: `Found ${
|
|
1138
|
+
title: `Found ${final2.length} ${unit2}${final2.length !== 1 ? "s" : ""} for "${args.query}" (literal fallback, ${scanned} session${scanned !== 1 ? "s" : ""})`
|
|
1126
1139
|
});
|
|
1127
1140
|
const out2 = {
|
|
1128
1141
|
ok: true,
|
|
1129
|
-
results:
|
|
1142
|
+
results: final2,
|
|
1130
1143
|
scanned,
|
|
1131
|
-
total,
|
|
1132
|
-
truncated:
|
|
1144
|
+
total: outTotal2,
|
|
1145
|
+
truncated: truncated2,
|
|
1133
1146
|
matchMode: "literal",
|
|
1134
|
-
degradeKind: "fallback"
|
|
1147
|
+
degradeKind: "fallback",
|
|
1148
|
+
group: groupMode
|
|
1135
1149
|
};
|
|
1136
1150
|
return JSON.stringify(out2);
|
|
1137
1151
|
}
|
|
1138
1152
|
}
|
|
1153
|
+
const {
|
|
1154
|
+
final,
|
|
1155
|
+
total: outTotal,
|
|
1156
|
+
truncated
|
|
1157
|
+
} = applyGroupAndSlice(smartResult.results, smartResult.total, false);
|
|
1158
|
+
const unit = isGrouped ? "session" : "result";
|
|
1139
1159
|
ctx.metadata({
|
|
1140
|
-
title: `Found ${
|
|
1160
|
+
title: `Found ${final.length} ${unit}${final.length !== 1 ? "s" : ""} for "${args.query}" (${matchMode}, ${scanned} session${scanned !== 1 ? "s" : ""})`
|
|
1141
1161
|
});
|
|
1142
1162
|
const out = {
|
|
1143
1163
|
ok: true,
|
|
1144
|
-
results:
|
|
1164
|
+
results: final,
|
|
1145
1165
|
scanned,
|
|
1146
|
-
total:
|
|
1147
|
-
truncated
|
|
1166
|
+
total: outTotal,
|
|
1167
|
+
truncated,
|
|
1148
1168
|
matchMode: smartResult.matchMode,
|
|
1149
|
-
degradeKind: smartResult.degradeKind
|
|
1169
|
+
degradeKind: smartResult.degradeKind,
|
|
1170
|
+
group: groupMode
|
|
1150
1171
|
};
|
|
1151
1172
|
return JSON.stringify(out);
|
|
1152
1173
|
} catch (e) {
|
package/dist/search.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../src/search.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,cAAc,EAEpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EACV,cAAc,EAIf,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAKL,KAAK,MAAM,
|
|
1
|
+
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../src/search.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,cAAc,EAEpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EACV,cAAc,EAIf,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAKL,KAAK,MAAM,EAIZ,MAAM,YAAY,CAAC;AA+RpB,wBAAgB,MAAM,CACpB,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,cAAc,EACxB,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,MAAM,GACb,cAAc,CAgXhB"}
|
package/dist/types.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export type Limits = {
|
|
|
11
11
|
export declare const DEFAULTS: Limits;
|
|
12
12
|
export type MatchMode = "literal" | "smart" | "fuzzy";
|
|
13
13
|
export type DegradeKind = "none" | "time" | "budget" | "fallback";
|
|
14
|
+
export type GroupMode = "part" | "session";
|
|
14
15
|
export type SearchResult = {
|
|
15
16
|
sessionID: string;
|
|
16
17
|
sessionTitle: string;
|
|
@@ -31,6 +32,8 @@ export type SearchResult = {
|
|
|
31
32
|
matchedTerms?: string[];
|
|
32
33
|
/** Present when explain=true */
|
|
33
34
|
matchReasons?: string[];
|
|
35
|
+
/** Present when group:"session" — number of part-level hits in this session */
|
|
36
|
+
hitCount?: number;
|
|
34
37
|
};
|
|
35
38
|
export type SearchOutput = {
|
|
36
39
|
ok: true;
|
|
@@ -42,6 +45,8 @@ export type SearchOutput = {
|
|
|
42
45
|
matchMode?: MatchMode;
|
|
43
46
|
/** What happened during ranking */
|
|
44
47
|
degradeKind?: DegradeKind;
|
|
48
|
+
/** Which grouping was applied */
|
|
49
|
+
group?: GroupMode;
|
|
45
50
|
};
|
|
46
51
|
export type MessageOutput = {
|
|
47
52
|
ok: true;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,KAAK,2FAMR,CAAC;AAEX,MAAM,MAAM,MAAM,GAAG;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,eAAO,MAAM,QAAQ,EAAE,MAQtB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,CAAC;AACtD,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ,GAAG,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,KAAK,2FAMR,CAAC;AAEX,MAAM,MAAM,MAAM,GAAG;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,eAAO,MAAM,QAAQ,EAAE,MAQtB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,CAAC;AACtD,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ,GAAG,UAAU,CAAC;AAClE,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,SAAS,CAAC;AAE3C,MAAM,MAAM,YAAY,GAAG;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sCAAsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,sCAAsC;IACtC,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,gCAAgC;IAChC,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,EAAE,EAAE,IAAI,CAAC;IACT,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,OAAO,CAAC;IACnB,mDAAmD;IACnD,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,mCAAmC;IACnC,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,iCAAiC;IACjC,KAAK,CAAC,EAAE,SAAS,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,EAAE,EAAE,IAAI,CAAC;IACT,OAAO,EAAE;QACP,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;QAC3B,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,OAAO,EAAE;QACP,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE;QACP,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;QAC3B,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,EAAE,EAAE,IAAI,CAAC;IACT,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,OAAO,EAAE;QACP,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;IACF,aAAa,EAAE,OAAO,CAAC;IACvB,YAAY,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,EAAE,EAAE,IAAI,CAAC;IACT,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,OAAO,EAAE;QACP,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;IACF,UAAU,EAAE;QACV,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;QACjB,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,OAAO,CAAC;KAClB,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9C,IAAI,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3C,QAAQ,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,EAAE,EAAE,IAAI,CAAC;IACT,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,EAAE,EAAE,KAAK,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,wBAAgB,MAAM,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAYzC"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "opencode-session-recall",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.9.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Everything your agent ever did is already in the database — this plugin lets it look",
|
|
7
7
|
"main": "./dist/opencode-session-recall.js",
|