-
Notifications
You must be signed in to change notification settings - Fork 2
Stabilize memory context output (roadmap Sprint 1) #123
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,7 +16,7 @@ import { disabledPostAnswer, runPostAnswerTurn, runPreAnswerTurn } from "@/lib/s | |
|
|
||
| const namespaceSchema = z.enum(["real_life", "au"]); | ||
| export const latestContextPackInputSchema = z.object({ namespace: namespaceSchema, pack_type: z.enum(["daily", "master"]).optional() }); | ||
| export const memoryContextInputSchema = z.object({ namespace: namespaceSchema, query: z.string().optional(), current_task: z.string().optional(), max_items: z.number().int().positive().max(100).optional(), include_risks: z.boolean().optional(), include_people: z.boolean().optional(), include_projects: z.boolean().optional() }); | ||
| export const memoryContextInputSchema = z.object({ namespace: namespaceSchema, query: z.string().optional(), current_task: z.string().optional(), max_items: z.number().int().positive().max(100).optional(), include_risks: z.boolean().optional(), include_people: z.boolean().optional(), include_projects: z.boolean().optional(), max_payload_chars: z.number().int().positive().max(200000).optional(), debug: z.boolean().optional() }); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Confirm the server registration schema and whether args are passed through untouched.
rg -nP 'registerTool\(\s*"get_memory_context"' -A3 lib/services/pandora-mcp-server.ts
rg -nP 'max_payload_chars|debug' lib/services/pandora-mcp-server.tsRepository: besfeng23/Memory Length of output: 1775 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Inspect the tool implementation and the surrounding validation flow.
ast-grep outline lib/services/pandora-mcp-tools.ts --view expanded
sed -n '1,260p' lib/services/pandora-mcp-tools.ts | cat -n
echo
echo '---'
echo
# Find the exact handler signature and any re-parsing of args.
rg -n "getMemoryContextTool|memoryContextInputSchema|parse\(|safeParse\(|args: unknown|inputSchema" lib/services/pandora-mcp-tools.ts lib/services/pandora-mcp-server.tsRepository: besfeng23/Memory Length of output: 47229 🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n '"`@modelcontextprotocol/sdk`"|mcp' package.json package-lock.json pnpm-lock.yaml yarn.lockRepository: besfeng23/Memory Length of output: 388 🌐 Web query:
💡 Result: In the Model Context Protocol (MCP) TypeScript SDK, the handling of input arguments depends on how you define your Citations:
🌐 Web query:
💡 Result: In the Model Context Protocol (MCP) SDK, handling of "unknown" or additional properties within a tool's Citations:
🌐 Web query:
💡 Result: In the MCP TypeScript SDK (v2), the registerTool method associates a tool with a name, metadata, and a handler [1]. The inputSchema parameter defines the argument structure and handles validation using the Standard Schema interface [2][3]. Key details regarding your query: 1. Input Schema and Validation: The registerTool method expects an inputSchema that conforms to the Standard Schema specification (e.g., Zod, ArkType, or Valibot) [2][3]. This schema is used by the SDK to advertise the tool's capabilities via tools/list and to validate incoming arguments during tools/call [3]. 2. Handling Unknown Keys: The behavior regarding unknown keys in the input depends on the underlying schema library configuration (e.g., Zod) [4]. By default, Zod objects often "strip" unknown keys [4]. To change this behavior: - To reject unknown keys: Use.strict on your Zod schema [4]. - To preserve unknown keys: Use.passthrough on your Zod schema [4]. 3. Handler Arguments: When an inputSchema is provided, the handler is invoked with (args, ctx) [5]. The args object contains the validated and processed parameters [5]. 4. Compatibility: While v2 natively requires a full Standard Schema object (e.g., z.object({...})), recent updates (as of April 2026) added compatibility to automatically wrap "raw" Zod shapes (e.g., { key: z.string }) into a z.object for convenience [6][1]. Important Note on Schema Types: Passing a plain JSON Schema object (instead of a Standard Schema compatible object) can cause the SDK to misinterpret the schema or silently drop it [7]. If you need to use a raw JSON Schema, you must wrap it using a tool like fromJsonSchema (available in Citations:
🌐 Web query:
💡 Result: In the Model Context Protocol (MCP) TypeScript SDK, the Citations:
🌐 Web query:
💡 Result: When working with Zod schemas in the Model Context Protocol (MCP) TypeScript SDK, the handling of unknown keys depends on the Zod configuration and the SDK version [1][2]. Default Zod object validation behavior is to strip unknown keys [1]. If you want your tool handlers or schema validation to behave differently, you can use the following Zod methods: 1..strip: This is the default Zod behavior. Any keys present in the input that are not defined in your schema are removed during validation [1]. 2..passthrough: Use this method if you want to preserve unknown keys in the validated output [1][3]. 3..strict: Use this method if you want validation to fail when unknown keys are present [1]. In the context of the MCP SDK, there has been a transition toward using default (stripping) behavior to improve type safety and consistency, moving away from older SDK versions that heavily utilized.passthrough [2]. If you are encountering issues where arguments are missing or being stripped unexpectedly: - Ensure you are passing the schema object correctly to the tool registration method [4]. - If you need to allow additional parameters that are not explicitly defined in your schema, chain.passthrough to your Zod object definition [1][3]. - When working with Zod v4 compatibility in newer SDK versions, be aware of how internal parsing methods are handled, as the SDK has undergone changes to maintain cross-version compatibility [5][6]. For general Zod parsing (outside of the direct tool handler flow), you can use Zod's.parse or.safeParse methods, which respect the.strip/.passthrough/.strict configuration defined on the schema [7][8]. Citations:
Mirror 🤖 Prompt for AI Agents |
||
| export const captureMemoryEventInputSchema = z.object({ namespace: namespaceSchema, raw_text: z.string().trim().min(1).max(8000), source: z.string().trim().max(120).optional(), source_ref: z.string().trim().max(500).optional(), importance: z.number().int().min(1).max(10).optional(), sensitivity: z.enum(["low", "medium", "high", "private"]).optional() }); | ||
| export const distillContextPackInputSchema = z.object({ namespace: namespaceSchema, pack_type: z.enum(["daily", "master"]) }); | ||
| const runtime = (capture = false, distill = false) => ({ config: { memoryCaptureApiEnabled: capture, memoryContextApiEnabled: true, memoryDistillationEnabled: distill }, gates: { memoryCaptureApiEnabled: { envVar: "PANDORA_ENABLE_MCP_CAPTURE" }, memoryContextApiEnabled: { envVar: "PANDORA_ENABLE_MCP" }, memoryDistillationEnabled: { envVar: "PANDORA_ENABLE_MCP_DISTILLATION" } } }) as never; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| /* eslint-disable @typescript-eslint/no-explicit-any */ | ||
| import { describe, expect, it } from "vitest"; | ||
| import { extractPeopleMentions, compactContextResponse } from "@/lib/services/memory-distillation-service"; | ||
|
|
||
| function ev(id: string, raw_text: string): any { | ||
| return { id, namespace: "au", user_id: "u", source: "chatgpt_user_direct", raw_text, status: "captured", created_by: "u", created_at: "2026-07-03T00:00:00Z" }; | ||
| } | ||
|
|
||
| describe("Sprint 1 — stabilize output (people_map + payload)", () => { | ||
| it("drops junk capitalized sentence-openers and keeps real names", () => { | ||
| const events = [ | ||
| ev("e1", "REINFORCED AU MEMORY RULE. Janine Tan is the character. She is agentic. Mang Jun is raw. Do not link real identity. Keep this. User asked to save."), | ||
| ]; | ||
| const people = extractPeopleMentions(events); | ||
| const names = people.map((p) => p.name); | ||
|
|
||
| expect(names).toContain("Janine Tan"); | ||
| expect(names).toContain("Mang Jun"); | ||
| for (const junk of ["The", "Do", "She", "He", "Keep", "User", "Rule", "Status", "Source"]) { | ||
| expect(names).not.toContain(junk); | ||
| } | ||
| }); | ||
|
|
||
| it("counts each event once per person (no per-occurrence id duplication)", () => { | ||
| const events = [ev("e1", "Janine Tan. Janine Tan. Janine Tan smiled at Janine Tan.")]; | ||
| const [person] = extractPeopleMentions(events); | ||
| expect(person.name).toBe("Janine Tan"); | ||
| expect(person.event_ids).toEqual(["e1"]); | ||
| }); | ||
|
|
||
| it("merges a single-token alias into its full name but keeps distinct aliases separate", () => { | ||
| const events = [ | ||
| ev("e1", "Janine Tan arrived."), | ||
| ev("e2", "Janine waited."), | ||
| ev("e3", "Jana ran off."), | ||
| ]; | ||
| const names = extractPeopleMentions(events).map((p) => p.name); | ||
| expect(names).toContain("Janine Tan"); | ||
| expect(names).not.toContain("Janine"); // merged into "Janine Tan" | ||
| expect(names).toContain("Jana"); // distinct alias kept | ||
| const janine = extractPeopleMentions(events).find((p) => p.name === "Janine Tan")!; | ||
| expect(janine.event_ids.sort()).toEqual(["e1", "e2"]); | ||
| }); | ||
|
|
||
| it("caps people count and event ids per person", () => { | ||
| const firsts = ["Aaron", "Bella", "Cara", "Dana", "Ella", "Faye", "Gina", "Hana", "Iris", "Jane", "Kira", "Lena", "Mona", "Nora", "Opal"]; | ||
| const manyPeople = firsts.map((first, i) => ev(`p${i}`, `${first} Zeta did a thing.`)); | ||
| expect(extractPeopleMentions(manyPeople).length).toBe(12); | ||
|
|
||
| const manyEvents = Array.from({ length: 12 }, (_, i) => ev(`e${i}`, "Janine Tan noted something.")); | ||
| const [person] = extractPeopleMentions(manyEvents); | ||
| expect(person.event_ids.length).toBe(8); | ||
| }); | ||
|
|
||
| it("slims an oversized context response under the payload budget, and debug bypasses it", () => { | ||
| const pack: any = { | ||
| title: "Pandora master context pack", | ||
| summary: "short summary", | ||
| key_points: [], | ||
| active_projects: [], | ||
| people_map: [{ name: "Janine Tan", event_ids: Array.from({ length: 500 }, (_, i) => `event-${i}`), notes: ["a note"] }], | ||
| decisions: [], | ||
| risks: [], | ||
| open_loops: [], | ||
| }; | ||
|
|
||
| const slim = compactContextResponse(pack, [], { max_payload_chars: 3000 }); | ||
| expect(JSON.stringify(slim).length).toBeLessThanOrEqual(3000); | ||
| expect(slim.people_map[0].event_ids.length).toBeLessThanOrEqual(3); | ||
|
|
||
| const full = compactContextResponse(pack, [], { debug: true }); | ||
| expect(JSON.stringify(full).length).toBeGreaterThan(3000); | ||
| expect(full.people_map[0].event_ids.length).toBe(500); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When an active context pack is decision-heavy (or has older persisted
decisionsentries with large text), the newmax_payload_charscap can still be exceeded because slimming truncateskey_points,open_loops,risks, andactive_projectsbut leavesdecisionsunbounded. In that scenarioget_memory_contextcan continue returning oversized payloads despite the new budget, sodecisionsneeds to be capped/trimmed as part of the same slimming pass or the function should re-check and keep trimming until it is actually under budget.Useful? React with 👍 / 👎.