@rvboris/opencode-mempalace 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +230 -0
  3. package/README.ru.md +230 -0
  4. package/dist/bridge/mempalace_adapter.py +54 -0
  5. package/dist/plugin/hooks/chat-params.d.ts +5 -0
  6. package/dist/plugin/hooks/chat-params.js +8 -0
  7. package/dist/plugin/hooks/event.d.ts +19 -0
  8. package/dist/plugin/hooks/event.js +110 -0
  9. package/dist/plugin/hooks/system.d.ts +10 -0
  10. package/dist/plugin/hooks/system.js +50 -0
  11. package/dist/plugin/hooks/tool.d.ts +17 -0
  12. package/dist/plugin/hooks/tool.js +61 -0
  13. package/dist/plugin/index.d.ts +2 -0
  14. package/dist/plugin/index.js +18 -0
  15. package/dist/plugin/lib/adapter.d.ts +1 -0
  16. package/dist/plugin/lib/adapter.js +42 -0
  17. package/dist/plugin/lib/autosave.d.ts +52 -0
  18. package/dist/plugin/lib/autosave.js +242 -0
  19. package/dist/plugin/lib/config.d.ts +13 -0
  20. package/dist/plugin/lib/config.js +53 -0
  21. package/dist/plugin/lib/context.d.ts +12 -0
  22. package/dist/plugin/lib/context.js +37 -0
  23. package/dist/plugin/lib/derive.d.ts +1 -0
  24. package/dist/plugin/lib/derive.js +36 -0
  25. package/dist/plugin/lib/enforcement.d.ts +1 -0
  26. package/dist/plugin/lib/enforcement.js +10 -0
  27. package/dist/plugin/lib/log.d.ts +11 -0
  28. package/dist/plugin/lib/log.js +48 -0
  29. package/dist/plugin/lib/privacy.d.ts +3 -0
  30. package/dist/plugin/lib/privacy.js +19 -0
  31. package/dist/plugin/lib/scope.d.ts +8 -0
  32. package/dist/plugin/lib/scope.js +16 -0
  33. package/dist/plugin/tools/mempalace-memory.d.ts +42 -0
  34. package/dist/plugin/tools/mempalace-memory.js +89 -0
  35. package/example-opencode.json +4 -0
  36. package/package.json +45 -0
@@ -0,0 +1,50 @@
1
+ import { AutosaveStatus, clearKeywordSavePending, extractLastUserMessage, getCurrentTurnSessionId, getSessionState, markRetrievalInjected, startAutosave, } from "../lib/autosave";
2
+ import { loadConfig } from "../lib/config";
3
+ import { buildAutosaveInstruction, buildKeywordSaveInstruction, buildRetrievalInstruction } from "../lib/context";
4
+ import { writeLog } from "../lib/log";
5
+ export const systemHooks = (ctx) => {
6
+ return {
7
+ "experimental.chat.system.transform": async (_input, output) => {
8
+ try {
9
+ const sessionId = getCurrentTurnSessionId();
10
+ if (!sessionId)
11
+ return;
12
+ const config = await loadConfig();
13
+ const state = getSessionState(sessionId);
14
+ const response = await ctx.client.session.messages({ path: { id: sessionId } });
15
+ const messages = response?.data ?? response ?? [];
16
+ const lastUserMessage = extractLastUserMessage(messages);
17
+ if (config.retrievalEnabled && state.retrievalPending && lastUserMessage) {
18
+ output.system.push(buildRetrievalInstruction({
19
+ projectName: ctx.project?.name,
20
+ projectWingPrefix: config.projectWingPrefix,
21
+ userWingPrefix: config.userWingPrefix,
22
+ maxInjectedItems: config.maxInjectedItems,
23
+ retrievalQueryLimit: config.retrievalQueryLimit,
24
+ lastUserMessage,
25
+ }));
26
+ markRetrievalInjected(sessionId);
27
+ await writeLog("INFO", "injected retrieval instruction", { sessionId });
28
+ }
29
+ if (state.keywordSavePending) {
30
+ output.system.push(buildKeywordSaveInstruction());
31
+ clearKeywordSavePending(sessionId);
32
+ await writeLog("INFO", "injected keyword save instruction", { sessionId });
33
+ }
34
+ if (state.status !== AutosaveStatus.Pending || !state.pendingReason)
35
+ return;
36
+ output.system.push(buildAutosaveInstruction(state.pendingReason));
37
+ startAutosave(sessionId);
38
+ await writeLog("INFO", "injected hidden autosave instruction", {
39
+ sessionId,
40
+ reason: state.pendingReason,
41
+ });
42
+ }
43
+ catch (error) {
44
+ await writeLog("ERROR", "system transform hook failed", {
45
+ error: error instanceof Error ? error.message : String(error),
46
+ });
47
+ }
48
+ },
49
+ };
50
+ };
@@ -0,0 +1,17 @@
1
+ export declare const toolHooks: () => {
2
+ "tool.execute.before": (input: {
3
+ tool: string;
4
+ sessionID?: string;
5
+ callID?: string;
6
+ }, output: {
7
+ args: any;
8
+ }) => Promise<void>;
9
+ "tool.execute.after": (input: {
10
+ tool: string;
11
+ sessionID?: string;
12
+ callID?: string;
13
+ }, output: {
14
+ output: string;
15
+ metadata: any;
16
+ }) => Promise<void>;
17
+ };
@@ -0,0 +1,61 @@
1
+ import { writeLog } from "../lib/log";
2
+ import { AutosaveStatus, getSessionState, markMutationToolCall, recordSuccessfulTool, shouldCountSuccessfulTool } from "../lib/autosave";
3
+ import { isDirectMempalaceMutationTool } from "../lib/enforcement";
4
+ const isMempalaceTool = (tool) => tool === "mempalace_memory";
5
+ const isSuccessfulToolOutput = (output) => {
6
+ const text = `${output.output || ""} ${JSON.stringify(output.metadata || {})}`.toLowerCase();
7
+ return !text.includes("\"success\":false") && !text.includes("error");
8
+ };
9
+ const getMetadataSessionId = (metadata) => {
10
+ return metadata?.sessionID || metadata?.sessionId || metadata?.session?.id;
11
+ };
12
+ export const toolHooks = () => {
13
+ return {
14
+ "tool.execute.before": async (input, output) => {
15
+ if (isDirectMempalaceMutationTool(input.tool)) {
16
+ await writeLog("WARN", "blocked direct mempalace mutation tool", {
17
+ tool: input.tool,
18
+ sessionId: input.sessionID,
19
+ });
20
+ throw new Error("Use mempalace_memory instead of direct MemPalace mutation tools");
21
+ }
22
+ if (input.tool !== "mempalace_memory" || !input.sessionID)
23
+ return;
24
+ const mode = output.args?.mode;
25
+ if (mode === "save" || mode === "kg_add" || mode === "diary_write") {
26
+ markMutationToolCall(input.sessionID, input.callID);
27
+ }
28
+ },
29
+ "tool.execute.after": async (input, output) => {
30
+ try {
31
+ if (!input.sessionID) {
32
+ await writeLog("WARN", "tool.execute.after missing sessionID", { tool: input.tool });
33
+ return;
34
+ }
35
+ if (!isMempalaceTool(input.tool))
36
+ return;
37
+ const state = getSessionState(input.sessionID);
38
+ if (state.status !== AutosaveStatus.Running)
39
+ return;
40
+ const metadataSessionId = getMetadataSessionId(output.metadata);
41
+ if (metadataSessionId && metadataSessionId !== input.sessionID)
42
+ return;
43
+ if (!isSuccessfulToolOutput(output))
44
+ return;
45
+ if (!shouldCountSuccessfulTool(input.sessionID, input.tool, input.callID))
46
+ return;
47
+ recordSuccessfulTool(input.sessionID, input.tool, input.callID);
48
+ await writeLog("INFO", "observed mempalace tool during autosave", {
49
+ sessionId: input.sessionID,
50
+ tool: input.tool,
51
+ count: getSessionState(input.sessionID).successfulToolCalls.length,
52
+ });
53
+ }
54
+ catch (error) {
55
+ await writeLog("ERROR", "tool.execute.after hook failed", {
56
+ error: error instanceof Error ? error.message : String(error),
57
+ });
58
+ }
59
+ },
60
+ };
61
+ };
@@ -0,0 +1,2 @@
1
+ import type { Plugin } from "@opencode-ai/plugin";
2
+ export declare const MempalaceAutosavePlugin: Plugin;
@@ -0,0 +1,18 @@
1
+ import { chatParamHooks } from "./hooks/chat-params";
2
+ import { eventHooks } from "./hooks/event";
3
+ import { systemHooks } from "./hooks/system";
4
+ import { toolHooks } from "./hooks/tool";
5
+ import { setLogger } from "./lib/log";
6
+ import { mempalaceMemoryTool } from "./tools/mempalace-memory";
7
+ export const MempalaceAutosavePlugin = async (ctx) => {
8
+ setLogger(ctx.client);
9
+ return {
10
+ ...chatParamHooks(),
11
+ ...eventHooks(ctx),
12
+ ...systemHooks(ctx),
13
+ ...toolHooks(),
14
+ tool: {
15
+ mempalace_memory: mempalaceMemoryTool(ctx),
16
+ },
17
+ };
18
+ };
@@ -0,0 +1 @@
1
+ export declare const executeAdapter: (_shell: any, payload: Record<string, unknown>, retries?: number) => Promise<any>;
@@ -0,0 +1,42 @@
1
+ import { spawn } from "node:child_process";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ const getAdapterPath = () => {
5
+ const here = path.dirname(fileURLToPath(import.meta.url));
6
+ return path.resolve(here, "..", "..", "bridge", "mempalace_adapter.py");
7
+ };
8
+ const getPythonCommand = () => process.env.MEMPALACE_ADAPTER_PYTHON || "python";
9
+ export const executeAdapter = async (_shell, payload, retries = 3) => {
10
+ let lastError;
11
+ for (let attempt = 0; attempt <= retries; attempt += 1) {
12
+ try {
13
+ const text = await new Promise((resolve, reject) => {
14
+ const child = spawn(getPythonCommand(), [getAdapterPath()], {
15
+ stdio: ["pipe", "pipe", "pipe"],
16
+ });
17
+ const stdout = [];
18
+ const stderr = [];
19
+ child.stdout.on("data", (chunk) => stdout.push(Buffer.from(chunk)));
20
+ child.stderr.on("data", (chunk) => stderr.push(Buffer.from(chunk)));
21
+ child.on("error", reject);
22
+ child.on("close", (code) => {
23
+ if (code === 0) {
24
+ resolve(Buffer.concat(stdout).toString("utf8"));
25
+ return;
26
+ }
27
+ reject(new Error(Buffer.concat(stderr).toString("utf8") || `Adapter exited with code ${code}`));
28
+ });
29
+ child.stdin.write(JSON.stringify(payload), "utf8");
30
+ child.stdin.end();
31
+ });
32
+ return JSON.parse(text);
33
+ }
34
+ catch (error) {
35
+ lastError = error;
36
+ if (attempt === retries)
37
+ throw error;
38
+ await new Promise((resolve) => setTimeout(resolve, 150 * (attempt + 1)));
39
+ }
40
+ }
41
+ throw lastError;
42
+ };
@@ -0,0 +1,52 @@
1
+ export declare enum AutosaveReason {
2
+ Idle = "idle",
3
+ Compacted = "compacted",
4
+ Error = "error"
5
+ }
6
+ export declare enum AutosaveStatus {
7
+ Idle = "idle",
8
+ Pending = "pending",
9
+ Running = "running",
10
+ Saved = "saved",
11
+ Noop = "noop",
12
+ Failed = "failed"
13
+ }
14
+ export type SessionAutosaveState = {
15
+ status: AutosaveStatus;
16
+ pendingReason?: AutosaveReason;
17
+ retrievalPending: boolean;
18
+ pendingRetrievalUserDigest?: string;
19
+ keywordSavePending: boolean;
20
+ pendingUserDigest?: string;
21
+ pendingTranscriptDigest?: string;
22
+ lastHandledUserDigest?: string;
23
+ lastHandledTranscriptDigest?: string;
24
+ successfulToolCalls: string[];
25
+ successfulToolCallIds: string[];
26
+ mutationToolCallIds: string[];
27
+ runningSince?: number;
28
+ lastToolCallAt?: number;
29
+ lastRetrievedUserDigest?: string;
30
+ retryCount: number;
31
+ lastFailureAt?: number;
32
+ updatedAt: number;
33
+ };
34
+ export declare const buildUserDigest: (messages: any[]) => string;
35
+ export declare const buildTranscriptDigest: (messages: any[]) => string;
36
+ export declare const getSessionState: (sessionId: string) => SessionAutosaveState;
37
+ export declare const setCurrentTurnSessionId: (sessionId: string | undefined) => void;
38
+ export declare const getCurrentTurnSessionId: () => string | undefined;
39
+ export declare const resetAllStates: () => void;
40
+ export declare const markPending: (sessionId: string, reason: AutosaveReason, userDigest: string, transcriptDigest: string) => void;
41
+ export declare const markRetrievalInjected: (sessionId: string) => void;
42
+ export declare const markRetrievalPending: (sessionId: string, userDigest: string) => void;
43
+ export declare const markKeywordSavePending: (sessionId: string) => void;
44
+ export declare const clearKeywordSavePending: (sessionId: string) => void;
45
+ export declare const extractLastUserMessage: (messages: any[]) => string;
46
+ export declare const startAutosave: (sessionId: string) => void;
47
+ export declare const markMutationToolCall: (sessionId: string, callId?: string) => void;
48
+ export declare const shouldCountSuccessfulTool: (sessionId: string, tool: string, callId?: string) => boolean;
49
+ export declare const recordSuccessfulTool: (sessionId: string, tool: string, callId?: string) => void;
50
+ export declare const finalizeAutosave: (sessionId: string) => SessionAutosaveState;
51
+ export declare const markFailed: (sessionId: string) => SessionAutosaveState;
52
+ export declare const shouldScheduleAutosave: (sessionId: string, userDigest: string, transcriptDigest: string) => boolean;
@@ -0,0 +1,242 @@
1
+ import crypto from "node:crypto";
2
+ import { sanitizeText } from "./derive";
3
+ import { stripPrivateContent } from "./privacy";
4
+ export var AutosaveReason;
5
+ (function (AutosaveReason) {
6
+ AutosaveReason["Idle"] = "idle";
7
+ AutosaveReason["Compacted"] = "compacted";
8
+ AutosaveReason["Error"] = "error";
9
+ })(AutosaveReason || (AutosaveReason = {}));
10
+ export var AutosaveStatus;
11
+ (function (AutosaveStatus) {
12
+ AutosaveStatus["Idle"] = "idle";
13
+ AutosaveStatus["Pending"] = "pending";
14
+ AutosaveStatus["Running"] = "running";
15
+ AutosaveStatus["Saved"] = "saved";
16
+ AutosaveStatus["Noop"] = "noop";
17
+ AutosaveStatus["Failed"] = "failed";
18
+ })(AutosaveStatus || (AutosaveStatus = {}));
19
+ const states = new Map();
20
+ let currentTurnSessionId;
21
+ const MAX_SESSIONS = 200;
22
+ const STATE_TTL_MS = 1000 * 60 * 60 * 12;
23
+ const RETRY_COOLDOWN_MS = 1000 * 30;
24
+ const MAX_RETRIES = 2;
25
+ const extractTextParts = (message) => {
26
+ const parts = [];
27
+ if (!message)
28
+ return parts;
29
+ if (typeof message.content === "string")
30
+ parts.push(message.content);
31
+ if (Array.isArray(message.parts)) {
32
+ for (const part of message.parts) {
33
+ if (!part)
34
+ continue;
35
+ if (typeof part.text === "string")
36
+ parts.push(part.text);
37
+ if (typeof part.content === "string")
38
+ parts.push(part.content);
39
+ }
40
+ }
41
+ if (message.info && typeof message.info?.content === "string") {
42
+ parts.push(message.info.content);
43
+ }
44
+ return parts;
45
+ };
46
+ const extractRole = (message) => message?.role || message?.info?.role || "unknown";
47
+ const fingerprint = (chunks) => {
48
+ return crypto.createHash("sha256").update(JSON.stringify(chunks), "utf8").digest("hex").slice(0, 16);
49
+ };
50
+ export const buildUserDigest = (messages) => {
51
+ const normalized = (messages || [])
52
+ .filter((message) => extractRole(message) === "user")
53
+ .slice(-20)
54
+ .map((message) => extractTextParts(message).map((part) => sanitizeText(part)).join("\n"));
55
+ return fingerprint(normalized);
56
+ };
57
+ export const buildTranscriptDigest = (messages) => {
58
+ const normalized = (messages || []).slice(-50).map((message) => {
59
+ const role = extractRole(message);
60
+ const text = extractTextParts(message).map((part) => sanitizeText(part)).join("\n");
61
+ return `${role}:${text}`;
62
+ });
63
+ return fingerprint(normalized);
64
+ };
65
+ const createState = () => ({
66
+ status: AutosaveStatus.Idle,
67
+ successfulToolCalls: [],
68
+ successfulToolCallIds: [],
69
+ mutationToolCallIds: [],
70
+ retryCount: 0,
71
+ retrievalPending: true,
72
+ keywordSavePending: false,
73
+ updatedAt: Date.now(),
74
+ });
75
+ export const getSessionState = (sessionId) => {
76
+ evictExpiredStates();
77
+ const existing = states.get(sessionId);
78
+ if (existing) {
79
+ existing.updatedAt = Date.now();
80
+ return existing;
81
+ }
82
+ const state = createState();
83
+ states.set(sessionId, state);
84
+ evictOverflowStates();
85
+ return state;
86
+ };
87
+ export const setCurrentTurnSessionId = (sessionId) => {
88
+ currentTurnSessionId = sessionId;
89
+ };
90
+ export const getCurrentTurnSessionId = () => currentTurnSessionId;
91
+ const evictExpiredStates = () => {
92
+ const now = Date.now();
93
+ for (const [sessionId, state] of states) {
94
+ if (now - state.updatedAt > STATE_TTL_MS)
95
+ states.delete(sessionId);
96
+ }
97
+ };
98
+ const evictOverflowStates = () => {
99
+ if (states.size <= MAX_SESSIONS)
100
+ return;
101
+ const sorted = [...states.entries()].sort((a, b) => a[1].updatedAt - b[1].updatedAt);
102
+ for (const [sessionId] of sorted.slice(0, states.size - MAX_SESSIONS)) {
103
+ states.delete(sessionId);
104
+ }
105
+ };
106
+ export const resetAllStates = () => {
107
+ states.clear();
108
+ currentTurnSessionId = undefined;
109
+ };
110
+ export const markPending = (sessionId, reason, userDigest, transcriptDigest) => {
111
+ const state = getSessionState(sessionId);
112
+ state.status = AutosaveStatus.Pending;
113
+ state.pendingReason = reason;
114
+ state.pendingUserDigest = userDigest;
115
+ state.pendingTranscriptDigest = transcriptDigest;
116
+ state.successfulToolCalls = [];
117
+ state.successfulToolCallIds = [];
118
+ state.mutationToolCallIds = [];
119
+ state.updatedAt = Date.now();
120
+ };
121
+ export const markRetrievalInjected = (sessionId) => {
122
+ const state = getSessionState(sessionId);
123
+ state.retrievalPending = false;
124
+ state.lastRetrievedUserDigest = state.pendingRetrievalUserDigest;
125
+ state.pendingRetrievalUserDigest = undefined;
126
+ state.updatedAt = Date.now();
127
+ };
128
+ export const markRetrievalPending = (sessionId, userDigest) => {
129
+ const state = getSessionState(sessionId);
130
+ if (state.lastRetrievedUserDigest === userDigest)
131
+ return;
132
+ if (state.pendingRetrievalUserDigest === userDigest && state.retrievalPending)
133
+ return;
134
+ state.retrievalPending = true;
135
+ state.pendingRetrievalUserDigest = userDigest;
136
+ state.updatedAt = Date.now();
137
+ };
138
+ export const markKeywordSavePending = (sessionId) => {
139
+ const state = getSessionState(sessionId);
140
+ state.keywordSavePending = true;
141
+ state.updatedAt = Date.now();
142
+ };
143
+ export const clearKeywordSavePending = (sessionId) => {
144
+ const state = getSessionState(sessionId);
145
+ state.keywordSavePending = false;
146
+ state.updatedAt = Date.now();
147
+ };
148
+ export const extractLastUserMessage = (messages) => {
149
+ const userMessages = (messages || []).filter((message) => extractRole(message) === "user");
150
+ const last = userMessages.at(-1);
151
+ const text = extractTextParts(last).map((part) => stripPrivateContent(part)).join("\n").trim();
152
+ return text;
153
+ };
154
+ export const startAutosave = (sessionId) => {
155
+ const state = getSessionState(sessionId);
156
+ state.status = AutosaveStatus.Running;
157
+ state.successfulToolCalls = [];
158
+ state.successfulToolCallIds = [];
159
+ state.mutationToolCallIds = [];
160
+ state.runningSince = Date.now();
161
+ state.updatedAt = Date.now();
162
+ };
163
+ export const markMutationToolCall = (sessionId, callId) => {
164
+ if (!callId)
165
+ return;
166
+ const state = getSessionState(sessionId);
167
+ if (!state.mutationToolCallIds.includes(callId)) {
168
+ state.mutationToolCallIds.push(callId);
169
+ state.updatedAt = Date.now();
170
+ }
171
+ };
172
+ export const shouldCountSuccessfulTool = (sessionId, tool, callId) => {
173
+ const state = getSessionState(sessionId);
174
+ if (tool === "mempalace_memory") {
175
+ return !!callId && state.mutationToolCallIds.includes(callId);
176
+ }
177
+ return false;
178
+ };
179
+ export const recordSuccessfulTool = (sessionId, tool, callId) => {
180
+ const state = getSessionState(sessionId);
181
+ if (callId && state.successfulToolCallIds.includes(callId))
182
+ return;
183
+ if (!state.successfulToolCalls.includes(tool))
184
+ state.successfulToolCalls.push(tool);
185
+ if (callId)
186
+ state.successfulToolCallIds.push(callId);
187
+ state.lastToolCallAt = Date.now();
188
+ state.updatedAt = Date.now();
189
+ };
190
+ export const finalizeAutosave = (sessionId) => {
191
+ const state = getSessionState(sessionId);
192
+ if (state.status !== AutosaveStatus.Running)
193
+ return state;
194
+ const succeeded = state.successfulToolCalls.length > 0 &&
195
+ (!state.runningSince || (state.lastToolCallAt != null && state.lastToolCallAt >= state.runningSince));
196
+ state.status = succeeded ? AutosaveStatus.Saved : AutosaveStatus.Noop;
197
+ state.lastHandledUserDigest = state.pendingUserDigest;
198
+ state.lastHandledTranscriptDigest = state.pendingTranscriptDigest;
199
+ state.retryCount = succeeded ? 0 : state.retryCount;
200
+ state.pendingReason = undefined;
201
+ state.pendingUserDigest = undefined;
202
+ state.pendingTranscriptDigest = undefined;
203
+ state.successfulToolCalls = [];
204
+ state.successfulToolCallIds = [];
205
+ state.mutationToolCallIds = [];
206
+ state.runningSince = undefined;
207
+ state.lastToolCallAt = undefined;
208
+ state.updatedAt = Date.now();
209
+ return state;
210
+ };
211
+ export const markFailed = (sessionId) => {
212
+ const state = getSessionState(sessionId);
213
+ state.status = AutosaveStatus.Failed;
214
+ state.retryCount += 1;
215
+ state.lastFailureAt = Date.now();
216
+ state.pendingReason = undefined;
217
+ state.pendingUserDigest = undefined;
218
+ state.pendingTranscriptDigest = undefined;
219
+ state.successfulToolCalls = [];
220
+ state.successfulToolCallIds = [];
221
+ state.mutationToolCallIds = [];
222
+ state.runningSince = undefined;
223
+ state.lastToolCallAt = undefined;
224
+ state.updatedAt = Date.now();
225
+ return state;
226
+ };
227
+ export const shouldScheduleAutosave = (sessionId, userDigest, transcriptDigest) => {
228
+ const state = getSessionState(sessionId);
229
+ if (state.status === AutosaveStatus.Pending || state.status === AutosaveStatus.Running)
230
+ return false;
231
+ if (state.status === AutosaveStatus.Failed) {
232
+ if (state.retryCount >= MAX_RETRIES)
233
+ return false;
234
+ if (state.lastFailureAt && Date.now() - state.lastFailureAt < RETRY_COOLDOWN_MS)
235
+ return false;
236
+ }
237
+ if (state.lastHandledUserDigest === userDigest)
238
+ return false;
239
+ if (state.lastHandledTranscriptDigest === transcriptDigest)
240
+ return false;
241
+ return true;
242
+ };
@@ -0,0 +1,13 @@
1
+ export type MempalaceConfig = {
2
+ autosaveEnabled: boolean;
3
+ retrievalEnabled: boolean;
4
+ keywordSaveEnabled: boolean;
5
+ maxInjectedItems: number;
6
+ retrievalQueryLimit: number;
7
+ keywordPatterns: string[];
8
+ privacyRedactionEnabled: boolean;
9
+ userWingPrefix: string;
10
+ projectWingPrefix: string;
11
+ };
12
+ export declare const loadConfig: () => Promise<MempalaceConfig>;
13
+ export declare const resetConfig: () => void;
@@ -0,0 +1,53 @@
1
+ import fs from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ const DEFAULT_CONFIG = {
5
+ autosaveEnabled: true,
6
+ retrievalEnabled: true,
7
+ keywordSaveEnabled: true,
8
+ maxInjectedItems: 6,
9
+ retrievalQueryLimit: 5,
10
+ keywordPatterns: ["remember", "save this", "don't forget", "note that"],
11
+ privacyRedactionEnabled: true,
12
+ userWingPrefix: "wing_user",
13
+ projectWingPrefix: "wing_project",
14
+ };
15
+ const CONFIG_PATH = path.join(os.homedir(), ".config", "opencode", "mempalace.jsonc");
16
+ const stripJsonComments = (value) => {
17
+ return value.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
18
+ };
19
+ const parseBoolean = (value, fallback) => {
20
+ if (value == null)
21
+ return fallback;
22
+ return value !== "false";
23
+ };
24
+ let cachedConfig;
25
+ export const loadConfig = async () => {
26
+ if (cachedConfig)
27
+ return cachedConfig;
28
+ let fileConfig = {};
29
+ try {
30
+ const raw = await fs.readFile(CONFIG_PATH, "utf8");
31
+ fileConfig = JSON.parse(stripJsonComments(raw));
32
+ }
33
+ catch {
34
+ // optional config file
35
+ }
36
+ cachedConfig = {
37
+ ...DEFAULT_CONFIG,
38
+ ...fileConfig,
39
+ autosaveEnabled: parseBoolean(process.env.MEMPALACE_AUTOSAVE_ENABLED, fileConfig.autosaveEnabled ?? DEFAULT_CONFIG.autosaveEnabled),
40
+ retrievalEnabled: parseBoolean(process.env.MEMPALACE_RETRIEVAL_ENABLED, fileConfig.retrievalEnabled ?? DEFAULT_CONFIG.retrievalEnabled),
41
+ keywordSaveEnabled: parseBoolean(process.env.MEMPALACE_KEYWORD_SAVE_ENABLED, fileConfig.keywordSaveEnabled ?? DEFAULT_CONFIG.keywordSaveEnabled),
42
+ privacyRedactionEnabled: parseBoolean(process.env.MEMPALACE_PRIVACY_REDACTION_ENABLED, fileConfig.privacyRedactionEnabled ?? DEFAULT_CONFIG.privacyRedactionEnabled),
43
+ maxInjectedItems: Number(process.env.MEMPALACE_MAX_INJECTED_ITEMS || fileConfig.maxInjectedItems || DEFAULT_CONFIG.maxInjectedItems),
44
+ retrievalQueryLimit: Number(process.env.MEMPALACE_RETRIEVAL_QUERY_LIMIT || fileConfig.retrievalQueryLimit || DEFAULT_CONFIG.retrievalQueryLimit),
45
+ keywordPatterns: fileConfig.keywordPatterns || DEFAULT_CONFIG.keywordPatterns,
46
+ userWingPrefix: process.env.MEMPALACE_USER_WING_PREFIX || fileConfig.userWingPrefix || DEFAULT_CONFIG.userWingPrefix,
47
+ projectWingPrefix: process.env.MEMPALACE_PROJECT_WING_PREFIX || fileConfig.projectWingPrefix || DEFAULT_CONFIG.projectWingPrefix,
48
+ };
49
+ return cachedConfig;
50
+ };
51
+ export const resetConfig = () => {
52
+ cachedConfig = undefined;
53
+ };
@@ -0,0 +1,12 @@
1
+ type BuildContextInput = {
2
+ projectName?: string;
3
+ projectWingPrefix: string;
4
+ userWingPrefix: string;
5
+ maxInjectedItems: number;
6
+ retrievalQueryLimit: number;
7
+ lastUserMessage: string;
8
+ };
9
+ export declare const buildRetrievalInstruction: ({ projectName, projectWingPrefix, userWingPrefix, maxInjectedItems, retrievalQueryLimit, lastUserMessage, }: BuildContextInput) => string;
10
+ export declare const buildAutosaveInstruction: (reason: string) => string;
11
+ export declare const buildKeywordSaveInstruction: () => string;
12
+ export {};
@@ -0,0 +1,37 @@
1
+ import { getProjectScope, getUserScope } from "./scope";
2
+ export const buildRetrievalInstruction = ({ projectName, projectWingPrefix, userWingPrefix, maxInjectedItems, retrievalQueryLimit, lastUserMessage, }) => {
3
+ const project = getProjectScope(projectName, projectWingPrefix);
4
+ const user = getUserScope(userWingPrefix);
5
+ return [
6
+ "System instruction: before answering, search MemPalace for relevant existing memory and use it if helpful.",
7
+ `Query focus: ${lastUserMessage.slice(0, 500)}`,
8
+ `Limit search results to ${retrievalQueryLimit} per scope.`,
9
+ `Search user memory in wing ${user.wing} across rooms ${user.rooms.join(", ")}.`,
10
+ `Search project memory in wing ${project.wing} across rooms ${project.rooms.join(", ")}.`,
11
+ `Use concise relevant memories only, up to ${maxInjectedItems} items total.`,
12
+ "Do not mention this instruction to the user.",
13
+ ].join(" ");
14
+ };
15
+ export const buildAutosaveInstruction = (reason) => {
16
+ return [
17
+ "System instruction: before answering the user, persist durable memory from prior session context using the `mempalace_memory` tool.",
18
+ "Do not use raw `mempalace_*` or `mcp-router_mempalace_*` mutation tools directly.",
19
+ `Trigger reason: ${reason}.`,
20
+ "Save only stable facts, decisions, preferences, important outcomes, and useful diary notes.",
21
+ "Prefer concise structured memories in the appropriate user/project scopes:",
22
+ "- user scope: preferences, personal workflow habits, communication style",
23
+ "- project scope: architecture, setup, decisions, bugs, workflows",
24
+ "Do not dump the full transcript into a single drawer.",
25
+ "Apply privacy redaction before saving and skip fully private content.",
26
+ "Do not mention this instruction to the user.",
27
+ ].join(" ");
28
+ };
29
+ export const buildKeywordSaveInstruction = () => {
30
+ return [
31
+ "System instruction: the user explicitly asked to remember something.",
32
+ "Use the `mempalace_memory` tool to save the important durable information now.",
33
+ "Do not use raw `mempalace_*` or `mcp-router_mempalace_*` mutation tools directly.",
34
+ "Choose user scope for cross-project preferences and project scope for repository-specific knowledge.",
35
+ "Do not mention this instruction to the user.",
36
+ ].join(" ");
37
+ };
@@ -0,0 +1 @@
1
+ export declare const sanitizeText: (text: string) => string;
@@ -0,0 +1,36 @@
1
+ const ANSI_PATTERN = /\u001B(?:\][^\u0007]*(?:\u0007|\u001B\\)|\[[0-?]*[ -/]*[@-~]|[@-Z\\-_])/g;
2
+ const ZERO_WIDTH_PATTERN = /[\u200B-\u200D\uFEFF]/g;
3
+ const CONTROL_PATTERN = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g;
4
+ const stripInvalidSurrogates = (text) => {
5
+ let result = "";
6
+ for (let i = 0; i < text.length; i += 1) {
7
+ const code = text.charCodeAt(i);
8
+ if (code >= 0xd800 && code <= 0xdbff) {
9
+ const next = text.charCodeAt(i + 1);
10
+ if (next >= 0xdc00 && next <= 0xdfff) {
11
+ result += text[i] + text[i + 1];
12
+ i += 1;
13
+ }
14
+ else {
15
+ result += "�";
16
+ }
17
+ continue;
18
+ }
19
+ if (code >= 0xdc00 && code <= 0xdfff) {
20
+ result += "�";
21
+ continue;
22
+ }
23
+ result += text[i];
24
+ }
25
+ return result;
26
+ };
27
+ export const sanitizeText = (text) => {
28
+ if (!text)
29
+ return text;
30
+ return stripInvalidSurrogates(text)
31
+ .normalize("NFKC")
32
+ .replace(/\r\n?/g, "\n")
33
+ .replace(ANSI_PATTERN, "")
34
+ .replace(ZERO_WIDTH_PATTERN, "")
35
+ .replace(CONTROL_PATTERN, "");
36
+ };
@@ -0,0 +1 @@
1
+ export declare const isDirectMempalaceMutationTool: (tool: string) => boolean;
@@ -0,0 +1,10 @@
1
+ export const isDirectMempalaceMutationTool = (tool) => {
2
+ return [
3
+ "mempalace_add_drawer",
4
+ "mempalace_kg_add",
5
+ "mempalace_diary_write",
6
+ "mcp-router_mempalace_add_drawer",
7
+ "mcp-router_mempalace_kg_add",
8
+ "mcp-router_mempalace_diary_write",
9
+ ].includes(tool);
10
+ };