@squirrelnotes.app/mcp 2.1.1-test.341 → 2.1.1-test.342
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 +5 -3
- package/dist/stdio-handler.js +255 -30
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ MCP does not (R-038).
|
|
|
72
72
|
|
|
73
73
|
| Tool | Description |
|
|
74
74
|
|------|-------------|
|
|
75
|
-
| `search_memory` | Finds memories by the words in their text, entity and tags: BM25 over the decrypted set, times a recency decay per category (half-life 30 days for `project_state`, a year for preferences and mental models, two years for entity facts), with the app's trigram matching. The decrypted list is reused for up to 60 s, and re-read after any write from this process. Reads every page of `GET /public/memory` and searches the decrypted set on your machine; it never asks the server for one memory, so the server cannot tell which ones were consulted. Leaves out superseded and invalidated memories unless asked. A record that fails to decrypt or verify is skipped and counted (`unverifiable`), never shown. |
|
|
75
|
+
| `search_memory` | Finds memories by the words in their text, entity and tags: BM25 over the decrypted set, times a recency decay per category (half-life 30 days for `project_state`, a year for preferences and mental models, two years for entity facts), with the app's trigram matching. Common English words and contraction fragments (a built-in list of 144: "what", "does", "he", "is", the "s" of "what's"…) are dropped from the query unless it has nothing else. The response names its `ranking` (`lexical` here; `hybrid` with the embeddings package; `none` for a listing) and `total` (matches before `limit`). Each hit has `score`, which orders that response only, and two scores that compare across searches: `lexicalScore` (0–1, the share of the query's content words the memory holds, a prefix counting half) and `semanticScore` (cosine similarity; always `null` in this package). As a guide (ADR-0444, synthetic data): a hit below `lexicalScore` 0.67, and below `semanticScore` 0.45 with embeddings, is rarely the answer (about 1 in 20); at `lexicalScore` ≥ 0.67 (two thirds of the words, as delivered, rounded to two places) about 9 in 10 are, at `semanticScore` ≥ 0.45 about half. `query` is optional when `category` or `entity` is given: the matching memories are then listed, newest first, `limit` at a time; pass `nextCursor` back as `cursor` for the next page (`null` on the last, and always `null` for a search). A memory edited during a walk can be missed, so compare the count against `total` or start again. With neither a query nor a filter, the call is refused. The decrypted list is reused for up to 60 s, and re-read after any write from this process. Reads every page of `GET /public/memory` and searches the decrypted set on your machine; it never asks the server for one memory, so the server cannot tell which ones were consulted. Leaves out superseded and invalidated memories unless asked. A record that fails to decrypt or verify is skipped and counted (`unverifiable`), never shown. |
|
|
76
76
|
| `remember_memory` | Saves one memory (`text`, `category`, `entity`, optional `tags`, `sourceNoteId`). It is never pinned: only you pin, in the app. Returns near-duplicates so the agent can supersede instead of adding a copy. |
|
|
77
77
|
| `supersede_memory` | Replaces an active memory: the old one is kept, marked superseded and linked to the new one. Guarded by the memory's revision, so a concurrent change is refused (409), not overwritten. Both records are written unpinned, even if the old one was pinned. |
|
|
78
78
|
| `invalidate_memory` | Marks an active memory as wrong. The text is kept; the memory is written unpinned. |
|
|
@@ -128,8 +128,10 @@ opt in, change `args` to `["-y", "@squirrelnotes.app/mcp-embeddings"]` and add
|
|
|
128
128
|
- The model runs in the MCP process on your machine. Memory text is not sent
|
|
129
129
|
anywhere to be embedded. Search results still go to your agent.
|
|
130
130
|
- Vectors are kept only in an AES-256-GCM file in your OS cache directory,
|
|
131
|
-
mode `0600`. They are never sent, logged or returned
|
|
132
|
-
|
|
131
|
+
mode `0600`. They are never sent, logged or returned; each hit carries only its
|
|
132
|
+
similarity, one rounded number, as `semanticScore`.
|
|
133
|
+
- Tools, inputs and output keys are identical (`ranking` says `hybrid` when
|
|
134
|
+
meaning was used). If the model is refused or
|
|
133
135
|
cannot load, search is this package's lexical search.
|
|
134
136
|
|
|
135
137
|
This package has no ML dependency: `onnxruntime-node` is installed only by the
|
package/dist/stdio-handler.js
CHANGED
|
@@ -22613,8 +22613,15 @@ function intersectSets(a, b) {
|
|
|
22613
22613
|
function searchableText(p) {
|
|
22614
22614
|
return normalizeText([p.text, p.entity, ...p.tags].join(" "));
|
|
22615
22615
|
}
|
|
22616
|
+
function hasSearchableText(query) {
|
|
22617
|
+
return normalizeText(query).length > 0;
|
|
22618
|
+
}
|
|
22616
22619
|
function queryTerms(query) {
|
|
22617
|
-
|
|
22620
|
+
const tokens = normalizeText(query).split(" ").filter(Boolean);
|
|
22621
|
+
const isStop = (w, i) => STOPWORDS.has(w) || NT_STEMS.has(w) && tokens[i + 1] === "t";
|
|
22622
|
+
const content = [...new Set(tokens.filter((w, i) => !isStop(w, i)))];
|
|
22623
|
+
const words = [...new Set(tokens)];
|
|
22624
|
+
return content.length > 0 ? content : words;
|
|
22618
22625
|
}
|
|
22619
22626
|
function recencyWeight(category, lastValidatedAt, now2) {
|
|
22620
22627
|
const at = Date.parse(lastValidatedAt);
|
|
@@ -22700,7 +22707,7 @@ function termCoverage(text, p) {
|
|
|
22700
22707
|
}
|
|
22701
22708
|
return total / terms.length;
|
|
22702
22709
|
}
|
|
22703
|
-
var DECAY_HALF_LIFE_DAYS, BM25_K1, BM25_B, PREFIX_WEIGHT, DAY_MS;
|
|
22710
|
+
var DECAY_HALF_LIFE_DAYS, BM25_K1, BM25_B, PREFIX_WEIGHT, DAY_MS, STOPWORDS, NT_STEMS;
|
|
22704
22711
|
var init_memory_search = __esm({
|
|
22705
22712
|
"src/memory-search.ts"() {
|
|
22706
22713
|
"use strict";
|
|
@@ -22714,6 +22721,158 @@ var init_memory_search = __esm({
|
|
|
22714
22721
|
BM25_B = 0.75;
|
|
22715
22722
|
PREFIX_WEIGHT = 0.5;
|
|
22716
22723
|
DAY_MS = 864e5;
|
|
22724
|
+
STOPWORDS = /* @__PURE__ */ new Set([
|
|
22725
|
+
"a",
|
|
22726
|
+
"about",
|
|
22727
|
+
"above",
|
|
22728
|
+
"after",
|
|
22729
|
+
"again",
|
|
22730
|
+
"all",
|
|
22731
|
+
"am",
|
|
22732
|
+
"an",
|
|
22733
|
+
"and",
|
|
22734
|
+
"any",
|
|
22735
|
+
"are",
|
|
22736
|
+
"as",
|
|
22737
|
+
"at",
|
|
22738
|
+
"be",
|
|
22739
|
+
"because",
|
|
22740
|
+
"been",
|
|
22741
|
+
"before",
|
|
22742
|
+
"being",
|
|
22743
|
+
"below",
|
|
22744
|
+
"between",
|
|
22745
|
+
"both",
|
|
22746
|
+
"but",
|
|
22747
|
+
"by",
|
|
22748
|
+
"can",
|
|
22749
|
+
"could",
|
|
22750
|
+
"did",
|
|
22751
|
+
"do",
|
|
22752
|
+
"does",
|
|
22753
|
+
"doing",
|
|
22754
|
+
"during",
|
|
22755
|
+
"each",
|
|
22756
|
+
"few",
|
|
22757
|
+
"for",
|
|
22758
|
+
"from",
|
|
22759
|
+
"further",
|
|
22760
|
+
"had",
|
|
22761
|
+
"has",
|
|
22762
|
+
"have",
|
|
22763
|
+
"having",
|
|
22764
|
+
"he",
|
|
22765
|
+
"her",
|
|
22766
|
+
"here",
|
|
22767
|
+
"hers",
|
|
22768
|
+
"herself",
|
|
22769
|
+
"him",
|
|
22770
|
+
"himself",
|
|
22771
|
+
"his",
|
|
22772
|
+
"how",
|
|
22773
|
+
"i",
|
|
22774
|
+
"if",
|
|
22775
|
+
"in",
|
|
22776
|
+
"into",
|
|
22777
|
+
"is",
|
|
22778
|
+
"it",
|
|
22779
|
+
"its",
|
|
22780
|
+
"itself",
|
|
22781
|
+
"just",
|
|
22782
|
+
"me",
|
|
22783
|
+
"more",
|
|
22784
|
+
"most",
|
|
22785
|
+
"my",
|
|
22786
|
+
"myself",
|
|
22787
|
+
"no",
|
|
22788
|
+
"nor",
|
|
22789
|
+
"not",
|
|
22790
|
+
"of",
|
|
22791
|
+
"off",
|
|
22792
|
+
"on",
|
|
22793
|
+
"once",
|
|
22794
|
+
"only",
|
|
22795
|
+
"or",
|
|
22796
|
+
"other",
|
|
22797
|
+
"our",
|
|
22798
|
+
"ours",
|
|
22799
|
+
"ourselves",
|
|
22800
|
+
"out",
|
|
22801
|
+
"over",
|
|
22802
|
+
"own",
|
|
22803
|
+
"same",
|
|
22804
|
+
"she",
|
|
22805
|
+
"should",
|
|
22806
|
+
"so",
|
|
22807
|
+
"some",
|
|
22808
|
+
"such",
|
|
22809
|
+
"than",
|
|
22810
|
+
"that",
|
|
22811
|
+
"the",
|
|
22812
|
+
"their",
|
|
22813
|
+
"theirs",
|
|
22814
|
+
"them",
|
|
22815
|
+
"themselves",
|
|
22816
|
+
"then",
|
|
22817
|
+
"there",
|
|
22818
|
+
"these",
|
|
22819
|
+
"they",
|
|
22820
|
+
"this",
|
|
22821
|
+
"those",
|
|
22822
|
+
"through",
|
|
22823
|
+
"to",
|
|
22824
|
+
"too",
|
|
22825
|
+
"under",
|
|
22826
|
+
"until",
|
|
22827
|
+
"up",
|
|
22828
|
+
"very",
|
|
22829
|
+
"was",
|
|
22830
|
+
"we",
|
|
22831
|
+
"were",
|
|
22832
|
+
"what",
|
|
22833
|
+
"when",
|
|
22834
|
+
"where",
|
|
22835
|
+
"which",
|
|
22836
|
+
"while",
|
|
22837
|
+
"who",
|
|
22838
|
+
"whom",
|
|
22839
|
+
"whose",
|
|
22840
|
+
"why",
|
|
22841
|
+
"will",
|
|
22842
|
+
"with",
|
|
22843
|
+
"would",
|
|
22844
|
+
"you",
|
|
22845
|
+
"your",
|
|
22846
|
+
"yours",
|
|
22847
|
+
"yourself",
|
|
22848
|
+
"yourselves",
|
|
22849
|
+
// What contractions leave once normalizeText turns the apostrophe into a
|
|
22850
|
+
// space ("what's" → "what s", "doesn't" → "doesn t"). A one-letter term
|
|
22851
|
+
// would prefix-match every word starting with that letter. "don", "won" and
|
|
22852
|
+
// "haven" are also words or names, so they are not here: queryTerms drops
|
|
22853
|
+
// them only before a "t" (NT_STEMS).
|
|
22854
|
+
"s",
|
|
22855
|
+
"t",
|
|
22856
|
+
"d",
|
|
22857
|
+
"ll",
|
|
22858
|
+
"m",
|
|
22859
|
+
"re",
|
|
22860
|
+
"ve",
|
|
22861
|
+
"aren",
|
|
22862
|
+
"couldn",
|
|
22863
|
+
"didn",
|
|
22864
|
+
"doesn",
|
|
22865
|
+
"hadn",
|
|
22866
|
+
"hasn",
|
|
22867
|
+
"isn",
|
|
22868
|
+
"mustn",
|
|
22869
|
+
"needn",
|
|
22870
|
+
"shouldn",
|
|
22871
|
+
"wasn",
|
|
22872
|
+
"weren",
|
|
22873
|
+
"wouldn"
|
|
22874
|
+
]);
|
|
22875
|
+
NT_STEMS = /* @__PURE__ */ new Set(["don", "won", "haven"]);
|
|
22717
22876
|
}
|
|
22718
22877
|
});
|
|
22719
22878
|
|
|
@@ -22730,6 +22889,9 @@ function blendRanks(lexical, dense) {
|
|
|
22730
22889
|
for (const [id, s] of scores) scores.set(id, s / max);
|
|
22731
22890
|
return scores;
|
|
22732
22891
|
}
|
|
22892
|
+
function aboveFloor(similarities, minSimilarity) {
|
|
22893
|
+
return similarities.filter((m) => m.similarity >= minSimilarity);
|
|
22894
|
+
}
|
|
22733
22895
|
var RRF_K;
|
|
22734
22896
|
var init_memory_rank = __esm({
|
|
22735
22897
|
"src/memory-rank.ts"() {
|
|
@@ -22785,24 +22947,66 @@ async function listAllMemories(client, key) {
|
|
|
22785
22947
|
return { memories, unverifiable };
|
|
22786
22948
|
}
|
|
22787
22949
|
async function rankForSearch(query, all, eligible, semantic, key, now2) {
|
|
22950
|
+
const byId = new Map(eligible.map((m) => [m.id, m]));
|
|
22951
|
+
const hit = (id, score, similarity2) => ({
|
|
22952
|
+
id,
|
|
22953
|
+
score,
|
|
22954
|
+
lexicalScore: termCoverage(query, byId.get(id).payload),
|
|
22955
|
+
semanticScore: similarity2 ? similarity2.get(id) ?? null : null
|
|
22956
|
+
});
|
|
22788
22957
|
const lexical = scoreMemories(query, eligible, now2);
|
|
22789
|
-
|
|
22790
|
-
|
|
22958
|
+
const lexicalOnly = () => ({ ranked: lexical.map((x) => hit(x.id, x.score, null)), fused: false });
|
|
22959
|
+
if (!semantic) return lexicalOnly();
|
|
22960
|
+
const scored = await semantic.score(
|
|
22791
22961
|
query,
|
|
22792
22962
|
all.map((m) => ({ id: m.id, revision: m.revision, text: memoryEmbeddingText(m.payload) })),
|
|
22793
22963
|
new Set(eligible.map((m) => m.id)),
|
|
22794
22964
|
key
|
|
22795
22965
|
);
|
|
22796
|
-
if (!
|
|
22797
|
-
const
|
|
22966
|
+
if (!scored) return lexicalOnly();
|
|
22967
|
+
const similarity = new Map(scored.similarities.map((m) => [m.id, m.similarity]));
|
|
22798
22968
|
const decay = (id) => {
|
|
22799
22969
|
const p = byId.get(id).payload;
|
|
22800
22970
|
return recencyWeight(p.category, p.lastValidatedAt, now2);
|
|
22801
22971
|
};
|
|
22802
|
-
const dense =
|
|
22803
|
-
const ranked = [...blendRanks(lexical.map((x) => x.id), dense)].map(([id, score]) => ({ id, score, updatedAt: byId.get(id).payload.updatedAt })).sort((a, b) => b.score - a.score || b.updatedAt.localeCompare(a.updatedAt) || (a.id < b.id ? -1 : 1)).map(({ id, score }) => (
|
|
22972
|
+
const dense = aboveFloor(scored.similarities, scored.minSimilarity).map(({ id, similarity: sim }) => ({ id, weighed: sim * decay(id) })).sort((a, b) => b.weighed - a.weighed || (a.id < b.id ? -1 : 1)).map((x) => x.id);
|
|
22973
|
+
const ranked = [...blendRanks(lexical.map((x) => x.id), dense)].map(([id, score]) => ({ id, score, updatedAt: byId.get(id).payload.updatedAt })).sort((a, b) => b.score - a.score || b.updatedAt.localeCompare(a.updatedAt) || (a.id < b.id ? -1 : 1)).map(({ id, score }) => hit(id, score, similarity));
|
|
22804
22974
|
return { ranked, fused: true };
|
|
22805
22975
|
}
|
|
22976
|
+
function listByRecency(eligible) {
|
|
22977
|
+
return [...eligible].sort(compareRecency);
|
|
22978
|
+
}
|
|
22979
|
+
function compareRecency(a, b) {
|
|
22980
|
+
return b.payload.updatedAt.localeCompare(a.payload.updatedAt) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
22981
|
+
}
|
|
22982
|
+
function encodeListCursor(m) {
|
|
22983
|
+
const position = { u: m.payload.updatedAt, i: m.id };
|
|
22984
|
+
return Buffer.from(JSON.stringify(position)).toString("base64url");
|
|
22985
|
+
}
|
|
22986
|
+
function decodeListCursor(cursor) {
|
|
22987
|
+
try {
|
|
22988
|
+
const parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
22989
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
22990
|
+
const { u, i } = parsed;
|
|
22991
|
+
return typeof u === "string" && typeof i === "string" && u.length <= 64 && i.length <= 64 ? { u, i } : null;
|
|
22992
|
+
} catch {
|
|
22993
|
+
return null;
|
|
22994
|
+
}
|
|
22995
|
+
}
|
|
22996
|
+
function listPage(eligible, after, limit) {
|
|
22997
|
+
const ordered = listByRecency(eligible);
|
|
22998
|
+
const start = after === null ? ordered : ordered.filter((m) => compareRecency(m, { id: after.i, payload: { updatedAt: after.u } }) > 0);
|
|
22999
|
+
const page = start.slice(0, limit);
|
|
23000
|
+
const last = page[page.length - 1];
|
|
23001
|
+
return { page, nextCursor: start.length > limit && last ? encodeListCursor(last) : null };
|
|
23002
|
+
}
|
|
23003
|
+
function hitScores(h) {
|
|
23004
|
+
return {
|
|
23005
|
+
score: round(h.score, 2),
|
|
23006
|
+
lexicalScore: round(h.lexicalScore, 2),
|
|
23007
|
+
semanticScore: h.semanticScore === null ? null : round(h.semanticScore, 3)
|
|
23008
|
+
};
|
|
23009
|
+
}
|
|
22806
23010
|
function view(m, score) {
|
|
22807
23011
|
const p = m.payload;
|
|
22808
23012
|
return {
|
|
@@ -22823,7 +23027,7 @@ function view(m, score) {
|
|
|
22823
23027
|
reviewedAt: p.reviewedAt,
|
|
22824
23028
|
flaggedAt: p.flaggedAt ?? null,
|
|
22825
23029
|
updatedAt: p.updatedAt,
|
|
22826
|
-
...score
|
|
23030
|
+
...typeof score === "number" ? { score: round(score, 2) } : score ?? {}
|
|
22827
23031
|
};
|
|
22828
23032
|
}
|
|
22829
23033
|
function chunkNote(title, markdown, category) {
|
|
@@ -23060,33 +23264,45 @@ function registerMemoryTools(server, context, options = {}) {
|
|
|
23060
23264
|
};
|
|
23061
23265
|
server.tool(
|
|
23062
23266
|
"search_memory",
|
|
23063
|
-
|
|
23267
|
+
SEARCH_MEMORY_DESCRIPTION,
|
|
23064
23268
|
{
|
|
23065
|
-
query: external_exports.string().
|
|
23269
|
+
query: external_exports.string().max(500).optional().describe("Words to look for. Optional when category or entity is given: then the matching memories are listed, newest first"),
|
|
23066
23270
|
category: external_exports.enum(MEMORY_CATEGORIES).optional().describe("Only this category"),
|
|
23067
23271
|
entity: external_exports.string().max(MAX_ENTITY).optional().describe("Only memories about this entity (case-insensitive)"),
|
|
23068
23272
|
includeSuperseded: external_exports.boolean().default(false),
|
|
23069
23273
|
includeInvalidated: external_exports.boolean().default(false),
|
|
23070
|
-
limit: external_exports.number().int().min(1).max(20).default(5)
|
|
23274
|
+
limit: external_exports.number().int().min(1).max(20).default(5),
|
|
23275
|
+
cursor: external_exports.string().max(200).optional().describe("A listing only (no query): the nextCursor of the previous page, to get the next one")
|
|
23071
23276
|
},
|
|
23072
|
-
async ({ query, category, entity, includeSuperseded, includeInvalidated, limit }) => {
|
|
23277
|
+
async ({ query, category, entity, includeSuperseded, includeInvalidated, limit, cursor }) => {
|
|
23278
|
+
const wantEntity = entity?.trim().toLowerCase() || void 0;
|
|
23279
|
+
const wantQuery = query?.trim() ? query : void 0;
|
|
23280
|
+
if (wantQuery === void 0 && !category && !wantEntity) return refuse("search_memory", NO_QUERY_OR_FILTER);
|
|
23281
|
+
if (cursor !== void 0 && wantQuery !== void 0) return refuse("search_memory", CURSOR_WITH_QUERY);
|
|
23282
|
+
const after = cursor === void 0 ? null : decodeListCursor(cursor);
|
|
23283
|
+
if (cursor !== void 0 && after === null) return refuse("search_memory", BAD_CURSOR);
|
|
23073
23284
|
try {
|
|
23074
23285
|
const ctx = await unlock();
|
|
23075
23286
|
const { client, key } = ctx;
|
|
23076
23287
|
const { memories, unverifiable } = await loadAll(client, key);
|
|
23077
|
-
const wantEntity = entity?.trim().toLowerCase();
|
|
23078
23288
|
const eligible = memories.filter(
|
|
23079
23289
|
({ payload: p }) => p.state === "active" || p.state === "superseded" && includeSuperseded || p.state === "invalidated" && includeInvalidated
|
|
23080
23290
|
).filter(({ payload: p }) => !category || p.category === category).filter(({ payload: p }) => !wantEntity || p.entity.trim().toLowerCase() === wantEntity);
|
|
23291
|
+
if (wantQuery === void 0) {
|
|
23292
|
+
const { page, nextCursor } = listPage(eligible, after, limit);
|
|
23293
|
+
const results2 = page.map((m) => view(m, NO_SCORES));
|
|
23294
|
+
mcpLog(`memory: search_memory listed=${eligible.length} results=${results2.length} unverifiable=${unverifiable}`);
|
|
23295
|
+
const ranking2 = "none";
|
|
23296
|
+
return ok({ ranking: ranking2, results: results2, total: eligible.length, nextCursor, searched: memories.length, unverifiable });
|
|
23297
|
+
}
|
|
23081
23298
|
const byId = new Map(eligible.map((m) => [m.id, m]));
|
|
23082
|
-
const { ranked, fused } = await rankForSearch(
|
|
23083
|
-
const results = ranked.slice(0, limit).map((x) => view(byId.get(x.id), x
|
|
23299
|
+
const { ranked, fused } = await rankForSearch(wantQuery, memories, eligible, options.semantic ?? null, key, Date.now());
|
|
23300
|
+
const results = ranked.slice(0, limit).map((x) => view(byId.get(x.id), hitScores(x)));
|
|
23301
|
+
const ranking = fused ? "hybrid" : "lexical";
|
|
23084
23302
|
mcpLog(`memory: search_memory searched=${memories.length} results=${results.length} unverifiable=${unverifiable}`);
|
|
23085
|
-
|
|
23086
|
-
|
|
23087
|
-
|
|
23088
|
-
}
|
|
23089
|
-
return ok({ results, searched: memories.length, unverifiable });
|
|
23303
|
+
const body = { ranking, results, total: ranked.length, nextCursor: null, searched: memories.length, unverifiable };
|
|
23304
|
+
if (!hasSearchableText(wantQuery)) return ok({ ...body, note: fused ? MEANING_ONLY_NOTE : NO_SEARCHABLE_TERMS });
|
|
23305
|
+
return ok(body);
|
|
23090
23306
|
} catch (err) {
|
|
23091
23307
|
return fail("search_memory", err);
|
|
23092
23308
|
}
|
|
@@ -23317,7 +23533,7 @@ async function supersedeLanded(client, key, newId, oldId) {
|
|
|
23317
23533
|
return false;
|
|
23318
23534
|
}
|
|
23319
23535
|
}
|
|
23320
|
-
var MEMORY_CATEGORIES, MEMORY_STATES, VIAS, MAX_TEXT, MAX_ENTITY, MAX_REASON, MAX_TAGS, MAX_TAG, MAX_CLIENT_NAME, uuidArg, MAX_IMPORT, CREATED_REVISION, MAX_ENCRYPTED_PAYLOAD_LENGTH, NoteVerificationError, MemoryTooLongError, isStr, isStrOrNull, oneOf, NEAR_DUPLICATE_SCORE, NEAR_DUPLICATE_LIMIT, STORAGE_MESSAGES, KeyContextError, isRetryable, USER_DATA_TEXT, NEEDS_REVIEW_TEXT, PROFILE_TOKEN_BUDGET, CHARS_PER_TOKEN, PROFILE_URI, NO_SEARCHABLE_TERMS, MEANING_ONLY_NOTE, NO_PINNED_MESSAGE, MEMORY_INSTRUCTIONS, NEEDS_REVIEW_HELP, now, MEMORY_CACHE_TTL_MS;
|
|
23536
|
+
var MEMORY_CATEGORIES, MEMORY_STATES, VIAS, MAX_TEXT, MAX_ENTITY, MAX_REASON, MAX_TAGS, MAX_TAG, MAX_CLIENT_NAME, uuidArg, MAX_IMPORT, CREATED_REVISION, MAX_ENCRYPTED_PAYLOAD_LENGTH, NoteVerificationError, MemoryTooLongError, isStr, isStrOrNull, oneOf, NEAR_DUPLICATE_SCORE, NEAR_DUPLICATE_LIMIT, round, NO_SCORES, STORAGE_MESSAGES, KeyContextError, isRetryable, USER_DATA_TEXT, NEEDS_REVIEW_TEXT, PROFILE_TOKEN_BUDGET, CHARS_PER_TOKEN, PROFILE_URI, NO_SEARCHABLE_TERMS, MEANING_ONLY_NOTE, CURSOR_WITH_QUERY, BAD_CURSOR, NO_QUERY_OR_FILTER, NO_PINNED_MESSAGE, MEMORY_INSTRUCTIONS, NEEDS_REVIEW_HELP, SEARCH_MEMORY_DESCRIPTION, now, MEMORY_CACHE_TTL_MS;
|
|
23321
23537
|
var init_memory = __esm({
|
|
23322
23538
|
"src/memory.ts"() {
|
|
23323
23539
|
"use strict";
|
|
@@ -23357,6 +23573,8 @@ var init_memory = __esm({
|
|
|
23357
23573
|
oneOf = (xs, x) => isStr(x) && xs.includes(x);
|
|
23358
23574
|
NEAR_DUPLICATE_SCORE = 0.6;
|
|
23359
23575
|
NEAR_DUPLICATE_LIMIT = 3;
|
|
23576
|
+
round = (x, places) => Math.round(x * 10 ** places) / 10 ** places;
|
|
23577
|
+
NO_SCORES = { score: null, lexicalScore: null, semanticScore: null };
|
|
23360
23578
|
STORAGE_MESSAGES = ["Memory limit reached", "Memory storage limit reached"];
|
|
23361
23579
|
KeyContextError = class extends Error {
|
|
23362
23580
|
constructor(reason) {
|
|
@@ -23374,9 +23592,13 @@ var init_memory = __esm({
|
|
|
23374
23592
|
PROFILE_URI = "memory://profile";
|
|
23375
23593
|
NO_SEARCHABLE_TERMS = "The query has no searchable characters: memory search matches ASCII letters and digits only, so accented or non-Latin text cannot be searched.";
|
|
23376
23594
|
MEANING_ONLY_NOTE = "The query has no ASCII letters or digits, which word matching ignores: these results were found only by meaning, through local embeddings.";
|
|
23595
|
+
CURSOR_WITH_QUERY = "A cursor pages a listing (a category or entity with no query); a search does not page. Leave out the cursor, or the query.";
|
|
23596
|
+
BAD_CURSOR = "That cursor is not one search_memory returned. Use the nextCursor of the previous page, or leave it out to start again.";
|
|
23597
|
+
NO_QUERY_OR_FILTER = "Nothing to search for: give a query, or a category or entity to list the memories that match it.";
|
|
23377
23598
|
NO_PINNED_MESSAGE = "No pinned memories yet. Only the user pins memories, in the Squirrel Notes app (Memory); agents cannot pin. Pinned memories appear here. Use search_memory to look for anything else.";
|
|
23378
23599
|
MEMORY_INSTRUCTIONS = "Squirrel Notes keeps the user's encrypted notes and long-term memories. At the start of a session, call get_memory_profile (or read the resource memory://profile) to load the memories the user has pinned as always relevant (only the user pins, in the app). Use search_memory before asking the user something they may already have told you, and remember_memory, supersede_memory and invalidate_memory to keep memories current. " + NEEDS_REVIEW_TEXT;
|
|
23379
23600
|
NEEDS_REVIEW_HELP = "`needsReview: true` means the memory was written by an agent and the user has not confirmed it yet; it is still in effect. " + USER_DATA_TEXT;
|
|
23601
|
+
SEARCH_MEMORY_DESCRIPTION = `Search the user's long-term memories (short facts and preferences saved by agents or the user). Ranks active memories by their relevance to the query: matching words in the text, entity and tags, and, when the user has installed local embeddings, similar meaning. Common English words (stopwords such as "what", "does", "he", "is") are ignored by word matching unless the query has nothing else. Word matching uses ASCII letters and digits only (as in the app's search): other letters, such as accented or non-Latin ones, are ignored by it, so without local embeddings a query or memory written only in them cannot be found. The response says which ranking ran: "hybrid" (words and meaning), "lexical" (words only) or "none" (a listing). Each hit has \`score\`, which orders this response only and cannot be compared across searches, and two scores that can: \`lexicalScore\` (0\u20131, the share of the query's words, stopwords aside, that the memory contains; a query word that only begins a memory word counts half) and \`semanticScore\` (the cosine similarity of meaning, null unless the ranking is hybrid). As a guide, measured on a synthetic test set: a hit with semanticScore below 0.45 and lexicalScore below 0.67 is rarely the answer (about 1 in 20); at semanticScore 0.45 or more about half are, and at lexicalScore 0.67 or more (two thirds of the query's words, as delivered) about 9 in 10 are. semanticScore is a cosine, from -1 to 1 (in practice about -0.1 to 0.9). Without a query, a category or entity filter lists the matching memories, newest first, \`limit\` (at most 20) at a time: pass the response's \`nextCursor\` as \`cursor\` for the next page (null on the last). \`total\` is how many match in all; a memory edited during a walk may be missed, so compare against \`total\` or start again. ${NEEDS_REVIEW_HELP} Superseded and invalidated memories are left out unless asked for.`;
|
|
23380
23602
|
now = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
23381
23603
|
MEMORY_CACHE_TTL_MS = 6e4;
|
|
23382
23604
|
}
|
|
@@ -24610,6 +24832,7 @@ var memory_semantic_exports = {};
|
|
|
24610
24832
|
__export(memory_semantic_exports, {
|
|
24611
24833
|
RRF_K: () => RRF_K,
|
|
24612
24834
|
SemanticMemoryIndex: () => SemanticMemoryIndex,
|
|
24835
|
+
aboveFloor: () => aboveFloor,
|
|
24613
24836
|
blendRanks: () => blendRanks,
|
|
24614
24837
|
memoryEmbeddingText: () => memoryEmbeddingText
|
|
24615
24838
|
});
|
|
@@ -24648,15 +24871,18 @@ var init_memory_semantic = __esm({
|
|
|
24648
24871
|
batchSize;
|
|
24649
24872
|
settled;
|
|
24650
24873
|
/**
|
|
24651
|
-
*
|
|
24652
|
-
*
|
|
24653
|
-
* lexically".
|
|
24874
|
+
* Every candidate that has a vector, with its cosine similarity to `query`,
|
|
24875
|
+
* best first, and the model's minimum alongside, from one embedding of the
|
|
24876
|
+
* query; or null, meaning "rank lexically". search_memory fuses those at or
|
|
24877
|
+
* above the minimum (`aboveFloor`), weighed by recency, and reports each
|
|
24878
|
+
* hit's similarity as `semanticScore` (ADR-0444, amended). A candidate not
|
|
24879
|
+
* embedded within the time budget is absent.
|
|
24654
24880
|
*
|
|
24655
24881
|
* `all` is every memory the list returned: it decides which vectors are kept
|
|
24656
24882
|
* on file. `candidates` is the subset search_memory has already filtered
|
|
24657
24883
|
* (state, category, entity).
|
|
24658
24884
|
*/
|
|
24659
|
-
async
|
|
24885
|
+
async score(query, all, candidates2, dek) {
|
|
24660
24886
|
const epoch = this.epoch;
|
|
24661
24887
|
const state = await this.settledState();
|
|
24662
24888
|
if (state.kind !== "ready") return null;
|
|
@@ -24669,12 +24895,11 @@ var init_memory_semantic = __esm({
|
|
|
24669
24895
|
const scored = [];
|
|
24670
24896
|
for (const id of candidates2) {
|
|
24671
24897
|
const e = vectors.get(id);
|
|
24672
|
-
if (
|
|
24673
|
-
const similarity = dot(q, e.vector);
|
|
24674
|
-
if (similarity >= embedder.minSimilarity) scored.push({ id, similarity });
|
|
24898
|
+
if (e) scored.push({ id, similarity: dot(q, e.vector) });
|
|
24675
24899
|
}
|
|
24676
24900
|
await this.writeChain;
|
|
24677
|
-
|
|
24901
|
+
scored.sort((a, b) => b.similarity - a.similarity || (a.id < b.id ? -1 : 1));
|
|
24902
|
+
return { similarities: scored, minSimilarity: embedder.minSimilarity };
|
|
24678
24903
|
} catch {
|
|
24679
24904
|
this.state = { kind: "off" };
|
|
24680
24905
|
this.options.log("memory: semantic search failed in the embedding runtime; using lexical search for the rest of this session");
|
package/package.json
CHANGED