@tpsdev-ai/flair 0.44.9 → 0.44.10
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 +188 -2
- package/dist/rem/runner.js +167 -23
- package/dist/resources/MemoryBootstrap.js +197 -80
- package/dist/resources/MemoryReflect.js +15 -13
- 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/rem.md +6 -0
- 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,42 @@ 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
|
+
* line AND every org event (flair#1199 — events are content too; before this
|
|
98
|
+
* they were assembled but NEVER charged, so a maxTokens=4000 request serialized
|
|
99
|
+
* at 6286) is gated against the remaining budget, so the sum of selected CONTENT
|
|
100
|
+
* never exceeds `maxTokens`. `tokenEstimate` HONESTLY reports the real serialized
|
|
101
|
+
* payload (`JSON.stringify(responseBody)`), which includes the FIXED structured-
|
|
102
|
+
* container JSON scaffolding (keys/braces, counters, the sections map) and so may
|
|
103
|
+
* exceed `maxTokens` slightly by that bounded overhead — measurement is decoupled
|
|
104
|
+
* from budgeting, but the overhead is now scaffolding ONLY, never uncounted
|
|
105
|
+
* content. The connector-conformance suite asserts tokenEstimate <= maxTokens
|
|
106
|
+
* within a small tolerance for that scaffolding. flair#1207: #1199 had ALSO
|
|
107
|
+
* folded a per-item structured overhead + a scaffolding reserve INTO the
|
|
108
|
+
* selection budget, which silently shrank recall below 0.44.6 for the same
|
|
109
|
+
* `maxTokens`; that per-item overhead is a reporting concern (already captured by
|
|
110
|
+
* `tokenEstimate`) and no longer shrinks the content budget.
|
|
111
|
+
*
|
|
112
|
+
* COUNT CONTRACT (flair#1207): `memoriesIncluded + memoriesTruncated <=
|
|
113
|
+
* memoriesAvailable` — included and truncated are disjoint sets of UNIQUE own
|
|
114
|
+
* memories (a memory budget-skipped in one section but admitted in another counts
|
|
115
|
+
* as included, never both). `teammateFindingsMatched` is the teammate match pool
|
|
116
|
+
* that cleared the relevance floor; `teammateFindingsIncluded +
|
|
117
|
+
* teammateFindingsTruncated == teammateFindingsMatched`, so "truncated" means
|
|
118
|
+
* "relevant but no budget," not "every candidate not selected."
|
|
105
119
|
* `predictedHint` is present only when subjects were provided but `predicted`
|
|
106
120
|
* came back empty.
|
|
107
121
|
*/
|
|
108
122
|
// Collision surfacing (flair#681) tunables.
|
|
109
123
|
const COLLISION_WINDOW_DAYS = 7;
|
|
110
124
|
const MAX_COLLISION_ENTRIES = 10;
|
|
125
|
+
// flair#1199/#1206 — the default cap on how many org events bootstrap ships.
|
|
126
|
+
// Overridable per-request via `maxEvents`. Event slots are scarce AND (as of
|
|
127
|
+
// #1199) token-charged, so this bounds both the count and the spend; the shared
|
|
128
|
+
// tokenBudget is the harder ceiling (an event that doesn't fit is skipped even
|
|
129
|
+
// under the cap).
|
|
130
|
+
const MAX_ORG_EVENTS = 10;
|
|
111
131
|
// ─── Bootstrap scale fix (flair-bootstrap-scale-fix) tunables ───────────────
|
|
112
132
|
//
|
|
113
133
|
// Own-scoped, non-permanent memories (the "recent" adaptive-window source,
|
|
@@ -152,10 +172,10 @@ const TASK_RELEVANCE_FLOOR = 0.3;
|
|
|
152
172
|
// line — while `tokenEstimate` keeps reporting the true serialized size, which
|
|
153
173
|
// may exceed `maxTokens` by the scaffolding overhead. See the module-doc CAP
|
|
154
174
|
// CONTRACT above.
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
175
|
+
// Token estimate (~4 chars per token for English text) now lives in the
|
|
176
|
+
// harper-free ./token-estimate.js module, so the content-selection budget, the
|
|
177
|
+
// reported `tokenEstimate`, and the flair#1213 conformance tokenEstimate
|
|
178
|
+
// invariant are all computed with ONE definition. See that module's header.
|
|
159
179
|
// `agentId` is the BOOTSTRAPPING agent (the caller) — used only to decide
|
|
160
180
|
// whether to annotate attribution, never to change what's read (that
|
|
161
181
|
// boundary is resolveReadScope()'s job, upstream of this function). A
|
|
@@ -193,6 +213,15 @@ export class BootstrapMemories extends Resource {
|
|
|
193
213
|
subjects, // e.g., ["flair", "auth"] — entities to preload context for
|
|
194
214
|
includeTrust = false, // flair#744 slice 1 — opt-in per-memory trust block
|
|
195
215
|
abstain = false, // flair#744 slice 2 — opt-in task-relevance abstention
|
|
216
|
+
// flair#1199 — org-event knobs. `maxEvents` caps how many events ship
|
|
217
|
+
// (default MAX_ORG_EVENTS); `includeEventDetail` gates the verbose per-event
|
|
218
|
+
// `detail` JSON (default OFF — mirrors the includeContext opt-in). By
|
|
219
|
+
// default bootstrap ships LEAN events (id/kind/summary/createdAt/targetIds/
|
|
220
|
+
// scope); `detail` restates the summary + migration internals and is pure
|
|
221
|
+
// bloat for a connector, so it is opt-in. Both are also counted against the
|
|
222
|
+
// shared tokenBudget below (before #1199 the events array was assembled but
|
|
223
|
+
// NEVER charged, so a maxTokens=4000 request serialized well past budget).
|
|
224
|
+
maxEvents, includeEventDetail = false,
|
|
196
225
|
// flair#1199 — whether to assemble the prose `context` string. The
|
|
197
226
|
// structured containers (soul/memories/predicted/teammateFindings) are the
|
|
198
227
|
// CANONICAL payload; `context` is a human/agent-readable MIRROR of the same
|
|
@@ -263,16 +292,37 @@ export class BootstrapMemories extends Resource {
|
|
|
263
292
|
let memoriesIncluded = 0;
|
|
264
293
|
let memoriesAvailable = 0;
|
|
265
294
|
let memoriesTruncated = 0;
|
|
295
|
+
// flair#1207 — count arithmetic by UNIQUE own-memory id, not raw increments.
|
|
296
|
+
// The invariant `memoriesIncluded + memoriesTruncated <= memoriesAvailable`
|
|
297
|
+
// MUST hold (0.44.9 reported available:3 included:2 truncated:2 — 2+2 > 3).
|
|
298
|
+
// Two bugs produced the over-count, both fixed by keying off these sets:
|
|
299
|
+
// (1) a memory truncated for budget in an EARLY section (e.g. recent's
|
|
300
|
+
// 40% sub-budget) reappears in the task-relevant candidate pool and is
|
|
301
|
+
// counted AGAIN (truncated twice, or truncated-then-included) — the
|
|
302
|
+
// same physical memory in two denominators; and
|
|
303
|
+
// (2) the task-relevant loop's "already included" exclusion set was built
|
|
304
|
+
// POSITIONALLY (recent.filter by index) and omitted `predicted`, so a
|
|
305
|
+
// predicted memory could be re-admitted to `relevant`.
|
|
306
|
+
// `includedOwnIds` is now THE authoritative "own memory already placed" set
|
|
307
|
+
// (used as the exclusion set in the predicted + task-relevant loops, replacing
|
|
308
|
+
// the positional hack); `truncatedOwnIds` records own budget-skips. Final
|
|
309
|
+
// counts are DERIVED from these sets (truncated = truncated-but-not-included),
|
|
310
|
+
// so both are disjoint subsets of memoriesAvailable and the invariant holds.
|
|
311
|
+
const includedOwnIds = new Set();
|
|
312
|
+
const truncatedOwnIds = new Set();
|
|
266
313
|
// flair#1199 — cross-agent teammate findings included (a DIFFERENT
|
|
267
314
|
// denominator than own memories): counting these into `memoriesIncluded`
|
|
268
315
|
// is what let one client see included(9) > available(3).
|
|
269
316
|
let teammateFindingsIncluded = 0;
|
|
270
317
|
// flair#1207 — teammate findings SKIPPED for size in the task-relevant
|
|
271
|
-
// packing loop
|
|
272
|
-
//
|
|
273
|
-
// "
|
|
274
|
-
//
|
|
275
|
-
// `
|
|
318
|
+
// packing loop (cleared the relevance floor but didn't fit the BUDGET).
|
|
319
|
+
// Reported ALONGSIDE `teammateFindingsMatched` (the whole floor-clearing pool
|
|
320
|
+
// considered), so "truncated" unambiguously means "relevant-but-no-budget",
|
|
321
|
+
// NOT "every candidate not selected" — 0.44.9's `truncated:89` beside
|
|
322
|
+
// `included:4` read as "89 relevant findings cut" with no pool to anchor it
|
|
323
|
+
// (heskew's #1207 nit). Both are disjoint-and-exhaustive over the matched
|
|
324
|
+
// pool (each matched teammate finding is EITHER included OR truncated), so
|
|
325
|
+
// teammateFindingsIncluded + teammateFindingsTruncated == teammateFindingsMatched.
|
|
276
326
|
let teammateFindingsTruncated = 0;
|
|
277
327
|
// flair#1182 (part 1) — self-describing bootstrap. These structured
|
|
278
328
|
// container keys are ALWAYS emitted on the response (empty `{}`/`[]` when
|
|
@@ -574,10 +624,10 @@ export class BootstrapMemories extends Resource {
|
|
|
574
624
|
if (includeTrust)
|
|
575
625
|
includedTrustMemories.push({ m, section: "permanent" });
|
|
576
626
|
tokenBudget -= cost;
|
|
577
|
-
|
|
627
|
+
includedOwnIds.add(m.id); // #1207 — count by unique own-memory id
|
|
578
628
|
}
|
|
579
629
|
else {
|
|
580
|
-
|
|
630
|
+
truncatedOwnIds.add(m.id); // #1207 — budget-skip, deduped against inclusions at the end
|
|
581
631
|
}
|
|
582
632
|
}
|
|
583
633
|
// --- 3. Recent memories (adaptive window) ---
|
|
@@ -641,7 +691,7 @@ export class BootstrapMemories extends Resource {
|
|
|
641
691
|
const line = formatMemory(m, agentId);
|
|
642
692
|
const cost = estimateTokens(line); // #1207 — prose-line cost only; overhead is a reporting concern (tokenEstimate), not a selection constraint
|
|
643
693
|
if (recentSpent + cost > recentBudget) {
|
|
644
|
-
|
|
694
|
+
truncatedOwnIds.add(m.id); // #1207 — budget-skip; may still be admitted later via the task-relevant loop (deduped at the end)
|
|
645
695
|
continue;
|
|
646
696
|
}
|
|
647
697
|
sections.recent.push(line);
|
|
@@ -650,7 +700,7 @@ export class BootstrapMemories extends Resource {
|
|
|
650
700
|
includedTrustMemories.push({ m, section: "recent" });
|
|
651
701
|
recentSpent += cost;
|
|
652
702
|
tokenBudget -= cost;
|
|
653
|
-
|
|
703
|
+
includedOwnIds.add(m.id); // #1207 — count by unique own-memory id
|
|
654
704
|
}
|
|
655
705
|
// --- 3b. Subject-predicted context ---
|
|
656
706
|
// When subjects are provided (e.g., ["flair", "auth"]), load memories
|
|
@@ -661,10 +711,10 @@ export class BootstrapMemories extends Resource {
|
|
|
661
711
|
? subjects.map((s) => s.toLowerCase())
|
|
662
712
|
: [];
|
|
663
713
|
if (predictedSubjects.length > 0 && tokenBudget > 200) {
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
714
|
+
// flair#1207 — exclude by the AUTHORITATIVE included-own set (permanent +
|
|
715
|
+
// recent actually admitted), not the old positional `recent.filter(by
|
|
716
|
+
// index)` hack, which mis-tracked when the recent loop skipped an early
|
|
717
|
+
// memory for budget and admitted a later one.
|
|
668
718
|
// Draws from the SAME bounded own-scoped, non-permanent set "recent"
|
|
669
719
|
// uses (nonPermanentActive — see that fetch's doc above for the
|
|
670
720
|
// shared-source rationale and OWN_NONPERMANENT_FETCH_LIMIT's bound).
|
|
@@ -672,7 +722,7 @@ export class BootstrapMemories extends Resource {
|
|
|
672
722
|
// query's own condition but kept for parity/clarity with the
|
|
673
723
|
// pre-refactor filter shape.
|
|
674
724
|
const subjectMemories = nonPermanentActive
|
|
675
|
-
.filter((m) => !
|
|
725
|
+
.filter((m) => !includedOwnIds.has(m.id) &&
|
|
676
726
|
m.subject &&
|
|
677
727
|
predictedSubjects.includes(m.subject.toLowerCase()) &&
|
|
678
728
|
m.durability !== "permanent" // already loaded
|
|
@@ -684,7 +734,7 @@ export class BootstrapMemories extends Resource {
|
|
|
684
734
|
const line = formatMemory(m, agentId);
|
|
685
735
|
const cost = estimateTokens(line); // #1207 — prose-line cost only; overhead is a reporting concern (tokenEstimate), not a selection constraint
|
|
686
736
|
if (predictedSpent + cost > predictedBudget) {
|
|
687
|
-
|
|
737
|
+
truncatedOwnIds.add(m.id); // #1207 — budget-skip (deduped against inclusions at the end)
|
|
688
738
|
continue;
|
|
689
739
|
}
|
|
690
740
|
sections.predicted.push(line);
|
|
@@ -693,8 +743,7 @@ export class BootstrapMemories extends Resource {
|
|
|
693
743
|
includedTrustMemories.push({ m, section: "predicted" });
|
|
694
744
|
predictedSpent += cost;
|
|
695
745
|
tokenBudget -= cost;
|
|
696
|
-
|
|
697
|
-
includedIds.add(m.id);
|
|
746
|
+
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
747
|
}
|
|
699
748
|
}
|
|
700
749
|
// --- 3c. Active relationships for predicted subjects ---
|
|
@@ -748,11 +797,13 @@ export class BootstrapMemories extends Resource {
|
|
|
748
797
|
}
|
|
749
798
|
catch { }
|
|
750
799
|
if (queryEmbedding) {
|
|
751
|
-
//
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
800
|
+
// flair#1207 — exclude own memories ALREADY placed via the authoritative
|
|
801
|
+
// set (permanent + recent + predicted actually admitted). The old set was
|
|
802
|
+
// built positionally (recent.filter by index) AND omitted `predicted`, so
|
|
803
|
+
// a predicted memory could be re-admitted here — double-counting it into
|
|
804
|
+
// memoriesIncluded and shipping it twice (once in `predicted`, once in
|
|
805
|
+
// `memories`). Keying off includedOwnIds fixes both.
|
|
806
|
+
const includedIds = includedOwnIds;
|
|
756
807
|
// Bounded HNSW candidate pool (flair-bootstrap-scale-fix) — replaces
|
|
757
808
|
// the full-corpus JS dot-product scan (`allMemories` × queryEmbedding,
|
|
758
809
|
// O(org corpus size) every bootstrap). K formula (Kern-approved):
|
|
@@ -860,14 +911,14 @@ export class BootstrapMemories extends Resource {
|
|
|
860
911
|
const cost = estimateTokens(line); // #1207 — prose-line cost only; overhead is a reporting concern (tokenEstimate), not a selection constraint
|
|
861
912
|
if (cost > tokenBudget) {
|
|
862
913
|
// flair#1207 — a size-skip in the score-ordered task-relevant loop
|
|
863
|
-
// is no longer silent: record it on the
|
|
864
|
-
//
|
|
914
|
+
// is no longer silent: record it on the denominator matching the
|
|
915
|
+
// record's origin (own → truncatedOwnIds, teammate → the separate
|
|
865
916
|
// teammateFindingsTruncated), so a client can distinguish "no relevant
|
|
866
917
|
// finding" from "a relevant finding didn't fit the budget".
|
|
867
918
|
if (m._source)
|
|
868
919
|
teammateFindingsTruncated++;
|
|
869
920
|
else
|
|
870
|
-
|
|
921
|
+
truncatedOwnIds.add(m.id);
|
|
871
922
|
continue;
|
|
872
923
|
}
|
|
873
924
|
if (m._source) {
|
|
@@ -901,7 +952,7 @@ export class BootstrapMemories extends Resource {
|
|
|
901
952
|
if (includeTrust)
|
|
902
953
|
includedTrustMemories.push({ m, section: "relevant" });
|
|
903
954
|
tokenBudget -= cost;
|
|
904
|
-
|
|
955
|
+
includedOwnIds.add(m.id); // #1207 — count by unique own-memory id
|
|
905
956
|
}
|
|
906
957
|
}
|
|
907
958
|
}
|
|
@@ -1049,6 +1100,15 @@ export class BootstrapMemories extends Resource {
|
|
|
1049
1100
|
const isRelevant = !targets || targets.length === 0 || targets.includes(agentId);
|
|
1050
1101
|
if (!isRelevant)
|
|
1051
1102
|
continue;
|
|
1103
|
+
// flair#1200 — suppress zero-row no-op auto-heal migration events at
|
|
1104
|
+
// render. On a healthy store every boot emits a "migration graph-heal
|
|
1105
|
+
// success (0 rows processed)" ledger event beside an "HNSW graph-heal:
|
|
1106
|
+
// recall verified healthy" observability event — near-identical, zero
|
|
1107
|
+
// signal, and (as of #1199) token-charged. Filtered HERE only: the
|
|
1108
|
+
// ledger still records every migration on the table (invariant IV is
|
|
1109
|
+
// untouched — this is a display filter, never a write-path change).
|
|
1110
|
+
if (isZeroRowNoOpEvent(event))
|
|
1111
|
+
continue;
|
|
1052
1112
|
eventResults.push(event);
|
|
1053
1113
|
}
|
|
1054
1114
|
// flair#1200 — collapse byte-identical duplicate events before rendering.
|
|
@@ -1056,10 +1116,10 @@ export class BootstrapMemories extends Resource {
|
|
|
1056
1116
|
// that double-fires, or the same broadcast emitted from two paths); each
|
|
1057
1117
|
// physical row has a distinct id/createdAt (OrgEvent.post keys the id off
|
|
1058
1118
|
// 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
|
-
//
|
|
1119
|
+
// and render as exact dupes. Org-event slots are scarce, so dedup BEFORE
|
|
1120
|
+
// admission — otherwise ~half the slots are wasted on duplicates. Keyed on
|
|
1121
|
+
// the CONTENT (kind + summary + detail + targets), keeping the most-recent
|
|
1122
|
+
// occurrence per signature.
|
|
1063
1123
|
const eventBySignature = new Map();
|
|
1064
1124
|
for (const evt of eventResults) {
|
|
1065
1125
|
const sig = JSON.stringify([
|
|
@@ -1072,36 +1132,76 @@ export class BootstrapMemories extends Resource {
|
|
|
1072
1132
|
if (!prev || (evt.createdAt || "") > (prev.createdAt || ""))
|
|
1073
1133
|
eventBySignature.set(sig, evt);
|
|
1074
1134
|
}
|
|
1135
|
+
// flair#1199 — admit events in RECENCY order (most recent first, the
|
|
1136
|
+
// score-analogue for events) against the SHARED tokenBudget, capped at
|
|
1137
|
+
// `maxEvents`. Before #1199 the events array was assembled uncounted and
|
|
1138
|
+
// NEVER charged: a maxTokens=4000 request serialized at 6286 (+57%), the
|
|
1139
|
+
// bulk being 10 events each shipping a `detail` JSON. Now each event's
|
|
1140
|
+
// REAL serialized cost (the structured object that actually ships — lean by
|
|
1141
|
+
// default, `detail` only under includeEventDetail) is charged against the
|
|
1142
|
+
// remaining budget, so events respect maxTokens the same way every other
|
|
1143
|
+
// content section does. An event that doesn't fit is skipped, not silently
|
|
1144
|
+
// over-budget; smaller later events may still fit (hence continue, not
|
|
1145
|
+
// break), and admission stops at the maxEvents cap.
|
|
1146
|
+
const eventCap = Number.isFinite(maxEvents) && maxEvents >= 0
|
|
1147
|
+
? Math.floor(maxEvents)
|
|
1148
|
+
: MAX_ORG_EVENTS;
|
|
1075
1149
|
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({
|
|
1150
|
+
.sort((a, b) => (b.createdAt || "").localeCompare(a.createdAt || ""));
|
|
1151
|
+
for (const evt of dedupedEvents) {
|
|
1152
|
+
if (sections.events.length >= eventCap)
|
|
1153
|
+
break;
|
|
1154
|
+
// The structured object a connector actually reads (flair#1206). Lean by
|
|
1155
|
+
// default; `detail` (the verbose migration-internals/summary-restating
|
|
1156
|
+
// JSON) only when explicitly requested. Optional fields are omitted when
|
|
1157
|
+
// absent so the object stays compact.
|
|
1158
|
+
const structured = {
|
|
1092
1159
|
id: evt.id,
|
|
1093
1160
|
kind: evt.kind,
|
|
1094
1161
|
summary: evt.summary,
|
|
1095
|
-
...(evt.detail != null ? { detail: evt.detail } : {}),
|
|
1162
|
+
...(includeEventDetail && evt.detail != null ? { detail: evt.detail } : {}),
|
|
1096
1163
|
...(Array.isArray(evt.targetIds) && evt.targetIds.length > 0 ? { targetIds: evt.targetIds } : {}),
|
|
1097
1164
|
createdAt: evt.createdAt ?? null,
|
|
1098
1165
|
...(evt.scope != null ? { scope: evt.scope } : {}),
|
|
1099
|
-
}
|
|
1166
|
+
};
|
|
1167
|
+
// Charge the REAL serialized cost of what ships (structured object on the
|
|
1168
|
+
// /mcp path; the prose line is a subset of it). This is the #1199 fix:
|
|
1169
|
+
// events are content and must be budgeted like content.
|
|
1170
|
+
const cost = estimateTokens(JSON.stringify(structured));
|
|
1171
|
+
if (cost > tokenBudget)
|
|
1172
|
+
continue;
|
|
1173
|
+
const elapsed = Date.now() - new Date(evt.createdAt).getTime();
|
|
1174
|
+
const mins = Math.floor(elapsed / 60_000);
|
|
1175
|
+
const relTime = mins < 60 ? `${mins}min ago` : `${Math.floor(mins / 60)}h ago`;
|
|
1176
|
+
sections.events.push(`- ${evt.kind}: ${evt.summary} (${relTime})`);
|
|
1177
|
+
// Same admitted event ⇒ `sections.events` (the count), the `tokenEstimate`
|
|
1178
|
+
// charge, and the structured delivery all key off ONE thing.
|
|
1179
|
+
includedEvents.push(structured);
|
|
1180
|
+
tokenBudget -= cost;
|
|
1100
1181
|
}
|
|
1101
1182
|
}
|
|
1102
1183
|
catch {
|
|
1103
1184
|
// non-fatal: OrgEvent table may not exist yet
|
|
1104
1185
|
}
|
|
1186
|
+
// flair#1207 — derive the own-memory counters from the unique-id sets so the
|
|
1187
|
+
// invariant memoriesIncluded + memoriesTruncated <= memoriesAvailable holds.
|
|
1188
|
+
// `truncated` counts only own memories that were budget-skipped AND never
|
|
1189
|
+
// ultimately admitted (a memory skipped in `recent` but later admitted via
|
|
1190
|
+
// the task-relevant loop is INCLUDED, not truncated) — so included/truncated
|
|
1191
|
+
// are disjoint subsets of the own corpus (memoriesAvailable), never
|
|
1192
|
+
// double-counting the same physical memory across sections.
|
|
1193
|
+
memoriesIncluded = includedOwnIds.size;
|
|
1194
|
+
let memoriesTruncatedUnique = 0;
|
|
1195
|
+
for (const id of truncatedOwnIds)
|
|
1196
|
+
if (!includedOwnIds.has(id))
|
|
1197
|
+
memoriesTruncatedUnique++;
|
|
1198
|
+
memoriesTruncated = memoriesTruncatedUnique;
|
|
1199
|
+
// flair#1207 — the teammate MATCH POOL considered (cleared the relevance
|
|
1200
|
+
// floor, drawn from the bounded candidate pool): every matched teammate
|
|
1201
|
+
// finding is EITHER included OR budget-truncated, so this equals their sum.
|
|
1202
|
+
// Reporting it anchors `teammateFindingsTruncated` as "relevant-but-no-budget"
|
|
1203
|
+
// rather than an unexplained large number beside a small `included`.
|
|
1204
|
+
const teammateFindingsMatched = teammateFindingsIncluded + teammateFindingsTruncated;
|
|
1105
1205
|
// --- Build context string ---
|
|
1106
1206
|
const parts = [];
|
|
1107
1207
|
if (sections.soul.length > 0) {
|
|
@@ -1258,8 +1358,14 @@ export class BootstrapMemories extends Resource {
|
|
|
1258
1358
|
},
|
|
1259
1359
|
soulTokens,
|
|
1260
1360
|
memoryTokens,
|
|
1361
|
+
// flair#1199 — the content-selection budget this response was built
|
|
1362
|
+
// against (echoed so a connector can relate tokenEstimate to the budget it
|
|
1363
|
+
// asked for — and so the conformance tokenEstimate<=maxTokens invariant is
|
|
1364
|
+
// self-contained). Defaults to 4000 when unset.
|
|
1365
|
+
maxTokens,
|
|
1261
1366
|
// flair#1199 — own memories included (denominator: memoriesAvailable, also
|
|
1262
|
-
// own-scoped)
|
|
1367
|
+
// own-scoped). flair#1207 — DERIVED from unique own-memory ids, so
|
|
1368
|
+
// memoriesIncluded + memoriesTruncated <= memoriesAvailable always holds.
|
|
1263
1369
|
memoriesIncluded,
|
|
1264
1370
|
memoriesAvailable,
|
|
1265
1371
|
// Cross-agent teammate findings included — a SEPARATE denominator, labelled
|
|
@@ -1267,24 +1373,35 @@ export class BootstrapMemories extends Resource {
|
|
|
1267
1373
|
teammateFindingsIncluded,
|
|
1268
1374
|
memoriesTruncated,
|
|
1269
1375
|
// flair#1207 — teammate findings skipped for size in the task-relevant loop
|
|
1270
|
-
// (own size-skips there
|
|
1271
|
-
//
|
|
1272
|
-
//
|
|
1376
|
+
// (own size-skips there feed truncatedOwnIds). Surfacing this makes a
|
|
1377
|
+
// size-skip self-describing: "a relevant teammate finding didn't fit" is
|
|
1378
|
+
// now distinguishable from "no relevant teammate finding".
|
|
1273
1379
|
teammateFindingsTruncated,
|
|
1380
|
+
// flair#1207 — the teammate match POOL considered (cleared the relevance
|
|
1381
|
+
// floor). teammateFindingsIncluded + teammateFindingsTruncated ==
|
|
1382
|
+
// teammateFindingsMatched, so "truncated" reads as "relevant-but-no-budget"
|
|
1383
|
+
// against a stated pool, not an unanchored large number.
|
|
1384
|
+
teammateFindingsMatched,
|
|
1274
1385
|
};
|
|
1275
1386
|
// flair#1199 — tokenEstimate must reflect the ACTUAL serialized payload the
|
|
1276
1387
|
// caller receives (the structured containers included), not just the prose
|
|
1277
1388
|
// `context`. The old `soulTokens + memoryTokens` counted only the context
|
|
1278
1389
|
// string, so it under-reported by ~2× once the structured fields shipped
|
|
1279
1390
|
// alongside. Measured over the assembled body (the ~1-line tokenEstimate
|
|
1280
|
-
// field it omits is negligible).
|
|
1281
|
-
//
|
|
1282
|
-
//
|
|
1283
|
-
// the
|
|
1284
|
-
//
|
|
1285
|
-
//
|
|
1286
|
-
//
|
|
1287
|
-
// payload
|
|
1391
|
+
// field it omits is negligible). CAP CONTRACT: this is an HONEST report of
|
|
1392
|
+
// the real serialized size. Every CONTENT section — soul, memories, findings,
|
|
1393
|
+
// AND events (flair#1199) — is now gated against the shared `maxTokens`
|
|
1394
|
+
// budget, so no section blows the budget with uncounted content the way the
|
|
1395
|
+
// 0.44.9 events array did (maxTokens=4000 → 6286). tokenEstimate may still
|
|
1396
|
+
// exceed `maxTokens` slightly, by the FIXED structural JSON scaffolding
|
|
1397
|
+
// (container keys/braces, counters, sections map, char/4 rounding) — that is
|
|
1398
|
+
// genuine payload the caller pays for, and it is small and bounded, NOT
|
|
1399
|
+
// uncounted content. The connector-conformance suite asserts
|
|
1400
|
+
// tokenEstimate <= maxTokens within a small tolerance for exactly that
|
|
1401
|
+
// scaffolding. Do NOT "fix" a small over-maxTokens tokenEstimate by shrinking
|
|
1402
|
+
// memory selection — that is the #1199→#1207 regression (it dropped relevant
|
|
1403
|
+
// findings, charging a per-item overhead against the content budget). If the
|
|
1404
|
+
// real payload consistently overruns for a use case, raise `maxTokens`.
|
|
1288
1405
|
const tokenEstimate = estimateTokens(JSON.stringify(responseBody));
|
|
1289
1406
|
return { ...responseBody, tokenEstimate };
|
|
1290
1407
|
}
|
|
@@ -40,7 +40,7 @@ import { Resource, databases, models, logger } from "harper";
|
|
|
40
40
|
import { randomBytes } from "node:crypto";
|
|
41
41
|
import { isAdmin, allowVerified } from "./agent-auth.js";
|
|
42
42
|
import { patchRecordSilent } from "./table-helpers.js";
|
|
43
|
-
import { buildReflectionPrompt, buildExecutePrompt, resolveReflectActor, generateCandidates, dedupeCandidates, } from "./memory-reflect-lib.js";
|
|
43
|
+
import { buildReflectionPrompt, buildExecutePrompt, resolveReflectActor, generateCandidates, dedupeCandidates, memoryMatchesReflectScope, buildStagedCandidateRow, } from "./memory-reflect-lib.js";
|
|
44
44
|
export class ReflectMemories extends Resource {
|
|
45
45
|
// Self-authorize via the Ed25519 agent verify (auth reshape removes the gate's
|
|
46
46
|
// admin elevation). Any verified agent may reflect; the isAdmin checks in post()
|
|
@@ -75,15 +75,12 @@ export class ReflectMemories extends Resource {
|
|
|
75
75
|
continue;
|
|
76
76
|
if (record.durability === "permanent")
|
|
77
77
|
continue; // permanent memories don't need reflection
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
// scope="all" passes everything
|
|
78
|
+
// Scope selection — the cross-user-bleed boundary (#1205b-1). See
|
|
79
|
+
// memoryMatchesReflectScope's doc: scope:"tagged" admits ONLY the one
|
|
80
|
+
// adk:<app>:<user> tag's memories, so a candidate distilled here can
|
|
81
|
+
// never cite another user's memory.
|
|
82
|
+
if (!memoryMatchesReflectScope(record, { scope, tag, sinceDate }))
|
|
83
|
+
continue;
|
|
87
84
|
const { embedding, ...rest } = record;
|
|
88
85
|
memories.push(rest);
|
|
89
86
|
if (memories.length >= maxMemories)
|
|
@@ -152,7 +149,11 @@ export class ReflectMemories extends Resource {
|
|
|
152
149
|
const generatedAt = new Date().toISOString();
|
|
153
150
|
const staged = [];
|
|
154
151
|
for (const c of toStage) {
|
|
155
|
-
|
|
152
|
+
// #1205b-1: buildStagedCandidateRow stamps `scopeTag` when this run was
|
|
153
|
+
// scope:"tagged" — the authoritative per-user tag promotion consumes
|
|
154
|
+
// directly (closing the #1205a source-re-read seam). Non-tagged runs
|
|
155
|
+
// leave scopeTag absent, unchanged.
|
|
156
|
+
const row = buildStagedCandidateRow({
|
|
156
157
|
id: `cand_${randomBytes(8).toString("hex")}`,
|
|
157
158
|
agentId,
|
|
158
159
|
claim: c.claim,
|
|
@@ -160,8 +161,9 @@ export class ReflectMemories extends Resource {
|
|
|
160
161
|
rationalePrompt: executePrompt,
|
|
161
162
|
generatedBy: resolvedModel,
|
|
162
163
|
generatedAt,
|
|
163
|
-
|
|
164
|
-
|
|
164
|
+
scope,
|
|
165
|
+
tag,
|
|
166
|
+
});
|
|
165
167
|
await databases.flair.MemoryCandidate.put(row);
|
|
166
168
|
staged.push(row);
|
|
167
169
|
}
|