@tpsdev-ai/flair 0.44.7 → 0.44.8
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
CHANGED
|
@@ -14328,7 +14328,13 @@ program
|
|
|
14328
14328
|
console.error(`${render.icons.error} No context available.`);
|
|
14329
14329
|
process.exit(1);
|
|
14330
14330
|
}
|
|
14331
|
-
|
|
14331
|
+
// flair#1199 — the budget footer reflects the PROSE the human injects
|
|
14332
|
+
// (stdout = result.context), not the full serialized payload. tokenEstimate
|
|
14333
|
+
// now measures the whole response (structured containers + prose), which
|
|
14334
|
+
// the CLI's structured fields the human doesn't read would inflate.
|
|
14335
|
+
const tokensUsed = typeof result.context === "string" && result.context.length > 0
|
|
14336
|
+
? Math.ceil(result.context.length / 4)
|
|
14337
|
+
: (result.tokenEstimate ?? 0);
|
|
14332
14338
|
const maxTokens = parseInt(opts.maxTokens, 10);
|
|
14333
14339
|
const included = result.memoriesIncluded ?? 0;
|
|
14334
14340
|
const truncated = result.memoriesTruncated ?? 0;
|
|
@@ -59,7 +59,11 @@ import { bestSemanticSimilarity, evaluateAbstention } from "./abstention.js";
|
|
|
59
59
|
*
|
|
60
60
|
* Request:
|
|
61
61
|
* { agentId, currentTask?, maxTokens?, includeSoul?, since?,
|
|
62
|
-
* channel?, surface?, subjects?, entities? }
|
|
62
|
+
* channel?, surface?, subjects?, entities?, includeContext? }
|
|
63
|
+
* `includeContext` (flair#1199): whether to assemble the prose `context`
|
|
64
|
+
* mirror. Default true here (the resource/REST/CLI path); the /mcp bootstrap
|
|
65
|
+
* wrapper passes false so a token-budgeted connector — which reads the
|
|
66
|
+
* structured containers — never receives the same bodies twice.
|
|
63
67
|
* `entities` (flair#681): the caller's own declared attention-plane
|
|
64
68
|
* vocabulary strings (see resources/entity-vocab.ts) for collision
|
|
65
69
|
* surfacing's entity-overlap join. Invalid entries are silently dropped
|
|
@@ -69,12 +73,21 @@ import { bestSemanticSimilarity, evaluateAbstention } from "./abstention.js";
|
|
|
69
73
|
*
|
|
70
74
|
* Response:
|
|
71
75
|
* { context, sections, tokenEstimate, memoriesIncluded, memoriesAvailable,
|
|
72
|
-
* agentId, scope, soul, memories, predicted
|
|
76
|
+
* teammateFindingsIncluded, agentId, scope, soul, memories, predicted,
|
|
77
|
+
* teammateFindings[, currentTaskHint][, predictedHint] }
|
|
73
78
|
* The self-describing keys (flair#1182 part 1) — `agentId` (resolved caller),
|
|
74
79
|
* `scope` (read model applied to the caller), `soul`/`memories`/`predicted`
|
|
75
80
|
* (the caller's OWN records as structured containers), and `currentTaskHint`
|
|
76
81
|
* (present only when currentTask is absent/blank) — are ALWAYS emitted so a
|
|
77
82
|
* client can tell an empty instance from one that doesn't support them.
|
|
83
|
+
* flair#1199 — the structured containers are CANONICAL; `context` is a prose
|
|
84
|
+
* mirror (opt-in via includeContext). Cross-agent teammate findings ship in
|
|
85
|
+
* the `teammateFindings` container (own memories in `memories`), counted by
|
|
86
|
+
* `teammateFindingsIncluded` (a separate denominator from `memoriesIncluded`,
|
|
87
|
+
* which is own-scoped so it never exceeds `memoriesAvailable`). `tokenEstimate`
|
|
88
|
+
* reflects the ACTUAL serialized payload, and `maxTokens` bounds it.
|
|
89
|
+
* `predictedHint` is present only when subjects were provided but `predicted`
|
|
90
|
+
* came back empty.
|
|
78
91
|
*/
|
|
79
92
|
// Collision surfacing (flair#681) tunables.
|
|
80
93
|
const COLLISION_WINDOW_DAYS = 7;
|
|
@@ -110,6 +123,17 @@ const MAX_CANDIDATE_POOL = 100;
|
|
|
110
123
|
// `minScore` request param) — preserved verbatim from the original raw
|
|
111
124
|
// JS dot-product scan's `.filter((s) => s.score > 0.3)`.
|
|
112
125
|
const TASK_RELEVANCE_FLOOR = 0.3;
|
|
126
|
+
// flair#1199 — per-memory structured-payload overhead charged against the token
|
|
127
|
+
// budget IN ADDITION to the rendered prose line. Each included memory ships as a
|
|
128
|
+
// structured object ({id, content, durability, createdAt, updatedAt, agentId,
|
|
129
|
+
// subject, section, ...}) which is the CANONICAL payload; its JSON keys cost
|
|
130
|
+
// ~30-40 tokens beyond the prose line the fill loop measures. Charging it here
|
|
131
|
+
// keeps the ACTUAL serialized payload (measured by tokenEstimate) within
|
|
132
|
+
// maxTokens — the prose line alone under-charged, so the structured containers
|
|
133
|
+
// crossed the cap. Sized to the non-content JSON a lean memory carries (id +
|
|
134
|
+
// createdAt + updatedAt + agentId + durability + subject + section + key names,
|
|
135
|
+
// ~55-70 tokens); conservative (errs slightly high, never low).
|
|
136
|
+
const STRUCT_ITEM_OVERHEAD_TOKENS = 70;
|
|
113
137
|
// Rough token estimate: ~4 chars per token for English text
|
|
114
138
|
function estimateTokens(text) {
|
|
115
139
|
return Math.ceil(text.length / 4);
|
|
@@ -151,7 +175,16 @@ export class BootstrapMemories extends Resource {
|
|
|
151
175
|
subjects, // e.g., ["flair", "auth"] — entities to preload context for
|
|
152
176
|
includeTrust = false, // flair#744 slice 1 — opt-in per-memory trust block
|
|
153
177
|
abstain = false, // flair#744 slice 2 — opt-in task-relevance abstention
|
|
154
|
-
|
|
178
|
+
// flair#1199 — whether to assemble the prose `context` string. The
|
|
179
|
+
// structured containers (soul/memories/predicted/teammateFindings) are the
|
|
180
|
+
// CANONICAL payload; `context` is a human/agent-readable MIRROR of the same
|
|
181
|
+
// bytes. Default TRUE here (the resource/REST/CLI path has always emitted
|
|
182
|
+
// prose, and every direct caller reads it) — but the /mcp bootstrap wrapper
|
|
183
|
+
// (resources/mcp-tools.ts) passes `false` by default, so a token-budgeted
|
|
184
|
+
// connector, which consumes the structured fields, never receives the same
|
|
185
|
+
// bodies twice. When false, `context` is a compact structural pointer (no
|
|
186
|
+
// bodies), so nothing crosses the wire twice on that path.
|
|
187
|
+
includeContext = true, } = data || {};
|
|
155
188
|
// Authenticated identity lives on getContext().request — `this.request` is
|
|
156
189
|
// NOT populated on Harper v5 Resources. Reading it returned undefined and
|
|
157
190
|
// the scope check was silently bypassed, letting a non-admin agent read
|
|
@@ -189,27 +222,59 @@ export class BootstrapMemories extends Resource {
|
|
|
189
222
|
collision: [],
|
|
190
223
|
events: [],
|
|
191
224
|
};
|
|
192
|
-
|
|
225
|
+
// flair#1199 — the structured containers (soul/memories/predicted/
|
|
226
|
+
// teammateFindings) are the CANONICAL payload, and `tokenEstimate` now
|
|
227
|
+
// reports the ACTUAL serialized bytes. Reserve headroom for the JSON
|
|
228
|
+
// scaffolding those containers and the self-describing keys (scope/sections/
|
|
229
|
+
// counters) add on top of the raw content, so `maxTokens` bounds the real
|
|
230
|
+
// payload — not just the prose. Without this the containers ship on top of
|
|
231
|
+
// the memory-line budget and blow the cap (the reported 4000→4275+ overrun).
|
|
232
|
+
const structOverheadReserve = Math.min(600, Math.floor(maxTokens * 0.15));
|
|
233
|
+
// Single shared budget across soul + every memory section (soul used to be
|
|
234
|
+
// budgeted SEPARATELY and added ON TOP, so context alone could reach
|
|
235
|
+
// 1.4×maxTokens; #1199). Content selected therefore stays within
|
|
236
|
+
// maxTokens − reserve, and the serialized payload within maxTokens.
|
|
237
|
+
let tokenBudget = Math.max(0, maxTokens - structOverheadReserve);
|
|
238
|
+
// Own memories included in the payload (permanent + recent + predicted +
|
|
239
|
+
// own task-relevant). Denominator is `memoriesAvailable` (own-scoped), so
|
|
240
|
+
// memoriesIncluded ≤ memoriesAvailable always holds (#1199 coherent
|
|
241
|
+
// counters). Cross-agent teammate findings are counted separately below.
|
|
193
242
|
let memoriesIncluded = 0;
|
|
194
243
|
let memoriesAvailable = 0;
|
|
195
244
|
let memoriesTruncated = 0;
|
|
245
|
+
// flair#1199 — cross-agent teammate findings included (a DIFFERENT
|
|
246
|
+
// denominator than own memories): counting these into `memoriesIncluded`
|
|
247
|
+
// is what let one client see included(9) > available(3).
|
|
248
|
+
let teammateFindingsIncluded = 0;
|
|
196
249
|
// flair#1182 (part 1) — self-describing bootstrap. These structured
|
|
197
250
|
// container keys are ALWAYS emitted on the response (empty `{}`/`[]` when
|
|
198
251
|
// the caller has nothing), so a client can tell an *empty* instance from
|
|
199
252
|
// one that doesn't support these keys at all — and can read the caller's
|
|
200
253
|
// own soul/memories as structured data instead of parsing the `context`
|
|
201
|
-
// markdown string.
|
|
202
|
-
// (permanent/recent/relevant/predicted are all agentId==self
|
|
203
|
-
//
|
|
204
|
-
//
|
|
254
|
+
// markdown string. `soul`/`memories`/`predicted` are scoped to the CALLER'S
|
|
255
|
+
// OWN records only (permanent/recent/relevant/predicted are all agentId==self
|
|
256
|
+
// reads). flair#1199 — cross-agent teammate findings now have their OWN
|
|
257
|
+
// structured container (`teammateFindings`, below), attributed via `source`,
|
|
258
|
+
// rather than living only in the prose `context` (which is opt-in as of
|
|
259
|
+
// #1199); the own-only containers still carry no other agent's data.
|
|
205
260
|
const soulMap = {};
|
|
206
261
|
const includedOwnMemories = [];
|
|
207
262
|
const includedPredicted = [];
|
|
263
|
+
// flair#1199 — teammate (cross-agent) findings get their OWN structured
|
|
264
|
+
// container. Before #1199 they lived ONLY in the prose `context`; now that
|
|
265
|
+
// `context` is an opt-in mirror (default off on the /mcp path), they need a
|
|
266
|
+
// structured home so a connector that consumes the containers still sees
|
|
267
|
+
// them. Kept SEPARATE from `memories`/`predicted` (which stay own-only per
|
|
268
|
+
// the #1182 boundary) and clearly attributed via `source`.
|
|
269
|
+
const includedTeammateFindings = [];
|
|
208
270
|
const leanMemory = (m, section) => ({
|
|
209
271
|
id: m.id,
|
|
210
272
|
content: m.content,
|
|
211
273
|
durability: m.durability ?? null,
|
|
212
274
|
createdAt: m.createdAt ?? null,
|
|
275
|
+
// #1201 — the record's own last-write time, so a structured consumer can
|
|
276
|
+
// compute freshness off the same anchor the trust block's ageDays uses.
|
|
277
|
+
updatedAt: m.updatedAt ?? null,
|
|
213
278
|
agentId: m.agentId ?? agentId,
|
|
214
279
|
subject: m.subject ?? null,
|
|
215
280
|
section,
|
|
@@ -257,12 +322,15 @@ export class BootstrapMemories extends Resource {
|
|
|
257
322
|
if (maxChars > 100) {
|
|
258
323
|
const truncated = `**${entry.key}:** ${entry.line.slice(entry.key.length + 6, entry.key.length + 6 + maxChars)}…(truncated)`;
|
|
259
324
|
sections.soul.push(truncated);
|
|
260
|
-
|
|
325
|
+
const cost = estimateTokens(truncated);
|
|
326
|
+
soulTokens += cost;
|
|
327
|
+
tokenBudget -= cost; // #1199 — soul draws from the shared budget
|
|
261
328
|
}
|
|
262
329
|
continue;
|
|
263
330
|
}
|
|
264
331
|
sections.soul.push(entry.line);
|
|
265
332
|
soulTokens += entry.tokens;
|
|
333
|
+
tokenBudget -= entry.tokens; // #1199 — soul draws from the shared budget
|
|
266
334
|
}
|
|
267
335
|
}
|
|
268
336
|
// --- 1b. Skill assignments (ordered by priority, conflict detection) ---
|
|
@@ -388,14 +456,23 @@ export class BootstrapMemories extends Resource {
|
|
|
388
456
|
// requested, so a non-trust bootstrap fetches (and returns) exactly what it
|
|
389
457
|
// did before. These records are never returned raw when the block is off,
|
|
390
458
|
// so widening the select cannot change the off-path response bytes.
|
|
459
|
+
// #1201 — `updatedAt` is projected on BOTH paths (not just the trust path):
|
|
460
|
+
// the structured `memories`/`predicted` containers carry it so a consumer
|
|
461
|
+
// can compute freshness, and the trust block's ageDays keys off it.
|
|
391
462
|
const OWN_SELECT = includeTrust
|
|
392
|
-
? ["id", "agentId", "content", "durability", "createdAt", "supersedes", "subject", "validTo", "expiresAt", "_safetyFlags", "provenance", "usageCount", "validFrom"]
|
|
393
|
-
: ["id", "agentId", "content", "durability", "createdAt", "supersedes", "subject", "validTo", "expiresAt", "_safetyFlags"];
|
|
463
|
+
? ["id", "agentId", "content", "durability", "createdAt", "updatedAt", "supersedes", "subject", "validTo", "expiresAt", "_safetyFlags", "provenance", "usageCount", "validFrom"]
|
|
464
|
+
: ["id", "agentId", "content", "durability", "createdAt", "updatedAt", "supersedes", "subject", "validTo", "expiresAt", "_safetyFlags"];
|
|
394
465
|
// flair#744 slice 1 — the Memory records that became visible lines in the
|
|
395
466
|
// memory-bearing sections (permanent/recent/predicted/relevant/teammate),
|
|
396
467
|
// collected as they're added so the opt-in `trust` array below can carry a
|
|
397
468
|
// self-contained block per included memory. Stays empty (and unused) when
|
|
398
|
-
// includeTrust is off.
|
|
469
|
+
// includeTrust is off. flair#1201 — each entry carries the SECTION it landed
|
|
470
|
+
// in, so a trust entry's `matchQuality` is legible: null on a lifecycle
|
|
471
|
+
// section (permanent/recent/predicted — not a retrieval surface) reads as
|
|
472
|
+
// "not scored", not as a scoring failure, and a band on a retrieval section
|
|
473
|
+
// (relevant/teammate) is applied by the SAME rule to own and teammate
|
|
474
|
+
// records. Fixes the "own recent → null while teammate → strong looks like
|
|
475
|
+
// my own records scored worse" misread.
|
|
399
476
|
const includedTrustMemories = [];
|
|
400
477
|
// flair#744 slice 2 — the best absolute semantic similarity seen while
|
|
401
478
|
// scoring the task-relevant candidate pool (section 4). Drives the opt-in
|
|
@@ -451,12 +528,12 @@ export class BootstrapMemories extends Resource {
|
|
|
451
528
|
const permanent = permanentRows.filter((m) => !permanentSupersededIds.has(m.id));
|
|
452
529
|
for (const m of permanent) {
|
|
453
530
|
const line = formatMemory(m, agentId);
|
|
454
|
-
const cost = estimateTokens(line);
|
|
531
|
+
const cost = estimateTokens(line) + STRUCT_ITEM_OVERHEAD_TOKENS; // #1199 structured cost
|
|
455
532
|
if (cost <= tokenBudget) {
|
|
456
533
|
sections.permanent.push(line);
|
|
457
534
|
includedOwnMemories.push(leanMemory(m, "permanent"));
|
|
458
535
|
if (includeTrust)
|
|
459
|
-
includedTrustMemories.push(m);
|
|
536
|
+
includedTrustMemories.push({ m, section: "permanent" });
|
|
460
537
|
tokenBudget -= cost;
|
|
461
538
|
memoriesIncluded++;
|
|
462
539
|
}
|
|
@@ -523,7 +600,7 @@ export class BootstrapMemories extends Resource {
|
|
|
523
600
|
let recentSpent = 0;
|
|
524
601
|
for (const m of recent) {
|
|
525
602
|
const line = formatMemory(m, agentId);
|
|
526
|
-
const cost = estimateTokens(line);
|
|
603
|
+
const cost = estimateTokens(line) + STRUCT_ITEM_OVERHEAD_TOKENS; // #1199 structured cost
|
|
527
604
|
if (recentSpent + cost > recentBudget) {
|
|
528
605
|
memoriesTruncated++;
|
|
529
606
|
continue;
|
|
@@ -531,7 +608,7 @@ export class BootstrapMemories extends Resource {
|
|
|
531
608
|
sections.recent.push(line);
|
|
532
609
|
includedOwnMemories.push(leanMemory(m, "recent"));
|
|
533
610
|
if (includeTrust)
|
|
534
|
-
includedTrustMemories.push(m);
|
|
611
|
+
includedTrustMemories.push({ m, section: "recent" });
|
|
535
612
|
recentSpent += cost;
|
|
536
613
|
tokenBudget -= cost;
|
|
537
614
|
memoriesIncluded++;
|
|
@@ -566,7 +643,7 @@ export class BootstrapMemories extends Resource {
|
|
|
566
643
|
let predictedSpent = 0;
|
|
567
644
|
for (const m of subjectMemories) {
|
|
568
645
|
const line = formatMemory(m, agentId);
|
|
569
|
-
const cost = estimateTokens(line);
|
|
646
|
+
const cost = estimateTokens(line) + STRUCT_ITEM_OVERHEAD_TOKENS; // #1199 structured cost
|
|
570
647
|
if (predictedSpent + cost > predictedBudget) {
|
|
571
648
|
memoriesTruncated++;
|
|
572
649
|
continue;
|
|
@@ -574,7 +651,7 @@ export class BootstrapMemories extends Resource {
|
|
|
574
651
|
sections.predicted.push(line);
|
|
575
652
|
includedPredicted.push(leanMemory(m, "predicted"));
|
|
576
653
|
if (includeTrust)
|
|
577
|
-
includedTrustMemories.push(m);
|
|
654
|
+
includedTrustMemories.push({ m, section: "predicted" });
|
|
578
655
|
predictedSpent += cost;
|
|
579
656
|
tokenBudget -= cost;
|
|
580
657
|
memoriesIncluded++;
|
|
@@ -741,22 +818,42 @@ export class BootstrapMemories extends Resource {
|
|
|
741
818
|
// section double-spends.
|
|
742
819
|
for (const { memory: m } of scored) {
|
|
743
820
|
const line = formatMemory(m, agentId);
|
|
744
|
-
const cost = estimateTokens(line);
|
|
821
|
+
const cost = estimateTokens(line) + STRUCT_ITEM_OVERHEAD_TOKENS; // #1199 structured cost
|
|
745
822
|
if (cost > tokenBudget)
|
|
746
823
|
continue;
|
|
747
824
|
if (m._source) {
|
|
748
825
|
sections.teammate.push(line);
|
|
826
|
+
// flair#1199 — cross-agent teammate findings join their OWN
|
|
827
|
+
// structured container (attributed via `source`), so a connector
|
|
828
|
+
// consuming the containers still sees them when prose `context` is
|
|
829
|
+
// off. Counted separately (teammateFindingsIncluded), NOT into
|
|
830
|
+
// memoriesIncluded — that different-denominator mix is what let
|
|
831
|
+
// included exceed available.
|
|
832
|
+
includedTeammateFindings.push({
|
|
833
|
+
id: m.id,
|
|
834
|
+
content: m.content,
|
|
835
|
+
durability: m.durability ?? null,
|
|
836
|
+
createdAt: m.createdAt ?? null,
|
|
837
|
+
updatedAt: m.updatedAt ?? null,
|
|
838
|
+
subject: m.subject ?? null,
|
|
839
|
+
source: m._source,
|
|
840
|
+
section: "teammate",
|
|
841
|
+
});
|
|
842
|
+
if (includeTrust)
|
|
843
|
+
includedTrustMemories.push({ m, section: "teammate" });
|
|
844
|
+
tokenBudget -= cost;
|
|
845
|
+
teammateFindingsIncluded++;
|
|
749
846
|
}
|
|
750
847
|
else {
|
|
751
848
|
sections.relevant.push(line);
|
|
752
849
|
// flair#1182 — own task-relevant records join the `memories`
|
|
753
|
-
// container
|
|
850
|
+
// container.
|
|
754
851
|
includedOwnMemories.push(leanMemory(m, "relevant"));
|
|
852
|
+
if (includeTrust)
|
|
853
|
+
includedTrustMemories.push({ m, section: "relevant" });
|
|
854
|
+
tokenBudget -= cost;
|
|
855
|
+
memoriesIncluded++;
|
|
755
856
|
}
|
|
756
|
-
if (includeTrust)
|
|
757
|
-
includedTrustMemories.push(m);
|
|
758
|
-
tokenBudget -= cost;
|
|
759
|
-
memoriesIncluded++;
|
|
760
857
|
}
|
|
761
858
|
}
|
|
762
859
|
}
|
|
@@ -905,8 +1002,30 @@ export class BootstrapMemories extends Resource {
|
|
|
905
1002
|
continue;
|
|
906
1003
|
eventResults.push(event);
|
|
907
1004
|
}
|
|
908
|
-
|
|
909
|
-
|
|
1005
|
+
// flair#1200 — collapse byte-identical duplicate events before rendering.
|
|
1006
|
+
// The same logical event can land in the table more than once (a producer
|
|
1007
|
+
// that double-fires, or the same broadcast emitted from two paths); each
|
|
1008
|
+
// physical row has a distinct id/createdAt (OrgEvent.post keys the id off
|
|
1009
|
+
// a millisecond timestamp), so they aren't caught by primary-key upsert
|
|
1010
|
+
// and render as exact dupes. Org-event slots are scarce (10), so dedup
|
|
1011
|
+
// BEFORE the slice — otherwise ~half the slots are wasted on duplicates.
|
|
1012
|
+
// Keyed on the CONTENT (kind + summary + detail + targets), keeping the
|
|
1013
|
+
// most-recent occurrence per signature.
|
|
1014
|
+
const eventBySignature = new Map();
|
|
1015
|
+
for (const evt of eventResults) {
|
|
1016
|
+
const sig = JSON.stringify([
|
|
1017
|
+
evt.kind ?? "",
|
|
1018
|
+
evt.summary ?? "",
|
|
1019
|
+
evt.detail ?? "",
|
|
1020
|
+
Array.isArray(evt.targetIds) ? [...evt.targetIds].sort() : (evt.targetIds ?? null),
|
|
1021
|
+
]);
|
|
1022
|
+
const prev = eventBySignature.get(sig);
|
|
1023
|
+
if (!prev || (evt.createdAt || "") > (prev.createdAt || ""))
|
|
1024
|
+
eventBySignature.set(sig, evt);
|
|
1025
|
+
}
|
|
1026
|
+
const dedupedEvents = [...eventBySignature.values()]
|
|
1027
|
+
.sort((a, b) => (a.createdAt || "").localeCompare(b.createdAt || ""));
|
|
1028
|
+
for (const evt of dedupedEvents.slice(0, 10)) {
|
|
910
1029
|
const elapsed = Date.now() - new Date(evt.createdAt).getTime();
|
|
911
1030
|
const mins = Math.floor(elapsed / 60_000);
|
|
912
1031
|
const relTime = mins < 60 ? `${mins}min ago` : `${Math.floor(mins / 60)}h ago`;
|
|
@@ -959,9 +1078,27 @@ export class BootstrapMemories extends Resource {
|
|
|
959
1078
|
if (sections.events.length > 0) {
|
|
960
1079
|
parts.push("## Recent Org Events\n" + sections.events.join("\n"));
|
|
961
1080
|
}
|
|
962
|
-
const
|
|
1081
|
+
const fullContext = parts.join("\n\n");
|
|
1082
|
+
// flair#1199 — the structured containers are canonical; `context` is a prose
|
|
1083
|
+
// MIRROR of the same bytes. When includeContext is off (the /mcp default),
|
|
1084
|
+
// ship a compact structural pointer instead of re-embedding every body — so
|
|
1085
|
+
// no field's bytes cross the wire twice. Always a string, so a client can
|
|
1086
|
+
// still tell an empty instance from an unsupported one. When on, the prose
|
|
1087
|
+
// is the full assembled context (the resource/REST/CLI behaviour, unchanged).
|
|
1088
|
+
const context = includeContext
|
|
1089
|
+
? fullContext
|
|
1090
|
+
: (fullContext.length === 0
|
|
1091
|
+
? ""
|
|
1092
|
+
: `Structured payload in soul/memories/predicted/teammateFindings `
|
|
1093
|
+
+ `(${memoriesIncluded} own + ${teammateFindingsIncluded} teammate memories, `
|
|
1094
|
+
+ `${sections.soul.length} soul entries). Pass includeContext:true for the assembled prose context.`);
|
|
963
1095
|
const soulTokens = sections.soul.reduce((sum, line) => sum + estimateTokens(line), 0);
|
|
964
|
-
|
|
1096
|
+
// #1199 — memory-line token spend (informational breakdown), independent of
|
|
1097
|
+
// the reserve/soul now sharing the budget. Sum of the rendered memory lines.
|
|
1098
|
+
const memoryTokens = [
|
|
1099
|
+
...sections.permanent, ...sections.recent, ...sections.predicted,
|
|
1100
|
+
...sections.relevant, ...sections.teammate,
|
|
1101
|
+
].reduce((sum, line) => sum + estimateTokens(line), 0);
|
|
965
1102
|
// flair#744 slice 1 — opt-in per-memory trust block. Bootstrap renders
|
|
966
1103
|
// memories as text lines rather than result objects, so the block is
|
|
967
1104
|
// surfaced as a `trust` array of self-contained entries (each carries its
|
|
@@ -969,9 +1106,12 @@ export class BootstrapMemories extends Resource {
|
|
|
969
1106
|
// HERE, in the response tail, strictly after all read-scope resolution and
|
|
970
1107
|
// purely for the response — never consulted for any authority decision
|
|
971
1108
|
// (#735-spirit zero-authority invariant). Default OFF ⇒ the `trust` key is
|
|
972
|
-
// absent ⇒ the response is byte-identical to pre-slice-1.
|
|
1109
|
+
// absent ⇒ the response is byte-identical to pre-slice-1. flair#1201 — each
|
|
1110
|
+
// entry carries its `section` so `matchQuality: null` on a lifecycle section
|
|
1111
|
+
// reads as "not a retrieval surface", not as a scoring failure on the
|
|
1112
|
+
// caller's own records.
|
|
973
1113
|
const trust = includeTrust
|
|
974
|
-
? includedTrustMemories.map((m) => ({ id: m.id, ...buildTrustBlock(m) }))
|
|
1114
|
+
? includedTrustMemories.map(({ m, section }) => ({ id: m.id, section, ...buildTrustBlock(m) }))
|
|
975
1115
|
: undefined;
|
|
976
1116
|
// flair#744 slice 2 — opt-in abstention verdict for the task-relevance
|
|
977
1117
|
// surface. Present ONLY when `abstain` is requested (byte-identical to
|
|
@@ -1000,7 +1140,17 @@ export class BootstrapMemories extends Resource {
|
|
|
1000
1140
|
const currentTaskHint = taskProvided
|
|
1001
1141
|
? undefined
|
|
1002
1142
|
: "No currentTask was provided. Pass currentTask (a short description of what you're working on) to enable task-relevant memory retrieval, teammate findings, and collision surfacing.";
|
|
1003
|
-
|
|
1143
|
+
// flair#1199 — when `subjects` were provided but nothing surfaced in
|
|
1144
|
+
// `predicted`, say WHY (like currentTaskHint), so an empty `predicted: []`
|
|
1145
|
+
// next to a non-empty `subjects` doesn't read as broken. Predicted fills
|
|
1146
|
+
// from your OWN non-permanent memories whose `subject` matches one of the
|
|
1147
|
+
// provided subjects; it stays empty until you've tagged memories that way.
|
|
1148
|
+
const predictedHint = (predictedSubjects.length > 0 && includedPredicted.length === 0)
|
|
1149
|
+
? `No memories tagged with the requested subjects (${predictedSubjects.join(", ")}) were found. `
|
|
1150
|
+
+ `predicted surfaces your own non-permanent memories whose subject matches one of the provided `
|
|
1151
|
+
+ `subjects — it fills as you store memories tagged with these subjects.`
|
|
1152
|
+
: undefined;
|
|
1153
|
+
const responseBody = {
|
|
1004
1154
|
context,
|
|
1005
1155
|
// flair#1182 (part 1) — always-present self-describing keys: who the
|
|
1006
1156
|
// server resolved the caller as, the read model applied, and the caller's
|
|
@@ -1011,7 +1161,12 @@ export class BootstrapMemories extends Resource {
|
|
|
1011
1161
|
soul: soulMap,
|
|
1012
1162
|
memories: includedOwnMemories,
|
|
1013
1163
|
predicted: includedPredicted,
|
|
1164
|
+
// flair#1199 — cross-agent teammate findings as a structured container
|
|
1165
|
+
// (always present, `[]` when none), so a connector that consumes the
|
|
1166
|
+
// containers still gets them when prose `context` is off.
|
|
1167
|
+
teammateFindings: includedTeammateFindings,
|
|
1014
1168
|
...(currentTaskHint ? { currentTaskHint } : {}),
|
|
1169
|
+
...(predictedHint ? { predictedHint } : {}),
|
|
1015
1170
|
...(trust ? { trust } : {}),
|
|
1016
1171
|
...(abstention ? { abstention } : {}),
|
|
1017
1172
|
sections: {
|
|
@@ -1027,12 +1182,25 @@ export class BootstrapMemories extends Resource {
|
|
|
1027
1182
|
collision: sections.collision.length,
|
|
1028
1183
|
events: sections.events.length,
|
|
1029
1184
|
},
|
|
1030
|
-
tokenEstimate: soulTokens + memoryTokens,
|
|
1031
1185
|
soulTokens,
|
|
1032
1186
|
memoryTokens,
|
|
1187
|
+
// flair#1199 — own memories included (denominator: memoriesAvailable, also
|
|
1188
|
+
// own-scoped), so memoriesIncluded ≤ memoriesAvailable always holds.
|
|
1033
1189
|
memoriesIncluded,
|
|
1034
1190
|
memoriesAvailable,
|
|
1191
|
+
// Cross-agent teammate findings included — a SEPARATE denominator, labelled
|
|
1192
|
+
// so it's never confused with the own-memory counters.
|
|
1193
|
+
teammateFindingsIncluded,
|
|
1035
1194
|
memoriesTruncated,
|
|
1036
1195
|
};
|
|
1196
|
+
// flair#1199 — tokenEstimate must reflect the ACTUAL serialized payload the
|
|
1197
|
+
// caller receives (the structured containers included), not just the prose
|
|
1198
|
+
// `context`. The old `soulTokens + memoryTokens` counted only the context
|
|
1199
|
+
// string, so it under-reported by ~2× once the structured fields shipped
|
|
1200
|
+
// alongside — the reported "maxTokens 4000 → tokenEstimate 4275 while the
|
|
1201
|
+
// real payload was well over the cap". Measured over the assembled body
|
|
1202
|
+
// (the ~1-line tokenEstimate field it omits is negligible).
|
|
1203
|
+
const tokenEstimate = estimateTokens(JSON.stringify(responseBody));
|
|
1204
|
+
return { ...responseBody, tokenEstimate };
|
|
1037
1205
|
}
|
|
1038
1206
|
}
|
|
@@ -364,6 +364,13 @@ async function bootstrap(agent, args) {
|
|
|
364
364
|
surface: args?.surface,
|
|
365
365
|
subjects: args?.subjects,
|
|
366
366
|
entities: args?.entities,
|
|
367
|
+
// flair#1199 — a /mcp connector consumes the STRUCTURED containers
|
|
368
|
+
// (soul/memories/predicted/teammateFindings), so the prose `context` mirror
|
|
369
|
+
// is OFF by default here: shipping both doubled the payload past maxTokens
|
|
370
|
+
// (the reported ~2× overrun). The resource itself defaults includeContext
|
|
371
|
+
// true (the REST/CLI prose path); this wrapper flips it for the connector,
|
|
372
|
+
// and forwards an explicit true when a caller wants the prose anyway.
|
|
373
|
+
includeContext: args?.includeContext === true,
|
|
367
374
|
};
|
|
368
375
|
// flair#744 slice 1 — opt-in per-memory trust block array. Forwarded ONLY
|
|
369
376
|
// when requested so a plain bootstrap delegates a byte-identical body.
|
|
@@ -636,6 +643,7 @@ export const TOOLS = {
|
|
|
636
643
|
},
|
|
637
644
|
includeTrust: { type: "boolean", description: "Also return a `trust` array with a per-included-memory trust-evidence block (provenance, author, usage, freshness, supersession). Default false." },
|
|
638
645
|
abstain: { type: "boolean", description: "Opt into a task-relevance abstention verdict: also return an `abstention` object ({ abstained, bestScore, threshold }) reporting whether any memory covered `currentTask` above a global confidence threshold. Default false." },
|
|
646
|
+
includeContext: { type: "boolean", description: "Also return the prose `context` string — a human-readable mirror of the structured soul/memories/predicted/teammateFindings containers (which are the canonical payload). Default false here: the structured fields already carry everything, so shipping the prose too would double the payload." },
|
|
639
647
|
},
|
|
640
648
|
},
|
|
641
649
|
},
|
|
@@ -141,6 +141,7 @@ export function buildTrustBlock(record, now = Date.now()) {
|
|
|
141
141
|
const validFrom = typeof record.validFrom === "string" ? record.validFrom : null;
|
|
142
142
|
const validTo = typeof record.validTo === "string" ? record.validTo : null;
|
|
143
143
|
const createdAt = typeof record.createdAt === "string" ? record.createdAt : null;
|
|
144
|
+
const updatedAt = typeof record.updatedAt === "string" ? record.updatedAt : null;
|
|
144
145
|
const validToMs = parseTime(validTo);
|
|
145
146
|
const validFromMs = parseTime(validFrom);
|
|
146
147
|
let validityStatus = "valid";
|
|
@@ -150,9 +151,13 @@ export function buildTrustBlock(record, now = Date.now()) {
|
|
|
150
151
|
else if (Number.isFinite(validFromMs) && validFromMs > now) {
|
|
151
152
|
validityStatus = "future";
|
|
152
153
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
// #1201 — freshness keys off the record's OWN last-write time (updatedAt),
|
|
155
|
+
// falling back to createdAt. A record updated today must not read as stale
|
|
156
|
+
// off its original createdAt. updatedAt is the record's own field, so this
|
|
157
|
+
// reintroduces no lineage-inheritance (#1189).
|
|
158
|
+
const freshnessMs = parseTime(updatedAt ?? createdAt);
|
|
159
|
+
const ageDays = Number.isFinite(freshnessMs)
|
|
160
|
+
? Math.max(0, Math.floor((now - freshnessMs) / MS_PER_DAY))
|
|
156
161
|
: null;
|
|
157
162
|
return {
|
|
158
163
|
author: typeof record.agentId === "string" ? record.agentId : null,
|
|
@@ -165,6 +170,7 @@ export function buildTrustBlock(record, now = Date.now()) {
|
|
|
165
170
|
validFrom,
|
|
166
171
|
validTo,
|
|
167
172
|
createdAt,
|
|
173
|
+
updatedAt,
|
|
168
174
|
ageDays,
|
|
169
175
|
supersedes: typeof record.supersedes === "string" ? record.supersedes : null,
|
|
170
176
|
// flair#744 refinement — confidence band from the result's absolute
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tpsdev-ai/flair",
|
|
3
|
-
"version": "0.44.
|
|
3
|
+
"version": "0.44.8",
|
|
4
4
|
"packageManager": "bun@1.3.10",
|
|
5
5
|
"description": "Identity, memory, and soul for AI agents. Cryptographic identity (Ed25519), semantic memory with local embeddings, and persistent personality — all in a single process.",
|
|
6
6
|
"type": "module",
|