opencode-mem 1.0.0 → 2.0.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/README.md +80 -477
- package/dist/config.d.ts +5 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +46 -20
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -88
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +1 -8
- package/dist/services/ai/ai-provider-factory.d.ts +8 -0
- package/dist/services/ai/ai-provider-factory.d.ts.map +1 -0
- package/dist/services/ai/ai-provider-factory.js +25 -0
- package/dist/services/ai/providers/anthropic-messages.d.ts +13 -0
- package/dist/services/ai/providers/anthropic-messages.d.ts.map +1 -0
- package/dist/services/ai/providers/anthropic-messages.js +176 -0
- package/dist/services/ai/providers/base-provider.d.ts +21 -0
- package/dist/services/ai/providers/base-provider.d.ts.map +1 -0
- package/dist/services/ai/providers/base-provider.js +6 -0
- package/dist/services/ai/providers/openai-chat-completion.d.ts +12 -0
- package/dist/services/ai/providers/openai-chat-completion.d.ts.map +1 -0
- package/dist/services/ai/providers/openai-chat-completion.js +181 -0
- package/dist/services/ai/providers/openai-responses.d.ts +14 -0
- package/dist/services/ai/providers/openai-responses.d.ts.map +1 -0
- package/dist/services/ai/providers/openai-responses.js +191 -0
- package/dist/services/ai/session/ai-session-manager.d.ts +21 -0
- package/dist/services/ai/session/ai-session-manager.d.ts.map +1 -0
- package/dist/services/ai/session/ai-session-manager.js +165 -0
- package/dist/services/ai/session/session-types.d.ts +43 -0
- package/dist/services/ai/session/session-types.d.ts.map +1 -0
- package/dist/services/ai/session/session-types.js +1 -0
- package/dist/services/ai/tools/tool-schema.d.ts +41 -0
- package/dist/services/ai/tools/tool-schema.d.ts.map +1 -0
- package/dist/services/ai/tools/tool-schema.js +24 -0
- package/dist/services/api-handlers.d.ts +11 -3
- package/dist/services/api-handlers.d.ts.map +1 -1
- package/dist/services/api-handlers.js +143 -30
- package/dist/services/auto-capture.d.ts +1 -30
- package/dist/services/auto-capture.d.ts.map +1 -1
- package/dist/services/auto-capture.js +199 -396
- package/dist/services/cleanup-service.d.ts +3 -0
- package/dist/services/cleanup-service.d.ts.map +1 -1
- package/dist/services/cleanup-service.js +31 -4
- package/dist/services/client.d.ts +1 -0
- package/dist/services/client.d.ts.map +1 -1
- package/dist/services/client.js +3 -11
- package/dist/services/sqlite/connection-manager.d.ts.map +1 -1
- package/dist/services/sqlite/connection-manager.js +8 -4
- package/dist/services/user-memory-learning.d.ts +3 -0
- package/dist/services/user-memory-learning.d.ts.map +1 -0
- package/dist/services/user-memory-learning.js +157 -0
- package/dist/services/user-prompt/user-prompt-manager.d.ts +38 -0
- package/dist/services/user-prompt/user-prompt-manager.d.ts.map +1 -0
- package/dist/services/user-prompt/user-prompt-manager.js +164 -0
- package/dist/services/web-server-worker.js +27 -6
- package/dist/services/web-server.d.ts.map +1 -1
- package/dist/services/web-server.js +0 -5
- package/dist/types/index.d.ts +5 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/web/app.js +210 -120
- package/dist/web/index.html +14 -10
- package/dist/web/styles.css +326 -1
- package/package.json +4 -1
- package/dist/services/compaction.d.ts +0 -92
- package/dist/services/compaction.d.ts.map +0 -1
- package/dist/services/compaction.js +0 -421
- package/dist/services/sqlite-client.d.ts +0 -116
- package/dist/services/sqlite-client.d.ts.map +0 -1
- package/dist/services/sqlite-client.js +0 -284
- package/dist/services/web-server-lock.d.ts +0 -12
- package/dist/services/web-server-lock.d.ts.map +0 -1
- package/dist/services/web-server-lock.js +0 -157
- package/dist/web/favicon.svg +0 -14
|
@@ -2,154 +2,14 @@ import { memoryClient } from "./client.js";
|
|
|
2
2
|
import { getTags } from "./tags.js";
|
|
3
3
|
import { log } from "./logger.js";
|
|
4
4
|
import { CONFIG } from "../config.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
tokenThreshold;
|
|
9
|
-
minTokens;
|
|
10
|
-
enabled;
|
|
11
|
-
maxMemories;
|
|
12
|
-
constructor() {
|
|
13
|
-
this.tokenThreshold = CONFIG.autoCaptureTokenThreshold;
|
|
14
|
-
this.minTokens = CONFIG.autoCaptureMinTokens;
|
|
15
|
-
this.maxMemories = CONFIG.autoCaptureMaxMemories;
|
|
16
|
-
this.enabled =
|
|
17
|
-
CONFIG.autoCaptureEnabled &&
|
|
18
|
-
!!CONFIG.memoryModel &&
|
|
19
|
-
!!CONFIG.memoryApiUrl &&
|
|
20
|
-
!!CONFIG.memoryApiKey;
|
|
21
|
-
if (CONFIG.autoCaptureEnabled && !this.enabled) {
|
|
22
|
-
log("Auto-capture disabled: external API not configured (memoryModel, memoryApiUrl, memoryApiKey required)");
|
|
23
|
-
}
|
|
24
|
-
if (this.enabled && CONFIG.memoryApiUrl?.includes("ollama")) {
|
|
25
|
-
log("Warning: Ollama may not support tool calling. Auto-capture might fail.");
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
isEnabled() {
|
|
29
|
-
return this.enabled;
|
|
30
|
-
}
|
|
31
|
-
getDisabledReason() {
|
|
32
|
-
if (!CONFIG.autoCaptureEnabled)
|
|
33
|
-
return "Auto-capture disabled in config";
|
|
34
|
-
if (!CONFIG.memoryModel)
|
|
35
|
-
return "memoryModel not configured";
|
|
36
|
-
if (!CONFIG.memoryApiUrl)
|
|
37
|
-
return "memoryApiUrl not configured";
|
|
38
|
-
if (!CONFIG.memoryApiKey)
|
|
39
|
-
return "memoryApiKey not configured";
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
toggle() {
|
|
43
|
-
this.enabled = !this.enabled;
|
|
44
|
-
return this.enabled;
|
|
45
|
-
}
|
|
46
|
-
getOrCreateBuffer(sessionID) {
|
|
47
|
-
if (!this.buffers.has(sessionID)) {
|
|
48
|
-
this.buffers.set(sessionID, {
|
|
49
|
-
sessionID,
|
|
50
|
-
lastCaptureTokens: 0,
|
|
51
|
-
lastCaptureTime: Date.now(),
|
|
52
|
-
lastCapturedMessageIndex: -1,
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
return this.buffers.get(sessionID);
|
|
56
|
-
}
|
|
57
|
-
checkTokenThreshold(sessionID, totalTokens) {
|
|
58
|
-
if (!this.enabled)
|
|
59
|
-
return false;
|
|
60
|
-
if (this.capturing.has(sessionID))
|
|
61
|
-
return false;
|
|
62
|
-
const buffer = this.getOrCreateBuffer(sessionID);
|
|
63
|
-
if (totalTokens < this.minTokens)
|
|
64
|
-
return false;
|
|
65
|
-
const tokensSinceCapture = totalTokens - buffer.lastCaptureTokens;
|
|
66
|
-
if (tokensSinceCapture >= this.tokenThreshold) {
|
|
67
|
-
buffer.lastCaptureTokens = totalTokens;
|
|
68
|
-
return true;
|
|
69
|
-
}
|
|
70
|
-
return false;
|
|
71
|
-
}
|
|
72
|
-
getSystemPrompt(hasContext) {
|
|
73
|
-
const summaryGuidance = CONFIG.autoCaptureSummaryMaxLength > 0
|
|
74
|
-
? `Keep summaries under ${CONFIG.autoCaptureSummaryMaxLength} characters.`
|
|
75
|
-
: "Extract key details and important information. Be concise but complete.";
|
|
76
|
-
const contextNote = hasContext
|
|
77
|
-
? `\n\nIMPORTANT: Messages marked [CONTEXT] were already analyzed in previous capture. They are provided for context only. Focus your extraction on messages marked [NEW]. Do not duplicate memories from context messages.`
|
|
78
|
-
: "";
|
|
79
|
-
return `You are a memory extraction assistant analyzing PAST conversations between a USER and an AI ASSISTANT.
|
|
80
|
-
|
|
81
|
-
IMPORTANT CONTEXT:
|
|
82
|
-
- The conversation below has ALREADY HAPPENED
|
|
83
|
-
- You are NOT the assistant in this conversation
|
|
84
|
-
- Your job is to EXTRACT MEMORIES from this past conversation
|
|
85
|
-
- DO NOT try to continue or respond to the conversation
|
|
86
|
-
- DO NOT execute any tasks mentioned in the conversation${contextNote}
|
|
87
|
-
|
|
88
|
-
EXTRACTION GUIDELINES:
|
|
89
|
-
|
|
90
|
-
Categorize each memory by scope:
|
|
91
|
-
- "user": Cross-project user behaviors, preferences, patterns, requests
|
|
92
|
-
Examples: "prefers TypeScript", "likes concise responses", "often asks about complexity analysis"
|
|
93
|
-
- "project": Project-specific knowledge, decisions, architecture, context
|
|
94
|
-
Examples: "uses Bun runtime", "API at /api/v1", "working on opencode-mem plugin"
|
|
95
|
-
|
|
96
|
-
Memory categorization:
|
|
97
|
-
- Choose appropriate type: preference, architecture, workflow, bug-fix, configuration, pattern, request, context
|
|
98
|
-
- Be specific and descriptive with categories
|
|
99
|
-
- Focus on WHAT WAS DISCUSSED, not what should be done
|
|
100
|
-
|
|
101
|
-
Summary guidelines:
|
|
102
|
-
- ${summaryGuidance}
|
|
103
|
-
- Only extract memories worth long-term retention
|
|
104
|
-
- Be selective: quality over quantity
|
|
105
|
-
- Each memory should be atomic and independent
|
|
106
|
-
- Maximum ${this.maxMemories} memories per capture
|
|
107
|
-
- Extract facts, decisions, and context - NOT tasks or actions
|
|
108
|
-
|
|
109
|
-
Use the save_memories function to save extracteories.`;
|
|
110
|
-
}
|
|
111
|
-
markCapturing(sessionID) {
|
|
112
|
-
this.capturing.add(sessionID);
|
|
113
|
-
}
|
|
114
|
-
clearBuffer(sessionID) {
|
|
115
|
-
const buffer = this.buffers.get(sessionID);
|
|
116
|
-
if (buffer) {
|
|
117
|
-
this.buffers.set(sessionID, {
|
|
118
|
-
sessionID,
|
|
119
|
-
lastCaptureTokens: buffer.lastCaptureTokens,
|
|
120
|
-
lastCaptureTime: Date.now(),
|
|
121
|
-
lastCapturedMessageIndex: buffer.lastCapturedMessageIndex,
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
this.capturing.delete(sessionID);
|
|
125
|
-
}
|
|
126
|
-
getStats(sessionID) {
|
|
127
|
-
const buffer = this.buffers.get(sessionID);
|
|
128
|
-
if (!buffer)
|
|
129
|
-
return null;
|
|
130
|
-
return {
|
|
131
|
-
lastCaptureTokens: buffer.lastCaptureTokens,
|
|
132
|
-
timeSinceCapture: Date.now() - buffer.lastCaptureTime,
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
cleanup(sessionID) {
|
|
136
|
-
this.buffers.delete(sessionID);
|
|
137
|
-
this.capturing.delete(sessionID);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
export async function performAutoCapture(ctx, service, sessionID, directory) {
|
|
5
|
+
import { userPromptManager } from "./user-prompt/user-prompt-manager.js";
|
|
6
|
+
const MAX_TOOL_INPUT_LENGTH = 100;
|
|
7
|
+
export async function performAutoCapture(ctx, sessionID, directory) {
|
|
141
8
|
try {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
title: "Auto-Capture",
|
|
147
|
-
message: "Analyzing conversation...",
|
|
148
|
-
variant: "info",
|
|
149
|
-
duration: 2000,
|
|
150
|
-
},
|
|
151
|
-
})
|
|
152
|
-
.catch(() => { });
|
|
9
|
+
const prompt = userPromptManager.getLastUncapturedPrompt(sessionID);
|
|
10
|
+
if (!prompt) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
153
13
|
if (!ctx.client) {
|
|
154
14
|
throw new Error("Client not available");
|
|
155
15
|
}
|
|
@@ -157,147 +17,58 @@ export async function performAutoCapture(ctx, service, sessionID, directory) {
|
|
|
157
17
|
path: { id: sessionID },
|
|
158
18
|
});
|
|
159
19
|
if (!response.data) {
|
|
160
|
-
log("Auto-capture
|
|
161
|
-
service.clearBuffer(sessionID);
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
const allMessages = response.data;
|
|
165
|
-
if (allMessages.length === 0) {
|
|
166
|
-
service.clearBuffer(sessionID);
|
|
20
|
+
log("Auto-capture: no messages in session", { sessionID });
|
|
167
21
|
return;
|
|
168
22
|
}
|
|
169
|
-
const
|
|
170
|
-
const
|
|
171
|
-
if (
|
|
172
|
-
|
|
173
|
-
log("Auto-capture: message deletion detected, resetting index", { sessionID });
|
|
174
|
-
}
|
|
175
|
-
const contextWindow = CONFIG.autoCaptureContextWindow;
|
|
176
|
-
const startIndex = Math.max(0, lastIndex - contextWindow + 1);
|
|
177
|
-
const messagesToAnalyze = allMessages.slice(startIndex);
|
|
178
|
-
if (messagesToAnalyze.length === 0) {
|
|
179
|
-
service.clearBuffer(sessionID);
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
const userMessages = messagesToAnalyze.filter((m) => m?.info?.role === "user");
|
|
183
|
-
const assistantMessages = messagesToAnalyze.filter((m) => m?.info?.role === "assistant");
|
|
184
|
-
if (userMessages.length === 0 || assistantMessages.length === 0) {
|
|
185
|
-
service.clearBuffer(sessionID);
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
let hasCompletePair = false;
|
|
189
|
-
for (let i = 0; i < messagesToAnalyze.length - 1; i++) {
|
|
190
|
-
const current = messagesToAnalyze[i];
|
|
191
|
-
const next = messagesToAnalyze[i + 1];
|
|
192
|
-
if (current?.info?.role === "user" && next?.info?.role === "assistant") {
|
|
193
|
-
hasCompletePair = true;
|
|
194
|
-
break;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
if (!hasCompletePair) {
|
|
198
|
-
service.clearBuffer(sessionID);
|
|
23
|
+
const messages = response.data;
|
|
24
|
+
const promptIndex = messages.findIndex((m) => m.info?.id === prompt.messageId);
|
|
25
|
+
if (promptIndex === -1) {
|
|
26
|
+
log("Auto-capture: prompt message not found", { sessionID, messageId: prompt.messageId });
|
|
199
27
|
return;
|
|
200
28
|
}
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
const msg = messagesToAnalyze[i];
|
|
204
|
-
if (!msg)
|
|
205
|
-
continue;
|
|
206
|
-
const globalIndex = startIndex + i;
|
|
207
|
-
const isNewMessage = globalIndex > lastIndex;
|
|
208
|
-
const role = msg.info?.role;
|
|
209
|
-
if (role !== "user" && role !== "assistant")
|
|
210
|
-
continue;
|
|
211
|
-
const roleLabel = role.toUpperCase();
|
|
212
|
-
const marker = isNewMessage ? "[NEW]" : "[CONTEXT]";
|
|
213
|
-
let content = "";
|
|
214
|
-
if (msg.parts && Array.isArray(msg.parts)) {
|
|
215
|
-
const textParts = msg.parts.filter((p) => p.type === "text" && p.text);
|
|
216
|
-
content = textParts.map((p) => p.text).join("\n");
|
|
217
|
-
const toolParts = msg.parts.filter((p) => p.type === "tool");
|
|
218
|
-
if (toolParts.length > 0) {
|
|
219
|
-
content += "\n[Tools: " + toolParts.map((p) => p.name || "unknown").join(", ") + "]";
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
if (content) {
|
|
223
|
-
conversationParts.push(`${marker} ${roleLabel}: ${content}`);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
if (conversationParts.length === 0) {
|
|
227
|
-
service.clearBuffer(sessionID);
|
|
29
|
+
const aiMessages = messages.slice(promptIndex + 1);
|
|
30
|
+
if (aiMessages.length === 0) {
|
|
228
31
|
return;
|
|
229
32
|
}
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
const contextMessageCount = messagesToAnalyze.length - newMessageCount;
|
|
233
|
-
const conversationText = `=== CONVERSATION TO ANALYZE ===
|
|
234
|
-
|
|
235
|
-
Metadata:
|
|
236
|
-
- Total messages in session: ${allMessages.length}
|
|
237
|
-
- Messages in this analysis: ${messagesToAnalyze.length}
|
|
238
|
-
- Context messages (already captured): ${contextMessageCount}
|
|
239
|
-
- New messages (focus here): ${newMessageCount}
|
|
240
|
-
${lastIndex >= 0 ? `- Previous capture ended at message index: ${lastIndex}` : "- This is the first capture for this session"}
|
|
241
|
-
|
|
242
|
-
The following is a past conversation between a USER and an AI ASSISTANT.
|
|
243
|
-
Extract meaningful memories from this conversation.
|
|
244
|
-
|
|
245
|
-
${conversationBody}
|
|
246
|
-
|
|
247
|
-
=== END OF CONVERSATION ===`;
|
|
248
|
-
const systemPrompt = service.getSystemPrompt(lastIndex >= 0);
|
|
249
|
-
const captureResponse = await summarizeWithAI(ctx, sessionID, systemPrompt, conversationText);
|
|
250
|
-
if (!captureResponse || !captureResponse.memories || captureResponse.memories.length === 0) {
|
|
251
|
-
service.clearBuffer(sessionID);
|
|
33
|
+
const { textResponses, toolCalls } = extractAIContent(aiMessages);
|
|
34
|
+
if (textResponses.length === 0 && toolCalls.length === 0) {
|
|
252
35
|
return;
|
|
253
36
|
}
|
|
254
37
|
const tags = getTags(directory);
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
type: memory.type,
|
|
262
|
-
source: "auto-capture",
|
|
263
|
-
sessionID,
|
|
264
|
-
reasoning: memory.reasoning,
|
|
265
|
-
captureTimestamp: Date.now(),
|
|
266
|
-
displayName: tagInfo.displayName,
|
|
267
|
-
userName: tagInfo.userName,
|
|
268
|
-
userEmail: tagInfo.userEmail,
|
|
269
|
-
projectPath: tagInfo.projectPath,
|
|
270
|
-
projectName: tagInfo.projectName,
|
|
271
|
-
gitRepoUrl: tagInfo.gitRepoUrl,
|
|
272
|
-
});
|
|
273
|
-
if (result.success) {
|
|
274
|
-
results.push({ scope: memory.scope, id: result.id });
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
if (results.length === 0) {
|
|
278
|
-
service.clearBuffer(sessionID);
|
|
38
|
+
const latestMemory = await getLatestProjectMemory(tags.project.tag);
|
|
39
|
+
const context = buildMarkdownContext(prompt.content, textResponses, toolCalls, latestMemory);
|
|
40
|
+
const summaryResult = await generateSummary(ctx, context, sessionID);
|
|
41
|
+
if (!summaryResult || summaryResult.type === "skip") {
|
|
42
|
+
log("Auto-capture: skipped non-technical conversation", { sessionID });
|
|
43
|
+
userPromptManager.deletePrompt(prompt.id);
|
|
279
44
|
return;
|
|
280
45
|
}
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
await ctx.client?.tui
|
|
284
|
-
.showToast({
|
|
285
|
-
body: {
|
|
286
|
-
title: "Memory Captured",
|
|
287
|
-
message: `Saved ${userCount} user + ${projectCount} project memories`,
|
|
288
|
-
variant: "success",
|
|
289
|
-
duration: 3000,
|
|
290
|
-
},
|
|
291
|
-
})
|
|
292
|
-
.catch(() => { });
|
|
293
|
-
log("Auto-capture: success", {
|
|
46
|
+
const result = await memoryClient.addMemory(summaryResult.summary, tags.project.tag, {
|
|
47
|
+
source: "auto-capture",
|
|
294
48
|
sessionID,
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
49
|
+
promptId: prompt.id,
|
|
50
|
+
captureTimestamp: Date.now(),
|
|
51
|
+
displayName: tags.project.displayName,
|
|
52
|
+
userName: tags.project.userName,
|
|
53
|
+
userEmail: tags.project.userEmail,
|
|
54
|
+
projectPath: tags.project.projectPath,
|
|
55
|
+
projectName: tags.project.projectName,
|
|
56
|
+
gitRepoUrl: tags.project.gitRepoUrl,
|
|
298
57
|
});
|
|
299
|
-
|
|
300
|
-
|
|
58
|
+
if (result.success) {
|
|
59
|
+
userPromptManager.linkMemoryToPrompt(prompt.id, result.id);
|
|
60
|
+
userPromptManager.markAsCaptured(prompt.id);
|
|
61
|
+
await ctx.client?.tui
|
|
62
|
+
.showToast({
|
|
63
|
+
body: {
|
|
64
|
+
title: "Memory Captured",
|
|
65
|
+
message: "Project memory saved from conversation",
|
|
66
|
+
variant: "success",
|
|
67
|
+
duration: 3000,
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
.catch(() => { });
|
|
71
|
+
}
|
|
301
72
|
}
|
|
302
73
|
catch (error) {
|
|
303
74
|
log("Auto-capture error", { sessionID, error: String(error) });
|
|
@@ -311,141 +82,173 @@ ${conversationBody}
|
|
|
311
82
|
},
|
|
312
83
|
})
|
|
313
84
|
.catch(() => { });
|
|
314
|
-
service.clearBuffer(sessionID);
|
|
315
85
|
}
|
|
316
86
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
87
|
+
function extractAIContent(messages) {
|
|
88
|
+
const textResponses = [];
|
|
89
|
+
const toolCalls = [];
|
|
90
|
+
for (const msg of messages) {
|
|
91
|
+
if (msg.info?.role !== "assistant")
|
|
92
|
+
continue;
|
|
93
|
+
if (!msg.parts || !Array.isArray(msg.parts))
|
|
94
|
+
continue;
|
|
95
|
+
const textParts = msg.parts.filter((p) => p.type === "text" && p.text);
|
|
96
|
+
if (textParts.length > 0) {
|
|
97
|
+
const text = textParts.map((p) => p.text).join("\n");
|
|
98
|
+
if (text.trim()) {
|
|
99
|
+
textResponses.push(text.trim());
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const toolParts = msg.parts.filter((p) => p.type === "tool");
|
|
103
|
+
for (const tool of toolParts) {
|
|
104
|
+
const name = tool.tool || "unknown";
|
|
105
|
+
let input = "";
|
|
106
|
+
if (tool.state?.input) {
|
|
107
|
+
const inputObj = tool.state.input;
|
|
108
|
+
if (typeof inputObj === "string") {
|
|
109
|
+
input = inputObj;
|
|
110
|
+
}
|
|
111
|
+
else if (typeof inputObj === "object") {
|
|
112
|
+
const params = [];
|
|
113
|
+
for (const [key, value] of Object.entries(inputObj)) {
|
|
114
|
+
params.push(`${key}: ${JSON.stringify(value)}`);
|
|
115
|
+
}
|
|
116
|
+
input = params.join(", ");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (input.length > MAX_TOOL_INPUT_LENGTH) {
|
|
120
|
+
input = input.substring(0, MAX_TOOL_INPUT_LENGTH) + "...";
|
|
121
|
+
}
|
|
122
|
+
toolCalls.push({ name, input });
|
|
123
|
+
}
|
|
320
124
|
}
|
|
321
|
-
|
|
322
|
-
|
|
125
|
+
return { textResponses, toolCalls };
|
|
126
|
+
}
|
|
127
|
+
async function getLatestProjectMemory(containerTag) {
|
|
128
|
+
try {
|
|
129
|
+
const result = await memoryClient.listMemories(containerTag, 1);
|
|
130
|
+
log("Auto-capture: latest memory list result", { result });
|
|
131
|
+
log("Auto-capture: container tag", { containerTag });
|
|
132
|
+
if (!result.success || result.memories.length === 0) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
const latest = result.memories[0];
|
|
136
|
+
if (!latest) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const content = latest.summary;
|
|
140
|
+
if (content.length <= 500) {
|
|
141
|
+
return content;
|
|
142
|
+
}
|
|
143
|
+
return content.substring(0, 500) + "...";
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return null;
|
|
323
147
|
}
|
|
324
|
-
return await callExternalAPIWithToolCalling(systemPrompt, conversationPrompt);
|
|
325
148
|
}
|
|
326
|
-
function
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
149
|
+
function buildMarkdownContext(userPrompt, textResponses, toolCalls, latestMemory) {
|
|
150
|
+
const sections = [];
|
|
151
|
+
if (latestMemory) {
|
|
152
|
+
sections.push(`## Previous Memory Context`);
|
|
153
|
+
sections.push(`---`);
|
|
154
|
+
sections.push(latestMemory);
|
|
155
|
+
sections.push(`---\n`);
|
|
156
|
+
}
|
|
157
|
+
sections.push(`## User Request`);
|
|
158
|
+
sections.push(`---`);
|
|
159
|
+
sections.push(userPrompt);
|
|
160
|
+
sections.push(`---\n`);
|
|
161
|
+
if (textResponses.length > 0) {
|
|
162
|
+
sections.push(`## AI Response`);
|
|
163
|
+
sections.push(`---`);
|
|
164
|
+
sections.push(textResponses.join("\n\n"));
|
|
165
|
+
sections.push(`---\n`);
|
|
166
|
+
}
|
|
167
|
+
if (toolCalls.length > 0) {
|
|
168
|
+
sections.push(`## Tools Used`);
|
|
169
|
+
sections.push(`---`);
|
|
170
|
+
for (const tool of toolCalls) {
|
|
171
|
+
if (tool.input) {
|
|
172
|
+
sections.push(`- ${tool.name}(${tool.input})`);
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
sections.push(`- ${tool.name}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
sections.push(`---\n`);
|
|
179
|
+
}
|
|
180
|
+
return sections.join("\n");
|
|
181
|
+
}
|
|
182
|
+
async function generateSummary(ctx, context, sessionID) {
|
|
183
|
+
if (!CONFIG.memoryModel || !CONFIG.memoryApiUrl || !CONFIG.memoryApiKey) {
|
|
184
|
+
throw new Error("External API not configured for auto-capture");
|
|
185
|
+
}
|
|
186
|
+
const { AIProviderFactory } = await import("./ai/ai-provider-factory.js");
|
|
187
|
+
const providerConfig = {
|
|
188
|
+
model: CONFIG.memoryModel,
|
|
189
|
+
apiUrl: CONFIG.memoryApiUrl,
|
|
190
|
+
apiKey: CONFIG.memoryApiKey,
|
|
191
|
+
maxIterations: CONFIG.autoCaptureMaxIterations,
|
|
192
|
+
iterationTimeout: CONFIG.autoCaptureIterationTimeout,
|
|
193
|
+
};
|
|
194
|
+
const provider = AIProviderFactory.createProvider(CONFIG.memoryProvider, providerConfig);
|
|
195
|
+
const systemPrompt = `You are a technical memory recorder for a software development project.
|
|
196
|
+
|
|
197
|
+
RULES:
|
|
198
|
+
1. ONLY capture technical work (code, bugs, features, architecture, config)
|
|
199
|
+
2. SKIP non-technical by returning type="skip"
|
|
200
|
+
3. NO meta-commentary or behavior analysis
|
|
201
|
+
4. Include specific file names, functions, technical details
|
|
202
|
+
|
|
203
|
+
FORMAT:
|
|
204
|
+
## Request
|
|
205
|
+
[1-2 sentences: what was requested]
|
|
206
|
+
|
|
207
|
+
## Outcome
|
|
208
|
+
[1-2 sentences: what was done, include files/functions]
|
|
209
|
+
|
|
210
|
+
SKIP if: greetings, casual chat, no code/decisions made
|
|
211
|
+
CAPTURE if: code changed, bug fixed, feature added, decision made
|
|
212
|
+
|
|
213
|
+
EXAMPLES:
|
|
214
|
+
Technical → type="feature":
|
|
215
|
+
## Request
|
|
216
|
+
Fix function returning null.
|
|
217
|
+
## Outcome
|
|
218
|
+
Changed searchMemories() to listMemories() in auto-capture.ts:166.
|
|
219
|
+
|
|
220
|
+
Non-technical → type="skip", summary="":
|
|
221
|
+
User greeted, AI introduced capabilities.`;
|
|
222
|
+
const userPrompt = `${context}
|
|
223
|
+
|
|
224
|
+
Analyze this conversation. If it contains technical work (code, bugs, features, decisions), create a concise summary. If it's non-technical (greetings, casual chat, incomplete requests), return type="skip" with empty summary.`;
|
|
225
|
+
const toolSchema = {
|
|
331
226
|
type: "function",
|
|
332
227
|
function: {
|
|
333
|
-
name: "
|
|
334
|
-
description: "Save
|
|
228
|
+
name: "save_memory",
|
|
229
|
+
description: "Save the conversation summary as a memory",
|
|
335
230
|
parameters: {
|
|
336
231
|
type: "object",
|
|
337
232
|
properties: {
|
|
338
|
-
|
|
339
|
-
type: "
|
|
340
|
-
description: "
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
type: "string",
|
|
346
|
-
description: summaryDescription,
|
|
347
|
-
},
|
|
348
|
-
scope: {
|
|
349
|
-
type: "string",
|
|
350
|
-
enum: ["user", "project"],
|
|
351
|
-
description: "user: cross-project user preferences/behaviors. project: project-specific knowledge/decisions.",
|
|
352
|
-
},
|
|
353
|
-
type: {
|
|
354
|
-
type: "string",
|
|
355
|
-
description: "Category of this memory (e.g., preference, architecture, workflow, bug-fix, configuration, pattern, etc). Choose the most appropriate category.",
|
|
356
|
-
},
|
|
357
|
-
reasoning: {
|
|
358
|
-
type: "string",
|
|
359
|
-
description: "Why this memory is important and worth retaining",
|
|
360
|
-
},
|
|
361
|
-
},
|
|
362
|
-
required: ["summary", "scope", "type"],
|
|
363
|
-
},
|
|
233
|
+
summary: {
|
|
234
|
+
type: "string",
|
|
235
|
+
description: "Markdown-formatted summary of the conversation",
|
|
236
|
+
},
|
|
237
|
+
type: {
|
|
238
|
+
type: "string",
|
|
239
|
+
description: "Type of memory: 'skip' for non-technical conversations, or technical type (feature, bug-fix, refactor, analysis, configuration, discussion, other)",
|
|
364
240
|
},
|
|
365
241
|
},
|
|
366
|
-
required: ["
|
|
242
|
+
required: ["summary", "type"],
|
|
367
243
|
},
|
|
368
244
|
},
|
|
369
245
|
};
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
const timeout = setTimeout(() => controller.abort(), 30000);
|
|
374
|
-
try {
|
|
375
|
-
const tools = [createToolCallSchema()];
|
|
376
|
-
const requestBody = {
|
|
377
|
-
model: CONFIG.memoryModel,
|
|
378
|
-
messages: [
|
|
379
|
-
{ role: "system", content: systemPrompt },
|
|
380
|
-
{ role: "user", content: conversationPrompt },
|
|
381
|
-
],
|
|
382
|
-
tools: tools,
|
|
383
|
-
tool_choice: { type: "function", name: "save_memories" },
|
|
384
|
-
temperature: 0.3,
|
|
385
|
-
};
|
|
386
|
-
const response = await fetch(`${CONFIG.memoryApiUrl}/chat/completions`, {
|
|
387
|
-
method: "POST",
|
|
388
|
-
headers: {
|
|
389
|
-
"Content-Type": "application/json",
|
|
390
|
-
Authorization: `Bearer ${CONFIG.memoryApiKey}`,
|
|
391
|
-
},
|
|
392
|
-
body: JSON.stringify(requestBody),
|
|
393
|
-
signal: controller.signal,
|
|
394
|
-
});
|
|
395
|
-
if (!response.ok) {
|
|
396
|
-
const errorText = await response.text().catch(() => response.statusText);
|
|
397
|
-
log("Auto-capture API error", {
|
|
398
|
-
status: response.status,
|
|
399
|
-
error: errorText,
|
|
400
|
-
});
|
|
401
|
-
throw new Error(`API error: ${response.status} - ${errorText}`);
|
|
402
|
-
}
|
|
403
|
-
const data = (await response.json());
|
|
404
|
-
if (!data.choices || !data.choices[0]) {
|
|
405
|
-
throw new Error("Invalid API response format");
|
|
406
|
-
}
|
|
407
|
-
const choice = data.choices[0];
|
|
408
|
-
if (!choice.message.tool_calls || choice.message.tool_calls.length === 0) {
|
|
409
|
-
log("Auto-capture: tool calling not used", {
|
|
410
|
-
finishReason: choice.finish_reason,
|
|
411
|
-
});
|
|
412
|
-
throw new Error("Tool calling not supported or not used by provider");
|
|
413
|
-
}
|
|
414
|
-
const toolCall = choice.message.tool_calls[0];
|
|
415
|
-
if (!toolCall || toolCall.function.name !== "save_memories") {
|
|
416
|
-
throw new Error("Invalid tool call response");
|
|
417
|
-
}
|
|
418
|
-
const parsed = JSON.parse(toolCall.function.arguments);
|
|
419
|
-
return validateCaptureResponse(parsed);
|
|
420
|
-
}
|
|
421
|
-
catch (error) {
|
|
422
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
423
|
-
throw new Error("API request timeout (30s)");
|
|
424
|
-
}
|
|
425
|
-
throw error;
|
|
426
|
-
}
|
|
427
|
-
finally {
|
|
428
|
-
clearTimeout(timeout);
|
|
246
|
+
const result = await provider.executeToolCall(systemPrompt, userPrompt, toolSchema, sessionID);
|
|
247
|
+
if (!result.success || !result.data) {
|
|
248
|
+
throw new Error(result.error || "Failed to generate summary");
|
|
429
249
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
}
|
|
435
|
-
if (!Array.isArray(data.memories)) {
|
|
436
|
-
throw new Error("memories field is not an array");
|
|
437
|
-
}
|
|
438
|
-
const validMemories = data.memories.filter((m) => {
|
|
439
|
-
return (m &&
|
|
440
|
-
typeof m === "object" &&
|
|
441
|
-
typeof m.summary === "string" &&
|
|
442
|
-
m.summary.trim().length > 0 &&
|
|
443
|
-
(m.scope === "user" || m.scope === "project") &&
|
|
444
|
-
typeof m.type === "string" &&
|
|
445
|
-
m.type.trim().length > 0);
|
|
446
|
-
});
|
|
447
|
-
if (validMemories.length === 0) {
|
|
448
|
-
throw new Error("No valid memories in response");
|
|
449
|
-
}
|
|
450
|
-
return { memories: validMemories };
|
|
250
|
+
return {
|
|
251
|
+
summary: result.data.summary,
|
|
252
|
+
type: result.data.type,
|
|
253
|
+
};
|
|
451
254
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cleanup-service.d.ts","sourceRoot":"","sources":["../../src/services/cleanup-service.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"cleanup-service.d.ts","sourceRoot":"","sources":["../../src/services/cleanup-service.ts"],"names":[],"mappings":"AAOA,UAAU,aAAa;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,qBAAqB,EAAE,MAAM,CAAC;CAC/B;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,eAAe,CAAa;IACpC,OAAO,CAAC,SAAS,CAAkB;IAE7B,gBAAgB,IAAI,OAAO,CAAC,OAAO,CAAC;IAcpC,UAAU,IAAI,OAAO,CAAC,aAAa,CAAC;IAqG1C,SAAS;;;;;;CAQV;AAED,eAAO,MAAM,cAAc,gBAAuB,CAAC"}
|