@luisarg/memory-auto 0.1.2 → 0.1.4
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/README.md +19 -10
- package/dist/index.d.ts +4 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +92 -30
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/server/launcher.mjs +5 -1
- package/server/rebuild_index.py +191 -0
- package/server/server.py +107 -24
- package/server/store.py +14 -8
- package/server/test_get_profile.py +78 -0
package/README.md
CHANGED
|
@@ -38,16 +38,25 @@ Environment variables used at load time: `DSH_MEMORY_PATH`,
|
|
|
38
38
|
|
|
39
39
|
## Behavior
|
|
40
40
|
|
|
41
|
-
- `session
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
- `
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
41
|
+
- `session/created` — registers the session and resolves the project name
|
|
42
|
+
(`openspec/` present → directory basename; else `package.json` name; else
|
|
43
|
+
`pyproject.toml` `[project].name`; else first `#` heading of `README.md`;
|
|
44
|
+
else directory basename).
|
|
45
|
+
- `session/disposed` — digests the transcript, skipping sessions with no
|
|
46
|
+
activity. A dispose-time `ctx.effect` batch-digests any session still
|
|
47
|
+
pending, using the tracked activity summary as a stand-in transcript.
|
|
48
|
+
- `agent/status` with status `idle` — runs the auto-capture gate
|
|
49
|
+
(`idleCheckpoint`): skips sessions without activity or already delivered, and
|
|
50
|
+
on the first idle of a session also digests the tracked activity summary.
|
|
51
|
+
- `session/event` — tracks activity and queues checkpoints:
|
|
52
|
+
- `tool/call` (also accepted as `tool_call`) whose `args.command` matches
|
|
53
|
+
`git commit` queues a commit checkpoint.
|
|
54
|
+
- `compaction/start` queues a pre-compaction checkpoint (fires whenever there
|
|
55
|
+
is activity, even if one was already delivered).
|
|
56
|
+
- `user/message` and `assistant/message` are recorded as tracked activity.
|
|
57
|
+
- `agent/pre-step` — delivers the queued checkpoint by pushing it onto
|
|
58
|
+
`payload.context` (or `payload.messages`), and the agent writes entries with
|
|
59
|
+
the `store_*` MCP tools.
|
|
51
60
|
|
|
52
61
|
Extraction failures are retried with bounded backoff and logged; a failed
|
|
53
62
|
digest never takes the agent down.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import Schema from "@deepseek-ai/schemastery";
|
|
2
2
|
import { Context } from "@deepseek-ai/cordis";
|
|
3
|
-
|
|
4
3
|
//#region src/plugin.d.ts
|
|
5
|
-
declare const name = "memory-auto";
|
|
6
|
-
interface Config {
|
|
4
|
+
export declare const name = "memory-auto";
|
|
5
|
+
export interface Config {
|
|
7
6
|
memoryPath: string;
|
|
8
7
|
serverDir: string;
|
|
9
8
|
provider: string;
|
|
@@ -12,8 +11,7 @@ interface Config {
|
|
|
12
11
|
minTranscriptChars: number;
|
|
13
12
|
enabled: boolean;
|
|
14
13
|
}
|
|
15
|
-
declare const Config: Schema<Config>;
|
|
16
|
-
declare function apply(ctx: Context, config: Config): void;
|
|
14
|
+
export declare const Config: Schema<Config>;
|
|
15
|
+
export declare function apply(ctx: Context, config: Config): void;
|
|
17
16
|
//#endregion
|
|
18
|
-
export { Config, apply, name };
|
|
19
17
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/plugin.ts"],"
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/plugin.ts"],"mappings":";;;qBAgCa;iBAEI;EACf;EACA;EACA;EACA;EACA;EACA;EACA;;qBAGW,QAAQ,OAAO;wBAuDZ,MAAM,KAAK,SAAS,QAAQ"}
|
package/dist/index.js
CHANGED
|
@@ -7,10 +7,54 @@ import { readFile } from "node:fs/promises";
|
|
|
7
7
|
import { spawn } from "node:child_process";
|
|
8
8
|
import readline from "node:readline";
|
|
9
9
|
import { BlockAssembler, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
10
|
-
|
|
11
10
|
//#region src/pure.ts
|
|
11
|
+
/**
|
|
12
|
+
* DSH memory plugin (adapted from the OpenCode memory plugin).
|
|
13
|
+
*
|
|
14
|
+
* Pure helpers only: prompt builders, transcript chunking, JSON repair and
|
|
15
|
+
* entry validation. No I/O and no harness wiring — `plugin.ts` owns the hook
|
|
16
|
+
* registration and `digest.ts` owns the LLM call and vault writes.
|
|
17
|
+
*/
|
|
12
18
|
const CHECKPOINT_MARKER = "[memory-checkpoint]";
|
|
13
19
|
/**
|
|
20
|
+
* Entry types the vault's MCP server can store via `store_*` tools. The
|
|
21
|
+
* extraction prompt is restricted to these so every produced entry has a
|
|
22
|
+
* write path (no `idea`/`context`/`source` — those have no store tool).
|
|
23
|
+
*/
|
|
24
|
+
const EXTRACTABLE_TYPES = [
|
|
25
|
+
"decision",
|
|
26
|
+
"fact",
|
|
27
|
+
"learning",
|
|
28
|
+
"convention"
|
|
29
|
+
];
|
|
30
|
+
/**
|
|
31
|
+
* Shared entry vocabulary: one definition list, used by BOTH the internal
|
|
32
|
+
* extraction prompt and the agent-facing checkpoint prompt. Keeping one source
|
|
33
|
+
* stops the two from drifting, which is how the checkpoint prompt ended up
|
|
34
|
+
* asking agents to write entries it never defined.
|
|
35
|
+
*/
|
|
36
|
+
const ENTRY_TYPE_GLOSS = {
|
|
37
|
+
decision: "architectural or design choices that were made",
|
|
38
|
+
fact: "stable, verifiable statements about the project (versions, constraints)",
|
|
39
|
+
learning: "non-obvious lessons, debugging insights, or solutions found",
|
|
40
|
+
convention: "style rules, naming patterns, coding conventions agreed"
|
|
41
|
+
};
|
|
42
|
+
/** Content shape, quoted by both prompts. */
|
|
43
|
+
const ENTRY_CONTENT_RULE = "a single paragraph — no headings, no bullet lists, no markdown structure";
|
|
44
|
+
/**
|
|
45
|
+
* What "notable" means, shared by both prompts. The extraction model is told
|
|
46
|
+
* these rules; the agent writing checkpoints needs them just as much.
|
|
47
|
+
*/
|
|
48
|
+
const ENTRY_SELECTION_RULES = [
|
|
49
|
+
"Skip trivia (greetings, \"ok\", \"thanks\", restating the request).",
|
|
50
|
+
"Prefer fewer, high-signal entries over many weak ones.",
|
|
51
|
+
"Do not record anything you cannot ground in this session's activity."
|
|
52
|
+
];
|
|
53
|
+
/** Entry-type clauses as bullet lines, in EXTRACTABLE_TYPES order. */
|
|
54
|
+
function entryTypeBullets() {
|
|
55
|
+
return EXTRACTABLE_TYPES.map((t) => `- **${t}**: ${ENTRY_TYPE_GLOSS[t]}.`);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
14
58
|
* Resolve the project name from a working directory.
|
|
15
59
|
* Priority:
|
|
16
60
|
* 1. OpenSpec presence: if `openspec/` exists, use the basename.
|
|
@@ -59,7 +103,13 @@ function buildCheckpointPrompt(state, activitySummary) {
|
|
|
59
103
|
"Tracked activity:",
|
|
60
104
|
activitySummary.trim() || "(none recorded)",
|
|
61
105
|
"",
|
|
62
|
-
"Write OKF entries for
|
|
106
|
+
"Write OKF entries for anything notable using the `store_*` MCP tools.",
|
|
107
|
+
"Entry types:",
|
|
108
|
+
...entryTypeBullets(),
|
|
109
|
+
"",
|
|
110
|
+
`Set \`content\` to ${ENTRY_CONTENT_RULE}, and \`description\` to a one-sentence summary.`,
|
|
111
|
+
"Tag with lowercase-kebab tags (at least one, e.g. architecture/python/testing).",
|
|
112
|
+
...ENTRY_SELECTION_RULES,
|
|
63
113
|
"If nothing is notable, say so explicitly and exit."
|
|
64
114
|
].join("\n");
|
|
65
115
|
}
|
|
@@ -89,22 +139,17 @@ function buildCommitCheckpointPrompt(state) {
|
|
|
89
139
|
return [
|
|
90
140
|
`${CHECKPOINT_MARKER} Memory capture after \`git commit\` in project \`${state.project}\`.`,
|
|
91
141
|
"",
|
|
92
|
-
"Review the staged/committed changes and write OKF entries
|
|
142
|
+
"Review the staged/committed changes and write OKF entries through the `store_*` MCP tools.",
|
|
143
|
+
"Entry types:",
|
|
144
|
+
...entryTypeBullets(),
|
|
145
|
+
"",
|
|
146
|
+
`Set \`content\` to ${ENTRY_CONTENT_RULE}, and \`description\` to a one-sentence summary.`,
|
|
147
|
+
"Tag with lowercase-kebab tags (at least one, e.g. architecture/python/testing).",
|
|
148
|
+
...ENTRY_SELECTION_RULES,
|
|
93
149
|
"If nothing is notable, say so explicitly and exit."
|
|
94
150
|
].join("\n");
|
|
95
151
|
}
|
|
96
152
|
/**
|
|
97
|
-
* Entry types the vault's MCP server can store via `store_*` tools. The
|
|
98
|
-
* extraction prompt is restricted to these so every produced entry has a
|
|
99
|
-
* write path (no `idea`/`context`/`source` — those have no store tool).
|
|
100
|
-
*/
|
|
101
|
-
const EXTRACTABLE_TYPES = [
|
|
102
|
-
"decision",
|
|
103
|
-
"fact",
|
|
104
|
-
"learning",
|
|
105
|
-
"convention"
|
|
106
|
-
];
|
|
107
|
-
/**
|
|
108
153
|
* Build the system + user messages for the in-process extraction call.
|
|
109
154
|
* The system part instructs the model to return a JSON array restricted to
|
|
110
155
|
* EXTRACTABLE_TYPES; the user part carries the transcript.
|
|
@@ -113,7 +158,7 @@ function buildExtractionPrompt(project, transcript, contextFiles = {}) {
|
|
|
113
158
|
const sysParts = ["You are an assistant that extracts durable knowledge from a session transcript."];
|
|
114
159
|
if (contextFiles.criticalFacts?.trim()) sysParts.push(`Always-loaded context: CRITICAL_FACTS.md\n${contextFiles.criticalFacts.trim()}`);
|
|
115
160
|
if (contextFiles.claude?.trim()) sysParts.push(`Always-loaded context: _CLAUDE.md\n${contextFiles.claude.trim()}`);
|
|
116
|
-
sysParts.push(`For the transcript of project \`${project}\`, identify:`,
|
|
161
|
+
sysParts.push(`For the transcript of project \`${project}\`, identify:`, ...entryTypeBullets(), "", "Return a JSON array. Each element must have exactly:", ` - "entry_type": one of ${EXTRACTABLE_TYPES.map((t) => `"${t}"`).join(" | ")}`, ` - "content": ${ENTRY_CONTENT_RULE}`, " - \"description\": a one-sentence summary of `content` (queryable)", " - \"tags\": an array of lowercase-kebab tags (never empty if possible, at least 1 like architecture/python/testing)", " - \"confidence\": a number 0.0-1.0", " - \"openspec_change_id\": (optional) the change slug if the transcript names it", "", "Rules:", ...ENTRY_SELECTION_RULES.map((r) => `- ${r}`), "", "Return only the JSON array. No prose, no markdown fences.");
|
|
117
162
|
return {
|
|
118
163
|
system: sysParts.join("\n"),
|
|
119
164
|
user: `Project: ${project}\n\nTranscript:\n---\n${transcript}\n---`
|
|
@@ -213,7 +258,6 @@ function validateEntries(payload) {
|
|
|
213
258
|
}
|
|
214
259
|
return valid;
|
|
215
260
|
}
|
|
216
|
-
|
|
217
261
|
//#endregion
|
|
218
262
|
//#region src/digest.ts
|
|
219
263
|
const MIN_DIGEST_TRANSCRIPT_CHARS = 200;
|
|
@@ -233,9 +277,9 @@ function transcriptOfDSM(events) {
|
|
|
233
277
|
return text ? `## assistant\n${text}` : null;
|
|
234
278
|
}
|
|
235
279
|
if (t === "tool/call" || t === "tool_call") {
|
|
236
|
-
const name
|
|
280
|
+
const name = d.tool ?? d.name ?? "tool";
|
|
237
281
|
const args = d.args ?? d.arguments ?? {};
|
|
238
|
-
return `## tool_call ${name
|
|
282
|
+
return `## tool_call ${name}\n${JSON.stringify(args).slice(0, 1e3)}`;
|
|
239
283
|
}
|
|
240
284
|
if (t === "tool/result" || t === "tool_result") return `## tool_result\n${typeof d.output === "string" ? d.output : JSON.stringify(d).slice(0, 1e3)}`;
|
|
241
285
|
if (t.startsWith("compaction")) return `## ${t}\n${JSON.stringify(d).slice(0, 500)}`;
|
|
@@ -387,13 +431,13 @@ function connectMcp(memoryPath, serverDir) {
|
|
|
387
431
|
}).then(() => {
|
|
388
432
|
if (closed) throw new Error("memory-vault-server closed during handshake");
|
|
389
433
|
resolve({
|
|
390
|
-
callTool: (name
|
|
391
|
-
name
|
|
392
|
-
arguments: args
|
|
434
|
+
callTool: (name, args, timeoutMs = MCP_CALL_TIMEOUT_MS) => send("tools/call", {
|
|
435
|
+
name,
|
|
436
|
+
arguments: args
|
|
393
437
|
}, timeoutMs).then((result) => {
|
|
394
438
|
if (result?.isError) {
|
|
395
439
|
const text = Array.isArray(result.content) ? result.content.map((c) => c?.text ?? "").join("") : JSON.stringify(result);
|
|
396
|
-
throw new Error(`tool ${name
|
|
440
|
+
throw new Error(`tool ${name} failed: ${text}`);
|
|
397
441
|
}
|
|
398
442
|
return result;
|
|
399
443
|
}),
|
|
@@ -427,7 +471,7 @@ async function writeEntries(client, project, entries) {
|
|
|
427
471
|
content: e.content,
|
|
428
472
|
...e.description ? { description: e.description } : {},
|
|
429
473
|
tags: e.tags,
|
|
430
|
-
confidence: e.confidence,
|
|
474
|
+
...e.entry_type === "fact" ? { confidence: e.confidence } : {},
|
|
431
475
|
...e.openspec_change_id ? { openspec_change_id: e.openspec_change_id } : {}
|
|
432
476
|
});
|
|
433
477
|
upserted += 1;
|
|
@@ -492,9 +536,22 @@ async function digestSessionDSM(ctx, config, sessionId, directory, project, even
|
|
|
492
536
|
await client.close().catch(() => {});
|
|
493
537
|
}
|
|
494
538
|
}
|
|
495
|
-
|
|
496
539
|
//#endregion
|
|
497
540
|
//#region src/plugin.ts
|
|
541
|
+
/**
|
|
542
|
+
* Harness wiring for `memory-auto`. Registers exactly these hooks:
|
|
543
|
+
*
|
|
544
|
+
* - `session/created` resolve the project name for the session
|
|
545
|
+
* - `session/disposed` digest the transcript
|
|
546
|
+
* - `agent/status` (idle) auto-capture gate
|
|
547
|
+
* - `session/event` activity tracking; `tool/call` with a
|
|
548
|
+
* `git commit` command and `compaction/start`
|
|
549
|
+
* queue checkpoints
|
|
550
|
+
* - `agent/pre-step` deliver the queued checkpoint to the agent
|
|
551
|
+
* - `ctx.effect` dispose batch-digest sessions still pending
|
|
552
|
+
*
|
|
553
|
+
* The agent writes the entries; this plugin only prompts it.
|
|
554
|
+
*/
|
|
498
555
|
const name = "memory-auto";
|
|
499
556
|
const Config = Schema.object({
|
|
500
557
|
memoryPath: Schema.string().default(process.env.DSH_MEMORY_PATH ?? ""),
|
|
@@ -607,7 +664,9 @@ function apply(ctx, config) {
|
|
|
607
664
|
return;
|
|
608
665
|
}
|
|
609
666
|
const dir = sessionDirs.get(sid) ?? "";
|
|
610
|
-
|
|
667
|
+
const proj = await projectFor(dir);
|
|
668
|
+
const evts = session?.events ?? [];
|
|
669
|
+
await digestSessionDSM(ctx, digestConfig, sid, dir, proj, evts);
|
|
611
670
|
});
|
|
612
671
|
ctx.on("agent/status", async (payload) => {
|
|
613
672
|
const agent = payload?.agent;
|
|
@@ -619,10 +678,12 @@ function apply(ctx, config) {
|
|
|
619
678
|
if (idleCheckpoint(st, summary(sid))) {
|
|
620
679
|
console.log(`[memory-auto] idle checkpoint digest for ${sid}`);
|
|
621
680
|
const dir = sessionDirs.get(sid) ?? "";
|
|
622
|
-
|
|
681
|
+
const proj = await projectFor(dir);
|
|
682
|
+
const fakeEvents = [{
|
|
623
683
|
type: "user/message",
|
|
624
684
|
data: { text: summary(sid) }
|
|
625
|
-
}]
|
|
685
|
+
}];
|
|
686
|
+
await digestSessionDSM(ctx, digestConfig, sid, dir, proj, fakeEvents);
|
|
626
687
|
}
|
|
627
688
|
});
|
|
628
689
|
ctx.on("session/event", async (session, event) => {
|
|
@@ -679,17 +740,18 @@ function apply(ctx, config) {
|
|
|
679
740
|
for (const [sid, dir] of sessionDirs) {
|
|
680
741
|
if (!states.get(sid)?.hasActivity) continue;
|
|
681
742
|
projectFor(dir).then((proj) => {
|
|
682
|
-
|
|
743
|
+
const fakeEvents = [{
|
|
683
744
|
type: "user/message",
|
|
684
745
|
data: { text: summary(sid) }
|
|
685
|
-
}]
|
|
746
|
+
}];
|
|
747
|
+
digestSessionDSM(ctx, digestConfig, sid, dir, proj, fakeEvents);
|
|
686
748
|
});
|
|
687
749
|
}
|
|
688
750
|
};
|
|
689
751
|
});
|
|
690
752
|
console.log(`[memory-auto] active memoryPath=${memoryPath} serverDir=${serverDir} llm=${config.provider}/${config.model}`);
|
|
691
753
|
}
|
|
692
|
-
|
|
693
754
|
//#endregion
|
|
694
755
|
export { Config, apply, name };
|
|
756
|
+
|
|
695
757
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["sysParts: string[]","chunks: string[]","valid: ValidEntry[]","name","messages: Message[]","options: GenerateOptions","child: ChildProcess","msg: any","args","entries: ValidEntry[]","client: McpClient","Config: Schema<Config>","digestConfig: DigestConfig","sid: string | undefined"],"sources":["../src/pure.ts","../src/digest.ts","../src/plugin.ts"],"sourcesContent":["/**\n * DSH memory plugin (adapted from the OpenCode memory plugin).\n *\n * Implements session lifecycle hooks:\n * - session.created: project name resolution only (no memory auto-injection)\n * - session.idle: auto-capture gate (skip if no activity or already delivered)\n * - tool.execute.after: detect `git commit*` and queue a checkpoint\n * - tui.prompt.append: deliver queued checkpoint on next user message\n * - experimental.session.compacting: pre-compaction capture (always fires)\n * - session.end: invoke post-session digest\n * - /brain search|recall|profile: opt-in reads via MCP (2s health check)\n * - /checkpoint: manual structured review\n * - OpenCode version guard: warn on < 1.17.10, disable gracefully\n */\n\nimport { readFile } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { join, basename } from \"node:path\";\n\n// ── Constants ────────────────────────────────────────────────────────────\n\nexport const MIN_OPENCODE_VERSION = \"1.17.10\";\nexport const MCP_UNREACHABLE =\n \"> ⚠️ Memory server unreachable — search cannot be completed.\";\nconst CHECKPOINT_MARKER = \"[memory-checkpoint]\";\n\n// ── Version guard ────────────────────────────────────────────────────────\n\n/** Compare two \"x.y.z\" semver strings. Returns negative/0/positive. */\nfunction compareSemver(a: string, b: string): number {\n const [a1, a2, a3] = a.split(\".\").map((n) => parseInt(n, 10) || 0);\n const [b1, b2, b3] = b.split(\".\").map((n) => parseInt(n, 10) || 0);\n if (a1 !== b1) return a1 - b1;\n if (a2 !== b2) return a2 - b2;\n return (a3 || 0) - (b3 || 0);\n}\n\nexport function isSupportedVersion(version: string): boolean {\n return compareSemver(version, MIN_OPENCODE_VERSION) >= 0;\n}\n\n// ── Project name resolution ──────────────────────────────────────────────\n\n/**\n * Resolve the project name from a working directory.\n * Priority:\n * 1. OpenSpec presence: if `openspec/` exists, use the basename.\n * 2. package.json -> name\n * 3. pyproject.toml -> [project] -> name\n * 4. README.md: first 5 lines, heading pattern `# <ProjectName>`\n * 5. Fallback: basename of working directory\n */\nexport async function resolveProjectName(cwd: string): Promise<string> {\n if (existsSync(join(cwd, \"openspec\"))) {\n return basename(cwd);\n }\n // package.json\n const pkgPath = join(cwd, \"package.json\");\n if (existsSync(pkgPath)) {\n try {\n const pkg = JSON.parse(await readFile(pkgPath, \"utf-8\"));\n if (typeof pkg.name === \"string\" && pkg.name.trim()) {\n return pkg.name.trim();\n }\n } catch {\n // ignore parse errors; try next strategy\n }\n }\n // pyproject.toml (minimal regex parse)\n const pyprojectPath = join(cwd, \"pyproject.toml\");\n if (existsSync(pyprojectPath)) {\n try {\n const text = await readFile(pyprojectPath, \"utf-8\");\n const m = text.match(/\\[project\\][^\\[]*?name\\s*=\\s*[\"']([^\"']+)[\"']/);\n if (m) return m[1];\n } catch {\n // ignore\n }\n }\n // README.md heading\n const readmePath = join(cwd, \"README.md\");\n if (existsSync(readmePath)) {\n try {\n const text = await readFile(readmePath, \"utf-8\");\n const head = text.split(\"\\n\").slice(0, 5);\n for (const line of head) {\n const m = line.match(/^#\\s+(.+)$/);\n if (m) return m[1].trim();\n }\n } catch {\n // ignore\n }\n }\n return basename(cwd);\n}\n\n// ── Checkpoint state ─────────────────────────────────────────────────────\n\nexport interface SessionState {\n project: string;\n hasActivity: boolean;\n checkpointDelivered: boolean;\n queuedCheckpoint: string | null;\n}\n\nexport function createSessionState(project: string): SessionState {\n return {\n project,\n hasActivity: false,\n checkpointDelivered: false,\n queuedCheckpoint: null,\n };\n}\n\n/**\n * Build the checkpoint prompt body. Pure function — exported for testing.\n */\nexport function buildCheckpointPrompt(state: SessionState, activitySummary: string): string {\n return [\n `${CHECKPOINT_MARKER} End-of-session memory capture for project \\`${state.project}\\`.`,\n \"\",\n \"Tracked activity:\",\n activitySummary.trim() || \"(none recorded)\",\n \"\",\n \"Write OKF entries for any notable decisions, facts, or learnings using the `store_*` MCP tools.\",\n \"If nothing is notable, say so explicitly and exit.\",\n ].join(\"\\n\");\n}\n\n/**\n * Decide whether to deliver a checkpoint on `session.idle`.\n * Returns the prompt to deliver, or null to skip.\n */\nexport function idleCheckpoint(\n state: SessionState,\n activitySummary: string,\n): string | null {\n if (!state.hasActivity) return null;\n if (state.checkpointDelivered) return null;\n state.checkpointDelivered = true;\n return buildCheckpointPrompt(state, activitySummary);\n}\n\n/**\n * Decide whether to fire on `experimental.session.compacting`.\n * Per spec: always fires when activity exists, even if checkpoint was delivered.\n */\nexport function compactingCheckpoint(\n state: SessionState,\n activitySummary: string,\n): string | null {\n if (!state.hasActivity) return null;\n return buildCheckpointPrompt(state, activitySummary);\n}\n\n// ── Git commit detection ─────────────────────────────────────────────────\n\nconst GIT_COMMIT_PATTERN = /git\\s+commit\\b/;\n\nexport function isGitCommit(command: string): boolean {\n return GIT_COMMIT_PATTERN.test(command);\n}\n\nexport function buildCommitCheckpointPrompt(state: SessionState): string {\n return [\n `${CHECKPOINT_MARKER} Memory capture after \\`git commit\\` in project \\`${state.project}\\`.`,\n \"\",\n \"Review the staged/committed changes and write OKF entries for any notable decisions, facts, or learnings.\",\n \"If nothing is notable, say so explicitly and exit.\",\n ].join(\"\\n\");\n}\n\n// ── In-process session digest (ctx.llm) ──────────────────────────────────\n\n/**\n * Entry types the vault's MCP server can store via `store_*` tools. The\n * extraction prompt is restricted to these so every produced entry has a\n * write path (no `idea`/`context`/`source` — those have no store tool).\n */\nexport const EXTRACTABLE_TYPES = ['decision', 'fact', 'learning', 'convention'] as const\nexport type ExtractableType = (typeof EXTRACTABLE_TYPES)[number]\n\n/** One validated OKF entry ready to be stored through the vault server. */\nexport interface ValidEntry {\n entry_type: ExtractableType\n content: string\n description: string\n tags: string[]\n confidence: number\n openspec_change_id: string | null\n}\n\n/** Optional vault context files to embed in the extraction prompt. */\nexport interface DigestContextFiles {\n criticalFacts?: string\n claude?: string\n}\n\n/**\n * Build the system + user messages for the in-process extraction call.\n * The system part instructs the model to return a JSON array restricted to\n * EXTRACTABLE_TYPES; the user part carries the transcript.\n */\nexport function buildExtractionPrompt(\n project: string,\n transcript: string,\n contextFiles: DigestContextFiles = {},\n): { system: string; user: string } {\n const sysParts: string[] = [\n 'You are an assistant that extracts durable knowledge from a session transcript.',\n ]\n if (contextFiles.criticalFacts?.trim()) {\n sysParts.push(`Always-loaded context: CRITICAL_FACTS.md\\n${contextFiles.criticalFacts.trim()}`)\n }\n if (contextFiles.claude?.trim()) {\n sysParts.push(`Always-loaded context: _CLAUDE.md\\n${contextFiles.claude.trim()}`)\n }\n sysParts.push(\n `For the transcript of project \\`${project}\\`, identify:`,\n '- **Decisions**: architectural or design choices that were made.',\n '- **Facts**: stable, verifiable statements about the project (versions, conventions, constraints).',\n '- **Learnings**: non-obvious lessons, debugging insights, or solutions found.',\n '- **Conventions**: style rules, naming patterns, coding conventions agreed.',\n '',\n 'Return a JSON array. Each element must have exactly:',\n ' - \"entry_type\": one of \"decision\" | \"fact\" | \"learning\" | \"convention\"',\n ' - \"content\": a single-paragraph statement (no headings, no lists)',\n ' - \"description\": a one-sentence summary of `content` (queryable)',\n ' - \"tags\": an array of lowercase-kebab tags (never empty if possible, at least 1 like architecture/python/testing)',\n ' - \"confidence\": a number 0.0-1.0',\n ' - \"openspec_change_id\": (optional) the change slug if the transcript names it',\n '',\n 'Rules:',\n '- Skip trivial exchanges (greetings, \"ok\", \"thanks\", or anything with no project knowledge).',\n '- Prefer fewer, higher-signal entries over many weak ones.',\n '- Do not include anything not present in the transcript.',\n '',\n 'Return only the JSON array. No prose, no markdown fences.',\n )\n return {\n system: sysParts.join('\\n'),\n user: `Project: ${project}\\n\\nTranscript:\\n---\\n${transcript}\\n---`,\n }\n}\n\n/**\n * Split an oversized transcript into overlapping chunks (mirrors the legacy\n * digest script: 25k chars per chunk, 1k overlap, at most `cap` chunks).\n */\nexport function chunkTranscript(text: string, maxLen = 50_000, chunkSize = 25_000, overlap = 1_000, cap = 3): string[] {\n if (text.length <= maxLen) return [text]\n const chunks: string[] = []\n let i = 0\n while (i < text.length) {\n chunks.push(text.slice(i, i + chunkSize))\n i += chunkSize - overlap\n if (chunks.length >= cap) break\n }\n return chunks\n}\n\n/** Best-effort repair of common LLM JSON output; returns parsed value or null. */\nexport function repairJson(text: string): unknown {\n const s = text.trim()\n // Strip code fences: ```json ... ```\n const fenced = s.replace(/^```(?:json)?\\s*/i, '').replace(/\\s*```$/, '').trim()\n // Trailing commas: `,]` / `,}` -> `]` / `}`\n const noTrailing = fenced.replace(/,(\\s*[\\]}])/g, '$1')\n const candidates = [s, fenced, noTrailing]\n // Single-quote to double-quote conversion only when no double quotes exist.\n if (noTrailing.includes(\"'\") && !noTrailing.includes('\"')) {\n candidates.push(convertSingleQuotes(noTrailing))\n }\n for (const c of candidates) {\n try {\n return JSON.parse(c)\n } catch {\n // try the next candidate\n }\n }\n return null\n}\n\nfunction convertSingleQuotes(text: string): string {\n let out = ''\n let inString = false\n let i = 0\n while (i < text.length) {\n const ch = text[i]\n if (inString && ch === '\\\\') {\n if (i + 1 < text.length && text[i + 1] === \"'\") {\n out += \"'\"\n i += 2\n continue\n }\n out += ch\n if (i + 1 < text.length) {\n out += text[i + 1]\n i += 2\n continue\n }\n i += 1\n continue\n }\n if (ch === \"'\") {\n inString = !inString\n out += '\"'\n i += 1\n continue\n }\n out += ch\n i += 1\n }\n return out\n}\n\n/**\n * Validate a parsed extraction payload into OKF entries. Unknown types,\n * empty content and malformed values are dropped; tags and confidence are\n * normalized. Returns the valid entries (possibly empty).\n */\nexport function validateEntries(payload: unknown): ValidEntry[] {\n if (!Array.isArray(payload)) return []\n const valid: ValidEntry[] = []\n for (const item of payload) {\n if (typeof item !== 'object' || item === null) continue\n const raw = item as Record<string, unknown>\n const entryType = raw.entry_type\n if (typeof entryType !== 'string' || !(EXTRACTABLE_TYPES as readonly string[]).includes(entryType)) continue\n const content = typeof raw.content === 'string' ? raw.content.trim() : ''\n if (!content) continue\n const tags = Array.isArray(raw.tags) ? raw.tags.filter((t): t is string => typeof t === 'string' && t.length > 0) : []\n let confidence = 1\n if (typeof raw.confidence === 'number' && Number.isFinite(raw.confidence)) {\n confidence = Math.max(0, Math.min(1, raw.confidence))\n }\n const description = typeof raw.description === 'string' ? raw.description.trim() : ''\n const changeId = typeof raw.openspec_change_id === 'string' && raw.openspec_change_id ? raw.openspec_change_id : null\n valid.push({\n entry_type: entryType as ExtractableType,\n content,\n description,\n tags,\n confidence,\n openspec_change_id: changeId,\n })\n }\n return valid\n}\n\n// The full hook wiring is exposed for testing; the actual OpenCode integration\n// is done in `register.ts` (the entry point the OpenCode runtime loads via\n// package.json \"main\"). This module intentionally has no default export:\n// opencode 1.18.x only loads plugin modules with a single export.\nexport const __testing = {\n createSessionState,\n isSupportedVersion,\n resolveProjectName,\n idleCheckpoint,\n compactingCheckpoint,\n isGitCommit,\n buildCheckpointPrompt,\n buildCommitCheckpointPrompt,\n};\n","import { spawn, type ChildProcess } from 'node:child_process'\nimport { existsSync } from 'node:fs'\nimport { join } from 'node:path'\nimport readline from 'node:readline'\nimport { BlockAssembler, createUserMessage } from '@deepseek-ai/dsh-llm'\nimport type { FinishReason, GenerateOptions, Message } from '@deepseek-ai/dsh-llm'\nimport type { Context } from '@deepseek-ai/cordis'\nimport {\n buildExtractionPrompt,\n chunkTranscript,\n repairJson,\n validateEntries,\n type ValidEntry,\n} from './pure.js'\n\n// Post-session digest runner: in-process LLM extraction via the harness's own\n// `ctx.llm` service (no external CLI, no credentials of its own), followed by\n// OKF writes through the vault's MCP server over stdio (`store_*` tools keep\n// the Markdown source of truth and the SQLite FTS5 index in sync).\n\nconst MIN_DIGEST_TRANSCRIPT_CHARS = 200\nconst LLM_RETRIES = 3\nconst LLM_RETRY_DELAYS_MS = [2_000, 5_000]\nconst MCP_CALL_TIMEOUT_MS = 60_000\n\nfunction log(msg: string) {\n console.log(`[memory-auto] ${msg}`)\n}\n\nexport function transcriptOfDSM(events: any[]): string {\n // DSH SessionEvent -> transcript\n return (events ?? [])\n .map((ev: any) => {\n const t = ev?.type ?? ''\n const d = ev?.data ?? ev\n if (t === 'user/message' || t === 'user_message') {\n const text = typeof d.text === 'string' ? d.text : typeof d.content === 'string' ? d.content : JSON.stringify(d).slice(0, 500)\n return `## user\\n${text}`\n }\n if (t === 'assistant/message' || t === 'assistant_message') {\n const text = typeof d.text === 'string' ? d.text : typeof d.content === 'string' ? d.content : ''\n return text ? `## assistant\\n${text}` : null\n }\n if (t === 'tool/call' || t === 'tool_call') {\n const name = d.tool ?? d.name ?? 'tool'\n const args = d.args ?? d.arguments ?? {}\n return `## tool_call ${name}\\n${JSON.stringify(args).slice(0, 1000)}`\n }\n if (t === 'tool/result' || t === 'tool_result') {\n const out = typeof d.output === 'string' ? d.output : JSON.stringify(d).slice(0, 1000)\n return `## tool_result\\n${out}`\n }\n if (t.startsWith('compaction')) return `## ${t}\\n${JSON.stringify(d).slice(0, 500)}`\n return null\n })\n .filter((x): x is string => Boolean(x))\n .join('\\n\\n')\n}\n\n/** Translate a terminal stream finish into a thrown error, mirroring dsh's own summarizers. */\nfunction finishError(finish: FinishReason): Error | undefined {\n switch (finish.kind) {\n case 'error':\n case 'aborted': {\n const error = new Error(finish.failure.message) as Error & { code?: string }\n error.code = finish.failure.code\n return error\n }\n default:\n return undefined\n }\n}\n\nexport interface DigestConfig {\n memoryPath: string\n serverDir: string\n provider: string\n model: string\n maxTokens: number\n minTranscriptChars: number\n}\n\n/**\n * Run one extraction call through the harness's `ctx.llm` service: build the\n * prompt, stream, assemble, repair and validate the JSON entries.\n */\nexport async function extractEntriesWithLlm(\n ctx: Context,\n config: DigestConfig,\n project: string,\n transcript: string,\n contextFiles?: { criticalFacts?: string; claude?: string },\n signal?: AbortSignal,\n): Promise<ValidEntry[]> {\n const { system, user } = buildExtractionPrompt(project, transcript, contextFiles)\n const assembler = new BlockAssembler()\n const messages: Message[] = [\n createUserMessage({\n content: [{ type: 'text', text: user }],\n source: { kind: 'plugin', plugin: 'memory-auto' },\n }),\n ]\n const options: GenerateOptions = {\n provider: config.provider,\n model: config.model,\n messages,\n system,\n maxTokens: config.maxTokens,\n ...(signal === undefined ? {} : { signal }),\n }\n for await (const chunk of ctx.llm.stream(options)) assembler.push(chunk)\n const error = finishError(assembler.finish)\n if (error !== undefined) throw error\n const text = assembler\n .blocks()\n .filter((b) => b.type === 'text')\n .map((b) => b.text)\n .join('')\n const parsed = repairJson(text)\n return validateEntries(parsed)\n}\n\n/** Minimal MCP stdio client for the vault server (spawned via its launcher: uv, pip venv fallback). */\nexport interface McpClient {\n callTool(name: string, args: Record<string, unknown>, timeoutMs?: number): Promise<unknown>\n close(): Promise<void>\n}\n\nexport function connectMcp(memoryPath: string, serverDir: string): Promise<McpClient> {\n return new Promise((resolve, reject) => {\n // Single decision point: the server bundle's launcher.mjs picks `uv run`\n // or a pip-managed .venv. Legacy hand-made server dirs without a launcher\n // keep the old direct `uv run` spawn.\n const launcher = join(serverDir, 'launcher.mjs')\n const [command, args] = existsSync(launcher)\n ? [process.execPath, [launcher]]\n : ['uv', ['run', '--directory', serverDir, 'python', 'server.py']]\n const child: ChildProcess = spawn(\n command,\n args,\n {\n env: { ...process.env, MEMORY_PATH: memoryPath, UV_CACHE_DIR: process.env.UV_CACHE_DIR ?? '/tmp/uv-cache' },\n stdio: ['pipe', 'pipe', 'inherit'],\n },\n )\n const pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void; timer: NodeJS.Timeout }>()\n let nextId = 1\n let closed = false\n\n const failAll = (err: Error) => {\n for (const [, p] of pending) {\n clearTimeout(p.timer)\n p.reject(err)\n }\n pending.clear()\n }\n\n child.on('error', (err) => {\n closed = true\n failAll(new Error(`memory-vault-server spawn failed: ${err.message}`))\n reject(err)\n })\n child.on('exit', (code) => {\n if (closed) return\n closed = true\n failAll(new Error(`memory-vault-server exited unexpectedly (code ${code})`))\n reject(new Error(`memory-vault-server exited before initialize (code ${code})`))\n })\n\n const rl = readline.createInterface({ input: child.stdout!, crlfDelay: Infinity })\n rl.on('line', (line) => {\n let msg: any\n try {\n msg = JSON.parse(line)\n } catch {\n return\n }\n if (typeof msg?.id === 'number') {\n const p = pending.get(msg.id)\n if (!p) return\n pending.delete(msg.id)\n clearTimeout(p.timer)\n if (msg.error) p.reject(new Error(`MCP error: ${msg.error.message ?? JSON.stringify(msg.error)}`))\n else p.resolve(msg.result)\n }\n })\n\n const send = (method: string, params: unknown, timeoutMs: number = MCP_CALL_TIMEOUT_MS): Promise<unknown> =>\n new Promise((res, rej) => {\n if (closed || !child.stdin?.writable) {\n rej(new Error('memory-vault-server is not running'))\n return\n }\n const id = nextId++\n const timer = setTimeout(() => {\n pending.delete(id)\n rej(new Error(`MCP call ${method} timed out after ${timeoutMs}ms`))\n }, timeoutMs)\n pending.set(id, { resolve: res, reject: rej, timer })\n child.stdin!.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\\n')\n })\n\n const notify = (method: string, params: unknown) => {\n if (!closed && child.stdin?.writable) {\n child.stdin.write(JSON.stringify({ jsonrpc: '2.0', method, params }) + '\\n')\n }\n }\n\n // initialize handshake (notifications are fire-and-forget: no id, no reply)\n void send('initialize', {\n protocolVersion: '2025-06-18',\n capabilities: {},\n clientInfo: { name: 'memory-auto', version: '0.1.0' },\n })\n .then(() => {\n notify('notifications/initialized', {})\n })\n .then(() => {\n if (closed) throw new Error('memory-vault-server closed during handshake')\n resolve({\n callTool: (name, args, timeoutMs = MCP_CALL_TIMEOUT_MS) =>\n send('tools/call', { name, arguments: args }, timeoutMs).then((result: any) => {\n if (result?.isError) {\n const text = Array.isArray(result.content) ? result.content.map((c: any) => c?.text ?? '').join('') : JSON.stringify(result)\n throw new Error(`tool ${name} failed: ${text}`)\n }\n return result\n }),\n close: async () => {\n if (closed) return\n closed = true\n for (const [, p] of pending) clearTimeout(p.timer)\n pending.clear()\n if (child.exitCode !== null) return\n child.kill()\n await new Promise((r) => child.once('exit', r))\n },\n })\n })\n .catch((err) => {\n closed = true\n child.kill()\n reject(err)\n })\n })\n}\n\n/**\n * Write validated OKF entries through the vault server's `store_*` tools,\n * which upsert both the Markdown file and the SQLite FTS5 index.\n */\nexport async function writeEntries(client: McpClient, project: string, entries: ValidEntry[]): Promise<{ upserted: number; failed: number }> {\n let upserted = 0\n let failed = 0\n for (const e of entries) {\n try {\n await client.callTool(`store_${e.entry_type}`, {\n project,\n content: e.content,\n ...(e.description ? { description: e.description } : {}),\n tags: e.tags,\n confidence: e.confidence,\n ...(e.openspec_change_id ? { openspec_change_id: e.openspec_change_id } : {}),\n })\n upserted += 1\n } catch (err) {\n failed += 1\n console.warn(`[memory-auto] store_${e.entry_type} failed:`, err instanceof Error ? err.message : err)\n }\n }\n return { upserted, failed }\n}\n\n/**\n * Full post-session digest: transcript -> ctx.llm extraction (with retries) ->\n * OKF writes via the vault MCP server. Never throws; logs the outcome.\n */\nexport async function digestSessionDSM(\n ctx: Context,\n config: DigestConfig,\n sessionId: string,\n directory: string,\n project: string,\n events: any[],\n signal?: AbortSignal,\n): Promise<void> {\n const header = `## context\\nproject: ${project}\\ndirectory: ${directory}\\n`\n const transcript = (header + transcriptOfDSM(events)).trim()\n if (!transcript) {\n log(`digest skip ${sessionId}: empty`)\n return\n }\n if (transcript.length < (config.minTranscriptChars ?? MIN_DIGEST_TRANSCRIPT_CHARS)) {\n log(`digest skip ${sessionId}: too short ${transcript.length}`)\n return\n }\n\n const chunks = chunkTranscript(transcript)\n const entries: ValidEntry[] = []\n for (const chunk of chunks) {\n let attempt = 0\n for (;;) {\n try {\n const got = await extractEntriesWithLlm(ctx, config, project, chunk, undefined, signal)\n entries.push(...got)\n break\n } catch (err) {\n attempt += 1\n if (attempt >= LLM_RETRIES || signal?.aborted) {\n console.warn(`[memory-auto] digest extraction failed after ${attempt} attempt(s):`, err instanceof Error ? err.message : err)\n break\n }\n const delay = LLM_RETRY_DELAYS_MS[attempt - 1] ?? 5_000\n log(`digest extraction retry ${attempt}/${LLM_RETRIES} in ${delay}ms`)\n await new Promise((r) => setTimeout(r, delay))\n }\n }\n if (signal?.aborted) break\n }\n\n if (entries.length === 0) {\n log(`digest ${sessionId}: no entries extracted`)\n return\n }\n\n let client: McpClient\n try {\n client = await connectMcp(config.memoryPath, config.serverDir)\n } catch (err) {\n console.warn(`[memory-auto] digest ${sessionId}: cannot reach vault server:`, err instanceof Error ? err.message : err)\n return\n }\n try {\n const { upserted, failed } = await writeEntries(client, project, entries)\n log(`digest ${sessionId}: ${upserted} upserted, ${failed} failed (${entries.length} extracted)`)\n } finally {\n await client.close().catch(() => {})\n }\n}\n","import { cpSync, existsSync, mkdirSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { dirname, isAbsolute, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport type { Context } from '@deepseek-ai/cordis'\nimport Schema from '@deepseek-ai/schemastery'\nimport {\n createSessionState,\n resolveProjectName,\n idleCheckpoint,\n compactingCheckpoint,\n isGitCommit,\n buildCheckpointPrompt,\n buildCommitCheckpointPrompt,\n type SessionState,\n} from './pure.js'\nimport { digestSessionDSM, type DigestConfig } from './digest.js'\n\nexport const name = 'memory-auto'\n\nexport interface Config {\n memoryPath: string\n serverDir: string\n provider: string\n model: string\n maxTokens: number\n minTranscriptChars: number\n enabled: boolean\n}\n\nexport const Config: Schema<Config> = Schema.object({\n memoryPath: Schema.string().default(process.env.DSH_MEMORY_PATH ?? ''),\n serverDir: Schema.string().default(process.env.DSH_MEMORY_SERVER_DIR ?? ''),\n provider: Schema.string().default('deepseek-official'),\n model: Schema.string().default('deepseek-v4-flash'),\n maxTokens: Schema.number().default(2048),\n minTranscriptChars: Schema.number().default(200),\n enabled: Schema.boolean().default(true),\n})\n\n/** Requires the harness LLM service: extraction runs in-process via ctx.llm. */\nexport const inject = ['llm']\n\n/**\n * Resolve the harness home the same way the harness does (`$DSH_HOME`, or\n * `~/.dsh`). Paths must never depend on the launch cwd: DSH does not chdir.\n */\nfunction dshHome(): string {\n const env = process.env.DSH_HOME?.trim()\n return env && env.length > 0 ? env : join(homedir(), '.dsh')\n}\n\n/** Absolute paths stay; empty/relative values resolve under the harness home. */\nfunction resolveUnderHome(value: string, fallbackSegment: string): string {\n const v = value.trim()\n if (v.length === 0) return join(dshHome(), fallbackSegment)\n return isAbsolute(v) ? v : join(dshHome(), v)\n}\n\nconst packageRoot = dirname(dirname(fileURLToPath(import.meta.url)))\n\n/** Copy the bundled dir into `target` when `key` is missing there. */\nfunction ensure(target: string, bundled: string, key: string): boolean {\n if (existsSync(join(target, key))) return false\n if (!existsSync(bundled)) return false\n mkdirSync(target, { recursive: true })\n cpSync(bundled, target, { recursive: true })\n return true\n}\n\n/** Copy one bundled file into `target` when missing (upgrades add files 0.1.1 → 0.1.2). */\nfunction ensureFile(target: string, bundled: string, file: string): boolean {\n const dest = join(target, file)\n if (existsSync(dest)) return false\n const src = join(bundled, file)\n if (!existsSync(src)) return false\n mkdirSync(target, { recursive: true })\n cpSync(src, dest)\n return true\n}\n\n// DSH session shape minimal\ntype DSHEvt = any\ntype DSHSession = { id: string; events: DSHEvt[]; cwd?: string }\n\nexport function apply(ctx: Context, config: Config) {\n if (!config.enabled) {\n console.log('[memory-auto] disabled via config')\n return\n }\n\n const memoryPath = resolveUnderHome(config.memoryPath, 'memory-vault')\n const serverDir = resolveUnderHome(config.serverDir, 'memory-vault-server')\n\n // Self-contained install: first boot copies the bundled server and vault\n // starter under the harness home when they are missing.\n if (ensure(serverDir, join(packageRoot, 'server'), 'server.py')) {\n console.log(`[memory-auto] installed memory-vault-server -> ${serverDir}`)\n }\n if (ensure(memoryPath, join(packageRoot, 'vault'), 'type-registry.yaml')) {\n console.log(`[memory-auto] installed vault starter -> ${memoryPath}`)\n }\n // launcher.mjs runs the server via uv or the pip-venv fallback; upgrades of\n // existing installs (server.py already present) still need the new files.\n const bundledServer = join(packageRoot, 'server')\n for (const file of ['launcher.mjs', 'requirements.txt']) {\n if (ensureFile(serverDir, bundledServer, file)) {\n console.log(`[memory-auto] installed ${file} -> ${serverDir}`)\n }\n }\n\n const digestConfig: DigestConfig = {\n memoryPath,\n serverDir,\n provider: config.provider,\n model: config.model,\n maxTokens: config.maxTokens,\n minTranscriptChars: config.minTranscriptChars,\n }\n\n if (!existsSync(join(serverDir, 'server.py'))) {\n console.warn(\n `[memory-auto] vault server not found at ${serverDir} and not bundled — the digest cannot write. ` +\n 'Set DSH_MEMORY_SERVER_DIR (or run `node scripts/bundle-assets.mjs` in a checkout).',\n )\n }\n if (!existsSync(join(memoryPath, 'type-registry.yaml'))) {\n console.warn(`[memory-auto] vault starter not found at ${memoryPath} — searches will fail until it exists.`)\n }\n\n const states = new Map<string, SessionState>()\n const activities = new Map<string, string[]>()\n const queued = new Map<string, string>()\n const sessionDirs = new Map<string, string>()\n const projectCache = new Map<string, string>()\n\n const ACTIVITY_MAX_LINES = 20\n const ACTIVITY_MAX_CHARS = 80\n\n const track = (sessionId: string, line: string) => {\n const st = states.get(sessionId)\n if (!st) return\n st.hasActivity = true\n const list = activities.get(sessionId) ?? []\n if (!list.includes(line)) {\n list.push(line)\n if (list.length > ACTIVITY_MAX_LINES) list.shift()\n activities.set(sessionId, list)\n }\n }\n\n const summary = (sid: string) => (activities.get(sid) ?? []).join('\\n')\n\n const projectFor = async (dir: string): Promise<string> => {\n const cached = projectCache.get(dir)\n if (cached) return cached\n const p = await resolveProjectName(dir || process.cwd())\n projectCache.set(dir, p)\n return p\n }\n\n // session/created -> init state\n ctx.on('session/created', async (session: DSHSession) => {\n const sid = (session as any)?.id ?? (session as any)?.sessionId\n const dir = (session as any)?.cwd ?? (session as any)?.directory ?? ''\n if (!sid) return\n const proj = await projectFor(dir)\n if (!states.has(sid)) {\n states.set(sid, createSessionState(proj))\n activities.set(sid, [])\n }\n sessionDirs.set(sid, dir)\n console.log(`[memory-auto] session created ${sid} project=${proj}`)\n })\n\n // session/disposed -> digest\n ctx.on('session/disposed', async (session: DSHSession) => {\n const sid = (session as any)?.id ?? (session as any)?.sessionId\n if (!sid) return\n const st = states.get(sid)\n if (!st) return\n if (!st.hasActivity) {\n console.log(`[memory-auto] digest skip ${sid}: no activity`)\n return\n }\n const dir = sessionDirs.get(sid) ?? ''\n const proj = await projectFor(dir)\n const evts: DSHEvt[] = (session as any)?.events ?? []\n await digestSessionDSM(ctx, digestConfig, sid, dir, proj, evts)\n })\n\n // agent/status idle -> in-session capture gate\n ctx.on('agent/status', async (payload: any) => {\n const agent = payload?.agent\n const status = payload?.status ?? payload?.agentStatus\n if (status !== 'idle') return\n const sid: string | undefined = agent?.sessionId ?? payload?.sessionId ?? agent?.id\n if (!sid) return\n const st = states.get(sid)\n if (!st) return\n const text = idleCheckpoint(st, summary(sid))\n if (text) {\n console.log(`[memory-auto] idle checkpoint digest for ${sid}`)\n const dir = sessionDirs.get(sid) ?? ''\n const proj = await projectFor(dir)\n const fakeEvents = [{ type: 'user/message', data: { text: summary(sid) } }]\n await digestSessionDSM(ctx, digestConfig, sid, dir, proj, fakeEvents)\n }\n })\n\n // session/event -> git commit detect + compaction start\n ctx.on('session/event', async (session: DSHSession, event: DSHEvt) => {\n const sid = (session as any)?.id ?? (session as any)?.sessionId ?? (event as any)?.sessionId\n if (!sid) return\n const t = event?.type ?? ''\n const d = event?.data ?? {}\n\n // compaction/start -> checkpoint injection (delivered on the next pre-step)\n if (t === 'compaction/start') {\n const st = states.get(sid)\n if (!st) return\n const txt = compactingCheckpoint(st, summary(sid))\n if (txt) {\n console.log(`[memory-auto] compaction checkpoint queued for ${sid}`)\n queued.set(sid, txt)\n }\n return\n }\n\n // tool/call -> git commit detection + activity track\n if (t === 'tool/call' || t === 'tool_call') {\n const cmd = d?.args?.command ?? d?.command ?? ''\n if (typeof cmd === 'string' && isGitCommit(cmd)) {\n const st = states.get(sid)\n if (st) {\n st.hasActivity = true\n queued.set(sid, buildCommitCheckpointPrompt(st))\n console.log(`[memory-auto] git commit queued for ${sid}`)\n }\n return\n }\n if (typeof cmd === 'string') {\n track(sid, `bash: ${cmd.trim().slice(0, ACTIVITY_MAX_CHARS)}`)\n }\n return\n }\n if (t === 'tool/result') {\n // ignore\n return\n }\n if (t === 'user/message' || t === 'assistant/message') {\n // track activity\n track(sid, `${t}: ${(d?.text ?? '').slice(0, ACTIVITY_MAX_CHARS)}`)\n }\n })\n\n // agent/pre-step Waterfall -> deliver queued checkpoint\n ctx.on('agent/pre-step', async (payload: any, next: any) => {\n const sid: string | undefined = payload?.agent?.sessionId ?? payload?.sessionId\n if (sid) {\n const q = queued.get(sid)\n if (q) {\n queued.delete(sid)\n // Best effort: append the checkpoint as user context\n if (Array.isArray(payload?.context)) payload.context.push(q)\n else if (Array.isArray(payload?.messages)) payload.messages.push({ role: 'user', content: q })\n else console.log(`[memory-auto] deliver queued checkpoint for ${sid}`)\n }\n }\n return next()\n })\n\n // dispose -> batch digest remaining sessions\n ctx.effect(() => {\n return () => {\n console.log(`[memory-auto] dispose batch ${sessionDirs.size} sessions`)\n // fire-and-forget digest for each remaining session\n for (const [sid, dir] of sessionDirs) {\n const st = states.get(sid)\n if (!st?.hasActivity) continue\n projectFor(dir).then((proj) => {\n const fakeEvents = [{ type: 'user/message', data: { text: summary(sid) } }]\n void digestSessionDSM(ctx, digestConfig, sid, dir, proj, fakeEvents)\n })\n }\n }\n })\n\n console.log(`[memory-auto] active memoryPath=${memoryPath} serverDir=${serverDir} llm=${config.provider}/${config.model}`)\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/pure.ts","../src/digest.ts","../src/plugin.ts"],"sourcesContent":["/**\n * DSH memory plugin (adapted from the OpenCode memory plugin).\n *\n * Pure helpers only: prompt builders, transcript chunking, JSON repair and\n * entry validation. No I/O and no harness wiring — `plugin.ts` owns the hook\n * registration and `digest.ts` owns the LLM call and vault writes.\n */\n\nimport { readFile } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { join, basename } from \"node:path\";\n\n// ── Constants ────────────────────────────────────────────────────────────\n\nexport const MIN_OPENCODE_VERSION = \"1.17.10\";\nexport const MCP_UNREACHABLE =\n \"> ⚠️ Memory server unreachable — search cannot be completed.\";\nconst CHECKPOINT_MARKER = \"[memory-checkpoint]\";\n\n/**\n * Entry types the vault's MCP server can store via `store_*` tools. The\n * extraction prompt is restricted to these so every produced entry has a\n * write path (no `idea`/`context`/`source` — those have no store tool).\n */\nexport const EXTRACTABLE_TYPES = ['decision', 'fact', 'learning', 'convention'] as const\nexport type ExtractableType = (typeof EXTRACTABLE_TYPES)[number]\n\n/**\n * Shared entry vocabulary: one definition list, used by BOTH the internal\n * extraction prompt and the agent-facing checkpoint prompt. Keeping one source\n * stops the two from drifting, which is how the checkpoint prompt ended up\n * asking agents to write entries it never defined.\n */\nconst ENTRY_TYPE_GLOSS: Record<ExtractableType, string> = {\n decision: \"architectural or design choices that were made\",\n fact: \"stable, verifiable statements about the project (versions, constraints)\",\n learning: \"non-obvious lessons, debugging insights, or solutions found\",\n convention: \"style rules, naming patterns, coding conventions agreed\",\n}\n\n/** Content shape, quoted by both prompts. */\nconst ENTRY_CONTENT_RULE =\n \"a single paragraph — no headings, no bullet lists, no markdown structure\"\n\n/**\n * What \"notable\" means, shared by both prompts. The extraction model is told\n * these rules; the agent writing checkpoints needs them just as much.\n */\nconst ENTRY_SELECTION_RULES = [\n \"Skip trivia (greetings, \\\"ok\\\", \\\"thanks\\\", restating the request).\",\n \"Prefer fewer, high-signal entries over many weak ones.\",\n \"Do not record anything you cannot ground in this session's activity.\",\n] as const\n\n/** Entry-type clauses as bullet lines, in EXTRACTABLE_TYPES order. */\nfunction entryTypeBullets(): string[] {\n return EXTRACTABLE_TYPES.map((t) => `- **${t}**: ${ENTRY_TYPE_GLOSS[t]}.`)\n}\n\n\n// ── Version guard ────────────────────────────────────────────────────────\n\n/** Compare two \"x.y.z\" semver strings. Returns negative/0/positive. */\nfunction compareSemver(a: string, b: string): number {\n const [a1, a2, a3] = a.split(\".\").map((n) => parseInt(n, 10) || 0);\n const [b1, b2, b3] = b.split(\".\").map((n) => parseInt(n, 10) || 0);\n if (a1 !== b1) return a1 - b1;\n if (a2 !== b2) return a2 - b2;\n return (a3 || 0) - (b3 || 0);\n}\n\nexport function isSupportedVersion(version: string): boolean {\n return compareSemver(version, MIN_OPENCODE_VERSION) >= 0;\n}\n\n// ── Project name resolution ──────────────────────────────────────────────\n\n/**\n * Resolve the project name from a working directory.\n * Priority:\n * 1. OpenSpec presence: if `openspec/` exists, use the basename.\n * 2. package.json -> name\n * 3. pyproject.toml -> [project] -> name\n * 4. README.md: first 5 lines, heading pattern `# <ProjectName>`\n * 5. Fallback: basename of working directory\n */\nexport async function resolveProjectName(cwd: string): Promise<string> {\n if (existsSync(join(cwd, \"openspec\"))) {\n return basename(cwd);\n }\n // package.json\n const pkgPath = join(cwd, \"package.json\");\n if (existsSync(pkgPath)) {\n try {\n const pkg = JSON.parse(await readFile(pkgPath, \"utf-8\"));\n if (typeof pkg.name === \"string\" && pkg.name.trim()) {\n return pkg.name.trim();\n }\n } catch {\n // ignore parse errors; try next strategy\n }\n }\n // pyproject.toml (minimal regex parse)\n const pyprojectPath = join(cwd, \"pyproject.toml\");\n if (existsSync(pyprojectPath)) {\n try {\n const text = await readFile(pyprojectPath, \"utf-8\");\n const m = text.match(/\\[project\\][^\\[]*?name\\s*=\\s*[\"']([^\"']+)[\"']/);\n if (m) return m[1];\n } catch {\n // ignore\n }\n }\n // README.md heading\n const readmePath = join(cwd, \"README.md\");\n if (existsSync(readmePath)) {\n try {\n const text = await readFile(readmePath, \"utf-8\");\n const head = text.split(\"\\n\").slice(0, 5);\n for (const line of head) {\n const m = line.match(/^#\\s+(.+)$/);\n if (m) return m[1].trim();\n }\n } catch {\n // ignore\n }\n }\n return basename(cwd);\n}\n\n// ── Checkpoint state ─────────────────────────────────────────────────────\n\nexport interface SessionState {\n project: string;\n hasActivity: boolean;\n checkpointDelivered: boolean;\n queuedCheckpoint: string | null;\n}\n\nexport function createSessionState(project: string): SessionState {\n return {\n project,\n hasActivity: false,\n checkpointDelivered: false,\n queuedCheckpoint: null,\n };\n}\n\n/**\n * Build the checkpoint prompt body. Pure function — exported for testing.\n */\nexport function buildCheckpointPrompt(state: SessionState, activitySummary: string): string {\n return [\n `${CHECKPOINT_MARKER} End-of-session memory capture for project \\`${state.project}\\`.`,\n \"\",\n \"Tracked activity:\",\n activitySummary.trim() || \"(none recorded)\",\n \"\",\n \"Write OKF entries for anything notable using the `store_*` MCP tools.\",\n \"Entry types:\",\n ...entryTypeBullets(),\n \"\",\n `Set \\`content\\` to ${ENTRY_CONTENT_RULE}, and \\`description\\` to a one-sentence summary.`,\n \"Tag with lowercase-kebab tags (at least one, e.g. architecture/python/testing).\",\n ...ENTRY_SELECTION_RULES,\n \"If nothing is notable, say so explicitly and exit.\",\n ].join(\"\\n\");\n}\n\n/**\n * Decide whether to deliver a checkpoint on `session.idle`.\n * Returns the prompt to deliver, or null to skip.\n */\nexport function idleCheckpoint(\n state: SessionState,\n activitySummary: string,\n): string | null {\n if (!state.hasActivity) return null;\n if (state.checkpointDelivered) return null;\n state.checkpointDelivered = true;\n return buildCheckpointPrompt(state, activitySummary);\n}\n\n/**\n * Decide whether to fire on `experimental.session.compacting`.\n * Per spec: always fires when activity exists, even if checkpoint was delivered.\n */\nexport function compactingCheckpoint(\n state: SessionState,\n activitySummary: string,\n): string | null {\n if (!state.hasActivity) return null;\n return buildCheckpointPrompt(state, activitySummary);\n}\n\n// ── Git commit detection ─────────────────────────────────────────────────\n\nconst GIT_COMMIT_PATTERN = /git\\s+commit\\b/;\n\nexport function isGitCommit(command: string): boolean {\n return GIT_COMMIT_PATTERN.test(command);\n}\n\nexport function buildCommitCheckpointPrompt(state: SessionState): string {\n return [\n `${CHECKPOINT_MARKER} Memory capture after \\`git commit\\` in project \\`${state.project}\\`.`,\n \"\",\n \"Review the staged/committed changes and write OKF entries through the `store_*` MCP tools.\",\n \"Entry types:\",\n ...entryTypeBullets(),\n \"\",\n `Set \\`content\\` to ${ENTRY_CONTENT_RULE}, and \\`description\\` to a one-sentence summary.`,\n \"Tag with lowercase-kebab tags (at least one, e.g. architecture/python/testing).\",\n ...ENTRY_SELECTION_RULES,\n \"If nothing is notable, say so explicitly and exit.\",\n ].join(\"\\n\");\n}\n\n// ── In-process session digest (ctx.llm) ──────────────────────────────────\n\n/** One validated OKF entry ready to be stored through the vault server. */\nexport interface ValidEntry {\n entry_type: ExtractableType\n content: string\n description: string\n tags: string[]\n confidence: number\n openspec_change_id: string | null\n}\n\n/** Optional vault context files to embed in the extraction prompt. */\nexport interface DigestContextFiles {\n criticalFacts?: string\n claude?: string\n}\n\n/**\n * Build the system + user messages for the in-process extraction call.\n * The system part instructs the model to return a JSON array restricted to\n * EXTRACTABLE_TYPES; the user part carries the transcript.\n */\nexport function buildExtractionPrompt(\n project: string,\n transcript: string,\n contextFiles: DigestContextFiles = {},\n): { system: string; user: string } {\n const sysParts: string[] = [\n 'You are an assistant that extracts durable knowledge from a session transcript.',\n ]\n if (contextFiles.criticalFacts?.trim()) {\n sysParts.push(`Always-loaded context: CRITICAL_FACTS.md\\n${contextFiles.criticalFacts.trim()}`)\n }\n if (contextFiles.claude?.trim()) {\n sysParts.push(`Always-loaded context: _CLAUDE.md\\n${contextFiles.claude.trim()}`)\n }\n sysParts.push(\n `For the transcript of project \\`${project}\\`, identify:`,\n ...entryTypeBullets(),\n '',\n 'Return a JSON array. Each element must have exactly:',\n ` - \"entry_type\": one of ${EXTRACTABLE_TYPES.map((t) => `\"${t}\"`).join(' | ')}`,\n ` - \"content\": ${ENTRY_CONTENT_RULE}`,\n ' - \"description\": a one-sentence summary of `content` (queryable)',\n ' - \"tags\": an array of lowercase-kebab tags (never empty if possible, at least 1 like architecture/python/testing)',\n ' - \"confidence\": a number 0.0-1.0',\n ' - \"openspec_change_id\": (optional) the change slug if the transcript names it',\n '',\n 'Rules:',\n ...ENTRY_SELECTION_RULES.map((r) => `- ${r}`),\n '',\n 'Return only the JSON array. No prose, no markdown fences.',\n )\n return {\n system: sysParts.join('\\n'),\n user: `Project: ${project}\\n\\nTranscript:\\n---\\n${transcript}\\n---`,\n }\n}\n\n/**\n * Split an oversized transcript into overlapping chunks (mirrors the legacy\n * digest script: 25k chars per chunk, 1k overlap, at most `cap` chunks).\n */\nexport function chunkTranscript(text: string, maxLen = 50_000, chunkSize = 25_000, overlap = 1_000, cap = 3): string[] {\n if (text.length <= maxLen) return [text]\n const chunks: string[] = []\n let i = 0\n while (i < text.length) {\n chunks.push(text.slice(i, i + chunkSize))\n i += chunkSize - overlap\n if (chunks.length >= cap) break\n }\n return chunks\n}\n\n/** Best-effort repair of common LLM JSON output; returns parsed value or null. */\nexport function repairJson(text: string): unknown {\n const s = text.trim()\n // Strip code fences: ```json ... ```\n const fenced = s.replace(/^```(?:json)?\\s*/i, '').replace(/\\s*```$/, '').trim()\n // Trailing commas: `,]` / `,}` -> `]` / `}`\n const noTrailing = fenced.replace(/,(\\s*[\\]}])/g, '$1')\n const candidates = [s, fenced, noTrailing]\n // Single-quote to double-quote conversion only when no double quotes exist.\n if (noTrailing.includes(\"'\") && !noTrailing.includes('\"')) {\n candidates.push(convertSingleQuotes(noTrailing))\n }\n for (const c of candidates) {\n try {\n return JSON.parse(c)\n } catch {\n // try the next candidate\n }\n }\n return null\n}\n\nfunction convertSingleQuotes(text: string): string {\n let out = ''\n let inString = false\n let i = 0\n while (i < text.length) {\n const ch = text[i]\n if (inString && ch === '\\\\') {\n if (i + 1 < text.length && text[i + 1] === \"'\") {\n out += \"'\"\n i += 2\n continue\n }\n out += ch\n if (i + 1 < text.length) {\n out += text[i + 1]\n i += 2\n continue\n }\n i += 1\n continue\n }\n if (ch === \"'\") {\n inString = !inString\n out += '\"'\n i += 1\n continue\n }\n out += ch\n i += 1\n }\n return out\n}\n\n/**\n * Validate a parsed extraction payload into OKF entries. Unknown types,\n * empty content and malformed values are dropped; tags and confidence are\n * normalized. Returns the valid entries (possibly empty).\n */\nexport function validateEntries(payload: unknown): ValidEntry[] {\n if (!Array.isArray(payload)) return []\n const valid: ValidEntry[] = []\n for (const item of payload) {\n if (typeof item !== 'object' || item === null) continue\n const raw = item as Record<string, unknown>\n const entryType = raw.entry_type\n if (typeof entryType !== 'string' || !(EXTRACTABLE_TYPES as readonly string[]).includes(entryType)) continue\n const content = typeof raw.content === 'string' ? raw.content.trim() : ''\n if (!content) continue\n const tags = Array.isArray(raw.tags) ? raw.tags.filter((t): t is string => typeof t === 'string' && t.length > 0) : []\n let confidence = 1\n if (typeof raw.confidence === 'number' && Number.isFinite(raw.confidence)) {\n confidence = Math.max(0, Math.min(1, raw.confidence))\n }\n const description = typeof raw.description === 'string' ? raw.description.trim() : ''\n const changeId = typeof raw.openspec_change_id === 'string' && raw.openspec_change_id ? raw.openspec_change_id : null\n valid.push({\n entry_type: entryType as ExtractableType,\n content,\n description,\n tags,\n confidence,\n openspec_change_id: changeId,\n })\n }\n return valid\n}\n\n// The full hook wiring is exposed for testing; the actual OpenCode integration\n// is done in `register.ts` (the entry point the OpenCode runtime loads via\n// package.json \"main\"). This module intentionally has no default export:\n// opencode 1.18.x only loads plugin modules with a single export.\nexport const __testing = {\n createSessionState,\n isSupportedVersion,\n resolveProjectName,\n idleCheckpoint,\n compactingCheckpoint,\n isGitCommit,\n buildCheckpointPrompt,\n buildCommitCheckpointPrompt,\n};\n","import { spawn, type ChildProcess } from 'node:child_process'\nimport { existsSync } from 'node:fs'\nimport { join } from 'node:path'\nimport readline from 'node:readline'\nimport { BlockAssembler, createUserMessage } from '@deepseek-ai/dsh-llm'\nimport type { FinishReason, GenerateOptions, Message } from '@deepseek-ai/dsh-llm'\nimport type { Context } from '@deepseek-ai/cordis'\nimport {\n buildExtractionPrompt,\n chunkTranscript,\n repairJson,\n validateEntries,\n type ValidEntry,\n} from './pure.js'\n\n// Post-session digest runner: in-process LLM extraction via the harness's own\n// `ctx.llm` service (no external CLI, no credentials of its own), followed by\n// OKF writes through the vault's MCP server over stdio (`store_*` tools keep\n// the Markdown source of truth and the SQLite FTS5 index in sync).\n\nconst MIN_DIGEST_TRANSCRIPT_CHARS = 200\nconst LLM_RETRIES = 3\nconst LLM_RETRY_DELAYS_MS = [2_000, 5_000]\nconst MCP_CALL_TIMEOUT_MS = 60_000\n\nfunction log(msg: string) {\n console.log(`[memory-auto] ${msg}`)\n}\n\nexport function transcriptOfDSM(events: any[]): string {\n // DSH SessionEvent -> transcript\n return (events ?? [])\n .map((ev: any) => {\n const t = ev?.type ?? ''\n const d = ev?.data ?? ev\n if (t === 'user/message' || t === 'user_message') {\n const text = typeof d.text === 'string' ? d.text : typeof d.content === 'string' ? d.content : JSON.stringify(d).slice(0, 500)\n return `## user\\n${text}`\n }\n if (t === 'assistant/message' || t === 'assistant_message') {\n const text = typeof d.text === 'string' ? d.text : typeof d.content === 'string' ? d.content : ''\n return text ? `## assistant\\n${text}` : null\n }\n if (t === 'tool/call' || t === 'tool_call') {\n const name = d.tool ?? d.name ?? 'tool'\n const args = d.args ?? d.arguments ?? {}\n return `## tool_call ${name}\\n${JSON.stringify(args).slice(0, 1000)}`\n }\n if (t === 'tool/result' || t === 'tool_result') {\n const out = typeof d.output === 'string' ? d.output : JSON.stringify(d).slice(0, 1000)\n return `## tool_result\\n${out}`\n }\n if (t.startsWith('compaction')) return `## ${t}\\n${JSON.stringify(d).slice(0, 500)}`\n return null\n })\n .filter((x): x is string => Boolean(x))\n .join('\\n\\n')\n}\n\n/** Translate a terminal stream finish into a thrown error, mirroring dsh's own summarizers. */\nfunction finishError(finish: FinishReason): Error | undefined {\n switch (finish.kind) {\n case 'error':\n case 'aborted': {\n const error = new Error(finish.failure.message) as Error & { code?: string }\n error.code = finish.failure.code\n return error\n }\n default:\n return undefined\n }\n}\n\nexport interface DigestConfig {\n memoryPath: string\n serverDir: string\n provider: string\n model: string\n maxTokens: number\n minTranscriptChars: number\n}\n\n/**\n * Run one extraction call through the harness's `ctx.llm` service: build the\n * prompt, stream, assemble, repair and validate the JSON entries.\n */\nexport async function extractEntriesWithLlm(\n ctx: Context,\n config: DigestConfig,\n project: string,\n transcript: string,\n contextFiles?: { criticalFacts?: string; claude?: string },\n signal?: AbortSignal,\n): Promise<ValidEntry[]> {\n const { system, user } = buildExtractionPrompt(project, transcript, contextFiles)\n const assembler = new BlockAssembler()\n const messages: Message[] = [\n createUserMessage({\n content: [{ type: 'text', text: user }],\n source: { kind: 'plugin', plugin: 'memory-auto' },\n }),\n ]\n const options: GenerateOptions = {\n provider: config.provider,\n model: config.model,\n messages,\n system,\n maxTokens: config.maxTokens,\n ...(signal === undefined ? {} : { signal }),\n }\n for await (const chunk of ctx.llm.stream(options)) assembler.push(chunk)\n const error = finishError(assembler.finish)\n if (error !== undefined) throw error\n const text = assembler\n .blocks()\n .filter((b) => b.type === 'text')\n .map((b) => b.text)\n .join('')\n const parsed = repairJson(text)\n return validateEntries(parsed)\n}\n\n/** Minimal MCP stdio client for the vault server (spawned via its launcher: uv, pip venv fallback). */\nexport interface McpClient {\n callTool(name: string, args: Record<string, unknown>, timeoutMs?: number): Promise<unknown>\n close(): Promise<void>\n}\n\nexport function connectMcp(memoryPath: string, serverDir: string): Promise<McpClient> {\n return new Promise((resolve, reject) => {\n // Single decision point: the server bundle's launcher.mjs picks `uv run`\n // or a pip-managed .venv. Legacy hand-made server dirs without a launcher\n // keep the old direct `uv run` spawn.\n const launcher = join(serverDir, 'launcher.mjs')\n const [command, args] = existsSync(launcher)\n ? [process.execPath, [launcher]]\n : ['uv', ['run', '--directory', serverDir, 'python', 'server.py']]\n const child: ChildProcess = spawn(\n command,\n args,\n {\n env: { ...process.env, MEMORY_PATH: memoryPath, UV_CACHE_DIR: process.env.UV_CACHE_DIR ?? '/tmp/uv-cache' },\n stdio: ['pipe', 'pipe', 'inherit'],\n },\n )\n const pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void; timer: NodeJS.Timeout }>()\n let nextId = 1\n let closed = false\n\n const failAll = (err: Error) => {\n for (const [, p] of pending) {\n clearTimeout(p.timer)\n p.reject(err)\n }\n pending.clear()\n }\n\n child.on('error', (err) => {\n closed = true\n failAll(new Error(`memory-vault-server spawn failed: ${err.message}`))\n reject(err)\n })\n child.on('exit', (code) => {\n if (closed) return\n closed = true\n failAll(new Error(`memory-vault-server exited unexpectedly (code ${code})`))\n reject(new Error(`memory-vault-server exited before initialize (code ${code})`))\n })\n\n const rl = readline.createInterface({ input: child.stdout!, crlfDelay: Infinity })\n rl.on('line', (line) => {\n let msg: any\n try {\n msg = JSON.parse(line)\n } catch {\n return\n }\n if (typeof msg?.id === 'number') {\n const p = pending.get(msg.id)\n if (!p) return\n pending.delete(msg.id)\n clearTimeout(p.timer)\n if (msg.error) p.reject(new Error(`MCP error: ${msg.error.message ?? JSON.stringify(msg.error)}`))\n else p.resolve(msg.result)\n }\n })\n\n const send = (method: string, params: unknown, timeoutMs: number = MCP_CALL_TIMEOUT_MS): Promise<unknown> =>\n new Promise((res, rej) => {\n if (closed || !child.stdin?.writable) {\n rej(new Error('memory-vault-server is not running'))\n return\n }\n const id = nextId++\n const timer = setTimeout(() => {\n pending.delete(id)\n rej(new Error(`MCP call ${method} timed out after ${timeoutMs}ms`))\n }, timeoutMs)\n pending.set(id, { resolve: res, reject: rej, timer })\n child.stdin!.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\\n')\n })\n\n const notify = (method: string, params: unknown) => {\n if (!closed && child.stdin?.writable) {\n child.stdin.write(JSON.stringify({ jsonrpc: '2.0', method, params }) + '\\n')\n }\n }\n\n // initialize handshake (notifications are fire-and-forget: no id, no reply)\n void send('initialize', {\n protocolVersion: '2025-06-18',\n capabilities: {},\n clientInfo: { name: 'memory-auto', version: '0.1.0' },\n })\n .then(() => {\n notify('notifications/initialized', {})\n })\n .then(() => {\n if (closed) throw new Error('memory-vault-server closed during handshake')\n resolve({\n callTool: (name, args, timeoutMs = MCP_CALL_TIMEOUT_MS) =>\n send('tools/call', { name, arguments: args }, timeoutMs).then((result: any) => {\n if (result?.isError) {\n const text = Array.isArray(result.content) ? result.content.map((c: any) => c?.text ?? '').join('') : JSON.stringify(result)\n throw new Error(`tool ${name} failed: ${text}`)\n }\n return result\n }),\n close: async () => {\n if (closed) return\n closed = true\n for (const [, p] of pending) clearTimeout(p.timer)\n pending.clear()\n if (child.exitCode !== null) return\n child.kill()\n await new Promise((r) => child.once('exit', r))\n },\n })\n })\n .catch((err) => {\n closed = true\n child.kill()\n reject(err)\n })\n })\n}\n\n/**\n * Write validated OKF entries through the vault server's `store_*` tools,\n * which upsert both the Markdown file and the SQLite FTS5 index.\n */\nexport async function writeEntries(client: McpClient, project: string, entries: ValidEntry[]): Promise<{ upserted: number; failed: number }> {\n let upserted = 0\n let failed = 0\n for (const e of entries) {\n try {\n await client.callTool(`store_${e.entry_type}`, {\n project,\n content: e.content,\n ...(e.description ? { description: e.description } : {}),\n tags: e.tags,\n // Only store_fact accepts and persists confidence. Sending it for every\n // type silently discarded it, so send it where it actually lands.\n ...(e.entry_type === 'fact' ? { confidence: e.confidence } : {}),\n ...(e.openspec_change_id ? { openspec_change_id: e.openspec_change_id } : {}),\n })\n upserted += 1\n } catch (err) {\n failed += 1\n console.warn(`[memory-auto] store_${e.entry_type} failed:`, err instanceof Error ? err.message : err)\n }\n }\n return { upserted, failed }\n}\n\n/**\n * Full post-session digest: transcript -> ctx.llm extraction (with retries) ->\n * OKF writes via the vault MCP server. Never throws; logs the outcome.\n */\nexport async function digestSessionDSM(\n ctx: Context,\n config: DigestConfig,\n sessionId: string,\n directory: string,\n project: string,\n events: any[],\n signal?: AbortSignal,\n): Promise<void> {\n const header = `## context\\nproject: ${project}\\ndirectory: ${directory}\\n`\n const transcript = (header + transcriptOfDSM(events)).trim()\n if (!transcript) {\n log(`digest skip ${sessionId}: empty`)\n return\n }\n if (transcript.length < (config.minTranscriptChars ?? MIN_DIGEST_TRANSCRIPT_CHARS)) {\n log(`digest skip ${sessionId}: too short ${transcript.length}`)\n return\n }\n\n const chunks = chunkTranscript(transcript)\n const entries: ValidEntry[] = []\n for (const chunk of chunks) {\n let attempt = 0\n for (;;) {\n try {\n const got = await extractEntriesWithLlm(ctx, config, project, chunk, undefined, signal)\n entries.push(...got)\n break\n } catch (err) {\n attempt += 1\n if (attempt >= LLM_RETRIES || signal?.aborted) {\n console.warn(`[memory-auto] digest extraction failed after ${attempt} attempt(s):`, err instanceof Error ? err.message : err)\n break\n }\n const delay = LLM_RETRY_DELAYS_MS[attempt - 1] ?? 5_000\n log(`digest extraction retry ${attempt}/${LLM_RETRIES} in ${delay}ms`)\n await new Promise((r) => setTimeout(r, delay))\n }\n }\n if (signal?.aborted) break\n }\n\n if (entries.length === 0) {\n log(`digest ${sessionId}: no entries extracted`)\n return\n }\n\n let client: McpClient\n try {\n client = await connectMcp(config.memoryPath, config.serverDir)\n } catch (err) {\n console.warn(`[memory-auto] digest ${sessionId}: cannot reach vault server:`, err instanceof Error ? err.message : err)\n return\n }\n try {\n const { upserted, failed } = await writeEntries(client, project, entries)\n log(`digest ${sessionId}: ${upserted} upserted, ${failed} failed (${entries.length} extracted)`)\n } finally {\n await client.close().catch(() => {})\n }\n}\n","/**\n * Harness wiring for `memory-auto`. Registers exactly these hooks:\n *\n * - `session/created` resolve the project name for the session\n * - `session/disposed` digest the transcript\n * - `agent/status` (idle) auto-capture gate\n * - `session/event` activity tracking; `tool/call` with a\n * `git commit` command and `compaction/start`\n * queue checkpoints\n * - `agent/pre-step` deliver the queued checkpoint to the agent\n * - `ctx.effect` dispose batch-digest sessions still pending\n *\n * The agent writes the entries; this plugin only prompts it.\n */\nimport { cpSync, existsSync, mkdirSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { dirname, isAbsolute, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport type { Context } from '@deepseek-ai/cordis'\nimport Schema from '@deepseek-ai/schemastery'\nimport {\n createSessionState,\n resolveProjectName,\n idleCheckpoint,\n compactingCheckpoint,\n isGitCommit,\n buildCheckpointPrompt,\n buildCommitCheckpointPrompt,\n type SessionState,\n} from './pure.js'\nimport { digestSessionDSM, type DigestConfig } from './digest.js'\n\nexport const name = 'memory-auto'\n\nexport interface Config {\n memoryPath: string\n serverDir: string\n provider: string\n model: string\n maxTokens: number\n minTranscriptChars: number\n enabled: boolean\n}\n\nexport const Config: Schema<Config> = Schema.object({\n memoryPath: Schema.string().default(process.env.DSH_MEMORY_PATH ?? ''),\n serverDir: Schema.string().default(process.env.DSH_MEMORY_SERVER_DIR ?? ''),\n provider: Schema.string().default('deepseek-official'),\n model: Schema.string().default('deepseek-v4-flash'),\n maxTokens: Schema.number().default(2048),\n minTranscriptChars: Schema.number().default(200),\n enabled: Schema.boolean().default(true),\n})\n\n/** Requires the harness LLM service: extraction runs in-process via ctx.llm. */\nexport const inject = ['llm']\n\n/**\n * Resolve the harness home the same way the harness does (`$DSH_HOME`, or\n * `~/.dsh`). Paths must never depend on the launch cwd: DSH does not chdir.\n */\nfunction dshHome(): string {\n const env = process.env.DSH_HOME?.trim()\n return env && env.length > 0 ? env : join(homedir(), '.dsh')\n}\n\n/** Absolute paths stay; empty/relative values resolve under the harness home. */\nfunction resolveUnderHome(value: string, fallbackSegment: string): string {\n const v = value.trim()\n if (v.length === 0) return join(dshHome(), fallbackSegment)\n return isAbsolute(v) ? v : join(dshHome(), v)\n}\n\nconst packageRoot = dirname(dirname(fileURLToPath(import.meta.url)))\n\n/** Copy the bundled dir into `target` when `key` is missing there. */\nfunction ensure(target: string, bundled: string, key: string): boolean {\n if (existsSync(join(target, key))) return false\n if (!existsSync(bundled)) return false\n mkdirSync(target, { recursive: true })\n cpSync(bundled, target, { recursive: true })\n return true\n}\n\n/** Copy one bundled file into `target` when missing (upgrades add files 0.1.1 → 0.1.2). */\nfunction ensureFile(target: string, bundled: string, file: string): boolean {\n const dest = join(target, file)\n if (existsSync(dest)) return false\n const src = join(bundled, file)\n if (!existsSync(src)) return false\n mkdirSync(target, { recursive: true })\n cpSync(src, dest)\n return true\n}\n\n// DSH session shape minimal\ntype DSHEvt = any\ntype DSHSession = { id: string; events: DSHEvt[]; cwd?: string }\n\nexport function apply(ctx: Context, config: Config) {\n if (!config.enabled) {\n console.log('[memory-auto] disabled via config')\n return\n }\n\n const memoryPath = resolveUnderHome(config.memoryPath, 'memory-vault')\n const serverDir = resolveUnderHome(config.serverDir, 'memory-vault-server')\n\n // Self-contained install: first boot copies the bundled server and vault\n // starter under the harness home when they are missing.\n if (ensure(serverDir, join(packageRoot, 'server'), 'server.py')) {\n console.log(`[memory-auto] installed memory-vault-server -> ${serverDir}`)\n }\n if (ensure(memoryPath, join(packageRoot, 'vault'), 'type-registry.yaml')) {\n console.log(`[memory-auto] installed vault starter -> ${memoryPath}`)\n }\n // launcher.mjs runs the server via uv or the pip-venv fallback; upgrades of\n // existing installs (server.py already present) still need the new files.\n const bundledServer = join(packageRoot, 'server')\n for (const file of ['launcher.mjs', 'requirements.txt']) {\n if (ensureFile(serverDir, bundledServer, file)) {\n console.log(`[memory-auto] installed ${file} -> ${serverDir}`)\n }\n }\n\n const digestConfig: DigestConfig = {\n memoryPath,\n serverDir,\n provider: config.provider,\n model: config.model,\n maxTokens: config.maxTokens,\n minTranscriptChars: config.minTranscriptChars,\n }\n\n if (!existsSync(join(serverDir, 'server.py'))) {\n console.warn(\n `[memory-auto] vault server not found at ${serverDir} and not bundled — the digest cannot write. ` +\n 'Set DSH_MEMORY_SERVER_DIR (or run `node scripts/bundle-assets.mjs` in a checkout).',\n )\n }\n if (!existsSync(join(memoryPath, 'type-registry.yaml'))) {\n console.warn(`[memory-auto] vault starter not found at ${memoryPath} — searches will fail until it exists.`)\n }\n\n const states = new Map<string, SessionState>()\n const activities = new Map<string, string[]>()\n const queued = new Map<string, string>()\n const sessionDirs = new Map<string, string>()\n const projectCache = new Map<string, string>()\n\n const ACTIVITY_MAX_LINES = 20\n const ACTIVITY_MAX_CHARS = 80\n\n const track = (sessionId: string, line: string) => {\n const st = states.get(sessionId)\n if (!st) return\n st.hasActivity = true\n const list = activities.get(sessionId) ?? []\n if (!list.includes(line)) {\n list.push(line)\n if (list.length > ACTIVITY_MAX_LINES) list.shift()\n activities.set(sessionId, list)\n }\n }\n\n const summary = (sid: string) => (activities.get(sid) ?? []).join('\\n')\n\n const projectFor = async (dir: string): Promise<string> => {\n const cached = projectCache.get(dir)\n if (cached) return cached\n const p = await resolveProjectName(dir || process.cwd())\n projectCache.set(dir, p)\n return p\n }\n\n // session/created -> init state\n ctx.on('session/created', async (session: DSHSession) => {\n const sid = (session as any)?.id ?? (session as any)?.sessionId\n const dir = (session as any)?.cwd ?? (session as any)?.directory ?? ''\n if (!sid) return\n const proj = await projectFor(dir)\n if (!states.has(sid)) {\n states.set(sid, createSessionState(proj))\n activities.set(sid, [])\n }\n sessionDirs.set(sid, dir)\n console.log(`[memory-auto] session created ${sid} project=${proj}`)\n })\n\n // session/disposed -> digest\n ctx.on('session/disposed', async (session: DSHSession) => {\n const sid = (session as any)?.id ?? (session as any)?.sessionId\n if (!sid) return\n const st = states.get(sid)\n if (!st) return\n if (!st.hasActivity) {\n console.log(`[memory-auto] digest skip ${sid}: no activity`)\n return\n }\n const dir = sessionDirs.get(sid) ?? ''\n const proj = await projectFor(dir)\n const evts: DSHEvt[] = (session as any)?.events ?? []\n await digestSessionDSM(ctx, digestConfig, sid, dir, proj, evts)\n })\n\n // agent/status idle -> in-session capture gate\n ctx.on('agent/status', async (payload: any) => {\n const agent = payload?.agent\n const status = payload?.status ?? payload?.agentStatus\n if (status !== 'idle') return\n const sid: string | undefined = agent?.sessionId ?? payload?.sessionId ?? agent?.id\n if (!sid) return\n const st = states.get(sid)\n if (!st) return\n const text = idleCheckpoint(st, summary(sid))\n if (text) {\n console.log(`[memory-auto] idle checkpoint digest for ${sid}`)\n const dir = sessionDirs.get(sid) ?? ''\n const proj = await projectFor(dir)\n const fakeEvents = [{ type: 'user/message', data: { text: summary(sid) } }]\n await digestSessionDSM(ctx, digestConfig, sid, dir, proj, fakeEvents)\n }\n })\n\n // session/event -> git commit detect + compaction start\n ctx.on('session/event', async (session: DSHSession, event: DSHEvt) => {\n const sid = (session as any)?.id ?? (session as any)?.sessionId ?? (event as any)?.sessionId\n if (!sid) return\n const t = event?.type ?? ''\n const d = event?.data ?? {}\n\n // compaction/start -> checkpoint injection (delivered on the next pre-step)\n if (t === 'compaction/start') {\n const st = states.get(sid)\n if (!st) return\n const txt = compactingCheckpoint(st, summary(sid))\n if (txt) {\n console.log(`[memory-auto] compaction checkpoint queued for ${sid}`)\n queued.set(sid, txt)\n }\n return\n }\n\n // tool/call -> git commit detection + activity track\n if (t === 'tool/call' || t === 'tool_call') {\n const cmd = d?.args?.command ?? d?.command ?? ''\n if (typeof cmd === 'string' && isGitCommit(cmd)) {\n const st = states.get(sid)\n if (st) {\n st.hasActivity = true\n queued.set(sid, buildCommitCheckpointPrompt(st))\n console.log(`[memory-auto] git commit queued for ${sid}`)\n }\n return\n }\n if (typeof cmd === 'string') {\n track(sid, `bash: ${cmd.trim().slice(0, ACTIVITY_MAX_CHARS)}`)\n }\n return\n }\n if (t === 'tool/result') {\n // ignore\n return\n }\n if (t === 'user/message' || t === 'assistant/message') {\n // track activity\n track(sid, `${t}: ${(d?.text ?? '').slice(0, ACTIVITY_MAX_CHARS)}`)\n }\n })\n\n // agent/pre-step Waterfall -> deliver queued checkpoint\n ctx.on('agent/pre-step', async (payload: any, next: any) => {\n const sid: string | undefined = payload?.agent?.sessionId ?? payload?.sessionId\n if (sid) {\n const q = queued.get(sid)\n if (q) {\n queued.delete(sid)\n // Best effort: append the checkpoint as user context\n if (Array.isArray(payload?.context)) payload.context.push(q)\n else if (Array.isArray(payload?.messages)) payload.messages.push({ role: 'user', content: q })\n else console.log(`[memory-auto] deliver queued checkpoint for ${sid}`)\n }\n }\n return next()\n })\n\n // dispose -> batch digest remaining sessions\n ctx.effect(() => {\n return () => {\n console.log(`[memory-auto] dispose batch ${sessionDirs.size} sessions`)\n // fire-and-forget digest for each remaining session\n for (const [sid, dir] of sessionDirs) {\n const st = states.get(sid)\n if (!st?.hasActivity) continue\n projectFor(dir).then((proj) => {\n const fakeEvents = [{ type: 'user/message', data: { text: summary(sid) } }]\n void digestSessionDSM(ctx, digestConfig, sid, dir, proj, fakeEvents)\n })\n }\n }\n })\n\n console.log(`[memory-auto] active memoryPath=${memoryPath} serverDir=${serverDir} llm=${config.provider}/${config.model}`)\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@luisarg/memory-auto",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Auto-captura memoria DSH (idle, commit, compaction, dispose)",
|
|
6
6
|
"repository": {
|
|
@@ -30,20 +30,20 @@
|
|
|
30
30
|
"build": "tsdown",
|
|
31
31
|
"dev": "tsdown --watch",
|
|
32
32
|
"test": "vitest run --passWithNoTests",
|
|
33
|
-
"prepare": "tsdown
|
|
33
|
+
"prepare": "tsdown",
|
|
34
34
|
"typecheck": "tsc --noEmit"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@deepseek-ai/cordis": "4.0.
|
|
38
|
-
"@deepseek-ai/schemastery": "3.18.
|
|
37
|
+
"@deepseek-ai/cordis": "4.0.2",
|
|
38
|
+
"@deepseek-ai/schemastery": "3.18.2"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"@deepseek-ai/dsh-llm": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"typescript": "^
|
|
45
|
-
"tsdown": "^0.
|
|
46
|
-
"vitest": "^
|
|
47
|
-
"@deepseek-ai/dsh-llm": "0.1.0-rc.
|
|
44
|
+
"typescript": "^7.0.2",
|
|
45
|
+
"tsdown": "^0.23.0",
|
|
46
|
+
"vitest": "^5.0.0",
|
|
47
|
+
"@deepseek-ai/dsh-llm": "0.1.0-rc.8"
|
|
48
48
|
}
|
|
49
49
|
}
|
package/server/launcher.mjs
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { spawn, spawnSync } from 'node:child_process'
|
|
14
14
|
import { existsSync } from 'node:fs'
|
|
15
|
+
import { tmpdir } from 'node:os'
|
|
15
16
|
import { dirname, join } from 'node:path'
|
|
16
17
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
17
18
|
|
|
@@ -57,7 +58,10 @@ function main() {
|
|
|
57
58
|
const withUv = hasUv()
|
|
58
59
|
const { command, args } = resolveRunner(withUv)
|
|
59
60
|
if (!withUv) ensurePipEnv()
|
|
60
|
-
|
|
61
|
+
// Default the uv cache under the OS temp dir (Windows has no /tmp); an
|
|
62
|
+
// explicit UV_CACHE_DIR from the patch layer or env still wins.
|
|
63
|
+
const env = { UV_CACHE_DIR: join(tmpdir(), 'uv-cache'), ...process.env }
|
|
64
|
+
const child = spawn(command, args, { cwd: DIR, env, stdio: 'inherit' })
|
|
61
65
|
for (const sig of ['SIGTERM', 'SIGINT']) process.on(sig, () => child.kill(sig))
|
|
62
66
|
child.on('error', (err) => {
|
|
63
67
|
console.error(`[memory-vault-server] spawn failed (${command}): ${err.message}`)
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Rebuild the SQLite FTS5 index from the OKF Markdown bundle (index-only).
|
|
2
|
+
|
|
3
|
+
Walks `projects/<project>/<type>/*.md` and `raw/*.md`, parses each file and
|
|
4
|
+
inserts rows directly into SQLite. Markdown stays the source of truth: this
|
|
5
|
+
script never rewrites .md files (upsert_entry's write path would duplicate
|
|
6
|
+
them with today's date on a fresh DB).
|
|
7
|
+
|
|
8
|
+
Idempotent: row ids derive from file paths, dedup keys from content, so
|
|
9
|
+
re-running converges. Run after importing a bundle into a new vault dir
|
|
10
|
+
(memory.db* may be deleted first; the script also works on a populated DB):
|
|
11
|
+
|
|
12
|
+
uv run --directory memory-vault-server python rebuild_index.py \
|
|
13
|
+
--memory-path /path/to/vault
|
|
14
|
+
|
|
15
|
+
DB-only data (profiles) is NOT covered — migrate profiles separately.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import hashlib
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
_store_mod = None # set in main() once MEMORY_PATH points at the target bundle
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _id_for(relpath: Path) -> str:
|
|
30
|
+
"""Stable row id derived from the file path (idempotent re-runs)."""
|
|
31
|
+
digest = hashlib.sha1(str(relpath).encode()).hexdigest()[:24]
|
|
32
|
+
return f"rebuild-{digest}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _collect(memory_path: Path):
|
|
36
|
+
"""Walk the bundle -> (entries, errors); entries are
|
|
37
|
+
(singular, project, relpath, parsed) tuples, DB-only types skipped."""
|
|
38
|
+
entries: list[tuple[str, str, Path, dict]] = []
|
|
39
|
+
errors: list[str] = []
|
|
40
|
+
dir_to_singular = _store_mod._DIR_TO_SINGULAR
|
|
41
|
+
parse = _store_mod._parse_okf_file
|
|
42
|
+
|
|
43
|
+
def handle(path: Path, parsed: dict | None, default_project: str, singular: str):
|
|
44
|
+
if parsed is None:
|
|
45
|
+
errors.append(f"unparseable: {path.relative_to(memory_path)}")
|
|
46
|
+
return
|
|
47
|
+
# Defensive: trust the directory, not the parsed label.
|
|
48
|
+
parsed["entry_type"] = singular
|
|
49
|
+
parsed["project"] = parsed.get("project") or default_project
|
|
50
|
+
entries.append((singular, parsed["project"], path.relative_to(memory_path), parsed))
|
|
51
|
+
|
|
52
|
+
projects_dir = memory_path / "projects"
|
|
53
|
+
if projects_dir.is_dir():
|
|
54
|
+
# Projects may nest (e.g. `@deepseek-ai/dsh-root`): any directory whose
|
|
55
|
+
# name is a type dir is indexed; its project is the dir path below
|
|
56
|
+
# `projects/`, joined with "/".
|
|
57
|
+
def scan(prefix: Path, project: str):
|
|
58
|
+
for d in sorted(child for child in prefix.iterdir() if child.is_dir()):
|
|
59
|
+
singular = dir_to_singular.get(d.name)
|
|
60
|
+
if singular is None:
|
|
61
|
+
if d.name != ".obsidian":
|
|
62
|
+
scan(d, f"{project}/{d.name}" if project else d.name)
|
|
63
|
+
continue
|
|
64
|
+
if singular == "profile":
|
|
65
|
+
continue
|
|
66
|
+
for f in sorted(d.glob("*.md")):
|
|
67
|
+
if f.name == "index.md":
|
|
68
|
+
continue
|
|
69
|
+
handle(f, parse(f), project, singular)
|
|
70
|
+
|
|
71
|
+
for proj_dir in sorted(p for p in projects_dir.iterdir() if p.is_dir()):
|
|
72
|
+
scan(proj_dir, proj_dir.name)
|
|
73
|
+
|
|
74
|
+
raw_dir = memory_path / "raw"
|
|
75
|
+
if raw_dir.is_dir():
|
|
76
|
+
for f in sorted(raw_dir.glob("*.md")):
|
|
77
|
+
handle(f, parse(f), "", "source")
|
|
78
|
+
|
|
79
|
+
return entries, errors
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def rebuild(memory_path: Path) -> tuple[int, int, list[str]]:
|
|
83
|
+
"""Insert all bundle entries into the index -> (inserted, updated, errors)."""
|
|
84
|
+
store = _store_mod.MemoryStore(storage_path=memory_path)
|
|
85
|
+
store.initialize()
|
|
86
|
+
|
|
87
|
+
entries, errors = _collect(memory_path)
|
|
88
|
+
if not entries:
|
|
89
|
+
print("No entries found in bundle; nothing to do.", file=sys.stderr)
|
|
90
|
+
return 0, 0, errors
|
|
91
|
+
|
|
92
|
+
n_inserted = n_updated = 0
|
|
93
|
+
for singular, project, relpath, e in entries:
|
|
94
|
+
try:
|
|
95
|
+
dedup_key = (
|
|
96
|
+
store._make_dedup_key(singular, project, e["content"])
|
|
97
|
+
if singular != "source"
|
|
98
|
+
else _id_for(relpath)
|
|
99
|
+
)
|
|
100
|
+
existing = store.db.execute(
|
|
101
|
+
"SELECT id FROM entries WHERE dedup_key = ?", (dedup_key,)
|
|
102
|
+
).fetchone()
|
|
103
|
+
if existing:
|
|
104
|
+
if existing["id"] != _id_for(relpath):
|
|
105
|
+
# Same content already indexed (bundle duplicates collapse):
|
|
106
|
+
# adopt this file's id so re-runs converge, keep row content.
|
|
107
|
+
store.db.execute(
|
|
108
|
+
"UPDATE entries SET id = ?, created_at = ?, updated_at = ? WHERE dedup_key = ?",
|
|
109
|
+
(_id_for(relpath), e["created_at"], e["updated_at"], dedup_key),
|
|
110
|
+
)
|
|
111
|
+
store.db.commit()
|
|
112
|
+
n_updated += 1
|
|
113
|
+
continue
|
|
114
|
+
store._insert_row(
|
|
115
|
+
_id_for(relpath),
|
|
116
|
+
singular,
|
|
117
|
+
project,
|
|
118
|
+
e["content"],
|
|
119
|
+
e["tags"],
|
|
120
|
+
float(e["confidence"] or 1.0),
|
|
121
|
+
e["openspec_change_id"],
|
|
122
|
+
dedup_key,
|
|
123
|
+
e["created_at"],
|
|
124
|
+
e["updated_at"],
|
|
125
|
+
)
|
|
126
|
+
n_inserted += 1
|
|
127
|
+
except Exception as exc: # noqa: BLE001 — one bad file must not stop the rebuild
|
|
128
|
+
errors.append(f"failed: {project}/{relpath}: {exc}")
|
|
129
|
+
return n_inserted, n_updated, errors
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def regen_missing_indexes(memory_path: Path, store) -> int:
|
|
133
|
+
"""Regenerate `<project>/index.md` only where it is missing (nested too)."""
|
|
134
|
+
projects_dir = memory_path / "projects"
|
|
135
|
+
if not projects_dir.is_dir():
|
|
136
|
+
return 0
|
|
137
|
+
n = 0
|
|
138
|
+
|
|
139
|
+
def scan(prefix: Path):
|
|
140
|
+
for d in sorted(child for child in prefix.iterdir() if child.is_dir()):
|
|
141
|
+
if d.name in _store_mod._DIR_TO_SINGULAR or d.name == ".obsidian":
|
|
142
|
+
continue # type dir or editor cache — not a project
|
|
143
|
+
if not (d / "index.md").exists():
|
|
144
|
+
project = str(d.relative_to(projects_dir)).replace(os.sep, "/")
|
|
145
|
+
store._regenerate_project_index(project)
|
|
146
|
+
n += 1
|
|
147
|
+
scan(d)
|
|
148
|
+
|
|
149
|
+
for proj_dir in sorted(p for p in projects_dir.iterdir() if p.is_dir()):
|
|
150
|
+
if not (proj_dir / "index.md").exists():
|
|
151
|
+
store._regenerate_project_index(proj_dir.name)
|
|
152
|
+
n += 1
|
|
153
|
+
scan(proj_dir)
|
|
154
|
+
return n
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def main(argv: list[str] | None = None) -> int:
|
|
158
|
+
global _store_mod
|
|
159
|
+
parser = argparse.ArgumentParser(description="Rebuild SQLite index from OKF bundle")
|
|
160
|
+
parser.add_argument(
|
|
161
|
+
"--memory-path",
|
|
162
|
+
default=None,
|
|
163
|
+
help="OKF bundle directory (default: $MEMORY_PATH or repo memory-vault)",
|
|
164
|
+
)
|
|
165
|
+
args = parser.parse_args(argv)
|
|
166
|
+
|
|
167
|
+
# store.py builds its type maps from MEMORY_PATH at import time — point it
|
|
168
|
+
# at the target bundle before importing.
|
|
169
|
+
if args.memory_path:
|
|
170
|
+
os.environ["MEMORY_PATH"] = str(Path(args.memory_path).resolve())
|
|
171
|
+
import store as _store_mod
|
|
172
|
+
|
|
173
|
+
memory_path = Path(os.environ.get("MEMORY_PATH", "")).resolve()
|
|
174
|
+
if not memory_path.is_dir():
|
|
175
|
+
print(f"error: {memory_path} is not a directory", file=sys.stderr)
|
|
176
|
+
return 2
|
|
177
|
+
|
|
178
|
+
print(f"Rebuilding index from {memory_path} ...")
|
|
179
|
+
n_ins, n_upd, errors = rebuild(memory_path)
|
|
180
|
+
store = _store_mod.MemoryStore(storage_path=memory_path)
|
|
181
|
+
store.initialize()
|
|
182
|
+
n_index = regen_missing_indexes(memory_path, store)
|
|
183
|
+
print(f"Inserted: {n_ins}, already-indexed: {n_upd}, index.md regenerated: {n_index}")
|
|
184
|
+
for err in errors:
|
|
185
|
+
print(f" {err}", file=sys.stderr)
|
|
186
|
+
print(f"Errors: {len(errors)}")
|
|
187
|
+
return 0 if not errors else 1
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
if __name__ == "__main__":
|
|
191
|
+
raise SystemExit(main())
|
package/server/server.py
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
"""MCP server exposing memory store tools via the Model Context Protocol.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Ten tools (per openspec/specs/memory-mcp-server/spec.md):
|
|
4
4
|
search_memory, store_decision, store_fact, store_learning,
|
|
5
|
-
store_convention, store_profile,
|
|
5
|
+
store_convention, store_profile, store_source, export_memories,
|
|
6
|
+
get_profile, ping.
|
|
7
|
+
|
|
8
|
+
Note: `type-registry.yaml` also declares `context` and `idea`. They are valid
|
|
9
|
+
`entries.entry_type` values and legal filters here, but no `store_*` tool
|
|
10
|
+
creates them — nothing writes them today.
|
|
6
11
|
|
|
7
12
|
All reads are explicit (no background polling). Server validates storage
|
|
8
13
|
accessibility at startup.
|
|
@@ -190,7 +195,13 @@ def _tool_definitions() -> list[Tool]:
|
|
|
190
195
|
return [
|
|
191
196
|
Tool(
|
|
192
197
|
name="search_memory",
|
|
193
|
-
description=
|
|
198
|
+
description=(
|
|
199
|
+
"Search memory entries across projects. Omit project to search all projects. "
|
|
200
|
+
"The query is tokenized and OR-matched (any term hits), ranked by relevance. "
|
|
201
|
+
"Tags are OR-matched too: passing ['ci','release'] returns entries with either tag. "
|
|
202
|
+
"Filters narrow the result set; at most 50 entries are returned. "
|
|
203
|
+
"Source entries are excluded unless entry_type='source' with no other filter."
|
|
204
|
+
),
|
|
194
205
|
inputSchema={
|
|
195
206
|
"type": "object",
|
|
196
207
|
"properties": {
|
|
@@ -203,78 +214,143 @@ def _tool_definitions() -> list[Tool]:
|
|
|
203
214
|
),
|
|
204
215
|
Tool(
|
|
205
216
|
name="store_decision",
|
|
206
|
-
description=
|
|
217
|
+
description=(
|
|
218
|
+
"Store a decision: an architectural or design choice that was made and why. "
|
|
219
|
+
"Deduplicated by content hash, so re-storing the same text updates instead of "
|
|
220
|
+
"duplicating."
|
|
221
|
+
),
|
|
207
222
|
inputSchema={
|
|
208
223
|
"type": "object",
|
|
209
224
|
"required": ["project", "content"],
|
|
210
225
|
"properties": {
|
|
211
226
|
"project": {"type": "string"},
|
|
212
|
-
"content": {
|
|
213
|
-
|
|
214
|
-
|
|
227
|
+
"content": {
|
|
228
|
+
"type": "string",
|
|
229
|
+
"description": "One paragraph, no headings or bullet lists.",
|
|
230
|
+
},
|
|
231
|
+
"description": {
|
|
232
|
+
"type": "string",
|
|
233
|
+
"description": "One-sentence queryable summary; derived from content if omitted.",
|
|
234
|
+
},
|
|
235
|
+
"tags": {
|
|
236
|
+
"type": "array",
|
|
237
|
+
"items": {"type": "string"},
|
|
238
|
+
"description": "Lowercase-kebab tags, e.g. architecture/python/testing.",
|
|
239
|
+
},
|
|
215
240
|
"openspec_change_id": {"type": "string"},
|
|
216
241
|
},
|
|
217
242
|
},
|
|
218
243
|
),
|
|
219
244
|
Tool(
|
|
220
245
|
name="store_fact",
|
|
221
|
-
description=
|
|
246
|
+
description=(
|
|
247
|
+
"Store a fact: a stable, verifiable statement about the project (version, "
|
|
248
|
+
"constraint, path, endpoint). Deduplicated by content hash. Prefer one atomic "
|
|
249
|
+
"fact per call over a bundle of several."
|
|
250
|
+
),
|
|
222
251
|
inputSchema={
|
|
223
252
|
"type": "object",
|
|
224
253
|
"required": ["project", "content"],
|
|
225
254
|
"properties": {
|
|
226
255
|
"project": {"type": "string"},
|
|
227
|
-
"content": {
|
|
228
|
-
|
|
229
|
-
|
|
256
|
+
"content": {
|
|
257
|
+
"type": "string",
|
|
258
|
+
"description": "One paragraph, no headings or bullet lists.",
|
|
259
|
+
},
|
|
260
|
+
"description": {
|
|
261
|
+
"type": "string",
|
|
262
|
+
"description": "One-sentence queryable summary; derived from content if omitted.",
|
|
263
|
+
},
|
|
264
|
+
"tags": {
|
|
265
|
+
"type": "array",
|
|
266
|
+
"items": {"type": "string"},
|
|
267
|
+
"description": "Lowercase-kebab tags, e.g. architecture/python/testing.",
|
|
268
|
+
},
|
|
230
269
|
"confidence": {"type": "number", "minimum": 0.0, "maximum": 1.0},
|
|
231
270
|
},
|
|
232
271
|
},
|
|
233
272
|
),
|
|
234
273
|
Tool(
|
|
235
274
|
name="store_learning",
|
|
236
|
-
description=
|
|
275
|
+
description=(
|
|
276
|
+
"Store a learning: a non-obvious lesson, debugging insight, or solution found — "
|
|
277
|
+
"something that cost effort and would otherwise be rediscovered. Deduplicated by "
|
|
278
|
+
"content hash."
|
|
279
|
+
),
|
|
237
280
|
inputSchema={
|
|
238
281
|
"type": "object",
|
|
239
282
|
"required": ["project", "content"],
|
|
240
283
|
"properties": {
|
|
241
284
|
"project": {"type": "string"},
|
|
242
|
-
"content": {
|
|
243
|
-
|
|
244
|
-
|
|
285
|
+
"content": {
|
|
286
|
+
"type": "string",
|
|
287
|
+
"description": "One paragraph, no headings or bullet lists.",
|
|
288
|
+
},
|
|
289
|
+
"description": {
|
|
290
|
+
"type": "string",
|
|
291
|
+
"description": "One-sentence queryable summary; derived from content if omitted.",
|
|
292
|
+
},
|
|
293
|
+
"tags": {
|
|
294
|
+
"type": "array",
|
|
295
|
+
"items": {"type": "string"},
|
|
296
|
+
"description": "Lowercase-kebab tags, e.g. architecture/python/testing.",
|
|
297
|
+
},
|
|
245
298
|
},
|
|
246
299
|
},
|
|
247
300
|
),
|
|
248
301
|
Tool(
|
|
249
302
|
name="store_convention",
|
|
250
|
-
description=
|
|
303
|
+
description=(
|
|
304
|
+
"Store a convention: an agreed style rule, naming pattern, or coding standard. "
|
|
305
|
+
"Deduplicated by content hash."
|
|
306
|
+
),
|
|
251
307
|
inputSchema={
|
|
252
308
|
"type": "object",
|
|
253
309
|
"required": ["project", "content"],
|
|
254
310
|
"properties": {
|
|
255
311
|
"project": {"type": "string"},
|
|
256
|
-
"content": {
|
|
257
|
-
|
|
258
|
-
|
|
312
|
+
"content": {
|
|
313
|
+
"type": "string",
|
|
314
|
+
"description": "One paragraph, no headings or bullet lists.",
|
|
315
|
+
},
|
|
316
|
+
"description": {
|
|
317
|
+
"type": "string",
|
|
318
|
+
"description": "One-sentence queryable summary; derived from content if omitted.",
|
|
319
|
+
},
|
|
320
|
+
"tags": {
|
|
321
|
+
"type": "array",
|
|
322
|
+
"items": {"type": "string"},
|
|
323
|
+
"description": "Lowercase-kebab tags, e.g. architecture/python/testing.",
|
|
324
|
+
},
|
|
259
325
|
},
|
|
260
326
|
},
|
|
261
327
|
),
|
|
262
328
|
Tool(
|
|
263
329
|
name="store_profile",
|
|
264
|
-
description=
|
|
330
|
+
description=(
|
|
331
|
+
"Replace the tech profile for a project. One profile per project: the content "
|
|
332
|
+
"overwrites the previous one, it does not append. Pass the complete profile text."
|
|
333
|
+
),
|
|
265
334
|
inputSchema={
|
|
266
335
|
"type": "object",
|
|
267
336
|
"required": ["project", "content"],
|
|
268
337
|
"properties": {
|
|
269
338
|
"project": {"type": "string"},
|
|
270
|
-
"content": {
|
|
339
|
+
"content": {
|
|
340
|
+
"type": "string",
|
|
341
|
+
"description": "The full profile — replaces the stored one.",
|
|
342
|
+
},
|
|
271
343
|
"tags": {"type": "array", "items": {"type": "string"}},
|
|
272
344
|
},
|
|
273
345
|
},
|
|
274
346
|
),
|
|
275
347
|
Tool(
|
|
276
348
|
name="store_source",
|
|
277
|
-
description=
|
|
349
|
+
description=(
|
|
350
|
+
"Store an external source reference (article, transcript, PDF, video, link) under "
|
|
351
|
+
"raw/. Immutable: a later store with the same URL returns the existing entry, and "
|
|
352
|
+
"reusing a title slug with different content is rejected."
|
|
353
|
+
),
|
|
278
354
|
inputSchema={
|
|
279
355
|
"type": "object",
|
|
280
356
|
"required": ["url", "title", "description", "source_kind"],
|
|
@@ -294,7 +370,10 @@ def _tool_definitions() -> list[Tool]:
|
|
|
294
370
|
),
|
|
295
371
|
Tool(
|
|
296
372
|
name="export_memories",
|
|
297
|
-
description=
|
|
373
|
+
description=(
|
|
374
|
+
"Export every stored entry for one project, newest first, with no result limit. "
|
|
375
|
+
"Use for a full project dump, not for lookups — search_memory is cheaper."
|
|
376
|
+
),
|
|
298
377
|
inputSchema={
|
|
299
378
|
"type": "object",
|
|
300
379
|
"required": ["project"],
|
|
@@ -306,7 +385,11 @@ def _tool_definitions() -> list[Tool]:
|
|
|
306
385
|
),
|
|
307
386
|
Tool(
|
|
308
387
|
name="get_profile",
|
|
309
|
-
description=
|
|
388
|
+
description=(
|
|
389
|
+
"Retrieve the stored tech profile for a project. Returns profile entries by "
|
|
390
|
+
"default; pass entry_type to query another type instead (then it is a "
|
|
391
|
+
"most-recent-first lookup capped at 10)."
|
|
392
|
+
),
|
|
310
393
|
inputSchema={
|
|
311
394
|
"type": "object",
|
|
312
395
|
"required": ["project"],
|
package/server/store.py
CHANGED
|
@@ -263,7 +263,10 @@ class MemoryStore:
|
|
|
263
263
|
db_path = self.storage_path / "memory.db"
|
|
264
264
|
if db_path.exists() and db_path.stat().st_size > 0:
|
|
265
265
|
try:
|
|
266
|
-
|
|
266
|
+
# timeout=30: with WAL a live vault may be written by another
|
|
267
|
+
# client (DSH + opencode). Without it the probe waits only 5 s
|
|
268
|
+
# and a busy lock surfaces as "corrupt database".
|
|
269
|
+
test_conn = sqlite3.connect(str(db_path), timeout=30)
|
|
267
270
|
row = test_conn.execute("PRAGMA integrity_check").fetchone()
|
|
268
271
|
test_conn.close()
|
|
269
272
|
if row[0].lower() != "ok":
|
|
@@ -271,7 +274,7 @@ class MemoryStore:
|
|
|
271
274
|
except sqlite3.DatabaseError as e:
|
|
272
275
|
raise RuntimeError(f"corrupt database: {e}") from e
|
|
273
276
|
|
|
274
|
-
self._db = sqlite3.connect(str(db_path))
|
|
277
|
+
self._db = sqlite3.connect(str(db_path), timeout=30)
|
|
275
278
|
self._db.row_factory = sqlite3.Row
|
|
276
279
|
self.db.execute("PRAGMA journal_mode=WAL")
|
|
277
280
|
self.db.execute("PRAGMA foreign_keys=ON")
|
|
@@ -767,12 +770,15 @@ class MemoryStore:
|
|
|
767
770
|
project: str,
|
|
768
771
|
entry_type: str | None = None,
|
|
769
772
|
) -> list[dict]:
|
|
770
|
-
"""Retrieve profile
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
773
|
+
"""Retrieve the profile for a project.
|
|
774
|
+
|
|
775
|
+
Defaults to ``profile`` entries: the tool is named get_profile, so an
|
|
776
|
+
unfiltered call returning the 10 most recent rows of any type was a
|
|
777
|
+
silent wrong answer. Pass ``entry_type`` to use it as a recency query.
|
|
778
|
+
"""
|
|
779
|
+
entry_type = entry_type or "profile"
|
|
780
|
+
sql = "SELECT * FROM entries WHERE project = ? AND entry_type = ?"
|
|
781
|
+
params: list = [project, entry_type]
|
|
776
782
|
sql += " ORDER BY updated_at DESC LIMIT 10"
|
|
777
783
|
rows = self.db.execute(sql, params).fetchall()
|
|
778
784
|
return [dict(r) for r in rows]
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Self-check: get_profile answers with the profile, not with recent noise.
|
|
3
|
+
|
|
4
|
+
Regression guard for the bug where an unfiltered get_profile returned the 10
|
|
5
|
+
most recently updated rows of ANY type, so a caller asking for the profile of a
|
|
6
|
+
busy project got unrelated decisions and facts.
|
|
7
|
+
|
|
8
|
+
Run: python3 memory-vault-server/test_get_profile.py
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import shutil
|
|
16
|
+
import sys
|
|
17
|
+
import tempfile
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
SERVER_DIR = Path(__file__).resolve().parent
|
|
21
|
+
REPO_VAULT = SERVER_DIR.parent / "memory-vault"
|
|
22
|
+
sys.path.insert(0, str(SERVER_DIR))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def main() -> int:
|
|
26
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
27
|
+
# store.py resolves the type registry from MEMORY_PATH at import time,
|
|
28
|
+
# so seed the throwaway vault before importing it.
|
|
29
|
+
vault = Path(tmp)
|
|
30
|
+
(vault / "projects").mkdir()
|
|
31
|
+
shutil.copy(REPO_VAULT / "type-registry.yaml", vault / "type-registry.yaml")
|
|
32
|
+
os.environ["MEMORY_PATH"] = str(vault)
|
|
33
|
+
import store as store_mod
|
|
34
|
+
|
|
35
|
+
s = store_mod.MemoryStore(storage_path=vault)
|
|
36
|
+
s.initialize()
|
|
37
|
+
|
|
38
|
+
s.upsert_profile(project="proj", content="PROFILE: python + sqlite")
|
|
39
|
+
# Written afterwards, so these outrank the profile in updated_at:
|
|
40
|
+
# the old code returned them and called it a profile.
|
|
41
|
+
s.upsert_entry("decision", "proj", "DECISION: use sqlite for storage")
|
|
42
|
+
s.upsert_entry("fact", "proj", "FACT: python 3.11 is required")
|
|
43
|
+
|
|
44
|
+
got = s.get_profile(project="proj")
|
|
45
|
+
assert len(got) == 1, f"expected only the profile row, got {len(got)}: {got}"
|
|
46
|
+
assert got[0]["entry_type"] == "profile", got[0]["entry_type"]
|
|
47
|
+
assert "PROFILE" in got[0]["content"], got[0]["content"]
|
|
48
|
+
|
|
49
|
+
# entry_type=None must behave exactly like the default.
|
|
50
|
+
assert s.get_profile(project="proj", entry_type=None) == got
|
|
51
|
+
|
|
52
|
+
# Explicit entry_type still works (and is now a plain recency lookup).
|
|
53
|
+
facts = s.get_profile(project="proj", entry_type="fact")
|
|
54
|
+
assert len(facts) == 1 and facts[0]["entry_type"] == "fact", facts
|
|
55
|
+
|
|
56
|
+
# A project with no profile yields nothing rather than unrelated rows.
|
|
57
|
+
s.upsert_entry("fact", "other", "FACT: unrelated project")
|
|
58
|
+
assert s.get_profile(project="other") == [], "profile leaked across projects"
|
|
59
|
+
|
|
60
|
+
# The MCP tool handler agrees (binds the tool default to the store fix).
|
|
61
|
+
# Needs the pinned `mcp` version from requirements.txt; skip when the
|
|
62
|
+
# ambient one is older instead of failing the whole check.
|
|
63
|
+
try:
|
|
64
|
+
import server
|
|
65
|
+
|
|
66
|
+
found = json.loads(server.handle_get_profile(s, {"project": "proj"}))
|
|
67
|
+
assert len(found) == 1 and found[0]["entry_type"] == "profile", found
|
|
68
|
+
except ImportError as exc:
|
|
69
|
+
print(f"skip: MCP handler assertion ({exc})")
|
|
70
|
+
|
|
71
|
+
s._db.close()
|
|
72
|
+
|
|
73
|
+
print("ok: get_profile returns the profile, not the most recent rows")
|
|
74
|
+
return 0
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
raise SystemExit(main())
|