@cchez/memory-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DESIGN.md +188 -0
- package/README.md +484 -0
- package/db/.env.example +16 -0
- package/db/docker-compose.yml +33 -0
- package/dist/embedding.js +54 -0
- package/dist/index.js +60 -0
- package/dist/qdrant.js +349 -0
- package/dist/server.js +67 -0
- package/dist/tools/correct.js +57 -0
- package/dist/tools/delete.js +12 -0
- package/dist/tools/episode.js +65 -0
- package/dist/tools/list.js +37 -0
- package/dist/tools/search.js +98 -0
- package/dist/tools/store.js +71 -0
- package/package.json +66 -0
- package/skills/memory-correct/SKILL.md +83 -0
- package/skills/memory-save/SKILL.md +209 -0
- package/skills/memory-search/SKILL.md +156 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { MEMORY_TYPES, storeMemory } from "./store.js";
|
|
3
|
+
const episodeMemorySchema = z.object({
|
|
4
|
+
content: z.string().min(1).describe("从 episode 中提炼出的可复用记忆"),
|
|
5
|
+
memory_type: z.enum(MEMORY_TYPES).describe("该条记忆的类型"),
|
|
6
|
+
tags: z.array(z.string()).optional().describe("该条记忆的 tags"),
|
|
7
|
+
confidence: z
|
|
8
|
+
.number()
|
|
9
|
+
.min(0)
|
|
10
|
+
.max(1)
|
|
11
|
+
.optional()
|
|
12
|
+
.default(0.7)
|
|
13
|
+
.describe("该条记忆可信度,默认 0.7"),
|
|
14
|
+
});
|
|
15
|
+
export const captureEpisodeSchema = z.object({
|
|
16
|
+
episode_id: z.string().min(1).describe("任务/调试 episode ID,由 agent 或调用方生成"),
|
|
17
|
+
source: z.string().min(1).describe("episode 来源,如 agent/claude-code"),
|
|
18
|
+
summary: z
|
|
19
|
+
.string()
|
|
20
|
+
.min(1)
|
|
21
|
+
.describe("episode 摘要:问题、关键尝试、最终结论;不会自动存 raw trace"),
|
|
22
|
+
observations: z
|
|
23
|
+
.array(episodeMemorySchema)
|
|
24
|
+
.min(1)
|
|
25
|
+
.max(10)
|
|
26
|
+
.describe("确认值得长期保存的结构化观察,最多 10 条"),
|
|
27
|
+
related_ids: z.array(z.string()).optional().describe("该 episode 关联的已有记忆 ID"),
|
|
28
|
+
});
|
|
29
|
+
export async function captureEpisodeTool(input) {
|
|
30
|
+
const stored = [];
|
|
31
|
+
const episodeSummary = await storeMemory({
|
|
32
|
+
content: `Episode ${input.episode_id}: ${input.summary}`,
|
|
33
|
+
source: input.source,
|
|
34
|
+
memory_type: "summary",
|
|
35
|
+
tags: ["episode", ...(input.related_ids ?? [])],
|
|
36
|
+
status: "active",
|
|
37
|
+
confidence: 0.7,
|
|
38
|
+
episode_id: input.episode_id,
|
|
39
|
+
related_ids: input.related_ids,
|
|
40
|
+
});
|
|
41
|
+
stored.push({
|
|
42
|
+
id: episodeSummary.id,
|
|
43
|
+
collection: episodeSummary.collection,
|
|
44
|
+
memory_type: "summary",
|
|
45
|
+
});
|
|
46
|
+
for (const observation of input.observations) {
|
|
47
|
+
const result = await storeMemory({
|
|
48
|
+
content: observation.content,
|
|
49
|
+
source: input.source,
|
|
50
|
+
memory_type: observation.memory_type,
|
|
51
|
+
tags: observation.tags,
|
|
52
|
+
status: "active",
|
|
53
|
+
confidence: observation.confidence,
|
|
54
|
+
episode_id: input.episode_id,
|
|
55
|
+
related_ids: input.related_ids,
|
|
56
|
+
last_verified_at: new Date().toISOString(),
|
|
57
|
+
});
|
|
58
|
+
stored.push({
|
|
59
|
+
id: result.id,
|
|
60
|
+
collection: result.collection,
|
|
61
|
+
memory_type: observation.memory_type,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return { episode_id: input.episode_id, stored };
|
|
65
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { listMemories } from "../qdrant.js";
|
|
3
|
+
import { MEMORY_TYPES } from "./store.js";
|
|
4
|
+
export const listMemoriesSchema = z.object({
|
|
5
|
+
collection: z
|
|
6
|
+
.enum(["coding", "workspace"])
|
|
7
|
+
.describe("目标 collection:coding=技术记忆, workspace=团队记忆"),
|
|
8
|
+
limit: z
|
|
9
|
+
.number()
|
|
10
|
+
.int()
|
|
11
|
+
.min(1)
|
|
12
|
+
.max(100)
|
|
13
|
+
.optional()
|
|
14
|
+
.default(20)
|
|
15
|
+
.describe("返回条数,默认 20,最大 100"),
|
|
16
|
+
offset: z
|
|
17
|
+
.number()
|
|
18
|
+
.int()
|
|
19
|
+
.min(0)
|
|
20
|
+
.optional()
|
|
21
|
+
.default(0)
|
|
22
|
+
.describe("分页偏移量"),
|
|
23
|
+
source: z.string().optional().describe("按来源过滤"),
|
|
24
|
+
memory_type: z
|
|
25
|
+
.enum(MEMORY_TYPES)
|
|
26
|
+
.optional()
|
|
27
|
+
.describe("按类型过滤:rule / decision / fact / summary / preference"),
|
|
28
|
+
include_inactive: z
|
|
29
|
+
.boolean()
|
|
30
|
+
.optional()
|
|
31
|
+
.default(false)
|
|
32
|
+
.describe("是否包含 superseded/deprecated 记忆,默认 false"),
|
|
33
|
+
});
|
|
34
|
+
export async function listMemoriesTool(input) {
|
|
35
|
+
const { collection, limit, offset, source, memory_type, include_inactive } = input;
|
|
36
|
+
return listMemories(collection, limit, offset, source, memory_type, include_inactive);
|
|
37
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getEmbeddingProvider } from "../embedding.js";
|
|
3
|
+
import { searchMemory } from "../qdrant.js";
|
|
4
|
+
import { MEMORY_TYPES } from "./store.js";
|
|
5
|
+
const COLLECTION_NAMES = ["coding", "workspace"];
|
|
6
|
+
const SEARCH_MODES = ["vector", "keyword", "hybrid"];
|
|
7
|
+
export const searchMemorySchema = z.object({
|
|
8
|
+
query: z.string().min(1).describe("自然语言搜索查询"),
|
|
9
|
+
collections: z
|
|
10
|
+
.array(z.enum(COLLECTION_NAMES))
|
|
11
|
+
.optional()
|
|
12
|
+
.describe("搜索范围:['coding']仅技术记忆, ['workspace']仅团队记忆, 省略则同时搜两个"),
|
|
13
|
+
limit: z
|
|
14
|
+
.number()
|
|
15
|
+
.int()
|
|
16
|
+
.min(1)
|
|
17
|
+
.max(20)
|
|
18
|
+
.optional()
|
|
19
|
+
.default(5)
|
|
20
|
+
.describe("返回结果数量,默认 5"),
|
|
21
|
+
source: z.string().optional().describe("按来源过滤,如 slack/channel-name"),
|
|
22
|
+
score_threshold: z
|
|
23
|
+
.number()
|
|
24
|
+
.min(0)
|
|
25
|
+
.max(1)
|
|
26
|
+
.optional()
|
|
27
|
+
.describe("最低相似度阈值 (0-1),过滤低相关结果"),
|
|
28
|
+
tags: z
|
|
29
|
+
.array(z.string())
|
|
30
|
+
.optional()
|
|
31
|
+
.describe("按标签过滤,所有指定标签都必须匹配"),
|
|
32
|
+
memory_type: z
|
|
33
|
+
.enum(MEMORY_TYPES)
|
|
34
|
+
.optional()
|
|
35
|
+
.describe("按类型过滤:rule / decision / fact / summary / preference"),
|
|
36
|
+
include_inactive: z
|
|
37
|
+
.boolean()
|
|
38
|
+
.optional()
|
|
39
|
+
.default(false)
|
|
40
|
+
.describe("是否包含 superseded/deprecated 记忆,默认 false"),
|
|
41
|
+
mode: z
|
|
42
|
+
.enum(SEARCH_MODES)
|
|
43
|
+
.optional()
|
|
44
|
+
.default("hybrid")
|
|
45
|
+
.describe("检索模式:vector 仅语义检索,keyword 仅关键词检索,hybrid 混合检索(默认)"),
|
|
46
|
+
use_recency: z
|
|
47
|
+
.boolean()
|
|
48
|
+
.optional()
|
|
49
|
+
.default(true)
|
|
50
|
+
.describe("是否对 fact/summary/preference 应用时间新鲜度加权,默认 true"),
|
|
51
|
+
use_mmr: z
|
|
52
|
+
.boolean()
|
|
53
|
+
.optional()
|
|
54
|
+
.default(true)
|
|
55
|
+
.describe("是否使用 MMR 降低重复结果,默认 true"),
|
|
56
|
+
});
|
|
57
|
+
export async function searchMemoryTool(input) {
|
|
58
|
+
const { query, collections, limit, source, score_threshold, tags, memory_type, include_inactive, mode, use_recency, use_mmr, } = input;
|
|
59
|
+
const cols = collections ?? ["coding", "workspace"];
|
|
60
|
+
const vector = mode === "keyword" ? undefined : await getEmbeddingProvider().embed(query);
|
|
61
|
+
const results = await searchMemory({
|
|
62
|
+
vector,
|
|
63
|
+
query,
|
|
64
|
+
collections: cols,
|
|
65
|
+
limit,
|
|
66
|
+
sourceFilter: source,
|
|
67
|
+
scoreThreshold: score_threshold,
|
|
68
|
+
tagsFilter: tags,
|
|
69
|
+
memoryTypeFilter: memory_type,
|
|
70
|
+
includeInactive: include_inactive,
|
|
71
|
+
mode,
|
|
72
|
+
useRecency: use_recency,
|
|
73
|
+
useMmr: use_mmr,
|
|
74
|
+
});
|
|
75
|
+
return results.map((r) => ({
|
|
76
|
+
id: r.id,
|
|
77
|
+
content: r.content,
|
|
78
|
+
source: r.source,
|
|
79
|
+
memory_type: r.memory_type,
|
|
80
|
+
collection: r.collection,
|
|
81
|
+
tags: r.tags,
|
|
82
|
+
score: r.score,
|
|
83
|
+
vector_score: r.vector_score,
|
|
84
|
+
keyword_score: r.keyword_score,
|
|
85
|
+
recency_score: r.recency_score,
|
|
86
|
+
status: r.status,
|
|
87
|
+
created_at: r.created_at,
|
|
88
|
+
updated_at: r.updated_at,
|
|
89
|
+
supersedes: r.supersedes,
|
|
90
|
+
superseded_by: r.superseded_by,
|
|
91
|
+
correction_reason: r.correction_reason,
|
|
92
|
+
confidence: r.confidence,
|
|
93
|
+
episode_id: r.episode_id,
|
|
94
|
+
related_ids: r.related_ids,
|
|
95
|
+
valid_until: r.valid_until,
|
|
96
|
+
last_verified_at: r.last_verified_at,
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createHash } from "crypto";
|
|
3
|
+
import { getEmbeddingProvider } from "../embedding.js";
|
|
4
|
+
import { upsertMemory, collectionForType, COLLECTIONS } from "../qdrant.js";
|
|
5
|
+
export const MEMORY_TYPES = ["rule", "decision", "fact", "summary", "preference"];
|
|
6
|
+
export const MEMORY_STATUSES = ["active", "superseded", "deprecated"];
|
|
7
|
+
export const storeMemorySchema = z.object({
|
|
8
|
+
content: z.string().min(1).describe("知识内容(摘要或原文)"),
|
|
9
|
+
source: z.string().min(1).describe("来源,如 slack/pod-pay-pilots 或 agent/claude-code"),
|
|
10
|
+
memory_type: z
|
|
11
|
+
.enum(MEMORY_TYPES)
|
|
12
|
+
.describe("记忆类型 — coding collection: rule(编码规则/约束), decision(技术决策), preference(工具偏好); workspace collection: fact(团队事实/状态), summary(Slack/Confluence摘要)"),
|
|
13
|
+
tags: z.array(z.string()).optional().describe("可选标签列表"),
|
|
14
|
+
status: z
|
|
15
|
+
.enum(MEMORY_STATUSES)
|
|
16
|
+
.optional()
|
|
17
|
+
.default("active")
|
|
18
|
+
.describe("记忆生命周期状态,默认 active"),
|
|
19
|
+
confidence: z
|
|
20
|
+
.number()
|
|
21
|
+
.min(0)
|
|
22
|
+
.max(1)
|
|
23
|
+
.optional()
|
|
24
|
+
.describe("记忆可信度 0-1,用于后续审计和重打分"),
|
|
25
|
+
episode_id: z
|
|
26
|
+
.string()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe("产生该记忆的 episode/task ID"),
|
|
29
|
+
related_ids: z
|
|
30
|
+
.array(z.string())
|
|
31
|
+
.optional()
|
|
32
|
+
.describe("相关记忆 ID 列表"),
|
|
33
|
+
supersedes: z
|
|
34
|
+
.string()
|
|
35
|
+
.optional()
|
|
36
|
+
.describe("该记忆修订/替代的旧记忆 ID"),
|
|
37
|
+
correction_reason: z
|
|
38
|
+
.string()
|
|
39
|
+
.optional()
|
|
40
|
+
.describe("修订原因,通常来自用户纠正或事实过期"),
|
|
41
|
+
valid_until: z
|
|
42
|
+
.string()
|
|
43
|
+
.optional()
|
|
44
|
+
.describe("事实类记忆的有效期 ISO 时间"),
|
|
45
|
+
last_verified_at: z
|
|
46
|
+
.string()
|
|
47
|
+
.optional()
|
|
48
|
+
.describe("该记忆最后被验证的 ISO 时间"),
|
|
49
|
+
});
|
|
50
|
+
export async function storeMemory(input) {
|
|
51
|
+
const { content, source, memory_type, tags, status, confidence, episode_id, related_ids, supersedes, correction_reason, valid_until, last_verified_at, } = input;
|
|
52
|
+
const id = createHash("sha256").update(content).digest("hex").slice(0, 32);
|
|
53
|
+
const embedder = getEmbeddingProvider();
|
|
54
|
+
const vector = await embedder.embed(content);
|
|
55
|
+
await upsertMemory(id, vector, {
|
|
56
|
+
content,
|
|
57
|
+
source,
|
|
58
|
+
memory_type,
|
|
59
|
+
tags,
|
|
60
|
+
status,
|
|
61
|
+
confidence,
|
|
62
|
+
episode_id,
|
|
63
|
+
related_ids,
|
|
64
|
+
supersedes,
|
|
65
|
+
correction_reason,
|
|
66
|
+
valid_until,
|
|
67
|
+
last_verified_at,
|
|
68
|
+
created_at: new Date().toISOString(),
|
|
69
|
+
});
|
|
70
|
+
return { id, collection: COLLECTIONS[collectionForType(memory_type)] };
|
|
71
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cchez/memory-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "AI memory MCP server with Qdrant vector storage and Ollama/OpenAI embeddings",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"memory-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/",
|
|
12
|
+
"skills/",
|
|
13
|
+
"db/docker-compose.yml",
|
|
14
|
+
"db/.env.example",
|
|
15
|
+
"README.md",
|
|
16
|
+
"DESIGN.md"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"dev": "node --env-file=.env ./node_modules/tsx/dist/cli.mjs src/index.ts",
|
|
20
|
+
"dev:server": "node --env-file=.env ./node_modules/tsx/dist/cli.mjs src/server.ts",
|
|
21
|
+
"build": "tsc",
|
|
22
|
+
"start": "node dist/index.js",
|
|
23
|
+
"start:server": "node dist/server.js",
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"prepublishOnly": "npm run build",
|
|
26
|
+
"pack:dry-run": "npm pack --dry-run"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public",
|
|
33
|
+
"registry": "https://registry.npmjs.org/"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"mcp",
|
|
37
|
+
"memory",
|
|
38
|
+
"ai-agent",
|
|
39
|
+
"qdrant",
|
|
40
|
+
"ollama",
|
|
41
|
+
"claude"
|
|
42
|
+
],
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "git+https://github.com/miuid/mcps.git",
|
|
46
|
+
"directory": "memory-mcp"
|
|
47
|
+
},
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/miuid/mcps/issues"
|
|
50
|
+
},
|
|
51
|
+
"homepage": "https://github.com/miuid/mcps/tree/main/memory-mcp#readme",
|
|
52
|
+
"license": "UNLICENSED",
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
55
|
+
"@qdrant/js-client-rest": "^1.9.0",
|
|
56
|
+
"express": "^4.18.2",
|
|
57
|
+
"openai": "^4.47.0",
|
|
58
|
+
"zod": "^3.22.4"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@types/express": "^4.17.21",
|
|
62
|
+
"@types/node": "^20.12.0",
|
|
63
|
+
"tsx": "^4.22.4",
|
|
64
|
+
"typescript": "^5.4.4"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: memory-correct
|
|
3
|
+
description: >
|
|
4
|
+
Correct, revise, deprecate, or supersede existing memory mcp records when the
|
|
5
|
+
user says a stored memory is wrong/stale or new evidence contradicts it. Use
|
|
6
|
+
correct_memory to preserve an auditable revision chain instead of deleting and
|
|
7
|
+
re-saving. Do not use for brand-new unrelated memories; use memory-save.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Memory Correct Skill
|
|
11
|
+
|
|
12
|
+
## Purpose
|
|
13
|
+
|
|
14
|
+
Keep memory trustworthy. A stale wrong memory is worse than no memory because
|
|
15
|
+
future agents will confidently apply bad context. Corrections should preserve
|
|
16
|
+
history while making the new truth the default search result.
|
|
17
|
+
|
|
18
|
+
**Announce at start:** "Correcting memory."
|
|
19
|
+
|
|
20
|
+
## When to Trigger
|
|
21
|
+
|
|
22
|
+
Use this skill when:
|
|
23
|
+
|
|
24
|
+
- The user says a retrieved memory is wrong, stale, misleading, or outdated
|
|
25
|
+
- The user says "actually", "that's wrong", "update that memory", or "revise this"
|
|
26
|
+
- New verified evidence contradicts an existing memory
|
|
27
|
+
- A fact/config/rule changed and future searches should prefer the new version
|
|
28
|
+
|
|
29
|
+
Do not use this skill when:
|
|
30
|
+
|
|
31
|
+
- The new information is unrelated to an existing memory
|
|
32
|
+
- The old record contains a secret or sensitive data that must be removed; use `delete_memory`
|
|
33
|
+
- You cannot identify the old memory ID and collection after searching/listing
|
|
34
|
+
|
|
35
|
+
## Step 1 — Find the Old Memory
|
|
36
|
+
|
|
37
|
+
If the old memory ID and collection are already visible in the current result,
|
|
38
|
+
use them. Otherwise call `search_memory` with `include_inactive: true` only if
|
|
39
|
+
you are auditing prior corrections; default active search is enough for normal
|
|
40
|
+
corrections.
|
|
41
|
+
|
|
42
|
+
## Step 2 — Write Replacement Content
|
|
43
|
+
|
|
44
|
+
Write `corrected_content` as a full standalone memory, not a diff.
|
|
45
|
+
|
|
46
|
+
Good:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
Airwallex KYC RFI template ID is kyc_rfi_v3_au as of 2026-06-28. The older
|
|
50
|
+
kyc_rfi_v2_au value is deprecated.
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Bad:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
Actually it is v3.
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Step 3 — Call `correct_memory`
|
|
60
|
+
|
|
61
|
+
Use:
|
|
62
|
+
|
|
63
|
+
- `id`: old memory ID
|
|
64
|
+
- `collection`: old memory collection
|
|
65
|
+
- `corrected_content`: replacement memory
|
|
66
|
+
- `correction_reason`: short reason, e.g. "user corrected stale template ID"
|
|
67
|
+
- `confidence`: `0.9` for user-confirmed corrections; lower if inferred
|
|
68
|
+
|
|
69
|
+
Omit `source`, `memory_type`, and `tags` when the old values are still correct.
|
|
70
|
+
Override them only when the classification itself was wrong.
|
|
71
|
+
|
|
72
|
+
## Step 4 — Confirm Briefly
|
|
73
|
+
|
|
74
|
+
Tell the user the old memory was superseded by the new memory:
|
|
75
|
+
|
|
76
|
+
> "Updated memory. Old ID `[old_id]` is superseded by `[new_id]`."
|
|
77
|
+
|
|
78
|
+
## Self-Check
|
|
79
|
+
|
|
80
|
+
- [ ] Did you identify the old memory ID and collection?
|
|
81
|
+
- [ ] Is the replacement content standalone and future-readable?
|
|
82
|
+
- [ ] Did you use `correct_memory`, not delete + store?
|
|
83
|
+
- [ ] Did you preserve audit history unless deletion was required for safety?
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: memory-save
|
|
3
|
+
description: >
|
|
4
|
+
Save valuable learnings, decisions, rules, or facts to the memory mcp
|
|
5
|
+
knowledge base. Triggers automatically when the agent discovers something
|
|
6
|
+
worth preserving: a coding constraint, an architectural decision, a resolved
|
|
7
|
+
ambiguity, a team process, or a configuration fact. Also triggers when the
|
|
8
|
+
user explicitly asks to save, remember, or store something. Do NOT save
|
|
9
|
+
trivial, transient, or already-known information. If the user corrects or
|
|
10
|
+
invalidates an existing memory, use correct_memory rather than delete + store.
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Memory Save Skill
|
|
14
|
+
|
|
15
|
+
## Purpose
|
|
16
|
+
|
|
17
|
+
Capture knowledge that would otherwise be lost between sessions. The knowledge
|
|
18
|
+
base is only as useful as what gets put into it. This skill enforces quality
|
|
19
|
+
over quantity — save things that are non-obvious, durable, and reusable.
|
|
20
|
+
|
|
21
|
+
**Announce at start:** "Saving to memory."
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## When to Trigger
|
|
26
|
+
|
|
27
|
+
**Automatic triggers (save without being asked):**
|
|
28
|
+
- A coding rule, constraint, or convention is discovered or confirmed
|
|
29
|
+
("methods must stay under complexity 15 or CI fails")
|
|
30
|
+
- An architectural or technology decision is made or ratified
|
|
31
|
+
("we chose Qdrant over pgvector because of Docker-friendliness")
|
|
32
|
+
- A configuration fact is established that future agents will need
|
|
33
|
+
("Airwallex RFI template ID for KYC is ...")
|
|
34
|
+
- An ambiguity is resolved that took effort to figure out
|
|
35
|
+
("the `source` field format is `system/channel-name`")
|
|
36
|
+
- The user says "remember this", "save this", "store this", "note this down"
|
|
37
|
+
|
|
38
|
+
**Correction triggers (use `correct_memory`, not `store_memory`):**
|
|
39
|
+
- The user says a retrieved memory is wrong, outdated, misleading, or no longer true
|
|
40
|
+
- New evidence contradicts an existing memory found during `search_memory`
|
|
41
|
+
- A rule, fact, or decision has changed but the old memory should remain auditable
|
|
42
|
+
- The user says "update that memory", "revise this", "actually X", or "that's wrong"
|
|
43
|
+
|
|
44
|
+
**Do NOT save:**
|
|
45
|
+
- Things already in the knowledge base (check first if unsure)
|
|
46
|
+
- Transient task state ("currently working on PR #123")
|
|
47
|
+
- General knowledge not specific to this project/team
|
|
48
|
+
- Unverified assumptions or guesses
|
|
49
|
+
- Sensitive credentials or secrets
|
|
50
|
+
|
|
51
|
+
If a correction trigger fires, first identify the old memory ID and collection
|
|
52
|
+
from the search/list result, then call `correct_memory` with the replacement
|
|
53
|
+
content and a short `correction_reason`. Do not call `delete_memory` for normal
|
|
54
|
+
corrections; deletion is only for sensitive, duplicated, or intentionally
|
|
55
|
+
removed records.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Step 1 — Assess Save Worthiness
|
|
60
|
+
|
|
61
|
+
Before saving, answer these three questions:
|
|
62
|
+
|
|
63
|
+
1. **Is it durable?** Will this still be true / relevant in 3+ months?
|
|
64
|
+
- Rule: "complexity ≤ 15" → yes
|
|
65
|
+
- Task state: "fixing bug in PR #42" → no
|
|
66
|
+
|
|
67
|
+
2. **Is it non-obvious?** Would a new engineer or agent know this without being told?
|
|
68
|
+
- Config fact: "Airwallex uses hosted flow, not redirect" → yes
|
|
69
|
+
- General fact: "TypeScript supports generics" → no
|
|
70
|
+
|
|
71
|
+
3. **Is it actionable?** Does knowing this change how work gets done?
|
|
72
|
+
- Decision: "use SHA-256 content hash as Qdrant point ID for dedup" → yes
|
|
73
|
+
- Meeting note: "we had standup at 9:30" → no
|
|
74
|
+
|
|
75
|
+
If all three are yes → save. If any is no → skip (or ask user if unsure).
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Step 2 — Classify the Memory
|
|
80
|
+
|
|
81
|
+
Choose the correct `memory_type`:
|
|
82
|
+
|
|
83
|
+
| Type | When to use | Collection |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| `rule` | Coding constraints, CI rules, style enforcements, hard limits | `coding` |
|
|
86
|
+
| `decision` | Architecture choices, technology selections, design tradeoffs | `coding` |
|
|
87
|
+
| `preference` | Team/user preferences, conventions, soft defaults | `coding` |
|
|
88
|
+
| `fact` | Current state, configuration values, team facts, integration details | `workspace` |
|
|
89
|
+
| `summary` | Distilled summaries of longer discussions, Slack threads, meetings | `workspace` |
|
|
90
|
+
|
|
91
|
+
The collection is auto-routed by `memory_type` — you don't need to specify it.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Step 3 — Write Good Content
|
|
96
|
+
|
|
97
|
+
The `content` field is what gets embedded and searched. Write it for future
|
|
98
|
+
retrieval, not for the current moment.
|
|
99
|
+
|
|
100
|
+
**Good content:**
|
|
101
|
+
- Starts with the key fact/rule/decision upfront
|
|
102
|
+
- Includes enough context to be understood standalone (no "as mentioned above")
|
|
103
|
+
- Uses specific names, numbers, and system names where relevant
|
|
104
|
+
- Is 1–5 sentences: dense but not padded
|
|
105
|
+
|
|
106
|
+
**Bad content:**
|
|
107
|
+
- "We decided X" (vague — decided what exactly, and why?)
|
|
108
|
+
- A dump of raw Slack messages (noise drowns signal)
|
|
109
|
+
- A single word or phrase (not enough context for semantic search)
|
|
110
|
+
|
|
111
|
+
**Template by type:**
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
rule:
|
|
115
|
+
"[Rule statement]. Applies to [scope]. Reason: [why this rule exists].
|
|
116
|
+
Consequence of violation: [what breaks]."
|
|
117
|
+
|
|
118
|
+
decision:
|
|
119
|
+
"Decision: [what was decided]. Context: [why this decision was needed].
|
|
120
|
+
Rationale: [key reason(s)]. Alternatives rejected: [if relevant]."
|
|
121
|
+
|
|
122
|
+
fact:
|
|
123
|
+
"[System/entity]: [specific fact]. As of [date if time-sensitive].
|
|
124
|
+
Source: [where this came from]."
|
|
125
|
+
|
|
126
|
+
summary:
|
|
127
|
+
"[Topic] discussion summary ([date/channel]):
|
|
128
|
+
Key points: [1-3 bullets]. Decisions/actions: [if any]."
|
|
129
|
+
|
|
130
|
+
preference:
|
|
131
|
+
"[Team/user] prefers [specific preference] for [context].
|
|
132
|
+
Reason: [if known]."
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Step 4 — Choose Tags
|
|
138
|
+
|
|
139
|
+
Tags are optional but improve filtered retrieval. Pick 2–5 specific tags:
|
|
140
|
+
|
|
141
|
+
- Use existing tag vocabulary if you've seen tags in prior search results
|
|
142
|
+
- Prefer specific over generic: `rfi-webhook` over `webhook`
|
|
143
|
+
- Include: system names, feature names, ticket IDs, technology names
|
|
144
|
+
- Always include the domain: `payments`, `onboarding`, `billing`, etc.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Step 5 — Execute Save
|
|
149
|
+
|
|
150
|
+
If this is correcting an existing memory, skip this step and use Step 5b.
|
|
151
|
+
|
|
152
|
+
Call `store_memory` with:
|
|
153
|
+
- `content`: written per Step 3 template
|
|
154
|
+
- `source`: `agent/claude-code` for agent-discovered learnings;
|
|
155
|
+
`user/manual` for user-requested saves;
|
|
156
|
+
`slack/<channel>` / `confluence/<page>` for external source ingestion
|
|
157
|
+
- `memory_type`: from Step 2
|
|
158
|
+
- `tags`: from Step 4
|
|
159
|
+
|
|
160
|
+
The tool returns `{ id, collection }`. Note which collection it went to —
|
|
161
|
+
confirm it matches the expected routing from Step 2.
|
|
162
|
+
|
|
163
|
+
**If `store_memory` returns the same ID as a previous save**, the content was
|
|
164
|
+
identical — this is a silent dedup (idempotent upsert). That's correct behaviour.
|
|
165
|
+
|
|
166
|
+
## Step 5b — Execute Correction
|
|
167
|
+
|
|
168
|
+
Call `correct_memory` when an old memory should be revised but kept auditable:
|
|
169
|
+
|
|
170
|
+
- `id`: old memory ID
|
|
171
|
+
- `collection`: old memory collection (`coding` or `workspace`)
|
|
172
|
+
- `corrected_content`: full replacement memory, standalone and future-readable
|
|
173
|
+
- `correction_reason`: short reason, e.g. "user corrected stale config value"
|
|
174
|
+
- `memory_type`, `source`, `tags`: optional; omit to inherit from old memory
|
|
175
|
+
- `confidence`: use `0.9` for user-confirmed corrections; lower it if inferred
|
|
176
|
+
|
|
177
|
+
`correct_memory` writes a new active memory and marks the old one as
|
|
178
|
+
`superseded`. Future searches hide the superseded memory by default; audit flows
|
|
179
|
+
can pass `include_inactive: true`.
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Step 6 — Confirm to User
|
|
184
|
+
|
|
185
|
+
For **automatic saves** (agent-initiated), confirm briefly:
|
|
186
|
+
|
|
187
|
+
> "Saved to memory [collection/type]: [one-line summary of what was saved]."
|
|
188
|
+
|
|
189
|
+
For **manual saves** (user asked), confirm with the ID in case they need to delete it later:
|
|
190
|
+
|
|
191
|
+
> "Saved. ID: `[id]`, collection: `[collection]`, type: `[memory_type]`."
|
|
192
|
+
|
|
193
|
+
For **corrections**, confirm both sides of the revision chain:
|
|
194
|
+
|
|
195
|
+
> "Updated memory. Old ID `[old_id]` is superseded by `[new_id]`."
|
|
196
|
+
|
|
197
|
+
Do not ask for confirmation before saving — act, then report. If the user
|
|
198
|
+
wants to remove it, they can use `delete_memory(id, collection)`.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Self-Check Before Finishing
|
|
203
|
+
|
|
204
|
+
- [ ] Did the content pass all three save-worthiness tests (durable, non-obvious, actionable)?
|
|
205
|
+
- [ ] Is `memory_type` correctly classified (not just "summary" for everything)?
|
|
206
|
+
- [ ] Is `content` written for future retrieval (standalone, specific, dense)?
|
|
207
|
+
- [ ] If this revised an existing memory, did you use `correct_memory` instead of delete + store?
|
|
208
|
+
- [ ] Was the save confirmed to the user?
|
|
209
|
+
- [ ] For automatic saves: did you save silently during work rather than interrupting flow?
|