@tpsdev-ai/flair 0.24.0 → 0.25.1
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 +13 -4
- package/dist/cli.js +32 -11
- package/dist/resources/Memory.js +75 -3
- package/dist/resources/MemoryBootstrap.js +85 -3
- package/dist/resources/RecordUsage.js +32 -80
- package/dist/resources/SemanticSearch.js +71 -3
- package/dist/resources/abstention.js +146 -0
- package/dist/resources/mcp-tools.js +42 -5
- package/dist/resources/semantic-retrieval-core.js +24 -2
- package/dist/resources/trust-block.js +187 -0
- package/dist/resources/usage-recording.js +160 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -134,6 +134,15 @@ flair memory search "native addon loading in sandboxed runtimes"
|
|
|
134
134
|
# → [0.67] Harper v5 sandbox blocks node:module but process.dlopen works
|
|
135
135
|
```
|
|
136
136
|
|
|
137
|
+
### Trust-Graded Recall
|
|
138
|
+
Recall can carry an opt-in **trust-evidence block** per result — provenance (verified vs claimed author), usage signal, freshness/validity, supersession — so an agent weighs *what to trust*, not just *what matched*. On top of it:
|
|
139
|
+
|
|
140
|
+
- **Confidence bands** (`matchQuality`): each result is labeled `strong` / `moderate` / `breadcrumb` from its absolute similarity — a weak-but-relevant hit is taken for what it is, not mistaken for a confident one.
|
|
141
|
+
- **First-class abstention** (`abstain`): when nothing clears a confidence floor, recall returns an honest "no memory covers this" verdict instead of the N weakest matches.
|
|
142
|
+
- **Citation-on-write** (`usedMemoryIds`) + **`record_usage`**: report which memories actually grounded an answer — a deduped, principal-bound usage signal (honest evidence, never retrieval-popularity) that strengthens future recall.
|
|
143
|
+
|
|
144
|
+
Opt-in and additive (`includeTrust` / `abstain` on the recall path) — off by default, byte-identical when unused. Reachable today via the authenticated HTTP API and the native `/mcp` tools; first-class exposure in the `flair` CLI, `@tpsdev-ai/flair-client`, and the `flair-mcp` bridge is a follow-up. Full arc in the [CHANGELOG](CHANGELOG.md) (flair#744).
|
|
145
|
+
|
|
137
146
|
### Tiered Durability
|
|
138
147
|
Not all memories are equal:
|
|
139
148
|
|
|
@@ -340,7 +349,7 @@ Add to your `CLAUDE.md`:
|
|
|
340
349
|
|
|
341
350
|
At the start of every session, run mcp__flair__bootstrap before responding.
|
|
342
351
|
|
|
343
|
-
Your agent's memory **follows it across CLIs** — same Flair instance, same agent identity, switch from Claude Code to Gemini CLI to Codex CLI without losing state. The
|
|
352
|
+
Your agent's memory **follows it across CLIs** — same Flair instance, same agent identity, switch from Claude Code to Gemini CLI to Codex CLI without losing state. The `flair-mcp` server — the bridge these CLIs connect to — exposes `memory_store`, `memory_search`, `memory_update`, `memory_get`, `memory_delete`, `relationship_store`, `bootstrap`, `soul_set`, `soul_get`, `flair_workspace_set`, and `flair_orgevent`. (Flair's in-Harper native `/mcp` surface is a separate, still-experimental tool set with `attention` + `record_usage` — see Trust-Graded Recall above.)
|
|
344
353
|
|
|
345
354
|
For per-CLI config snippets (Gemini CLI's `~/.gemini/settings.json`, Codex CLI's `~/.codex/config.toml`, etc.), see **[docs/mcp-clients.md](docs/mcp-clients.md)**. For a deeper Claude Code walk-through with `CLAUDE.md` patterns, see [docs/claude-code.md](docs/claude-code.md).
|
|
346
355
|
|
|
@@ -353,7 +362,7 @@ Use Flair as the memory backend for n8n's AI Agent. Same memories readable from
|
|
|
353
362
|
@tpsdev-ai/n8n-nodes-flair
|
|
354
363
|
```
|
|
355
364
|
|
|
356
|
-
|
|
365
|
+
Three nodes ship: **Flair Chat Memory** (Memory port, conversation buffer), **Flair Search** (Tool port, semantic search + get-by-subject), and **Flair Write** (Tool port, store memories). Setup walkthrough, subject/sessionId patterns, and security guidance in **[docs/n8n.md](docs/n8n.md)**.
|
|
357
366
|
|
|
358
367
|
### JavaScript / TypeScript (Client Library)
|
|
359
368
|
|
|
@@ -541,13 +550,13 @@ Flair is in active development and daily use. We dogfood it — the agents that
|
|
|
541
550
|
- ✅ Web admin UI (principals, connectors, IdPs, instance config)
|
|
542
551
|
- ✅ Federation (hub-and-spoke sync with signed requests and pairing tokens)
|
|
543
552
|
- ✅ OpenClaw memory plugin
|
|
544
|
-
- ✅ MCP server for Claude Code / Cursor /
|
|
553
|
+
- ✅ MCP server for Claude Code / Cursor / Codex / Gemini CLI
|
|
545
554
|
- ✅ Lightweight client library (`@tpsdev-ai/flair-client`)
|
|
546
555
|
- ✅ Portable agent identity (export/import between instances)
|
|
547
556
|
- ✅ `flair --version`, `flair upgrade`
|
|
557
|
+
- ✅ First-run soul wizard (interactive personality setup)
|
|
548
558
|
|
|
549
559
|
**What's next:**
|
|
550
|
-
- [ ] First-run soul wizard (interactive personality setup)
|
|
551
560
|
- [ ] Git-backed memory sync
|
|
552
561
|
- [ ] Encryption at rest (opt-in AES-256-GCM per memory)
|
|
553
562
|
- [ ] Harper Fabric deployment (managed multi-office)
|
package/dist/cli.js
CHANGED
|
@@ -1641,6 +1641,23 @@ export function decideAfterVerify(result, previousVersion) {
|
|
|
1641
1641
|
if (result.ok)
|
|
1642
1642
|
return { kind: "ok" };
|
|
1643
1643
|
const reason = result.error ?? "post-restart verification failed";
|
|
1644
|
+
// A HEALTHY instance whose ONLY failure was that the verifier couldn't
|
|
1645
|
+
// authenticate (401/403) is demonstrably UP: the public /Health answered
|
|
1646
|
+
// 2xx AND the server responded to the authenticated probe — it rejected our
|
|
1647
|
+
// credentials, it did not fail to respond. A version we couldn't READ is not
|
|
1648
|
+
// grounds to roll back a RUNNING instance. Rolling back here is destructive
|
|
1649
|
+
// AND self-defeating — the rollback's own re-verify hits the identical
|
|
1650
|
+
// missing-credential wall, producing the false "ROLLBACK ALSO FAILED / state
|
|
1651
|
+
// UNKNOWN" for an instance that was healthy the whole time. (The real
|
|
1652
|
+
// incident: /HealthDetail became a verified-read in flair#747, so a machine
|
|
1653
|
+
// with no admin-pass/agent key authenticates fine against the pre-upgrade
|
|
1654
|
+
// version but not post-restart — the pre-flight credential check can't
|
|
1655
|
+
// anticipate a version that changes /HealthDetail's auth requirement.)
|
|
1656
|
+
// Report it as up-but-unverified and NEVER roll back. This supersedes
|
|
1657
|
+
// flair#741 fix #3's "prefer the known-good version" default, which the
|
|
1658
|
+
// incident proved wrong for a healthy instance.
|
|
1659
|
+
if (isCredentialOnlyFailure(result))
|
|
1660
|
+
return { kind: "healthy-unverified", reason };
|
|
1644
1661
|
if (!previousVersion)
|
|
1645
1662
|
return { kind: "cannot-rollback", reason };
|
|
1646
1663
|
return { kind: "rollback", reason, toVersion: previousVersion };
|
|
@@ -8352,18 +8369,22 @@ program
|
|
|
8352
8369
|
console.log(`✅ verified: healthy, authenticated${verify.version ? `, running ${verify.version}` : ""}`);
|
|
8353
8370
|
return;
|
|
8354
8371
|
}
|
|
8355
|
-
|
|
8356
|
-
|
|
8357
|
-
|
|
8358
|
-
|
|
8359
|
-
|
|
8360
|
-
|
|
8361
|
-
|
|
8362
|
-
|
|
8363
|
-
|
|
8364
|
-
|
|
8365
|
-
console.
|
|
8372
|
+
// flair#741 follow-through: a healthy instance the verifier just couldn't
|
|
8373
|
+
// authenticate against. The upgrade SUCCEEDED — the new version's server is
|
|
8374
|
+
// up (public /Health passed); we simply couldn't read its version over the
|
|
8375
|
+
// authenticated /HealthDetail. Report the caveat and STOP — never roll back
|
|
8376
|
+
// a running instance over a credentials gap. (decideAfterVerify only
|
|
8377
|
+
// returns this for isCredentialOnlyFailure(verify), so the old
|
|
8378
|
+
// "print an honest note but roll back anyway" branch that used to sit below
|
|
8379
|
+
// is gone — that credentials case can no longer reach the rollback path.)
|
|
8380
|
+
if (verdict.kind === "healthy-unverified") {
|
|
8381
|
+
console.log(`✅ upgrade complete: the instance is up and healthy${expectedFlairVersion ? ` on @tpsdev-ai/flair@${expectedFlairVersion}` : ""}.`);
|
|
8382
|
+
console.log(` The version could not be verified — the checker couldn't authenticate to /HealthDetail (${verdict.reason}).`);
|
|
8383
|
+
console.log(" The server is confirmed running (public /Health passed); this is a verification gap, not an upgrade failure — nothing was rolled back.");
|
|
8384
|
+
console.log(" To enable full post-upgrade verification: set FLAIR_ADMIN_PASS, or run `flair init` to provision ~/.flair/admin-pass or an agent key.");
|
|
8385
|
+
return;
|
|
8366
8386
|
}
|
|
8387
|
+
console.error(`❌ post-restart verification failed: ${verdict.reason}`);
|
|
8367
8388
|
if (verdict.kind === "cannot-rollback") {
|
|
8368
8389
|
console.error(" Cannot roll back automatically: the previously-installed @tpsdev-ai/flair version is unknown.");
|
|
8369
8390
|
console.error(" Check the instance now: flair doctor");
|
package/dist/resources/Memory.js
CHANGED
|
@@ -9,6 +9,24 @@ import { checkRateLimit, rateLimitResponse } from "./rate-limiter.js";
|
|
|
9
9
|
import { DEDUP_COSINE_THRESHOLD_DEFAULT, DEDUP_LEXICAL_THRESHOLD_DEFAULT, DEDUP_MIN_CONTENT_LENGTH, computeMatchConfidence, cosineSimilarity, isConservativeMatch, } from "./dedup.js";
|
|
10
10
|
import { buildProvenance, makeAuthGate, makeReadScope, makeByIdReadGate, resolveAuthGate, stampAttribution, FORBIDDEN, UNAUTH, } from "./record-type-kit.js";
|
|
11
11
|
import { RECORD_TYPES } from "./record-types.js";
|
|
12
|
+
import { attachTrust } from "./trust-block.js";
|
|
13
|
+
import { recordCitations } from "./usage-recording.js";
|
|
14
|
+
/**
|
|
15
|
+
* flair#744 slice 1 — read the opt-in `includeTrust` flag for a by-id get.
|
|
16
|
+
* Two entry shapes: an in-process caller (resources/mcp-tools.ts's memory_get)
|
|
17
|
+
* passes it explicitly via the `opts` arg; an HTTP `GET /Memory/<id>?includeTrust=true`
|
|
18
|
+
* carries it as a query param on the RequestTarget. Defensive across the
|
|
19
|
+
* RequestTarget/URLSearchParams shapes Harper may hand us; anything other than
|
|
20
|
+
* a literal "true" reads as off, so the default response stays byte-identical.
|
|
21
|
+
*/
|
|
22
|
+
function wantsTrust(target, opts) {
|
|
23
|
+
if (opts?.includeTrust === true)
|
|
24
|
+
return true;
|
|
25
|
+
const raw = target?.get?.("includeTrust") ??
|
|
26
|
+
target?.searchParams?.get?.("includeTrust") ??
|
|
27
|
+
undefined;
|
|
28
|
+
return raw === "true" || raw === true;
|
|
29
|
+
}
|
|
12
30
|
/**
|
|
13
31
|
* Owner ids a non-admin agent may READ (resolveAllowedOwners) live in
|
|
14
32
|
* ./memory-read-scope.ts — still exported/used elsewhere (admin tooling).
|
|
@@ -439,7 +457,7 @@ export class Memory extends databases.flair.Memory {
|
|
|
439
457
|
* with Memory's own "open-within-org" read-scope resolver above — same
|
|
440
458
|
* dispatch shape Relationship.ts/WorkspaceState.ts's get() overrides use.
|
|
441
459
|
*/
|
|
442
|
-
async get(target) {
|
|
460
|
+
async get(target, opts) {
|
|
443
461
|
// Collection / query reads — the `GET /Memory/?<query>` form and the bare
|
|
444
462
|
// collection — arrive as a RequestTarget with `isCollection === true`, and
|
|
445
463
|
// are governed by search() (same owner/grant scoping). Only a genuine by-id
|
|
@@ -451,11 +469,24 @@ export class Memory extends databases.flair.Memory {
|
|
|
451
469
|
// get (RequestTarget with isCollection false, or a bare id) falls through.
|
|
452
470
|
// makeByIdReadGate re-applies this same guard internally (delegating to
|
|
453
471
|
// this.search via `.call(this, ...)`) — kept here too as documentation of
|
|
454
|
-
// the invariant at the call site, harmless no-op double-check.
|
|
472
|
+
// the invariant at the call site, harmless no-op double-check. The trust
|
|
473
|
+
// block (flair#744) is NOT attached on the collection path — that routes to
|
|
474
|
+
// search(), which is out of this slice's by-id `get` surface.
|
|
455
475
|
if (!target || (typeof target === "object" && target.isCollection)) {
|
|
456
476
|
return this.search(target);
|
|
457
477
|
}
|
|
458
|
-
|
|
478
|
+
const result = await memoryByIdReadGate.call(this, target, (t) => super.get(t));
|
|
479
|
+
// flair#744 slice 1 — opt-in inline trust-evidence block, attached ONLY to
|
|
480
|
+
// a genuine by-id record (never a NOT_FOUND `Response`, never null), and
|
|
481
|
+
// ONLY after the ownership/read-scope gate above has already resolved. The
|
|
482
|
+
// block informs the reader; it never re-enters an authority decision
|
|
483
|
+
// (#735-spirit zero-authority invariant). Default OFF ⇒ the record is
|
|
484
|
+
// returned untouched (attachTrust returns the same reference) ⇒
|
|
485
|
+
// byte-identical to pre-slice-1.
|
|
486
|
+
if (result && typeof result === "object" && !(result instanceof Response) && typeof result.agentId === "string") {
|
|
487
|
+
return attachTrust(result, wantsTrust(target, opts));
|
|
488
|
+
}
|
|
489
|
+
return result;
|
|
459
490
|
}
|
|
460
491
|
/**
|
|
461
492
|
* Override search() to scope collection GETs by authenticated agent.
|
|
@@ -538,6 +569,16 @@ export class Memory extends databases.flair.Memory {
|
|
|
538
569
|
if (attr.denied)
|
|
539
570
|
return attr.denied;
|
|
540
571
|
}
|
|
572
|
+
// flair#744 slice A: citation-on-write — consume-and-strip, same
|
|
573
|
+
// discipline as `claimedClient` below. Pull the optional
|
|
574
|
+
// `usedMemoryIds` off the write body now, BEFORE anything else touches
|
|
575
|
+
// `content`, so it is NEVER persisted on the Memory record itself.
|
|
576
|
+
// Recording happens POST-COMMIT, only when this was present (see the
|
|
577
|
+
// failure-isolated recordCitations() call near the return below) —
|
|
578
|
+
// omitted ⇒ `undefined` ⇒ zero new calls, byte-identical behavior.
|
|
579
|
+
const usedMemoryIds = content?.usedMemoryIds;
|
|
580
|
+
if (content && typeof content === "object")
|
|
581
|
+
delete content.usedMemoryIds;
|
|
541
582
|
content.durability ||= "standard";
|
|
542
583
|
content.createdAt = new Date().toISOString();
|
|
543
584
|
content.updatedAt = content.createdAt;
|
|
@@ -649,6 +690,20 @@ export class Memory extends databases.flair.Memory {
|
|
|
649
690
|
// Now the safe failure state is two active records (recoverable), never
|
|
650
691
|
// a lost write — and the failure is logged, never silently swallowed.
|
|
651
692
|
await closeSupersededIfNeeded(ctx, content, "post");
|
|
693
|
+
// flair#744 slice A: citation-on-write — POST-COMMIT, fully
|
|
694
|
+
// failure-isolated. The write above already succeeded and `result` is
|
|
695
|
+
// final; crediting each cited memory through the shared usage ledger
|
|
696
|
+
// (same path as POST /RecordUsage) must never affect this response —
|
|
697
|
+
// any failure here is logged server-side and swallowed, never surfaced
|
|
698
|
+
// to the caller, never rolls back or retries the write.
|
|
699
|
+
if (usedMemoryIds !== undefined) {
|
|
700
|
+
try {
|
|
701
|
+
await recordCitations(ctx, auth, usedMemoryIds, new Date().toISOString());
|
|
702
|
+
}
|
|
703
|
+
catch (err) {
|
|
704
|
+
console.error("Memory.post: citation recording failed (write already committed)", err);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
652
707
|
return buildWriteResponse(content, result, dedupMatch);
|
|
653
708
|
}
|
|
654
709
|
async put(content) {
|
|
@@ -690,6 +745,13 @@ export class Memory extends databases.flair.Memory {
|
|
|
690
745
|
if (attr.denied)
|
|
691
746
|
return attr.denied;
|
|
692
747
|
}
|
|
748
|
+
// flair#744 slice A: citation-on-write — same consume-and-strip
|
|
749
|
+
// discipline as post() above. Strip BEFORE anything else touches
|
|
750
|
+
// `content` so it is never persisted on the row; recorded post-commit
|
|
751
|
+
// below only when present.
|
|
752
|
+
const usedMemoryIds = content?.usedMemoryIds;
|
|
753
|
+
if (content && typeof content === "object")
|
|
754
|
+
delete content.usedMemoryIds;
|
|
693
755
|
const now = new Date().toISOString();
|
|
694
756
|
content.updatedAt = now;
|
|
695
757
|
// Set defaults that post() sets — put() is also used for new records via CLI
|
|
@@ -821,6 +883,16 @@ export class Memory extends databases.flair.Memory {
|
|
|
821
883
|
const result = await super.put(content);
|
|
822
884
|
// ── THEN close the superseded record (see post()) ───────────────────────
|
|
823
885
|
await closeSupersededIfNeeded(ctx, content, "put");
|
|
886
|
+
// flair#744 slice A: citation-on-write — POST-COMMIT, fully
|
|
887
|
+
// failure-isolated (see post()'s identical comment above).
|
|
888
|
+
if (usedMemoryIds !== undefined) {
|
|
889
|
+
try {
|
|
890
|
+
await recordCitations(ctx, auth, usedMemoryIds, new Date().toISOString());
|
|
891
|
+
}
|
|
892
|
+
catch (err) {
|
|
893
|
+
console.error("Memory.put: citation recording failed (write already committed)", err);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
824
896
|
return buildWriteResponse(content, result, dedupMatch);
|
|
825
897
|
}
|
|
826
898
|
async delete(id) {
|
|
@@ -14,7 +14,9 @@ import { buildCollisionEntries, buildEntityMatchCondition, freshPresenceByAgent,
|
|
|
14
14
|
// bootstrap call never trips SemanticSearch's rate-limit/reranker/
|
|
15
15
|
// retrievalCount hit-tracking side effects (see resources/
|
|
16
16
|
// semantic-retrieval-core.ts's module doc for the full boundary).
|
|
17
|
-
import { retrieveCandidates } from "./semantic-retrieval-core.js";
|
|
17
|
+
import { retrieveCandidates, DEFAULT_SELECT } from "./semantic-retrieval-core.js";
|
|
18
|
+
import { buildTrustBlock } from "./trust-block.js";
|
|
19
|
+
import { bestSemanticSimilarity, evaluateAbstention } from "./abstention.js";
|
|
18
20
|
/**
|
|
19
21
|
* POST /MemoryBootstrap
|
|
20
22
|
*
|
|
@@ -141,6 +143,8 @@ export class BootstrapMemories extends Resource {
|
|
|
141
143
|
const { agentId: bodyAgentId, currentTask, maxTokens = 4000, includeSoul = true, since, channel, // e.g., "discord", "tps-mail", "claude-code"
|
|
142
144
|
surface, // e.g., "tps-build", "tps-review", "cli-session"
|
|
143
145
|
subjects, // e.g., ["flair", "auth"] — entities to preload context for
|
|
146
|
+
includeTrust = false, // flair#744 slice 1 — opt-in per-memory trust block
|
|
147
|
+
abstain = false, // flair#744 slice 2 — opt-in task-relevance abstention
|
|
144
148
|
} = data || {};
|
|
145
149
|
// Authenticated identity lives on getContext().request — `this.request` is
|
|
146
150
|
// NOT populated on Harper v5 Resources. Reading it returned undefined and
|
|
@@ -348,8 +352,28 @@ export class BootstrapMemories extends Resource {
|
|
|
348
352
|
memoriesAvailable = ownMemoriesAvailable;
|
|
349
353
|
// Fields every own-scoped lifecycle slice below needs — explicit (no raw
|
|
350
354
|
// `embedding`), matching the "select, no raw embedding" pushdown
|
|
351
|
-
// requirement.
|
|
352
|
-
|
|
355
|
+
// requirement. flair#744 slice 1: when the caller opts into the per-memory
|
|
356
|
+
// trust block, widen the projection with the extra stored fields the block
|
|
357
|
+
// reads (`provenance`, `usageCount`, `validFrom`) — additive, and only when
|
|
358
|
+
// requested, so a non-trust bootstrap fetches (and returns) exactly what it
|
|
359
|
+
// did before. These records are never returned raw when the block is off,
|
|
360
|
+
// so widening the select cannot change the off-path response bytes.
|
|
361
|
+
const OWN_SELECT = includeTrust
|
|
362
|
+
? ["id", "agentId", "content", "durability", "createdAt", "supersedes", "subject", "validTo", "expiresAt", "_safetyFlags", "provenance", "usageCount", "validFrom"]
|
|
363
|
+
: ["id", "agentId", "content", "durability", "createdAt", "supersedes", "subject", "validTo", "expiresAt", "_safetyFlags"];
|
|
364
|
+
// flair#744 slice 1 — the Memory records that became visible lines in the
|
|
365
|
+
// memory-bearing sections (permanent/recent/predicted/relevant/teammate),
|
|
366
|
+
// collected as they're added so the opt-in `trust` array below can carry a
|
|
367
|
+
// self-contained block per included memory. Stays empty (and unused) when
|
|
368
|
+
// includeTrust is off.
|
|
369
|
+
const includedTrustMemories = [];
|
|
370
|
+
// flair#744 slice 2 — the best absolute semantic similarity seen while
|
|
371
|
+
// scoring the task-relevant candidate pool (section 4). Drives the opt-in
|
|
372
|
+
// abstention verdict on the *task-relevance* surface only (bootstrap always
|
|
373
|
+
// returns identity/permanent/recent regardless — abstention is about "does
|
|
374
|
+
// any memory cover your current task", not the whole session load). Stays
|
|
375
|
+
// null when there's no currentTask / no embedding ⇒ never abstains.
|
|
376
|
+
let taskBestSimilarity = null;
|
|
353
377
|
// --- 2. Permanent memories (always included, highest priority) ---
|
|
354
378
|
// Own-scoped pushdown: `agentId==self` + `durability==permanent`, both
|
|
355
379
|
// @indexed (a seek, not a scan) — strictly narrower than the prior
|
|
@@ -400,6 +424,8 @@ export class BootstrapMemories extends Resource {
|
|
|
400
424
|
const cost = estimateTokens(line);
|
|
401
425
|
if (cost <= tokenBudget) {
|
|
402
426
|
sections.permanent.push(line);
|
|
427
|
+
if (includeTrust)
|
|
428
|
+
includedTrustMemories.push(m);
|
|
403
429
|
tokenBudget -= cost;
|
|
404
430
|
memoriesIncluded++;
|
|
405
431
|
}
|
|
@@ -472,6 +498,8 @@ export class BootstrapMemories extends Resource {
|
|
|
472
498
|
continue;
|
|
473
499
|
}
|
|
474
500
|
sections.recent.push(line);
|
|
501
|
+
if (includeTrust)
|
|
502
|
+
includedTrustMemories.push(m);
|
|
475
503
|
recentSpent += cost;
|
|
476
504
|
tokenBudget -= cost;
|
|
477
505
|
memoriesIncluded++;
|
|
@@ -512,6 +540,8 @@ export class BootstrapMemories extends Resource {
|
|
|
512
540
|
continue;
|
|
513
541
|
}
|
|
514
542
|
sections.predicted.push(line);
|
|
543
|
+
if (includeTrust)
|
|
544
|
+
includedTrustMemories.push(m);
|
|
515
545
|
predictedSpent += cost;
|
|
516
546
|
tokenBudget -= cost;
|
|
517
547
|
memoriesIncluded++;
|
|
@@ -612,7 +642,37 @@ export class BootstrapMemories extends Resource {
|
|
|
612
642
|
// even though `conditions` already scoped the query.
|
|
613
643
|
isAllowed: scope.isAllowed,
|
|
614
644
|
ctx,
|
|
645
|
+
// flair#744 slice 1: widen the projection with `provenance` (omitted
|
|
646
|
+
// by the default select) ONLY when the trust block was requested, so
|
|
647
|
+
// the block's provenance fields are populated for teammate/relevant
|
|
648
|
+
// records without changing a non-trust bootstrap's fetch.
|
|
649
|
+
select: includeTrust ? [...DEFAULT_SELECT, "provenance"] : undefined,
|
|
650
|
+
// flair#744 slice 2 + confidence-band refinement: attach the absolute
|
|
651
|
+
// cosine confidence (HNSW-leg, pre keyword bump) when abstention OR
|
|
652
|
+
// the trust block is requested — abstention reads the best of it for
|
|
653
|
+
// its verdict, and the per-memory trust block classifies each
|
|
654
|
+
// task-relevant candidate's into a `matchQuality` band (Kern BINDING
|
|
655
|
+
// condition 2: matchQuality needs `_semSimilarity`, so includeTrust
|
|
656
|
+
// must turn this on too). Only the task-relevant candidates get a
|
|
657
|
+
// similarity — permanent/recent/predicted come from raw own-scoped
|
|
658
|
+
// reads with no retrieval score, so their block's matchQuality is
|
|
659
|
+
// null (honest: those are lifecycle-window loads, not a retrieval
|
|
660
|
+
// surface — confidence bands are a retrieval-surface feature). The
|
|
661
|
+
// field is never returned raw (bootstrap renders memories as text
|
|
662
|
+
// lines + a trust array), so no strip is needed here. Neither flag ⇒
|
|
663
|
+
// candidates carry no `_semSimilarity` and the response is unchanged.
|
|
664
|
+
withSemSimilarity: abstain || includeTrust,
|
|
615
665
|
});
|
|
666
|
+
// flair#744 slice 2: the best-match confidence for the abstention
|
|
667
|
+
// decision — the max absolute cosine across the retrieved pool, read
|
|
668
|
+
// ONLY from `_semSimilarity` (never any principal/authority field). Note
|
|
669
|
+
// this floor (ABSTENTION_THRESHOLD ≈ 0.15) sits BELOW bootstrap's own
|
|
670
|
+
// long-standing TASK_RELEVANCE_FLOOR (0.3) that gates `scored` below, so
|
|
671
|
+
// an abstaining task (bestSim < 0.15) already has no candidate passing
|
|
672
|
+
// the floor — abstention only ADDS an explicit "nothing covered this"
|
|
673
|
+
// signal, it never removes a memory the reader would otherwise have seen.
|
|
674
|
+
if (abstain)
|
|
675
|
+
taskBestSimilarity = bestSemanticSimilarity(candidates);
|
|
616
676
|
// Preserve the ORIGINAL score > 0.3 floor exactly (bootstrap's own
|
|
617
677
|
// historical relevance floor — distinct from SemanticSearch's
|
|
618
678
|
// `minScore` request param — strict inequality, applied client-side;
|
|
@@ -658,6 +718,8 @@ export class BootstrapMemories extends Resource {
|
|
|
658
718
|
else {
|
|
659
719
|
sections.relevant.push(line);
|
|
660
720
|
}
|
|
721
|
+
if (includeTrust)
|
|
722
|
+
includedTrustMemories.push(m);
|
|
661
723
|
tokenBudget -= cost;
|
|
662
724
|
memoriesIncluded++;
|
|
663
725
|
}
|
|
@@ -865,8 +927,28 @@ export class BootstrapMemories extends Resource {
|
|
|
865
927
|
const context = parts.join("\n\n");
|
|
866
928
|
const soulTokens = sections.soul.reduce((sum, line) => sum + estimateTokens(line), 0);
|
|
867
929
|
const memoryTokens = maxTokens - tokenBudget;
|
|
930
|
+
// flair#744 slice 1 — opt-in per-memory trust block. Bootstrap renders
|
|
931
|
+
// memories as text lines rather than result objects, so the block is
|
|
932
|
+
// surfaced as a `trust` array of self-contained entries (each carries its
|
|
933
|
+
// own `id` to correlate to a rendered line), one per INCLUDED memory. Built
|
|
934
|
+
// HERE, in the response tail, strictly after all read-scope resolution and
|
|
935
|
+
// purely for the response — never consulted for any authority decision
|
|
936
|
+
// (#735-spirit zero-authority invariant). Default OFF ⇒ the `trust` key is
|
|
937
|
+
// absent ⇒ the response is byte-identical to pre-slice-1.
|
|
938
|
+
const trust = includeTrust
|
|
939
|
+
? includedTrustMemories.map((m) => ({ id: m.id, ...buildTrustBlock(m) }))
|
|
940
|
+
: undefined;
|
|
941
|
+
// flair#744 slice 2 — opt-in abstention verdict for the task-relevance
|
|
942
|
+
// surface. Present ONLY when `abstain` is requested (byte-identical to
|
|
943
|
+
// pre-slice-2 otherwise); scoped to whether any memory covered
|
|
944
|
+
// `currentTask` (bestScore null ⇒ no task/embedding ⇒ abstained:false). The
|
|
945
|
+
// decision reads only the confidence number — never a principal — against
|
|
946
|
+
// the single GLOBAL threshold.
|
|
947
|
+
const abstention = abstain ? evaluateAbstention(taskBestSimilarity) : undefined;
|
|
868
948
|
return {
|
|
869
949
|
context,
|
|
950
|
+
...(trust ? { trust } : {}),
|
|
951
|
+
...(abstention ? { abstention } : {}),
|
|
870
952
|
sections: {
|
|
871
953
|
soul: sections.soul.length,
|
|
872
954
|
skills: sections.skills.length,
|
|
@@ -11,6 +11,14 @@
|
|
|
11
11
|
* REPLACES `retrievalBoost` in `compositeScore` outright (see that
|
|
12
12
|
* function's doc).
|
|
13
13
|
*
|
|
14
|
+
* flair#744 slice A: the actual ledger-write core below (formerly this
|
|
15
|
+
* class's private `_recordOne()`) has moved to ./usage-recording.ts's
|
|
16
|
+
* `recordUsageContribution()` — a shared, single-implementation extraction
|
|
17
|
+
* also used by citation-on-write (resources/Memory.ts's post()/put()) so
|
|
18
|
+
* there is exactly ONE place the (agentId, memoryId) ledger logic lives.
|
|
19
|
+
* This endpoint's own request handling (auth, rate limit, batch validation,
|
|
20
|
+
* the no-enumeration response) is unchanged.
|
|
21
|
+
*
|
|
14
22
|
* WHY THIS IS ITS OWN ENDPOINT, NOT `Memory.put()` (Sherlock, K&S verdict —
|
|
15
23
|
* FLAIR-USAGE-FEEDBACK-SIGNAL.md): usage feedback is fundamentally a
|
|
16
24
|
* CROSS-AGENT write — agent B reports using agent A's memory, so the write
|
|
@@ -21,10 +29,10 @@
|
|
|
21
29
|
* memory through this surface — not just the count. So this endpoint does
|
|
22
30
|
* the narrowest possible thing instead: a TARGETED `usageCount`-only
|
|
23
31
|
* bump (read-full-record, merge just this one field, write — see
|
|
24
|
-
*
|
|
25
|
-
* bypassing the `Memory` RESOURCE class
|
|
26
|
-
* auth model is verified-agent +
|
|
27
|
-
* requirement.
|
|
32
|
+
* ./usage-recording.ts's recordUsageContribution()) against the RAW
|
|
33
|
+
* `Memory` table object, entirely bypassing the `Memory` RESOURCE class
|
|
34
|
+
* (and its ownership gate) — its own auth model is verified-agent +
|
|
35
|
+
* within-org + explicitly NO ownership requirement.
|
|
28
36
|
*
|
|
29
37
|
* WHY THIS ISN'T A @table-BACKED RESOURCE: the actual dedup ledger (one row
|
|
30
38
|
* per (agentId, memoryId) contribution) lives in the `MemoryUsage` table
|
|
@@ -37,13 +45,14 @@
|
|
|
37
45
|
* SAME class of gotcha resources/Memory.ts documents for a raw HTTP POST to
|
|
38
46
|
* `/Memory`, but here it bites even an in-process call). So the ledger row
|
|
39
47
|
* itself is written via `.put()` (upsert with the deterministic composite
|
|
40
|
-
* id — see
|
|
41
|
-
* plain action `Resource` (same shape
|
|
42
|
-
* resources/SemanticSearch.ts) rather than
|
|
43
|
-
* actually routes here. It talks to BOTH
|
|
44
|
-
* their raw table objects (bypassing each
|
|
45
|
-
* wrapper — the same "call the other table
|
|
46
|
-
* resources/Memory.ts already uses for `MemoryGrant` in
|
|
48
|
+
* id — see ./usage-recording.ts's recordUsageContribution()), and this
|
|
49
|
+
* endpoint's OWN HTTP surface lives on a plain action `Resource` (same shape
|
|
50
|
+
* as resources/MemoryReflect.ts / resources/SemanticSearch.ts) rather than
|
|
51
|
+
* extending a @table class, so POST actually routes here. It talks to BOTH
|
|
52
|
+
* `Memory` and `MemoryUsage` via their raw table objects (bypassing each
|
|
53
|
+
* resource class's own auth wrapper — the same "call the other table
|
|
54
|
+
* directly" pattern resources/Memory.ts already uses for `MemoryGrant` in
|
|
55
|
+
* hasWriteGrant()).
|
|
47
56
|
*
|
|
48
57
|
* ANTI-GAMING (Sherlock): usage feedback is a write that affects RANKING —
|
|
49
58
|
* an abuse surface (inflate usageCount to boost a memory). Three-layer
|
|
@@ -73,13 +82,16 @@
|
|
|
73
82
|
* sanitized (control-character-stripped, length-capped) and stored as-is,
|
|
74
83
|
* for audit/analytics only. Treat it as untrusted data, always.
|
|
75
84
|
*/
|
|
76
|
-
import { Resource
|
|
85
|
+
import { Resource } from "@harperfast/harper";
|
|
77
86
|
import { resolveAgentAuth } from "./agent-auth.js";
|
|
78
87
|
import { checkRateLimit, rateLimitResponse } from "./rate-limiter.js";
|
|
79
|
-
import {
|
|
88
|
+
import { recordUsageContribution, MAX_USAGE_IDS_PER_CALL } from "./usage-recording.js";
|
|
80
89
|
const UNAUTH = () => new Response(JSON.stringify({ error: "authentication required" }), { status: 401, headers: { "Content-Type": "application/json" } });
|
|
81
90
|
const BAD_REQUEST = (msg) => new Response(JSON.stringify({ error: msg }), { status: 400, headers: { "Content-Type": "application/json" } });
|
|
82
|
-
|
|
91
|
+
// flair#744 slice A: sourced from the shared module (./usage-recording.ts)
|
|
92
|
+
// so RecordUsage.post()'s validated-batch cap and citation-on-write's
|
|
93
|
+
// advisory-batch cap can never drift apart.
|
|
94
|
+
const MAX_IDS_PER_CALL = MAX_USAGE_IDS_PER_CALL;
|
|
83
95
|
const MAX_ATTRIBUTION_LENGTH = 500;
|
|
84
96
|
/**
|
|
85
97
|
* Strip control/non-printable characters and cap length. Attribution is
|
|
@@ -148,7 +160,12 @@ export class RecordUsage extends Resource {
|
|
|
148
160
|
const now = new Date().toISOString();
|
|
149
161
|
for (const memoryId of memoryIds) {
|
|
150
162
|
try {
|
|
151
|
-
|
|
163
|
+
// flair#744 slice A: the ledger-write core now lives in
|
|
164
|
+
// ./usage-recording.ts's recordUsageContribution() — a shared,
|
|
165
|
+
// single-implementation extraction (also used by citation-on-write,
|
|
166
|
+
// resources/Memory.ts's post()/put()). Byte-identical behavior to
|
|
167
|
+
// the former private _recordOne() this replaced.
|
|
168
|
+
await recordUsageContribution(ctx, agentId, memoryId, attribution, now);
|
|
152
169
|
}
|
|
153
170
|
catch (err) {
|
|
154
171
|
// Never let one bad id fail the whole batch, and never let an
|
|
@@ -162,69 +179,4 @@ export class RecordUsage extends Resource {
|
|
|
162
179
|
// counted / were not found — see RECORDED_RESPONSE's doc.
|
|
163
180
|
return RECORDED_RESPONSE;
|
|
164
181
|
}
|
|
165
|
-
/**
|
|
166
|
-
* One (agentId, memoryId) contribution.
|
|
167
|
-
*
|
|
168
|
-
* Ledger-row-create FIRST, THEN the Memory.usageCount bump — so a crash
|
|
169
|
-
* between the two leaves the SAFE failure state (ledger row exists, count
|
|
170
|
-
* not yet bumped: a later retry just re-checks and no-ops) rather than the
|
|
171
|
-
* reverse (count bumped, no ledger row → a retry would double-count).
|
|
172
|
-
*
|
|
173
|
-
* Every discrete Harper call is wrapped INDIVIDUALLY in its own
|
|
174
|
-
* withDetachedTxn — never one wrap around a multi-step helper. A request
|
|
175
|
-
* that reads/writes MULTIPLE tables (or the same table twice) in sequence
|
|
176
|
-
* can otherwise inherit a closed transaction from a prior call's drained
|
|
177
|
-
* chain (table-helpers.ts's withDetachedTxn doc); resources/Memory.ts's
|
|
178
|
-
* closeSupersededRecord documents exactly why a single wrap around a
|
|
179
|
-
* multi-await async function does NOT protect a later call inside it —
|
|
180
|
-
* this mirrors that function's literal shape (get wrapped, then a
|
|
181
|
-
* SEPARATE put wrapped) rather than delegating to the generic
|
|
182
|
-
* patchRecord() helper, which would combine both into one un-safe wrap.
|
|
183
|
-
*
|
|
184
|
-
* The final get-then-put for the increment is a best-effort (non-atomic)
|
|
185
|
-
* read-modify-write, same class of race already accepted elsewhere in
|
|
186
|
-
* this codebase for count fields (e.g. retrievalCount's bump in
|
|
187
|
-
* SemanticSearch.ts) — a concurrent contribution from a DIFFERENT agent
|
|
188
|
-
* landing between this call's read and write could lose one increment.
|
|
189
|
-
* Re-fetching immediately before the write (rather than reusing the
|
|
190
|
-
* earlier existence-check read) narrows, without eliminating, that
|
|
191
|
-
* window. Not solved here: bounded, low-severity (an undercount, never an
|
|
192
|
-
* inflation), and orthogonal to the anti-gaming properties the cap/floor
|
|
193
|
-
* and dedup ledger actually defend.
|
|
194
|
-
*/
|
|
195
|
-
async _recordOne(ctx, agentId, memoryId, attribution, now) {
|
|
196
|
-
const ledgerId = `${agentId}:${memoryId}`;
|
|
197
|
-
// Bypasses resources/MemoryUsage.ts's own auth wrapper by design — this
|
|
198
|
-
// IS the trusted internal caller that resource's module doc describes
|
|
199
|
-
// (same "raw table object" pattern resources/Memory.ts uses for
|
|
200
|
-
// MemoryGrant).
|
|
201
|
-
const existingContribution = await withDetachedTxn(ctx, () => databases.flair.MemoryUsage.get(ledgerId)).catch(() => null);
|
|
202
|
-
if (existingContribution)
|
|
203
|
-
return; // already counted by this agent — silent no-op
|
|
204
|
-
const memoryExists = await withDetachedTxn(ctx, () => databases.flair.Memory.get(memoryId)).catch(() => null);
|
|
205
|
-
if (!memoryExists)
|
|
206
|
-
return; // no such memory — silent no-op (no enumeration)
|
|
207
|
-
const ledgerRecord = { id: ledgerId, agentId, memoryId, createdAt: now };
|
|
208
|
-
if (attribution)
|
|
209
|
-
ledgerRecord.attribution = attribution;
|
|
210
|
-
// .put(), not .post(): Harper's raw TableResource has no default post()
|
|
211
|
-
// implementation for a static-style (non-`isCollection`-instantiated)
|
|
212
|
-
// call — confirmed live ("The MemoryUsage does not have a post method
|
|
213
|
-
// implemented", statusCode 405) — the SAME class of gotcha
|
|
214
|
-
// resources/Memory.ts documents for HTTP POST, but here it bites even
|
|
215
|
-
// this in-process call. Irrelevant anyway: ledgerId is already a
|
|
216
|
-
// deterministic composite key, so this is a create-with-explicit-id —
|
|
217
|
-
// exactly what PUT (upsert) is for, not an auto-generated-id insert.
|
|
218
|
-
await withDetachedTxn(ctx, () => databases.flair.MemoryUsage.put(ledgerRecord));
|
|
219
|
-
// Targeted usageCount-ONLY bump: read-full-record, merge just this one
|
|
220
|
-
// field, write — against the RAW Memory table, NEVER Memory.put() (the
|
|
221
|
-
// resource class): that would 403 this cross-agent write via its
|
|
222
|
-
// ownership check, and bypassing that check directly would risk letting
|
|
223
|
-
// this endpoint smuggle OTHER field changes through instead of just the
|
|
224
|
-
// count (module doc's "WHY THIS IS ITS OWN ENDPOINT").
|
|
225
|
-
const fresh = await withDetachedTxn(ctx, () => databases.flair.Memory.get(memoryId)).catch(() => null);
|
|
226
|
-
if (!fresh)
|
|
227
|
-
return; // deleted between the checks above and now — no-op
|
|
228
|
-
await withDetachedTxn(ctx, () => databases.flair.Memory.put({ ...fresh, usageCount: (fresh.usageCount ?? 0) + 1 }));
|
|
229
|
-
}
|
|
230
182
|
}
|