Dashboard › Code › Session s7cc57a5f186
s7cc57a5f186f0d02I'm investigating the "lore" project (an AI memory system, a monorepo with packages/core, packages/gateway, packages/opencode, packages/pi).
An AI agent (OpenCode) working in a DIFFERENT project saw three subagents/skills named lore-curator, lore-distill, lore-query-expand in its available agent/skill list. These are lore's INTERNAL worker subagents and should NOT be visible to the user-facing agent in arbitrary projects.
Do a VERY THOROUGH read-only exploration to find:
Where are lore-curator, lore-distill, lore-query-expand defined/registered? Search the entire repo for these exact strings. They appear to be OpenCode subagent types. Look in packages/opencode/src/ especially. Look for how the OpenCode plugin registers agents/subagents/skills via the @opencode-ai/plugin API.
How does the OpenCode plugin register these? Look at packages/opencode/src/index.ts and any related files. Look for agent, subagent, skill, tool hooks in the plugin. Find where these three workers get exposed as agents that show up in OpenCode's agent list.
Are these meant to be internal-only? Look for any naming/visibility config — e.g. should they be mode: subagent, hidden, or invoked only internally by lore itself (not exposed to the host OpenCode agent picker)? Look at how they're described/configured.
The query-expand / curator / distill workers: Find their actual implementation. Search packages/core/src/ for "query-expand", "queryExpand", "curator", "distill" worker definitions. Understand if these are LLM worker prompts that lore runs internally vs. OpenCode-exposed agents.
Report findings as a structured summary with exact file:line references and quoted code/config. Focus on WHY these would appear in the host project's agent/skill picker and whether that's intended. Do NOT make any edits — read only.
I'm going to search for those exact strings to see what comes up.
I'll do a thorough exploration. Let me start by searching for the exact strings across the entire repo.
[]
...output truncated... Full output saved to: /home/byk/.local/share/opencode/tool-output/tool_eb356e4b7001fjpcuy9Sg0tz24 input (such as query parameters or database content) is used as JSX tag names via `jsx()` or `createElement()` during server-side rendering.\n\n## Impact\n\nAn attacker who can control tag names used in JSX rendering may inject unintended HTML into the generated output.\n\nThis may lead to:\n\n* Injection of unexpected HTML elements or attributes\n* Corruption of the HTML structure\n* Cross-site scripting (XSS) when combined with unsafe usage patterns\n\nThis issue only affects applications that construct JSX tag names from untrusted input. Applications using static or allowlisted tag names are not ...
Let me read the full dependabot alerts output to get all the details.
Found 106 matches (more matches available)
/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/test/worker-health.test.ts:
Line 39: recordWorkerFailure("s1", "lore-distill", "no-auth");
Line 40: recordWorkerFailure("s1", "lore-distill", "no-auth");
Line 46: recordWorkerFailure("s1", "lore-distill", "no-auth");
Line 47: recordWorkerFailure("s1", "lore-distill", "no-auth");
Line 48: recordWorkerFailure("s1", "lore-distill", "no-auth");
Line 56: recordWorkerFailure("s1", "lore-distill", "no-auth");
Line 72: recordWorkerFailure("s1", "lore-distill", "no-auth");
Line 89: recordWorkerFailure("s1", "lore-distill", "no-auth");
Line 103: recordWorkerFailure("s1", "lore-distill", "no-auth");
Line 107: recordWorkerFailure("s1", "lore-distill", "no-auth");
Line 115: recordWorkerFailure("s1", "lore-distill", "no-auth");
Line 127: ...
Found 5 matches
/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/src/worker-health.ts:
Line 48: | "lore-query-expand"
/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/opencode/test/index.test.ts:
Line 84: expect(agents["lore-query-expand"]).toEqual({
/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/opencode/src/index.ts:
Line 279: "lore-query-expand": {
/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/src/cost-tracker.ts:
Line 670: "lore-query-expand": "recall",
/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/core/src/search.ts:
Line 539: workerID: "lore-query-expand",
/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/opencode/src/index.ts
I need to look at the main entry point in packages/opencode/src/index.ts, particularly the section around line 271-290 where the agents get registered, to understand the broader structure.
The core registration is in packages/opencode/src/index.ts around line 271. Let me read that file thoroughly, plus the test file and the plugin API context.
<path>/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/opencode/src/index.ts</path>
<type>file</type>
<content>
200: }
201: return null;
202: })();
203: }
204: const result = await loreInitPromise;
205: if (result) {
206: loreActive = true;
207: gatewayBase = result;
208: }
209: }
210: processLoreActive = loreActive;
211: processLoreBase = gatewayBase;
212: }
213:
214: if (!loreActive && !loreDisabled) {
215: const inTestEnv =
216: process.env.NODE_ENV === "test" ||
217: process.argv.some((a) => a.includes(".test."));
218: if (!inTestEnv) {
219: const base = "Lore failed to start — memory features are unavailable.";
220: const msg = lastGatewayStartError
221: ? `${base} Gateway error: ${lastGatewayStartError}` +
222: (/not found|not exported|cannot find module/i.test(
223: ...
<path>/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/opencode/test/index.test.ts</path>
<type>file</type>
<content>
1: import { describe, test, expect } from "vitest";
2: import { fileURLToPath } from "node:url";
3: import { LorePlugin } from "../src/index";
4: import type { Plugin } from "@opencode-ai/plugin";
5:
6: /**
7: * Minimal mock of the OpenCode client. Only stubs the methods the plugin
8: * actually calls during initialization.
9: */
10: function createMockClient() {
11: return {
12: tui: {
13: showToast: () => Promise.resolve(),
14: },
15: session: {
16: get: () => Promise.resolve({ data: {} }),
17: list: () => Promise.resolve({ data: [] }),
18: create: () => Promise.resolve({ data: { id: "worker_1" } }),
19: messages: () => Promise.resolve({ data: [] }),
20: message: () => Promise.resolve({ data: null }),
21: prompt: () => Promise.resolve({ data: {} ...
The plugin registers three worker agents through the config hook with hidden: true, which should prevent them from appearing in the agent picker. But the user is seeing them in OpenCode's agent list in a different project, so there's a disconnect between the intended behavior and what's actually happening.
I need to verify whether hidden is actually a recognized configuration option in the OpenCode plugin API, or if the flag isn't being respected properly. I should also trace how these worker agents get invoked—looking at the session creation code and how the agent names are referenced when spawning workers.
The registration is clear. Now let me understand the @opencode-ai/plugin API to see whether hidden actually suppresses agents from the picker, and read the rest of index.ts plus worker-spawning code.
<path>/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/opencode/src/index.ts</path>
<type>file</type>
<content>
1: import type { Plugin, Hooks } from "@opencode-ai/plugin";
2: import {
3: log,
4: getGitRemote,
5: discoverWorkspaceRoot,
6: installFetchInterceptor,
7: } from "@loreai/core";
8:
9: /**
10: * Lore plugin for OpenCode — transparent LLM proxy routing.
11: *
12: * Instead of overwriting provider baseURLs (which loses original auth and
13: * URL context), this plugin installs a fetch-level interceptor that
14: * transparently reroutes outgoing LLM API calls through the Lore gateway.
15: * The SDK builds requests normally (correct auth, correct URL for each
16: * provider), and the interceptor redirects them while preserving all
17: * original headers. The gateway forwards non-managed headers upstream.
18: *
19: * Per-request context (session ID, agent name, provider ID) is injected
20: * via the ...
<path>/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/opencode/src/index.ts</path>
<type>file</type>
<content>
360: };
361:
362: // Startup banner — visible in stderr so silent failures are obvious.
363: // Suppressed in test env to keep vitest output clean.
364: if (!processInitDone) {
365: const projectPath = discoverWorkspaceRoot(ctx.worktree || ctx.directory);
366: if (process.env.NODE_ENV !== "test") {
367: process.stderr.write(`[lore] active: ${projectPath}\n`);
368: }
369:
370: if (loreActive) {
371: // Install the fetch interceptor once per process. It transparently
372: // reroutes outgoing LLM API calls through the gateway while
373: // preserving original auth headers and URLs. ...
I see the agents are registered with hidden: true in the config hook, but they're still showing up in the host project's picker, which suggests either the field isn't being respected or there's something else exposing them. Let me check the plugin API types to see if hidden is actually a supported property for suppressing agents from the UI.
Now let me examine the @opencode-ai/plugin API types to understand whether hidden actually hides agents, and what the proper agent config schema is.
(no output)
---worktree node_modules--- ---packages/opencode node_modules--- total 8 drwxr-xr-x 1 byk byk 18 Jun 8 12:27 . drwxr-xr-x 1 byk byk 58 Jun 8 12:27 .. lrwxrwxrwx 1 byk byk 90 Jun 8 12:27 plugin -> ../../../../node_modules/.pnpm/@opencode-ai+plugin@1.2.15/node_modules/@opencode-ai/plugin lrwxrwxrwx 1 byk byk 84 Jun 8 12:27 sdk -> ../../../../node_modules/.pnpm/@opencode-ai+sdk@1.2.15/node_modules/@opencode-ai/sdk
Let me explore the plugin/SDK type definitions to understand the agent config schema and the hidden field.
(no output)
(no output)
/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/node_modules/.pnpm/@opencode-ai+plugin@1.2.15/node_modules/@opencode-ai/plugin total 4 drwxr-xr-x 1 byk byk 32 Jun 8 12:27 . drwxr-xr-x 1 byk byk 18 Jun 8 12:27 .. drwxr-xr-x 1 byk byk 148 Jun 8 12:27 dist -rw-r--r-- 1 byk byk 710 Jun 8 12:27 package.json
total 32 drwxr-xr-x 1 byk byk 148 Jun 8 12:27 . drwxr-xr-x 1 byk byk 32 Jun 8 12:27 .. -rw-r--r-- 1 byk byk 78 Jun 8 12:27 example.d.ts -rw-r--r-- 1 byk byk 436 Jun 8 12:27 example.js -rw-r--r-- 1 byk byk 6407 Jun 8 12:27 index.d.ts -rw-r--r-- 1 byk byk 24 Jun 8 12:27 index.js -rw-r--r-- 1 byk byk 3248 Jun 8 12:27 shell.d.ts -rw-rw-r-- 1 byk byk 0 Jan 13 14:05 shell.js -rw-r--r-- 1 byk byk 1242 Jun 8 12:27 tool.d.ts -rw-r--r-- 1 byk byk 92 Jun 8 12:27 tool.js
@opencode-ai+sdk@1.16.2/node_modules/@opencode-ai/sdk @opencode-ai+sdk@1.2.15/node_modules/@opencode-ai/sdk --- total 24 drwxr-xr-x 1 byk byk 126 Jun 8 12:27 . drwxr-xr-x 1 byk byk 32 Jun 8 12:27 .. -rw-r--r-- 1 byk byk 317 Jun 8 12:27 client.d.ts -rw-r--r-- 1 byk byk 741 Jun 8 12:27 client.js drwxr-xr-x 1 byk byk 172 Jun 8 12:27 gen -rw-r--r-- 1 byk byk 302 Jun 8 12:27 index.d.ts -rw-r--r-- 1 byk byk 419 Jun 8 12:27 index.js -rw-r--r-- 1 byk byk 565 Jun 8 12:27 server.d.ts -rw-r--r-- 1 byk byk 2955 Jun 8 12:27 server.js drwxr-xr-x 1 byk byk 122 Jun 8 12:27 v2
<path>/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/node_modules/.pnpm/@opencode-ai+plugin@1.2.15/node_modules/@opencode-ai/plugin/dist/index.d.ts</path>
<type>file</type>
<content>
1: import type { Event, createOpencodeClient, Project, Model, Provider, Permission, UserMessage, Message, Part, Auth, Config } from "@opencode-ai/sdk";
2: import type { BunShell } from "./shell";
3: import { type ToolDefinition } from "./tool";
4: export * from "./tool";
5: export type ProviderContext = {
6: source: "env" | "config" | "custom" | "api";
7: info: Provider;
8: options: Record<string, any>;
9: };
10: export type PluginInput = {
11: client: ReturnType<typeof createOpencodeClient>;
12: project: Project;
13: directory: string;
14: worktree: string;
15: serverUrl: URL;
16: $: BunShell;
17: };
18: export type Plugin = (input: PluginInput) => Promise<Hooks>;
19: export type AuthHook = {
20: provider: ...
I need to check the Config type definition from the SDK to see if there's a hidden field and a mode field in the agent config schema.
The config hook takes a Config from the SDK. Let me find the Config and agent config schema definition to verify whether hidden and mode are valid fields.
ls: cannot access '/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/node_modules/.pnpm/@opencode-ai+sdk@1.2.15/node_modules/@opencode-ai/sdk/gen/': No such file or directory
No files found
/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/node_modules/.pnpm/@opencode-ai+sdk@1.2.15/node_modules/@opencode-ai/sdk/dist/v2/gen/core/auth.gen.d.ts /home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/node_modules/.pnpm/@opencode-ai+sdk@1.2.15/node_modules/@opencode-ai/sdk/dist/v2/gen/core/bodySerializer.gen.d.ts /home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/node_modules/.pnpm/@opencode-ai+sdk@1.2.15/node_modules/@opencode-ai/sdk/dist/v2/gen/core/params.gen.d.ts /home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/node_modules/.pnpm/@opencode-ai+sdk@1.2.15/node_modules/@opencode-ai/sdk/dist/v2/gen/core/pathSerializer.gen.d.ts /home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/node_modules/.pnpm/@opencode-ai+sdk@1.2.15/node_modules/@opencode-ai/sdk/dist...
Found 28 matches
/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/node_modules/.pnpm/@opencode-ai+sdk@1.2.15/node_modules/@opencode-ai/sdk/dist/gen/types.gen.d.ts:
Line 110: mode: string;
Line 835: export type AgentConfig = {
Line 843: disable?: boolean;
Line 848: mode?: "subagent" | "primary" | "all";
Line 868: } | boolean | ("subagent" | "primary" | "all") | number | {
Line 940: * Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.
Line 962: * Enable or disable the MCP server on startup
Line 994: * Enable or disable the MCP server on startup
Line 1004: * OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection. ...
Let me read the full AgentConfig type and the agent config section to understand the valid fields and how the picker handles them.
<path>/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/node_modules/.pnpm/@opencode-ai+sdk@1.2.15/node_modules/@opencode-ai/sdk/dist/gen/types.gen.d.ts</path>
<type>file</type>
<content>
835: export type AgentConfig = {
836: model?: string;
837: temperature?: number;
838: top_p?: number;
839: prompt?: string;
840: tools?: {
841: [key: string]: boolean;
842: };
843: disable?: boolean;
844: /**
845: * Description of when to use the agent
846: */
847: description?: string;
848: mode?: "subagent" | "primary" | "all";
849: /**
850: * Hex color code for the agent (e.g., #FF5733)
851: */
852: color?: string;
853: /**
854: * Maximum number of agentic iterations before forcing text-only response
855: */
856: maxSteps?: number;
857: permission?: {
858: edit?: "ask" | "allow" | "deny";
859: bash?: ("ask" | "allow" | "deny") | {
860: ...
I'm spotting a critical mismatch here. The AgentConfig type definition doesn't actually include a hidden field—it only has disable to turn off an agent entirely and mode to control where it appears in the UI. But the plugin code is trying to set hidden: true, which isn't a valid property on that type.
TypeScript allows this to slip through because of an index signature that accepts arbitrary keys, so the code compiles without errors. However, OpenCode itself ignores the hidden property entirely, meaning these worker agents aren't being hidden at all. They're showing up in the agent/skill list because they're registered with no mode specified, which defaults to making them visible. To actually hide them, the plugin would need to use either disable: true (which would prevent lore from calling them) or mode: "subagent" (which would keep them out of the primary picker but they'd still appear elsewhere). ...
Critical finding: AgentConfig (lines 835-877) has no hidden field. Valid fields are disable?: boolean (843) and mode?: "subagent" | "primary" | "all" (848). Let me confirm the runtime Agent type and check the newer SDK version, plus how the agent config map is typed.
<path>/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/node_modules/.pnpm/@opencode-ai+sdk@1.2.15/node_modules/@opencode-ai/sdk/dist/gen/types.gen.d.ts</path>
<type>file</type>
<content>
1090: * Model to use in the format of provider/model, eg anthropic/claude-2
1091: */
1092: model?: string;
1093: /**
1094: * Small model to use for tasks like title generation in the format of provider/model
1095: */
1096: small_model?: string;
1097: /**
1098: * Custom username to display in conversations instead of system username
1099: */
1100: username?: string;
1101: /**
1102: * @deprecated Use `agent` field instead.
1103: */
1104: mode?: {
1105: build?: AgentConfig;
1106: plan?: AgentConfig;
1107: [key: string]: AgentConfig | undefined;
1108: };
1109: /**
1110: * Agent configuration, see https://opencode.ai/docs/agent
1111: */
1112: ...
<path>/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/node_modules/.pnpm/@opencode-ai+sdk@1.2.15/node_modules/@opencode-ai/sdk/dist/gen/types.gen.d.ts</path>
<type>file</type>
<content>
1380: newHeader?: string;
1381: hunks: Array<{
1382: oldStart: number;
1383: oldLines: number;
1384: newStart: number;
1385: newLines: number;
1386: lines: Array<string>;
1387: }>;
1388: index?: string;
1389: };
1390: encoding?: "base64";
1391: mimeType?: string;
1392: };
1393: export type File = {
1394: path: string;
1395: added: number;
1396: removed: number;
1397: status: "added" | "deleted" | "modified";
1398: };
1399: export type Agent = {
1400: name: string;
1401: description?: string;
1402: mode: "subagent" | "primary" | "all";
1403: builtIn: boolean;
1404: topP?: number;
1405: temperature?: number;
1406: ...
The Agent type doesn't have a hidden field—only mode, disable, and other recognized properties. The lore plugin is setting hidden: true, which isn't a valid OpenCode field, but it passes type checking because AgentConfig has an index signature that allows unknown keys. The agents themselves are being registered without specifying a mode.
I need to check what OpenCode's default mode is when agents are defined without one, and whether the newer SDK version (1.16.2) added support for hidden as a field. The project is using 1.2.15, so I should verify against that version's documentation and see if there's any git history explaining why hidden was used here.
Confirmed: the runtime Agent type (1399-1428) and AgentConfig (835-877) both have no hidden field — only mode and disable. Let me check the newer SDK 1.16.2 to see if hidden exists there, and verify OpenCode's default agent mode behavior.
835:export type AgentConfig = {
843: disable?: boolean;
848: mode?: "subagent" | "primary" | "all";
1104: mode?: {
packages/gateway/src/pipeline.ts:4566: // 1. The user can actually see it (not hidden in <system-reminder> tags)
packages/opencode/test/index.test.ts:66: test("registers hidden worker agents", async () => {
packages/opencode/test/index.test.ts:74: { hidden: boolean; description: string }
packages/opencode/test/index.test.ts:77: hidden: true,
packages/opencode/test/index.test.ts:81: hidden: true,
packages/opencode/test/index.test.ts:85: hidden: true,
packages/opencode/test/index.test.ts:97: agent: { "my-agent": { hidden: false, description: "Custom" } },
packages/opencode/test/index.test.ts:103: hidden: false,
packages/gateway/src/llm-adapter.ts:806: // failure to surface loudly. Log it at `warn` (hidden unless
packages/opencode/src/index.ts:264: // Disable built-in compaction (gateway handles it), register hidden
packages/opencode/src/index.ts:272: hidden: true,
packages/opencode/src/index.ts:276: ...
I've confirmed that both SDK versions lack a hidden field in AgentConfig—only disable and mode options exist. The lore plugin is using hidden: true as a configuration option that doesn't actually exist, but it's being silently accepted because the config object allows arbitrary properties.
Since these three worker agents have no mode or disable settings, they'll be visible by default. I need to check the OpenCode documentation to confirm whether the default mode is "all" (appearing in both the primary picker and subagent list) or "subagent" (appearing only in the subagent/skill picker). Given that the user mentioned seeing them in the available agent/skill list, they're likely defaulting to one of these modes, but I should verify which one.
Both SDK versions confirm: no hidden field exists in AgentConfig. Let me confirm OpenCode's default agent mode behavior via the docs, and check git history of this code to understand intent.
Agents | OpenCode [Skip to content](#_top)
[  OpenCode](/docs/)
[app.header.home](/)[app.header.docs](/docs/)
[](https://github.com/anomalyco/opencode)[](https://opencode.ai/discord)
Search CtrlK
Cancel
- [Intro](/docs/)
- [Config](/docs/config/)
- [Providers](/docs/providers/)
- [Network](/docs/network/)
- [Enterprise](/docs/enterprise/)
- [Troubleshooting](/docs/troubleshooting/)
- [Windows](/docs/windows-wsl)
- Usage
- [Go](/docs/go/)
- [TUI](/docs/tui/)
- [CLI](/docs/cli/)
- [Web](/docs/web/)
- [IDE](/docs/ide/)
- [Zen](/docs/zen/)
- [Share](/docs/share/)
- [GitHub](/docs/github/)
- [GitLab](/docs/gitlab/)
- Configure
- [Tools](/docs/tools/)
- [Rules](/docs/rules/)
- [Agents](/docs/agents/)
- [Models](/docs/models/)
- [Themes](/docs/themes/)
- ...
e618322 fix(gateway): resolve bun export to source to prevent stale in-process bundle (#668)
67d0bc0 feat(docs): persona-based docs site + LORE_UPSTREAM_EXTRA_HEADERS (#625)
aa7f3ad fix(gateway,core,opencode): auto-detect remote-gateway mode and harden project attribution (#618)
7ccc536 test(ci): quiet test harness noise + pretest hook for bundle (#608)
2db852f fix(gateway,opencode,pi): transparent provider routing via fetch-level interception (#574)
---blame for the agent block---
76f2a4b refactor: split into @loreai/core + opencode-lore monorepo (#71)
diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts
new file mode 100644
index 0000000..efd221b
--- /dev/null
+++ b/packages/opencode/src/index.ts
@@ -0,0 +1,797 @@
+import type { Plugin, Hooks } from "@opencode-ai/plugin";
+import { join } from "path";
+import {
+ load,
+ config,
+ ensureProject,
+ isFirstRun,
+ temporal,
+ ltm,
+ distillation,
+ curator,
+ transform,
+ setModelLimits,
+ ...
Looking at the OpenCode documentation, the mode parameter defaults to all when not specified, and hidden: true is indeed a valid field for hiding subagents from the autocomplete menu, though it comes with specific constraints.
The critical issue is that hidden: true only applies when mode: "subagent" is explicitly set. The lore plugin registers its agents with hidden: true but omits the mode parameter entirely, so they default to mode: "all" — meaning they function as both primary agents and subagents simultaneously. Since hidden is only honored for mode: "subagent" agents, the flag gets silently ignored, and the agents end up appearing in both the primary agent Tab cycle picker and the @ mention autocomplete menu in the host project. ...
Now I have the definitive answer from OpenCode's own docs. Let me confirm part 4 — how the workers actually execute (internal LLM calls vs OpenCode agents) — by examining the worker implementations.
<path>/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/core/src/search.ts</path>
<type>file</type>
<content>
505:
506: import { QUERY_EXPANSION_SYSTEM } from "./prompt";
507: import * as log from "./log";
508: import { db } from "./db";
509: import type { LLMClient } from "./types";
510:
511: /**
512: * Expand a user query into multiple search variants using the configured LLM.
513: * Returns `[original, ...expanded]`. The original is always first.
514: *
515: * Uses a 3-second timeout — if the LLM is slow, returns only the original query.
516: * Errors are caught silently (logged) and the original query is returned.
517: *
518: * @param llm LLM client for prompt calls
519: * @param query The original user query
520: * @param model Optional model override
521: */
522: export async function expandQuery(
523: llm: LLMClient,
524: query: string,
525: model?: { providerID: string; modelID: string ...
<path>/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/core/src/distillation.ts</path>
<type>file</type>
<content>
1015:
1016: // Pass the explicit worker model through — never fall back to config().model
1017: // which is the project/session model and may be from a different provider
1018: // than the worker's upstream URL. Cross-provider model names → 404.
1019: // When model is undefined, the gateway's cross-provider guard (pipeline.ts)
1020: // validates or skips the call before the adapter's defaultModel is used.
1021: const model = input.model;
1022: const sourceTokens = input.messages.reduce((sum, m) => sum + m.tokens, 0);
1023: const maxTokens = distillTokenBudget(sourceTokens);
1024: const responseText = await input.llm.prompt(
1025: DISTILLATION_SYSTEM,
1026: userContent,
1027: {
1028: model,
1029: workerID: "lore-distill",
1030: thinking: false,
1031: urgent: ...
So the workers actually execute through lore's internal LLM client via batch-queue, not through OpenCode's agent system—the worker IDs like "lore-distill" are just labels for cost tracking and health monitoring. The three agents registered in OpenCode's config aren't actually being invoked as OpenCode agents at all.
But then why register them? The eval harness does use session.prompt with agent: "lore-distill", so the registration makes those agents available there. In production, the plugin might register them defensively or because the gateway could spawn sub-sessions. I need to verify whether the LLMClient's workerID maps to the x-lore-agent header and whether the registration is actually necessary for production, or if it's just vestigial from the eval setup.
Let me check if other plugins like Pi have similar agent registration and whether there's a skills directory involved.
Let me verify there's no separate skill registration and check whether the Pi adapter does the same, plus confirm the worker→agent header relationship in the gateway.
No files found
<path>/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/src/cost-tracker.ts</path>
<type>file</type>
<content>
655:
656: // Daily budget throttle: accumulate spend and update velocity EMA
657: maybeResetDay();
658: dailySpend += call.total;
659: addDailyCost(dailySpendDate, "conversation", call.total);
660: updateCostRate(call.total);
661: }
662:
663: /** Worker ID → cost bucket mapping. */
664: type WorkerBucket = keyof SessionCosts["workers"];
665:
666: const WORKER_BUCKETS: Record<string, WorkerBucket> = {
667: "lore-distill": "distillation",
668: "lore-curator": "curation",
669: "lore-compact": "compaction",
670: "lore-query-expand": "recall",
671: };
672:
673: /**
674: * Record a worker LLM call's cost (called from LLM adapter / batch queue). ...
Found 4 matches /home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/src/batch-queue.ts: Line 119: workerID?: string; Line 925: pending.workerID, Line 1002: workerID: item.workerID, Line 1104: workerID: opts?.workerID,
The workerID is just an internal label for cost tracking and worker health within the gateway's batch queue, not part of OpenCode's agent registration system. I should verify the pi package structure and check if there are any markdown agent definitions in the repo that might be registering these agents separately.
I have a comprehensive picture. Let me do a final check for any markdown-based agent/skill definitions in the repo that could also register these.
./.opencode/plans/1779179476405-misty-engine.md ./.opencode/plans/1779193803902-crisp-cabin.md ./.opencode/plans/1779207982918-brave-sailor.md ./.opencode/plans/1779216785710-happy-pixel.md ./.opencode/plans/1779225662690-sunny-harbor.md ./.opencode/plans/1779259765283-quiet-cactus.md ./.opencode/plans/1779262739322-misty-engine.md ./.opencode/plans/1779267004304-stellar-planet.md ./.opencode/plans/1779271065379-glowing-island.md ./.opencode/plans/1779280560993-playful-nebula.md --- any *.md mentioning lore-distill ---
I've found where these workers are registered—it's in the config hook at a specific location in the codebase, where they're added as OpenCode agents with hidden status and descriptions but no mode specified.
The core issue is that OpenCode's hidden field only works when mode: "subagent" is set, but these agents default to mode: "all" since no mode is specified. This means they appear in both the primary agent picker and the @ mention/skill autocomplete across all projects, even though the code comment and intent clearly mark them as internal-only. The test only checks the object shape, not whether OpenCode actually hides them, giving false confidence that the hiding works.
The workers themselves run entirely through lore's internal LLMClient system, not through OpenCode's agent infrastructure, so they may not need to be registered in OpenCode at all—the fix would be adding mode: "subagent" to make the hidden flag take effect.
I now have a complete, verified picture. Here is my structured report.
lore-curator, lore-distill, lore-query-expand appear in the host OpenCode agent/skill pickerThe lore OpenCode plugin registers these three workers as OpenCode agents in its config hook using { hidden: true, description: ... } but never sets mode: "subagent". Per OpenCode's own documentation:
mode is specified, an agent defaults to mode: "all" (visible as both a primary agent in the Tab/cycle picker and a subagent in the @-mention / Task / skill list).hidden: true only applies to mode: "subagent" agents.So the hidden: true flag is silently ignored, and all three internal workers leak into the host project's agent/skill picker. This is a bug, not intended behavior — the code comment literally says "register hidden worker agents."
The only place these become OpenCode-visible agents is the plugin's config hook:
packages/opencode/src/index.ts:263-284
const hooks: Hooks = {
// Disable built-in compaction (gateway handles it), register hidden
// worker agents, and redirect all provider baseURLs through the gateway.
config: async (input) => {
const cfg = input as Record<string, unknown>;
cfg.compaction = { auto: false, prune: false };
cfg.agent = {
...(cfg.agent as Record<string, unknown> | undefined),
"lore-distill": {
hidden: true,
description: "Lore memory distillation worker",
},
"lore-curator": {
hidden: true,
description: "Lore knowledge curator worker",
},
"lore-query-expand": {
hidden: true,
description: "Lore query expansion worker",
},
};
},
This runs in every project where the lore plugin loads (LorePlugin is invoked per-project), so the workers appear in arbitrary host projects. ...