From da42624ad46a58b853e6f76701cc70518dd04720 Mon Sep 17 00:00:00 2001 From: Soham Bafana Date: Mon, 3 Aug 2026 01:30:52 -0400 Subject: [PATCH 1/2] Clarify agent contact memory lifecycle --- skills/cued/SKILL.md | 22 ++++++++++++++++++---- src/cli-contacts-memory.test.ts | 22 ++++++++++++++++++++++ src/cli.ts | 21 ++++++++++++--------- 3 files changed, 52 insertions(+), 13 deletions(-) diff --git a/skills/cued/SKILL.md b/skills/cued/SKILL.md index 0758d445..6e6d6d99 100644 --- a/skills/cued/SKILL.md +++ b/skills/cued/SKILL.md @@ -116,9 +116,14 @@ Add a memory: cued contacts memory add contact-id-here "Works on applied AI; likely useful for Cued enrichment feedback." --source local_messages --confidence 90 --evidence '{"message_ids":["message-id-here"]}' ``` -List current memories: +Load current memories before answering about a person or writing new memory: ```bash -cued contacts memory list contact-id-here --limit 20 +cued contacts memory show contact-id-here --limit 50 +``` + +Load the full history before reconciling new evidence, including invalidated memories: +```bash +cued contacts memory show contact-id-here --all ``` Replace stale or incorrect information: @@ -126,11 +131,20 @@ Replace stale or incorrect information: cued contacts memory add contact-id-here "Now works at ExampleCo, based on LinkedIn profile and recent messages." --source linkedin --confidence 95 --evidence '{"urls":["https://www.linkedin.com/in/example"],"message_ids":["message-id-here"]}' --supersedes memory-id-here ``` -Mark a memory stale without replacement: +Invalidate a memory without replacement: ```bash -cued contacts memory stale memory-id-here +cued contacts memory invalidate memory-id-here ``` +Whenever new evidence could change an existing memory, reconcile before writing: +1. Load the full memory history with `cued contacts memory show --all`. +2. Leave supported current memories unchanged; a newer source alone does not make a memory stale. +3. Add genuinely new, non-conflicting facts once with evidence. +4. Replace a contradicted current memory atomically with `memory add ... --supersedes `. +5. Use `memory invalidate ` only when evidence disproves or makes the memory unusable and there is no supported replacement. + +Never create a second memory that restates a current one. Never invalidate a memory merely because the latest search did not mention it, and never revive an invalidated memory without new supporting evidence. + Before writing a memory, verify at least one of: - deterministic handle evidence: LinkedIn profile URL/id, email, phone, or platform id; - strong DM history with the contact; diff --git a/src/cli-contacts-memory.test.ts b/src/cli-contacts-memory.test.ts index ace1d835..395a042d 100644 --- a/src/cli-contacts-memory.test.ts +++ b/src/cli-contacts-memory.test.ts @@ -126,6 +126,28 @@ describe("contacts memory CLI", () => { ).toThrow(); }); + it("shows the full memory history and invalidates a selected memory", () => { + const home = createHome(); + const memory = JSON.parse( + runCli(home, ["contacts", "memory", "add", "contact-1", "No longer current"]), + ) as { id: string }; + + const invalidated = JSON.parse( + runCli(home, ["contacts", "memory", "invalidate", memory.id]), + ) as { id: string; stale_at: number | null }; + expect(invalidated).toMatchObject({ id: memory.id, stale_at: expect.any(Number) }); + + const current = JSON.parse(runCli(home, ["contacts", "memory", "show", "contact-1"])) as Array<{ + id: string; + }>; + const all = JSON.parse( + runCli(home, ["contacts", "memory", "show", "contact-1", "--all"]), + ) as Array<{ id: string }>; + + expect(current).toEqual([]); + expect(all.map((row) => row.id)).toEqual([memory.id]); + }); + it("dry-runs contact merge batches from a JSON file", () => { const home = createHome(); insertContact(home, "contact-2", "Ava Duplicate"); diff --git a/src/cli.ts b/src/cli.ts index d59d59ea..2d9fd4fe 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -156,8 +156,8 @@ Usage: cued contacts merge [--reason TEXT] cued contacts merge-batch [--apply] cued contacts memory add [--source SOURCE] [--confidence 0-100] [--evidence JSON] [--supersedes MEMORY_ID] - cued contacts memory list [--limit N] [--include-stale] - cued contacts memory stale + cued contacts memory show [--all] [--limit N] + cued contacts memory invalidate cued attachments list [--message ID] [--conversation ID] [--platform PLATFORM] [--account ACCOUNT] [--limit N] cued attachments fetch [--variant original] [--max-bytes N] [--allow-large] [--no-extract] cued attachments search [--conversation ID] [--platform PLATFORM] [--account ACCOUNT] [--limit N] @@ -1193,33 +1193,36 @@ async function main(): Promise { ); return; } - case "list": { + case "list": + case "show": { const contactId = args[0]; if (!contactId) { throw new Error( - "Usage: cued contacts memory list [--limit N] [--include-stale]", + "Usage: cued contacts memory show [--all] [--limit N]", ); } + const includeAll = args.includes("--all") || args.includes("--include-stale"); printJson( db.listContactMemories({ contactId, - limit: parseIntegerFlag(args, "--limit"), - includeStale: args.includes("--include-stale"), + limit: parseIntegerFlag(args, "--limit") ?? (includeAll ? 500 : undefined), + includeStale: includeAll, }), ); return; } + case "invalidate": case "stale": { const memoryId = args[0]; if (!memoryId) { - throw new Error("Usage: cued contacts memory stale "); + throw new Error("Usage: cued contacts memory invalidate "); } printJson(db.markContactMemoryStale(memoryId)); return; } default: throw new Error( - "Usage: cued contacts memory add [--source SOURCE] [--confidence 0-100] [--evidence JSON] [--supersedes MEMORY_ID] | cued contacts memory list [--limit N] [--include-stale] | cued contacts memory stale ", + "Usage: cued contacts memory add [--source SOURCE] [--confidence 0-100] [--evidence JSON] [--supersedes MEMORY_ID] | cued contacts memory show [--all] [--limit N] | cued contacts memory invalidate ", ); } } finally { @@ -1256,7 +1259,7 @@ async function main(): Promise { } if (subcommand !== "merge" || !rest[0] || !rest[1]) { throw new Error( - "Usage: cued contacts merge [--reason TEXT] | cued contacts merge-batch [--apply] | cued contacts memory add|list|stale ...", + "Usage: cued contacts merge [--reason TEXT] | cued contacts merge-batch [--apply] | cued contacts memory add|show|invalidate ...", ); } response = await sendDaemonRequest({ From ca185e8c2d640d3c0640b58d97e8f97c629cedd3 Mon Sep 17 00:00:00 2001 From: Soham Bafana Date: Mon, 3 Aug 2026 01:38:10 -0400 Subject: [PATCH 2/2] Make memory reads exhaustive for agents --- skills/cued/agents/openai.yaml | 2 +- skills/cued/evals/contact-memories.md | 21 +++++++++++++++------ src/cli-contacts-memory.test.ts | 17 +++++++++++++++++ src/cli.ts | 4 +++- src/db/database.ts | 7 +++++-- 5 files changed, 41 insertions(+), 10 deletions(-) diff --git a/skills/cued/agents/openai.yaml b/skills/cued/agents/openai.yaml index e8102987..1658716b 100644 --- a/skills/cued/agents/openai.yaml +++ b/skills/cued/agents/openai.yaml @@ -1,7 +1,7 @@ interface: display_name: "Cued" short_description: "Inspect Cued through the local CLI and manage contact memories." - default_prompt: "Use $cued to query Cued through `cued sql` for reads; do not open ~/.cued/local.db directly with sqlite3 because the DB may be encrypted. Use cued contacts memory add/list/stale for successful useful contact memories." + default_prompt: "Use $cued to query Cued through `cued sql` for reads; do not open ~/.cued/local.db directly with sqlite3 because the DB may be encrypted. Load memories with cued contacts memory show before adding evidence-backed memories, and invalidate only memories disproved by new evidence." policy: allow_implicit_invocation: true diff --git a/skills/cued/evals/contact-memories.md b/skills/cued/evals/contact-memories.md index d1b4c5ea..e795da73 100644 --- a/skills/cued/evals/contact-memories.md +++ b/skills/cued/evals/contact-memories.md @@ -8,7 +8,7 @@ These evals are lightweight checks for agents using `$cued` to enrich contacts a - Requires deterministic identity evidence or strong local interaction evidence before writing. - Stores compact natural-language `body` plus structured `evidence_json`. - Does not update canonical profile fields from weak evidence. -- Uses `--supersedes` or `cued contacts memory stale` for incorrect or outdated memories. +- Uses `--supersedes` or `cued contacts memory invalidate` for incorrect or outdated memories. - Treats `no_write` as success for weak, bot, duplicate, or ambiguous contacts. ## Positive Cases @@ -107,15 +107,24 @@ Expected behavior: ## Supersede Case -1. Write a memory with local evidence. -2. Write a replacement memory using `--supersedes `. -3. Verify the old memory no longer appears in the default list and appears with `--include-stale`. +1. Load the full history with `memory show --all`. +2. Write a memory with local evidence. +3. Write a replacement memory using `--supersedes `. +4. Verify the old memory no longer appears in the current view and appears with `--all`. ```bash -cued contacts memory list contact-id-here -cued contacts memory list contact-id-here --include-stale +cued contacts memory show contact-id-here +cued contacts memory show contact-id-here --all ``` +## Omitted Evidence Case + +1. Load a supported current memory and its evidence. +2. Inspect a newer verified source that does not mention the claim and does not contradict it. +3. Verify the agent leaves the memory current and writes no duplicate. + +Expected behavior: absence from the newer source is not invalidation evidence. The agent uses `invalidate` only when evidence disproves the claim or makes it unusable. + ## Web Enrichment Cases Profile graph enrichment: diff --git a/src/cli-contacts-memory.test.ts b/src/cli-contacts-memory.test.ts index 395a042d..d001a2cd 100644 --- a/src/cli-contacts-memory.test.ts +++ b/src/cli-contacts-memory.test.ts @@ -148,6 +148,23 @@ describe("contacts memory CLI", () => { expect(all.map((row) => row.id)).toEqual([memory.id]); }); + it("does not truncate exhaustive memory reads", () => { + const home = createHome(); + const db = new CuedDatabase(join(home, "local.db")); + try { + for (let index = 0; index < 501; index += 1) { + db.addContactMemory({ contactId: "contact-1", body: `Memory ${index}` }); + } + } finally { + db.close(); + } + + const all = JSON.parse( + runCli(home, ["contacts", "memory", "show", "contact-1", "--all"]), + ) as Array<{ id: string }>; + expect(all).toHaveLength(501); + }); + it("dry-runs contact merge batches from a JSON file", () => { const home = createHome(); insertContact(home, "contact-2", "Ava Duplicate"); diff --git a/src/cli.ts b/src/cli.ts index 2d9fd4fe..f8ca0fd8 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1202,10 +1202,12 @@ async function main(): Promise { ); } const includeAll = args.includes("--all") || args.includes("--include-stale"); + const explicitLimit = parseIntegerFlag(args, "--limit"); printJson( db.listContactMemories({ contactId, - limit: parseIntegerFlag(args, "--limit") ?? (includeAll ? 500 : undefined), + limit: explicitLimit, + all: memoryCommand === "show" && explicitLimit === undefined, includeStale: includeAll, }), ); diff --git a/src/db/database.ts b/src/db/database.ts index c4cc5c95..e699a108 100644 --- a/src/db/database.ts +++ b/src/db/database.ts @@ -2283,6 +2283,7 @@ export class CuedDatabase { contactId: string; includeStale?: boolean; limit?: number; + all?: boolean; }): ContactMemoryRow[] { const contactId = input.contactId.trim(); if (!contactId) { @@ -2297,7 +2298,9 @@ export class CuedDatabase { if (!input.includeStale) { filters.push("cn.stale_at IS NULL"); } - params.push(limit); + if (!input.all) { + params.push(limit); + } return this.sqlite .prepare( @@ -2319,7 +2322,7 @@ export class CuedDatabase { LEFT JOIN contacts c ON c.id = cn.contact_id WHERE ${filters.join(" AND ")} ORDER BY cn.created_at DESC - LIMIT ? + ${input.all ? "" : "LIMIT ?"} `, ) .all(...params) as ContactMemoryRow[];