Repository navigation
fix(cli): register OpenCode 2 hooks via the setup context - #2385
Conversation
|
Thank you so much, @HazielMagallanes, this is a genuinely careful piece of work. You didn't just switch
That compatibility table is the piece we were missing when #2089 was reviewed. We checked your central claim against the published
#2204 independently reports the same registration working on 2.0.3. So the reason #2089 left On coordination with #2089: @AmirF194's PR is already reviewed and approved in the requested shape, and we've committed to landing it, so it goes first. After that, this PR becomes the delta you described yourself. Right now the two conflict in A few small things while you're in there, each with the reason:
Please also say "Fixes #2204" in the description, since this is what that issue asked for. CI is still queued on our side because the runner pool is busy, which has nothing to do with your diff. Thanks again. This fills the gap #2204 asked about without undoing anything #2089 fixed. |
|
Thanks for opening this — it has been seen, and it is queued. This note is automated, but it is not a brush-off: it exists so you know where your PR stands instead of having to guess from silence. Current review status: working through a backlog. What that means for this PR, concretely:
Things that will genuinely speed it up whenever review does happen:
If this fixes a bug, a reproduction we can run is worth more than a description of the symptom. Thanks for contributing, and sorry in advance for the wait. |
|
Addressing the review, @DeusData — pushed c03575e:
Real-host proof (OpenCode 2.0.8, A real One note for anyone reproducing this: Still pending on our side: the rebase and folding #2089's two assertions into the single contract test — #2089 has not merged yet, so I did not want to add a second, disagreeing test in the meantime. I'll rebase and flip those assertions as soon as it lands. Description updated to say |
|
Quick update, @HazielMagallanes: #2089 is now merged. As expected, this branch now conflicts with |
|
I'll work on it in a minute ^^ |
cb307a4 to
534274c
Compare
|
Rebased onto current
CI is re-running on the new head. Thanks again for the review, and for the explicit steer on updating the existing test rather than adding one beside it. |
|
Adding a fourth data point — this host is on OpenCode 2.0.25, newer than the 2.0.8 / 2.0.21 already verified above, and it reproduces both behaviours. Environment
1. The 0.11.0 module fails to load. It exports only the named V1 factory, so the 2.x loader refuses it on every start/reload and export const CodebaseMemory = async (ctx) => ({
'tool.execute.after': async (input, output) => { … },
});I checked the published 2. The installer re-creates the problem on the next run (#2204). Reproduced against 0.11.0 in an isolated The generated header tells readers "remove the markers to take ownership", but and the result cannot be parsed at all: So a hand-port is not a durable end state — and once the managed block itself carries Happy to re-run on a preview build if that helps the review. |
|
Thanks for the completed rebase and consolidated contract test, and thanks to @crisacm for the additional OpenCode 2.0.25 report. I have read both updates. The hook-registration change remains pending re-review; the separate marker-less installer issue is tracked by #2641, so it need not be folded into this PR. |
|
Thanks again for the rebase, consolidated contract test, and additional OpenCode report. The hook-registration update still needs our re-review; #2641 remains the separate marker-less installer issue. Sorry for the delay—I’ll follow up soon. |
|
Thank you so much for this, @HazielMagallanes, and sorry it took us a while to get to a full review. Keeping the OpenCode augmentation working on the newer plugin loader is genuinely useful, and the change is tidy: it reuses the existing We would like to take this, with a few changes first:
Once these are in and CI is green, we will merge. Thanks again for picking this up - it is a nice improvement for OpenCode users. |
- keep the `server(ctx)` entry that OpenCode 1.18.x dispatches hooks from - fill `setup(ctx)` with the V2 hook registrations (execute.after, compaction) and fail open per domain, so one unavailable registration neither breaks the host nor disables the other - prefer `content` over `output` in appendResult and return the result unchanged when no text surface exists, so a tool's own output is never hidden - persist the post-compaction note as a keyed session instruction (`codebase-memory-session-note`) that survives compaction instead of editing the system prompt; hosts without that channel skip reinjection - resolve each session's directory with ctx.session.get (cached), because ctx.location is the plugin instance's location, not the session's - update the DeusData#2089 emission test in place to state the 2.x contract Fixes DeusData#2204 Refs DeusData#2077 DeusData#2089 Signed-off-by: HazielMagallanes <[email protected]>
00d2cea to
98a66b7
Compare
|
All five points are addressed, @DeusData — head
CI is re-running on the new head. Thanks again for the careful review — the session-location finding came directly from doing the real-host test you asked for. |
|
Superseding the local-only capture with a reproducible public-repo run, @DeusData — head The public
The capture needed |
|
Thank you so much for the fast and careful turnaround, @HazielMagallanes, and for going the extra mile with a reproducible run on a public checkout. That is exactly the proof we asked for, and it already paid off: the Where this stands against our last note:
Two things are left: point 2 of our last note, and one small edge in point 1.
For the proof, the tool-call half of your recipe can stay as it is; the compaction half would then show the note arriving through the compaction (for example in the summary after Once these two are in and CI is green, we will take a final look. Thanks again - this is careful, well-documented work, and OpenCode 2 users will benefit from it. |
The OpenCode 2 compaction hook receives the compaction request, whose messages are the input the summary is built from. Add the post-compaction session note there as a text message, mirroring the 1.18.x path that pushes it into output.context. Session instruction entries are rendered into the system array of every later request, so the note no longer goes through ctx.session.instructions.entry, and event.system stays untouched. Registration and the push still fail open. Pin it in a new agent_clients test: the push sits inside the compaction registration, carries a user text part, is guarded by an array check, and the generated module names neither a system property nor an instruction entry. The DeusData#2077 test drops the assertions that pinned the old channel. Refs DeusData#2204 Signed-off-by: Martin Vogel <[email protected]>
Tool.Result allows content: [], and for it the array branch of appendResult pushed a text part that held only the session note, so the note became the whole content of the tool result. Add a text part only when the array already holds a part; an empty array now falls through to the string output branch, or the result is returned unchanged. Pin it in a new agent_clients test: the part-adding push sits inside the non-empty array branch, ahead of the string output fallback, and no unguarded array branch is left. Refs DeusData#2204 Signed-off-by: Martin Vogel <[email protected]>
|
Thank you so much, @HazielMagallanes - this PR has come a long way, and the care you put into it shows: the per-domain fail-open registration, the session-directory fix you found through the real 2.2.5 run, and a reproduction recipe anyone can follow. Since maintainer edits are enabled, we went ahead and made the two remaining changes from our last note ourselves, so you do not have to do another round for them. Two small commits are now on top of your branch:
One request: could you re-run the compaction half of your recipe on OpenCode 2.2.5 against the new head (d3d0bc8)? Please check that the compaction still completes without an error with the plain message object we push, that the note shows up in the compaction itself - for example in the summary after Please pull before pushing again, so these two commits are not lost. Thanks again for the patience and the thorough work - OpenCode 2 users will benefit from it. |
…al host The V2 compaction hook pushed a plain object into the compaction request's event.messages. OpenCode's Message is a schema class at v2.2.5, and nobody has run this path on a real 2.x host. The try/catch only guards the array push, which cannot throw. If OpenCode rejects the object, that happens inside its own compaction, out of the plugin's reach, so an untested note could break compaction for every OpenCode 2 user. Losing the note costs far less. Remove the V2 compaction registration and the sessionHooksRegistered flag it needed. The V2 tool hook, proven on OpenCode 2.2.5, stays, and so does the 1.18.x experimental.session.compacting path. OpenCode 2 sessions still get the session note on the first tool result of a session. A later change can add a note after compaction together with a real 2.x run. The replacement test pins that the generated module registers no session hook and touches neither event.messages, a system property nor an instruction entry. Refs DeusData#2204 Signed-off-by: Martin Vogel <[email protected]>
|
Thank you again, @HazielMagallanes. We made a call on the last open point, and it means no more work for you: please skip the compaction re-run we asked for this morning. One more maintainer commit is on your branch (fb88365). It removes the OpenCode 2 compaction note, including the version we added ourselves. Here is why:
What stays is the part you proved on 2.2.5: the tool hook, per-domain fail-open registration, per-session directory resolution, and the session note on a session's first tool result. The 1.18.x compaction path is unchanged. A note after compaction can come back in a later PR together with a real 2.x run. If you ever want to pick that up, we would be glad to review it. We will merge once CI is green on this head. Thank you so much for the patience and the careful work: your real-host run found a bug that generated-text tests could never have caught, and OpenCode 2 users will benefit from it. |
|
Merged as 587f134. Thank you so much, @HazielMagallanes, for sticking with this through several review rounds. Your real run on OpenCode 2.2.5 found the session-directory bug that generated-text tests could never have caught, and your reproduction recipe gave us something anyone can follow. OpenCode 2 users now get graph context on their searches and reads because of this work. If you ever want to bring back a note after compaction on OpenCode 2 together with a real 2.x run, we would be glad to review it. |
Summary
The generated OpenCode plugin serves both loader generations from one default export:
default.serverand dispatches the hooks it returns (unchanged).default.server, validates{ id, setup | effect }and callssetup(ctx), whose context exposes the tool and session hook domains. The plugin registersexecute.afterandcompactionthere.Tool output is extended through
appendResult:contentfirst (string, then text parts), then a stringoutput; when neither text surface exists the result is returned unchanged, so a tool's own output is never hidden.After compaction, the session-start note is stored as a keyed session instruction via
ctx.session.instructions.entry.put— a<context>entry that survives compaction and reverts — instead of editing the system prompt. Hosts without that channel skip reinjection rather than failing.Each registration is wrapped in try/catch and the matching
server()hook yields once its domain has registered (toolHooksRegistered,sessionHooksRegistered), so a host that dispatches both entries cannot append twice. The V2 path resolves each session's own location withctx.session.get(cached per session) instead ofctx.location, which is the plugin instance's location, not the session's; if the session directory cannot be resolved, augmentation is skipped.Why
setup()is not empty#2089 settled the
server: async (ctx) => { … }, setup() {}shape against the 1.18.x loaders. On current OpenCode 2.x, however,serveris ignored: the 2.x plugin loader decodes the default export as{ id, setup } | { id, effect }and invokessetup(ctx), whose context exposesctx.tool.hook("execute.after", …)andctx.session.hook("compaction", …). With an emptysetup, the plugin loads without error but registers zero hooks. This PR keeps theserverentry requested in #2089 and fillssetupwith the same behaviour, guarded for older loaders.Reproduction (OpenCode 2.2.x)
Prerequisites: OpenCode 2.2.x (tested 2.2.5), a tool-capable model,
jq, and a build of this branch.Expected:
graph project="cbm-pr-proof" is indexedplus the session note.{"data":[{"key":"codebase-memory-session-note","value":"[codebase-memory] … cbm-pr-proof …"}]}.Troubleshooting:
grep 'cbm-augment' ~/.local/share/opencode/log/opencode.log | tail→loading plugin, neverfailed to load plugin.~/.cache/codebase-memory-mcp/logs/hook-augment-timeouts.logshowsdeadline_exceeded, start the OpenCode service with that variable in its environment (e.g.opencode service stop && CBM_HOOK_DEADLINE_MS=15000 opencode service start) — CBM's internal 2 s hook limit, not this plugin.instructions.entrystep needs OpenCode 2.2.x; the tool-call step works on 2.x.Captured evidence
OpenCode 2.2.5, public
codebase-memory-mcpcheckout, project aliascbm-pr-proof:(The capture had
CBM_HOOK_DEADLINE_MS=15000set for CBM's 2 s hook deadline on this machine; the plugin itself does not set it.)Verification
scripts/test.sh --suites agent_clients: 40 passed. The run used a temporary module-dump helper that was removed afterwards; the adapter code did not change.make lint-formatpasses with clang-format 20.1.8 (CI's pinned version).tests/test_agent_clients.cis edited in place; no whole-file formatting is applied totests/, so the whitespace-only reflow is gone.Fixes #2204
Refs #2077 #2089