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.
Files changed (87) hide show
  1. package/README.md +1 -1
  2. package/dist/adapters/opencode/auto-capture-host.d.ts +2 -2
  3. package/dist/adapters/opencode/auto-capture-host.js +5 -15
  4. package/dist/adapters/opencode/auto-capture-summary.js +9 -6
  5. package/dist/adapters/opencode/backfill-models.js +1 -1
  6. package/dist/adapters/opencode/backfill-startup.js +5 -4
  7. package/dist/adapters/opencode/conversation.d.ts +5 -1
  8. package/dist/adapters/opencode/conversation.js +23 -23
  9. package/dist/adapters/opencode/import-command.d.ts +0 -12
  10. package/dist/adapters/opencode/import-command.js +1 -17
  11. package/dist/adapters/opencode/internal-capture-sessions.d.ts +0 -3
  12. package/dist/adapters/opencode/internal-capture-sessions.js +0 -26
  13. package/dist/adapters/opencode/memory-tool.d.ts +3 -4
  14. package/dist/adapters/opencode/memory-tool.js +8 -50
  15. package/dist/adapters/opencode/opencode-host.d.ts +27 -0
  16. package/dist/adapters/opencode/opencode-host.js +30 -0
  17. package/dist/adapters/opencode/opencode-import-models.d.ts +2 -2
  18. package/dist/adapters/opencode/opencode-import-models.js +8 -10
  19. package/dist/adapters/opencode/opencode-provider.d.ts +23 -62
  20. package/dist/adapters/opencode/opencode-provider.js +75 -378
  21. package/dist/adapters/opencode/package-skills.d.ts +1 -6
  22. package/dist/adapters/opencode/package-skills.js +2 -17
  23. package/dist/adapters/opencode/profile-learning.d.ts +1 -2
  24. package/dist/adapters/opencode/profile-learning.js +15 -19
  25. package/dist/adapters/opencode/profile-llm-client.d.ts +3 -2
  26. package/dist/adapters/opencode/profile-llm-client.js +8 -17
  27. package/dist/adapters/opencode/profile-model.d.ts +2 -18
  28. package/dist/adapters/opencode/profile-model.js +18 -69
  29. package/dist/adapters/opencode/prompt-model.d.ts +9 -0
  30. package/dist/adapters/opencode/prompt-model.js +21 -0
  31. package/dist/adapters/opencode/skills-tool.d.ts +3 -3
  32. package/dist/adapters/opencode/skills-tool.js +4 -30
  33. package/dist/adapters/opencode/toast.d.ts +11 -0
  34. package/dist/adapters/opencode/toast.js +11 -0
  35. package/dist/adapters/opencode/user-prompt.d.ts +2 -2
  36. package/dist/adapters/opencode/user-prompt.js +4 -5
  37. package/dist/config-template.d.ts +1 -1
  38. package/dist/config-template.js +2 -6
  39. package/dist/config.d.ts +0 -9
  40. package/dist/config.js +0 -9
  41. package/dist/core/retrieval.d.ts +0 -21
  42. package/dist/core/retrieval.js +0 -29
  43. package/dist/importer/handled-lookup.d.ts +17 -0
  44. package/dist/importer/handled-lookup.js +13 -0
  45. package/dist/importer/importer.js +3 -3
  46. package/dist/importer/ledger.d.ts +9 -0
  47. package/dist/importer/ledger.js +21 -0
  48. package/dist/importer/opencode-reader.d.ts +1 -1
  49. package/dist/importer/opencode-reader.js +88 -34
  50. package/dist/importer/opencode-v2-units.d.ts +16 -0
  51. package/dist/importer/opencode-v2-units.js +93 -0
  52. package/dist/importer/settings-models.d.ts +11 -6
  53. package/dist/importer/settings-models.js +15 -11
  54. package/dist/plugin.d.ts +0 -3
  55. package/dist/plugin.js +1 -7
  56. package/dist/services/global-config-writer.js +2 -3
  57. package/dist/services/settings-snapshot.js +6 -6
  58. package/dist/services/turso/vector-search.js +4 -2
  59. package/dist/shared/api/schemas.d.ts +1 -1
  60. package/dist/utils/memory-limits.d.ts +0 -1
  61. package/dist/utils/memory-limits.js +0 -1
  62. package/dist/v2/adapter.d.ts +16 -84
  63. package/dist/v2/adapter.js +17 -88
  64. package/dist/v2/memory-bridge.js +1 -1
  65. package/dist/v2/plugin.js +27 -16
  66. package/dist/v2/session-events.d.ts +30 -0
  67. package/dist/v2/session-events.js +130 -0
  68. package/dist/v2/startup.d.ts +14 -0
  69. package/dist/v2/startup.js +210 -0
  70. package/dist/v2/tools.d.ts +106 -0
  71. package/dist/v2/tools.js +89 -0
  72. package/dist/web/assets/index-DBSeQOs6.js +171 -0
  73. package/dist/web/index.html +1 -1
  74. package/package.json +2 -14
  75. package/dist/adapters/opencode/in-process-client.d.ts +0 -2
  76. package/dist/adapters/opencode/in-process-client.js +0 -16
  77. package/dist/adapters/opencode/opencode-diagnostics.d.ts +0 -7
  78. package/dist/adapters/opencode/opencode-diagnostics.js +0 -37
  79. package/dist/adapters/opencode/opencode-host-config.d.ts +0 -13
  80. package/dist/adapters/opencode/opencode-host-config.js +0 -53
  81. package/dist/adapters/opencode/opencode-sdk-client.d.ts +0 -6
  82. package/dist/adapters/opencode/opencode-sdk-client.js +0 -38
  83. package/dist/index.d.ts +0 -28
  84. package/dist/index.js +0 -620
  85. package/dist/v2/legacy-client.d.ts +0 -15
  86. package/dist/v2/legacy-client.js +0 -275
  87. 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.
@@ -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 V1 OpenCode history without changing its database or WAL sidecars. */
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 validateSchema(db) {
12
- const required = {
13
- session: ["id", "directory", "project_id", "parent_id", "time_created"],
14
- project: ["id", "worktree"],
15
- message: ["id", "session_id", "time_created", "data"],
16
- part: ["id", "message_id", "session_id", "time_created", "data"],
17
- };
18
- for (const [table, columns] of Object.entries(required)) {
19
- const found = db
20
- .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?")
21
- .get(table);
22
- if (!found)
23
- throw new Error(`Unsupported OpenCode V1 database: missing ${table} table`);
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 V1 database: missing ${table}.${column}`);
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
- validateSchema(db);
206
+ const sets = detectTableSets(db);
158
207
  // omms's own capture/profile calls run in OpenCode sessions; never import them as history.
159
- const hasTitle = db.prepare("PRAGMA table_info(session)").all().some((column) => column.name === "title");
160
- const notInternal = hasTitle
161
- ? `AND (s.title IS NULL OR s.title NOT IN (${INTERNAL_CAPTURE_SESSION_TITLES.map(() => "?").join(", ")}))`
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 session s WHERE ${where} ${notInternal}`)
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 session s LEFT JOIN project p ON p.id = s.project_id
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 sessions = db.prepare(query).all(...args).map((row) => ({
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 V1 OpenCode history without changing its database or WAL sidecars. */
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
- ...listed,
217
- loadUnits: (sessionId) => buildWindows(opened.db, sessionId, filters),
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 v1 provider list. */
20
+ /** The models of each connected provider in an OpenCode server provider list. */
22
21
  export declare function connectedProviderModels(list: ProviderList): SettingsModel[];
23
- /** Connected models from an OpenCode client; null when the client returns no list. */
24
- export declare function listOpencodeClientModels(client: Pick<OpencodeClient, "provider">): Promise<SettingsModel[] | null>;
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 a client or a registered
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(client?: Pick<OpencodeClient, "provider">, readStandalone?: (() => Promise<StandaloneResult>) | null): Promise<{
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 v1 provider list. */
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
- /** Connected models from an OpenCode client; null when the client returns no list. */
37
- export async function listOpencodeClientModels(client) {
38
- const result = await client.provider.list();
39
- if (!result.data)
40
- return null;
41
- return connectedProviderModels(result.data);
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 a client or a registered
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(client, readStandalone = readStandaloneModels) {
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 (client) {
57
- models = await listOpencodeClientModels(client);
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 { OmmsPlugin };
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 chatMessage.maxMemories
289
- // and compaction.enabled write the leaf, never a literal dotted key.
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 (chatMessage.maxMemories, compaction.*).
100
- * The runtime merge is shallow, so a project chatMessage object replaces the
101
- * global one: a missing count means the default even when the global file sets
102
- * a value, and the source must not be inferred from dotted-key presence alone.
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 limit identifiers (chatMessage.maxMemories) are resolved by the
131
- // nested rule above, so the runtime lookup only handles flat keys.
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. Score memorisations by content alone.
84
- const base = kind === "memorisation" ? contentSim : contentSim * 0.6 + finalTagsSim * 0.4;
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)),