om-memory-system 5.3.0 → 5.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +4 -0
  2. package/dist/adapters/claude-code/hook-client.d.ts +1 -1
  3. package/dist/adapters/claude-code/hook-client.js +15 -2
  4. package/dist/adapters/opencode/tui-recall.js +13 -3
  5. package/dist/adapters/pi/extension.d.ts +1 -0
  6. package/dist/adapters/pi/extension.js +28 -11
  7. package/dist/adapters/pi/recall-rows.d.ts +31 -0
  8. package/dist/adapters/pi/recall-rows.js +148 -0
  9. package/dist/adapters/pi/session-context.d.ts +22 -0
  10. package/dist/adapters/pi/session-context.js +125 -0
  11. package/dist/config-template.d.ts +1 -1
  12. package/dist/config-template.js +18 -3
  13. package/dist/config.d.ts +6 -0
  14. package/dist/config.js +5 -4
  15. package/dist/core/context-budget.d.ts +2 -0
  16. package/dist/core/context-budget.js +2 -1
  17. package/dist/core/opening-context.d.ts +45 -0
  18. package/dist/core/opening-context.js +60 -0
  19. package/dist/core/recall-summary.d.ts +11 -7
  20. package/dist/core/recall-summary.js +14 -8
  21. package/dist/core/retrieval.d.ts +16 -0
  22. package/dist/core/retrieval.js +33 -11
  23. package/dist/importer/claude-hook-api.d.ts +5 -4
  24. package/dist/importer/claude-hook-api.js +58 -41
  25. package/dist/importer/claude-hook-context.d.ts +49 -0
  26. package/dist/importer/claude-hook-context.js +264 -0
  27. package/dist/services/ai/local-api-url.d.ts +2 -0
  28. package/dist/services/ai/local-api-url.js +12 -0
  29. package/dist/services/ai/provider-config.d.ts +2 -0
  30. package/dist/services/ai/provider-config.js +3 -1
  31. package/dist/services/ai/providers/google-gemini.js +2 -1
  32. package/dist/services/ai/providers/openai-responses.js +2 -1
  33. package/dist/services/chat/chat-runtime.js +5 -1
  34. package/dist/services/chat/chat-status.js +4 -1
  35. package/dist/services/global-config-writer.js +39 -14
  36. package/dist/services/injection-options-config.d.ts +53 -0
  37. package/dist/services/injection-options-config.js +56 -0
  38. package/dist/services/settings-snapshot.js +44 -12
  39. package/dist/services/visible-recall-config.d.ts +7 -2
  40. package/dist/services/visible-recall-config.js +2 -1
  41. package/dist/utils/memory-limits.d.ts +2 -1
  42. package/dist/utils/memory-limits.js +6 -3
  43. package/dist/v2/adapter.d.ts +2 -0
  44. package/dist/v2/adapter.js +107 -33
  45. package/dist/v2/memory-bridge.d.ts +13 -0
  46. package/dist/v2/memory-bridge.js +17 -0
  47. package/dist/v2/recall-reporter.d.ts +6 -3
  48. package/dist/v2/recall-reporter.js +4 -27
  49. package/dist/v2/session-parent.d.ts +14 -0
  50. package/dist/v2/session-parent.js +26 -0
  51. package/dist/web/assets/index-7p-aYnWZ.css +2 -0
  52. package/dist/web/assets/{index-CJ_K-hMG.js → index-CaFFlcl1.js} +17 -17
  53. package/dist/web/index.html +2 -2
  54. package/package.json +1 -1
  55. package/dist/web/assets/index-ccH97VE2.css +0 -2
package/README.md CHANGED
@@ -82,6 +82,10 @@ Each host starts the shared web app when needed, unless it is disabled. Browse
82
82
  captured notes on **Project memories**, or ask your agent to search memory for
83
83
  an earlier decision.
84
84
 
85
+ Each host adds only the memories that match your prompt. An optional setting
86
+ also adds matching memories to the first message of a new session. See
87
+ [Host differences](docs/using-memory.md#host-differences).
88
+
85
89
  ### Terminal command (optional)
86
90
 
87
91
  A global install puts `om-memory-system` on your `PATH`:
@@ -1,5 +1,5 @@
1
1
  import type { EnsureDeps } from "../../services/web-ensure.js";
2
- export declare const CLAUDE_HOOK_EVENTS: readonly ["session-start", "user-prompt-submit", "stop"];
2
+ export declare const CLAUDE_HOOK_EVENTS: readonly ["session-start", "user-prompt-submit", "subagent-start", "stop"];
3
3
  export type ClaudeHookEvent = (typeof CLAUDE_HOOK_EVENTS)[number];
4
4
  /** Claude Code caps added context at 10,000 characters; keep a margin. */
5
5
  export declare const CLAUDE_CONTEXT_LIMIT = 9500;
@@ -3,7 +3,12 @@ import { AUTH_HEADER } from "../../services/auth-token.js";
3
3
  // Claude Code hook client: read the hook input, find or start the OMMS web
4
4
  // app, send one request, print the added context, and log one metadata line.
5
5
  // It loads no store, embedding model, or engine; the web app does that work.
6
- export const CLAUDE_HOOK_EVENTS = ["session-start", "user-prompt-submit", "stop"];
6
+ export const CLAUDE_HOOK_EVENTS = [
7
+ "session-start",
8
+ "user-prompt-submit",
9
+ "subagent-start",
10
+ "stop",
11
+ ];
7
12
  /** Claude Code caps added context at 10,000 characters; keep a margin. */
8
13
  export const CLAUDE_CONTEXT_LIMIT = 9_500;
9
14
  /**
@@ -16,11 +21,13 @@ const MAX_INPUT_BYTES = 1024 * 1024;
16
21
  const BUDGETS = {
17
22
  "session-start": { startMs: 15_000, requestMs: 3_000 },
18
23
  "user-prompt-submit": { startMs: 5_000, requestMs: 3_000 },
24
+ "subagent-start": { startMs: 5_000, requestMs: 3_000 },
19
25
  stop: { startMs: 30_000, requestMs: 5_000 },
20
26
  };
21
27
  const HOOK_EVENT_NAMES = {
22
28
  "session-start": "SessionStart",
23
29
  "user-prompt-submit": "UserPromptSubmit",
30
+ "subagent-start": "SubagentStart",
24
31
  stop: "Stop",
25
32
  };
26
33
  /** Read stdin up to a time and size limit. Late, oversized, or broken input is no input. */
@@ -83,6 +90,12 @@ function parseInput(text, event) {
83
90
  return null;
84
91
  return input;
85
92
  }
93
+ /** The one input field each retrieval event adds to the request, beside the common ones. */
94
+ const RETRIEVE_FIELD = {
95
+ "session-start": "source",
96
+ "user-prompt-submit": "prompt",
97
+ "subagent-start": "agent_type",
98
+ };
86
99
  function requestFor(event, input) {
87
100
  const optional = (key) => (input[key] === undefined ? {} : { [key]: input[key] });
88
101
  if (event === "stop")
@@ -103,7 +116,7 @@ function requestFor(event, input) {
103
116
  session_id: input.session_id,
104
117
  cwd: input.cwd,
105
118
  ...optional("transcript_path"),
106
- ...optional(event === "session-start" ? "source" : "prompt"),
119
+ ...optional(RETRIEVE_FIELD[event]),
107
120
  },
108
121
  };
109
122
  }
@@ -11,6 +11,7 @@ import { recallRpc } from "../../v2/recall-rpc.js";
11
11
  export const MAX_ROWS = 5;
12
12
  const FOLD_STORE_KEY = "omms.recall.fold";
13
13
  const SLOT = "session.composer.top";
14
+ const APP_SLOT = "app";
14
15
  const DEFAULT_DEBOUNCE_MS = 150;
15
16
  /**
16
17
  * Host events that pull the recall list. The server records a recall when a model step starts (the
@@ -32,10 +33,11 @@ const PULL_EVENT_TYPES = new Set([
32
33
  "session.step.failed",
33
34
  "session.execution.failed",
34
35
  ]);
36
+ const SUMMARY_MODES = new Set(["search", "recent", "opening", "restore"]);
35
37
  function isSummary(value) {
36
38
  const v = value;
37
39
  return (!!v &&
38
- (v.mode === "search" || v.mode === "recent") &&
40
+ SUMMARY_MODES.has(v.mode) &&
39
41
  Array.isArray(v.items) &&
40
42
  v.items.every((item) => !!item && typeof item.headline === "string"));
41
43
  }
@@ -196,7 +198,8 @@ export function startTuiRecall(deps) {
196
198
  repaintFold();
197
199
  };
198
200
  // The host owns a keymap layer through the component that created it and removes it when that
199
- // component is disposed. A slot remount therefore needs a fresh layer, so this runs once per render.
201
+ // component is disposed. The row slot unmounts on screen changes, so the layer lives in the
202
+ // "app" slot instead, which stays mounted for the whole TUI.
200
203
  const registerCommand = () => {
201
204
  layerHandle = guard(() => deps.keymap.layer(() => ({
202
205
  mode: "global",
@@ -261,7 +264,6 @@ export function startTuiRecall(deps) {
261
264
  return id;
262
265
  };
263
266
  follow();
264
- registerCommand();
265
267
  return deps.jsx("box", {
266
268
  flexDirection: "column",
267
269
  get children() {
@@ -276,6 +278,13 @@ export function startTuiRecall(deps) {
276
278
  }
277
279
  };
278
280
  const removeSlot = guard(() => deps.ui.slot({ append: SLOT, render }));
281
+ // Draws nothing. It exists so the command layer is owned by a component that is not disposed
282
+ // when the session view changes.
283
+ const renderCommandOwner = () => {
284
+ registerCommand();
285
+ return null;
286
+ };
287
+ const removeCommandSlot = guard(() => deps.ui.slot({ append: APP_SLOT, render: renderCommandOwner }));
279
288
  const offPush = guard(() => rpc?.events.on("recalled", (event) => {
280
289
  const id = event?.data?.sessionID;
281
290
  if (typeof id === "string" && sessions.has(id))
@@ -297,6 +306,7 @@ export function startTuiRecall(deps) {
297
306
  release(offData);
298
307
  release(offPush);
299
308
  release(removeSlot);
309
+ release(removeCommandSlot);
300
310
  release(layerHandle);
301
311
  };
302
312
  }
@@ -12,6 +12,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
12
  * - `memory` tool -> shared add/search/profile/list/forget/help and
13
13
  * portability operations
14
14
  * - `skills` tool -> shared skill-learning operations (skills-tool.ts)
15
+ * - `/omms-recall` -> fold or unfold the recall rows (recall-rows.ts)
15
16
  *
16
17
  * Pi types are imported type-only: the compiled extension carries no Pi
17
18
  * runtime dependency and uses the host Pi runtime provided at load time.
@@ -12,7 +12,8 @@ import { createPiLiveModels } from "./live-model.js";
12
12
  import { registerPiHistoryImportCommand } from "./import-command.js";
13
13
  import { registerPiSkillsTool } from "./skills-tool.js";
14
14
  import { performPiProfileLearning } from "./profile.js";
15
- import { buildRetrievalSection, retrievalWrapperBytes, wrapRetrievalSection, } from "../../core/retrieval.js";
15
+ import { createPiRecallRows } from "./recall-rows.js";
16
+ import { createPiSessionContext } from "./session-context.js";
16
17
  import { DEFAULT_RETRIEVAL_MAX_TOKENS, tokensToByteCeiling } from "../../core/context-budget.js";
17
18
  import { ommsStatusLabel } from "../../services/status-glyphs.js";
18
19
  const GLOBAL_PLUGIN_WARMUP_KEY = Symbol.for("omms.plugin.warmedup");
@@ -67,6 +68,7 @@ function lastSettledUserPrompt(entries) {
67
68
  * - `memory` tool -> shared add/search/profile/list/forget/help and
68
69
  * portability operations
69
70
  * - `skills` tool -> shared skill-learning operations (skills-tool.ts)
71
+ * - `/omms-recall` -> fold or unfold the recall rows (recall-rows.ts)
70
72
  *
71
73
  * Pi types are imported type-only: the compiled extension carries no Pi
72
74
  * runtime dependency and uses the host Pi runtime provided at load time.
@@ -79,6 +81,8 @@ export default function ommsPiExtension(pi) {
79
81
  let latestCtx = null;
80
82
  let backfillSessionId = null;
81
83
  let statusVersion = 0;
84
+ const recallRows = createPiRecallRows();
85
+ const sessionContext = createPiSessionContext(recallRows);
82
86
  const startStatus = (ctx, status) => {
83
87
  const version = ++statusVersion;
84
88
  if (ctx.hasUI)
@@ -101,7 +105,7 @@ export default function ommsPiExtension(pi) {
101
105
  const level = notification.variant === "success" ? "info" : notification.variant;
102
106
  ctx.ui.notify(`${notification.title}: ${notification.message}`, level);
103
107
  };
104
- pi.on("session_start", async (_event, ctx) => {
108
+ pi.on("session_start", async (event, ctx) => {
105
109
  const status = startStatus(ctx, "warming");
106
110
  try {
107
111
  latestCtx = ctx;
@@ -221,6 +225,10 @@ export default function ommsPiExtension(pi) {
221
225
  else {
222
226
  finishStatus(ctx, status, "connected");
223
227
  }
228
+ if (event.reason === "resume") {
229
+ sessionContext.markSeen(sessionId);
230
+ await sessionContext.loadRestore(ctx);
231
+ }
224
232
  }
225
233
  catch (error) {
226
234
  log("Pi session_start error", { error: String(error) }, "error");
@@ -234,19 +242,13 @@ export default function ommsPiExtension(pi) {
234
242
  refreshConfigIfChanged(ctx.cwd);
235
243
  // The default guards partial CONFIG stubs in tests.
236
244
  const budgetBytes = tokensToByteCeiling(CONFIG.retrievalMaxTokens ?? DEFAULT_RETRIEVAL_MAX_TOKENS);
237
- if (!isConfigured() || !CONFIG.chatMessage.enabled)
245
+ if (!isConfigured() || (!CONFIG.chatMessage.enabled && !CONFIG.compaction?.enabled))
238
246
  return;
239
247
  const status = startStatus(ctx, "recalling");
240
248
  try {
241
- // The wrapper counts against the captured allowance, so the packed
242
- // section reserves its bytes.
243
- const section = await buildRetrievalSection(event.prompt, ctx.cwd, ctx.sessionManager.getSessionId(), { maxBytes: budgetBytes, wrapperBytes: retrievalWrapperBytes() });
249
+ const extra = await sessionContext.recall(ctx, event.prompt, budgetBytes);
244
250
  finishStatus(ctx, status, "connected");
245
- if (!section)
246
- return;
247
- return {
248
- systemPrompt: event.systemPrompt + "\n\n" + wrapRetrievalSection(section),
249
- };
251
+ return extra ? { systemPrompt: event.systemPrompt + extra } : undefined;
250
252
  }
251
253
  catch (error) {
252
254
  log("Pi before_agent_start retrieval error", { error: String(error) }, "error");
@@ -254,6 +256,12 @@ export default function ommsPiExtension(pi) {
254
256
  return undefined;
255
257
  }
256
258
  });
259
+ // OMMS adds memories after compaction; it never replaces Pi's own summary (no
260
+ // session_before_compact handler).
261
+ pi.on("session_compact", async (_event, ctx) => {
262
+ sessionContext.markSeen(ctx.sessionManager.getSessionId());
263
+ await sessionContext.loadRestore(ctx);
264
+ });
257
265
  pi.on("agent_settled", async (_event, ctx) => {
258
266
  if (!isConfigured())
259
267
  return;
@@ -301,6 +309,8 @@ export default function ommsPiExtension(pi) {
301
309
  });
302
310
  pi.on("session_shutdown", async (_event, ctx) => {
303
311
  clearStatus(ctx);
312
+ recallRows.reset(ctx);
313
+ sessionContext.reset();
304
314
  latestCtx = null;
305
315
  captureState = createPiCaptureState();
306
316
  promptsSinceProfileAnalysis = [];
@@ -350,6 +360,13 @@ export default function ommsPiExtension(pi) {
350
360
  };
351
361
  },
352
362
  });
363
+ pi.registerCommand("omms-recall", {
364
+ description: "Unfold or fold the OMMS recall rows",
365
+ handler: async (_args, ctx) => {
366
+ refreshConfigIfChanged(ctx.cwd);
367
+ await recallRows.toggleAll(ctx, CONFIG.visibleRecall !== false);
368
+ },
369
+ });
353
370
  registerPiSkillsTool(pi);
354
371
  registerPiHistoryImportCommand(pi, () => latestCtx);
355
372
  }
@@ -0,0 +1,31 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { type RecallSummary } from "../../core/recall-summary.js";
3
+ /**
4
+ * Fold rows for OMMS recalls above the Pi editor (change add-pi-visible-recall, TDR-062).
5
+ *
6
+ * Rows are drawn only in Pi's interactive terminal (`ctx.mode === "tui"`). RPC, JSON and print
7
+ * runs, subagent children among them, record and draw nothing. Summaries and fold state live in
8
+ * memory only: Pi offers no extension store outside the session file, and a session entry would
9
+ * write memory text into the session file. Logs carry the session id, mode and count only.
10
+ */
11
+ export declare const RECALL_WIDGET_KEY = "omms-recall";
12
+ /** Latest summaries kept per session. */
13
+ export declare const MAX_RECALL_SUMMARIES = 20;
14
+ /** Most rows shown at once. */
15
+ export declare const MAX_RECALL_ROWS = 5;
16
+ /** Longest query kept per summary. An unfolded row shows at most 300 characters of it. */
17
+ export declare const MAX_RECALL_QUERY_CHARS = 300;
18
+ export interface PiRecallRows {
19
+ /**
20
+ * Record one recall and redraw. Does nothing outside the interactive terminal. `query` is the
21
+ * prompt for a `search` row, the configured query for an `opening` row, and ignored for `restore`.
22
+ */
23
+ record(ctx: ExtensionContext, query: string, section: string | null, enabled: boolean, mode?: "search" | "opening" | "restore"): Promise<void>;
24
+ /** `/omms-recall`: unfold every shown row, or fold them all when all are open. */
25
+ toggleAll(ctx: ExtensionContext, enabled: boolean): Promise<void>;
26
+ /** Forget summaries and fold state, and clear the widget. */
27
+ reset(ctx: ExtensionContext): void;
28
+ /** Summaries of the current session, oldest first. */
29
+ summaries(): readonly RecallSummary[];
30
+ }
31
+ export declare function createPiRecallRows(): PiRecallRows;
@@ -0,0 +1,148 @@
1
+ import { buildRecallSummary, callTitle, unfoldLines, } from "../../core/recall-summary.js";
2
+ import { log } from "../../services/logger.js";
3
+ /**
4
+ * Fold rows for OMMS recalls above the Pi editor (change add-pi-visible-recall, TDR-062).
5
+ *
6
+ * Rows are drawn only in Pi's interactive terminal (`ctx.mode === "tui"`). RPC, JSON and print
7
+ * runs, subagent children among them, record and draw nothing. Summaries and fold state live in
8
+ * memory only: Pi offers no extension store outside the session file, and a session entry would
9
+ * write memory text into the session file. Logs carry the session id, mode and count only.
10
+ */
11
+ export const RECALL_WIDGET_KEY = "omms-recall";
12
+ /** Latest summaries kept per session. */
13
+ export const MAX_RECALL_SUMMARIES = 20;
14
+ /** Most rows shown at once. */
15
+ export const MAX_RECALL_ROWS = 5;
16
+ /** Longest query kept per summary. An unfolded row shows at most 300 characters of it. */
17
+ export const MAX_RECALL_QUERY_CHARS = 300;
18
+ /** Cut by code point. Used only when Pi's own width helper cannot load. */
19
+ function plainCut(text, width) {
20
+ const chars = Array.from(text);
21
+ if (chars.length <= width)
22
+ return text;
23
+ if (width <= 3)
24
+ return chars.slice(0, Math.max(0, width)).join("");
25
+ return `${chars.slice(0, width - 3).join("")}...`;
26
+ }
27
+ // Pi resolves its own packages for an extension, so this import works inside Pi without a
28
+ // dependency (TDR-062). It stays dynamic so the extension carries no static Pi runtime import.
29
+ let cutLoad;
30
+ function loadCut() {
31
+ cutLoad ??= import("@earendil-works/pi-tui").then((tui) => (text, width) => tui.truncateToWidth(text, width), () => plainCut);
32
+ return cutLoad;
33
+ }
34
+ function paint(theme, text) {
35
+ try {
36
+ return theme.fg("muted", text);
37
+ }
38
+ catch {
39
+ return text;
40
+ }
41
+ }
42
+ function errorCode(error) {
43
+ return error instanceof Error ? error.name : "unknown";
44
+ }
45
+ export function createPiRecallRows() {
46
+ let sessionID = null;
47
+ let ring = [];
48
+ let lastSeq = 0;
49
+ const open = new Set();
50
+ const forget = () => {
51
+ sessionID = null;
52
+ ring = [];
53
+ lastSeq = 0;
54
+ open.clear();
55
+ };
56
+ const shown = () => ring.slice(-MAX_RECALL_ROWS);
57
+ const rowLines = () => shown().flatMap((summary) => {
58
+ const unfolded = open.has(summary.seq ?? 0);
59
+ const title = `${unfolded ? "▾" : "▸"} ● ${callTitle(summary)}`;
60
+ return unfolded ? [title, ...unfoldLines(summary).map((line) => ` ${line}`)] : [title];
61
+ });
62
+ const setWidget = (ctx, lines, cut) => {
63
+ try {
64
+ if (!lines) {
65
+ ctx.ui.setWidget(RECALL_WIDGET_KEY, undefined);
66
+ return;
67
+ }
68
+ ctx.ui.setWidget(RECALL_WIDGET_KEY, (_tui, theme) => ({
69
+ render(width) {
70
+ try {
71
+ return lines.map((line) => paint(theme, cut(line, width)));
72
+ }
73
+ catch {
74
+ return [];
75
+ }
76
+ },
77
+ invalidate() { },
78
+ }), { placement: "aboveEditor" });
79
+ }
80
+ catch (error) {
81
+ log("Pi recall: could not draw rows", { sessionID, code: errorCode(error) }, "error");
82
+ }
83
+ };
84
+ const draw = async (ctx, enabled) => {
85
+ const cut = await loadCut();
86
+ setWidget(ctx, enabled && ring.length > 0 ? rowLines() : undefined, cut);
87
+ };
88
+ return {
89
+ async record(ctx, query, section, enabled, mode = "search") {
90
+ try {
91
+ if (ctx.mode !== "tui" || !section)
92
+ return;
93
+ const built = buildRecallSummary(section, mode, query.slice(0, MAX_RECALL_QUERY_CHARS));
94
+ if (!built)
95
+ return;
96
+ const id = ctx.sessionManager.getSessionId();
97
+ if (id !== sessionID) {
98
+ forget();
99
+ sessionID = id;
100
+ }
101
+ const summary = { ...built, seq: ++lastSeq };
102
+ ring = [...ring, summary].slice(-MAX_RECALL_SUMMARIES);
103
+ for (const seq of open)
104
+ if (!ring.some((kept) => kept.seq === seq))
105
+ open.delete(seq);
106
+ log("Pi recall: summary recorded", {
107
+ sessionID: id,
108
+ mode: summary.mode,
109
+ count: summary.items.length,
110
+ });
111
+ await draw(ctx, enabled);
112
+ }
113
+ catch (error) {
114
+ log("Pi recall: could not record summary", { sessionID, code: errorCode(error) }, "error");
115
+ }
116
+ },
117
+ async toggleAll(ctx, enabled) {
118
+ try {
119
+ if (ctx.mode !== "tui")
120
+ return;
121
+ const seqs = shown().map((summary) => summary.seq ?? 0);
122
+ const unfold = seqs.some((seq) => !open.has(seq));
123
+ for (const seq of seqs) {
124
+ if (unfold)
125
+ open.add(seq);
126
+ else
127
+ open.delete(seq);
128
+ }
129
+ await draw(ctx, enabled);
130
+ }
131
+ catch (error) {
132
+ log("Pi recall: could not toggle rows", { sessionID, code: errorCode(error) }, "error");
133
+ }
134
+ },
135
+ reset(ctx) {
136
+ const hadRows = ring.length > 0;
137
+ forget();
138
+ try {
139
+ if (hadRows && ctx.mode === "tui")
140
+ ctx.ui.setWidget(RECALL_WIDGET_KEY, undefined);
141
+ }
142
+ catch (error) {
143
+ log("Pi recall: could not clear rows", { code: errorCode(error) }, "error");
144
+ }
145
+ },
146
+ summaries: () => ring,
147
+ };
148
+ }
@@ -0,0 +1,22 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import type { PiRecallRows } from "./recall-rows.js";
3
+ /**
4
+ * What Pi adds to the system prompt of one run beyond the prompt's own recall (change
5
+ * align-recall-host-parity, design D1d, D4, D5): the opening section on the first prompt of a
6
+ * main session, and the session's own memories after a compaction or a resume. In memory only.
7
+ */
8
+ /** Session ids remembered as seen, and restored sessions kept. The oldest is dropped first. */
9
+ export declare const MAX_TRACKED_SESSIONS = 500;
10
+ /** A Pi child started by om-pi-subagents (`--mode rpc --no-session --no-extensions`). */
11
+ export declare function isPiChild(): boolean;
12
+ export interface PiSessionContext {
13
+ /** Note that the session has had a prompt, so a later prompt is not its first. */
14
+ markSeen(sessionId: string): void;
15
+ /** Load the session's own memories for restore, replacing any earlier load. */
16
+ loadRestore(ctx: ExtensionContext): Promise<void>;
17
+ /** Text to append to the system prompt for this run, or null. */
18
+ recall(ctx: ExtensionContext, prompt: string, budgetBytes: number): Promise<string | null>;
19
+ /** Forget every restored session. */
20
+ reset(): void;
21
+ }
22
+ export declare function createPiSessionContext(rows: PiRecallRows): PiSessionContext;
@@ -0,0 +1,125 @@
1
+ import { CONFIG } from "../../config.js";
2
+ import { buildFirstPromptSections, openingQueryFor } from "../../core/opening-context.js";
3
+ import { buildRetrievalSection, formatMemoriesForCompaction, retrievalWrapperBytes, wrapRetrievalSection, } from "../../core/retrieval.js";
4
+ import { memoryClient } from "../../services/client.js";
5
+ import { log } from "../../services/logger.js";
6
+ import { getTags } from "../../services/tags.js";
7
+ import { utf8ByteLength } from "../../utils/context-limit.js";
8
+ /** The blank line before each section. The budget counts it for the second section on. */
9
+ const SECTION_SEPARATOR = "\n\n";
10
+ const SECTION_SEPARATOR_BYTES = 2;
11
+ /**
12
+ * What Pi adds to the system prompt of one run beyond the prompt's own recall (change
13
+ * align-recall-host-parity, design D1d, D4, D5): the opening section on the first prompt of a
14
+ * main session, and the session's own memories after a compaction or a resume. In memory only.
15
+ */
16
+ /** Session ids remembered as seen, and restored sessions kept. The oldest is dropped first. */
17
+ export const MAX_TRACKED_SESSIONS = 500;
18
+ const HOST = "pi";
19
+ /** A Pi child started by om-pi-subagents (`--mode rpc --no-session --no-extensions`). */
20
+ export function isPiChild() {
21
+ return process.env.OMPS_CHILD === "1";
22
+ }
23
+ function hasEarlierUserEntry(branch) {
24
+ return (Array.isArray(branch) &&
25
+ branch.some((entry) => entry?.type === "message" && entry.message?.role === "user"));
26
+ }
27
+ function remember(map, key, value) {
28
+ map.delete(key);
29
+ map.set(key, value);
30
+ if (map.size > MAX_TRACKED_SESSIONS)
31
+ map.delete(map.keys().next().value);
32
+ }
33
+ export function createPiSessionContext(rows) {
34
+ const seen = new Map();
35
+ const restored = new Map();
36
+ const restoreEnabled = () => !isPiChild() && CONFIG.compaction?.enabled === true;
37
+ async function loadRestore(ctx) {
38
+ if (!restoreEnabled())
39
+ return;
40
+ const sessionId = ctx.sessionManager.getSessionId();
41
+ try {
42
+ const result = await memoryClient.searchMemoriesBySessionID(sessionId, getTags(ctx.cwd).project.tag, CONFIG.compaction.memoryLimit);
43
+ const own = result.success
44
+ ? result.results.filter((memory) => memory.metadata?.host === HOST)
45
+ : [];
46
+ if (own.length > 0)
47
+ remember(restored, sessionId, { memories: own, announced: false });
48
+ else
49
+ restored.delete(sessionId);
50
+ }
51
+ catch (error) {
52
+ log("Pi restore load failed", { code: error instanceof Error ? error.name : "unknown" }, "error");
53
+ }
54
+ }
55
+ return {
56
+ markSeen(sessionId) {
57
+ remember(seen, sessionId, true);
58
+ },
59
+ loadRestore,
60
+ async recall(ctx, prompt, budgetBytes) {
61
+ const sessionId = ctx.sessionManager.getSessionId();
62
+ const child = isPiChild();
63
+ const visible = CONFIG.visibleRecall !== false;
64
+ const budget = { maxBytes: budgetBytes, wrapperBytes: retrievalWrapperBytes() };
65
+ // A session this process has not seen: first prompt when its branch holds no user entry,
66
+ // else a resumed session.
67
+ const unseen = !child && !seen.has(sessionId);
68
+ if (unseen)
69
+ remember(seen, sessionId, true);
70
+ const resumed = unseen && hasEarlierUserEntry(ctx.sessionManager.getBranch());
71
+ const openingQuery = unseen && !resumed
72
+ ? openingQueryFor({
73
+ openingContext: CONFIG.openingContext,
74
+ openingContextQuery: CONFIG.openingContextQuery,
75
+ })
76
+ : null;
77
+ let promptSection = null;
78
+ let openingSection = null;
79
+ if (CONFIG.chatMessage.enabled && (!child || CONFIG.subagentMemory)) {
80
+ if (openingQuery) {
81
+ ({ promptSection, openingSection } = await buildFirstPromptSections({
82
+ prompt,
83
+ openingQuery,
84
+ directory: ctx.cwd,
85
+ sessionId,
86
+ budget,
87
+ openingReserveBytes: SECTION_SEPARATOR_BYTES,
88
+ }));
89
+ }
90
+ else {
91
+ const section = await buildRetrievalSection(prompt, ctx.cwd, sessionId, budget);
92
+ promptSection = section ? wrapRetrievalSection(section) : null;
93
+ }
94
+ }
95
+ if (resumed)
96
+ await loadRestore(ctx);
97
+ let restoredSection = null;
98
+ const entry = restoreEnabled() ? restored.get(sessionId) : undefined;
99
+ if (entry) {
100
+ const left = budgetBytes -
101
+ (promptSection ? utf8ByteLength(promptSection) + SECTION_SEPARATOR_BYTES : 0);
102
+ const text = left > budget.wrapperBytes
103
+ ? formatMemoriesForCompaction(entry.memories, {
104
+ maxBytes: left,
105
+ wrapperBytes: budget.wrapperBytes,
106
+ })
107
+ : "";
108
+ restoredSection = text ? wrapRetrievalSection(text) : null;
109
+ }
110
+ // Screen-only rows; the returned text stays the same with them on or off.
111
+ await rows.record(ctx, prompt, promptSection, visible);
112
+ if (openingSection)
113
+ await rows.record(ctx, openingQuery ?? "", openingSection, visible, "opening");
114
+ if (restoredSection && entry && !entry.announced) {
115
+ entry.announced = true;
116
+ await rows.record(ctx, "", restoredSection, visible, "restore");
117
+ }
118
+ const parts = [promptSection, openingSection, restoredSection].filter(Boolean);
119
+ return parts.length > 0 ? SECTION_SEPARATOR + parts.join(SECTION_SEPARATOR) : null;
120
+ },
121
+ reset() {
122
+ restored.clear();
123
+ },
124
+ };
125
+ }