opcode-pg-memory 2.2.7 → 2.3.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.
- package/dist/cli.js +21 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +30 -20981
- package/dist/index.js.map +1 -0
- package/dist/mcp-server.js +26 -6
- package/dist/mcp-server.js.map +1 -0
- package/dist/src/cache/semantic-cache.js +399 -0
- package/dist/src/cache/semantic-cache.js.map +1 -0
- package/dist/src/cli.js +404 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/config.d.ts +5 -0
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +89 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/db/init-db.js +545 -0
- package/dist/src/db/init-db.js.map +1 -0
- package/dist/src/hooks/message-part-updated.js +203 -0
- package/dist/src/hooks/message-part-updated.js.map +1 -0
- package/dist/src/hooks/message-updated.js +347 -0
- package/dist/src/hooks/message-updated.js.map +1 -0
- package/dist/src/hooks/session-compacting.js +179 -0
- package/dist/src/hooks/session-compacting.js.map +1 -0
- package/dist/src/hooks/session-completed.js +337 -0
- package/dist/src/hooks/session-completed.js.map +1 -0
- package/dist/src/hooks/session-created.js +206 -0
- package/dist/src/hooks/session-created.js.map +1 -0
- package/dist/src/hooks/tool-execute.js +267 -0
- package/dist/src/hooks/tool-execute.js.map +1 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +643 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/mcp/hindsight-reflect-omo.js +318 -0
- package/dist/src/mcp/hindsight-reflect-omo.js.map +1 -0
- package/dist/src/mcp/hindsight-reflect.js +838 -0
- package/dist/src/mcp/hindsight-reflect.js.map +1 -0
- package/dist/src/mcp/recall-memory-omo.js +263 -0
- package/dist/src/mcp/recall-memory-omo.js.map +1 -0
- package/dist/src/mcp/recall-memory.d.ts +6 -0
- package/dist/src/mcp/recall-memory.d.ts.map +1 -1
- package/dist/src/mcp/recall-memory.js +900 -0
- package/dist/src/mcp/recall-memory.js.map +1 -0
- package/dist/src/omo/adapter.js +583 -0
- package/dist/src/omo/adapter.js.map +1 -0
- package/dist/src/omo/types.js +44 -0
- package/dist/src/omo/types.js.map +1 -0
- package/dist/src/services/db-polling.d.ts +30 -0
- package/dist/src/services/db-polling.d.ts.map +1 -0
- package/dist/src/services/db-polling.js +97 -0
- package/dist/src/services/db-polling.js.map +1 -0
- package/dist/src/services/event-synchronizer.d.ts +15 -0
- package/dist/src/services/event-synchronizer.d.ts.map +1 -0
- package/dist/src/services/event-synchronizer.js +119 -0
- package/dist/src/services/event-synchronizer.js.map +1 -0
- package/dist/src/services/keyword.js +29 -0
- package/dist/src/services/keyword.js.map +1 -0
- package/dist/src/services/logger.js +42 -0
- package/dist/src/services/logger.js.map +1 -0
- package/dist/src/services/opencode-schema-adapter.d.ts +34 -0
- package/dist/src/services/opencode-schema-adapter.d.ts.map +1 -0
- package/dist/src/services/opencode-schema-adapter.js +96 -0
- package/dist/src/services/opencode-schema-adapter.js.map +1 -0
- package/dist/src/services/privacy.js +23 -0
- package/dist/src/services/privacy.js.map +1 -0
- package/dist/src/topic/segment-manager.js +447 -0
- package/dist/src/topic/segment-manager.js.map +1 -0
- package/dist/src/types.d.ts +20 -2
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +8 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils/embedding.js +180 -0
- package/dist/src/utils/embedding.js.map +1 -0
- package/dist/src/utils/token-budget.js +152 -0
- package/dist/src/utils/token-budget.js.map +1 -0
- package/package.json +6 -6
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.hindsightReflect = exports.recallMemory = exports.OpenCodePGMemory = void 0;
|
|
18
|
+
const init_db_1 = require("./db/init-db");
|
|
19
|
+
const tool_execute_1 = require("./hooks/tool-execute");
|
|
20
|
+
const message_part_updated_1 = require("./hooks/message-part-updated");
|
|
21
|
+
const session_compacting_1 = require("./hooks/session-compacting");
|
|
22
|
+
const recall_memory_1 = require("./mcp/recall-memory");
|
|
23
|
+
const hindsight_reflect_1 = require("./mcp/hindsight-reflect");
|
|
24
|
+
const semantic_cache_1 = require("./cache/semantic-cache");
|
|
25
|
+
const logger_1 = require("./services/logger");
|
|
26
|
+
const db_polling_1 = require("./services/db-polling");
|
|
27
|
+
const event_synchronizer_1 = require("./services/event-synchronizer");
|
|
28
|
+
const logger = (0, logger_1.createLogger)('plugin');
|
|
29
|
+
const token_budget_1 = require("./utils/token-budget");
|
|
30
|
+
const keyword_1 = require("./services/keyword");
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// Default Configuration
|
|
33
|
+
// ============================================================================
|
|
34
|
+
const DEFAULT_PLUGIN_CONFIG = {
|
|
35
|
+
database: {
|
|
36
|
+
host: process.env.PG_HOST || 'localhost',
|
|
37
|
+
port: parseInt(process.env.PG_PORT || '5432', 10),
|
|
38
|
+
database: process.env.PG_DATABASE || 'opencode_memory',
|
|
39
|
+
user: process.env.PG_USER || 'opencode',
|
|
40
|
+
password: process.env.PG_PASSWORD || '',
|
|
41
|
+
ssl: process.env.PG_SSL === 'true',
|
|
42
|
+
},
|
|
43
|
+
embedding: {
|
|
44
|
+
model: 'text-embedding-3-small',
|
|
45
|
+
dimensions: 1536,
|
|
46
|
+
batchSize: 100,
|
|
47
|
+
},
|
|
48
|
+
cache: {
|
|
49
|
+
initialThreshold: 0.92,
|
|
50
|
+
adjustmentStep: 0.02,
|
|
51
|
+
minThreshold: 0.85,
|
|
52
|
+
maxThreshold: 0.97,
|
|
53
|
+
enabled: true,
|
|
54
|
+
},
|
|
55
|
+
reflection: {
|
|
56
|
+
observationThreshold: 30,
|
|
57
|
+
segmentThreshold: 5,
|
|
58
|
+
modelSize: '7b',
|
|
59
|
+
offPeakHours: [1, 2, 3, 4, 5],
|
|
60
|
+
enabled: true,
|
|
61
|
+
},
|
|
62
|
+
tokenBudget: {
|
|
63
|
+
contextLimitRatio: 0.05,
|
|
64
|
+
minTokens: 500,
|
|
65
|
+
maxTokens: 4000,
|
|
66
|
+
},
|
|
67
|
+
retrieval: {
|
|
68
|
+
defaultStrategies: ['semantic', 'bm25', 'graph'],
|
|
69
|
+
rerankEnabled: true,
|
|
70
|
+
maxResults: 10,
|
|
71
|
+
weights: {
|
|
72
|
+
semantic: 0.5,
|
|
73
|
+
recency: 0.3,
|
|
74
|
+
importance: 0.2,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
// ============================================================================
|
|
79
|
+
// Internal Plugin State (NOT exported)
|
|
80
|
+
// ============================================================================
|
|
81
|
+
class OpenCodePGMemoryPlugin {
|
|
82
|
+
pool = null;
|
|
83
|
+
config;
|
|
84
|
+
cacheManager = null;
|
|
85
|
+
initialized = false;
|
|
86
|
+
cleanupIntervals = [];
|
|
87
|
+
constructor(config = {}) {
|
|
88
|
+
this.config = this.mergeConfig(config);
|
|
89
|
+
}
|
|
90
|
+
mergeConfig(userConfig) {
|
|
91
|
+
return {
|
|
92
|
+
...DEFAULT_PLUGIN_CONFIG,
|
|
93
|
+
...userConfig,
|
|
94
|
+
database: { ...DEFAULT_PLUGIN_CONFIG.database, ...userConfig.database },
|
|
95
|
+
embedding: { ...DEFAULT_PLUGIN_CONFIG.embedding, ...userConfig.embedding },
|
|
96
|
+
cache: { ...DEFAULT_PLUGIN_CONFIG.cache, ...userConfig.cache },
|
|
97
|
+
reflection: { ...DEFAULT_PLUGIN_CONFIG.reflection, ...userConfig.reflection },
|
|
98
|
+
tokenBudget: { ...DEFAULT_PLUGIN_CONFIG.tokenBudget, ...userConfig.tokenBudget },
|
|
99
|
+
retrieval: { ...DEFAULT_PLUGIN_CONFIG.retrieval, ...userConfig.retrieval },
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
async initialize() {
|
|
103
|
+
if (this.initialized)
|
|
104
|
+
return;
|
|
105
|
+
logger.info('Initializing plugin...');
|
|
106
|
+
try {
|
|
107
|
+
this.pool = await (0, init_db_1.initializeDatabase)(this.config.database);
|
|
108
|
+
if (this.config.cache.enabled) {
|
|
109
|
+
this.cacheManager = (0, semantic_cache_1.createCacheManager)(this.pool, this.config.cache);
|
|
110
|
+
}
|
|
111
|
+
this.startCleanupTasks();
|
|
112
|
+
this.initialized = true;
|
|
113
|
+
logger.info('Plugin initialized successfully');
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
logger.error('Plugin initialization failed:', error);
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async close() {
|
|
121
|
+
if (!this.initialized)
|
|
122
|
+
return;
|
|
123
|
+
logger.info('Closing plugin...');
|
|
124
|
+
for (const interval of this.cleanupIntervals) {
|
|
125
|
+
clearInterval(interval);
|
|
126
|
+
}
|
|
127
|
+
this.cleanupIntervals = [];
|
|
128
|
+
await (0, init_db_1.closeDatabase)();
|
|
129
|
+
this.pool = null;
|
|
130
|
+
this.cacheManager = null;
|
|
131
|
+
this.initialized = false;
|
|
132
|
+
logger.info('Plugin closed');
|
|
133
|
+
}
|
|
134
|
+
startCleanupTasks() {
|
|
135
|
+
// Clean up expired message part accumulators every 5 minutes
|
|
136
|
+
this.cleanupIntervals.push(setInterval(() => {
|
|
137
|
+
(0, message_part_updated_1.cleanupExpiredAccumulators)(300000);
|
|
138
|
+
}, 300000));
|
|
139
|
+
// Clean up expired cache entries daily
|
|
140
|
+
this.cleanupIntervals.push(setInterval(async () => {
|
|
141
|
+
if (this.cacheManager) {
|
|
142
|
+
await this.cacheManager.cleanupExpiredCache(30);
|
|
143
|
+
}
|
|
144
|
+
}, 24 * 60 * 60 * 1000));
|
|
145
|
+
}
|
|
146
|
+
getPool() {
|
|
147
|
+
if (!this.pool) {
|
|
148
|
+
throw new Error('Plugin not initialized. Call initialize() first.');
|
|
149
|
+
}
|
|
150
|
+
return this.pool;
|
|
151
|
+
}
|
|
152
|
+
getCacheManager() {
|
|
153
|
+
return this.cacheManager;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// ============================================================================
|
|
157
|
+
// Plugin Export - Official OpenCode Plugin API format
|
|
158
|
+
// ============================================================================
|
|
159
|
+
const OpenCodePGMemory = async (ctx) => {
|
|
160
|
+
// Read config from environment
|
|
161
|
+
const config = buildConfigFromEnv();
|
|
162
|
+
// Create internal state
|
|
163
|
+
const plugin = new OpenCodePGMemoryPlugin(config);
|
|
164
|
+
await plugin.initialize();
|
|
165
|
+
const pool = plugin.getPool();
|
|
166
|
+
const cacheManager = plugin.getCacheManager();
|
|
167
|
+
const logger = (0, logger_1.createLogger)('plugin');
|
|
168
|
+
const eventSync = new event_synchronizer_1.EventSynchronizer(pool, {
|
|
169
|
+
mode: (process.env.PG_MEMORY_SYNC_MODE || 'hybrid'),
|
|
170
|
+
pollingIntervalMs: parseInt(process.env.PG_MEMORY_POLL_INTERVAL || '5000'),
|
|
171
|
+
});
|
|
172
|
+
let dbPolling = null;
|
|
173
|
+
if (process.env.PG_MEMORY_DB_POLLING !== 'false') {
|
|
174
|
+
dbPolling = new db_polling_1.OpenCodeDBPollingSource(pool, eventSync, {
|
|
175
|
+
intervalMs: parseInt(process.env.PG_MEMORY_POLL_INTERVAL || '5000'),
|
|
176
|
+
maxBatchSize: 100,
|
|
177
|
+
backoffBaseMs: 1000,
|
|
178
|
+
backoffMaxMs: 60000,
|
|
179
|
+
});
|
|
180
|
+
dbPolling.start().catch(err => logger.warn('DB polling start failed', err));
|
|
181
|
+
}
|
|
182
|
+
// Cleanup on process exit
|
|
183
|
+
const cleanup = () => {
|
|
184
|
+
if (dbPolling)
|
|
185
|
+
dbPolling.stop();
|
|
186
|
+
plugin.close().catch((err) => logger.error('Cleanup error:', err));
|
|
187
|
+
};
|
|
188
|
+
process.on('exit', cleanup);
|
|
189
|
+
process.on('SIGINT', cleanup);
|
|
190
|
+
process.on('SIGTERM', cleanup);
|
|
191
|
+
// Track sessions that have received memory injection
|
|
192
|
+
const injectedSessions = new Set();
|
|
193
|
+
const injectedSystemPrompt = new Set();
|
|
194
|
+
// ==========================================================================
|
|
195
|
+
// Return hooks object
|
|
196
|
+
// ==========================================================================
|
|
197
|
+
return {
|
|
198
|
+
// -----------------------------------------------------------------------
|
|
199
|
+
// Unified event hook - receives ALL bus events
|
|
200
|
+
// -----------------------------------------------------------------------
|
|
201
|
+
event: async (input) => {
|
|
202
|
+
const { type, properties } = input.event;
|
|
203
|
+
try {
|
|
204
|
+
const sid = properties.sessionID || properties.session?.id;
|
|
205
|
+
if (!sid)
|
|
206
|
+
return;
|
|
207
|
+
await eventSync.handleEvent({
|
|
208
|
+
id: `${type}:${sid}:${Date.now()}`,
|
|
209
|
+
type: type,
|
|
210
|
+
sessionId: sid,
|
|
211
|
+
timestamp: Date.now(),
|
|
212
|
+
version: 1,
|
|
213
|
+
source: 'hook',
|
|
214
|
+
data: properties,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
console.error(`[PG Memory] Error handling event '${type}':`, error);
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
// -----------------------------------------------------------------------
|
|
222
|
+
// chat.message - inject relevant memories on first message
|
|
223
|
+
// -----------------------------------------------------------------------
|
|
224
|
+
'chat.message': async (input, output) => {
|
|
225
|
+
try {
|
|
226
|
+
// Only inject on the first message of a session
|
|
227
|
+
if (!injectedSessions.has(input.sessionID)) {
|
|
228
|
+
injectedSessions.add(input.sessionID);
|
|
229
|
+
logger.info('First message in session', { sessionID: input.sessionID });
|
|
230
|
+
// Retrieve relevant facts for this session
|
|
231
|
+
const contextLimit = 128000; // default model context limit
|
|
232
|
+
const budget = (0, token_budget_1.calculateTokenBudget)(contextLimit, config.tokenBudget || {});
|
|
233
|
+
const facts = await retrieveFactsForInjection(input.sessionID, { maxTokens: typeof budget === 'number' ? budget : 2000 }, pool, { minConfidence: 0.5, minWeight: 0.3 });
|
|
234
|
+
if (facts.length > 0) {
|
|
235
|
+
// Format as context block
|
|
236
|
+
const contextBlock = formatMemoryContext(facts);
|
|
237
|
+
// Inject as synthetic part (visible to LLM, hidden in TUI)
|
|
238
|
+
if (output.parts && Array.isArray(output.parts)) {
|
|
239
|
+
output.parts.unshift({
|
|
240
|
+
id: `prt_pgmemory-context-${Date.now()}`,
|
|
241
|
+
sessionID: input.sessionID,
|
|
242
|
+
messageID: output.message?.id || input.messageID || '',
|
|
243
|
+
type: 'text',
|
|
244
|
+
text: contextBlock,
|
|
245
|
+
synthetic: true,
|
|
246
|
+
});
|
|
247
|
+
logger.info(`Injected ${facts.length} memories`, { sessionID: input.sessionID });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
logger.debug('No memories to inject');
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// Check for memory keywords AFTER first-message injection
|
|
255
|
+
const userText = output.parts
|
|
256
|
+
.filter((p) => p.type === 'text' && !p.synthetic)
|
|
257
|
+
.map((p) => p.text || '')
|
|
258
|
+
.join(' ');
|
|
259
|
+
if ((0, keyword_1.detectMemoryKeyword)(userText)) {
|
|
260
|
+
logger.info('Memory keyword detected', { sessionID: input.sessionID });
|
|
261
|
+
output.parts.push({
|
|
262
|
+
id: `prt_pgmemory-nudge-${Date.now()}`,
|
|
263
|
+
sessionID: input.sessionID,
|
|
264
|
+
messageID: output.message?.id || input.messageID || '',
|
|
265
|
+
type: 'text',
|
|
266
|
+
text: keyword_1.MEMORY_NUDGE_MESSAGE,
|
|
267
|
+
synthetic: true,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
logger.error('Failed to inject memories in chat.message', error);
|
|
273
|
+
// Non-blocking: never crash the message flow
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
// -----------------------------------------------------------------------
|
|
277
|
+
// tool.execute.before - intercept before tool execution
|
|
278
|
+
// -----------------------------------------------------------------------
|
|
279
|
+
'tool.execute.before': async (input, output) => {
|
|
280
|
+
try {
|
|
281
|
+
await (0, tool_execute_1.handleToolExecuteBefore)({
|
|
282
|
+
session: { id: input.sessionID },
|
|
283
|
+
tool: { name: input.tool, parameters: output.args || {} },
|
|
284
|
+
messageId: input.callID,
|
|
285
|
+
}, { parameters: output.args }, pool);
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
logger.error('Error in tool.execute.before:', error);
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
// -----------------------------------------------------------------------
|
|
292
|
+
// tool.execute.after - intercept after tool execution
|
|
293
|
+
// -----------------------------------------------------------------------
|
|
294
|
+
'tool.execute.after': async (input, output) => {
|
|
295
|
+
try {
|
|
296
|
+
const result = {
|
|
297
|
+
success: !(output.output && output.output.toLowerCase().includes('error')),
|
|
298
|
+
data: output.output,
|
|
299
|
+
error: output.output && output.output.toLowerCase().includes('error') ? output.output : undefined,
|
|
300
|
+
};
|
|
301
|
+
await (0, tool_execute_1.handleToolExecuteAfter)({
|
|
302
|
+
session: { id: input.sessionID },
|
|
303
|
+
tool: { name: input.tool, parameters: input.args || {} },
|
|
304
|
+
result,
|
|
305
|
+
messageId: input.callID,
|
|
306
|
+
executionTimeMs: output.metadata?.executionTimeMs || 0,
|
|
307
|
+
}, {}, pool);
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
logger.error('Error in tool.execute.after:', error);
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
// -----------------------------------------------------------------------
|
|
314
|
+
// experimental.chat.system.transform - inject MCP tool usage instructions
|
|
315
|
+
// -----------------------------------------------------------------------
|
|
316
|
+
'experimental.chat.system.transform': async (input, output) => {
|
|
317
|
+
try {
|
|
318
|
+
const sessionId = input.sessionID || 'default';
|
|
319
|
+
if (injectedSystemPrompt.has(sessionId))
|
|
320
|
+
return;
|
|
321
|
+
injectedSystemPrompt.add(sessionId);
|
|
322
|
+
output.system = output.system || [];
|
|
323
|
+
output.system.push(`## PG Memory Tools Available
|
|
324
|
+
|
|
325
|
+
You have access to long-term memory via the pg-memory plugin. These tools help you reuse knowledge across sessions:
|
|
326
|
+
|
|
327
|
+
### recall_memory — search historical memories
|
|
328
|
+
Call this BEFORE starting any new task. It retrieves relevant entities, observations, and reflections from past sessions.
|
|
329
|
+
- Example: recall_memory({ query: "database connection pool tuning" })
|
|
330
|
+
- Best practice: always pass your current task goal as the query
|
|
331
|
+
|
|
332
|
+
### hindsight_reflect — reflect on session
|
|
333
|
+
Call this AFTER completing significant work to extract reusable patterns.
|
|
334
|
+
- Example: hindsight_reflect({ trigger_type: "manual" })
|
|
335
|
+
- Reflexions are automatically available in future sessions
|
|
336
|
+
|
|
337
|
+
### When to use
|
|
338
|
+
- Before diving into a new problem → recall_memory(query=<your goal>)
|
|
339
|
+
- After completing a major task → hindsight_reflect()
|
|
340
|
+
- When you need historical context about a specific topic → recall_memory(topic_segment_id=<id>)
|
|
341
|
+
`);
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
logger.error('Error in system.transform:', error);
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
// -----------------------------------------------------------------------
|
|
348
|
+
// experimental.session.compacting - session compaction hook
|
|
349
|
+
// -----------------------------------------------------------------------
|
|
350
|
+
'experimental.session.compacting': async (input, output) => {
|
|
351
|
+
try {
|
|
352
|
+
// Show compaction notification
|
|
353
|
+
if (ctx.client?.tui?.showToast) {
|
|
354
|
+
ctx.client.tui.showToast({
|
|
355
|
+
body: {
|
|
356
|
+
title: 'PG Memory Compaction',
|
|
357
|
+
message: 'Compacting with pg-memory context...',
|
|
358
|
+
variant: 'warning',
|
|
359
|
+
duration: 3000,
|
|
360
|
+
},
|
|
361
|
+
}).catch(() => { });
|
|
362
|
+
}
|
|
363
|
+
await (0, session_compacting_1.handleSessionCompacting)({
|
|
364
|
+
session: { id: input.sessionID },
|
|
365
|
+
messagesToCompact: output.context || [],
|
|
366
|
+
compactionStrategy: 'prune',
|
|
367
|
+
}, { preserveMessageIds: [] }, pool);
|
|
368
|
+
// Show success toast
|
|
369
|
+
if (ctx.client?.tui?.showToast) {
|
|
370
|
+
setTimeout(() => {
|
|
371
|
+
ctx.client.tui.showToast({
|
|
372
|
+
body: {
|
|
373
|
+
title: 'Compaction Complete',
|
|
374
|
+
message: 'PG Memory preserved high-value observations',
|
|
375
|
+
variant: 'success',
|
|
376
|
+
duration: 2000,
|
|
377
|
+
},
|
|
378
|
+
}).catch(() => { });
|
|
379
|
+
}, 500);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
logger.error('Error in experimental.session.compacting:', error);
|
|
384
|
+
}
|
|
385
|
+
},
|
|
386
|
+
// -----------------------------------------------------------------------
|
|
387
|
+
// Custom MCP Tools
|
|
388
|
+
// -----------------------------------------------------------------------
|
|
389
|
+
tool: {
|
|
390
|
+
recall_memory: {
|
|
391
|
+
description: '从长期记忆中检索相关事实、实体、观察和反思,支持多策略并行检索(语义+BM25+图遍历)。使用多维评分函数:Relevance = 0.5*SemSim + 0.3/(1+RecencyDays) + 0.2*Importance',
|
|
392
|
+
args: {
|
|
393
|
+
query: {
|
|
394
|
+
type: 'string',
|
|
395
|
+
description: '检索查询文本',
|
|
396
|
+
},
|
|
397
|
+
session_id: {
|
|
398
|
+
type: 'string',
|
|
399
|
+
description: '当前会话ID,用于上下文过滤',
|
|
400
|
+
},
|
|
401
|
+
retrieval_strategies: {
|
|
402
|
+
type: 'array',
|
|
403
|
+
items: { type: 'string', enum: ['semantic', 'bm25', 'graph', 'keyword'] },
|
|
404
|
+
default: ['semantic', 'bm25', 'graph'],
|
|
405
|
+
description: '检索策略组合',
|
|
406
|
+
},
|
|
407
|
+
max_results: {
|
|
408
|
+
type: 'number',
|
|
409
|
+
minimum: 1,
|
|
410
|
+
maximum: 50,
|
|
411
|
+
default: 10,
|
|
412
|
+
description: '返回结果数量上限',
|
|
413
|
+
},
|
|
414
|
+
filters: {
|
|
415
|
+
type: 'object',
|
|
416
|
+
properties: {
|
|
417
|
+
entity_types: {
|
|
418
|
+
type: 'array',
|
|
419
|
+
items: { type: 'string' },
|
|
420
|
+
description: '实体类型过滤',
|
|
421
|
+
},
|
|
422
|
+
tier_levels: {
|
|
423
|
+
type: 'array',
|
|
424
|
+
items: { enum: ['permanent', 'project', 'session'] },
|
|
425
|
+
description: '层级过滤',
|
|
426
|
+
},
|
|
427
|
+
min_confidence: {
|
|
428
|
+
type: 'number',
|
|
429
|
+
minimum: 0,
|
|
430
|
+
maximum: 1,
|
|
431
|
+
default: 0.5,
|
|
432
|
+
description: '最低置信度',
|
|
433
|
+
},
|
|
434
|
+
time_range_days: {
|
|
435
|
+
type: 'number',
|
|
436
|
+
description: '时间范围(天)',
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
rerank: {
|
|
441
|
+
type: 'boolean',
|
|
442
|
+
default: true,
|
|
443
|
+
description: '是否使用交叉编码器重排序',
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
execute: async (args, _context) => {
|
|
447
|
+
return (0, recall_memory_1.recallMemory)(args, pool, {
|
|
448
|
+
maxResults: plugin.config.retrieval.maxResults,
|
|
449
|
+
rerankEnabled: plugin.config.retrieval.rerankEnabled,
|
|
450
|
+
weights: plugin.config.retrieval.weights,
|
|
451
|
+
});
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
hindsight_reflect: {
|
|
455
|
+
description: '对会话观察进行反思,归纳经验模式,生成可复用的反思记录。每30-50条经验触发一次,使用7B蒸馏模型在低峰期执行。',
|
|
456
|
+
args: {
|
|
457
|
+
session_id: {
|
|
458
|
+
type: 'string',
|
|
459
|
+
description: '要反思的会话ID',
|
|
460
|
+
},
|
|
461
|
+
trigger_type: {
|
|
462
|
+
type: 'string',
|
|
463
|
+
enum: ['threshold', 'scheduled', 'manual'],
|
|
464
|
+
default: 'threshold',
|
|
465
|
+
description: '触发类型',
|
|
466
|
+
},
|
|
467
|
+
observation_threshold: {
|
|
468
|
+
type: 'number',
|
|
469
|
+
minimum: 10,
|
|
470
|
+
maximum: 100,
|
|
471
|
+
default: 30,
|
|
472
|
+
description: '触发反思的观察数量阈值',
|
|
473
|
+
},
|
|
474
|
+
model_size: {
|
|
475
|
+
type: 'string',
|
|
476
|
+
enum: ['7b', '14b', 'full'],
|
|
477
|
+
default: '7b',
|
|
478
|
+
description: '使用的模型规模',
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
execute: async (args, _context) => {
|
|
482
|
+
return (0, hindsight_reflect_1.hindsightReflect)(args, pool, {
|
|
483
|
+
observationThreshold: plugin.config.reflection.observationThreshold,
|
|
484
|
+
modelSize: plugin.config.reflection.modelSize,
|
|
485
|
+
offPeakHours: plugin.config.reflection.offPeakHours,
|
|
486
|
+
});
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
};
|
|
491
|
+
};
|
|
492
|
+
exports.OpenCodePGMemory = OpenCodePGMemory;
|
|
493
|
+
// ============================================================================
|
|
494
|
+
// Helpers
|
|
495
|
+
// ============================================================================
|
|
496
|
+
/**
|
|
497
|
+
* Build plugin config from environment variables, with sensible defaults.
|
|
498
|
+
*/
|
|
499
|
+
function buildConfigFromEnv() {
|
|
500
|
+
return {
|
|
501
|
+
database: {
|
|
502
|
+
host: process.env.PG_HOST || 'localhost',
|
|
503
|
+
port: parseInt(process.env.PG_PORT || '5432', 10),
|
|
504
|
+
database: process.env.PG_DATABASE || 'opencode_memory',
|
|
505
|
+
user: process.env.PG_USER || 'opencode',
|
|
506
|
+
password: process.env.PG_PASSWORD || '',
|
|
507
|
+
ssl: process.env.PG_SSL === 'true',
|
|
508
|
+
},
|
|
509
|
+
embedding: {
|
|
510
|
+
model: process.env.EMBEDDING_MODEL || 'text-embedding-3-small',
|
|
511
|
+
dimensions: parseInt(process.env.EMBEDDING_DIMENSIONS || '1536', 10),
|
|
512
|
+
batchSize: parseInt(process.env.EMBEDDING_BATCH_SIZE || '100', 10),
|
|
513
|
+
},
|
|
514
|
+
cache: {
|
|
515
|
+
enabled: process.env.CACHE_ENABLED !== 'false',
|
|
516
|
+
initialThreshold: parseFloat(process.env.CACHE_INITIAL_THRESHOLD || '0.92'),
|
|
517
|
+
adjustmentStep: parseFloat(process.env.CACHE_ADJUSTMENT_STEP || '0.02'),
|
|
518
|
+
minThreshold: parseFloat(process.env.CACHE_MIN_THRESHOLD || '0.85'),
|
|
519
|
+
maxThreshold: parseFloat(process.env.CACHE_MAX_THRESHOLD || '0.97'),
|
|
520
|
+
},
|
|
521
|
+
reflection: {
|
|
522
|
+
enabled: process.env.REFLECTION_ENABLED !== 'false',
|
|
523
|
+
observationThreshold: parseInt(process.env.REFLECTION_OBSERVATION_THRESHOLD || '30', 10),
|
|
524
|
+
segmentThreshold: parseInt(process.env.REFLECTION_SEGMENT_THRESHOLD || '5', 10),
|
|
525
|
+
modelSize: process.env.REFLECTION_MODEL_SIZE || '7b',
|
|
526
|
+
offPeakHours: (process.env.REFLECTION_OFF_PEAK_HOURS || '1,2,3,4,5')
|
|
527
|
+
.split(',')
|
|
528
|
+
.map((n) => parseInt(n.trim(), 10))
|
|
529
|
+
.filter((n) => !isNaN(n)),
|
|
530
|
+
},
|
|
531
|
+
tokenBudget: {
|
|
532
|
+
contextLimitRatio: parseFloat(process.env.TOKEN_CONTEXT_LIMIT_RATIO || '0.05'),
|
|
533
|
+
minTokens: parseInt(process.env.TOKEN_MIN_TOKENS || '500', 10),
|
|
534
|
+
maxTokens: parseInt(process.env.TOKEN_MAX_TOKENS || '4000', 10),
|
|
535
|
+
},
|
|
536
|
+
retrieval: {
|
|
537
|
+
defaultStrategies: (process.env.RETRIEVAL_STRATEGIES || 'semantic,bm25,graph')
|
|
538
|
+
.split(',')
|
|
539
|
+
.map((s) => s.trim()),
|
|
540
|
+
rerankEnabled: process.env.RETRIEVAL_RERANK !== 'false',
|
|
541
|
+
maxResults: parseInt(process.env.RETRIEVAL_MAX_RESULTS || '10', 10),
|
|
542
|
+
weights: {
|
|
543
|
+
semantic: parseFloat(process.env.RETRIEVAL_WEIGHT_SEMANTIC || '0.5'),
|
|
544
|
+
recency: parseFloat(process.env.RETRIEVAL_WEIGHT_RECENCY || '0.3'),
|
|
545
|
+
importance: parseFloat(process.env.RETRIEVAL_WEIGHT_IMPORTANCE || '0.2'),
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Normalize session data from various event formats into a consistent shape
|
|
552
|
+
* compatible with the existing handler functions.
|
|
553
|
+
*/
|
|
554
|
+
function normalizeSessionData(data) {
|
|
555
|
+
return {
|
|
556
|
+
id: data.id || data.external_id || '',
|
|
557
|
+
projectId: data.projectId || data.project_id || undefined,
|
|
558
|
+
model: {
|
|
559
|
+
id: data.model?.id || data.modelID || data.model_id || 'unknown',
|
|
560
|
+
contextLimit: data.model?.contextLimit || data.contextLimit || data.model_context_limit || 128000,
|
|
561
|
+
name: data.model?.name || data.modelName || data.model_name || 'unknown',
|
|
562
|
+
},
|
|
563
|
+
messages: data.messages || [],
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
// ============================================================================
|
|
567
|
+
// Chat Message Helpers
|
|
568
|
+
// ============================================================================
|
|
569
|
+
/**
|
|
570
|
+
* Format memory facts into a context block for LLM consumption.
|
|
571
|
+
*/
|
|
572
|
+
function formatMemoryContext(facts) {
|
|
573
|
+
if (facts.length === 0)
|
|
574
|
+
return '';
|
|
575
|
+
const lines = ['[PG MEMORY]', 'Relevant context from previous sessions:', ''];
|
|
576
|
+
for (const f of facts) {
|
|
577
|
+
const typeLabel = f.type === 'reflection' ? 'REFLECTION'
|
|
578
|
+
: f.type === 'observation' ? 'OBSERVATION'
|
|
579
|
+
: f.type === 'entity' ? 'ENTITY'
|
|
580
|
+
: f.type.toUpperCase();
|
|
581
|
+
const tierLabel = f.tier ? ` [${f.tier}]` : '';
|
|
582
|
+
const score = f.relevanceScore ? ` (${(f.relevanceScore * 100).toFixed(0)}%)` : '';
|
|
583
|
+
lines.push(`- [${typeLabel}${tierLabel}] ${f.content.substring(0, 200)}${score}`);
|
|
584
|
+
}
|
|
585
|
+
lines.push('', 'Tip: Use recall_memory(query="...") to search more specific memories, or /pg-memory-reflect to summarize this session.');
|
|
586
|
+
return lines.join('\n');
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Simplified inline memory retrieval for chat.message injection.
|
|
590
|
+
* Queries entities with high weight directly from PG.
|
|
591
|
+
*/
|
|
592
|
+
async function retrieveFactsForInjection(sessionId, budget, pool, config) {
|
|
593
|
+
const facts = [];
|
|
594
|
+
const maxTokens = budget.maxTokens || 2000;
|
|
595
|
+
let usedTokens = 0;
|
|
596
|
+
try {
|
|
597
|
+
const { rows } = await pool.query(`
|
|
598
|
+
SELECT name, type, tier, weight, description, confidence,
|
|
599
|
+
EXTRACT(EPOCH FROM (NOW() - last_seen_at)) / 86400.0 AS days_ago
|
|
600
|
+
FROM entities
|
|
601
|
+
WHERE weight >= $1 AND confidence >= $2
|
|
602
|
+
ORDER BY tier = 'permanent' DESC, tier = 'project' DESC, weight DESC
|
|
603
|
+
LIMIT 20
|
|
604
|
+
`, [config?.minWeight || 0.5, config?.minConfidence || 0.5]);
|
|
605
|
+
for (const row of rows) {
|
|
606
|
+
const content = row.description
|
|
607
|
+
? `${row.name}: ${row.description.substring(0, 150)}`
|
|
608
|
+
: `${row.name} (${row.type})`;
|
|
609
|
+
const tokens = Math.ceil(content.length / 4);
|
|
610
|
+
if (usedTokens + tokens > maxTokens)
|
|
611
|
+
break;
|
|
612
|
+
usedTokens += tokens;
|
|
613
|
+
const recency = Math.max(0, 1 - (row.days_ago || 0) / 90);
|
|
614
|
+
facts.push({
|
|
615
|
+
type: 'entity',
|
|
616
|
+
content,
|
|
617
|
+
tier: row.tier,
|
|
618
|
+
tokens,
|
|
619
|
+
relevanceScore: (row.weight / 10) * 0.6 + recency * 0.4,
|
|
620
|
+
metadata: { entityName: row.name, entityType: row.type, tier: row.tier },
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
catch (err) {
|
|
625
|
+
// Non-blocking fallback - return empty
|
|
626
|
+
}
|
|
627
|
+
return facts;
|
|
628
|
+
}
|
|
629
|
+
// ============================================================================
|
|
630
|
+
// Re-exports (types and utilities for external consumers)
|
|
631
|
+
// NOTE: Selective exports to avoid ambiguity with ./types
|
|
632
|
+
// ============================================================================
|
|
633
|
+
__exportStar(require("./types"), exports);
|
|
634
|
+
__exportStar(require("./db/init-db"), exports);
|
|
635
|
+
__exportStar(require("./utils/token-budget"), exports);
|
|
636
|
+
__exportStar(require("./cache/semantic-cache"), exports);
|
|
637
|
+
var recall_memory_2 = require("./mcp/recall-memory");
|
|
638
|
+
Object.defineProperty(exports, "recallMemory", { enumerable: true, get: function () { return recall_memory_2.recallMemory; } });
|
|
639
|
+
var hindsight_reflect_2 = require("./mcp/hindsight-reflect");
|
|
640
|
+
Object.defineProperty(exports, "hindsightReflect", { enumerable: true, get: function () { return hindsight_reflect_2.hindsightReflect; } });
|
|
641
|
+
// Default export for OpenCode plugin loader
|
|
642
|
+
exports.default = exports.OpenCodePGMemory;
|
|
643
|
+
//# sourceMappingURL=index.js.map
|