@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.
@@ -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, agentId, scope, soul,
77
- * memories, predicted, teammateFindings, events[, currentTaskHint][, predictedHint] }
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
- * flair#1199 CAP CONTRACT (corrected): `maxTokens` is the HARD cap on CONTENT
94
- * SELECTION — the shared `tokenBudget` starts at `maxTokens` and every admitted
95
- * soul/memory/finding line is gated against the remaining budget, so the sum of
96
- * selected CONTENT never exceeds `maxTokens`. `tokenEstimate` HONESTLY reports
97
- * the real serialized payload (`JSON.stringify(responseBody)`), which includes
98
- * the structured-container JSON scaffolding and so MAY exceed `maxTokens` by
99
- * that overhead measurement (honest reporting) is deliberately decoupled from
100
- * budgeting (what to select). This is the flair#1207 fix: #1199 had folded a
101
- * per-item structured overhead + a scaffolding reserve INTO the selection
102
- * budget, which silently shrank recall below 0.44.6 for the same `maxTokens`;
103
- * the overhead is a reporting concern (already captured by `tokenEstimate`),
104
- * never a selection constraint, so it no longer shrinks the content budget.
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
- // Rough token estimate: ~4 chars per token for English text
156
- function estimateTokens(text) {
157
- return Math.ceil(text.length / 4);
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. That loop `continue`s past an over-budget record silently;
272
- // without a counter, a client can't tell "no relevant teammate finding" from
273
- // "a relevant one existed but didn't fit the budget" (Sherlock's #1207
274
- // self-describing-size-skip note; own-memory size-skips already increment
275
- // `memoriesTruncated`, but that loop never did now both do).
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 cost = estimateTokens(line); // #1207 — prose-line cost only; overhead is a reporting concern (tokenEstimate), not a selection constraint
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(leanMemory(m, "permanent"));
675
+ includedOwnMemories.push(struct);
574
676
  if (includeTrust)
575
677
  includedTrustMemories.push({ m, section: "permanent" });
576
678
  tokenBudget -= cost;
577
- memoriesIncluded++;
679
+ includedOwnIds.add(m.id); // #1207 — count by unique own-memory id
578
680
  }
579
681
  else {
580
- memoriesTruncated++;
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 cost = estimateTokens(line); // #1207 — prose-line cost only; overhead is a reporting concern (tokenEstimate), not a selection constraint
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
- memoriesTruncated++;
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(leanMemory(m, "recent"));
751
+ includedOwnMemories.push(struct);
649
752
  if (includeTrust)
650
753
  includedTrustMemories.push({ m, section: "recent" });
651
754
  recentSpent += cost;
652
755
  tokenBudget -= cost;
653
- memoriesIncluded++;
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
- const includedIds = new Set([
665
- ...permanent.map((m) => m.id),
666
- ...recent.filter((_, i) => i < sections.recent.length).map((m) => m.id),
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) => !includedIds.has(m.id) &&
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 cost = estimateTokens(line); // #1207 — prose-line cost only; overhead is a reporting concern (tokenEstimate), not a selection constraint
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
- memoriesTruncated++;
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(leanMemory(m, "predicted"));
795
+ includedPredicted.push(struct);
692
796
  if (includeTrust)
693
797
  includedTrustMemories.push({ m, section: "predicted" });
694
798
  predictedSpent += cost;
695
799
  tokenBudget -= cost;
696
- memoriesIncluded++;
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
- // Score all non-included memories by relevance
752
- const includedIds = new Set([
753
- ...permanent.map((m) => m.id),
754
- ...recent.filter((_, i) => i < sections.recent.length).map((m) => m.id),
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
- const cost = estimateTokens(line); // #1207 prose-line cost only; overhead is a reporting concern (tokenEstimate), not a selection constraint
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 counter matching the record's
864
- // denominator (own → memoriesTruncated, teammate → the separate
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
- memoriesTruncated++;
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(leanMemory(m, "relevant"));
1018
+ includedOwnMemories.push(struct);
901
1019
  if (includeTrust)
902
1020
  includedTrustMemories.push({ m, section: "relevant" });
903
1021
  tokenBudget -= cost;
904
- memoriesIncluded++;
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 (10), so dedup
1060
- // BEFORE the slice — otherwise ~half the slots are wasted on duplicates.
1061
- // Keyed on the CONTENT (kind + summary + detail + targets), keeping the
1062
- // most-recent occurrence per signature.
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) => (a.createdAt || "").localeCompare(b.createdAt || ""));
1077
- for (const evt of dedupedEvents.slice(0, 10)) {
1078
- const elapsed = Date.now() - new Date(evt.createdAt).getTime();
1079
- const mins = Math.floor(elapsed / 60_000);
1080
- const relTime = mins < 60 ? `${mins}min ago` : `${Math.floor(mins / 60)}h ago`;
1081
- sections.events.push(`- ${evt.kind}: ${evt.summary} (${relTime})`);
1082
- // flair#1206 the SAME deduped+sliced event, structured, so a connector
1083
- // reading the containers gets it when prose `context` is off (the /mcp
1084
- // default). Same set as the prose line ⇒ `sections.events` (the count),
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 }) => ({ id: m.id, section, ...buildTrustBlock(m) }))
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), so memoriesIncluded memoriesAvailable always holds.
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 increment memoriesTruncated). Surfacing this makes
1271
- // a size-skip self-describing: "a relevant teammate finding didn't fit"
1272
- // is now distinguishable from "no relevant teammate finding".
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). flair#1207 CAP CONTRACT: this is an HONEST
1281
- // report of the real serialized size, NOT a value bounded by `maxTokens`.
1282
- // `maxTokens` is the hard cap on CONTENT SELECTION (the shared tokenBudget);
1283
- // the structured-container JSON scaffolding is genuine payload the caller
1284
- // pays for, so tokenEstimate MAY exceed `maxTokens` by that overhead. Do not
1285
- // "fix" an over-maxTokens tokenEstimate by shrinking selection that is the
1286
- // exact #1199→#1207 regression (it dropped relevant findings). If the real
1287
- // payload consistently overruns for a use case, raise `maxTokens`.
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
  }