@cerefox/memory 1.14.2 → 1.14.4
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/AGENT_GUIDE.md +1 -1
- package/dist/bin/cerefox.js +657 -418
- package/dist/server-assets/_shared/ef-meta/index.ts +3 -3
- package/dist/server-assets/_shared/mcp-tools/_utils.ts +38 -6
- package/dist/server-assets/_shared/mcp-tools/metadata-search.ts +103 -21
- package/dist/server-assets/_shared/mcp-tools/search.ts +256 -66
- package/dist/server-assets/supabase/functions/cerefox-metadata-search/index.ts +81 -4
- package/dist/server-assets/supabase/functions/cerefox-search/index.ts +50 -16
- package/docs/guides/connect-agents.md +21 -8
- package/docs/guides/response-limits.md +64 -9
- package/package.json +3 -3
|
@@ -4,6 +4,7 @@ import { isVersionRequest, versionResponse } from "../../../_shared/ef-meta/inde
|
|
|
4
4
|
import { efAuthGate } from "../../../_shared/ef-auth/index.ts";
|
|
5
5
|
import { callerIdentity } from "../../../_shared/mcp-tools/identity.ts";
|
|
6
6
|
import { reviewWorkflowEnabled } from "../../../_shared/mcp-tools/feature-flags.ts";
|
|
7
|
+
import { resolveByteBudget } from "../../../_shared/mcp-tools/_utils.ts";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* cerefox-metadata-search -- Supabase Edge Function
|
|
@@ -103,8 +104,12 @@ Deno.serve(async (req: Request): Promise<Response> => {
|
|
|
103
104
|
const include_content = body.include_content ?? false;
|
|
104
105
|
const requested_max_bytes = body.max_bytes;
|
|
105
106
|
|
|
107
|
+
// One implementation of this arithmetic, shared with every other surface
|
|
108
|
+
// that takes a budget (#268): non-numeric and null both mean "unset" and
|
|
109
|
+
// fall back to the ceiling, while a real number of zero or less is
|
|
110
|
+
// honoured as "almost no budget".
|
|
106
111
|
const max_bytes = include_content
|
|
107
|
-
?
|
|
112
|
+
? resolveByteBudget(requested_max_bytes, MAX_BYTES)
|
|
108
113
|
: null;
|
|
109
114
|
|
|
110
115
|
const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
|
|
@@ -154,18 +159,90 @@ Deno.serve(async (req: Request): Promise<Response> => {
|
|
|
154
159
|
});
|
|
155
160
|
}
|
|
156
161
|
|
|
157
|
-
|
|
162
|
+
let rows = (data ?? []) as Array<Record<string, unknown>>;
|
|
163
|
+
|
|
164
|
+
// Never answer with a shorter list than what matched (#268).
|
|
165
|
+
//
|
|
166
|
+
// The RPC applies `p_max_bytes` server-side by stopping at the first row
|
|
167
|
+
// whose content does not fit, so this list has two indistinguishable
|
|
168
|
+
// causes: fewer documents matched, or the budget cut it short. When the
|
|
169
|
+
// first row is the oversized one the array comes back EMPTY, and a caller
|
|
170
|
+
// reads `[]` as "this knowledge does not exist" and stops looking — the
|
|
171
|
+
// false negative #254 exists to prevent, reached here through a sibling
|
|
172
|
+
// that never got the guard.
|
|
173
|
+
//
|
|
174
|
+
// The response shape stays a bare array, because Custom GPTs are
|
|
175
|
+
// configured against it: every matching document is listed, and only
|
|
176
|
+
// CONTENT is negotiable. Rows the budget could not afford come back
|
|
177
|
+
// content-free and marked, so `results.length` is always the true count
|
|
178
|
+
// and nothing is held back silently.
|
|
179
|
+
//
|
|
180
|
+
// Cost: a second RPC round-trip whenever a content-bearing search returns
|
|
181
|
+
// less than a full page, which is the common case rather than a rare one.
|
|
182
|
+
// The RPC signals no total, so the only alternative to asking is guessing
|
|
183
|
+
// whether a short list was cut — and guessing wrong is the bug.
|
|
184
|
+
if (include_content && max_bytes !== null && rows.length < limit) {
|
|
185
|
+
const { data: headerData, error: probeError } = await supabase.rpc(
|
|
186
|
+
"cerefox_metadata_search",
|
|
187
|
+
{ ...params, p_include_content: false, p_max_bytes: null },
|
|
188
|
+
);
|
|
189
|
+
// supabase-js RESOLVES with `{ data: null, error }` for PostgREST and
|
|
190
|
+
// network failures rather than throwing, so a probe failure must be read
|
|
191
|
+
// from the error, not inferred from an empty list — reading it as "no
|
|
192
|
+
// documents" is the very false empty this branch prevents (#261).
|
|
193
|
+
if (probeError && rows.length === 0) {
|
|
194
|
+
// Falling through here would ship exactly the false empty this block
|
|
195
|
+
// exists to prevent — `200 []`, which a caller reads as "no such
|
|
196
|
+
// knowledge". An error is the honest answer: it says the question was
|
|
197
|
+
// not resolved, rather than answering it wrongly.
|
|
198
|
+
return new Response(
|
|
199
|
+
JSON.stringify({
|
|
200
|
+
error:
|
|
201
|
+
`Nothing fit max_bytes=${max_bytes} with include_content, and the follow-up ` +
|
|
202
|
+
`query that lists what matched failed: ${probeError.message}. This is NOT a ` +
|
|
203
|
+
`confirmed empty result — retry with a larger max_bytes, or include_content: false.`,
|
|
204
|
+
}),
|
|
205
|
+
{ status: 502, headers: { ...CORS_HEADERS, "Content-Type": "application/json" } },
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
if (!probeError) {
|
|
209
|
+
const headers = (headerData ?? []) as Array<Record<string, unknown>>;
|
|
210
|
+
if (headers.length > rows.length) {
|
|
211
|
+
const withContent = new Map(rows.map((r) => [r.document_id as string, r]));
|
|
212
|
+
// The probe carries the full, correctly ordered match set; the
|
|
213
|
+
// content-bearing rows are folded into it by id so ordering is the
|
|
214
|
+
// RPC's, not an artefact of which rows happened to fit.
|
|
215
|
+
const merged = headers.map(
|
|
216
|
+
(h) => withContent.get(h.document_id as string) ?? { ...h, content_omitted: true },
|
|
217
|
+
);
|
|
218
|
+
// Two queries, two chances to disagree: the RPC orders by
|
|
219
|
+
// `updated_at DESC` with no tiebreaker under a LIMIT, and a
|
|
220
|
+
// concurrent write between the calls shifts the window. Anything the
|
|
221
|
+
// content query returned that the probe did not is APPENDED rather
|
|
222
|
+
// than dropped — losing a document we already hold, while fixing a
|
|
223
|
+
// bug about losing documents, would be its own joke.
|
|
224
|
+
const seen = new Set(merged.map((r) => r.document_id as string));
|
|
225
|
+
for (const r of rows) {
|
|
226
|
+
if (!seen.has(r.document_id as string)) merged.push(r);
|
|
227
|
+
}
|
|
228
|
+
rows = merged;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Fire-and-forget usage logging. Counts what MATCHED, not what the budget
|
|
234
|
+
// allowed through: a budget-wiped search logging `0` misreports the store
|
|
235
|
+
// as empty in analytics (#259).
|
|
158
236
|
Promise.resolve(supabase.rpc("cerefox_log_usage", {
|
|
159
237
|
p_operation: "metadata_search",
|
|
160
238
|
p_access_path: "edge-function",
|
|
161
239
|
p_requestor: identityValue ?? null,
|
|
162
240
|
p_query_text: JSON.stringify(metadata_filter),
|
|
163
|
-
p_result_count:
|
|
241
|
+
p_result_count: rows.length,
|
|
164
242
|
p_project_id: project_id,
|
|
165
243
|
})).catch(() => {});
|
|
166
244
|
|
|
167
245
|
// Presentation only: the same shared reader every other surface uses.
|
|
168
|
-
const rows = (data ?? []) as Array<Record<string, unknown>>;
|
|
169
246
|
const showReview = await reviewWorkflowEnabled(supabase);
|
|
170
247
|
const out = showReview
|
|
171
248
|
? rows
|
|
@@ -5,7 +5,7 @@ import { efAuthGate } from "../../../_shared/ef-auth/index.ts";
|
|
|
5
5
|
import { callerIdentity } from "../../../_shared/mcp-tools/identity.ts";
|
|
6
6
|
import { capEmbeddingInput } from "../../../_shared/embeddings/index.ts";
|
|
7
7
|
// One implementation of the byte budget, shared with the MCP tools (#254).
|
|
8
|
-
import { applyByteBudget } from "../../../_shared/mcp-tools/_utils.ts";
|
|
8
|
+
import { applyByteBudget, resolveByteBudget } from "../../../_shared/mcp-tools/_utils.ts";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* cerefox-search — Supabase Edge Function
|
|
@@ -36,7 +36,16 @@ import { applyByteBudget } from "../../../_shared/mcp-tools/_utils.ts";
|
|
|
36
36
|
* `truncated` flag when results were dropped.
|
|
37
37
|
*
|
|
38
38
|
* Response: { results: [...], query, mode, match_count, project_name?,
|
|
39
|
-
* truncated: boolean, response_bytes: number
|
|
39
|
+
* truncated: boolean, response_bytes: number,
|
|
40
|
+
* matched: number, degraded: boolean, note?: string }
|
|
41
|
+
*
|
|
42
|
+
* `matched` is how many results the query found, before the byte budget.
|
|
43
|
+
* `degraded` is true when everything that matched was larger than max_bytes:
|
|
44
|
+
* the items then carry NO content, they name what exists so the caller can
|
|
45
|
+
* re-ask with a larger budget. That header list is capped to max_bytes and so
|
|
46
|
+
* may be a SUBSET of `matched`, except that one item is always returned even
|
|
47
|
+
* if it exceeds the budget — an empty `results` would read as "nothing was
|
|
48
|
+
* found", which is `matched: 0` and nothing else (#254, #257, #261).
|
|
40
49
|
*
|
|
41
50
|
* Example agent prompt:
|
|
42
51
|
* "Invoke the cerefox-search edge function with query='knowledge management'
|
|
@@ -150,14 +159,6 @@ async function lookupProjectId(
|
|
|
150
159
|
return data[0].id;
|
|
151
160
|
}
|
|
152
161
|
|
|
153
|
-
/**
|
|
154
|
-
* Apply a byte budget to an array of result rows.
|
|
155
|
-
*
|
|
156
|
-
* Each row is serialised to JSON to measure its size. Rows are included in
|
|
157
|
-
* order until the next row would push the running total over `maxBytes`.
|
|
158
|
-
* Rows are always kept or dropped whole — content is never truncated
|
|
159
|
-
* mid-document. Returns the accepted rows and a `truncated` flag.
|
|
160
|
-
*/
|
|
161
162
|
const headers = {
|
|
162
163
|
"Content-Type": "application/json",
|
|
163
164
|
"Access-Control-Allow-Origin": "*",
|
|
@@ -214,7 +215,11 @@ Deno.serve(async (req: Request) => {
|
|
|
214
215
|
} = body;
|
|
215
216
|
|
|
216
217
|
// Enforce ceiling: agents may request less but never more than MAX_BYTES.
|
|
217
|
-
|
|
218
|
+
// Sanitised before clamping: `NaN` compares false against every budget
|
|
219
|
+
// check, so a non-numeric value bypassed the ceiling entirely (#266).
|
|
220
|
+
// A number of 0 or less still means "almost no budget"; only a missing or
|
|
221
|
+
// non-numeric value falls back to the ceiling (#267).
|
|
222
|
+
const max_bytes = resolveByteBudget(requested_max_bytes, MAX_BYTES);
|
|
218
223
|
// Clamp match_count to [1, MAX_MATCH_COUNT] (bounds query work; see MAX_MATCH_COUNT).
|
|
219
224
|
const match_count = Math.min(Math.max(1, Math.floor(Number(raw_match_count)) || 5), MAX_MATCH_COUNT);
|
|
220
225
|
|
|
@@ -362,10 +367,33 @@ Deno.serve(async (req: Request) => {
|
|
|
362
367
|
// knowledge does not exist" to whatever is on the other end, so send the
|
|
363
368
|
// rows WITHOUT their content instead: the caller learns what matched, how
|
|
364
369
|
// big it is, and can re-ask with a larger budget or fetch one document.
|
|
370
|
+
//
|
|
371
|
+
// Both content columns have to go: `cerefox_search_docs` (mode "docs")
|
|
372
|
+
// returns `full_content`, while `cerefox_hybrid_search` and the FTS RPC
|
|
373
|
+
// return `content`. Stripping one of them shipped in v1.14.2 and returned
|
|
374
|
+
// 83 KB of chunk text against a 3 KB budget while the response claimed the
|
|
375
|
+
// content had been omitted (#257).
|
|
365
376
|
const degraded = accepted.length === 0 && matched.length > 0;
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
377
|
+
// And the headers themselves are held to the budget the caller asked for:
|
|
378
|
+
// a `match_count` of 200 makes even a content-free list large. At least one
|
|
379
|
+
// survives regardless: an empty `results` is the "nothing was found" shape
|
|
380
|
+
// #254 exists to prevent, and a header row can exceed a very small budget
|
|
381
|
+
// on its own (#259).
|
|
382
|
+
const listed = degraded
|
|
383
|
+
? (() => {
|
|
384
|
+
const headerRows = matched.map(
|
|
385
|
+
({ full_content: _full, content: _chunk, ...header }) => header,
|
|
386
|
+
);
|
|
387
|
+
const fitted = applyByteBudget(headerRows, max_bytes).accepted;
|
|
388
|
+
return fitted.length > 0 ? fitted : headerRows.slice(0, 1);
|
|
389
|
+
})()
|
|
390
|
+
: [];
|
|
391
|
+
const results = degraded ? listed : accepted;
|
|
392
|
+
// What is actually being returned, which on a degraded response is the
|
|
393
|
+
// header list rather than the (empty) content-bearing set.
|
|
394
|
+
const responseBytes = degraded
|
|
395
|
+
? new TextEncoder().encode(JSON.stringify(results)).length
|
|
396
|
+
: usedBytes;
|
|
369
397
|
|
|
370
398
|
// Fire-and-forget usage logging (never blocks the response)
|
|
371
399
|
Promise.resolve(supabase.rpc("cerefox_log_usage", {
|
|
@@ -388,14 +416,20 @@ Deno.serve(async (req: Request) => {
|
|
|
388
416
|
project_name: project_name ?? null,
|
|
389
417
|
metadata_filter: metadata_filter ?? null,
|
|
390
418
|
truncated,
|
|
391
|
-
response_bytes:
|
|
419
|
+
response_bytes: responseBytes,
|
|
392
420
|
matched: matched.length,
|
|
393
421
|
degraded,
|
|
394
422
|
...(degraded
|
|
395
423
|
? {
|
|
396
424
|
note:
|
|
397
425
|
`${matched.length} result(s) matched but none fit max_bytes=${max_bytes}; ` +
|
|
398
|
-
`content omitted
|
|
426
|
+
`content omitted${
|
|
427
|
+
listed.length < matched.length
|
|
428
|
+
? `, and only ${listed.length} of them are listed here` +
|
|
429
|
+
" (the rest did not fit either)"
|
|
430
|
+
: ""
|
|
431
|
+
}. This is NOT an empty result. Raise max_bytes, or fetch one ` +
|
|
432
|
+
`document with cerefox-get-document.`,
|
|
399
433
|
}
|
|
400
434
|
: {}),
|
|
401
435
|
}),
|
|
@@ -638,7 +638,7 @@ In the action editor, paste this schema (replace `<your-project-ref>`):
|
|
|
638
638
|
openapi: 3.1.0
|
|
639
639
|
info:
|
|
640
640
|
title: Cerefox Knowledge Base
|
|
641
|
-
version: 4.
|
|
641
|
+
version: 4.3.0
|
|
642
642
|
servers:
|
|
643
643
|
- url: https://<your-project-ref>.supabase.co/functions/v1
|
|
644
644
|
paths:
|
|
@@ -697,7 +697,10 @@ paths:
|
|
|
697
697
|
Response size budget in bytes (server hard ceiling 200000).
|
|
698
698
|
Whole results are dropped (never truncated mid-document) until
|
|
699
699
|
the budget is met; the response sets `truncated: true` when this
|
|
700
|
-
happens.
|
|
700
|
+
happens. One exception: when nothing fits at all, a single
|
|
701
|
+
content-free header is returned even if it exceeds the budget,
|
|
702
|
+
because an empty results array would read as "nothing was found"
|
|
703
|
+
(see degraded). Advanced; leave unset for the default.
|
|
701
704
|
author:
|
|
702
705
|
type: string
|
|
703
706
|
description: >
|
|
@@ -713,9 +716,11 @@ paths:
|
|
|
713
716
|
chunk_count, total_chars, best_score, is_partial.
|
|
714
717
|
matched is how many results the query found, before the byte budget.
|
|
715
718
|
When degraded is true, everything that matched was larger than max_bytes, so the
|
|
716
|
-
items carry NO
|
|
717
|
-
larger max_bytes or fetch one document.
|
|
718
|
-
|
|
719
|
+
items carry NO content of any kind — they name what exists so you can re-ask with
|
|
720
|
+
a larger max_bytes or fetch one document. That header list is itself capped to
|
|
721
|
+
max_bytes, so it may be a SUBSET of matched (at least one item is always
|
|
722
|
+
returned). Read matched, never results.length, to know how much the query found:
|
|
723
|
+
"nothing was found" is matched == 0.
|
|
719
724
|
is_partial is true when the document exceeded the small-to-big threshold — in that
|
|
720
725
|
case full_content contains matched chunks plus their neighbours rather than the
|
|
721
726
|
complete document, and total_chars still reflects the full document size.
|
|
@@ -1046,8 +1051,10 @@ paths:
|
|
|
1046
1051
|
type: integer
|
|
1047
1052
|
default: 200000
|
|
1048
1053
|
description: >
|
|
1049
|
-
Response size budget in bytes when include_content is true
|
|
1050
|
-
|
|
1054
|
+
Response size budget in bytes when include_content is true.
|
|
1055
|
+
Content is dropped whole, never truncated mid-document, and a
|
|
1056
|
+
document whose content is dropped is still listed with
|
|
1057
|
+
content_omitted: true. Advanced; leave unset for the default.
|
|
1051
1058
|
author:
|
|
1052
1059
|
type: string
|
|
1053
1060
|
description: >
|
|
@@ -1062,6 +1069,12 @@ paths:
|
|
|
1062
1069
|
version_count, content_hash, content }], plus review_status
|
|
1063
1070
|
only while the store's review workflow is on (the key is absent
|
|
1064
1071
|
when it is off).
|
|
1072
|
+
The array lists EVERY matching document, so its length is the true
|
|
1073
|
+
match count. When include_content is true and max_bytes cannot
|
|
1074
|
+
carry a document's text, that document is still listed, with its
|
|
1075
|
+
content omitted and "content_omitted": true set on the item — the
|
|
1076
|
+
list is never silently shortened, and an empty array always means
|
|
1077
|
+
nothing matched.
|
|
1065
1078
|
content_hash is the concurrency token — pass it back as
|
|
1066
1079
|
expected_content_hash when updating via ingestNote.
|
|
1067
1080
|
```
|
|
@@ -1130,7 +1143,7 @@ If the same content was already ingested (SHA-256 hash match), returns `"skipped
|
|
|
1130
1143
|
| `mode` | string | `"docs"` | `"docs"` = full document results (recommended) |
|
|
1131
1144
|
| `alpha` | number | 0.7 | Semantic weight (0 = FTS only, 1 = semantic only) |
|
|
1132
1145
|
| `min_score` | number | 0.5 | Minimum cosine similarity threshold |
|
|
1133
|
-
| `max_bytes` | number | 200000 | Response size budget in bytes. Results are dropped whole (never truncated mid-document) once the budget is reached. The response includes `truncated: true` and `response_bytes` when the limit was hit. See "Response size limit" below. |
|
|
1146
|
+
| `max_bytes` | number | 200000 | Response size budget in bytes. Results are dropped whole (never truncated mid-document) once the budget is reached. The response includes `truncated: true` and `response_bytes` when the limit was hit. When nothing fits, `degraded: true` and a content-free header list come back instead of an empty `results` — always at least one item, even if it exceeds the budget. See "Response size limit" below. |
|
|
1134
1147
|
|
|
1135
1148
|
**Response envelope fields:**
|
|
1136
1149
|
|
|
@@ -8,11 +8,18 @@ explains how response size limits work and how to tune them.
|
|
|
8
8
|
|
|
9
9
|
## The key principle: opt-in limits, never truncate the web UI
|
|
10
10
|
|
|
11
|
-
The web UI
|
|
12
|
-
|
|
11
|
+
The web UI never truncates results. It has no size limit — the browser can handle
|
|
12
|
+
arbitrarily large responses and there is no LLM context window to worry about.
|
|
13
13
|
|
|
14
|
-
Limits
|
|
15
|
-
agent's context window matters
|
|
14
|
+
Limits apply on the MCP, Edge Function **and CLI** paths. On MCP and the Edge Functions
|
|
15
|
+
they exist because an AI agent's context window matters; the CLI applies the same default
|
|
16
|
+
so that one setting (`CEREFOX_MAX_RESPONSE_BYTES`) governs every non-browser path, and
|
|
17
|
+
raises or lowers it per call with `--max-bytes`.
|
|
18
|
+
|
|
19
|
+
> **Changed in v0.10.2.** The CLI originally returned everything, like the web UI. It now
|
|
20
|
+
> honours `CEREFOX_MAX_RESPONSE_BYTES` (200 000 default) and prints
|
|
21
|
+
> `(results truncated at N bytes; use --max-bytes to raise)` when results are dropped.
|
|
22
|
+
> This guide described the pre-v0.10.2 behaviour until v1.14.4.
|
|
16
23
|
|
|
17
24
|
---
|
|
18
25
|
|
|
@@ -21,7 +28,7 @@ agent's context window matters. Callers always choose whether to apply a limit.
|
|
|
21
28
|
| Path | Limit behaviour |
|
|
22
29
|
|------|----------------|
|
|
23
30
|
| Web UI (`/search`) | **No limit** — all results returned |
|
|
24
|
-
| CLI (`cerefox search`) |
|
|
31
|
+
| CLI (`cerefox search`) | Defaults to `CEREFOX_MAX_RESPONSE_BYTES` (200 000); raise or lower per call with `--max-bytes`. Announces truncation. |
|
|
25
32
|
| Local MCP server (`cerefox mcp`) | Defaults to `CEREFOX_MAX_RESPONSE_BYTES` (200 000); agent can request less |
|
|
26
33
|
| Edge Function (`cerefox-search`) | Defaults to 200 000 bytes; agent can request less via `max_bytes` body param |
|
|
27
34
|
| Remote MCP (`cerefox-mcp` Edge Function) | Defaults to 200 000 bytes; agent can request less via `max_bytes` tool param |
|
|
@@ -30,13 +37,61 @@ agent's context window matters. Callers always choose whether to apply a limit.
|
|
|
30
37
|
|
|
31
38
|
## How limits are applied
|
|
32
39
|
|
|
33
|
-
Truncation is always **whole-document**:
|
|
34
|
-
|
|
40
|
+
Truncation is always **whole-document**: a result is returned in full or not at all.
|
|
41
|
+
Cerefox never cuts a document mid-content.
|
|
42
|
+
|
|
43
|
+
A result that does not fit is **skipped**, not treated as the end of the list, so the
|
|
44
|
+
returned set is not necessarily the top N by rank: one oversized document ranked first
|
|
45
|
+
does not hide the smaller results behind it (v1.14.3 on the MCP tool; v1.14.4 on the
|
|
46
|
+
`cerefox-search` Edge Function and the CLI, which both still stopped at the first
|
|
47
|
+
oversized row). Anything skipped is named in the footer, so what is missing is always
|
|
48
|
+
visible.
|
|
35
49
|
|
|
36
50
|
When truncation occurs:
|
|
37
|
-
- The
|
|
51
|
+
- The MCP tool appends a footer naming what was held back:
|
|
52
|
+
`[3 of 12 result(s) shown; 9 did not fit max_bytes=8000: Plan › Rollout (chunk 4) [id: …] and 8 more. Raise max_bytes, narrow the query, or lower match_count.]`
|
|
38
53
|
- The Edge Function includes `"truncated": true` and `"response_bytes": N` in the JSON response.
|
|
39
54
|
|
|
55
|
+
**The reply as a whole stays inside the budget**, footer and warnings included:
|
|
56
|
+
they are measured in the same rendered bytes the caller receives (v1.14.3).
|
|
57
|
+
When the budget is tight the framing gives way before the results do.
|
|
58
|
+
|
|
59
|
+
**When nothing fits at all** — the smallest matching document is larger than
|
|
60
|
+
the whole budget — the reply is NOT "no results found", which an agent acts on
|
|
61
|
+
as "this knowledge does not exist". It is a header list naming what matched,
|
|
62
|
+
its size and its id, prefixed with a warning and the remedy. The same case on
|
|
63
|
+
the Edge Function sets `"degraded": true`, returns items with no content, and
|
|
64
|
+
reports `"matched"`: read that, not `results.length`, to know what the query
|
|
65
|
+
found. A reply may exceed `max_bytes` only by the framing that cannot be dropped
|
|
66
|
+
without misleading you: the notice that results were held back, or the
|
|
67
|
+
below-confidence advisory. Both are a few dozen bytes, and neither is ever
|
|
68
|
+
traded for content. Returning 1 of 5 results without saying so, or presenting
|
|
69
|
+
weak candidates as confident ones, would be worse than a small overrun.
|
|
70
|
+
|
|
71
|
+
### Metadata search follows the same rules (v1.14.4)
|
|
72
|
+
|
|
73
|
+
`cerefox_metadata_search` and the `cerefox-metadata-search` Edge Function apply
|
|
74
|
+
`max_bytes` only when `include_content: true`, and the budget is applied by the
|
|
75
|
+
database, which stops at the first document whose content does not fit. That
|
|
76
|
+
made two silent failures possible until v1.14.4, and both are now closed:
|
|
77
|
+
|
|
78
|
+
- **The reply is never empty when documents matched.** If the first document is
|
|
79
|
+
the oversized one, the budget used to empty the result set — the MCP tool
|
|
80
|
+
returned "No documents match", the Edge Function returned `[]`. Both now say
|
|
81
|
+
what matched: the tool with a warning and a header list, the Edge Function by
|
|
82
|
+
listing every matching document with content omitted.
|
|
83
|
+
- **Documents are never held back silently.** The MCP tool appends
|
|
84
|
+
`[2 of 7 document(s) shown; 5 did not fit max_bytes=20000. …]`. The Edge
|
|
85
|
+
Function keeps its array shape and lists **every** matching document, marking
|
|
86
|
+
the ones whose content did not fit with `"content_omitted": true` — so
|
|
87
|
+
`results.length` is always the true match count and only content is dropped.
|
|
88
|
+
|
|
89
|
+
`max_bytes` is resolved the same way on every path: `null`, absent, empty or
|
|
90
|
+
non-numeric all mean **unset** and fall back to the server ceiling, and none of
|
|
91
|
+
them disables the limit. A real number of zero or less means "almost no
|
|
92
|
+
budget" and is honoured as such — a caller whose allowance has run out is not
|
|
93
|
+
handed the largest possible reply.
|
|
94
|
+
|
|
40
95
|
---
|
|
41
96
|
|
|
42
97
|
## The server ceiling — agents can request less, never more
|
|
@@ -142,7 +197,7 @@ threshold (it is a SQL DEFAULT in `rpcs.sql`, changed via `cerefox server deploy
|
|
|
142
197
|
| Question | Answer |
|
|
143
198
|
|----------|--------|
|
|
144
199
|
| Does the web UI truncate results? | No — unlimited |
|
|
145
|
-
| Does the CLI truncate results? |
|
|
200
|
+
| Does the CLI truncate results? | Yes — at `CEREFOX_MAX_RESPONSE_BYTES`, or `--max-bytes`. It says so when it does. |
|
|
146
201
|
| What is the default MCP response limit? | 200 000 bytes |
|
|
147
202
|
| Can an agent request a smaller limit? | Yes — `max_bytes` tool parameter |
|
|
148
203
|
| Can an agent exceed the server ceiling? | No — always capped |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cerefox/memory",
|
|
3
|
-
"version": "1.14.
|
|
3
|
+
"version": "1.14.4",
|
|
4
4
|
"description": "Cerefox — user-owned shared memory for AI agents. CLI + stdio MCP server + web UI + ingestion for a knowledge base on your own Supabase project (or fully self-hosted with Cerefox Local).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://github.com/fstamatelopoulos/cerefox",
|
|
@@ -39,12 +39,12 @@
|
|
|
39
39
|
"CHANGELOG.md"
|
|
40
40
|
],
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@hono/node-server": "^2.
|
|
42
|
+
"@hono/node-server": "^2.1.1",
|
|
43
43
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
44
44
|
"@supabase/supabase-js": "^2.45.0",
|
|
45
45
|
"cli-progress": "^3.12.0",
|
|
46
46
|
"commander": "^14.0.3",
|
|
47
|
-
"hono": "^4.
|
|
47
|
+
"hono": "^4.13.7",
|
|
48
48
|
"mammoth": "^1.9.0",
|
|
49
49
|
"ora": "^9.4.0",
|
|
50
50
|
"picocolors": "^1.0.0",
|