opencode-session-recall 0.2.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 ADDED
@@ -0,0 +1,146 @@
1
+ # opencode-session-recall
2
+
3
+ **Give your agent a memory that survives compaction — without building another memory system.**
4
+
5
+ Most agent "memory" solutions add a new subsystem: vector databases, embedding pipelines, separate knowledge stores. They duplicate your data into yet another place that needs to be maintained, synced, and debugged.
6
+
7
+ `opencode-session-recall` takes a different approach: **your conversation history is already the richest source of context you have.** opencode stores every message, every tool output, every reasoning trace in its database — even after compaction prunes them from the agent's context window. This plugin simply gives the agent tools to search and retrieve what's already there.
8
+
9
+ No embeddings. No vector store. No data duplication. Just direct access to the context your agent already generated.
10
+
11
+ ## What this enables
12
+
13
+ **"We already solved this."** The agent searches its own history and finds the solution from 2 hours ago that got compacted away, instead of solving the same problem again.
14
+
15
+ **"How did we do it in that other project?"** Cross-project search finds the JWT middleware implementation from the auth project, the Docker config from the deployment project, the test patterns from the API project.
16
+
17
+ **"What did you originally ask for?"** After 50+ tool calls and 3 compactions, the agent can pull up the user's exact original requirements to make sure it's still on track.
18
+
19
+ **"What was that error?"** The full stack trace from the tool output that got pruned is still there. The agent retrieves it instead of reproducing the error.
20
+
21
+ **"Show me what happened."** Browse any session chronologically, play back the conversation, understand the narrative of how a problem was investigated and solved.
22
+
23
+ ## Install
24
+
25
+ ```jsonc
26
+ {
27
+ "plugin": [
28
+ "opencode-session-recall",
29
+
30
+ // Enable cross-project search
31
+ ["opencode-session-recall", { "global": true }],
32
+ ],
33
+ }
34
+ ```
35
+
36
+ ## Tools
37
+
38
+ Five tools, designed around how agents actually navigate conversation history:
39
+
40
+ ### `recall` — Search
41
+
42
+ The primary tool. Full-text search across text, tool outputs, tool inputs, reasoning, and subtask descriptions. Searches the current session by default, or widen to all project sessions or all sessions globally.
43
+
44
+ ```
45
+ recall({ query: "authentication", scope: "project" })
46
+ recall({ query: "error", after: <2 days ago>, type: "tool" })
47
+ recall({ query: "JWT", sessionID: "ses_from_another_project" })
48
+ ```
49
+
50
+ | Param | Default | Description |
51
+ | ---------------- | ----------- | --------------------------------------------- |
52
+ | `query` | required | Text to search for (case-insensitive) |
53
+ | `scope` | `"session"` | `"session"`, `"project"`, or `"global"` |
54
+ | `sessionID` | — | Target a specific session (overrides scope) |
55
+ | `type` | `"all"` | `"text"`, `"tool"`, `"reasoning"`, or `"all"` |
56
+ | `role` | `"all"` | `"user"`, `"assistant"`, or `"all"` |
57
+ | `before`/`after` | — | Timestamp filters (ms epoch) |
58
+ | `width` | `200` | Snippet size (50-1000 chars) |
59
+ | `sessions` | `10` | Max sessions to scan |
60
+ | `results` | `10` | Max results to return |
61
+
62
+ ### `recall_get` — Retrieve
63
+
64
+ Get the full content of a specific message, including all parts. Tool outputs are returned in their original form, even if they were pruned from context. Use after `recall` finds something interesting.
65
+
66
+ ```
67
+ recall_get({ sessionID: "ses_abc", messageID: "msg_def" })
68
+ ```
69
+
70
+ ### `recall_context` — Expand
71
+
72
+ Get a window of messages around a specific message. After `recall` finds a match, see what was asked before it and what happened after. Supports symmetric and asymmetric windows.
73
+
74
+ ```
75
+ recall_context({ sessionID: "ses_abc", messageID: "msg_def", window: 3 })
76
+ recall_context({ sessionID: "ses_abc", messageID: "msg_def", before: 1, after: 5 })
77
+ ```
78
+
79
+ Returns `hasMoreBefore`/`hasMoreAfter` so the agent knows if it's at a boundary.
80
+
81
+ ### `recall_messages` — Browse
82
+
83
+ Paginated message browsing. Walk through a session chronologically, read the beginning, check the most recent messages, or filter by role. Also supports content filtering to combine search and pagination.
84
+
85
+ ```
86
+ recall_messages({ limit: 5, role: "user", reverse: true })
87
+ recall_messages({ sessionID: "ses_abc", offset: 10, limit: 10 })
88
+ recall_messages({ query: "npm", role: "user", reverse: true })
89
+ ```
90
+
91
+ Defaults to the current session. Pagination metadata includes `total`, `hasMore`, and `offset`.
92
+
93
+ ### `recall_sessions` — Discover
94
+
95
+ List sessions by title. The starting point for cross-session and cross-project work.
96
+
97
+ ```
98
+ recall_sessions({ scope: "project", search: "auth" })
99
+ recall_sessions({ scope: "global", search: "deployment" })
100
+ ```
101
+
102
+ ## Real-world workflow
103
+
104
+ This is what it actually looks like when an agent uses these tools to answer "what have we been doing with our UniFi network?" across a 3-week, 600+ message session in a different project:
105
+
106
+ ```
107
+ 1. recall_sessions({ scope: "global", search: "unifi" })
108
+ → discovers the ubiopti project session
109
+
110
+ 2. recall_messages({ sessionID: "...", limit: 5, role: "user", reverse: true })
111
+ → reads the most recent user messages to understand current state
112
+
113
+ 3. recall({ query: "kickout threshold", sessionID: "...", width: 500 })
114
+ → finds the technical root cause analysis in tool outputs
115
+
116
+ 4. recall_context({ sessionID: "...", messageID: "...", window: 3 })
117
+ → expands around the Ubiquiti support chat to see the full interaction
118
+
119
+ 5. recall({ query: "iwpriv", sessionID: "...", after: <recent timestamp> })
120
+ → finds only recent mentions, not the whole session history
121
+ ```
122
+
123
+ Five tool calls, complete narrative reconstructed across projects.
124
+
125
+ ## Options
126
+
127
+ | Option | Type | Default | Description |
128
+ | --------- | --------- | ------- | --------------------------------------------------- |
129
+ | `primary` | `boolean` | `true` | Register tools as primary (available to all agents) |
130
+ | `global` | `boolean` | `false` | Enable cross-project search via `scope: "global"` |
131
+
132
+ ## How it works
133
+
134
+ This plugin doesn't create a separate memory store. It reads what opencode already has.
135
+
136
+ 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. The plugin accesses all of this through the opencode SDK.
137
+
138
+ - Uses the opencode SDK client (no direct database queries, no separate storage)
139
+ - Zero setup — no embeddings to generate, no indexes to build, no data to sync
140
+ - Sessions are scanned newest-first with bounded concurrency
141
+ - Respects abort signals for long-running searches
142
+ - Global scope is disabled by default
143
+
144
+ ## License
145
+
146
+ MIT
@@ -0,0 +1,4 @@
1
+ import { type ToolDefinition } from "@opencode-ai/plugin";
2
+ import type { OpencodeClient } from "@opencode-ai/sdk/v2";
3
+ export declare function context(client: OpencodeClient): ToolDefinition;
4
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,cAAc,EAEpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAI1D,wBAAgB,OAAO,CAAC,MAAM,EAAE,cAAc,GAAG,cAAc,CAuG9D"}
@@ -0,0 +1,12 @@
1
+ import type { Part, Message } from "@opencode-ai/sdk/v2";
2
+ import type { PartOutput, MessageItem } from "./types.js";
3
+ export declare function matches(text: string, query: string): boolean;
4
+ export declare function searchable(part: Part): string[];
5
+ export declare function snippet(text: string, query: string, width?: number): string;
6
+ export declare function pruned(part: Part): boolean;
7
+ export declare function format(part: Part): PartOutput;
8
+ export declare function formatMsg(msg: {
9
+ info: Message;
10
+ parts: Array<Part>;
11
+ }): MessageItem;
12
+ //# sourceMappingURL=extract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract.d.ts","sourceRoot":"","sources":["../src/extract.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,IAAI,EAEJ,OAAO,EAGR,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAW1D,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAE5D;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,EAAE,CA2B/C;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,SAAM,GAAG,MAAM,CAexE;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAI1C;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,IAAI,GAAG,UAAU,CA0D7C;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE;IAC7B,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;CACpB,GAAG,WAAW,CAgBd"}
package/dist/get.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { type ToolDefinition } from "@opencode-ai/plugin";
2
+ import type { OpencodeClient } from "@opencode-ai/sdk/v2";
3
+ export declare function get(client: OpencodeClient): ToolDefinition;
4
+ //# sourceMappingURL=get.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get.d.ts","sourceRoot":"","sources":["../src/get.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,cAAc,EAEpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAK1D,wBAAgB,GAAG,CAAC,MAAM,EAAE,cAAc,GAAG,cAAc,CA0D1D"}
@@ -0,0 +1,7 @@
1
+ import type { Plugin } from "@opencode-ai/plugin";
2
+ declare const _default: {
3
+ id: string;
4
+ server: Plugin;
5
+ };
6
+ export default _default;
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;;;;;AAkFlD,wBAGE"}
package/dist/index.js ADDED
@@ -0,0 +1,726 @@
1
+ // src/index.ts
2
+ import { createOpencodeClient } from "@opencode-ai/sdk/v2";
3
+
4
+ // src/sessions.ts
5
+ import {
6
+ tool
7
+ } from "@opencode-ai/plugin";
8
+
9
+ // src/types.ts
10
+ function errmsg(e) {
11
+ if (e instanceof Error) return e.message;
12
+ if (typeof e === "string") return e;
13
+ if (e && typeof e === "object" && "data" in e) {
14
+ const data = e.data;
15
+ if (data?.message) return data.message;
16
+ }
17
+ try {
18
+ return JSON.stringify(e);
19
+ } catch {
20
+ return String(e);
21
+ }
22
+ }
23
+
24
+ // src/sessions.ts
25
+ function sessions(client, unscoped, global) {
26
+ return tool({
27
+ description: `List sessions from the opencode database. Use this FIRST to discover which sessions exist, then search their content with recall. Returns session titles, directories, and timestamps. For cross-project discovery, use scope "global" (requires plugin option global: true).`,
28
+ args: {
29
+ scope: tool.schema.enum(["project", "global"]).default("project").describe("project = current project, global = all projects"),
30
+ search: tool.schema.string().optional().describe("Filter by session title"),
31
+ limit: tool.schema.number().min(1).max(100).default(20).describe("Max sessions to return")
32
+ },
33
+ async execute(args, ctx) {
34
+ ctx.metadata({
35
+ title: args.search ? `Listing ${args.scope} sessions matching "${args.search}"` : `Listing ${args.scope} sessions`
36
+ });
37
+ if (args.scope === "global" && !global) {
38
+ const err = {
39
+ ok: false,
40
+ error: "Global scope disabled. Enable via plugin option: global: true"
41
+ };
42
+ return JSON.stringify(err);
43
+ }
44
+ try {
45
+ const items = [];
46
+ if (args.scope === "global") {
47
+ const result = await unscoped.experimental.session.list({
48
+ search: args.search,
49
+ limit: args.limit
50
+ });
51
+ if (result.error) {
52
+ const err = {
53
+ ok: false,
54
+ error: `Failed to list sessions: ${errmsg(result.error)}`
55
+ };
56
+ return JSON.stringify(err);
57
+ }
58
+ if (result.data) {
59
+ for (const s of result.data) {
60
+ items.push({
61
+ id: s.id,
62
+ title: s.title,
63
+ directory: s.directory,
64
+ project: s.project ? { name: s.project.name, worktree: s.project.worktree } : void 0,
65
+ time: { created: s.time.created, updated: s.time.updated },
66
+ archived: s.time.archived != null
67
+ });
68
+ }
69
+ }
70
+ } else {
71
+ const result = await client.session.list({
72
+ search: args.search,
73
+ limit: args.limit
74
+ });
75
+ if (result.error) {
76
+ const err = {
77
+ ok: false,
78
+ error: `Failed to list sessions: ${errmsg(result.error)}`
79
+ };
80
+ return JSON.stringify(err);
81
+ }
82
+ if (result.data) {
83
+ for (const s of result.data) {
84
+ items.push({
85
+ id: s.id,
86
+ title: s.title,
87
+ directory: s.directory,
88
+ time: { created: s.time.created, updated: s.time.updated },
89
+ archived: s.time.archived != null
90
+ });
91
+ }
92
+ }
93
+ }
94
+ ctx.metadata({
95
+ title: `Found ${items.length} ${args.scope} sessions${args.search ? ` matching "${args.search}"` : ""}`
96
+ });
97
+ const out = {
98
+ ok: true,
99
+ sessions: items,
100
+ returned: items.length,
101
+ scope: args.scope
102
+ };
103
+ return JSON.stringify(out);
104
+ } catch (e) {
105
+ const err = { ok: false, error: errmsg(e) };
106
+ return JSON.stringify(err);
107
+ }
108
+ }
109
+ });
110
+ }
111
+
112
+ // src/search.ts
113
+ import {
114
+ tool as tool2
115
+ } from "@opencode-ai/plugin";
116
+
117
+ // src/extract.ts
118
+ var INPUT_SEARCH_LIMIT = 1e4;
119
+ function input(val) {
120
+ const raw = JSON.stringify(val);
121
+ return raw.length > INPUT_SEARCH_LIMIT ? raw.slice(0, INPUT_SEARCH_LIMIT) : raw;
122
+ }
123
+ function matches(text, query) {
124
+ return text.toLowerCase().includes(query.toLowerCase());
125
+ }
126
+ function searchable(part) {
127
+ switch (part.type) {
128
+ case "text":
129
+ case "reasoning":
130
+ return part.text ? [part.text] : [];
131
+ case "tool": {
132
+ const result = [];
133
+ const state = part.state;
134
+ if (state.status === "completed") {
135
+ if (state.output) result.push(state.output);
136
+ if (state.title) result.push(state.title);
137
+ if (state.input) result.push(input(state.input));
138
+ }
139
+ if (state.status === "error") {
140
+ if (state.error) result.push(state.error);
141
+ if (state.input) result.push(input(state.input));
142
+ }
143
+ if (state.status === "running" || state.status === "pending") {
144
+ if (state.input) result.push(input(state.input));
145
+ }
146
+ return result;
147
+ }
148
+ case "subtask":
149
+ return [part.description, part.prompt];
150
+ default:
151
+ return [];
152
+ }
153
+ }
154
+ function snippet(text, query, width = 200) {
155
+ const lower = text.toLowerCase();
156
+ const idx = lower.indexOf(query.toLowerCase());
157
+ if (idx === -1)
158
+ return text.slice(0, width) + (text.length > width ? "..." : "");
159
+ const half = Math.floor(width / 2);
160
+ let start = Math.max(0, idx - half);
161
+ let end = Math.min(text.length, start + width);
162
+ if (end - start < width && start > 0) start = Math.max(0, end - width);
163
+ let result = text.slice(start, end);
164
+ if (start > 0) result = "..." + result;
165
+ if (end < text.length) result = result + "...";
166
+ return result;
167
+ }
168
+ function pruned(part) {
169
+ if (part.type !== "tool") return false;
170
+ if (part.state.status !== "completed") return false;
171
+ return part.state.time.compacted != null;
172
+ }
173
+ function format(part) {
174
+ const base = { id: part.id, type: part.type, pruned: pruned(part) };
175
+ switch (part.type) {
176
+ case "text":
177
+ case "reasoning":
178
+ return { ...base, content: part.text };
179
+ case "tool": {
180
+ const state = part.state;
181
+ if (state.status === "completed")
182
+ return {
183
+ ...base,
184
+ toolName: part.tool,
185
+ title: state.title,
186
+ input: state.input,
187
+ output: state.output
188
+ };
189
+ if (state.status === "error")
190
+ return {
191
+ ...base,
192
+ toolName: part.tool,
193
+ input: state.input,
194
+ error: state.error
195
+ };
196
+ return {
197
+ ...base,
198
+ toolName: part.tool,
199
+ input: state.input
200
+ };
201
+ }
202
+ case "subtask":
203
+ return { ...base, content: `[subtask] ${part.description}` };
204
+ case "compaction":
205
+ return {
206
+ ...base,
207
+ content: `[compaction boundary${part.auto ? " (auto)" : ""}]`
208
+ };
209
+ case "file":
210
+ return { ...base, content: `[file] ${part.filename ?? part.url}` };
211
+ case "snapshot":
212
+ return { ...base, content: `[snapshot] ${part.snapshot}` };
213
+ case "patch":
214
+ return { ...base, content: `[patch] ${part.files.join(", ")}` };
215
+ case "agent":
216
+ return { ...base, content: `[agent] ${part.name}` };
217
+ case "retry":
218
+ return {
219
+ ...base,
220
+ content: `[retry] attempt ${part.attempt}`,
221
+ error: part.error.data.message
222
+ };
223
+ case "step-start":
224
+ return { ...base, content: "[step-start]" };
225
+ case "step-finish":
226
+ return { ...base, content: `[step-finish] ${part.reason}` };
227
+ default:
228
+ return { ...base, content: `[${part.type}]` };
229
+ }
230
+ }
231
+ function formatMsg(msg) {
232
+ const info = msg.info;
233
+ let model;
234
+ if (info.role === "assistant") model = info.modelID;
235
+ else model = info.model.modelID;
236
+ return {
237
+ message: {
238
+ id: info.id,
239
+ role: info.role,
240
+ time: info.time.created,
241
+ agent: info.agent,
242
+ model
243
+ },
244
+ parts: msg.parts.map(format)
245
+ };
246
+ }
247
+
248
+ // src/search.ts
249
+ var CONCURRENCY = 3;
250
+ function meta(s) {
251
+ return { id: s.id, title: s.title, directory: s.directory };
252
+ }
253
+ function scan(messages2, session, query, type, role, limit, before, after, width) {
254
+ const results = [];
255
+ let total = 0;
256
+ for (const msg of messages2) {
257
+ if (results.length >= limit) break;
258
+ const ts = msg.info.time.created;
259
+ if (before != null && ts >= before) continue;
260
+ if (after != null && ts <= after) continue;
261
+ if (role !== "all" && msg.info.role !== role) continue;
262
+ for (const part of msg.parts) {
263
+ if (results.length >= limit) break;
264
+ if (type !== "all" && part.type !== type) continue;
265
+ const texts = searchable(part);
266
+ let matched = false;
267
+ for (const text of texts) {
268
+ if (!matches(text, query)) continue;
269
+ total++;
270
+ if (matched) continue;
271
+ matched = true;
272
+ if (results.length < limit) {
273
+ results.push({
274
+ sessionID: session.id,
275
+ sessionTitle: session.title,
276
+ directory: session.directory,
277
+ messageID: msg.info.id,
278
+ role: msg.info.role,
279
+ time: msg.info.time.created,
280
+ partID: part.id,
281
+ partType: part.type,
282
+ pruned: pruned(part),
283
+ snippet: snippet(text, query, width),
284
+ toolName: part.type === "tool" ? part.tool : void 0
285
+ });
286
+ }
287
+ }
288
+ }
289
+ }
290
+ return { results, total };
291
+ }
292
+ function search(client, unscoped, global) {
293
+ return tool2({
294
+ description: `Search your conversation history in the opencode database. Use this to recover context lost to compaction \u2014 original tool outputs, earlier messages, reasoning, and user instructions that were pruned from your context window.
295
+
296
+ Searches text content, tool inputs/outputs, and reasoning. Returns matching snippets with session/message IDs you can pass to recall_get for full content.
297
+
298
+ Start with scope "session" (fastest). Widen to "project" if not found. Use sessionID param to target a specific session found via recall_sessions. Use role "user" to find original requirements.`,
299
+ args: {
300
+ query: tool2.schema.string().min(1).describe("Text to search for (case-insensitive)"),
301
+ scope: tool2.schema.enum(["session", "project", "global"]).default("session").describe(
302
+ "session = current, project = all project sessions, global = all"
303
+ ),
304
+ sessionID: tool2.schema.string().optional().describe("Search a specific session (overrides scope)"),
305
+ type: tool2.schema.enum(["text", "tool", "reasoning", "all"]).default("all").describe("Filter by part type"),
306
+ role: tool2.schema.enum(["user", "assistant", "all"]).default("all").describe("Filter by message role"),
307
+ sessions: tool2.schema.number().min(1).max(50).default(10).describe("Max sessions to scan"),
308
+ results: tool2.schema.number().min(1).max(50).default(10).describe("Max results to return"),
309
+ title: tool2.schema.string().optional().describe("Filter sessions by title"),
310
+ before: tool2.schema.number().optional().describe("Only match messages before this timestamp (ms epoch)"),
311
+ after: tool2.schema.number().optional().describe("Only match messages after this timestamp (ms epoch)"),
312
+ width: tool2.schema.number().min(50).max(1e3).default(200).describe("Snippet width in characters")
313
+ },
314
+ async execute(args, ctx) {
315
+ ctx.metadata({ title: `Searching ${args.scope} for "${args.query}"` });
316
+ if (args.scope === "global" && !args.sessionID && !global) {
317
+ const err = {
318
+ ok: false,
319
+ error: "Global scope disabled. Enable via plugin option: global: true"
320
+ };
321
+ return JSON.stringify(err);
322
+ }
323
+ try {
324
+ let targets = [];
325
+ if (args.sessionID) {
326
+ let title = "";
327
+ let directory = "";
328
+ try {
329
+ const sess = await client.session.get({
330
+ sessionID: args.sessionID
331
+ });
332
+ if (sess.data) {
333
+ title = sess.data.title;
334
+ directory = sess.data.directory;
335
+ }
336
+ } catch {
337
+ }
338
+ targets = [{ id: args.sessionID, title, directory }];
339
+ } else if (args.scope === "session") {
340
+ let title = "";
341
+ let directory = "";
342
+ try {
343
+ const sess = await client.session.get({ sessionID: ctx.sessionID });
344
+ if (sess.data) {
345
+ title = sess.data.title;
346
+ directory = sess.data.directory;
347
+ }
348
+ } catch {
349
+ }
350
+ targets = [{ id: ctx.sessionID, title, directory }];
351
+ } else if (args.scope === "project") {
352
+ const resp = await client.session.list({
353
+ search: args.title,
354
+ limit: args.sessions
355
+ });
356
+ if (resp.error) {
357
+ const err = {
358
+ ok: false,
359
+ error: `Failed to list sessions: ${errmsg(resp.error)}`
360
+ };
361
+ return JSON.stringify(err);
362
+ }
363
+ if (resp.data) targets = resp.data.map(meta);
364
+ } else {
365
+ const resp = await unscoped.experimental.session.list({
366
+ search: args.title,
367
+ limit: args.sessions
368
+ });
369
+ if (resp.error) {
370
+ const err = {
371
+ ok: false,
372
+ error: `Failed to list sessions: ${errmsg(resp.error)}`
373
+ };
374
+ return JSON.stringify(err);
375
+ }
376
+ if (resp.data) targets = resp.data.map(meta);
377
+ }
378
+ const collected = [];
379
+ let scanned = 0;
380
+ let total = 0;
381
+ let early = false;
382
+ for (let i = 0; i < targets.length; i += CONCURRENCY) {
383
+ if (ctx.abort.aborted) {
384
+ early = true;
385
+ break;
386
+ }
387
+ if (collected.length >= args.results) {
388
+ early = true;
389
+ break;
390
+ }
391
+ const remaining = args.results - collected.length;
392
+ const batch = targets.slice(i, i + CONCURRENCY);
393
+ const loaded = await Promise.all(
394
+ batch.map(async (t) => {
395
+ try {
396
+ const resp = await client.session.messages({ sessionID: t.id });
397
+ return { session: t, messages: resp.data ?? [] };
398
+ } catch {
399
+ return { session: t, messages: [] };
400
+ }
401
+ })
402
+ );
403
+ for (const { session: sess, messages: msgs } of loaded) {
404
+ if (collected.length >= args.results) {
405
+ early = true;
406
+ break;
407
+ }
408
+ const result = scan(
409
+ msgs,
410
+ sess,
411
+ args.query,
412
+ args.type,
413
+ args.role,
414
+ remaining,
415
+ args.before,
416
+ args.after,
417
+ args.width
418
+ );
419
+ collected.push(...result.results);
420
+ total += result.total;
421
+ }
422
+ scanned += batch.length;
423
+ }
424
+ const final = collected.slice(0, args.results);
425
+ ctx.metadata({
426
+ title: `Found ${final.length} result${final.length !== 1 ? "s" : ""} for "${args.query}" (${scanned} session${scanned !== 1 ? "s" : ""} searched)`
427
+ });
428
+ const out = {
429
+ ok: true,
430
+ results: final,
431
+ scanned,
432
+ total,
433
+ truncated: early || total > final.length
434
+ };
435
+ return JSON.stringify(out);
436
+ } catch (e) {
437
+ const err = { ok: false, error: errmsg(e) };
438
+ return JSON.stringify(err);
439
+ }
440
+ }
441
+ });
442
+ }
443
+
444
+ // src/get.ts
445
+ import {
446
+ tool as tool3
447
+ } from "@opencode-ai/plugin";
448
+ function get(client) {
449
+ return tool3({
450
+ description: `Retrieve the full content of a specific message from any session, including all parts (text, tool outputs, reasoning, etc). Use after recall to get the complete content of a search result. For tool parts, returns the original output even if it was pruned from your context window. Large outputs may be truncated by the opencode runtime.`,
451
+ args: {
452
+ sessionID: tool3.schema.string().describe("Session containing the message"),
453
+ messageID: tool3.schema.string().describe("Message to retrieve")
454
+ },
455
+ async execute(args, ctx) {
456
+ ctx.metadata({
457
+ title: `Retrieving message ${args.messageID.slice(0, 20)}...`
458
+ });
459
+ try {
460
+ const result = await client.session.message({
461
+ sessionID: args.sessionID,
462
+ messageID: args.messageID
463
+ });
464
+ if (!result.data) {
465
+ const msg = result.error ? errmsg(result.error) : `Message not found: ${args.messageID}`;
466
+ const err = { ok: false, error: msg };
467
+ return JSON.stringify(err);
468
+ }
469
+ const item = formatMsg(result.data);
470
+ let title;
471
+ let directory;
472
+ try {
473
+ const sess = await client.session.get({ sessionID: args.sessionID });
474
+ if (sess.data) {
475
+ title = sess.data.title;
476
+ directory = sess.data.directory;
477
+ }
478
+ } catch {
479
+ }
480
+ ctx.metadata({
481
+ title: `${item.message.role} message (${item.parts.length} part${item.parts.length !== 1 ? "s" : ""})${title ? ` from "${title}"` : ""}`
482
+ });
483
+ const out = {
484
+ ok: true,
485
+ message: item.message,
486
+ parts: item.parts,
487
+ context: { sessionTitle: title, directory }
488
+ };
489
+ return JSON.stringify(out);
490
+ } catch (e) {
491
+ const err = { ok: false, error: errmsg(e) };
492
+ return JSON.stringify(err);
493
+ }
494
+ }
495
+ });
496
+ }
497
+
498
+ // src/context.ts
499
+ import {
500
+ tool as tool4
501
+ } from "@opencode-ai/plugin";
502
+ function context(client) {
503
+ return tool4({
504
+ description: `Get messages surrounding a specific message in a session. Use after recall finds a match and you need conversation context \u2014 what was asked before, what came after. Returns a window of messages centered on the target.`,
505
+ args: {
506
+ sessionID: tool4.schema.string().describe("Session containing the message"),
507
+ messageID: tool4.schema.string().describe("Center message to get context around"),
508
+ window: tool4.schema.number().min(0).max(10).default(3).describe(
509
+ "Number of messages to include before AND after the target (symmetric). Overridden by before/after if set."
510
+ ),
511
+ before: tool4.schema.number().min(0).max(10).optional().describe(
512
+ "Messages to include before the target (overrides window for the before side)"
513
+ ),
514
+ after: tool4.schema.number().min(0).max(10).optional().describe(
515
+ "Messages to include after the target (overrides window for the after side)"
516
+ )
517
+ },
518
+ async execute(args, ctx) {
519
+ ctx.metadata({ title: "Getting context around message..." });
520
+ const nb = args.before ?? args.window;
521
+ const na = args.after ?? args.window;
522
+ try {
523
+ const resp = await client.session.messages({
524
+ sessionID: args.sessionID
525
+ });
526
+ if (resp.error) {
527
+ const err = { ok: false, error: errmsg(resp.error) };
528
+ return JSON.stringify(err);
529
+ }
530
+ if (!resp.data) {
531
+ const err = { ok: false, error: "No messages returned" };
532
+ return JSON.stringify(err);
533
+ }
534
+ const msgs = resp.data;
535
+ const idx = msgs.findIndex((m) => m.info.id === args.messageID);
536
+ if (idx === -1) {
537
+ const err = {
538
+ ok: false,
539
+ error: `Message not found: ${args.messageID}`
540
+ };
541
+ return JSON.stringify(err);
542
+ }
543
+ const start = Math.max(0, idx - nb);
544
+ const end = Math.min(msgs.length, idx + na + 1);
545
+ const slice = msgs.slice(start, end);
546
+ const items = slice.map((m) => {
547
+ const item = formatMsg(m);
548
+ return { ...item, center: m.info.id === args.messageID };
549
+ });
550
+ let title;
551
+ let directory;
552
+ try {
553
+ const sess = await client.session.get({ sessionID: args.sessionID });
554
+ if (sess.data) {
555
+ title = sess.data.title;
556
+ directory = sess.data.directory;
557
+ }
558
+ } catch {
559
+ }
560
+ ctx.metadata({
561
+ title: `Context: ${items.length} messages around target${title ? ` from "${title}"` : ""}`
562
+ });
563
+ const out = {
564
+ ok: true,
565
+ messages: items,
566
+ context: { sessionTitle: title, directory },
567
+ hasMoreBefore: start > 0,
568
+ hasMoreAfter: end < msgs.length
569
+ };
570
+ return JSON.stringify(out);
571
+ } catch (e) {
572
+ const err = { ok: false, error: errmsg(e) };
573
+ return JSON.stringify(err);
574
+ }
575
+ }
576
+ });
577
+ }
578
+
579
+ // src/messages.ts
580
+ import {
581
+ tool as tool5
582
+ } from "@opencode-ai/plugin";
583
+ function msgMatches(msg, query) {
584
+ for (const part of msg.parts) {
585
+ for (const text of searchable(part)) {
586
+ if (matches(text, query)) return true;
587
+ }
588
+ }
589
+ return false;
590
+ }
591
+ function messages(client) {
592
+ return tool5({
593
+ description: `Browse messages in a session chronologically with pagination. Use to play back conversation history, see what happened in order, or find the user's original requirements. Use reverse=true to start from the most recent messages (offset 0 = newest). Use offset to paginate through results.`,
594
+ args: {
595
+ sessionID: tool5.schema.string().optional().describe(
596
+ "Session to browse. Defaults to current session if not provided."
597
+ ),
598
+ offset: tool5.schema.number().min(0).default(0).describe(
599
+ "Skip this many messages from the start (or end if reversed)"
600
+ ),
601
+ limit: tool5.schema.number().min(1).max(50).default(10).describe("Max messages to return"),
602
+ role: tool5.schema.enum(["user", "assistant", "all"]).default("all").describe("Filter by message role"),
603
+ reverse: tool5.schema.boolean().default(false).describe("If true, start from most recent messages"),
604
+ query: tool5.schema.string().min(1).optional().describe(
605
+ "Only include messages containing this text (searches all parts)"
606
+ )
607
+ },
608
+ async execute(args, ctx) {
609
+ const sid = args.sessionID ?? ctx.sessionID;
610
+ if (!sid) {
611
+ const err = {
612
+ ok: false,
613
+ error: "No sessionID provided and no current session available"
614
+ };
615
+ return JSON.stringify(err);
616
+ }
617
+ ctx.metadata({ title: "Browsing messages..." });
618
+ try {
619
+ const resp = await client.session.messages({ sessionID: sid });
620
+ if (resp.error) {
621
+ const err = { ok: false, error: errmsg(resp.error) };
622
+ return JSON.stringify(err);
623
+ }
624
+ if (!resp.data) {
625
+ const err = { ok: false, error: "No messages returned" };
626
+ return JSON.stringify(err);
627
+ }
628
+ let filtered = resp.data;
629
+ if (args.role !== "all")
630
+ filtered = filtered.filter((m) => m.info.role === args.role);
631
+ if (args.query)
632
+ filtered = filtered.filter((m) => msgMatches(m, args.query));
633
+ const ordered = args.reverse ? [...filtered].reverse() : filtered;
634
+ const slice = ordered.slice(args.offset, args.offset + args.limit);
635
+ const items = slice.map(formatMsg);
636
+ let title;
637
+ let directory;
638
+ try {
639
+ const sess = await client.session.get({ sessionID: sid });
640
+ if (sess.data) {
641
+ title = sess.data.title;
642
+ directory = sess.data.directory;
643
+ }
644
+ } catch {
645
+ }
646
+ ctx.metadata({
647
+ title: `Showing ${items.length} of ${filtered.length} messages (offset ${args.offset})${title ? ` from "${title}"` : ""}`
648
+ });
649
+ const out = {
650
+ ok: true,
651
+ messages: items,
652
+ context: { sessionTitle: title, directory },
653
+ pagination: {
654
+ offset: args.offset,
655
+ returned: items.length,
656
+ total: filtered.length,
657
+ hasMore: args.offset + args.limit < filtered.length
658
+ }
659
+ };
660
+ return JSON.stringify(out);
661
+ } catch (e) {
662
+ const err = { ok: false, error: errmsg(e) };
663
+ return JSON.stringify(err);
664
+ }
665
+ }
666
+ });
667
+ }
668
+
669
+ // src/index.ts
670
+ var TOOLS = [
671
+ "recall",
672
+ "recall_get",
673
+ "recall_sessions",
674
+ "recall_context",
675
+ "recall_messages"
676
+ ];
677
+ var server = async (ctx, options) => {
678
+ const opts = options ?? {};
679
+ const primary = opts.primary !== false;
680
+ const global = opts.global === true;
681
+ const inner = ctx.client._client;
682
+ if (!inner?.getConfig)
683
+ throw new Error(
684
+ "opencode-session-recall: SDK internals changed \u2014 cannot extract fetch transport"
685
+ );
686
+ const cfg = inner.getConfig();
687
+ if (!cfg.fetch)
688
+ throw new Error("opencode-session-recall: SDK client has no custom fetch");
689
+ const { "x-opencode-directory": _, ...rest } = cfg.headers ?? {};
690
+ const client = createOpencodeClient({
691
+ baseUrl: cfg.baseUrl,
692
+ fetch: cfg.fetch,
693
+ headers: cfg.headers,
694
+ directory: ctx.directory
695
+ });
696
+ const unscoped = createOpencodeClient({
697
+ baseUrl: cfg.baseUrl,
698
+ fetch: cfg.fetch,
699
+ headers: rest
700
+ });
701
+ return {
702
+ tool: {
703
+ recall_sessions: sessions(client, unscoped, global),
704
+ recall: search(client, unscoped, global),
705
+ recall_get: get(client),
706
+ recall_context: context(client),
707
+ recall_messages: messages(client)
708
+ },
709
+ ...primary && {
710
+ config: async (c) => {
711
+ c.experimental ??= {};
712
+ const existing = c.experimental.primary_tools ?? [];
713
+ const deduped = new Set(existing);
714
+ for (const t of TOOLS) deduped.add(t);
715
+ c.experimental.primary_tools = [...deduped];
716
+ }
717
+ }
718
+ };
719
+ };
720
+ var index_default = {
721
+ id: "opencode-session-recall",
722
+ server
723
+ };
724
+ export {
725
+ index_default as default
726
+ };
@@ -0,0 +1,4 @@
1
+ import { type ToolDefinition } from "@opencode-ai/plugin";
2
+ import type { OpencodeClient } from "@opencode-ai/sdk/v2";
3
+ export declare function messages(client: OpencodeClient): ToolDefinition;
4
+ //# sourceMappingURL=messages.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,cAAc,EAEpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,cAAc,EAAQ,MAAM,qBAAqB,CAAC;AAahE,wBAAgB,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,cAAc,CA0G/D"}
@@ -0,0 +1,4 @@
1
+ import { type ToolDefinition } from "@opencode-ai/plugin";
2
+ import type { OpencodeClient } from "@opencode-ai/sdk/v2";
3
+ export declare function search(client: OpencodeClient, unscoped: OpencodeClient, global: boolean): ToolDefinition;
4
+ //# sourceMappingURL=search.d.ts.map
@@ -0,0 +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;AA6E7B,wBAAgB,MAAM,CACpB,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,cAAc,EACxB,MAAM,EAAE,OAAO,GACd,cAAc,CA2MhB"}
@@ -0,0 +1,4 @@
1
+ import { type ToolDefinition } from "@opencode-ai/plugin";
2
+ import type { OpencodeClient } from "@opencode-ai/sdk/v2";
3
+ export declare function sessions(client: OpencodeClient, unscoped: OpencodeClient, global: boolean): ToolDefinition;
4
+ //# sourceMappingURL=sessions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../src/sessions.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,cAAc,EAEpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAQ1D,wBAAgB,QAAQ,CACtB,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,cAAc,EACxB,MAAM,EAAE,OAAO,GACd,cAAc,CA0GhB"}
@@ -0,0 +1,107 @@
1
+ export type SearchResult = {
2
+ sessionID: string;
3
+ sessionTitle: string;
4
+ directory: string;
5
+ messageID: string;
6
+ role: "user" | "assistant";
7
+ time: number;
8
+ partID: string;
9
+ partType: string;
10
+ pruned: boolean;
11
+ snippet: string;
12
+ toolName?: string;
13
+ };
14
+ export type SearchOutput = {
15
+ ok: true;
16
+ results: SearchResult[];
17
+ scanned: number;
18
+ total: number;
19
+ truncated: boolean;
20
+ };
21
+ export type MessageOutput = {
22
+ ok: true;
23
+ message: {
24
+ id: string;
25
+ role: "user" | "assistant";
26
+ time: number;
27
+ agent?: string;
28
+ model?: string;
29
+ };
30
+ parts: PartOutput[];
31
+ context: {
32
+ sessionTitle?: string;
33
+ directory?: string;
34
+ };
35
+ };
36
+ export type PartOutput = {
37
+ id: string;
38
+ type: string;
39
+ pruned: boolean;
40
+ content?: string;
41
+ toolName?: string;
42
+ title?: string;
43
+ input?: unknown;
44
+ output?: string;
45
+ error?: string;
46
+ };
47
+ export type MessageItem = {
48
+ message: {
49
+ id: string;
50
+ role: "user" | "assistant";
51
+ time: number;
52
+ agent?: string;
53
+ model?: string;
54
+ };
55
+ parts: PartOutput[];
56
+ center?: boolean;
57
+ };
58
+ export type ContextOutput = {
59
+ ok: true;
60
+ messages: MessageItem[];
61
+ context: {
62
+ sessionTitle?: string;
63
+ directory?: string;
64
+ };
65
+ hasMoreBefore: boolean;
66
+ hasMoreAfter: boolean;
67
+ };
68
+ export type MessagesOutput = {
69
+ ok: true;
70
+ messages: MessageItem[];
71
+ context: {
72
+ sessionTitle?: string;
73
+ directory?: string;
74
+ };
75
+ pagination: {
76
+ offset: number;
77
+ returned: number;
78
+ total: number;
79
+ hasMore: boolean;
80
+ };
81
+ };
82
+ export type SessionItem = {
83
+ id: string;
84
+ title: string;
85
+ directory: string;
86
+ project?: {
87
+ name?: string;
88
+ worktree: string;
89
+ };
90
+ time: {
91
+ created: number;
92
+ updated: number;
93
+ };
94
+ archived: boolean;
95
+ };
96
+ export type SessionsOutput = {
97
+ ok: true;
98
+ sessions: SessionItem[];
99
+ returned: number;
100
+ scope: string;
101
+ };
102
+ export type ErrorOutput = {
103
+ ok: false;
104
+ error: string;
105
+ };
106
+ export declare function errmsg(e: unknown): string;
107
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/package.json",
3
+ "name": "opencode-session-recall",
4
+ "version": "0.2.1",
5
+ "type": "module",
6
+ "description": "Agent memory without a memory system — search and retrieve opencode conversation history that was lost to compaction, across sessions and projects",
7
+ "main": "./dist/index.js",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./server": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "scripts": {
22
+ "dev": "opencode plugin dev",
23
+ "typecheck": "tsc --noEmit",
24
+ "compile": "tsup src/index.ts --format esm && tsc --emitDeclarationOnly --outDir dist",
25
+ "prepublishOnly": "npm run typecheck && npm run compile"
26
+ },
27
+ "keywords": [
28
+ "opencode",
29
+ "opencode-plugin",
30
+ "plugin",
31
+ "recall",
32
+ "history",
33
+ "compaction",
34
+ "context",
35
+ "search",
36
+ "memory"
37
+ ],
38
+ "author": "maelos",
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "license": "MIT",
43
+ "peerDependencies": {
44
+ "@opencode-ai/plugin": ">=1.2.0"
45
+ },
46
+ "dependencies": {
47
+ "@opencode-ai/sdk": "^1.3.2",
48
+ "zod": "^4.3.6"
49
+ },
50
+ "devDependencies": {
51
+ "@opencode-ai/plugin": "^1.4.3",
52
+ "tsup": "^8.5.1",
53
+ "typescript": "^6.0.2"
54
+ }
55
+ }