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.
- 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/in-process-client.d.ts +2 -0
- package/dist/adapters/opencode/in-process-client.js +16 -0
- package/dist/adapters/opencode/opencode-host-config.d.ts +3 -0
- package/dist/adapters/opencode/opencode-host-config.js +2 -0
- package/dist/adapters/opencode/tui-recall.d.ts +68 -0
- package/dist/adapters/opencode/tui-recall.js +312 -0
- 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 +20 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.js +8 -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 +32 -0
- package/dist/core/recall-summary.js +77 -0
- 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/index.js +1 -1
- 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/single-flight.d.ts +2 -0
- package/dist/services/single-flight.js +12 -0
- package/dist/services/visible-recall-config.d.ts +39 -0
- package/dist/services/visible-recall-config.js +28 -0
- package/dist/services/web-server.d.ts +3 -0
- package/dist/services/web-server.js +26 -8
- 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 +110 -31
- package/dist/v2/legacy-client.js +2 -1
- package/dist/v2/memory-bridge.d.ts +15 -0
- package/dist/v2/memory-bridge.js +21 -0
- package/dist/v2/recall-reporter.d.ts +27 -0
- package/dist/v2/recall-reporter.js +105 -0
- package/dist/v2/recall-rpc.d.ts +51 -0
- package/dist/v2/recall-rpc.js +38 -0
- 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/opencode/tui.tsx +22 -3
- 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
|
}
|
|
@@ -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 {
|
|
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;
|