fix(cli): never create note-only content for an empty OpenCode 2 result

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 #2204

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-11-01 09:33:17 +01:00
1 parent f531be2500
commit d3d0bc8452
2 files changed
+32 -2

No files matched your search

+6 -2
View File
@@ -491,14 +491,18 @@ char *cbm_client_adapter_opencode(const char *binary_path) {
* string/parts surface the model reads, output is the tool's schema-typed
* value). Append to content first, then a string output; if neither text
* surface exists, leave the result untouched so the tool's own output is
* never hidden. The caller assigns the returned result onto event.result. */
* never hidden. A parts array gets a new text part only when it already
* holds a part: `content: []` is a valid result, and a part added there
* would be the note alone, so an empty array falls through to the string
* output (or the unchanged result). The caller assigns the returned result
* onto event.result. */
sb_append(&sb,
"function appendResult(result, extra) {\n"
" if (!result || typeof result !== 'object') return result;\n"
" if (typeof result.content === 'string') {\n"
" return { ...result, content: result.content + '\\n' + extra };\n"
" }\n"
" if (Array.isArray(result.content)) {\n"
" if (Array.isArray(result.content) && result.content.length > 0) {\n"
" const content = result.content.slice();\n"
" for (let i = content.length - 1; i >= 0; i--) {\n"
" if (content[i] && content[i].type === 'text') {\n"
+26
View File
@@ -1432,6 +1432,31 @@ TEST(client_adapter_opencode_v2_compaction_note_goes_into_the_compaction_message
PASS();
}
/* #2204: Tool.Result allows `content: []`. appendResult adds a text part to a
* parts array only when the array already holds a part; an empty array falls
* through to the string `output` (or the unchanged result), so the note never
* becomes the whole content of a tool result. */
TEST(client_adapter_opencode_v2_append_result_never_creates_note_only_content) {
char *js = cbm_client_adapter_opencode("/usr/local/bin/codebase-memory-mcp");
ASSERT_NOT_NULL(js);
const char *parts =
strstr(js, "if (Array.isArray(result.content) && result.content.length > 0) {");
const char *add_part = strstr(js, "content.push({ type: 'text', text: extra });");
const char *output_fallback = strstr(js, "if (typeof result.output === 'string') {");
ASSERT_NOT_NULL(parts);
ASSERT_NOT_NULL(add_part);
ASSERT_NOT_NULL(output_fallback);
/* The only part-adding push sits inside the non-empty branch; the string
* output fallback, where `content: []` lands, comes after it. */
ASSERT_TRUE(parts < add_part);
ASSERT_TRUE(add_part < output_fallback);
ASSERT_NULL(strstr(add_part + 1, "content.push("));
/* No unguarded array branch is left. */
ASSERT_NULL(strstr(js, "if (Array.isArray(result.content)) {"));
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
@@ -1505,6 +1530,7 @@ SUITE(agent_clients) {
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_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);
}