@elvisw/dsh-magic-context 0.0.0-bootstrap.0 → 0.0.0-stage

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 (66) hide show
  1. package/README.md +2 -452
  2. package/package.json +4 -108
  3. package/LICENSE +0 -23
  4. package/NOTICE +0 -93
  5. package/README.zh-CN.md +0 -368
  6. package/cordis.patch.yml +0 -20
  7. package/lib/client.js +0 -2270
  8. package/lib/core/compartments.js +0 -424
  9. package/lib/core/corpus-store.js +0 -176
  10. package/lib/core/directive-detect.js +0 -147
  11. package/lib/core/domain-reconcile.js +0 -394
  12. package/lib/core/domains.js +0 -127
  13. package/lib/core/embedding-local.js +0 -247
  14. package/lib/core/global-autostage.js +0 -223
  15. package/lib/core/global-domains.js +0 -302
  16. package/lib/core/journal.js +0 -248
  17. package/lib/core/llm-deadline.js +0 -53
  18. package/lib/core/machine-domain.js +0 -106
  19. package/lib/core/maintenance.js +0 -380
  20. package/lib/core/memory-bridge.js +0 -818
  21. package/lib/core/memory-categories.js +0 -84
  22. package/lib/core/memory-scoring.js +0 -65
  23. package/lib/core/memory-search.js +0 -414
  24. package/lib/core/memory-write.js +0 -1176
  25. package/lib/core/policy.js +0 -111
  26. package/lib/core/profile-confirmation.js +0 -55
  27. package/lib/core/profile-review.js +0 -375
  28. package/lib/core/profile-write.js +0 -283
  29. package/lib/core/reference-seeds.js +0 -63
  30. package/lib/core/route-health.js +0 -106
  31. package/lib/core/stores.js +0 -1021
  32. package/lib/core/tagger.js +0 -7
  33. package/lib/core/types.js +0 -15
  34. package/lib/core/vector-backfill.js +0 -140
  35. package/lib/core/vector-lane.js +0 -184
  36. package/lib/dsh/command-surface.js +0 -376
  37. package/lib/dsh/compaction-engine.js +0 -231
  38. package/lib/dsh/config-switches.js +0 -56
  39. package/lib/dsh/config.js +0 -13
  40. package/lib/dsh/context-pressure.js +0 -36
  41. package/lib/dsh/drop-landing.js +0 -42
  42. package/lib/dsh/emergency.js +0 -89
  43. package/lib/dsh/flush-command.js +0 -148
  44. package/lib/dsh/global-review.js +0 -412
  45. package/lib/dsh/historian.js +0 -907
  46. package/lib/dsh/live-journal.js +0 -81
  47. package/lib/dsh/maintenance-wiring.js +0 -229
  48. package/lib/dsh/management-pass.js +0 -564
  49. package/lib/dsh/memory-lane.js +0 -535
  50. package/lib/dsh/pre-step.js +0 -675
  51. package/lib/dsh/project-drift.js +0 -116
  52. package/lib/dsh/recomp.js +0 -192
  53. package/lib/dsh/reduce-guidance.js +0 -188
  54. package/lib/dsh/seams.js +0 -588
  55. package/lib/dsh/search-hint.js +0 -186
  56. package/lib/dsh/session-notice.js +0 -100
  57. package/lib/dsh/status-command.js +0 -75
  58. package/lib/dsh/telemetry.js +0 -349
  59. package/lib/dsh/tool-bridge.js +0 -1009
  60. package/lib/dsh/volatile.js +0 -89
  61. package/lib/index.js +0 -931
  62. package/tools/migrate-profile-package-name.mjs +0 -317
  63. package/vendor/onnx/LICENSE +0 -27
  64. package/vendor/onnx/ort-wasm-simd-threaded.mjs +0 -59
  65. package/vendor/onnx/ort-wasm-simd-threaded.wasm +0 -0
  66. package/vendor/onnx/ort.node.min.mjs +0 -8
@@ -1,818 +0,0 @@
1
- /**
2
- * The project-memory bridge — the plugin's OWN corpus (`memories.db`, cut
3
- * over from the legacy shared store 2026-10-13), read by the DSH adapter.
4
- *
5
- * Facts this module is built on (shape inherited verbatim from Magic
6
- * Context's v95 storage layer; the schema is OURS now — see corpus-store.ts):
7
- * - `memories` has NO `harness` column: project memories are shared across
8
- * sessions on this machine through the corpus;
9
- * - `project_path` stores an IDENTITY (`git:<root-commit-sha>` or
10
- * `dir:<hash>`), not a path; legacy rows may still hold raw paths, so the
11
- * bridge matches the identity and the normalized path;
12
- * - the schema version lives in `schema_migrations` (lane `< 10000`);
13
- * a database newer than `MEMORY_SCHEMA_MAX_VERSION` is
14
- * fail-closed (read nothing) rather than guessed at;
15
- * - injectable rows are `status IN ('active','permanent')` and not expired;
16
- * domain-shared rows additionally need `shareable = 1`,
17
- * `scope IN ('project','ecosystem','universe')` and a category in the
18
- * active domains' `share_categories` allowlist;
19
- * - selection priority is permanent → importance → max(last_seen_at,
20
- * verified_at) → id, trimmed to a token budget.
21
- *
22
- * WRITES ARE DELIBERATELY ABSENT: inserting memories also means computing
23
- * Magic Context's normalized hash, bumping `project_state.project_memory_epoch`
24
- * only when the injectable set changes, appending `memory_mutation_log`, and
25
- * honoring the authority guard — invariants owned by Magic Context's storage
26
- * layer. The adapter recalls; it does not fork the memory store.
27
- */
28
- import { DatabaseSync } from 'node:sqlite';
29
- import { createHash } from 'node:crypto';
30
- import { spawnSync } from 'node:child_process';
31
- import { existsSync } from 'node:fs';
32
- import { homedir } from 'node:os';
33
- import { join, normalize, resolve } from 'node:path';
34
- import { estimateTokens, MEMORY_VISIBILITY_SENTINEL } from "./compartments.js";
35
- import { memoryCategoryOrder } from "./memory-categories.js";
36
- import { scoreMemory } from "./memory-scoring.js";
37
- import { errText } from "./types.js";
38
- /** The corpus schema ceiling this build understands. Own lineage: the shape
39
- * was inherited verbatim from upstream MC's v95 store at the 2026-10-13
40
- * cutover (the adapter stopped sharing `context.db`; see corpus-store.ts),
41
- * and every future `schema_migrations` row is OURS — a version above this
42
- * ceiling can only mean the corpus is newer than this build, which must
43
- * fence to read-only exactly as before. */
44
- export const MEMORY_SCHEMA_MAX_VERSION = 95;
45
- /** MC's terminal mutation types (`storage-memory-mutation-log.ts:9-15`): an
46
- * `update` queued after one of these must not resurrect the memory. */
47
- export const TERMINAL_MUTATION_TYPES = new Set(['archive', 'delete', 'superseded']);
48
- /** MC's `MAX_MEMORY_REPLACEMENT_DEPTH` — the supersede-chain walk cap. */
49
- export const MAX_MEMORY_REPLACEMENT_DEPTH = 8;
50
- /** Default page size for one cursor read (the real log holds hundreds of rows). */
51
- export const DEFAULT_MUTATION_PAGE_LIMIT = 1_000;
52
- /** The default domain sharing allowlist when `share_categories` is absent. */
53
- export const DEFAULT_SHARE_CATEGORIES = ['CONSTRAINTS'];
54
- /** Direct-caller fallback for the corpus path (scripts, tools). The lane itself
55
- * resolves the corpus explicitly — beside the adapter store in the profile's
56
- * dataDir — and never consults this (cutover 2026-10-13: no more sharing
57
- * upstream MC's `context.db`). */
58
- export function defaultMemoryDbPath() {
59
- return join(homedir(), '.local', 'share', 'cortexkit', 'magic-context-dsh-poc', 'memories.db');
60
- }
61
- /** Default m[0] budget for `<user-profile>` (Magic Context's own number). */
62
- export const PROFILE_M0_BUDGET_TOKENS = 4000;
63
- const SELECT_COLUMNS = 'id, project_path, category, content, importance, shareable, status, expires_at, updated_at, last_seen_at, verified_at';
64
- /** Magic Context's normalized-hash whitespace set, for stable digests only. */
65
- export function normalizeMemoryContent(content) {
66
- return content.toLowerCase().replace(/\s+/g, ' ').trim();
67
- }
68
- /**
69
- * Directory identity, byte-for-byte compatible with Magic Context's
70
- * `directoryFallback` (`project-identity.ts`): MD5 of `path.resolve(directory)`
71
- * truncated to 12 hex characters.
72
- *
73
- * Do NOT normalize separators here: Magic Context hashes the resolved path
74
- * verbatim, so on Windows the digest contains the backslashes. A
75
- * forward-slash canonicalisation silently orphans every memory of a non-git
76
- * project (measured: `dir:1afbff92eb86` vs a wrong `dir:931289036571` for the
77
- * same directory).
78
- */
79
- export function directoryIdentity(directory) {
80
- const canonical = resolve(directory);
81
- return `dir:${createHash('md5').update(canonical, 'utf8').digest('hex').slice(0, 12)}`;
82
- }
83
- /** Cache of git identities, since a directory's root commit cannot change. */
84
- const gitIdentityCache = new Map();
85
- /**
86
- * `git:<root commit sha>` for a directory inside a git repository, matching
87
- * Magic Context's primary identity. Best effort by design: a missing git, a
88
- * non-repository, or an empty history yields undefined and the caller falls
89
- * back to {@link directoryIdentity}. Magic Context itself retries git with a
90
- * cooldown; the adapter instead caches the verdict for the process lifetime.
91
- */
92
- export function gitProjectIdentity(directory, timeoutMs = 3_000) {
93
- const key = resolve(directory);
94
- const cached = gitIdentityCache.get(key);
95
- if (cached !== undefined || gitIdentityCache.has(key))
96
- return cached;
97
- let identity;
98
- try {
99
- const result = spawnSync('git', ['rev-list', '--max-parents=0', 'HEAD'], {
100
- cwd: key,
101
- encoding: 'utf8',
102
- timeout: timeoutMs,
103
- windowsHide: true,
104
- });
105
- // Multi-root histories: pick the lexicographically smallest root sha so the
106
- // identity is a pure function of the root-commit set (MC project-identity.ts).
107
- const first = result.status === 0
108
- ? (result.stdout ?? '').split('\n').map(line => line.trim().slice(0, 64)).filter(line => /^[0-9a-f]{7,64}$/.test(line)).sort()[0]
109
- : undefined;
110
- identity = first === undefined ? undefined : `git:${first}`;
111
- }
112
- catch {
113
- identity = undefined;
114
- }
115
- gitIdentityCache.set(key, identity);
116
- return identity;
117
- }
118
- /** The identity Magic Context would use for a directory: git when possible. */
119
- export function resolveProjectIdentity(directory) {
120
- return gitProjectIdentity(directory) ?? directoryIdentity(directory);
121
- }
122
- /** The one identity normalizer: `git:`/`dir:` identities pass through, any
123
- * other string resolves as a directory. Every caller (session scope, the
124
- * `ctx_memory` `project` argument, recall scoping) shares this spelling. */
125
- export function resolveIdentitySpec(spec) {
126
- if (spec.startsWith('git:') || spec.startsWith('dir:'))
127
- return spec;
128
- return resolveProjectIdentity(spec);
129
- }
130
- function identityCandidates(project) {
131
- if (project === undefined || project.length === 0)
132
- return [];
133
- if (project.startsWith('git:') || project.startsWith('dir:'))
134
- return [project];
135
- // A directory: resolve it the way Magic Context would (git root commit, then
136
- // the dir hash), and keep the raw normalized path as a legacy candidate for
137
- // rows written before identities replaced paths.
138
- const primary = resolveProjectIdentity(project);
139
- const legacy = normalize(project).replace(/\\/g, '/');
140
- return primary === legacy ? [primary] : [primary, legacy];
141
- }
142
- function priorityOf(row) {
143
- return [
144
- row.permanent ? 1 : 0,
145
- row.importance,
146
- Math.max(row.lastSeenAt, row.verifiedAt ?? 0),
147
- row.id,
148
- ];
149
- }
150
- function comparePriority(left, right) {
151
- const a = priorityOf(left);
152
- const b = priorityOf(right);
153
- for (let index = 0; index < a.length; index += 1) {
154
- const difference = (b[index] ?? 0) - (a[index] ?? 0);
155
- if (difference !== 0)
156
- return difference;
157
- }
158
- return 0;
159
- }
160
- function categoryRank(category) {
161
- return memoryCategoryOrder(category);
162
- }
163
- /** Render one memory as a baseline line (category-tagged, MC render order). */
164
- export function renderMemoryLine(row) {
165
- const badge = row.permanent ? ', permanent' : '';
166
- return `- [${row.category || 'MEMORY'}${badge}] ${row.content.replace(/\s+/g, ' ').trim()}`;
167
- }
168
- /** Render one profile row as a baseline line. */
169
- export function renderProfileLine(row) {
170
- return `- ${row.content.replace(/\s+/g, ' ').trim()}`;
171
- }
172
- /**
173
- * Open the shared memory database read-only. Any incompatibility (missing
174
- * file, unknown schema version, missing table) disables recall with a loud
175
- * warning — never an exception into the turn, never a write.
176
- */
177
- export function openMemoryBridge(options) {
178
- const dbPath = options.dbPath ?? defaultMemoryDbPath();
179
- const unavailable = (detail) => {
180
- options.logger.warn(`magic-context memory bridge disabled: ${detail} (${dbPath})`);
181
- return {
182
- dbPath,
183
- status: { ok: false, detail },
184
- recall: () => undefined,
185
- // No space resolution without a store: recall is off, and so is every
186
- // domain-scoped read that would have used this.
187
- searchSpace: () => undefined,
188
- maxMemoryId: () => 0,
189
- mutations: () => undefined,
190
- maxMemoryMutationId: () => 0,
191
- projectMemoryEpochFingerprint: () => '',
192
- profileRecall: () => undefined,
193
- close: () => undefined,
194
- };
195
- };
196
- if (!existsSync(dbPath))
197
- return unavailable('database not found');
198
- let db;
199
- try {
200
- db = new DatabaseSync(dbPath, { readOnly: true });
201
- }
202
- catch (error) {
203
- return unavailable(`cannot open (${errText(error)})`);
204
- }
205
- try {
206
- const versionRow = db.prepare('SELECT MAX(version) AS version FROM schema_migrations WHERE version < 10000').get();
207
- const schemaVersion = versionRow?.version ?? undefined;
208
- if (schemaVersion === undefined || schemaVersion === null) {
209
- db.close();
210
- return unavailable('schema_migrations carries no recognized version');
211
- }
212
- if (schemaVersion > MEMORY_SCHEMA_MAX_VERSION) {
213
- db.close();
214
- return unavailable(`schema v${String(schemaVersion)} is newer than the adapter understands (v${String(MEMORY_SCHEMA_MAX_VERSION)})`);
215
- }
216
- const table = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'memories'").get();
217
- if (table === undefined) {
218
- db.close();
219
- return unavailable('memories table missing');
220
- }
221
- const columns = new Set(db.prepare('PRAGMA table_info(memories)').all().map(row => row.name));
222
- for (const required of ['id', 'project_path', 'category', 'content', 'status', 'updated_at']) {
223
- if (!columns.has(required)) {
224
- db.close();
225
- return unavailable(`memories.${required} missing (older schema than expected)`);
226
- }
227
- }
228
- const selfStatement = db.prepare(`SELECT ${SELECT_COLUMNS} FROM memories
229
- WHERE project_path = ?
230
- AND status IN ('active','permanent')
231
- AND (expires_at IS NULL OR expires_at > ?)`);
232
- const foreignStatement = db.prepare(`SELECT ${SELECT_COLUMNS} FROM memories
233
- WHERE project_path = ?
234
- AND status IN ('active','permanent')
235
- AND (expires_at IS NULL OR expires_at > ?)
236
- AND shareable = 1
237
- AND scope IN ('project','ecosystem','universe')`);
238
- // B1: the watermark is the VISIBILITY-qualified max — expired/archived
239
- // rows and non-shared peer rows must never force a HARD rebuild when they
240
- // land above it (MC freezes the max over the visible pool, not the table).
241
- const maxIdStatement = db.prepare(`SELECT MAX(id) AS id FROM memories WHERE project_path = ?`
242
- + ` AND status IN ('active','permanent') AND (expires_at IS NULL OR expires_at > ?)`);
243
- /** WP3: THE activation function (see MemoryBridgeOptions.domainResolver). */
244
- const peersFor = (identities) => {
245
- const resolve = options.domainResolver;
246
- if (resolve === undefined)
247
- return { members: [], peers: [] };
248
- const domains = [];
249
- let answered = false;
250
- for (const identity of identities) {
251
- const answer = resolve(identity);
252
- if (answer === undefined)
253
- continue;
254
- answered = true;
255
- for (const domain of answer) {
256
- if (!domains.some(existing => existing.identity === domain.identity))
257
- domains.push(domain);
258
- }
259
- }
260
- // No domain has this identity yet: no peers. The old fallback to the
261
- // shared store's `workspaces` membership died with the 2026-10-13
262
- // corpus cutover — the own corpus carries no workspace tables, and
263
- // cross-project sharing is exclusively domain-driven.
264
- if (!answered)
265
- return { members: [], peers: [] };
266
- const members = domains.map(domain => domain.identity).filter(identity => !identities.includes(identity));
267
- if (members.length === 0)
268
- return { members: [], peers: [] };
269
- // The SQL filter is a single `category IN (...)`, so the effective
270
- // allowlist is the union of the activated domains'. That cannot widen what
271
- // a domain is allowed to share because publishing pre-checks the category
272
- // against ITS OWN allowlist (core/global-domains).
273
- const categories = [...new Set(domains.flatMap(domain => [...domain.shareCategories]))]
274
- .sort((left, right) => left.localeCompare(right));
275
- return {
276
- domains: domains.map(domain => domain.identity).join('+'),
277
- members,
278
- shareCategories: categories,
279
- peers: domains,
280
- };
281
- };
282
- // WP8: the user profile. `user_memories` is optional (older contracts lack
283
- // it) and its stamp columns are probed rather than assumed, so a schema
284
- // drift degrades to "no profile" instead of taking the recall path down.
285
- let profileStatement;
286
- if (db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'user_memories'").get() !== undefined) {
287
- const profileColumns = new Set(db.prepare('PRAGMA table_info(user_memories)').all().map(row => row.name));
288
- if (profileColumns.has('id') && profileColumns.has('content') && profileColumns.has('status')) {
289
- const promoted = profileColumns.has('promoted_at')
290
- ? 'promoted_at'
291
- : profileColumns.has('created_at') ? 'created_at' : 'id';
292
- const updated = profileColumns.has('updated_at') ? 'updated_at' : promoted;
293
- profileStatement = db.prepare('SELECT id, content, ' + promoted + ' AS promoted_at, ' + updated + ' AS updated_at '
294
- + "FROM user_memories WHERE status = 'active' ORDER BY " + promoted + ' ASC, id ASC');
295
- }
296
- }
297
- const toRow = (row) => ({ id: row.id,
298
- projectPath: row.project_path,
299
- category: row.category ?? '',
300
- content: row.content,
301
- importance: row.importance ?? 50,
302
- shareable: row.shareable === 1,
303
- permanent: row.status === 'permanent',
304
- updatedAt: row.updated_at,
305
- lastSeenAt: row.last_seen_at ?? row.updated_at,
306
- ...row.verified_at === null ? {} : { verifiedAt: row.verified_at },
307
- });
308
- const maxFor = (identities, now) => {
309
- let highest = 0;
310
- for (const identity of identities) {
311
- const row = maxIdStatement.get(identity, now);
312
- if (row?.id != null && row.id > highest)
313
- highest = row.id;
314
- }
315
- return highest;
316
- };
317
- /** B1: the peer-side max carries the share filter (shareable + scope +
318
- * category allowlist) — a hidden peer row above the watermark is not
319
- * "visible", so freezing it would only produce a phantom m[1] line. */
320
- const maxForeignId = (peerIdentities, categories, now) => {
321
- if (peerIdentities.length === 0 || categories.length === 0)
322
- return 0;
323
- const placeholders = peerIdentities.map(() => '?').join(', ');
324
- const categoryPlaceholders = categories.map(() => '?').join(', ');
325
- const row = db.prepare(`SELECT MAX(id) AS id FROM memories WHERE project_path IN (${placeholders})`
326
- + ` AND status IN ('active','permanent') AND (expires_at IS NULL OR expires_at > ?)`
327
- + ` AND shareable = 1 AND scope IN ('project','ecosystem','universe')`
328
- + ` AND category IN (${categoryPlaceholders})`).get(...peerIdentities, now, ...categories);
329
- return row?.id ?? 0;
330
- };
331
- /** The watermark max over the visible pool: own rows (eligible) plus peer
332
- * rows (eligible AND shared). */
333
- const maxVisibleId = (ownIdentities, peerIdentities, categories, now) => Math.max(maxFor(ownIdentities, now), maxForeignId(peerIdentities, categories, now));
334
- // ── The non-additive change lane (`memory_mutation_log`, contract §6) ────
335
- // Everything below is strictly read-only. Both tables are probed, because
336
- // an older schema (or a synthetic fixture) may lack them: a missing table
337
- // means "no deltas", never a failure. The same applies to the optional
338
- // `memories` columns — the lane must never take the recall path down.
339
- const memoryColumnNames = new Set(db.prepare('PRAGMA table_info(memories)').all().map(row => row.name));
340
- const memoryColumn = (name) => memoryColumnNames.has(name) ? name : `NULL AS ${name}`;
341
- let hasMutationLog = false;
342
- let mutationStatement;
343
- let mutationMaxStatement;
344
- let epochStatement;
345
- let mutationMemoryStatement;
346
- try {
347
- hasMutationLog = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'memory_mutation_log'").get() !== undefined;
348
- const hasProjectState = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'project_state'").get() !== undefined;
349
- const MUTATION_COLUMNS = 'id, project_path, mutation_type, target_memory_id, superseded_by_id, category, new_content, queued_at';
350
- mutationStatement = hasMutationLog
351
- ? db.prepare(`SELECT ${MUTATION_COLUMNS} FROM memory_mutation_log
352
- WHERE project_path = ? AND id > ? ORDER BY id ASC LIMIT ?`)
353
- : undefined;
354
- mutationMaxStatement = hasMutationLog
355
- ? db.prepare('SELECT MAX(id) AS id FROM memory_mutation_log WHERE project_path = ?')
356
- : undefined;
357
- epochStatement = hasProjectState
358
- ? db.prepare('SELECT project_memory_epoch AS epoch FROM project_state WHERE project_path = ?')
359
- : undefined;
360
- // The mutation payload is resolved against the *live* memory row: MC
361
- // re-reads the target for `update` (a later write is the truth) and walks
362
- // `superseded_by_memory_id` to the terminal replacement (§6.3-§6.4).
363
- mutationMemoryStatement = db.prepare(`SELECT id, project_path, category, content, ${memoryColumn('scope')}, ${memoryColumn('shareable')}, `
364
- + `status, ${memoryColumn('expires_at')}, ${memoryColumn('superseded_by_memory_id')} `
365
- + 'FROM memories WHERE id = ?');
366
- }
367
- catch (error) {
368
- mutationStatement = undefined;
369
- mutationMaxStatement = undefined;
370
- epochStatement = undefined;
371
- mutationMemoryStatement = undefined;
372
- options.logger.warn('magic-context memory mutation lane disabled: '
373
- + `${errText(error)} (${dbPath})`);
374
- }
375
- const epochOf = (identity) => {
376
- if (epochStatement === undefined)
377
- return 0;
378
- const row = epochStatement.get(identity);
379
- return row?.epoch ?? 0;
380
- };
381
- /**
382
- * The epoch identity of a baseline. A bare epoch integer is not enough:
383
- * domain membership changes move the epoch of a *peer*, and MC compares
384
- * a fingerprint that hashes every member's epoch (`workspaces.ts:312-334`).
385
- */
386
- const epochFingerprint = (identities) => {
387
- const pairs = [...new Set(identities)].sort().map(identity => `${identity}:${String(epochOf(identity))}`);
388
- return createHash('sha1').update(pairs.join('|')).digest('hex').slice(0, 16);
389
- };
390
- const maxMutationFor = (identities) => {
391
- if (mutationMaxStatement === undefined)
392
- return 0;
393
- let highest = 0;
394
- for (const identity of identities) {
395
- const row = mutationMaxStatement.get(identity);
396
- if (row?.id != null && row.id > highest)
397
- highest = row.id;
398
- }
399
- return highest;
400
- };
401
- /**
402
- * MC's `coalesceMutations` (`storage-memory-mutation-log.ts:144-229`): one
403
- * row per target memory, and a terminal state beats a later non-terminal
404
- * one so an `update` queued after an `archive`/`delete`/`superseded`
405
- * cannot resurrect the memory. Input arrives oldest-first.
406
- */
407
- const coalesceMutations = (rows) => {
408
- const byTarget = new Map();
409
- for (const row of rows) {
410
- const selected = byTarget.get(row.targetMemoryId);
411
- if (selected === undefined) {
412
- byTarget.set(row.targetMemoryId, row);
413
- continue;
414
- }
415
- if (TERMINAL_MUTATION_TYPES.has(selected.mutationType)
416
- && !TERMINAL_MUTATION_TYPES.has(row.mutationType))
417
- continue;
418
- byTarget.set(row.targetMemoryId, row);
419
- }
420
- return [...byTarget.values()].sort((left, right) => left.id - right.id);
421
- };
422
- const eligibleRow = (row, now) => (row.status === 'active' || row.status === 'permanent')
423
- && (row.expires_at === null || row.expires_at > now);
424
- /** Own rows are always ours; a peer's row only while it is shared here. */
425
- const visibleRow = (row, identities, shared) => identities.includes(row.project_path)
426
- || (row.shareable === 1
427
- && row.scope !== null
428
- && ['project', 'ecosystem', 'universe'].includes(row.scope)
429
- && row.category !== null
430
- && shared.has(row.category));
431
- /**
432
- * Walk `superseded_by_memory_id` to the terminal replacement (§6.3 step 4),
433
- * bounded by MC's depth cap. Returns undefined when the chain is broken,
434
- * cyclic, too deep, or the replacement is not currently eligible/visible —
435
- * the caller then degrades `<superseded .../>` to `<removed .../>`.
436
- */
437
- const resolveReplacement = (startId, identities, shared, now) => {
438
- let current = startId;
439
- const visited = new Set();
440
- for (let depth = 0; depth < MAX_MEMORY_REPLACEMENT_DEPTH; depth += 1) {
441
- if (current === undefined || visited.has(current) || mutationMemoryStatement === undefined)
442
- return undefined;
443
- visited.add(current);
444
- const row = mutationMemoryStatement.get(current);
445
- if (row === undefined)
446
- return undefined;
447
- if (!eligibleRow(row, now) || !visibleRow(row, identities, shared))
448
- return undefined;
449
- const next = row.superseded_by_memory_id;
450
- if (next === null || next === undefined)
451
- return row;
452
- current = next;
453
- }
454
- return undefined;
455
- };
456
- const toMutationRow = (raw) => ({
457
- id: raw.id,
458
- projectPath: raw.project_path,
459
- mutationType: raw.mutation_type,
460
- targetMemoryId: raw.target_memory_id,
461
- ...raw.superseded_by_id === null ? {} : { supersededById: raw.superseded_by_id },
462
- ...raw.category === null ? {} : { category: raw.category },
463
- ...raw.new_content === null ? {} : { newContent: raw.new_content },
464
- queuedAt: raw.queued_at,
465
- });
466
- /** Fold a coalesced row into the value object `<memory-updates>` renders. */
467
- const toUpdateLine = (row, identities, shared, now) => {
468
- const target = mutationMemoryStatement?.get(row.targetMemoryId);
469
- const realCategory = row.category !== undefined && row.category.length > 0
470
- && row.category !== MEMORY_VISIBILITY_SENTINEL
471
- ? row.category
472
- : target?.category ?? undefined;
473
- // A peer's change is only rendered when its category is shared with us.
474
- if (!identities.includes(row.projectPath)
475
- && !(realCategory !== undefined && shared.has(realCategory)))
476
- return undefined;
477
- const base = {
478
- id: row.id,
479
- targetMemoryId: row.targetMemoryId,
480
- mutationType: row.mutationType,
481
- ...realCategory === undefined ? {} : { category: realCategory },
482
- };
483
- if (row.mutationType === 'superseded') {
484
- const replacement = resolveReplacement(row.supersededById, identities, shared, now);
485
- return {
486
- ...base,
487
- ...row.supersededById === undefined ? {} : { supersededById: row.supersededById },
488
- replacementResolvable: replacement !== undefined,
489
- };
490
- }
491
- if (row.mutationType !== 'update')
492
- return base;
493
- // MC re-reads the live row for the update payload; `new_content` is the
494
- // queued snapshot and the fallback when the row is already gone.
495
- const content = target?.content ?? row.newContent;
496
- if (content === undefined || content.length === 0)
497
- return base;
498
- return { ...base, content };
499
- };
500
- const readMutations = (request) => {
501
- try {
502
- const identities = identityCandidates(request.project);
503
- const auto = peersFor(identities);
504
- const peers = request.domainProjects ?? auto.members;
505
- const peerIdentities = peers.flatMap(candidate => identityCandidates(candidate));
506
- const allIdentities = [...identities, ...peerIdentities];
507
- const shared = new Set(request.shareCategories ?? auto.shareCategories ?? DEFAULT_SHARE_CATEGORIES);
508
- const limit = Math.max(1, Math.floor(request.limit ?? DEFAULT_MUTATION_PAGE_LIMIT));
509
- const fingerprint = epochFingerprint(allIdentities);
510
- const raw = [];
511
- const seenIds = new Set();
512
- for (const identity of allIdentities) {
513
- if (mutationStatement === undefined)
514
- break;
515
- // `limit + 1` so a truncated page is detectable rather than guessed.
516
- const page = mutationStatement.all(identity, request.afterId, limit + 1);
517
- for (const item of page) {
518
- if (seenIds.has(item.id))
519
- continue;
520
- seenIds.add(item.id);
521
- raw.push(toMutationRow(item));
522
- }
523
- }
524
- const ordered = raw.sort((left, right) => left.id - right.id);
525
- const hasMore = ordered.length > limit;
526
- const page = hasMore ? ordered.slice(0, limit) : ordered;
527
- const cursor = page.length === 0
528
- ? request.afterId
529
- : Math.max(request.afterId, page[page.length - 1].id);
530
- const coalesced = coalesceMutations(page);
531
- const updates = coalesced
532
- .map(row => toUpdateLine(row, identities, shared, request.now))
533
- .filter((line) => line !== undefined);
534
- return {
535
- updates,
536
- rows: coalesced,
537
- cursor,
538
- currentCursor: maxMutationFor(allIdentities),
539
- projectEpochFingerprint: fingerprint,
540
- projectEpoch: identities[0] === undefined ? 0 : epochOf(identities[0]),
541
- hasMore,
542
- ...identities[0] === undefined ? {} : { identity: identities[0] },
543
- members: peers,
544
- };
545
- }
546
- catch (error) {
547
- options.logger.warn(`magic-context memory mutation read failed (continuing without deltas): `
548
- + `${errText(error)}`);
549
- return undefined;
550
- }
551
- };
552
- return {
553
- dbPath,
554
- status: { ok: true, schemaVersion },
555
- searchSpace: (project) => {
556
- try {
557
- const identities = identityCandidates(project);
558
- if (identities.length === 0)
559
- return undefined;
560
- const auto = peersFor(identities);
561
- const foreignIdentities = auto.members.flatMap(candidate => identityCandidates(candidate));
562
- return {
563
- identity: identities[0],
564
- ownIdentities: identities,
565
- foreignIdentities,
566
- members: auto.members,
567
- ...auto.domains === undefined ? {} : { domains: auto.domains },
568
- ...auto.shareCategories === undefined ? {} : { shareCategories: auto.shareCategories },
569
- };
570
- }
571
- catch {
572
- return undefined;
573
- }
574
- },
575
- maxMemoryId: (project) => {
576
- try {
577
- const identities = identityCandidates(project);
578
- const auto = peersFor(identities);
579
- const peerIdentities = auto.members.flatMap(candidate => identityCandidates(candidate));
580
- return maxVisibleId(identities, peerIdentities, auto.shareCategories ?? DEFAULT_SHARE_CATEGORIES, Date.now());
581
- }
582
- catch {
583
- return 0;
584
- }
585
- },
586
- maxMemoryMutationId: (project) => {
587
- try {
588
- const identities = identityCandidates(project);
589
- const peers = peersFor(identities).members.flatMap(candidate => identityCandidates(candidate));
590
- return maxMutationFor([...identities, ...peers]);
591
- }
592
- catch {
593
- return 0;
594
- }
595
- },
596
- projectMemoryEpochFingerprint: (project) => {
597
- try {
598
- const identities = identityCandidates(project);
599
- const peers = peersFor(identities).members.flatMap(candidate => identityCandidates(candidate));
600
- return epochFingerprint([...identities, ...peers]);
601
- }
602
- catch {
603
- return '';
604
- }
605
- },
606
- mutations: readMutations,
607
- profileRecall: (request) => {
608
- try {
609
- if (profileStatement === undefined)
610
- return undefined;
611
- const rows = profileStatement.all().map(row => ({
612
- id: row.id,
613
- content: row.content,
614
- promotedAt: row.promoted_at ?? row.id,
615
- updatedAt: row.updated_at ?? row.promoted_at ?? row.id,
616
- }));
617
- if (rows.length === 0)
618
- return undefined;
619
- const budget = request.budgetTokens ?? PROFILE_M0_BUDGET_TOKENS;
620
- const watermark = request.maxId;
621
- const pool = watermark === undefined ? rows : rows.filter(row => row.id <= watermark);
622
- const additions = watermark === undefined ? [] : rows.filter(row => row.id > watermark);
623
- const pick = (candidates, tokens) => {
624
- const selected = [];
625
- let used = 0;
626
- for (const row of candidates) {
627
- const cost = estimateTokens(renderProfileLine(row));
628
- // `continue`, not `break`: one expensive entry must not hide the
629
- // cheaper ones behind it (the same rule the memory picker keeps).
630
- if (used + cost > tokens)
631
- continue;
632
- used += cost;
633
- selected.push(row);
634
- }
635
- return selected;
636
- };
637
- const selectedPool = pick(pool, budget);
638
- const selectedDelta = pick(additions, Math.max(1, Math.floor(budget * 0.25)));
639
- if (selectedPool.length === 0 && selectedDelta.length === 0 && rows.length === 0)
640
- return undefined;
641
- const digestOf = (list) => createHash('sha1').update(list.map(row => `${String(row.id)}:${String(row.updatedAt)}`).join('|')).digest('hex');
642
- return {
643
- rows: selectedPool,
644
- lines: selectedPool.map(renderProfileLine),
645
- digest: digestOf(pool),
646
- foldDigest: digestOf(rows),
647
- newRows: selectedDelta,
648
- newLines: selectedDelta.map(renderProfileLine),
649
- currentMaxId: rows.reduce((max, row) => Math.max(max, row.id), 0),
650
- };
651
- }
652
- catch (error) {
653
- options.logger.warn(`magic-context profile read failed (continuing without it): ${errText(error)}`);
654
- return undefined;
655
- }
656
- },
657
- recall: (request) => {
658
- try {
659
- // B3 (MC single_store_reads): one deferred read transaction so the
660
- // pool query, the max and the watermark observe ONE snapshot — a
661
- // concurrent writer between statements must not tear the digest.
662
- db.exec('BEGIN');
663
- try {
664
- const identities = identityCandidates(request.project);
665
- const auto = peersFor(identities);
666
- const rows = [];
667
- const seen = new Set();
668
- for (const identity of identities) {
669
- for (const raw of selfStatement.all(identity, request.now)) {
670
- const row = toRow(raw);
671
- if (!seen.has(row.id)) {
672
- seen.add(row.id);
673
- rows.push(row);
674
- }
675
- }
676
- }
677
- const shared = new Set(request.shareCategories ?? auto.shareCategories ?? DEFAULT_SHARE_CATEGORIES);
678
- const peers = request.domainProjects ?? auto.members;
679
- const peerIdentities = peers.flatMap(candidate => identityCandidates(candidate));
680
- for (const identity of peerIdentities) {
681
- for (const raw of foreignStatement.all(identity, request.now)) {
682
- const row = toRow(raw);
683
- if (seen.has(row.id))
684
- continue;
685
- if (identities.includes(row.projectPath))
686
- continue;
687
- if (!shared.has(row.category))
688
- continue;
689
- seen.add(row.id);
690
- rows.push(row);
691
- }
692
- }
693
- const context = {
694
- ...identities[0] === undefined ? {} : { identity: identities[0] },
695
- ...auto.domains === undefined ? {} : { domains: auto.domains },
696
- members: peers,
697
- };
698
- if (rows.length === 0)
699
- return undefined;
700
- const needle = request.query?.trim().toLowerCase();
701
- const filtered = needle === undefined || needle.length === 0
702
- ? rows
703
- : rows.filter(row => row.content.toLowerCase().includes(needle) || row.category.toLowerCase().includes(needle));
704
- if (filtered.length === 0)
705
- return undefined;
706
- // S(t) decay scoring (schema v7): retired rows drop out of the
707
- // injection pool but stay in eligibleIds — searchable, and
708
- // `<memory-updates>` pointers for them stay resolvable. The picker
709
- // prefers hotter rows (score desc, MC priority as the tiebreak);
710
- // the RENDER order below stays MC's category order, so the injected
711
- // block still reads like the OpenCode/Pi block.
712
- const scoring = request.scoring;
713
- const scoringActive = scoring !== undefined && scoring.config.enabled;
714
- const retiredIds = new Set();
715
- const scoreOf = (row) => {
716
- if (scoring === undefined)
717
- return 0;
718
- const stat = scoring.statsById.get(row.id);
719
- return scoreMemory({
720
- importance: row.importance,
721
- category: row.category,
722
- hits: stat?.hits ?? 0,
723
- ...stat?.lastHitAt === undefined ? {} : { lastHitAt: stat.lastHitAt },
724
- }, scoring.config, request.now);
725
- };
726
- if (scoringActive && scoring !== undefined) {
727
- for (const row of filtered) {
728
- const stat = scoring.statsById.get(row.id);
729
- if (stat?.localArchived === true || scoreOf(row) < scoring.config.archiveThreshold) {
730
- retiredIds.add(row.id);
731
- }
732
- }
733
- }
734
- // The additive lane: rows above the stored watermark are in-session
735
- // additions and render in m[1], so they never disturb the m[0] pool
736
- // digest below (Magic Context's maxMemoryId contract).
737
- const watermark = request.maxMemoryId;
738
- const pool = watermark === undefined ? filtered : filtered.filter(row => row.id <= watermark);
739
- const additions = watermark === undefined ? [] : filtered.filter(row => row.id > watermark);
740
- const pick = (candidates, budgetTokens) => {
741
- // Priority order picks the budget (S(t) score desc first when the
742
- // decay scoring is on, MC priority as the tiebreak); the render
743
- // order is MC's category order, so the injected block reads like
744
- // the OpenCode/Pi block.
745
- const usable = scoringActive
746
- ? candidates.filter(row => !retiredIds.has(row.id))
747
- : candidates;
748
- const selected = [];
749
- let used = 0;
750
- const ranked = scoringActive
751
- ? [...usable].sort((left, right) => scoreOf(right) - scoreOf(left) || comparePriority(left, right))
752
- : [...usable].sort(comparePriority);
753
- for (const row of ranked) {
754
- if (selected.length >= request.limit)
755
- break;
756
- const cost = estimateTokens(renderMemoryLine(row));
757
- if (used + cost > budgetTokens)
758
- continue;
759
- used += cost;
760
- selected.push(row);
761
- }
762
- return selected;
763
- };
764
- const poolSelected = pick(pool, request.budgetTokens);
765
- // MC budgets the additions at 25% of the memory injection budget.
766
- const deltaSelected = pick(additions, Math.max(1, Math.floor(request.budgetTokens * 0.25)));
767
- if (poolSelected.length === 0 && deltaSelected.length === 0)
768
- return undefined;
769
- const order = (selected) => [...selected].sort((left, right) => categoryRank(left.category) - categoryRank(right.category) || right.updatedAt - left.updatedAt);
770
- const orderedPool = order(poolSelected);
771
- const orderedAdditions = order(deltaSelected);
772
- // Digest covers the m[0] pool ONLY: a new memory may not bust the cache.
773
- const digest = createHash('sha1')
774
- .update(orderedPool.map(row => `${String(row.id)}:${String(row.updatedAt)}`).join('|'))
775
- .digest('hex');
776
- // B1: visibility-qualified max (own eligible + peers eligible AND shared).
777
- const currentMaxMemoryId = maxVisibleId(identities, peerIdentities, [...shared], request.now);
778
- return {
779
- rows: orderedPool,
780
- lines: orderedPool.map(renderMemoryLine),
781
- digest,
782
- newRows: orderedAdditions,
783
- newLines: orderedAdditions.map(renderMemoryLine),
784
- currentMaxMemoryId,
785
- eligibleIds: rows.map(row => row.id),
786
- eligibleRows: rows,
787
- ...context,
788
- };
789
- }
790
- finally {
791
- // COMMIT ends the read snapshot; a failed body leaves the tx
792
- // aborted and the END is a no-op (readonly — nothing to roll back).
793
- try {
794
- db.exec('COMMIT');
795
- }
796
- catch {
797
- /* aborted transaction */
798
- }
799
- }
800
- }
801
- catch (error) {
802
- options.logger.warn(`magic-context memory recall failed (continuing without memories): ${errText(error)}`);
803
- return undefined;
804
- }
805
- },
806
- close: () => db.close(),
807
- };
808
- }
809
- catch (error) {
810
- try {
811
- db.close();
812
- }
813
- catch {
814
- /* best effort */
815
- }
816
- return unavailable(`probe failed (${errText(error)})`);
817
- }
818
- }