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.
- package/README.md +7 -11
- package/bin/omms-launch.mjs +8 -4
- package/dist/adapters/opencode/conversation.js +4 -2
- package/dist/adapters/opencode/memory-tool.d.ts +4 -0
- package/dist/adapters/opencode/memory-tool.js +53 -0
- package/dist/adapters/opencode/package-skills.d.ts +1 -1
- package/dist/adapters/opencode/package-skills.js +17 -15
- package/dist/adapters/pi/extension.js +7 -0
- package/dist/cli/memory-command.d.ts +2 -0
- package/dist/cli/memory-command.js +35 -9
- package/dist/cli/web-command.js +25 -9
- package/dist/cli/web-step-aside.d.ts +17 -0
- package/dist/cli/web-step-aside.js +34 -0
- package/dist/config-paths.d.ts +5 -0
- package/dist/config-paths.js +15 -0
- package/dist/config-template.d.ts +1 -1
- package/dist/config-template.js +8 -0
- package/dist/config.d.ts +51 -40
- package/dist/config.js +8 -15
- package/dist/core/capture.d.ts +2 -1
- package/dist/core/capture.js +8 -1
- package/dist/core/host.d.ts +12 -0
- package/dist/core/memorisation-operations.d.ts +33 -0
- package/dist/core/memorisation-operations.js +160 -0
- package/dist/core/memorisation-settings.d.ts +11 -0
- package/dist/core/memorisation-settings.js +48 -0
- package/dist/core/memorisation.d.ts +43 -0
- package/dist/core/memorisation.js +76 -0
- package/dist/core/memorise-turn.d.ts +11 -0
- package/dist/core/memorise-turn.js +64 -0
- package/dist/core/memory-operations.d.ts +6 -1
- package/dist/core/memory-operations.js +29 -0
- package/dist/core/memory-tool-text.js +1 -1
- package/dist/core/retrieval.d.ts +3 -2
- package/dist/core/retrieval.js +11 -4
- package/dist/core/tool-outcome.d.ts +6 -0
- package/dist/core/tool-outcome.js +31 -0
- package/dist/importer/claude-conversation.js +8 -1
- package/dist/importer/claude-turn-units.js +1 -0
- package/dist/importer/importer.js +3 -3
- package/dist/importer/opencode-reader.js +2 -1
- package/dist/importer/pi-conversation.js +11 -5
- package/dist/index.js +2 -42
- package/dist/services/api-handlers.js +13 -35
- package/dist/services/capture-diagnostics.d.ts +3 -2
- package/dist/services/capture-diagnostics.js +5 -1
- package/dist/services/cleanup-service.js +2 -1
- package/dist/services/client-session-read.d.ts +16 -0
- package/dist/services/client-session-read.js +32 -0
- package/dist/services/client.d.ts +6 -1
- package/dist/services/client.js +20 -36
- package/dist/services/deduplication-service.js +12 -1
- package/dist/services/global-config-writer.js +14 -1
- package/dist/services/linked-search.d.ts +15 -0
- package/dist/services/linked-search.js +23 -2
- package/dist/services/live-captured-entries.js +2 -1
- package/dist/services/memorisation-routes.d.ts +16 -0
- package/dist/services/memorisation-routes.js +94 -0
- package/dist/services/memorisation-store.d.ts +45 -0
- package/dist/services/memorisation-store.js +120 -0
- package/dist/services/memory-kind.d.ts +6 -0
- package/dist/services/memory-kind.js +6 -0
- package/dist/services/memory-portability-service.js +2 -1
- package/dist/services/memory-replace.d.ts +8 -0
- package/dist/services/memory-replace.js +26 -0
- package/dist/services/retrieval-rerank.d.ts +3 -1
- package/dist/services/retrieval-rerank.js +1 -1
- package/dist/services/settings-snapshot.js +6 -0
- package/dist/services/turso/exact-scan.d.ts +2 -1
- package/dist/services/turso/exact-scan.js +4 -3
- package/dist/services/turso/keyword-search.d.ts +2 -1
- package/dist/services/turso/keyword-search.js +3 -2
- package/dist/services/turso/vector-search.d.ts +10 -9
- package/dist/services/turso/vector-search.js +38 -95
- package/dist/services/turso/vector-writes.d.ts +8 -0
- package/dist/services/turso/vector-writes.js +80 -0
- package/dist/services/web-ensure.d.ts +1 -0
- package/dist/services/web-ensure.js +5 -1
- package/dist/services/web-memory-rows.d.ts +12 -0
- package/dist/services/web-memory-rows.js +58 -0
- package/dist/services/web-route-modules.js +5 -0
- package/dist/services/web-update.d.ts +6 -1
- package/dist/services/web-update.js +12 -1
- package/dist/v2/adapter.d.ts +13 -1
- package/dist/v2/adapter.js +7 -0
- package/dist/web/assets/index-CoYuPYCS.css +2 -0
- package/dist/web/assets/index-DQgI670D.js +171 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/skills/omms-memory/SKILL.md +18 -0
- package/skills/omms-remember/SKILL.md +118 -0
- package/dist/web/assets/index-2BNRLGqe.css +0 -2
- 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
|
}
|
package/dist/core/retrieval.d.ts
CHANGED
|
@@ -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
|
|
16
|
-
* left, so a fully private prompt
|
|
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
|
/**
|
package/dist/core/retrieval.js
CHANGED
|
@@ -29,13 +29,20 @@ export async function searchSectionMemories(query, projectTag, sessionId) {
|
|
|
29
29
|
return { results, queryVector: search.queryVector };
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
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
|
-
|
|
42
|
+
const text = prompt.replace(SKILL_BLOCK, "");
|
|
43
|
+
if (isFullyPrivate(text))
|
|
37
44
|
return null;
|
|
38
|
-
return stripPrivateContent(
|
|
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(
|
|
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
|
-
|
|
217
|
+
const reason = result.reason ?? `extractor-skip:${result.type ?? "unknown"}`;
|
|
218
|
+
await ledger.skip(key, reason);
|
|
218
219
|
unit.status = "skipped";
|
|
219
|
-
unit.reason =
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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:
|
|
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
|
|
1003
|
+
for (const shard of await tursoShardManager.getAllShards("project", "")) {
|
|
1026
1004
|
const db = await tursoConnectionManager.getConnection(shard.dbPath);
|
|
1027
|
-
const row = await db.get(
|
|
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(
|
|
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
|
-
|
|
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"
|
|
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
|
+
}
|