Skip to content

feat: readOnly option, and a text fallback for recall without structured output - #56

Open
mhuinquiry wants to merge 3 commits into
kuitos:mainfrom
mhuinquiry:read-only-option
Open

mhuinquiry wants to merge 3 commits into
kuitos:mainfrom
mhuinquiry:read-only-option

Conversation

@mhuinquiry

@mhuinquiry mhuinquiry commented Oct 7, 2026 •

Copy link
Copy Markdown

Summary

Two additions for hosts that want Claude Code's memory but can't use all of it:

  1. readOnly: true — read Claude Code's memory without ever writing to the Claude config directory.
  2. Recall text fallback (1.x) — when the model has no structured output, recall retries as text instead of silently selecting nothing.

Why

readOnly. I'm using this plugin in an OpenCode-based host where the rule is one-way: the agent may read what Claude Code remembers, but must never add to it (nothing from that host may end up in Claude Code's context). Turning off extract and autodream isn't enough: memory_save / memory_delete are still offered to the main agent, the prompt tells it to "write to it directly" and to save when asked to remember, MemoryStore creates the memory folder, and on 2.x the log file lands under CLAUDE_CONFIG_DIR. I wrapped all of that from the outside first; an option is cleaner and keeps working across upgrades.

Text fallback. On 1.x the selector fork asks for format: json_schema. With models that can't produce structured output, the server returns a StructuredOutputError even though the text answer is correct (in my case the model replied ["staging.md"]) — forkFailure treats it as a failed fork, so recall never fires. The 2.x host already uses the text path (buildGeneratePrompt + parseSelectedMemories).

Changes

feat(config): readOnly — with readOnly: true:

  • parseConfig forces extract.enabled / autodream.enabled off, whatever they say
  • MemoryStore skips mkdir; save / delete throw (defence in depth — nothing should reach them)
  • buildMemoryToolSpecs drops memory_save / memory_delete; list / search / read stay
  • only the recall agent is registered (V1 config hook, V2 agent.transform)
  • V2 uses a no-op logger (its file lives under the config directory)
  • buildMemorySystemPrompt uses a read-only variant: says the memory is read-only, keeps the reading rules (WHEN_TO_ACCESS_READ_ONLY, TRUSTING_RECALL, searching past context), index and recalled memories; drops types/how-to-save/what-not-to-save/persistence and the update/delete bullets

fix(recall): text fallback — createV1Host().generate: the first StructuredOutputError switches that host to text mode (schema described in the prompt via buildGeneratePrompt, moved from host/v2/host.ts to the shared src/host/generate.ts); other model errors still fail. parseSelectedMemories also accepts a bare JSON array.

docs — README configuration + FAQ, AGENTS.md notes/structure.

Default behaviour is unchanged (readOnly defaults to false; the fallback only runs after a StructuredOutputError).

Tests

bun run lint        → clean
bun run typecheck   → clean
bun test            → 343 pass, 0 fail (10 new)
bun run build       → ok; no @opencode-ai/plugin / @opencode/plugin in dist/*.js; no module-level Map/Set in src/

New: test/readOnly.test.ts (config forcing, store mkdir/save/delete, read-only prompt, V1 tools + agents, a full V1 turn with recall that leaves every file under CLAUDE_CONFIG_DIR byte-identical and never lists sessions, no folder for an unseen project, V2 tools + agents + no log file) and test/recall/structuredFallback.test.ts (retry once then stay in text mode, other errors still throw, bare array parsing). I checked that the store and fallback tests fail with the respective change reverted.

Context: in my host (on 2.0.2, the newest release its dependency policy allows) I first hit the structured-output problem with a real model and worked around it with an adapter that does the same thing as this fallback — there recall went from never surfacing the memory to surfacing it. This PR's own code is covered by the unit tests above; I haven't run it against a real model.

🤖 Generated with Claude Code

mhuinquiry and others added 3 commits October 7, 2026 09:15
For hosts that may read what Claude Code remembers but must not add to it.
With `readOnly: true` nothing under CLAUDE_CONFIG_DIR is written:

- parseConfig forces extract / autodream off
- MemoryStore skips mkdir; save / delete throw
- memory_save / memory_delete are not offered (list / search / read stay)
- only the recall agent is registered (V1 and V2)
- V2 logs nowhere (its log file lives under the config directory)
- the prompt is a read-only variant: no "write to it directly", no
  how-to-save, no update/delete bullets

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On 1.x the selector fork asks for json_schema output. A model or provider
without structured output gets a StructuredOutputError from the server
even though its text answer is right, so recall silently selected nothing.

The first StructuredOutputError switches that host to the text path V2
already uses (buildGeneratePrompt, now shared in src/host/generate.ts) and
parseSelectedMemories, which now also accepts a bare JSON array. Other
model errors still fail the call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant