Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,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",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify all hardcoded iii-sdk version pins and compare with package.json
rg -n --type ts 'iii-sdk@' src
cat package.json | jq '.dependencies["iii-sdk"]'

Repository: rohitg00/agentmemory

Length of output: 425


Hardcoded iii-sdk version in CLI overrides declared dependency.

package.json declares iii-sdk as 0.19.0, but src/cli.ts:2312-2322 explicitly runs install iii-sdk@0.11.2. This forces a downgrade or conflict in runtime dependencies.

Update the hardcoded version in src/cli.ts to match the 0.19.0 specified in package.json.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` at line 67, The CLI install path is hardcoding an outdated
iii-sdk version and overriding the dependency declared in package.json. Update
the iii-sdk install/version reference in src/cli.ts within the CLI install logic
to use the same 0.19.0 version already declared, so the runtime dependency stays
consistent. Use the identifiers around the install command in the CLI entry flow
to locate and change the version string.

"zod": "^4.0.0"
},
"optionalDependencies": {
Expand All @@ -76,7 +76,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"
Expand Down
35 changes: 32 additions & 3 deletions src/functions/compress-synthetic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: <narrative>`.
if (
n === "conversation" ||
n === "chat" ||
n === "prompt" ||
n === "message" ||
n === "turn"
) {
return "conversation";
}
const hasWord = (word: string) =>
new RegExp(`(^|_)${word}(_|$)`).test(n) ||
n === word ||
Expand All @@ -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<string, unknown>;
Expand Down Expand Up @@ -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),
Expand Down
55 changes: 53 additions & 2 deletions src/functions/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
Comment on lines 180 to 185

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fallback path is unreachable for title-less observations.

Line 182 requires o.title, so observations with empty titles are filtered out before your new fallback renderer can recover them. That breaks the stated compatibility path for older records.

Proposed fix
 const important = observations.filter(
   (o) =>
-    o.title &&
     o.importance >= 5 &&
     (o.narrative || "").trim().length > 0,
 );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const important = observations.filter(
(o) => o.title && o.importance >= 5,
(o) =>
o.title &&
o.importance >= 5 &&
(o.narrative || "").trim().length > 0,
);
const important = observations.filter(
(o) =>
o.importance >= 5 &&
(o.narrative || "").trim().length > 0,
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/functions/context.ts` around lines 180 - 185, The fallback renderer in
context.ts is unreachable for observations without titles because the important
filter currently requires o.title before the new compatibility path can run.
Update the filtering logic in the observations processing block so title-less
records can still pass through when they meet the other criteria, and keep the
fallback handling in the same context flow so older records are rendered instead
of being dropped.


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);
Comment on lines +207 to +210

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Generic-title detection should normalize before set lookup.

GENERIC_TITLES.has(o.title) is case-sensitive and doesn’t trim, so values like "Conversation" or " conversation " bypass fallback unexpectedly.

Proposed fix
-const titleIsGeneric =
-  !o.title ||
-  o.title === o.type ||
-  GENERIC_TITLES.has(o.title);
+const normalizedTitle = (o.title || "").trim().toLowerCase();
+const titleIsGeneric =
+  !normalizedTitle ||
+  normalizedTitle === o.type ||
+  GENERIC_TITLES.has(normalizedTitle);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const titleIsGeneric =
!o.title ||
o.title === o.type ||
GENERIC_TITLES.has(o.title);
const normalizedTitle = (o.title || "").trim().toLowerCase();
const titleIsGeneric =
!normalizedTitle ||
normalizedTitle === o.type ||
GENERIC_TITLES.has(normalizedTitle);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/functions/context.ts` around lines 207 - 210, The generic-title check in
context.ts is doing a direct GENERIC_TITLES.has(o.title) lookup, so it misses
titles with different casing or extra whitespace; update the title normalization
used in the titleIsGeneric logic before the set lookup. Use the existing title
handling around o.title/o.type to trim and lowercase (or otherwise normalize
consistently) and then compare against GENERIC_TITLES so values like
“Conversation” and “ conversation ” correctly fall back.

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({
Expand Down