@tpsdev-ai/flair 0.12.0 → 0.13.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/dist/cli.js
CHANGED
|
@@ -510,35 +510,11 @@ export async function seedAgentViaOpsApi(opsPortOrUrl, agentId, pubKeyB64url, ad
|
|
|
510
510
|
throw new Error(`Operations API insert failed (${res.status}): ${text}`);
|
|
511
511
|
}
|
|
512
512
|
}
|
|
513
|
-
//
|
|
514
|
-
//
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
operation: "insert",
|
|
519
|
-
database: "flair",
|
|
520
|
-
table: "Agent",
|
|
521
|
-
records: [{ id: agentId, name: agentId, publicKey: pubKeyB64url, createdAt: new Date().toISOString() }],
|
|
522
|
-
};
|
|
523
|
-
// Only send Authorization header if adminPass is provided.
|
|
524
|
-
// Matches the auth pattern in api() which respects authorizeLocal=true.
|
|
525
|
-
const headers = { "Content-Type": "application/json" };
|
|
526
|
-
if (adminPass) {
|
|
527
|
-
headers.Authorization = `Basic ${Buffer.from(`admin:${adminPass}`).toString("base64")}`;
|
|
528
|
-
}
|
|
529
|
-
const res = await fetch(`${baseUrl}/Agent`, {
|
|
530
|
-
method: "POST",
|
|
531
|
-
headers,
|
|
532
|
-
body: JSON.stringify(body),
|
|
533
|
-
signal: AbortSignal.timeout(10_000),
|
|
534
|
-
});
|
|
535
|
-
if (!res.ok) {
|
|
536
|
-
const text = await res.text().catch(() => "");
|
|
537
|
-
if (res.status === 409 || text.includes("duplicate") || text.includes("already exists"))
|
|
538
|
-
return;
|
|
539
|
-
throw new Error(`REST API insert failed (${res.status}): ${text}`);
|
|
540
|
-
}
|
|
541
|
-
}
|
|
513
|
+
// NOTE: agent records are seeded exclusively via the Harper operations API
|
|
514
|
+
// (seedAgentViaOpsApi above). A former seedAgentViaRestApi() helper POSTed the
|
|
515
|
+
// ops-insert body to the REST root, which Harper 405s as a collection POST to
|
|
516
|
+
// /Agent (the Agent table resource has no POST handler). It was removed in the
|
|
517
|
+
// #499 fix; do not reintroduce a REST-root insert path.
|
|
542
518
|
// ─── FederationInstance seed via ops API ──────────────────────────────────────
|
|
543
519
|
//
|
|
544
520
|
// Remote init writes FederationInstance through the ops API (Basic auth with
|
|
@@ -1980,8 +1956,21 @@ program
|
|
|
1980
1956
|
writeConfig(httpPort);
|
|
1981
1957
|
}
|
|
1982
1958
|
else {
|
|
1983
|
-
// Flair already initialized — resolve admin pass from env
|
|
1959
|
+
// Flair already initialized — resolve admin pass from env, falling back to
|
|
1960
|
+
// the persisted ~/.flair/admin-pass written at first install. Agent
|
|
1961
|
+
// seeding now goes through the ops API (#499), which always requires Basic
|
|
1962
|
+
// admin auth (it does NOT honor authorizeLocal), so a re-run of
|
|
1963
|
+
// `flair install --agent <new-id>` against an already-initialized local
|
|
1964
|
+
// Flair needs a real pass even when no env var is set.
|
|
1984
1965
|
adminPass = process.env.FLAIR_ADMIN_PASS ?? process.env.HDB_ADMIN_PASSWORD ?? "";
|
|
1966
|
+
if (!adminPass) {
|
|
1967
|
+
try {
|
|
1968
|
+
const persisted = join(homedir(), ".flair", "admin-pass");
|
|
1969
|
+
if (existsSync(persisted))
|
|
1970
|
+
adminPass = readFileSync(persisted, "utf-8").trim();
|
|
1971
|
+
}
|
|
1972
|
+
catch { /* fall through with empty pass */ }
|
|
1973
|
+
}
|
|
1985
1974
|
}
|
|
1986
1975
|
const httpUrl = `http://127.0.0.1:${httpPort}`;
|
|
1987
1976
|
// ── Step 2: Detect MCP clients ──
|
|
@@ -2040,17 +2029,16 @@ program
|
|
|
2040
2029
|
writeFileSync(pubPath, Buffer.from(kp.publicKey));
|
|
2041
2030
|
const pubKeyB64url = b64url(kp.publicKey);
|
|
2042
2031
|
console.log(`Keypair written: ${privPath} ✓`);
|
|
2043
|
-
// Seed agent
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
}
|
|
2032
|
+
// Seed agent via the Harper operations API. The Agent table is a REST
|
|
2033
|
+
// resource without a POST handler, so the insert body must go to the ops
|
|
2034
|
+
// API (POST <opsUrl>/ with {operation:"insert",...}), NOT the REST root
|
|
2035
|
+
// (which 405s the body as a collection POST to /Agent). This is the same
|
|
2036
|
+
// path `flair agent add` uses. (#499)
|
|
2037
|
+
const seedOpsTarget = opts.opsTarget ?? opsPort;
|
|
2038
|
+
console.log(opts.opsTarget
|
|
2039
|
+
? `Seeding agent '${agentId}' via operations API (--ops-target)...`
|
|
2040
|
+
: `Seeding agent '${agentId}' via operations API...`);
|
|
2041
|
+
await seedAgentViaOpsApi(seedOpsTarget, agentId, pubKeyB64url, adminUser, adminPass);
|
|
2054
2042
|
console.log(`Agent '${agentId}' registered ✓`);
|
|
2055
2043
|
}
|
|
2056
2044
|
// ── Step 4: Wire MCP clients ──
|
|
@@ -2209,10 +2197,16 @@ agent
|
|
|
2209
2197
|
if (adminPass) {
|
|
2210
2198
|
const opsPort = resolveOpsPort(opts);
|
|
2211
2199
|
const auth = Buffer.from(`${DEFAULT_ADMIN_USER}:${adminPass}`).toString("base64");
|
|
2200
|
+
// List every Agent without null-scanning the primary key. A
|
|
2201
|
+
// `starts_with ""` on `id` makes Harper search the index for nulls, which
|
|
2202
|
+
// the bundled Harper (5.0.21) rejects with "id is not indexed for nulls".
|
|
2203
|
+
// Use `createdAt > 1970-01-01` as the total "select all" predicate: every
|
|
2204
|
+
// Agent row has a non-null createdAt (schema: createdAt: String!), and its
|
|
2205
|
+
// index is built — same pattern as the `flair reembed` Memory scan. (#500)
|
|
2212
2206
|
const res = await fetch(`http://127.0.0.1:${opsPort}/`, {
|
|
2213
2207
|
method: "POST",
|
|
2214
2208
|
headers: { "Content-Type": "application/json", Authorization: `Basic ${auth}` },
|
|
2215
|
-
body: JSON.stringify({ operation: "
|
|
2209
|
+
body: JSON.stringify({ operation: "search_by_conditions", schema: "flair", table: "Agent", operator: "and", conditions: [{ search_attribute: "createdAt", search_type: "greater_than", search_value: "1970-01-01" }], get_attributes: ["id", "name", "createdAt"] }),
|
|
2216
2210
|
});
|
|
2217
2211
|
if (!res.ok) {
|
|
2218
2212
|
const text = await res.text().catch(() => "");
|
|
@@ -4198,7 +4192,7 @@ rem
|
|
|
4198
4192
|
console.log(result.prompt ?? "(no prompt returned)");
|
|
4199
4193
|
console.log("--- End Prompt ---\n");
|
|
4200
4194
|
console.log("Feed the prompt above to your LLM, then write insights back with:");
|
|
4201
|
-
console.log(" flair memory add --agent <id> --content <insight> --durability persistent");
|
|
4195
|
+
console.log(" flair memory add --agent <id> --content <insight> --durability persistent --derived-from <source-ids>");
|
|
4202
4196
|
}
|
|
4203
4197
|
catch (err) {
|
|
4204
4198
|
console.error(`Error: ${err.message}`);
|
|
@@ -7010,6 +7004,7 @@ memory.command("add [content]").requiredOption("--agent <id>")
|
|
|
7010
7004
|
.option("--durability <d>", "standard").option("--tags <csv>")
|
|
7011
7005
|
.option("--summary <text>", "agent-set multi-sentence dense compression (3-tier chain: subject → summary → content; ops-wkoh)")
|
|
7012
7006
|
.option("--subject <text>", "one-line title / entity this memory is about")
|
|
7007
|
+
.option("--derived-from <csv>", "Comma-separated source Memory IDs this memory was distilled/reflected from (sets Memory.derivedFrom; used by the `rem rapid` reflection loop)")
|
|
7013
7008
|
.action(async (contentArg, opts) => {
|
|
7014
7009
|
const content = contentArg ?? opts.content;
|
|
7015
7010
|
if (!content) {
|
|
@@ -7026,6 +7021,9 @@ memory.command("add [content]").requiredOption("--agent <id>")
|
|
|
7026
7021
|
body.summary = opts.summary;
|
|
7027
7022
|
if (opts.subject)
|
|
7028
7023
|
body.subject = opts.subject;
|
|
7024
|
+
if (opts.derivedFrom) {
|
|
7025
|
+
body.derivedFrom = String(opts.derivedFrom).split(",").map((x) => x.trim()).filter(Boolean);
|
|
7026
|
+
}
|
|
7029
7027
|
const out = await api("PUT", `/Memory/${memId}`, body);
|
|
7030
7028
|
console.log(JSON.stringify(out, null, 2));
|
|
7031
7029
|
});
|
|
@@ -7649,12 +7647,17 @@ soul.command("set")
|
|
|
7649
7647
|
.option("--durability <d>", "permanent")
|
|
7650
7648
|
.option("--json", "Emit raw JSON response (also: pipe + FLAIR_OUTPUT=json)")
|
|
7651
7649
|
.action(async (opts) => {
|
|
7652
|
-
|
|
7653
|
-
|
|
7650
|
+
// PUT /Soul/{agentId:key} (upsert by id), matching flair-client's soul.set().
|
|
7651
|
+
// The Soul table resource has no POST handler, so a collection POST /Soul
|
|
7652
|
+
// 405s; the record must be written by its primary key. (#498)
|
|
7653
|
+
const id = `${opts.agent}:${opts.key}`;
|
|
7654
|
+
const out = await api("PUT", `/Soul/${encodeURIComponent(id)}`, {
|
|
7655
|
+
id,
|
|
7654
7656
|
agentId: opts.agent,
|
|
7655
7657
|
key: opts.key,
|
|
7656
7658
|
value: opts.value,
|
|
7657
7659
|
durability: opts.durability,
|
|
7660
|
+
createdAt: new Date().toISOString(),
|
|
7658
7661
|
});
|
|
7659
7662
|
const mode = render.resolveOutputMode(opts);
|
|
7660
7663
|
if (mode === "json") {
|
|
@@ -17,44 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { Resource, databases } from "@harperfast/harper";
|
|
19
19
|
import { isAdmin, allowVerified } from "./agent-auth.js";
|
|
20
|
-
|
|
21
|
-
const m = s.match(/^(\d+)([dhm])$/);
|
|
22
|
-
if (!m)
|
|
23
|
-
return 30 * 86400_000;
|
|
24
|
-
const n = Number(m[1]);
|
|
25
|
-
if (m[2] === "d")
|
|
26
|
-
return n * 86400_000;
|
|
27
|
-
if (m[2] === "h")
|
|
28
|
-
return n * 3600_000;
|
|
29
|
-
if (m[2] === "m")
|
|
30
|
-
return n * 60_000;
|
|
31
|
-
return 30 * 86400_000;
|
|
32
|
-
}
|
|
33
|
-
function evaluate(record, now, olderThanMs) {
|
|
34
|
-
const ageMs = record.createdAt ? now - new Date(record.createdAt).getTime() : 0;
|
|
35
|
-
const count = record.retrievalCount ?? 0;
|
|
36
|
-
const daysSinceRetrieved = record.lastRetrieved
|
|
37
|
-
? (now - new Date(record.lastRetrieved).getTime()) / 86400_000
|
|
38
|
-
: Infinity;
|
|
39
|
-
const { embedding, ...memory } = record;
|
|
40
|
-
// Promote: high retrieval + persistent durability
|
|
41
|
-
if (record.durability === "persistent" && count >= 5) {
|
|
42
|
-
return { memory, suggestion: "promote", reason: `Retrieved ${count} times — strong promotion candidate for permanent` };
|
|
43
|
-
}
|
|
44
|
-
// Promote: standard → persistent if retrieved frequently
|
|
45
|
-
if (record.durability === "standard" && count >= 3 && ageMs > 7 * 86400_000) {
|
|
46
|
-
return { memory, suggestion: "promote", reason: `Retrieved ${count} times over ${Math.round(ageMs / 86400_000)} days — worth persisting` };
|
|
47
|
-
}
|
|
48
|
-
// Archive: old + never retrieved
|
|
49
|
-
if (daysSinceRetrieved > 30 && count === 0 && ageMs > olderThanMs) {
|
|
50
|
-
return { memory, suggestion: "archive", reason: `Never retrieved, ${Math.round(ageMs / 86400_000)} days old` };
|
|
51
|
-
}
|
|
52
|
-
// Archive: last retrieved > 60 days
|
|
53
|
-
if (daysSinceRetrieved > 60 && count < 2) {
|
|
54
|
-
return { memory, suggestion: "archive", reason: `Not retrieved in ${Math.round(daysSinceRetrieved)} days (only ${count} total retrievals)` };
|
|
55
|
-
}
|
|
56
|
-
return { memory, suggestion: "keep", reason: `Retrieved ${count} times, ${Math.round(daysSinceRetrieved)} days since last retrieval` };
|
|
57
|
-
}
|
|
20
|
+
import { evaluate, parseDuration } from "./memory-consolidate-lib.js";
|
|
58
21
|
export class ConsolidateMemories extends Resource {
|
|
59
22
|
// Self-authorize via the Ed25519 agent verify (auth reshape removes the gate's
|
|
60
23
|
// admin elevation). Any verified agent may consolidate; the isAdmin checks in
|
|
@@ -4,40 +4,10 @@ import { getEmbedding, getMode } from "./embeddings-provider.js";
|
|
|
4
4
|
import { patchRecord, withDetachedTxn } from "./table-helpers.js";
|
|
5
5
|
import { checkRateLimit, rateLimitResponse } from "./rate-limiter.js";
|
|
6
6
|
import { wrapUntrusted } from "./content-safety.js";
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
standard: 0.7,
|
|
12
|
-
ephemeral: 0.4,
|
|
13
|
-
};
|
|
14
|
-
// Half-life in days for exponential decay per durability level
|
|
15
|
-
const DECAY_HALF_LIFE_DAYS = {
|
|
16
|
-
permanent: Infinity, // never decays
|
|
17
|
-
persistent: 90,
|
|
18
|
-
standard: 30,
|
|
19
|
-
ephemeral: 7,
|
|
20
|
-
};
|
|
21
|
-
function recencyFactor(createdAt, durability) {
|
|
22
|
-
const halfLife = DECAY_HALF_LIFE_DAYS[durability] ?? 30;
|
|
23
|
-
if (halfLife === Infinity)
|
|
24
|
-
return 1.0;
|
|
25
|
-
const ageDays = (Date.now() - Date.parse(createdAt)) / (1000 * 60 * 60 * 24);
|
|
26
|
-
const lambda = Math.LN2 / halfLife;
|
|
27
|
-
return Math.exp(-lambda * ageDays);
|
|
28
|
-
}
|
|
29
|
-
function retrievalBoost(retrievalCount) {
|
|
30
|
-
if (!retrievalCount || retrievalCount <= 0)
|
|
31
|
-
return 1.0;
|
|
32
|
-
return 1.0 + 0.1 * Math.log2(retrievalCount); // gentle boost: 10 retrievals → ~1.33x
|
|
33
|
-
}
|
|
34
|
-
function compositeScore(semanticScore, record) {
|
|
35
|
-
const durability = record.durability ?? "standard";
|
|
36
|
-
const dWeight = DURABILITY_WEIGHTS[durability] ?? 0.7;
|
|
37
|
-
const rFactor = record.createdAt ? recencyFactor(record.createdAt, durability) : 1.0;
|
|
38
|
-
const rBoost = retrievalBoost(record.retrievalCount ?? 0);
|
|
39
|
-
return semanticScore * dWeight * rFactor * rBoost;
|
|
40
|
-
}
|
|
7
|
+
// Temporal decay + relevance scoring (incl. the OPS-AYGD retrievalBoost cap +
|
|
8
|
+
// relevance floor) lives in ./scoring.ts — a Harper-free module so it can be
|
|
9
|
+
// unit-tested directly (see test/unit/temporal-scoring.test.ts).
|
|
10
|
+
import { compositeScore } from "./scoring.js";
|
|
41
11
|
// Convert HNSW cosine distance (1 - similarity) to similarity score
|
|
42
12
|
function distanceToSimilarity(distance) {
|
|
43
13
|
return 1 - distance;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// ─── Memory Consolidation — pure evaluation logic ───────────────────────────
|
|
2
|
+
// Pure helpers extracted from MemoryConsolidate.ts (Flair #502) so they can be
|
|
3
|
+
// unit-tested directly. Importing MemoryConsolidate.ts pulls in the Harper
|
|
4
|
+
// runtime (`databases` / `Resource`, storage init) and can't run outside a live
|
|
5
|
+
// Harper; this module has no Harper dependency, so
|
|
6
|
+
// test/unit/memory-consolidate.test.ts exercises the real shipped code.
|
|
7
|
+
export function parseDuration(s) {
|
|
8
|
+
const m = s.match(/^(\d+)([dhm])$/);
|
|
9
|
+
if (!m)
|
|
10
|
+
return 30 * 86400_000;
|
|
11
|
+
const n = Number(m[1]);
|
|
12
|
+
if (m[2] === "d")
|
|
13
|
+
return n * 86400_000;
|
|
14
|
+
if (m[2] === "h")
|
|
15
|
+
return n * 3600_000;
|
|
16
|
+
if (m[2] === "m")
|
|
17
|
+
return n * 60_000;
|
|
18
|
+
return 30 * 86400_000;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Classify a single memory record as a promote/archive/keep candidate.
|
|
22
|
+
*
|
|
23
|
+
* Idle age is `now - (lastRetrieved ?? createdAt)`: a memory that was just
|
|
24
|
+
* written and never read has lastRetrieved=null, so without the createdAt
|
|
25
|
+
* fallback its idle age would be Infinity and a minutes-old memory would read
|
|
26
|
+
* as maximally stale and be archived (Flair #502). The createdAt fallback
|
|
27
|
+
* measures "never retrieved" from when the memory was born.
|
|
28
|
+
*
|
|
29
|
+
* A grace window (olderThanMs — the maintenance olderThan) guarantees a
|
|
30
|
+
* freshly-created memory is never an archive candidate regardless of retrieval
|
|
31
|
+
* count: archival only considers memories that have had a fair chance to be read.
|
|
32
|
+
*/
|
|
33
|
+
export function evaluate(record, now, olderThanMs) {
|
|
34
|
+
const ageMs = record.createdAt ? now - new Date(record.createdAt).getTime() : 0;
|
|
35
|
+
const count = record.retrievalCount ?? 0;
|
|
36
|
+
const lastUse = record.lastRetrieved ?? record.createdAt;
|
|
37
|
+
const idleMs = lastUse ? now - new Date(lastUse).getTime() : 0;
|
|
38
|
+
const daysIdle = idleMs / 86400_000;
|
|
39
|
+
const everRetrieved = record.lastRetrieved != null;
|
|
40
|
+
const { embedding, ...memory } = record;
|
|
41
|
+
// Promote: high retrieval + persistent durability
|
|
42
|
+
if (record.durability === "persistent" && count >= 5) {
|
|
43
|
+
return { memory, suggestion: "promote", reason: `Retrieved ${count} times — strong promotion candidate for permanent` };
|
|
44
|
+
}
|
|
45
|
+
// Promote: standard → persistent if retrieved frequently
|
|
46
|
+
if (record.durability === "standard" && count >= 3 && ageMs > 7 * 86400_000) {
|
|
47
|
+
return { memory, suggestion: "promote", reason: `Retrieved ${count} times over ${Math.round(ageMs / 86400_000)} days — worth persisting` };
|
|
48
|
+
}
|
|
49
|
+
// Grace window: a freshly-created memory is never an archive candidate,
|
|
50
|
+
// regardless of retrieval count (Flair #502). Archive branches below are
|
|
51
|
+
// reachable only for memories older than the maintenance window.
|
|
52
|
+
if (ageMs <= olderThanMs) {
|
|
53
|
+
return { memory, suggestion: "keep", reason: everRetrieved
|
|
54
|
+
? `Retrieved ${count} times, ${Math.round(daysIdle)} days since last retrieval`
|
|
55
|
+
: `Created ${Math.round(ageMs / 86400_000)} days ago, not yet retrieved (within grace window)` };
|
|
56
|
+
}
|
|
57
|
+
// Archive: old + never retrieved
|
|
58
|
+
if (daysIdle > 30 && count === 0) {
|
|
59
|
+
return { memory, suggestion: "archive", reason: everRetrieved
|
|
60
|
+
? `Not retrieved in ${Math.round(daysIdle)} days, ${Math.round(ageMs / 86400_000)} days old`
|
|
61
|
+
: `Never retrieved, ${Math.round(ageMs / 86400_000)} days old` };
|
|
62
|
+
}
|
|
63
|
+
// Archive: idle > 60 days with few retrievals
|
|
64
|
+
if (daysIdle > 60 && count < 2) {
|
|
65
|
+
return { memory, suggestion: "archive", reason: everRetrieved
|
|
66
|
+
? `Not retrieved in ${Math.round(daysIdle)} days (only ${count} total retrievals)`
|
|
67
|
+
: `Never retrieved, ${Math.round(ageMs / 86400_000)} days old (0 total retrievals)` };
|
|
68
|
+
}
|
|
69
|
+
return { memory, suggestion: "keep", reason: everRetrieved
|
|
70
|
+
? `Retrieved ${count} times, ${Math.round(daysIdle)} days since last retrieval`
|
|
71
|
+
: `Created ${Math.round(ageMs / 86400_000)} days ago, not yet retrieved` };
|
|
72
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// ─── Temporal Decay + Relevance Scoring ─────────────────────────────────────
|
|
2
|
+
// Pure scoring functions extracted from SemanticSearch.ts (OPS-AYGD) so they can
|
|
3
|
+
// be unit-tested directly. Importing SemanticSearch.ts pulls in the Harper runtime
|
|
4
|
+
// (storage init) and can't run outside a live Harper; this module has no Harper
|
|
5
|
+
// dependency, so test/unit/temporal-scoring.test.ts exercises the real shipped code.
|
|
6
|
+
export const DURABILITY_WEIGHTS = {
|
|
7
|
+
permanent: 1.0,
|
|
8
|
+
persistent: 0.9,
|
|
9
|
+
standard: 0.7,
|
|
10
|
+
ephemeral: 0.4,
|
|
11
|
+
};
|
|
12
|
+
// Half-life in days for exponential decay per durability level
|
|
13
|
+
export const DECAY_HALF_LIFE_DAYS = {
|
|
14
|
+
permanent: Infinity, // never decays
|
|
15
|
+
persistent: 90,
|
|
16
|
+
standard: 30,
|
|
17
|
+
ephemeral: 7,
|
|
18
|
+
};
|
|
19
|
+
export function recencyFactor(createdAt, durability) {
|
|
20
|
+
const halfLife = DECAY_HALF_LIFE_DAYS[durability] ?? 30;
|
|
21
|
+
if (halfLife === Infinity)
|
|
22
|
+
return 1.0;
|
|
23
|
+
const ageDays = (Date.now() - Date.parse(createdAt)) / (1000 * 60 * 60 * 24);
|
|
24
|
+
const lambda = Math.LN2 / halfLife;
|
|
25
|
+
return Math.exp(-lambda * ageDays);
|
|
26
|
+
}
|
|
27
|
+
// OPS-AYGD: the retrieval boost was unbounded and OVERRODE semantic ranking —
|
|
28
|
+
// recall-eval 2026-06-19 showed composite p@3 0.83 vs raw 1.00, with a popular doc
|
|
29
|
+
// magnetised into 5/6 queries (its rBoost lifted a 0.55-0.65 semantic score above
|
|
30
|
+
// the correct docs). Bounded to a gentle nudge that breaks near-ties without
|
|
31
|
+
// overriding a clear semantic winner. Tuned offline against the real corpus
|
|
32
|
+
// (floor 0.5 + cap 1.1 → composite p@3 recovers to 1.00, magnet eliminated).
|
|
33
|
+
// A query-relative tie-breaker gate (boost only within ~0.05 of the top raw score)
|
|
34
|
+
// is the principled follow-up for graduated boosting; it needs the search loop to
|
|
35
|
+
// pass the candidate-set top score, so it's deferred to its own change.
|
|
36
|
+
export const RBOOST_CAP = 1.1; // max +10% — a tie-breaker, not an override
|
|
37
|
+
export const RBOOST_RELEVANCE_FLOOR = 0.5; // no boost at all for clearly-irrelevant docs
|
|
38
|
+
export function retrievalBoost(retrievalCount) {
|
|
39
|
+
if (!retrievalCount || retrievalCount <= 0)
|
|
40
|
+
return 1.0;
|
|
41
|
+
return Math.min(1.0 + 0.1 * Math.log2(retrievalCount), RBOOST_CAP); // gentle, capped
|
|
42
|
+
}
|
|
43
|
+
export function compositeScore(semanticScore, record) {
|
|
44
|
+
const durability = record.durability ?? "standard";
|
|
45
|
+
const dWeight = DURABILITY_WEIGHTS[durability] ?? 0.7;
|
|
46
|
+
const rFactor = record.createdAt ? recencyFactor(record.createdAt, durability) : 1.0;
|
|
47
|
+
// OPS-AYGD: only apply the retrieval boost when the record is genuinely relevant to
|
|
48
|
+
// this query (semanticScore clears the floor). Below the floor, a popular doc gets
|
|
49
|
+
// no lift — kills the cross-query magnet while preserving boosts for relevant docs.
|
|
50
|
+
const rBoost = semanticScore >= RBOOST_RELEVANCE_FLOOR ? retrievalBoost(record.retrievalCount ?? 0) : 1.0;
|
|
51
|
+
return semanticScore * dWeight * rFactor * rBoost;
|
|
52
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tpsdev-ai/flair",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Identity, memory, and soul for AI agents. Cryptographic identity (Ed25519), semantic memory with local embeddings, and persistent personality — all in a single process.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|