opencode-memory-pro 1.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/LICENSE +24 -0
- package/README.md +409 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +398 -0
- package/dist/embedder.d.ts +26 -0
- package/dist/embedder.js +260 -0
- package/dist/extract.d.ts +4 -0
- package/dist/extract.js +181 -0
- package/dist/graph.js +701 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +953 -0
- package/dist/llm.d.ts +14 -0
- package/dist/llm.js +212 -0
- package/dist/logger.d.ts +9 -0
- package/dist/logger.js +126 -0
- package/dist/ports.d.ts +34 -0
- package/dist/ports.js +129 -0
- package/dist/preference.d.ts +10 -0
- package/dist/preference.js +125 -0
- package/dist/scope.d.ts +2 -0
- package/dist/scope.js +48 -0
- package/dist/store.d.ts +194 -0
- package/dist/store.js +2738 -0
- package/dist/summarize.d.ts +52 -0
- package/dist/summarize.js +350 -0
- package/dist/tools/episodic.d.ts +68 -0
- package/dist/tools/episodic.js +145 -0
- package/dist/tools/feedback.d.ts +51 -0
- package/dist/tools/feedback.js +112 -0
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.js +3 -0
- package/dist/tools/memory.d.ts +293 -0
- package/dist/tools/memory.js +1487 -0
- package/dist/types.d.ts +489 -0
- package/dist/types.js +54 -0
- package/dist/utils.d.ts +18 -0
- package/dist/utils.js +214 -0
- package/package.json +49 -0
package/dist/config.js
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { clamp, expandHomePath, parseJsonObject, toBoolean, toNumber } from "./utils.js";
|
|
4
|
+
import { log } from "./logger.js";
|
|
5
|
+
const DEFAULT_DB_PATH = "~/.opencode/memory/lancedb";
|
|
6
|
+
const DEFAULT_OLLAMA_BASE_URL = "http://127.0.0.1:11434";
|
|
7
|
+
const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
|
|
8
|
+
const SIDECAR_FILE = "opencode-memory-pro.json";
|
|
9
|
+
export function resolveMemoryConfig(config, worktree) {
|
|
10
|
+
const legacyRaw = (config?.memory ?? {});
|
|
11
|
+
const sidecarRaw = loadSidecarConfig(worktree);
|
|
12
|
+
const raw = mergeMemoryConfig(legacyRaw, sidecarRaw);
|
|
13
|
+
const embeddingRaw = (raw.embedding ?? {});
|
|
14
|
+
const retrievalRaw = (raw.retrieval ?? {});
|
|
15
|
+
const modeRaw = firstString(process.env.OPENCODE_MEMORY_PRO_RETRIEVAL_MODE, retrievalRaw.mode) ?? "hybrid";
|
|
16
|
+
const mode = modeRaw === "vector" ? "vector" : "hybrid";
|
|
17
|
+
const provider = firstString(process.env.OPENCODE_MEMORY_PRO_PROVIDER, raw.provider) ?? "opencode-memory-pro";
|
|
18
|
+
const dbPath = expandHomePath(firstString(process.env.OPENCODE_MEMORY_PRO_DB_PATH, raw.dbPath) ?? DEFAULT_DB_PATH);
|
|
19
|
+
const vectorWeight = clamp(toNumber(process.env.OPENCODE_MEMORY_PRO_VECTOR_WEIGHT ?? retrievalRaw.vectorWeight, 0.7), 0, 1);
|
|
20
|
+
const bm25Weight = clamp(toNumber(process.env.OPENCODE_MEMORY_PRO_BM25_WEIGHT ?? retrievalRaw.bm25Weight, 0.3), 0, 1);
|
|
21
|
+
const weightSum = vectorWeight + bm25Weight;
|
|
22
|
+
const normalizedVectorWeight = weightSum > 0 ? vectorWeight / weightSum : 0.7;
|
|
23
|
+
const normalizedBm25Weight = weightSum > 0 ? bm25Weight / weightSum : 0.3;
|
|
24
|
+
const rrfK = Math.max(1, Math.floor(toNumber(process.env.OPENCODE_MEMORY_PRO_RRF_K ?? retrievalRaw.rrfK, 60)));
|
|
25
|
+
const recencyBoost = toBoolean(process.env.OPENCODE_MEMORY_PRO_RECENCY_BOOST ?? retrievalRaw.recencyBoost, true);
|
|
26
|
+
const recencyHalfLifeHours = Math.max(1, toNumber(process.env.OPENCODE_MEMORY_PRO_RECENCY_HALF_LIFE_HOURS ?? retrievalRaw.recencyHalfLifeHours, 72));
|
|
27
|
+
const importanceWeight = clamp(toNumber(process.env.OPENCODE_MEMORY_PRO_IMPORTANCE_WEIGHT ?? retrievalRaw.importanceWeight, 0.4), 0, 2);
|
|
28
|
+
const feedbackWeight = clamp(toNumber(process.env.OPENCODE_MEMORY_PRO_FEEDBACK_WEIGHT ?? retrievalRaw.feedbackWeight, 0.3), 0, 1);
|
|
29
|
+
const embeddingProvider = resolveEmbeddingProvider(firstString(process.env.OPENCODE_MEMORY_PRO_EMBEDDING_PROVIDER, embeddingRaw.provider));
|
|
30
|
+
const embeddingModel = embeddingProvider === "openai"
|
|
31
|
+
? firstString(process.env.OPENCODE_MEMORY_PRO_OPENAI_MODEL, process.env.OPENCODE_MEMORY_PRO_EMBEDDING_MODEL, embeddingRaw.model)
|
|
32
|
+
: firstString(process.env.OPENCODE_MEMORY_PRO_EMBEDDING_MODEL, embeddingRaw.model) ?? "nomic-embed-text";
|
|
33
|
+
const embeddingBaseUrl = embeddingProvider === "openai"
|
|
34
|
+
? firstString(process.env.OPENCODE_MEMORY_PRO_OPENAI_BASE_URL, embeddingRaw.baseUrl) ?? DEFAULT_OPENAI_BASE_URL
|
|
35
|
+
: firstString(process.env.OPENCODE_MEMORY_PRO_OLLAMA_BASE_URL, embeddingRaw.baseUrl) ?? DEFAULT_OLLAMA_BASE_URL;
|
|
36
|
+
const embeddingApiKey = embeddingProvider === "openai"
|
|
37
|
+
? firstString(process.env.OPENCODE_MEMORY_PRO_OPENAI_API_KEY, embeddingRaw.apiKey)
|
|
38
|
+
: undefined;
|
|
39
|
+
const timeoutEnv = embeddingProvider === "openai"
|
|
40
|
+
? process.env.OPENCODE_MEMORY_PRO_OPENAI_TIMEOUT_MS ?? process.env.OPENCODE_MEMORY_PRO_EMBEDDING_TIMEOUT_MS
|
|
41
|
+
: process.env.OPENCODE_MEMORY_PRO_EMBEDDING_TIMEOUT_MS;
|
|
42
|
+
const timeoutRaw = timeoutEnv ?? embeddingRaw.timeoutMs;
|
|
43
|
+
const retryRaw = (embeddingRaw.retry ?? {});
|
|
44
|
+
const retryEnabled = toBoolean(process.env.OPENCODE_MEMORY_PRO_EMBEDDING_RETRY_ENABLED ?? retryRaw.enabled, true);
|
|
45
|
+
const retryMaxAttempts = Math.max(1, Math.floor(toNumber(process.env.OPENCODE_MEMORY_PRO_EMBEDDING_RETRY_MAX_ATTEMPTS ?? retryRaw.maxAttempts, 3)));
|
|
46
|
+
const retryInitialDelayMs = Math.max(100, Math.floor(toNumber(process.env.OPENCODE_MEMORY_PRO_EMBEDDING_RETRY_INITIAL_DELAY_MS ?? retryRaw.initialDelayMs, 1000)));
|
|
47
|
+
const retryBackoffMultiplier = Math.max(1, toNumber(process.env.OPENCODE_MEMORY_PRO_EMBEDDING_RETRY_BACKOFF_MULTIPLIER ?? retryRaw.backoffMultiplier, 2));
|
|
48
|
+
const injection = resolveInjectionConfig(raw, process.env);
|
|
49
|
+
const dedup = resolveDedupConfig(raw, process.env);
|
|
50
|
+
const graph = resolveGraphConfig(raw, process.env);
|
|
51
|
+
// LLM_CAPTURE (1.1): capture mode + LLM settings for SDK-transport
|
|
52
|
+
// extraction/summarization. See resolveCaptureConfig.
|
|
53
|
+
const capture = resolveCaptureConfig(raw, process.env);
|
|
54
|
+
// MEMORY_LIFECYCLE_TOOLS: offline store-level summarization (0.9) —
|
|
55
|
+
// extractive digests of old memories. See resolveSummarizeConfig.
|
|
56
|
+
const summarize = resolveSummarizeConfig(raw, process.env);
|
|
57
|
+
const resolvedConfig = {
|
|
58
|
+
provider,
|
|
59
|
+
dbPath,
|
|
60
|
+
embedding: {
|
|
61
|
+
provider: embeddingProvider,
|
|
62
|
+
model: embeddingModel ?? "",
|
|
63
|
+
baseUrl: embeddingBaseUrl,
|
|
64
|
+
apiKey: embeddingApiKey,
|
|
65
|
+
timeoutMs: Math.max(500, Math.floor(toNumber(timeoutRaw, 6000))),
|
|
66
|
+
retry: {
|
|
67
|
+
enabled: retryEnabled,
|
|
68
|
+
maxAttempts: retryMaxAttempts,
|
|
69
|
+
initialDelayMs: retryInitialDelayMs,
|
|
70
|
+
backoffMultiplier: retryBackoffMultiplier,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
retrieval: {
|
|
74
|
+
mode,
|
|
75
|
+
vectorWeight: normalizedVectorWeight,
|
|
76
|
+
bm25Weight: normalizedBm25Weight,
|
|
77
|
+
minScore: clamp(toNumber(process.env.OPENCODE_MEMORY_PRO_MIN_SCORE ?? retrievalRaw.minScore, 0.2), 0, 1),
|
|
78
|
+
rrfK,
|
|
79
|
+
recencyBoost,
|
|
80
|
+
recencyHalfLifeHours,
|
|
81
|
+
importanceWeight,
|
|
82
|
+
feedbackWeight,
|
|
83
|
+
},
|
|
84
|
+
injection,
|
|
85
|
+
dedup,
|
|
86
|
+
graph,
|
|
87
|
+
summarize,
|
|
88
|
+
capture,
|
|
89
|
+
// SCOPING_TOGGLE: "global" (default) collapses all scopes to "global"
|
|
90
|
+
// (single-user mode); "project" restores upstream per-project scoping.
|
|
91
|
+
scoping: (process.env.OPENCODE_MEMORY_PRO_SCOPING ?? raw.scoping ?? "global") === "project" ? "project" : "global",
|
|
92
|
+
includeGlobalScope: toBoolean(process.env.OPENCODE_MEMORY_PRO_INCLUDE_GLOBAL_SCOPE ?? raw.includeGlobalScope, true),
|
|
93
|
+
globalDetectionThreshold: Math.max(1, Math.floor(toNumber(process.env.OPENCODE_MEMORY_PRO_GLOBAL_DETECTION_THRESHOLD ?? raw.globalDetectionThreshold, 2))),
|
|
94
|
+
globalDiscountFactor: clamp(toNumber(process.env.OPENCODE_MEMORY_PRO_GLOBAL_DISCOUNT_FACTOR ?? raw.globalDiscountFactor, 0.7), 0, 1),
|
|
95
|
+
unusedDaysThreshold: Math.max(1, Math.floor(toNumber(process.env.OPENCODE_MEMORY_PRO_UNUSED_DAYS_THRESHOLD ?? raw.unusedDaysThreshold, 30))),
|
|
96
|
+
minCaptureChars: Math.max(30, Math.floor(toNumber(process.env.OPENCODE_MEMORY_PRO_MIN_CAPTURE_CHARS ?? raw.minCaptureChars, 80))),
|
|
97
|
+
maxEntriesPerScope: Math.max(50, Math.floor(toNumber(process.env.OPENCODE_MEMORY_PRO_MAX_ENTRIES_PER_SCOPE ?? raw.maxEntriesPerScope, 3000))),
|
|
98
|
+
retention: resolveRetentionConfig(raw, process.env),
|
|
99
|
+
logging: resolveLoggingConfig(raw, process.env),
|
|
100
|
+
};
|
|
101
|
+
validateEmbeddingConfig(resolvedConfig.embedding);
|
|
102
|
+
return resolvedConfig;
|
|
103
|
+
}
|
|
104
|
+
// LOGGING_CONFIG (1.1.4): level = minimum level emitted (debug|info|warn|error),
|
|
105
|
+
// file = append-only crash-surviving log sink. Environment overrides always win
|
|
106
|
+
// so an emergency OPENCODE_MEMORY_PRO_LOG_FILE works before sidecar resolution.
|
|
107
|
+
function resolveLoggingConfig(raw, env) {
|
|
108
|
+
const loggingRaw = (raw.logging ?? {});
|
|
109
|
+
const levelRaw = firstString(env.OPENCODE_MEMORY_PRO_LOG_LEVEL, loggingRaw.level) ?? "info";
|
|
110
|
+
const level = levelRaw === "debug" || levelRaw === "warn" || levelRaw === "error" || levelRaw === "info" ? levelRaw : "info";
|
|
111
|
+
return {
|
|
112
|
+
level,
|
|
113
|
+
file: firstString(env.OPENCODE_MEMORY_PRO_LOG_FILE, loggingRaw.file) ?? null,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function resolveEmbeddingProvider(raw) {
|
|
117
|
+
if (!raw || raw === "ollama")
|
|
118
|
+
return "ollama";
|
|
119
|
+
if (raw === "openai")
|
|
120
|
+
return "openai";
|
|
121
|
+
throw new Error(`[opencode-memory-pro] Invalid embedding provider "${raw}". Expected "ollama" or "openai".`);
|
|
122
|
+
}
|
|
123
|
+
function resolveInjectionMode(raw) {
|
|
124
|
+
if (raw === "fixed" || raw === "budget" || raw === "adaptive")
|
|
125
|
+
return raw;
|
|
126
|
+
return "fixed";
|
|
127
|
+
}
|
|
128
|
+
function resolveSummarizationMode(raw) {
|
|
129
|
+
if (raw === "none" || raw === "truncate" || raw === "extract" || raw === "auto")
|
|
130
|
+
return raw;
|
|
131
|
+
return "none";
|
|
132
|
+
}
|
|
133
|
+
function resolveCodeTruncationMode(raw) {
|
|
134
|
+
if (raw === "smart" || raw === "signature" || raw === "preserve")
|
|
135
|
+
return raw;
|
|
136
|
+
return "smart";
|
|
137
|
+
}
|
|
138
|
+
function resolveDedupConfig(raw, env) {
|
|
139
|
+
const dedupRaw = (raw.dedup ?? {});
|
|
140
|
+
const enabled = toBoolean(env.OPENCODE_MEMORY_PRO_DEDUP_ENABLED ?? dedupRaw.enabled, true);
|
|
141
|
+
const writeThreshold = clamp(toNumber(env.OPENCODE_MEMORY_PRO_DEDUP_WRITE_THRESHOLD ?? dedupRaw.writeThreshold, 0.92), 0.0, 1.0);
|
|
142
|
+
const consolidateThreshold = clamp(toNumber(env.OPENCODE_MEMORY_PRO_DEDUP_CONSOLIDATE_THRESHOLD ?? dedupRaw.consolidateThreshold, 0.95), 0.0, 1.0);
|
|
143
|
+
const candidateLimit = clamp(toNumber(env.OPENCODE_MEMORY_PRO_DEDUP_CANDIDATE_LIMIT ?? dedupRaw.candidateLimit, 50), 10, 200);
|
|
144
|
+
if (candidateLimit !== toNumber(dedupRaw.candidateLimit, 50)) {
|
|
145
|
+
const original = toNumber(dedupRaw.candidateLimit, 50);
|
|
146
|
+
if (original !== 50) {
|
|
147
|
+
log("warn", `[config] dedup.candidateLimit clamped from ${original} to ${candidateLimit}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return { enabled, writeThreshold, consolidateThreshold, candidateLimit };
|
|
151
|
+
}
|
|
152
|
+
// LLM_CAPTURE (1.1): capture mode + LLM settings. mode is "heuristics"
|
|
153
|
+
// (keyword-driven, offline, zero LLM — exactly the historical pipeline) or
|
|
154
|
+
// "llm" (SDK-transport structured extraction + LLM-written digests). The LLM
|
|
155
|
+
// is addressed by opencode provider ID + model ID; opencode owns routing,
|
|
156
|
+
// auth, and base URLs, so the plugin never sees an API key or baseUrl.
|
|
157
|
+
// Defaults point at the user's chosen summarization model.
|
|
158
|
+
function resolveCaptureConfig(raw, env) {
|
|
159
|
+
const captureRaw = (raw.capture ?? {});
|
|
160
|
+
const llmRaw = (captureRaw.llm ?? {});
|
|
161
|
+
const modeRaw = firstString(env.OPENCODE_MEMORY_PRO_CAPTURE_MODE, captureRaw.mode) ?? "heuristics";
|
|
162
|
+
const mode = modeRaw === "llm" ? "llm" : "heuristics";
|
|
163
|
+
return {
|
|
164
|
+
mode,
|
|
165
|
+
llm: {
|
|
166
|
+
provider: firstString(env.OPENCODE_MEMORY_PRO_CAPTURE_LLM_PROVIDER, llmRaw.provider) ?? "openrouter",
|
|
167
|
+
model: firstString(env.OPENCODE_MEMORY_PRO_CAPTURE_LLM_MODEL, llmRaw.model) ?? "z-ai/glm-5.3-flash",
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
// GRAPH_STORE_PHASE1 marker: resolves the offline entity graph settings
|
|
172
|
+
function resolveGraphConfig(raw, env) {
|
|
173
|
+
const graphRaw = (raw.graph ?? {});
|
|
174
|
+
const enabled = toBoolean(env.OPENCODE_MEMORY_PRO_GRAPH_ENABLED ?? graphRaw.enabled, true);
|
|
175
|
+
const boostLambda = clamp(toNumber(env.OPENCODE_MEMORY_PRO_GRAPH_BOOST_LAMBDA ?? graphRaw.boostLambda, 0.3), 0, 1);
|
|
176
|
+
const dbPath = expandHomePath(firstString(env.OPENCODE_MEMORY_PRO_GRAPH_DB_PATH, graphRaw.dbPath) ?? "~/.opencode/memory/graph.db");
|
|
177
|
+
const maxEntitiesPerMemory = Math.max(5, Math.floor(toNumber(graphRaw.maxEntitiesPerMemory, 20)));
|
|
178
|
+
const maxEdgeProvenance = Math.max(5, Math.floor(toNumber(graphRaw.maxEdgeProvenance, 20)));
|
|
179
|
+
// GRAPH_STORE_PHASE2: typed-relation extraction ("X uses Y", "X depends on Z", ...)
|
|
180
|
+
// on top of the co-occurrence graph. On by default; env override available.
|
|
181
|
+
const typedEdges = toBoolean(env.OPENCODE_MEMORY_PRO_GRAPH_TYPED_EDGES ?? graphRaw.typedEdges, true);
|
|
182
|
+
// GRAPH_STORE_PHASE2B: graph-expansion recall (BFS from query entities).
|
|
183
|
+
const expansionEnabled = toBoolean(env.OPENCODE_MEMORY_PRO_GRAPH_EXPANSION_ENABLED ?? graphRaw.expansionEnabled, true);
|
|
184
|
+
const maxHops = Math.min(4, Math.max(1, Math.floor(toNumber(env.OPENCODE_MEMORY_PRO_GRAPH_MAX_HOPS ?? graphRaw.maxHops, 2))));
|
|
185
|
+
const expansionLimit = Math.max(1, Math.floor(toNumber(env.OPENCODE_MEMORY_PRO_GRAPH_EXPANSION_LIMIT ?? graphRaw.expansionLimit, 5)));
|
|
186
|
+
const expansionLambda = clamp(toNumber(env.OPENCODE_MEMORY_PRO_GRAPH_EXPANSION_LAMBDA ?? graphRaw.expansionLambda, 0.3), 0, 1);
|
|
187
|
+
return { enabled, dbPath, boostLambda, maxEntitiesPerMemory, maxEdgeProvenance, typedEdges, expansionEnabled, maxHops, expansionLimit, expansionLambda };
|
|
188
|
+
}
|
|
189
|
+
// MEMORY_RETENTION (1.0): memory-level digest-then-hide expiry, layered on top
|
|
190
|
+
// of the events-table TTL. A memory is expired when it is old enough
|
|
191
|
+
// (minAgeDays) AND unused for unusedDays (lastRecalled, or timestamp if never
|
|
192
|
+
// recalled), then folded into an extractive digest and marked status:"digested"
|
|
193
|
+
// (hidden from recall, never deleted). minGroupSize = smallest per-category
|
|
194
|
+
// group that earns a digest; targetChars = digest length; minImportance =
|
|
195
|
+
// importance floor (protects high-value rows); protectedCategories = never
|
|
196
|
+
// expired (default: digests themselves).
|
|
197
|
+
function resolveRetentionConfig(raw, env) {
|
|
198
|
+
const rawRetention = raw.retention ?? {};
|
|
199
|
+
let eventsDays = Math.floor(toNumber(env.OPENCODE_MEMORY_PRO_RETENTION_EVENTS_DAYS ?? rawRetention.effectivenessEventsDays, 90));
|
|
200
|
+
if (eventsDays < 0) {
|
|
201
|
+
log("warn", `[config] retention.effectivenessEventsDays cannot be negative (${eventsDays}), using 90`);
|
|
202
|
+
eventsDays = 90;
|
|
203
|
+
}
|
|
204
|
+
const memoryRaw = rawRetention.memory ?? {};
|
|
205
|
+
const protectedRaw = memoryRaw.protectedCategories;
|
|
206
|
+
const memory = {
|
|
207
|
+
enabled: toBoolean(env.OPENCODE_MEMORY_PRO_RETENTION_MEMORY_ENABLED ?? memoryRaw.enabled, true),
|
|
208
|
+
unusedDays: Math.min(3650, Math.max(30, Math.floor(toNumber(env.OPENCODE_MEMORY_PRO_RETENTION_MEMORY_UNUSED_DAYS ?? memoryRaw.unusedDays, 60)))),
|
|
209
|
+
minAgeDays: Math.min(3650, Math.max(30, Math.floor(toNumber(env.OPENCODE_MEMORY_PRO_RETENTION_MEMORY_MIN_AGE_DAYS ?? memoryRaw.minAgeDays, 180)))),
|
|
210
|
+
minGroupSize: Math.min(100, Math.max(1, Math.floor(toNumber(env.OPENCODE_MEMORY_PRO_RETENTION_MEMORY_MIN_GROUP_SIZE ?? memoryRaw.minGroupSize, 2)))),
|
|
211
|
+
targetChars: Math.min(2000, Math.max(100, Math.floor(toNumber(env.OPENCODE_MEMORY_PRO_RETENTION_MEMORY_TARGET_CHARS ?? memoryRaw.targetChars, 500)))),
|
|
212
|
+
minImportance: clamp(toNumber(env.OPENCODE_MEMORY_PRO_RETENTION_MEMORY_MIN_IMPORTANCE ?? memoryRaw.minImportance, 0.3), 0, 1),
|
|
213
|
+
protectedCategories: Array.isArray(protectedRaw) && protectedRaw.length > 0
|
|
214
|
+
? protectedRaw.filter((c) => typeof c === "string")
|
|
215
|
+
: ["digest"],
|
|
216
|
+
};
|
|
217
|
+
return { effectivenessEventsDays: eventsDays, memory };
|
|
218
|
+
}
|
|
219
|
+
// MEMORY_LIFECYCLE_TOOLS: defaults for the offline digest summarizer (0.9).
|
|
220
|
+
// minAgeDays = how old a memory must be before it is digest-eligible;
|
|
221
|
+
// minGroupSize = smallest group that earns a digest; targetChars = digest
|
|
222
|
+
// length; replace = whether originals are marked "digested" (hidden from
|
|
223
|
+
// recall) once absorbed.
|
|
224
|
+
function resolveSummarizeConfig(raw, env) {
|
|
225
|
+
const summarizeRaw = (raw.summarize ?? {});
|
|
226
|
+
return {
|
|
227
|
+
enabled: toBoolean(env.OPENCODE_MEMORY_PRO_SUMMARIZE_ENABLED ?? summarizeRaw.enabled, true),
|
|
228
|
+
minAgeDays: Math.min(3650, Math.max(7, Math.floor(toNumber(env.OPENCODE_MEMORY_PRO_SUMMARIZE_MIN_AGE_DAYS ?? summarizeRaw.minAgeDays, 30)))),
|
|
229
|
+
minGroupSize: Math.min(100, Math.max(2, Math.floor(toNumber(env.OPENCODE_MEMORY_PRO_SUMMARIZE_MIN_GROUP_SIZE ?? summarizeRaw.minGroupSize, 3)))),
|
|
230
|
+
targetChars: Math.min(2000, Math.max(100, Math.floor(toNumber(env.OPENCODE_MEMORY_PRO_SUMMARIZE_TARGET_CHARS ?? summarizeRaw.targetChars, 500)))),
|
|
231
|
+
replace: toBoolean(env.OPENCODE_MEMORY_PRO_SUMMARIZE_REPLACE ?? summarizeRaw.replace, false),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
function resolveInjectionConfig(raw, env) {
|
|
235
|
+
const injectionRaw = (raw.injection ?? {});
|
|
236
|
+
const codeSummarizationRaw = (injectionRaw.codeSummarization ?? {});
|
|
237
|
+
return {
|
|
238
|
+
mode: resolveInjectionMode(env.OPENCODE_MEMORY_PRO_INJECTION_MODE ?? injectionRaw.mode),
|
|
239
|
+
maxMemories: Math.max(1, Math.floor(toNumber(env.OPENCODE_MEMORY_PRO_INJECTION_MAX_MEMORIES ?? injectionRaw.maxMemories, 3))),
|
|
240
|
+
minMemories: Math.max(1, Math.floor(toNumber(env.OPENCODE_MEMORY_PRO_INJECTION_MIN_MEMORIES ?? injectionRaw.minMemories, 1))),
|
|
241
|
+
budgetTokens: Math.max(256, Math.floor(toNumber(env.OPENCODE_MEMORY_PRO_INJECTION_BUDGET_TOKENS ?? injectionRaw.budgetTokens, 4096))),
|
|
242
|
+
maxCharsPerMemory: Math.max(100, Math.floor(toNumber(env.OPENCODE_MEMORY_PRO_INJECTION_MAX_CHARS ?? injectionRaw.maxCharsPerMemory, 1200))),
|
|
243
|
+
summarization: resolveSummarizationMode(env.OPENCODE_MEMORY_PRO_INJECTION_SUMMARIZATION ?? injectionRaw.summarization),
|
|
244
|
+
summaryTargetChars: Math.max(50, Math.floor(toNumber(env.OPENCODE_MEMORY_PRO_INJECTION_SUMMARY_TARGET_CHARS ?? injectionRaw.summaryTargetChars, 300))),
|
|
245
|
+
scoreDropTolerance: clamp(toNumber(env.OPENCODE_MEMORY_PRO_INJECTION_SCORE_DROP_TOLERANCE ?? injectionRaw.scoreDropTolerance, 0.15), 0, 1),
|
|
246
|
+
injectionFloor: clamp(toNumber(env.OPENCODE_MEMORY_PRO_INJECTION_FLOOR ?? injectionRaw.injectionFloor, 0.2), 0, 1),
|
|
247
|
+
codeSummarization: {
|
|
248
|
+
enabled: toBoolean(env.OPENCODE_MEMORY_PRO_CODE_SUMMARIZATION_ENABLED ?? codeSummarizationRaw.enabled, true),
|
|
249
|
+
pureCodeThreshold: Math.max(100, Math.floor(toNumber(codeSummarizationRaw.pureCodeThreshold, 500))),
|
|
250
|
+
maxCodeLines: Math.max(5, Math.floor(toNumber(codeSummarizationRaw.maxCodeLines, 15))),
|
|
251
|
+
codeTruncationMode: resolveCodeTruncationMode(codeSummarizationRaw.codeTruncationMode),
|
|
252
|
+
preserveComments: toBoolean(codeSummarizationRaw.preserveComments, true),
|
|
253
|
+
preserveImports: toBoolean(codeSummarizationRaw.preserveImports, false),
|
|
254
|
+
},
|
|
255
|
+
taskTypeProfiles: {
|
|
256
|
+
coding: {
|
|
257
|
+
maxMemories: Math.max(1, Math.floor(toNumber(env.OPENCODE_MEMORY_PRO_INJECTION_CODING_MAX_MEMORIES, 4))),
|
|
258
|
+
budgetTokens: Math.max(256, Math.floor(toNumber(env.OPENCODE_MEMORY_PRO_INJECTION_CODING_BUDGET_TOKENS, 5120))),
|
|
259
|
+
summaryTargetChars: Math.max(50, Math.floor(toNumber(env.OPENCODE_MEMORY_PRO_INJECTION_CODING_SUMMARY_CHARS, 400))),
|
|
260
|
+
categoryWeights: { decision: 1.5, entity: 1.2, fact: 1.0, preference: 0.8, other: 0.5 },
|
|
261
|
+
},
|
|
262
|
+
documentation: {
|
|
263
|
+
maxMemories: Math.max(1, Math.floor(toNumber(env.OPENCODE_MEMORY_PRO_INJECTION_DOCS_MAX_MEMORIES, 3))),
|
|
264
|
+
budgetTokens: Math.max(256, Math.floor(toNumber(env.OPENCODE_MEMORY_PRO_INJECTION_DOCS_BUDGET_TOKENS, 3072))),
|
|
265
|
+
summaryTargetChars: Math.max(50, Math.floor(toNumber(env.OPENCODE_MEMORY_PRO_INJECTION_DOCS_SUMMARY_CHARS, 500))),
|
|
266
|
+
categoryWeights: { decision: 1.4, fact: 1.3, entity: 1.2, preference: 0.8, other: 0.5 },
|
|
267
|
+
},
|
|
268
|
+
review: {
|
|
269
|
+
maxMemories: Math.max(1, Math.floor(toNumber(env.OPENCODE_MEMORY_PRO_INJECTION_REVIEW_MAX_MEMORIES, 3))),
|
|
270
|
+
budgetTokens: Math.max(256, Math.floor(toNumber(env.OPENCODE_MEMORY_PRO_INJECTION_REVIEW_BUDGET_TOKENS, 4096))),
|
|
271
|
+
summaryTargetChars: Math.max(50, Math.floor(toNumber(env.OPENCODE_MEMORY_PRO_INJECTION_REVIEW_SUMMARY_CHARS, 300))),
|
|
272
|
+
categoryWeights: { preference: 1.4, decision: 1.2, entity: 1.0, fact: 0.9, other: 0.5 },
|
|
273
|
+
},
|
|
274
|
+
release: {
|
|
275
|
+
maxMemories: Math.max(1, Math.floor(toNumber(env.OPENCODE_MEMORY_PRO_INJECTION_RELEASE_MAX_MEMORIES, 4))),
|
|
276
|
+
budgetTokens: Math.max(256, Math.floor(toNumber(env.OPENCODE_MEMORY_PRO_INJECTION_RELEASE_BUDGET_TOKENS, 6144))),
|
|
277
|
+
summaryTargetChars: Math.max(50, Math.floor(toNumber(env.OPENCODE_MEMORY_PRO_INJECTION_RELEASE_SUMMARY_CHARS, 350))),
|
|
278
|
+
categoryWeights: { decision: 1.5, entity: 1.3, fact: 1.2, preference: 0.8, other: 0.5 },
|
|
279
|
+
},
|
|
280
|
+
general: {
|
|
281
|
+
maxMemories: Math.max(1, Math.floor(toNumber(env.OPENCODE_MEMORY_PRO_INJECTION_GENERAL_MAX_MEMORIES, 3))),
|
|
282
|
+
budgetTokens: Math.max(256, Math.floor(toNumber(env.OPENCODE_MEMORY_PRO_INJECTION_GENERAL_BUDGET_TOKENS, 4096))),
|
|
283
|
+
summaryTargetChars: Math.max(50, Math.floor(toNumber(env.OPENCODE_MEMORY_PRO_INJECTION_GENERAL_SUMMARY_CHARS, 300))),
|
|
284
|
+
categoryWeights: { decision: 1.3, fact: 1.0, entity: 1.0, preference: 0.9, other: 0.5 },
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function validateEmbeddingConfig(embedding) {
|
|
290
|
+
if (embedding.provider !== "openai")
|
|
291
|
+
return;
|
|
292
|
+
if (!embedding.apiKey) {
|
|
293
|
+
throw new Error("[opencode-memory-pro] OpenAI embedding provider requires apiKey. Set embedding.apiKey or OPENCODE_MEMORY_PRO_OPENAI_API_KEY.");
|
|
294
|
+
}
|
|
295
|
+
if (!embedding.model) {
|
|
296
|
+
throw new Error("[opencode-memory-pro] OpenAI embedding provider requires model. Set embedding.model or OPENCODE_MEMORY_PRO_OPENAI_MODEL.");
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function loadSidecarConfig(worktree) {
|
|
300
|
+
if (process.env.OPENCODE_MEMORY_PRO_SKIP_SIDECAR === "true") {
|
|
301
|
+
return {};
|
|
302
|
+
}
|
|
303
|
+
const configPath = firstString(process.env.OPENCODE_MEMORY_PRO_CONFIG_PATH);
|
|
304
|
+
const candidates = [
|
|
305
|
+
join(expandHomePath("~/.opencode"), SIDECAR_FILE),
|
|
306
|
+
join(expandHomePath("~/.config/opencode"), SIDECAR_FILE),
|
|
307
|
+
worktree ? join(worktree, ".opencode", SIDECAR_FILE) : undefined,
|
|
308
|
+
configPath,
|
|
309
|
+
];
|
|
310
|
+
let merged = {};
|
|
311
|
+
for (const candidate of candidates) {
|
|
312
|
+
if (!candidate)
|
|
313
|
+
continue;
|
|
314
|
+
const parsed = readConfigFile(candidate);
|
|
315
|
+
if (parsed) {
|
|
316
|
+
merged = mergeMemoryConfig(merged, parsed);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return merged;
|
|
320
|
+
}
|
|
321
|
+
function readConfigFile(filePath) {
|
|
322
|
+
const expanded = expandHomePath(filePath);
|
|
323
|
+
if (!existsSync(expanded))
|
|
324
|
+
return null;
|
|
325
|
+
try {
|
|
326
|
+
return parseJsonObject(readFileSync(expanded, "utf8"), {});
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
export function mergeMemoryConfig(base, override) {
|
|
333
|
+
return {
|
|
334
|
+
...base,
|
|
335
|
+
...override,
|
|
336
|
+
embedding: {
|
|
337
|
+
...(base.embedding ?? {}),
|
|
338
|
+
...(override.embedding ?? {}),
|
|
339
|
+
},
|
|
340
|
+
retrieval: {
|
|
341
|
+
...(base.retrieval ?? {}),
|
|
342
|
+
...(override.retrieval ?? {}),
|
|
343
|
+
},
|
|
344
|
+
injection: {
|
|
345
|
+
...(base.injection ?? {}),
|
|
346
|
+
...(override.injection ?? {}),
|
|
347
|
+
codeSummarization: {
|
|
348
|
+
...((base.injection ?? {}).codeSummarization ?? {}),
|
|
349
|
+
...((override.injection ?? {}).codeSummarization ?? {}),
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
dedup: {
|
|
353
|
+
...(base.dedup ?? {}),
|
|
354
|
+
...(override.dedup ?? {}),
|
|
355
|
+
},
|
|
356
|
+
graph: {
|
|
357
|
+
...(base.graph ?? {}),
|
|
358
|
+
...(override.graph ?? {}),
|
|
359
|
+
},
|
|
360
|
+
capture: {
|
|
361
|
+
...(base.capture ?? {}),
|
|
362
|
+
...(override.capture ?? {}),
|
|
363
|
+
llm: {
|
|
364
|
+
...((base.capture ?? {}).llm ?? {}),
|
|
365
|
+
...((override.capture ?? {}).llm ?? {}),
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
// CONFIG_MERGE_FIX (1.2.1): retention/summarize/logging were replaced
|
|
369
|
+
// wholesale by a sidecar fragment — a sidecar that only sets e.g.
|
|
370
|
+
// retention.memory silently dropped every other legacy retention key
|
|
371
|
+
// (effectivenessEventsDays, protectedCategories, ...). Matches the
|
|
372
|
+
// deep-merge pattern used for embedding/retrieval/injection/dedup.
|
|
373
|
+
retention: {
|
|
374
|
+
...(base.retention ?? {}),
|
|
375
|
+
...(override.retention ?? {}),
|
|
376
|
+
memory: {
|
|
377
|
+
...((base.retention ?? {}).memory ?? {}),
|
|
378
|
+
...((override.retention ?? {}).memory ?? {}),
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
summarize: {
|
|
382
|
+
...(base.summarize ?? {}),
|
|
383
|
+
...(override.summarize ?? {}),
|
|
384
|
+
},
|
|
385
|
+
logging: {
|
|
386
|
+
...(base.logging ?? {}),
|
|
387
|
+
...(override.logging ?? {}),
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
function firstString(...values) {
|
|
392
|
+
for (const value of values) {
|
|
393
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
394
|
+
return value.trim();
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return undefined;
|
|
398
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { EmbedderHealth, EmbeddingConfig } from "./types.js";
|
|
2
|
+
export interface Embedder {
|
|
3
|
+
readonly model: string;
|
|
4
|
+
embed(text: string): Promise<number[]>;
|
|
5
|
+
dim(): Promise<number>;
|
|
6
|
+
}
|
|
7
|
+
export declare function getEmbedderHealth(): EmbedderHealth;
|
|
8
|
+
export declare function setEmbedderHealth(health: Partial<EmbedderHealth>): void;
|
|
9
|
+
export declare function resetEmbedderHealth(): void;
|
|
10
|
+
export declare class OllamaEmbedder implements Embedder {
|
|
11
|
+
private readonly config;
|
|
12
|
+
readonly model: string;
|
|
13
|
+
private cachedDim;
|
|
14
|
+
constructor(config: EmbeddingConfig);
|
|
15
|
+
embed(text: string): Promise<number[]>;
|
|
16
|
+
dim(): Promise<number>;
|
|
17
|
+
}
|
|
18
|
+
export declare class OpenAIEmbedder implements Embedder {
|
|
19
|
+
private readonly config;
|
|
20
|
+
readonly model: string;
|
|
21
|
+
private cachedDim;
|
|
22
|
+
constructor(config: EmbeddingConfig);
|
|
23
|
+
embed(text: string): Promise<number[]>;
|
|
24
|
+
dim(): Promise<number>;
|
|
25
|
+
}
|
|
26
|
+
export declare function createEmbedder(config: EmbeddingConfig): Embedder;
|