om-memory-system 5.2.0 → 5.3.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/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 +302 -0
- package/dist/config-template.d.ts +1 -1
- package/dist/config-template.js +5 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +3 -0
- package/dist/core/recall-summary.d.ts +28 -0
- package/dist/core/recall-summary.js +71 -0
- package/dist/index.js +1 -1
- 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 +34 -0
- package/dist/services/visible-recall-config.js +27 -0
- package/dist/services/web-server.d.ts +3 -0
- package/dist/services/web-server.js +26 -8
- package/dist/v2/adapter.js +6 -1
- package/dist/v2/legacy-client.js +2 -1
- package/dist/v2/memory-bridge.d.ts +2 -0
- package/dist/v2/memory-bridge.js +4 -0
- package/dist/v2/recall-reporter.d.ts +24 -0
- package/dist/v2/recall-reporter.js +128 -0
- package/dist/v2/recall-rpc.d.ts +51 -0
- package/dist/v2/recall-rpc.js +38 -0
- package/opencode/tui.tsx +22 -3
- package/package.json +1 -1
|
@@ -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,302 @@
|
|
|
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 DEFAULT_DEBOUNCE_MS = 150;
|
|
15
|
+
/**
|
|
16
|
+
* Host events that pull the recall list. The server records a recall when a model step starts (the
|
|
17
|
+
* `context` hook), so only the events around a step, plus session creation and view changes, can
|
|
18
|
+
* coincide with a new recall. The end events catch a missed `recalled` push right after the step.
|
|
19
|
+
* The failure events are end events too: the summary is recorded in the `context` hook after
|
|
20
|
+
* `session.step.started` fires, so if the push is absent and the step then fails, no later event
|
|
21
|
+
* in this set would pull it.
|
|
22
|
+
* Streaming, part, message, inbox and instruction events fire dozens of times per reply and never
|
|
23
|
+
* change the list, so they must stay out of this set.
|
|
24
|
+
*/
|
|
25
|
+
const PULL_EVENT_TYPES = new Set([
|
|
26
|
+
"session.created",
|
|
27
|
+
"session.viewed",
|
|
28
|
+
"session.execution.started",
|
|
29
|
+
"session.step.started",
|
|
30
|
+
"session.step.ended",
|
|
31
|
+
"session.execution.succeeded",
|
|
32
|
+
"session.step.failed",
|
|
33
|
+
"session.execution.failed",
|
|
34
|
+
]);
|
|
35
|
+
function isSummary(value) {
|
|
36
|
+
const v = value;
|
|
37
|
+
return (!!v &&
|
|
38
|
+
(v.mode === "search" || v.mode === "recent") &&
|
|
39
|
+
Array.isArray(v.items) &&
|
|
40
|
+
v.items.every((item) => !!item && typeof item.headline === "string"));
|
|
41
|
+
}
|
|
42
|
+
/** FNV-1a, 32 bit: short, deterministic, no crypto needed for a fold key. */
|
|
43
|
+
function hash(text) {
|
|
44
|
+
let h = 0x811c9dc5;
|
|
45
|
+
for (let i = 0; i < text.length; i++) {
|
|
46
|
+
h ^= text.charCodeAt(i);
|
|
47
|
+
h = Math.imul(h, 0x01000193);
|
|
48
|
+
}
|
|
49
|
+
return (h >>> 0).toString(16);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Stable per-summary key. A summary with a server sequence number is keyed by session id and that
|
|
53
|
+
* number, so it survives the ring dropping older entries. An older server sends none, so the key
|
|
54
|
+
* falls back to the session id, a hash of mode, query and each memory's id (or headline when it
|
|
55
|
+
* has none), and a counter for identical summaries.
|
|
56
|
+
*/
|
|
57
|
+
export function rowKeys(sessionID, summaries) {
|
|
58
|
+
const seen = new Map();
|
|
59
|
+
return summaries.map((summary) => {
|
|
60
|
+
if (typeof summary.seq === "number")
|
|
61
|
+
return `${sessionID}:${summary.seq}`;
|
|
62
|
+
const body = [
|
|
63
|
+
summary.mode,
|
|
64
|
+
summary.query ?? "",
|
|
65
|
+
...summary.items.map((item) => item.id ?? item.headline),
|
|
66
|
+
].join("\u0000");
|
|
67
|
+
const base = `${sessionID}:${hash(body)}`;
|
|
68
|
+
const count = seen.get(base) ?? 0;
|
|
69
|
+
seen.set(base, count + 1);
|
|
70
|
+
return count === 0 ? base : `${base}#${count}`;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function guard(fn) {
|
|
74
|
+
try {
|
|
75
|
+
return fn();
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function release(handle) {
|
|
82
|
+
if (typeof handle === "function")
|
|
83
|
+
guard(() => handle());
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Draw one fold row per recall summary of the session in view. Returns a stop function that removes
|
|
87
|
+
* every hook it added.
|
|
88
|
+
*/
|
|
89
|
+
export function startTuiRecall(deps) {
|
|
90
|
+
const debounceMs = deps.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
91
|
+
const sessions = new Map();
|
|
92
|
+
const local = new Map();
|
|
93
|
+
let lastViewed;
|
|
94
|
+
let stopped = false;
|
|
95
|
+
let layerHandle;
|
|
96
|
+
// Two signals: the list changes when summaries arrive, the text changes when a row folds.
|
|
97
|
+
const [listRevision, setListRevision] = deps.createSignal(0);
|
|
98
|
+
const [foldRevision, setFoldRevision] = deps.createSignal(0);
|
|
99
|
+
const repaintList = () => void guard(() => setListRevision((n) => n + 1));
|
|
100
|
+
const repaintFold = () => void guard(() => setFoldRevision((n) => n + 1));
|
|
101
|
+
const rpc = guard(() => deps.client.rpc(recallRpc));
|
|
102
|
+
const fold = guard(() => deps.storage.store(FOLD_STORE_KEY, { initial: { open: {} } }));
|
|
103
|
+
const sessionState = (id) => {
|
|
104
|
+
let state = sessions.get(id);
|
|
105
|
+
if (!state) {
|
|
106
|
+
state = { enabled: false, summaries: [], inflight: false, dirty: false };
|
|
107
|
+
sessions.set(id, state);
|
|
108
|
+
}
|
|
109
|
+
return state;
|
|
110
|
+
};
|
|
111
|
+
const apply = (id, enabled, summaries) => {
|
|
112
|
+
const state = sessionState(id);
|
|
113
|
+
state.enabled = enabled;
|
|
114
|
+
state.summaries = summaries;
|
|
115
|
+
repaintList();
|
|
116
|
+
};
|
|
117
|
+
async function pull(id) {
|
|
118
|
+
const state = sessionState(id);
|
|
119
|
+
state.inflight = true;
|
|
120
|
+
try {
|
|
121
|
+
const reply = await rpc.list({ sessionID: id });
|
|
122
|
+
if (stopped)
|
|
123
|
+
return;
|
|
124
|
+
const ok = reply?.enabled === true && Array.isArray(reply.items);
|
|
125
|
+
apply(id, ok, ok ? reply.items.filter(isSummary) : []);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
if (!stopped)
|
|
129
|
+
apply(id, false, []);
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
state.inflight = false;
|
|
133
|
+
if (state.dirty && !stopped) {
|
|
134
|
+
state.dirty = false;
|
|
135
|
+
request(id, debounceMs);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** At most one `list` per session per window; a request during a call asks for one more after it. */
|
|
140
|
+
function request(id, delayMs) {
|
|
141
|
+
if (stopped || !rpc)
|
|
142
|
+
return;
|
|
143
|
+
const state = sessionState(id);
|
|
144
|
+
if (state.timer)
|
|
145
|
+
return;
|
|
146
|
+
if (state.inflight) {
|
|
147
|
+
state.dirty = true;
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
state.timer = setTimeout(() => {
|
|
151
|
+
state.timer = undefined;
|
|
152
|
+
void pull(id);
|
|
153
|
+
}, delayMs);
|
|
154
|
+
state.timer.unref?.();
|
|
155
|
+
}
|
|
156
|
+
const isOpen = (key) => local.get(key) ?? guard(() => fold?.[0]?.open?.[key] === true) ?? false;
|
|
157
|
+
const persist = (changes) => {
|
|
158
|
+
if (!fold)
|
|
159
|
+
return;
|
|
160
|
+
guard(() => {
|
|
161
|
+
const done = fold[1]((draft) => {
|
|
162
|
+
draft.open ??= {};
|
|
163
|
+
for (const [key, open] of changes) {
|
|
164
|
+
if (open)
|
|
165
|
+
draft.open[key] = true;
|
|
166
|
+
else
|
|
167
|
+
delete draft.open[key];
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
Promise.resolve(done).catch(() => undefined);
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
const rowsFor = (id) => {
|
|
174
|
+
const state = sessions.get(id);
|
|
175
|
+
if (!state?.enabled || state.summaries.length === 0)
|
|
176
|
+
return [];
|
|
177
|
+
const keys = rowKeys(id, state.summaries);
|
|
178
|
+
return state.summaries
|
|
179
|
+
.map((summary, index) => ({ key: keys[index], summary }))
|
|
180
|
+
.slice(-MAX_ROWS);
|
|
181
|
+
};
|
|
182
|
+
const toggle = (key) => {
|
|
183
|
+
const next = !isOpen(key);
|
|
184
|
+
local.set(key, next);
|
|
185
|
+
persist([[key, next]]);
|
|
186
|
+
repaintFold();
|
|
187
|
+
};
|
|
188
|
+
const toggleAll = () => {
|
|
189
|
+
if (!lastViewed)
|
|
190
|
+
return;
|
|
191
|
+
const rows = rowsFor(lastViewed);
|
|
192
|
+
const target = rows.some((row) => !isOpen(row.key));
|
|
193
|
+
for (const row of rows)
|
|
194
|
+
local.set(row.key, target);
|
|
195
|
+
persist(rows.map((row) => [row.key, target]));
|
|
196
|
+
repaintFold();
|
|
197
|
+
};
|
|
198
|
+
// The host owns a keymap layer through the component that created it and removes it when that
|
|
199
|
+
// component is disposed. A slot remount therefore needs a fresh layer, so this runs once per render.
|
|
200
|
+
const registerCommand = () => {
|
|
201
|
+
layerHandle = guard(() => deps.keymap.layer(() => ({
|
|
202
|
+
mode: "global",
|
|
203
|
+
commands: [
|
|
204
|
+
{
|
|
205
|
+
id: "omms.recall.fold",
|
|
206
|
+
title: "Toggle OMMS recall rows",
|
|
207
|
+
palette: true,
|
|
208
|
+
slash: { name: "omms-recall" },
|
|
209
|
+
run: () => void guard(toggleAll),
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
})));
|
|
213
|
+
};
|
|
214
|
+
const drawRow = ({ key, summary }) => deps.jsx("box", {
|
|
215
|
+
flexDirection: "column",
|
|
216
|
+
onMouseDown: () => void guard(() => toggle(key)),
|
|
217
|
+
get children() {
|
|
218
|
+
return [
|
|
219
|
+
deps.jsx("text", {
|
|
220
|
+
get fg() {
|
|
221
|
+
return guard(deps.muted);
|
|
222
|
+
},
|
|
223
|
+
get children() {
|
|
224
|
+
foldRevision();
|
|
225
|
+
return guard(() => `${isOpen(key) ? "▾" : "▸"} ● ${callTitle(summary)}`) ?? "";
|
|
226
|
+
},
|
|
227
|
+
}),
|
|
228
|
+
deps.jsx("text", {
|
|
229
|
+
get visible() {
|
|
230
|
+
foldRevision();
|
|
231
|
+
return isOpen(key);
|
|
232
|
+
},
|
|
233
|
+
get fg() {
|
|
234
|
+
return guard(deps.muted);
|
|
235
|
+
},
|
|
236
|
+
get children() {
|
|
237
|
+
foldRevision();
|
|
238
|
+
if (!isOpen(key))
|
|
239
|
+
return "";
|
|
240
|
+
return (guard(() => unfoldLines(summary)
|
|
241
|
+
.map((line) => ` ${line}`)
|
|
242
|
+
.join("\n")) ?? "");
|
|
243
|
+
},
|
|
244
|
+
}),
|
|
245
|
+
];
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
const render = (input) => {
|
|
249
|
+
try {
|
|
250
|
+
// The host passes slot input as reactive props and may keep this claim mounted while the
|
|
251
|
+
// session in view changes, so the id is read on each paint. `tracked` makes the follow-up
|
|
252
|
+
// idempotent: a repaint with the same id asks for no new pull.
|
|
253
|
+
let tracked;
|
|
254
|
+
const follow = () => {
|
|
255
|
+
const id = guard(() => input?.sessionID);
|
|
256
|
+
if (id && id !== tracked) {
|
|
257
|
+
tracked = id;
|
|
258
|
+
lastViewed = id;
|
|
259
|
+
request(id, 0);
|
|
260
|
+
}
|
|
261
|
+
return id;
|
|
262
|
+
};
|
|
263
|
+
follow();
|
|
264
|
+
registerCommand();
|
|
265
|
+
return deps.jsx("box", {
|
|
266
|
+
flexDirection: "column",
|
|
267
|
+
get children() {
|
|
268
|
+
listRevision();
|
|
269
|
+
const id = follow();
|
|
270
|
+
return (id ? guard(() => rowsFor(id).map(drawRow)) : undefined) ?? [];
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
const removeSlot = guard(() => deps.ui.slot({ append: SLOT, render }));
|
|
279
|
+
const offPush = guard(() => rpc?.events.on("recalled", (event) => {
|
|
280
|
+
const id = event?.data?.sessionID;
|
|
281
|
+
if (typeof id === "string" && sessions.has(id))
|
|
282
|
+
request(id, debounceMs);
|
|
283
|
+
}));
|
|
284
|
+
// Only sessions the slot has drawn are refreshed, and an event without a session id is no hint.
|
|
285
|
+
const offData = guard(() => deps.data.listen((event) => {
|
|
286
|
+
const details = event?.details;
|
|
287
|
+
if (typeof details?.type !== "string" || !PULL_EVENT_TYPES.has(details.type))
|
|
288
|
+
return;
|
|
289
|
+
const id = details.data?.sessionID ?? details.properties?.sessionID;
|
|
290
|
+
if (typeof id === "string" && sessions.has(id))
|
|
291
|
+
request(id, debounceMs);
|
|
292
|
+
}));
|
|
293
|
+
return () => {
|
|
294
|
+
stopped = true;
|
|
295
|
+
for (const state of sessions.values())
|
|
296
|
+
clearTimeout(state.timer);
|
|
297
|
+
release(offData);
|
|
298
|
+
release(offPush);
|
|
299
|
+
release(removeSlot);
|
|
300
|
+
release(layerHandle);
|
|
301
|
+
};
|
|
302
|
+
}
|
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
* src/config.ts until that file reached its size ceiling; the text is
|
|
4
4
|
* unchanged. Shared by config.ts and the global settings writer.
|
|
5
5
|
*/
|
|
6
|
-
export declare const CONFIG_TEMPLATE = "{\n // ============================================\n // omms (Opinionated Modular Memory System) Configuration\n // ============================================\n \n // Storage location for the vector database. Leave unset to use the\n // default ~/.omms/data (a legacy ~/.opencode-mem/data store is migrated\n // there automatically on first start; see docs/omms-migration.md).\n // \"storagePath\": \"~/.omms/data\",\n\n \"userEmailOverride\": \"\",\n \"userNameOverride\": \"\",\n \n // ============================================\n // Embedding Model (for similarity search)\n // ============================================\n // Local = Hugging Face / ONNX via @huggingface/transformers (not Apple MLX).\n // Remote = set embeddingApiUrl (OpenAI-compatible /embeddings); embeddingApiKey is optional.\n \n // Default: Nomic Embed v1 (768 dimensions, 8192 context, multilingual)\n \"embeddingModel\": \"Xenova/nomic-embed-text-v1\",\n\n // Opt-in Nomic task prefixes (search_document: / search_query:). After enabling,\n // re-index existing memories so store and query vectors stay aligned.\n // \"embeddingUseTaskPrefixes\": true,\n \n // Auto-detected dimensions (no need to set manually)\n // \"embeddingDimensions\": 768,\n \n // Other recommended local models:\n // \"embeddingModel\": \"Xenova/jina-embeddings-v2-base-en\", // 768 dims, English-only, 8192 context\n // \"embeddingModel\": \"Xenova/jina-embeddings-v2-small-en\", // 512 dims, faster, 8192 context\n // \"embeddingModel\": \"Xenova/all-MiniLM-L6-v2\", // 384 dims, very fast, 512 context\n // \"embeddingModel\": \"Xenova/all-mpnet-base-v2\", // 768 dims, good quality, 512 context\n \n // Optional: OpenAI-compatible API for embeddings (URL required; key only if the server needs one)\n // \"embeddingApiUrl\": \"https://api.openai.com/v1\",\n // \"embeddingApiKey\": \"env://OPENAI_API_KEY\", // or \"sk-...\" / \"file:///path/to/key\"\n // \"embeddingModel\": \"text-embedding-3-small\", // 1536 dims, auto-detected\n \n // ============================================\n // Web Server Settings\n // ============================================\n \n // Start a background import of past chats on each host's next start.\n \"autoBackfill\": true,\n // \"piBackfillModel\": \"inherit\", // or \"external\", or \"provider/model\"\n // \"opencodeBackfillModel\": \"inherit\", // or \"external\", or \"provider/model\"\n // Directory maps for history recorded in moved or deleted directories.\n // \"importPathMaps\": [{ \"from\": \"~/code/app-feat-x\", \"to\": \"~/code/app\" }],\n // Unresolved directories to hide on the Directory maps page. They stay unimported.\n // \"importIgnoredDirectories\": [\"/private/tmp/scratch\"],\n // Claude Code's folder, when it is not ~/.claude (like CLAUDE_CONFIG_DIR).\n // \"claudeConfigDir\": \"~/.claude-work\",\n\n // Register the web app to start when you log in.\n \"webServerAutoStart\": true,\n // Enable web UI for managing memories (accessible at http://localhost:4747)\n \"webServerEnabled\": true,\n \n // Port for web UI server\n \"webServerPort\": 4747,\n \n // Host address for web UI (use 127.0.0.1 for local only, 0.0.0.0 for network access)\n \"webServerHost\": \"127.0.0.1\",\n\n // Optional HTTP Basic Auth for the web UI. Accepts literal, env://, or file:// secrets.\n // \"webServerAuthPassword\": \"\",\n // \"webServerAuthUsername\": \"\",\n\n // Deprecated: imported once into the API token table, then no longer read.\n // Create API tokens on the Settings page (Keys and access) instead.\n // \"webServerApiToken\": \"env://OMMS_WEB_TOKEN\",\n \n // ============================================\n // Database Settings\n // ============================================\n \n // Maximum vectors per database shard (auto-creates new shard when limit reached)\n \"maxVectorsPerShard\": 50000,\n \n // Automatically delete old memories based on retention period\n \"autoCleanupEnabled\": true,\n \n // Days to keep memories before auto-cleanup (only if autoCleanupEnabled is true)\n \"autoCleanupRetentionDays\": 30,\n \n // Automatically detect and remove duplicate memories\n \"deduplicationEnabled\": true,\n \n // Similarity threshold (0-1) for detecting duplicates (higher = stricter)\n \"deduplicationSimilarityThreshold\": 0.90,\n \n // ============================================\n // Memory Scope Settings\n // ============================================\n\n // Default scope for memory list/search queries\n // \"project\" keeps queries within the current project, \"all-projects\" searches across all project shards\n \"memory\": {\n \"defaultScope\": \"project\"\n },\n\n // ============================================\n // Model for auto-capture and profile learning (same rule in OpenCode and Pi)\n // ============================================\n\n // Which model summarises your work, in this order:\n // 1. The host model below: opencodeProvider/opencodeModel in OpenCode,\n // piProvider/piModel in Pi. Set the model to \"inherit\" to follow\n // whatever model the session is using, or to \"external\" to send\n // every call to the external API below.\n // 2. If no host model is set: the external API (memoryModel/memoryApiUrl/\n // memoryApiKey further down).\n // 3. If neither is set: the session's own model.\n // If the host model fails and the external API is configured, the external\n // API is used instead.\n //\n // Host models go through OpenCode's or Pi's own sign-in (OAuth like Claude\n // Pro/Max, GitHub Copilot, ChatGPT, or your own key there), so no separate API\n // key is needed here.\n //\n // Examples (OpenCode names from 'opencode providers list'; Pi names from its model list):\n // \"opencodeProvider\": \"anthropic\", \"opencodeModel\": \"claude-haiku-4-5-20251001\"\n // \"opencodeProvider\": \"openai\", \"opencodeModel\": \"inherit\"\n // \"piProvider\": \"openai-codex\", \"piModel\": \"gpt-5.6-luna\"\n //\n // \"opencodeProvider\": \"anthropic\",\n // \"opencodeModel\": \"claude-haiku-4-5-20251001\",\n // \"piProvider\": \"openai-codex\",\n // \"piModel\": \"gpt-5.6-luna\",\n\n // ============================================\n // Auto-Capture Settings\n // ============================================\n \n // Auto-capture runs in the background without blocking your main session.\n // Note: Ollama may not support tool calling. Use OpenAI, Anthropic, or Groq for best results.\n \n \"autoCaptureEnabled\": true,\n \n // Provider type: \"openai-chat\" | \"openai-responses\" | \"anthropic\" | \"minimax\" | \"orcarouter\" | \"google-gemini\"\n // Note: \"openai-chat\" is a generic OpenAI API-compatible mode.\n // Any service that follows the OpenAI Chat Completions API can use it via custom \"memoryApiUrl\".\n \"memoryProvider\": \"openai-chat\",\n \n // External API (step 2 above, and the fallback when a host model fails).\n // Uncomment all 3 lines and replace memoryApiKey before use:\n // \"memoryModel\": \"gpt-4o-mini\",\n // \"memoryApiUrl\": \"https://api.openai.com/v1\",\n // \"memoryApiKey\": \"sk-...\",\n\n // API Key Formats:\n // Direct value: \"sk-...\"\n // From file: \"file://~/.config/litellm-key.txt\"\n // From env variable: \"env://LITELLM_API_KEY\"\n \n // Examples for different providers:\n // Any OpenAI-compatible endpoint can use the \"openai-chat\" provider pattern below.\n // Common examples: DeepSeek, Qwen (via Alibaba Cloud ModelStudio),\n // Zhipu GLM (BigModel platform), and Kimi (Moonshot AI platform).\n\n // OpenAI Chat Completion (default, backward compatible):\n // \"memoryProvider\": \"openai-chat\"\n // \"memoryModel\": \"gpt-4o-mini\"\n // \"memoryApiUrl\": \"https://api.openai.com/v1\"\n // \"memoryApiKey\": \"sk-...\"\n\n // DeepSeek (OpenAI-compatible example):\n // \"memoryProvider\": \"openai-chat\"\n // \"memoryModel\": \"deepseek-chat\"\n // \"memoryApiUrl\": \"https://api.deepseek.com/v1\"\n // \"memoryApiKey\": \"sk-...\"\n \n // OpenAI Responses API (recommended, with session support):\n // \"memoryProvider\": \"openai-responses\"\n // \"memoryModel\": \"gpt-4o\"\n // \"memoryApiUrl\": \"https://api.openai.com/v1\"\n // \"memoryApiKey\": \"sk-...\"\n \n // Anthropic (with session support):\n // \"memoryProvider\": \"anthropic\"\n // \"memoryModel\": \"claude-3-5-haiku-20241022\"\n // \"memoryApiUrl\": \"https://api.anthropic.com/v1\"\n // \"memoryApiKey\": \"sk-ant-...\"\n\n // MiniMax (Anthropic Messages-compatible endpoint, with session support):\n // \"memoryProvider\": \"minimax\"\n // \"memoryModel\": \"MiniMax-M3\"\n // \"memoryApiUrl\": \"https://api.minimax.io\" // global endpoint\n // \"memoryApiKey\": \"<MiniMax API key>\"\n // // China endpoint: \"memoryApiUrl\": \"https://api.minimaxi.com\"\n // // Optional adaptive thinking for MiniMax-M3:\n // \"memoryExtraParams\": { \"thinking\": { \"type\": \"adaptive\" } }\n\n // OrcaRouter (OpenAI-compatible gateway, namespaced model IDs, with session support):\n // \"memoryProvider\": \"orcarouter\"\n // \"memoryApiKey\": \"<OrcaRouter API key>\"\n // // memoryApiUrl and memoryModel are optional \u2014 they default to\n // // https://api.orcarouter.ai/v1 and \"orcarouter/auto\" (a routing alias).\n // // OrcaRouter rejects bare model names, so if you set memoryModel, use a\n // // namespaced ID such as \"openai/gpt-5.5\" or \"deepseek/deepseek-v4-flash\".\n // \"memoryModel\": \"openai/gpt-5.5\"\n\n // Groq (OpenAI-compatible, use openai-chat provider):\n // \"memoryProvider\": \"openai-chat\"\n // \"memoryModel\": \"llama-3.3-70b-versatile\"\n // \"memoryApiUrl\": \"https://api.groq.com/openai/v1\"\n // \"memoryApiKey\": \"gsk_...\"\n \n // Maximum iterations for multi-turn AI analysis (for openai-responses, anthropic, and minimax)\n \"autoCaptureMaxIterations\": 5,\n \n // Timeout per iteration in milliseconds (30 seconds default)\n \"autoCaptureIterationTimeout\": 30000,\n\n // Tries for one turn's capture, on Pi and OpenCode, 2 and 4 seconds apart. After the\n // last one fails, a turn the model could not reach goes to the capture retry queue.\n \"autoCaptureMaxRetries\": 3,\n\n // Maximum UTF-8 bytes for the auto-capture markdown context sent to the summary model.\n // Prevents HTTP 400 context overflows on models with ~131K token windows (e.g. Groq Llama).\n // Rough guide: tokens \u2248 bytes / 4 for mixed code/prose.\n \"autoCaptureMaxContextBytes\": 131072,\n \n // Days to keep AI session history before cleanup\n \"aiSessionRetentionDays\": 7,\n\n // Capture diagnostics: every capture attempt always writes one metadata line\n // (model, stop reason, sizes, outcome) to ~/.omms/omms.log, with no\n // conversation text. Set captureTrace to true to also write each attempt's\n // prompt and raw reply to ~/.omms/traces/ for debugging. Traces can contain\n // conversation content: <private> text and common API key formats are\n // redacted, files are readable only by you, and they are deleted after\n // captureTraceRetentionDays. Only this global file can turn tracing on.\n // \"captureTrace\": false,\n // \"captureTraceRetentionDays\": 7,\n // \"captureAttemptRetentionDays\": 30,\n\n // Skill learning: record each full turn (prompt, replies, tool calls with\n // their outcome, and the skills used) in ~/.omms/data/skills.db, so OMMS can\n // rank your skills and draft better versions. Off by default. A project config\n // can turn it off but never on. No model call is made to record a turn.\n // \"skillLearning\": false,\n // Remove <private> text and common API key formats before a turn is stored.\n // Global config only. Turning this off stores text as written.\n // \"skillLearningRedact\": true,\n // Folders OMMS reads skills from. Absolute or starting with ~/. Global config\n // only. Unset: ~/.agents/skills and the Claude Code folder's skills folder.\n // \"skillFolders\": [\"~/.agents/skills\"],\n\n // Capture retry queue: when a live capture fails because the model cannot be\n // reached (network error, timeout, HTTP 408, 429 or 5xx), OMMS keeps a copy of\n // the turn in ~/.omms/data and tries it again later. The copy has <private>\n // text and common API key formats removed. Turns are deleted when the retry\n // ends or after captureRetryRetentionHours (0 to 720). 0 turns the queue off.\n // Only this global file can set it.\n // \"captureRetryRetentionHours\": 72,\n\n // Temperature for AI API requests (set to false to omit parameter for models that don't support it)\n // Some reasoning models (like o1, o3, gpt-5) don't support temperature parameter\n // Set to false and add \"memoryTemperature\": false in config when using such models\n \"memoryTemperature\": 0.3,\n\n // Extra parameters to include in API request body\n // Useful for local inference servers (e.g. llama-server with --jinja) that support\n // additional parameters like disabling thinking/reasoning mode\n // Example for Qwen3 models: { \"enable_thinking\": false }\n // \"memoryExtraParams\": {},\n\n // Language for auto-capture summaries (default: \"auto\" for auto-detection)\n // Options: \"auto\", \"en\", \"id\", \"zh\", \"ja\", \"es\", \"fr\", \"de\", \"ru\", \"pt\", \"ar\", \"ko\"\n // \"autoCaptureLanguage\": \"auto\",\n\n // ============================================\n // Toast Notifications\n // ============================================\n\n // Show toast when memory is auto-captured\n \"showAutoCaptureToasts\": true,\n\n // Show toast when user profile is updated\n \"showUserProfileToasts\": true,\n\n // Show toast for error messages\n \"showErrorToasts\": true,\n\n // ============================================\n // User Profile System\n // ============================================\n\n // Analyze user prompts every N prompts to build/update your user profile\n // When N uncaptured prompts accumulate, AI will analyze them to identify:\n // - User preferences (code style, communication style, tool preferences)\n // - User patterns (recurring topics, problem domains, technical interests)\n // - User workflows (development habits, sequences, learning style)\n // - Skill level (overall and per-domain assessment)\n \"userProfileAnalysisInterval\": 10,\n\n // Days before inactive items (all types) are eligible for removal\n \"userProfileStaleDays\": 2,\n\n // Days before an inactive workflow is eligible for removal. A workflow seen\n // userProfileMinEvidenceForRetention times or more is kept.\n \"userProfileWorkflowStaleDays\": 30,\n\n // Number of preferences shown in UI\n \"userProfileDisplayPreferences\": 20,\n \n // Number of patterns shown in UI\n \"userProfileDisplayPatterns\": 15,\n \n // Number of workflows shown in UI\n \"userProfileDisplayWorkflows\": 10,\n \n // Number of preferences injected into LLM conversation context\n // Keep this small \u2014 the strongest signals are enough; more dilute LLM attention\n \"userProfileInjectPreferences\": 5,\n \n // Number of patterns injected into LLM conversation context\n \"userProfileInjectPatterns\": 5,\n \n // Number of workflows injected into LLM conversation context\n \"userProfileInjectWorkflows\": 3,\n \n // Days before preference confidence starts to decay (if not reinforced)\n // Preferences that aren't seen again will gradually lose confidence and be removed\n \"userProfileConfidenceDecayDays\": 30,\n \n // Number of profile versions to keep in changelog (for rollback/debugging)\n // Older versions are automatically cleaned up\n \"userProfileChangelogRetentionCount\": 5,\n\n // Minimum evidence count for a preference/pattern to survive confidence decay\n // Items confirmed fewer times are more likely to be pruned when confidence decays\n \"userProfileMinEvidenceForRetention\": 3,\n\n // Merge duplicate or vague profile items every N analysed prompts (about 1 model call per 100 items)\n \"userProfileAutoCleanupEnabled\": true,\n \"userProfileAutoCleanupInterval\": 500,\n\n // Enable LLM validation of existing preferences against recent behavior.\n // When enabled, each analysis round checks if top-5 preferences still match recent prompts.\n // Experimental \u2014 disabled by default.\n \"userProfileValidationEnabled\": false,\n\n // ============================================\n // Search Settings\n // ============================================\n \n // Minimum similarity score (0-1) for memory search results. Leave unset to\n // follow the default for your embedding model: 0.5 for qwen3-embedding:0.6b\n // (the only model measured), 0.6 for every other embedder.\n // \"similarityThreshold\": 0.5,\n\n // Maximum number of memories to return in search results.\n // Manual searches can ask for fewer; prompt retrieval uses this ceiling.\n \"maxMemories\": 10,\n\n // ============================================\n // Memory Context Controls\n // ============================================\n\n // Recent memories added at fresh session start (OpenCode V1 and Claude Code).\n // Pi and OpenCode V2 search the store with each prompt instead.\n \"chatMessage\": {\n \"enabled\": true,\n // Recent memories injected at session start.\n \"maxMemories\": 3,\n \"excludeCurrentSession\": true,\n // When to inject: \"first\" message of a session or \"always\".\n // \"injectOn\": \"first\",\n },\n\n // OpenCode profile-learning input limit, in UTF-8 bytes. Smaller values can\n // omit prompts from that input. Does not limit Pi or Claude Code profile input.\n \"userProfileMaxContextBytes\": 32768,\n\n // Shared budget for automatic memory context (profile text, memories, and\n // formatting) across all hosts, in approximate tokens: ceil(UTF-8 bytes / 4).\n // Covers prompt retrieval, recent-memory injection, and session-memory\n // restoration. Not an exact provider token limit or a spending limit.\n \"retrievalMaxTokens\": 2000,\n\n // ============================================\n // Advanced Settings\n // ============================================\n \n // Inject user profile into AI context (preferences, patterns, workflows)\n \"injectProfile\": true\n}\n";
|
|
6
|
+
export declare const CONFIG_TEMPLATE = "{\n // ============================================\n // omms (Opinionated Modular Memory System) Configuration\n // ============================================\n \n // Storage location for the vector database. Leave unset to use the\n // default ~/.omms/data (a legacy ~/.opencode-mem/data store is migrated\n // there automatically on first start; see docs/omms-migration.md).\n // \"storagePath\": \"~/.omms/data\",\n\n \"userEmailOverride\": \"\",\n \"userNameOverride\": \"\",\n \n // ============================================\n // Embedding Model (for similarity search)\n // ============================================\n // Local = Hugging Face / ONNX via @huggingface/transformers (not Apple MLX).\n // Remote = set embeddingApiUrl (OpenAI-compatible /embeddings); embeddingApiKey is optional.\n \n // Default: Nomic Embed v1 (768 dimensions, 8192 context, multilingual)\n \"embeddingModel\": \"Xenova/nomic-embed-text-v1\",\n\n // Opt-in Nomic task prefixes (search_document: / search_query:). After enabling,\n // re-index existing memories so store and query vectors stay aligned.\n // \"embeddingUseTaskPrefixes\": true,\n \n // Auto-detected dimensions (no need to set manually)\n // \"embeddingDimensions\": 768,\n \n // Other recommended local models:\n // \"embeddingModel\": \"Xenova/jina-embeddings-v2-base-en\", // 768 dims, English-only, 8192 context\n // \"embeddingModel\": \"Xenova/jina-embeddings-v2-small-en\", // 512 dims, faster, 8192 context\n // \"embeddingModel\": \"Xenova/all-MiniLM-L6-v2\", // 384 dims, very fast, 512 context\n // \"embeddingModel\": \"Xenova/all-mpnet-base-v2\", // 768 dims, good quality, 512 context\n \n // Optional: OpenAI-compatible API for embeddings (URL required; key only if the server needs one)\n // \"embeddingApiUrl\": \"https://api.openai.com/v1\",\n // \"embeddingApiKey\": \"env://OPENAI_API_KEY\", // or \"sk-...\" / \"file:///path/to/key\"\n // \"embeddingModel\": \"text-embedding-3-small\", // 1536 dims, auto-detected\n \n // ============================================\n // Web Server Settings\n // ============================================\n \n // Start a background import of past chats on each host's next start.\n \"autoBackfill\": true,\n // \"piBackfillModel\": \"inherit\", // or \"external\", or \"provider/model\"\n // \"opencodeBackfillModel\": \"inherit\", // or \"external\", or \"provider/model\"\n // Directory maps for history recorded in moved or deleted directories.\n // \"importPathMaps\": [{ \"from\": \"~/code/app-feat-x\", \"to\": \"~/code/app\" }],\n // Unresolved directories to hide on the Directory maps page. They stay unimported.\n // \"importIgnoredDirectories\": [\"/private/tmp/scratch\"],\n // Claude Code's folder, when it is not ~/.claude (like CLAUDE_CONFIG_DIR).\n // \"claudeConfigDir\": \"~/.claude-work\",\n\n // Register the web app to start when you log in.\n \"webServerAutoStart\": true,\n // Enable web UI for managing memories (accessible at http://localhost:4747)\n \"webServerEnabled\": true,\n \n // Port for web UI server\n \"webServerPort\": 4747,\n \n // Host address for web UI (use 127.0.0.1 for local only, 0.0.0.0 for network access)\n \"webServerHost\": \"127.0.0.1\",\n\n // Optional HTTP Basic Auth for the web UI. Accepts literal, env://, or file:// secrets.\n // \"webServerAuthPassword\": \"\",\n // \"webServerAuthUsername\": \"\",\n\n // Deprecated: imported once into the API token table, then no longer read.\n // Create API tokens on the Settings page (Keys and access) instead.\n // \"webServerApiToken\": \"env://OMMS_WEB_TOKEN\",\n \n // ============================================\n // Database Settings\n // ============================================\n \n // Maximum vectors per database shard (auto-creates new shard when limit reached)\n \"maxVectorsPerShard\": 50000,\n \n // Automatically delete old memories based on retention period\n \"autoCleanupEnabled\": true,\n \n // Days to keep memories before auto-cleanup (only if autoCleanupEnabled is true)\n \"autoCleanupRetentionDays\": 30,\n \n // Automatically detect and remove duplicate memories\n \"deduplicationEnabled\": true,\n \n // Similarity threshold (0-1) for detecting duplicates (higher = stricter)\n \"deduplicationSimilarityThreshold\": 0.90,\n \n // ============================================\n // Memory Scope Settings\n // ============================================\n\n // Default scope for memory list/search queries\n // \"project\" keeps queries within the current project, \"all-projects\" searches across all project shards\n \"memory\": {\n \"defaultScope\": \"project\"\n },\n\n // ============================================\n // Model for auto-capture and profile learning (same rule in OpenCode and Pi)\n // ============================================\n\n // Which model summarises your work, in this order:\n // 1. The host model below: opencodeProvider/opencodeModel in OpenCode,\n // piProvider/piModel in Pi. Set the model to \"inherit\" to follow\n // whatever model the session is using, or to \"external\" to send\n // every call to the external API below.\n // 2. If no host model is set: the external API (memoryModel/memoryApiUrl/\n // memoryApiKey further down).\n // 3. If neither is set: the session's own model.\n // If the host model fails and the external API is configured, the external\n // API is used instead.\n //\n // Host models go through OpenCode's or Pi's own sign-in (OAuth like Claude\n // Pro/Max, GitHub Copilot, ChatGPT, or your own key there), so no separate API\n // key is needed here.\n //\n // Examples (OpenCode names from 'opencode providers list'; Pi names from its model list):\n // \"opencodeProvider\": \"anthropic\", \"opencodeModel\": \"claude-haiku-4-5-20251001\"\n // \"opencodeProvider\": \"openai\", \"opencodeModel\": \"inherit\"\n // \"piProvider\": \"openai-codex\", \"piModel\": \"gpt-5.6-luna\"\n //\n // \"opencodeProvider\": \"anthropic\",\n // \"opencodeModel\": \"claude-haiku-4-5-20251001\",\n // \"piProvider\": \"openai-codex\",\n // \"piModel\": \"gpt-5.6-luna\",\n\n // ============================================\n // Auto-Capture Settings\n // ============================================\n \n // Auto-capture runs in the background without blocking your main session.\n // Note: Ollama may not support tool calling. Use OpenAI, Anthropic, or Groq for best results.\n \n \"autoCaptureEnabled\": true,\n \n // Provider type: \"openai-chat\" | \"openai-responses\" | \"anthropic\" | \"minimax\" | \"orcarouter\" | \"google-gemini\"\n // Note: \"openai-chat\" is a generic OpenAI API-compatible mode.\n // Any service that follows the OpenAI Chat Completions API can use it via custom \"memoryApiUrl\".\n \"memoryProvider\": \"openai-chat\",\n \n // External API (step 2 above, and the fallback when a host model fails).\n // Uncomment all 3 lines and replace memoryApiKey before use:\n // \"memoryModel\": \"gpt-4o-mini\",\n // \"memoryApiUrl\": \"https://api.openai.com/v1\",\n // \"memoryApiKey\": \"sk-...\",\n\n // API Key Formats:\n // Direct value: \"sk-...\"\n // From file: \"file://~/.config/litellm-key.txt\"\n // From env variable: \"env://LITELLM_API_KEY\"\n \n // Examples for different providers:\n // Any OpenAI-compatible endpoint can use the \"openai-chat\" provider pattern below.\n // Common examples: DeepSeek, Qwen (via Alibaba Cloud ModelStudio),\n // Zhipu GLM (BigModel platform), and Kimi (Moonshot AI platform).\n\n // OpenAI Chat Completion (default, backward compatible):\n // \"memoryProvider\": \"openai-chat\"\n // \"memoryModel\": \"gpt-4o-mini\"\n // \"memoryApiUrl\": \"https://api.openai.com/v1\"\n // \"memoryApiKey\": \"sk-...\"\n\n // DeepSeek (OpenAI-compatible example):\n // \"memoryProvider\": \"openai-chat\"\n // \"memoryModel\": \"deepseek-chat\"\n // \"memoryApiUrl\": \"https://api.deepseek.com/v1\"\n // \"memoryApiKey\": \"sk-...\"\n \n // OpenAI Responses API (recommended, with session support):\n // \"memoryProvider\": \"openai-responses\"\n // \"memoryModel\": \"gpt-4o\"\n // \"memoryApiUrl\": \"https://api.openai.com/v1\"\n // \"memoryApiKey\": \"sk-...\"\n \n // Anthropic (with session support):\n // \"memoryProvider\": \"anthropic\"\n // \"memoryModel\": \"claude-3-5-haiku-20241022\"\n // \"memoryApiUrl\": \"https://api.anthropic.com/v1\"\n // \"memoryApiKey\": \"sk-ant-...\"\n\n // MiniMax (Anthropic Messages-compatible endpoint, with session support):\n // \"memoryProvider\": \"minimax\"\n // \"memoryModel\": \"MiniMax-M3\"\n // \"memoryApiUrl\": \"https://api.minimax.io\" // global endpoint\n // \"memoryApiKey\": \"<MiniMax API key>\"\n // // China endpoint: \"memoryApiUrl\": \"https://api.minimaxi.com\"\n // // Optional adaptive thinking for MiniMax-M3:\n // \"memoryExtraParams\": { \"thinking\": { \"type\": \"adaptive\" } }\n\n // OrcaRouter (OpenAI-compatible gateway, namespaced model IDs, with session support):\n // \"memoryProvider\": \"orcarouter\"\n // \"memoryApiKey\": \"<OrcaRouter API key>\"\n // // memoryApiUrl and memoryModel are optional \u2014 they default to\n // // https://api.orcarouter.ai/v1 and \"orcarouter/auto\" (a routing alias).\n // // OrcaRouter rejects bare model names, so if you set memoryModel, use a\n // // namespaced ID such as \"openai/gpt-5.5\" or \"deepseek/deepseek-v4-flash\".\n // \"memoryModel\": \"openai/gpt-5.5\"\n\n // Groq (OpenAI-compatible, use openai-chat provider):\n // \"memoryProvider\": \"openai-chat\"\n // \"memoryModel\": \"llama-3.3-70b-versatile\"\n // \"memoryApiUrl\": \"https://api.groq.com/openai/v1\"\n // \"memoryApiKey\": \"gsk_...\"\n \n // Maximum iterations for multi-turn AI analysis (for openai-responses, anthropic, and minimax)\n \"autoCaptureMaxIterations\": 5,\n \n // Timeout per iteration in milliseconds (30 seconds default)\n \"autoCaptureIterationTimeout\": 30000,\n\n // Tries for one turn's capture, on Pi and OpenCode, 2 and 4 seconds apart. After the\n // last one fails, a turn the model could not reach goes to the capture retry queue.\n \"autoCaptureMaxRetries\": 3,\n\n // Maximum UTF-8 bytes for the auto-capture markdown context sent to the summary model.\n // Prevents HTTP 400 context overflows on models with ~131K token windows (e.g. Groq Llama).\n // Rough guide: tokens \u2248 bytes / 4 for mixed code/prose.\n \"autoCaptureMaxContextBytes\": 131072,\n \n // Days to keep AI session history before cleanup\n \"aiSessionRetentionDays\": 7,\n\n // Capture diagnostics: every capture attempt always writes one metadata line\n // (model, stop reason, sizes, outcome) to ~/.omms/omms.log, with no\n // conversation text. Set captureTrace to true to also write each attempt's\n // prompt and raw reply to ~/.omms/traces/ for debugging. Traces can contain\n // conversation content: <private> text and common API key formats are\n // redacted, files are readable only by you, and they are deleted after\n // captureTraceRetentionDays. Only this global file can turn tracing on.\n // \"captureTrace\": false,\n // \"captureTraceRetentionDays\": 7,\n // \"captureAttemptRetentionDays\": 30,\n\n // Skill learning: record each full turn (prompt, replies, tool calls with\n // their outcome, and the skills used) in ~/.omms/data/skills.db, so OMMS can\n // rank your skills and draft better versions. Off by default. A project config\n // can turn it off but never on. No model call is made to record a turn.\n // \"skillLearning\": false,\n // Remove <private> text and common API key formats before a turn is stored.\n // Global config only. Turning this off stores text as written.\n // \"skillLearningRedact\": true,\n // Folders OMMS reads skills from. Absolute or starting with ~/. Global config\n // only. Unset: ~/.agents/skills and the Claude Code folder's skills folder.\n // \"skillFolders\": [\"~/.agents/skills\"],\n\n // Visible recall: the OpenCode tui entrypoint draws a row in the session for\n // each memory recall. On by default. A project config can turn it off but\n // never on.\n // \"visibleRecall\": true,\n\n // Capture retry queue: when a live capture fails because the model cannot be\n // reached (network error, timeout, HTTP 408, 429 or 5xx), OMMS keeps a copy of\n // the turn in ~/.omms/data and tries it again later. The copy has <private>\n // text and common API key formats removed. Turns are deleted when the retry\n // ends or after captureRetryRetentionHours (0 to 720). 0 turns the queue off.\n // Only this global file can set it.\n // \"captureRetryRetentionHours\": 72,\n\n // Temperature for AI API requests (set to false to omit parameter for models that don't support it)\n // Some reasoning models (like o1, o3, gpt-5) don't support temperature parameter\n // Set to false and add \"memoryTemperature\": false in config when using such models\n \"memoryTemperature\": 0.3,\n\n // Extra parameters to include in API request body\n // Useful for local inference servers (e.g. llama-server with --jinja) that support\n // additional parameters like disabling thinking/reasoning mode\n // Example for Qwen3 models: { \"enable_thinking\": false }\n // \"memoryExtraParams\": {},\n\n // Language for auto-capture summaries (default: \"auto\" for auto-detection)\n // Options: \"auto\", \"en\", \"id\", \"zh\", \"ja\", \"es\", \"fr\", \"de\", \"ru\", \"pt\", \"ar\", \"ko\"\n // \"autoCaptureLanguage\": \"auto\",\n\n // ============================================\n // Toast Notifications\n // ============================================\n\n // Show toast when memory is auto-captured\n \"showAutoCaptureToasts\": true,\n\n // Show toast when user profile is updated\n \"showUserProfileToasts\": true,\n\n // Show toast for error messages\n \"showErrorToasts\": true,\n\n // ============================================\n // User Profile System\n // ============================================\n\n // Analyze user prompts every N prompts to build/update your user profile\n // When N uncaptured prompts accumulate, AI will analyze them to identify:\n // - User preferences (code style, communication style, tool preferences)\n // - User patterns (recurring topics, problem domains, technical interests)\n // - User workflows (development habits, sequences, learning style)\n // - Skill level (overall and per-domain assessment)\n \"userProfileAnalysisInterval\": 10,\n\n // Days before inactive items (all types) are eligible for removal\n \"userProfileStaleDays\": 2,\n\n // Days before an inactive workflow is eligible for removal. A workflow seen\n // userProfileMinEvidenceForRetention times or more is kept.\n \"userProfileWorkflowStaleDays\": 30,\n\n // Number of preferences shown in UI\n \"userProfileDisplayPreferences\": 20,\n \n // Number of patterns shown in UI\n \"userProfileDisplayPatterns\": 15,\n \n // Number of workflows shown in UI\n \"userProfileDisplayWorkflows\": 10,\n \n // Number of preferences injected into LLM conversation context\n // Keep this small \u2014 the strongest signals are enough; more dilute LLM attention\n \"userProfileInjectPreferences\": 5,\n \n // Number of patterns injected into LLM conversation context\n \"userProfileInjectPatterns\": 5,\n \n // Number of workflows injected into LLM conversation context\n \"userProfileInjectWorkflows\": 3,\n \n // Days before preference confidence starts to decay (if not reinforced)\n // Preferences that aren't seen again will gradually lose confidence and be removed\n \"userProfileConfidenceDecayDays\": 30,\n \n // Number of profile versions to keep in changelog (for rollback/debugging)\n // Older versions are automatically cleaned up\n \"userProfileChangelogRetentionCount\": 5,\n\n // Minimum evidence count for a preference/pattern to survive confidence decay\n // Items confirmed fewer times are more likely to be pruned when confidence decays\n \"userProfileMinEvidenceForRetention\": 3,\n\n // Merge duplicate or vague profile items every N analysed prompts (about 1 model call per 100 items)\n \"userProfileAutoCleanupEnabled\": true,\n \"userProfileAutoCleanupInterval\": 500,\n\n // Enable LLM validation of existing preferences against recent behavior.\n // When enabled, each analysis round checks if top-5 preferences still match recent prompts.\n // Experimental \u2014 disabled by default.\n \"userProfileValidationEnabled\": false,\n\n // ============================================\n // Search Settings\n // ============================================\n \n // Minimum similarity score (0-1) for memory search results. Leave unset to\n // follow the default for your embedding model: 0.5 for qwen3-embedding:0.6b\n // (the only model measured), 0.6 for every other embedder.\n // \"similarityThreshold\": 0.5,\n\n // Maximum number of memories to return in search results.\n // Manual searches can ask for fewer; prompt retrieval uses this ceiling.\n \"maxMemories\": 10,\n\n // ============================================\n // Memory Context Controls\n // ============================================\n\n // Recent memories added at fresh session start (OpenCode V1 and Claude Code).\n // Pi and OpenCode V2 search the store with each prompt instead.\n \"chatMessage\": {\n \"enabled\": true,\n // Recent memories injected at session start.\n \"maxMemories\": 3,\n \"excludeCurrentSession\": true,\n // When to inject: \"first\" message of a session or \"always\".\n // \"injectOn\": \"first\",\n },\n\n // OpenCode profile-learning input limit, in UTF-8 bytes. Smaller values can\n // omit prompts from that input. Does not limit Pi or Claude Code profile input.\n \"userProfileMaxContextBytes\": 32768,\n\n // Shared budget for automatic memory context (profile text, memories, and\n // formatting) across all hosts, in approximate tokens: ceil(UTF-8 bytes / 4).\n // Covers prompt retrieval, recent-memory injection, and session-memory\n // restoration. Not an exact provider token limit or a spending limit.\n \"retrievalMaxTokens\": 2000,\n\n // ============================================\n // Advanced Settings\n // ============================================\n \n // Inject user profile into AI context (preferences, patterns, workflows)\n \"injectProfile\": true\n}\n";
|
package/dist/config-template.js
CHANGED
|
@@ -254,6 +254,11 @@ export const CONFIG_TEMPLATE = `{
|
|
|
254
254
|
// only. Unset: ~/.agents/skills and the Claude Code folder's skills folder.
|
|
255
255
|
// "skillFolders": ["~/.agents/skills"],
|
|
256
256
|
|
|
257
|
+
// Visible recall: the OpenCode tui entrypoint draws a row in the session for
|
|
258
|
+
// each memory recall. On by default. A project config can turn it off but
|
|
259
|
+
// never on.
|
|
260
|
+
// "visibleRecall": true,
|
|
261
|
+
|
|
257
262
|
// Capture retry queue: when a live capture fails because the model cannot be
|
|
258
263
|
// reached (network error, timeout, HTTP 408, 429 or 5xx), OMMS keeps a copy of
|
|
259
264
|
// the turn in ~/.omms/data and tries it again later. The copy has <private>
|
package/dist/config.d.ts
CHANGED
|
@@ -188,6 +188,7 @@ declare function buildConfig(fileConfig: OmmsConfig): {
|
|
|
188
188
|
maxAgeDays: number | undefined;
|
|
189
189
|
injectOn: "first" | "always";
|
|
190
190
|
};
|
|
191
|
+
visibleRecall: boolean;
|
|
191
192
|
skillLearning: boolean;
|
|
192
193
|
skillLearningRedact: boolean;
|
|
193
194
|
skillFolders: string[] | undefined;
|
|
@@ -298,6 +299,7 @@ export declare let CONFIG: {
|
|
|
298
299
|
maxAgeDays: number | undefined;
|
|
299
300
|
injectOn: "first" | "always";
|
|
300
301
|
};
|
|
302
|
+
visibleRecall: boolean;
|
|
301
303
|
skillLearning: boolean;
|
|
302
304
|
skillLearningRedact: boolean;
|
|
303
305
|
skillFolders: string[] | undefined;
|
package/dist/config.js
CHANGED
|
@@ -10,6 +10,7 @@ import { getAutoCaptureProviderStatus } from "./services/ai/live-model-choice.js
|
|
|
10
10
|
import { parseBackfillModel } from "./importer/backfill-model.js";
|
|
11
11
|
import { parseIgnoredDirectories, parseImportPathMaps } from "./importer/import-path-maps.js";
|
|
12
12
|
import { normaliseSkillConfig, projectSkillOverride, } from "./services/skill-learning/skill-config.js";
|
|
13
|
+
import * as visibleRecall from "./services/visible-recall-config.js";
|
|
13
14
|
import { checkedMemoryLimit, MEMORY_LIMITS } from "./utils/memory-limits.js";
|
|
14
15
|
import { defaultSimilarityThreshold } from "./utils/threshold-default.js";
|
|
15
16
|
import { RERANKER_TIMEOUT_DEFAULT_MS } from "./services/request-timeouts.js";
|
|
@@ -371,6 +372,7 @@ function buildConfig(fileConfig) {
|
|
|
371
372
|
captureAttemptRetentionDays: normalizeAutoCleanupRetentionDays(fileConfig.captureAttemptRetentionDays ?? DEFAULTS.captureAttemptRetentionDays),
|
|
372
373
|
captureRetryRetentionHours: normalizeCaptureRetryRetentionHours(fileConfig.captureRetryRetentionHours),
|
|
373
374
|
...normaliseSkillConfig(fileConfig),
|
|
375
|
+
...visibleRecall.visibleRecallFromFile(fileConfig),
|
|
374
376
|
autoBackfill: fileConfig.autoBackfill ?? DEFAULTS.autoBackfill,
|
|
375
377
|
opencodeBackfillModel: fileConfig.opencodeBackfillModel ?? DEFAULTS.opencodeBackfillModel,
|
|
376
378
|
piBackfillModel: fileConfig.piBackfillModel ?? DEFAULTS.piBackfillModel,
|
|
@@ -543,6 +545,7 @@ export function initConfig(directory, options = {}) {
|
|
|
543
545
|
delete projectOverrides.skillLearningRedact;
|
|
544
546
|
delete projectOverrides.skillFolders;
|
|
545
547
|
Object.assign(projectOverrides, skillRule.override);
|
|
548
|
+
visibleRecall.applyProjectVisibleRecall(projectConfig, projectOverrides, directory, log);
|
|
546
549
|
delete projectOverrides.autoBackfill;
|
|
547
550
|
delete projectOverrides.opencodeBackfillModel;
|
|
548
551
|
delete projectOverrides.piBackfillModel;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** One memory a recall injected, reduced to what a fold row shows. */
|
|
2
|
+
export type RecallItem = {
|
|
3
|
+
relevance: number;
|
|
4
|
+
id?: string;
|
|
5
|
+
headline: string;
|
|
6
|
+
};
|
|
7
|
+
/** One recall as OMMS made it: a prompt search, or the recent memories a session opens with. */
|
|
8
|
+
export type RecallSummary = {
|
|
9
|
+
mode: "search" | "recent";
|
|
10
|
+
query: string | null;
|
|
11
|
+
items: RecallItem[];
|
|
12
|
+
/** Per-session counter the v2 reporter sets when it stores the summary; the TUI keys rows by it. */
|
|
13
|
+
seq?: number;
|
|
14
|
+
};
|
|
15
|
+
/** Query on one line, cut to `max` characters. */
|
|
16
|
+
export declare function queryLine(query: string, max?: number): string;
|
|
17
|
+
/** One line that names a memory: the first line under `## Request`, else the first line. */
|
|
18
|
+
export declare function headline(body: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* Summarise a formatted retrieval section for a host to show. Returns null when the section holds
|
|
21
|
+
* no memory (a profile-only section included), so the host draws nothing. A `recent` recall has
|
|
22
|
+
* no query, whatever the caller passes.
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildRecallSummary(section: string, mode: "search" | "recent", query: string | null): RecallSummary | null;
|
|
25
|
+
/** `memory [mode=search, query="...", limit=4]`: the call's name and arguments, as a tool row shows them. */
|
|
26
|
+
export declare function callTitle(summary: RecallSummary): string;
|
|
27
|
+
/** Body of an unfolded row, one string per line, without indentation (the host indents). */
|
|
28
|
+
export declare function unfoldLines(summary: RecallSummary): string[];
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const QUERY_CUT = 60;
|
|
2
|
+
const UNFOLDED_QUERY_CUT = 300;
|
|
3
|
+
const HEADLINE_CUT = 140;
|
|
4
|
+
/** Query on one line, cut to `max` characters. */
|
|
5
|
+
export function queryLine(query, max = QUERY_CUT) {
|
|
6
|
+
const line = query.replace(/\s+/g, " ").trim();
|
|
7
|
+
return line.length > max ? `${line.slice(0, max - 3)}...` : line;
|
|
8
|
+
}
|
|
9
|
+
// Entry shape comes from `formatContextForPrompt`: `<memory id="..." relevance="N%">`, with the
|
|
10
|
+
// id missing when the result had none. A body ends at its own `</memory>` when another entry, a
|
|
11
|
+
// section close or the end of the text follows it. The Claude Code hook can cut a block short and
|
|
12
|
+
// leave tags open, so a body also ends at a section close or at the end of the text. An opening
|
|
13
|
+
// tag inside the text, even at the start of a line, does not end it: a memory can quote the tag.
|
|
14
|
+
const SECTION_CLOSE = String.raw `<\/project_knowledge>|<\/memory_context>|<\/omms-retrieval>`;
|
|
15
|
+
const MEMORY_ENTRY = new RegExp(String.raw `<memory(?:\s+id="([^"]*)")?\s+relevance="(\d+)%">([\s\S]*?)` +
|
|
16
|
+
String.raw `(?=<\/memory>\s*(?:<memory\s|${SECTION_CLOSE}|$)|${SECTION_CLOSE}|$)`, "g");
|
|
17
|
+
/** One line that names a memory: the first line under `## Request`, else the first line. */
|
|
18
|
+
export function headline(body) {
|
|
19
|
+
const lines = body
|
|
20
|
+
.replace(/<\/memory>\s*$/, "")
|
|
21
|
+
.split("\n")
|
|
22
|
+
.map((line) => line.trim())
|
|
23
|
+
.filter(Boolean);
|
|
24
|
+
const heading = lines.findIndex((line) => /^##\s*request\s*$/i.test(line));
|
|
25
|
+
const line = (heading >= 0 ? lines[heading + 1] : lines[0]) ?? "";
|
|
26
|
+
return line.length > HEADLINE_CUT ? `${line.slice(0, HEADLINE_CUT - 3)}...` : line;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Summarise a formatted retrieval section for a host to show. Returns null when the section holds
|
|
30
|
+
* no memory (a profile-only section included), so the host draws nothing. A `recent` recall has
|
|
31
|
+
* no query, whatever the caller passes.
|
|
32
|
+
*/
|
|
33
|
+
export function buildRecallSummary(section, mode, query) {
|
|
34
|
+
const items = [];
|
|
35
|
+
for (const match of section.matchAll(MEMORY_ENTRY)) {
|
|
36
|
+
const text = headline(match[3] ?? "");
|
|
37
|
+
if (!text)
|
|
38
|
+
continue;
|
|
39
|
+
const item = { relevance: Number(match[2]), headline: text };
|
|
40
|
+
if (match[1])
|
|
41
|
+
item.id = match[1];
|
|
42
|
+
items.push(item);
|
|
43
|
+
}
|
|
44
|
+
if (items.length === 0)
|
|
45
|
+
return null;
|
|
46
|
+
return { mode, query: mode === "recent" ? null : query, items };
|
|
47
|
+
}
|
|
48
|
+
/** Escape backslashes and double quotes so the quoted value in the title reads as one string. */
|
|
49
|
+
function escapeQuoted(text) {
|
|
50
|
+
return text.replace(/[\\"]/g, "\\$&");
|
|
51
|
+
}
|
|
52
|
+
/** `memory [mode=search, query="...", limit=4]`: the call's name and arguments, as a tool row shows them. */
|
|
53
|
+
export function callTitle(summary) {
|
|
54
|
+
const args = [`mode=${summary.mode}`];
|
|
55
|
+
if (summary.query)
|
|
56
|
+
args.push(`query="${escapeQuoted(queryLine(summary.query))}"`);
|
|
57
|
+
args.push(`limit=${summary.items.length}`);
|
|
58
|
+
return `memory [${args.join(", ")}]`;
|
|
59
|
+
}
|
|
60
|
+
/** Body of an unfolded row, one string per line, without indentation (the host indents). */
|
|
61
|
+
export function unfoldLines(summary) {
|
|
62
|
+
const lines = [`mode: ${summary.mode}`];
|
|
63
|
+
if (summary.query)
|
|
64
|
+
lines.push(`query: ${queryLine(summary.query, UNFOLDED_QUERY_CUT)}`);
|
|
65
|
+
lines.push(`limit: ${summary.items.length}`);
|
|
66
|
+
summary.items.forEach((item, index) => {
|
|
67
|
+
const id = item.id ? ` id=${item.id}` : "";
|
|
68
|
+
lines.push(`${index + 1}. ${item.relevance}%${id} ${item.headline}`);
|
|
69
|
+
});
|
|
70
|
+
return lines;
|
|
71
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -161,7 +161,7 @@ export async function configureOpencodeHostTransport(ctx) {
|
|
|
161
161
|
if (hostConfig.fetch) {
|
|
162
162
|
setHostFetch(hostConfig.fetch);
|
|
163
163
|
}
|
|
164
|
-
else {
|
|
164
|
+
else if (!hostConfig.inProcess) {
|
|
165
165
|
log("OpenCode host fetch unavailable; falling back to global fetch", {
|
|
166
166
|
clientKeys: hostConfig.clientKeys,
|
|
167
167
|
sdkConfigCount: hostConfig.sdkConfigCount,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Like setInterval, but skips a tick while the previous run of the task is still pending. */
|
|
2
|
+
export function setSingleFlightInterval(task, ms) {
|
|
3
|
+
let running = false;
|
|
4
|
+
return setInterval(() => {
|
|
5
|
+
if (running)
|
|
6
|
+
return;
|
|
7
|
+
running = true;
|
|
8
|
+
void task().finally(() => {
|
|
9
|
+
running = false;
|
|
10
|
+
});
|
|
11
|
+
}, ms);
|
|
12
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `visibleRecall` setting (change add-opencode-v2-visible-recall), kept out
|
|
3
|
+
* of `src/config.ts` to save its lines. Pure on purpose: callers pass the file
|
|
4
|
+
* config and a log function, and the loaded `CONFIG` is never imported here,
|
|
5
|
+
* because many tests stub `src/config.js`.
|
|
6
|
+
*
|
|
7
|
+
* Read the effective value from `CONFIG.visibleRecall`. It is on by default,
|
|
8
|
+
* and only an explicit `false` turns it off.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* The key as it appears in a config file. `OmmsConfig` in `src/config.ts` has
|
|
12
|
+
* no field for it, to save lines, so the functions take `FileConfigWithKey`,
|
|
13
|
+
* which any config object satisfies. A hand-edited file can hold any value,
|
|
14
|
+
* which is why they compare with `=== false`.
|
|
15
|
+
*/
|
|
16
|
+
export interface VisibleRecallFileKey {
|
|
17
|
+
/** Draw a row in the OpenCode TUI for each memory recall. Default true. A project can only turn it off. */
|
|
18
|
+
visibleRecall?: boolean;
|
|
19
|
+
}
|
|
20
|
+
type FileConfigWithKey = VisibleRecallFileKey & object;
|
|
21
|
+
/** Read the setting from a merged file config. Unset means on. */
|
|
22
|
+
export declare function visibleRecallFromFile(fileConfig: FileConfigWithKey): {
|
|
23
|
+
visibleRecall: boolean;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Apply the project rule to `overrides`, the copy of the project config that
|
|
27
|
+
* is merged over the global one. A project may turn the rows off, so a value
|
|
28
|
+
* of `false` stays. Any other project value is dropped, so the global value
|
|
29
|
+
* decides. A project value of `true` is also reported through `log`.
|
|
30
|
+
*/
|
|
31
|
+
export declare function applyProjectVisibleRecall(project: FileConfigWithKey, overrides: FileConfigWithKey, directory: string, log: (message: string, data: {
|
|
32
|
+
directory: string;
|
|
33
|
+
}) => void): void;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `visibleRecall` setting (change add-opencode-v2-visible-recall), kept out
|
|
3
|
+
* of `src/config.ts` to save its lines. Pure on purpose: callers pass the file
|
|
4
|
+
* config and a log function, and the loaded `CONFIG` is never imported here,
|
|
5
|
+
* because many tests stub `src/config.js`.
|
|
6
|
+
*
|
|
7
|
+
* Read the effective value from `CONFIG.visibleRecall`. It is on by default,
|
|
8
|
+
* and only an explicit `false` turns it off.
|
|
9
|
+
*/
|
|
10
|
+
/** Read the setting from a merged file config. Unset means on. */
|
|
11
|
+
export function visibleRecallFromFile(fileConfig) {
|
|
12
|
+
return { visibleRecall: fileConfig.visibleRecall !== false };
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Apply the project rule to `overrides`, the copy of the project config that
|
|
16
|
+
* is merged over the global one. A project may turn the rows off, so a value
|
|
17
|
+
* of `false` stays. Any other project value is dropped, so the global value
|
|
18
|
+
* decides. A project value of `true` is also reported through `log`.
|
|
19
|
+
*/
|
|
20
|
+
export function applyProjectVisibleRecall(project, overrides, directory, log) {
|
|
21
|
+
if (project.visibleRecall === false)
|
|
22
|
+
return;
|
|
23
|
+
delete overrides.visibleRecall;
|
|
24
|
+
if (project.visibleRecall === true) {
|
|
25
|
+
log("Project config cannot turn on visibleRecall; the value was ignored", { directory });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -48,6 +48,7 @@ export declare class WebServer {
|
|
|
48
48
|
private holdOffTimer;
|
|
49
49
|
private portsExhaustedNotified;
|
|
50
50
|
private takeoverFailures;
|
|
51
|
+
private lastHealthFailure;
|
|
51
52
|
private readonly maxFallbackPort;
|
|
52
53
|
private settingsImportJobs?;
|
|
53
54
|
private backfillControlsInstance?;
|
|
@@ -99,6 +100,8 @@ export declare class WebServer {
|
|
|
99
100
|
isRunning(): boolean;
|
|
100
101
|
isServerOwner(): boolean;
|
|
101
102
|
getUrl(): string;
|
|
103
|
+
/** Why the last `checkServerAvailable` call said no, or null when it said yes. */
|
|
104
|
+
getLastHealthFailure(): string | null;
|
|
102
105
|
checkServerAvailable(): Promise<boolean>;
|
|
103
106
|
private handleRequest;
|
|
104
107
|
private contentTypeFor;
|
|
@@ -13,10 +13,13 @@ import { AUTH_HEADER, getOrCreateAuthToken, isAuthorizedApiRequest } from "./aut
|
|
|
13
13
|
import { WebAuth } from "./web-auth.js";
|
|
14
14
|
import { NODE_HTTP_IDLE_TIMEOUT_MS } from "./request-timeouts.js";
|
|
15
15
|
import { testReranker } from "./reranker-test.js";
|
|
16
|
+
import { setSingleFlightInterval } from "./single-flight.js";
|
|
16
17
|
import { handleListTags, handleListKeywords, handleListMemories, handleAddMemory, handleDeleteMemory, handleBulkDelete, handleUpdateMemory, handleSearch, handleStats, handlePinMemory, handleUnpinMemory, handleRunCleanup, handleRunDeduplication, handleDetectMigration, handleRunMigration, handleDetectTagMigration, handleRunTagMigrationBatch, handleGetTagMigrationProgress, handleDeletePrompt, handleBulkDeletePrompts, handleGetUserProfile, handleGetProfileChangelog, handleGetProfileSnapshot, handleRefreshProfile, handleUpdateProfileItem, } from "./api-handlers.js";
|
|
17
18
|
import { routeModules } from "./web-route-modules.js";
|
|
18
19
|
const isBun = typeof globalThis.Bun !== "undefined";
|
|
19
20
|
const MIN_FAILED_TAKEOVERS = 3;
|
|
21
|
+
/** A busy owner can answer late. Two seconds was short enough to call a live owner dead. */
|
|
22
|
+
const HEALTH_CHECK_TIMEOUT_MS = 8_000;
|
|
20
23
|
export function attachNodeDisconnectHandlers(req, res, onDisconnect) {
|
|
21
24
|
let disconnected = false;
|
|
22
25
|
const disconnectOnce = () => {
|
|
@@ -175,6 +178,7 @@ export class WebServer {
|
|
|
175
178
|
holdOffTimer = null;
|
|
176
179
|
portsExhaustedNotified = false;
|
|
177
180
|
takeoverFailures = 0;
|
|
181
|
+
lastHealthFailure = null;
|
|
178
182
|
maxFallbackPort;
|
|
179
183
|
settingsImportJobs;
|
|
180
184
|
backfillControlsInstance;
|
|
@@ -584,7 +588,7 @@ export class WebServer {
|
|
|
584
588
|
if (this.healthCheckInterval) {
|
|
585
589
|
return;
|
|
586
590
|
}
|
|
587
|
-
this.healthCheckInterval =
|
|
591
|
+
this.healthCheckInterval = setSingleFlightInterval(() => this.waiterTick(), 5000);
|
|
588
592
|
}
|
|
589
593
|
/** One check of a web app that waits for the port. */
|
|
590
594
|
async waiterTick() {
|
|
@@ -667,6 +671,8 @@ export class WebServer {
|
|
|
667
671
|
log("Web server port held by a non-responsive process; falling back to next port", {
|
|
668
672
|
previousPort: this.config.port,
|
|
669
673
|
newPort: nextPort,
|
|
674
|
+
lastFailure: this.lastHealthFailure,
|
|
675
|
+
healthTimeoutMs: HEALTH_CHECK_TIMEOUT_MS,
|
|
670
676
|
}, "warn");
|
|
671
677
|
this.config.port = nextPort;
|
|
672
678
|
}
|
|
@@ -743,6 +749,10 @@ export class WebServer {
|
|
|
743
749
|
getUrl() {
|
|
744
750
|
return webServerUrl(this.config.host, this.config.port);
|
|
745
751
|
}
|
|
752
|
+
/** Why the last `checkServerAvailable` call said no, or null when it said yes. */
|
|
753
|
+
getLastHealthFailure() {
|
|
754
|
+
return this.lastHealthFailure;
|
|
755
|
+
}
|
|
746
756
|
async checkServerAvailable() {
|
|
747
757
|
try {
|
|
748
758
|
// Only a web app on this machine answers here, so the local token file is enough.
|
|
@@ -751,20 +761,28 @@ export class WebServer {
|
|
|
751
761
|
const response = await fetch(`${this.getUrl()}${endpoint}`, {
|
|
752
762
|
method: "GET",
|
|
753
763
|
headers,
|
|
754
|
-
signal: AbortSignal.timeout(
|
|
764
|
+
signal: AbortSignal.timeout(HEALTH_CHECK_TIMEOUT_MS),
|
|
755
765
|
});
|
|
756
|
-
if (!response.ok)
|
|
766
|
+
if (!response.ok) {
|
|
767
|
+
this.lastHealthFailure = `http ${response.status}`;
|
|
757
768
|
return false;
|
|
769
|
+
}
|
|
758
770
|
// Fallback spans 10 neighbor ports; any 2xx from an unrelated local
|
|
759
771
|
// service must not be mistaken for an omms owner. Require the
|
|
760
772
|
// response to carry our API envelope.
|
|
761
773
|
const body = (await response.json());
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
return body.success === true;
|
|
774
|
+
const ok = body.success === true && (endpoint !== "/api/health" || body.status === "ok");
|
|
775
|
+
this.lastHealthFailure = ok ? null : "unexpected response";
|
|
776
|
+
return ok;
|
|
766
777
|
}
|
|
767
|
-
catch {
|
|
778
|
+
catch (error) {
|
|
779
|
+
const name = error instanceof Error ? error.name : "";
|
|
780
|
+
this.lastHealthFailure =
|
|
781
|
+
name === "TimeoutError"
|
|
782
|
+
? "timeout"
|
|
783
|
+
: error instanceof Error
|
|
784
|
+
? error.message
|
|
785
|
+
: String(error);
|
|
768
786
|
return false;
|
|
769
787
|
}
|
|
770
788
|
}
|
package/dist/v2/adapter.js
CHANGED
|
@@ -4,6 +4,7 @@ import { SKILLS_TOOL_INPUT_SCHEMA } from "../core/skills-tool.js";
|
|
|
4
4
|
import { log } from "../services/logger.js";
|
|
5
5
|
import { utf8ByteLength } from "../utils/context-limit.js";
|
|
6
6
|
import { eventBelongsToLocation, legacyToolResult, toLegacyEvent } from "./legacy-client.js";
|
|
7
|
+
import { createRecallReporter } from "./recall-reporter.js";
|
|
7
8
|
const memoryInput = {
|
|
8
9
|
type: "object",
|
|
9
10
|
properties: {
|
|
@@ -75,7 +76,9 @@ export async function registerV2Adapter(ctx, legacy, memory) {
|
|
|
75
76
|
// until their message appears in a model request's history (the context
|
|
76
77
|
// hook), and are only then recorded for auto-capture and profile learning.
|
|
77
78
|
const pendingPrompts = new Map();
|
|
79
|
+
const recall = await createRecallReporter(ctx, memory, setBounded);
|
|
78
80
|
const forgetSession = (sessionID) => {
|
|
81
|
+
recall.forget(sessionID);
|
|
79
82
|
retrievals.delete(sessionID);
|
|
80
83
|
restored.delete(sessionID);
|
|
81
84
|
messageIDs.delete(sessionID);
|
|
@@ -186,7 +189,7 @@ export async function registerV2Adapter(ctx, legacy, memory) {
|
|
|
186
189
|
log("v2 prompt: memory retrieval failed", { sessionID, error: String(error) }, "error");
|
|
187
190
|
return null;
|
|
188
191
|
});
|
|
189
|
-
setBounded(retrievals, sessionID, { promise, budgetBytes });
|
|
192
|
+
setBounded(retrievals, sessionID, { promise, budgetBytes, query: text });
|
|
190
193
|
});
|
|
191
194
|
await ctx.session.hook("context", async (event) => {
|
|
192
195
|
const sessionID = event.sessionID;
|
|
@@ -201,6 +204,7 @@ export async function registerV2Adapter(ctx, legacy, memory) {
|
|
|
201
204
|
const section = await settledRetrieval(retrieval);
|
|
202
205
|
if (section) {
|
|
203
206
|
event.system.push({ type: "text", text: section });
|
|
207
|
+
recall.record(sessionID, retrieval, retrieval.query, section);
|
|
204
208
|
remainingBytes = retrieval.budgetBytes - utf8ByteLength(section);
|
|
205
209
|
}
|
|
206
210
|
else {
|
|
@@ -266,6 +270,7 @@ export async function registerV2Adapter(ctx, legacy, memory) {
|
|
|
266
270
|
restored.clear();
|
|
267
271
|
messageIDs.clear();
|
|
268
272
|
pendingPrompts.clear();
|
|
273
|
+
recall.clear();
|
|
269
274
|
await legacy.dispose?.();
|
|
270
275
|
};
|
|
271
276
|
}
|
package/dist/v2/legacy-client.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
+
import { markInProcessClient } from "../adapters/opencode/in-process-client.js";
|
|
3
4
|
function textFromParts(parts = []) {
|
|
4
5
|
return parts
|
|
5
6
|
.filter((part) => part.type === "text")
|
|
@@ -216,7 +217,7 @@ export function createLegacyClient(ctx) {
|
|
|
216
217
|
},
|
|
217
218
|
},
|
|
218
219
|
};
|
|
219
|
-
return client;
|
|
220
|
+
return markInProcessClient(client);
|
|
220
221
|
}
|
|
221
222
|
export function legacyToolResult(value) {
|
|
222
223
|
if (typeof value === "string")
|
|
@@ -8,6 +8,8 @@ export interface V2MemoryBridge {
|
|
|
8
8
|
isConfigured(): boolean;
|
|
9
9
|
isInjectionEnabled(): boolean;
|
|
10
10
|
isCompactionEnabled(): boolean;
|
|
11
|
+
/** Effective `visibleRecall` setting for this project, from a fresh config read. */
|
|
12
|
+
isVisibleRecallEnabled(): boolean;
|
|
11
13
|
isInternalPrompt(sessionID: string, text: string): boolean;
|
|
12
14
|
recordPrompt(sessionID: string, messageID: string, text: string): Promise<void>;
|
|
13
15
|
/**
|
package/dist/v2/memory-bridge.js
CHANGED
|
@@ -11,6 +11,10 @@ export function createV2MemoryBridge(directory) {
|
|
|
11
11
|
isConfigured,
|
|
12
12
|
isInjectionEnabled: () => isConfigured() && Boolean(CONFIG.chatMessage.enabled),
|
|
13
13
|
isCompactionEnabled: () => isConfigured() && Boolean(CONFIG.compaction.enabled),
|
|
14
|
+
isVisibleRecallEnabled() {
|
|
15
|
+
refreshConfigIfChanged(directory);
|
|
16
|
+
return CONFIG.visibleRecall !== false;
|
|
17
|
+
},
|
|
14
18
|
isInternalPrompt,
|
|
15
19
|
async recordPrompt(sessionID, messageID, text) {
|
|
16
20
|
await recordUserPrompt(sessionID, messageID, directory, text);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Context } from "@opencode/plugin/promise/plugin";
|
|
2
|
+
import type { V2MemoryBridge } from "./memory-bridge.js";
|
|
3
|
+
/** Latest summaries kept per session. The TUI shows recent rows, so older ones are dropped. */
|
|
4
|
+
export declare const MAX_RECALL_SUMMARIES = 20;
|
|
5
|
+
/** Longest query kept per summary. The TUI shows at most 300 characters of it. */
|
|
6
|
+
export declare const MAX_RECALL_QUERY_CHARS = 300;
|
|
7
|
+
type Bound = <V>(map: Map<string, V>, key: string, value: V) => void;
|
|
8
|
+
export interface RecallReporter {
|
|
9
|
+
/**
|
|
10
|
+
* Record the summary of one prompt retrieval. `token` identifies the retrieval: model steps
|
|
11
|
+
* that reuse it record once. A section with no memory entries records nothing, and neither
|
|
12
|
+
* does a subagent session (one with a parent). The call does not wait for the session lookup.
|
|
13
|
+
*/
|
|
14
|
+
record(sessionID: string, token: object, query: string, section: string | null): void;
|
|
15
|
+
forget(sessionID: string): void;
|
|
16
|
+
clear(): void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Keeps a bounded per-session ring of recall summaries and serves it over plugin RPC to the TUI
|
|
20
|
+
* entrypoint. It never throws and never touches what is injected. Logs carry the mode, count and
|
|
21
|
+
* session id only, never memory text or the query.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createRecallReporter(ctx: Context, memory: V2MemoryBridge, bound: Bound): Promise<RecallReporter>;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { buildRecallSummary } from "../core/recall-summary.js";
|
|
2
|
+
import { log } from "../services/logger.js";
|
|
3
|
+
import { recallRpc } from "./recall-rpc.js";
|
|
4
|
+
/** Latest summaries kept per session. The TUI shows recent rows, so older ones are dropped. */
|
|
5
|
+
export const MAX_RECALL_SUMMARIES = 20;
|
|
6
|
+
/** Longest query kept per summary. The TUI shows at most 300 characters of it. */
|
|
7
|
+
export const MAX_RECALL_QUERY_CHARS = 300;
|
|
8
|
+
/**
|
|
9
|
+
* Keeps a bounded per-session ring of recall summaries and serves it over plugin RPC to the TUI
|
|
10
|
+
* entrypoint. It never throws and never touches what is injected. Logs carry the mode, count and
|
|
11
|
+
* session id only, never memory text or the query.
|
|
12
|
+
*/
|
|
13
|
+
export async function createRecallReporter(ctx, memory, bound) {
|
|
14
|
+
const rings = new Map();
|
|
15
|
+
// Last number given per session. It keeps rising when the ring drops old entries.
|
|
16
|
+
const sequences = new Map();
|
|
17
|
+
const recorded = new WeakSet();
|
|
18
|
+
// One lookup per session. The promise is cached so concurrent records keep their order.
|
|
19
|
+
const subagents = new Map();
|
|
20
|
+
// Records waiting on a session lookup. `forget` and `clear` cancel them, so a late lookup cannot
|
|
21
|
+
// store a summary for a session whose state is gone. Entries leave when their lookup settles.
|
|
22
|
+
const pending = new Map();
|
|
23
|
+
let emit;
|
|
24
|
+
try {
|
|
25
|
+
const registration = await ctx.rpc.register(recallRpc, {
|
|
26
|
+
// The contract is plain JSON schema, so the host types the input as unknown.
|
|
27
|
+
list: async (input) => ({
|
|
28
|
+
enabled: memory.isVisibleRecallEnabled(),
|
|
29
|
+
items: [...(rings.get(input.sessionID) ?? [])],
|
|
30
|
+
}),
|
|
31
|
+
});
|
|
32
|
+
emit = (sessionID) => registration.events.emit("recalled", { sessionID });
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
log("v2 recall: rpc registration failed", { error: String(error) }, "error");
|
|
36
|
+
}
|
|
37
|
+
const notify = (sessionID) => {
|
|
38
|
+
try {
|
|
39
|
+
emit?.(sessionID).catch((error) => log("v2 recall: push failed", { sessionID, error: String(error) }, "error"));
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
log("v2 recall: push failed", { sessionID, error: String(error) }, "error");
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
// A session with a parent is a subagent session. A failed lookup counts as a root session, so
|
|
46
|
+
// the summary still shows.
|
|
47
|
+
const isSubagent = (sessionID) => {
|
|
48
|
+
const known = subagents.get(sessionID);
|
|
49
|
+
if (known)
|
|
50
|
+
return known;
|
|
51
|
+
const lookup = (async () => {
|
|
52
|
+
try {
|
|
53
|
+
const info = await ctx.session.get({ sessionID });
|
|
54
|
+
return Boolean(info?.parentID);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
log("v2 recall: session lookup failed", { sessionID, error: String(error) }, "error");
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
})();
|
|
61
|
+
bound(subagents, sessionID, lookup);
|
|
62
|
+
return lookup;
|
|
63
|
+
};
|
|
64
|
+
const store = (sessionID, built) => {
|
|
65
|
+
const seq = (sequences.get(sessionID) ?? 0) + 1;
|
|
66
|
+
bound(sequences, sessionID, seq);
|
|
67
|
+
const summary = { ...built, seq };
|
|
68
|
+
const ring = [...(rings.get(sessionID) ?? []), summary].slice(-MAX_RECALL_SUMMARIES);
|
|
69
|
+
bound(rings, sessionID, ring);
|
|
70
|
+
log("v2 recall: summary recorded", {
|
|
71
|
+
sessionID,
|
|
72
|
+
mode: summary.mode,
|
|
73
|
+
count: summary.items.length,
|
|
74
|
+
});
|
|
75
|
+
notify(sessionID);
|
|
76
|
+
};
|
|
77
|
+
return {
|
|
78
|
+
record(sessionID, token, query, section) {
|
|
79
|
+
if (recorded.has(token))
|
|
80
|
+
return;
|
|
81
|
+
recorded.add(token);
|
|
82
|
+
try {
|
|
83
|
+
const summary = section
|
|
84
|
+
? buildRecallSummary(section, "search", query.slice(0, MAX_RECALL_QUERY_CHARS))
|
|
85
|
+
: null;
|
|
86
|
+
if (!summary)
|
|
87
|
+
return;
|
|
88
|
+
const ticket = { cancelled: false };
|
|
89
|
+
const tickets = pending.get(sessionID) ?? new Set();
|
|
90
|
+
tickets.add(ticket);
|
|
91
|
+
pending.set(sessionID, tickets);
|
|
92
|
+
void isSubagent(sessionID).then((subagent) => {
|
|
93
|
+
tickets.delete(ticket);
|
|
94
|
+
if (tickets.size === 0 && pending.get(sessionID) === tickets)
|
|
95
|
+
pending.delete(sessionID);
|
|
96
|
+
if (subagent || ticket.cancelled)
|
|
97
|
+
return;
|
|
98
|
+
try {
|
|
99
|
+
store(sessionID, summary);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
log("v2 recall: could not record summary", { sessionID, error: String(error) }, "error");
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
log("v2 recall: could not record summary", { sessionID, error: String(error) }, "error");
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
forget: (sessionID) => {
|
|
111
|
+
for (const ticket of pending.get(sessionID) ?? [])
|
|
112
|
+
ticket.cancelled = true;
|
|
113
|
+
pending.delete(sessionID);
|
|
114
|
+
rings.delete(sessionID);
|
|
115
|
+
sequences.delete(sessionID);
|
|
116
|
+
subagents.delete(sessionID);
|
|
117
|
+
},
|
|
118
|
+
clear: () => {
|
|
119
|
+
for (const tickets of pending.values())
|
|
120
|
+
for (const ticket of tickets)
|
|
121
|
+
ticket.cancelled = true;
|
|
122
|
+
pending.clear();
|
|
123
|
+
rings.clear();
|
|
124
|
+
sequences.clear();
|
|
125
|
+
subagents.clear();
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin RPC contract between the OpenCode v2 server entrypoint (records recall summaries) and the
|
|
3
|
+
* terminal TUI entrypoint (draws them). Plain data on purpose: no host SDK import, so both sides and
|
|
4
|
+
* the tests can load it. The TUI pulls `list` and treats `recalled` as a refresh hint only; a missing
|
|
5
|
+
* push never breaks the rows (TDR-060).
|
|
6
|
+
*/
|
|
7
|
+
export declare const RECALL_RPC_ID = "omms-recall";
|
|
8
|
+
export declare const recallRpc: {
|
|
9
|
+
readonly id: "omms-recall";
|
|
10
|
+
readonly methods: {
|
|
11
|
+
readonly list: {
|
|
12
|
+
readonly input: {
|
|
13
|
+
readonly type: "object";
|
|
14
|
+
readonly properties: {
|
|
15
|
+
readonly sessionID: {
|
|
16
|
+
readonly type: "string";
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
readonly required: readonly ["sessionID"];
|
|
20
|
+
};
|
|
21
|
+
readonly output: {
|
|
22
|
+
readonly type: "object";
|
|
23
|
+
readonly properties: {
|
|
24
|
+
readonly enabled: {
|
|
25
|
+
readonly type: "boolean";
|
|
26
|
+
};
|
|
27
|
+
readonly items: {
|
|
28
|
+
readonly type: "array";
|
|
29
|
+
readonly items: {
|
|
30
|
+
readonly type: "object";
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
readonly required: readonly ["enabled", "items"];
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
readonly events: {
|
|
39
|
+
readonly recalled: {
|
|
40
|
+
readonly schema: {
|
|
41
|
+
readonly type: "object";
|
|
42
|
+
readonly properties: {
|
|
43
|
+
readonly sessionID: {
|
|
44
|
+
readonly type: "string";
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
readonly required: readonly ["sessionID"];
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin RPC contract between the OpenCode v2 server entrypoint (records recall summaries) and the
|
|
3
|
+
* terminal TUI entrypoint (draws them). Plain data on purpose: no host SDK import, so both sides and
|
|
4
|
+
* the tests can load it. The TUI pulls `list` and treats `recalled` as a refresh hint only; a missing
|
|
5
|
+
* push never breaks the rows (TDR-060).
|
|
6
|
+
*/
|
|
7
|
+
export const RECALL_RPC_ID = "omms-recall";
|
|
8
|
+
export const recallRpc = {
|
|
9
|
+
id: RECALL_RPC_ID,
|
|
10
|
+
methods: {
|
|
11
|
+
// `enabled` is the effective `visibleRecall` setting for the server's project, so the TUI never
|
|
12
|
+
// reads config. Summaries are recorded and served even when it is false.
|
|
13
|
+
list: {
|
|
14
|
+
input: {
|
|
15
|
+
type: "object",
|
|
16
|
+
properties: { sessionID: { type: "string" } },
|
|
17
|
+
required: ["sessionID"],
|
|
18
|
+
},
|
|
19
|
+
output: {
|
|
20
|
+
type: "object",
|
|
21
|
+
properties: {
|
|
22
|
+
enabled: { type: "boolean" },
|
|
23
|
+
items: { type: "array", items: { type: "object" } },
|
|
24
|
+
},
|
|
25
|
+
required: ["enabled", "items"],
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
events: {
|
|
30
|
+
recalled: {
|
|
31
|
+
schema: {
|
|
32
|
+
type: "object",
|
|
33
|
+
properties: { sessionID: { type: "string" } },
|
|
34
|
+
required: ["sessionID"],
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
};
|
package/opencode/tui.tsx
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OpenCode TUI entry: shows "omms: ✔" in the prompt footer,
|
|
3
|
-
*
|
|
4
|
-
* the logic lives in
|
|
2
|
+
* OpenCode TUI entry: shows "omms: ✔" in the prompt footer, a toast when a
|
|
3
|
+
* newer om-memory-system is on npm, and one fold row per OMMS recall above the
|
|
4
|
+
* prompt. OpenCode compiles this file itself; the logic lives in
|
|
5
|
+
* dist/adapters/opencode/tui-status.js and tui-recall.js.
|
|
5
6
|
*
|
|
6
7
|
* Installed from npm, this file sits in node_modules, where OpenCode does not
|
|
7
8
|
* map bare `solid-js` or JSX runtime imports to its own copies. The
|
|
@@ -41,7 +42,25 @@ export default {
|
|
|
41
42
|
} catch {
|
|
42
43
|
setText("omms: ✘");
|
|
43
44
|
}
|
|
45
|
+
// Recall rows are independent of the footer: a failure here leaves the footer as it is.
|
|
46
|
+
let stopRecall: (() => void) | undefined;
|
|
47
|
+
try {
|
|
48
|
+
const { startTuiRecall } = await import("../dist/adapters/opencode/tui-recall.js");
|
|
49
|
+
stopRecall = startTuiRecall({
|
|
50
|
+
client: context.client,
|
|
51
|
+
data: context.data,
|
|
52
|
+
ui: context.ui,
|
|
53
|
+
keymap: context.keymap,
|
|
54
|
+
storage: context.storage,
|
|
55
|
+
createSignal,
|
|
56
|
+
jsx,
|
|
57
|
+
muted: () => context.theme?.text?.muted,
|
|
58
|
+
});
|
|
59
|
+
} catch {
|
|
60
|
+
// No rows; the footer keeps working.
|
|
61
|
+
}
|
|
44
62
|
return () => {
|
|
63
|
+
stopRecall?.();
|
|
45
64
|
stop?.();
|
|
46
65
|
removeSlot();
|
|
47
66
|
};
|