om-memory-system 5.2.0 → 5.4.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.
Files changed (69) 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/in-process-client.d.ts +2 -0
  5. package/dist/adapters/opencode/in-process-client.js +16 -0
  6. package/dist/adapters/opencode/opencode-host-config.d.ts +3 -0
  7. package/dist/adapters/opencode/opencode-host-config.js +2 -0
  8. package/dist/adapters/opencode/tui-recall.d.ts +68 -0
  9. package/dist/adapters/opencode/tui-recall.js +312 -0
  10. package/dist/adapters/pi/extension.d.ts +1 -0
  11. package/dist/adapters/pi/extension.js +28 -11
  12. package/dist/adapters/pi/recall-rows.d.ts +31 -0
  13. package/dist/adapters/pi/recall-rows.js +148 -0
  14. package/dist/adapters/pi/session-context.d.ts +22 -0
  15. package/dist/adapters/pi/session-context.js +125 -0
  16. package/dist/config-template.d.ts +1 -1
  17. package/dist/config-template.js +20 -0
  18. package/dist/config.d.ts +8 -0
  19. package/dist/config.js +8 -4
  20. package/dist/core/context-budget.d.ts +2 -0
  21. package/dist/core/context-budget.js +2 -1
  22. package/dist/core/opening-context.d.ts +45 -0
  23. package/dist/core/opening-context.js +60 -0
  24. package/dist/core/recall-summary.d.ts +32 -0
  25. package/dist/core/recall-summary.js +77 -0
  26. package/dist/core/retrieval.d.ts +16 -0
  27. package/dist/core/retrieval.js +33 -11
  28. package/dist/importer/claude-hook-api.d.ts +5 -4
  29. package/dist/importer/claude-hook-api.js +58 -41
  30. package/dist/importer/claude-hook-context.d.ts +49 -0
  31. package/dist/importer/claude-hook-context.js +264 -0
  32. package/dist/index.js +1 -1
  33. package/dist/services/ai/local-api-url.d.ts +2 -0
  34. package/dist/services/ai/local-api-url.js +12 -0
  35. package/dist/services/ai/provider-config.d.ts +2 -0
  36. package/dist/services/ai/provider-config.js +3 -1
  37. package/dist/services/ai/providers/google-gemini.js +2 -1
  38. package/dist/services/ai/providers/openai-responses.js +2 -1
  39. package/dist/services/chat/chat-runtime.js +5 -1
  40. package/dist/services/chat/chat-status.js +4 -1
  41. package/dist/services/global-config-writer.js +39 -14
  42. package/dist/services/injection-options-config.d.ts +53 -0
  43. package/dist/services/injection-options-config.js +56 -0
  44. package/dist/services/settings-snapshot.js +44 -12
  45. package/dist/services/single-flight.d.ts +2 -0
  46. package/dist/services/single-flight.js +12 -0
  47. package/dist/services/visible-recall-config.d.ts +39 -0
  48. package/dist/services/visible-recall-config.js +28 -0
  49. package/dist/services/web-server.d.ts +3 -0
  50. package/dist/services/web-server.js +26 -8
  51. package/dist/utils/memory-limits.d.ts +2 -1
  52. package/dist/utils/memory-limits.js +6 -3
  53. package/dist/v2/adapter.d.ts +2 -0
  54. package/dist/v2/adapter.js +110 -31
  55. package/dist/v2/legacy-client.js +2 -1
  56. package/dist/v2/memory-bridge.d.ts +15 -0
  57. package/dist/v2/memory-bridge.js +21 -0
  58. package/dist/v2/recall-reporter.d.ts +27 -0
  59. package/dist/v2/recall-reporter.js +105 -0
  60. package/dist/v2/recall-rpc.d.ts +51 -0
  61. package/dist/v2/recall-rpc.js +38 -0
  62. package/dist/v2/session-parent.d.ts +14 -0
  63. package/dist/v2/session-parent.js +26 -0
  64. package/dist/web/assets/{index-CJ_K-hMG.js → index-BhQR88tU.js} +16 -16
  65. package/dist/web/assets/index-BmFUZbGr.css +2 -0
  66. package/dist/web/index.html +2 -2
  67. package/opencode/tui.tsx +22 -3
  68. package/package.json +1 -1
  69. 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
  }
@@ -0,0 +1,2 @@
1
+ export declare function markInProcessClient<T extends object>(client: T): T;
2
+ export declare function isInProcessClient(client: unknown): boolean;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Marks a host client that talks to OpenCode in-process. The V2 legacy client
3
+ * is one: it calls the plugin context directly, so it has no HTTP transport
4
+ * and no fetch to borrow. The transport setup skips its "no host fetch"
5
+ * warning for it.
6
+ */
7
+ const IN_PROCESS_CLIENT = Symbol.for("omms.opencode.inProcessClient");
8
+ export function markInProcessClient(client) {
9
+ Object.defineProperty(client, IN_PROCESS_CLIENT, { value: true });
10
+ return client;
11
+ }
12
+ export function isInProcessClient(client) {
13
+ return (typeof client === "object" &&
14
+ client !== null &&
15
+ client[IN_PROCESS_CLIENT] === true);
16
+ }
@@ -2,6 +2,9 @@ export type HostClientConfig = {
2
2
  readonly baseUrl: string | undefined;
3
3
  readonly fetch: typeof fetch | undefined;
4
4
  readonly headers?: RequestInit["headers"];
5
+ /** Present when the host client runs in-process (OpenCode v2): no HTTP
6
+ * transport, so a missing fetch is expected and not worth a warning. */
7
+ readonly inProcess?: true;
5
8
  readonly clientKeys: readonly string[];
6
9
  readonly sdkConfigCount: number;
7
10
  };
@@ -1,3 +1,4 @@
1
+ import { isInProcessClient } from "./in-process-client.js";
1
2
  export function getHostClientConfig(ctx) {
2
3
  const client = toRecord(ctx.client);
3
4
  if (!client) {
@@ -11,6 +12,7 @@ export function getHostClientConfig(ctx) {
11
12
  baseUrl: typeof baseUrl === "string" ? baseUrl : undefined,
12
13
  fetch: isFetch(customFetch) ? customFetch : undefined,
13
14
  ...(isHeadersInit(headers) ? { headers } : {}),
15
+ ...(isInProcessClient(client) ? { inProcess: true } : {}),
14
16
  clientKeys: Object.keys(client),
15
17
  sdkConfigCount: configs.length,
16
18
  };
@@ -0,0 +1,68 @@
1
+ import { type RecallSummary } from "../../core/recall-summary.js";
2
+ /**
3
+ * Fold rows for OMMS recalls in the OpenCode terminal TUI (TDR-060). The TUI entry (`opencode/tui.tsx`)
4
+ * passes its host pieces in, so this module never imports a host SDK, storage, capture or config. Every
5
+ * host call sits behind a guard: a missing RPC handler, a failing storage call or a throwing render
6
+ * draws nothing for that part and never throws into the host. Nothing here logs: memory text must not
7
+ * reach a log.
8
+ */
9
+ /** Most rows shown at once. Older summaries of the session are dropped from the view. */
10
+ export declare const MAX_ROWS = 5;
11
+ /** Persisted fold state: only open rows are stored, keyed by `rowKey`. */
12
+ export interface FoldState {
13
+ open: Record<string, boolean>;
14
+ }
15
+ /** The slice of `ctx.client.rpc(recallRpc)` this module uses. */
16
+ export interface RecallRpcClient {
17
+ list(input: {
18
+ sessionID: string;
19
+ }): Promise<{
20
+ enabled: boolean;
21
+ items: RecallSummary[];
22
+ }>;
23
+ events: {
24
+ on(name: string, handler: (event: any) => void): unknown;
25
+ };
26
+ }
27
+ export interface TuiRecallDeps {
28
+ client: {
29
+ rpc(definition: unknown): RecallRpcClient;
30
+ };
31
+ data: {
32
+ listen(handler: (event: any) => void): unknown;
33
+ };
34
+ ui: {
35
+ slot(spec: {
36
+ append: string;
37
+ render: (input: {
38
+ sessionID?: string;
39
+ }) => unknown;
40
+ }): unknown;
41
+ };
42
+ keymap: {
43
+ layer(factory: () => unknown): unknown;
44
+ };
45
+ storage: {
46
+ store(key: string, options: {
47
+ initial: FoldState;
48
+ }): [FoldState, (change: (draft: FoldState) => void) => unknown];
49
+ };
50
+ createSignal: <T>(value: T) => [() => T, (next: T | ((previous: T) => T)) => unknown];
51
+ jsx: (tag: string, props: Record<string, unknown>) => unknown;
52
+ /** Theme colour for row text; read on each paint. */
53
+ muted: () => unknown;
54
+ /** Quiet period that collapses a burst of host events into one `list` call. */
55
+ debounceMs?: number;
56
+ }
57
+ /**
58
+ * Stable per-summary key. A summary with a server sequence number is keyed by session id and that
59
+ * number, so it survives the ring dropping older entries. An older server sends none, so the key
60
+ * falls back to the session id, a hash of mode, query and each memory's id (or headline when it
61
+ * has none), and a counter for identical summaries.
62
+ */
63
+ export declare function rowKeys(sessionID: string, summaries: RecallSummary[]): string[];
64
+ /**
65
+ * Draw one fold row per recall summary of the session in view. Returns a stop function that removes
66
+ * every hook it added.
67
+ */
68
+ export declare function startTuiRecall(deps: TuiRecallDeps): () => void;
@@ -0,0 +1,312 @@
1
+ import { callTitle, unfoldLines } from "../../core/recall-summary.js";
2
+ import { recallRpc } from "../../v2/recall-rpc.js";
3
+ /**
4
+ * Fold rows for OMMS recalls in the OpenCode terminal TUI (TDR-060). The TUI entry (`opencode/tui.tsx`)
5
+ * passes its host pieces in, so this module never imports a host SDK, storage, capture or config. Every
6
+ * host call sits behind a guard: a missing RPC handler, a failing storage call or a throwing render
7
+ * draws nothing for that part and never throws into the host. Nothing here logs: memory text must not
8
+ * reach a log.
9
+ */
10
+ /** Most rows shown at once. Older summaries of the session are dropped from the view. */
11
+ export const MAX_ROWS = 5;
12
+ const FOLD_STORE_KEY = "omms.recall.fold";
13
+ const SLOT = "session.composer.top";
14
+ const APP_SLOT = "app";
15
+ const DEFAULT_DEBOUNCE_MS = 150;
16
+ /**
17
+ * Host events that pull the recall list. The server records a recall when a model step starts (the
18
+ * `context` hook), so only the events around a step, plus session creation and view changes, can
19
+ * coincide with a new recall. The end events catch a missed `recalled` push right after the step.
20
+ * The failure events are end events too: the summary is recorded in the `context` hook after
21
+ * `session.step.started` fires, so if the push is absent and the step then fails, no later event
22
+ * in this set would pull it.
23
+ * Streaming, part, message, inbox and instruction events fire dozens of times per reply and never
24
+ * change the list, so they must stay out of this set.
25
+ */
26
+ const PULL_EVENT_TYPES = new Set([
27
+ "session.created",
28
+ "session.viewed",
29
+ "session.execution.started",
30
+ "session.step.started",
31
+ "session.step.ended",
32
+ "session.execution.succeeded",
33
+ "session.step.failed",
34
+ "session.execution.failed",
35
+ ]);
36
+ const SUMMARY_MODES = new Set(["search", "recent", "opening", "restore"]);
37
+ function isSummary(value) {
38
+ const v = value;
39
+ return (!!v &&
40
+ SUMMARY_MODES.has(v.mode) &&
41
+ Array.isArray(v.items) &&
42
+ v.items.every((item) => !!item && typeof item.headline === "string"));
43
+ }
44
+ /** FNV-1a, 32 bit: short, deterministic, no crypto needed for a fold key. */
45
+ function hash(text) {
46
+ let h = 0x811c9dc5;
47
+ for (let i = 0; i < text.length; i++) {
48
+ h ^= text.charCodeAt(i);
49
+ h = Math.imul(h, 0x01000193);
50
+ }
51
+ return (h >>> 0).toString(16);
52
+ }
53
+ /**
54
+ * Stable per-summary key. A summary with a server sequence number is keyed by session id and that
55
+ * number, so it survives the ring dropping older entries. An older server sends none, so the key
56
+ * falls back to the session id, a hash of mode, query and each memory's id (or headline when it
57
+ * has none), and a counter for identical summaries.
58
+ */
59
+ export function rowKeys(sessionID, summaries) {
60
+ const seen = new Map();
61
+ return summaries.map((summary) => {
62
+ if (typeof summary.seq === "number")
63
+ return `${sessionID}:${summary.seq}`;
64
+ const body = [
65
+ summary.mode,
66
+ summary.query ?? "",
67
+ ...summary.items.map((item) => item.id ?? item.headline),
68
+ ].join("\u0000");
69
+ const base = `${sessionID}:${hash(body)}`;
70
+ const count = seen.get(base) ?? 0;
71
+ seen.set(base, count + 1);
72
+ return count === 0 ? base : `${base}#${count}`;
73
+ });
74
+ }
75
+ function guard(fn) {
76
+ try {
77
+ return fn();
78
+ }
79
+ catch {
80
+ return undefined;
81
+ }
82
+ }
83
+ function release(handle) {
84
+ if (typeof handle === "function")
85
+ guard(() => handle());
86
+ }
87
+ /**
88
+ * Draw one fold row per recall summary of the session in view. Returns a stop function that removes
89
+ * every hook it added.
90
+ */
91
+ export function startTuiRecall(deps) {
92
+ const debounceMs = deps.debounceMs ?? DEFAULT_DEBOUNCE_MS;
93
+ const sessions = new Map();
94
+ const local = new Map();
95
+ let lastViewed;
96
+ let stopped = false;
97
+ let layerHandle;
98
+ // Two signals: the list changes when summaries arrive, the text changes when a row folds.
99
+ const [listRevision, setListRevision] = deps.createSignal(0);
100
+ const [foldRevision, setFoldRevision] = deps.createSignal(0);
101
+ const repaintList = () => void guard(() => setListRevision((n) => n + 1));
102
+ const repaintFold = () => void guard(() => setFoldRevision((n) => n + 1));
103
+ const rpc = guard(() => deps.client.rpc(recallRpc));
104
+ const fold = guard(() => deps.storage.store(FOLD_STORE_KEY, { initial: { open: {} } }));
105
+ const sessionState = (id) => {
106
+ let state = sessions.get(id);
107
+ if (!state) {
108
+ state = { enabled: false, summaries: [], inflight: false, dirty: false };
109
+ sessions.set(id, state);
110
+ }
111
+ return state;
112
+ };
113
+ const apply = (id, enabled, summaries) => {
114
+ const state = sessionState(id);
115
+ state.enabled = enabled;
116
+ state.summaries = summaries;
117
+ repaintList();
118
+ };
119
+ async function pull(id) {
120
+ const state = sessionState(id);
121
+ state.inflight = true;
122
+ try {
123
+ const reply = await rpc.list({ sessionID: id });
124
+ if (stopped)
125
+ return;
126
+ const ok = reply?.enabled === true && Array.isArray(reply.items);
127
+ apply(id, ok, ok ? reply.items.filter(isSummary) : []);
128
+ }
129
+ catch {
130
+ if (!stopped)
131
+ apply(id, false, []);
132
+ }
133
+ finally {
134
+ state.inflight = false;
135
+ if (state.dirty && !stopped) {
136
+ state.dirty = false;
137
+ request(id, debounceMs);
138
+ }
139
+ }
140
+ }
141
+ /** At most one `list` per session per window; a request during a call asks for one more after it. */
142
+ function request(id, delayMs) {
143
+ if (stopped || !rpc)
144
+ return;
145
+ const state = sessionState(id);
146
+ if (state.timer)
147
+ return;
148
+ if (state.inflight) {
149
+ state.dirty = true;
150
+ return;
151
+ }
152
+ state.timer = setTimeout(() => {
153
+ state.timer = undefined;
154
+ void pull(id);
155
+ }, delayMs);
156
+ state.timer.unref?.();
157
+ }
158
+ const isOpen = (key) => local.get(key) ?? guard(() => fold?.[0]?.open?.[key] === true) ?? false;
159
+ const persist = (changes) => {
160
+ if (!fold)
161
+ return;
162
+ guard(() => {
163
+ const done = fold[1]((draft) => {
164
+ draft.open ??= {};
165
+ for (const [key, open] of changes) {
166
+ if (open)
167
+ draft.open[key] = true;
168
+ else
169
+ delete draft.open[key];
170
+ }
171
+ });
172
+ Promise.resolve(done).catch(() => undefined);
173
+ });
174
+ };
175
+ const rowsFor = (id) => {
176
+ const state = sessions.get(id);
177
+ if (!state?.enabled || state.summaries.length === 0)
178
+ return [];
179
+ const keys = rowKeys(id, state.summaries);
180
+ return state.summaries
181
+ .map((summary, index) => ({ key: keys[index], summary }))
182
+ .slice(-MAX_ROWS);
183
+ };
184
+ const toggle = (key) => {
185
+ const next = !isOpen(key);
186
+ local.set(key, next);
187
+ persist([[key, next]]);
188
+ repaintFold();
189
+ };
190
+ const toggleAll = () => {
191
+ if (!lastViewed)
192
+ return;
193
+ const rows = rowsFor(lastViewed);
194
+ const target = rows.some((row) => !isOpen(row.key));
195
+ for (const row of rows)
196
+ local.set(row.key, target);
197
+ persist(rows.map((row) => [row.key, target]));
198
+ repaintFold();
199
+ };
200
+ // The host owns a keymap layer through the component that created it and removes it when that
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.
203
+ const registerCommand = () => {
204
+ layerHandle = guard(() => deps.keymap.layer(() => ({
205
+ mode: "global",
206
+ commands: [
207
+ {
208
+ id: "omms.recall.fold",
209
+ title: "Toggle OMMS recall rows",
210
+ palette: true,
211
+ slash: { name: "omms-recall" },
212
+ run: () => void guard(toggleAll),
213
+ },
214
+ ],
215
+ })));
216
+ };
217
+ const drawRow = ({ key, summary }) => deps.jsx("box", {
218
+ flexDirection: "column",
219
+ onMouseDown: () => void guard(() => toggle(key)),
220
+ get children() {
221
+ return [
222
+ deps.jsx("text", {
223
+ get fg() {
224
+ return guard(deps.muted);
225
+ },
226
+ get children() {
227
+ foldRevision();
228
+ return guard(() => `${isOpen(key) ? "▾" : "▸"} ● ${callTitle(summary)}`) ?? "";
229
+ },
230
+ }),
231
+ deps.jsx("text", {
232
+ get visible() {
233
+ foldRevision();
234
+ return isOpen(key);
235
+ },
236
+ get fg() {
237
+ return guard(deps.muted);
238
+ },
239
+ get children() {
240
+ foldRevision();
241
+ if (!isOpen(key))
242
+ return "";
243
+ return (guard(() => unfoldLines(summary)
244
+ .map((line) => ` ${line}`)
245
+ .join("\n")) ?? "");
246
+ },
247
+ }),
248
+ ];
249
+ },
250
+ });
251
+ const render = (input) => {
252
+ try {
253
+ // The host passes slot input as reactive props and may keep this claim mounted while the
254
+ // session in view changes, so the id is read on each paint. `tracked` makes the follow-up
255
+ // idempotent: a repaint with the same id asks for no new pull.
256
+ let tracked;
257
+ const follow = () => {
258
+ const id = guard(() => input?.sessionID);
259
+ if (id && id !== tracked) {
260
+ tracked = id;
261
+ lastViewed = id;
262
+ request(id, 0);
263
+ }
264
+ return id;
265
+ };
266
+ follow();
267
+ return deps.jsx("box", {
268
+ flexDirection: "column",
269
+ get children() {
270
+ listRevision();
271
+ const id = follow();
272
+ return (id ? guard(() => rowsFor(id).map(drawRow)) : undefined) ?? [];
273
+ },
274
+ });
275
+ }
276
+ catch {
277
+ return null;
278
+ }
279
+ };
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 }));
288
+ const offPush = guard(() => rpc?.events.on("recalled", (event) => {
289
+ const id = event?.data?.sessionID;
290
+ if (typeof id === "string" && sessions.has(id))
291
+ request(id, debounceMs);
292
+ }));
293
+ // Only sessions the slot has drawn are refreshed, and an event without a session id is no hint.
294
+ const offData = guard(() => deps.data.listen((event) => {
295
+ const details = event?.details;
296
+ if (typeof details?.type !== "string" || !PULL_EVENT_TYPES.has(details.type))
297
+ return;
298
+ const id = details.data?.sessionID ?? details.properties?.sessionID;
299
+ if (typeof id === "string" && sessions.has(id))
300
+ request(id, debounceMs);
301
+ }));
302
+ return () => {
303
+ stopped = true;
304
+ for (const state of sessions.values())
305
+ clearTimeout(state.timer);
306
+ release(offData);
307
+ release(offPush);
308
+ release(removeSlot);
309
+ release(removeCommandSlot);
310
+ release(layerHandle);
311
+ };
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;