open-memex 0.5.0 → 0.6.0-alpha.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/src/mcp.ts CHANGED
@@ -52,8 +52,10 @@ import {
52
52
  memoryResolveArgs,
53
53
  memoryPrStatusArgs,
54
54
  TOOL_DESCRIPTIONS,
55
+ withOutboxNote,
55
56
  type ToolResult,
56
57
  } from "./tools/ops.ts";
58
+ import { outboxDraftCount } from "./submit.ts";
57
59
 
58
60
  // Server version tracks package.json — never hardcode it here again.
59
61
  // package.json sits two levels above this file in both layouts
@@ -75,40 +77,49 @@ const SERVER_VERSION: string = (() => {
75
77
  * client at connect time. Still advisory — no MCP consumer offers a hard
76
78
  * session-start hook — but it is the strongest signal available.
77
79
  */
78
- const SERVER_INSTRUCTIONS = `You are connected to an open-memex local memory MCP server
79
- (eleven memory_* tools: add, search, list, supersede, forget, status, submit,
80
- propose, promote, resolve, pr_status).
80
+ const SERVER_INSTRUCTIONS = `You are connected to an open-memex local memory MCP server.
81
+ Its tools are memory_add, memory_search, memory_list, memory_supersede,
82
+ memory_forget, memory_status, memory_submit, memory_propose, memory_promote,
83
+ memory_resolve, memory_pr_status — always call them by these full names.
81
84
 
82
- - At the START of this session, when you finish a meaningful chunk of work,
83
- after the user commits (git commit), and after any memory_* action completes,
84
- call memory_status. If the project outbox has drafts waiting for review,
85
+ - The server tells you when project outbox drafts are waiting for review —
86
+ in tool results, and in these session-start instructions. When it does,
85
87
  summarize them (one line each) and ask the user which ones to sync into the
86
- repo. Sync NOTHING the user did not name.
87
- - When the user says "sync memory" (or "同步记忆"), treat it as a request to run
88
- the sync flow: call memory_status, summarize the outbox drafts, and ask which
89
- ones to sync. ALWAYS use the memory_status tool for this — never browse the
90
- appdata directory directly.
88
+ repo; sync NOTHING the user did not name. If you already asked about these
89
+ drafts this session, don't ask again. When the server reports none waiting,
90
+ do nothing.
91
+ - When the user says "sync memory" (or "同步记忆"), call memory_status,
92
+ summarize the outbox drafts (one line each), and ask which ones to sync.
93
+ ALWAYS use the memory_status tool for this — never browse the memory data
94
+ directory directly.
91
95
  - After memory_submit, ask ONE follow-up: "want me to create a branch + push +
92
96
  open the PR, or will you handle it yourself?" NEVER create branches, push, or
93
97
  open PRs without the user's explicit approval. A "yes, you do it" covers the
94
98
  whole chain — do NOT re-ask at each step.
95
- - BE PROACTIVE: when the user shares something worth remembering across sessions
96
- (a decision, a preference, a project convention, a fix and its cause), call
97
- memory_add without being asked. Keep each memory to one self-contained statement.
98
- - At the same checkpoints (session start, end of a work chunk, after the user
99
- commits, after any memory_* action), DISTILL the session: propose 1–3 short
100
- memories capturing the useful conclusion — what was learned or decided, how an
101
- issue was resolved, what to avoid, where the authoritative doc lives — not the
102
- raw transcript. Save NOTHING the user did not approve; on approval call
103
- memory_add with source "inference" at the confirmed scope. If the knowledge
104
- already lives in project docs, save a \`reference\` memory pointing at the doc
105
- instead of copying it. Long-form notes are fine ONLY when the user explicitly
106
- asks to save one.
99
+ - BE PROACTIVE about facts the user states directly: when the user shares a
100
+ decision, preference, project convention, or fix-and-cause worth remembering
101
+ across sessions, call memory_add without being asked. Keep each memory to one
102
+ self-contained statement, and add a brief "(noted in memory)" so the user
103
+ sees it worked.
104
+ - For conclusions YOU infer (the user never stated them): when you finish a
105
+ task the user would describe in one sentence, consider distilling the
106
+ session — if there is something worth keeping,
107
+ propose 1–3 short memories capturing the useful conclusion (what was learned
108
+ or decided, how an issue was resolved, what to avoid, where the authoritative
109
+ doc lives — not the raw transcript), each with its proposed scope. Save
110
+ NOTHING the user did not approve; on approval call memory_add with source
111
+ "inference" at the approved scope. If the knowledge already lives in project
112
+ docs, save it as type "reference" pointing at the doc instead of copying it.
113
+ Long-form notes are fine ONLY when the user explicitly asks to save one.
107
114
  - Before asking the user about past decisions, conventions, or preferences they
108
- may have told you before, call memory_search first.
115
+ may have told you before, call memory_search first — try a few keyword
116
+ variants (including the user's own language) when the first search comes up
117
+ empty.
109
118
  - Memories default to this project's scope; use the personal scope for facts
110
- about the user that hold across all projects.
111
- - personal scope memories NEVER leave this machine.`;
119
+ about the user that hold across all projects. When a saved fact becomes
120
+ outdated, call memory_supersede (find the old memory's id with memory_search
121
+ first) instead of adding a duplicate.
122
+ `;
112
123
 
113
124
  /** Adapt a framework-agnostic op result to an MCP tool response. */
114
125
  function toMcp(p: Promise<ToolResult>) {
@@ -164,9 +175,17 @@ export async function runMcpServer() {
164
175
  };
165
176
  };
166
177
 
178
+ // D53: session-start outbox state, pushed. The stdio server starts fresh per
179
+ // session, so construction-time state ≈ session-start state.
180
+ const n = outboxDraftCount(scope.key);
181
+ const instructions =
182
+ n > 0
183
+ ? `${SERVER_INSTRUCTIONS}\n\nSession start: the project outbox has ${n} draft${n === 1 ? "" : "s"} waiting for review — call memory_status to see them.`
184
+ : SERVER_INSTRUCTIONS;
185
+
167
186
  const server = new McpServer(
168
187
  { name: "open-memex", version: SERVER_VERSION },
169
- { instructions: SERVER_INSTRUCTIONS },
188
+ { instructions },
170
189
  );
171
190
 
172
191
  server.registerTool(
@@ -175,7 +194,7 @@ export async function runMcpServer() {
175
194
  description: TOOL_DESCRIPTIONS.memory_add,
176
195
  inputSchema: z.object(memoryAddArgs),
177
196
  },
178
- withSync((args) => toMcp(addMemory(getScope, cfg, args))),
197
+ withSync((args) => toMcp(withOutboxNote(getScope().key, addMemory(getScope, cfg, args), "call memory_status to review"))),
179
198
  );
180
199
 
181
200
  server.registerTool(
@@ -204,7 +223,7 @@ export async function runMcpServer() {
204
223
  description: TOOL_DESCRIPTIONS.memory_supersede,
205
224
  inputSchema: z.object(memorySupersedeArgs),
206
225
  },
207
- withSync((args) => toMcp(supersedeMemory(cfg, args))),
226
+ withSync((args) => toMcp(withOutboxNote(getScope().key, supersedeMemory(cfg, args), "call memory_status to review"))),
208
227
  );
209
228
 
210
229
  server.registerTool(
@@ -214,7 +233,7 @@ export async function runMcpServer() {
214
233
  inputSchema: z.object(memoryForgetArgs),
215
234
  annotations: { destructiveHint: true },
216
235
  },
217
- withSync((args) => toMcp(forgetMemory(args))),
236
+ withSync((args) => toMcp(withOutboxNote(getScope().key, forgetMemory(args), "call memory_status to review"))),
218
237
  );
219
238
 
220
239
  server.registerTool(
@@ -233,7 +252,7 @@ export async function runMcpServer() {
233
252
  description: TOOL_DESCRIPTIONS.memory_submit,
234
253
  inputSchema: z.object(memorySubmitArgs),
235
254
  },
236
- withSync((args) => toMcp(submitMemoriesOp(args))),
255
+ withSync((args) => toMcp(withOutboxNote(getScope().key, submitMemoriesOp(args), "call memory_status to review"))),
237
256
  );
238
257
 
239
258
  server.registerTool(
@@ -242,7 +261,7 @@ export async function runMcpServer() {
242
261
  description: TOOL_DESCRIPTIONS.memory_propose,
243
262
  inputSchema: z.object(memoryProposeArgs),
244
263
  },
245
- withSync((args) => toMcp(proposeMemoriesOp(args))),
264
+ withSync((args) => toMcp(withOutboxNote(getScope().key, proposeMemoriesOp(args), "call memory_status to review"))),
246
265
  );
247
266
 
248
267
  server.registerTool(
package/src/paths.ts CHANGED
@@ -21,6 +21,15 @@ export interface Paths {
21
21
 
22
22
  let _cached: Paths | null = null;
23
23
 
24
+ /**
25
+ * Read-only data-root path — never creates the directory. For existence
26
+ * checks (D50 first-run detection) that must not pollute storage; contrast
27
+ * `paths()`, which mkdirs as a side effect.
28
+ */
29
+ export function dataRootPath(): string {
30
+ return dataRoot();
31
+ }
32
+
24
33
  export function paths(): Paths {
25
34
  if (_cached) return _cached;
26
35
  const root = dataRoot();
package/src/submit.ts CHANGED
@@ -153,6 +153,22 @@ export function getSyncStatus(): SyncStatus {
153
153
  };
154
154
  }
155
155
 
156
+ /**
157
+ * D53: cheap outbox draft count — one indexed query, no git I/O — for the
158
+ * push-not-poll note appended to mutating tool results and to the MCP
159
+ * session-start instructions. Same outbox definition as getSyncStatus
160
+ * (file under the scope's outbox dir), without the expensive parts.
161
+ */
162
+ export function outboxDraftCount(scopeKey: string): number {
163
+ const outboxDir = memoriesDirPath(scopeKey);
164
+ const row = db()
165
+ .prepare(
166
+ `SELECT COUNT(*) AS n FROM memories WHERE scope_key = ? AND instr(file_path, ?) = 1`,
167
+ )
168
+ .get(scopeKey, outboxDir + path.sep) as { n: number } | undefined;
169
+ return row?.n ?? 0;
170
+ }
171
+
156
172
  export function formatSyncStatus(st: SyncStatus): string {
157
173
  const lines: string[] = [];
158
174
  lines.push(`project: ${st.projectName} (${st.scopeKey})`);
@@ -13,6 +13,7 @@ import {
13
13
  memorySupersedeArgs,
14
14
  memoryForgetArgs,
15
15
  TOOL_DESCRIPTIONS,
16
+ withOutboxNote,
16
17
  } from "./ops.ts";
17
18
 
18
19
  export function makeTools(getScope: () => Scope, cfg: MyOMemoryConfig) {
@@ -20,7 +21,7 @@ export function makeTools(getScope: () => Scope, cfg: MyOMemoryConfig) {
20
21
  description: TOOL_DESCRIPTIONS.memory_add,
21
22
  args: memoryAddArgs,
22
23
  async execute(args) {
23
- return addMemory(getScope, cfg, args);
24
+ return withOutboxNote(getScope().key, addMemory(getScope, cfg, args));
24
25
  },
25
26
  });
26
27
 
@@ -44,7 +45,7 @@ export function makeTools(getScope: () => Scope, cfg: MyOMemoryConfig) {
44
45
  description: TOOL_DESCRIPTIONS.memory_supersede,
45
46
  args: memorySupersedeArgs,
46
47
  async execute(args) {
47
- return supersedeMemory(cfg, args);
48
+ return withOutboxNote(getScope().key, supersedeMemory(cfg, args));
48
49
  },
49
50
  });
50
51
 
@@ -52,7 +53,7 @@ export function makeTools(getScope: () => Scope, cfg: MyOMemoryConfig) {
52
53
  description: TOOL_DESCRIPTIONS.memory_forget,
53
54
  args: memoryForgetArgs,
54
55
  async execute(args) {
55
- return forgetMemory(args);
56
+ return withOutboxNote(getScope().key, forgetMemory(args));
56
57
  },
57
58
  });
58
59
 
package/src/tools/ops.ts CHANGED
@@ -24,7 +24,7 @@ import { upsertFromFile, deleteFromIndex } from "../store/sync.ts";
24
24
  import { findDuplicates, supersede } from "../store/lifecycle.ts";
25
25
  import { db } from "../store/db.ts";
26
26
  import { redact } from "../redact.ts";
27
- import { getSyncStatus, formatSyncStatus, submitMemories } from "../submit.ts";
27
+ import { getSyncStatus, formatSyncStatus, submitMemories, outboxDraftCount } from "../submit.ts";
28
28
  import { getPrStatus, formatPrStatus, applyPrStatus } from "../github.ts";
29
29
  import {
30
30
  proposeMemories,
@@ -39,6 +39,26 @@ export interface ToolResult {
39
39
  output: string;
40
40
  }
41
41
 
42
+ /**
43
+ * D53: push, don't poll. Append the project-outbox pending count to mutating
44
+ * tool results so agents learn about drafts waiting for review without a
45
+ * checkpoint poll. Silent when the outbox is empty. `reviewHint` names the
46
+ * tool to call (MCP); transports without that tool leave it generic.
47
+ */
48
+ export async function withOutboxNote(
49
+ scopeKey: string,
50
+ p: Promise<ToolResult>,
51
+ reviewHint?: string,
52
+ ): Promise<ToolResult> {
53
+ const r = await p;
54
+ const n = outboxDraftCount(scopeKey);
55
+ if (n > 0) {
56
+ const tail = reviewHint ? ` — ${reviewHint}` : " for review";
57
+ r.output += `\n[open-memex: ${n} draft${n === 1 ? "" : "s"} waiting in the project outbox${tail}]`;
58
+ }
59
+ return r;
60
+ }
61
+
42
62
  /** LLM-facing tool descriptions, shared by the opencode plugin and the MCP server. */
43
63
  export const TOOL_DESCRIPTIONS = {
44
64
  memory_add: