@mnemoverse/mcp-memory-server 0.8.0 → 0.8.2
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 +39 -5
- package/dist/index.d.ts +23 -1
- package/dist/index.js +664 -122
- package/dist/index.js.map +1 -1
- package/dist/names.d.ts +168 -0
- package/dist/names.js +216 -0
- package/dist/names.js.map +1 -0
- package/dist/render.d.ts +80 -6
- package/dist/render.js +99 -10
- package/dist/render.js.map +1 -1
- package/dist/requests.d.ts +69 -0
- package/dist/requests.js +76 -0
- package/dist/requests.js.map +1 -0
- package/dist/scope.d.ts +223 -0
- package/dist/scope.js +480 -0
- package/dist/scope.js.map +1 -0
- package/dist/teaching.d.ts +68 -24
- package/dist/teaching.js +124 -30
- package/dist/teaching.js.map +1 -1
- package/package.json +4 -3
package/dist/render.js
CHANGED
|
@@ -10,11 +10,27 @@
|
|
|
10
10
|
* shapes without atom_id/created_at degrade gracefully: those parts of
|
|
11
11
|
* the line are simply omitted.
|
|
12
12
|
*/
|
|
13
|
+
import { MAX_DOMAIN_TAG_LITERAL, exactLiteral } from "./names.js";
|
|
13
14
|
/**
|
|
14
|
-
* Sanitize a string for inline interpolation into tool output (CN-032:
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* Sanitize a string for inline interpolation into tool output (CN-032: hostile
|
|
16
|
+
* connectors choose their own agent_name). The single implementation, imported
|
|
17
|
+
* by index.ts and injected into src/scope.ts for the machine-shaped room
|
|
18
|
+
* fields (address, room_id — core charset-validates both; this is a defensive
|
|
19
|
+
* second pass).
|
|
20
|
+
*
|
|
21
|
+
* NOT for anything the reader must reproduce. This is lossy and non-injective
|
|
22
|
+
* by design — non-ASCII becomes spaces, whitespace is collapsed and trimmed,
|
|
23
|
+
* the tail is cut — so two distinct values can come out as one string and a
|
|
24
|
+
* padded value comes out as its clean twin. For a domain name, an id, or
|
|
25
|
+
* anything else that gets compared or sent back, use src/names.ts
|
|
26
|
+
* (`exactLiteral`), which prints exactly or refuses to print.
|
|
27
|
+
*
|
|
28
|
+
* NOT for room NAMES either, as of 0.8.1 (`roomNamePhrase`, src/names.ts).
|
|
29
|
+
* They are display-only, but this sanitiser did not make them harmless so much
|
|
30
|
+
* as it made them wrong: "проект" rendered "(unnamed room)", and "Zoë" was
|
|
31
|
+
* quoted as "Zo" — a different name presented as the name. The exact literal
|
|
32
|
+
* is single-line with quotes and invisibles escaped, so it carries the same
|
|
33
|
+
* anti-injection property without the renaming.
|
|
18
34
|
*/
|
|
19
35
|
export function safeInline(s, cap = 200) {
|
|
20
36
|
return (s ?? "")
|
|
@@ -36,6 +52,38 @@ export function formatAuthorTag(p) {
|
|
|
36
52
|
return "";
|
|
37
53
|
return p.is_external ? ` [by ${who} · external]` : ` [by ${who}]`;
|
|
38
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* ` @"domain"` — which store the memory actually came from, printed so it can
|
|
57
|
+
* be re-sent.
|
|
58
|
+
*
|
|
59
|
+
* Absent before 0.8.1, and its absence was a real trap: an unscoped search for
|
|
60
|
+
* a common name returned five different people's "Maria Chen" from five
|
|
61
|
+
* different projects, ranked together, with nothing on the line to tell them
|
|
62
|
+
* apart (dogfood, 2026-08-07). A reader could not answer "is this mine?"
|
|
63
|
+
* without re-querying scoped.
|
|
64
|
+
*
|
|
65
|
+
* The tag went out through `safeInline`, which defeated the one job it has. It
|
|
66
|
+
* is a DISAMBIGUATOR between stores whose names differ by a space or a
|
|
67
|
+
* character set, and the sanitiser erases exactly those differences:
|
|
68
|
+
* `"проект:acme"` and `"план:acme"` both printed `@:acme`, merging the two
|
|
69
|
+
* stores the tag exists to separate. Worse, `" general"` sanitised to `general`
|
|
70
|
+
* and was then SUPPRESSED by the check below, so a memory from a padded store
|
|
71
|
+
* rendered as if it came from the caller's default bucket. Now the value is
|
|
72
|
+
* printed as an exact JSON literal (src/names.ts) and only the literal string
|
|
73
|
+
* `"general"` is suppressed.
|
|
74
|
+
*
|
|
75
|
+
* Still omitted when the server doesn't send a domain, and for the caller's own
|
|
76
|
+
* default bucket — labelling everything `@"general"` would be noise on the
|
|
77
|
+
* common case. When the literal will not fit, the tag says so rather than
|
|
78
|
+
* disappearing: an absent tag means "the default bucket", which would be a
|
|
79
|
+
* false statement about the store.
|
|
80
|
+
*/
|
|
81
|
+
export function formatDomainTag(domain) {
|
|
82
|
+
if (!domain || domain === "general")
|
|
83
|
+
return "";
|
|
84
|
+
const exact = exactLiteral(domain, MAX_DOMAIN_TAG_LITERAL);
|
|
85
|
+
return exact ? ` @${exact.literal}` : ` @(domain cannot be printed exactly)`;
|
|
86
|
+
}
|
|
39
87
|
/**
|
|
40
88
|
* ` · 2026-08-01 21:04Z` — minute-precision UTC, compact enough for a line
|
|
41
89
|
* tail, precise enough to order a same-day room conversation by eye.
|
|
@@ -52,19 +100,40 @@ export function formatDateTag(createdAt) {
|
|
|
52
100
|
}
|
|
53
101
|
/**
|
|
54
102
|
* One memory_read result line:
|
|
55
|
-
* `N.
|
|
103
|
+
* `N. content (concepts) @"domain" [by X] · 2026-08-01 21:04Z\n id: <uuid>`
|
|
56
104
|
*
|
|
57
105
|
* The id sits on its own indented line: full-width (feedback/delete need
|
|
58
106
|
* the EXACT id, truncation would break them) without crowding the content
|
|
59
107
|
* line a model actually reads.
|
|
108
|
+
*
|
|
109
|
+
* NO SCORE, as of 0.8.1 (Eduard's call). The line used to lead with the
|
|
110
|
+
* server's `relevance` rendered as a percentage, and that was wrong twice
|
|
111
|
+
* over:
|
|
112
|
+
*
|
|
113
|
+
* - It read as confidence and wasn't. The engine's relevance floor
|
|
114
|
+
* (`min_relevance`, default 0.3) is low enough that a query about
|
|
115
|
+
* something never stored still returns near-neighbours — dogfooding got a
|
|
116
|
+
* real person's profile at "73%" for a question about someone fictional,
|
|
117
|
+
* and month-old notes at "73%" for "what's new". A number that in practice
|
|
118
|
+
* never bottoms out cannot say "I don't know", but a reader takes it as
|
|
119
|
+
* though it can. (An earlier draft of this comment claimed there was NO
|
|
120
|
+
* floor. There is one; it is simply too low to mean anything — review,
|
|
121
|
+
* 2026-08-08.)
|
|
122
|
+
* - It wasn't a percentage of anything. Positive feedback pushes the score
|
|
123
|
+
* above 1.0, so reads showed "112%".
|
|
124
|
+
*
|
|
125
|
+
* Rank order still carries the ranking, which is the part that is true. A
|
|
126
|
+
* genuinely dependable signal is worth surfacing and is on the 0.9 list —
|
|
127
|
+
* this is a deliberate removal until there is one, not a decision that
|
|
128
|
+
* scores are useless. `relevance` stays on the type because the server sends
|
|
129
|
+
* it; we simply do not put it in front of a reader yet.
|
|
60
130
|
*/
|
|
61
131
|
export function formatReadItem(item, index) {
|
|
62
|
-
const relevance = ((item?.relevance ?? 0) * 100).toFixed(0);
|
|
63
132
|
const content = item?.content ?? "(empty)";
|
|
64
133
|
const concepts = Array.isArray(item?.concepts) && item.concepts.length > 0
|
|
65
134
|
? ` (${item.concepts.join(", ")})`
|
|
66
135
|
: "";
|
|
67
|
-
const head = `${index + 1}.
|
|
136
|
+
const head = `${index + 1}. ${content}${concepts}${formatDomainTag(item?.domain)}${formatAuthorTag(item?.provenance)}${formatDateTag(item?.created_at)}`;
|
|
68
137
|
return item?.atom_id ? `${head}\n id: ${item.atom_id}` : head;
|
|
69
138
|
}
|
|
70
139
|
/**
|
|
@@ -77,15 +146,35 @@ export function formatRecentItem(item, index) {
|
|
|
77
146
|
? ` (${item.concepts.join(", ")})`
|
|
78
147
|
: "";
|
|
79
148
|
const date = formatDateTag(item?.created_at).replace(/^ · /, "");
|
|
80
|
-
const head = `${index + 1}. ${date ? `[${date}] ` : ""}${content}${concepts}${formatAuthorTag(item?.provenance)}`;
|
|
149
|
+
const head = `${index + 1}. ${date ? `[${date}] ` : ""}${content}${concepts}${formatDomainTag(item?.domain)}${formatAuthorTag(item?.provenance)}`;
|
|
81
150
|
return item?.atom_id ? `${head}\n id: ${item.atom_id}` : head;
|
|
82
151
|
}
|
|
83
|
-
/**
|
|
152
|
+
/**
|
|
153
|
+
* Full feed page: items newest-first + how to continue / that it's over.
|
|
154
|
+
*
|
|
155
|
+
* Returns the BODY only — no escape legend. The legend belongs to the final
|
|
156
|
+
* answer, and the caller (src/index.ts) appends it AFTER capResult: appended
|
|
157
|
+
* here it sat before the cap, which truncates from the end, so the one
|
|
158
|
+
* sentence explaining that an escape like \u200b is ONE character was the
|
|
159
|
+
* first thing cut from every page long enough to be capped (truth F6,
|
|
160
|
+
* 2026-08-08). Once-per-answer still holds — withDomainEscapeLegend is
|
|
161
|
+
* at-most-once by construction (src/names.ts).
|
|
162
|
+
*/
|
|
84
163
|
export function formatRecentPage(items, nextCursor) {
|
|
85
164
|
const lines = items.map((it, i) => formatRecentItem(it, i));
|
|
86
165
|
// Defense-in-depth (CN-032 posture): the cursor is server-supplied and
|
|
87
166
|
// interpolated into instructional text — only echo it when it matches the
|
|
88
|
-
// opaque urlsafe-base64 shape ours always has.
|
|
167
|
+
// opaque urlsafe-base64 shape ours always has. Core mirrors the same regex on
|
|
168
|
+
// its side, so a cursor that fails here is a contract violation, not a value.
|
|
169
|
+
//
|
|
170
|
+
// KNOWN DEFECT, not fixed in 0.8.1 and recorded in the CHANGELOG: this one
|
|
171
|
+
// boolean decides an existence claim about a different thing. `false` means
|
|
172
|
+
// EITHER "the server said there is nothing older" OR "the server said there IS
|
|
173
|
+
// more and I refuse to print the token", and both print `(end of feed —
|
|
174
|
+
// nothing older)`. That is could-not-render spelled exactly like does-not-
|
|
175
|
+
// exist — the collision this release is about, surviving in the one place the
|
|
176
|
+
// fix did not reach. It needs a third branch and therefore a new sentence,
|
|
177
|
+
// which is a behaviour change.
|
|
89
178
|
const cursorOk = nextCursor != null && /^[A-Za-z0-9_=-]{1,512}$/.test(nextCursor);
|
|
90
179
|
const tail = cursorOk
|
|
91
180
|
? `\n\nMore older entries exist — pass cursor: ${nextCursor}`
|
package/dist/render.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render.js","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;
|
|
1
|
+
{"version":3,"file":"render.js","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,sBAAsB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AA8BlE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,UAAU,CAAC,CAA4B,EAAE,GAAG,GAAG,GAAG;IAChE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;SACb,OAAO,CAAC,eAAe,EAAE,GAAG,CAAC;SAC7B,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;SACpB,IAAI,EAAE;SACN,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AACnB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,CAAqB;IACnD,IAAI,CAAC,CAAC;QAAE,OAAO,EAAE,CAAC;IAClB,MAAM,GAAG,GAAG,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,UAAU,IAAI,EAAE,CAAC;IAC1D,MAAM,GAAG,GAAG,UAAU,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IAChC,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,CAAC;IACpB,OAAO,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,QAAQ,GAAG,cAAc,CAAC,CAAC,CAAC,QAAQ,GAAG,GAAG,CAAC;AACpE,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,UAAU,eAAe,CAAC,MAAe;IAC7C,IAAI,CAAC,MAAM,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,EAAE,CAAC;IAC/C,MAAM,KAAK,GAAG,YAAY,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAC;IAC3D,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,sCAAsC,CAAC;AAC/E,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,SAAkB;IAC9C,IAAI,CAAC,SAAS;QAAE,OAAO,EAAE,CAAC;IAC1B,MAAM,CAAC,GAAG,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC;IAC9B,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;QAAE,OAAO,EAAE,CAAC;IACzC,MAAM,GAAG,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC;IAC5B,OAAO,MAAM,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC;AACxD,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,UAAU,cAAc,CAAC,IAAc,EAAE,KAAa;IAC1D,MAAM,OAAO,GAAG,IAAI,EAAE,OAAO,IAAI,SAAS,CAAC;IAC3C,MAAM,QAAQ,GACZ,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC;QACvD,CAAC,CAAC,KAAK,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;QAClC,CAAC,CAAC,EAAE,CAAC;IACT,MAAM,IAAI,GAAG,GAAG,KAAK,GAAG,CAAC,KAAK,OAAO,GAAG,QAAQ,GAAG,eAAe,CAChE,IAAI,EAAE,MAAM,CACb,GAAG,eAAe,CAAC,IAAI,EAAE,UAAU,CAAC,GAAG,aAAa,CAAC,IAAI,EAAE,UAAU,CAAC,EAAE,CAAC;IAC1E,OAAO,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI,YAAY,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAClE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAgB,EAAE,KAAa;IAC9D,MAAM,OAAO,GAAG,IAAI,EAAE,OAAO,IAAI,SAAS,CAAC;IAC3C,MAAM,QAAQ,GACZ,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC;QACvD,CAAC,CAAC,KAAK,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;QAClC,CAAC,CAAC,EAAE,CAAC;IACT,MAAM,IAAI,GAAG,aAAa,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACjE,MAAM,IAAI,GAAG,GAAG,KAAK,GAAG,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,OAAO,GAAG,QAAQ,GAAG,eAAe,CAC3F,IAAI,EAAE,MAAM,CACb,GAAG,eAAe,CAAC,IAAI,EAAE,UAAU,CAAC,EAAE,CAAC;IACxC,OAAO,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI,YAAY,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAClE,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAmB,EAAE,UAA0B;IAC9E,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,gBAAgB,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;IAC5D,uEAAuE;IACvE,0EAA0E;IAC1E,8EAA8E;IAC9E,8EAA8E;IAC9E,EAAE;IACF,2EAA2E;IAC3E,4EAA4E;IAC5E,+EAA+E;IAC/E,wEAAwE;IACxE,2EAA2E;IAC3E,8EAA8E;IAC9E,2EAA2E;IAC3E,+BAA+B;IAC/B,MAAM,QAAQ,GAAG,UAAU,IAAI,IAAI,IAAI,yBAAyB,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAClF,MAAM,IAAI,GAAG,QAAQ;QACnB,CAAC,CAAC,+CAA+C,UAAU,EAAE;QAC7D,CAAC,CAAC,mCAAmC,CAAC;IACxC,OAAO,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;AACnC,CAAC"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request bodies, built by pure functions so the wire contract can be TESTED.
|
|
3
|
+
*
|
|
4
|
+
* Why this module exists. 0.8.1 is a patch, and its rule is that wording may
|
|
5
|
+
* change but where data is written or read from may not. The branch broke that
|
|
6
|
+
* rule twice and neither break was caught:
|
|
7
|
+
*
|
|
8
|
+
* 1. A `trim()` on the way out normalised past a deliberate 400-guard in
|
|
9
|
+
* core, so a padded room address would have written into a shared room
|
|
10
|
+
* visible to other accounts.
|
|
11
|
+
* 2. The revert of that trim dropped `|| undefined` on the read path, so
|
|
12
|
+
* `domain: ""` became `WHERE domain = ''` — a store that cannot exist —
|
|
13
|
+
* turning a search of every domain into a guaranteed miss.
|
|
14
|
+
*
|
|
15
|
+
* The guard test at the time grepped src/index.ts for the ABSENCE of a trim.
|
|
16
|
+
* That cannot catch a DELETED coercion, which is exactly what shipped: 60 tests
|
|
17
|
+
* green with the divergence in place (reviews, 2026-08-08). A denylist over
|
|
18
|
+
* source text is not a contract; a function whose output you can compare is.
|
|
19
|
+
*
|
|
20
|
+
* Rule for changing anything here: if a body changes for ANY input, that is a
|
|
21
|
+
* MINOR release, however obviously correct the change looks. The tests pin the
|
|
22
|
+
* whole matrix — whitespace, non-breaking and zero-width spaces, padded room
|
|
23
|
+
* addresses, "0", ":" — because every one of those was a real finding.
|
|
24
|
+
*
|
|
25
|
+
* Deliberately NOT normalising: core matches domains byte-for-byte and rejects
|
|
26
|
+
* non-canonical room addresses on purpose. Cleaning input here silently moves
|
|
27
|
+
* data and defeats a security guard. Normalisation belongs in 0.9.0, together
|
|
28
|
+
* with memory_delete_domain, with room addresses exempt, and with zero-width
|
|
29
|
+
* characters handled — `trim()` does not strip those.
|
|
30
|
+
*/
|
|
31
|
+
export interface ReadArgs {
|
|
32
|
+
query: string;
|
|
33
|
+
top_k?: number;
|
|
34
|
+
domain?: string;
|
|
35
|
+
order_by?: "relevance" | "recency";
|
|
36
|
+
since?: string;
|
|
37
|
+
until?: string;
|
|
38
|
+
exclude_author?: string;
|
|
39
|
+
}
|
|
40
|
+
export interface RecentArgs {
|
|
41
|
+
domain?: string;
|
|
42
|
+
since?: string;
|
|
43
|
+
until?: string;
|
|
44
|
+
exclude_author?: string;
|
|
45
|
+
limit?: number;
|
|
46
|
+
cursor?: string;
|
|
47
|
+
}
|
|
48
|
+
export interface WriteArgs {
|
|
49
|
+
content: string;
|
|
50
|
+
concepts?: string[];
|
|
51
|
+
domain?: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The scope actually searched: what core receives, and therefore the only value
|
|
55
|
+
* any message about the result may describe.
|
|
56
|
+
*
|
|
57
|
+
* `|| undefined` rather than a null check, because core filters on
|
|
58
|
+
* `domain is not None` — an empty string would become a real, impossible
|
|
59
|
+
* filter. Everything else passes through untouched.
|
|
60
|
+
*/
|
|
61
|
+
export declare function searchedScope(domain?: string): string | undefined;
|
|
62
|
+
/** memory_read. The temporal keys are omitted entirely when unused so the body
|
|
63
|
+
* stays byte-identical for callers that never pass them (#404). */
|
|
64
|
+
export declare function readRequestBody(a: ReadArgs): Record<string, unknown>;
|
|
65
|
+
/** memory_list_recent. */
|
|
66
|
+
export declare function recentRequestBody(a: RecentArgs): Record<string, unknown>;
|
|
67
|
+
/** memory_write. `"general"` is the server-side default made explicit, and is
|
|
68
|
+
* what 0.8.0 sent — NOT a normalisation of the caller's value. */
|
|
69
|
+
export declare function writeRequestBody(a: WriteArgs): Record<string, unknown>;
|
package/dist/requests.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request bodies, built by pure functions so the wire contract can be TESTED.
|
|
3
|
+
*
|
|
4
|
+
* Why this module exists. 0.8.1 is a patch, and its rule is that wording may
|
|
5
|
+
* change but where data is written or read from may not. The branch broke that
|
|
6
|
+
* rule twice and neither break was caught:
|
|
7
|
+
*
|
|
8
|
+
* 1. A `trim()` on the way out normalised past a deliberate 400-guard in
|
|
9
|
+
* core, so a padded room address would have written into a shared room
|
|
10
|
+
* visible to other accounts.
|
|
11
|
+
* 2. The revert of that trim dropped `|| undefined` on the read path, so
|
|
12
|
+
* `domain: ""` became `WHERE domain = ''` — a store that cannot exist —
|
|
13
|
+
* turning a search of every domain into a guaranteed miss.
|
|
14
|
+
*
|
|
15
|
+
* The guard test at the time grepped src/index.ts for the ABSENCE of a trim.
|
|
16
|
+
* That cannot catch a DELETED coercion, which is exactly what shipped: 60 tests
|
|
17
|
+
* green with the divergence in place (reviews, 2026-08-08). A denylist over
|
|
18
|
+
* source text is not a contract; a function whose output you can compare is.
|
|
19
|
+
*
|
|
20
|
+
* Rule for changing anything here: if a body changes for ANY input, that is a
|
|
21
|
+
* MINOR release, however obviously correct the change looks. The tests pin the
|
|
22
|
+
* whole matrix — whitespace, non-breaking and zero-width spaces, padded room
|
|
23
|
+
* addresses, "0", ":" — because every one of those was a real finding.
|
|
24
|
+
*
|
|
25
|
+
* Deliberately NOT normalising: core matches domains byte-for-byte and rejects
|
|
26
|
+
* non-canonical room addresses on purpose. Cleaning input here silently moves
|
|
27
|
+
* data and defeats a security guard. Normalisation belongs in 0.9.0, together
|
|
28
|
+
* with memory_delete_domain, with room addresses exempt, and with zero-width
|
|
29
|
+
* characters handled — `trim()` does not strip those.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* The scope actually searched: what core receives, and therefore the only value
|
|
33
|
+
* any message about the result may describe.
|
|
34
|
+
*
|
|
35
|
+
* `|| undefined` rather than a null check, because core filters on
|
|
36
|
+
* `domain is not None` — an empty string would become a real, impossible
|
|
37
|
+
* filter. Everything else passes through untouched.
|
|
38
|
+
*/
|
|
39
|
+
export function searchedScope(domain) {
|
|
40
|
+
return domain || undefined;
|
|
41
|
+
}
|
|
42
|
+
/** memory_read. The temporal keys are omitted entirely when unused so the body
|
|
43
|
+
* stays byte-identical for callers that never pass them (#404). */
|
|
44
|
+
export function readRequestBody(a) {
|
|
45
|
+
return {
|
|
46
|
+
query: a.query,
|
|
47
|
+
top_k: a.top_k || 5,
|
|
48
|
+
domain: searchedScope(a.domain),
|
|
49
|
+
include_associations: true,
|
|
50
|
+
...(a.order_by ? { order_by: a.order_by } : {}),
|
|
51
|
+
...(a.since ? { since: a.since } : {}),
|
|
52
|
+
...(a.until ? { until: a.until } : {}),
|
|
53
|
+
...(a.exclude_author ? { exclude_author: a.exclude_author } : {}),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** memory_list_recent. */
|
|
57
|
+
export function recentRequestBody(a) {
|
|
58
|
+
return {
|
|
59
|
+
domain: searchedScope(a.domain),
|
|
60
|
+
since: a.since || undefined,
|
|
61
|
+
until: a.until || undefined,
|
|
62
|
+
exclude_author: a.exclude_author || undefined,
|
|
63
|
+
limit: a.limit || 20,
|
|
64
|
+
cursor: a.cursor || undefined,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** memory_write. `"general"` is the server-side default made explicit, and is
|
|
68
|
+
* what 0.8.0 sent — NOT a normalisation of the caller's value. */
|
|
69
|
+
export function writeRequestBody(a) {
|
|
70
|
+
return {
|
|
71
|
+
content: a.content,
|
|
72
|
+
concepts: a.concepts || [],
|
|
73
|
+
domain: a.domain || "general",
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=requests.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"requests.js","sourceRoot":"","sources":["../src/requests.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AA2BH;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,MAAe;IAC3C,OAAO,MAAM,IAAI,SAAS,CAAC;AAC7B,CAAC;AAED;mEACmE;AACnE,MAAM,UAAU,eAAe,CAAC,CAAW;IACzC,OAAO;QACL,KAAK,EAAE,CAAC,CAAC,KAAK;QACd,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC;QACnB,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC;QAC/B,oBAAoB,EAAE,IAAI;QAC1B,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/C,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACtC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACtC,GAAG,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAClE,CAAC;AACJ,CAAC;AAED,0BAA0B;AAC1B,MAAM,UAAU,iBAAiB,CAAC,CAAa;IAC7C,OAAO;QACL,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC;QAC/B,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,SAAS;QAC3B,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,SAAS;QAC3B,cAAc,EAAE,CAAC,CAAC,cAAc,IAAI,SAAS;QAC7C,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE;QACpB,MAAM,EAAE,CAAC,CAAC,MAAM,IAAI,SAAS;KAC9B,CAAC;AACJ,CAAC;AAED;kEACkE;AAClE,MAAM,UAAU,gBAAgB,CAAC,CAAY;IAC3C,OAAO;QACL,OAAO,EAAE,CAAC,CAAC,OAAO;QAClB,QAAQ,EAAE,CAAC,CAAC,QAAQ,IAAI,EAAE;QAC1B,MAAM,EAAE,CAAC,CAAC,MAAM,IAAI,SAAS;KAC9B,CAAC;AACJ,CAAC"}
|
package/dist/scope.d.ts
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search-scope honesty — the note that turns "nothing found" from an assertion
|
|
3
|
+
* about the WORLD into a statement about where we actually looked.
|
|
4
|
+
*
|
|
5
|
+
* Why this module exists (incident 2026-08-07). Two agents lost a working day
|
|
6
|
+
* to the same silent failure: a message was written into a shared room, and an
|
|
7
|
+
* unscoped `memory_list_recent` answered "Nothing new since your watermark."
|
|
8
|
+
* The write was fine, the index was fine, and the read was fine — an UNSCOPED
|
|
9
|
+
* read simply never covers rooms.
|
|
10
|
+
*
|
|
11
|
+
* That is not a filter oversight; it is how the store is built. A room is a
|
|
12
|
+
* separate storage tenant (core ADR-019): with no `domain`, the request runs
|
|
13
|
+
* against the caller's OWN org, and room atoms live in the room's org. So the
|
|
14
|
+
* unscoped feed cannot see them and never could.
|
|
15
|
+
*
|
|
16
|
+
* The defect is therefore not the scoping — it is the WORDING. "Nothing new"
|
|
17
|
+
* is a claim about absence, and absence is only knowable within the scope you
|
|
18
|
+
* searched. An empty result from an unscoped read must therefore say what it did
|
|
19
|
+
* not cover: which rooms went unsearched and how to read them, that every room
|
|
20
|
+
* it has is archived and reachable by no path, or that it could not find out.
|
|
21
|
+
* Silence is correct in exactly one case — the caller has no rooms, so nothing
|
|
22
|
+
* was missed — and that case is a distinct state below rather than the falsy
|
|
23
|
+
* spelling of the other three.
|
|
24
|
+
*
|
|
25
|
+
* Kept out of index.ts so the copy can be unit-tested without booting the
|
|
26
|
+
* stdio transport, matching render.ts and teaching.ts.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* How to NAME the scope inside a sentence, so the sentence is true on its own
|
|
30
|
+
* rather than corrected by a paragraph underneath it.
|
|
31
|
+
*
|
|
32
|
+
* "Nothing new since your watermark." was left untouched by the first two
|
|
33
|
+
* passes of this release — the very sentence this module's header names as the
|
|
34
|
+
* lie that cost two agents a day. A note was appended to it instead, so the
|
|
35
|
+
* assembled answer read as two voices: one asserting you were caught up, the
|
|
36
|
+
* next saying that assertion was meaningless. The sibling branch in memory_read
|
|
37
|
+
* had it right all along by naming its filters inside the sentence (review,
|
|
38
|
+
* 2026-08-08).
|
|
39
|
+
*
|
|
40
|
+
* The name inside that sentence then went through safeInline, which made the
|
|
41
|
+
* sentence false in the case that matters most: a read on `" engineering"` — the
|
|
42
|
+
* padded second store this release is about — answered `Nothing in
|
|
43
|
+
* "engineering"`, naming a DIFFERENT store, and a whitespace-only scope printed
|
|
44
|
+
* as `""`, i.e. as no scope at all. It is now the exact literal (src/names.ts),
|
|
45
|
+
* and when a name cannot be reproduced the sentence names nothing rather than
|
|
46
|
+
* naming something else.
|
|
47
|
+
*
|
|
48
|
+
* A room address stays unnamed ("that room") — unchanged, and deliberately: the
|
|
49
|
+
* address is already in the caller's hand, and this sentence has no need to
|
|
50
|
+
* print another principal's display string. Where a room's NAME is printed at
|
|
51
|
+
* all ({@link liveRoomsNote} below, the room tools in index.ts) it goes through
|
|
52
|
+
* `roomNamePhrase` (src/names.ts) — exactly, or declared unprintable — never
|
|
53
|
+
* through the lossy sanitiser, which renamed "проект" to "(unnamed room)".
|
|
54
|
+
*
|
|
55
|
+
* Lives here rather than in index.ts so the sentence can be unit-tested: this is
|
|
56
|
+
* the module about saying where we looked, and index.ts opens a stdio transport
|
|
57
|
+
* on import.
|
|
58
|
+
*/
|
|
59
|
+
export declare function scopeLabel(searched: string | undefined): string;
|
|
60
|
+
/**
|
|
61
|
+
* "Nothing new since your watermark" is also what you get for a watermark in
|
|
62
|
+
* the FUTURE — a mixed-up timezone or a bad relative-date calculation reads
|
|
63
|
+
* exactly like a clean bill of health (dogfood, 2026-08-07). If the caller's
|
|
64
|
+
* `since` is ahead of now, say so and show the current time; that is the whole
|
|
65
|
+
* diagnosis, and the caller cannot reach it from "nothing new".
|
|
66
|
+
*
|
|
67
|
+
* The explaining clause claims a fact about TIME, never about any store. It
|
|
68
|
+
* used to read "nothing has been written after it YET" — an absence claim over
|
|
69
|
+
* EVERY store, printed directly above the scope disclosure admitting a room
|
|
70
|
+
* went unsearched, and premised on an admittedly-approximate client clock
|
|
71
|
+
* (review, 2026-08-08). The window is empty because it asks for entries newer
|
|
72
|
+
* than a moment that has not happened; what any store holds is not this note's
|
|
73
|
+
* to assert.
|
|
74
|
+
*
|
|
75
|
+
* `nowMs` is injected so the note is testable without freezing the clock.
|
|
76
|
+
*/
|
|
77
|
+
export declare function futureSinceNote(since: string | undefined, nowMs: number): string;
|
|
78
|
+
/**
|
|
79
|
+
* The disclosure for the {@link NamedScope} arm `no-such-domain`: the caller
|
|
80
|
+
* scoped a read to a domain that is not in `/memory/stats.domains`.
|
|
81
|
+
*
|
|
82
|
+
* WHAT IT DOES, which is not what it was called. Until 0.8.1 this was
|
|
83
|
+
* `nearestDomainNote`, and the name outlived the behaviour twice over: the
|
|
84
|
+
* "nearest"/prefix guess it was named for was deleted as unsound (see the body),
|
|
85
|
+
* and even before that it was never a nearest-neighbour search. What remains is
|
|
86
|
+
* two branches, and neither is a proximity search:
|
|
87
|
+
*
|
|
88
|
+
* 1. an EXACT case-insensitive twin, when one exists in the known list and
|
|
89
|
+
* both names can be printed reproducibly — the casing slip that silently
|
|
90
|
+
* forks a namespace, which is the one miss a reader can act on
|
|
91
|
+
* (dogfood, 2026-08-07);
|
|
92
|
+
* 2. otherwise a NAME-FREE statement that no store OF THE CALLER'S OWN
|
|
93
|
+
* carries that exact name — bounded to its evidence: `/memory/stats.domains`
|
|
94
|
+
* covers the caller's own org bucket only, so rooms and other principals'
|
|
95
|
+
* stores are invisible to it and the sentence may not claim them (review,
|
|
96
|
+
* 2026-08-08; the destructive sibling in index.ts already said "in your
|
|
97
|
+
* own store"). It is always safe to say and must never go quiet — silence
|
|
98
|
+
* here is byte-identical to "the store is there, your query merely
|
|
99
|
+
* missed".
|
|
100
|
+
*
|
|
101
|
+
* Deliberately conservative: only case differs. A whitespace or zero-width twin
|
|
102
|
+
* (`" engineering"` beside `"engineering"`) is NOT diagnosed here — extending the
|
|
103
|
+
* rule is a behaviour decision, and `memory_stats` closes that loop by printing
|
|
104
|
+
* both names exactly. Recorded in CHANGELOG's "Known and NOT fixed here".
|
|
105
|
+
*/
|
|
106
|
+
export declare function noSuchDomainNote(domain: string, knownDomains: readonly string[]): string;
|
|
107
|
+
/** The subset of a room record anything here reads. Every field is normalised
|
|
108
|
+
* to a string-or-absent by {@link asRoom} before it is stored. */
|
|
109
|
+
export interface RoomSummary {
|
|
110
|
+
room_id?: string;
|
|
111
|
+
name?: string;
|
|
112
|
+
address?: string;
|
|
113
|
+
role?: string;
|
|
114
|
+
scope?: string;
|
|
115
|
+
archived?: boolean;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Why a probe could not answer. Both are "we do not know", and they are kept
|
|
119
|
+
* apart because the reader's next move differs: a fetch failure is worth
|
|
120
|
+
* retrying, a shape this client cannot read is not.
|
|
121
|
+
*/
|
|
122
|
+
export type UncheckedReason = "fetch-failed" | "unrecognised-shape";
|
|
123
|
+
/**
|
|
124
|
+
* What we know about the shared rooms an UNSCOPED read did not cover.
|
|
125
|
+
*
|
|
126
|
+
* `note` is the disclosure such a read must append, and it is present on
|
|
127
|
+
* exactly the states that have something to disclose. Other consumers of the
|
|
128
|
+
* same payload (memory_list_rooms) read the facts and ignore it.
|
|
129
|
+
*/
|
|
130
|
+
export type RoomScope = {
|
|
131
|
+
/** The probe answered, and the caller has no rooms at all. Nothing was
|
|
132
|
+
* missed, so there is nothing to disclose — hence no `note` field. */
|
|
133
|
+
readonly state: "none";
|
|
134
|
+
} | {
|
|
135
|
+
/** At least one room can still be read by passing its address. */
|
|
136
|
+
readonly state: "live";
|
|
137
|
+
/** Every room in the list, live and archived — for memory_list_rooms. */
|
|
138
|
+
readonly rooms: readonly RoomSummary[];
|
|
139
|
+
readonly live: readonly RoomSummary[];
|
|
140
|
+
readonly archived: number;
|
|
141
|
+
readonly note: string;
|
|
142
|
+
} | {
|
|
143
|
+
/** Rooms exist and NONE of them can be read: every one is archived. */
|
|
144
|
+
readonly state: "archived-only";
|
|
145
|
+
readonly rooms: readonly RoomSummary[];
|
|
146
|
+
readonly archived: number;
|
|
147
|
+
readonly note: string;
|
|
148
|
+
} | {
|
|
149
|
+
/** We could not look. Not evidence in either direction. */
|
|
150
|
+
readonly state: "unknown";
|
|
151
|
+
readonly reason: UncheckedReason;
|
|
152
|
+
readonly note: string;
|
|
153
|
+
};
|
|
154
|
+
/** What we know about the ONE store a scoped read named. */
|
|
155
|
+
export type NamedScope = {
|
|
156
|
+
/** The scope exists and we are in it, so an empty result is a real miss. */
|
|
157
|
+
readonly state: "present";
|
|
158
|
+
readonly name: string;
|
|
159
|
+
} | {
|
|
160
|
+
/** No store with that byte-exact name in the caller's own bucket. */
|
|
161
|
+
readonly state: "no-such-domain";
|
|
162
|
+
readonly name: string;
|
|
163
|
+
readonly note: string;
|
|
164
|
+
} | {
|
|
165
|
+
/** That address is not among the rooms this client can see. */
|
|
166
|
+
readonly state: "no-such-room";
|
|
167
|
+
readonly name: string;
|
|
168
|
+
readonly note: string;
|
|
169
|
+
} | {
|
|
170
|
+
/** We could not look — so nothing may be concluded about the name. */
|
|
171
|
+
readonly state: "unchecked";
|
|
172
|
+
readonly name: string;
|
|
173
|
+
readonly probed: "domains" | "rooms";
|
|
174
|
+
readonly reason: UncheckedReason;
|
|
175
|
+
readonly note: string;
|
|
176
|
+
};
|
|
177
|
+
/**
|
|
178
|
+
* Where a read looked, and what we know about what it therefore did not cover.
|
|
179
|
+
*
|
|
180
|
+
* The two halves are ONE value on purpose. A scoped read must not probe the room
|
|
181
|
+
* list (a room address is checked against the rooms, a domain against the
|
|
182
|
+
* domains — CodeRabbit #65), and an unscoped read must always probe it; as two
|
|
183
|
+
* parameters those invariants were merely unwritten, and both were broken at
|
|
184
|
+
* different points in this release. Here a `named` scope has no room knowledge
|
|
185
|
+
* to be wrong about, and an `own-domains` scope cannot exist without it.
|
|
186
|
+
*/
|
|
187
|
+
export type ReadScope = {
|
|
188
|
+
readonly kind: "named";
|
|
189
|
+
readonly named: NamedScope;
|
|
190
|
+
} | {
|
|
191
|
+
readonly kind: "own-domains";
|
|
192
|
+
readonly rooms: RoomScope;
|
|
193
|
+
};
|
|
194
|
+
/** The disclosure for either kind of scope — "" only where the type says there
|
|
195
|
+
* is genuinely nothing to disclose. */
|
|
196
|
+
export declare function readScopeNote(scope: ReadScope): string;
|
|
197
|
+
/**
|
|
198
|
+
* Classify a `/memory/rooms` body. Total: every payload maps to exactly one
|
|
199
|
+
* state, and a payload that is not a list of rooms maps to `unknown` — never to
|
|
200
|
+
* `none`, which is the substitution that turned a contract violation into "you
|
|
201
|
+
* have no rooms".
|
|
202
|
+
*/
|
|
203
|
+
export declare function classifyRooms(payload: unknown, sanitize: (s: string | undefined | null) => string): RoomScope;
|
|
204
|
+
/** Fetch the caller's rooms and classify them. Never throws — a failure is a
|
|
205
|
+
* state, not an exception, because every caller has to render it. */
|
|
206
|
+
export declare function probeRoomScope(fetchRooms: () => Promise<unknown>, sanitize: (s: string | undefined | null) => string): Promise<RoomScope>;
|
|
207
|
+
/**
|
|
208
|
+
* Build the {@link ReadScope} for one read: where it looked, and what we know
|
|
209
|
+
* about what it therefore did not cover. One GET per call — the room list for
|
|
210
|
+
* an unscoped read or an `xroom:` address, `/memory/stats` for a plain domain
|
|
211
|
+
* — and callers reach it on zero-result paths only. That is MORE probing than
|
|
212
|
+
* 0.8.0 did, not "exactly as before" (an earlier version of this comment):
|
|
213
|
+
* 0.8.0 made no probe at all on a domain-scoped, room-scoped or filtered read
|
|
214
|
+
* and none anywhere in the feed; only the plain unscoped read probed stats.
|
|
215
|
+
* The added reads are disclosed in the 0.8.1 CHANGELOG entry, rate cost
|
|
216
|
+
* included.
|
|
217
|
+
*
|
|
218
|
+
* `searched` must be the value that went over the wire — `searchedScope(domain)`
|
|
219
|
+
* — so that the diagnosis and the request can never describe different stores.
|
|
220
|
+
* A falsy value takes the own-domains path because that is what the request did:
|
|
221
|
+
* core filters on `domain is not None`, and `searchedScope` drops `""`.
|
|
222
|
+
*/
|
|
223
|
+
export declare function probeReadScope(searched: string | undefined, fetchStats: () => Promise<unknown>, fetchRooms: () => Promise<unknown>, sanitize: (s: string | undefined | null) => string): Promise<ReadScope>;
|