opencode-raven 2.1.2 → 2.1.3

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.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/index.ts +160 -43
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -188,7 +188,7 @@ Edit config manually or use `/raven` commands:
188
188
  },
189
189
  "excludeAgents": [],
190
190
  "excludeTools": [],
191
- "timeout": 180
191
+ "timeout": 600
192
192
  }
193
193
  ```
194
194
 
@@ -204,7 +204,7 @@ Edit config manually or use `/raven` commands:
204
204
  | `onDemandMcpServers` | Context7, Exa, Grep.app | MCPs Raven connects to internally on demand, without registering full schemas in OpenCode global MCP config. Supports OpenCode-style `remote` and `local` entries. Each server supports `enabled`, defaulting to `true`. |
205
205
  | `excludeAgents` | `[]` | Agents that bypass Raven routing, case-insensitive. |
206
206
  | `excludeTools` | `[]` | Exact tools that never get blocked, even if matched by auto-routed global MCP prefixes. |
207
- | `timeout` | `180` | Max seconds for a `raven_seek` call. On timeout the session is kept for inspection. |
207
+ | `timeout` | `600` | Max seconds for a `raven_seek` call. Provider/API retry errors and no-initial-response startup failures are reported quickly; after provider failures, routed tools tell the user how to switch Raven's model instead of retrying Raven. On timeout the session is kept for inspection. |
208
208
  | `stats` | auto | Session and global estimated context saved by Raven. Managed automatically. |
209
209
 
210
210
  Use `ravenInstructions` for extra Raven-only guidance, such as how to use custom MCPs:
package/index.ts CHANGED
@@ -203,7 +203,7 @@ const DEFAULT_CONFIG: RavenConfig = {
203
203
  onDemandMcpServers: DEFAULT_ON_DEMAND_MCP_SERVERS,
204
204
  excludeAgents: [],
205
205
  excludeTools: [],
206
- timeout: 180,
206
+ timeout: 600,
207
207
  }
208
208
 
209
209
  function parseRavenMd(raw: string): { frontmatter: Record<string, any>; prompt: string } {
@@ -494,6 +494,8 @@ export default ((input: PluginInput) => {
494
494
  const ravenTaskCalls = new Set<string>()
495
495
  const sessionAgents = new Map<string, string>()
496
496
  const ravenSessionParents = new Map<string, string>()
497
+ const ravenErrorWaiters = new Map<string, (error: Error) => void>()
498
+ const ravenProgressWaiters = new Map<string, () => void>()
497
499
  let globalMcpServers: string[] = []
498
500
  let duplicateMcpServers: string[] = []
499
501
  let updateInfo: { current: string; latest?: string; available: boolean } | undefined
@@ -504,6 +506,7 @@ export default ((input: PluginInput) => {
504
506
  let mcpRefreshScheduled = false
505
507
  let mcpRefreshPromise: Promise<{ refreshed: string[]; failed: string[] }> | undefined
506
508
  let lastNotifiedMcpFailureKey = ""
509
+ let ravenProviderIssue: string | undefined
507
510
 
508
511
  resetOnDemandMcpRuntimeStatuses()
509
512
 
@@ -570,9 +573,26 @@ export default ((input: PluginInput) => {
570
573
  }
571
574
 
572
575
  function rerouteMessage(tool: string, args: any): string {
576
+ if (ravenProviderIssue) {
577
+ return `Raven is currently unavailable: ${ravenProviderIssue}
578
+
579
+ ${ravenRecoveryInstructions()}`
580
+ }
573
581
  return `The '${tool}' tool call is blocked by Raven. Your next tool call should be raven_seek(query="${attemptedQuery(tool, args).replace(/"/g, "'")}").`
574
582
  }
575
583
 
584
+ function ravenRecoveryInstructions(): string {
585
+ return `Stop trying Raven-routed tools or MCPs for this request. Inform the user that Raven cannot run until its model/provider is changed or fixed.
586
+
587
+ Tell the user to switch Raven's model with:
588
+ /raven model <provider/model>
589
+
590
+ Example:
591
+ /raven model openai/gpt-5.5
592
+
593
+ Then restart opencode for the model change to take effect. If the task can be completed without Raven, continue without routed tools and clearly mention what could not be checked.`
594
+ }
595
+
576
596
  function routeSummary(): string {
577
597
  const tools = config.routeTools?.length ? config.routeTools.join(", ") : "(none)"
578
598
  const mcps = globalMcpServers.length ? `${globalMcpServers.join(", ")} (auto-detected)` : "none detected"
@@ -673,6 +693,49 @@ export default ((input: PluginInput) => {
673
693
  }
674
694
  }
675
695
 
696
+ function providerErrorMessage(error: any): string {
697
+ const name = String(error?.name ?? "")
698
+ const data = error?.data && typeof error.data === "object" ? error.data : {}
699
+ const status = typeof data.statusCode === "number" ? ` HTTP ${data.statusCode}.` : ""
700
+ const body = typeof data.responseBody === "string" && data.responseBody.trim() ? ` ${truncateText(data.responseBody, 500)}` : ""
701
+ const message = String(data.message ?? error?.message ?? error ?? "Unknown provider error")
702
+ return `Raven provider/API error${name ? ` (${name})` : ""}.${status} ${message}${body}`.trim()
703
+ }
704
+
705
+ function waitForRavenProviderError(sessionId: string): Promise<never> {
706
+ return new Promise((_resolve, reject) => {
707
+ ravenErrorWaiters.set(sessionId, reject)
708
+ })
709
+ }
710
+
711
+ function waitForRavenFirstProgress(sessionId: string, ms: number): Promise<never> {
712
+ return new Promise((_resolve, reject) => {
713
+ const timer = setTimeout(() => {
714
+ ravenProgressWaiters.delete(sessionId)
715
+ reject(new Error(`Raven did not produce an initial response within ${Math.round(ms / 1000)}s. If the Raven provider showed a retry dialog, this is likely an auth/quota/subscription issue. Check the Raven subagent/provider account, or retry after switching Raven to a working model.`))
716
+ }, ms)
717
+ ravenProgressWaiters.set(sessionId, () => {
718
+ clearTimeout(timer)
719
+ ravenProgressWaiters.delete(sessionId)
720
+ })
721
+ })
722
+ }
723
+
724
+ function reportRavenProviderError(sessionId: string, error: any) {
725
+ const waiter = ravenErrorWaiters.get(sessionId)
726
+ if (!waiter) return
727
+ waiter(new Error(providerErrorMessage(error)))
728
+ }
729
+
730
+ function reportRavenProgress(sessionId: string) {
731
+ ravenProgressWaiters.get(sessionId)?.()
732
+ }
733
+
734
+ function clearRavenWaiters(sessionId: string) {
735
+ ravenErrorWaiters.delete(sessionId)
736
+ ravenProgressWaiters.get(sessionId)?.()
737
+ }
738
+
676
739
  type McpConnection = {
677
740
  client: Client
678
741
  transport: any
@@ -1238,7 +1301,7 @@ export default ((input: PluginInput) => {
1238
1301
  },
1239
1302
  async execute(args, context) {
1240
1303
  const started = Date.now()
1241
- const timeout = (config.timeout ?? 180) * 1000
1304
+ const timeout = (config.timeout ?? 600) * 1000
1242
1305
  try {
1243
1306
  // Create a Raven session
1244
1307
  const session = await client.session.create({
@@ -1266,55 +1329,76 @@ export default ((input: PluginInput) => {
1266
1329
  appendFileSync(logFile, `${ts} ${sessionId} "${q}"\n`)
1267
1330
  } catch { /* non-fatal */ }
1268
1331
 
1269
- // Send the query to Raven with timeout
1270
- const result = await Promise.race([
1271
- client.session.prompt({
1272
- path: { id: sessionId },
1273
- body: {
1274
- agent: "raven",
1275
- parts: [{ type: "text", text: args.query }],
1276
- },
1277
- }),
1278
- new Promise<never>((_, reject) =>
1279
- setTimeout(() => reject(new Error(`Raven timed out after ${timeout / 1000}s — session kept: ${sessionId}`)), timeout)
1280
- ),
1281
- ])
1282
-
1283
- const elapsed = ((Date.now() - started) / 1000).toFixed(1)
1284
-
1285
- // Extract text from the response
1286
- const parts = (result as any)?.data?.parts ?? []
1287
- const textParts = parts
1288
- .filter((p: any) => p.type === "text" && p.text)
1289
- .map((p: any) => p.text)
1290
- const output = textParts.join("\n") || "Raven returned no results."
1291
-
1292
- // Context saved = Raven output/reasoning/cache context - compact answer returned to main session.
1293
- let savedCandidate = 0
1332
+ // Send the query to Raven with timeout. Provider/API errors are also
1333
+ // surfaced through message.updated events; fail as soon as one lands
1334
+ // so the caller does not wait for the full timeout on quota/auth issues.
1335
+ let timer: ReturnType<typeof setTimeout> | undefined
1336
+ const providerError = waitForRavenProviderError(sessionId)
1337
+ const firstProgress = waitForRavenFirstProgress(sessionId, Math.min(timeout, 30_000))
1294
1338
  try {
1295
- savedCandidate = await countRavenSavedCandidateBytes(sessionId)
1296
- } catch { /* best-effort */ }
1297
- if (savedCandidate <= 0) {
1298
- for (const part of parts) {
1299
- if (part.args) savedCandidate += JSON.stringify(part.args).length
1300
- if (part.content) savedCandidate += typeof part.content === "string" ? part.content.length : JSON.stringify(part.content).length
1339
+ const result = await Promise.race([
1340
+ client.session.prompt({
1341
+ path: { id: sessionId },
1342
+ body: {
1343
+ agent: "raven",
1344
+ parts: [{ type: "text", text: args.query }],
1345
+ },
1346
+ }),
1347
+ providerError,
1348
+ firstProgress,
1349
+ new Promise<never>((_, reject) => {
1350
+ timer = setTimeout(() => reject(new Error(`Raven timed out after ${timeout / 1000}s — session kept: ${sessionId}`)), timeout)
1351
+ }),
1352
+ ])
1353
+ if (timer) clearTimeout(timer)
1354
+ clearRavenWaiters(sessionId)
1355
+
1356
+ const elapsed = ((Date.now() - started) / 1000).toFixed(1)
1357
+
1358
+ // Extract text from the response
1359
+ const parts = (result as any)?.data?.parts ?? []
1360
+ const textParts = parts
1361
+ .filter((p: any) => p.type === "text" && p.text)
1362
+ .map((p: any) => p.text)
1363
+ const output = textParts.join("\n") || "Raven returned no results."
1364
+
1365
+ // Context saved = Raven output/reasoning/cache context - compact answer returned to main session.
1366
+ let savedCandidate = 0
1367
+ try {
1368
+ savedCandidate = await countRavenSavedCandidateBytes(sessionId)
1369
+ } catch { /* best-effort */ }
1370
+ if (savedCandidate <= 0) {
1371
+ for (const part of parts) {
1372
+ if (part.args) savedCandidate += JSON.stringify(part.args).length
1373
+ if (part.content) savedCandidate += typeof part.content === "string" ? part.content.length : JSON.stringify(part.content).length
1374
+ }
1301
1375
  }
1376
+ const saved = Math.max(0, savedCandidate - output.length)
1377
+ addBytes(saved)
1378
+ ravenProviderIssue = undefined
1379
+
1380
+ return { title: "Raven Seek", metadata: { sessionId }, output: `${output}\n\n*Raven searched for ${elapsed}s — ${formatBytes(savedCandidate)} handled, ${formatTokens(savedCandidate)} tokens*` }
1381
+ } finally {
1382
+ if (timer) clearTimeout(timer)
1383
+ clearRavenWaiters(sessionId)
1302
1384
  }
1303
- const saved = Math.max(0, savedCandidate - output.length)
1304
- addBytes(saved)
1305
-
1306
- return { title: "Raven Seek", metadata: { sessionId }, output: `${output}\n\n*Raven searched for ${elapsed}s — ${formatBytes(savedCandidate)} handled, ${formatTokens(savedCandidate)} tokens*` }
1307
1385
  } catch (err: any) {
1308
1386
  const elapsed = ((Date.now() - started) / 1000).toFixed(1)
1309
1387
  const msg = String(err?.message ?? err ?? "").toLowerCase()
1310
1388
  const hint =
1311
1389
  /rate.?limit|too many requests|429/i.test(msg) ? "Raven rate limited — wait 30s then retry with a narrower query."
1312
- : /quota|usage.?limit|billing|insufficient.*(?:credit|balance|quota)/i.test(msg) ? "Raven API quota exhausted — proceed without search, tell user what's missing."
1390
+ : /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."
1391
+ : /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."
1313
1392
  : /token|context.?length|too large|too long/i.test(msg) ? "Raven query too large — shorten your query and retry."
1314
1393
  : /model|unavailable|down|not found/i.test(msg) ? "Raven model unavailable — retry later, or proceed without search."
1394
+ : /did not produce an initial response|retry dialog/i.test(msg) ? err.message
1315
1395
  : /timeout|timed.?out|session kept/i.test(msg) ? err.message
1316
1396
  : `Raven search failed. Proceed without search — note gaps for the user. [${err.message || err}]`
1317
- return { title: "Raven Seek", output: `${hint}\n\n*Attempt took ${elapsed}s*` }
1397
+ if (/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)) {
1398
+ ravenProviderIssue = hint
1399
+ }
1400
+ const recovery = ravenProviderIssue ? `\n\n${ravenRecoveryInstructions()}` : ""
1401
+ return { title: "Raven Seek", output: `${hint}${recovery}\n\n*Attempt took ${elapsed}s*` }
1318
1402
  }
1319
1403
  },
1320
1404
  }),
@@ -1332,7 +1416,7 @@ export default ((input: PluginInput) => {
1332
1416
  }
1333
1417
 
1334
1418
  const operation = args.operation as RavenMcpOperation
1335
- const timeout = (config.timeout ?? 180) * 1000
1419
+ const timeout = (config.timeout ?? 600) * 1000
1336
1420
  let connected = false
1337
1421
  try {
1338
1422
  const connection = await getMcpConnection(context.sessionID, args.server)
@@ -1415,6 +1499,37 @@ export default ((input: PluginInput) => {
1415
1499
  if (evt?.type === "session.created" && evt?.properties?.parentID) {
1416
1500
  ravenSessionParents.set(evt.properties.parentID, evt.properties.id)
1417
1501
  }
1502
+ if (evt?.type === "message.updated") {
1503
+ const info = evt.properties?.info
1504
+ if (info?.role === "assistant" && info?.sessionID && info.error && ravenSessions.has(info.sessionID)) {
1505
+ reportRavenProviderError(info.sessionID, info.error)
1506
+ }
1507
+ }
1508
+ if (evt?.type === "message.part.updated") {
1509
+ const part = evt.properties?.part
1510
+ if (part?.sessionID && ravenSessions.has(part.sessionID)) {
1511
+ if (part.type === "retry" && part.error) {
1512
+ reportRavenProviderError(part.sessionID, part.error)
1513
+ } else {
1514
+ reportRavenProgress(part.sessionID)
1515
+ }
1516
+ }
1517
+ }
1518
+ if (evt?.type === "session.status") {
1519
+ const sessionId = evt.properties?.sessionID
1520
+ const status = evt.properties?.status
1521
+ if (sessionId && ravenSessions.has(sessionId)) {
1522
+ if (status?.type === "retry") {
1523
+ reportRavenProviderError(sessionId, { name: "RetryError", data: { message: status.message } })
1524
+ }
1525
+ }
1526
+ }
1527
+ if (evt?.type === "session.error") {
1528
+ const sessionId = evt.properties?.sessionID
1529
+ if (sessionId && evt.properties?.error && ravenSessions.has(sessionId)) {
1530
+ reportRavenProviderError(sessionId, evt.properties.error)
1531
+ }
1532
+ }
1418
1533
 
1419
1534
  if (updateToastPending) {
1420
1535
  updateToastPending = false
@@ -1515,6 +1630,7 @@ export default ((input: PluginInput) => {
1515
1630
  } else {
1516
1631
  config.model = model
1517
1632
  saveConfig(config)
1633
+ ravenProviderIssue = undefined
1518
1634
  output.parts.push({ type: "text", text: `Raven model set to: ${model}\nRestart opencode for the change to take effect.` })
1519
1635
  }
1520
1636
  } else if (arg.startsWith("effort ")) {
@@ -1529,7 +1645,7 @@ export default ((input: PluginInput) => {
1529
1645
  } else if (arg.startsWith("timeout ")) {
1530
1646
  const secs = parseInt(raw.slice(8).trim(), 10)
1531
1647
  if (!secs || secs < 10) {
1532
- output.parts.push({ type: "text", text: `Usage: /raven timeout <seconds>\nMust be at least 10. Current: ${config.timeout ?? 180}s` })
1648
+ output.parts.push({ type: "text", text: `Usage: /raven timeout <seconds>\nMust be at least 10. Current: ${config.timeout ?? 600}s` })
1533
1649
  } else {
1534
1650
  config.timeout = secs
1535
1651
  saveConfig(config)
@@ -1539,7 +1655,8 @@ export default ((input: PluginInput) => {
1539
1655
  const enabled = config.enabled ? "enabled" : "disabled"
1540
1656
  const model = config.model || fm.model || "(default)"
1541
1657
  const effort = config.reasoning_effort || fm.reasoning_effort || "(default)"
1542
- const timeout = config.timeout ?? 180
1658
+ const timeout = config.timeout ?? 600
1659
+ const providerIssue = ravenProviderIssue ? `\nProvider issue: ${ravenProviderIssue}` : ""
1543
1660
  let update = "Update: unable to check npm."
1544
1661
  try {
1545
1662
  const info = await getUpdateInfo()
@@ -1547,7 +1664,7 @@ export default ((input: PluginInput) => {
1547
1664
  ? `Update: ${info.latest} available. Run /raven update, then restart opencode.`
1548
1665
  : `Update: up to date${info.latest ? ` (latest ${info.latest})` : ""}.`
1549
1666
  } catch { /* keep fallback */ }
1550
- output.parts.push({ type: "text", text: `Raven is ${enabled}. Version: ${PACKAGE_VERSION}. Model: ${model}. Reasoning: ${effort}. Timeout: ${timeout}s\n${update}\n\n${routeSummary()}\n\n${mcpSummary()}\n\n${statsText()}\n\nRun /raven help for commands.` })
1667
+ output.parts.push({ type: "text", text: `Raven is ${enabled}. Version: ${PACKAGE_VERSION}. Model: ${model}. Reasoning: ${effort}. Timeout: ${timeout}s${providerIssue}\n${update}\n\n${routeSummary()}\n\n${mcpSummary()}\n\n${statsText()}\n\nRun /raven help for commands.` })
1551
1668
  }
1552
1669
  },
1553
1670
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-raven",
3
- "version": "2.1.2",
3
+ "version": "2.1.3",
4
4
  "description": "Ready-to-use search tools and optional on-demand MCPs for OpenCode, routed through Raven to save your main model's context and cost.",
5
5
  "main": "./index.ts",
6
6
  "exports": {