@elvisw/dsh-magic-context 0.0.0-bootstrap.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -0
- package/NOTICE +93 -0
- package/README.md +453 -0
- package/README.zh-CN.md +368 -0
- package/cordis.patch.yml +20 -0
- package/lib/client.js +2270 -0
- package/lib/core/compartments.js +424 -0
- package/lib/core/corpus-store.js +176 -0
- package/lib/core/directive-detect.js +147 -0
- package/lib/core/domain-reconcile.js +394 -0
- package/lib/core/domains.js +127 -0
- package/lib/core/embedding-local.js +247 -0
- package/lib/core/global-autostage.js +223 -0
- package/lib/core/global-domains.js +302 -0
- package/lib/core/journal.js +248 -0
- package/lib/core/llm-deadline.js +53 -0
- package/lib/core/machine-domain.js +106 -0
- package/lib/core/maintenance.js +380 -0
- package/lib/core/memory-bridge.js +818 -0
- package/lib/core/memory-categories.js +84 -0
- package/lib/core/memory-scoring.js +65 -0
- package/lib/core/memory-search.js +414 -0
- package/lib/core/memory-write.js +1176 -0
- package/lib/core/policy.js +111 -0
- package/lib/core/profile-confirmation.js +55 -0
- package/lib/core/profile-review.js +375 -0
- package/lib/core/profile-write.js +283 -0
- package/lib/core/reference-seeds.js +63 -0
- package/lib/core/route-health.js +106 -0
- package/lib/core/stores.js +1021 -0
- package/lib/core/tagger.js +7 -0
- package/lib/core/types.js +15 -0
- package/lib/core/vector-backfill.js +140 -0
- package/lib/core/vector-lane.js +184 -0
- package/lib/dsh/command-surface.js +376 -0
- package/lib/dsh/compaction-engine.js +231 -0
- package/lib/dsh/config-switches.js +56 -0
- package/lib/dsh/config.js +13 -0
- package/lib/dsh/context-pressure.js +36 -0
- package/lib/dsh/drop-landing.js +42 -0
- package/lib/dsh/emergency.js +89 -0
- package/lib/dsh/flush-command.js +148 -0
- package/lib/dsh/global-review.js +412 -0
- package/lib/dsh/historian.js +907 -0
- package/lib/dsh/live-journal.js +81 -0
- package/lib/dsh/maintenance-wiring.js +229 -0
- package/lib/dsh/management-pass.js +564 -0
- package/lib/dsh/memory-lane.js +535 -0
- package/lib/dsh/pre-step.js +675 -0
- package/lib/dsh/project-drift.js +116 -0
- package/lib/dsh/recomp.js +192 -0
- package/lib/dsh/reduce-guidance.js +188 -0
- package/lib/dsh/seams.js +588 -0
- package/lib/dsh/search-hint.js +186 -0
- package/lib/dsh/session-notice.js +100 -0
- package/lib/dsh/status-command.js +75 -0
- package/lib/dsh/telemetry.js +349 -0
- package/lib/dsh/tool-bridge.js +1009 -0
- package/lib/dsh/volatile.js +89 -0
- package/lib/index.js +931 -0
- package/package.json +110 -0
- package/tools/migrate-profile-package-name.mjs +317 -0
- package/vendor/onnx/LICENSE +27 -0
- package/vendor/onnx/ort-wasm-simd-threaded.mjs +59 -0
- package/vendor/onnx/ort-wasm-simd-threaded.wasm +0 -0
- 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
|
+
}
|