mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
fix(cli): add no OpenCode 2 compaction note until one has run on a real 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 #2204 Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
1 parent
d3d0bc8452
commit
fb88365dc0
4 files changed
+34
-82
No files matched your search
@@ -722,7 +722,7 @@ overwrite user-modified agents.
|
||||
| Codex CLI | Detected | `$CODEX_HOME/config.toml` | Managed `AGENTS.md` activation pointer, skill, three read-only agents; `SessionStart` + `SubagentStart`, `PreToolUse` graph lookup for `Bash` searches |
|
||||
| Gemini CLI | Detected | `.gemini/settings.json` | `GEMINI.md`, three explicit read/graph-tool subagents; `BeforeTool`, `AfterTool` `read_file` coverage, and `SessionStart` |
|
||||
| Zed | Detected | platform `settings.json` (JSONC) | `AGENTS.md` + shared skill |
|
||||
| OpenCode | Detected | `$OPENCODE_CONFIG` or resolved global config | `AGENTS.md`, skill, three deny-by-default read-only agents; plugin adds grep/glob graph lookup, post-`read` coverage, first-tool-result session context, and post-compaction reinjection |
|
||||
| OpenCode | Detected | `$OPENCODE_CONFIG` or resolved global config | `AGENTS.md`, skill, three deny-by-default read-only agents; plugin adds grep/glob graph lookup, post-`read` coverage, first-tool-result session context, and post-compaction reinjection on OpenCode 1.18.x |
|
||||
| Antigravity | Detected | `.gemini/config/mcp_config.json` | `.gemini/GEMINI.md` |
|
||||
| Aider | Detected | none | `CONVENTIONS.md` via `.aider.conf.yml` |
|
||||
| KiloCode | Detected | `.config/kilo/kilo.jsonc` | Rule + three graph-tool subagents with deny-by-default permissions |
|
||||
|
||||
+11
-40
@@ -428,14 +428,15 @@ char *cbm_client_adapter_opencode(const char *binary_path) {
|
||||
"// context surfaces other clients get through hook configuration: graph\n"
|
||||
"// lookup after grep/glob, index-coverage notes after read, session-start\n"
|
||||
"// tier routing (carried on the first tool result of each session), and\n"
|
||||
"// reinjection after compaction inside the compaction request itself.\n"
|
||||
"// on OpenCode 1.18.x reinjection after compaction.\n"
|
||||
"//\n"
|
||||
"// Two runtimes load this module:\n"
|
||||
"// - OpenCode 1.18.x dispatches the hooks returned by server(ctx).\n"
|
||||
"// - OpenCode 2 ignores server and calls setup(ctx), which registers the\n"
|
||||
"// same behaviour on the context's tool and session hook domains. Each\n"
|
||||
"// registration fails open, and the matching server() hook yields once\n"
|
||||
"// its domain has registered.\n");
|
||||
"// tool hook on the context's tool domain. The registration fails open,\n"
|
||||
"// and the server() tool hook yields once it has registered. OpenCode 2\n"
|
||||
"// gets no note after compaction: no compaction channel has been run on\n"
|
||||
"// a real 2.x host yet.\n");
|
||||
sb_append(&sb, "import { spawn } from 'node:child_process';\n\n");
|
||||
sb_append(&sb, "const BIN = '");
|
||||
sb_append(&sb, bin);
|
||||
@@ -519,11 +520,10 @@ char *cbm_client_adapter_opencode(const char *binary_path) {
|
||||
" return result;\n"
|
||||
"}\n\n");
|
||||
|
||||
/* Per-domain registration flags: a host that dispatches both server() and
|
||||
* setup() with domains must not append the note twice. The server() hook
|
||||
* for a domain yields once setup() registered that same domain. */
|
||||
sb_append(&sb, "let toolHooksRegistered = false;\n"
|
||||
"let sessionHooksRegistered = false;\n\n");
|
||||
/* A host that dispatches both server() and setup() with a tool domain must
|
||||
* not append the note twice: the server() tool hook yields once setup()
|
||||
* registered the tool domain. */
|
||||
sb_append(&sb, "let toolHooksRegistered = false;\n\n");
|
||||
|
||||
sb_append(
|
||||
&sb,
|
||||
@@ -575,7 +575,6 @@ char *cbm_client_adapter_opencode(const char *binary_path) {
|
||||
" // Documented (experimental) compaction surface: output.context is the\n"
|
||||
" // mutable array of context strings for the rebuilt session.\n"
|
||||
" 'experimental.session.compacting': async (_input, output) => {\n"
|
||||
" if (sessionHooksRegistered) return;\n"
|
||||
" const note = await lifecycle();\n"
|
||||
" if (note && Array.isArray(output?.context)) {\n"
|
||||
" output.context.push(note);\n"
|
||||
@@ -584,8 +583,8 @@ char *cbm_client_adapter_opencode(const char *binary_path) {
|
||||
" };\n"
|
||||
" },\n"
|
||||
" // OpenCode 2 runtime: default.server is ignored and setup(ctx) registers\n"
|
||||
" // the hooks on the context domains. Each registration fails open, so one\n"
|
||||
" // missing domain cannot break the host or the other domain.\n"
|
||||
" // the tool hook on the context's tool domain. The registration fails\n"
|
||||
" // open, so a missing domain cannot break the host.\n"
|
||||
" async setup(ctx) {\n"
|
||||
" const seen = new Set();\n"
|
||||
" const dirs = new Map();\n"
|
||||
@@ -646,34 +645,6 @@ char *cbm_client_adapter_opencode(const char *binary_path) {
|
||||
" // Fail open: the server() tool hook stays active.\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" // OpenCode 2 compaction: the hook receives the compaction request, and\n"
|
||||
" // its messages are the input the summary is built from. The note is\n"
|
||||
" // added there as a text message, as the server() path adds it to\n"
|
||||
" // output.context, so it stays inside the compaction and out of the\n"
|
||||
" // system prompt. Hosts without the hook or the array skip reinjection\n"
|
||||
" // rather than failing the plugin.\n"
|
||||
" if (ctx?.session?.hook) {\n"
|
||||
" try {\n"
|
||||
" await ctx.session.hook('compaction', async (event) => {\n"
|
||||
" if (!Array.isArray(event?.messages)) return;\n"
|
||||
" const dir = await dirFor(event?.sessionID);\n"
|
||||
" if (!dir) return;\n"
|
||||
" const note = await lifecycle(dir);\n"
|
||||
" if (!note) return;\n"
|
||||
" try {\n"
|
||||
" event.messages.push({\n"
|
||||
" role: 'user',\n"
|
||||
" content: [{ type: 'text', text: note }],\n"
|
||||
" });\n"
|
||||
" } catch {\n"
|
||||
" // Fail open: reinjection is best-effort.\n"
|
||||
" }\n"
|
||||
" });\n"
|
||||
" sessionHooksRegistered = true;\n"
|
||||
" } catch {\n"
|
||||
" // Fail open: the server() compaction hook stays active.\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" },\n"
|
||||
"};\n");
|
||||
|
||||
|
||||
@@ -56,9 +56,10 @@ char *cbm_client_adapter_pi(const char *binary_path);
|
||||
* silently — no error, no signal. That risk is accepted deliberately for the
|
||||
* server() path and recorded here so it is not rediscovered as a mystery.
|
||||
* OpenCode 2 ignores `server` and calls `setup(ctx)`, which registers
|
||||
* `execute.after` and `compaction` on the context domains. Each registration
|
||||
* fails open, and the post-compaction note is added as a text message to the
|
||||
* compaction request's `messages` (the summary input), never to `system`.
|
||||
* `execute.after` on the context's tool domain and fails open. It registers no
|
||||
* `compaction` hook: pushing a plain object into `event.messages` would fail,
|
||||
* if at all, inside OpenCode's own compaction where the plugin cannot catch it,
|
||||
* so a note after compaction needs a real 2.x run first.
|
||||
*
|
||||
* Returns NULL on allocation failure or when binary_path is NULL/empty. */
|
||||
char *cbm_client_adapter_opencode(const char *binary_path);
|
||||
|
||||
+18
-38
@@ -1344,11 +1344,10 @@ TEST(client_adapter_opencode_covers_lifecycle_read_and_compaction) {
|
||||
|
||||
/* #2077/#2089/#2204: the two OpenCode runtimes read different entries of the
|
||||
* default export. 1.18.x dispatches the hooks returned by `server(ctx)`; 2.x
|
||||
* ignores `server` and calls `setup(ctx)`, whose context provides the tool and
|
||||
* session hook domains. Registrations fail open per domain, so the matching
|
||||
* server() hook yields once its domain is registered. The post-compaction note
|
||||
* goes into the compaction request's messages, never the system prompt (pinned
|
||||
* by the compaction test below). */
|
||||
* ignores `server` and calls `setup(ctx)`, whose context provides the tool hook
|
||||
* domain. The registration fails open, and the server() tool hook yields once
|
||||
* it is registered. OpenCode 2 adds no note to a compaction (pinned by the
|
||||
* compaction test below). */
|
||||
TEST(client_adapter_opencode_exports_the_v2_default_definition_issue2077) {
|
||||
char *js = cbm_client_adapter_opencode("/usr/local/bin/codebase-memory-mcp");
|
||||
ASSERT_NOT_NULL(js);
|
||||
@@ -1360,8 +1359,6 @@ TEST(client_adapter_opencode_exports_the_v2_default_definition_issue2077) {
|
||||
* 2.x, so setup is no longer the empty placeholder #2089 pinned. */
|
||||
ASSERT_NOT_NULL(strstr(js, "async setup(ctx) {"));
|
||||
ASSERT_NOT_NULL(strstr(js, "ctx.tool.hook('execute.after'"));
|
||||
ASSERT_NOT_NULL(strstr(js, "ctx.session.hook('compaction'"));
|
||||
ASSERT_NULL(strstr(js, "ctx.session.hook('context'"));
|
||||
/* content is the surface the model reads; prefer it over output, and never
|
||||
* synthesize content that would hide the tool's own result. */
|
||||
const char *content_first = strstr(js, "if (typeof result.content === 'string') {");
|
||||
@@ -1382,11 +1379,9 @@ TEST(client_adapter_opencode_exports_the_v2_default_definition_issue2077) {
|
||||
* location in a multi-project server. */
|
||||
ASSERT_NULL(strstr(js, "ctx?.location?.directory"));
|
||||
ASSERT_NULL(strstr(js, "pendingReinject"));
|
||||
/* Registrations fail open per domain; the server() hooks yield per domain. */
|
||||
/* The registration fails open; the server() tool hook yields once it is in. */
|
||||
ASSERT_NOT_NULL(strstr(js, "toolHooksRegistered = true;"));
|
||||
ASSERT_NOT_NULL(strstr(js, "sessionHooksRegistered = true;"));
|
||||
ASSERT_NOT_NULL(strstr(js, "if (toolHooksRegistered) return;"));
|
||||
ASSERT_NOT_NULL(strstr(js, "if (sessionHooksRegistered) return;"));
|
||||
ASSERT_NOT_NULL(strstr(js, "catch {"));
|
||||
ASSERT_NOT_NULL(strstr(js, "event.result = appendResult(event.result, extra);"));
|
||||
/* The empty setup placeholder and the old named export must both be gone. */
|
||||
@@ -1396,38 +1391,23 @@ TEST(client_adapter_opencode_exports_the_v2_default_definition_issue2077) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* #2204: on OpenCode 2 the post-compaction note stays inside the compaction,
|
||||
* at the level the 1.18.x path uses (output.context). The `compaction` hook
|
||||
* receives the compaction request, whose `messages` are the input the summary
|
||||
* is built from, so the note is pushed there as a text message. It must never
|
||||
* reach the system prompt: not through `event.system`, and not through a
|
||||
* session instruction entry, which OpenCode renders into the system array of
|
||||
* every later request. */
|
||||
TEST(client_adapter_opencode_v2_compaction_note_goes_into_the_compaction_messages) {
|
||||
/* #2204: OpenCode 2 gets no note after compaction until a compaction channel has
|
||||
* run on a real 2.x host. A plain object pushed into `event.messages` would be
|
||||
* rejected, if at all, inside OpenCode's own compaction, where the plugin's
|
||||
* try/catch cannot reach it, so an untested channel could break compaction for
|
||||
* every OpenCode 2 user. The note must not reach the system prompt either: not
|
||||
* through `event.system`, and not through a session instruction entry. The
|
||||
* 1.18.x server() path keeps its documented compaction surface. */
|
||||
TEST(client_adapter_opencode_v2_adds_no_note_to_the_compaction) {
|
||||
char *js = cbm_client_adapter_opencode("/usr/local/bin/codebase-memory-mcp");
|
||||
ASSERT_NOT_NULL(js);
|
||||
const char *hook = strstr(js, "ctx.session.hook('compaction'");
|
||||
const char *push = strstr(js, "event.messages.push({");
|
||||
const char *registered = strstr(js, "sessionHooksRegistered = true;");
|
||||
ASSERT_NOT_NULL(hook);
|
||||
ASSERT_NOT_NULL(push);
|
||||
ASSERT_NOT_NULL(registered);
|
||||
/* The push lives inside the compaction registration. */
|
||||
ASSERT_TRUE(hook < push);
|
||||
ASSERT_TRUE(push < registered);
|
||||
/* A text message that carries the note. */
|
||||
const char *role = strstr(push, "role: 'user',");
|
||||
const char *text = strstr(push, "content: [{ type: 'text', text: note }],");
|
||||
ASSERT_NOT_NULL(role);
|
||||
ASSERT_NOT_NULL(text);
|
||||
ASSERT_TRUE(role < registered);
|
||||
ASSERT_TRUE(text < registered);
|
||||
/* Fail open: a host whose event has no messages array skips reinjection. */
|
||||
ASSERT_NOT_NULL(strstr(js, "if (!Array.isArray(event?.messages)) return;"));
|
||||
/* Never the system prompt, and no session instruction entry. */
|
||||
ASSERT_NULL(strstr(js, "ctx.session.hook("));
|
||||
ASSERT_NULL(strstr(js, "event.messages"));
|
||||
ASSERT_NULL(strstr(js, "sessionHooksRegistered"));
|
||||
ASSERT_NULL(strstr(js, ".system"));
|
||||
ASSERT_NULL(strstr(js, "instructions"));
|
||||
ASSERT_NULL(strstr(js, "entry.put"));
|
||||
ASSERT_NOT_NULL(strstr(js, "'experimental.session.compacting'"));
|
||||
free(js);
|
||||
PASS();
|
||||
}
|
||||
@@ -1529,7 +1509,7 @@ SUITE(agent_clients) {
|
||||
RUN_TEST(client_adapter_opencode_sends_the_required_hook_event);
|
||||
RUN_TEST(client_adapter_opencode_covers_lifecycle_read_and_compaction);
|
||||
RUN_TEST(client_adapter_opencode_exports_the_v2_default_definition_issue2077);
|
||||
RUN_TEST(client_adapter_opencode_v2_compaction_note_goes_into_the_compaction_messages);
|
||||
RUN_TEST(client_adapter_opencode_v2_adds_no_note_to_the_compaction);
|
||||
RUN_TEST(client_adapter_opencode_v2_append_result_never_creates_note_only_content);
|
||||
RUN_TEST(client_adapter_opencode_bounds_a_hung_hook_augment_child);
|
||||
RUN_TEST(client_adapter_rejects_missing_binary_path);
|
||||
|
||||
Reference in new issue
Block a user