Skip to content

[BUG] Auto-capture and profile learning never run on OpenCode v2: session.idle is not delivered and SDK transport is never initialized #331

Description

@DustinxDU

Summary

Auto-capture and user-profile learning are 100% non-functional on OpenCode v2. The v2 plugin adapter (src/v2/) has never been published to npm, and the code on main appears to have two gaps that prevent internal structured-output sessions from ever running.

This is not gated behind configuration — with any provider/model combination, user_prompts.capture_attempts stays 0 and captured stays 0 for every single prompt.

Environment

opencode v2.0.20 (@opencode/cli)
plugin opencode-mem built from main (package.json version 2.26.0)
Node v25.7.0
OS macOS, arm64
Install path source build, loaded via ~/.config/opencode/plugins/opencode-mem.ts re-exporting dist/plugin.js

The published npm 2.26.0 cannot be used at all on v2 — it exports { id, server } and is rejected by the v2 plugin schema:

PluginModule.LoadError: Plugin must export a default definition with an id
and an effect or setup function.
(cause: SchemaError(Missing key at ["default"]["effect"] / ["default"]["setup"]))

src/v2/plugin.ts on main does export { id, setup } and loads fine, so the build below is from main.


Problem 1 — session.idle is never delivered (event name changed in v2)

The legacy capture trigger is:

// src/index.ts
if (event.type === "session.idle") { /* ... */ idleTimeout = setTimeout(...)

The v2 adapter forwards every event through toLegacyEvent(), but only maps one event name:

// src/v2/legacy-client.ts
const type = rawType === "session.compaction.ended" ? "session.compacted" : rawType;

session.idle has no mapping. I instrumented ctx.event.subscribe() on a real v2.0.20 session and dumped every session.* event actually emitted:

session.execution.started      session.step.started
session.execution.succeeded    session.step.ended
session.text.started           session.step.streamed
session.text.ended             session.text.delta
session.reasoning.delta        session.inbox.delivered
session.instructions.updated   session.inbox.enqueued
session.renamed                session.usage.updated

There is no session.idle in v2. The equivalent is session.execution.succeeded. Because the name never matches, the 10s debounce timer is never armed and performAutoCapture() is never called.

Suggested fix

const type =
  rawType === "session.compaction.ended" ? "session.compacted"
  : rawType === "session.execution.succeeded" ? "session.idle"
  : rawType;

I verified this alone changes the symptom from capture_attempts = 0 to capture_attempts > 0 (capture is attempted and then fails for the reason in Problem 2).


Problem 2 — _useSdkTransport is never enabled, so structured output always throws

After Problem 1 is fixed, every capture attempt fails three times with:

auto-capture: opencode provider failed, falling back to external API:
  {"error":"Error: opencode-mem: v2 server base URL not initialized;
            call createV2Client(serverUrl) first"}
Auto-capture final error after 3 attempts

Root cause, in src/index.ts:

export async function configureOpencodeHostTransport(ctx) {
  const { createV2Client, ... } = await loadOpencodeProvider();
  const serverUrl = hostConfig.baseUrl ?? ctx.serverUrl;
  if (serverUrl) {                       // <-- never true on a native v2 ctx
    setV2Client(createV2Client(serverUrl, { fetch, headers }));
  }
}

A native v2 context has no serverUrl, so createV2Client() is never called. That means the module-level _useSdkTransport (in services/ai/opencode-provider.ts) stays false, and generateStructuredOutput() can never take its SDK branch:

if (_useSdkTransport && hasV2SessionClient(client)) {
  return generateViaSdkClient(client, {...});   // unreachable on v2
}
const baseUrl = _v2BaseUrl;                     // always undefined
if (!baseUrl) throw new Error("opencode-mem: v2 server base URL not initialized...");

Note hasV2SessionClient() would return true for the object createLegacyClient(ctx) produces (it exposes session.create/prompt/delete) — only the _useSdkTransport flag is missing.

Workaround I used

src/v2/plugin.ts currently does:

const { setV2Client } = await loadOpencodeProvider();
setV2Client(legacyClient);

Setting _useSdkTransport requires calling createV2Client() (it is the only writer of that flag), so I added a call after it:

const { setV2Client, createV2Client } = await loadOpencodeProvider();
setV2Client(legacyClient);
createV2Client("<unused>", { fetch: globalThis.fetch });

The URL is never read — the SDK branch returns before _v2BaseUrl is consulted, and createLazyV2Client() is fully lazy so discarding its return value has no side effects. This makes capture work end to end.

I am not claiming this is the right shape for the fix — it is a demonstration that the flag is the only blocker. A cleaner fix might export an explicit setUseSdkTransport(true), or have the v2 adapter pass a real server URL.


Also worth checking (may be intentional)

createLegacyClient().provider.list() derives the connected-provider set from ctx.model.list():

const models = await ctx.model.list();
const connected = [...new Set(models.data.map(m => m.providerID).filter(Boolean))];

At plugin init time this returned only the always-available built-in provider, so any API-key provider configured in opencode.json was reported as "not connected". If the v2 host populates models asynchronously, this race would make opencodeProvider-based capture unreliable even after the two fixes above. I did not chase this further because the external-API path (memoryApiUrl + memoryApiKey) works correctly.

Separately, I hit this on the OpenCode Zen free tier, which may be expected but is easy to trip over:

Generate.UnavailableError: OpenCode's free tier can only be used from within OpenCode

so opencode/*-free models cannot be used for auto-capture's internal background sessions. Worth a note in the README.

Impact

For anyone on OpenCode v2, memory accumulates only via explicit memory tool calls or import. Automatic capture, user-profile learning, and compaction injection silently never run. There are no crash or error banners — the only signal is capture_attempts = 0 in user-prompts.db.

Given src/v2/ is still unpublished, I suspect nobody has run this path against a released v2 build yet. Related: #300 asks about hardware requirements, which is also unanswered.

Repro

  1. Build main (bun install && bunx tsc && cd web && bun run build), load dist/plugin.js from a plugin file in ~/.config/opencode/plugins/.
  2. Have any multi-turn technical conversation on opencode v2.
  3. Wait >10s after the turn goes idle.
  4. Inspect ~/.opencode-mem/data/user-prompts.db:
    select count(*) from user_prompts;                    -- grows
    select count(*) from user_prompts where captured=1;   -- stays 0
    select count(*) from user_prompts where capture_attempts > 0; -- stays 0
    No memory is written and ~/.opencode-mem/opencode-mem.log contains no capture lines.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions