@tpsdev-ai/flair 0.44.9 → 0.44.11
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/dist/cli.js +193 -2
- package/dist/rem/runner.js +211 -23
- package/dist/resources/AutoPromoteCandidates.js +203 -0
- package/dist/resources/MemoryBootstrap.js +344 -100
- package/dist/resources/MemoryReflect.js +15 -13
- package/dist/resources/auto-promote-lib.js +137 -0
- package/dist/resources/mcp-tools.js +242 -19
- package/dist/resources/memory-bootstrap-lib.js +58 -0
- package/dist/resources/memory-reflect-lib.js +70 -0
- package/dist/resources/token-estimate.js +25 -0
- package/docs/mcp-clients.md +8 -0
- package/docs/rem.md +19 -2
- package/package.json +1 -1
- package/schemas/memory.graphql +9 -0
|
@@ -2,7 +2,7 @@ import { Resource, databases } from "harper";
|
|
|
2
2
|
import { allowVerified, resolveAgentAuth } from "./agent-auth.js";
|
|
3
3
|
import { getEmbedding } from "./embeddings-provider.js";
|
|
4
4
|
import { wrapUntrusted } from "./content-safety.js";
|
|
5
|
-
import { isTeammate, formatTeamLine } from "./memory-bootstrap-lib.js";
|
|
5
|
+
import { isTeammate, formatTeamLine, isZeroRowNoOpEvent } from "./memory-bootstrap-lib.js";
|
|
6
6
|
import { resolveReadScope } from "./memory-read-scope.js";
|
|
7
7
|
import { isValidEntity } from "./entity-vocab.js";
|
|
8
8
|
import { withDetachedTxn } from "./table-helpers.js";
|
|
@@ -17,6 +17,7 @@ import { buildCollisionEntries, buildEntityMatchCondition, freshPresenceByAgent,
|
|
|
17
17
|
import { retrieveCandidates, DEFAULT_SELECT } from "./semantic-retrieval-core.js";
|
|
18
18
|
import { buildTrustBlock } from "./trust-block.js";
|
|
19
19
|
import { bestSemanticSimilarity, evaluateAbstention } from "./abstention.js";
|
|
20
|
+
import { estimateTokens } from "./token-estimate.js";
|
|
20
21
|
/**
|
|
21
22
|
* POST /MemoryBootstrap
|
|
22
23
|
*
|
|
@@ -72,9 +73,10 @@ import { bestSemanticSimilarity, evaluateAbstention } from "./abstention.js";
|
|
|
72
73
|
* row's `entities`.
|
|
73
74
|
*
|
|
74
75
|
* Response:
|
|
75
|
-
* { context, sections, tokenEstimate, memoriesIncluded, memoriesAvailable,
|
|
76
|
-
* teammateFindingsIncluded, teammateFindingsTruncated,
|
|
77
|
-
*
|
|
76
|
+
* { context, sections, tokenEstimate, maxTokens, memoriesIncluded, memoriesAvailable,
|
|
77
|
+
* memoriesTruncated, teammateFindingsIncluded, teammateFindingsTruncated,
|
|
78
|
+
* teammateFindingsMatched, agentId, scope, soul, memories, predicted,
|
|
79
|
+
* teammateFindings, events[, currentTaskHint][, predictedHint] }
|
|
78
80
|
* The self-describing keys (flair#1182 part 1) — `agentId` (resolved caller),
|
|
79
81
|
* `scope` (read model applied to the caller), `soul`/`memories`/`predicted`
|
|
80
82
|
* (the caller's OWN records as structured containers), and `currentTaskHint`
|
|
@@ -90,24 +92,62 @@ import { bestSemanticSimilarity, evaluateAbstention } from "./abstention.js";
|
|
|
90
92
|
* when the prose `context` is off (the /mcp default); before #1206 they lived
|
|
91
93
|
* ONLY in the prose string and were orphaned at includeContext=false.
|
|
92
94
|
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
95
|
+
* CAP CONTRACT: `maxTokens` is the HARD cap on CONTENT SELECTION — the shared
|
|
96
|
+
* `tokenBudget` starts at `maxTokens` and every admitted soul/memory/finding
|
|
97
|
+
* AND every org event (flair#1199 — events are content too; before this they
|
|
98
|
+
* were assembled but NEVER charged, so a maxTokens=4000 request serialized at
|
|
99
|
+
* 6286) is gated against the remaining budget, so the sum of selected CONTENT
|
|
100
|
+
* never exceeds `maxTokens`. Each item is charged the cost of what it ACTUALLY
|
|
101
|
+
* SHIPS on the requested surface (see contentCost): on the /mcp connector path
|
|
102
|
+
* (includeContext=false) the prose `context` is a pointer, so only the
|
|
103
|
+
* STRUCTURED container object ships and is charged; on the REST/CLI prose path
|
|
104
|
+
* (includeContext=true) the prose IS the shipped surface and is charged (0.44.6
|
|
105
|
+
* capacity — flair#1207). `tokenEstimate` HONESTLY reports the real serialized
|
|
106
|
+
* payload (`JSON.stringify(responseBody)`). On the connector path — where the
|
|
107
|
+
* selection charge and `tokenEstimate` measure the SAME structured bytes —
|
|
108
|
+
* `tokenEstimate` exceeds `maxTokens` only by the FIXED JSON scaffolding
|
|
109
|
+
* (keys/braces, counters, the sections map, hints); the connector-conformance
|
|
110
|
+
* budgetCap asserts tokenEstimate <= maxTokens within a small tolerance for
|
|
111
|
+
* exactly that scaffolding. On the prose path the payload ALSO carries the
|
|
112
|
+
* structured mirror, so `tokenEstimate` may exceed `maxTokens` by that mirror —
|
|
113
|
+
* that overage is honest measurement, and shrinking prose selection to hide it
|
|
114
|
+
* is the flair#1207 regression (below). flair#1199 (0.44.11): charging the
|
|
115
|
+
* PROSE line but shipping the heavier STRUCTURED object on the /mcp path let
|
|
116
|
+
* teammate findings ride OUTSIDE the enforced budget (soulTokens 377 +
|
|
117
|
+
* memoryTokens 3574 = 3951 prose, just under a 4000 cap, yet tokenEstimate 5337
|
|
118
|
+
* = +33%; teammateFindingsIncluded crept 4→5) — fixed by charging structured
|
|
119
|
+
* on the connector path. flair#1207: #1199 had ALSO folded a per-item
|
|
120
|
+
* structured overhead + a scaffolding reserve INTO the selection budget, which
|
|
121
|
+
* silently shrank recall below 0.44.6 for the same `maxTokens`; that flat
|
|
122
|
+
* per-item overhead is a reporting concern (already captured by `tokenEstimate`)
|
|
123
|
+
* and no longer shrinks the content budget — the 0.44.11 fix charges the
|
|
124
|
+
* item's REAL shipped serialization, not a flat surcharge, and only on the path
|
|
125
|
+
* where that serialization is the shipped surface.
|
|
126
|
+
*
|
|
127
|
+
* COUNT CONTRACT (flair#1207): `memoriesIncluded + memoriesTruncated <=
|
|
128
|
+
* memoriesAvailable` — included and truncated are disjoint sets of UNIQUE own
|
|
129
|
+
* memories (a memory budget-skipped in one section but admitted in another counts
|
|
130
|
+
* as included, never both). `teammateFindingsMatched` is the teammate match pool
|
|
131
|
+
* that cleared the relevance floor; `teammateFindingsIncluded +
|
|
132
|
+
* teammateFindingsTruncated == teammateFindingsMatched`, so "truncated" means
|
|
133
|
+
* "relevant but no budget," not "every candidate not selected."
|
|
105
134
|
* `predictedHint` is present only when subjects were provided but `predicted`
|
|
106
135
|
* came back empty.
|
|
107
136
|
*/
|
|
108
137
|
// Collision surfacing (flair#681) tunables.
|
|
109
138
|
const COLLISION_WINDOW_DAYS = 7;
|
|
110
139
|
const MAX_COLLISION_ENTRIES = 10;
|
|
140
|
+
// flair#1201/#1225 — trust-block sections that are a lifecycle-window LOAD, not
|
|
141
|
+
// a retrieval surface. A trust entry from one of these carries `matchQuality:
|
|
142
|
+
// null` (no relevance score to band), which is CORRECT (Kern's #1220 ruling),
|
|
143
|
+
// never a scoring failure — see the `matchQualityNote` in the response tail.
|
|
144
|
+
const LIFECYCLE_SECTIONS = new Set(["permanent", "recent", "predicted"]);
|
|
145
|
+
// flair#1199/#1206 — the default cap on how many org events bootstrap ships.
|
|
146
|
+
// Overridable per-request via `maxEvents`. Event slots are scarce AND (as of
|
|
147
|
+
// #1199) token-charged, so this bounds both the count and the spend; the shared
|
|
148
|
+
// tokenBudget is the harder ceiling (an event that doesn't fit is skipped even
|
|
149
|
+
// under the cap).
|
|
150
|
+
const MAX_ORG_EVENTS = 10;
|
|
111
151
|
// ─── Bootstrap scale fix (flair-bootstrap-scale-fix) tunables ───────────────
|
|
112
152
|
//
|
|
113
153
|
// Own-scoped, non-permanent memories (the "recent" adaptive-window source,
|
|
@@ -152,10 +192,10 @@ const TASK_RELEVANCE_FLOOR = 0.3;
|
|
|
152
192
|
// line — while `tokenEstimate` keeps reporting the true serialized size, which
|
|
153
193
|
// may exceed `maxTokens` by the scaffolding overhead. See the module-doc CAP
|
|
154
194
|
// CONTRACT above.
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
195
|
+
// Token estimate (~4 chars per token for English text) now lives in the
|
|
196
|
+
// harper-free ./token-estimate.js module, so the content-selection budget, the
|
|
197
|
+
// reported `tokenEstimate`, and the flair#1213 conformance tokenEstimate
|
|
198
|
+
// invariant are all computed with ONE definition. See that module's header.
|
|
159
199
|
// `agentId` is the BOOTSTRAPPING agent (the caller) — used only to decide
|
|
160
200
|
// whether to annotate attribution, never to change what's read (that
|
|
161
201
|
// boundary is resolveReadScope()'s job, upstream of this function). A
|
|
@@ -193,6 +233,15 @@ export class BootstrapMemories extends Resource {
|
|
|
193
233
|
subjects, // e.g., ["flair", "auth"] — entities to preload context for
|
|
194
234
|
includeTrust = false, // flair#744 slice 1 — opt-in per-memory trust block
|
|
195
235
|
abstain = false, // flair#744 slice 2 — opt-in task-relevance abstention
|
|
236
|
+
// flair#1199 — org-event knobs. `maxEvents` caps how many events ship
|
|
237
|
+
// (default MAX_ORG_EVENTS); `includeEventDetail` gates the verbose per-event
|
|
238
|
+
// `detail` JSON (default OFF — mirrors the includeContext opt-in). By
|
|
239
|
+
// default bootstrap ships LEAN events (id/kind/summary/createdAt/targetIds/
|
|
240
|
+
// scope); `detail` restates the summary + migration internals and is pure
|
|
241
|
+
// bloat for a connector, so it is opt-in. Both are also counted against the
|
|
242
|
+
// shared tokenBudget below (before #1199 the events array was assembled but
|
|
243
|
+
// NEVER charged, so a maxTokens=4000 request serialized well past budget).
|
|
244
|
+
maxEvents, includeEventDetail = false,
|
|
196
245
|
// flair#1199 — whether to assemble the prose `context` string. The
|
|
197
246
|
// structured containers (soul/memories/predicted/teammateFindings) are the
|
|
198
247
|
// CANONICAL payload; `context` is a human/agent-readable MIRROR of the same
|
|
@@ -263,16 +312,37 @@ export class BootstrapMemories extends Resource {
|
|
|
263
312
|
let memoriesIncluded = 0;
|
|
264
313
|
let memoriesAvailable = 0;
|
|
265
314
|
let memoriesTruncated = 0;
|
|
315
|
+
// flair#1207 — count arithmetic by UNIQUE own-memory id, not raw increments.
|
|
316
|
+
// The invariant `memoriesIncluded + memoriesTruncated <= memoriesAvailable`
|
|
317
|
+
// MUST hold (0.44.9 reported available:3 included:2 truncated:2 — 2+2 > 3).
|
|
318
|
+
// Two bugs produced the over-count, both fixed by keying off these sets:
|
|
319
|
+
// (1) a memory truncated for budget in an EARLY section (e.g. recent's
|
|
320
|
+
// 40% sub-budget) reappears in the task-relevant candidate pool and is
|
|
321
|
+
// counted AGAIN (truncated twice, or truncated-then-included) — the
|
|
322
|
+
// same physical memory in two denominators; and
|
|
323
|
+
// (2) the task-relevant loop's "already included" exclusion set was built
|
|
324
|
+
// POSITIONALLY (recent.filter by index) and omitted `predicted`, so a
|
|
325
|
+
// predicted memory could be re-admitted to `relevant`.
|
|
326
|
+
// `includedOwnIds` is now THE authoritative "own memory already placed" set
|
|
327
|
+
// (used as the exclusion set in the predicted + task-relevant loops, replacing
|
|
328
|
+
// the positional hack); `truncatedOwnIds` records own budget-skips. Final
|
|
329
|
+
// counts are DERIVED from these sets (truncated = truncated-but-not-included),
|
|
330
|
+
// so both are disjoint subsets of memoriesAvailable and the invariant holds.
|
|
331
|
+
const includedOwnIds = new Set();
|
|
332
|
+
const truncatedOwnIds = new Set();
|
|
266
333
|
// flair#1199 — cross-agent teammate findings included (a DIFFERENT
|
|
267
334
|
// denominator than own memories): counting these into `memoriesIncluded`
|
|
268
335
|
// is what let one client see included(9) > available(3).
|
|
269
336
|
let teammateFindingsIncluded = 0;
|
|
270
337
|
// flair#1207 — teammate findings SKIPPED for size in the task-relevant
|
|
271
|
-
// packing loop
|
|
272
|
-
//
|
|
273
|
-
// "
|
|
274
|
-
//
|
|
275
|
-
// `
|
|
338
|
+
// packing loop (cleared the relevance floor but didn't fit the BUDGET).
|
|
339
|
+
// Reported ALONGSIDE `teammateFindingsMatched` (the whole floor-clearing pool
|
|
340
|
+
// considered), so "truncated" unambiguously means "relevant-but-no-budget",
|
|
341
|
+
// NOT "every candidate not selected" — 0.44.9's `truncated:89` beside
|
|
342
|
+
// `included:4` read as "89 relevant findings cut" with no pool to anchor it
|
|
343
|
+
// (heskew's #1207 nit). Both are disjoint-and-exhaustive over the matched
|
|
344
|
+
// pool (each matched teammate finding is EITHER included OR truncated), so
|
|
345
|
+
// teammateFindingsIncluded + teammateFindingsTruncated == teammateFindingsMatched.
|
|
276
346
|
let teammateFindingsTruncated = 0;
|
|
277
347
|
// flair#1182 (part 1) — self-describing bootstrap. These structured
|
|
278
348
|
// container keys are ALWAYS emitted on the response (empty `{}`/`[]` when
|
|
@@ -318,6 +388,34 @@ export class BootstrapMemories extends Resource {
|
|
|
318
388
|
subject: m.subject ?? null,
|
|
319
389
|
section,
|
|
320
390
|
});
|
|
391
|
+
// flair#1199 (0.44.11) — the REAL cost an admitted content item adds to the
|
|
392
|
+
// serialized payload the caller RECEIVES, which is exactly what
|
|
393
|
+
// `tokenEstimate` measures. This is the fix for the teammate-findings
|
|
394
|
+
// budget blowout: the selector used to charge every memory/finding its
|
|
395
|
+
// PROSE line (`formatMemory`) but, on the /mcp connector path, ship the
|
|
396
|
+
// heavier STRUCTURED container object — and `tokenEstimate` measures the
|
|
397
|
+
// structured object. A teammate finding carries `id` + TWO ISO timestamps +
|
|
398
|
+
// `source` + `section` + JSON field names/quotes that the prose line does
|
|
399
|
+
// not, so the shipped object runs ~1.5–1.7× its prose line. Charging prose
|
|
400
|
+
// but shipping structured let several teammate findings ride OUTSIDE the
|
|
401
|
+
// enforced budget: a maxTokens=4000 bootstrap reported soulTokens 377 +
|
|
402
|
+
// memoryTokens 3574 = 3951 (prose, just under cap) yet serialized at 5337
|
|
403
|
+
// (+33%), and teammateFindingsIncluded crept 4→5. So charge what SHIPS:
|
|
404
|
+
// - /mcp connector path (includeContext=false): the prose `context` is a
|
|
405
|
+
// compact pointer (no bodies), so ONLY the structured container ships —
|
|
406
|
+
// charge its serialized size. Now the sum of admitted content ≈
|
|
407
|
+
// `tokenEstimate` minus the FIXED JSON scaffolding, so `tokenEstimate`
|
|
408
|
+
// stays within maxTokens + the small scaffolding tolerance.
|
|
409
|
+
// - REST/CLI prose path (includeContext=true): the prose IS the primary
|
|
410
|
+
// shipped surface and flair#1207 deliberately fixed the selection budget
|
|
411
|
+
// at 0.44.6 (prose) capacity — `tokenEstimate` is HONEST there and may
|
|
412
|
+
// exceed maxTokens by the structured mirror it also ships. Charging
|
|
413
|
+
// structured on THAT path would re-shrink prose recall below 0.44.6
|
|
414
|
+
// (the exact #1207 regression). So keep charging prose on the prose
|
|
415
|
+
// path. This is "fix the budget INPUT, not the cap": the figure the
|
|
416
|
+
// selector tests against maxTokens is now the figure that becomes
|
|
417
|
+
// `tokenEstimate` on each path.
|
|
418
|
+
const contentCost = (structured, proseLine) => includeContext ? estimateTokens(proseLine) : estimateTokens(JSON.stringify(structured));
|
|
321
419
|
// --- 1. Soul records (budgeted — prioritized by key importance) ---
|
|
322
420
|
// Soul is who you are, but we still need to respect token budgets.
|
|
323
421
|
// Workspace files (SOUL.md, AGENTS.md) can be massive — they're already
|
|
@@ -567,17 +665,21 @@ export class BootstrapMemories extends Resource {
|
|
|
567
665
|
const permanent = permanentRows.filter((m) => !permanentSupersededIds.has(m.id));
|
|
568
666
|
for (const m of permanent) {
|
|
569
667
|
const line = formatMemory(m, agentId);
|
|
570
|
-
const
|
|
668
|
+
const struct = leanMemory(m, "permanent");
|
|
669
|
+
// #1199 (0.44.11) — charge what SHIPS (structured on the /mcp path, prose
|
|
670
|
+
// on the REST path); see contentCost. #1207 stays honored: on the prose
|
|
671
|
+
// path this is still the prose-line cost, so REST recall is unchanged.
|
|
672
|
+
const cost = contentCost(struct, line);
|
|
571
673
|
if (cost <= tokenBudget) {
|
|
572
674
|
sections.permanent.push(line);
|
|
573
|
-
includedOwnMemories.push(
|
|
675
|
+
includedOwnMemories.push(struct);
|
|
574
676
|
if (includeTrust)
|
|
575
677
|
includedTrustMemories.push({ m, section: "permanent" });
|
|
576
678
|
tokenBudget -= cost;
|
|
577
|
-
|
|
679
|
+
includedOwnIds.add(m.id); // #1207 — count by unique own-memory id
|
|
578
680
|
}
|
|
579
681
|
else {
|
|
580
|
-
|
|
682
|
+
truncatedOwnIds.add(m.id); // #1207 — budget-skip, deduped against inclusions at the end
|
|
581
683
|
}
|
|
582
684
|
}
|
|
583
685
|
// --- 3. Recent memories (adaptive window) ---
|
|
@@ -639,18 +741,19 @@ export class BootstrapMemories extends Resource {
|
|
|
639
741
|
let recentSpent = 0;
|
|
640
742
|
for (const m of recent) {
|
|
641
743
|
const line = formatMemory(m, agentId);
|
|
642
|
-
const
|
|
744
|
+
const struct = leanMemory(m, "recent");
|
|
745
|
+
const cost = contentCost(struct, line); // #1199 (0.44.11) — charge what ships; see contentCost
|
|
643
746
|
if (recentSpent + cost > recentBudget) {
|
|
644
|
-
|
|
747
|
+
truncatedOwnIds.add(m.id); // #1207 — budget-skip; may still be admitted later via the task-relevant loop (deduped at the end)
|
|
645
748
|
continue;
|
|
646
749
|
}
|
|
647
750
|
sections.recent.push(line);
|
|
648
|
-
includedOwnMemories.push(
|
|
751
|
+
includedOwnMemories.push(struct);
|
|
649
752
|
if (includeTrust)
|
|
650
753
|
includedTrustMemories.push({ m, section: "recent" });
|
|
651
754
|
recentSpent += cost;
|
|
652
755
|
tokenBudget -= cost;
|
|
653
|
-
|
|
756
|
+
includedOwnIds.add(m.id); // #1207 — count by unique own-memory id
|
|
654
757
|
}
|
|
655
758
|
// --- 3b. Subject-predicted context ---
|
|
656
759
|
// When subjects are provided (e.g., ["flair", "auth"]), load memories
|
|
@@ -661,10 +764,10 @@ export class BootstrapMemories extends Resource {
|
|
|
661
764
|
? subjects.map((s) => s.toLowerCase())
|
|
662
765
|
: [];
|
|
663
766
|
if (predictedSubjects.length > 0 && tokenBudget > 200) {
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
767
|
+
// flair#1207 — exclude by the AUTHORITATIVE included-own set (permanent +
|
|
768
|
+
// recent actually admitted), not the old positional `recent.filter(by
|
|
769
|
+
// index)` hack, which mis-tracked when the recent loop skipped an early
|
|
770
|
+
// memory for budget and admitted a later one.
|
|
668
771
|
// Draws from the SAME bounded own-scoped, non-permanent set "recent"
|
|
669
772
|
// uses (nonPermanentActive — see that fetch's doc above for the
|
|
670
773
|
// shared-source rationale and OWN_NONPERMANENT_FETCH_LIMIT's bound).
|
|
@@ -672,7 +775,7 @@ export class BootstrapMemories extends Resource {
|
|
|
672
775
|
// query's own condition but kept for parity/clarity with the
|
|
673
776
|
// pre-refactor filter shape.
|
|
674
777
|
const subjectMemories = nonPermanentActive
|
|
675
|
-
.filter((m) => !
|
|
778
|
+
.filter((m) => !includedOwnIds.has(m.id) &&
|
|
676
779
|
m.subject &&
|
|
677
780
|
predictedSubjects.includes(m.subject.toLowerCase()) &&
|
|
678
781
|
m.durability !== "permanent" // already loaded
|
|
@@ -682,19 +785,19 @@ export class BootstrapMemories extends Resource {
|
|
|
682
785
|
let predictedSpent = 0;
|
|
683
786
|
for (const m of subjectMemories) {
|
|
684
787
|
const line = formatMemory(m, agentId);
|
|
685
|
-
const
|
|
788
|
+
const struct = leanMemory(m, "predicted");
|
|
789
|
+
const cost = contentCost(struct, line); // #1199 (0.44.11) — charge what ships; see contentCost
|
|
686
790
|
if (predictedSpent + cost > predictedBudget) {
|
|
687
|
-
|
|
791
|
+
truncatedOwnIds.add(m.id); // #1207 — budget-skip (deduped against inclusions at the end)
|
|
688
792
|
continue;
|
|
689
793
|
}
|
|
690
794
|
sections.predicted.push(line);
|
|
691
|
-
includedPredicted.push(
|
|
795
|
+
includedPredicted.push(struct);
|
|
692
796
|
if (includeTrust)
|
|
693
797
|
includedTrustMemories.push({ m, section: "predicted" });
|
|
694
798
|
predictedSpent += cost;
|
|
695
799
|
tokenBudget -= cost;
|
|
696
|
-
|
|
697
|
-
includedIds.add(m.id);
|
|
800
|
+
includedOwnIds.add(m.id); // #1207 — count by unique own-memory id; also the task-relevant loop's exclusion set (no predicted→relevant double-admit)
|
|
698
801
|
}
|
|
699
802
|
}
|
|
700
803
|
// --- 3c. Active relationships for predicted subjects ---
|
|
@@ -748,11 +851,13 @@ export class BootstrapMemories extends Resource {
|
|
|
748
851
|
}
|
|
749
852
|
catch { }
|
|
750
853
|
if (queryEmbedding) {
|
|
751
|
-
//
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
854
|
+
// flair#1207 — exclude own memories ALREADY placed via the authoritative
|
|
855
|
+
// set (permanent + recent + predicted actually admitted). The old set was
|
|
856
|
+
// built positionally (recent.filter by index) AND omitted `predicted`, so
|
|
857
|
+
// a predicted memory could be re-admitted here — double-counting it into
|
|
858
|
+
// memoriesIncluded and shipping it twice (once in `predicted`, once in
|
|
859
|
+
// `memories`). Keying off includedOwnIds fixes both.
|
|
860
|
+
const includedIds = includedOwnIds;
|
|
756
861
|
// Bounded HNSW candidate pool (flair-bootstrap-scale-fix) — replaces
|
|
757
862
|
// the full-corpus JS dot-product scan (`allMemories` × queryEmbedding,
|
|
758
863
|
// O(org corpus size) every bootstrap). K formula (Kern-approved):
|
|
@@ -857,17 +962,39 @@ export class BootstrapMemories extends Resource {
|
|
|
857
962
|
// section double-spends.
|
|
858
963
|
for (const { memory: m } of scored) {
|
|
859
964
|
const line = formatMemory(m, agentId);
|
|
860
|
-
|
|
965
|
+
// flair#1199 (0.44.11) — build the STRUCTURED container object BEFORE
|
|
966
|
+
// the budget check so the finding is charged the cost of what actually
|
|
967
|
+
// ships (structured on the /mcp path), not its cheaper prose line. A
|
|
968
|
+
// teammate finding's structured object (id + two ISO timestamps +
|
|
969
|
+
// source + section) runs well over its prose line, and it — not the
|
|
970
|
+
// prose — is what `tokenEstimate` measures on the connector path. This
|
|
971
|
+
// is the fix for the teammate-findings blowout: charging prose but
|
|
972
|
+
// shipping structured let extra findings ride outside the enforced
|
|
973
|
+
// budget (see contentCost). Cross-agent findings (`m._source` set)
|
|
974
|
+
// ship in `teammateFindings`; own findings in `memories`.
|
|
975
|
+
const struct = m._source
|
|
976
|
+
? {
|
|
977
|
+
id: m.id,
|
|
978
|
+
content: m.content,
|
|
979
|
+
durability: m.durability ?? null,
|
|
980
|
+
createdAt: m.createdAt ?? null,
|
|
981
|
+
updatedAt: m.updatedAt ?? null,
|
|
982
|
+
subject: m.subject ?? null,
|
|
983
|
+
source: m._source,
|
|
984
|
+
section: "teammate",
|
|
985
|
+
}
|
|
986
|
+
: leanMemory(m, "relevant");
|
|
987
|
+
const cost = contentCost(struct, line);
|
|
861
988
|
if (cost > tokenBudget) {
|
|
862
989
|
// flair#1207 — a size-skip in the score-ordered task-relevant loop
|
|
863
|
-
// is no longer silent: record it on the
|
|
864
|
-
//
|
|
990
|
+
// is no longer silent: record it on the denominator matching the
|
|
991
|
+
// record's origin (own → truncatedOwnIds, teammate → the separate
|
|
865
992
|
// teammateFindingsTruncated), so a client can distinguish "no relevant
|
|
866
993
|
// finding" from "a relevant finding didn't fit the budget".
|
|
867
994
|
if (m._source)
|
|
868
995
|
teammateFindingsTruncated++;
|
|
869
996
|
else
|
|
870
|
-
|
|
997
|
+
truncatedOwnIds.add(m.id);
|
|
871
998
|
continue;
|
|
872
999
|
}
|
|
873
1000
|
if (m._source) {
|
|
@@ -878,16 +1005,7 @@ export class BootstrapMemories extends Resource {
|
|
|
878
1005
|
// off. Counted separately (teammateFindingsIncluded), NOT into
|
|
879
1006
|
// memoriesIncluded — that different-denominator mix is what let
|
|
880
1007
|
// included exceed available.
|
|
881
|
-
includedTeammateFindings.push(
|
|
882
|
-
id: m.id,
|
|
883
|
-
content: m.content,
|
|
884
|
-
durability: m.durability ?? null,
|
|
885
|
-
createdAt: m.createdAt ?? null,
|
|
886
|
-
updatedAt: m.updatedAt ?? null,
|
|
887
|
-
subject: m.subject ?? null,
|
|
888
|
-
source: m._source,
|
|
889
|
-
section: "teammate",
|
|
890
|
-
});
|
|
1008
|
+
includedTeammateFindings.push(struct);
|
|
891
1009
|
if (includeTrust)
|
|
892
1010
|
includedTrustMemories.push({ m, section: "teammate" });
|
|
893
1011
|
tokenBudget -= cost;
|
|
@@ -897,11 +1015,11 @@ export class BootstrapMemories extends Resource {
|
|
|
897
1015
|
sections.relevant.push(line);
|
|
898
1016
|
// flair#1182 — own task-relevant records join the `memories`
|
|
899
1017
|
// container.
|
|
900
|
-
includedOwnMemories.push(
|
|
1018
|
+
includedOwnMemories.push(struct);
|
|
901
1019
|
if (includeTrust)
|
|
902
1020
|
includedTrustMemories.push({ m, section: "relevant" });
|
|
903
1021
|
tokenBudget -= cost;
|
|
904
|
-
|
|
1022
|
+
includedOwnIds.add(m.id); // #1207 — count by unique own-memory id
|
|
905
1023
|
}
|
|
906
1024
|
}
|
|
907
1025
|
}
|
|
@@ -1049,6 +1167,15 @@ export class BootstrapMemories extends Resource {
|
|
|
1049
1167
|
const isRelevant = !targets || targets.length === 0 || targets.includes(agentId);
|
|
1050
1168
|
if (!isRelevant)
|
|
1051
1169
|
continue;
|
|
1170
|
+
// flair#1200 — suppress zero-row no-op auto-heal migration events at
|
|
1171
|
+
// render. On a healthy store every boot emits a "migration graph-heal
|
|
1172
|
+
// success (0 rows processed)" ledger event beside an "HNSW graph-heal:
|
|
1173
|
+
// recall verified healthy" observability event — near-identical, zero
|
|
1174
|
+
// signal, and (as of #1199) token-charged. Filtered HERE only: the
|
|
1175
|
+
// ledger still records every migration on the table (invariant IV is
|
|
1176
|
+
// untouched — this is a display filter, never a write-path change).
|
|
1177
|
+
if (isZeroRowNoOpEvent(event))
|
|
1178
|
+
continue;
|
|
1052
1179
|
eventResults.push(event);
|
|
1053
1180
|
}
|
|
1054
1181
|
// flair#1200 — collapse byte-identical duplicate events before rendering.
|
|
@@ -1056,10 +1183,10 @@ export class BootstrapMemories extends Resource {
|
|
|
1056
1183
|
// that double-fires, or the same broadcast emitted from two paths); each
|
|
1057
1184
|
// physical row has a distinct id/createdAt (OrgEvent.post keys the id off
|
|
1058
1185
|
// a millisecond timestamp), so they aren't caught by primary-key upsert
|
|
1059
|
-
// and render as exact dupes. Org-event slots are scarce
|
|
1060
|
-
//
|
|
1061
|
-
//
|
|
1062
|
-
//
|
|
1186
|
+
// and render as exact dupes. Org-event slots are scarce, so dedup BEFORE
|
|
1187
|
+
// admission — otherwise ~half the slots are wasted on duplicates. Keyed on
|
|
1188
|
+
// the CONTENT (kind + summary + detail + targets), keeping the most-recent
|
|
1189
|
+
// occurrence per signature.
|
|
1063
1190
|
const eventBySignature = new Map();
|
|
1064
1191
|
for (const evt of eventResults) {
|
|
1065
1192
|
const sig = JSON.stringify([
|
|
@@ -1072,36 +1199,76 @@ export class BootstrapMemories extends Resource {
|
|
|
1072
1199
|
if (!prev || (evt.createdAt || "") > (prev.createdAt || ""))
|
|
1073
1200
|
eventBySignature.set(sig, evt);
|
|
1074
1201
|
}
|
|
1202
|
+
// flair#1199 — admit events in RECENCY order (most recent first, the
|
|
1203
|
+
// score-analogue for events) against the SHARED tokenBudget, capped at
|
|
1204
|
+
// `maxEvents`. Before #1199 the events array was assembled uncounted and
|
|
1205
|
+
// NEVER charged: a maxTokens=4000 request serialized at 6286 (+57%), the
|
|
1206
|
+
// bulk being 10 events each shipping a `detail` JSON. Now each event's
|
|
1207
|
+
// REAL serialized cost (the structured object that actually ships — lean by
|
|
1208
|
+
// default, `detail` only under includeEventDetail) is charged against the
|
|
1209
|
+
// remaining budget, so events respect maxTokens the same way every other
|
|
1210
|
+
// content section does. An event that doesn't fit is skipped, not silently
|
|
1211
|
+
// over-budget; smaller later events may still fit (hence continue, not
|
|
1212
|
+
// break), and admission stops at the maxEvents cap.
|
|
1213
|
+
const eventCap = Number.isFinite(maxEvents) && maxEvents >= 0
|
|
1214
|
+
? Math.floor(maxEvents)
|
|
1215
|
+
: MAX_ORG_EVENTS;
|
|
1075
1216
|
const dedupedEvents = [...eventBySignature.values()]
|
|
1076
|
-
.sort((a, b) => (
|
|
1077
|
-
for (const evt of dedupedEvents
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
//
|
|
1083
|
-
//
|
|
1084
|
-
|
|
1085
|
-
// the `tokenEstimate` charge (this array is always in the body), and the
|
|
1086
|
-
// delivery all key off one thing. Optional fields (detail/targetIds/scope)
|
|
1087
|
-
// are omitted when absent so the object stays lean. The targetIds
|
|
1088
|
-
// relevance filter and #1200 content-signature dedup were already applied
|
|
1089
|
-
// upstream (eventResults → eventBySignature), so this is a pure move from
|
|
1090
|
-
// prose to structured — no scope widening, no re-introduced duplicates.
|
|
1091
|
-
includedEvents.push({
|
|
1217
|
+
.sort((a, b) => (b.createdAt || "").localeCompare(a.createdAt || ""));
|
|
1218
|
+
for (const evt of dedupedEvents) {
|
|
1219
|
+
if (sections.events.length >= eventCap)
|
|
1220
|
+
break;
|
|
1221
|
+
// The structured object a connector actually reads (flair#1206). Lean by
|
|
1222
|
+
// default; `detail` (the verbose migration-internals/summary-restating
|
|
1223
|
+
// JSON) only when explicitly requested. Optional fields are omitted when
|
|
1224
|
+
// absent so the object stays compact.
|
|
1225
|
+
const structured = {
|
|
1092
1226
|
id: evt.id,
|
|
1093
1227
|
kind: evt.kind,
|
|
1094
1228
|
summary: evt.summary,
|
|
1095
|
-
...(evt.detail != null ? { detail: evt.detail } : {}),
|
|
1229
|
+
...(includeEventDetail && evt.detail != null ? { detail: evt.detail } : {}),
|
|
1096
1230
|
...(Array.isArray(evt.targetIds) && evt.targetIds.length > 0 ? { targetIds: evt.targetIds } : {}),
|
|
1097
1231
|
createdAt: evt.createdAt ?? null,
|
|
1098
1232
|
...(evt.scope != null ? { scope: evt.scope } : {}),
|
|
1099
|
-
}
|
|
1233
|
+
};
|
|
1234
|
+
// Charge the REAL serialized cost of what ships (structured object on the
|
|
1235
|
+
// /mcp path; the prose line is a subset of it). This is the #1199 fix:
|
|
1236
|
+
// events are content and must be budgeted like content.
|
|
1237
|
+
const cost = estimateTokens(JSON.stringify(structured));
|
|
1238
|
+
if (cost > tokenBudget)
|
|
1239
|
+
continue;
|
|
1240
|
+
const elapsed = Date.now() - new Date(evt.createdAt).getTime();
|
|
1241
|
+
const mins = Math.floor(elapsed / 60_000);
|
|
1242
|
+
const relTime = mins < 60 ? `${mins}min ago` : `${Math.floor(mins / 60)}h ago`;
|
|
1243
|
+
sections.events.push(`- ${evt.kind}: ${evt.summary} (${relTime})`);
|
|
1244
|
+
// Same admitted event ⇒ `sections.events` (the count), the `tokenEstimate`
|
|
1245
|
+
// charge, and the structured delivery all key off ONE thing.
|
|
1246
|
+
includedEvents.push(structured);
|
|
1247
|
+
tokenBudget -= cost;
|
|
1100
1248
|
}
|
|
1101
1249
|
}
|
|
1102
1250
|
catch {
|
|
1103
1251
|
// non-fatal: OrgEvent table may not exist yet
|
|
1104
1252
|
}
|
|
1253
|
+
// flair#1207 — derive the own-memory counters from the unique-id sets so the
|
|
1254
|
+
// invariant memoriesIncluded + memoriesTruncated <= memoriesAvailable holds.
|
|
1255
|
+
// `truncated` counts only own memories that were budget-skipped AND never
|
|
1256
|
+
// ultimately admitted (a memory skipped in `recent` but later admitted via
|
|
1257
|
+
// the task-relevant loop is INCLUDED, not truncated) — so included/truncated
|
|
1258
|
+
// are disjoint subsets of the own corpus (memoriesAvailable), never
|
|
1259
|
+
// double-counting the same physical memory across sections.
|
|
1260
|
+
memoriesIncluded = includedOwnIds.size;
|
|
1261
|
+
let memoriesTruncatedUnique = 0;
|
|
1262
|
+
for (const id of truncatedOwnIds)
|
|
1263
|
+
if (!includedOwnIds.has(id))
|
|
1264
|
+
memoriesTruncatedUnique++;
|
|
1265
|
+
memoriesTruncated = memoriesTruncatedUnique;
|
|
1266
|
+
// flair#1207 — the teammate MATCH POOL considered (cleared the relevance
|
|
1267
|
+
// floor, drawn from the bounded candidate pool): every matched teammate
|
|
1268
|
+
// finding is EITHER included OR budget-truncated, so this equals their sum.
|
|
1269
|
+
// Reporting it anchors `teammateFindingsTruncated` as "relevant-but-no-budget"
|
|
1270
|
+
// rather than an unexplained large number beside a small `included`.
|
|
1271
|
+
const teammateFindingsMatched = teammateFindingsIncluded + teammateFindingsTruncated;
|
|
1105
1272
|
// --- Build context string ---
|
|
1106
1273
|
const parts = [];
|
|
1107
1274
|
if (sections.soul.length > 0) {
|
|
@@ -1176,9 +1343,30 @@ export class BootstrapMemories extends Resource {
|
|
|
1176
1343
|
// absent ⇒ the response is byte-identical to pre-slice-1. flair#1201 — each
|
|
1177
1344
|
// entry carries its `section` so `matchQuality: null` on a lifecycle section
|
|
1178
1345
|
// reads as "not a retrieval surface", not as a scoring failure on the
|
|
1179
|
-
// caller's own records.
|
|
1346
|
+
// caller's own records. flair#1225 (0.44.11) — the null on a lifecycle
|
|
1347
|
+
// section is now SELF-EXPLAINING (matchQualityNote below), not just legible
|
|
1348
|
+
// via `section`.
|
|
1180
1349
|
const trust = includeTrust
|
|
1181
|
-
? includedTrustMemories.map(({ m, section }) =>
|
|
1350
|
+
? includedTrustMemories.map(({ m, section }) => {
|
|
1351
|
+
const block = buildTrustBlock(m);
|
|
1352
|
+
// flair#1225 — Kern ruled (on #1220) that a null `matchQuality` on an
|
|
1353
|
+
// own-recent (lifecycle) entry is CORRECT: lifecycle sections
|
|
1354
|
+
// (permanent/recent/predicted) are a window LOAD, not a retrieval
|
|
1355
|
+
// surface, so there is no similarity to band — the null means "not
|
|
1356
|
+
// scored here", never a scoring failure on the caller's own records
|
|
1357
|
+
// (the #1201 misread: own-recent null beside a teammate band). Behavior
|
|
1358
|
+
// is unchanged (per Kern); this only makes the null self-describing in
|
|
1359
|
+
// the payload — Fix 3's "any absent field says why" — so a connector
|
|
1360
|
+
// reads it right without knowing the #1201 contract.
|
|
1361
|
+
const matchQualityNote = block.matchQuality === null
|
|
1362
|
+
? (LIFECYCLE_SECTIONS.has(section)
|
|
1363
|
+
? `matchQuality is null because '${section}' is a lifecycle-window section, not a retrieval `
|
|
1364
|
+
+ `surface — there is no relevance score to band. This is correct (per flair#1225), not a scoring failure.`
|
|
1365
|
+
: "matchQuality is null because no semantic similarity was attached to this result "
|
|
1366
|
+
+ "(e.g. a by-id read or a keyword-only degraded match).")
|
|
1367
|
+
: undefined;
|
|
1368
|
+
return { id: m.id, section, ...block, ...(matchQualityNote ? { matchQualityNote } : {}) };
|
|
1369
|
+
})
|
|
1182
1370
|
: undefined;
|
|
1183
1371
|
// flair#744 slice 2 — opt-in abstention verdict for the task-relevance
|
|
1184
1372
|
// surface. Present ONLY when `abstain` is requested (byte-identical to
|
|
@@ -1217,6 +1405,36 @@ export class BootstrapMemories extends Resource {
|
|
|
1217
1405
|
+ `predicted surfaces your own non-permanent memories whose subject matches one of the provided `
|
|
1218
1406
|
+ `subjects — it fills as you store memories tagged with these subjects.`
|
|
1219
1407
|
: undefined;
|
|
1408
|
+
// flair#1182 (0.44.11) — GENERALIZE the empty-container "say why" rule
|
|
1409
|
+
// beyond predictedHint. `events: []` (deliberate no-op filtering) was
|
|
1410
|
+
// byte-indistinguishable from the 0.44.8 silent-drop regression, where a
|
|
1411
|
+
// container that SHOULD have had content shipped empty — a connector could
|
|
1412
|
+
// only tell the difference by diffing against a previous payload. The rule
|
|
1413
|
+
// (now applied consistently across the structured containers): any container
|
|
1414
|
+
// that ships EMPTY carries a short hint naming WHY it is empty and what
|
|
1415
|
+
// fills it, so "deliberately empty" is never confused with "silently
|
|
1416
|
+
// dropped". Present ONLY when the container is empty (a populated container
|
|
1417
|
+
// needs no hint), so a healthy payload is unchanged.
|
|
1418
|
+
// events: [] — no org event in the lookback window was relevant to the
|
|
1419
|
+
// caller after zero-row no-op auto-heal filtering (#1200). Present-but-empty
|
|
1420
|
+
// by design, not a drop.
|
|
1421
|
+
const eventsHint = includedEvents.length === 0
|
|
1422
|
+
? "No org events in the lookback window were relevant to you (org-wide, or targeted at you) "
|
|
1423
|
+
+ "after zero-row no-op auto-heal filtering. This container is present-but-empty by design, not dropped."
|
|
1424
|
+
: undefined;
|
|
1425
|
+
// teammateFindings: [] — name WHICH legitimate empty this is (no task → no
|
|
1426
|
+
// retrieval; matched-but-budget-truncated; or nothing cleared the relevance
|
|
1427
|
+
// floor), so it never reads as a silent drop.
|
|
1428
|
+
const teammateFindingsHint = includedTeammateFindings.length === 0
|
|
1429
|
+
? (!taskProvided
|
|
1430
|
+
? "No teammateFindings: cross-agent findings are retrieved against your currentTask, and none was provided. "
|
|
1431
|
+
+ "Pass currentTask to populate this."
|
|
1432
|
+
: teammateFindingsTruncated > 0
|
|
1433
|
+
? `No teammateFindings fit the token budget: ${teammateFindingsTruncated} relevant cross-agent finding(s) `
|
|
1434
|
+
+ "cleared the relevance floor but were budget-truncated. Raise maxTokens to include them."
|
|
1435
|
+
: "No cross-agent (teammate) memory cleared the task-relevance floor for this currentTask. "
|
|
1436
|
+
+ "This container is present-but-empty by design, not dropped.")
|
|
1437
|
+
: undefined;
|
|
1220
1438
|
const responseBody = {
|
|
1221
1439
|
context,
|
|
1222
1440
|
// flair#1182 (part 1) — always-present self-describing keys: who the
|
|
@@ -1241,6 +1459,11 @@ export class BootstrapMemories extends Resource {
|
|
|
1241
1459
|
events: includedEvents,
|
|
1242
1460
|
...(currentTaskHint ? { currentTaskHint } : {}),
|
|
1243
1461
|
...(predictedHint ? { predictedHint } : {}),
|
|
1462
|
+
// flair#1182 (0.44.11) — empty-container hints, present only when the
|
|
1463
|
+
// container ships empty (see above), so a deliberately-empty container is
|
|
1464
|
+
// never confused with a silent drop.
|
|
1465
|
+
...(eventsHint ? { eventsHint } : {}),
|
|
1466
|
+
...(teammateFindingsHint ? { teammateFindingsHint } : {}),
|
|
1244
1467
|
...(trust ? { trust } : {}),
|
|
1245
1468
|
...(abstention ? { abstention } : {}),
|
|
1246
1469
|
sections: {
|
|
@@ -1258,8 +1481,14 @@ export class BootstrapMemories extends Resource {
|
|
|
1258
1481
|
},
|
|
1259
1482
|
soulTokens,
|
|
1260
1483
|
memoryTokens,
|
|
1484
|
+
// flair#1199 — the content-selection budget this response was built
|
|
1485
|
+
// against (echoed so a connector can relate tokenEstimate to the budget it
|
|
1486
|
+
// asked for — and so the conformance tokenEstimate<=maxTokens invariant is
|
|
1487
|
+
// self-contained). Defaults to 4000 when unset.
|
|
1488
|
+
maxTokens,
|
|
1261
1489
|
// flair#1199 — own memories included (denominator: memoriesAvailable, also
|
|
1262
|
-
// own-scoped)
|
|
1490
|
+
// own-scoped). flair#1207 — DERIVED from unique own-memory ids, so
|
|
1491
|
+
// memoriesIncluded + memoriesTruncated <= memoriesAvailable always holds.
|
|
1263
1492
|
memoriesIncluded,
|
|
1264
1493
|
memoriesAvailable,
|
|
1265
1494
|
// Cross-agent teammate findings included — a SEPARATE denominator, labelled
|
|
@@ -1267,24 +1496,39 @@ export class BootstrapMemories extends Resource {
|
|
|
1267
1496
|
teammateFindingsIncluded,
|
|
1268
1497
|
memoriesTruncated,
|
|
1269
1498
|
// flair#1207 — teammate findings skipped for size in the task-relevant loop
|
|
1270
|
-
// (own size-skips there
|
|
1271
|
-
//
|
|
1272
|
-
//
|
|
1499
|
+
// (own size-skips there feed truncatedOwnIds). Surfacing this makes a
|
|
1500
|
+
// size-skip self-describing: "a relevant teammate finding didn't fit" is
|
|
1501
|
+
// now distinguishable from "no relevant teammate finding".
|
|
1273
1502
|
teammateFindingsTruncated,
|
|
1503
|
+
// flair#1207 — the teammate match POOL considered (cleared the relevance
|
|
1504
|
+
// floor). teammateFindingsIncluded + teammateFindingsTruncated ==
|
|
1505
|
+
// teammateFindingsMatched, so "truncated" reads as "relevant-but-no-budget"
|
|
1506
|
+
// against a stated pool, not an unanchored large number.
|
|
1507
|
+
teammateFindingsMatched,
|
|
1274
1508
|
};
|
|
1275
1509
|
// flair#1199 — tokenEstimate must reflect the ACTUAL serialized payload the
|
|
1276
1510
|
// caller receives (the structured containers included), not just the prose
|
|
1277
1511
|
// `context`. The old `soulTokens + memoryTokens` counted only the context
|
|
1278
1512
|
// string, so it under-reported by ~2× once the structured fields shipped
|
|
1279
1513
|
// alongside. Measured over the assembled body (the ~1-line tokenEstimate
|
|
1280
|
-
// field it omits is negligible).
|
|
1281
|
-
//
|
|
1282
|
-
//
|
|
1283
|
-
// the
|
|
1284
|
-
//
|
|
1285
|
-
//
|
|
1286
|
-
//
|
|
1287
|
-
//
|
|
1514
|
+
// field it omits is negligible). CAP CONTRACT: this is an HONEST report of
|
|
1515
|
+
// the real serialized size. Every CONTENT section — soul, memories, findings,
|
|
1516
|
+
// AND events (flair#1199) — is now gated against the shared `maxTokens`
|
|
1517
|
+
// budget, so no section blows the budget with uncounted content the way the
|
|
1518
|
+
// 0.44.9 events array did (maxTokens=4000 → 6286). flair#1199 (0.44.11): on
|
|
1519
|
+
// the /mcp connector path each content item is charged its STRUCTURED shipped
|
|
1520
|
+
// cost — the same bytes tokenEstimate measures — so tokenEstimate exceeds
|
|
1521
|
+
// `maxTokens` only by the FIXED structural JSON scaffolding (container
|
|
1522
|
+
// keys/braces, counters, sections map, hints, char/4 rounding): genuine
|
|
1523
|
+
// payload the caller pays for, small and bounded, NOT uncounted content. The
|
|
1524
|
+
// connector-conformance suite asserts tokenEstimate <= maxTokens within a
|
|
1525
|
+
// small tolerance for exactly that scaffolding. On the PROSE path
|
|
1526
|
+
// (includeContext=true) the payload ALSO carries the structured mirror, so
|
|
1527
|
+
// tokenEstimate legitimately exceeds `maxTokens` by that mirror — do NOT
|
|
1528
|
+
// "fix" THAT overrun by shrinking prose selection: that is the #1199→#1207
|
|
1529
|
+
// regression (it dropped relevant findings, charging a flat per-item overhead
|
|
1530
|
+
// against the content budget). If the real payload consistently overruns for
|
|
1531
|
+
// a use case, raise `maxTokens`.
|
|
1288
1532
|
const tokenEstimate = estimateTokens(JSON.stringify(responseBody));
|
|
1289
1533
|
return { ...responseBody, tokenEstimate };
|
|
1290
1534
|
}
|