@tpsdev-ai/flair 0.27.1 → 0.29.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/README.md +1 -1
- package/dist/bridges/builtins/index.js +1 -1
- package/dist/bridges/discover.js +1 -1
- package/dist/bridges/runtime/load-plugin.js +1 -1
- package/dist/bridges/scaffold.js +3 -3
- package/dist/bridges/types.js +1 -1
- package/dist/cli.js +148 -26
- package/dist/rem/runner.js +2 -2
- package/dist/resources/MemoryReflect.js +1 -1
- package/dist/resources/RecordUsage.js +15 -1
- package/dist/resources/embeddings-provider.js +8 -38
- package/dist/resources/mcp-tools.js +9 -1
- package/dist/resources/memory-reflect-lib.js +1 -1
- package/dist/resources/migrations/embedding-stamp.js +23 -0
- package/dist/resources/migrations/graph-heal.js +250 -0
- package/dist/resources/migrations/registry.js +11 -5
- package/dist/resources/models-dir.js +55 -0
- package/dist/resources/record-types.js +2 -2
- package/dist/resources/rerank-provider.js +244 -36
- package/dist/resources/usage-recording.js +72 -6
- package/dist/resources/version.js +33 -0
- package/docs/assets/flair-cross-orchestrator.cast +1 -1
- package/docs/bridges.md +3 -3
- package/docs/mcp-clients.md +3 -1
- package/docs/n8n.md +1 -2
- package/docs/notes/rem-ux.md +1 -1
- package/docs/rem.md +2 -0
- package/docs/rerank-provisioning.md +40 -6
- package/package.json +4 -1
- package/schemas/memory.graphql +37 -12
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* graph-heal.ts — the recall graph-heal OBSERVABILITY migration.
|
|
3
|
+
*
|
|
4
|
+
* WHAT ACTUALLY HEALS: nothing in this file. The heal is entirely
|
|
5
|
+
* schema-driven — `schemas/memory.graphql`'s `embedding` field now declares
|
|
6
|
+
* `@indexed(type: "HNSW", M: 16)` (M:16 = Harper's own default, a zero
|
|
7
|
+
* behavior change). On the first boot after upgrade, Harper structurally
|
|
8
|
+
* diffs the persisted per-attribute HNSW descriptor (built with NO options on
|
|
9
|
+
* every pre-this-change install) against the schema and, because
|
|
10
|
+
* `canonicalizeIndexOptions` does NOT inject defaults (`{type:"HNSW"}` and
|
|
11
|
+
* `{type:"HNSW",M:16}` are DIFFERENT canonical keys — verified against the
|
|
12
|
+
* installed @harperfast/harper source, resources/databases.js:
|
|
13
|
+
* canonicalizeIndexOptions + the `indexOptionsChanged` reset of
|
|
14
|
+
* `lastIndexedKey` to undefined), clears the graph store and rebuilds it
|
|
15
|
+
* CLEANLY from the already-correct stored vectors. That rebuild heals the
|
|
16
|
+
* stale/asymmetric reverse-edge corruption that an OLDER (pre-fix) Harper's
|
|
17
|
+
* INCREMENTAL HNSW update left on disk during the July bulk embedding-stamp
|
|
18
|
+
* re-embed — a fresh index of the same vectors finds the true neighbors, the
|
|
19
|
+
* mangled live graph did not. The corruption is HISTORICAL: the installed
|
|
20
|
+
* Harper (5.1.22) already fixes that incremental-update path —
|
|
21
|
+
* HierarchicalNavigableSmallWorld reconstructs the prior vector from the
|
|
22
|
+
* stored node and carries its real connections into the reverse-edge cleanup
|
|
23
|
+
* the old build skipped — so current Harper no longer produces it; it just
|
|
24
|
+
* still persists on disk in stores re-embedded under the old engine. The heal
|
|
25
|
+
* is therefore version-agnostic: it repairs any such store regardless of the
|
|
26
|
+
* Harper now running. No re-embed happens — only the graph is rebuilt from the
|
|
27
|
+
* vectors already on disk.
|
|
28
|
+
*
|
|
29
|
+
* WHY THIS MIGRATION EXISTS ANYWAY: the heal is invisible — Harper triggers
|
|
30
|
+
* it, this code cannot force it, and there is no Harper API to observe it
|
|
31
|
+
* directly. So this migration's ONLY job is to VERIFY + LEDGER that recall is
|
|
32
|
+
* healthy after the boot, so the heal is auditable and version-gated (runs
|
|
33
|
+
* once per upgrade, then the runner's state-file short-circuit — state.ts —
|
|
34
|
+
* skips it). It touches ZERO rows: riskClass 'derived-only', countPending()
|
|
35
|
+
* is always 0, run() writes one structural-only OrgEvent and returns
|
|
36
|
+
* `processed: 0`. The runner (resources/migrations/runner.ts) then records
|
|
37
|
+
* success in the state file, short-circuiting every subsequent boot at this
|
|
38
|
+
* version.
|
|
39
|
+
*
|
|
40
|
+
* NON-THROWING throughout (invariant II — the runner halts-not-bricks): a
|
|
41
|
+
* canary that can't run, an index still mid-rebuild, or a ledger-write
|
|
42
|
+
* failure all degrade to "not verified this boot, retry next boot" or a
|
|
43
|
+
* best-effort skip — never an exception into the boot path.
|
|
44
|
+
*
|
|
45
|
+
* DEFENSE-IN-DEPTH RULE (a prudent, version-independent default — NOT a
|
|
46
|
+
* workaround for a live bug): any BULK re-embed of the corpus should be paired
|
|
47
|
+
* with a structural graph REBUILD (a full runIndexing pass) rather than
|
|
48
|
+
* relying on the engine's incremental `index()` updates to converge. The
|
|
49
|
+
* installed Harper (5.1.22) DOES converge — its update path is fixed — but the
|
|
50
|
+
* rebuild keeps recall correct regardless of engine version, closes the rare
|
|
51
|
+
* decode-failure edge case, and is what heals a store already corrupted on
|
|
52
|
+
* disk by an older engine. The lever flair uses for that rebuild is a
|
|
53
|
+
* structural-schema change (the M:16 descriptor bump that ships with this
|
|
54
|
+
* migration) — see the guard comments in
|
|
55
|
+
* resources/migrations/embedding-stamp.ts and the `flair reembed` path in
|
|
56
|
+
* src/cli.ts. Do NOT use resources/MemoryReindex.ts's `_reindex` for graph
|
|
57
|
+
* correctness — it re-PUTs the same vector through the incremental path and
|
|
58
|
+
* does not rebuild the graph.
|
|
59
|
+
*/
|
|
60
|
+
import { databases } from "@harperfast/harper";
|
|
61
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
62
|
+
import { dirname, join } from "node:path";
|
|
63
|
+
import { fileURLToPath } from "node:url";
|
|
64
|
+
export const GRAPH_HEAL_ID = "graph-heal";
|
|
65
|
+
/** How many rows to scan for a canary that carries a real embedding. Bounded — detect() must stay cheap. */
|
|
66
|
+
const CANARY_SCAN_LIMIT = 8;
|
|
67
|
+
function defaultMemoryTable() {
|
|
68
|
+
return databases.flair.Memory;
|
|
69
|
+
}
|
|
70
|
+
function defaultOrgEventTable() {
|
|
71
|
+
return databases.flair.OrgEvent;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Same "resolve the running package's own version" idiom as
|
|
75
|
+
* resources/health.ts / resources/migration-boot.ts. Inlined (not imported
|
|
76
|
+
* from migration-boot) to avoid an import cycle: migration-boot → registry →
|
|
77
|
+
* graph-heal. Purely informational — it only stamps the ledger detail blob;
|
|
78
|
+
* a "dev" fallback is harmless.
|
|
79
|
+
*/
|
|
80
|
+
function resolveRunningVersionLocal() {
|
|
81
|
+
try {
|
|
82
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
83
|
+
const candidates = [
|
|
84
|
+
join(here, "..", "..", "..", "package.json"), // dist/resources/migrations → root
|
|
85
|
+
join(here, "..", "..", "package.json"),
|
|
86
|
+
];
|
|
87
|
+
for (const p of candidates) {
|
|
88
|
+
if (existsSync(p)) {
|
|
89
|
+
const pkg = JSON.parse(readFileSync(p, "utf-8"));
|
|
90
|
+
if (pkg.version)
|
|
91
|
+
return String(pkg.version);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
/* fall through */
|
|
97
|
+
}
|
|
98
|
+
return process.env.npm_package_version ?? "dev";
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Pick a canary row that carries a real embedding vector: scan a bounded
|
|
102
|
+
* handful of rows and `.get()` the first one whose stored `embedding` is a
|
|
103
|
+
* non-empty array. `.get()` (a primary-key read) always returns the full
|
|
104
|
+
* record including `embedding`, independent of any `select`/projection
|
|
105
|
+
* quirk. Returns null when the corpus has no embedded rows at all — a fresh
|
|
106
|
+
* or empty instance, where there is simply nothing to heal or verify.
|
|
107
|
+
*/
|
|
108
|
+
async function pickCanary(table) {
|
|
109
|
+
for await (const row of table.search({ select: ["id"], limit: CANARY_SCAN_LIMIT })) {
|
|
110
|
+
const id = String(row.id ?? "");
|
|
111
|
+
if (!id)
|
|
112
|
+
continue;
|
|
113
|
+
const full = await table.get(id);
|
|
114
|
+
const emb = full?.embedding;
|
|
115
|
+
if (Array.isArray(emb) && emb.length > 0)
|
|
116
|
+
return { id, embedding: emb };
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The id of the HNSW nearest neighbor of `target` — the exact cosine-sort
|
|
122
|
+
* query the retrieval path (resources/semantic-retrieval-core.ts) uses,
|
|
123
|
+
* bounded to rank 1. Throws if the index is unavailable / mid-rebuild; the
|
|
124
|
+
* caller treats a throw as "not verified this boot".
|
|
125
|
+
*/
|
|
126
|
+
async function topNeighborId(table, target) {
|
|
127
|
+
for await (const row of table.search({
|
|
128
|
+
sort: { attribute: "embedding", target, distance: "cosine" },
|
|
129
|
+
select: ["id"],
|
|
130
|
+
limit: 1,
|
|
131
|
+
})) {
|
|
132
|
+
const id = row.id;
|
|
133
|
+
return id ? String(id) : null;
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Canary self-recall: a healthy HNSW graph returns the canary row itself as
|
|
139
|
+
* the rank-1 neighbor of its OWN stored vector. Cheap, read-only, bounded —
|
|
140
|
+
* and the cleanest black-box proof the graph rebuilt and is serving (a
|
|
141
|
+
* mid-rebuild index throws or returns nothing; a store with no embedded rows
|
|
142
|
+
* has no canary). Never throws (invariant II).
|
|
143
|
+
*/
|
|
144
|
+
async function recallHealthy(table) {
|
|
145
|
+
try {
|
|
146
|
+
const canary = await pickCanary(table);
|
|
147
|
+
if (!canary)
|
|
148
|
+
return { verified: false, hasData: false };
|
|
149
|
+
const top = await topNeighborId(table, canary.embedding);
|
|
150
|
+
return { verified: top === canary.id, hasData: true };
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return { verified: false, hasData: true };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/** Count rows carrying a real (non-hash-fallback) embedding — the vectors that live in the HNSW graph. Best-effort. */
|
|
157
|
+
async function countEmbeddedVectors(table) {
|
|
158
|
+
try {
|
|
159
|
+
let n = 0;
|
|
160
|
+
for await (const row of table.search({ select: ["embeddingModel"] })) {
|
|
161
|
+
const m = row.embeddingModel;
|
|
162
|
+
if (typeof m === "string" && m.length > 0 && m !== "hash-512d")
|
|
163
|
+
n++;
|
|
164
|
+
}
|
|
165
|
+
return n;
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Structural-only ledger OrgEvent (Sherlock discipline, same as
|
|
173
|
+
* resources/migrations/ledger.ts): counts, outcome, version — NEVER a memory
|
|
174
|
+
* id or content (the canary row's id is deliberately omitted). Written via an
|
|
175
|
+
* internal (no-HTTP-context) put — resolveAgentAuth(undefined) → internal, the
|
|
176
|
+
* same trusted server-internal write path the migration ledger already uses.
|
|
177
|
+
*/
|
|
178
|
+
async function writeGraphHealEvent(obs, table) {
|
|
179
|
+
const countNote = obs.embeddedVectorCount != null
|
|
180
|
+
? ` (${obs.embeddedVectorCount} embedded vector${obs.embeddedVectorCount === 1 ? "" : "s"})`
|
|
181
|
+
: "";
|
|
182
|
+
await table.put({
|
|
183
|
+
id: `migration-graph-heal-verified-${obs.at}`,
|
|
184
|
+
authorId: "flair-migrations",
|
|
185
|
+
kind: "migration",
|
|
186
|
+
scope: "full",
|
|
187
|
+
summary: `HNSW graph-heal: recall ${obs.verified ? "verified healthy" : "unconfirmed"}${countNote}`,
|
|
188
|
+
detail: JSON.stringify({
|
|
189
|
+
migrationId: GRAPH_HEAL_ID,
|
|
190
|
+
verified: obs.verified,
|
|
191
|
+
canaryRank1: obs.verified,
|
|
192
|
+
embeddedVectorCount: obs.embeddedVectorCount,
|
|
193
|
+
runningVersion: obs.runningVersion,
|
|
194
|
+
verifiedAt: obs.at,
|
|
195
|
+
}),
|
|
196
|
+
refId: GRAPH_HEAL_ID,
|
|
197
|
+
createdAt: obs.at,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* `getTable`/`getOrgEventTable`/`getVersion`/`now` are injectable so tests can
|
|
202
|
+
* exercise detect/countPending/run against fakes (matching the mocking style
|
|
203
|
+
* of embedding-stamp.ts / the unit tests). The real HNSW self-recall + ledger
|
|
204
|
+
* path is exercised end-to-end in
|
|
205
|
+
* test/integration/hnsw-graph-heal-e2e.test.ts.
|
|
206
|
+
*/
|
|
207
|
+
export function createGraphHealMigration(getTable = defaultMemoryTable, getOrgEventTable = defaultOrgEventTable, getVersion = resolveRunningVersionLocal, now = () => new Date()) {
|
|
208
|
+
return {
|
|
209
|
+
id: GRAPH_HEAL_ID,
|
|
210
|
+
riskClass: "derived-only",
|
|
211
|
+
affectsTables: ["Memory"],
|
|
212
|
+
/**
|
|
213
|
+
* True only when recall is confirmed healthy AND there is embedded data
|
|
214
|
+
* to verify — i.e. the graph rebuilt and is serving. Returns false (the
|
|
215
|
+
* runner marks the migration completed for this boot WITHOUT writing a
|
|
216
|
+
* success state entry, so detect() runs again next boot) when the index
|
|
217
|
+
* is still mid-rebuild or the store has nothing embedded yet. This is the
|
|
218
|
+
* enrollment gate: only a confirmed-healthy boot proceeds to run() +
|
|
219
|
+
* ledger + the version short-circuit.
|
|
220
|
+
*/
|
|
221
|
+
async detect() {
|
|
222
|
+
const { verified } = await recallHealthy(getTable());
|
|
223
|
+
return verified;
|
|
224
|
+
},
|
|
225
|
+
/** Observe-only — never any pending row work. */
|
|
226
|
+
async countPending() {
|
|
227
|
+
return 0;
|
|
228
|
+
},
|
|
229
|
+
/**
|
|
230
|
+
* Called exactly once (countPending() is 0 → the runner's batch loop runs
|
|
231
|
+
* once and breaks). Re-verifies recall for the ledger snapshot, counts
|
|
232
|
+
* the embedded vectors, writes ONE structural-only OrgEvent, and returns
|
|
233
|
+
* `processed: 0` so the completion gate (count+marker, derived-only)
|
|
234
|
+
* passes and the runner records success → short-circuit next boot.
|
|
235
|
+
*/
|
|
236
|
+
async run(_batchSize) {
|
|
237
|
+
const table = getTable();
|
|
238
|
+
const { verified } = await recallHealthy(table);
|
|
239
|
+
const embeddedVectorCount = await countEmbeddedVectors(table);
|
|
240
|
+
try {
|
|
241
|
+
await writeGraphHealEvent({ verified, embeddedVectorCount, runningVersion: getVersion(), at: now().toISOString() }, getOrgEventTable());
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
// Observability must never brick the runner — a ledger-write failure
|
|
245
|
+
// is swallowed; the heal itself already happened schema-side.
|
|
246
|
+
}
|
|
247
|
+
return { processed: 0, touchedIds: [] };
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* registry.ts — the MigrationRegistry: an ordered list of registered
|
|
3
|
-
* migrations. embedding-stamp always
|
|
4
|
-
* variant registers ONLY when shouldRegisterSyntheticMigration() is
|
|
5
|
-
* (see synthetic-test-migration.ts's doc for the exact gating rule).
|
|
3
|
+
* migrations. embedding-stamp and graph-heal always register; the synthetic
|
|
4
|
+
* CI-only variant registers ONLY when shouldRegisterSyntheticMigration() is
|
|
5
|
+
* true (see synthetic-test-migration.ts's doc for the exact gating rule).
|
|
6
6
|
*/
|
|
7
7
|
import { createEmbeddingStampMigration } from "./embedding-stamp.js";
|
|
8
|
+
import { createGraphHealMigration } from "./graph-heal.js";
|
|
8
9
|
import { createSyntheticTestMigration, shouldRegisterSyntheticMigration } from "./synthetic-test-migration.js";
|
|
9
10
|
export class MigrationRegistry {
|
|
10
11
|
migrations = [];
|
|
@@ -23,12 +24,17 @@ export class MigrationRegistry {
|
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
26
|
/**
|
|
26
|
-
* Builds the production registry. Always includes embedding-stamp
|
|
27
|
-
* conditionally includes the CI-only synthetic migration.
|
|
27
|
+
* Builds the production registry. Always includes embedding-stamp and
|
|
28
|
+
* graph-heal; conditionally includes the CI-only synthetic migration.
|
|
29
|
+
*
|
|
30
|
+
* Order note: graph-heal registers AFTER embedding-stamp so that if a boot
|
|
31
|
+
* ever runs both, the (verify-only) recall check ledgers the state AFTER any
|
|
32
|
+
* re-embed work that cycle already applied — never a stale pre-embed reading.
|
|
28
33
|
*/
|
|
29
34
|
export function buildRegistry(env = process.env) {
|
|
30
35
|
const registry = new MigrationRegistry();
|
|
31
36
|
registry.register(createEmbeddingStampMigration());
|
|
37
|
+
registry.register(createGraphHealMigration());
|
|
32
38
|
if (shouldRegisterSyntheticMigration(env)) {
|
|
33
39
|
registry.register(createSyntheticTestMigration());
|
|
34
40
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* models-dir.ts — the ONE place that decides where model GGUFs live.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from embeddings-provider.ts (flair#815) so the reranker
|
|
5
|
+
* (rerank-provider.ts) can share the exact same resolution WITHOUT importing
|
|
6
|
+
* embeddings-provider: several unit-isolated tests `mock.module()` the whole
|
|
7
|
+
* embeddings-provider module (with only the named exports THEY consume), and
|
|
8
|
+
* bun's module cache is process-wide — so any new cross-module import of
|
|
9
|
+
* embeddings-provider from another resource makes those mocks incomplete and
|
|
10
|
+
* kills unrelated test files at module load. This module is tiny, pure
|
|
11
|
+
* node-stdlib, and never mocked, so both providers (and any future model
|
|
12
|
+
* consumer) can depend on it safely.
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync } from "node:fs";
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the directory model GGUFs live in / download into — shared by the
|
|
19
|
+
* embedding engine (embeddings-boot.ts's `register()` options) and the
|
|
20
|
+
* cross-encoder reranker (rerank-provider.ts, flair#815 — it used to hardcode
|
|
21
|
+
* `<cwd>/models`, silently failing open wherever Harper's cwd wasn't the
|
|
22
|
+
* models location).
|
|
23
|
+
*
|
|
24
|
+
* Resolution order (everything writable, never the read-only package dir):
|
|
25
|
+
* 1. FLAIR_MODELS_DIR — explicit operator/docker override.
|
|
26
|
+
* 2. <ROOTPATH>/models — Harper's data dir (Flair passes ROOTPATH =
|
|
27
|
+
* ~/.flair/data when it spawns Harper). User-
|
|
28
|
+
* owned and writable even on sudo-global installs.
|
|
29
|
+
* 3. <cwd>/models — ONLY if a model already lives there. Backward
|
|
30
|
+
* compat for existing writable installs that
|
|
31
|
+
* downloaded into the package dir before this fix;
|
|
32
|
+
* never used as a download target on fresh installs.
|
|
33
|
+
* 4. ~/.flair/data/models — last-resort default when ROOTPATH is unset.
|
|
34
|
+
*
|
|
35
|
+
* The chosen dir is always writable, so the embeddings engine can download the
|
|
36
|
+
* model on first use without hitting EACCES on a root-owned package dir.
|
|
37
|
+
*
|
|
38
|
+
* Tested independently (test/unit/embeddings-models-dir.test.ts, via
|
|
39
|
+
* embeddings-provider's re-export) as the single documented source of truth
|
|
40
|
+
* for this default.
|
|
41
|
+
*/
|
|
42
|
+
export function resolveModelsDir() {
|
|
43
|
+
const override = process.env.FLAIR_MODELS_DIR;
|
|
44
|
+
if (override)
|
|
45
|
+
return override;
|
|
46
|
+
const rootPath = process.env.ROOTPATH;
|
|
47
|
+
if (rootPath)
|
|
48
|
+
return join(rootPath, "models");
|
|
49
|
+
// Backward compat: a prior (writable) install may have the model cached in
|
|
50
|
+
// the package dir. Reuse it rather than re-downloading — but only if present.
|
|
51
|
+
const cwdModels = join(process.cwd(), "models");
|
|
52
|
+
if (existsSync(cwdModels))
|
|
53
|
+
return cwdModels;
|
|
54
|
+
return join(homedir(), ".flair", "data", "models");
|
|
55
|
+
}
|
|
@@ -127,8 +127,8 @@
|
|
|
127
127
|
*
|
|
128
128
|
* `remEligible` — MUST be `false` for every entry in v1 (typed as the
|
|
129
129
|
* literal `false`, not `boolean`, so a stray `true` is a compile error, not
|
|
130
|
-
* just a runtime policy mistake). REM's nightly gather step
|
|
131
|
-
*
|
|
130
|
+
* just a runtime policy mistake). REM's nightly gather step (see docs/rem.md)
|
|
131
|
+
* is hardcoded to `GET /Memory?agentId=…`
|
|
132
132
|
* — single table, single agent, by construction. There is no multi-table
|
|
133
133
|
* distillation input path to opt into yet; the field exists so a future
|
|
134
134
|
* REM generalization doesn't require a breaking schema change to every
|