@tpsdev-ai/flair 0.20.1 → 0.22.0
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/README.md +39 -11
- package/SECURITY.md +52 -20
- package/config.yaml +32 -5
- package/dist/cli.js +2235 -260
- package/dist/deploy.js +96 -15
- package/dist/doctor-client.js +312 -0
- package/dist/fleet-presence.js +98 -0
- package/dist/fleet-verify.js +291 -0
- package/dist/install/clients.js +18 -0
- package/dist/mcp-client-assertion.js +364 -0
- package/dist/probe.js +97 -0
- package/dist/rem/restore.js +1 -1
- package/dist/rem/runner.js +82 -12
- package/dist/resources/Admin.js +1 -1
- package/dist/resources/AdminConnectors.js +1 -1
- package/dist/resources/AdminDashboard.js +1 -1
- package/dist/resources/AdminIdp.js +1 -1
- package/dist/resources/AdminInstance.js +1 -1
- package/dist/resources/AdminMemory.js +2 -2
- package/dist/resources/AdminPrincipals.js +1 -1
- package/dist/resources/Agent.js +14 -0
- package/dist/resources/AgentCard.js +1 -1
- package/dist/resources/AgentSeed.js +1 -1
- package/dist/resources/AttentionQuery.js +356 -0
- package/dist/resources/Credential.js +11 -1
- package/dist/resources/Federation.js +121 -2
- package/dist/resources/Integration.js +1 -1
- package/dist/resources/MCPClientMetadata.js +88 -0
- package/dist/resources/Memory.js +127 -22
- package/dist/resources/MemoryBootstrap.js +441 -85
- package/dist/resources/MemoryGrant.js +1 -1
- package/dist/resources/MemoryReflect.js +105 -49
- package/dist/resources/MemoryUsage.js +104 -0
- package/dist/resources/OAuth.js +69 -5
- package/dist/resources/OrgEvent.js +11 -0
- package/dist/resources/OrgEventCatchup.js +1 -1
- package/dist/resources/Presence.js +268 -17
- package/dist/resources/RecordUsage.js +230 -0
- package/dist/resources/Relationship.js +108 -26
- package/dist/resources/SemanticSearch.js +88 -320
- package/dist/resources/SkillScan.js +15 -0
- package/dist/resources/Soul.js +14 -0
- package/dist/resources/WorkspaceLatest.js +1 -1
- package/dist/resources/WorkspaceState.js +12 -1
- package/dist/resources/agent-auth.js +61 -3
- package/dist/resources/agentcard-fields.js +2 -2
- package/dist/resources/auth-middleware.js +42 -14
- package/dist/resources/bm25-filter.js +1 -1
- package/dist/resources/bm25.js +13 -7
- package/dist/resources/collision-lib.js +157 -0
- package/dist/resources/dedup.js +2 -2
- package/dist/resources/ed25519-auth.js +2 -2
- package/dist/resources/embeddings-boot.js +149 -0
- package/dist/resources/embeddings-provider.js +364 -106
- package/dist/resources/entity-vocab.js +139 -0
- package/dist/resources/federation-nonce-store.js +195 -0
- package/dist/resources/health.js +97 -6
- package/dist/resources/instance-identity.js +53 -0
- package/dist/resources/mcp-client-metadata-fields.js +112 -0
- package/dist/resources/mcp-handler.js +1 -1
- package/dist/resources/mcp-tools.js +88 -10
- package/dist/resources/memory-bootstrap-lib.js +1 -1
- package/dist/resources/memory-read-scope.js +58 -71
- package/dist/resources/memory-reflect-lib.js +289 -0
- package/dist/resources/memory-visibility.js +37 -0
- package/dist/resources/migration-boot.js +143 -0
- package/dist/resources/migrations/dir-safety.js +71 -0
- package/dist/resources/migrations/embedding-stamp.js +178 -0
- package/dist/resources/migrations/envelope.js +43 -0
- package/dist/resources/migrations/export.js +38 -0
- package/dist/resources/migrations/ledger.js +55 -0
- package/dist/resources/migrations/lock.js +154 -0
- package/dist/resources/migrations/progress.js +31 -0
- package/dist/resources/migrations/registry.js +36 -0
- package/dist/resources/migrations/risk-policy.js +23 -0
- package/dist/resources/migrations/runner.js +456 -0
- package/dist/resources/migrations/snapshot.js +127 -0
- package/dist/resources/migrations/source-fields.js +93 -0
- package/dist/resources/migrations/space.js +73 -0
- package/dist/resources/migrations/state.js +64 -0
- package/dist/resources/migrations/status.js +39 -0
- package/dist/resources/migrations/synthetic-test-migration.js +93 -0
- package/dist/resources/migrations/types.js +9 -0
- package/dist/resources/presence-internal.js +29 -0
- package/dist/resources/provenance.js +50 -0
- package/dist/resources/rate-limiter.js +8 -1
- package/dist/resources/scoring.js +124 -5
- package/dist/resources/semantic-retrieval-core.js +317 -0
- package/dist/version-check.js +167 -0
- package/dist/version-handshake.js +122 -0
- package/package.json +4 -5
- package/schemas/agent.graphql +7 -0
- package/schemas/event.graphql +5 -0
- package/schemas/federation.graphql +12 -0
- package/schemas/memory.graphql +82 -0
- package/schemas/schema.graphql +5 -43
- package/schemas/workspace.graphql +3 -0
- package/dist/resources/IngestEvents.js +0 -162
- package/dist/resources/IssueTokens.js +0 -19
- package/dist/resources/ObsAgentSnapshot.js +0 -13
- package/dist/resources/ObsEventFeed.js +0 -13
- package/dist/resources/ObsOffice.js +0 -19
- package/dist/resources/ObservationCenter.js +0 -23
- package/ui/observation-center.html +0 -385
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import { Resource, databases } from "@harperfast/harper";
|
|
2
|
-
import { allowVerified } from "./agent-auth.js";
|
|
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
5
|
import { isTeammate, formatTeamLine } from "./memory-bootstrap-lib.js";
|
|
6
6
|
import { resolveReadScope } from "./memory-read-scope.js";
|
|
7
|
+
import { isValidEntity } from "./entity-vocab.js";
|
|
8
|
+
import { withDetachedTxn } from "./table-helpers.js";
|
|
9
|
+
import { getPresenceRoster } from "./presence-internal.js";
|
|
10
|
+
import { buildCollisionEntries, buildEntityMatchCondition, freshPresenceByAgent, } from "./collision-lib.js";
|
|
11
|
+
// The bounded HNSW candidate-pool retrieval for the task-relevant/teammate/
|
|
12
|
+
// collision surfaces (flair-bootstrap-scale-fix) — the SAME pure core
|
|
13
|
+
// SemanticSearch.ts's post() wraps, called bare here so an internal
|
|
14
|
+
// bootstrap call never trips SemanticSearch's rate-limit/reranker/
|
|
15
|
+
// retrievalCount hit-tracking side effects (see resources/
|
|
16
|
+
// semantic-retrieval-core.ts's module doc for the full boundary).
|
|
17
|
+
import { retrieveCandidates } from "./semantic-retrieval-core.js";
|
|
7
18
|
/**
|
|
8
19
|
* POST /MemoryBootstrap
|
|
9
20
|
*
|
|
@@ -14,17 +25,31 @@ import { resolveReadScope } from "./memory-read-scope.js";
|
|
|
14
25
|
* 3. Recent memories (adaptive window)
|
|
15
26
|
* 4. Task-relevant memories (semantic search if currentTask provided)
|
|
16
27
|
* 4b. Teammate findings relevant to your task (flair#550 — the SAME scored
|
|
17
|
-
* task-relevant set as #4, split by origin:
|
|
18
|
-
*
|
|
19
|
-
* #4, attributed via "[via <agentId>]"
|
|
20
|
-
*
|
|
21
|
-
*
|
|
28
|
+
* task-relevant set as #4, split by origin: any other in-org agent's
|
|
29
|
+
* NON-PRIVATE memory that scores against currentTask lands here instead
|
|
30
|
+
* of #4, attributed via "[via <agentId>]" — no MemoryGrant required
|
|
31
|
+
* (open-within-org read, per #578). Presentation only — what's readable
|
|
32
|
+
* is entirely resolveReadScope()'s job; this only changes how an
|
|
33
|
+
* already-read cross-agent record is formatted/sectioned)
|
|
22
34
|
* 5. Relationship context (active relationships for mentioned entities)
|
|
23
35
|
* 6. Predicted context (based on channel/surface/subject hints)
|
|
24
36
|
* 7. Team roster (other active agents in this office + a search-first nudge —
|
|
25
|
-
* bootstrap loads the caller's own memories plus
|
|
26
|
-
*
|
|
27
|
-
* section nudges toward memory_search for
|
|
37
|
+
* bootstrap loads the caller's own memories plus every other in-org
|
|
38
|
+
* agent's non-private memories (open-within-org read, never anyone's
|
|
39
|
+
* private ones), so this section nudges toward memory_search for
|
|
40
|
+
* anything beyond that window)
|
|
41
|
+
* 8. Others in the room (flair#681, the attention-plane flagship —
|
|
42
|
+
* collision surfacing): joins two independently-scoped surfaces —
|
|
43
|
+
* WorkspaceState/OrgEvent entity overlap (exact vocabulary-string
|
|
44
|
+
* match against the caller's OWN declared `entities`, read via the
|
|
45
|
+
* SAME internal server-side path #678's AttentionQuery established —
|
|
46
|
+
* never broadening WorkspaceState's per-agent read model) and the
|
|
47
|
+
* semantic teammate-Memory matches #550 (above, 4b) ALREADY computed
|
|
48
|
+
* (no new embedding code — Memory is the semantic surface, WorkspaceState/
|
|
49
|
+
* OrgEvent are the entity surface, per the K&S verdict). Gated on
|
|
50
|
+
* freshness (Presence, via the SAME internal roster path, never the raw
|
|
51
|
+
* table) and #550's existing relevance floor. See resources/
|
|
52
|
+
* collision-lib.ts for the pure join/rank/format logic.
|
|
28
53
|
*
|
|
29
54
|
* Prediction: when context signals (channel, surface, subjects) are provided,
|
|
30
55
|
* the bootstrap loads more aggressively — Flair is fast enough that the
|
|
@@ -32,11 +57,51 @@ import { resolveReadScope } from "./memory-read-scope.js";
|
|
|
32
57
|
*
|
|
33
58
|
* Request:
|
|
34
59
|
* { agentId, currentTask?, maxTokens?, includeSoul?, since?,
|
|
35
|
-
* channel?, surface?, subjects? }
|
|
60
|
+
* channel?, surface?, subjects?, entities? }
|
|
61
|
+
* `entities` (flair#681): the caller's own declared attention-plane
|
|
62
|
+
* vocabulary strings (see resources/entity-vocab.ts) for collision
|
|
63
|
+
* surfacing's entity-overlap join. Invalid entries are silently dropped
|
|
64
|
+
* (never a 400 — this is an optional awareness hint, not a write path).
|
|
65
|
+
* When omitted, falls back to the caller's own most-recent WorkspaceState
|
|
66
|
+
* row's `entities`.
|
|
36
67
|
*
|
|
37
68
|
* Response:
|
|
38
69
|
* { context, sections, tokenEstimate, memoriesIncluded, memoriesAvailable }
|
|
39
70
|
*/
|
|
71
|
+
// Collision surfacing (flair#681) tunables.
|
|
72
|
+
const COLLISION_WINDOW_DAYS = 7;
|
|
73
|
+
const MAX_COLLISION_ENTRIES = 10;
|
|
74
|
+
// ─── Bootstrap scale fix (flair-bootstrap-scale-fix) tunables ───────────────
|
|
75
|
+
//
|
|
76
|
+
// Own-scoped, non-permanent memories (the "recent" adaptive-window source,
|
|
77
|
+
// ALSO reused as the "predicted" subject-match source — see the fetch below)
|
|
78
|
+
// are pulled bounded + createdAt-desc instead of the full org corpus. 500 is
|
|
79
|
+
// a generous ceiling: recent's own display is budget-limited (40% of
|
|
80
|
+
// remaining tokenBudget, in practice a handful of lines) and predicted's
|
|
81
|
+
// subject match is a narrow filter over the same set — an agent with more
|
|
82
|
+
// than 500 non-permanent memories in total would only miss an
|
|
83
|
+
// older-than-the-500th subject-tagged predicted candidate, a theoretical
|
|
84
|
+
// edge case traded for turning an O(org) scan into an O(own) bounded seek.
|
|
85
|
+
// If the recall harness ever shows this bound is too tight, widen it —
|
|
86
|
+
// never reintroduce the unbounded org-wide load.
|
|
87
|
+
const OWN_NONPERMANENT_FETCH_LIMIT = 500;
|
|
88
|
+
// Candidate-pool K formula (Kern-approved, K&S verdict on
|
|
89
|
+
// FLAIR-BOOTSTRAP-SCALE-FIX.md): K = max(3 × expected fill count,
|
|
90
|
+
// 5 × teammate count, MIN_CANDIDATE_POOL), capped at MAX_CANDIDATE_POOL.
|
|
91
|
+
// "Expected fill count" estimates how many formatted memory lines could fit
|
|
92
|
+
// in the remaining token budget (AVG_LINE_TOKEN_ESTIMATE is deliberately
|
|
93
|
+
// generous/low so the estimate — and thus K — errs LARGE, never small). The
|
|
94
|
+
// greedy token-budget fill loop AND collision's "one top cross-agent hit per
|
|
95
|
+
// teammate" (flair#681) both draw from this SAME pool, so it needs depth for
|
|
96
|
+
// both. If the recall harness ever shows a delta, widen K — never add a
|
|
97
|
+
// second scan (Kern's explicit instruction).
|
|
98
|
+
const AVG_LINE_TOKEN_ESTIMATE = 60;
|
|
99
|
+
const MIN_CANDIDATE_POOL = 50;
|
|
100
|
+
const MAX_CANDIDATE_POOL = 100;
|
|
101
|
+
// Bootstrap's own historical relevance floor (distinct from SemanticSearch's
|
|
102
|
+
// `minScore` request param) — preserved verbatim from the original raw
|
|
103
|
+
// JS dot-product scan's `.filter((s) => s.score > 0.3)`.
|
|
104
|
+
const TASK_RELEVANCE_FLOOR = 0.3;
|
|
40
105
|
// Rough token estimate: ~4 chars per token for English text
|
|
41
106
|
function estimateTokens(text) {
|
|
42
107
|
return Math.ceil(text.length / 4);
|
|
@@ -44,10 +109,11 @@ function estimateTokens(text) {
|
|
|
44
109
|
// `agentId` is the BOOTSTRAPPING agent (the caller) — used only to decide
|
|
45
110
|
// whether to annotate attribution, never to change what's read (that
|
|
46
111
|
// boundary is resolveReadScope()'s job, upstream of this function). A
|
|
47
|
-
// cross-agent record always carries `_source` (
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
// teammate's finding" check; own
|
|
112
|
+
// cross-agent record always carries `_source` (tagged by
|
|
113
|
+
// retrieveCandidates() — see resources/semantic-retrieval-core.ts — when the
|
|
114
|
+
// record's own agentId differs from the bootstrapping agent), so
|
|
115
|
+
// `m._source !== agentId` is the "is this a teammate's finding" check; own
|
|
116
|
+
// memories never carry `_source` at all.
|
|
51
117
|
function formatMemory(m, agentId) {
|
|
52
118
|
const tag = m.durability === "permanent" ? "🔒" : m.durability === "persistent" ? "📌" : "📝";
|
|
53
119
|
const date = m.createdAt ? ` (${m.createdAt.slice(0, 10)})` : "";
|
|
@@ -110,6 +176,7 @@ export class BootstrapMemories extends Resource {
|
|
|
110
176
|
relationships: [],
|
|
111
177
|
relevant: [],
|
|
112
178
|
teammate: [],
|
|
179
|
+
collision: [],
|
|
113
180
|
events: [],
|
|
114
181
|
};
|
|
115
182
|
let tokenBudget = maxTokens;
|
|
@@ -199,9 +266,10 @@ export class BootstrapMemories extends Resource {
|
|
|
199
266
|
}
|
|
200
267
|
// --- 1c. Team roster + cross-agent search nudge ---
|
|
201
268
|
// Soul is still caller-own-only (unaffected here). Memory loading below
|
|
202
|
-
// (step 2) now also includes
|
|
203
|
-
//
|
|
204
|
-
//
|
|
269
|
+
// (step 2) now also includes every other in-org agent's non-private
|
|
270
|
+
// memories (open-within-org read, no MemoryGrant needed — #578) — but
|
|
271
|
+
// this section stays: memory_search/SemanticSearch remains the
|
|
272
|
+
// deliberate, query-driven way to find a teammate's finding, vs.
|
|
205
273
|
// bootstrap's fixed recent/permanent window. This section is fixed-cost
|
|
206
274
|
// (no query text to format per agent) so it's cheap enough to always
|
|
207
275
|
// include, not budgeted.
|
|
@@ -211,8 +279,13 @@ export class BootstrapMemories extends Resource {
|
|
|
211
279
|
// records missing either field are legacy agents/active — a strict
|
|
212
280
|
// `!== "agent"` check would silently drop them. Assumes single-tenant
|
|
213
281
|
// (one instance = one office); grant-filtered roster is the multi-tenant follow-up.
|
|
282
|
+
// Hoisted out of the try block below (not just team-roster-local) — the
|
|
283
|
+
// task-relevant candidate pool's K formula (flair-bootstrap-scale-fix)
|
|
284
|
+
// needs the teammate count too. Stays `[]` on an Agent.search() failure
|
|
285
|
+
// (older/standalone deployments without the table), which K's formula
|
|
286
|
+
// tolerates fine (falls back to its other terms).
|
|
287
|
+
let teammateIds = [];
|
|
214
288
|
try {
|
|
215
|
-
const teammateIds = [];
|
|
216
289
|
for await (const record of databases.flair.Agent.search()) {
|
|
217
290
|
if (isTeammate(record, agentId))
|
|
218
291
|
teammateIds.push(record.id);
|
|
@@ -224,59 +297,104 @@ export class BootstrapMemories extends Resource {
|
|
|
224
297
|
catch {
|
|
225
298
|
// Agent table may not exist in older / standalone deployments
|
|
226
299
|
}
|
|
227
|
-
//
|
|
228
|
-
// Read-scope: own (any visibility) +
|
|
229
|
-
//
|
|
230
|
-
// in resolveReadScope():
|
|
231
|
-
// (so the table itself never
|
|
232
|
-
// `scope.isAllowed` re-checks
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
//
|
|
300
|
+
// ─── Read-scope (flair-bootstrap-scale-fix) ─────────────────────────────
|
|
301
|
+
// Read-scope: own (any visibility) + every OTHER in-org agent's
|
|
302
|
+
// non-private memory — open-within-org read (#578), no MemoryGrant
|
|
303
|
+
// consulted at all. Centralized in resolveReadScope(): `scope.condition`
|
|
304
|
+
// is pushed into every Harper query below (so the table itself never
|
|
305
|
+
// returns an out-of-scope row), and `scope.isAllowed` re-checks
|
|
306
|
+
// in-process as defense-in-depth on every candidate (Sherlock's
|
|
307
|
+
// non-negotiable — the pushdown condition is the primary gate, this is
|
|
308
|
+
// the belt) — this is the #550 foundation: bootstrap can now safely
|
|
309
|
+
// expand beyond own-only without a parallel scoping rule, and that rule
|
|
310
|
+
// tracks resolveReadScope()'s model automatically (this file never
|
|
311
|
+
// re-implements the rule, so it never has to change when the rule does).
|
|
312
|
+
//
|
|
313
|
+
// The org-wide "load everything, then filter/scan in JS" this section
|
|
314
|
+
// used to do (`Memory.search({conditions:[scope.condition]})`, no
|
|
315
|
+
// limit/select — every row's full embedding vector dragged into RAM on
|
|
316
|
+
// every bootstrap) is replaced by targeted, bounded queries per
|
|
317
|
+
// consumer: own-scoped pushdowns for the permanent/recent/predicted
|
|
318
|
+
// lifecycle slices below (agentId==self — strictly NARROWER than the
|
|
319
|
+
// full open-within-org scope, so no filter is dropped), a cheap
|
|
320
|
+
// own-scoped count for `memoriesAvailable`, and a bounded HNSW candidate
|
|
321
|
+
// pool (via retrieveCandidates(), further down) for the task-relevant/
|
|
322
|
+
// teammate/collision surfaces — the only consumer that legitimately
|
|
323
|
+
// spans the org. Each bounded query still re-checks `scope.isAllowed()`
|
|
324
|
+
// on every record even where it's provably a no-op (e.g. an
|
|
325
|
+
// agentId==self-only query) — uniform defense-in-depth, never skipped
|
|
326
|
+
// because "the filter already pushed down."
|
|
236
327
|
const scope = await resolveReadScope(agentId);
|
|
237
|
-
|
|
238
|
-
|
|
328
|
+
// `memoriesAvailable`: dropped the org-wide exact count (computing it
|
|
329
|
+
// exactly WAS the scan being removed — `visibility != private` isn't
|
|
330
|
+
// index-seekable, so even a bare count would scan). Replaced with the
|
|
331
|
+
// own-scoped count (`agentId==self`, cheap indexed seek) — a more
|
|
332
|
+
// meaningful "how much do I actually have" figure, and O(own) not
|
|
333
|
+
// O(org). Cosmetic change, called out to K&S in the spec.
|
|
334
|
+
let ownMemoriesAvailable = 0;
|
|
335
|
+
const availabilityRows = withDetachedTxn(ctx, () => databases.flair.Memory.search({
|
|
336
|
+
conditions: [{ attribute: "agentId", comparator: "equals", value: agentId }],
|
|
337
|
+
select: ["id", "expiresAt", "validTo"],
|
|
338
|
+
}));
|
|
339
|
+
for await (const record of availabilityRows) {
|
|
239
340
|
if (!scope.isAllowed(record))
|
|
240
341
|
continue;
|
|
241
342
|
if (record.expiresAt && Date.parse(record.expiresAt) < Date.now())
|
|
242
343
|
continue;
|
|
243
|
-
// ops-hesq: a past validTo ALWAYS means the record has been closed out
|
|
244
|
-
// (server supersede path — Memory.ts closeSupersededRecord — sets
|
|
245
|
-
// validTo without necessarily setting `archived`), same root cause and
|
|
246
|
-
// fix as ops-9rc6's SemanticSearch/bm25-filter exclusion. Unconditional
|
|
247
|
-
// so a server-superseded record can't resurface in bootstrap just
|
|
248
|
-
// because its successor isn't co-present in this result set (the
|
|
249
|
-
// supersededIds filter further down only catches co-presence). A
|
|
250
|
-
// record with no validTo, or a future validTo, is unaffected.
|
|
251
344
|
if (record.validTo && Date.parse(record.validTo) < Date.now())
|
|
252
345
|
continue;
|
|
253
|
-
|
|
254
|
-
// SemanticSearch.ts already uses: formatMemory() below only USES this
|
|
255
|
-
// when the record also carries _safetyFlags (labels the untrusted-data
|
|
256
|
-
// wrapper with whose memory it is), it never forces wrapping on its own.
|
|
257
|
-
// Real Harper's search() results are non-extensible objects — mutating
|
|
258
|
-
// `record._source = ...` directly throws ("object is not extensible");
|
|
259
|
-
// shallow-copy instead of mutating in place.
|
|
260
|
-
allMemories.push(record.agentId !== agentId ? { ...record, _source: record.agentId } : record);
|
|
346
|
+
ownMemoriesAvailable++;
|
|
261
347
|
}
|
|
262
|
-
memoriesAvailable =
|
|
263
|
-
//
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
348
|
+
memoriesAvailable = ownMemoriesAvailable;
|
|
349
|
+
// Fields every own-scoped lifecycle slice below needs — explicit (no raw
|
|
350
|
+
// `embedding`), matching the "select, no raw embedding" pushdown
|
|
351
|
+
// requirement.
|
|
352
|
+
const OWN_SELECT = ["id", "agentId", "content", "durability", "createdAt", "supersedes", "subject", "validTo", "expiresAt", "_safetyFlags"];
|
|
353
|
+
// --- 2. Permanent memories (always included, highest priority) ---
|
|
354
|
+
// Own-scoped pushdown: `agentId==self` + `durability==permanent`, both
|
|
355
|
+
// @indexed (a seek, not a scan) — strictly narrower than the prior
|
|
356
|
+
// load-then-filter (own records are always visible to their own agent
|
|
357
|
+
// regardless of visibility, so agentId==self alone is the correct,
|
|
358
|
+
// no-filter-dropped condition here; no other agent's data enters this
|
|
359
|
+
// query at all).
|
|
360
|
+
const permanentRows = [];
|
|
361
|
+
const permanentQuery = withDetachedTxn(ctx, () => databases.flair.Memory.search({
|
|
362
|
+
conditions: [
|
|
363
|
+
{ attribute: "agentId", comparator: "equals", value: agentId },
|
|
364
|
+
{ attribute: "durability", comparator: "equals", value: "permanent" },
|
|
365
|
+
],
|
|
366
|
+
select: OWN_SELECT,
|
|
367
|
+
}));
|
|
368
|
+
for await (const record of permanentQuery) {
|
|
369
|
+
if (!scope.isAllowed(record))
|
|
370
|
+
continue;
|
|
371
|
+
if (record.expiresAt && Date.parse(record.expiresAt) < Date.now())
|
|
372
|
+
continue;
|
|
373
|
+
// A past validTo ALWAYS means the record has been closed out (server
|
|
374
|
+
// supersede path — Memory.ts closeSupersededRecord — sets validTo
|
|
375
|
+
// without necessarily setting `archived`), same root cause and fix as
|
|
376
|
+
// SemanticSearch.ts's unconditional past-validTo exclusion.
|
|
377
|
+
// Unconditional so a server-superseded record can't resurface just
|
|
378
|
+
// because its successor isn't co-present in THIS bounded set (the
|
|
379
|
+
// per-set supersededIds filter below only catches co-presence).
|
|
380
|
+
if (record.validTo && Date.parse(record.validTo) < Date.now())
|
|
381
|
+
continue;
|
|
382
|
+
permanentRows.push(record);
|
|
268
383
|
}
|
|
269
|
-
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
const
|
|
279
|
-
|
|
384
|
+
// Per-set supersededIds (flair-bootstrap-scale-fix, Kern-approved
|
|
385
|
+
// narrowing): computed from THIS bounded set alone, never cross-applied
|
|
386
|
+
// to the recent/predicted sets or the candidate pool below — each is
|
|
387
|
+
// independent. The uncovered case (predecessor and successor landing in
|
|
388
|
+
// DIFFERENT bounded sets) is a theoretical gap already covered by the
|
|
389
|
+
// unconditional past-validTo guard above (the primary supersede
|
|
390
|
+
// defense); this co-presence check is a secondary belt, same as before
|
|
391
|
+
// this refactor, just now scoped per-set instead of per the old
|
|
392
|
+
// org-wide load.
|
|
393
|
+
const permanentSupersededIds = new Set();
|
|
394
|
+
for (const m of permanentRows)
|
|
395
|
+
if (m.supersedes)
|
|
396
|
+
permanentSupersededIds.add(m.supersedes);
|
|
397
|
+
const permanent = permanentRows.filter((m) => !permanentSupersededIds.has(m.id));
|
|
280
398
|
for (const m of permanent) {
|
|
281
399
|
const line = formatMemory(m, agentId);
|
|
282
400
|
const cost = estimateTokens(line);
|
|
@@ -290,10 +408,43 @@ export class BootstrapMemories extends Resource {
|
|
|
290
408
|
}
|
|
291
409
|
}
|
|
292
410
|
// --- 3. Recent memories (adaptive window) ---
|
|
411
|
+
// Own-scoped, non-permanent, bounded + createdAt-desc pushdown (agentId
|
|
412
|
+
// and durability are both @indexed) — replaces the org-wide load's
|
|
413
|
+
// post-hoc JS filter. This SAME fetched set also feeds "predicted"
|
|
414
|
+
// (3b, below): both draw from "my own non-permanent memories" bounded to
|
|
415
|
+
// OWN_NONPERMANENT_FETCH_LIMIT (see that constant's doc for the bound's
|
|
416
|
+
// rationale) — same shared-source relationship the pre-refactor code
|
|
417
|
+
// had via `ownMemories`, just bounded now instead of org-wide.
|
|
418
|
+
const nonPermanentRows = [];
|
|
419
|
+
const nonPermanentQuery = withDetachedTxn(ctx, () => databases.flair.Memory.search({
|
|
420
|
+
conditions: [
|
|
421
|
+
{ attribute: "agentId", comparator: "equals", value: agentId },
|
|
422
|
+
{ attribute: "durability", comparator: "not_equal", value: "permanent" },
|
|
423
|
+
],
|
|
424
|
+
select: OWN_SELECT,
|
|
425
|
+
sort: { attribute: "createdAt", descending: true },
|
|
426
|
+
limit: OWN_NONPERMANENT_FETCH_LIMIT,
|
|
427
|
+
}));
|
|
428
|
+
for await (const record of nonPermanentQuery) {
|
|
429
|
+
if (!scope.isAllowed(record))
|
|
430
|
+
continue;
|
|
431
|
+
if (record.expiresAt && Date.parse(record.expiresAt) < Date.now())
|
|
432
|
+
continue;
|
|
433
|
+
if (record.validTo && Date.parse(record.validTo) < Date.now())
|
|
434
|
+
continue;
|
|
435
|
+
nonPermanentRows.push(record);
|
|
436
|
+
}
|
|
437
|
+
// Per-set supersededIds — independent of `permanentSupersededIds` above
|
|
438
|
+
// (see that block's doc for the full per-set rationale).
|
|
439
|
+
const nonPermanentSupersededIds = new Set();
|
|
440
|
+
for (const m of nonPermanentRows)
|
|
441
|
+
if (m.supersedes)
|
|
442
|
+
nonPermanentSupersededIds.add(m.supersedes);
|
|
443
|
+
const nonPermanentActive = nonPermanentRows.filter((m) => !nonPermanentSupersededIds.has(m.id));
|
|
293
444
|
// Start with 48h. If nothing found, widen to 7d, then 30d.
|
|
294
445
|
// This prevents empty recent sections for agents that were idle.
|
|
295
|
-
const nonPermanent =
|
|
296
|
-
.filter((m) => m.
|
|
446
|
+
const nonPermanent = nonPermanentActive
|
|
447
|
+
.filter((m) => m.createdAt)
|
|
297
448
|
.sort((a, b) => (b.createdAt || "").localeCompare(a.createdAt || ""));
|
|
298
449
|
let effectiveSince;
|
|
299
450
|
if (since) {
|
|
@@ -338,7 +489,13 @@ export class BootstrapMemories extends Resource {
|
|
|
338
489
|
...permanent.map((m) => m.id),
|
|
339
490
|
...recent.filter((_, i) => i < sections.recent.length).map((m) => m.id),
|
|
340
491
|
]);
|
|
341
|
-
|
|
492
|
+
// Draws from the SAME bounded own-scoped, non-permanent set "recent"
|
|
493
|
+
// uses (nonPermanentActive — see that fetch's doc above for the
|
|
494
|
+
// shared-source rationale and OWN_NONPERMANENT_FETCH_LIMIT's bound).
|
|
495
|
+
// `durability !== "permanent"` is now redundant with the source
|
|
496
|
+
// query's own condition but kept for parity/clarity with the
|
|
497
|
+
// pre-refactor filter shape.
|
|
498
|
+
const subjectMemories = nonPermanentActive
|
|
342
499
|
.filter((m) => !includedIds.has(m.id) &&
|
|
343
500
|
m.subject &&
|
|
344
501
|
predictedSubjects.includes(m.subject.toLowerCase()) &&
|
|
@@ -394,11 +551,21 @@ export class BootstrapMemories extends Resource {
|
|
|
394
551
|
// Relationship table may not exist yet
|
|
395
552
|
}
|
|
396
553
|
}
|
|
554
|
+
// Collision surfacing's semantic-match candidates (flair#681) — the
|
|
555
|
+
// BEST (highest-scoring) cross-agent memory per teammate from #550's
|
|
556
|
+
// `scored` list below, captured here (before that list's tokens get
|
|
557
|
+
// spent on the relevant/teammate sections) so the collision block can
|
|
558
|
+
// reuse the IDENTICAL scored+floor-gated set without recomputing or
|
|
559
|
+
// re-embedding anything. Stays empty when there's no currentTask (no
|
|
560
|
+
// `scored` list is ever built) or no cross-agent hits.
|
|
561
|
+
const semanticTeammateMatches = [];
|
|
397
562
|
// --- 4. Task-relevant memories (semantic search) ---
|
|
398
563
|
if (currentTask && tokenBudget > 200) {
|
|
399
564
|
let queryEmbedding = null;
|
|
400
565
|
try {
|
|
401
|
-
|
|
566
|
+
// flair#504 Phase 2: 'query' — currentTask is the bootstrap's
|
|
567
|
+
// task-relevance search query, not stored content.
|
|
568
|
+
queryEmbedding = await getEmbedding(currentTask, "query");
|
|
402
569
|
}
|
|
403
570
|
catch { }
|
|
404
571
|
if (queryEmbedding) {
|
|
@@ -407,25 +574,79 @@ export class BootstrapMemories extends Resource {
|
|
|
407
574
|
...permanent.map((m) => m.id),
|
|
408
575
|
...recent.filter((_, i) => i < sections.recent.length).map((m) => m.id),
|
|
409
576
|
]);
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
577
|
+
// Bounded HNSW candidate pool (flair-bootstrap-scale-fix) — replaces
|
|
578
|
+
// the full-corpus JS dot-product scan (`allMemories` × queryEmbedding,
|
|
579
|
+
// O(org corpus size) every bootstrap). K formula (Kern-approved):
|
|
580
|
+
// max(3 × expected fill, 5 × teammate count, MIN_CANDIDATE_POOL),
|
|
581
|
+
// capped at MAX_CANDIDATE_POOL — deep enough for BOTH the
|
|
582
|
+
// token-budget fill loop below AND collision's "one top cross-agent
|
|
583
|
+
// hit per teammate" (flair#681), which draws from the SAME pool. If
|
|
584
|
+
// the recall harness ever shows a delta, widen K — never add a
|
|
585
|
+
// second scan (Kern's explicit instruction).
|
|
586
|
+
const expectedFill = Math.max(1, Math.ceil(tokenBudget / AVG_LINE_TOKEN_ESTIMATE));
|
|
587
|
+
const candidatePoolK = Math.min(MAX_CANDIDATE_POOL, Math.max(3 * expectedFill, 5 * teammateIds.length, MIN_CANDIDATE_POOL));
|
|
588
|
+
const candidates = await retrieveCandidates({
|
|
589
|
+
queryEmbedding,
|
|
590
|
+
conditions: [scope.condition],
|
|
591
|
+
limit: candidatePoolK,
|
|
592
|
+
// HNSW-leg pushdown ONLY (K&S verdict): no BM25 fusion, no
|
|
593
|
+
// reranker for bootstrap — a different cost profile (BM25 over the
|
|
594
|
+
// org corpus for a one-shot session-load could be MORE expensive
|
|
595
|
+
// than HNSW-only unless cached across sessions; the reranker is a
|
|
596
|
+
// generative call per candidate). Both are explicit opt-in
|
|
597
|
+
// follow-ons, gated on their own harness runs.
|
|
598
|
+
hybrid: false,
|
|
599
|
+
// Per-set (this K-bounded pool only, never cross-applied to the
|
|
600
|
+
// permanent/recent/predicted sets above) — see this function's
|
|
601
|
+
// supersededIds docs above and resources/
|
|
602
|
+
// semantic-retrieval-core.ts's own doc for the full caveat. The
|
|
603
|
+
// unconditional past-validTo guard (inside retrieveCandidates)
|
|
604
|
+
// stays the primary supersede defense either way.
|
|
605
|
+
includeSuperseded: false,
|
|
606
|
+
// Matches the original raw JS dot product exactly — no
|
|
607
|
+
// composite/durability-recency weighting for bootstrap's own
|
|
608
|
+
// relevance ranking.
|
|
609
|
+
scoring: "raw",
|
|
610
|
+
agentId,
|
|
611
|
+
// Sherlock's non-negotiable belt: re-checked on every candidate
|
|
612
|
+
// even though `conditions` already scoped the query.
|
|
613
|
+
isAllowed: scope.isAllowed,
|
|
614
|
+
ctx,
|
|
615
|
+
});
|
|
616
|
+
// Preserve the ORIGINAL score > 0.3 floor exactly (bootstrap's own
|
|
617
|
+
// historical relevance floor — distinct from SemanticSearch's
|
|
618
|
+
// `minScore` request param — strict inequality, applied client-side;
|
|
619
|
+
// `candidates` are already `_score`-sorted best-first, so filtering
|
|
620
|
+
// preserves that order). `retrieveCandidates()`'s cosine similarity
|
|
621
|
+
// replaces the raw JS dot product as the ranking signal (HNSW-only,
|
|
622
|
+
// no BM25/rerank) — the K&S-ratified, closest-to-a-wash choice; the
|
|
623
|
+
// recall harness gates any regression from this ranking-signal
|
|
624
|
+
// change (magnitude-sensitive dot product → normalized cosine).
|
|
625
|
+
const scored = candidates
|
|
626
|
+
.filter((m) => !includedIds.has(m.id) && m._score > TASK_RELEVANCE_FLOOR)
|
|
627
|
+
.map((m) => ({ memory: m, score: m._score }));
|
|
628
|
+
// flair#681: the collision block's semantic surface — one candidate
|
|
629
|
+
// per teammate (the highest-scoring hit; `scored` is already sorted
|
|
630
|
+
// desc, so the first occurrence of a given `_source` IS the best
|
|
631
|
+
// one). `m._source` is only ever set for a cross-agent record (see
|
|
632
|
+
// retrieveCandidates()'s `_source` tagging) — an own memory never
|
|
633
|
+
// contributes here.
|
|
634
|
+
const seenCollisionAgents = new Set();
|
|
635
|
+
for (const { memory: m, score } of scored) {
|
|
636
|
+
if (!m._source || seenCollisionAgents.has(m._source))
|
|
637
|
+
continue;
|
|
638
|
+
seenCollisionAgents.add(m._source);
|
|
639
|
+
semanticTeammateMatches.push({ agentId: m._source, score, content: m.summary || m.content || "" });
|
|
640
|
+
}
|
|
421
641
|
// #550: split the scored, task-relevant set by origin. Own findings
|
|
422
|
-
// go to `relevant` as before;
|
|
423
|
-
// read-scoped by
|
|
424
|
-
//
|
|
425
|
-
// `teammate` section so the agent can tell it apart
|
|
426
|
-
// Both draw from the SAME `tokenBudget` in one
|
|
427
|
-
// highest-relevance memories win the remaining
|
|
428
|
-
// which section they land in, so neither
|
|
642
|
+
// go to `relevant` as before; any other in-org agent's non-private
|
|
643
|
+
// record — already read-scoped by resolveReadScope(), no grant
|
|
644
|
+
// required (`m._source` is only ever set for a cross-agent record) —
|
|
645
|
+
// goes to the new `teammate` section so the agent can tell it apart
|
|
646
|
+
// at a glance. Both draw from the SAME `tokenBudget` in one
|
|
647
|
+
// score-ordered pass — highest-relevance memories win the remaining
|
|
648
|
+
// budget regardless of which section they land in, so neither
|
|
649
|
+
// section double-spends.
|
|
429
650
|
for (const { memory: m } of scored) {
|
|
430
651
|
const line = formatMemory(m, agentId);
|
|
431
652
|
const cost = estimateTokens(line);
|
|
@@ -442,6 +663,133 @@ export class BootstrapMemories extends Resource {
|
|
|
442
663
|
}
|
|
443
664
|
}
|
|
444
665
|
}
|
|
666
|
+
// --- 4c. Collision surfacing (flair#681 — "others in the room") ---
|
|
667
|
+
// Joins two independently-scoped surfaces into a single ranked list:
|
|
668
|
+
// - Entity overlap (WorkspaceState + OrgEvent): exact vocabulary-string
|
|
669
|
+
// match, high-precision, no separate relevance score needed.
|
|
670
|
+
// - Semantic match (Memory, via #550/4 above): `semanticTeammateMatches`,
|
|
671
|
+
// already floor-gated (score > 0.3) — reused as-is, no new scoring.
|
|
672
|
+
// Gated on freshness (Presence, via the internal roster path) — a
|
|
673
|
+
// teammate absent from the roster, or whose presenceStatus is "offline",
|
|
674
|
+
// never surfaces regardless of how strong the entity/semantic match is.
|
|
675
|
+
// Best-effort: any failure here (WorkspaceState/OrgEvent/Presence briefly
|
|
676
|
+
// unavailable) must never break bootstrap's core memory context.
|
|
677
|
+
try {
|
|
678
|
+
// The caller's own declared entities: an explicit `entities` field on
|
|
679
|
+
// the request (validated against the SAME closed vocabulary every
|
|
680
|
+
// write path gates writes on — resources/entity-vocab.ts; invalid
|
|
681
|
+
// entries are silently dropped, not a 400, since this is an optional
|
|
682
|
+
// awareness hint), falling back to the caller's own most-recent
|
|
683
|
+
// WorkspaceState row's `entities` when not declared. Reading the
|
|
684
|
+
// caller's OWN WorkspaceState rows is not a scoping concern (an agent
|
|
685
|
+
// always has read access to its own data) — this raw read exists
|
|
686
|
+
// purely because MemoryBootstrap.ts already reads every other table
|
|
687
|
+
// (Soul/Agent/Memory/Relationship/OrgEvent) directly, the same idiom.
|
|
688
|
+
let callerEntities = Array.isArray(data?.entities)
|
|
689
|
+
? data.entities.filter((e) => isValidEntity(e))
|
|
690
|
+
: [];
|
|
691
|
+
if (callerEntities.length === 0) {
|
|
692
|
+
const ownRows = withDetachedTxn(ctx, () => databases.flair.WorkspaceState.search({
|
|
693
|
+
conditions: [{ attribute: "agentId", comparator: "equals", value: agentId }],
|
|
694
|
+
select: ["entities", "timestamp"],
|
|
695
|
+
}));
|
|
696
|
+
let latestEntities = [];
|
|
697
|
+
let latestTs = "";
|
|
698
|
+
for await (const row of ownRows) {
|
|
699
|
+
if (!Array.isArray(row.entities) || row.entities.length === 0)
|
|
700
|
+
continue;
|
|
701
|
+
if ((row.timestamp || "") > latestTs) {
|
|
702
|
+
latestTs = row.timestamp || "";
|
|
703
|
+
latestEntities = row.entities;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
callerEntities = latestEntities;
|
|
707
|
+
}
|
|
708
|
+
const entityMatches = [];
|
|
709
|
+
if (callerEntities.length > 0) {
|
|
710
|
+
const sinceIso = new Date(Date.now() - COLLISION_WINDOW_DAYS * 24 * 3600_000).toISOString();
|
|
711
|
+
// buildEntityMatchCondition, NOT a hand-rolled OR wrapper: Harper's
|
|
712
|
+
// query engine throws ("An 'or' operator requires at least two
|
|
713
|
+
// conditions") for a single-entity OR condition — see collision-lib.ts's
|
|
714
|
+
// doc. A single declared entity is the common case, so this matters.
|
|
715
|
+
const entityCondition = buildEntityMatchCondition(callerEntities);
|
|
716
|
+
const byAgent = new Map();
|
|
717
|
+
// WorkspaceState — the INTERNAL server-side path (Sherlock Option 1,
|
|
718
|
+
// binding per the K&S verdict): the RAW generated table object,
|
|
719
|
+
// never the exported `WorkspaceState` resource class — that class's
|
|
720
|
+
// search() re-applies strict per-agent scoping keyed off THIS
|
|
721
|
+
// caller's own identity, which would just filter every teammate's
|
|
722
|
+
// row back out. This does NOT broaden WorkspaceState's general
|
|
723
|
+
// (still per-agent, still 403) read model — see resources/
|
|
724
|
+
// AttentionQuery.ts's module doc for the full rationale (the exact
|
|
725
|
+
// pattern this reuses).
|
|
726
|
+
const wsRows = withDetachedTxn(ctx, () => databases.flair.WorkspaceState.search({
|
|
727
|
+
conditions: [entityCondition, { attribute: "timestamp", comparator: "greater_than_equal", value: sinceIso }],
|
|
728
|
+
select: ["agentId", "entities", "summary", "taskId", "timestamp"],
|
|
729
|
+
}));
|
|
730
|
+
for await (const row of wsRows) {
|
|
731
|
+
if (row.agentId === agentId)
|
|
732
|
+
continue; // exclude self
|
|
733
|
+
const overlap = (Array.isArray(row.entities) ? row.entities : []).filter((e) => callerEntities.includes(e));
|
|
734
|
+
if (overlap.length === 0)
|
|
735
|
+
continue;
|
|
736
|
+
const candidate = {
|
|
737
|
+
agentId: row.agentId, entities: overlap, summary: row.summary ?? null,
|
|
738
|
+
taskId: row.taskId ?? null, timestamp: row.timestamp, source: "workspace",
|
|
739
|
+
};
|
|
740
|
+
const existing = byAgent.get(row.agentId);
|
|
741
|
+
if (!existing || existing.timestamp < candidate.timestamp)
|
|
742
|
+
byAgent.set(row.agentId, candidate);
|
|
743
|
+
}
|
|
744
|
+
// OrgEvent — org-open read model, no per-agent scoping to respect
|
|
745
|
+
// (mirrors resources/AttentionQuery.ts's queryOrgEvent).
|
|
746
|
+
const evRows = withDetachedTxn(ctx, () => databases.flair.OrgEvent.search({
|
|
747
|
+
conditions: [entityCondition, { attribute: "createdAt", comparator: "greater_than_equal", value: sinceIso }],
|
|
748
|
+
select: ["authorId", "entities", "summary", "createdAt", "expiresAt"],
|
|
749
|
+
}));
|
|
750
|
+
const now = Date.now();
|
|
751
|
+
for await (const row of evRows) {
|
|
752
|
+
if (row.authorId === agentId)
|
|
753
|
+
continue; // exclude self
|
|
754
|
+
if (row.expiresAt && new Date(row.expiresAt).getTime() < now)
|
|
755
|
+
continue;
|
|
756
|
+
const overlap = (Array.isArray(row.entities) ? row.entities : []).filter((e) => callerEntities.includes(e));
|
|
757
|
+
if (overlap.length === 0)
|
|
758
|
+
continue;
|
|
759
|
+
const candidate = {
|
|
760
|
+
agentId: row.authorId, entities: overlap, summary: row.summary ?? null,
|
|
761
|
+
taskId: null, timestamp: row.createdAt, source: "event",
|
|
762
|
+
};
|
|
763
|
+
const existing = byAgent.get(row.authorId);
|
|
764
|
+
if (!existing || existing.timestamp < candidate.timestamp)
|
|
765
|
+
byAgent.set(row.authorId, candidate);
|
|
766
|
+
}
|
|
767
|
+
entityMatches.push(...byAgent.values());
|
|
768
|
+
}
|
|
769
|
+
// Freshness gate: the SAME internal Presence roster path #678
|
|
770
|
+
// established (never the raw table) — see resources/
|
|
771
|
+
// presence-internal.ts. `resolveAgentAuth` is called independently
|
|
772
|
+
// here (not reusing the manual agentId-scoping derivation above,
|
|
773
|
+
// which is deliberately narrow per its own bug-fix comment) purely to
|
|
774
|
+
// build the delegation verdict this internal read needs.
|
|
775
|
+
const collisionAuth = await resolveAgentAuth(ctx);
|
|
776
|
+
const roster = await getPresenceRoster(collisionAuth);
|
|
777
|
+
const freshByAgent = freshPresenceByAgent(roster);
|
|
778
|
+
const collisionEntries = buildCollisionEntries(entityMatches, semanticTeammateMatches, freshByAgent, agentId);
|
|
779
|
+
for (const entry of collisionEntries.slice(0, MAX_COLLISION_ENTRIES)) {
|
|
780
|
+
const line = `- ${entry.line}`;
|
|
781
|
+
const cost = estimateTokens(line);
|
|
782
|
+
if (cost > tokenBudget)
|
|
783
|
+
continue;
|
|
784
|
+
sections.collision.push(line);
|
|
785
|
+
tokenBudget -= cost;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
catch {
|
|
789
|
+
// Collision surfacing is best-effort awareness, never a hard
|
|
790
|
+
// dependency — WorkspaceState/OrgEvent/Presence being briefly
|
|
791
|
+
// unavailable must not break bootstrap's core memory context.
|
|
792
|
+
}
|
|
445
793
|
// --- 5. Recent OrgEvents for this agent ---
|
|
446
794
|
try {
|
|
447
795
|
const eventSince = data?.lastBootAt
|
|
@@ -499,11 +847,18 @@ export class BootstrapMemories extends Resource {
|
|
|
499
847
|
}
|
|
500
848
|
// #550: teammate findings relevant to the current task — right after the
|
|
501
849
|
// agent's own task-relevant knowledge. Empty section renders nothing
|
|
502
|
-
// (no header) so a bootstrap with no
|
|
850
|
+
// (no header) so a bootstrap with no task-relevant teammate findings for
|
|
503
851
|
// this task looks exactly as it did before this feature.
|
|
504
852
|
if (sections.teammate.length > 0) {
|
|
505
853
|
parts.push("## Teammate findings relevant to your task\n" + sections.teammate.join("\n"));
|
|
506
854
|
}
|
|
855
|
+
// flair#681: the attention-plane flagship — "the office moment". Empty
|
|
856
|
+
// section renders nothing (no header), same convention as every other
|
|
857
|
+
// optional section here: no entity/semantic overlap with a fresh
|
|
858
|
+
// teammate looks exactly like bootstrap did before this feature.
|
|
859
|
+
if (sections.collision.length > 0) {
|
|
860
|
+
parts.push("## Others in the room\n" + sections.collision.join("\n"));
|
|
861
|
+
}
|
|
507
862
|
if (sections.events.length > 0) {
|
|
508
863
|
parts.push("## Recent Org Events\n" + sections.events.join("\n"));
|
|
509
864
|
}
|
|
@@ -522,6 +877,7 @@ export class BootstrapMemories extends Resource {
|
|
|
522
877
|
relationships: sections.relationships.length,
|
|
523
878
|
relevant: sections.relevant.length,
|
|
524
879
|
teammate: sections.teammate.length,
|
|
880
|
+
collision: sections.collision.length,
|
|
525
881
|
events: sections.events.length,
|
|
526
882
|
},
|
|
527
883
|
tokenEstimate: soulTokens + memoryTokens,
|