mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
fix(cli): keep the OpenCode 2 compaction note inside the compaction
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 #2077 test drops the assertions that pinned the old channel. Refs #2204 Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
1 parent
98a66b7356
commit
f531be2500
3 files changed
+54
-23
No files matched your search
+12
-14
@@ -428,7 +428,7 @@ 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 through the session context channel.\n"
|
||||
"// reinjection after compaction inside the compaction request itself.\n"
|
||||
"//\n"
|
||||
"// Two runtimes load this module:\n"
|
||||
"// - OpenCode 1.18.x dispatches the hooks returned by server(ctx).\n"
|
||||
@@ -642,26 +642,24 @@ char *cbm_client_adapter_opencode(const char *binary_path) {
|
||||
" // Fail open: the server() tool hook stays active.\n"
|
||||
" }\n"
|
||||
" }\n"
|
||||
" // OpenCode 2 compaction: persist the note as a keyed session\n"
|
||||
" // instruction (a <context> entry) instead of editing the system\n"
|
||||
" // prompt; entries survive compaction. Hosts without the channel\n"
|
||||
" // skip reinjection rather than failing the plugin.\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"
|
||||
" const sessionID = event?.sessionID;\n"
|
||||
" if (typeof sessionID !== 'string' || sessionID === '') return;\n"
|
||||
" const entry = ctx?.session?.instructions?.entry;\n"
|
||||
" if (typeof entry?.put !== 'function') return;\n"
|
||||
" const dir = await dirFor(sessionID);\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"
|
||||
" await entry.put({\n"
|
||||
" sessionID,\n"
|
||||
" key: 'codebase-memory-session-note',\n"
|
||||
" value: note,\n"
|
||||
" event.messages.push({\n"
|
||||
" role: 'user',\n"
|
||||
" content: [{ type: 'text', text: note }],\n"
|
||||
" });\n"
|
||||
" } catch {\n"
|
||||
" // Fail open: reinjection is best-effort.\n"
|
||||
|
||||
@@ -57,8 +57,8 @@ char *cbm_client_adapter_pi(const char *binary_path);
|
||||
* 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 stored as a keyed session
|
||||
* instruction (`<context>` entry) instead of the system prompt.
|
||||
* 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`.
|
||||
*
|
||||
* Returns NULL on allocation failure or when binary_path is NULL/empty. */
|
||||
char *cbm_client_adapter_opencode(const char *binary_path);
|
||||
|
||||
@@ -1347,8 +1347,8 @@ TEST(client_adapter_opencode_covers_lifecycle_read_and_compaction) {
|
||||
* 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 through a keyed session instruction (a <context> entry that survives
|
||||
* compaction), never the system prompt. */
|
||||
* goes into the compaction request's messages, never the system prompt (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);
|
||||
@@ -1370,10 +1370,8 @@ TEST(client_adapter_opencode_exports_the_v2_default_definition_issue2077) {
|
||||
ASSERT_NOT_NULL(output_fallback);
|
||||
ASSERT_TRUE(content_first < output_fallback);
|
||||
ASSERT_NULL(strstr(js, "if (result.content === undefined)"));
|
||||
/* Compaction uses the durable session context channel, not the system
|
||||
* prompt; older hosts without it skip reinjection. */
|
||||
/* The V2 hooks read the session id with optional chaining (fail open). */
|
||||
ASSERT_NOT_NULL(strstr(js, "event?.sessionID"));
|
||||
ASSERT_NOT_NULL(strstr(js, "ctx?.session?.instructions?.entry"));
|
||||
/* The plugin instance's location is not the session's location in a
|
||||
* multi-project server: resolve each session's directory. */
|
||||
ASSERT_NOT_NULL(strstr(js, "const dir = await dirFor("));
|
||||
@@ -1383,8 +1381,6 @@ TEST(client_adapter_opencode_exports_the_v2_default_definition_issue2077) {
|
||||
/* No fallback to the plugin instance's location: that is not the session's
|
||||
* location in a multi-project server. */
|
||||
ASSERT_NULL(strstr(js, "ctx?.location?.directory"));
|
||||
ASSERT_NOT_NULL(strstr(js, "key: 'codebase-memory-session-note'"));
|
||||
ASSERT_NULL(strstr(js, "event.system.push"));
|
||||
ASSERT_NULL(strstr(js, "pendingReinject"));
|
||||
/* Registrations fail open per domain; the server() hooks yield per domain. */
|
||||
ASSERT_NOT_NULL(strstr(js, "toolHooksRegistered = true;"));
|
||||
@@ -1400,6 +1396,42 @@ 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) {
|
||||
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, ".system"));
|
||||
ASSERT_NULL(strstr(js, "instructions"));
|
||||
ASSERT_NULL(strstr(js, "entry.put"));
|
||||
free(js);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* augment() is awaited inline inside 'tool.execute.after', so a hook-augment
|
||||
* that never exits would stall every grep/glob/read completion in the host
|
||||
* session until the agent is restarted. The generated plugin must bound the
|
||||
@@ -1472,6 +1504,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_bounds_a_hung_hook_augment_child);
|
||||
RUN_TEST(client_adapter_rejects_missing_binary_path);
|
||||
}
|
||||
Reference in new issue
Block a user