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
22 changes: 18 additions & 4 deletions skills/cued/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,21 +116,35 @@ 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:
```bash
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 <contact-id> --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 <memory-id>`.
5. Use `memory invalidate <memory-id>` 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;
Expand Down
2 changes: 1 addition & 1 deletion skills/cued/agents/openai.yaml
Original file line number Diff line number Diff line change
@@ -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
21 changes: 15 additions & 6 deletions skills/cued/evals/contact-memories.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -107,15 +107,24 @@ Expected behavior:

## Supersede Case

1. Write a memory with local evidence.
2. Write a replacement memory using `--supersedes <old-memory-id>`.
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 <contact-id> --all`.
2. Write a memory with local evidence.
3. Write a replacement memory using `--supersedes <old-memory-id>`.
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:
Expand Down
39 changes: 39 additions & 0 deletions src/cli-contacts-memory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,45 @@ 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("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");
Expand Down
23 changes: 14 additions & 9 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,8 @@ Usage:
cued contacts merge <primary-contact-id> <secondary-contact-id> [--reason TEXT]
cued contacts merge-batch <merges.json> [--apply]
cued contacts memory add <contact-id> <memory> [--source SOURCE] [--confidence 0-100] [--evidence JSON] [--supersedes MEMORY_ID]
cued contacts memory list <contact-id> [--limit N] [--include-stale]
cued contacts memory stale <memory-id>
cued contacts memory show <contact-id> [--all] [--limit N]
cued contacts memory invalidate <memory-id>
cued attachments list [--message ID] [--conversation ID] [--platform PLATFORM] [--account ACCOUNT] [--limit N]
cued attachments fetch <attachment-id> [--variant original] [--max-bytes N] [--allow-large] [--no-extract]
cued attachments search <query> [--conversation ID] [--platform PLATFORM] [--account ACCOUNT] [--limit N]
Expand Down Expand Up @@ -1193,33 +1193,38 @@ async function main(): Promise<void> {
);
return;
}
case "list": {
case "list":
case "show": {
const contactId = args[0];
if (!contactId) {
throw new Error(
"Usage: cued contacts memory list <contact-id> [--limit N] [--include-stale]",
"Usage: cued contacts memory show <contact-id> [--all] [--limit N]",
);
}
const includeAll = args.includes("--all") || args.includes("--include-stale");
const explicitLimit = parseIntegerFlag(args, "--limit");
printJson(
db.listContactMemories({
contactId,
limit: parseIntegerFlag(args, "--limit"),
includeStale: args.includes("--include-stale"),
limit: explicitLimit,
all: memoryCommand === "show" && explicitLimit === undefined,
includeStale: includeAll,
}),
);
return;
}
case "invalidate":
case "stale": {
const memoryId = args[0];
if (!memoryId) {
throw new Error("Usage: cued contacts memory stale <memory-id>");
throw new Error("Usage: cued contacts memory invalidate <memory-id>");
}
printJson(db.markContactMemoryStale(memoryId));
return;
}
default:
throw new Error(
"Usage: cued contacts memory add <contact-id> <memory> [--source SOURCE] [--confidence 0-100] [--evidence JSON] [--supersedes MEMORY_ID] | cued contacts memory list <contact-id> [--limit N] [--include-stale] | cued contacts memory stale <memory-id>",
"Usage: cued contacts memory add <contact-id> <memory> [--source SOURCE] [--confidence 0-100] [--evidence JSON] [--supersedes MEMORY_ID] | cued contacts memory show <contact-id> [--all] [--limit N] | cued contacts memory invalidate <memory-id>",
);
}
} finally {
Expand Down Expand Up @@ -1256,7 +1261,7 @@ async function main(): Promise<void> {
}
if (subcommand !== "merge" || !rest[0] || !rest[1]) {
throw new Error(
"Usage: cued contacts merge <primary-contact-id> <secondary-contact-id> [--reason TEXT] | cued contacts merge-batch <merges.json> [--apply] | cued contacts memory add|list|stale ...",
"Usage: cued contacts merge <primary-contact-id> <secondary-contact-id> [--reason TEXT] | cued contacts merge-batch <merges.json> [--apply] | cued contacts memory add|show|invalidate ...",
);
}
response = await sendDaemonRequest({
Expand Down
7 changes: 5 additions & 2 deletions src/db/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2283,6 +2283,7 @@ export class CuedDatabase {
contactId: string;
includeStale?: boolean;
limit?: number;
all?: boolean;
}): ContactMemoryRow[] {
const contactId = input.contactId.trim();
if (!contactId) {
Expand All @@ -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(
Expand All @@ -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[];
Expand Down
Loading