@tpsdev-ai/flair 0.44.8 → 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 +292 -28
- package/dist/doctor-client.js +55 -1
- package/dist/install/clients.js +50 -2
- package/dist/rem/runner.js +167 -23
- package/dist/resources/MemoryBootstrap.js +267 -65
- package/dist/resources/MemoryReflect.js +15 -13
- package/dist/resources/mcp-tools.js +243 -20
- 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/dist/resources/trust-block.js +16 -7
- package/docs/integrations.md +3 -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
|
-
*
|
|
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`
|
|
@@ -84,14 +86,48 @@ import { bestSemanticSimilarity, evaluateAbstention } from "./abstention.js";
|
|
|
84
86
|
* mirror (opt-in via includeContext). Cross-agent teammate findings ship in
|
|
85
87
|
* the `teammateFindings` container (own memories in `memories`), counted by
|
|
86
88
|
* `teammateFindingsIncluded` (a separate denominator from `memoriesIncluded`,
|
|
87
|
-
* which is own-scoped so it never exceeds `memoriesAvailable`).
|
|
88
|
-
*
|
|
89
|
+
* which is own-scoped so it never exceeds `memoriesAvailable`). flair#1206 —
|
|
90
|
+
* org events ship in their OWN structured `events` container (ALWAYS present,
|
|
91
|
+
* `[]` when none), so a connector reading the structured payload gets them even
|
|
92
|
+
* when the prose `context` is off (the /mcp default); before #1206 they lived
|
|
93
|
+
* ONLY in the prose string and were orphaned at includeContext=false.
|
|
94
|
+
*
|
|
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."
|
|
89
119
|
* `predictedHint` is present only when subjects were provided but `predicted`
|
|
90
120
|
* came back empty.
|
|
91
121
|
*/
|
|
92
122
|
// Collision surfacing (flair#681) tunables.
|
|
93
123
|
const COLLISION_WINDOW_DAYS = 7;
|
|
94
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;
|
|
95
131
|
// ─── Bootstrap scale fix (flair-bootstrap-scale-fix) tunables ───────────────
|
|
96
132
|
//
|
|
97
133
|
// Own-scoped, non-permanent memories (the "recent" adaptive-window source,
|
|
@@ -123,21 +159,23 @@ const MAX_CANDIDATE_POOL = 100;
|
|
|
123
159
|
// `minScore` request param) — preserved verbatim from the original raw
|
|
124
160
|
// JS dot-product scan's `.filter((s) => s.score > 0.3)`.
|
|
125
161
|
const TASK_RELEVANCE_FLOOR = 0.3;
|
|
126
|
-
// flair#
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
162
|
+
// flair#1207 — the per-item structured-payload overhead that #1199 charged
|
|
163
|
+
// against the content-selection budget (a `+ STRUCT_ITEM_OVERHEAD_TOKENS = 70`
|
|
164
|
+
// added to every item's cost, PLUS a `structOverheadReserve` pre-deducted from
|
|
165
|
+
// the starting budget) has been REMOVED. It conflated measurement with
|
|
166
|
+
// budgeting: `tokenEstimate` already measures the real serialized payload
|
|
167
|
+
// (JSON.stringify(responseBody)) — the structured JSON scaffolding overhead is
|
|
168
|
+
// captured there, honestly. Folding it into the SELECTION budget too
|
|
169
|
+
// double-penalized and silently shrank recall below 0.44.6 for the same
|
|
170
|
+
// `maxTokens` (6 findings → 3). The content budget is now `maxTokens` again
|
|
171
|
+
// (0.44.6 selection capacity), and each item's cost is just the rendered prose
|
|
172
|
+
// line — while `tokenEstimate` keeps reporting the true serialized size, which
|
|
173
|
+
// may exceed `maxTokens` by the scaffolding overhead. See the module-doc CAP
|
|
174
|
+
// CONTRACT above.
|
|
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.
|
|
141
179
|
// `agentId` is the BOOTSTRAPPING agent (the caller) — used only to decide
|
|
142
180
|
// whether to annotate attribution, never to change what's read (that
|
|
143
181
|
// boundary is resolveReadScope()'s job, upstream of this function). A
|
|
@@ -175,6 +213,15 @@ export class BootstrapMemories extends Resource {
|
|
|
175
213
|
subjects, // e.g., ["flair", "auth"] — entities to preload context for
|
|
176
214
|
includeTrust = false, // flair#744 slice 1 — opt-in per-memory trust block
|
|
177
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,
|
|
178
225
|
// flair#1199 — whether to assemble the prose `context` string. The
|
|
179
226
|
// structured containers (soul/memories/predicted/teammateFindings) are the
|
|
180
227
|
// CANONICAL payload; `context` is a human/agent-readable MIRROR of the same
|
|
@@ -222,19 +269,22 @@ export class BootstrapMemories extends Resource {
|
|
|
222
269
|
collision: [],
|
|
223
270
|
events: [],
|
|
224
271
|
};
|
|
225
|
-
// flair#
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
// scaffolding
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
|
|
272
|
+
// flair#1207 — the content-SELECTION budget is `maxTokens`, matching 0.44.6
|
|
273
|
+
// capacity. #1199 pre-deducted a `structOverheadReserve` (min(600, 15% of
|
|
274
|
+
// maxTokens)) here to "reserve headroom" for the structured-container JSON
|
|
275
|
+
// scaffolding, on the theory that `maxTokens` should bound the serialized
|
|
276
|
+
// payload. That silently shrank the content budget and, combined with the
|
|
277
|
+
// per-item overhead (also removed), cut recall below 0.44.6 for the same
|
|
278
|
+
// `maxTokens` (#1207). The reserve is gone: `maxTokens` is the HARD cap on
|
|
279
|
+
// CONTENT SELECTION only. `tokenEstimate` (below) still reports the real
|
|
280
|
+
// serialized size honestly — which may exceed `maxTokens` by the scaffolding
|
|
281
|
+
// overhead, exactly as 0.44.6's payload did (0.44.6 just under-measured it).
|
|
233
282
|
// Single shared budget across soul + every memory section (soul used to be
|
|
234
283
|
// budgeted SEPARATELY and added ON TOP, so context alone could reach
|
|
235
|
-
// 1.4×maxTokens; #1199
|
|
236
|
-
//
|
|
237
|
-
|
|
284
|
+
// 1.4×maxTokens; #1199 folded soul into this shared budget — that part
|
|
285
|
+
// stays). Every admitted line is gated against the remaining budget, so the
|
|
286
|
+
// sum of selected CONTENT never exceeds `maxTokens`.
|
|
287
|
+
let tokenBudget = Math.max(0, maxTokens);
|
|
238
288
|
// Own memories included in the payload (permanent + recent + predicted +
|
|
239
289
|
// own task-relevant). Denominator is `memoriesAvailable` (own-scoped), so
|
|
240
290
|
// memoriesIncluded ≤ memoriesAvailable always holds (#1199 coherent
|
|
@@ -242,10 +292,38 @@ export class BootstrapMemories extends Resource {
|
|
|
242
292
|
let memoriesIncluded = 0;
|
|
243
293
|
let memoriesAvailable = 0;
|
|
244
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();
|
|
245
313
|
// flair#1199 — cross-agent teammate findings included (a DIFFERENT
|
|
246
314
|
// denominator than own memories): counting these into `memoriesIncluded`
|
|
247
315
|
// is what let one client see included(9) > available(3).
|
|
248
316
|
let teammateFindingsIncluded = 0;
|
|
317
|
+
// flair#1207 — teammate findings SKIPPED for size in the task-relevant
|
|
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.
|
|
326
|
+
let teammateFindingsTruncated = 0;
|
|
249
327
|
// flair#1182 (part 1) — self-describing bootstrap. These structured
|
|
250
328
|
// container keys are ALWAYS emitted on the response (empty `{}`/`[]` when
|
|
251
329
|
// the caller has nothing), so a client can tell an *empty* instance from
|
|
@@ -267,6 +345,17 @@ export class BootstrapMemories extends Resource {
|
|
|
267
345
|
// them. Kept SEPARATE from `memories`/`predicted` (which stay own-only per
|
|
268
346
|
// the #1182 boundary) and clearly attributed via `source`.
|
|
269
347
|
const includedTeammateFindings = [];
|
|
348
|
+
// flair#1206 — org events get their OWN structured container. Before #1206
|
|
349
|
+
// they lived ONLY in the prose `context` string ("## Recent Org Events"),
|
|
350
|
+
// so at includeContext=false (the /mcp default) they were counted in
|
|
351
|
+
// `sections.events` and measured into `tokenEstimate` (when prose was on) but
|
|
352
|
+
// NEVER delivered in any field a connector could read — orphaned. Populated
|
|
353
|
+
// from the SAME deduped+sliced set the prose lines are (so count, charge and
|
|
354
|
+
// delivery all key off one thing), ALWAYS emitted (`[]` when none), and the
|
|
355
|
+
// targetIds relevance filter is already applied upstream (see the OrgEvent
|
|
356
|
+
// read below). Declared out here so it is in scope for the response body even
|
|
357
|
+
// if the events read (in a try/catch) yields nothing.
|
|
358
|
+
const includedEvents = [];
|
|
270
359
|
const leanMemory = (m, section) => ({
|
|
271
360
|
id: m.id,
|
|
272
361
|
content: m.content,
|
|
@@ -528,17 +617,17 @@ export class BootstrapMemories extends Resource {
|
|
|
528
617
|
const permanent = permanentRows.filter((m) => !permanentSupersededIds.has(m.id));
|
|
529
618
|
for (const m of permanent) {
|
|
530
619
|
const line = formatMemory(m, agentId);
|
|
531
|
-
const cost = estimateTokens(line)
|
|
620
|
+
const cost = estimateTokens(line); // #1207 — prose-line cost only; overhead is a reporting concern (tokenEstimate), not a selection constraint
|
|
532
621
|
if (cost <= tokenBudget) {
|
|
533
622
|
sections.permanent.push(line);
|
|
534
623
|
includedOwnMemories.push(leanMemory(m, "permanent"));
|
|
535
624
|
if (includeTrust)
|
|
536
625
|
includedTrustMemories.push({ m, section: "permanent" });
|
|
537
626
|
tokenBudget -= cost;
|
|
538
|
-
|
|
627
|
+
includedOwnIds.add(m.id); // #1207 — count by unique own-memory id
|
|
539
628
|
}
|
|
540
629
|
else {
|
|
541
|
-
|
|
630
|
+
truncatedOwnIds.add(m.id); // #1207 — budget-skip, deduped against inclusions at the end
|
|
542
631
|
}
|
|
543
632
|
}
|
|
544
633
|
// --- 3. Recent memories (adaptive window) ---
|
|
@@ -600,9 +689,9 @@ export class BootstrapMemories extends Resource {
|
|
|
600
689
|
let recentSpent = 0;
|
|
601
690
|
for (const m of recent) {
|
|
602
691
|
const line = formatMemory(m, agentId);
|
|
603
|
-
const cost = estimateTokens(line)
|
|
692
|
+
const cost = estimateTokens(line); // #1207 — prose-line cost only; overhead is a reporting concern (tokenEstimate), not a selection constraint
|
|
604
693
|
if (recentSpent + cost > recentBudget) {
|
|
605
|
-
|
|
694
|
+
truncatedOwnIds.add(m.id); // #1207 — budget-skip; may still be admitted later via the task-relevant loop (deduped at the end)
|
|
606
695
|
continue;
|
|
607
696
|
}
|
|
608
697
|
sections.recent.push(line);
|
|
@@ -611,7 +700,7 @@ export class BootstrapMemories extends Resource {
|
|
|
611
700
|
includedTrustMemories.push({ m, section: "recent" });
|
|
612
701
|
recentSpent += cost;
|
|
613
702
|
tokenBudget -= cost;
|
|
614
|
-
|
|
703
|
+
includedOwnIds.add(m.id); // #1207 — count by unique own-memory id
|
|
615
704
|
}
|
|
616
705
|
// --- 3b. Subject-predicted context ---
|
|
617
706
|
// When subjects are provided (e.g., ["flair", "auth"]), load memories
|
|
@@ -622,10 +711,10 @@ export class BootstrapMemories extends Resource {
|
|
|
622
711
|
? subjects.map((s) => s.toLowerCase())
|
|
623
712
|
: [];
|
|
624
713
|
if (predictedSubjects.length > 0 && tokenBudget > 200) {
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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.
|
|
629
718
|
// Draws from the SAME bounded own-scoped, non-permanent set "recent"
|
|
630
719
|
// uses (nonPermanentActive — see that fetch's doc above for the
|
|
631
720
|
// shared-source rationale and OWN_NONPERMANENT_FETCH_LIMIT's bound).
|
|
@@ -633,7 +722,7 @@ export class BootstrapMemories extends Resource {
|
|
|
633
722
|
// query's own condition but kept for parity/clarity with the
|
|
634
723
|
// pre-refactor filter shape.
|
|
635
724
|
const subjectMemories = nonPermanentActive
|
|
636
|
-
.filter((m) => !
|
|
725
|
+
.filter((m) => !includedOwnIds.has(m.id) &&
|
|
637
726
|
m.subject &&
|
|
638
727
|
predictedSubjects.includes(m.subject.toLowerCase()) &&
|
|
639
728
|
m.durability !== "permanent" // already loaded
|
|
@@ -643,9 +732,9 @@ export class BootstrapMemories extends Resource {
|
|
|
643
732
|
let predictedSpent = 0;
|
|
644
733
|
for (const m of subjectMemories) {
|
|
645
734
|
const line = formatMemory(m, agentId);
|
|
646
|
-
const cost = estimateTokens(line)
|
|
735
|
+
const cost = estimateTokens(line); // #1207 — prose-line cost only; overhead is a reporting concern (tokenEstimate), not a selection constraint
|
|
647
736
|
if (predictedSpent + cost > predictedBudget) {
|
|
648
|
-
|
|
737
|
+
truncatedOwnIds.add(m.id); // #1207 — budget-skip (deduped against inclusions at the end)
|
|
649
738
|
continue;
|
|
650
739
|
}
|
|
651
740
|
sections.predicted.push(line);
|
|
@@ -654,8 +743,7 @@ export class BootstrapMemories extends Resource {
|
|
|
654
743
|
includedTrustMemories.push({ m, section: "predicted" });
|
|
655
744
|
predictedSpent += cost;
|
|
656
745
|
tokenBudget -= cost;
|
|
657
|
-
|
|
658
|
-
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)
|
|
659
747
|
}
|
|
660
748
|
}
|
|
661
749
|
// --- 3c. Active relationships for predicted subjects ---
|
|
@@ -709,11 +797,13 @@ export class BootstrapMemories extends Resource {
|
|
|
709
797
|
}
|
|
710
798
|
catch { }
|
|
711
799
|
if (queryEmbedding) {
|
|
712
|
-
//
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
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;
|
|
717
807
|
// Bounded HNSW candidate pool (flair-bootstrap-scale-fix) — replaces
|
|
718
808
|
// the full-corpus JS dot-product scan (`allMemories` × queryEmbedding,
|
|
719
809
|
// O(org corpus size) every bootstrap). K formula (Kern-approved):
|
|
@@ -818,9 +908,19 @@ export class BootstrapMemories extends Resource {
|
|
|
818
908
|
// section double-spends.
|
|
819
909
|
for (const { memory: m } of scored) {
|
|
820
910
|
const line = formatMemory(m, agentId);
|
|
821
|
-
const cost = estimateTokens(line)
|
|
822
|
-
if (cost > tokenBudget)
|
|
911
|
+
const cost = estimateTokens(line); // #1207 — prose-line cost only; overhead is a reporting concern (tokenEstimate), not a selection constraint
|
|
912
|
+
if (cost > tokenBudget) {
|
|
913
|
+
// flair#1207 — a size-skip in the score-ordered task-relevant loop
|
|
914
|
+
// is no longer silent: record it on the denominator matching the
|
|
915
|
+
// record's origin (own → truncatedOwnIds, teammate → the separate
|
|
916
|
+
// teammateFindingsTruncated), so a client can distinguish "no relevant
|
|
917
|
+
// finding" from "a relevant finding didn't fit the budget".
|
|
918
|
+
if (m._source)
|
|
919
|
+
teammateFindingsTruncated++;
|
|
920
|
+
else
|
|
921
|
+
truncatedOwnIds.add(m.id);
|
|
823
922
|
continue;
|
|
923
|
+
}
|
|
824
924
|
if (m._source) {
|
|
825
925
|
sections.teammate.push(line);
|
|
826
926
|
// flair#1199 — cross-agent teammate findings join their OWN
|
|
@@ -852,7 +952,7 @@ export class BootstrapMemories extends Resource {
|
|
|
852
952
|
if (includeTrust)
|
|
853
953
|
includedTrustMemories.push({ m, section: "relevant" });
|
|
854
954
|
tokenBudget -= cost;
|
|
855
|
-
|
|
955
|
+
includedOwnIds.add(m.id); // #1207 — count by unique own-memory id
|
|
856
956
|
}
|
|
857
957
|
}
|
|
858
958
|
}
|
|
@@ -1000,6 +1100,15 @@ export class BootstrapMemories extends Resource {
|
|
|
1000
1100
|
const isRelevant = !targets || targets.length === 0 || targets.includes(agentId);
|
|
1001
1101
|
if (!isRelevant)
|
|
1002
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;
|
|
1003
1112
|
eventResults.push(event);
|
|
1004
1113
|
}
|
|
1005
1114
|
// flair#1200 — collapse byte-identical duplicate events before rendering.
|
|
@@ -1007,10 +1116,10 @@ export class BootstrapMemories extends Resource {
|
|
|
1007
1116
|
// that double-fires, or the same broadcast emitted from two paths); each
|
|
1008
1117
|
// physical row has a distinct id/createdAt (OrgEvent.post keys the id off
|
|
1009
1118
|
// a millisecond timestamp), so they aren't caught by primary-key upsert
|
|
1010
|
-
// and render as exact dupes. Org-event slots are scarce
|
|
1011
|
-
//
|
|
1012
|
-
//
|
|
1013
|
-
//
|
|
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.
|
|
1014
1123
|
const eventBySignature = new Map();
|
|
1015
1124
|
for (const evt of eventResults) {
|
|
1016
1125
|
const sig = JSON.stringify([
|
|
@@ -1023,18 +1132,76 @@ export class BootstrapMemories extends Resource {
|
|
|
1023
1132
|
if (!prev || (evt.createdAt || "") > (prev.createdAt || ""))
|
|
1024
1133
|
eventBySignature.set(sig, evt);
|
|
1025
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;
|
|
1026
1149
|
const dedupedEvents = [...eventBySignature.values()]
|
|
1027
|
-
.sort((a, b) => (
|
|
1028
|
-
for (const evt of dedupedEvents
|
|
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 = {
|
|
1159
|
+
id: evt.id,
|
|
1160
|
+
kind: evt.kind,
|
|
1161
|
+
summary: evt.summary,
|
|
1162
|
+
...(includeEventDetail && evt.detail != null ? { detail: evt.detail } : {}),
|
|
1163
|
+
...(Array.isArray(evt.targetIds) && evt.targetIds.length > 0 ? { targetIds: evt.targetIds } : {}),
|
|
1164
|
+
createdAt: evt.createdAt ?? null,
|
|
1165
|
+
...(evt.scope != null ? { scope: evt.scope } : {}),
|
|
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;
|
|
1029
1173
|
const elapsed = Date.now() - new Date(evt.createdAt).getTime();
|
|
1030
1174
|
const mins = Math.floor(elapsed / 60_000);
|
|
1031
1175
|
const relTime = mins < 60 ? `${mins}min ago` : `${Math.floor(mins / 60)}h ago`;
|
|
1032
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;
|
|
1033
1181
|
}
|
|
1034
1182
|
}
|
|
1035
1183
|
catch {
|
|
1036
1184
|
// non-fatal: OrgEvent table may not exist yet
|
|
1037
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;
|
|
1038
1205
|
// --- Build context string ---
|
|
1039
1206
|
const parts = [];
|
|
1040
1207
|
if (sections.soul.length > 0) {
|
|
@@ -1165,6 +1332,13 @@ export class BootstrapMemories extends Resource {
|
|
|
1165
1332
|
// (always present, `[]` when none), so a connector that consumes the
|
|
1166
1333
|
// containers still gets them when prose `context` is off.
|
|
1167
1334
|
teammateFindings: includedTeammateFindings,
|
|
1335
|
+
// flair#1206 — org events as a structured container (always present, `[]`
|
|
1336
|
+
// when none, same self-describing-empty-state pattern as the containers
|
|
1337
|
+
// above). Before #1206 events lived ONLY in the prose `context`, so at the
|
|
1338
|
+
// /mcp default (includeContext=false) they were counted+measured but never
|
|
1339
|
+
// delivered. Deduped (#1200) and targetIds-scoped (same set as the prose
|
|
1340
|
+
// "## Recent Org Events" lines), so count/charge/delivery all agree.
|
|
1341
|
+
events: includedEvents,
|
|
1168
1342
|
...(currentTaskHint ? { currentTaskHint } : {}),
|
|
1169
1343
|
...(predictedHint ? { predictedHint } : {}),
|
|
1170
1344
|
...(trust ? { trust } : {}),
|
|
@@ -1184,22 +1358,50 @@ export class BootstrapMemories extends Resource {
|
|
|
1184
1358
|
},
|
|
1185
1359
|
soulTokens,
|
|
1186
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,
|
|
1187
1366
|
// flair#1199 — own memories included (denominator: memoriesAvailable, also
|
|
1188
|
-
// own-scoped)
|
|
1367
|
+
// own-scoped). flair#1207 — DERIVED from unique own-memory ids, so
|
|
1368
|
+
// memoriesIncluded + memoriesTruncated <= memoriesAvailable always holds.
|
|
1189
1369
|
memoriesIncluded,
|
|
1190
1370
|
memoriesAvailable,
|
|
1191
1371
|
// Cross-agent teammate findings included — a SEPARATE denominator, labelled
|
|
1192
1372
|
// so it's never confused with the own-memory counters.
|
|
1193
1373
|
teammateFindingsIncluded,
|
|
1194
1374
|
memoriesTruncated,
|
|
1375
|
+
// flair#1207 — teammate findings skipped for size in the task-relevant loop
|
|
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".
|
|
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,
|
|
1195
1385
|
};
|
|
1196
1386
|
// flair#1199 — tokenEstimate must reflect the ACTUAL serialized payload the
|
|
1197
1387
|
// caller receives (the structured containers included), not just the prose
|
|
1198
1388
|
// `context`. The old `soulTokens + memoryTokens` counted only the context
|
|
1199
1389
|
// string, so it under-reported by ~2× once the structured fields shipped
|
|
1200
|
-
// alongside
|
|
1201
|
-
//
|
|
1202
|
-
//
|
|
1390
|
+
// alongside. Measured over the assembled body (the ~1-line tokenEstimate
|
|
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`.
|
|
1203
1405
|
const tokenEstimate = estimateTokens(JSON.stringify(responseBody));
|
|
1204
1406
|
return { ...responseBody, tokenEstimate };
|
|
1205
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
|
}
|