From f8a71383e177cbb32128dcd3167c936f86a4ed2b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 7 Jun 2026 03:09:30 +0000 Subject: [PATCH 1/2] deps(deps): bump the minor-and-patch group with 3 updates Bumps the minor-and-patch group with 3 updates: [@anthropic-ai/sdk](https://github.com/anthropics/anthropic-sdk-typescript), iii-sdk and [tsdown](https://github.com/rolldown/tsdown). Updates `@anthropic-ai/sdk` from 0.100.1 to 0.102.0 - [Release notes](https://github.com/anthropics/anthropic-sdk-typescript/releases) - [Changelog](https://github.com/anthropics/anthropic-sdk-typescript/blob/main/CHANGELOG.md) - [Commits](https://github.com/anthropics/anthropic-sdk-typescript/compare/sdk-v0.100.1...sdk-v0.102.0) Updates `iii-sdk` from 0.11.2 to 0.19.0 Updates `tsdown` from 0.21.10 to 0.22.2 - [Release notes](https://github.com/rolldown/tsdown/releases) - [Commits](https://github.com/rolldown/tsdown/compare/v0.21.10...v0.22.2) --- updated-dependencies: - dependency-name: "@anthropic-ai/sdk" dependency-version: 0.102.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-and-patch - dependency-name: iii-sdk dependency-version: 0.19.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-and-patch - dependency-name: tsdown dependency-version: 0.22.2 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-and-patch ... Signed-off-by: dependabot[bot] --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index e0a3b7416..b6c0f7070 100644 --- a/package.json +++ b/package.json @@ -59,10 +59,10 @@ }, "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.3.142", - "@anthropic-ai/sdk": "^0.100.1", + "@anthropic-ai/sdk": "^0.102.0", "@clack/prompts": "^1.2.0", "dotenv": "^17.4.2", - "iii-sdk": "0.11.2", + "iii-sdk": "0.19.0", "zod": "^4.0.0" }, "optionalDependencies": { @@ -74,7 +74,7 @@ }, "devDependencies": { "@types/node": "^25.9.1", - "tsdown": "^0.21.10", + "tsdown": "^0.22.2", "tsx": "^4.19.0", "typescript": "^6.0.3", "vitest": "^4.1.6" From 5167e1ef55f8f41203e3204c7381a485af8d666d Mon Sep 17 00:00:00 2001 From: Daniel Hart Date: Thu, 25 Jun 2026 02:38:16 +0000 Subject: [PATCH 2/2] fix(context,compress-synthetic): conversation observations render with full content The Hermes plugin (and any integration that uses tool_name: "conversation" as a generic catch-all for user/assistant turns) stored observations via the synthetic-compression path with type="other" and title="conversation". The memory-context renderer then produced lines like: - [other] conversation: which were easy to misread as 'type=conversation, empty content' and broke the user's mental model of what was being injected into the system prompt. This change has three parts: 1. inferType() now recognizes conversation-shaped tool names ("conversation", "chat", "prompt", "message", "turn") and returns the correct type. New observations are stored with type="conversation" and a meaningful title derived from the user prompt or tool input, instead of the literal tool name. 2. The context renderer now has a defensive fallback: when the stored title is empty, equals the type, or is one of a small set of generic tool names, it uses the subtitle or the first 80 chars of the narrative as the title. This handles observations that were stored before the inferType fix landed. 3. The filter is tightened to drop observations with empty narrative, so they never reach the renderer and produce dangling-colon lines like '- [other] conversation: '. The renderer also strips a title prefix from the narrative when they overlap, to avoid the user message appearing twice on the same line. Fixes the issue where the 100+ Hermes-plugin conversation observations were rendering as empty bullets in the system prompt's memory-context block across all six of the user's CLIs. --- src/functions/compress-synthetic.ts | 35 ++++++++++++++++-- src/functions/context.ts | 55 +++++++++++++++++++++++++++-- 2 files changed, 85 insertions(+), 5 deletions(-) diff --git a/src/functions/compress-synthetic.ts b/src/functions/compress-synthetic.ts index 28d17e979..30605bca1 100644 --- a/src/functions/compress-synthetic.ts +++ b/src/functions/compress-synthetic.ts @@ -26,6 +26,21 @@ function inferType( .replace(/([a-z])([A-Z])/g, "$1_$2") .replace(/[-\s]+/g, "_") .toLowerCase(); + // Recognize conversation-shaped tool names. Several integrations + // (notably the Hermes plugin) use tool_name: "conversation" as a + // generic catch-all for user/assistant turns. Without this branch the + // synthetic compressor stores the observation as type "other" with a + // title that is the literal string "conversation", which then renders in + // the memory-context block as `- [other] conversation: `. + if ( + n === "conversation" || + n === "chat" || + n === "prompt" || + n === "message" || + n === "turn" + ) { + return "conversation"; + } const hasWord = (word: string) => new RegExp(`(^|_)${word}(_|$)`).test(n) || n === word || @@ -34,13 +49,13 @@ function inferType( if (["fetch", "http", "web"].some(hasWord)) return "web_fetch"; if (["grep", "search", "glob", "find"].some(hasWord)) return "search"; if (["bash", "shell", "exec", "run"].some(hasWord)) return "command_run"; - if (["edit", "update", "patch", "replace"].some(hasWord)) return "file_edit"; + if (["edit", "update", "patch", "replace"].some(hasWord)) + return "file_edit"; if (["write", "create"].some(hasWord)) return "file_write"; if (["read", "view"].some(hasWord)) return "file_read"; if (["task", "agent"].some(hasWord)) return "subagent"; return "other"; } - function extractFiles(input: unknown): string[] { if (!input || typeof input !== "object") return []; const o = input as Record; @@ -90,7 +105,21 @@ export function buildSyntheticCompression( sessionId: raw.sessionId, timestamp: raw.timestamp, type: inferType(toolName, raw.hookType), - title: truncate(toolName || "observation", 80), + // For generic tool names ("conversation", "other", "observation", + // "tool") fall back to deriving a descriptive title from the user + // prompt or the tool input. Without this, a tool_name of + // "conversation" produces a title that is the literal string + // "conversation" — useless in the rendered memory-context block. + title: + toolName && + !["conversation", "other", "observation", "tool"].includes( + toolName.toLowerCase(), + ) + ? truncate(toolName, 80) + : truncate( + promptStr || inputStr || "observation", + 80, + ), subtitle: inputStr ? truncate(inputStr, 120) : undefined, facts: [], narrative: truncate(narrativeParts.join(" | "), 400), diff --git a/src/functions/context.ts b/src/functions/context.ts index 1e6102cf3..2f744b498 100644 --- a/src/functions/context.ts +++ b/src/functions/context.ts @@ -174,16 +174,67 @@ export function registerContextFunction( for (let j = 0; j < sessionsNeedingObs.length; j++) { const i = sessionsNeedingObs[j]; const observations = obsResults[j]; + // Tighten the filter: drop observations with empty narrative so + // they never reach the renderer and produce dangling-colon lines + // like `- [other] conversation: `. const important = observations.filter( - (o) => o.title && o.importance >= 5, + (o) => + o.title && + o.importance >= 5 && + (o.narrative || "").trim().length > 0, ); if (important.length > 0) { const top = important .sort((a, b) => b.importance - a.importance) .slice(0, 5); + // Defensive renderer: when the stored title is empty, equals the + // type, or is a generic tool name like "conversation", fall back + // to the subtitle (user message) as the title and use the + // narrative as the body. This handles observations stored before + // the inferType fix landed, where the synthetic compressor + // used the raw tool name as the title. Without this fallback + // those observations render as `- [other] conversation: ` with + // a dangling colon and no content. + const GENERIC_TITLES = new Set([ + "conversation", + "other", + "observation", + "tool", + ]); const items = top - .map((o) => `- [${o.type}] ${o.title}: ${o.narrative}`) + .map((o) => { + const titleIsGeneric = + !o.title || + o.title === o.type || + GENERIC_TITLES.has(o.title); + const title = titleIsGeneric + ? o.subtitle + ? o.subtitle.slice(0, 80) + : o.narrative + ? o.narrative + .slice(0, 80) + .replace(/\|/g, " ") + .trim() + : o.type + : o.title; + // Avoid title/content duplication when the title was + // derived from the start of the narrative. + let narrative = o.narrative || o.subtitle || ""; + if ( + title && + narrative + .toLowerCase() + .startsWith(title.toLowerCase().slice(0, 40)) + ) { + narrative = narrative + .slice(title.length) + .replace(/^[\s|:,\-]+/, "") + .trim(); + if (!narrative) narrative = o.narrative || ""; + } + return `- [${o.type}] ${title}: ${narrative}`; + }) .join("\n"); const content = `## Session ${sessions[i].id.slice(0, 8)} (${sessions[i].startedAt})\n${items}`; blocks.push({