om-memory-system 4.6.0 → 4.8.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.
@@ -1,6 +1,7 @@
1
1
  import { OPENCODE_IMPORT_COMMAND, OPENCODE_IMPORT_DESCRIPTION, runOpencodeImportCommand, } from "../adapters/opencode/import-command.js";
2
2
  import { addPackageSkill } from "../adapters/opencode/package-skills.js";
3
3
  import { log } from "../services/logger.js";
4
+ import { utf8ByteLength } from "../utils/context-limit.js";
4
5
  import { eventBelongsToLocation, legacyToolResult, toLegacyEvent } from "./legacy-client.js";
5
6
  const memoryInput = {
6
7
  type: "object",
@@ -158,6 +159,7 @@ export async function registerV2Adapter(ctx, legacy, memory) {
158
159
  },
159
160
  }));
160
161
  await ctx.session.hook("prompt", async (event) => {
162
+ const budgetBytes = memory.snapshotRequestBudget();
161
163
  const sessionID = event.sessionID;
162
164
  const text = event.prompt.text ?? "";
163
165
  setBounded(messageIDs, sessionID, event.messageID);
@@ -173,26 +175,39 @@ export async function registerV2Adapter(ctx, legacy, memory) {
173
175
  }
174
176
  if (!memory.isInjectionEnabled() || !text.trim())
175
177
  return;
176
- // Start the search now so it overlaps prompt admission; `context` awaits it.
177
- const promise = memory.retrieve(text, sessionID).catch((error) => {
178
+ // Start the search now with the entry snapshot, so it overlaps prompt admission.
179
+ // An edit during retrieval must not change this request's allowance.
180
+ const promise = memory.retrieve(text, sessionID, budgetBytes).catch((error) => {
178
181
  log("v2 prompt: memory retrieval failed", { sessionID, error: String(error) });
179
182
  return null;
180
183
  });
181
- setBounded(retrievals, sessionID, { promise });
184
+ setBounded(retrievals, sessionID, { promise, budgetBytes });
182
185
  });
183
186
  await ctx.session.hook("context", async (event) => {
184
187
  const sessionID = event.sessionID;
188
+ const retrieval = retrievals.get(sessionID);
189
+ const budgetBytes = retrieval?.budgetBytes ?? memory.snapshotRequestBudget();
185
190
  // Before chat.params, which stores the model on the recorded prompt row.
186
191
  await recordAdmittedPrompts(sessionID, event.messages);
187
- const retrieval = retrievals.get(sessionID);
192
+ // The retrieval and the restored section share one request budget: the
193
+ // restored section is formatted here, within what the retrieval left.
194
+ let remainingBytes = null;
188
195
  if (retrieval) {
189
196
  const section = await settledRetrieval(retrieval);
197
+ if (section) {
198
+ event.system.push({ type: "text", text: section });
199
+ remainingBytes = retrieval.budgetBytes - utf8ByteLength(section);
200
+ }
201
+ else {
202
+ remainingBytes = retrieval.budgetBytes;
203
+ }
204
+ }
205
+ const restoredMemories = restored.get(sessionID);
206
+ if (restoredMemories) {
207
+ const section = memory.formatRestoredSession(restoredMemories, remainingBytes ?? budgetBytes);
190
208
  if (section)
191
209
  event.system.push({ type: "text", text: section });
192
210
  }
193
- const restoredSection = restored.get(sessionID);
194
- if (restoredSection)
195
- event.system.push({ type: "text", text: restoredSection });
196
211
  if (legacy["chat.params"]) {
197
212
  await legacy["chat.params"]({
198
213
  message: { id: messageIDs.get(sessionID) ?? sessionID },
@@ -201,13 +216,16 @@ export async function registerV2Adapter(ctx, legacy, memory) {
201
216
  }
202
217
  });
203
218
  await ctx.session.hook("compaction", async (event) => {
219
+ memory.snapshotRequestBudget();
204
220
  if (!memory.isCompactionEnabled())
205
221
  return;
206
222
  const sessionID = event.sessionID;
207
223
  try {
208
- const section = await memory.restoreSession(sessionID);
209
- if (section)
210
- setBounded(restored, sessionID, section);
224
+ // Load once here; the restored section is formatted when a request
225
+ // emits it, against that request's captured budget.
226
+ const memories = await memory.restoreSession(sessionID);
227
+ if (memories)
228
+ setBounded(restored, sessionID, memories);
211
229
  else
212
230
  restored.delete(sessionID);
213
231
  }
@@ -10,9 +10,17 @@ export interface V2MemoryBridge {
10
10
  isCompactionEnabled(): boolean;
11
11
  isInternalPrompt(sessionID: string, text: string): boolean;
12
12
  recordPrompt(sessionID: string, messageID: string, text: string): Promise<void>;
13
- /** Wrapped per-prompt retrieval section, or null when nothing is relevant. */
14
- retrieve(prompt: string, sessionID: string): Promise<string | null>;
15
- /** The session's own memories formatted for restore after compaction, or null. */
16
- restoreSession(sessionID: string): Promise<string | null>;
13
+ /**
14
+ * One request's total byte allowance for memory context, from a fresh
15
+ * config read. Call once before the request's async memory work starts; an
16
+ * operation in flight keeps that captured total even if the config changes.
17
+ */
18
+ snapshotRequestBudget(): number;
19
+ /** Wrapped per-prompt retrieval section packed within `budgetBytes`, or null when nothing is relevant. */
20
+ retrieve(prompt: string, sessionID: string, budgetBytes: number): Promise<string | null>;
21
+ /** The session's own memories for restore after compaction, loaded once, or null. */
22
+ restoreSession(sessionID: string): Promise<unknown[] | null>;
23
+ /** Cached restored memories formatted within `budgetBytes`, or null when nothing fits. */
24
+ formatRestoredSession(memories: unknown[], budgetBytes: number): string | null;
17
25
  }
18
26
  export declare function createV2MemoryBridge(directory: string): V2MemoryBridge;
@@ -1,5 +1,6 @@
1
- import { CONFIG, isConfigured } from "../config.js";
2
- import { buildRetrievalSection, formatMemoriesForCompaction, wrapRetrievalSection, } from "../core/retrieval.js";
1
+ import { CONFIG, isConfigured, refreshConfigIfChanged } from "../config.js";
2
+ import { buildRetrievalSection, formatMemoriesForCompaction, retrievalWrapperBytes, wrapRetrievalSection, } from "../core/retrieval.js";
3
+ import { DEFAULT_RETRIEVAL_MAX_TOKENS, tokensToByteCeiling } from "../core/context-budget.js";
3
4
  import { isInternalPrompt, recordUserPrompt } from "../adapters/opencode/user-prompt.js";
4
5
  import { memoryClient } from "../services/client.js";
5
6
  import { V2_RETRIEVAL_TIMEOUT_MS } from "../services/request-timeouts.js";
@@ -14,8 +15,17 @@ export function createV2MemoryBridge(directory) {
14
15
  async recordPrompt(sessionID, messageID, text) {
15
16
  await recordUserPrompt(sessionID, messageID, directory, text);
16
17
  },
17
- async retrieve(prompt, sessionID) {
18
- const section = await buildRetrievalSection(prompt, directory, sessionID);
18
+ snapshotRequestBudget() {
19
+ refreshConfigIfChanged(directory);
20
+ // The default guards partial CONFIG stubs in tests.
21
+ return tokensToByteCeiling(CONFIG.retrievalMaxTokens ?? DEFAULT_RETRIEVAL_MAX_TOKENS);
22
+ },
23
+ async retrieve(prompt, sessionID, budgetBytes) {
24
+ // The wrapper counts against the request budget, so the packed section reserves it.
25
+ const section = await buildRetrievalSection(prompt, directory, sessionID, {
26
+ maxBytes: budgetBytes,
27
+ wrapperBytes: retrievalWrapperBytes(),
28
+ });
19
29
  return section ? wrapRetrievalSection(section) : null;
20
30
  },
21
31
  async restoreSession(sessionID) {
@@ -23,7 +33,14 @@ export function createV2MemoryBridge(directory) {
23
33
  const result = await memoryClient.searchMemoriesBySessionID(sessionID, tags.project.tag, CONFIG.compaction.memoryLimit);
24
34
  if (!result.success || result.results.length === 0)
25
35
  return null;
26
- return formatMemoriesForCompaction(result.results);
36
+ // Raw results: the request budget is only known when the restored
37
+ // section is emitted, so formatting happens in formatRestoredSession.
38
+ return result.results;
39
+ },
40
+ formatRestoredSession(memories, budgetBytes) {
41
+ if (memories.length === 0)
42
+ return null;
43
+ return formatMemoriesForCompaction(memories, { maxBytes: budgetBytes });
27
44
  },
28
45
  };
29
46
  }