om-memory-system 5.10.1 → 6.1.0-next.237
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 +1 -1
- package/dist/adapters/opencode/auto-capture-host.d.ts +2 -2
- package/dist/adapters/opencode/auto-capture-host.js +5 -15
- package/dist/adapters/opencode/auto-capture-summary.js +9 -6
- package/dist/adapters/opencode/backfill-models.js +1 -1
- package/dist/adapters/opencode/backfill-startup.js +5 -4
- package/dist/adapters/opencode/conversation.d.ts +5 -1
- package/dist/adapters/opencode/conversation.js +23 -23
- package/dist/adapters/opencode/import-command.d.ts +0 -12
- package/dist/adapters/opencode/import-command.js +1 -17
- package/dist/adapters/opencode/internal-capture-sessions.d.ts +0 -3
- package/dist/adapters/opencode/internal-capture-sessions.js +0 -26
- package/dist/adapters/opencode/memory-tool.d.ts +3 -4
- package/dist/adapters/opencode/memory-tool.js +8 -50
- package/dist/adapters/opencode/opencode-host.d.ts +27 -0
- package/dist/adapters/opencode/opencode-host.js +30 -0
- package/dist/adapters/opencode/opencode-import-models.d.ts +2 -2
- package/dist/adapters/opencode/opencode-import-models.js +8 -10
- package/dist/adapters/opencode/opencode-provider.d.ts +23 -62
- package/dist/adapters/opencode/opencode-provider.js +75 -378
- package/dist/adapters/opencode/package-skills.d.ts +1 -6
- package/dist/adapters/opencode/package-skills.js +2 -17
- package/dist/adapters/opencode/profile-learning.d.ts +1 -2
- package/dist/adapters/opencode/profile-learning.js +15 -19
- package/dist/adapters/opencode/profile-llm-client.d.ts +3 -2
- package/dist/adapters/opencode/profile-llm-client.js +8 -17
- package/dist/adapters/opencode/profile-model.d.ts +2 -18
- package/dist/adapters/opencode/profile-model.js +18 -69
- package/dist/adapters/opencode/prompt-model.d.ts +9 -0
- package/dist/adapters/opencode/prompt-model.js +21 -0
- package/dist/adapters/opencode/skills-tool.d.ts +3 -3
- package/dist/adapters/opencode/skills-tool.js +4 -30
- package/dist/adapters/opencode/toast.d.ts +11 -0
- package/dist/adapters/opencode/toast.js +11 -0
- package/dist/adapters/opencode/user-prompt.d.ts +2 -2
- package/dist/adapters/opencode/user-prompt.js +4 -5
- package/dist/config-template.d.ts +1 -1
- package/dist/config-template.js +2 -6
- package/dist/config.d.ts +0 -9
- package/dist/config.js +0 -9
- package/dist/core/retrieval.d.ts +0 -21
- package/dist/core/retrieval.js +0 -29
- package/dist/importer/handled-lookup.d.ts +17 -0
- package/dist/importer/handled-lookup.js +13 -0
- package/dist/importer/importer.js +3 -3
- package/dist/importer/ledger.d.ts +9 -0
- package/dist/importer/ledger.js +21 -0
- package/dist/importer/opencode-reader.d.ts +1 -1
- package/dist/importer/opencode-reader.js +88 -34
- package/dist/importer/opencode-v2-units.d.ts +16 -0
- package/dist/importer/opencode-v2-units.js +93 -0
- package/dist/importer/settings-models.d.ts +11 -6
- package/dist/importer/settings-models.js +15 -11
- package/dist/plugin.d.ts +0 -3
- package/dist/plugin.js +1 -7
- package/dist/services/global-config-writer.js +2 -3
- package/dist/services/settings-snapshot.js +6 -6
- package/dist/services/turso/vector-search.js +4 -2
- package/dist/shared/api/schemas.d.ts +1 -1
- package/dist/utils/memory-limits.d.ts +0 -1
- package/dist/utils/memory-limits.js +0 -1
- package/dist/v2/adapter.d.ts +16 -84
- package/dist/v2/adapter.js +17 -88
- package/dist/v2/memory-bridge.js +1 -1
- package/dist/v2/plugin.js +27 -16
- package/dist/v2/session-events.d.ts +30 -0
- package/dist/v2/session-events.js +130 -0
- package/dist/v2/startup.d.ts +14 -0
- package/dist/v2/startup.js +210 -0
- package/dist/v2/tools.d.ts +106 -0
- package/dist/v2/tools.js +89 -0
- package/dist/web/assets/index-DBSeQOs6.js +171 -0
- package/dist/web/index.html +1 -1
- package/package.json +2 -14
- package/dist/adapters/opencode/in-process-client.d.ts +0 -2
- package/dist/adapters/opencode/in-process-client.js +0 -16
- package/dist/adapters/opencode/opencode-diagnostics.d.ts +0 -7
- package/dist/adapters/opencode/opencode-diagnostics.js +0 -37
- package/dist/adapters/opencode/opencode-host-config.d.ts +0 -13
- package/dist/adapters/opencode/opencode-host-config.js +0 -53
- package/dist/adapters/opencode/opencode-sdk-client.d.ts +0 -6
- package/dist/adapters/opencode/opencode-sdk-client.js +0 -38
- package/dist/index.d.ts +0 -28
- package/dist/index.js +0 -620
- package/dist/v2/legacy-client.d.ts +0 -15
- package/dist/v2/legacy-client.js +0 -275
- package/dist/web/assets/index-DQgI670D.js +0 -171
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const exactKeyLookup = (ledger, { key, readOnly }) => readOnly ? ledger.peek(key) : ledger.get(key);
|
|
2
|
+
const HANDLED_LOOKUPS = {
|
|
3
|
+
opencode: async (ledger, query) => {
|
|
4
|
+
const exact = await exactKeyLookup(ledger, query);
|
|
5
|
+
if (exact?.status === "imported" || exact?.status === "skipped")
|
|
6
|
+
return exact;
|
|
7
|
+
return ((await ledger.findHandledByPrefix(query.sessionId, `opencode:${query.sessionId}:${query.window.userEntryId}:`, { readOnly: query.readOnly })) ?? exact);
|
|
8
|
+
},
|
|
9
|
+
};
|
|
10
|
+
/** The lookup for a host, chosen once per import run. */
|
|
11
|
+
export function handledLookupFor(host) {
|
|
12
|
+
return HANDLED_LOOKUPS[host] ?? exactKeyLookup;
|
|
13
|
+
}
|
|
@@ -8,6 +8,7 @@ import { tursoConnectionManager } from "../services/turso/connection-manager.js"
|
|
|
8
8
|
import { tursoShardManager } from "../services/turso/shard-manager.js";
|
|
9
9
|
import { extractPiConversationWindows, } from "./pi-conversation.js";
|
|
10
10
|
import { discoverPiSessions } from "./discovery.js";
|
|
11
|
+
import { handledLookupFor } from "./handled-lookup.js";
|
|
11
12
|
import { importDirectory } from "./import-project.js";
|
|
12
13
|
import { importSkillTurnOutput, importSkillTurnsOn, importWorkUnit, skillTurnsOnly, } from "./import-turn-outputs.js";
|
|
13
14
|
import { PiImportLedger, importLedgerDbPath } from "./ledger.js";
|
|
@@ -112,6 +113,7 @@ export async function importHistorySource(source, host, deps, filters, report) {
|
|
|
112
113
|
const ledger = deps.ledger ?? new PiImportLedger();
|
|
113
114
|
const ledgerFileExists = existsSync(importLedgerDbPath());
|
|
114
115
|
const ledgerAccessible = !dryRun || ledgerFileExists;
|
|
116
|
+
const lookupHandled = handledLookupFor(host);
|
|
115
117
|
if (!dryRun && memories) {
|
|
116
118
|
// A dry run only reads the ledger (peek), so it never writes its schema.
|
|
117
119
|
await ledger.get("__warmup__");
|
|
@@ -166,9 +168,7 @@ export async function importHistorySource(source, host, deps, filters, report) {
|
|
|
166
168
|
continue;
|
|
167
169
|
const existing = !ledgerAccessible
|
|
168
170
|
? null
|
|
169
|
-
: dryRun
|
|
170
|
-
? await ledger.peek(key)
|
|
171
|
-
: await ledger.get(key);
|
|
171
|
+
: await lookupHandled(ledger, { key, sessionId, window, readOnly: dryRun });
|
|
172
172
|
const terminalHandled = existing &&
|
|
173
173
|
(existing.status === "imported" || existing.status === "skipped") &&
|
|
174
174
|
!filters.force;
|
|
@@ -32,6 +32,15 @@ export declare class ImportLedger {
|
|
|
32
32
|
* A ledger file without the table reads as empty.
|
|
33
33
|
*/
|
|
34
34
|
peek(key: string): Promise<ImportLedgerRow | null>;
|
|
35
|
+
/**
|
|
36
|
+
* An imported or skipped memory row of one session whose key starts with `prefix`.
|
|
37
|
+
* Rows are found through the `session_id` index, then prefix-checked. Profile rows
|
|
38
|
+
* (`#profile` keys) share the prefix but do not show that a memory was imported.
|
|
39
|
+
* With `readOnly` it never creates the table, like `peek`.
|
|
40
|
+
*/
|
|
41
|
+
findHandledByPrefix(sessionId: string, prefix: string, options?: {
|
|
42
|
+
readOnly?: boolean;
|
|
43
|
+
}): Promise<ImportLedgerRow | null>;
|
|
35
44
|
/**
|
|
36
45
|
* Sessions whose ledger rows are all finished for every ticked output.
|
|
37
46
|
* Memory rows and profile rows (`project_hash = 'profile'`) are judged apart.
|
package/dist/importer/ledger.js
CHANGED
|
@@ -88,6 +88,27 @@ export class ImportLedger {
|
|
|
88
88
|
const row = await db.get(`SELECT * FROM import_ledger WHERE key = ?`, [key]);
|
|
89
89
|
return row ? rowToEntry(row) : null;
|
|
90
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* An imported or skipped memory row of one session whose key starts with `prefix`.
|
|
93
|
+
* Rows are found through the `session_id` index, then prefix-checked. Profile rows
|
|
94
|
+
* (`#profile` keys) share the prefix but do not show that a memory was imported.
|
|
95
|
+
* With `readOnly` it never creates the table, like `peek`.
|
|
96
|
+
*/
|
|
97
|
+
async findHandledByPrefix(sessionId, prefix, options = {}) {
|
|
98
|
+
const db = options.readOnly
|
|
99
|
+
? await tursoConnectionManager.getConnection(importLedgerDbPath())
|
|
100
|
+
: await this.db();
|
|
101
|
+
if (options.readOnly) {
|
|
102
|
+
const table = await db.get(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'import_ledger'`);
|
|
103
|
+
if (!table)
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
const row = await db.get(`SELECT * FROM import_ledger
|
|
107
|
+
WHERE session_id = ? AND status IN ('imported', 'skipped') AND substr(key, 1, ?) = ?
|
|
108
|
+
AND project_hash <> 'profile'
|
|
109
|
+
LIMIT 1`, [sessionId, prefix.length, prefix]);
|
|
110
|
+
return row ? rowToEntry(row) : null;
|
|
111
|
+
}
|
|
91
112
|
/**
|
|
92
113
|
* Sessions whose ledger rows are all finished for every ticked output.
|
|
93
114
|
* Memory rows and profile rows (`project_hash = 'profile'`) are judged apart.
|
|
@@ -35,5 +35,5 @@ export interface OpencodeOpenOptions {
|
|
|
35
35
|
};
|
|
36
36
|
signal?: AbortSignal;
|
|
37
37
|
}
|
|
38
|
-
/** Read
|
|
38
|
+
/** Read OpenCode history without changing its database or WAL sidecars. */
|
|
39
39
|
export declare function openOpencodeHistory(dbPath: string, filters?: OpencodeReaderFilters, options?: OpencodeOpenOptions): Promise<OpencodeHistory>;
|
|
@@ -6,27 +6,59 @@ import { DatabaseSync } from "node:sqlite";
|
|
|
6
6
|
import { boundToolInput, readToolPath } from "../core/tool-input.js";
|
|
7
7
|
import { opencodeMemorise, opencodeToolOutcome } from "../core/tool-outcome.js";
|
|
8
8
|
import { INTERNAL_CAPTURE_SESSION_TITLES } from "./opencode-internal-sessions.js";
|
|
9
|
+
import { buildV2Windows } from "./opencode-v2-units.js";
|
|
9
10
|
import { acquirePrivateSnapshot, opencodeSnapshots, OpencodeSnapshotError, } from "./opencode-snapshot.js";
|
|
10
11
|
export const DEFAULT_OPENCODE_DB = join(homedir(), ".local/share/opencode/opencode.db");
|
|
11
|
-
function
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (!
|
|
23
|
-
throw new Error(`Unsupported OpenCode
|
|
24
|
-
const names = new Set(db.prepare(`PRAGMA table_info(${table})`).all().map((column) => column.name));
|
|
12
|
+
function columnsOf(db, table) {
|
|
13
|
+
const found = db
|
|
14
|
+
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?")
|
|
15
|
+
.get(table);
|
|
16
|
+
if (!found)
|
|
17
|
+
return null;
|
|
18
|
+
return new Set(db.prepare(`PRAGMA table_info(${table})`).all().map((column) => column.name));
|
|
19
|
+
}
|
|
20
|
+
function requireColumns(db, required) {
|
|
21
|
+
return Object.entries(required).map(([table, columns]) => {
|
|
22
|
+
const names = columnsOf(db, table);
|
|
23
|
+
if (!names)
|
|
24
|
+
throw new Error(`Unsupported OpenCode database: missing ${table} table`);
|
|
25
25
|
for (const column of columns) {
|
|
26
26
|
if (!names.has(column))
|
|
27
|
-
throw new Error(`Unsupported OpenCode
|
|
27
|
+
throw new Error(`Unsupported OpenCode database: missing ${table}.${column}`);
|
|
28
28
|
}
|
|
29
|
+
return names;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* OpenCode 2.x keeps sessions in `session_v2` and `session_message`; older
|
|
34
|
+
* copies have `session`, `message` and `part`. A set counts as present when
|
|
35
|
+
* its session table exists, and is then checked in full.
|
|
36
|
+
*/
|
|
37
|
+
function detectTableSets(db) {
|
|
38
|
+
const v2 = columnsOf(db, "session_v2") !== null;
|
|
39
|
+
const v1 = columnsOf(db, "session") !== null;
|
|
40
|
+
if (!v2 && !v1) {
|
|
41
|
+
throw new Error("Unsupported OpenCode database: missing session_v2 and session_message tables, " +
|
|
42
|
+
"and missing session and message tables");
|
|
43
|
+
}
|
|
44
|
+
const titled = { v2: false, v1: false };
|
|
45
|
+
if (v2) {
|
|
46
|
+
const [sessions] = requireColumns(db, {
|
|
47
|
+
session_v2: ["id", "directory", "project_id", "parent_id", "time_created"],
|
|
48
|
+
session_message: ["id", "session_id", "type", "time_created", "data"],
|
|
49
|
+
});
|
|
50
|
+
titled.v2 = sessions.has("title");
|
|
51
|
+
}
|
|
52
|
+
if (v1) {
|
|
53
|
+
const [sessions] = requireColumns(db, {
|
|
54
|
+
session: ["id", "directory", "project_id", "parent_id", "time_created"],
|
|
55
|
+
message: ["id", "session_id", "time_created", "data"],
|
|
56
|
+
part: ["id", "message_id", "session_id", "time_created", "data"],
|
|
57
|
+
});
|
|
58
|
+
titled.v1 = sessions.has("title");
|
|
29
59
|
}
|
|
60
|
+
requireColumns(db, { project: ["id", "worktree"] });
|
|
61
|
+
return { v2, v1, titled };
|
|
30
62
|
}
|
|
31
63
|
function parseData(json) {
|
|
32
64
|
try {
|
|
@@ -153,19 +185,34 @@ function changedSinceOpen(dbPath, source) {
|
|
|
153
185
|
return false;
|
|
154
186
|
return existsSync(`${dbPath}-wal`) || fileStamp(dbPath) !== source.stamp;
|
|
155
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* Every session once: `session_v2` rows, plus `session` rows whose id has no
|
|
190
|
+
* `session_v2` row. A session therefore comes from one table set only.
|
|
191
|
+
*/
|
|
192
|
+
function sessionSourcesSql(sets) {
|
|
193
|
+
const parts = [];
|
|
194
|
+
if (sets.v2) {
|
|
195
|
+
parts.push(`SELECT s.id, s.parent_id, ${sets.titled.v2 ? "s.title" : "NULL"} AS title,
|
|
196
|
+
s.directory, s.project_id, s.time_created, 'v2' AS source FROM session_v2 s`);
|
|
197
|
+
}
|
|
198
|
+
if (sets.v1) {
|
|
199
|
+
parts.push(`SELECT s.id, s.parent_id, ${sets.titled.v1 ? "s.title" : "NULL"} AS title,
|
|
200
|
+
s.directory, s.project_id, s.time_created, 'v1' AS source FROM session s
|
|
201
|
+
${sets.v2 ? "WHERE NOT EXISTS (SELECT 1 FROM session_v2 v WHERE v.id = s.id)" : ""}`);
|
|
202
|
+
}
|
|
203
|
+
return parts.join(" UNION ALL ");
|
|
204
|
+
}
|
|
156
205
|
function readSessions(db, filters) {
|
|
157
|
-
|
|
206
|
+
const sets = detectTableSets(db);
|
|
158
207
|
// omms's own capture/profile calls run in OpenCode sessions; never import them as history.
|
|
159
|
-
const
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
: "";
|
|
163
|
-
const internalArgs = hasTitle ? [...INTERNAL_CAPTURE_SESSION_TITLES] : [];
|
|
208
|
+
const notInternal = `AND (s.title IS NULL OR s.title NOT IN (${INTERNAL_CAPTURE_SESSION_TITLES.map(() => "?").join(", ")}))`;
|
|
209
|
+
const internalArgs = [...INTERNAL_CAPTURE_SESSION_TITLES];
|
|
210
|
+
const sources = sessionSourcesSql(sets);
|
|
164
211
|
const count = (where) => Number(db
|
|
165
|
-
.prepare(`SELECT COUNT(*) AS count FROM
|
|
212
|
+
.prepare(`SELECT COUNT(*) AS count FROM (${sources}) s WHERE ${where} ${notInternal}`)
|
|
166
213
|
.get(...internalArgs).count);
|
|
167
|
-
const query = `SELECT s.id, s.directory, p.worktree AS project_worktree, s.time_created
|
|
168
|
-
FROM
|
|
214
|
+
const query = `SELECT s.id, s.directory, p.worktree AS project_worktree, s.time_created, s.source
|
|
215
|
+
FROM (${sources}) s LEFT JOIN project p ON p.id = s.project_id
|
|
169
216
|
WHERE s.parent_id IS NULL ${notInternal} ${filters.session ? "AND s.id = ?" : ""}
|
|
170
217
|
${filters.sessionIds ? "AND s.id IN (SELECT value FROM json_each(?))" : ""}
|
|
171
218
|
ORDER BY s.time_created, s.id ${filters.maxSessions ? "LIMIT ?" : ""}`;
|
|
@@ -176,19 +223,20 @@ function readSessions(db, filters) {
|
|
|
176
223
|
args.push(JSON.stringify(filters.sessionIds));
|
|
177
224
|
if (filters.maxSessions)
|
|
178
225
|
args.push(filters.maxSessions);
|
|
179
|
-
const
|
|
180
|
-
sessionId: row.id,
|
|
181
|
-
recordedDirectory: row.directory,
|
|
182
|
-
projectWorktree: row.project_worktree,
|
|
183
|
-
timeCreated: Number(row.time_created),
|
|
184
|
-
}));
|
|
226
|
+
const rows = db.prepare(query).all(...args);
|
|
185
227
|
return {
|
|
186
228
|
childSessions: count("s.parent_id IS NOT NULL"),
|
|
187
229
|
topLevelSessions: count("s.parent_id IS NULL"),
|
|
188
|
-
sessions
|
|
230
|
+
sessions: rows.map((row) => ({
|
|
231
|
+
sessionId: row.id,
|
|
232
|
+
recordedDirectory: row.directory,
|
|
233
|
+
projectWorktree: row.project_worktree,
|
|
234
|
+
timeCreated: Number(row.time_created),
|
|
235
|
+
})),
|
|
236
|
+
origin: new Map(rows.map((row) => [row.id, row.source === "v2" ? "v2" : "v1"])),
|
|
189
237
|
};
|
|
190
238
|
}
|
|
191
|
-
/** Read
|
|
239
|
+
/** Read OpenCode history without changing its database or WAL sidecars. */
|
|
192
240
|
export async function openOpencodeHistory(dbPath, filters = {}, options = {}) {
|
|
193
241
|
if (!existsSync(dbPath))
|
|
194
242
|
throw new Error("OpenCode database not found");
|
|
@@ -211,10 +259,16 @@ export async function openOpencodeHistory(dbPath, filters = {}, options = {}) {
|
|
|
211
259
|
throw error;
|
|
212
260
|
}
|
|
213
261
|
const opened = source;
|
|
262
|
+
const { origin, ...summary } = listed;
|
|
214
263
|
let closed = false;
|
|
215
264
|
return {
|
|
216
|
-
...
|
|
217
|
-
loadUnits: (sessionId) =>
|
|
265
|
+
...summary,
|
|
266
|
+
loadUnits: (sessionId) => {
|
|
267
|
+
const from = origin.get(sessionId);
|
|
268
|
+
if (from === "v2")
|
|
269
|
+
return buildV2Windows(opened.db, sessionId, filters);
|
|
270
|
+
return from === "v1" ? buildWindows(opened.db, sessionId, filters) : [];
|
|
271
|
+
},
|
|
218
272
|
close: async () => {
|
|
219
273
|
if (closed)
|
|
220
274
|
return;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import type { ImportWindow } from "./importer.js";
|
|
3
|
+
/** The part of the reader filters that decides which user messages start a unit. */
|
|
4
|
+
interface V2UnitFilters {
|
|
5
|
+
since?: number;
|
|
6
|
+
until?: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* A `user` message starts a unit. A `synthetic` or `compaction` message ends
|
|
10
|
+
* the current unit and starts none, so OMMS context and compaction summaries
|
|
11
|
+
* never become prompts and later assistant messages do not join the old unit.
|
|
12
|
+
* `assistant` messages add `text` replies and `tool` calls; every other
|
|
13
|
+
* message type is ignored.
|
|
14
|
+
*/
|
|
15
|
+
export declare function buildV2Windows(db: DatabaseSync, sessionId: string, filters: V2UnitFilters): ImportWindow[];
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { boundToolInput, readToolPath } from "../core/tool-input.js";
|
|
2
|
+
import { opencodeMemorise, opencodeToolOutcome } from "../core/tool-outcome.js";
|
|
3
|
+
function parseRecord(json) {
|
|
4
|
+
try {
|
|
5
|
+
const parsed = JSON.parse(json);
|
|
6
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
7
|
+
? parsed
|
|
8
|
+
: {};
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return {};
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function formatInput(value) {
|
|
15
|
+
return typeof value === "string" ? value : JSON.stringify(value ?? "");
|
|
16
|
+
}
|
|
17
|
+
function toolCall(item) {
|
|
18
|
+
const state = item.state && typeof item.state === "object" ? item.state : {};
|
|
19
|
+
const name = String(item.name ?? item.type);
|
|
20
|
+
const args = state.input ?? "";
|
|
21
|
+
const path = readToolPath(name, args);
|
|
22
|
+
return {
|
|
23
|
+
name,
|
|
24
|
+
input: boundToolInput(formatInput(args)),
|
|
25
|
+
...opencodeToolOutcome(state),
|
|
26
|
+
...opencodeMemorise(name, args, state),
|
|
27
|
+
...(path === undefined ? {} : { path }),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* A `user` message starts a unit. A `synthetic` or `compaction` message ends
|
|
32
|
+
* the current unit and starts none, so OMMS context and compaction summaries
|
|
33
|
+
* never become prompts and later assistant messages do not join the old unit.
|
|
34
|
+
* `assistant` messages add `text` replies and `tool` calls; every other
|
|
35
|
+
* message type is ignored.
|
|
36
|
+
*/
|
|
37
|
+
export function buildV2Windows(db, sessionId, filters) {
|
|
38
|
+
const messages = db
|
|
39
|
+
.prepare("SELECT id, type, time_created, data FROM session_message WHERE session_id = ? ORDER BY time_created, id")
|
|
40
|
+
.all(sessionId);
|
|
41
|
+
const units = [];
|
|
42
|
+
let current = null;
|
|
43
|
+
const finish = () => {
|
|
44
|
+
if (current?.sourceEntryIds?.length)
|
|
45
|
+
units.push(current);
|
|
46
|
+
current = null;
|
|
47
|
+
};
|
|
48
|
+
for (const message of messages) {
|
|
49
|
+
if (message.type === "user") {
|
|
50
|
+
finish();
|
|
51
|
+
const text = parseRecord(message.data).text;
|
|
52
|
+
const prompt = typeof text === "string" ? text.trim() : "";
|
|
53
|
+
const timestamp = Number(message.time_created);
|
|
54
|
+
if (prompt &&
|
|
55
|
+
(filters.since === undefined || timestamp >= filters.since) &&
|
|
56
|
+
(filters.until === undefined || timestamp <= filters.until)) {
|
|
57
|
+
current = {
|
|
58
|
+
userEntryId: message.id,
|
|
59
|
+
userPrompt: prompt,
|
|
60
|
+
userTimestamp: timestamp,
|
|
61
|
+
sourceTimestamp: timestamp,
|
|
62
|
+
textResponses: [],
|
|
63
|
+
toolCalls: [],
|
|
64
|
+
sourceEntryIds: [],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (message.type === "synthetic" || message.type === "compaction") {
|
|
70
|
+
finish();
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (message.type !== "assistant" || !current)
|
|
74
|
+
continue;
|
|
75
|
+
current.sourceEntryIds.push(message.id);
|
|
76
|
+
const content = parseRecord(message.data).content;
|
|
77
|
+
if (!Array.isArray(content))
|
|
78
|
+
continue;
|
|
79
|
+
for (const item of content) {
|
|
80
|
+
if (!item || typeof item !== "object")
|
|
81
|
+
continue;
|
|
82
|
+
const entry = item;
|
|
83
|
+
if (entry.type === "text" && typeof entry.text === "string") {
|
|
84
|
+
current.textResponses.push(entry.text);
|
|
85
|
+
}
|
|
86
|
+
else if (entry.type === "tool") {
|
|
87
|
+
current.toolCalls.push(toolCall(entry));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
finish();
|
|
92
|
+
return units;
|
|
93
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { OpencodeClient } from "@opencode-ai/sdk/v2/client";
|
|
2
1
|
import type { SettingsModel } from "./backfill-controls.js";
|
|
3
2
|
import type { StandaloneResult } from "./opencode-standalone-models.js";
|
|
4
3
|
export declare const OPENCODE_LIST_UNAVAILABLE = "OpenCode model list unavailable";
|
|
@@ -18,16 +17,22 @@ type ProviderList = {
|
|
|
18
17
|
}>;
|
|
19
18
|
}>;
|
|
20
19
|
};
|
|
21
|
-
/** The models of each connected provider in an OpenCode
|
|
20
|
+
/** The models of each connected provider in an OpenCode server provider list. */
|
|
22
21
|
export declare function connectedProviderModels(list: ProviderList): SettingsModel[];
|
|
23
|
-
/**
|
|
24
|
-
export
|
|
22
|
+
/** One model a running host lists, with its provider and display name. */
|
|
23
|
+
export type ListedModel = {
|
|
24
|
+
readonly providerID: string;
|
|
25
|
+
readonly id: string;
|
|
26
|
+
readonly name: string;
|
|
27
|
+
};
|
|
28
|
+
/** Settings rows for the models a running OpenCode plugin lists, grouped by provider. */
|
|
29
|
+
export declare function listOpencodeClientModels(listModels: () => Promise<readonly ListedModel[]>): Promise<SettingsModel[]>;
|
|
25
30
|
/**
|
|
26
|
-
* The OpenCode models for the Settings page. Without
|
|
31
|
+
* The OpenCode models for the Settings page. Without `listModels` or a registered
|
|
27
32
|
* OpenCode host, this starts a private OpenCode server through `readStandalone`.
|
|
28
33
|
* Pass `null` to skip that, for callers that need a live session.
|
|
29
34
|
*/
|
|
30
|
-
export declare function listOpencodeSettingsModels(
|
|
35
|
+
export declare function listOpencodeSettingsModels(listModels?: () => Promise<readonly ListedModel[]>, readStandalone?: (() => Promise<StandaloneResult>) | null): Promise<{
|
|
31
36
|
available: boolean;
|
|
32
37
|
models?: SettingsModel[];
|
|
33
38
|
reason?: string;
|
|
@@ -22,7 +22,7 @@ const STANDALONE_REASONS = {
|
|
|
22
22
|
empty: OPENCODE_NO_MODELS,
|
|
23
23
|
unreadable: OPENCODE_UNREADABLE,
|
|
24
24
|
};
|
|
25
|
-
/** The models of each connected provider in an OpenCode
|
|
25
|
+
/** The models of each connected provider in an OpenCode server provider list. */
|
|
26
26
|
export function connectedProviderModels(list) {
|
|
27
27
|
const connected = new Set(list.connected);
|
|
28
28
|
return list.all
|
|
@@ -33,28 +33,32 @@ export function connectedProviderModels(list) {
|
|
|
33
33
|
name: info.name,
|
|
34
34
|
})));
|
|
35
35
|
}
|
|
36
|
-
/**
|
|
37
|
-
export async function listOpencodeClientModels(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
/** Settings rows for the models a running OpenCode plugin lists, grouped by provider. */
|
|
37
|
+
export async function listOpencodeClientModels(listModels) {
|
|
38
|
+
// Providers keep the order they first appear in; models keep their order inside each.
|
|
39
|
+
const byProvider = new Map();
|
|
40
|
+
for (const model of await listModels()) {
|
|
41
|
+
const rows = byProvider.get(model.providerID) ?? [];
|
|
42
|
+
rows.push({ provider: model.providerID, model: model.id, name: model.name });
|
|
43
|
+
byProvider.set(model.providerID, rows);
|
|
44
|
+
}
|
|
45
|
+
return [...byProvider.values()].flat();
|
|
42
46
|
}
|
|
43
47
|
async function readStandaloneModels() {
|
|
44
48
|
const { readStandaloneOpencodeModels } = await import("./opencode-standalone-models.js");
|
|
45
49
|
return readStandaloneOpencodeModels();
|
|
46
50
|
}
|
|
47
51
|
/**
|
|
48
|
-
* The OpenCode models for the Settings page. Without
|
|
52
|
+
* The OpenCode models for the Settings page. Without `listModels` or a registered
|
|
49
53
|
* OpenCode host, this starts a private OpenCode server through `readStandalone`.
|
|
50
54
|
* Pass `null` to skip that, for callers that need a live session.
|
|
51
55
|
*/
|
|
52
|
-
export async function listOpencodeSettingsModels(
|
|
56
|
+
export async function listOpencodeSettingsModels(listModels, readStandalone = readStandaloneModels) {
|
|
53
57
|
const unavailable = (reason) => ({ available: false, reason });
|
|
54
58
|
try {
|
|
55
59
|
let models;
|
|
56
|
-
if (
|
|
57
|
-
models = await listOpencodeClientModels(
|
|
60
|
+
if (listModels) {
|
|
61
|
+
models = await listOpencodeClientModels(listModels);
|
|
58
62
|
}
|
|
59
63
|
else {
|
|
60
64
|
const { getOpencodeHostModels } = await import("./backfill-controls.js");
|
package/dist/plugin.d.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
declare const OmmsPlugin: import("@opencode-ai/plugin").Plugin;
|
|
2
1
|
export declare const id = "omms";
|
|
3
|
-
export { OmmsPlugin };
|
|
4
2
|
declare const _default: {
|
|
5
3
|
id: string;
|
|
6
|
-
server: import("@opencode-ai/plugin").Plugin;
|
|
7
4
|
setup: (context: import("@opencode/plugin/promise/plugin").Context) => Promise<import("@opencode/plugin/promise/plugin").Cleanup | void> | import("@opencode/plugin/promise/plugin").Cleanup | void;
|
|
8
5
|
};
|
|
9
6
|
export default _default;
|
package/dist/plugin.js
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
const { OmmsPlugin } = await import("./index.js");
|
|
2
1
|
const { default: OmmsPluginV2 } = await import("./v2/plugin.js");
|
|
3
2
|
// Stable plugin id: OpenCode scopes plugin storage and enable/disable rules by it,
|
|
4
3
|
// so it stays "omms" independent of the npm package name.
|
|
5
4
|
export const id = "omms";
|
|
6
|
-
export {
|
|
7
|
-
export default {
|
|
8
|
-
...OmmsPluginV2,
|
|
9
|
-
id,
|
|
10
|
-
server: OmmsPlugin,
|
|
11
|
-
};
|
|
5
|
+
export default { ...OmmsPluginV2, id };
|
|
@@ -35,7 +35,6 @@ const keys = new Set([
|
|
|
35
35
|
"claudeConfigDir",
|
|
36
36
|
"autoCaptureLanguage",
|
|
37
37
|
"maxMemories",
|
|
38
|
-
"chatMessage.maxMemories",
|
|
39
38
|
"autoCaptureMaxContextBytes",
|
|
40
39
|
"userProfileMaxContextBytes",
|
|
41
40
|
"retrievalMaxTokens",
|
|
@@ -285,8 +284,8 @@ options = {}) {
|
|
|
285
284
|
}
|
|
286
285
|
checkMemorisationSettingsEdits(edits, memorisationSettingsFrom(current));
|
|
287
286
|
for (const [key, value] of Object.entries(edits)) {
|
|
288
|
-
// A nested identifier maps to its JSONC leaf path, so
|
|
289
|
-
//
|
|
287
|
+
// A nested identifier maps to its JSONC leaf path, so compaction.enabled
|
|
288
|
+
// writes the leaf, never a literal dotted key.
|
|
290
289
|
const jsoncPath = (getMemoryLimitRule(key)?.path ?? key.split("."));
|
|
291
290
|
// null is the wire form of "reset to default": remove the key so the
|
|
292
291
|
// value follows the default again (undefined removes in jsonc modify).
|
|
@@ -96,10 +96,10 @@ function projectFile(directory) {
|
|
|
96
96
|
.find((path) => existsSync(path));
|
|
97
97
|
}
|
|
98
98
|
/**
|
|
99
|
-
* Snapshot entry for a nested setting (
|
|
100
|
-
* The runtime merge is shallow, so a project
|
|
101
|
-
*
|
|
102
|
-
*
|
|
99
|
+
* Snapshot entry for a nested setting (compaction.enabled).
|
|
100
|
+
* The runtime merge is shallow, so a project object replaces the global one:
|
|
101
|
+
* a missing leaf means the default even when the global file sets a value, and
|
|
102
|
+
* the source must not be inferred from dotted-key presence alone.
|
|
103
103
|
*/
|
|
104
104
|
function nestedSetting(path, defaultValue, global, project) {
|
|
105
105
|
const parent = path[0];
|
|
@@ -127,8 +127,8 @@ function nestedSetting(path, defaultValue, global, project) {
|
|
|
127
127
|
export function getSettingsSnapshot(directory) {
|
|
128
128
|
const global = readSettingsFile(getGlobalConfigSourcePath());
|
|
129
129
|
const project = readSettingsFile(projectFile(directory));
|
|
130
|
-
// Dotted
|
|
131
|
-
//
|
|
130
|
+
// Dotted identifiers (compaction.enabled) are resolved by the nested rule
|
|
131
|
+
// above, so the runtime lookup only handles flat keys.
|
|
132
132
|
const runtime = CONFIG;
|
|
133
133
|
const injection = injectionOptionsFromFile(global);
|
|
134
134
|
const injectionDefaults = injectionOptionsFromFile({});
|
|
@@ -80,8 +80,10 @@ export class TursoVectorSearch {
|
|
|
80
80
|
const exactMatchBoost = tagMatchBoost(queryText, memoryTagsStr);
|
|
81
81
|
const finalTagsSim = Math.max(tagsSim, exactMatchBoost);
|
|
82
82
|
// Every memorisation carries the same tag, so the tags half is near constant and
|
|
83
|
-
// pulls a close match down.
|
|
84
|
-
|
|
83
|
+
// pulls a close match down. An untagged memory has no tags half at all, and the
|
|
84
|
+
// blend would cap it at 0.6 of its content match. Score both by content alone.
|
|
85
|
+
const untagged = row.tags_dist == null && !memoryTagsStr.trim();
|
|
86
|
+
const base = kind === "memorisation" || untagged ? contentSim : contentSim * 0.6 + finalTagsSim * 0.4;
|
|
85
87
|
const similarity = boostedSimilarity(base, kwHits, String(row.id), keywordBoost, keywordMode);
|
|
86
88
|
return {
|
|
87
89
|
id: String(row.id),
|
|
@@ -265,8 +265,8 @@ export declare const UpdateProfileItemRequestSchema: z.ZodObject<{
|
|
|
265
265
|
}>;
|
|
266
266
|
index: z.ZodNumber;
|
|
267
267
|
action: z.ZodEnum<{
|
|
268
|
-
delete: "delete";
|
|
269
268
|
edit: "edit";
|
|
269
|
+
delete: "delete";
|
|
270
270
|
}>;
|
|
271
271
|
category: z.ZodOptional<z.ZodString>;
|
|
272
272
|
description: z.ZodOptional<z.ZodString>;
|
|
@@ -16,7 +16,6 @@ export interface MemoryLimitRule {
|
|
|
16
16
|
}
|
|
17
17
|
export declare const MEMORY_LIMITS: {
|
|
18
18
|
readonly maxMemories: MemoryLimitRule;
|
|
19
|
-
readonly "chatMessage.maxMemories": MemoryLimitRule;
|
|
20
19
|
readonly autoCaptureMaxContextBytes: MemoryLimitRule;
|
|
21
20
|
readonly userProfileMaxContextBytes: MemoryLimitRule;
|
|
22
21
|
readonly retrievalMaxTokens: MemoryLimitRule;
|
|
@@ -23,7 +23,6 @@ function rule(setting, path, defaultValue, unit, accepted, isValid, defaultFor)
|
|
|
23
23
|
}
|
|
24
24
|
export const MEMORY_LIMITS = {
|
|
25
25
|
maxMemories: rule("maxMemories", ["maxMemories"], 10, "results", "positive safe integers", isPositiveSafeInteger),
|
|
26
|
-
"chatMessage.maxMemories": rule("chatMessage.maxMemories", ["chatMessage", "maxMemories"], 3, "memories", "positive safe integers", isPositiveSafeInteger),
|
|
27
26
|
autoCaptureMaxContextBytes: rule("autoCaptureMaxContextBytes", ["autoCaptureMaxContextBytes"], 131072, "UTF-8 bytes", "integers from 16384 to 16777216", integerInRange(16384, 16777216)),
|
|
28
27
|
userProfileMaxContextBytes: rule("userProfileMaxContextBytes", ["userProfileMaxContextBytes"], 32768, "UTF-8 bytes", "integers from 1024 to 16777216", integerInRange(1024, 16777216)),
|
|
29
28
|
retrievalMaxTokens: rule("retrievalMaxTokens", ["retrievalMaxTokens"], 2000, "approximate tokens", "integers from 256 to 65536", integerInRange(256, 65536)),
|