@elvisw/dsh-magic-context 0.0.0-stage → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/LICENSE +23 -0
  2. package/NOTICE +93 -0
  3. package/README.md +501 -2
  4. package/README.zh-CN.md +398 -0
  5. package/cordis.patch.yml +20 -0
  6. package/lib/client.js +2753 -0
  7. package/lib/core/compartments.js +424 -0
  8. package/lib/core/corpus-store.js +176 -0
  9. package/lib/core/directive-detect.js +147 -0
  10. package/lib/core/domain-reconcile.js +394 -0
  11. package/lib/core/domains.js +127 -0
  12. package/lib/core/embedding-local.js +247 -0
  13. package/lib/core/global-autostage.js +223 -0
  14. package/lib/core/global-domains.js +302 -0
  15. package/lib/core/journal.js +248 -0
  16. package/lib/core/llm-deadline.js +53 -0
  17. package/lib/core/machine-domain.js +106 -0
  18. package/lib/core/maintenance.js +380 -0
  19. package/lib/core/memory-bridge.js +818 -0
  20. package/lib/core/memory-categories.js +84 -0
  21. package/lib/core/memory-scoring.js +65 -0
  22. package/lib/core/memory-search.js +414 -0
  23. package/lib/core/memory-write.js +1176 -0
  24. package/lib/core/policy.js +111 -0
  25. package/lib/core/profile-confirmation.js +55 -0
  26. package/lib/core/profile-review.js +375 -0
  27. package/lib/core/profile-write.js +283 -0
  28. package/lib/core/reference-seeds.js +63 -0
  29. package/lib/core/route-health.js +106 -0
  30. package/lib/core/stores.js +1021 -0
  31. package/lib/core/tagger.js +7 -0
  32. package/lib/core/types.js +15 -0
  33. package/lib/core/vector-backfill.js +140 -0
  34. package/lib/core/vector-lane.js +184 -0
  35. package/lib/dsh/command-surface.js +376 -0
  36. package/lib/dsh/compaction-engine.js +231 -0
  37. package/lib/dsh/config-switches.js +56 -0
  38. package/lib/dsh/config.js +13 -0
  39. package/lib/dsh/context-pressure.js +36 -0
  40. package/lib/dsh/drop-landing.js +42 -0
  41. package/lib/dsh/emergency.js +89 -0
  42. package/lib/dsh/flush-command.js +148 -0
  43. package/lib/dsh/global-review.js +412 -0
  44. package/lib/dsh/historian.js +907 -0
  45. package/lib/dsh/live-journal.js +81 -0
  46. package/lib/dsh/maintenance-wiring.js +229 -0
  47. package/lib/dsh/management-pass.js +564 -0
  48. package/lib/dsh/memory-lane.js +535 -0
  49. package/lib/dsh/pre-step.js +675 -0
  50. package/lib/dsh/project-drift.js +116 -0
  51. package/lib/dsh/recomp.js +192 -0
  52. package/lib/dsh/reduce-guidance.js +188 -0
  53. package/lib/dsh/seams.js +588 -0
  54. package/lib/dsh/search-hint.js +186 -0
  55. package/lib/dsh/session-notice.js +100 -0
  56. package/lib/dsh/status-command.js +75 -0
  57. package/lib/dsh/telemetry.js +349 -0
  58. package/lib/dsh/tool-bridge.js +1009 -0
  59. package/lib/dsh/volatile.js +89 -0
  60. package/lib/index.js +931 -0
  61. package/package.json +108 -4
  62. package/tools/migrate-profile-package-name.mjs +317 -0
  63. package/vendor/onnx/LICENSE +27 -0
  64. package/vendor/onnx/ort-wasm-simd-threaded.mjs +59 -0
  65. package/vendor/onnx/ort-wasm-simd-threaded.wasm +0 -0
  66. package/vendor/onnx/ort.node.min.mjs +8 -0
@@ -0,0 +1,535 @@
1
+ import { dirname, join } from 'node:path';
2
+ import { errText } from "../core/types.js";
3
+ import { openMemoryBridge, DEFAULT_SHARE_CATEGORIES, resolveIdentitySpec } from "../core/memory-bridge.js";
4
+ import { openMemoryWriter } from "../core/memory-write.js";
5
+ import { ensureOwnCorpusStore } from "../core/corpus-store.js";
6
+ import { syncMachineDomain } from "../core/machine-domain.js";
7
+ import { openProfileWriter } from "../core/profile-write.js";
8
+ import { planBackfill } from "../core/vector-lane.js";
9
+ import { runVectorBackfill } from "../core/vector-backfill.js";
10
+ import { openMemorySearch } from "../core/memory-search.js";
11
+ import { cosineSimilarity, defaultEmbeddingCacheDir, openLocalEmbedder } from "../core/embedding-local.js";
12
+ import { defineCtxMemoryTool } from "./tool-bridge.js";
13
+ import { liveBoolean, liveValue } from "./volatile.js";
14
+ import { DEFAULT_MEMORY_SCORING } from "../core/memory-scoring.js";
15
+ /**
16
+ * WP3 — THE activation function, exported so the lane and its tests cannot drift
17
+ * apart. Answers which domains a reader is inside, reading the adapter's own
18
+ * tables; `undefined` means "nothing is configured yet" (no enabled domain at
19
+ * all). Since the 2026-10-13 corpus cutover that answer simply resolves no
20
+ * peers — cross-project sharing is exclusively domain-driven.
21
+ */
22
+ export function makeDomainResolver(stores) {
23
+ return (identity) => {
24
+ const domains = stores.activeDomainIdentities(identity);
25
+ // Nothing configured yet: answer `undefined` (distinct from `[]`).
26
+ if (stores.listDomains().every(domain => !domain.enabled))
27
+ return undefined;
28
+ return domains.map(domain => ({
29
+ identity: domain.identity,
30
+ shareCategories: stores.getDomain(domain.domainId)?.shareCategories ?? [],
31
+ kind: domain.kind,
32
+ }));
33
+ };
34
+ }
35
+ export function openMemoryLane(ctx, config, stores) {
36
+ const memorySettings = config.memory;
37
+ // 0.9.0: the gate defaults ON — the old `memorySettings !== undefined &&`
38
+ // conservative presence gate predates the distribution defaults and made a
39
+ // fresh `dsh plugin add` install boot with ctx_memory silently missing.
40
+ // The explicit opt-out is `memory.enabled: false`.
41
+ const memoryEnabled = liveBoolean(memorySettings?.enabled, true);
42
+ // The corpus defaults to BESIDE the adapter store (the profile's dataDir),
43
+ // not a fixed homedir path: per-profile isolation for distribution installs,
44
+ // and the same directory as before wherever a profile already points dataDir
45
+ // at ~/.local/share/cortexkit/magic-context-dsh-poc. The bridge's own
46
+ // `defaultMemoryDbPath()` (fixed homedir path) remains only as the
47
+ // direct-caller fallback; the lane always resolves explicitly so the bridge
48
+ // never guesses.
49
+ const fallbackCorpusPath = () => join(dirname(stores.dbPath), 'memories.db');
50
+ // WP3: ONE activation source for every consumer inside the bridge (recall, the
51
+ // visibility watermark, the search space, the mutation lane, the epoch
52
+ // fingerprint). The domain resolver reads the adapter's own tables
53
+ // (design v2: no workspace takeover; the `contract` source lever was retired
54
+ // with the 2026-10-13 corpus cutover).
55
+ // D1, post-cutover: without this bootstrap a fresh machine has NO corpus
56
+ // file, and every opener below would silently degrade (0-byte file, no
57
+ // schema_migrations, dead lane). Lay the OWN corpus schema down FIRST; an
58
+ // existing store — valid, foreign-shaped, or fenced — is never touched
59
+ // ('exists'), and the openers' refusals keep guarding whatever it is.
60
+ let corpusBootstrap;
61
+ if (memoryEnabled) {
62
+ const target = memorySettings?.dbPath ?? fallbackCorpusPath();
63
+ const boot = ensureOwnCorpusStore(target);
64
+ if (boot.outcome !== 'exists') {
65
+ corpusBootstrap = boot.detail === undefined ? boot.outcome : `${boot.outcome} (${boot.detail})`;
66
+ ctx.logger.info(`magic-context dsh adapter: own corpus ${corpusBootstrap} at ${target}`);
67
+ try {
68
+ stores.setMeta('corpus_bootstrap', JSON.stringify({ outcome: boot.outcome, detail: boot.detail, at: Date.now() }));
69
+ }
70
+ catch {
71
+ // best effort only — the meta row is observability, not a gate
72
+ }
73
+ }
74
+ }
75
+ // WP10 projection at MOUNT: the machine domain row's existence and enabled
76
+ // flag are a CONFIG projection, not a pass artifact — the pre-step sync sits
77
+ // inside the execute band, so a freshly flipped switch stayed invisible until
78
+ // pressure crossed the threshold (measured 2026-10-12: enabled at 12:31, row
79
+ // still absent at 40% pressure 12:32). Membership still syncs per execute
80
+ // pass, where the identities the process has actually served are known.
81
+ if (memoryEnabled) {
82
+ try {
83
+ syncMachineDomain(stores, {
84
+ identities: [],
85
+ config: {
86
+ enabled: liveBoolean(config.memory?.global?.machine?.enabled, true),
87
+ ...config.memory?.global?.machine?.excludeProjects === undefined
88
+ ? {}
89
+ : { excludeProjects: config.memory.global.machine.excludeProjects },
90
+ },
91
+ now: Date.now(),
92
+ });
93
+ }
94
+ catch {
95
+ // projection only — the pre-step sync self-heals on the next execute pass
96
+ }
97
+ }
98
+ const memoryRecall = !memoryEnabled
99
+ ? undefined
100
+ : openMemoryBridge({
101
+ dbPath: memorySettings?.dbPath ?? fallbackCorpusPath(),
102
+ logger: ctx.logger,
103
+ domainResolver: makeDomainResolver(stores),
104
+ });
105
+ if (memoryRecall !== undefined) {
106
+ ctx.logger.info(`magic-context dsh adapter: memory bridge at ${memoryRecall.dbPath} `
107
+ + `(${memoryRecall.status.ok ? `schema v${String(memoryRecall.status.schemaVersion)}` : `unavailable: ${String(memoryRecall.status.detail)}`})`);
108
+ }
109
+ // The writer opens alongside the reader so facts can become durable
110
+ // memories. Magic Context's default is auto-promote ON; ours follows it,
111
+ // and every write still passes the authority guard (module-managed projects
112
+ // are never written directly).
113
+ const memoryWriter = memoryRecall === undefined || (memorySettings?.autoPromote === false && memorySettings?.write === false)
114
+ ? undefined
115
+ : openMemoryWriter({
116
+ // The writer has no default of its own (it must never guess a database
117
+ // to write into), so the caller resolves the shared path here.
118
+ dbPath: memoryRecall.dbPath,
119
+ logger: ctx.logger,
120
+ });
121
+ if (memoryWriter !== undefined) {
122
+ const status = memoryWriter.status;
123
+ ctx.logger.info(`magic-context dsh adapter: memory writer at ${memoryWriter.dbPath} `
124
+ + `(${status.ok ? (status.readOnly === true ? 'read-only' : 'read-write') : `unavailable: ${String(status.detail)}`})`);
125
+ }
126
+ // WP5: the `user` domain's candidate sink. Opened whenever the memory lane is
127
+ // usable at all — whether candidates are actually RECORDED is a live config
128
+ // decision (`memory.global.user.collect`, default off) read per pass, so the
129
+ // switch can be flipped without reopening the store.
130
+ const profileWriter = memoryRecall === undefined
131
+ ? undefined
132
+ : openProfileWriter({
133
+ dbPath: memoryRecall.dbPath,
134
+ logger: ctx.logger,
135
+ });
136
+ if (profileWriter !== undefined) {
137
+ ctx.logger.info(`magic-context dsh adapter: profile candidate writer at ${profileWriter.dbPath} `
138
+ + `(${profileWriter.status.ok ? 'read-write' : `unavailable: ${String(profileWriter.status.detail)}`})`);
139
+ }
140
+ // ── The memory search lane (ctx_memory action `search`) ───────────────────
141
+ // A read-only twin of MC ctx_search's memory half: FTS5 + stored-vector
142
+ // cosine with MC's 0.7/0.3 fusion (memory-search.ts). The embedder loads
143
+ // lazily on first use; any failure degrades the lane to FTS-only instead of
144
+ // failing the tool call.
145
+ const memorySearch = memoryEnabled && memoryRecall?.status.ok === true
146
+ ? openMemorySearch({ dbPath: memorySettings?.dbPath ?? memoryRecall.dbPath, logger: ctx.logger })
147
+ : undefined;
148
+ if (memorySearch !== undefined) {
149
+ ctx.logger.info('magic-context dsh adapter: memory search lane open (FTS + local MiniLM)');
150
+ }
151
+ // B4 gates only the BACKFILL lane below; the embedder itself stays available
152
+ // for the query-side embedding that scores the STORED vectors.
153
+ // The fill lane is on by default since the WASM embedding stack ships with
154
+ // the package (0.3.0): the runtime cost is bounded by memory-search's
155
+ // MAX_IN_MEMORY_EMBEDDINGS cap, and persistence is untouched (the lane fills
156
+ // in-memory scores only — the store's rows are never written).
157
+ const embedBackfill = liveBoolean(config.memory?.embedBackfill, true);
158
+ const embedder = memorySearch === undefined
159
+ ? undefined
160
+ : openLocalEmbedder({
161
+ cacheDir: defaultEmbeddingCacheDir(stores.dbPath),
162
+ logger: { info: (message) => ctx.logger.info(message), warn: (message) => ctx.logger.warn(message) },
163
+ });
164
+ let sharedSpacePromise;
165
+ // Captured when the tool registration hands over the journal (this closure is
166
+ // only ever reached through the tool, so that always happens first). Keeping
167
+ // it here avoids widening openMemoryLane's signature for one log line.
168
+ let laneJournal;
169
+ const resolveSharedSpace = () => {
170
+ if (memorySearch === undefined || embedder === undefined)
171
+ return Promise.resolve({ modelId: null });
172
+ if (sharedSpacePromise === undefined) {
173
+ sharedSpacePromise = (async () => {
174
+ // The gate's outcome used to be observable ONLY through ctx.logger,
175
+ // which does not reach the journal — so whether the semantic lane is
176
+ // live, degraded or dead was unknowable from the store. Every exit is
177
+ // journaled once per process now: `vector-lane` answers the question
178
+ // permanently, and a silent permanent degradation (the failure this
179
+ // lane is most prone to) shows up as a repeated parity-fail/embed-failed.
180
+ const record = (outcome, modelId, detail = {}, bootstrapModelId) => {
181
+ laneJournal?.info('vector-lane', `shared-space gate: ${outcome}`, {
182
+ outcome,
183
+ ...detail,
184
+ ...bootstrapModelId === undefined ? {} : { bootstrapModelId },
185
+ });
186
+ return bootstrapModelId === undefined ? { modelId } : { modelId, bootstrapModelId };
187
+ };
188
+ const dominant = memorySearch.dominantEmbeddingModel();
189
+ if (dominant === undefined) {
190
+ // No space exists yet. This is the ONLY case where the embedder may
191
+ // create one; dims mismatch and parity failure below stay closed.
192
+ return record('gate-closed', null, { reason: 'no dominant embedding space' }, embedder.modelId);
193
+ }
194
+ if (dominant.dims !== embedder.dims) {
195
+ return record('gate-closed', null, {
196
+ reason: 'dims mismatch',
197
+ dominantDims: dominant.dims,
198
+ embedderDims: embedder.dims,
199
+ });
200
+ }
201
+ const sample = memorySearch.paritySample(dominant.modelId);
202
+ if (sample === undefined) {
203
+ // Dims agree and there is nothing to compare against: the lane is used
204
+ // on the strength of the dimension match alone, and that is recorded.
205
+ return record('no-sample', dominant.modelId, { modelId: dominant.modelId });
206
+ }
207
+ try {
208
+ const [vector] = await embedder.embed([sample.content]);
209
+ if (vector === undefined)
210
+ return record('embed-empty', null, { modelId: dominant.modelId });
211
+ const parity = cosineSimilarity(vector, sample.embedding);
212
+ ctx.logger.info(`magic-context memory search: shared-space parity ${parity.toFixed(4)} `
213
+ + `against ${dominant.modelId.slice(0, 48)}`);
214
+ return parity >= 0.99
215
+ ? record('parity-pass', dominant.modelId, { modelId: dominant.modelId, parity: Number(parity.toFixed(6)) })
216
+ : record('parity-fail', null, { modelId: dominant.modelId, parity: Number(parity.toFixed(6)) });
217
+ }
218
+ catch (error) {
219
+ return record('embed-failed', null, { modelId: dominant.modelId, error: errText(error) });
220
+ }
221
+ })();
222
+ }
223
+ return sharedSpacePromise;
224
+ };
225
+ /** A2: per-session memory ids the injected block currently renders
226
+ * (MC getVisibleMemoryIds, in-memory: absent = don't filter). */
227
+ const visibleMemoryIdsBySession = new Map();
228
+ /**
229
+ * WP11: fill the vector lane. `undefined` when the lane cannot be filled at
230
+ * all (no reader, no embedder, no writable writer) — the caller then does
231
+ * nothing rather than guessing why.
232
+ *
233
+ * The shared-space gate is consulted HERE, through the same memoized
234
+ * `resolveSharedSpace` the query side uses, so there is exactly one answer to
235
+ * "can this adapter reproduce the store's vector space" for reading and
236
+ * writing alike.
237
+ */
238
+ const vectorBackfill = async (limit, journal) => {
239
+ if (memorySearch === undefined || embedder === undefined || memoryRecall === undefined)
240
+ return undefined;
241
+ if (memoryWriter === undefined || memoryWriter.status.ok !== true)
242
+ return undefined;
243
+ laneJournal = journal;
244
+ const space = await resolveSharedSpace();
245
+ const outcome = await runVectorBackfill({
246
+ plan: () => {
247
+ const plan = planBackfill(memoryRecall.dbPath, {
248
+ limit,
249
+ ...space.bootstrapModelId === undefined ? {} : { bootstrapModelId: space.bootstrapModelId },
250
+ });
251
+ return plan === undefined
252
+ ? undefined
253
+ : { modelId: plan.modelId, candidates: plan.candidates, total: plan.total };
254
+ },
255
+ contents: (ids) => memoryWriter.readContents(ids),
256
+ embed: (texts) => embedder.embed(texts),
257
+ dims: embedder.dims,
258
+ store: (input) => memoryWriter.storeEmbedding(input),
259
+ journal,
260
+ limit,
261
+ parityModelId: space.modelId,
262
+ ...space.bootstrapModelId === undefined ? {} : { bootstrapModelId: space.bootstrapModelId },
263
+ });
264
+ // The memo was `gate-closed / no space` for this process. Once the first
265
+ // rows exist, re-resolve so the query side can measure parity and read
266
+ // them; without this the write succeeds but only a restart uses it.
267
+ if (space.bootstrapModelId !== undefined && outcome.created > 0)
268
+ sharedSpacePromise = undefined;
269
+ return outcome;
270
+ };
271
+ /** The ctx_memory `search` closure: bridge space + shared-space gate. */
272
+ const memorySearchForTool = memorySearch === undefined
273
+ ? undefined
274
+ : async (request) => {
275
+ const bridge = memoryRecall;
276
+ if (bridge === undefined)
277
+ return { ok: false, reason: 'memory search is not available' };
278
+ const space = bridge.searchSpace(request.identity);
279
+ if (space === undefined) {
280
+ return { ok: false, reason: 'no project identity could be resolved for this session' };
281
+ }
282
+ const { modelId } = await resolveSharedSpace();
283
+ let queryEmbedding = null;
284
+ if (embedder !== undefined && modelId !== null) {
285
+ try {
286
+ const [vector] = await embedder.embed([request.query]);
287
+ queryEmbedding = vector ?? null;
288
+ }
289
+ catch {
290
+ queryEmbedding = null;
291
+ }
292
+ }
293
+ const outcome = await memorySearch.search({
294
+ query: request.query,
295
+ ...request.limit === undefined ? {} : { limit: request.limit },
296
+ now: Date.now(),
297
+ space: {
298
+ ownIdentities: space.ownIdentities,
299
+ foreignIdentities: space.foreignIdentities,
300
+ shareCategories: space.shareCategories ?? [...DEFAULT_SHARE_CATEGORIES],
301
+ },
302
+ queryEmbedding,
303
+ queryModelId: modelId,
304
+ ...request.visibleMemoryIds === undefined ? {} : { visibleMemoryIds: request.visibleMemoryIds },
305
+ // The in-memory fill lane scores candidates lacking stored vectors;
306
+ // default on (the WASM stack ships in-package), bounded by the
307
+ // search lane's MAX_IN_MEMORY_EMBEDDINGS cap. `memory.embedBackfill:
308
+ // false` turns it off for latency-sensitive setups.
309
+ ...embedBackfill && embedder !== undefined ? {
310
+ embedTexts: async (texts) => {
311
+ const vectors = await embedder.embed(texts);
312
+ return vectors.length === texts.length ? vectors : null;
313
+ },
314
+ } : {},
315
+ });
316
+ if (!outcome.ok)
317
+ return { ok: false, reason: outcome.reason };
318
+ // S(t) sidecar: a search hit strengthens the row for future injection
319
+ // (mon3stera's recordMemoryHit on the ctx_search path). Fail-open —
320
+ // scoring stats are advisory and must never fail the tool call.
321
+ try {
322
+ const now = Date.now();
323
+ for (const hit of outcome.hits)
324
+ stores.recordMemoryEvent(request.identity, hit.id, 'hit', now);
325
+ }
326
+ catch {
327
+ /* scoring stats are advisory */
328
+ }
329
+ return { ok: true, text: outcome.text, hits: outcome.hits.map(hit => ({ id: hit.id })) };
330
+ };
331
+ /**
332
+ * One wording for the writer's state, used by both the boot log and the
333
+ * `memory_write` meta the GUI (plugin page / telemetry badge) reads.
334
+ * A contract-version fence keeps `status.ok === true` because reads still
335
+ * work, so `readOnly` must be checked explicitly — otherwise a fully
336
+ * read-only writer would advertise itself as `read-write` (measured on a
337
+ * real-store copy whose schema_migrations was raised to v95).
338
+ */
339
+ const describeMemoryWrite = () => {
340
+ const status = memoryWriter?.status;
341
+ if (status === undefined) {
342
+ return memoryEnabled ? 'off' : 'disabled-by-config';
343
+ }
344
+ if (!status.ok)
345
+ return `unavailable: ${String(status.detail)}`;
346
+ if (status.readOnly === true) {
347
+ return `read-only: ${String(status.detail ?? 'corpus version is newer than the adapter understands')}`;
348
+ }
349
+ return `read-write${memorySettings?.autoPromote === false ? ' (promotion off)' : ' auto-promote'}`;
350
+ };
351
+ /**
352
+ * The auto-search hint lane's search (upstream `auto-search-runner`). Same space
353
+ * and shared-space gate as the tool's `search`, but the identity comes from the
354
+ * session — preStep has no tool exec — and the query runs FTS-only: the hint is
355
+ * a nudge, and waiting for the local embedding model to load would delay the
356
+ * turn it is meant to inform.
357
+ * @param session - the session whose cwd/config resolves the identity.
358
+ * @param query - the user's own words.
359
+ * @param limit - how many hits to consider.
360
+ * @returns ranked hits, or an empty list when the lane is unavailable.
361
+ */
362
+ const searchForSession = memorySearch === undefined || memoryRecall === undefined
363
+ ? undefined
364
+ : async (session, query, limit) => {
365
+ const spec = config.memory?.project ?? session.header?.cwd;
366
+ if (spec === undefined)
367
+ return [];
368
+ const space = memoryRecall.searchSpace(resolveIdentitySpec(spec));
369
+ if (space === undefined)
370
+ return [];
371
+ // FTS-only: no embedding request, so no model load on the turn path. The
372
+ // stored-vector lane stays available to ctx_search, where the agent asked.
373
+ const outcome = await memorySearch.search({
374
+ query,
375
+ limit,
376
+ now: Date.now(),
377
+ space: {
378
+ ownIdentities: space.ownIdentities,
379
+ foreignIdentities: space.foreignIdentities,
380
+ shareCategories: space.shareCategories ?? [...DEFAULT_SHARE_CATEGORIES],
381
+ },
382
+ queryEmbedding: null,
383
+ queryModelId: null,
384
+ });
385
+ return outcome.ok ? outcome.hits : [];
386
+ };
387
+ /**
388
+ * The volatile `memory.scoring` overrides, unwrapped at every read (nested
389
+ * schemastery references wrap each level — see volatile.ts's measured
390
+ * double-unwrap note). Unknown keys fall back to the mon3stera defaults.
391
+ */
392
+ const resolveScoringConfig = () => {
393
+ const raw = memorySettings?.scoring;
394
+ const numberOf = (value, fallback) => {
395
+ const resolved = liveValue(value);
396
+ return typeof resolved === 'number' && Number.isFinite(resolved) ? resolved : fallback;
397
+ };
398
+ const halfLives = { ...DEFAULT_MEMORY_SCORING.halfLives };
399
+ const rawHalfLives = liveValue(raw?.halfLives);
400
+ if (rawHalfLives !== null && typeof rawHalfLives === 'object') {
401
+ for (const [category, value] of Object.entries(rawHalfLives)) {
402
+ const resolved = liveValue(value);
403
+ if (resolved === null)
404
+ halfLives[category] = null;
405
+ else if (typeof resolved === 'number' && Number.isFinite(resolved) && resolved > 0) {
406
+ halfLives[category] = resolved;
407
+ }
408
+ }
409
+ }
410
+ const enabled = liveValue(raw?.enabled);
411
+ return {
412
+ enabled: typeof enabled === 'boolean' ? enabled : DEFAULT_MEMORY_SCORING.enabled,
413
+ alpha: numberOf(raw?.alpha, DEFAULT_MEMORY_SCORING.alpha),
414
+ beta: numberOf(raw?.beta, DEFAULT_MEMORY_SCORING.beta),
415
+ archiveThreshold: numberOf(raw?.archiveThreshold, DEFAULT_MEMORY_SCORING.archiveThreshold),
416
+ halfLives,
417
+ };
418
+ };
419
+ const memoryScoring = (identity) => {
420
+ let stats = [];
421
+ try {
422
+ stats = stores.listMemoryStats(identity);
423
+ }
424
+ catch {
425
+ // A closed store mid-remount degrades to no stats (cold start favored).
426
+ }
427
+ return {
428
+ config: resolveScoringConfig(),
429
+ statsById: new Map(stats.map(row => [row.memoryId, {
430
+ hits: row.hits,
431
+ ...row.lastHitAt === undefined ? {} : { lastHitAt: row.lastHitAt },
432
+ localArchived: row.localArchived,
433
+ }])),
434
+ };
435
+ };
436
+ return {
437
+ memoryEnabled,
438
+ memorySettings,
439
+ memoryRecall,
440
+ memoryWriter,
441
+ profileWriter,
442
+ vectorBackfill,
443
+ memorySearch,
444
+ visibleMemoryIdsBySession,
445
+ corpusBootstrap,
446
+ memorySearchForTool,
447
+ searchForSession,
448
+ memoryScoring,
449
+ describeMemoryWrite,
450
+ ctxMemoryRegistration: ({ activePolicy, defineTool, journal, reviewGlobal, promoteGlobal }) => {
451
+ // The shared-space gate runs inside the search closure below, so it needs
452
+ // the journal this registration carries.
453
+ laneJournal = journal;
454
+ // The registration is only reached when the bridge opened (index.ts wires
455
+ // it behind the same condition); narrowing here keeps the closures below
456
+ // reading a defined bridge instead of re-testing it three times.
457
+ if (memoryRecall === undefined) {
458
+ throw new Error('ctx_memory registration without a memory bridge');
459
+ }
460
+ const bridge = memoryRecall;
461
+ return {
462
+ defineTool: defineTool,
463
+ ...journal === undefined ? {} : { journal },
464
+ ...memoryWriter === undefined || config.memory?.write === false ? {} : { writer: memoryWriter },
465
+ ...memorySearchForTool === undefined ? {} : { search: memorySearchForTool },
466
+ ...reviewGlobal === undefined ? {} : { reviewGlobal },
467
+ ...promoteGlobal === undefined ? {} : { promoteGlobal },
468
+ // A2: ctx_memory search suppresses rows this session's injected
469
+ // block already renders (MC getVisibleMemoryIds).
470
+ visibleMemoryIdsOf: exec => {
471
+ const sessionId = exec?.agent?.session?.id;
472
+ return sessionId === undefined ? undefined : visibleMemoryIdsBySession.get(sessionId);
473
+ },
474
+ resolveIdentity: exec => {
475
+ const spec = config.memory?.project ?? exec?.agent?.session.header?.cwd;
476
+ return spec === undefined ? undefined : resolveIdentitySpec(spec);
477
+ },
478
+ // The `project` argument's resolver: an explicit absolute path (another
479
+ // project the model is working on) through the same normalizer. DSH has
480
+ // no per-turn working directory, so the model naming the project is the
481
+ // only cross-project scoping signal we have.
482
+ resolveIdentityOf: spec => {
483
+ try {
484
+ return resolveIdentitySpec(spec);
485
+ }
486
+ catch {
487
+ return undefined;
488
+ }
489
+ },
490
+ recall: (request, exec) => {
491
+ // The read path must resolve the SAME identity the writes use. The
492
+ // tool hands us the identity it already resolved (`request.identity`,
493
+ // set by tool-bridge); otherwise fall back to the configured project
494
+ // or this run's cwd, resolved through the same normalizer.
495
+ //
496
+ // This closure used to reference a free `exec` — and because `&&`
497
+ // binds tighter than `?:`, `config.memory?.project === undefined &&
498
+ // exec.agent...` evaluated the right-hand side in the DEFAULT
499
+ // configuration, throwing `ReferenceError: exec is not defined` on
500
+ // every ctx_memory call (measured live). Configuring memory.project
501
+ // short-circuited it, which is why the tests never saw it.
502
+ const spec = request.identity
503
+ ?? config.memory?.project
504
+ ?? (exec === undefined ? undefined : exec.agent?.session.header?.cwd);
505
+ const project = spec === undefined ? undefined : resolveIdentitySpec(spec);
506
+ // A fenced/unreadable store must say so: `{ empty: true }` would read
507
+ // as "this project has no memories", which is a different fact.
508
+ if (!bridge.status.ok) {
509
+ return { lines: [], digest: 'unavailable', reason: String(bridge.status.detail) };
510
+ }
511
+ const recalled = bridge.recall({
512
+ ...project === undefined ? {} : { project },
513
+ domainProjects: config.memory?.domainProjects,
514
+ shareCategories: config.memory?.shareCategories,
515
+ budgetTokens: activePolicy().memoryBudgetTokens,
516
+ limit: request.limit ?? activePolicy().memoryLimit,
517
+ now: Date.now(),
518
+ ...request.query === undefined ? {} : { query: request.query },
519
+ });
520
+ if (recalled === undefined)
521
+ return undefined;
522
+ // Explicit-retrieval telemetry (contract §9.1): hand back the ids the
523
+ // agent actually received so the tool can bump `retrieval_count`.
524
+ // The automatic m[0]/m[1] injection does NOT pass through here, so it
525
+ // can never inflate the counter.
526
+ return {
527
+ lines: recalled.lines,
528
+ digest: recalled.digest,
529
+ ids: [...recalled.rows, ...recalled.newRows].map(row => row.id),
530
+ };
531
+ },
532
+ };
533
+ },
534
+ };
535
+ }