opencode-raven 2.1.8 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,17 +562,29 @@ 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()
585
+ type DelegationKind = "seek" | "mcp"
535
586
  const ravenSessions = new Set<string>()
587
+ const ravenSessionKinds = new Map<string, DelegationKind>()
536
588
  const activeRavenPrompts = new Set<string>()
537
589
  const ravenTaskCalls = new Set<string>()
538
590
  const sessionAgents = new Map<string, string>()
@@ -550,10 +602,12 @@ export default (async (input: PluginInput) => {
550
602
  let mcpRefreshPromise: Promise<{ refreshed: string[]; failed: string[] }> | undefined
551
603
  let mcpRefreshQueue: Promise<unknown> = Promise.resolve()
552
604
  const mcpRefreshControllers = new Set<AbortController>()
553
- let lastNotifiedMcpFailureKey = ""
554
- let ravenProviderIssue: string | undefined
555
-
556
- 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()
557
611
 
558
612
  function resetOnDemandMcpRuntimeStatuses() {
559
613
  let changed = false
@@ -570,17 +624,17 @@ export default (async (input: PluginInput) => {
570
624
  }
571
625
 
572
626
  // ── Check if an agent is excluded from Raven enforcement (case-insensitive) ──
573
- function isExcluded(agent: string | undefined): boolean {
574
- if (!agent || !config.excludeAgents?.length) return false
575
- const lower = agent.toLowerCase()
576
- return config.excludeAgents.some((a) => a.toLowerCase() === lower)
577
- }
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
+ }
578
632
 
579
633
  function isRouteConfigured(toolName: string): boolean {
580
634
  const tool = toolName.toLowerCase()
581
635
  if (NEVER_ROUTE_TOOLS.has(tool)) return false
582
- if (config.routeTools?.some((name) => name.toLowerCase() === tool)) return true
583
- 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
584
638
  return globalMcpServers.some((server) => tool.startsWith(`${server.toLowerCase()}_`))
585
639
  }
586
640
 
@@ -610,31 +664,32 @@ export default (async (input: PluginInput) => {
610
664
  return query.length > 300 ? `${query.slice(0, 297)}...` : query
611
665
  }
612
666
 
613
- function rerouteMessage(tool: string, args: any): string {
614
- if (ravenProviderIssue) {
615
- return `Raven is currently unavailable: ${ravenProviderIssue}
616
-
617
- ${ravenRecoveryInstructions()}`
618
- }
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
+ }
619
673
  return `The '${tool}' tool call is blocked by Raven. Your next tool call should be raven_seek(query="${attemptedQuery(tool, args).replace(/"/g, "'")}").`
620
674
  }
621
675
 
622
- function ravenRecoveryInstructions(): string {
623
- 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.
624
-
625
- Tell the user to switch Raven's model with:
626
- /raven model <provider/model>
627
-
628
- Example:
629
- /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")}
630
685
 
631
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.`
632
687
  }
633
688
 
634
- function routeSummary(): string {
635
- const tools = config.routeTools?.length ? config.routeTools.join(", ") : "(none)"
636
- const mcps = globalMcpServers.length ? `${globalMcpServers.join(", ")} (auto-detected)` : "none detected"
637
- 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)"
638
693
  const duplicates = duplicateMcpServers.length ? `\n Duplicate MCP config: ${duplicateMcpServers.join(", ")} (configured globally and on-demand)` : ""
639
694
  return `Raven routing:\n Routed tools: ${tools}\n Routed global MCPs: ${mcps}\n Routed tool keywords: ${keywords}${duplicates}`
640
695
  }
@@ -642,23 +697,29 @@ Then restart opencode for the model change to take effect. If the task can be co
642
697
  function mcpSummary(): string {
643
698
  const buckets = onDemandMcpStatusBuckets()
644
699
  const total = buckets.loaded.length + buckets.failed.length + buckets.pending.length
645
- 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"}`
646
701
  const lines = [
647
702
  "Raven on-demand MCPs (always behind Raven):",
648
703
  ` Loaded: ${buckets.loaded.length ? buckets.loaded.join(", ") : "none"}`,
649
704
  ` Failed: ${buckets.failed.length ? buckets.failed.join(", ") : "none"}`,
650
705
  ` Pending: ${buckets.pending.length ? buckets.pending.join(", ") : "none"}`,
651
- ` Guidance detail: ${config.onDemandMcpDescriptionDetail ?? "full"}`,
706
+ ` Guidance detail: ${config.raven_mcp.descriptionDetail ?? "full"}`,
652
707
  ]
653
708
  return lines.join("\n")
654
709
  }
655
710
 
656
- function ravenAgentPrompt(): string {
657
- const extra = config.ravenInstructions?.trim()
658
- const onDemand = onDemandMcpGuidance("raven")
659
- 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.`
660
- return extra ? `${prompt}\n\nAdditional user instructions:\n${extra}` : prompt
661
- }
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
+ }
662
723
 
663
724
  function compactText(value: unknown): string {
664
725
  return String(value ?? "").replace(/\s+/g, " ").trim()
@@ -700,7 +761,7 @@ Then restart opencode for the model change to take effect. If the task can be co
700
761
  }
701
762
 
702
763
  function onDemandMcpGuidance(target: "delegate" | "raven" = "delegate", preferredOrder?: string[]): string {
703
- const servers = config.onDemandMcpServers ?? {}
764
+ const servers = config.raven_mcp.onDemandMcpServers ?? {}
704
765
  const activeEntries = activeOnDemandMcpEntries()
705
766
  const entries = orderedOnDemandMcpEntries(
706
767
  target === "raven"
@@ -712,7 +773,7 @@ Then restart opencode for the model change to take effect. If the task can be co
712
773
  if (!activeEntries.length) return "No enabled on-demand MCPs are configured."
713
774
  if (!entries.length) return "No loaded on-demand MCPs are available."
714
775
 
715
- const detail = config.onDemandMcpDescriptionDetail ?? "full"
776
+ const detail = config.raven_mcp.descriptionDetail ?? "full"
716
777
  const lines = entries.map(([name, server]) => {
717
778
  const metadata = mcpMetadata[name] ?? {}
718
779
  const userDescription = firstSentenceText(server.description)
@@ -725,10 +786,10 @@ Then restart opencode for the model change to take effect. If the task can be co
725
786
  .join("; ")
726
787
  return `- ${name}: ${summary} Tools: ${tools}`
727
788
  })
728
- if (target === "raven") {
729
- 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")}`
730
791
  }
731
- 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")}`
732
793
  }
733
794
 
734
795
  function resolveEnvPlaceholders(value: string): string {
@@ -817,7 +878,7 @@ Then restart opencode for the model change to take effect. If the task can be co
817
878
  let mcpIdleTimer: ReturnType<typeof setInterval> | undefined
818
879
 
819
880
  function mcpTimeoutMs(server: OnDemandMcpServer): number {
820
- return server.timeout ?? (config.timeout ?? 600) * 1000
881
+ return server.timeout ?? (config.raven_mcp.timeout ?? 600) * 1000
821
882
  }
822
883
 
823
884
  async function closeMcpClient(client: Client, transport: any) {
@@ -918,7 +979,7 @@ Then restart opencode for the model change to take effect. If the task can be co
918
979
  }
919
980
 
920
981
  function activeOnDemandMcpEntries(): [string, OnDemandMcpServer][] {
921
- return Object.entries(config.onDemandMcpServers ?? {}).filter(([, server]) => server.enabled !== false)
982
+ return Object.entries(config.raven_mcp.onDemandMcpServers ?? {}).filter(([, server]) => server.enabled !== false)
922
983
  }
923
984
 
924
985
  function waitForMcpConnection(attempt: McpConnectionAttempt, signal?: AbortSignal): Promise<McpConnection> {
@@ -1215,7 +1276,7 @@ Then restart opencode for the model change to take effect. If the task can be co
1215
1276
 
1216
1277
  function onDemandMcpStatus(): string {
1217
1278
  const entries = activeOnDemandMcpEntries()
1218
- 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."
1219
1280
  if (!entries.length) return "Raven on-demand MCPs: none enabled."
1220
1281
  const buckets = onDemandMcpStatusBuckets()
1221
1282
  const lines = entries.map(([name, server]) => {
@@ -1228,7 +1289,7 @@ Then restart opencode for the model change to take effect. If the task can be co
1228
1289
  const error = status === "failed" && metadata?.lastError ? `, error: ${metadata.lastError}` : ""
1229
1290
  return `- ${name} (${server.type}): ${displayMcpStatus(status)}, ${source}, ${generated}, ${tools}${avoided}${error}`
1230
1291
  })
1231
- 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")}`
1232
1293
  }
1233
1294
 
1234
1295
  function avoidedMcpSchemaBytes(): number {
@@ -1244,9 +1305,9 @@ Then restart opencode for the model change to take effect. If the task can be co
1244
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)`
1245
1306
  }
1246
1307
 
1247
- function helpText(): string {
1248
- 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`
1249
- }
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
+ }
1250
1311
 
1251
1312
  function formatMcpList(title: string, items: any[], fields: string[] = ["name", "description"]): string {
1252
1313
  if (!items.length) return `${title}: none found.`
@@ -1457,7 +1518,7 @@ Then restart opencode for the model change to take effect. If the task can be co
1457
1518
  } catch { /* MCP failure notifications are best-effort */ }
1458
1519
  }
1459
1520
 
1460
- async function notifyIfDuplicateMcps() {
1521
+ async function notifyIfDuplicateMcps() {
1461
1522
  try {
1462
1523
  if (!duplicateMcpServers.length) return
1463
1524
  const label = duplicateMcpServers.length === 1 ? duplicateMcpServers[0] : `${duplicateMcpServers.length} MCPs`
@@ -1469,9 +1530,257 @@ Then restart opencode for the model change to take effect. If the task can be co
1469
1530
  duration: 12000,
1470
1531
  },
1471
1532
  })
1472
- } catch { /* duplicate MCP notifications are best-effort */ }
1473
- }
1474
-
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 syncCreatedSession(sessionId: string, title: string, signal?: AbortSignal) {
1562
+ try {
1563
+ await client.session.update({
1564
+ path: { id: sessionId },
1565
+ body: { title },
1566
+ signal,
1567
+ })
1568
+ } catch (error) {
1569
+ if (signal?.aborted) throw error
1570
+ // Delegation still works if an older or disconnected TUI misses the sync event.
1571
+ }
1572
+ }
1573
+
1574
+ async function executeRavenDelegation(kind: DelegationKind, args: { query: string; sessionId?: string }, context: any) {
1575
+ const started = Date.now()
1576
+ const agent = delegationAgent(kind)
1577
+ const toolName = delegationToolName(kind)
1578
+ const title = delegationTitle(kind)
1579
+ const timeout = (kind === "mcp" ? config.raven_mcp.timeout : config.raven_seek.timeout) ?? 600
1580
+ const timeoutMs = timeout * 1000
1581
+ let sessionId: string | undefined
1582
+ let resumed = false
1583
+ let previousSavedCandidate = 0
1584
+
1585
+ try {
1586
+ const requestedSessionId = args.sessionId?.trim()
1587
+ if (args.sessionId !== undefined && !requestedSessionId) {
1588
+ return { title, output: "Cannot resume Raven: sessionId must not be empty." }
1589
+ }
1590
+
1591
+ let rootId = context.sessionID
1592
+ try {
1593
+ rootId = await rootSessionId(context.sessionID, context.abort)
1594
+ } catch (error) {
1595
+ if (context.abort.aborted) throw error
1596
+ // Falling back preserves delegation if an older OpenCode build cannot read the caller session.
1597
+ }
1598
+
1599
+ if (requestedSessionId) {
1600
+ const knownKind = ravenSessionKinds.get(requestedSessionId)
1601
+ const knownRavenSession = knownKind === kind || sessionAgents.get(requestedSessionId) === agent
1602
+ let existingSession: any
1603
+ try {
1604
+ const sessionResp = await client.session.get({ path: { id: requestedSessionId }, signal: context.abort })
1605
+ existingSession = (sessionResp as any)?.data ?? sessionResp
1606
+ } catch (error) {
1607
+ if (context.abort.aborted) throw error
1608
+ return { title, output: `Cannot resume Raven session ${requestedSessionId}: the session was not found or could not be read.` }
1609
+ }
1610
+
1611
+ if (!existingSession?.id || existingSession.id !== requestedSessionId) {
1612
+ return { title, output: `Cannot resume Raven session ${requestedSessionId}: the session was not found or could not be read.` }
1613
+ }
1614
+ const validParent = existingSession.parentID === rootId || existingSession.parentID === context.sessionID
1615
+ if (!validParent || existingSession.projectID !== input.project.id) {
1616
+ return { title, output: "Cannot resume Raven: sessionId is not part of this main session's Raven tree." }
1617
+ }
1618
+
1619
+ let messages: any[]
1620
+ try {
1621
+ const messagesResp = await client.session.messages({ path: { id: requestedSessionId }, signal: context.abort })
1622
+ const messageData = (messagesResp as any)?.data
1623
+ if (!Array.isArray(messageData)) {
1624
+ return { title, output: `Cannot resume Raven session ${requestedSessionId}: its message history could not be read.` }
1625
+ }
1626
+ messages = messageData
1627
+ } catch (error) {
1628
+ if (context.abort.aborted) throw error
1629
+ return { title, output: `Cannot resume Raven session ${requestedSessionId}: its message history could not be read.` }
1630
+ }
1631
+
1632
+ const userMessages = messages.filter((message: any) => message?.info?.role === "user")
1633
+ const persistedRavenSession = userMessages.length > 0
1634
+ && userMessages.every((message: any) => message.info.agent === agent)
1635
+ if (!persistedRavenSession && !(knownRavenSession && userMessages.length === 0)) {
1636
+ return { title, output: `Cannot resume Raven: sessionId does not belong to the ${agent} agent.` }
1637
+ }
1638
+
1639
+ sessionId = requestedSessionId
1640
+ resumed = true
1641
+ previousSavedCandidate = estimateRavenSavedCandidateBytes(messages)
1642
+ } else {
1643
+ const sessionTitle = `${toolName}: ${args.query.slice(0, 80)}`
1644
+ const session = await client.session.create({
1645
+ body: {
1646
+ parentID: rootId,
1647
+ title: sessionTitle,
1648
+ },
1649
+ signal: context.abort,
1650
+ })
1651
+ sessionId = (session as any)?.data?.id ?? (session as any)?.id
1652
+ if (!sessionId) return { title, output: "Failed to create Raven session." }
1653
+ await syncCreatedSession(sessionId, sessionTitle, context.abort)
1654
+ }
1655
+
1656
+ ravenSessions.add(sessionId)
1657
+ ravenSessionKinds.set(sessionId, kind)
1658
+ const activeSessionId = sessionId
1659
+ context.metadata({ metadata: { sessionId } })
1660
+
1661
+ try {
1662
+ writeFileSync(join(tmpdir(), "raven-sessions.log"), `${new Date().toISOString()} ${sessionId}\n`)
1663
+ } catch { /* non-fatal */ }
1664
+
1665
+ if (activeRavenPrompts.has(activeSessionId)) {
1666
+ return {
1667
+ title,
1668
+ metadata: { sessionId: activeSessionId },
1669
+ output: `Raven session \`${activeSessionId}\` is already handling a request. Wait for it to finish before sending another follow-up.`,
1670
+ }
1671
+ }
1672
+ activeRavenPrompts.add(activeSessionId)
1673
+
1674
+ let timer: ReturnType<typeof setTimeout> | undefined
1675
+ let promptCompleted = false
1676
+ let sessionAbort: Promise<unknown> | undefined
1677
+ const promptController = new AbortController()
1678
+ const abortPrompt = () => {
1679
+ if (promptController.signal.aborted) return
1680
+ promptController.abort(context.abort.reason)
1681
+ const abortController = new AbortController()
1682
+ const abortTimer = setTimeout(() => abortController.abort(), Math.min(timeoutMs, 5_000))
1683
+ sessionAbort = client.session.abort({
1684
+ path: { id: activeSessionId },
1685
+ signal: abortController.signal,
1686
+ throwOnError: true,
1687
+ }).catch(() => undefined).finally(() => clearTimeout(abortTimer))
1688
+ }
1689
+ if (context.abort.aborted) abortPrompt()
1690
+ else context.abort.addEventListener("abort", abortPrompt, { once: true })
1691
+ const providerError = waitForRavenProviderError(sessionId)
1692
+ const firstProgress = waitForRavenFirstProgress(sessionId, Math.min(timeoutMs, 30_000))
1693
+
1694
+ try {
1695
+ const result = await Promise.race([
1696
+ client.session.prompt({
1697
+ path: { id: sessionId },
1698
+ body: { agent, parts: [{ type: "text", text: args.query }] },
1699
+ signal: promptController.signal,
1700
+ throwOnError: true,
1701
+ }),
1702
+ providerError,
1703
+ firstProgress,
1704
+ new Promise<never>((_, reject) => {
1705
+ timer = setTimeout(() => {
1706
+ abortPrompt()
1707
+ reject(new Error(`Raven timed out after ${timeout}s - child session retained: ${sessionId}`))
1708
+ }, timeoutMs)
1709
+ }),
1710
+ ])
1711
+ promptCompleted = true
1712
+ if (timer) clearTimeout(timer)
1713
+ clearRavenWaiters(sessionId)
1714
+ if ((result as any)?.error) throw new Error(providerErrorMessage((result as any).error))
1715
+
1716
+ const elapsed = ((Date.now() - started) / 1000).toFixed(1)
1717
+ const parts = (result as any)?.data?.parts ?? []
1718
+ const output = parts
1719
+ .filter((part: any) => part.type === "text" && part.text)
1720
+ .map((part: any) => part.text)
1721
+ .join("\n") || "Raven returned no results."
1722
+
1723
+ let savedCandidate = 0
1724
+ const statsController = new AbortController()
1725
+ const abortStats = () => statsController.abort(context.abort.reason)
1726
+ const statsTimer = setTimeout(() => statsController.abort(), Math.min(timeoutMs, 5_000))
1727
+ if (context.abort.aborted) abortStats()
1728
+ else context.abort.addEventListener("abort", abortStats, { once: true })
1729
+ try {
1730
+ const totalSavedCandidate = await countRavenSavedCandidateBytes(sessionId, statsController.signal)
1731
+ savedCandidate = Math.max(0, totalSavedCandidate - previousSavedCandidate)
1732
+ } catch { /* best-effort */ }
1733
+ finally {
1734
+ clearTimeout(statsTimer)
1735
+ context.abort.removeEventListener("abort", abortStats)
1736
+ }
1737
+ if (savedCandidate <= 0) {
1738
+ for (const part of parts) {
1739
+ if (part.args) savedCandidate += JSON.stringify(part.args).length
1740
+ if (part.content) savedCandidate += typeof part.content === "string" ? part.content.length : JSON.stringify(part.content).length
1741
+ }
1742
+ }
1743
+
1744
+ const continuation = `Raven session: \`${sessionId}\`${resumed ? " (continued)" : ""}. Pass this as \`sessionId\` in a later \`${toolName}\` call to continue the same retrieval.`
1745
+ const activity = kind === "mcp" ? "used MCPs" : "searched"
1746
+ const resultOutput = `${output}\n\n*Raven ${activity} for ${elapsed}s - ${formatBytes(savedCandidate)} handled, ${formatTokens(savedCandidate)} tokens*\n\n${continuation}`
1747
+ addBytes(Math.max(0, savedCandidate - resultOutput.length))
1748
+ delete ravenProviderIssues[kind]
1749
+ return { title, metadata: { sessionId }, output: resultOutput }
1750
+ } finally {
1751
+ context.abort.removeEventListener("abort", abortPrompt)
1752
+ if (!promptCompleted) abortPrompt()
1753
+ if (sessionAbort) await sessionAbort
1754
+ if (timer) clearTimeout(timer)
1755
+ clearRavenWaiters(sessionId)
1756
+ activeRavenPrompts.delete(activeSessionId)
1757
+ }
1758
+ } catch (error: any) {
1759
+ const elapsed = ((Date.now() - started) / 1000).toFixed(1)
1760
+ const message = String(error?.message ?? error ?? "").toLowerCase()
1761
+ const missingSession = /session.*not found|not found.*session/i.test(message)
1762
+ const activity = kind === "mcp" ? "MCP retrieval" : "search"
1763
+ const hint =
1764
+ context.abort.aborted ? `Raven ${activity} cancelled.`
1765
+ : /rate.?limit|too many requests|429/i.test(message) ? "Raven rate limited - wait 30s, then retry with a narrower query."
1766
+ : /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."
1767
+ : /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."
1768
+ : /token|context.?length|too large|too long/i.test(message) ? "Raven query too large - shorten your query and retry."
1769
+ : missingSession ? `The Raven session is no longer available - retry without sessionId to start a new ${activity}.`
1770
+ : /model|unavailable|down|not found/i.test(message) ? "Raven model unavailable - retry later, or proceed without the missing information."
1771
+ : /did not produce an initial response|retry dialog|timeout|timed.?out|session retained/i.test(message) ? error.message
1772
+ : `Raven ${activity} failed. Note the resulting information gap. [${error.message || error}]`
1773
+ 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)) {
1774
+ ravenProviderIssues[kind] = hint
1775
+ }
1776
+ const recovery = ravenProviderIssues[kind] ? `\n\n${ravenRecoveryInstructions(kind)}` : ""
1777
+ const continuation = sessionId && !missingSession
1778
+ ? `\n\nRaven session: \`${sessionId}\`. Pass this as \`sessionId\` in a later \`${toolName}\` call to retry or continue.`
1779
+ : ""
1780
+ return { title, metadata: sessionId ? { sessionId } : undefined, output: `${hint}${recovery}\n\n*Attempt took ${elapsed}s*${continuation}` }
1781
+ }
1782
+ }
1783
+
1475
1784
  return {
1476
1785
  async dispose() {
1477
1786
  for (const controller of mcpRefreshControllers) controller.abort()
@@ -1506,26 +1815,38 @@ Then restart opencode for the model change to take effect. If the task can be co
1506
1815
 
1507
1816
  // Register Raven from Raven.md, with config file overrides
1508
1817
  configInput.agent = configInput.agent || {}
1509
- configInput.agent.raven = {
1510
- description: fm.description || "",
1511
- mode: fm.mode || "subagent",
1512
- hidden: fm.hidden !== undefined ? fm.hidden : false,
1513
- model: config.model || fm.model,
1514
- options: {
1515
- ...extractOptions(fm),
1516
- ...(config.reasoning_effort ? { reasoning_effort: config.reasoning_effort } : {}),
1517
- },
1518
- permission: fm.permission || {},
1519
- prompt: ravenAgentPrompt(),
1520
- }
1818
+ configInput.agent.raven = {
1819
+ description: fm.description || "",
1820
+ mode: fm.mode || "subagent",
1821
+ hidden: fm.hidden !== undefined ? fm.hidden : false,
1822
+ model: config.raven_seek.model || fm.model,
1823
+ options: {
1824
+ ...extractOptions(fm),
1825
+ ...(config.raven_seek.reasoning_effort ? { reasoning_effort: config.raven_seek.reasoning_effort } : {}),
1826
+ },
1827
+ permission: fm.permission || {},
1828
+ prompt: ravenAgentPrompt(),
1829
+ }
1830
+ configInput.agent["raven-mcp"] = {
1831
+ description: mcpFm.description || "",
1832
+ mode: mcpFm.mode || "subagent",
1833
+ hidden: mcpFm.hidden !== undefined ? mcpFm.hidden : true,
1834
+ model: config.raven_mcp.model || mcpFm.model,
1835
+ options: {
1836
+ ...extractOptions(mcpFm),
1837
+ ...(config.raven_mcp.reasoning_effort ? { reasoning_effort: config.raven_mcp.reasoning_effort } : {}),
1838
+ },
1839
+ permission: mcpFm.permission || {},
1840
+ prompt: ravenMcpAgentPrompt(),
1841
+ }
1521
1842
 
1522
1843
  // Register /raven command
1523
1844
  configInput.command = configInput.command || {}
1524
1845
  if (!configInput.command.raven) {
1525
- configInput.command.raven = {
1526
- template: "Manage Raven: /raven on|off|route|mcp|update|model <name>|status",
1527
- description: "Toggle Raven routing, manage routed/on-demand MCPs, or change Raven's model",
1528
- }
1846
+ configInput.command.raven = {
1847
+ template: "Manage Raven search routing, on-demand MCPs, models, timeouts, updates, and status",
1848
+ description: "Manage Raven search routing, on-demand MCPs, and per-agent models",
1849
+ }
1529
1850
  }
1530
1851
 
1531
1852
  scheduleOnDemandMcpRefresh()
@@ -1537,14 +1858,14 @@ Then restart opencode for the model change to take effect. If the task can be co
1537
1858
  // Register raven_seek tool — lets agents with task:false still delegate through Raven
1538
1859
  tool: {
1539
1860
  "raven_seek": tool({
1540
- description: "Unified Raven delegation tool. Creates a Raven child session or resumes one for follow-up research. Use this whenever a tool/MCP is blocked by Raven, or when grep, glob, WebFetch/fetch, websearch, docs lookup, GitHub search, on-demand MCP capabilities, or search-like bash would be used. Handles routed MCP requests, on-demand MCPs, local codebase search, filesystem discovery, specific URL/page reads, web/docs research, GitHub examples, and command-output/system inspection via Raven.",
1861
+ 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.",
1541
1862
  args: {
1542
- query: tool.schema.string().describe("What Raven should do. Include the original blocked tool/MCP request and relevant args, exact URLs when replacing WebFetch, and commands/output checks when replacing grep/rg/head over command output."),
1863
+ 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."),
1543
1864
  sessionId: tool.schema.string().optional().describe("Raven session ID returned by an earlier raven_seek call. Provide it only when this request should continue that session's research context."),
1544
1865
  },
1545
1866
  async execute(args, context) {
1546
1867
  const started = Date.now()
1547
- const timeout = (config.timeout ?? 600) * 1000
1868
+ const timeout = (config.raven_seek.timeout ?? 600) * 1000
1548
1869
  let sessionId: string | undefined
1549
1870
  let resumed = false
1550
1871
  let previousSavedCandidate = 0
@@ -1554,8 +1875,15 @@ Then restart opencode for the model change to take effect. If the task can be co
1554
1875
  return { title: "Raven Seek", output: "Cannot resume Raven: sessionId must not be empty." }
1555
1876
  }
1556
1877
 
1878
+ let rootId = context.sessionID
1879
+ try {
1880
+ rootId = await rootSessionId(context.sessionID, context.abort)
1881
+ } catch (error) {
1882
+ if (context.abort.aborted) throw error
1883
+ }
1884
+
1557
1885
  if (requestedSessionId) {
1558
- const knownRavenSession = ravenSessions.has(requestedSessionId) || sessionAgents.get(requestedSessionId) === "raven"
1886
+ const knownRavenSession = ravenSessionKinds.get(requestedSessionId) === "seek" || sessionAgents.get(requestedSessionId) === "raven"
1559
1887
  let existingSession: any
1560
1888
  try {
1561
1889
  const sessionResp = await client.session.get({ path: { id: requestedSessionId }, signal: context.abort })
@@ -1568,7 +1896,7 @@ Then restart opencode for the model change to take effect. If the task can be co
1568
1896
  if (!existingSession?.id || existingSession.id !== requestedSessionId) {
1569
1897
  return { title: "Raven Seek", output: `Cannot resume Raven session ${requestedSessionId}: the session was not found or could not be read.` }
1570
1898
  }
1571
- if (existingSession.parentID !== context.sessionID || existingSession.projectID !== input.project.id) {
1899
+ if ((existingSession.parentID !== rootId && existingSession.parentID !== context.sessionID) || existingSession.projectID !== input.project.id) {
1572
1900
  return { title: "Raven Seek", output: "Cannot resume Raven: sessionId is not a Raven child of this main session." }
1573
1901
  }
1574
1902
 
@@ -1596,10 +1924,11 @@ Then restart opencode for the model change to take effect. If the task can be co
1596
1924
  resumed = true
1597
1925
  previousSavedCandidate = estimateRavenSavedCandidateBytes(messages)
1598
1926
  } else {
1927
+ const sessionTitle = `raven_seek: ${args.query.slice(0, 80)}`
1599
1928
  const session = await client.session.create({
1600
1929
  body: {
1601
- parentID: context.sessionID,
1602
- title: `raven_seek: ${args.query.slice(0, 80)}`,
1930
+ parentID: rootId,
1931
+ title: sessionTitle,
1603
1932
  },
1604
1933
  signal: context.abort,
1605
1934
  })
@@ -1608,9 +1937,11 @@ Then restart opencode for the model change to take effect. If the task can be co
1608
1937
  if (!sessionId) {
1609
1938
  return { title: "Raven Seek", output: "Failed to create Raven session." }
1610
1939
  }
1940
+ await syncCreatedSession(sessionId, sessionTitle, context.abort)
1611
1941
  }
1612
1942
 
1613
1943
  ravenSessions.add(sessionId)
1944
+ ravenSessionKinds.set(sessionId, "seek")
1614
1945
  const activeSessionId = sessionId
1615
1946
 
1616
1947
  // Preserve the child session link for TUI clients that render it.
@@ -1712,7 +2043,7 @@ Then restart opencode for the model change to take effect. If the task can be co
1712
2043
  const continuation = `Raven session: \`${sessionId}\`${resumed ? " (continued)" : ""}. Pass this as \`sessionId\` in a later \`raven_seek\` call to continue the same research.`
1713
2044
  const resultOutput = `${output}\n\n*Raven searched for ${elapsed}s — ${formatBytes(savedCandidate)} handled, ${formatTokens(savedCandidate)} tokens*\n\n${continuation}`
1714
2045
  addBytes(Math.max(0, savedCandidate - resultOutput.length))
1715
- ravenProviderIssue = undefined
2046
+ delete ravenProviderIssues.seek
1716
2047
 
1717
2048
  return { title: "Raven Seek", metadata: { sessionId }, output: resultOutput }
1718
2049
  } finally {
@@ -1739,9 +2070,9 @@ Then restart opencode for the model change to take effect. If the task can be co
1739
2070
  : /timeout|timed.?out|session kept/i.test(msg) ? err.message
1740
2071
  : `Raven search failed. Proceed without search — note gaps for the user. [${err.message || err}]`
1741
2072
  if (!missingSession && /quota|usage.?limit|usage exceeded|free usage|subscribe to go|subscription|billing|auth|unauthorized|forbidden|401|403|api key|login|model|unavailable|down|not found|did not produce an initial response|retry dialog/i.test(msg)) {
1742
- ravenProviderIssue = hint
2073
+ ravenProviderIssues.seek = hint
1743
2074
  }
1744
- const recovery = ravenProviderIssue ? `\n\n${ravenRecoveryInstructions()}` : ""
2075
+ const recovery = ravenProviderIssues.seek ? `\n\n${ravenRecoveryInstructions("seek")}` : ""
1745
2076
  const continuation = sessionId && !missingSession
1746
2077
  ? `\n\nRaven session: \`${sessionId}\`. Pass this as \`sessionId\` in a later \`raven_seek\` call to retry or continue.`
1747
2078
  : ""
@@ -1749,8 +2080,18 @@ Then restart opencode for the model change to take effect. If the task can be co
1749
2080
  }
1750
2081
  },
1751
2082
  }),
1752
- "raven_mcp": tool({
1753
- 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.",
2083
+ "raven_mcp": tool({
2084
+ 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.",
2085
+ args: {
2086
+ 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."),
2087
+ 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."),
2088
+ },
2089
+ async execute(args, context) {
2090
+ return executeRavenDelegation("mcp", args, context)
2091
+ },
2092
+ }),
2093
+ "raven_mcp_bridge": tool({
2094
+ 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.",
1754
2095
  args: {
1755
2096
  server: tool.schema.string().describe("On-demand MCP server name from Raven config, e.g. context7, exa, grep_app, unityMCP."),
1756
2097
  operation: tool.schema.enum(["list_tools", "call_tool", "list_resources", "read_resource", "list_prompts", "get_prompt"]).describe("MCP operation to perform."),
@@ -1758,13 +2099,13 @@ Then restart opencode for the model change to take effect. If the task can be co
1758
2099
  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."),
1759
2100
  },
1760
2101
  async execute(args, context) {
1761
- if (!ravenSessions.has(context.sessionID) && sessionAgents.get(context.sessionID) !== "raven") {
1762
- return { title: "Raven MCP", output: "raven_mcp is only available inside Raven. Use raven_seek for MCP requests." }
2102
+ if (ravenSessionKinds.get(context.sessionID) !== "mcp" && sessionAgents.get(context.sessionID) !== "raven-mcp") {
2103
+ 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." }
1763
2104
  }
1764
2105
 
1765
2106
  const operation = args.operation as RavenMcpOperation
1766
2107
  const server = getOnDemandMcpServer(args.server)?.[1]
1767
- const timeout = server ? mcpTimeoutMs(server) : (config.timeout ?? 600) * 1000
2108
+ const timeout = server ? mcpTimeoutMs(server) : (config.raven_mcp.timeout ?? 600) * 1000
1768
2109
  const requestOptions = { signal: context.abort, timeout }
1769
2110
  let connected = false
1770
2111
  let connection: McpConnection | undefined
@@ -1846,11 +2187,15 @@ Then restart opencode for the model change to take effect. If the task can be co
1846
2187
 
1847
2188
  // Track agent ↔ session mapping for allowlist + Raven exclusion
1848
2189
  async "chat.message"(input: any, _output: any) {
1849
- if (input.agent) {
1850
- sessionAgents.set(input.sessionID, input.agent)
1851
- if (input.agent === "raven") {
1852
- ravenSessions.add(input.sessionID)
1853
- }
2190
+ if (input.agent) {
2191
+ sessionAgents.set(input.sessionID, input.agent)
2192
+ if (input.agent === "raven") {
2193
+ ravenSessions.add(input.sessionID)
2194
+ ravenSessionKinds.set(input.sessionID, "seek")
2195
+ } else if (input.agent === "raven-mcp") {
2196
+ ravenSessions.add(input.sessionID)
2197
+ ravenSessionKinds.set(input.sessionID, "mcp")
2198
+ }
1854
2199
  }
1855
2200
  },
1856
2201
 
@@ -1865,6 +2210,7 @@ Then restart opencode for the model change to take effect. If the task can be co
1865
2210
  const sessionId = evt.properties?.info?.id
1866
2211
  if (sessionId) {
1867
2212
  ravenSessions.delete(sessionId)
2213
+ ravenSessionKinds.delete(sessionId)
1868
2214
  activeRavenPrompts.delete(sessionId)
1869
2215
  sessionAgents.delete(sessionId)
1870
2216
  clearRavenWaiters(sessionId)
@@ -1920,22 +2266,16 @@ Then restart opencode for the model change to take effect. If the task can be co
1920
2266
  }
1921
2267
  },
1922
2268
 
1923
- // /raven help|on|off|route|mcp|model <name>|effort <value>|timeout <seconds>|stats|status
2269
+ // /raven help|route|seek ...|mcp ...|update|stats|status
1924
2270
  async "command.execute.before"(input: any, output: any) {
1925
2271
  if (input.command !== "raven") return
1926
2272
  output.parts.length = 0
1927
2273
  const raw = input.arguments.trim()
1928
2274
  const arg = raw.toLowerCase()
1929
2275
 
1930
- if (arg === "on") {
1931
- updateConfig({ enabled: true })
1932
- output.parts.push({ type: "text", text: "Raven tool/MCP routing enabled. Non-Raven agents will be redirected to raven_seek for configured tools." })
1933
- } else if (arg === "help") {
1934
- output.parts.push({ type: "text", text: helpText() })
1935
- } else if (arg === "off") {
1936
- updateConfig({ enabled: false })
1937
- output.parts.push({ type: "text", text: "Raven tool/MCP routing disabled. All agents can use tools directly." })
1938
- } else if (arg === "stats") {
2276
+ if (arg === "help") {
2277
+ output.parts.push({ type: "text", text: helpText() })
2278
+ } else if (arg === "stats") {
1939
2279
  output.parts.push({ type: "text", text: statsText() })
1940
2280
  } else if (arg === "route") {
1941
2281
  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.` })
@@ -1950,16 +2290,38 @@ Then restart opencode for the model change to take effect. If the task can be co
1950
2290
  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." })
1951
2291
  } else {
1952
2292
  updateConfig((latest) => {
1953
- const values = uniqueStrings(latest[key])
2293
+ const values = uniqueStrings(latest.raven_seek[key])
1954
2294
  const exists = values.some((value) => value.toLowerCase() === name.toLowerCase())
1955
2295
  const next = action === "add"
1956
2296
  ? exists ? values : [...values, name]
1957
2297
  : values.filter((value) => value.toLowerCase() !== name.toLowerCase())
1958
- return key === "routeTools" ? { routeTools: next } : { routeToolKeywords: next }
2298
+ return { raven_seek: key === "routeTools" ? { routeTools: next } : { routeToolKeywords: next } }
1959
2299
  })
1960
2300
  output.parts.push({ type: "text", text: routeSummary() })
1961
- }
1962
- } else if (arg === "mcp") {
2301
+ }
2302
+ } else if (arg.startsWith("seek ")) {
2303
+ const parts = raw.split(/\s+/)
2304
+ const action = parts[1]?.toLowerCase()
2305
+ const value = parts.slice(2).join(" ").trim()
2306
+ if (action === "model" && value) {
2307
+ updateConfig({ raven_seek: { model: value } })
2308
+ delete ravenProviderIssues.seek
2309
+ output.parts.push({ type: "text", text: `Raven Search model set to: ${value}\nRestart opencode for the change to take effect.` })
2310
+ } else if (action === "effort" && value) {
2311
+ updateConfig({ raven_seek: { reasoning_effort: value } })
2312
+ output.parts.push({ type: "text", text: `Raven Search reasoning effort set to: ${value}\nRestart opencode for the change to take effect.` })
2313
+ } else if (action === "timeout") {
2314
+ const seconds = Number(value)
2315
+ if (!Number.isInteger(seconds) || seconds < 10 || seconds > MAX_TIMEOUT_SECONDS) {
2316
+ 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` })
2317
+ } else {
2318
+ updateConfig({ raven_seek: { timeout: seconds } })
2319
+ output.parts.push({ type: "text", text: `Raven Search timeout set to ${seconds}s. Takes effect immediately.` })
2320
+ }
2321
+ } else {
2322
+ output.parts.push({ type: "text", text: "Usage:\n /raven seek model <provider/model>\n /raven seek effort <value>\n /raven seek timeout <seconds>" })
2323
+ }
2324
+ } else if (arg === "mcp") {
1963
2325
  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` })
1964
2326
  } else if (arg.startsWith("mcp ")) {
1965
2327
  const parts = raw.split(/\s+/)
@@ -1972,16 +2334,33 @@ Then restart opencode for the model change to take effect. If the task can be co
1972
2334
  type: "text",
1973
2335
  text: `On-demand MCP refresh complete.\n Refreshed: ${result.refreshed.length ? result.refreshed.join(", ") : "none"}\n Failed: ${result.failed.length ? result.failed.join(", ") : "none"}`,
1974
2336
  })
1975
- } else if (action === "detail") {
1976
- const detail = parts[2]?.toLowerCase()
2337
+ } else if (action === "detail") {
2338
+ const detail = parts[2]?.toLowerCase()
1977
2339
  if (detail !== "full" && detail !== "minimized") {
1978
- output.parts.push({ type: "text", text: `Usage: /raven mcp detail full|minimized\nCurrent: ${config.onDemandMcpDescriptionDetail ?? "full"}` })
2340
+ output.parts.push({ type: "text", text: `Usage: /raven mcp detail full|minimized\nCurrent: ${config.raven_mcp.descriptionDetail ?? "full"}` })
1979
2341
  } else {
1980
- updateConfig({ onDemandMcpDescriptionDetail: detail })
2342
+ updateConfig({ raven_mcp: { descriptionDetail: detail } })
1981
2343
  output.parts.push({ type: "text", text: `On-demand MCP guidance detail set to: ${detail}\nRestart opencode for startup instructions/tool descriptions to fully refresh.` })
1982
- }
1983
- } else {
1984
- 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" })
2344
+ }
2345
+ } else if (action === "model" && parts.slice(2).join(" ").trim()) {
2346
+ const model = parts.slice(2).join(" ").trim()
2347
+ updateConfig({ raven_mcp: { model } })
2348
+ delete ravenProviderIssues.mcp
2349
+ output.parts.push({ type: "text", text: `Raven MCP model set to: ${model}\nRestart opencode for the change to take effect.` })
2350
+ } else if (action === "effort" && parts.slice(2).join(" ").trim()) {
2351
+ const effort = parts.slice(2).join(" ").trim()
2352
+ updateConfig({ raven_mcp: { reasoning_effort: effort } })
2353
+ output.parts.push({ type: "text", text: `Raven MCP reasoning effort set to: ${effort}\nRestart opencode for the change to take effect.` })
2354
+ } else if (action === "timeout") {
2355
+ const seconds = Number(parts[2])
2356
+ if (!Number.isInteger(seconds) || seconds < 10 || seconds > MAX_TIMEOUT_SECONDS) {
2357
+ 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` })
2358
+ } else {
2359
+ updateConfig({ raven_mcp: { timeout: seconds } })
2360
+ output.parts.push({ type: "text", text: `Raven MCP timeout set to ${seconds}s. Takes effect immediately.` })
2361
+ }
2362
+ } else {
2363
+ 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>" })
1985
2364
  }
1986
2365
  } else if (arg === "update") {
1987
2366
  try {
@@ -1997,37 +2376,11 @@ Then restart opencode for the model change to take effect. If the task can be co
1997
2376
  } catch (err: any) {
1998
2377
  output.parts.push({ type: "text", text: `Raven update check failed: ${err?.message ?? err}\n\n${manualUpdateText()}` })
1999
2378
  }
2000
- } else if (arg.startsWith("model ")) {
2001
- const model = raw.slice(6).trim()
2002
- if (!model) {
2003
- output.parts.push({ type: "text", text: `Usage: /raven model <name>\nCurrent model: ${config.model || fm.model || "(default)"}` })
2004
- } else {
2005
- updateConfig({ model })
2006
- ravenProviderIssue = undefined
2007
- output.parts.push({ type: "text", text: `Raven model set to: ${model}\nRestart opencode for the change to take effect.` })
2008
- }
2009
- } else if (arg.startsWith("effort ")) {
2010
- const effort = raw.slice(7).trim()
2011
- if (!effort) {
2012
- output.parts.push({ type: "text", text: `Usage: /raven effort <value>\nCurrent: ${config.reasoning_effort || fm.reasoning_effort || "(default)"}` })
2013
- } else {
2014
- updateConfig({ reasoning_effort: effort })
2015
- output.parts.push({ type: "text", text: `Raven reasoning effort set to: ${effort}\nRestart opencode for the change to take effect.` })
2016
- }
2017
- } else if (arg.startsWith("timeout ")) {
2018
- const secs = Number(raw.slice(8).trim())
2019
- if (!Number.isInteger(secs) || secs < 10 || secs > MAX_TIMEOUT_SECONDS) {
2020
- output.parts.push({ type: "text", text: `Usage: /raven timeout <seconds>\nMust be between 10 and ${MAX_TIMEOUT_SECONDS}. Current: ${config.timeout ?? 600}s` })
2021
- } else {
2022
- updateConfig({ timeout: secs })
2023
- output.parts.push({ type: "text", text: `Raven timeout set to ${secs}s. Takes effect immediately.` })
2024
- }
2025
- } else {
2026
- const enabled = config.enabled ? "enabled" : "disabled"
2027
- const model = config.model || fm.model || "(default)"
2028
- const effort = config.reasoning_effort || fm.reasoning_effort || "(default)"
2029
- const timeout = config.timeout ?? 600
2030
- const providerIssue = ravenProviderIssue ? `\nProvider issue: ${ravenProviderIssue}` : ""
2379
+ } else {
2380
+ const seek = config.raven_seek
2381
+ const mcp = config.raven_mcp
2382
+ const seekIssue = ravenProviderIssues.seek ? `\n Provider issue: ${ravenProviderIssues.seek}` : ""
2383
+ const mcpIssue = ravenProviderIssues.mcp ? `\n Provider issue: ${ravenProviderIssues.mcp}` : ""
2031
2384
  let update = "Update: unable to check npm."
2032
2385
  try {
2033
2386
  const info = await getUpdateInfo()
@@ -2037,23 +2390,22 @@ Then restart opencode for the model change to take effect. If the task can be co
2037
2390
  ? `Update: up to date (latest ${info.latest}).`
2038
2391
  : "Update: unable to check npm."
2039
2392
  } catch { /* keep fallback */ }
2040
- 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.` })
2393
+ 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.` })
2041
2394
  }
2042
2395
  },
2043
2396
 
2044
2397
  async "tool.execute.before"(input: any, output: any) {
2045
- if (input.tool === "raven_seek" && (ravenSessions.has(input.sessionID) || sessionAgents.get(input.sessionID) === "raven")) {
2046
- throw new Error("raven_seek is disabled inside Raven. Use Raven's direct search/fetch tools instead.")
2047
- }
2048
-
2049
- if (input.tool === "raven_mcp" && !ravenSessions.has(input.sessionID) && sessionAgents.get(input.sessionID) !== "raven") {
2050
- 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.")
2051
- }
2052
-
2053
- if (!config.enabled) return
2054
- if (ravenSessions.has(input.sessionID)) return
2055
- if (isExcluded(sessionAgents.get(input.sessionID))) return
2056
- if (config.excludeTools?.some((name) => name.toLowerCase() === input.tool.toLowerCase())) return
2398
+ if ((input.tool === "raven_seek" || input.tool === "raven_mcp") && ravenSessions.has(input.sessionID)) {
2399
+ throw new Error(`${input.tool} cannot be called from inside a Raven agent. Use that agent's direct tools instead.`)
2400
+ }
2401
+
2402
+ if (input.tool === "raven_mcp_bridge" && ravenSessionKinds.get(input.sessionID) !== "mcp" && sessionAgents.get(input.sessionID) !== "raven-mcp") {
2403
+ throw new Error("raven_mcp_bridge is only available inside the Raven MCP agent. Use raven_mcp for on-demand MCP requests.")
2404
+ }
2405
+
2406
+ if (ravenSessions.has(input.sessionID)) return
2407
+ if (isExcluded(sessionAgents.get(input.sessionID))) return
2408
+ if (config.raven_seek.excludeTools?.some((name) => name.toLowerCase() === input.tool.toLowerCase())) return
2057
2409
 
2058
2410
  if (input.tool === "task" && output.args?.subagent_type === "raven") {
2059
2411
  ravenTaskCalls.add(input.callID)