om-memory-system 5.9.0 → 5.10.1

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.
Files changed (93) hide show
  1. package/README.md +7 -11
  2. package/bin/omms-launch.mjs +8 -4
  3. package/dist/adapters/opencode/conversation.js +4 -2
  4. package/dist/adapters/opencode/memory-tool.d.ts +4 -0
  5. package/dist/adapters/opencode/memory-tool.js +53 -0
  6. package/dist/adapters/opencode/package-skills.d.ts +1 -1
  7. package/dist/adapters/opencode/package-skills.js +17 -15
  8. package/dist/adapters/pi/extension.js +7 -0
  9. package/dist/cli/memory-command.d.ts +2 -0
  10. package/dist/cli/memory-command.js +35 -9
  11. package/dist/cli/web-command.js +25 -9
  12. package/dist/cli/web-step-aside.d.ts +17 -0
  13. package/dist/cli/web-step-aside.js +34 -0
  14. package/dist/config-paths.d.ts +5 -0
  15. package/dist/config-paths.js +15 -0
  16. package/dist/config-template.d.ts +1 -1
  17. package/dist/config-template.js +8 -0
  18. package/dist/config.d.ts +51 -40
  19. package/dist/config.js +8 -15
  20. package/dist/core/capture.d.ts +2 -1
  21. package/dist/core/capture.js +8 -1
  22. package/dist/core/host.d.ts +12 -0
  23. package/dist/core/memorisation-operations.d.ts +33 -0
  24. package/dist/core/memorisation-operations.js +160 -0
  25. package/dist/core/memorisation-settings.d.ts +11 -0
  26. package/dist/core/memorisation-settings.js +48 -0
  27. package/dist/core/memorisation.d.ts +43 -0
  28. package/dist/core/memorisation.js +76 -0
  29. package/dist/core/memorise-turn.d.ts +11 -0
  30. package/dist/core/memorise-turn.js +64 -0
  31. package/dist/core/memory-operations.d.ts +6 -1
  32. package/dist/core/memory-operations.js +29 -0
  33. package/dist/core/memory-tool-text.js +1 -1
  34. package/dist/core/retrieval.d.ts +3 -2
  35. package/dist/core/retrieval.js +11 -4
  36. package/dist/core/tool-outcome.d.ts +6 -0
  37. package/dist/core/tool-outcome.js +31 -0
  38. package/dist/importer/claude-conversation.js +8 -1
  39. package/dist/importer/claude-turn-units.js +1 -0
  40. package/dist/importer/importer.js +3 -3
  41. package/dist/importer/opencode-reader.js +2 -1
  42. package/dist/importer/pi-conversation.js +11 -5
  43. package/dist/index.js +2 -42
  44. package/dist/services/api-handlers.js +13 -35
  45. package/dist/services/capture-diagnostics.d.ts +3 -2
  46. package/dist/services/capture-diagnostics.js +5 -1
  47. package/dist/services/cleanup-service.js +2 -1
  48. package/dist/services/client-session-read.d.ts +16 -0
  49. package/dist/services/client-session-read.js +32 -0
  50. package/dist/services/client.d.ts +6 -1
  51. package/dist/services/client.js +20 -36
  52. package/dist/services/deduplication-service.js +12 -1
  53. package/dist/services/global-config-writer.js +14 -1
  54. package/dist/services/linked-search.d.ts +15 -0
  55. package/dist/services/linked-search.js +23 -2
  56. package/dist/services/live-captured-entries.js +2 -1
  57. package/dist/services/memorisation-routes.d.ts +16 -0
  58. package/dist/services/memorisation-routes.js +94 -0
  59. package/dist/services/memorisation-store.d.ts +45 -0
  60. package/dist/services/memorisation-store.js +120 -0
  61. package/dist/services/memory-kind.d.ts +6 -0
  62. package/dist/services/memory-kind.js +6 -0
  63. package/dist/services/memory-portability-service.js +2 -1
  64. package/dist/services/memory-replace.d.ts +8 -0
  65. package/dist/services/memory-replace.js +26 -0
  66. package/dist/services/retrieval-rerank.d.ts +3 -1
  67. package/dist/services/retrieval-rerank.js +1 -1
  68. package/dist/services/settings-snapshot.js +6 -0
  69. package/dist/services/turso/exact-scan.d.ts +2 -1
  70. package/dist/services/turso/exact-scan.js +4 -3
  71. package/dist/services/turso/keyword-search.d.ts +2 -1
  72. package/dist/services/turso/keyword-search.js +3 -2
  73. package/dist/services/turso/vector-search.d.ts +10 -9
  74. package/dist/services/turso/vector-search.js +38 -95
  75. package/dist/services/turso/vector-writes.d.ts +8 -0
  76. package/dist/services/turso/vector-writes.js +80 -0
  77. package/dist/services/web-ensure.d.ts +1 -0
  78. package/dist/services/web-ensure.js +5 -1
  79. package/dist/services/web-memory-rows.d.ts +12 -0
  80. package/dist/services/web-memory-rows.js +58 -0
  81. package/dist/services/web-route-modules.js +5 -0
  82. package/dist/services/web-update.d.ts +6 -1
  83. package/dist/services/web-update.js +12 -1
  84. package/dist/v2/adapter.d.ts +13 -1
  85. package/dist/v2/adapter.js +7 -0
  86. package/dist/web/assets/index-CoYuPYCS.css +2 -0
  87. package/dist/web/assets/index-DQgI670D.js +171 -0
  88. package/dist/web/index.html +2 -2
  89. package/package.json +1 -1
  90. package/skills/omms-memory/SKILL.md +18 -0
  91. package/skills/omms-remember/SKILL.md +118 -0
  92. package/dist/web/assets/index-2BNRLGqe.css +0 -2
  93. package/dist/web/assets/index-Bhk1P5Z7.js +0 -170
@@ -0,0 +1,64 @@
1
+ /**
2
+ * A memorisation reaches an agent only through `recall`. A turn in which the
3
+ * agent saved one must not also be captured as an ordinary memory, or the
4
+ * snapshot would come back through per-prompt recall. The host readers set
5
+ * `memorise` on each tool call from the raw arguments and the result; the
6
+ * detector reads only that field. No result text is kept.
7
+ */
8
+ // The OMMS memory tool: bare on OpenCode and Pi, `mcp__<server>__memory` on Claude Code.
9
+ const MEMORY_TOOL = /^(?:mcp__[\w-]*omms[\w-]*__)?memory$/;
10
+ const MEMORISE_MODES = new Set(["memorise", "memorize"]);
11
+ // `om-memory-system memory memorise`, with or without `npx`, flags before the mode word.
12
+ const CLI_MEMORISE = /\bom-memory-system\b[^|;&\n]*?\bmemory\s+(?:memorise|memorize)\b/;
13
+ function recordOf(args) {
14
+ let value = args;
15
+ if (typeof value === "string") {
16
+ try {
17
+ value = JSON.parse(value);
18
+ }
19
+ catch {
20
+ return undefined;
21
+ }
22
+ }
23
+ return value && typeof value === "object" && !Array.isArray(value)
24
+ ? value
25
+ : undefined;
26
+ }
27
+ /** `unknown` for a memorise call (a result still has to answer it), otherwise undefined. */
28
+ export function memoriseCallState(name, args) {
29
+ const record = recordOf(args);
30
+ if (!record)
31
+ return undefined;
32
+ if (MEMORY_TOOL.test(name)) {
33
+ const mode = typeof record.mode === "string" ? record.mode.trim().toLowerCase() : "";
34
+ return MEMORISE_MODES.has(mode) ? "unknown" : undefined;
35
+ }
36
+ if (name.toLowerCase() === "bash") {
37
+ const command = record.command;
38
+ return typeof command === "string" && CLI_MEMORISE.test(command) ? "unknown" : undefined;
39
+ }
40
+ return undefined;
41
+ }
42
+ /**
43
+ * The state a result gives a memorise call. A host error, or a JSON reply with
44
+ * `success: false`, is `failed`. The memory tool reports its own failures as a
45
+ * normal reply with `success: false`, so the host does not always mark them.
46
+ */
47
+ export function memoriseResultState(isError, text) {
48
+ if (isError)
49
+ return "failed";
50
+ try {
51
+ const reply = JSON.parse(text.trim());
52
+ if (reply && typeof reply === "object" && reply.success === false) {
53
+ return "failed";
54
+ }
55
+ }
56
+ catch {
57
+ // Not JSON: the host reported no error, so the call counts as saved.
58
+ }
59
+ return "saved";
60
+ }
61
+ /** True when any call in the turn saved a memorisation. */
62
+ export function turnSavedMemorisation(toolCalls) {
63
+ return toolCalls.some((call) => call.memorise === "saved");
64
+ }
@@ -1,7 +1,7 @@
1
1
  import { type MemoryScope } from "../services/client.js";
2
2
  import { getTags } from "../services/tags.js";
3
3
  import type { MemoryHost, MemoryType } from "../types/index.js";
4
- export type MemoryOperationMode = "add" | "search" | "profile" | "list" | "forget" | "help" | "migrate" | "list-shards" | "export" | "import" | "space";
4
+ export type MemoryOperationMode = "add" | "memorise" | "memorize" | "recall" | "search" | "profile" | "list" | "forget" | "help" | "migrate" | "list-shards" | "export" | "import" | "space";
5
5
  export interface MemoryOperationArgs {
6
6
  mode?: MemoryOperationMode;
7
7
  content?: string;
@@ -11,6 +11,11 @@ export interface MemoryOperationArgs {
11
11
  memoryId?: string;
12
12
  limit?: number;
13
13
  scope?: MemoryScope;
14
+ /** `memorise` mode: session snapshot fields, used instead of `content`. */
15
+ topic?: string;
16
+ done?: string;
17
+ doing?: string;
18
+ next?: string;
14
19
  fromPath?: string;
15
20
  fromHash?: string;
16
21
  outputPath?: string;
@@ -1,5 +1,8 @@
1
1
  import { CONFIG, isConfigured, refreshConfigIfChanged } from "../config.js";
2
2
  import { memoryClient } from "../services/client.js";
3
+ import { runMemorise, runRecall } from "./memorisation-operations.js";
4
+ import { memorisationSettingsFrom } from "./memorisation-settings.js";
5
+ import { linkedSettingsFrom } from "../services/linked-search.js";
3
6
  import { getLanguageName } from "../services/language-detector.js";
4
7
  import { stripPrivateContent, isFullyPrivate } from "../services/privacy.js";
5
8
  import { getTags } from "../services/tags.js";
@@ -29,6 +32,11 @@ export async function executeMemoryOperation(args, context) {
29
32
  error: "Memory system not configured properly.",
30
33
  };
31
34
  }
35
+ // Snapshot fields without a mode are a memorise call that lost its mode. Help
36
+ // text would read as success, so the model would never learn nothing was saved.
37
+ if (!args.mode && [args.topic, args.done, args.doing, args.next].some((v) => v !== undefined)) {
38
+ return { success: false, error: "mode is required: use memorise for snapshot fields" };
39
+ }
32
40
  const mode = args.mode || "help";
33
41
  if (mode === "space") {
34
42
  const { runSpaceMode } = await import("./space-tool.js");
@@ -70,6 +78,16 @@ export async function executeMemoryOperation(args, context) {
70
78
  description: `Store new memory (MATCH USER LANGUAGE: ${langName})`,
71
79
  args: ["content", "type?", "tags?"],
72
80
  },
81
+ {
82
+ command: "memorise",
83
+ description: `Save an exact memorisation with a short ms- ID: give content, or a session snapshot with topic, done, doing and next. Never reads session history (MATCH USER LANGUAGE: ${langName})`,
84
+ args: ["content?", "topic?", "done?", "doing?", "next?", "tags?"],
85
+ },
86
+ {
87
+ command: "recall",
88
+ description: "Fetch memorisations only: pass an ms- ID for the exact text, or text for ranked matches with a confidence score. Pass tags (the labels) to keep only memorisations that carry all of them; tags with no query lists the newest ones",
89
+ args: ["query?", "tags?", "limit?"],
90
+ },
73
91
  {
74
92
  command: "search",
75
93
  description: `Search memories via keywords (MATCH USER LANGUAGE: ${langName})`,
@@ -121,6 +139,12 @@ export async function executeMemoryOperation(args, context) {
121
139
  case "add": {
122
140
  if (!args.content)
123
141
  return { success: false, error: "content required" };
142
+ if (args.type === "memorisation") {
143
+ return {
144
+ success: false,
145
+ error: 'type "memorisation" is reserved. Use memorise mode to save a memorisation.',
146
+ };
147
+ }
124
148
  const sanitizedContent = stripPrivateContent(args.content);
125
149
  if (isFullyPrivate(args.content)) {
126
150
  return { success: false, error: "Private content blocked" };
@@ -148,6 +172,11 @@ export async function executeMemoryOperation(args, context) {
148
172
  tags: parsedTags,
149
173
  };
150
174
  }
175
+ case "memorise":
176
+ case "memorize":
177
+ return await runMemorise(args, context, tags, memorisationSettingsFrom(CONFIG), linkedSettingsFrom(CONFIG));
178
+ case "recall":
179
+ return await runRecall(args, tags, args.scope ?? CONFIG.memory.defaultScope, linkedSettingsFrom(CONFIG));
151
180
  case "search": {
152
181
  if (!args.query)
153
182
  return { success: false, error: "query required" };
@@ -2,5 +2,5 @@
2
2
  export const MEMORY_SEARCH_FIRST = "Search it before you debug or investigate a problem: earlier fixes and decisions are stored here.";
3
3
  /** The `memory` tool description, the same text for Pi and OpenCode. */
4
4
  export function memoryToolDescription(languageName) {
5
- return `Manage and query project memory (MATCH USER LANGUAGE: ${languageName}). ${MEMORY_SEARCH_FIRST} Use 'search' with technical keywords/tags, 'add' to store knowledge, 'profile' for preferences. Use migrate/list-shards/export/import when a project directory moves. Search/list scope: project or all-projects.`;
5
+ return `Manage and query project memory (MATCH USER LANGUAGE: ${languageName}). ${MEMORY_SEARCH_FIRST} Use 'search' with technical keywords/tags, 'add' to store knowledge, 'profile' for preferences. Use 'memorise' to save an exact note or a session snapshot (topic, done, doing, next) with a short ms- ID, and 'recall' to fetch it by ID, by text or by its labels (tags, all of which must match); memorisations never appear in 'search' or automatic recall. When the user quotes an ms- ID or asks what was planned or memorised ("what were we going to do with X"), use 'recall', not 'search'. Use migrate/list-shards/export/import when a project directory moves. Search/list scope: project or all-projects.`;
6
6
  }
@@ -12,8 +12,9 @@ export declare function searchSectionMemories(query: string, projectTag: string,
12
12
  queryVector?: ArrayLike<number>;
13
13
  } | null>;
14
14
  /**
15
- * The prompt text that may reach a search: `<private>` regions removed. Null when nothing is
16
- * left, so a fully private prompt runs no search and nothing of it reaches an embedding provider.
15
+ * The prompt text that may reach a search: skill blocks and `<private>` regions removed. Null when
16
+ * nothing is left, so a fully private prompt, or a skill run with no text of its own, runs no
17
+ * search and nothing of it reaches an embedding provider.
17
18
  */
18
19
  export declare function searchablePrompt(prompt: string): string | null;
19
20
  /**
@@ -29,13 +29,20 @@ export async function searchSectionMemories(query, projectTag, sessionId) {
29
29
  return { results, queryVector: search.queryVector };
30
30
  }
31
31
  /**
32
- * The prompt text that may reach a search: `<private>` regions removed. Null when nothing is
33
- * left, so a fully private prompt runs no search and nothing of it reaches an embedding provider.
32
+ * Pi expands `/skill:<name> text` into `<skill ...>skill body</skill>` followed by the user's text.
33
+ * The search uses the user's own words, so the whole block is cut out.
34
+ */
35
+ const SKILL_BLOCK = /<skill\b[^>]*>[\s\S]*?<\/skill>/gi;
36
+ /**
37
+ * The prompt text that may reach a search: skill blocks and `<private>` regions removed. Null when
38
+ * nothing is left, so a fully private prompt, or a skill run with no text of its own, runs no
39
+ * search and nothing of it reaches an embedding provider.
34
40
  */
35
41
  export function searchablePrompt(prompt) {
36
- if (isFullyPrivate(prompt))
42
+ const text = prompt.replace(SKILL_BLOCK, "");
43
+ if (isFullyPrivate(text))
37
44
  return null;
38
- return stripPrivateContent(prompt).trim() || null;
45
+ return stripPrivateContent(text).trim() || null;
39
46
  }
40
47
  /**
41
48
  * Prompt-aware semantic retrieval shared by every host (Pi `before_agent_start`,
@@ -10,4 +10,10 @@ export declare function errorOutcome(text: unknown): ToolOutcomeFields;
10
10
  * the history import read the same part shape.
11
11
  */
12
12
  export declare function opencodeToolOutcome(state: unknown): ToolOutcomeFields;
13
+ /**
14
+ * The `memorise` field for an OpenCode tool part, or nothing for any other
15
+ * call. A completed call is `saved` or `failed` by its reply, an errored call
16
+ * is `failed`, and a call still running is `unknown`.
17
+ */
18
+ export declare function opencodeMemorise(name: string, args: unknown, state: unknown): Pick<CaptureToolCall, "memorise">;
13
19
  export {};
@@ -1,3 +1,4 @@
1
+ import { memoriseCallState, memoriseResultState } from "./memorise-turn.js";
1
2
  export const MAX_TOOL_ERROR_LENGTH = 2000;
2
3
  /** Error text as the host reported it, cut to at most 2,000 characters. */
3
4
  export function boundToolError(text) {
@@ -20,3 +21,33 @@ export function opencodeToolOutcome(state) {
20
21
  return errorOutcome(error);
21
22
  return { outcome: "unknown" };
22
23
  }
24
+ /**
25
+ * The text an OpenCode tool part returned: V1 keeps it in `state.output`, the
26
+ * V2 plugin API in `state.content` text items. Used only to read a memorise
27
+ * reply; it is never stored.
28
+ */
29
+ function opencodeToolText(state) {
30
+ if (typeof state.output === "string")
31
+ return state.output;
32
+ if (!Array.isArray(state.content))
33
+ return "";
34
+ return state.content
35
+ .map((item) => (item && typeof item === "object" ? item.text : null))
36
+ .filter((text) => typeof text === "string")
37
+ .join("\n");
38
+ }
39
+ /**
40
+ * The `memorise` field for an OpenCode tool part, or nothing for any other
41
+ * call. A completed call is `saved` or `failed` by its reply, an errored call
42
+ * is `failed`, and a call still running is `unknown`.
43
+ */
44
+ export function opencodeMemorise(name, args, state) {
45
+ if (!memoriseCallState(name, args))
46
+ return {};
47
+ const fields = (state && typeof state === "object" ? state : {});
48
+ if (fields.status === "error")
49
+ return { memorise: "failed" };
50
+ if (fields.status !== "completed")
51
+ return { memorise: "unknown" };
52
+ return { memorise: memoriseResultState(false, opencodeToolText(fields)) };
53
+ }
@@ -1,4 +1,5 @@
1
1
  import { boundToolInput, readToolPath } from "../core/tool-input.js";
2
+ import { memoriseCallState, memoriseResultState } from "../core/memorise-turn.js";
2
3
  import { errorOutcome } from "../core/tool-outcome.js";
3
4
  /**
4
5
  * Entry types seen in real transcripts. A line with any other type is skipped
@@ -158,6 +159,9 @@ function addAssistantWork(open, entry) {
158
159
  const path = readToolPath(name, block.input);
159
160
  if (path !== undefined)
160
161
  call.path = path;
162
+ const memorise = memoriseCallState(name, block.input);
163
+ if (memorise)
164
+ call.memorise = memorise;
161
165
  open.toolCalls.push(call);
162
166
  if (typeof block.id === "string")
163
167
  open.callsById.set(block.id, call);
@@ -183,10 +187,13 @@ function addToolResults(open, entry) {
183
187
  const call = open.callsById.get(block.tool_use_id ?? "");
184
188
  if (!call)
185
189
  continue;
190
+ const text = toolResultText(block.content);
186
191
  if (block.is_error === true)
187
- Object.assign(call, errorOutcome(toolResultText(block.content)));
192
+ Object.assign(call, errorOutcome(text));
188
193
  else
189
194
  call.outcome = "ok";
195
+ if (call.memorise)
196
+ call.memorise = memoriseResultState(block.is_error === true, text);
190
197
  }
191
198
  }
192
199
  function windowsWithIndex(entries) {
@@ -51,6 +51,7 @@ export function workUnitOf(sessionId, window, directory) {
51
51
  toolCalls: window.toolCalls.map((call) => ({
52
52
  name: call.name,
53
53
  input: stripPrivateContent(call.input),
54
+ ...(call.memorise ? { memorise: call.memorise } : {}),
54
55
  })),
55
56
  sourceEntryIds: window.sourceEntryIds,
56
57
  ...(window.sourceTimestamp !== undefined ? { sourceTimestamp: window.sourceTimestamp } : {}),
@@ -214,11 +214,11 @@ export async function importHistorySource(source, host, deps, filters, report) {
214
214
  try {
215
215
  const result = await handleFinishedTurn(workUnit, { memories: true, skills: false }, { provider: deps.provider });
216
216
  if (result.status === "skipped") {
217
- await ledger.skip(key, `extractor-skip:${result.type ?? "unknown"}`);
217
+ const reason = result.reason ?? `extractor-skip:${result.type ?? "unknown"}`;
218
+ await ledger.skip(key, reason);
218
219
  unit.status = "skipped";
219
- unit.reason = `extractor-skip:${result.type ?? "unknown"}`;
220
+ unit.reason = reason;
220
221
  report.unitsSkipped++;
221
- const reason = unit.reason;
222
222
  report.skipReasons[reason] = (report.skipReasons[reason] ?? 0) + 1;
223
223
  }
224
224
  else {
@@ -4,7 +4,7 @@ import { join } from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
5
  import { DatabaseSync } from "node:sqlite";
6
6
  import { boundToolInput, readToolPath } from "../core/tool-input.js";
7
- import { opencodeToolOutcome } from "../core/tool-outcome.js";
7
+ import { opencodeMemorise, opencodeToolOutcome } from "../core/tool-outcome.js";
8
8
  import { INTERNAL_CAPTURE_SESSION_TITLES } from "./opencode-internal-sessions.js";
9
9
  import { acquirePrivateSnapshot, opencodeSnapshots, OpencodeSnapshotError, } from "./opencode-snapshot.js";
10
10
  export const DEFAULT_OPENCODE_DB = join(homedir(), ".local/share/opencode/opencode.db");
@@ -106,6 +106,7 @@ function buildWindows(db, sessionId, filters) {
106
106
  name,
107
107
  input: boundToolInput(formatInput(args)),
108
108
  ...opencodeToolOutcome(state),
109
+ ...opencodeMemorise(name, args, state),
109
110
  ...(path === undefined ? {} : { path }),
110
111
  });
111
112
  }
@@ -1,4 +1,5 @@
1
1
  import { boundToolInput, readToolPath } from "../core/tool-input.js";
2
+ import { memoriseCallState, memoriseResultState } from "../core/memorise-turn.js";
2
3
  import { errorOutcome } from "../core/tool-outcome.js";
3
4
  function entryText(content) {
4
5
  if (typeof content === "string")
@@ -28,12 +29,14 @@ function applyToolResult(entry, callsById) {
28
29
  const call = callsById.get(entry.message?.toolCallId ?? "");
29
30
  if (!call)
30
31
  return;
31
- if (entry.message?.isError === true) {
32
- Object.assign(call, errorOutcome(entryText(entry.message.content)));
33
- }
34
- else {
32
+ const text = entryText(entry.message?.content);
33
+ const isError = entry.message?.isError === true;
34
+ if (isError)
35
+ Object.assign(call, errorOutcome(text));
36
+ else
35
37
  call.outcome = "ok";
36
- }
38
+ if (call.memorise)
39
+ call.memorise = memoriseResultState(isError, text);
37
40
  }
38
41
  function formatToolInput(args) {
39
42
  if (!args)
@@ -94,6 +97,9 @@ function buildWindow(entries, userIndex) {
94
97
  const path = readToolPath(name, block.arguments);
95
98
  if (path !== undefined)
96
99
  call.path = path;
100
+ const memorise = memoriseCallState(name, block.arguments);
101
+ if (memorise)
102
+ call.memorise = memorise;
97
103
  toolCalls.push(call);
98
104
  if (typeof block.id === "string")
99
105
  callsById.set(block.id, call);
package/dist/index.js CHANGED
@@ -1,10 +1,8 @@
1
- import { tool } from "@opencode-ai/plugin";
2
1
  import { memoryClient } from "./services/client.js";
3
2
  import { getTags } from "./services/tags.js";
4
3
  import { performAutoCapture } from "./services/auto-capture.js";
5
4
  import { createOpenCodeAutoCaptureHost } from "./adapters/opencode/auto-capture-host.js";
6
5
  import { isInternalPrompt, isStructuredSummaryPromptMessage, recordUserPrompt, } from "./adapters/opencode/user-prompt.js";
7
- import { executeMemoryOperation } from "./core/memory-operations.js";
8
6
  import { memoryToolDescription } from "./core/memory-tool-text.js";
9
7
  import { OPENCODE_IMPORT_COMMAND, OPENCODE_IMPORT_DESCRIPTION, latestUserMessageModel, runOpencodeImportCommand, } from "./adapters/opencode/import-command.js";
10
8
  import { buildRecentMemoriesSection, formatMemoriesForCompaction } from "./core/retrieval.js";
@@ -22,6 +20,7 @@ import { getHostClientConfig } from "./adapters/opencode/opencode-host-config.js
22
20
  import { applySkillsPathConfig } from "./adapters/opencode/package-skills.js";
23
21
  import { loadOpencodeProvider } from "./adapters/opencode/opencode-provider-loader.js";
24
22
  import { opencodeSkillsTool } from "./adapters/opencode/skills-tool.js";
23
+ import { opencodeMemoryTool } from "./adapters/opencode/memory-tool.js";
25
24
  import { STRUCTURED_OUTPUT_AGENT, STRUCTURED_OUTPUT_TOOLS, } from "./adapters/opencode/opencode-provider.js";
26
25
  import { INTERNAL_CAPTURE_SESSION_TITLE, isInternalCaptureSessionTitle, isTrackedInternalCaptureSession, } from "./adapters/opencode/internal-capture-sessions.js";
27
26
  import { registerOpencodeProfileModel } from "./adapters/opencode/profile-model.js";
@@ -505,46 +504,7 @@ export const OmmsPlugin = async (ctx) => {
505
504
  }
506
505
  },
507
506
  tool: {
508
- memory: tool({
509
- description: memoryToolDescription(getLanguageName(CONFIG.autoCaptureLanguage || "en")),
510
- args: {
511
- mode: tool.schema
512
- .enum([
513
- "add",
514
- "search",
515
- "profile",
516
- "list",
517
- "forget",
518
- "help",
519
- "migrate",
520
- "list-shards",
521
- "export",
522
- "import",
523
- ])
524
- .optional(),
525
- content: tool.schema.string().optional(),
526
- query: tool.schema.string().optional(),
527
- tags: tool.schema.string().optional(),
528
- type: tool.schema.string().optional(),
529
- memoryId: tool.schema.string().optional(),
530
- limit: tool.schema.number().optional(),
531
- scope: tool.schema.enum(["project", "all-projects"]).optional(),
532
- fromPath: tool.schema.string().optional(),
533
- fromHash: tool.schema.string().optional(),
534
- outputPath: tool.schema.string().optional(),
535
- inputPath: tool.schema.string().optional(),
536
- dryRun: tool.schema.boolean().optional(),
537
- allowLinkedSource: tool.schema.boolean().optional(),
538
- },
539
- async execute(args) {
540
- const result = await executeMemoryOperation(args, {
541
- directory,
542
- tags,
543
- host: "opencode",
544
- });
545
- return JSON.stringify(result);
546
- },
547
- }),
507
+ memory: opencodeMemoryTool(memoryToolDescription(getLanguageName(CONFIG.autoCaptureLanguage || "en")), { directory, tags }),
548
508
  skills: opencodeSkillsTool(directory),
549
509
  },
550
510
  event: async (input) => {
@@ -12,6 +12,8 @@ import { userPromptManager } from "./user-prompt/user-prompt-manager.js";
12
12
  import { saveProfile } from "./user-profile/merge-and-save.js";
13
13
  import { sortProfileItems, stripProfileVectors } from "../utils/profile.js";
14
14
  import { resolveWebProfileUserId } from "./profile-identity.js";
15
+ import { UNTAGGED_NORMAL_PREDICATE } from "./memory-kind.js";
16
+ import { loadMemoryRows, memorisationIdsAmong, MEMORISATION_EDIT_REFUSAL, MEMORISATION_TYPE_REFUSAL, webUpdateRefusal, } from "./web-memory-rows.js";
15
17
  async function getAllMemoryShards() {
16
18
  await ensureTursoReady();
17
19
  const projectShards = await tursoShardManager.getAllShards("project", "");
@@ -101,37 +103,6 @@ export async function handleListTags() {
101
103
  return { success: false, error: String(error) };
102
104
  }
103
105
  }
104
- /** Reads stored memory rows for one project tag, or for every project and user shard. */
105
- async function loadMemoryRows(tag) {
106
- const allMemories = [];
107
- if (tag) {
108
- const { scope: tagScope, hash } = extractScopeFromTag(tag);
109
- const shards = await tursoShardManager.getAllShards(tagScope, hash);
110
- for (const shard of shards) {
111
- const db = await tursoConnectionManager.getConnection(shard.dbPath);
112
- const memories = await tursoVectorSearch.listMemories(db, tag, 10000);
113
- allMemories.push(...memories);
114
- }
115
- }
116
- else {
117
- // Iterate both project- and user-scoped shards. Previously this only
118
- // walked project shards, which silently hid user-scope memories from the
119
- // listing endpoint (Web UI navigation, /api/memories without a tag
120
- // filter, …). User-scope memories still showed up in /api/search and
121
- // /api/stats `byType`, but were invisible in /api/stats `byScope` and
122
- // unbrowseable in the UI — a confusing UX gap. The filter keeps the
123
- // defense-in-depth check on container_tag, just widens it to both
124
- // canonical scope markers.
125
- const projectShards = await tursoShardManager.getAllShards("project", "");
126
- const userShards = await tursoShardManager.getAllShards("user", "");
127
- for (const shard of [...projectShards, ...userShards]) {
128
- const db = await tursoConnectionManager.getConnection(shard.dbPath);
129
- const memories = await tursoVectorSearch.getAllMemories(db);
130
- allMemories.push(...memories.filter((m) => m.container_tag?.includes("_project_") || m.container_tag?.includes("_user_")));
131
- }
132
- }
133
- return allMemories;
134
- }
135
106
  /** Labels (comma-separated memory tags) with how many memories carry each one. */
136
107
  export async function handleListKeywords(tag) {
137
108
  try {
@@ -288,6 +259,8 @@ export async function handleAddMemory(data) {
288
259
  if (!data.content || !data.containerTag) {
289
260
  return { success: false, error: "content and containerTag are required" };
290
261
  }
262
+ if (data.type === "memorisation")
263
+ return { success: false, error: MEMORISATION_TYPE_REFUSAL };
291
264
  await ensureTursoReady();
292
265
  await embeddingService.warmup();
293
266
  const tags = (data.tags || []).map((t) => t.trim().toLowerCase());
@@ -367,6 +340,9 @@ export async function handleBulkDelete(ids, cascade = false) {
367
340
  try {
368
341
  if (!ids || ids.length === 0)
369
342
  return { success: false, error: "ids array is required" };
343
+ if ((await memorisationIdsAmong(ids)).length > 0) {
344
+ return { success: false, error: MEMORISATION_EDIT_REFUSAL };
345
+ }
370
346
  let deleted = 0;
371
347
  for (const id of ids) {
372
348
  const result = await handleDeleteMemory(id, cascade);
@@ -384,6 +360,9 @@ export async function handleUpdateMemory(id, data) {
384
360
  try {
385
361
  if (!id)
386
362
  return { success: false, error: "id is required" };
363
+ const refusal = await webUpdateRefusal(id, data);
364
+ if (refusal)
365
+ return { success: false, error: refusal };
387
366
  await embeddingService.warmup();
388
367
  // Find the existing memory first (read-only — no data modified yet)
389
368
  const shards = await getAllMemoryShards();
@@ -1020,11 +999,10 @@ export async function handleUpdateProfileItem(body) {
1020
999
  export async function handleDetectTagMigration() {
1021
1000
  try {
1022
1001
  await ensureTursoReady();
1023
- const projectShards = await tursoShardManager.getAllShards("project", "");
1024
1002
  let untaggedCount = 0;
1025
- for (const shard of projectShards) {
1003
+ for (const shard of await tursoShardManager.getAllShards("project", "")) {
1026
1004
  const db = await tursoConnectionManager.getConnection(shard.dbPath);
1027
- const row = await db.get("SELECT COUNT(*) as count FROM memories WHERE tags IS NULL OR tags = ''");
1005
+ const row = await db.get(`SELECT COUNT(*) as count FROM memories WHERE ${UNTAGGED_NORMAL_PREDICATE}`);
1028
1006
  untaggedCount += Number(row?.count ?? 0);
1029
1007
  }
1030
1008
  return { success: true, data: { needsMigration: untaggedCount > 0, count: untaggedCount } };
@@ -1066,7 +1044,7 @@ export async function handleRunTagMigrationBatch(batchSize = 5) {
1066
1044
  migrationQueue = [];
1067
1045
  for (const shard of await tursoShardManager.getAllShards("project", "")) {
1068
1046
  const db = await tursoConnectionManager.getConnection(shard.dbPath);
1069
- const rows = await db.all("SELECT id FROM memories WHERE tags IS NULL OR tags = '' ORDER BY id");
1047
+ const rows = await db.all(`SELECT id FROM memories WHERE ${UNTAGGED_NORMAL_PREDICATE} ORDER BY id`);
1070
1048
  for (const row of rows)
1071
1049
  migrationQueue.push({ id: String(row.id), dbPath: shard.dbPath });
1072
1050
  }
@@ -1,4 +1,4 @@
1
- import type { CaptureAttemptDiagnostics, CaptureAttemptOutcome, CaptureExtractionPath, CaptureFailureReason } from "../core/host.js";
1
+ import type { CaptureAttemptDiagnostics, CaptureAttemptOutcome, CaptureExtractionPath, CaptureFailureReason, CaptureSkipReason } from "../core/host.js";
2
2
  import type { MemoryHost, MemorySourceType } from "../types/index.js";
3
3
  import { restrictToCurrentUser } from "./private-path.js";
4
4
  /**
@@ -36,7 +36,8 @@ export interface CaptureAttemptRecord {
36
36
  replyChars: number | null;
37
37
  durationMs: number;
38
38
  outcome: CaptureAttemptOutcome;
39
- reason: CaptureFailureReason | null;
39
+ /** The failure code of a failed attempt, or the skip code of a turn skipped before the model ran. */
40
+ reason: CaptureFailureReason | CaptureSkipReason | null;
40
41
  }
41
42
  export declare const DEFAULT_CAPTURE_TRACE_RETENTION_DAYS = 7;
42
43
  export declare function buildCaptureAttemptRecord(context: CaptureAttemptContext, diagnostics: CaptureAttemptDiagnostics, outcome: CaptureAttemptOutcome, durationMs: number): CaptureAttemptRecord;
@@ -25,7 +25,11 @@ export function buildCaptureAttemptRecord(context, diagnostics, outcome, duratio
25
25
  replyChars: diagnostics.rawReply?.length ?? null,
26
26
  durationMs,
27
27
  outcome,
28
- reason: outcome === "failed" ? (diagnostics.failureReason ?? "call-error") : null,
28
+ reason: outcome === "failed"
29
+ ? (diagnostics.failureReason ?? "call-error")
30
+ : outcome === "skipped"
31
+ ? (diagnostics.skipReason ?? null)
32
+ : null,
29
33
  };
30
34
  }
31
35
  // Common key and token formats. Each match is replaced whole, except the
@@ -5,6 +5,7 @@ import { ensureTursoReady } from "./turso/ready.js";
5
5
  import { CONFIG } from "../config.js";
6
6
  import { log } from "./logger.js";
7
7
  import { userPromptManager } from "./user-prompt/user-prompt-manager.js";
8
+ import { memoryKindPredicate } from "./memory-kind.js";
8
9
  export class CleanupService {
9
10
  lastCleanupTime = 0;
10
11
  isRunning = false;
@@ -50,7 +51,7 @@ export class CleanupService {
50
51
  const db = await tursoConnectionManager.getConnection(shard.dbPath);
51
52
  const oldMemories = await db.all(`
52
53
  SELECT id, container_tag, is_pinned FROM memories
53
- WHERE updated_at < ?
54
+ WHERE updated_at < ? AND ${memoryKindPredicate()}
54
55
  `, [cutoffTime]);
55
56
  for (const memory of oldMemories) {
56
57
  try {
@@ -0,0 +1,16 @@
1
+ import type { MemoryKind } from "./memory-kind.js";
2
+ export declare function searchMemoriesBySessionID(sessionID: string, containerTag: string, limit: number, kind: MemoryKind): Promise<{
3
+ id: any;
4
+ memory: any;
5
+ similarity: number;
6
+ tags: any;
7
+ metadata: any;
8
+ containerTag: any;
9
+ displayName: any;
10
+ userName: any;
11
+ userEmail: any;
12
+ projectPath: any;
13
+ projectName: any;
14
+ gitRepoUrl: any;
15
+ createdAt: any;
16
+ }[]>;
@@ -0,0 +1,32 @@
1
+ import { extractScopeFromContainerTag } from "./memory-scope.js";
2
+ import { tursoShardManager } from "./turso/shard-manager.js";
3
+ import { tursoVectorSearch } from "./turso/vector-search.js";
4
+ import { tursoConnectionManager } from "./turso/connection-manager.js";
5
+ export async function searchMemoriesBySessionID(sessionID, containerTag, limit, kind) {
6
+ const { scope, hash } = extractScopeFromContainerTag(containerTag);
7
+ const shards = await tursoShardManager.getAllShards(scope, hash);
8
+ if (shards.length === 0)
9
+ return [];
10
+ const allMemories = [];
11
+ for (const shard of shards) {
12
+ const db = await tursoConnectionManager.getConnection(shard.dbPath);
13
+ const memories = await tursoVectorSearch.getMemoriesBySessionID(db, sessionID, kind);
14
+ allMemories.push(...memories);
15
+ }
16
+ allMemories.sort((a, b) => Number(b.created_at) - Number(a.created_at));
17
+ return allMemories.slice(0, limit).map((row) => ({
18
+ id: row.id,
19
+ memory: row.content,
20
+ similarity: 1.0,
21
+ tags: row.tags || [],
22
+ metadata: row.metadata || {},
23
+ containerTag: row.container_tag,
24
+ displayName: row.display_name,
25
+ userName: row.user_name,
26
+ userEmail: row.user_email,
27
+ projectPath: row.project_path,
28
+ projectName: row.project_name,
29
+ gitRepoUrl: row.git_repo_url,
30
+ createdAt: row.created_at,
31
+ }));
32
+ }