opencode-raven 2.1.7 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.ts CHANGED
@@ -11,7 +11,8 @@ import { homedir, tmpdir } from "node:os"
11
11
  // ── Resolve paths relative to this package (works in node_modules/) ──
12
12
  const PKG_DIR = import.meta.dirname!
13
13
 
14
- const RAVEN_MD = join(PKG_DIR, "Raven.md")
14
+ const RAVEN_MD = join(PKG_DIR, "Raven.md")
15
+ const RAVEN_MCP_MD = join(PKG_DIR, "RavenMcp.md")
15
16
  const MCP_GUIDANCE_MD = join(PKG_DIR, "mcp-guidance.md")
16
17
  const PACKAGE_JSON = JSON.parse(readFileSync(join(PKG_DIR, "package.json"), "utf-8"))
17
18
  const PACKAGE_NAME = PACKAGE_JSON.name || "opencode-raven"
@@ -62,7 +63,7 @@ const DEFAULT_ON_DEMAND_MCP_METADATA: Record<string, OnDemandMcpMetadata> = {
62
63
  },
63
64
  }
64
65
 
65
- const NEVER_ROUTE_TOOLS = new Set(["raven_seek", "raven_mcp", "task", "subtask"])
66
+ const NEVER_ROUTE_TOOLS = new Set(["raven_seek", "raven_mcp", "raven_mcp_bridge", "task", "subtask"])
66
67
  const MAX_TIMER_MS = 2_147_483_647
67
68
  const MAX_TIMEOUT_SECONDS = Math.floor(MAX_TIMER_MS / 1000)
68
69
 
@@ -214,38 +215,58 @@ type OnDemandMcpServer = RemoteOnDemandMcpServer | LocalOnDemandMcpServer
214
215
  type OnDemandMcpDescriptionDetail = "full" | "minimized"
215
216
  type RavenMcpOperation = "list_tools" | "call_tool" | "list_resources" | "read_resource" | "list_prompts" | "get_prompt"
216
217
 
217
- interface RavenConfig {
218
- enabled: boolean
219
- model?: string
220
- reasoning_effort?: string
221
- ravenInstructions?: string
222
- routeTools?: string[]
223
- routeToolKeywords?: string[]
224
- onDemandMcpDescriptionDetail?: OnDemandMcpDescriptionDetail
225
- onDemandMcpServers?: Record<string, OnDemandMcpServer>
226
- excludeAgents?: string[]
227
- excludeTools?: string[]
228
- timeout?: number
229
- stats?: { bytes: number }
230
- }
218
+ interface RavenSeekConfig {
219
+ model?: string
220
+ reasoning_effort?: string
221
+ instructions?: string
222
+ routeTools?: string[]
223
+ routeToolKeywords?: string[]
224
+ excludeAgents?: string[]
225
+ excludeTools?: string[]
226
+ timeout?: number
227
+ }
228
+
229
+ interface RavenMcpConfig {
230
+ model?: string
231
+ reasoning_effort?: string
232
+ instructions?: string
233
+ timeout?: number
234
+ descriptionDetail?: OnDemandMcpDescriptionDetail
235
+ onDemandMcpServers?: Record<string, OnDemandMcpServer>
236
+ }
237
+
238
+ interface RavenConfig {
239
+ raven_seek: RavenSeekConfig
240
+ raven_mcp: RavenMcpConfig
241
+ stats?: { bytes: number }
242
+ }
231
243
 
232
244
  // ── Parse Raven.md frontmatter ──
233
- const ravenMd = readFileSync(RAVEN_MD, "utf-8")
234
- const { frontmatter: fm, prompt: ravenPrompt } = parseRavenMd(ravenMd)
235
-
236
- const DEFAULT_CONFIG: RavenConfig = {
237
- enabled: true,
238
- model: fm.model,
239
- reasoning_effort: fm.reasoning_effort,
240
- ravenInstructions: "",
241
- routeTools: DEFAULT_ROUTE_TOOLS,
242
- routeToolKeywords: DEFAULT_ROUTE_TOOL_KEYWORDS,
243
- onDemandMcpDescriptionDetail: "full",
244
- onDemandMcpServers: DEFAULT_ON_DEMAND_MCP_SERVERS,
245
- excludeAgents: [],
246
- excludeTools: [],
247
- timeout: 600,
248
- }
245
+ const ravenMd = readFileSync(RAVEN_MD, "utf-8")
246
+ const { frontmatter: fm, prompt: ravenPrompt } = parseRavenMd(ravenMd)
247
+ const ravenMcpMd = readFileSync(RAVEN_MCP_MD, "utf-8")
248
+ const { frontmatter: mcpFm, prompt: ravenMcpPrompt } = parseRavenMd(ravenMcpMd)
249
+
250
+ const DEFAULT_CONFIG: RavenConfig = {
251
+ raven_seek: {
252
+ model: fm.model,
253
+ reasoning_effort: fm.reasoning_effort,
254
+ instructions: "",
255
+ routeTools: DEFAULT_ROUTE_TOOLS,
256
+ routeToolKeywords: DEFAULT_ROUTE_TOOL_KEYWORDS,
257
+ excludeAgents: [],
258
+ excludeTools: [],
259
+ timeout: 600,
260
+ },
261
+ raven_mcp: {
262
+ model: mcpFm.model,
263
+ reasoning_effort: mcpFm.reasoning_effort,
264
+ instructions: "",
265
+ timeout: 600,
266
+ descriptionDetail: "full",
267
+ onDemandMcpServers: DEFAULT_ON_DEMAND_MCP_SERVERS,
268
+ },
269
+ }
249
270
 
250
271
  function parseRavenMd(raw: string): { frontmatter: Record<string, any>; prompt: string } {
251
272
  const parts = raw.split("---")
@@ -440,26 +461,43 @@ export default (async (input: PluginInput) => {
440
461
  const mcpMetadataFile = join(ravenConfigDir, "autogenerated-on-demand-mcp-metadata.json")
441
462
  const dynamicGuidanceFile = join(ravenConfigDir, "autogenerated-on-demand-guidance.md")
442
463
 
443
- function normalizeConfig(raw: any): RavenConfig {
444
- const source = raw && typeof raw === "object" ? raw : {}
445
- const normalized: RavenConfig = { ...DEFAULT_CONFIG }
446
-
447
- normalized.enabled = source.enabled !== false
448
- normalized.model = typeof source.model === "string" ? source.model : DEFAULT_CONFIG.model
449
- normalized.reasoning_effort = typeof source.reasoning_effort === "string" ? source.reasoning_effort : DEFAULT_CONFIG.reasoning_effort
450
- normalized.ravenInstructions = typeof source.ravenInstructions === "string" ? source.ravenInstructions : DEFAULT_CONFIG.ravenInstructions
451
- normalized.routeTools = uniqueStrings(source.routeTools, DEFAULT_ROUTE_TOOLS)
452
- normalized.routeToolKeywords = uniqueStrings(source.routeToolKeywords, DEFAULT_ROUTE_TOOL_KEYWORDS)
453
- normalized.onDemandMcpDescriptionDetail = source.onDemandMcpDescriptionDetail === "minimized" ? "minimized" : "full"
454
- normalized.onDemandMcpServers = source.onDemandMcpServers === undefined
455
- ? cloneOnDemandMcpServers(DEFAULT_ON_DEMAND_MCP_SERVERS)
456
- : normalizeOnDemandMcpServers(source.onDemandMcpServers)
457
- normalized.excludeAgents = uniqueStrings(source.excludeAgents)
458
- normalized.excludeTools = uniqueStrings(source.excludeTools)
459
- normalized.timeout = typeof source.timeout === "number" && Number.isFinite(source.timeout)
460
- && source.timeout >= 10 && source.timeout <= MAX_TIMEOUT_SECONDS
461
- ? Math.floor(source.timeout)
462
- : DEFAULT_CONFIG.timeout
464
+ function normalizeConfig(raw: any): RavenConfig {
465
+ const source = raw && typeof raw === "object" ? raw : {}
466
+ const seekSource = source.raven_seek && typeof source.raven_seek === "object" ? source.raven_seek : source
467
+ const mcpSource = source.raven_mcp && typeof source.raven_mcp === "object" ? source.raven_mcp : source
468
+ const legacyInstructions = typeof source.ravenInstructions === "string" ? source.ravenInstructions : undefined
469
+ const normalized: RavenConfig = {
470
+ raven_seek: {
471
+ model: typeof seekSource.model === "string" ? seekSource.model : DEFAULT_CONFIG.raven_seek.model,
472
+ reasoning_effort: typeof seekSource.reasoning_effort === "string" ? seekSource.reasoning_effort : DEFAULT_CONFIG.raven_seek.reasoning_effort,
473
+ instructions: typeof seekSource.instructions === "string" ? seekSource.instructions : legacyInstructions ?? "",
474
+ routeTools: uniqueStrings(seekSource.routeTools, DEFAULT_ROUTE_TOOLS),
475
+ routeToolKeywords: uniqueStrings(seekSource.routeToolKeywords, DEFAULT_ROUTE_TOOL_KEYWORDS),
476
+ excludeAgents: uniqueStrings(seekSource.excludeAgents),
477
+ excludeTools: uniqueStrings(seekSource.excludeTools),
478
+ timeout: typeof seekSource.timeout === "number" && Number.isFinite(seekSource.timeout)
479
+ && seekSource.timeout >= 10 && seekSource.timeout <= MAX_TIMEOUT_SECONDS
480
+ ? Math.floor(seekSource.timeout)
481
+ : DEFAULT_CONFIG.raven_seek.timeout,
482
+ },
483
+ raven_mcp: {
484
+ model: typeof mcpSource.model === "string" ? mcpSource.model : DEFAULT_CONFIG.raven_mcp.model,
485
+ reasoning_effort: typeof mcpSource.reasoning_effort === "string" ? mcpSource.reasoning_effort : DEFAULT_CONFIG.raven_mcp.reasoning_effort,
486
+ instructions: typeof mcpSource.instructions === "string" ? mcpSource.instructions : legacyInstructions ?? "",
487
+ timeout: typeof mcpSource.timeout === "number" && Number.isFinite(mcpSource.timeout)
488
+ && mcpSource.timeout >= 10 && mcpSource.timeout <= MAX_TIMEOUT_SECONDS
489
+ ? Math.floor(mcpSource.timeout)
490
+ : DEFAULT_CONFIG.raven_mcp.timeout,
491
+ descriptionDetail: mcpSource.descriptionDetail === "minimized"
492
+ ? "minimized"
493
+ : mcpSource.descriptionDetail === "full"
494
+ ? "full"
495
+ : source.onDemandMcpDescriptionDetail === "minimized" ? "minimized" : "full",
496
+ onDemandMcpServers: mcpSource.onDemandMcpServers === undefined
497
+ ? cloneOnDemandMcpServers(DEFAULT_ON_DEMAND_MCP_SERVERS)
498
+ : normalizeOnDemandMcpServers(mcpSource.onDemandMcpServers),
499
+ },
500
+ }
463
501
  normalized.stats = source.stats && typeof source.stats === "object"
464
502
  && typeof source.stats.bytes === "number" && Number.isFinite(source.stats.bytes) && source.stats.bytes >= 0
465
503
  ? { bytes: Math.round(source.stats.bytes) }
@@ -472,7 +510,9 @@ export default (async (input: PluginInput) => {
472
510
  if (existsSync(configFile)) {
473
511
  try {
474
512
  const raw = JSON.parse(readFileSync(configFile, "utf-8"))
475
- return normalizeConfig(raw)
513
+ const normalized = normalizeConfig(raw)
514
+ if (!raw.raven_seek || !raw.raven_mcp) writeConfig(normalized)
515
+ return normalized
476
516
  } catch {
477
517
  // Preserve malformed or partially-written config so the user can repair it.
478
518
  return { ...DEFAULT_CONFIG }
@@ -487,7 +527,7 @@ export default (async (input: PluginInput) => {
487
527
  try {
488
528
  mkdirSync(ravenConfigDir, { recursive: true })
489
529
  writeFileSync(configFile, JSON.stringify(config, null, 2) + "\n")
490
- } catch { /* non-fatal: config won't persist but toggle still works in-session */ }
530
+ } catch { /* non-fatal: config changes remain in-session */ }
491
531
  }
492
532
 
493
533
  function loadMcpMetadata(): Record<string, OnDemandMcpMetadata> {
@@ -522,18 +562,31 @@ export default (async (input: PluginInput) => {
522
562
  const raw = existsSync(configFile) ? JSON.parse(readFileSync(configFile, "utf-8")) : config
523
563
  const latest = normalizeConfig(raw)
524
564
  const patch = typeof update === "function" ? update(latest) : update
525
- config = normalizeConfig({ ...latest, ...patch })
526
- writeConfig({ ...raw, ...patch })
565
+ config = normalizeConfig({
566
+ ...latest,
567
+ ...patch,
568
+ raven_seek: { ...latest.raven_seek, ...patch.raven_seek },
569
+ raven_mcp: { ...latest.raven_mcp, ...patch.raven_mcp },
570
+ })
571
+ writeConfig(config)
527
572
  } catch {
528
573
  // Keep malformed or partially-written external edits intact instead of replacing them.
529
574
  const patch = typeof update === "function" ? update(config) : update
530
- config = normalizeConfig({ ...config, ...patch })
575
+ config = normalizeConfig({
576
+ ...config,
577
+ ...patch,
578
+ raven_seek: { ...config.raven_seek, ...patch.raven_seek },
579
+ raven_mcp: { ...config.raven_mcp, ...patch.raven_mcp },
580
+ })
531
581
  }
532
582
  return config
533
583
  }
534
584
  let mcpMetadata = loadMcpMetadata()
535
- const ravenSessions = new Set<string>()
536
- const ravenTaskCalls = new Set<string>()
585
+ type DelegationKind = "seek" | "mcp"
586
+ const ravenSessions = new Set<string>()
587
+ const ravenSessionKinds = new Map<string, DelegationKind>()
588
+ const activeRavenPrompts = new Set<string>()
589
+ const ravenTaskCalls = new Set<string>()
537
590
  const sessionAgents = new Map<string, string>()
538
591
  const ravenSessionParents = new Map<string, string>()
539
592
  const ravenErrorWaiters = new Map<string, (error: Error) => void>()
@@ -549,10 +602,12 @@ export default (async (input: PluginInput) => {
549
602
  let mcpRefreshPromise: Promise<{ refreshed: string[]; failed: string[] }> | undefined
550
603
  let mcpRefreshQueue: Promise<unknown> = Promise.resolve()
551
604
  const mcpRefreshControllers = new Set<AbortController>()
552
- let lastNotifiedMcpFailureKey = ""
553
- let ravenProviderIssue: string | undefined
554
-
555
- resetOnDemandMcpRuntimeStatuses()
605
+ let lastNotifiedMcpFailureKey = ""
606
+ const ravenProviderIssues: Partial<Record<DelegationKind, string>> = {}
607
+
608
+ // Replace generated instructions from older releases before OpenCode consumes them.
609
+ saveDynamicGuidance()
610
+ resetOnDemandMcpRuntimeStatuses()
556
611
 
557
612
  function resetOnDemandMcpRuntimeStatuses() {
558
613
  let changed = false
@@ -569,17 +624,17 @@ export default (async (input: PluginInput) => {
569
624
  }
570
625
 
571
626
  // ── Check if an agent is excluded from Raven enforcement (case-insensitive) ──
572
- function isExcluded(agent: string | undefined): boolean {
573
- if (!agent || !config.excludeAgents?.length) return false
574
- const lower = agent.toLowerCase()
575
- return config.excludeAgents.some((a) => a.toLowerCase() === lower)
576
- }
627
+ function isExcluded(agent: string | undefined): boolean {
628
+ if (!agent || !config.raven_seek.excludeAgents?.length) return false
629
+ const lower = agent.toLowerCase()
630
+ return config.raven_seek.excludeAgents.some((a) => a.toLowerCase() === lower)
631
+ }
577
632
 
578
633
  function isRouteConfigured(toolName: string): boolean {
579
634
  const tool = toolName.toLowerCase()
580
635
  if (NEVER_ROUTE_TOOLS.has(tool)) return false
581
- if (config.routeTools?.some((name) => name.toLowerCase() === tool)) return true
582
- if (config.routeToolKeywords?.some((keyword) => tool.includes(keyword.toLowerCase()))) return true
636
+ if (config.raven_seek.routeTools?.some((name) => name.toLowerCase() === tool)) return true
637
+ if (config.raven_seek.routeToolKeywords?.some((keyword) => tool.includes(keyword.toLowerCase()))) return true
583
638
  return globalMcpServers.some((server) => tool.startsWith(`${server.toLowerCase()}_`))
584
639
  }
585
640
 
@@ -609,31 +664,32 @@ export default (async (input: PluginInput) => {
609
664
  return query.length > 300 ? `${query.slice(0, 297)}...` : query
610
665
  }
611
666
 
612
- function rerouteMessage(tool: string, args: any): string {
613
- if (ravenProviderIssue) {
614
- return `Raven is currently unavailable: ${ravenProviderIssue}
615
-
616
- ${ravenRecoveryInstructions()}`
617
- }
667
+ function rerouteMessage(tool: string, args: any): string {
668
+ if (ravenProviderIssues.seek) {
669
+ return `Raven search is currently unavailable: ${ravenProviderIssues.seek}
670
+
671
+ ${ravenRecoveryInstructions("seek")}`
672
+ }
618
673
  return `The '${tool}' tool call is blocked by Raven. Your next tool call should be raven_seek(query="${attemptedQuery(tool, args).replace(/"/g, "'")}").`
619
674
  }
620
675
 
621
- function ravenRecoveryInstructions(): string {
622
- 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.
623
-
624
- Tell the user to switch Raven's model with:
625
- /raven model <provider/model>
626
-
627
- Example:
628
- /raven model openai/gpt-5.5
676
+ function ravenRecoveryInstructions(kind: DelegationKind): string {
677
+ const command = kind === "mcp" ? "/raven mcp model <provider/model>" : "/raven seek model <provider/model>"
678
+ 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.
679
+
680
+ Tell the user to switch Raven's model with:
681
+ ${command}
682
+
683
+ Example:
684
+ ${command.replace("<provider/model>", "openai/gpt-5.5")}
629
685
 
630
686
  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.`
631
687
  }
632
688
 
633
- function routeSummary(): string {
634
- const tools = config.routeTools?.length ? config.routeTools.join(", ") : "(none)"
635
- const mcps = globalMcpServers.length ? `${globalMcpServers.join(", ")} (auto-detected)` : "none detected"
636
- const keywords = config.routeToolKeywords?.length ? config.routeToolKeywords.join(", ") : "(none)"
689
+ function routeSummary(): string {
690
+ const tools = config.raven_seek.routeTools?.length ? config.raven_seek.routeTools.join(", ") : "(none)"
691
+ const mcps = globalMcpServers.length ? `${globalMcpServers.join(", ")} (auto-detected)` : "none detected"
692
+ const keywords = config.raven_seek.routeToolKeywords?.length ? config.raven_seek.routeToolKeywords.join(", ") : "(none)"
637
693
  const duplicates = duplicateMcpServers.length ? `\n Duplicate MCP config: ${duplicateMcpServers.join(", ")} (configured globally and on-demand)` : ""
638
694
  return `Raven routing:\n Routed tools: ${tools}\n Routed global MCPs: ${mcps}\n Routed tool keywords: ${keywords}${duplicates}`
639
695
  }
@@ -641,23 +697,29 @@ Then restart opencode for the model change to take effect. If the task can be co
641
697
  function mcpSummary(): string {
642
698
  const buckets = onDemandMcpStatusBuckets()
643
699
  const total = buckets.loaded.length + buckets.failed.length + buckets.pending.length
644
- if (!total) return `Raven on-demand MCPs (always behind Raven): none configured.\n Guidance detail: ${config.onDemandMcpDescriptionDetail ?? "full"}`
700
+ if (!total) return `Raven on-demand MCPs (always behind Raven): none configured.\n Guidance detail: ${config.raven_mcp.descriptionDetail ?? "full"}`
645
701
  const lines = [
646
702
  "Raven on-demand MCPs (always behind Raven):",
647
703
  ` Loaded: ${buckets.loaded.length ? buckets.loaded.join(", ") : "none"}`,
648
704
  ` Failed: ${buckets.failed.length ? buckets.failed.join(", ") : "none"}`,
649
705
  ` Pending: ${buckets.pending.length ? buckets.pending.join(", ") : "none"}`,
650
- ` Guidance detail: ${config.onDemandMcpDescriptionDetail ?? "full"}`,
706
+ ` Guidance detail: ${config.raven_mcp.descriptionDetail ?? "full"}`,
651
707
  ]
652
708
  return lines.join("\n")
653
709
  }
654
710
 
655
- function ravenAgentPrompt(): string {
656
- const extra = config.ravenInstructions?.trim()
657
- const onDemand = onDemandMcpGuidance("raven")
658
- const prompt = `${ravenPrompt}\n\nOn-demand MCP guidance:\n${onDemand}\nUse direct MCP tools when they are available globally. Use raven_mcp for on-demand MCPs listed above. If you do not know the exact tool name for an on-demand MCP, call raven_mcp with operation list_tools first, then call the selected tool with operation call_tool. For call_tool and get_prompt, pass arguments as a JSON object string in argumentsJson.`
659
- return extra ? `${prompt}\n\nAdditional user instructions:\n${extra}` : prompt
660
- }
711
+ function ravenAgentPrompt(): string {
712
+ const extra = config.raven_seek.instructions?.trim()
713
+ const prompt = ravenPrompt
714
+ return extra ? `${prompt}\n\nAdditional user instructions:\n${extra}` : prompt
715
+ }
716
+
717
+ function ravenMcpAgentPrompt(): string {
718
+ const extra = config.raven_mcp.instructions?.trim()
719
+ const onDemand = onDemandMcpGuidance("raven")
720
+ const prompt = `${ravenMcpPrompt}\n\nOn-demand MCP guidance:\n${onDemand}`
721
+ return extra ? `${prompt}\n\nAdditional user instructions:\n${extra}` : prompt
722
+ }
661
723
 
662
724
  function compactText(value: unknown): string {
663
725
  return String(value ?? "").replace(/\s+/g, " ").trim()
@@ -699,7 +761,7 @@ Then restart opencode for the model change to take effect. If the task can be co
699
761
  }
700
762
 
701
763
  function onDemandMcpGuidance(target: "delegate" | "raven" = "delegate", preferredOrder?: string[]): string {
702
- const servers = config.onDemandMcpServers ?? {}
764
+ const servers = config.raven_mcp.onDemandMcpServers ?? {}
703
765
  const activeEntries = activeOnDemandMcpEntries()
704
766
  const entries = orderedOnDemandMcpEntries(
705
767
  target === "raven"
@@ -711,7 +773,7 @@ Then restart opencode for the model change to take effect. If the task can be co
711
773
  if (!activeEntries.length) return "No enabled on-demand MCPs are configured."
712
774
  if (!entries.length) return "No loaded on-demand MCPs are available."
713
775
 
714
- const detail = config.onDemandMcpDescriptionDetail ?? "full"
776
+ const detail = config.raven_mcp.descriptionDetail ?? "full"
715
777
  const lines = entries.map(([name, server]) => {
716
778
  const metadata = mcpMetadata[name] ?? {}
717
779
  const userDescription = firstSentenceText(server.description)
@@ -724,10 +786,10 @@ Then restart opencode for the model change to take effect. If the task can be co
724
786
  .join("; ")
725
787
  return `- ${name}: ${summary} Tools: ${tools}`
726
788
  })
727
- if (target === "raven") {
728
- return `Raven can handle these on-demand MCP capabilities through raven_mcp:\n${lines.join("\n")}`
789
+ if (target === "raven") {
790
+ return `Use raven_mcp_bridge for these on-demand MCP capabilities:\n${lines.join("\n")}`
729
791
  }
730
- return `Raven can handle these on-demand MCP capabilities. Delegate matching requests with raven_seek; include the target MCP server and likely tool name when obvious, but do not call raven_mcp directly:\n${lines.join("\n")}`
792
+ return `Non-Raven agents can use these on-demand MCP capabilities through the public raven_mcp delegator. Use raven_mcp for matching requests and include the target MCP server and likely capability when obvious. The raven-mcp agent itself must call raven_mcp_bridge directly instead:\n${lines.join("\n")}`
731
793
  }
732
794
 
733
795
  function resolveEnvPlaceholders(value: string): string {
@@ -816,7 +878,7 @@ Then restart opencode for the model change to take effect. If the task can be co
816
878
  let mcpIdleTimer: ReturnType<typeof setInterval> | undefined
817
879
 
818
880
  function mcpTimeoutMs(server: OnDemandMcpServer): number {
819
- return server.timeout ?? (config.timeout ?? 600) * 1000
881
+ return server.timeout ?? (config.raven_mcp.timeout ?? 600) * 1000
820
882
  }
821
883
 
822
884
  async function closeMcpClient(client: Client, transport: any) {
@@ -917,7 +979,7 @@ Then restart opencode for the model change to take effect. If the task can be co
917
979
  }
918
980
 
919
981
  function activeOnDemandMcpEntries(): [string, OnDemandMcpServer][] {
920
- return Object.entries(config.onDemandMcpServers ?? {}).filter(([, server]) => server.enabled !== false)
982
+ return Object.entries(config.raven_mcp.onDemandMcpServers ?? {}).filter(([, server]) => server.enabled !== false)
921
983
  }
922
984
 
923
985
  function waitForMcpConnection(attempt: McpConnectionAttempt, signal?: AbortSignal): Promise<McpConnection> {
@@ -1214,7 +1276,7 @@ Then restart opencode for the model change to take effect. If the task can be co
1214
1276
 
1215
1277
  function onDemandMcpStatus(): string {
1216
1278
  const entries = activeOnDemandMcpEntries()
1217
- if (!Object.keys(config.onDemandMcpServers ?? {}).length) return "Raven on-demand MCPs: none configured."
1279
+ if (!Object.keys(config.raven_mcp.onDemandMcpServers ?? {}).length) return "Raven on-demand MCPs: none configured."
1218
1280
  if (!entries.length) return "Raven on-demand MCPs: none enabled."
1219
1281
  const buckets = onDemandMcpStatusBuckets()
1220
1282
  const lines = entries.map(([name, server]) => {
@@ -1227,7 +1289,7 @@ Then restart opencode for the model change to take effect. If the task can be co
1227
1289
  const error = status === "failed" && metadata?.lastError ? `, error: ${metadata.lastError}` : ""
1228
1290
  return `- ${name} (${server.type}): ${displayMcpStatus(status)}, ${source}, ${generated}, ${tools}${avoided}${error}`
1229
1291
  })
1230
- return `Raven on-demand MCPs (${config.onDemandMcpDescriptionDetail ?? "full"} guidance):\n Loaded: ${buckets.loaded.length ? buckets.loaded.join(", ") : "none"}\n Failed: ${buckets.failed.length ? buckets.failed.join(", ") : "none"}\n Pending: ${buckets.pending.length ? buckets.pending.join(", ") : "none"}\n${lines.join("\n")}`
1292
+ return `Raven on-demand MCPs (${config.raven_mcp.descriptionDetail ?? "full"} guidance):\n Loaded: ${buckets.loaded.length ? buckets.loaded.join(", ") : "none"}\n Failed: ${buckets.failed.length ? buckets.failed.join(", ") : "none"}\n Pending: ${buckets.pending.length ? buckets.pending.join(", ") : "none"}\n${lines.join("\n")}`
1231
1293
  }
1232
1294
 
1233
1295
  function avoidedMcpSchemaBytes(): number {
@@ -1243,9 +1305,9 @@ Then restart opencode for the model change to take effect. If the task can be co
1243
1305
  return `Raven context saved:\n Avoided MCP schema load: ${formatBytes(avoided)} (~${formatTokens(avoided)} context)\n This session: ${formatBytes(sessionBytes)} (~${formatTokens(sessionBytes)} context)\n All time: ${formatBytes(totalBytes)} (~${formatTokens(totalBytes)} context)`
1244
1306
  }
1245
1307
 
1246
- function helpText(): string {
1247
- return `Raven commands:\n /raven help — show this help\n /raven on — enable tool/MCP routing\n /raven off — disable tool/MCP routing\n /raven route — show or edit routed tools/keywords\n /raven mcp — show or refresh on-demand MCP metadata\n /raven update — check npm, clear plugin cache if newer, then restart opencode\n /raven model <name> — change Raven's model (requires restart)\n /raven effort <value> — change Raven's reasoning effort (requires restart)\n /raven timeout <seconds> — change raven_seek timeout\n /raven stats — show context saved`
1248
- }
1308
+ function helpText(): string {
1309
+ return `Raven commands:\n /raven help - show this help\n /raven route - show or edit routed tools/keywords\n /raven seek model <name> - change the search model (requires restart)\n /raven seek effort <value> - change search reasoning effort (requires restart)\n /raven seek timeout <seconds> - change search timeout\n /raven mcp - show on-demand MCP metadata\n /raven mcp refresh [server] - refresh MCP metadata\n /raven mcp detail full|minimized - change MCP guidance detail\n /raven mcp model <name> - change the MCP model (requires restart)\n /raven mcp effort <value> - change MCP reasoning effort (requires restart)\n /raven mcp timeout <seconds> - change MCP delegation timeout\n /raven update - check for an update\n /raven stats - show context saved`
1310
+ }
1249
1311
 
1250
1312
  function formatMcpList(title: string, items: any[], fields: string[] = ["name", "description"]): string {
1251
1313
  if (!items.length) return `${title}: none found.`
@@ -1346,12 +1408,10 @@ Then restart opencode for the model change to take effect. If the task can be co
1346
1408
  return { current: PACKAGE_VERSION, latest, available: !!latest && compareVersions(latest, PACKAGE_VERSION) > 0 }
1347
1409
  }
1348
1410
 
1349
- async function countRavenSavedCandidateBytes(sessionId: string): Promise<number> {
1350
- const messagesResp = await client.session.messages({ path: { id: sessionId }, query: { limit: 200 } })
1351
- const messages = (messagesResp as any)?.data ?? []
1352
- const assistantMessages = messages.filter((m: any) =>
1353
- m?.info?.role === "assistant" && m?.info?.tokens?.output > 0
1354
- )
1411
+ function estimateRavenSavedCandidateBytes(messages: any[]): number {
1412
+ const assistantMessages = messages.filter((m: any) =>
1413
+ m?.info?.role === "assistant" && m?.info?.tokens?.output > 0
1414
+ )
1355
1415
  const first = assistantMessages[0]
1356
1416
  const last = assistantMessages[assistantMessages.length - 1]
1357
1417
  const t = last?.info?.tokens
@@ -1363,9 +1423,15 @@ Then restart opencode for the model change to take effect. If the task can be co
1363
1423
  const baselineTokens = firstTokens
1364
1424
  ? (firstTokens.input ?? 0) + (firstTokens.cache?.read ?? 0) + (firstTokens.cache?.write ?? 0)
1365
1425
  : 0
1366
- const savedCandidateTokens = Math.max(0, totalTokens - baselineTokens)
1367
- return savedCandidateTokens * 4
1368
- }
1426
+ const savedCandidateTokens = Math.max(0, totalTokens - baselineTokens)
1427
+ return savedCandidateTokens * 4
1428
+ }
1429
+
1430
+ async function countRavenSavedCandidateBytes(sessionId: string, signal?: AbortSignal): Promise<number> {
1431
+ const messagesResp = await client.session.messages({ path: { id: sessionId }, signal })
1432
+ const messages = (messagesResp as any)?.data ?? []
1433
+ return estimateRavenSavedCandidateBytes(messages)
1434
+ }
1369
1435
 
1370
1436
  async function getUpdateInfo(): Promise<{ current: string; latest?: string; available: boolean }> {
1371
1437
  if (updateInfo) return updateInfo
@@ -1452,7 +1518,7 @@ Then restart opencode for the model change to take effect. If the task can be co
1452
1518
  } catch { /* MCP failure notifications are best-effort */ }
1453
1519
  }
1454
1520
 
1455
- async function notifyIfDuplicateMcps() {
1521
+ async function notifyIfDuplicateMcps() {
1456
1522
  try {
1457
1523
  if (!duplicateMcpServers.length) return
1458
1524
  const label = duplicateMcpServers.length === 1 ? duplicateMcpServers[0] : `${duplicateMcpServers.length} MCPs`
@@ -1464,9 +1530,242 @@ Then restart opencode for the model change to take effect. If the task can be co
1464
1530
  duration: 12000,
1465
1531
  },
1466
1532
  })
1467
- } catch { /* duplicate MCP notifications are best-effort */ }
1468
- }
1469
-
1533
+ } catch { /* duplicate MCP notifications are best-effort */ }
1534
+ }
1535
+
1536
+ function delegationAgent(kind: DelegationKind): "raven" | "raven-mcp" {
1537
+ return kind === "mcp" ? "raven-mcp" : "raven"
1538
+ }
1539
+
1540
+ function delegationToolName(kind: DelegationKind): "raven_seek" | "raven_mcp" {
1541
+ return kind === "mcp" ? "raven_mcp" : "raven_seek"
1542
+ }
1543
+
1544
+ function delegationTitle(kind: DelegationKind): string {
1545
+ return kind === "mcp" ? "Raven MCP" : "Raven Seek"
1546
+ }
1547
+
1548
+ async function rootSessionId(sessionId: string, signal?: AbortSignal): Promise<string> {
1549
+ let current = sessionId
1550
+ const seen = new Set<string>()
1551
+ while (!seen.has(current)) {
1552
+ seen.add(current)
1553
+ const response = await client.session.get({ path: { id: current }, signal })
1554
+ const session = (response as any)?.data ?? response
1555
+ if (!session?.parentID) return current
1556
+ current = session.parentID
1557
+ }
1558
+ return sessionId
1559
+ }
1560
+
1561
+ async function executeRavenDelegation(kind: DelegationKind, args: { query: string; sessionId?: string }, context: any) {
1562
+ const started = Date.now()
1563
+ const agent = delegationAgent(kind)
1564
+ const toolName = delegationToolName(kind)
1565
+ const title = delegationTitle(kind)
1566
+ const timeout = (kind === "mcp" ? config.raven_mcp.timeout : config.raven_seek.timeout) ?? 600
1567
+ const timeoutMs = timeout * 1000
1568
+ let sessionId: string | undefined
1569
+ let resumed = false
1570
+ let previousSavedCandidate = 0
1571
+
1572
+ try {
1573
+ const requestedSessionId = args.sessionId?.trim()
1574
+ if (args.sessionId !== undefined && !requestedSessionId) {
1575
+ return { title, output: "Cannot resume Raven: sessionId must not be empty." }
1576
+ }
1577
+
1578
+ let rootId = context.sessionID
1579
+ try {
1580
+ rootId = await rootSessionId(context.sessionID, context.abort)
1581
+ } catch (error) {
1582
+ if (context.abort.aborted) throw error
1583
+ // Falling back preserves delegation if an older OpenCode build cannot read the caller session.
1584
+ }
1585
+
1586
+ if (requestedSessionId) {
1587
+ const knownKind = ravenSessionKinds.get(requestedSessionId)
1588
+ const knownRavenSession = knownKind === kind || sessionAgents.get(requestedSessionId) === agent
1589
+ let existingSession: any
1590
+ try {
1591
+ const sessionResp = await client.session.get({ path: { id: requestedSessionId }, signal: context.abort })
1592
+ existingSession = (sessionResp as any)?.data ?? sessionResp
1593
+ } catch (error) {
1594
+ if (context.abort.aborted) throw error
1595
+ return { title, output: `Cannot resume Raven session ${requestedSessionId}: the session was not found or could not be read.` }
1596
+ }
1597
+
1598
+ if (!existingSession?.id || existingSession.id !== requestedSessionId) {
1599
+ return { title, output: `Cannot resume Raven session ${requestedSessionId}: the session was not found or could not be read.` }
1600
+ }
1601
+ const validParent = existingSession.parentID === rootId || existingSession.parentID === context.sessionID
1602
+ if (!validParent || existingSession.projectID !== input.project.id) {
1603
+ return { title, output: "Cannot resume Raven: sessionId is not part of this main session's Raven tree." }
1604
+ }
1605
+
1606
+ let messages: any[]
1607
+ try {
1608
+ const messagesResp = await client.session.messages({ path: { id: requestedSessionId }, signal: context.abort })
1609
+ const messageData = (messagesResp as any)?.data
1610
+ if (!Array.isArray(messageData)) {
1611
+ return { title, output: `Cannot resume Raven session ${requestedSessionId}: its message history could not be read.` }
1612
+ }
1613
+ messages = messageData
1614
+ } catch (error) {
1615
+ if (context.abort.aborted) throw error
1616
+ return { title, output: `Cannot resume Raven session ${requestedSessionId}: its message history could not be read.` }
1617
+ }
1618
+
1619
+ const userMessages = messages.filter((message: any) => message?.info?.role === "user")
1620
+ const persistedRavenSession = userMessages.length > 0
1621
+ && userMessages.every((message: any) => message.info.agent === agent)
1622
+ if (!persistedRavenSession && !(knownRavenSession && userMessages.length === 0)) {
1623
+ return { title, output: `Cannot resume Raven: sessionId does not belong to the ${agent} agent.` }
1624
+ }
1625
+
1626
+ sessionId = requestedSessionId
1627
+ resumed = true
1628
+ previousSavedCandidate = estimateRavenSavedCandidateBytes(messages)
1629
+ } else {
1630
+ const session = await client.session.create({
1631
+ body: {
1632
+ parentID: rootId,
1633
+ title: `${toolName}: ${args.query.slice(0, 80)}`,
1634
+ },
1635
+ signal: context.abort,
1636
+ })
1637
+ sessionId = (session as any)?.data?.id ?? (session as any)?.id
1638
+ if (!sessionId) return { title, output: "Failed to create Raven session." }
1639
+ }
1640
+
1641
+ ravenSessions.add(sessionId)
1642
+ ravenSessionKinds.set(sessionId, kind)
1643
+ const activeSessionId = sessionId
1644
+ context.metadata({ metadata: { sessionId } })
1645
+
1646
+ try {
1647
+ writeFileSync(join(tmpdir(), "raven-sessions.log"), `${new Date().toISOString()} ${sessionId}\n`)
1648
+ } catch { /* non-fatal */ }
1649
+
1650
+ if (activeRavenPrompts.has(activeSessionId)) {
1651
+ return {
1652
+ title,
1653
+ metadata: { sessionId: activeSessionId },
1654
+ output: `Raven session \`${activeSessionId}\` is already handling a request. Wait for it to finish before sending another follow-up.`,
1655
+ }
1656
+ }
1657
+ activeRavenPrompts.add(activeSessionId)
1658
+
1659
+ let timer: ReturnType<typeof setTimeout> | undefined
1660
+ let promptCompleted = false
1661
+ let sessionAbort: Promise<unknown> | undefined
1662
+ const promptController = new AbortController()
1663
+ const abortPrompt = () => {
1664
+ if (promptController.signal.aborted) return
1665
+ promptController.abort(context.abort.reason)
1666
+ const abortController = new AbortController()
1667
+ const abortTimer = setTimeout(() => abortController.abort(), Math.min(timeoutMs, 5_000))
1668
+ sessionAbort = client.session.abort({
1669
+ path: { id: activeSessionId },
1670
+ signal: abortController.signal,
1671
+ throwOnError: true,
1672
+ }).catch(() => undefined).finally(() => clearTimeout(abortTimer))
1673
+ }
1674
+ if (context.abort.aborted) abortPrompt()
1675
+ else context.abort.addEventListener("abort", abortPrompt, { once: true })
1676
+ const providerError = waitForRavenProviderError(sessionId)
1677
+ const firstProgress = waitForRavenFirstProgress(sessionId, Math.min(timeoutMs, 30_000))
1678
+
1679
+ try {
1680
+ const result = await Promise.race([
1681
+ client.session.prompt({
1682
+ path: { id: sessionId },
1683
+ body: { agent, parts: [{ type: "text", text: args.query }] },
1684
+ signal: promptController.signal,
1685
+ throwOnError: true,
1686
+ }),
1687
+ providerError,
1688
+ firstProgress,
1689
+ new Promise<never>((_, reject) => {
1690
+ timer = setTimeout(() => {
1691
+ abortPrompt()
1692
+ reject(new Error(`Raven timed out after ${timeout}s - child session retained: ${sessionId}`))
1693
+ }, timeoutMs)
1694
+ }),
1695
+ ])
1696
+ promptCompleted = true
1697
+ if (timer) clearTimeout(timer)
1698
+ clearRavenWaiters(sessionId)
1699
+ if ((result as any)?.error) throw new Error(providerErrorMessage((result as any).error))
1700
+
1701
+ const elapsed = ((Date.now() - started) / 1000).toFixed(1)
1702
+ const parts = (result as any)?.data?.parts ?? []
1703
+ const output = parts
1704
+ .filter((part: any) => part.type === "text" && part.text)
1705
+ .map((part: any) => part.text)
1706
+ .join("\n") || "Raven returned no results."
1707
+
1708
+ let savedCandidate = 0
1709
+ const statsController = new AbortController()
1710
+ const abortStats = () => statsController.abort(context.abort.reason)
1711
+ const statsTimer = setTimeout(() => statsController.abort(), Math.min(timeoutMs, 5_000))
1712
+ if (context.abort.aborted) abortStats()
1713
+ else context.abort.addEventListener("abort", abortStats, { once: true })
1714
+ try {
1715
+ const totalSavedCandidate = await countRavenSavedCandidateBytes(sessionId, statsController.signal)
1716
+ savedCandidate = Math.max(0, totalSavedCandidate - previousSavedCandidate)
1717
+ } catch { /* best-effort */ }
1718
+ finally {
1719
+ clearTimeout(statsTimer)
1720
+ context.abort.removeEventListener("abort", abortStats)
1721
+ }
1722
+ if (savedCandidate <= 0) {
1723
+ for (const part of parts) {
1724
+ if (part.args) savedCandidate += JSON.stringify(part.args).length
1725
+ if (part.content) savedCandidate += typeof part.content === "string" ? part.content.length : JSON.stringify(part.content).length
1726
+ }
1727
+ }
1728
+
1729
+ const continuation = `Raven session: \`${sessionId}\`${resumed ? " (continued)" : ""}. Pass this as \`sessionId\` in a later \`${toolName}\` call to continue the same retrieval.`
1730
+ const activity = kind === "mcp" ? "used MCPs" : "searched"
1731
+ const resultOutput = `${output}\n\n*Raven ${activity} for ${elapsed}s - ${formatBytes(savedCandidate)} handled, ${formatTokens(savedCandidate)} tokens*\n\n${continuation}`
1732
+ addBytes(Math.max(0, savedCandidate - resultOutput.length))
1733
+ delete ravenProviderIssues[kind]
1734
+ return { title, metadata: { sessionId }, output: resultOutput }
1735
+ } finally {
1736
+ context.abort.removeEventListener("abort", abortPrompt)
1737
+ if (!promptCompleted) abortPrompt()
1738
+ if (sessionAbort) await sessionAbort
1739
+ if (timer) clearTimeout(timer)
1740
+ clearRavenWaiters(sessionId)
1741
+ activeRavenPrompts.delete(activeSessionId)
1742
+ }
1743
+ } catch (error: any) {
1744
+ const elapsed = ((Date.now() - started) / 1000).toFixed(1)
1745
+ const message = String(error?.message ?? error ?? "").toLowerCase()
1746
+ const missingSession = /session.*not found|not found.*session/i.test(message)
1747
+ const activity = kind === "mcp" ? "MCP retrieval" : "search"
1748
+ const hint =
1749
+ context.abort.aborted ? `Raven ${activity} cancelled.`
1750
+ : /rate.?limit|too many requests|429/i.test(message) ? "Raven rate limited - wait 30s, then retry with a narrower query."
1751
+ : /quota|usage.?limit|usage exceeded|free usage|subscribe to go|subscription|billing|insufficient.*(?:credit|balance|quota)/i.test(message) ? "Raven API quota/subscription exhausted - proceed without Raven and mention what information may be missing."
1752
+ : /auth|unauthorized|forbidden|401|403|api key|login/i.test(message) ? "Raven provider authentication failed - proceed without Raven and check the configured provider login or API key."
1753
+ : /token|context.?length|too large|too long/i.test(message) ? "Raven query too large - shorten your query and retry."
1754
+ : missingSession ? `The Raven session is no longer available - retry without sessionId to start a new ${activity}.`
1755
+ : /model|unavailable|down|not found/i.test(message) ? "Raven model unavailable - retry later, or proceed without the missing information."
1756
+ : /did not produce an initial response|retry dialog|timeout|timed.?out|session retained/i.test(message) ? error.message
1757
+ : `Raven ${activity} failed. Note the resulting information gap. [${error.message || error}]`
1758
+ 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(message)) {
1759
+ ravenProviderIssues[kind] = hint
1760
+ }
1761
+ const recovery = ravenProviderIssues[kind] ? `\n\n${ravenRecoveryInstructions(kind)}` : ""
1762
+ const continuation = sessionId && !missingSession
1763
+ ? `\n\nRaven session: \`${sessionId}\`. Pass this as \`sessionId\` in a later \`${toolName}\` call to retry or continue.`
1764
+ : ""
1765
+ return { title, metadata: sessionId ? { sessionId } : undefined, output: `${hint}${recovery}\n\n*Attempt took ${elapsed}s*${continuation}` }
1766
+ }
1767
+ }
1768
+
1470
1769
  return {
1471
1770
  async dispose() {
1472
1771
  for (const controller of mcpRefreshControllers) controller.abort()
@@ -1501,26 +1800,38 @@ Then restart opencode for the model change to take effect. If the task can be co
1501
1800
 
1502
1801
  // Register Raven from Raven.md, with config file overrides
1503
1802
  configInput.agent = configInput.agent || {}
1504
- configInput.agent.raven = {
1505
- description: fm.description || "",
1506
- mode: fm.mode || "subagent",
1507
- hidden: fm.hidden !== undefined ? fm.hidden : false,
1508
- model: config.model || fm.model,
1509
- options: {
1510
- ...extractOptions(fm),
1511
- ...(config.reasoning_effort ? { reasoning_effort: config.reasoning_effort } : {}),
1512
- },
1513
- permission: fm.permission || {},
1514
- prompt: ravenAgentPrompt(),
1515
- }
1803
+ configInput.agent.raven = {
1804
+ description: fm.description || "",
1805
+ mode: fm.mode || "subagent",
1806
+ hidden: fm.hidden !== undefined ? fm.hidden : false,
1807
+ model: config.raven_seek.model || fm.model,
1808
+ options: {
1809
+ ...extractOptions(fm),
1810
+ ...(config.raven_seek.reasoning_effort ? { reasoning_effort: config.raven_seek.reasoning_effort } : {}),
1811
+ },
1812
+ permission: fm.permission || {},
1813
+ prompt: ravenAgentPrompt(),
1814
+ }
1815
+ configInput.agent["raven-mcp"] = {
1816
+ description: mcpFm.description || "",
1817
+ mode: mcpFm.mode || "subagent",
1818
+ hidden: mcpFm.hidden !== undefined ? mcpFm.hidden : true,
1819
+ model: config.raven_mcp.model || mcpFm.model,
1820
+ options: {
1821
+ ...extractOptions(mcpFm),
1822
+ ...(config.raven_mcp.reasoning_effort ? { reasoning_effort: config.raven_mcp.reasoning_effort } : {}),
1823
+ },
1824
+ permission: mcpFm.permission || {},
1825
+ prompt: ravenMcpAgentPrompt(),
1826
+ }
1516
1827
 
1517
1828
  // Register /raven command
1518
1829
  configInput.command = configInput.command || {}
1519
1830
  if (!configInput.command.raven) {
1520
- configInput.command.raven = {
1521
- template: "Manage Raven: /raven on|off|route|mcp|update|model <name>|status",
1522
- description: "Toggle Raven routing, manage routed/on-demand MCPs, or change Raven's model",
1523
- }
1831
+ configInput.command.raven = {
1832
+ template: "Manage Raven search routing, on-demand MCPs, models, timeouts, updates, and status",
1833
+ description: "Manage Raven search routing, on-demand MCPs, and per-agent models",
1834
+ }
1524
1835
  }
1525
1836
 
1526
1837
  scheduleOnDemandMcpRefresh()
@@ -1529,33 +1840,93 @@ Then restart opencode for the model change to take effect. If the task can be co
1529
1840
  duplicateMcpToastPending = duplicateMcpServers.length > 0
1530
1841
  },
1531
1842
 
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
- async execute(args, context) {
1540
- const started = Date.now()
1541
- const timeout = (config.timeout ?? 600) * 1000
1542
- try {
1543
- // Create a Raven session
1544
- const session = await client.session.create({
1545
- body: {
1546
- parentID: context.sessionID,
1547
- title: `raven_seek: ${args.query.slice(0, 80)}`,
1548
- },
1549
- signal: context.abort,
1550
- })
1551
-
1552
- const sessionId = (session as any)?.data?.id ?? (session as any)?.id
1553
- if (!sessionId) {
1554
- return { title: "Raven Seek", output: "Failed to create Raven session." }
1555
- }
1556
-
1557
- ravenSessions.add(sessionId)
1558
-
1843
+ // Register raven_seek tool — lets agents with task:false still delegate through Raven
1844
+ tool: {
1845
+ "raven_seek": tool({
1846
+ description: "Search and evidence retrieval only. Creates or resumes a Raven Search session for routed local search, filesystem discovery, URL fetching, web/docs/GitHub search, globally configured MCP searches, or command-output inspection. Do not delegate code review, auditing, correctness judgments, implementation, debugging decisions, planning, or general task execution; the calling agent must perform that work.",
1847
+ args: {
1848
+ query: tool.schema.string().describe("A narrowly scoped search or evidence-retrieval request. Include original blocked tool args, exact URLs, paths, patterns, or bounded command-output checks. Do not ask Raven to review or audit code."),
1849
+ 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."),
1850
+ },
1851
+ async execute(args, context) {
1852
+ const started = Date.now()
1853
+ const timeout = (config.raven_seek.timeout ?? 600) * 1000
1854
+ let sessionId: string | undefined
1855
+ let resumed = false
1856
+ let previousSavedCandidate = 0
1857
+ try {
1858
+ const requestedSessionId = args.sessionId?.trim()
1859
+ if (args.sessionId !== undefined && !requestedSessionId) {
1860
+ return { title: "Raven Seek", output: "Cannot resume Raven: sessionId must not be empty." }
1861
+ }
1862
+
1863
+ let rootId = context.sessionID
1864
+ try {
1865
+ rootId = await rootSessionId(context.sessionID, context.abort)
1866
+ } catch (error) {
1867
+ if (context.abort.aborted) throw error
1868
+ }
1869
+
1870
+ if (requestedSessionId) {
1871
+ const knownRavenSession = ravenSessionKinds.get(requestedSessionId) === "seek" || sessionAgents.get(requestedSessionId) === "raven"
1872
+ let existingSession: any
1873
+ try {
1874
+ const sessionResp = await client.session.get({ path: { id: requestedSessionId }, signal: context.abort })
1875
+ existingSession = (sessionResp as any)?.data ?? sessionResp
1876
+ } catch (err) {
1877
+ if (context.abort.aborted) throw err
1878
+ return { title: "Raven Seek", output: `Cannot resume Raven session ${requestedSessionId}: the session was not found or could not be read.` }
1879
+ }
1880
+
1881
+ if (!existingSession?.id || existingSession.id !== requestedSessionId) {
1882
+ return { title: "Raven Seek", output: `Cannot resume Raven session ${requestedSessionId}: the session was not found or could not be read.` }
1883
+ }
1884
+ if ((existingSession.parentID !== rootId && existingSession.parentID !== context.sessionID) || existingSession.projectID !== input.project.id) {
1885
+ return { title: "Raven Seek", output: "Cannot resume Raven: sessionId is not a Raven child of this main session." }
1886
+ }
1887
+
1888
+ let messages: any[]
1889
+ try {
1890
+ const messagesResp = await client.session.messages({ path: { id: requestedSessionId }, signal: context.abort })
1891
+ const messageData = (messagesResp as any)?.data
1892
+ if (!Array.isArray(messageData)) {
1893
+ return { title: "Raven Seek", output: `Cannot resume Raven session ${requestedSessionId}: its message history could not be read.` }
1894
+ }
1895
+ messages = messageData
1896
+ } catch (err) {
1897
+ if (context.abort.aborted) throw err
1898
+ return { title: "Raven Seek", output: `Cannot resume Raven session ${requestedSessionId}: its message history could not be read.` }
1899
+ }
1900
+
1901
+ const userMessages = messages.filter((message: any) => message?.info?.role === "user")
1902
+ const persistedRavenSession = userMessages.length > 0
1903
+ && userMessages.every((message: any) => message.info.agent === "raven")
1904
+ if (!persistedRavenSession && !(knownRavenSession && userMessages.length === 0)) {
1905
+ return { title: "Raven Seek", output: "Cannot resume Raven: sessionId is not a Raven child of this main session." }
1906
+ }
1907
+
1908
+ sessionId = requestedSessionId
1909
+ resumed = true
1910
+ previousSavedCandidate = estimateRavenSavedCandidateBytes(messages)
1911
+ } else {
1912
+ const session = await client.session.create({
1913
+ body: {
1914
+ parentID: rootId,
1915
+ title: `raven_seek: ${args.query.slice(0, 80)}`,
1916
+ },
1917
+ signal: context.abort,
1918
+ })
1919
+
1920
+ sessionId = (session as any)?.data?.id ?? (session as any)?.id
1921
+ if (!sessionId) {
1922
+ return { title: "Raven Seek", output: "Failed to create Raven session." }
1923
+ }
1924
+ }
1925
+
1926
+ ravenSessions.add(sessionId)
1927
+ ravenSessionKinds.set(sessionId, "seek")
1928
+ const activeSessionId = sessionId
1929
+
1559
1930
  // Preserve the child session link for TUI clients that render it.
1560
1931
  context.metadata({ metadata: { sessionId } })
1561
1932
 
@@ -1564,15 +1935,35 @@ Then restart opencode for the model change to take effect. If the task can be co
1564
1935
  const logFile = join(tmpdir(), "raven-sessions.log")
1565
1936
  const ts = new Date().toISOString()
1566
1937
  writeFileSync(logFile, `${ts} ${sessionId}\n`)
1567
- } catch { /* non-fatal */ }
1938
+ } catch { /* non-fatal */ }
1939
+
1940
+ if (activeRavenPrompts.has(activeSessionId)) {
1941
+ return {
1942
+ title: "Raven Seek",
1943
+ metadata: { sessionId: activeSessionId },
1944
+ output: `Raven session \`${activeSessionId}\` is already handling a request. Wait for it to finish before sending another follow-up.`,
1945
+ }
1946
+ }
1947
+ activeRavenPrompts.add(activeSessionId)
1568
1948
 
1569
1949
  // Send the query to Raven with timeout. Provider/API errors are also
1570
1950
  // surfaced through message.updated events; fail as soon as one lands
1571
1951
  // so the caller does not wait for the full timeout on quota/auth issues.
1572
1952
  let timer: ReturnType<typeof setTimeout> | undefined
1573
1953
  let promptCompleted = false
1954
+ let sessionAbort: Promise<unknown> | undefined
1574
1955
  const promptController = new AbortController()
1575
- const abortPrompt = () => promptController.abort(context.abort.reason)
1956
+ const abortPrompt = () => {
1957
+ if (promptController.signal.aborted) return
1958
+ promptController.abort(context.abort.reason)
1959
+ const abortController = new AbortController()
1960
+ const abortTimer = setTimeout(() => abortController.abort(), Math.min(timeout, 5_000))
1961
+ sessionAbort = client.session.abort({
1962
+ path: { id: activeSessionId },
1963
+ signal: abortController.signal,
1964
+ throwOnError: true,
1965
+ }).catch(() => undefined).finally(() => clearTimeout(abortTimer))
1966
+ }
1576
1967
  if (context.abort.aborted) abortPrompt()
1577
1968
  else context.abort.addEventListener("abort", abortPrompt, { once: true })
1578
1969
  const providerError = waitForRavenProviderError(sessionId)
@@ -1586,12 +1977,13 @@ Then restart opencode for the model change to take effect. If the task can be co
1586
1977
  parts: [{ type: "text", text: args.query }],
1587
1978
  },
1588
1979
  signal: promptController.signal,
1980
+ throwOnError: true,
1589
1981
  }),
1590
1982
  providerError,
1591
1983
  firstProgress,
1592
1984
  new Promise<never>((_, reject) => {
1593
1985
  timer = setTimeout(() => {
1594
- promptController.abort()
1986
+ abortPrompt()
1595
1987
  reject(new Error(`Raven timed out after ${timeout / 1000}s — child session retained: ${sessionId}`))
1596
1988
  }, timeout)
1597
1989
  }),
@@ -1599,8 +1991,9 @@ Then restart opencode for the model change to take effect. If the task can be co
1599
1991
  promptCompleted = true
1600
1992
  if (timer) clearTimeout(timer)
1601
1993
  clearRavenWaiters(sessionId)
1994
+ if ((result as any)?.error) throw new Error(providerErrorMessage((result as any).error))
1602
1995
 
1603
- const elapsed = ((Date.now() - started) / 1000).toFixed(1)
1996
+ const elapsed = ((Date.now() - started) / 1000).toFixed(1)
1604
1997
 
1605
1998
  // Extract text from the response
1606
1999
  const parts = (result as any)?.data?.parts ?? []
@@ -1609,51 +2002,79 @@ Then restart opencode for the model change to take effect. If the task can be co
1609
2002
  .map((p: any) => p.text)
1610
2003
  const output = textParts.join("\n") || "Raven returned no results."
1611
2004
 
1612
- // Context saved = Raven output/reasoning/cache context - compact answer returned to main session.
1613
- let savedCandidate = 0
1614
- try {
1615
- savedCandidate = await countRavenSavedCandidateBytes(sessionId)
1616
- } catch { /* best-effort */ }
2005
+ // Context saved = Raven output/reasoning/cache context - compact answer returned to main session.
2006
+ let savedCandidate = 0
2007
+ const statsController = new AbortController()
2008
+ const abortStats = () => statsController.abort(context.abort.reason)
2009
+ const statsTimer = setTimeout(() => statsController.abort(), Math.min(timeout, 5_000))
2010
+ if (context.abort.aborted) abortStats()
2011
+ else context.abort.addEventListener("abort", abortStats, { once: true })
2012
+ try {
2013
+ const totalSavedCandidate = await countRavenSavedCandidateBytes(sessionId, statsController.signal)
2014
+ savedCandidate = Math.max(0, totalSavedCandidate - previousSavedCandidate)
2015
+ } catch { /* best-effort */ }
2016
+ finally {
2017
+ clearTimeout(statsTimer)
2018
+ context.abort.removeEventListener("abort", abortStats)
2019
+ }
1617
2020
  if (savedCandidate <= 0) {
1618
2021
  for (const part of parts) {
1619
2022
  if (part.args) savedCandidate += JSON.stringify(part.args).length
1620
2023
  if (part.content) savedCandidate += typeof part.content === "string" ? part.content.length : JSON.stringify(part.content).length
1621
2024
  }
1622
2025
  }
1623
- const saved = Math.max(0, savedCandidate - output.length)
1624
- addBytes(saved)
1625
- ravenProviderIssue = undefined
1626
-
1627
- return { title: "Raven Seek", metadata: { sessionId }, output: `${output}\n\n*Raven searched for ${elapsed}s — ${formatBytes(savedCandidate)} handled, ${formatTokens(savedCandidate)} tokens*` }
2026
+ const continuation = `Raven session: \`${sessionId}\`${resumed ? " (continued)" : ""}. Pass this as \`sessionId\` in a later \`raven_seek\` call to continue the same research.`
2027
+ const resultOutput = `${output}\n\n*Raven searched for ${elapsed}s — ${formatBytes(savedCandidate)} handled, ${formatTokens(savedCandidate)} tokens*\n\n${continuation}`
2028
+ addBytes(Math.max(0, savedCandidate - resultOutput.length))
2029
+ delete ravenProviderIssues.seek
2030
+
2031
+ return { title: "Raven Seek", metadata: { sessionId }, output: resultOutput }
1628
2032
  } finally {
1629
2033
  context.abort.removeEventListener("abort", abortPrompt)
1630
- if (!promptCompleted) promptController.abort()
2034
+ if (!promptCompleted) abortPrompt()
2035
+ if (sessionAbort) await sessionAbort
1631
2036
  if (timer) clearTimeout(timer)
1632
2037
  clearRavenWaiters(sessionId)
2038
+ activeRavenPrompts.delete(activeSessionId)
1633
2039
  }
1634
- } catch (err: any) {
1635
- const elapsed = ((Date.now() - started) / 1000).toFixed(1)
2040
+ } catch (err: any) {
2041
+ const elapsed = ((Date.now() - started) / 1000).toFixed(1)
1636
2042
  const msg = String(err?.message ?? err ?? "").toLowerCase()
2043
+ const missingSession = /session.*not found|not found.*session/i.test(msg)
1637
2044
  const hint =
1638
2045
  context.abort.aborted ? "Raven search cancelled."
1639
2046
  : /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
- : /model|unavailable|down|not found/i.test(msg) ? "Raven model unavailable — retry later, or proceed without search."
1644
- : /did not produce an initial response|retry dialog/i.test(msg) ? err.message
1645
- : /timeout|timed.?out|session kept/i.test(msg) ? err.message
1646
- : `Raven search failed. Proceed without search — note gaps for the user. [${err.message || err}]`
1647
- 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)) {
1648
- ravenProviderIssue = hint
1649
- }
1650
- const recovery = ravenProviderIssue ? `\n\n${ravenRecoveryInstructions()}` : ""
1651
- return { title: "Raven Seek", output: `${hint}${recovery}\n\n*Attempt took ${elapsed}s*` }
2047
+ : /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."
2048
+ : /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."
2049
+ : /token|context.?length|too large|too long/i.test(msg) ? "Raven query too large — shorten your query and retry."
2050
+ : missingSession ? "The Raven session is no longer available — retry without sessionId to start a new Raven search."
2051
+ : /model|unavailable|down|not found/i.test(msg) ? "Raven model unavailable — retry later, or proceed without search."
2052
+ : /did not produce an initial response|retry dialog/i.test(msg) ? err.message
2053
+ : /timeout|timed.?out|session kept/i.test(msg) ? err.message
2054
+ : `Raven search failed. Proceed without search — note gaps for the user. [${err.message || err}]`
2055
+ 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)) {
2056
+ ravenProviderIssues.seek = hint
2057
+ }
2058
+ const recovery = ravenProviderIssues.seek ? `\n\n${ravenRecoveryInstructions("seek")}` : ""
2059
+ const continuation = sessionId && !missingSession
2060
+ ? `\n\nRaven session: \`${sessionId}\`. Pass this as \`sessionId\` in a later \`raven_seek\` call to retry or continue.`
2061
+ : ""
2062
+ return { title: "Raven Seek", metadata: sessionId ? { sessionId } : undefined, output: `${hint}${recovery}\n\n*Attempt took ${elapsed}s*${continuation}` }
1652
2063
  }
1653
2064
  },
1654
2065
  }),
1655
- "raven_mcp": tool({
1656
- description: "Raven-only bridge for on-demand MCP servers. Non-Raven agents must not call this directly; use raven_seek instead. Raven uses this to list and call tools/resources/prompts from MCPs configured in Raven's onDemandMcpServers without exposing their full schemas to the main model.",
2066
+ "raven_mcp": tool({
2067
+ description: "On-demand MCP information retrieval only. Creates or resumes a dedicated Raven MCP session that selects and calls MCPs configured under raven_mcp.onDemandMcpServers. Use for matching MCP capabilities, not local search, code review, auditing, implementation, planning, or general task delegation.",
2068
+ args: {
2069
+ query: tool.schema.string().describe("The information to retrieve and, when known, the target MCP server or capability. The calling agent retains analysis and judgment."),
2070
+ sessionId: tool.schema.string().optional().describe("Raven MCP session ID returned by an earlier raven_mcp call. Provide it only to continue that MCP retrieval context."),
2071
+ },
2072
+ async execute(args, context) {
2073
+ return executeRavenDelegation("mcp", args, context)
2074
+ },
2075
+ }),
2076
+ "raven_mcp_bridge": tool({
2077
+ description: "Internal bridge for the Raven MCP agent. Other agents must not call this tool. Lists and calls tools, resources, and prompts from configured on-demand MCP servers without globally exposing their schemas.",
1657
2078
  args: {
1658
2079
  server: tool.schema.string().describe("On-demand MCP server name from Raven config, e.g. context7, exa, grep_app, unityMCP."),
1659
2080
  operation: tool.schema.enum(["list_tools", "call_tool", "list_resources", "read_resource", "list_prompts", "get_prompt"]).describe("MCP operation to perform."),
@@ -1661,13 +2082,13 @@ Then restart opencode for the model change to take effect. If the task can be co
1661
2082
  argumentsJson: tool.schema.string().optional().describe("JSON object string with arguments for call_tool or get_prompt, e.g. {\"query\":\"latest AI news\"}. Omit for no arguments."),
1662
2083
  },
1663
2084
  async execute(args, context) {
1664
- if (!ravenSessions.has(context.sessionID) && sessionAgents.get(context.sessionID) !== "raven") {
1665
- return { title: "Raven MCP", output: "raven_mcp is only available inside Raven. Use raven_seek for MCP requests." }
2085
+ if (ravenSessionKinds.get(context.sessionID) !== "mcp" && sessionAgents.get(context.sessionID) !== "raven-mcp") {
2086
+ return { title: "Raven MCP Bridge", output: "raven_mcp_bridge is only available inside the Raven MCP agent. Use raven_mcp for on-demand MCP requests." }
1666
2087
  }
1667
2088
 
1668
2089
  const operation = args.operation as RavenMcpOperation
1669
2090
  const server = getOnDemandMcpServer(args.server)?.[1]
1670
- const timeout = server ? mcpTimeoutMs(server) : (config.timeout ?? 600) * 1000
2091
+ const timeout = server ? mcpTimeoutMs(server) : (config.raven_mcp.timeout ?? 600) * 1000
1671
2092
  const requestOptions = { signal: context.abort, timeout }
1672
2093
  let connected = false
1673
2094
  let connection: McpConnection | undefined
@@ -1749,11 +2170,15 @@ Then restart opencode for the model change to take effect. If the task can be co
1749
2170
 
1750
2171
  // Track agent ↔ session mapping for allowlist + Raven exclusion
1751
2172
  async "chat.message"(input: any, _output: any) {
1752
- if (input.agent) {
1753
- sessionAgents.set(input.sessionID, input.agent)
1754
- if (input.agent === "raven") {
1755
- ravenSessions.add(input.sessionID)
1756
- }
2173
+ if (input.agent) {
2174
+ sessionAgents.set(input.sessionID, input.agent)
2175
+ if (input.agent === "raven") {
2176
+ ravenSessions.add(input.sessionID)
2177
+ ravenSessionKinds.set(input.sessionID, "seek")
2178
+ } else if (input.agent === "raven-mcp") {
2179
+ ravenSessions.add(input.sessionID)
2180
+ ravenSessionKinds.set(input.sessionID, "mcp")
2181
+ }
1757
2182
  }
1758
2183
  },
1759
2184
 
@@ -1768,6 +2193,8 @@ Then restart opencode for the model change to take effect. If the task can be co
1768
2193
  const sessionId = evt.properties?.info?.id
1769
2194
  if (sessionId) {
1770
2195
  ravenSessions.delete(sessionId)
2196
+ ravenSessionKinds.delete(sessionId)
2197
+ activeRavenPrompts.delete(sessionId)
1771
2198
  sessionAgents.delete(sessionId)
1772
2199
  clearRavenWaiters(sessionId)
1773
2200
  for (const [parentId, childId] of ravenSessionParents) {
@@ -1822,22 +2249,16 @@ Then restart opencode for the model change to take effect. If the task can be co
1822
2249
  }
1823
2250
  },
1824
2251
 
1825
- // /raven help|on|off|route|mcp|model <name>|effort <value>|timeout <seconds>|stats|status
2252
+ // /raven help|route|seek ...|mcp ...|update|stats|status
1826
2253
  async "command.execute.before"(input: any, output: any) {
1827
2254
  if (input.command !== "raven") return
1828
2255
  output.parts.length = 0
1829
2256
  const raw = input.arguments.trim()
1830
2257
  const arg = raw.toLowerCase()
1831
2258
 
1832
- if (arg === "on") {
1833
- updateConfig({ enabled: true })
1834
- output.parts.push({ type: "text", text: "Raven tool/MCP routing enabled. Non-Raven agents will be redirected to raven_seek for configured tools." })
1835
- } else if (arg === "help") {
1836
- output.parts.push({ type: "text", text: helpText() })
1837
- } else if (arg === "off") {
1838
- updateConfig({ enabled: false })
1839
- output.parts.push({ type: "text", text: "Raven tool/MCP routing disabled. All agents can use tools directly." })
1840
- } else if (arg === "stats") {
2259
+ if (arg === "help") {
2260
+ output.parts.push({ type: "text", text: helpText() })
2261
+ } else if (arg === "stats") {
1841
2262
  output.parts.push({ type: "text", text: statsText() })
1842
2263
  } else if (arg === "route") {
1843
2264
  output.parts.push({ type: "text", text: `${routeSummary()}\n\nUsage:\n /raven route tool add <tool_name>\n /raven route tool remove <tool_name>\n /raven route keyword add <keyword>\n /raven route keyword remove <keyword>\n\nGlobal MCPs are auto-routed by detected OpenCode MCP server name.` })
@@ -1852,16 +2273,38 @@ Then restart opencode for the model change to take effect. If the task can be co
1852
2273
  output.parts.push({ type: "text", text: "Usage:\n /raven route tool add <tool_name>\n /raven route tool remove <tool_name>\n /raven route keyword add <keyword>\n /raven route keyword remove <keyword>\n\nGlobal MCPs are auto-routed by detected OpenCode MCP server name." })
1853
2274
  } else {
1854
2275
  updateConfig((latest) => {
1855
- const values = uniqueStrings(latest[key])
2276
+ const values = uniqueStrings(latest.raven_seek[key])
1856
2277
  const exists = values.some((value) => value.toLowerCase() === name.toLowerCase())
1857
2278
  const next = action === "add"
1858
2279
  ? exists ? values : [...values, name]
1859
2280
  : values.filter((value) => value.toLowerCase() !== name.toLowerCase())
1860
- return key === "routeTools" ? { routeTools: next } : { routeToolKeywords: next }
2281
+ return { raven_seek: key === "routeTools" ? { routeTools: next } : { routeToolKeywords: next } }
1861
2282
  })
1862
2283
  output.parts.push({ type: "text", text: routeSummary() })
1863
- }
1864
- } else if (arg === "mcp") {
2284
+ }
2285
+ } else if (arg.startsWith("seek ")) {
2286
+ const parts = raw.split(/\s+/)
2287
+ const action = parts[1]?.toLowerCase()
2288
+ const value = parts.slice(2).join(" ").trim()
2289
+ if (action === "model" && value) {
2290
+ updateConfig({ raven_seek: { model: value } })
2291
+ delete ravenProviderIssues.seek
2292
+ output.parts.push({ type: "text", text: `Raven Search model set to: ${value}\nRestart opencode for the change to take effect.` })
2293
+ } else if (action === "effort" && value) {
2294
+ updateConfig({ raven_seek: { reasoning_effort: value } })
2295
+ output.parts.push({ type: "text", text: `Raven Search reasoning effort set to: ${value}\nRestart opencode for the change to take effect.` })
2296
+ } else if (action === "timeout") {
2297
+ const seconds = Number(value)
2298
+ if (!Number.isInteger(seconds) || seconds < 10 || seconds > MAX_TIMEOUT_SECONDS) {
2299
+ output.parts.push({ type: "text", text: `Usage: /raven seek timeout <seconds>\nMust be between 10 and ${MAX_TIMEOUT_SECONDS}. Current: ${config.raven_seek.timeout ?? 600}s` })
2300
+ } else {
2301
+ updateConfig({ raven_seek: { timeout: seconds } })
2302
+ output.parts.push({ type: "text", text: `Raven Search timeout set to ${seconds}s. Takes effect immediately.` })
2303
+ }
2304
+ } else {
2305
+ output.parts.push({ type: "text", text: "Usage:\n /raven seek model <provider/model>\n /raven seek effort <value>\n /raven seek timeout <seconds>" })
2306
+ }
2307
+ } else if (arg === "mcp") {
1865
2308
  output.parts.push({ type: "text", text: `${onDemandMcpStatus()}\n\nUsage:\n /raven mcp refresh [server_name]\n /raven mcp detail full\n /raven mcp detail minimized` })
1866
2309
  } else if (arg.startsWith("mcp ")) {
1867
2310
  const parts = raw.split(/\s+/)
@@ -1874,16 +2317,33 @@ Then restart opencode for the model change to take effect. If the task can be co
1874
2317
  type: "text",
1875
2318
  text: `On-demand MCP refresh complete.\n Refreshed: ${result.refreshed.length ? result.refreshed.join(", ") : "none"}\n Failed: ${result.failed.length ? result.failed.join(", ") : "none"}`,
1876
2319
  })
1877
- } else if (action === "detail") {
1878
- const detail = parts[2]?.toLowerCase()
2320
+ } else if (action === "detail") {
2321
+ const detail = parts[2]?.toLowerCase()
1879
2322
  if (detail !== "full" && detail !== "minimized") {
1880
- output.parts.push({ type: "text", text: `Usage: /raven mcp detail full|minimized\nCurrent: ${config.onDemandMcpDescriptionDetail ?? "full"}` })
2323
+ output.parts.push({ type: "text", text: `Usage: /raven mcp detail full|minimized\nCurrent: ${config.raven_mcp.descriptionDetail ?? "full"}` })
1881
2324
  } else {
1882
- updateConfig({ onDemandMcpDescriptionDetail: detail })
2325
+ updateConfig({ raven_mcp: { descriptionDetail: detail } })
1883
2326
  output.parts.push({ type: "text", text: `On-demand MCP guidance detail set to: ${detail}\nRestart opencode for startup instructions/tool descriptions to fully refresh.` })
1884
- }
1885
- } else {
1886
- output.parts.push({ type: "text", text: "Usage:\n /raven mcp\n /raven mcp refresh [server_name]\n /raven mcp detail full\n /raven mcp detail minimized" })
2327
+ }
2328
+ } else if (action === "model" && parts.slice(2).join(" ").trim()) {
2329
+ const model = parts.slice(2).join(" ").trim()
2330
+ updateConfig({ raven_mcp: { model } })
2331
+ delete ravenProviderIssues.mcp
2332
+ output.parts.push({ type: "text", text: `Raven MCP model set to: ${model}\nRestart opencode for the change to take effect.` })
2333
+ } else if (action === "effort" && parts.slice(2).join(" ").trim()) {
2334
+ const effort = parts.slice(2).join(" ").trim()
2335
+ updateConfig({ raven_mcp: { reasoning_effort: effort } })
2336
+ output.parts.push({ type: "text", text: `Raven MCP reasoning effort set to: ${effort}\nRestart opencode for the change to take effect.` })
2337
+ } else if (action === "timeout") {
2338
+ const seconds = Number(parts[2])
2339
+ if (!Number.isInteger(seconds) || seconds < 10 || seconds > MAX_TIMEOUT_SECONDS) {
2340
+ output.parts.push({ type: "text", text: `Usage: /raven mcp timeout <seconds>\nMust be between 10 and ${MAX_TIMEOUT_SECONDS}. Current: ${config.raven_mcp.timeout ?? 600}s` })
2341
+ } else {
2342
+ updateConfig({ raven_mcp: { timeout: seconds } })
2343
+ output.parts.push({ type: "text", text: `Raven MCP timeout set to ${seconds}s. Takes effect immediately.` })
2344
+ }
2345
+ } else {
2346
+ output.parts.push({ type: "text", text: "Usage:\n /raven mcp\n /raven mcp refresh [server_name]\n /raven mcp detail full|minimized\n /raven mcp model <provider/model>\n /raven mcp effort <value>\n /raven mcp timeout <seconds>" })
1887
2347
  }
1888
2348
  } else if (arg === "update") {
1889
2349
  try {
@@ -1899,37 +2359,11 @@ Then restart opencode for the model change to take effect. If the task can be co
1899
2359
  } catch (err: any) {
1900
2360
  output.parts.push({ type: "text", text: `Raven update check failed: ${err?.message ?? err}\n\n${manualUpdateText()}` })
1901
2361
  }
1902
- } else if (arg.startsWith("model ")) {
1903
- const model = raw.slice(6).trim()
1904
- if (!model) {
1905
- output.parts.push({ type: "text", text: `Usage: /raven model <name>\nCurrent model: ${config.model || fm.model || "(default)"}` })
1906
- } else {
1907
- updateConfig({ model })
1908
- ravenProviderIssue = undefined
1909
- output.parts.push({ type: "text", text: `Raven model set to: ${model}\nRestart opencode for the change to take effect.` })
1910
- }
1911
- } else if (arg.startsWith("effort ")) {
1912
- const effort = raw.slice(7).trim()
1913
- if (!effort) {
1914
- output.parts.push({ type: "text", text: `Usage: /raven effort <value>\nCurrent: ${config.reasoning_effort || fm.reasoning_effort || "(default)"}` })
1915
- } else {
1916
- updateConfig({ reasoning_effort: effort })
1917
- output.parts.push({ type: "text", text: `Raven reasoning effort set to: ${effort}\nRestart opencode for the change to take effect.` })
1918
- }
1919
- } else if (arg.startsWith("timeout ")) {
1920
- const secs = Number(raw.slice(8).trim())
1921
- if (!Number.isInteger(secs) || secs < 10 || secs > MAX_TIMEOUT_SECONDS) {
1922
- output.parts.push({ type: "text", text: `Usage: /raven timeout <seconds>\nMust be between 10 and ${MAX_TIMEOUT_SECONDS}. Current: ${config.timeout ?? 600}s` })
1923
- } else {
1924
- updateConfig({ timeout: secs })
1925
- output.parts.push({ type: "text", text: `Raven timeout set to ${secs}s. Takes effect immediately.` })
1926
- }
1927
- } else {
1928
- const enabled = config.enabled ? "enabled" : "disabled"
1929
- const model = config.model || fm.model || "(default)"
1930
- const effort = config.reasoning_effort || fm.reasoning_effort || "(default)"
1931
- const timeout = config.timeout ?? 600
1932
- const providerIssue = ravenProviderIssue ? `\nProvider issue: ${ravenProviderIssue}` : ""
2362
+ } else {
2363
+ const seek = config.raven_seek
2364
+ const mcp = config.raven_mcp
2365
+ const seekIssue = ravenProviderIssues.seek ? `\n Provider issue: ${ravenProviderIssues.seek}` : ""
2366
+ const mcpIssue = ravenProviderIssues.mcp ? `\n Provider issue: ${ravenProviderIssues.mcp}` : ""
1933
2367
  let update = "Update: unable to check npm."
1934
2368
  try {
1935
2369
  const info = await getUpdateInfo()
@@ -1939,23 +2373,22 @@ Then restart opencode for the model change to take effect. If the task can be co
1939
2373
  ? `Update: up to date (latest ${info.latest}).`
1940
2374
  : "Update: unable to check npm."
1941
2375
  } catch { /* keep fallback */ }
1942
- 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.` })
2376
+ output.parts.push({ type: "text", text: `Raven version: ${PACKAGE_VERSION}.\nSearch agent: ${seek.model || fm.model || "(default)"}. Reasoning: ${seek.reasoning_effort || fm.reasoning_effort || "(default)"}. Timeout: ${seek.timeout ?? 600}s.${seekIssue}\nMCP agent: ${mcp.model || mcpFm.model || "(default)"}. Reasoning: ${mcp.reasoning_effort || mcpFm.reasoning_effort || "(default)"}. Timeout: ${mcp.timeout ?? 600}s.${mcpIssue}\n${update}\n\n${routeSummary()}\n\n${mcpSummary()}\n\n${statsText()}\n\nRun /raven help for commands.` })
1943
2377
  }
1944
2378
  },
1945
2379
 
1946
2380
  async "tool.execute.before"(input: any, output: any) {
1947
- if (input.tool === "raven_seek" && (ravenSessions.has(input.sessionID) || sessionAgents.get(input.sessionID) === "raven")) {
1948
- throw new Error("raven_seek is disabled inside Raven. Use Raven's direct search/fetch tools instead.")
1949
- }
1950
-
1951
- if (input.tool === "raven_mcp" && !ravenSessions.has(input.sessionID) && sessionAgents.get(input.sessionID) !== "raven") {
1952
- throw new Error("raven_mcp is only available inside Raven. Use raven_seek(query=\"Use the relevant on-demand MCP to handle this request\") instead.")
1953
- }
1954
-
1955
- if (!config.enabled) return
1956
- if (ravenSessions.has(input.sessionID)) return
1957
- if (isExcluded(sessionAgents.get(input.sessionID))) return
1958
- if (config.excludeTools?.some((name) => name.toLowerCase() === input.tool.toLowerCase())) return
2381
+ if ((input.tool === "raven_seek" || input.tool === "raven_mcp") && ravenSessions.has(input.sessionID)) {
2382
+ throw new Error(`${input.tool} cannot be called from inside a Raven agent. Use that agent's direct tools instead.`)
2383
+ }
2384
+
2385
+ if (input.tool === "raven_mcp_bridge" && ravenSessionKinds.get(input.sessionID) !== "mcp" && sessionAgents.get(input.sessionID) !== "raven-mcp") {
2386
+ throw new Error("raven_mcp_bridge is only available inside the Raven MCP agent. Use raven_mcp for on-demand MCP requests.")
2387
+ }
2388
+
2389
+ if (ravenSessions.has(input.sessionID)) return
2390
+ if (isExcluded(sessionAgents.get(input.sessionID))) return
2391
+ if (config.raven_seek.excludeTools?.some((name) => name.toLowerCase() === input.tool.toLowerCase())) return
1959
2392
 
1960
2393
  if (input.tool === "task" && output.args?.subagent_type === "raven") {
1961
2394
  ravenTaskCalls.add(input.callID)