opencode-raven 2.1.7 → 2.1.8
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/CHANGELOG.md +9 -0
- package/README.md +12 -3
- package/index.ts +165 -67
- package/mcp-guidance.md +6 -2
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.1.8
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `raven_seek` now returns a model-visible session ID and accepts it through the optional `sessionId` argument, allowing follow-up research to continue in the same Raven session.
|
|
7
|
+
- Resumed sessions are validated as Raven children of the current main session before they can be prompted again.
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- Context estimates for resumed sessions count only the newly handled context instead of counting earlier Raven turns again.
|
|
11
|
+
|
|
3
12
|
## 2.1.7
|
|
4
13
|
|
|
5
14
|
### Fixed
|
package/README.md
CHANGED
|
@@ -54,10 +54,10 @@ In this run, Raven handled the search-heavy web research in a child session and
|
|
|
54
54
|
|
|
55
55
|
1. Your agent attempts to use a routed search, fetch, shell-discovery, or MCP tool.
|
|
56
56
|
2. Raven blocks the noisy direct call and gives the agent the exact `raven_seek` retry.
|
|
57
|
-
3. The agent
|
|
57
|
+
3. The agent creates a linked Raven child session or resumes one for a related follow-up.
|
|
58
58
|
4. Raven performs the tool-heavy work and returns only compact findings to the main session.
|
|
59
59
|
|
|
60
|
-
The main model receives Raven's final answer instead of its raw tool output. Child sessions remain available in OpenCode's session/subagent view for inspection.
|
|
60
|
+
The main model receives Raven's final answer instead of its raw tool output. Each result includes a Raven session ID that the main agent can pass back for a context-aware follow-up. Child sessions remain available in OpenCode's session/subagent view for inspection.
|
|
61
61
|
|
|
62
62
|
For direct control, mention `@Raven` in any chat. This sends the request straight to the Raven agent, but it is optional and not required for automatic routing.
|
|
63
63
|
|
|
@@ -65,9 +65,18 @@ Under the hood, Raven registers two tools:
|
|
|
65
65
|
|
|
66
66
|
| Tool | Purpose |
|
|
67
67
|
|------|---------|
|
|
68
|
-
| `raven_seek` | Creates a linked Raven child session and returns its compact result. |
|
|
68
|
+
| `raven_seek` | Creates or resumes a linked Raven child session and returns its compact result. |
|
|
69
69
|
| `raven_mcp` | Raven-only bridge to MCPs configured under `onDemandMcpServers`. |
|
|
70
70
|
|
|
71
|
+
For a related follow-up, the main agent can reuse the ID returned by the earlier call:
|
|
72
|
+
|
|
73
|
+
```text
|
|
74
|
+
raven_seek(query="Research the available options")
|
|
75
|
+
raven_seek(query="Compare the first two in more detail", sessionId="ses_...")
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Raven accepts a resumed session only when it is a Raven child of the current main session. Unrelated work should omit `sessionId` and start a fresh child.
|
|
79
|
+
|
|
71
80
|
OpenCode's built-in tools already exist; Raven routes selected calls instead of replacing those tools. Global MCPs also remain globally registered, so their schemas still enter the main session even when Raven routes their calls.
|
|
72
81
|
|
|
73
82
|
On-demand MCPs are different: Raven discovers and calls them internally, so their full schemas are not registered globally. Enabled servers are probed after startup to discover tools and health, then per-session connections are opened when Raven uses them. "On-demand" describes schema exposure and normal use, not the absence of a startup health probe.
|
package/index.ts
CHANGED
|
@@ -532,8 +532,9 @@ export default (async (input: PluginInput) => {
|
|
|
532
532
|
return config
|
|
533
533
|
}
|
|
534
534
|
let mcpMetadata = loadMcpMetadata()
|
|
535
|
-
const ravenSessions = new Set<string>()
|
|
536
|
-
const
|
|
535
|
+
const ravenSessions = new Set<string>()
|
|
536
|
+
const activeRavenPrompts = new Set<string>()
|
|
537
|
+
const ravenTaskCalls = new Set<string>()
|
|
537
538
|
const sessionAgents = new Map<string, string>()
|
|
538
539
|
const ravenSessionParents = new Map<string, string>()
|
|
539
540
|
const ravenErrorWaiters = new Map<string, (error: Error) => void>()
|
|
@@ -1346,12 +1347,10 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1346
1347
|
return { current: PACKAGE_VERSION, latest, available: !!latest && compareVersions(latest, PACKAGE_VERSION) > 0 }
|
|
1347
1348
|
}
|
|
1348
1349
|
|
|
1349
|
-
|
|
1350
|
-
const
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
m?.info?.role === "assistant" && m?.info?.tokens?.output > 0
|
|
1354
|
-
)
|
|
1350
|
+
function estimateRavenSavedCandidateBytes(messages: any[]): number {
|
|
1351
|
+
const assistantMessages = messages.filter((m: any) =>
|
|
1352
|
+
m?.info?.role === "assistant" && m?.info?.tokens?.output > 0
|
|
1353
|
+
)
|
|
1355
1354
|
const first = assistantMessages[0]
|
|
1356
1355
|
const last = assistantMessages[assistantMessages.length - 1]
|
|
1357
1356
|
const t = last?.info?.tokens
|
|
@@ -1363,9 +1362,15 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1363
1362
|
const baselineTokens = firstTokens
|
|
1364
1363
|
? (firstTokens.input ?? 0) + (firstTokens.cache?.read ?? 0) + (firstTokens.cache?.write ?? 0)
|
|
1365
1364
|
: 0
|
|
1366
|
-
const savedCandidateTokens = Math.max(0, totalTokens - baselineTokens)
|
|
1367
|
-
return savedCandidateTokens * 4
|
|
1368
|
-
}
|
|
1365
|
+
const savedCandidateTokens = Math.max(0, totalTokens - baselineTokens)
|
|
1366
|
+
return savedCandidateTokens * 4
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
async function countRavenSavedCandidateBytes(sessionId: string, signal?: AbortSignal): Promise<number> {
|
|
1370
|
+
const messagesResp = await client.session.messages({ path: { id: sessionId }, signal })
|
|
1371
|
+
const messages = (messagesResp as any)?.data ?? []
|
|
1372
|
+
return estimateRavenSavedCandidateBytes(messages)
|
|
1373
|
+
}
|
|
1369
1374
|
|
|
1370
1375
|
async function getUpdateInfo(): Promise<{ current: string; latest?: string; available: boolean }> {
|
|
1371
1376
|
if (updateInfo) return updateInfo
|
|
@@ -1529,33 +1534,85 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1529
1534
|
duplicateMcpToastPending = duplicateMcpServers.length > 0
|
|
1530
1535
|
},
|
|
1531
1536
|
|
|
1532
|
-
// Register raven_seek tool — lets agents with task:false still delegate through Raven
|
|
1533
|
-
tool: {
|
|
1534
|
-
"raven_seek": tool({
|
|
1535
|
-
description: "Unified Raven delegation tool. Use this whenever a tool/MCP is blocked by Raven, or when grep, glob, WebFetch/fetch, websearch, docs lookup, GitHub search, on-demand MCP capabilities, or search-like bash would be used. Handles routed MCP requests, on-demand MCPs, local codebase search, filesystem discovery, specific URL/page reads, web/docs research, GitHub examples, and command-output/system inspection via Raven.",
|
|
1536
|
-
args: {
|
|
1537
|
-
query: tool.schema.string().describe("What Raven should do. Include the original blocked tool/MCP request and relevant args, exact URLs when replacing WebFetch, and commands/output checks when replacing grep/rg/head over command output."),
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
const
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
}
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1537
|
+
// Register raven_seek tool — lets agents with task:false still delegate through Raven
|
|
1538
|
+
tool: {
|
|
1539
|
+
"raven_seek": tool({
|
|
1540
|
+
description: "Unified Raven delegation tool. Creates a Raven child session or resumes one for follow-up research. Use this whenever a tool/MCP is blocked by Raven, or when grep, glob, WebFetch/fetch, websearch, docs lookup, GitHub search, on-demand MCP capabilities, or search-like bash would be used. Handles routed MCP requests, on-demand MCPs, local codebase search, filesystem discovery, specific URL/page reads, web/docs research, GitHub examples, and command-output/system inspection via Raven.",
|
|
1541
|
+
args: {
|
|
1542
|
+
query: tool.schema.string().describe("What Raven should do. Include the original blocked tool/MCP request and relevant args, exact URLs when replacing WebFetch, and commands/output checks when replacing grep/rg/head over command output."),
|
|
1543
|
+
sessionId: tool.schema.string().optional().describe("Raven session ID returned by an earlier raven_seek call. Provide it only when this request should continue that session's research context."),
|
|
1544
|
+
},
|
|
1545
|
+
async execute(args, context) {
|
|
1546
|
+
const started = Date.now()
|
|
1547
|
+
const timeout = (config.timeout ?? 600) * 1000
|
|
1548
|
+
let sessionId: string | undefined
|
|
1549
|
+
let resumed = false
|
|
1550
|
+
let previousSavedCandidate = 0
|
|
1551
|
+
try {
|
|
1552
|
+
const requestedSessionId = args.sessionId?.trim()
|
|
1553
|
+
if (args.sessionId !== undefined && !requestedSessionId) {
|
|
1554
|
+
return { title: "Raven Seek", output: "Cannot resume Raven: sessionId must not be empty." }
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
if (requestedSessionId) {
|
|
1558
|
+
const knownRavenSession = ravenSessions.has(requestedSessionId) || sessionAgents.get(requestedSessionId) === "raven"
|
|
1559
|
+
let existingSession: any
|
|
1560
|
+
try {
|
|
1561
|
+
const sessionResp = await client.session.get({ path: { id: requestedSessionId }, signal: context.abort })
|
|
1562
|
+
existingSession = (sessionResp as any)?.data ?? sessionResp
|
|
1563
|
+
} catch (err) {
|
|
1564
|
+
if (context.abort.aborted) throw err
|
|
1565
|
+
return { title: "Raven Seek", output: `Cannot resume Raven session ${requestedSessionId}: the session was not found or could not be read.` }
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
if (!existingSession?.id || existingSession.id !== requestedSessionId) {
|
|
1569
|
+
return { title: "Raven Seek", output: `Cannot resume Raven session ${requestedSessionId}: the session was not found or could not be read.` }
|
|
1570
|
+
}
|
|
1571
|
+
if (existingSession.parentID !== context.sessionID || existingSession.projectID !== input.project.id) {
|
|
1572
|
+
return { title: "Raven Seek", output: "Cannot resume Raven: sessionId is not a Raven child of this main session." }
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
let messages: any[]
|
|
1576
|
+
try {
|
|
1577
|
+
const messagesResp = await client.session.messages({ path: { id: requestedSessionId }, signal: context.abort })
|
|
1578
|
+
const messageData = (messagesResp as any)?.data
|
|
1579
|
+
if (!Array.isArray(messageData)) {
|
|
1580
|
+
return { title: "Raven Seek", output: `Cannot resume Raven session ${requestedSessionId}: its message history could not be read.` }
|
|
1581
|
+
}
|
|
1582
|
+
messages = messageData
|
|
1583
|
+
} catch (err) {
|
|
1584
|
+
if (context.abort.aborted) throw err
|
|
1585
|
+
return { title: "Raven Seek", output: `Cannot resume Raven session ${requestedSessionId}: its message history could not be read.` }
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
const userMessages = messages.filter((message: any) => message?.info?.role === "user")
|
|
1589
|
+
const persistedRavenSession = userMessages.length > 0
|
|
1590
|
+
&& userMessages.every((message: any) => message.info.agent === "raven")
|
|
1591
|
+
if (!persistedRavenSession && !(knownRavenSession && userMessages.length === 0)) {
|
|
1592
|
+
return { title: "Raven Seek", output: "Cannot resume Raven: sessionId is not a Raven child of this main session." }
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
sessionId = requestedSessionId
|
|
1596
|
+
resumed = true
|
|
1597
|
+
previousSavedCandidate = estimateRavenSavedCandidateBytes(messages)
|
|
1598
|
+
} else {
|
|
1599
|
+
const session = await client.session.create({
|
|
1600
|
+
body: {
|
|
1601
|
+
parentID: context.sessionID,
|
|
1602
|
+
title: `raven_seek: ${args.query.slice(0, 80)}`,
|
|
1603
|
+
},
|
|
1604
|
+
signal: context.abort,
|
|
1605
|
+
})
|
|
1606
|
+
|
|
1607
|
+
sessionId = (session as any)?.data?.id ?? (session as any)?.id
|
|
1608
|
+
if (!sessionId) {
|
|
1609
|
+
return { title: "Raven Seek", output: "Failed to create Raven session." }
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
ravenSessions.add(sessionId)
|
|
1614
|
+
const activeSessionId = sessionId
|
|
1615
|
+
|
|
1559
1616
|
// Preserve the child session link for TUI clients that render it.
|
|
1560
1617
|
context.metadata({ metadata: { sessionId } })
|
|
1561
1618
|
|
|
@@ -1564,15 +1621,35 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1564
1621
|
const logFile = join(tmpdir(), "raven-sessions.log")
|
|
1565
1622
|
const ts = new Date().toISOString()
|
|
1566
1623
|
writeFileSync(logFile, `${ts} ${sessionId}\n`)
|
|
1567
|
-
} catch { /* non-fatal */ }
|
|
1624
|
+
} catch { /* non-fatal */ }
|
|
1625
|
+
|
|
1626
|
+
if (activeRavenPrompts.has(activeSessionId)) {
|
|
1627
|
+
return {
|
|
1628
|
+
title: "Raven Seek",
|
|
1629
|
+
metadata: { sessionId: activeSessionId },
|
|
1630
|
+
output: `Raven session \`${activeSessionId}\` is already handling a request. Wait for it to finish before sending another follow-up.`,
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
activeRavenPrompts.add(activeSessionId)
|
|
1568
1634
|
|
|
1569
1635
|
// Send the query to Raven with timeout. Provider/API errors are also
|
|
1570
1636
|
// surfaced through message.updated events; fail as soon as one lands
|
|
1571
1637
|
// so the caller does not wait for the full timeout on quota/auth issues.
|
|
1572
1638
|
let timer: ReturnType<typeof setTimeout> | undefined
|
|
1573
1639
|
let promptCompleted = false
|
|
1640
|
+
let sessionAbort: Promise<unknown> | undefined
|
|
1574
1641
|
const promptController = new AbortController()
|
|
1575
|
-
const abortPrompt = () =>
|
|
1642
|
+
const abortPrompt = () => {
|
|
1643
|
+
if (promptController.signal.aborted) return
|
|
1644
|
+
promptController.abort(context.abort.reason)
|
|
1645
|
+
const abortController = new AbortController()
|
|
1646
|
+
const abortTimer = setTimeout(() => abortController.abort(), Math.min(timeout, 5_000))
|
|
1647
|
+
sessionAbort = client.session.abort({
|
|
1648
|
+
path: { id: activeSessionId },
|
|
1649
|
+
signal: abortController.signal,
|
|
1650
|
+
throwOnError: true,
|
|
1651
|
+
}).catch(() => undefined).finally(() => clearTimeout(abortTimer))
|
|
1652
|
+
}
|
|
1576
1653
|
if (context.abort.aborted) abortPrompt()
|
|
1577
1654
|
else context.abort.addEventListener("abort", abortPrompt, { once: true })
|
|
1578
1655
|
const providerError = waitForRavenProviderError(sessionId)
|
|
@@ -1586,12 +1663,13 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1586
1663
|
parts: [{ type: "text", text: args.query }],
|
|
1587
1664
|
},
|
|
1588
1665
|
signal: promptController.signal,
|
|
1666
|
+
throwOnError: true,
|
|
1589
1667
|
}),
|
|
1590
1668
|
providerError,
|
|
1591
1669
|
firstProgress,
|
|
1592
1670
|
new Promise<never>((_, reject) => {
|
|
1593
1671
|
timer = setTimeout(() => {
|
|
1594
|
-
|
|
1672
|
+
abortPrompt()
|
|
1595
1673
|
reject(new Error(`Raven timed out after ${timeout / 1000}s — child session retained: ${sessionId}`))
|
|
1596
1674
|
}, timeout)
|
|
1597
1675
|
}),
|
|
@@ -1599,8 +1677,9 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1599
1677
|
promptCompleted = true
|
|
1600
1678
|
if (timer) clearTimeout(timer)
|
|
1601
1679
|
clearRavenWaiters(sessionId)
|
|
1680
|
+
if ((result as any)?.error) throw new Error(providerErrorMessage((result as any).error))
|
|
1602
1681
|
|
|
1603
|
-
const elapsed = ((Date.now() - started) / 1000).toFixed(1)
|
|
1682
|
+
const elapsed = ((Date.now() - started) / 1000).toFixed(1)
|
|
1604
1683
|
|
|
1605
1684
|
// Extract text from the response
|
|
1606
1685
|
const parts = (result as any)?.data?.parts ?? []
|
|
@@ -1609,46 +1688,64 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1609
1688
|
.map((p: any) => p.text)
|
|
1610
1689
|
const output = textParts.join("\n") || "Raven returned no results."
|
|
1611
1690
|
|
|
1612
|
-
// Context saved = Raven output/reasoning/cache context - compact answer returned to main session.
|
|
1613
|
-
let savedCandidate = 0
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1691
|
+
// Context saved = Raven output/reasoning/cache context - compact answer returned to main session.
|
|
1692
|
+
let savedCandidate = 0
|
|
1693
|
+
const statsController = new AbortController()
|
|
1694
|
+
const abortStats = () => statsController.abort(context.abort.reason)
|
|
1695
|
+
const statsTimer = setTimeout(() => statsController.abort(), Math.min(timeout, 5_000))
|
|
1696
|
+
if (context.abort.aborted) abortStats()
|
|
1697
|
+
else context.abort.addEventListener("abort", abortStats, { once: true })
|
|
1698
|
+
try {
|
|
1699
|
+
const totalSavedCandidate = await countRavenSavedCandidateBytes(sessionId, statsController.signal)
|
|
1700
|
+
savedCandidate = Math.max(0, totalSavedCandidate - previousSavedCandidate)
|
|
1701
|
+
} catch { /* best-effort */ }
|
|
1702
|
+
finally {
|
|
1703
|
+
clearTimeout(statsTimer)
|
|
1704
|
+
context.abort.removeEventListener("abort", abortStats)
|
|
1705
|
+
}
|
|
1617
1706
|
if (savedCandidate <= 0) {
|
|
1618
1707
|
for (const part of parts) {
|
|
1619
1708
|
if (part.args) savedCandidate += JSON.stringify(part.args).length
|
|
1620
1709
|
if (part.content) savedCandidate += typeof part.content === "string" ? part.content.length : JSON.stringify(part.content).length
|
|
1621
1710
|
}
|
|
1622
1711
|
}
|
|
1623
|
-
const
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1712
|
+
const continuation = `Raven session: \`${sessionId}\`${resumed ? " (continued)" : ""}. Pass this as \`sessionId\` in a later \`raven_seek\` call to continue the same research.`
|
|
1713
|
+
const resultOutput = `${output}\n\n*Raven searched for ${elapsed}s — ${formatBytes(savedCandidate)} handled, ${formatTokens(savedCandidate)} tokens*\n\n${continuation}`
|
|
1714
|
+
addBytes(Math.max(0, savedCandidate - resultOutput.length))
|
|
1715
|
+
ravenProviderIssue = undefined
|
|
1716
|
+
|
|
1717
|
+
return { title: "Raven Seek", metadata: { sessionId }, output: resultOutput }
|
|
1628
1718
|
} finally {
|
|
1629
1719
|
context.abort.removeEventListener("abort", abortPrompt)
|
|
1630
|
-
if (!promptCompleted)
|
|
1720
|
+
if (!promptCompleted) abortPrompt()
|
|
1721
|
+
if (sessionAbort) await sessionAbort
|
|
1631
1722
|
if (timer) clearTimeout(timer)
|
|
1632
1723
|
clearRavenWaiters(sessionId)
|
|
1724
|
+
activeRavenPrompts.delete(activeSessionId)
|
|
1633
1725
|
}
|
|
1634
|
-
} catch (err: any) {
|
|
1635
|
-
const elapsed = ((Date.now() - started) / 1000).toFixed(1)
|
|
1726
|
+
} catch (err: any) {
|
|
1727
|
+
const elapsed = ((Date.now() - started) / 1000).toFixed(1)
|
|
1636
1728
|
const msg = String(err?.message ?? err ?? "").toLowerCase()
|
|
1729
|
+
const missingSession = /session.*not found|not found.*session/i.test(msg)
|
|
1637
1730
|
const hint =
|
|
1638
1731
|
context.abort.aborted ? "Raven search cancelled."
|
|
1639
1732
|
: /rate.?limit|too many requests|429/i.test(msg) ? "Raven rate limited — wait 30s then retry with a narrower query."
|
|
1640
|
-
: /quota|usage.?limit|usage exceeded|free usage|subscribe to go|subscription|billing|insufficient.*(?:credit|balance|quota)/i.test(msg) ? "Raven API quota/subscription exhausted — proceed without Raven, tell the user Raven could not run, and mention what information may be missing."
|
|
1641
|
-
: /auth|unauthorized|forbidden|401|403|api key|login/i.test(msg) ? "Raven provider authentication failed — proceed without Raven and tell the user to check the Raven model/provider login or API key."
|
|
1642
|
-
: /token|context.?length|too large|too long/i.test(msg) ? "Raven query too large — shorten your query and retry."
|
|
1643
|
-
:
|
|
1644
|
-
: /
|
|
1645
|
-
: /
|
|
1646
|
-
:
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1733
|
+
: /quota|usage.?limit|usage exceeded|free usage|subscribe to go|subscription|billing|insufficient.*(?:credit|balance|quota)/i.test(msg) ? "Raven API quota/subscription exhausted — proceed without Raven, tell the user Raven could not run, and mention what information may be missing."
|
|
1734
|
+
: /auth|unauthorized|forbidden|401|403|api key|login/i.test(msg) ? "Raven provider authentication failed — proceed without Raven and tell the user to check the Raven model/provider login or API key."
|
|
1735
|
+
: /token|context.?length|too large|too long/i.test(msg) ? "Raven query too large — shorten your query and retry."
|
|
1736
|
+
: missingSession ? "The Raven session is no longer available — retry without sessionId to start a new Raven search."
|
|
1737
|
+
: /model|unavailable|down|not found/i.test(msg) ? "Raven model unavailable — retry later, or proceed without search."
|
|
1738
|
+
: /did not produce an initial response|retry dialog/i.test(msg) ? err.message
|
|
1739
|
+
: /timeout|timed.?out|session kept/i.test(msg) ? err.message
|
|
1740
|
+
: `Raven search failed. Proceed without search — note gaps for the user. [${err.message || err}]`
|
|
1741
|
+
if (!missingSession && /quota|usage.?limit|usage exceeded|free usage|subscribe to go|subscription|billing|auth|unauthorized|forbidden|401|403|api key|login|model|unavailable|down|not found|did not produce an initial response|retry dialog/i.test(msg)) {
|
|
1742
|
+
ravenProviderIssue = hint
|
|
1743
|
+
}
|
|
1744
|
+
const recovery = ravenProviderIssue ? `\n\n${ravenRecoveryInstructions()}` : ""
|
|
1745
|
+
const continuation = sessionId && !missingSession
|
|
1746
|
+
? `\n\nRaven session: \`${sessionId}\`. Pass this as \`sessionId\` in a later \`raven_seek\` call to retry or continue.`
|
|
1747
|
+
: ""
|
|
1748
|
+
return { title: "Raven Seek", metadata: sessionId ? { sessionId } : undefined, output: `${hint}${recovery}\n\n*Attempt took ${elapsed}s*${continuation}` }
|
|
1652
1749
|
}
|
|
1653
1750
|
},
|
|
1654
1751
|
}),
|
|
@@ -1768,6 +1865,7 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1768
1865
|
const sessionId = evt.properties?.info?.id
|
|
1769
1866
|
if (sessionId) {
|
|
1770
1867
|
ravenSessions.delete(sessionId)
|
|
1868
|
+
activeRavenPrompts.delete(sessionId)
|
|
1771
1869
|
sessionAgents.delete(sessionId)
|
|
1772
1870
|
clearRavenWaiters(sessionId)
|
|
1773
1871
|
for (const [parentId, childId] of ravenSessionParents) {
|
package/mcp-guidance.md
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Do not call tools or MCPs that Raven blocks directly. Raven may route search/fetch tools, docs/web/GitHub MCPs, user-configured tool/MCP prefixes, or on-demand MCP capabilities through `raven_seek` to save context.
|
|
4
4
|
|
|
5
|
-
Use `raven_seek(query="...")` as the next tool call for blocked tool/MCP requests. Include the original tool/MCP name, intent, and relevant arguments.
|
|
5
|
+
Use `raven_seek(query="...")` as the next tool call for blocked tool/MCP requests. Include the original tool/MCP name, intent, and relevant arguments.
|
|
6
|
+
|
|
7
|
+
When a request follows up on an earlier Raven result, pass the Raven session ID returned by that call as `sessionId` so Raven retains its research context. Omit `sessionId` for unrelated work.
|
|
6
8
|
|
|
7
9
|
For on-demand MCP requests, include the target MCP server and likely tool name in the `raven_seek` query when the listed capabilities make that obvious. Do not call `raven_mcp` directly unless you are Raven.
|
|
8
10
|
|
|
@@ -26,6 +28,8 @@ Examples:
|
|
|
26
28
|
|
|
27
29
|
`raven_seek(query="Check whether archivemount/libarchive supports ISO or UDF. Use docs, web, or command output as needed.")`
|
|
28
30
|
|
|
29
|
-
`raven_seek(query="Use the Linear MCP to find open bugs assigned to me and summarize the top 3")`
|
|
31
|
+
`raven_seek(query="Use the Linear MCP to find open bugs assigned to me and summarize the top 3")`
|
|
32
|
+
|
|
33
|
+
`raven_seek(query="Compare the first two bugs in more detail", sessionId="ses_...")`
|
|
30
34
|
|
|
31
35
|
Simple piped output filters like `command | grep ...`, `command | rg ...`, `command | findstr ...`, or `command | head ...` are allowed when they only filter bounded output from the immediately preceding command.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-raven",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.8",
|
|
4
4
|
"description": "Context-saving search and on-demand MCP routing for OpenCode through a focused Raven subagent.",
|
|
5
5
|
"main": "./index.ts",
|
|
6
6
|
"exports": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"LICENSE"
|
|
19
19
|
],
|
|
20
20
|
"scripts": {
|
|
21
|
+
"test": "bun test",
|
|
21
22
|
"typecheck": "tsc --noEmit"
|
|
22
23
|
},
|
|
23
24
|
"keywords": [
|