om-memory-system 5.3.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.
- package/README.md +4 -0
- package/dist/adapters/claude-code/hook-client.d.ts +1 -1
- package/dist/adapters/claude-code/hook-client.js +15 -2
- package/dist/adapters/opencode/tui-recall.js +13 -3
- package/dist/adapters/pi/extension.d.ts +1 -0
- package/dist/adapters/pi/extension.js +28 -11
- package/dist/adapters/pi/recall-rows.d.ts +31 -0
- package/dist/adapters/pi/recall-rows.js +148 -0
- package/dist/adapters/pi/session-context.d.ts +22 -0
- package/dist/adapters/pi/session-context.js +125 -0
- package/dist/config-template.d.ts +1 -1
- package/dist/config-template.js +18 -3
- package/dist/config.d.ts +6 -0
- package/dist/config.js +5 -4
- package/dist/core/context-budget.d.ts +2 -0
- package/dist/core/context-budget.js +2 -1
- package/dist/core/opening-context.d.ts +45 -0
- package/dist/core/opening-context.js +60 -0
- package/dist/core/recall-summary.d.ts +11 -7
- package/dist/core/recall-summary.js +14 -8
- package/dist/core/retrieval.d.ts +16 -0
- package/dist/core/retrieval.js +33 -11
- package/dist/importer/claude-hook-api.d.ts +5 -4
- package/dist/importer/claude-hook-api.js +58 -41
- package/dist/importer/claude-hook-context.d.ts +49 -0
- package/dist/importer/claude-hook-context.js +264 -0
- package/dist/services/ai/local-api-url.d.ts +2 -0
- package/dist/services/ai/local-api-url.js +12 -0
- package/dist/services/ai/provider-config.d.ts +2 -0
- package/dist/services/ai/provider-config.js +3 -1
- package/dist/services/ai/providers/google-gemini.js +2 -1
- package/dist/services/ai/providers/openai-responses.js +2 -1
- package/dist/services/chat/chat-runtime.js +5 -1
- package/dist/services/chat/chat-status.js +4 -1
- package/dist/services/global-config-writer.js +39 -14
- package/dist/services/injection-options-config.d.ts +53 -0
- package/dist/services/injection-options-config.js +56 -0
- package/dist/services/settings-snapshot.js +44 -12
- package/dist/services/visible-recall-config.d.ts +7 -2
- package/dist/services/visible-recall-config.js +2 -1
- package/dist/utils/memory-limits.d.ts +2 -1
- package/dist/utils/memory-limits.js +6 -3
- package/dist/v2/adapter.d.ts +2 -0
- package/dist/v2/adapter.js +107 -33
- package/dist/v2/memory-bridge.d.ts +13 -0
- package/dist/v2/memory-bridge.js +17 -0
- package/dist/v2/recall-reporter.d.ts +6 -3
- package/dist/v2/recall-reporter.js +4 -27
- package/dist/v2/session-parent.d.ts +14 -0
- package/dist/v2/session-parent.js +26 -0
- package/dist/web/assets/{index-CJ_K-hMG.js → index-BhQR88tU.js} +16 -16
- package/dist/web/assets/index-BmFUZbGr.css +2 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- 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 = [
|
|
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
|
|
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
|
|
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.
|
|
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 {
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|