@rvboris/opencode-mempalace 0.1.0 → 0.2.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 +114 -158
- package/README.ru.md +115 -159
- package/dist/bridge/mempalace_adapter.py +73 -40
- package/dist/plugin/hooks/event.d.ts +3 -10
- package/dist/plugin/hooks/event.js +77 -44
- package/dist/plugin/hooks/system.d.ts +5 -7
- package/dist/plugin/hooks/system.js +16 -19
- package/dist/plugin/hooks/tool.d.ts +2 -10
- package/dist/plugin/hooks/tool.js +4 -49
- package/dist/plugin/index.js +0 -2
- package/dist/plugin/lib/adapter.d.ts +2 -1
- package/dist/plugin/lib/adapter.js +56 -9
- package/dist/plugin/lib/autosave.d.ts +16 -21
- package/dist/plugin/lib/autosave.js +47 -101
- package/dist/plugin/lib/config.d.ts +4 -3
- package/dist/plugin/lib/config.js +58 -19
- package/dist/plugin/lib/constants.d.ts +81 -0
- package/dist/plugin/lib/constants.js +88 -0
- package/dist/plugin/lib/context.js +17 -16
- package/dist/plugin/lib/enforcement.js +2 -8
- package/dist/plugin/lib/log.d.ts +2 -7
- package/dist/plugin/lib/log.js +3 -2
- package/dist/plugin/lib/opencode.d.ts +5 -0
- package/dist/plugin/lib/opencode.js +16 -0
- package/dist/plugin/lib/scope.d.ts +2 -2
- package/dist/plugin/lib/scope.js +3 -2
- package/dist/plugin/lib/types.d.ts +79 -0
- package/dist/plugin/lib/types.js +15 -0
- package/dist/plugin/tools/mempalace-memory.d.ts +4 -8
- package/dist/plugin/tools/mempalace-memory.js +31 -23
- package/package.json +11 -2
- package/dist/plugin/hooks/chat-params.d.ts +0 -5
- package/dist/plugin/hooks/chat-params.js +0 -8
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { MessageLike } from "./types";
|
|
1
2
|
export declare enum AutosaveReason {
|
|
2
3
|
Idle = "idle",
|
|
3
4
|
Compacted = "compacted",
|
|
@@ -5,48 +6,42 @@ export declare enum AutosaveReason {
|
|
|
5
6
|
}
|
|
6
7
|
export declare enum AutosaveStatus {
|
|
7
8
|
Idle = "idle",
|
|
8
|
-
Pending = "pending",
|
|
9
|
-
Running = "running",
|
|
10
9
|
Saved = "saved",
|
|
11
10
|
Noop = "noop",
|
|
12
11
|
Failed = "failed"
|
|
13
12
|
}
|
|
14
13
|
export type SessionAutosaveState = {
|
|
15
14
|
status: AutosaveStatus;
|
|
16
|
-
pendingReason?: AutosaveReason;
|
|
17
15
|
retrievalPending: boolean;
|
|
18
16
|
pendingRetrievalUserDigest?: string;
|
|
19
17
|
keywordSavePending: boolean;
|
|
20
|
-
pendingUserDigest?: string;
|
|
21
|
-
pendingTranscriptDigest?: string;
|
|
22
18
|
lastHandledUserDigest?: string;
|
|
23
19
|
lastHandledTranscriptDigest?: string;
|
|
24
|
-
successfulToolCalls: string[];
|
|
25
|
-
successfulToolCallIds: string[];
|
|
26
|
-
mutationToolCallIds: string[];
|
|
27
|
-
runningSince?: number;
|
|
28
|
-
lastToolCallAt?: number;
|
|
29
20
|
lastRetrievedUserDigest?: string;
|
|
30
21
|
retryCount: number;
|
|
31
22
|
lastFailureAt?: number;
|
|
23
|
+
messageSnapshot?: SessionMessageSnapshot;
|
|
32
24
|
updatedAt: number;
|
|
33
25
|
};
|
|
34
|
-
export
|
|
35
|
-
|
|
26
|
+
export type SessionMessageSnapshot = {
|
|
27
|
+
userDigest: string;
|
|
28
|
+
transcriptDigest: string;
|
|
29
|
+
lastUserMessage: string;
|
|
30
|
+
transcript: string;
|
|
31
|
+
};
|
|
32
|
+
export declare const buildUserDigest: (messages: readonly MessageLike[] | null | undefined) => string;
|
|
33
|
+
export declare const buildTranscriptDigest: (messages: readonly MessageLike[] | null | undefined) => string;
|
|
34
|
+
export declare const buildMessageSnapshot: (messages: readonly MessageLike[] | null | undefined) => SessionMessageSnapshot;
|
|
36
35
|
export declare const getSessionState: (sessionId: string) => SessionAutosaveState;
|
|
37
|
-
export declare const setCurrentTurnSessionId: (sessionId: string | undefined) => void;
|
|
38
|
-
export declare const getCurrentTurnSessionId: () => string | undefined;
|
|
39
36
|
export declare const resetAllStates: () => void;
|
|
40
|
-
export declare const
|
|
37
|
+
export declare const markAutosaveComplete: (sessionId: string, userDigest: string, transcriptDigest: string, status: AutosaveStatus.Saved | AutosaveStatus.Noop) => SessionAutosaveState;
|
|
41
38
|
export declare const markRetrievalInjected: (sessionId: string) => void;
|
|
42
39
|
export declare const markRetrievalPending: (sessionId: string, userDigest: string) => void;
|
|
40
|
+
export declare const setMessageSnapshot: (sessionId: string, snapshot: SessionMessageSnapshot) => void;
|
|
41
|
+
export declare const getMessageSnapshot: (sessionId: string) => SessionMessageSnapshot | undefined;
|
|
43
42
|
export declare const markKeywordSavePending: (sessionId: string) => void;
|
|
44
43
|
export declare const clearKeywordSavePending: (sessionId: string) => void;
|
|
45
|
-
export declare const extractLastUserMessage: (messages:
|
|
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;
|
|
44
|
+
export declare const extractLastUserMessage: (messages: readonly MessageLike[] | null | undefined) => string;
|
|
51
45
|
export declare const markFailed: (sessionId: string) => SessionAutosaveState;
|
|
52
46
|
export declare const shouldScheduleAutosave: (sessionId: string, userDigest: string, transcriptDigest: string) => boolean;
|
|
47
|
+
export declare const buildTranscriptText: (messages: readonly MessageLike[] | null | undefined) => string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import { sanitizeText } from "./derive";
|
|
3
3
|
import { stripPrivateContent } from "./privacy";
|
|
4
|
+
import { MAX_USER_MESSAGES_DIGEST, MAX_TRANSCRIPT_MESSAGES_DIGEST } from "./constants";
|
|
4
5
|
export var AutosaveReason;
|
|
5
6
|
(function (AutosaveReason) {
|
|
6
7
|
AutosaveReason["Idle"] = "idle";
|
|
@@ -10,14 +11,11 @@ export var AutosaveReason;
|
|
|
10
11
|
export var AutosaveStatus;
|
|
11
12
|
(function (AutosaveStatus) {
|
|
12
13
|
AutosaveStatus["Idle"] = "idle";
|
|
13
|
-
AutosaveStatus["Pending"] = "pending";
|
|
14
|
-
AutosaveStatus["Running"] = "running";
|
|
15
14
|
AutosaveStatus["Saved"] = "saved";
|
|
16
15
|
AutosaveStatus["Noop"] = "noop";
|
|
17
16
|
AutosaveStatus["Failed"] = "failed";
|
|
18
17
|
})(AutosaveStatus || (AutosaveStatus = {}));
|
|
19
18
|
const states = new Map();
|
|
20
|
-
let currentTurnSessionId;
|
|
21
19
|
const MAX_SESSIONS = 200;
|
|
22
20
|
const STATE_TTL_MS = 1000 * 60 * 60 * 12;
|
|
23
21
|
const RETRY_COOLDOWN_MS = 1000 * 30;
|
|
@@ -28,45 +26,46 @@ const extractTextParts = (message) => {
|
|
|
28
26
|
return parts;
|
|
29
27
|
if (typeof message.content === "string")
|
|
30
28
|
parts.push(message.content);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
parts.push(part.text);
|
|
37
|
-
if (typeof part.content === "string")
|
|
38
|
-
parts.push(part.content);
|
|
39
|
-
}
|
|
29
|
+
for (const part of message.parts ?? []) {
|
|
30
|
+
if ("text" in part && typeof part.text === "string")
|
|
31
|
+
parts.push(part.text);
|
|
32
|
+
if ("content" in part && typeof part.content === "string")
|
|
33
|
+
parts.push(part.content);
|
|
40
34
|
}
|
|
41
|
-
if (message.info && typeof message.info
|
|
35
|
+
if (message.info && "content" in message.info && typeof message.info.content === "string") {
|
|
42
36
|
parts.push(message.info.content);
|
|
43
37
|
}
|
|
44
38
|
return parts;
|
|
45
39
|
};
|
|
46
|
-
const extractRole = (message) => message?.role
|
|
40
|
+
const extractRole = (message) => message?.role ?? message?.info?.role ?? "unknown";
|
|
47
41
|
const fingerprint = (chunks) => {
|
|
48
42
|
return crypto.createHash("sha256").update(JSON.stringify(chunks), "utf8").digest("hex").slice(0, 16);
|
|
49
43
|
};
|
|
50
44
|
export const buildUserDigest = (messages) => {
|
|
51
|
-
const normalized = (messages
|
|
45
|
+
const normalized = (messages ?? [])
|
|
52
46
|
.filter((message) => extractRole(message) === "user")
|
|
53
|
-
.slice(-
|
|
47
|
+
.slice(-MAX_USER_MESSAGES_DIGEST)
|
|
54
48
|
.map((message) => extractTextParts(message).map((part) => sanitizeText(part)).join("\n"));
|
|
55
49
|
return fingerprint(normalized);
|
|
56
50
|
};
|
|
57
51
|
export const buildTranscriptDigest = (messages) => {
|
|
58
|
-
const normalized = (messages
|
|
52
|
+
const normalized = (messages ?? []).slice(-MAX_TRANSCRIPT_MESSAGES_DIGEST).map((message) => {
|
|
59
53
|
const role = extractRole(message);
|
|
60
54
|
const text = extractTextParts(message).map((part) => sanitizeText(part)).join("\n");
|
|
61
55
|
return `${role}:${text}`;
|
|
62
56
|
});
|
|
63
57
|
return fingerprint(normalized);
|
|
64
58
|
};
|
|
59
|
+
export const buildMessageSnapshot = (messages) => {
|
|
60
|
+
return {
|
|
61
|
+
userDigest: buildUserDigest(messages),
|
|
62
|
+
transcriptDigest: buildTranscriptDigest(messages),
|
|
63
|
+
lastUserMessage: extractLastUserMessage(messages),
|
|
64
|
+
transcript: buildTranscriptText(messages),
|
|
65
|
+
};
|
|
66
|
+
};
|
|
65
67
|
const createState = () => ({
|
|
66
68
|
status: AutosaveStatus.Idle,
|
|
67
|
-
successfulToolCalls: [],
|
|
68
|
-
successfulToolCallIds: [],
|
|
69
|
-
mutationToolCallIds: [],
|
|
70
69
|
retryCount: 0,
|
|
71
70
|
retrievalPending: true,
|
|
72
71
|
keywordSavePending: false,
|
|
@@ -84,10 +83,6 @@ export const getSessionState = (sessionId) => {
|
|
|
84
83
|
evictOverflowStates();
|
|
85
84
|
return state;
|
|
86
85
|
};
|
|
87
|
-
export const setCurrentTurnSessionId = (sessionId) => {
|
|
88
|
-
currentTurnSessionId = sessionId;
|
|
89
|
-
};
|
|
90
|
-
export const getCurrentTurnSessionId = () => currentTurnSessionId;
|
|
91
86
|
const evictExpiredStates = () => {
|
|
92
87
|
const now = Date.now();
|
|
93
88
|
for (const [sessionId, state] of states) {
|
|
@@ -105,18 +100,17 @@ const evictOverflowStates = () => {
|
|
|
105
100
|
};
|
|
106
101
|
export const resetAllStates = () => {
|
|
107
102
|
states.clear();
|
|
108
|
-
currentTurnSessionId = undefined;
|
|
109
103
|
};
|
|
110
|
-
export const
|
|
104
|
+
export const markAutosaveComplete = (sessionId, userDigest, transcriptDigest, status) => {
|
|
111
105
|
const state = getSessionState(sessionId);
|
|
112
|
-
state.status =
|
|
113
|
-
state.
|
|
114
|
-
state.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
state.mutationToolCallIds = [];
|
|
106
|
+
state.status = status;
|
|
107
|
+
state.lastHandledUserDigest = userDigest;
|
|
108
|
+
state.lastHandledTranscriptDigest = transcriptDigest;
|
|
109
|
+
if (status === AutosaveStatus.Saved) {
|
|
110
|
+
state.retryCount = 0;
|
|
111
|
+
}
|
|
119
112
|
state.updatedAt = Date.now();
|
|
113
|
+
return state;
|
|
120
114
|
};
|
|
121
115
|
export const markRetrievalInjected = (sessionId) => {
|
|
122
116
|
const state = getSessionState(sessionId);
|
|
@@ -135,6 +129,14 @@ export const markRetrievalPending = (sessionId, userDigest) => {
|
|
|
135
129
|
state.pendingRetrievalUserDigest = userDigest;
|
|
136
130
|
state.updatedAt = Date.now();
|
|
137
131
|
};
|
|
132
|
+
export const setMessageSnapshot = (sessionId, snapshot) => {
|
|
133
|
+
const state = getSessionState(sessionId);
|
|
134
|
+
state.messageSnapshot = snapshot;
|
|
135
|
+
state.updatedAt = Date.now();
|
|
136
|
+
};
|
|
137
|
+
export const getMessageSnapshot = (sessionId) => {
|
|
138
|
+
return getSessionState(sessionId).messageSnapshot;
|
|
139
|
+
};
|
|
138
140
|
export const markKeywordSavePending = (sessionId) => {
|
|
139
141
|
const state = getSessionState(sessionId);
|
|
140
142
|
state.keywordSavePending = true;
|
|
@@ -146,88 +148,21 @@ export const clearKeywordSavePending = (sessionId) => {
|
|
|
146
148
|
state.updatedAt = Date.now();
|
|
147
149
|
};
|
|
148
150
|
export const extractLastUserMessage = (messages) => {
|
|
149
|
-
const userMessages = (messages
|
|
151
|
+
const userMessages = (messages ?? []).filter((message) => extractRole(message) === "user");
|
|
150
152
|
const last = userMessages.at(-1);
|
|
151
153
|
const text = extractTextParts(last).map((part) => stripPrivateContent(part)).join("\n").trim();
|
|
152
154
|
return text;
|
|
153
155
|
};
|
|
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
156
|
export const markFailed = (sessionId) => {
|
|
212
157
|
const state = getSessionState(sessionId);
|
|
213
158
|
state.status = AutosaveStatus.Failed;
|
|
214
159
|
state.retryCount += 1;
|
|
215
160
|
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
161
|
state.updatedAt = Date.now();
|
|
225
162
|
return state;
|
|
226
163
|
};
|
|
227
164
|
export const shouldScheduleAutosave = (sessionId, userDigest, transcriptDigest) => {
|
|
228
165
|
const state = getSessionState(sessionId);
|
|
229
|
-
if (state.status === AutosaveStatus.Pending || state.status === AutosaveStatus.Running)
|
|
230
|
-
return false;
|
|
231
166
|
if (state.status === AutosaveStatus.Failed) {
|
|
232
167
|
if (state.retryCount >= MAX_RETRIES)
|
|
233
168
|
return false;
|
|
@@ -240,3 +175,14 @@ export const shouldScheduleAutosave = (sessionId, userDigest, transcriptDigest)
|
|
|
240
175
|
return false;
|
|
241
176
|
return true;
|
|
242
177
|
};
|
|
178
|
+
export const buildTranscriptText = (messages) => {
|
|
179
|
+
const lines = [];
|
|
180
|
+
for (const message of messages ?? []) {
|
|
181
|
+
const role = extractRole(message);
|
|
182
|
+
const text = extractTextParts(message).map((part) => stripPrivateContent(part)).join("\n").trim();
|
|
183
|
+
if (!text)
|
|
184
|
+
continue;
|
|
185
|
+
lines.push(`${role.toUpperCase()}: ${text}`);
|
|
186
|
+
}
|
|
187
|
+
return lines.join("\n\n");
|
|
188
|
+
};
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
export type MempalaceConfig = {
|
|
1
|
+
export type MempalaceConfig = Readonly<{
|
|
2
2
|
autosaveEnabled: boolean;
|
|
3
3
|
retrievalEnabled: boolean;
|
|
4
4
|
keywordSaveEnabled: boolean;
|
|
5
|
+
autoMineExtractMode: string;
|
|
5
6
|
maxInjectedItems: number;
|
|
6
7
|
retrievalQueryLimit: number;
|
|
7
|
-
keywordPatterns: string[];
|
|
8
|
+
keywordPatterns: readonly string[];
|
|
8
9
|
privacyRedactionEnabled: boolean;
|
|
9
10
|
userWingPrefix: string;
|
|
10
11
|
projectWingPrefix: string;
|
|
11
|
-
}
|
|
12
|
+
}>;
|
|
12
13
|
export declare const loadConfig: () => Promise<MempalaceConfig>;
|
|
13
14
|
export declare const resetConfig: () => void;
|
|
@@ -1,25 +1,58 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { DEFAULT_EXTRACT_MODE, DEFAULT_INJECTED_ITEMS, DEFAULT_RETRIEVAL_LIMIT, DEFAULT_KEYWORD_PATTERNS, DEFAULT_PROJECT_WING_PREFIX, DEFAULT_USER_WING_PREFIX, CONFIG_PATH_SEGMENTS, ENV_KEYS, } from "./constants";
|
|
4
5
|
const DEFAULT_CONFIG = {
|
|
5
6
|
autosaveEnabled: true,
|
|
6
7
|
retrievalEnabled: true,
|
|
7
8
|
keywordSaveEnabled: true,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
autoMineExtractMode: DEFAULT_EXTRACT_MODE,
|
|
10
|
+
maxInjectedItems: DEFAULT_INJECTED_ITEMS,
|
|
11
|
+
retrievalQueryLimit: DEFAULT_RETRIEVAL_LIMIT,
|
|
12
|
+
keywordPatterns: DEFAULT_KEYWORD_PATTERNS,
|
|
11
13
|
privacyRedactionEnabled: true,
|
|
12
|
-
userWingPrefix:
|
|
13
|
-
projectWingPrefix:
|
|
14
|
+
userWingPrefix: DEFAULT_USER_WING_PREFIX,
|
|
15
|
+
projectWingPrefix: DEFAULT_PROJECT_WING_PREFIX,
|
|
14
16
|
};
|
|
15
|
-
const CONFIG_PATH = path.join(os.homedir(),
|
|
17
|
+
const CONFIG_PATH = path.join(os.homedir(), ...CONFIG_PATH_SEGMENTS);
|
|
16
18
|
const stripJsonComments = (value) => {
|
|
17
19
|
return value.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
|
|
18
20
|
};
|
|
19
21
|
const parseBoolean = (value, fallback) => {
|
|
20
22
|
if (value == null)
|
|
21
23
|
return fallback;
|
|
22
|
-
|
|
24
|
+
const normalized = value.trim().toLowerCase();
|
|
25
|
+
if (["true", "1", "yes", "on"].includes(normalized))
|
|
26
|
+
return true;
|
|
27
|
+
if (["false", "0", "no", "off", ""].includes(normalized))
|
|
28
|
+
return false;
|
|
29
|
+
return fallback;
|
|
30
|
+
};
|
|
31
|
+
const isRecord = (value) => {
|
|
32
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
33
|
+
};
|
|
34
|
+
const readBoolean = (value, fallback) => {
|
|
35
|
+
return typeof value === "boolean" ? value : fallback;
|
|
36
|
+
};
|
|
37
|
+
const readPositiveNumber = (value, fallback) => {
|
|
38
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0)
|
|
39
|
+
return Math.floor(value);
|
|
40
|
+
return fallback;
|
|
41
|
+
};
|
|
42
|
+
const readString = (value, fallback) => {
|
|
43
|
+
if (typeof value !== "string")
|
|
44
|
+
return fallback;
|
|
45
|
+
const normalized = value.trim();
|
|
46
|
+
return normalized || fallback;
|
|
47
|
+
};
|
|
48
|
+
const readStringArray = (value, fallback) => {
|
|
49
|
+
if (!Array.isArray(value))
|
|
50
|
+
return [...fallback];
|
|
51
|
+
const normalized = value
|
|
52
|
+
.filter((item) => typeof item === "string")
|
|
53
|
+
.map((item) => item.trim())
|
|
54
|
+
.filter(Boolean);
|
|
55
|
+
return normalized.length ? normalized : [...fallback];
|
|
23
56
|
};
|
|
24
57
|
let cachedConfig;
|
|
25
58
|
export const loadConfig = async () => {
|
|
@@ -28,23 +61,29 @@ export const loadConfig = async () => {
|
|
|
28
61
|
let fileConfig = {};
|
|
29
62
|
try {
|
|
30
63
|
const raw = await fs.readFile(CONFIG_PATH, "utf8");
|
|
31
|
-
|
|
64
|
+
const parsed = JSON.parse(stripJsonComments(raw));
|
|
65
|
+
if (isRecord(parsed)) {
|
|
66
|
+
fileConfig = parsed;
|
|
67
|
+
}
|
|
32
68
|
}
|
|
33
69
|
catch {
|
|
34
70
|
// optional config file
|
|
35
71
|
}
|
|
36
72
|
cachedConfig = {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
73
|
+
autosaveEnabled: parseBoolean(process.env[ENV_KEYS.autosaveEnabled], readBoolean(fileConfig.autosaveEnabled, DEFAULT_CONFIG.autosaveEnabled)),
|
|
74
|
+
retrievalEnabled: parseBoolean(process.env[ENV_KEYS.retrievalEnabled], readBoolean(fileConfig.retrievalEnabled, DEFAULT_CONFIG.retrievalEnabled)),
|
|
75
|
+
keywordSaveEnabled: parseBoolean(process.env[ENV_KEYS.keywordSaveEnabled], readBoolean(fileConfig.keywordSaveEnabled, DEFAULT_CONFIG.keywordSaveEnabled)),
|
|
76
|
+
autoMineExtractMode: readString(process.env[ENV_KEYS.extractMode] ?? fileConfig.autoMineExtractMode, DEFAULT_CONFIG.autoMineExtractMode),
|
|
77
|
+
privacyRedactionEnabled: parseBoolean(process.env[ENV_KEYS.privacyRedactionEnabled], readBoolean(fileConfig.privacyRedactionEnabled, DEFAULT_CONFIG.privacyRedactionEnabled)),
|
|
78
|
+
maxInjectedItems: readPositiveNumber(process.env[ENV_KEYS.maxInjectedItems] != null
|
|
79
|
+
? Number(process.env[ENV_KEYS.maxInjectedItems])
|
|
80
|
+
: fileConfig.maxInjectedItems, DEFAULT_CONFIG.maxInjectedItems),
|
|
81
|
+
retrievalQueryLimit: readPositiveNumber(process.env[ENV_KEYS.retrievalQueryLimit] != null
|
|
82
|
+
? Number(process.env[ENV_KEYS.retrievalQueryLimit])
|
|
83
|
+
: fileConfig.retrievalQueryLimit, DEFAULT_CONFIG.retrievalQueryLimit),
|
|
84
|
+
keywordPatterns: readStringArray(fileConfig.keywordPatterns, DEFAULT_CONFIG.keywordPatterns),
|
|
85
|
+
userWingPrefix: readString(process.env[ENV_KEYS.userWingPrefix] ?? fileConfig.userWingPrefix, DEFAULT_CONFIG.userWingPrefix),
|
|
86
|
+
projectWingPrefix: readString(process.env[ENV_KEYS.projectWingPrefix] ?? fileConfig.projectWingPrefix, DEFAULT_CONFIG.projectWingPrefix),
|
|
48
87
|
};
|
|
49
88
|
return cachedConfig;
|
|
50
89
|
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export declare const DEFAULT_ROOM: "workflow";
|
|
2
|
+
export declare const DEFAULT_TOPIC: "autosave";
|
|
3
|
+
export declare const DEFAULT_AGENT_NAME: "opencode";
|
|
4
|
+
export declare const DEFAULT_EXTRACT_MODE: "general";
|
|
5
|
+
export declare const DEFAULT_INJECTED_ITEMS: 6;
|
|
6
|
+
export declare const DEFAULT_RETRIEVAL_LIMIT: 5;
|
|
7
|
+
export declare const DEFAULT_LIMIT: 5;
|
|
8
|
+
export declare const DEFAULT_ADAPTER_TIMEOUT_MS: 15000;
|
|
9
|
+
export declare const DEFAULT_USER_WING_PREFIX: "wing_user";
|
|
10
|
+
export declare const DEFAULT_PROJECT_WING_PREFIX: "wing_project";
|
|
11
|
+
export declare const DEFAULT_KEYWORD_PATTERNS: readonly ["remember", "save this", "don't forget", "note that"];
|
|
12
|
+
export declare const CONFIG_PATH_SEGMENTS: readonly [".config", "opencode", "mempalace.jsonc"];
|
|
13
|
+
export declare const ENV_KEYS: {
|
|
14
|
+
readonly autosaveEnabled: "MEMPALACE_AUTOSAVE_ENABLED";
|
|
15
|
+
readonly retrievalEnabled: "MEMPALACE_RETRIEVAL_ENABLED";
|
|
16
|
+
readonly keywordSaveEnabled: "MEMPALACE_KEYWORD_SAVE_ENABLED";
|
|
17
|
+
readonly extractMode: "MEMPALACE_AUTO_MINE_EXTRACT_MODE";
|
|
18
|
+
readonly privacyRedactionEnabled: "MEMPALACE_PRIVACY_REDACTION_ENABLED";
|
|
19
|
+
readonly maxInjectedItems: "MEMPALACE_MAX_INJECTED_ITEMS";
|
|
20
|
+
readonly retrievalQueryLimit: "MEMPALACE_RETRIEVAL_QUERY_LIMIT";
|
|
21
|
+
readonly userWingPrefix: "MEMPALACE_USER_WING_PREFIX";
|
|
22
|
+
readonly projectWingPrefix: "MEMPALACE_PROJECT_WING_PREFIX";
|
|
23
|
+
readonly autosaveLogFile: "MEMPALACE_AUTOSAVE_LOG_FILE";
|
|
24
|
+
readonly adapterPython: "MEMPALACE_ADAPTER_PYTHON";
|
|
25
|
+
readonly adapterTimeoutMs: "MEMPALACE_ADAPTER_TIMEOUT_MS";
|
|
26
|
+
};
|
|
27
|
+
export declare const MAX_USER_MESSAGES_DIGEST: 20;
|
|
28
|
+
export declare const MAX_TRANSCRIPT_MESSAGES_DIGEST: 50;
|
|
29
|
+
export declare const DATE_ISO_SLICE: 10;
|
|
30
|
+
export declare const SERVICE_NAME: "mempalace-autosave";
|
|
31
|
+
export declare const LOG_FILE_NAME: "opencode_autosave.log";
|
|
32
|
+
export declare const DIRECT_MEMPALACE_MUTATION_TOOLS: readonly ["mempalace_add_drawer", "mempalace_kg_add", "mempalace_diary_write", "mcp-router_mempalace_add_drawer", "mcp-router_mempalace_kg_add", "mcp-router_mempalace_diary_write"];
|
|
33
|
+
export declare const COMPACTION_CONTEXT_MESSAGE: "MemPalace retrieval may be useful after compaction. Search relevant project and user memory if needed before answering.";
|
|
34
|
+
export declare const TOOL_DESCRIPTIONS: {
|
|
35
|
+
readonly mempalaceMemory: "Save or search memory in MemPalace with scope/privacy enforcement";
|
|
36
|
+
};
|
|
37
|
+
export declare const TOOL_ERROR_MESSAGES: {
|
|
38
|
+
readonly directMutationBlocked: "Use mempalace_memory instead of direct MemPalace mutation tools";
|
|
39
|
+
readonly invalidAdapterPayload: "Adapter returned an invalid JSON payload";
|
|
40
|
+
readonly emptyAdapterStdout: "Adapter returned empty stdout";
|
|
41
|
+
readonly adapterTimedOut: "Adapter execution timed out";
|
|
42
|
+
};
|
|
43
|
+
export declare const LOG_MESSAGES: {
|
|
44
|
+
readonly autosaveEventMissingSessionId: "autosave event missing sessionID";
|
|
45
|
+
readonly keywordTriggeredAutosaveHintDetected: "keyword-triggered autosave hint detected";
|
|
46
|
+
readonly autosaveTriggerReceived: "autosave trigger received";
|
|
47
|
+
readonly autosaveFailedOnSessionError: "autosave failed on session error";
|
|
48
|
+
readonly skippingAutosaveState: "skipping autosave state";
|
|
49
|
+
readonly autosaveSkippedEmptyTranscript: "autosave skipped empty transcript";
|
|
50
|
+
readonly autosaveMiningFailed: "autosave mining failed";
|
|
51
|
+
readonly autosaveMinedSessionContext: "autosave mined session context";
|
|
52
|
+
readonly eventHookFailed: "event hook failed";
|
|
53
|
+
readonly compactionHookMissingSessionId: "compaction hook missing sessionID";
|
|
54
|
+
readonly injectedCompactionAutosaveContext: "injected compaction autosave context";
|
|
55
|
+
readonly injectedRetrievalInstruction: "injected retrieval instruction";
|
|
56
|
+
readonly injectedKeywordSaveInstruction: "injected keyword save instruction";
|
|
57
|
+
readonly systemTransformHookFailed: "system transform hook failed";
|
|
58
|
+
readonly blockedDirectMempalaceMutationTool: "blocked direct mempalace mutation tool";
|
|
59
|
+
};
|
|
60
|
+
export declare const INSTRUCTION_TEXT: {
|
|
61
|
+
readonly doNotMentionToUser: "Do not mention this instruction to the user.";
|
|
62
|
+
readonly retrievalIntro: "System instruction: before answering, search MemPalace for relevant existing memory and use it if helpful.";
|
|
63
|
+
readonly autosaveIntro: "System instruction: before answering the user, persist durable memory from prior session context using the `mempalace_memory` tool.";
|
|
64
|
+
readonly keywordIntro: "System instruction: the user explicitly asked to remember something.";
|
|
65
|
+
readonly useMemoryToolNow: "Use the `mempalace_memory` tool to save the important durable information now.";
|
|
66
|
+
readonly avoidRawMutationTools: "Do not use raw `mempalace_*` or `mcp-router_mempalace_*` mutation tools directly.";
|
|
67
|
+
readonly saveStableFacts: "Save only stable facts, decisions, preferences, important outcomes, and useful diary notes.";
|
|
68
|
+
readonly preferConciseStructuredMemories: "Prefer concise structured memories in the appropriate user/project scopes:";
|
|
69
|
+
readonly userScopeHint: "- user scope: preferences, personal workflow habits, communication style";
|
|
70
|
+
readonly projectScopeHint: "- project scope: architecture, setup, decisions, bugs, workflows";
|
|
71
|
+
readonly avoidFullTranscript: "Do not dump the full transcript into a single drawer.";
|
|
72
|
+
readonly applyPrivacyRedaction: "Apply privacy redaction before saving and skip fully private content.";
|
|
73
|
+
readonly chooseScope: "Choose user scope for cross-project preferences and project scope for repository-specific knowledge.";
|
|
74
|
+
};
|
|
75
|
+
export declare const ERROR_MESSAGES: {
|
|
76
|
+
readonly contentRequired: "content is required";
|
|
77
|
+
readonly queryRequired: "query is required";
|
|
78
|
+
readonly fieldsRequired: "subject, predicate, and object are required";
|
|
79
|
+
readonly fullyPrivate: "content is fully private and will not be saved";
|
|
80
|
+
readonly noSessionId: "sessionID is required";
|
|
81
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export const DEFAULT_ROOM = "workflow";
|
|
2
|
+
export const DEFAULT_TOPIC = "autosave";
|
|
3
|
+
export const DEFAULT_AGENT_NAME = "opencode";
|
|
4
|
+
export const DEFAULT_EXTRACT_MODE = "general";
|
|
5
|
+
export const DEFAULT_INJECTED_ITEMS = 6;
|
|
6
|
+
export const DEFAULT_RETRIEVAL_LIMIT = 5;
|
|
7
|
+
export const DEFAULT_LIMIT = 5;
|
|
8
|
+
export const DEFAULT_ADAPTER_TIMEOUT_MS = 15_000;
|
|
9
|
+
export const DEFAULT_USER_WING_PREFIX = "wing_user";
|
|
10
|
+
export const DEFAULT_PROJECT_WING_PREFIX = "wing_project";
|
|
11
|
+
export const DEFAULT_KEYWORD_PATTERNS = ["remember", "save this", "don't forget", "note that"];
|
|
12
|
+
export const CONFIG_PATH_SEGMENTS = [".config", "opencode", "mempalace.jsonc"];
|
|
13
|
+
export const ENV_KEYS = {
|
|
14
|
+
autosaveEnabled: "MEMPALACE_AUTOSAVE_ENABLED",
|
|
15
|
+
retrievalEnabled: "MEMPALACE_RETRIEVAL_ENABLED",
|
|
16
|
+
keywordSaveEnabled: "MEMPALACE_KEYWORD_SAVE_ENABLED",
|
|
17
|
+
extractMode: "MEMPALACE_AUTO_MINE_EXTRACT_MODE",
|
|
18
|
+
privacyRedactionEnabled: "MEMPALACE_PRIVACY_REDACTION_ENABLED",
|
|
19
|
+
maxInjectedItems: "MEMPALACE_MAX_INJECTED_ITEMS",
|
|
20
|
+
retrievalQueryLimit: "MEMPALACE_RETRIEVAL_QUERY_LIMIT",
|
|
21
|
+
userWingPrefix: "MEMPALACE_USER_WING_PREFIX",
|
|
22
|
+
projectWingPrefix: "MEMPALACE_PROJECT_WING_PREFIX",
|
|
23
|
+
autosaveLogFile: "MEMPALACE_AUTOSAVE_LOG_FILE",
|
|
24
|
+
adapterPython: "MEMPALACE_ADAPTER_PYTHON",
|
|
25
|
+
adapterTimeoutMs: "MEMPALACE_ADAPTER_TIMEOUT_MS",
|
|
26
|
+
};
|
|
27
|
+
export const MAX_USER_MESSAGES_DIGEST = 20;
|
|
28
|
+
export const MAX_TRANSCRIPT_MESSAGES_DIGEST = 50;
|
|
29
|
+
export const DATE_ISO_SLICE = 10;
|
|
30
|
+
export const SERVICE_NAME = "mempalace-autosave";
|
|
31
|
+
export const LOG_FILE_NAME = "opencode_autosave.log";
|
|
32
|
+
export const DIRECT_MEMPALACE_MUTATION_TOOLS = [
|
|
33
|
+
"mempalace_add_drawer",
|
|
34
|
+
"mempalace_kg_add",
|
|
35
|
+
"mempalace_diary_write",
|
|
36
|
+
"mcp-router_mempalace_add_drawer",
|
|
37
|
+
"mcp-router_mempalace_kg_add",
|
|
38
|
+
"mcp-router_mempalace_diary_write",
|
|
39
|
+
];
|
|
40
|
+
export const COMPACTION_CONTEXT_MESSAGE = "MemPalace retrieval may be useful after compaction. Search relevant project and user memory if needed before answering.";
|
|
41
|
+
export const TOOL_DESCRIPTIONS = {
|
|
42
|
+
mempalaceMemory: "Save or search memory in MemPalace with scope/privacy enforcement",
|
|
43
|
+
};
|
|
44
|
+
export const TOOL_ERROR_MESSAGES = {
|
|
45
|
+
directMutationBlocked: "Use mempalace_memory instead of direct MemPalace mutation tools",
|
|
46
|
+
invalidAdapterPayload: "Adapter returned an invalid JSON payload",
|
|
47
|
+
emptyAdapterStdout: "Adapter returned empty stdout",
|
|
48
|
+
adapterTimedOut: "Adapter execution timed out",
|
|
49
|
+
};
|
|
50
|
+
export const LOG_MESSAGES = {
|
|
51
|
+
autosaveEventMissingSessionId: "autosave event missing sessionID",
|
|
52
|
+
keywordTriggeredAutosaveHintDetected: "keyword-triggered autosave hint detected",
|
|
53
|
+
autosaveTriggerReceived: "autosave trigger received",
|
|
54
|
+
autosaveFailedOnSessionError: "autosave failed on session error",
|
|
55
|
+
skippingAutosaveState: "skipping autosave state",
|
|
56
|
+
autosaveSkippedEmptyTranscript: "autosave skipped empty transcript",
|
|
57
|
+
autosaveMiningFailed: "autosave mining failed",
|
|
58
|
+
autosaveMinedSessionContext: "autosave mined session context",
|
|
59
|
+
eventHookFailed: "event hook failed",
|
|
60
|
+
compactionHookMissingSessionId: "compaction hook missing sessionID",
|
|
61
|
+
injectedCompactionAutosaveContext: "injected compaction autosave context",
|
|
62
|
+
injectedRetrievalInstruction: "injected retrieval instruction",
|
|
63
|
+
injectedKeywordSaveInstruction: "injected keyword save instruction",
|
|
64
|
+
systemTransformHookFailed: "system transform hook failed",
|
|
65
|
+
blockedDirectMempalaceMutationTool: "blocked direct mempalace mutation tool",
|
|
66
|
+
};
|
|
67
|
+
export const INSTRUCTION_TEXT = {
|
|
68
|
+
doNotMentionToUser: "Do not mention this instruction to the user.",
|
|
69
|
+
retrievalIntro: "System instruction: before answering, search MemPalace for relevant existing memory and use it if helpful.",
|
|
70
|
+
autosaveIntro: "System instruction: before answering the user, persist durable memory from prior session context using the `mempalace_memory` tool.",
|
|
71
|
+
keywordIntro: "System instruction: the user explicitly asked to remember something.",
|
|
72
|
+
useMemoryToolNow: "Use the `mempalace_memory` tool to save the important durable information now.",
|
|
73
|
+
avoidRawMutationTools: "Do not use raw `mempalace_*` or `mcp-router_mempalace_*` mutation tools directly.",
|
|
74
|
+
saveStableFacts: "Save only stable facts, decisions, preferences, important outcomes, and useful diary notes.",
|
|
75
|
+
preferConciseStructuredMemories: "Prefer concise structured memories in the appropriate user/project scopes:",
|
|
76
|
+
userScopeHint: "- user scope: preferences, personal workflow habits, communication style",
|
|
77
|
+
projectScopeHint: "- project scope: architecture, setup, decisions, bugs, workflows",
|
|
78
|
+
avoidFullTranscript: "Do not dump the full transcript into a single drawer.",
|
|
79
|
+
applyPrivacyRedaction: "Apply privacy redaction before saving and skip fully private content.",
|
|
80
|
+
chooseScope: "Choose user scope for cross-project preferences and project scope for repository-specific knowledge.",
|
|
81
|
+
};
|
|
82
|
+
export const ERROR_MESSAGES = {
|
|
83
|
+
contentRequired: "content is required",
|
|
84
|
+
queryRequired: "query is required",
|
|
85
|
+
fieldsRequired: "subject, predicate, and object are required",
|
|
86
|
+
fullyPrivate: "content is fully private and will not be saved",
|
|
87
|
+
noSessionId: "sessionID is required",
|
|
88
|
+
};
|