···11+---
22+description: Search through past OpenCode sessions to find relevant context, previous solutions, and historical decisions. Use this when you need to recall how something was done before or find related past work.
33+mode: subagent
44+model: anthropic/claude-haiku-4-5
55+temperature: 0.1
66+tools:
77+ "*": false
88+ search-history: true
99+ skill: true
1010+permission:
1111+ skill:
1212+ "session-search": allow
1313+ "*": deny
1414+---
1515+1616+You are the Archivist, a specialized agent that searches through OpenCode session history to find relevant past conversations, code changes, and decisions.
1717+1818+You are running inside an AI coding system as a subagent. The main agent invokes you when it needs to find relevant context from previous sessions.
1919+2020+## Your Purpose
2121+2222+When invoked, you will:
2323+1. Search through the local OpenCode session history
2424+2. Find sessions and messages relevant to the query
2525+3. Synthesize findings into a clear, actionable answer
2626+2727+## How to Search
2828+2929+First, load the `session-search` skill to understand the search strategies and storage structure.
3030+3131+Then use the `search-history` tool to find relevant sessions. You can:
3232+- Search by keywords, code patterns, file names, or concepts
3333+- Filter by project directory if the query is project-specific
3434+- List recent sessions to get an overview
3535+3636+## Search Strategies
3737+3838+1. **Start broad**: Use general keywords related to the query
3939+2. **Refine**: If too many results, add more specific terms or filter by directory
4040+3. **Cross-reference**: Search for related terms (e.g., if searching for "auth", also try "login", "authentication")
4141+4. **Check context**: Look at session titles and directories to understand the context
4242+4343+## Response Format
4444+4545+Your response should directly answer the question posed, using information from past sessions:
4646+4747+1. **Direct answer**: What was found that addresses the question
4848+2. **Relevant sessions**: List session IDs where this was discussed (so user can resume if needed)
4949+3. **Key details**: Important snippets or decisions from the history
5050+5151+Example response:
5252+```
5353+Based on past sessions, authentication was implemented using JWT tokens with a 24-hour expiry.
5454+5555+**Relevant sessions:**
5656+- ses_abc123 - "Implementing user auth" (2024-01-15)
5757+- ses_def456 - "Auth token refresh" (2024-01-20)
5858+5959+**Key details:**
6060+- Tokens are stored in httpOnly cookies
6161+- Refresh endpoint at /api/auth/refresh
6262+- Used jose library for JWT handling
6363+```
6464+6565+## Guidelines
6666+6767+- Be concise and direct - the main agent needs actionable information
6868+- Include session IDs so the user can explore further if needed
6969+- If nothing relevant is found, say so clearly
7070+- Focus on answering the specific question, not providing exhaustive history
7171+- Never fabricate information - only report what's actually in the history
7272+7373+IMPORTANT: Your final message is returned to the main agent. Make it comprehensive but focused on answering the original question.
···11+---
22+name: session-search
33+description: Advanced strategies for searching OpenCode session history. Restricted to the archivist agent.
44+---
55+66+# Session Search Skill
77+88+This skill provides advanced strategies for searching through OpenCode's session history storage.
99+1010+## Storage Structure
1111+1212+OpenCode stores data in `~/.local/share/opencode/storage/`:
1313+1414+```
1515+storage/
1616+├── session/ # Session metadata by project
1717+│ └── {projectHash}/
1818+│ └── ses_*.json # Session info (title, directory, timestamps)
1919+├── message/ # Messages organized by session
2020+│ └── ses_*/
2121+│ └── msg_*.json # Message metadata (role, agent, model)
2222+├── part/ # Actual message content
2323+│ └── msg_*/
2424+│ └── prt_*.json # Content parts (text, tool calls)
2525+└── project/ # Project metadata
2626+ └── {hash}.json # Worktree path, timestamps
2727+```
2828+2929+## Search Tool Usage
3030+3131+The `search-history` tool accepts:
3232+- `query`: Text pattern to search for (searches message content)
3333+- `directory`: Optional filter by project path (partial match)
3434+- `limit`: Max results (default 30)
3535+3636+### Examples
3737+3838+```typescript
3939+// Find all sessions mentioning "authentication"
4040+search-history({ query: "authentication" })
4141+4242+// Find sessions in a specific project
4343+search-history({ query: "database", directory: "myproject" })
4444+4545+// List recent sessions (empty query)
4646+search-history({ query: "", limit: 20 })
4747+```
4848+4949+## Search Strategies
5050+5151+### 1. Keyword Expansion
5252+Don't just search for the exact term. Try synonyms and related concepts:
5353+- "auth" → also try "login", "authentication", "jwt", "token"
5454+- "database" → also try "postgres", "sqlite", "db", "migration"
5555+- "api" → also try "endpoint", "route", "handler"
5656+5757+### 2. Code Pattern Search
5858+Search for code-specific patterns:
5959+- Function names: `handleAuth`, `validateToken`
6060+- File paths: `src/auth`, `lib/database`
6161+- Import statements: `import.*prisma`
6262+- Error messages: specific error text
6363+6464+### 3. Tool Usage Search
6565+Find when specific tools were used:
6666+- Edit operations: search for file paths that were edited
6767+- Bash commands: search for command names
6868+- Specific operations: "git push", "npm install"
6969+7070+### 4. Directory Filtering
7171+Use the `directory` parameter to scope searches:
7272+- Filter by project name: `directory: "myapp"`
7373+- Filter by path segment: `directory: "usr/projects"`
7474+7575+### 5. Iterative Refinement
7676+1. Start with broad search
7777+2. If too many results, add specificity
7878+3. If no results, broaden or try alternative terms
7979+4. Cross-reference multiple searches
8080+8181+## Understanding Results
8282+8383+### Session Info
8484+- `id`: Unique session identifier (can be used to reference)
8585+- `title`: Auto-generated session title
8686+- `directory`: Project worktree path
8787+- `updated`: Last activity timestamp
8888+8989+### Content Matches
9090+- `sessionID`: Which session contains this match
9191+- `snippet`: Context around the match (±100 chars)
9292+- `role`: user/assistant/tool
9393+9494+## Tips
9595+9696+1. **Recent vs Relevant**: The tool returns recent sessions first. Older but more relevant sessions may be further in results.
9797+9898+2. **Title Search**: Session titles are auto-generated from the conversation and can be good search targets.
9999+100100+3. **Multiple Searches**: Don't hesitate to run multiple searches with different terms to build a complete picture.
101101+102102+4. **Context Matters**: The snippet provides limited context. Session titles and directories help understand the broader context.
103103+104104+5. **No Results**: If no results found, the pattern may be too specific. Try shorter or more general terms.
+275
home/profiles/opencode/tool/search-history.ts
···11+import { tool } from "@opencode-ai/plugin"
22+import { $ } from "bun"
33+import { readdir, readFile } from "fs/promises"
44+import { join } from "path"
55+import { homedir } from "os"
66+77+const STORAGE_PATH = join(homedir(), ".local/share/opencode/storage")
88+99+interface SessionInfo {
1010+ id: string
1111+ title: string
1212+ directory: string
1313+ projectID: string
1414+ created: number
1515+ updated: number
1616+}
1717+1818+interface MessageMatch {
1919+ sessionID: string
2020+ messageID: string
2121+ snippet: string
2222+ role: string
2323+ timestamp?: number
2424+}
2525+2626+interface SearchResult {
2727+ sessions: SessionInfo[]
2828+ matches: MessageMatch[]
2929+ totalMatches: number
3030+}
3131+3232+async function getSessionInfo(sessionID: string): Promise<SessionInfo | null> {
3333+ try {
3434+ // Sessions are stored in directories named by project hash
3535+ const sessionDirs = await readdir(join(STORAGE_PATH, "session"))
3636+ for (const dir of sessionDirs) {
3737+ const sessionPath = join(STORAGE_PATH, "session", dir)
3838+ const files = await readdir(sessionPath).catch(() => [])
3939+ for (const file of files) {
4040+ if (file.startsWith(sessionID) || file.includes(sessionID)) {
4141+ const content = await readFile(join(sessionPath, file), "utf-8")
4242+ const data = JSON.parse(content)
4343+ return {
4444+ id: data.id,
4545+ title: data.title || "Untitled",
4646+ directory: data.directory || "",
4747+ projectID: data.projectID || "",
4848+ created: data.time?.created || 0,
4949+ updated: data.time?.updated || 0,
5050+ }
5151+ }
5252+ }
5353+ }
5454+ } catch {
5555+ // Fall back to searching message directories
5656+ }
5757+ return null
5858+}
5959+6060+async function searchWithRipgrep(
6161+ pattern: string,
6262+ directory?: string,
6363+ limit: number = 50
6464+): Promise<SearchResult> {
6565+ const matches: MessageMatch[] = []
6666+ const sessionIDs = new Set<string>()
6767+6868+ // Search through part storage (contains actual message content)
6969+ const partPath = join(STORAGE_PATH, "part")
7070+7171+ try {
7272+ // Use ripgrep to search JSON files, extracting context around matches
7373+ const rgResult = await $`rg -i -l ${pattern} ${partPath} --type json 2>/dev/null || true`.text()
7474+ const matchingFiles = rgResult.trim().split("\n").filter(Boolean)
7575+7676+ for (const file of matchingFiles.slice(0, limit * 2)) {
7777+ try {
7878+ const content = await readFile(file, "utf-8")
7979+ const data = JSON.parse(content)
8080+8181+ // Filter by directory if specified
8282+ if (directory) {
8383+ const sessionInfo = await getSessionInfo(data.sessionID)
8484+ if (sessionInfo && !sessionInfo.directory.includes(directory)) {
8585+ continue
8686+ }
8787+ }
8888+8989+ // Extract snippet around the match
9090+ const text = data.text || data.content || JSON.stringify(data)
9191+ const lowerText = text.toLowerCase()
9292+ const lowerPattern = pattern.toLowerCase()
9393+ const matchIndex = lowerText.indexOf(lowerPattern)
9494+9595+ if (matchIndex !== -1) {
9696+ const start = Math.max(0, matchIndex - 100)
9797+ const end = Math.min(text.length, matchIndex + pattern.length + 100)
9898+ const snippet = (start > 0 ? "..." : "") +
9999+ text.slice(start, end) +
100100+ (end < text.length ? "..." : "")
101101+102102+ matches.push({
103103+ sessionID: data.sessionID,
104104+ messageID: data.messageID,
105105+ snippet: snippet.replace(/\n/g, " ").trim(),
106106+ role: data.role || "unknown",
107107+ timestamp: data.time?.created,
108108+ })
109109+110110+ sessionIDs.add(data.sessionID)
111111+112112+ if (matches.length >= limit) break
113113+ }
114114+ } catch {
115115+ // Skip files that can't be parsed
116116+ }
117117+ }
118118+ } catch (e) {
119119+ // ripgrep not found or error
120120+ }
121121+122122+ // Also search message metadata for titles
123123+ const messagePath = join(STORAGE_PATH, "message")
124124+ try {
125125+ const rgResult = await $`rg -i -l ${pattern} ${messagePath} --type json 2>/dev/null || true`.text()
126126+ const matchingFiles = rgResult.trim().split("\n").filter(Boolean)
127127+128128+ for (const file of matchingFiles.slice(0, 20)) {
129129+ try {
130130+ const content = await readFile(file, "utf-8")
131131+ const data = JSON.parse(content)
132132+ if (data.sessionID) {
133133+ sessionIDs.add(data.sessionID)
134134+ }
135135+ } catch {
136136+ // Skip
137137+ }
138138+ }
139139+ } catch {
140140+ // Ignore errors
141141+ }
142142+143143+ // Get session info for all matched sessions
144144+ const sessions: SessionInfo[] = []
145145+ for (const sessionID of sessionIDs) {
146146+ const info = await getSessionInfo(sessionID)
147147+ if (info) {
148148+ // Apply directory filter for sessions too
149149+ if (!directory || info.directory.includes(directory)) {
150150+ sessions.push(info)
151151+ }
152152+ }
153153+ }
154154+155155+ // Sort sessions by most recent
156156+ sessions.sort((a, b) => b.updated - a.updated)
157157+158158+ return {
159159+ sessions: sessions.slice(0, 20),
160160+ matches: matches.slice(0, limit),
161161+ totalMatches: matches.length,
162162+ }
163163+}
164164+165165+async function listRecentSessions(
166166+ directory?: string,
167167+ limit: number = 20
168168+): Promise<SessionInfo[]> {
169169+ const sessions: SessionInfo[] = []
170170+171171+ try {
172172+ const sessionDirs = await readdir(join(STORAGE_PATH, "session"))
173173+174174+ for (const dir of sessionDirs) {
175175+ const sessionPath = join(STORAGE_PATH, "session", dir)
176176+ const files = await readdir(sessionPath).catch(() => [])
177177+178178+ for (const file of files) {
179179+ if (!file.endsWith(".json")) continue
180180+ try {
181181+ const content = await readFile(join(sessionPath, file), "utf-8")
182182+ const data = JSON.parse(content)
183183+184184+ if (directory && !data.directory?.includes(directory)) {
185185+ continue
186186+ }
187187+188188+ sessions.push({
189189+ id: data.id,
190190+ title: data.title || "Untitled",
191191+ directory: data.directory || "",
192192+ projectID: data.projectID || "",
193193+ created: data.time?.created || 0,
194194+ updated: data.time?.updated || 0,
195195+ })
196196+ } catch {
197197+ // Skip invalid files
198198+ }
199199+ }
200200+ }
201201+ } catch {
202202+ // Storage doesn't exist
203203+ }
204204+205205+ sessions.sort((a, b) => b.updated - a.updated)
206206+ return sessions.slice(0, limit)
207207+}
208208+209209+export default tool({
210210+ description:
211211+ "Search through OpenCode session history to find past conversations, code changes, and decisions. Use this to find relevant context from previous sessions.",
212212+ args: {
213213+ query: tool.schema
214214+ .string()
215215+ .describe(
216216+ "Search pattern to find in session history. Searches message content, titles, and tool outputs."
217217+ ),
218218+ directory: tool.schema
219219+ .string()
220220+ .optional()
221221+ .describe(
222222+ "Optional: Filter results to sessions from a specific project directory path (partial match)"
223223+ ),
224224+ limit: tool.schema
225225+ .number()
226226+ .optional()
227227+ .describe("Maximum number of matches to return (default: 30)"),
228228+ },
229229+ async execute(args) {
230230+ const limit = args.limit || 30
231231+232232+ if (!args.query || args.query.trim() === "") {
233233+ // List recent sessions if no query
234234+ const sessions = await listRecentSessions(args.directory, limit)
235235+ return JSON.stringify(
236236+ {
237237+ type: "recent_sessions",
238238+ sessions,
239239+ message: `Found ${sessions.length} recent sessions${args.directory ? ` in ${args.directory}` : ""}`,
240240+ },
241241+ null,
242242+ 2
243243+ )
244244+ }
245245+246246+ const results = await searchWithRipgrep(args.query, args.directory, limit)
247247+248248+ // Format output for the agent
249249+ let output = `## Search Results for "${args.query}"\n\n`
250250+251251+ if (results.sessions.length > 0) {
252252+ output += `### Relevant Sessions (${results.sessions.length})\n\n`
253253+ for (const session of results.sessions) {
254254+ const date = new Date(session.updated).toLocaleDateString()
255255+ output += `- **${session.title}** (${session.id})\n`
256256+ output += ` - Directory: \`${session.directory}\`\n`
257257+ output += ` - Last updated: ${date}\n\n`
258258+ }
259259+ }
260260+261261+ if (results.matches.length > 0) {
262262+ output += `### Content Matches (${results.totalMatches})\n\n`
263263+ for (const match of results.matches.slice(0, 15)) {
264264+ output += `**Session:** ${match.sessionID}\n`
265265+ output += `> ${match.snippet}\n\n`
266266+ }
267267+ }
268268+269269+ if (results.sessions.length === 0 && results.matches.length === 0) {
270270+ output += `No matches found for "${args.query}"${args.directory ? ` in ${args.directory}` : ""}\n`
271271+ }
272272+273273+ return output
274274+ },
275275+})