fix(integrations): reserve our own slot in the client stack, and drop whole entries
Two review findings on this PR. All three client-stack implementations appended our entry and then trimmed to four, so whenever a caller already sent four entries the one dropped was exactly the one the function exists to add. We vanished from our own stack while every caller claim survived. The character cap was worse: slicing the joined string severs an identifier, and the platform parses the fragment as a real client, so a truncated tail arrives as a client literally named "me". Both caps now drop whole entries and the reserved slot is ours, in the Python SDK, the TypeScript SDK and pi-agent. mcp-server has the same fix on the platform branch. The deepseek comment claimed the backend's allowlist recognizes DEEPSEEK_HARNESS. This PR introduced that wording, replacing a neutral one. It is not true until mem0ai/platform#3602 ships, so it now states the dependency. Two pi-agent tests: our entry survives a full caller stack, and every surviving entry is whole rather than a severed tail. Python side verified directly, a 4-entry caller stack keeps mem0-python and long entries are dropped whole. 312 passed 8 skipped, pi-agent 96, bundles clean, TS SDK builds. Claude-Session: https://claude.ai/code/session_01C7tEmH86HAr7GoAAKCEHZb
This commit is contained in:
@@ -26,8 +26,9 @@ export const name = "mem0";
|
||||
export const inject = ["tools", "systemPrompt"];
|
||||
|
||||
// Tags writes so Mem0's backend attributes them to this integration in
|
||||
// telemetry. The backend's KNOWN_EVENT_SOURCES allowlist recognizes this value;
|
||||
// anything outside it buckets into "OTHERS".
|
||||
// telemetry. Values outside the backend's KNOWN_EVENT_SOURCES allowlist bucket
|
||||
// into "OTHERS"; this one is added by mem0ai/platform#3602 and reads as OTHERS
|
||||
// until that ships.
|
||||
const SOURCE = "DEEPSEEK_HARNESS";
|
||||
|
||||
const DEFAULT_SEARCH_LIMIT = 10;
|
||||
|
||||
@@ -51,3 +51,30 @@ describe("applySurfaceHeaders", () => {
|
||||
expect(headers["X-Mem0-Client"].length).toBeLessThanOrEqual(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe("client stack bounding", () => {
|
||||
it("keeps our own entry when the caller already filled the stack", () => {
|
||||
// The defect: pushing then trimming to four dropped exactly the entry this
|
||||
// function exists to add, so we vanished from our own stack.
|
||||
const mem0 = client({ "X-Mem0-Client": "a/1, b/2, c/3, d/4" });
|
||||
applySurfaceHeaders(mem0);
|
||||
const stack = (mem0 as unknown as { headers: Record<string, string> }).headers["X-Mem0-Client"];
|
||||
|
||||
expect(stack).toMatch(/mem0-pi-agent\//);
|
||||
expect(stack.split(",").length).toBeLessThanOrEqual(4);
|
||||
});
|
||||
|
||||
it("drops whole entries at the character cap, never a fragment", () => {
|
||||
const long = `${"n".repeat(90)}/1.0, ${"m".repeat(90)}/1.0, ${"o".repeat(90)}/1.0`;
|
||||
const mem0 = client({ "X-Mem0-Client": long });
|
||||
applySurfaceHeaders(mem0);
|
||||
const stack = (mem0 as unknown as { headers: Record<string, string> }).headers["X-Mem0-Client"];
|
||||
|
||||
expect(stack.length).toBeLessThanOrEqual(200);
|
||||
expect(stack.endsWith("/0.0.0") || /mem0-pi-agent\/[\w.\-]+$/.test(stack)).toBe(true);
|
||||
// Every surviving entry is whole: name/version, no severed tail.
|
||||
for (const entry of stack.split(",")) {
|
||||
expect(entry.trim()).toMatch(/^[^/]+\/[^/]+$/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,7 +18,26 @@ const PLUGIN_VERSION = (() => {
|
||||
})();
|
||||
|
||||
const MAX_STACK_ENTRIES = 4;
|
||||
const MAX_HEADER_CHARS = 200;
|
||||
const MAX_STACK_CHARS = 200;
|
||||
|
||||
/**
|
||||
* Append our own entry and bound the result, dropping WHOLE entries.
|
||||
*
|
||||
* Neither cap cuts characters: slicing the joined string severs an identifier
|
||||
* and leaves a fragment the platform parses as a real client name. And the
|
||||
* reserved slot is ours, since it is the only entry this layer can vouch for.
|
||||
*/
|
||||
function boundedStack(callerEntries: string[], own: string): string {
|
||||
const kept: string[] = [];
|
||||
let budget = MAX_STACK_CHARS - own.length;
|
||||
for (const entry of callerEntries.slice(0, MAX_STACK_ENTRIES - 1)) {
|
||||
const cost = entry.length + ", ".length;
|
||||
if (cost > budget) break;
|
||||
budget -= cost;
|
||||
kept.push(entry);
|
||||
}
|
||||
return [...kept, own].join(", ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Stamp surface identity onto the shared client, once, at construction.
|
||||
@@ -42,9 +61,5 @@ export function applySurfaceHeaders(client: MemoryClient): void {
|
||||
.split(",")
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean);
|
||||
existing.push(`mem0-pi-agent/${PLUGIN_VERSION}`);
|
||||
headers["X-Mem0-Client"] = existing
|
||||
.slice(0, MAX_STACK_ENTRIES)
|
||||
.join(", ")
|
||||
.slice(0, MAX_HEADER_CHARS);
|
||||
headers["X-Mem0-Client"] = boundedStack(existing, `mem0-pi-agent/${PLUGIN_VERSION}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user