@tpsdev-ai/flair 0.28.0 → 0.29.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 +1 -1
- package/dist/bridges/builtins/index.js +1 -1
- package/dist/bridges/discover.js +1 -1
- package/dist/bridges/runtime/load-plugin.js +1 -1
- package/dist/bridges/scaffold.js +3 -3
- package/dist/bridges/types.js +1 -1
- package/dist/cli.js +126 -26
- package/dist/rem/runner.js +2 -2
- package/dist/resources/MemoryReflect.js +1 -1
- package/dist/resources/RecordUsage.js +15 -1
- package/dist/resources/embeddings-provider.js +8 -38
- package/dist/resources/mcp-tools.js +9 -1
- package/dist/resources/memory-reflect-lib.js +1 -1
- package/dist/resources/models-dir.js +55 -0
- package/dist/resources/record-types.js +2 -2
- package/dist/resources/rerank-provider.js +24 -4
- package/dist/resources/usage-recording.js +72 -6
- package/dist/resources/version.js +33 -0
- package/docs/assets/flair-cross-orchestrator.cast +1 -1
- package/docs/bridges.md +3 -3
- package/docs/mcp-clients.md +3 -1
- package/docs/n8n.md +1 -2
- package/docs/notes/rem-ux.md +1 -1
- package/docs/rem.md +2 -0
- package/package.json +1 -1
- package/schemas/memory.graphql +18 -11
package/README.md
CHANGED
|
@@ -67,7 +67,7 @@ If you need any of those specifically, use them. If you need crypto-pinned ident
|
|
|
67
67
|
|
|
68
68
|
Anthropic shipped [Claude Dreams](https://platform.claude.com/docs/en/managed-agents/dreams) (research preview, April 2026) — async pipeline that reads a memory store + session transcripts and produces a curated output store: duplicates merged, stale entries replaced, insights surfaced. Validates the category: agent memory accumulates drift and needs cleanup.
|
|
69
69
|
|
|
70
|
-
Flair ships both the on-demand curation surface (`flair rem rapid`) AND the scheduled nightly cycle, per
|
|
70
|
+
Flair ships both the on-demand curation surface (`flair rem rapid`) AND the scheduled nightly cycle, per [`docs/rem.md`](docs/rem.md) and its [distillation UX design note](docs/notes/rem-ux.md). **REM requires a configured generative backend** (Ollama, OpenAI, Anthropic, etc.) — without one, `flair rem rapid` fails with `Reflection error: No generative backend configured`. Config recipe (Ollama zero-key default, hosted-provider egress warning, clustered-deploy rules): [`docs/rem.md`](docs/rem.md).
|
|
71
71
|
|
|
72
72
|
- **`flair rem rapid`** — reflects and distills server-side by default, staging candidates in one bounded call. `--focus {lessons_learned, patterns, decisions, errors}` mirrors Dreams' `instructions` parameter. `--prompt-only` falls back to the bring-your-own-model handoff. Outputs *candidates*, not a wholesale store swap.
|
|
73
73
|
- **`flair rem candidates` / `flair rem promote <id> --rationale "<why>"` / `flair rem reject <id>`** — review and promote distilled candidates with required rationale.
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* To add a new built-in:
|
|
11
11
|
* - YAML: write `src/bridges/builtins/<name>.ts` exporting a `YamlBridgeDescriptor`
|
|
12
12
|
* - Code plugin: write `src/bridges/builtins/<name>.ts` exporting a `MemoryBridge`
|
|
13
|
-
* Then register it here. See
|
|
13
|
+
* Then register it here. See docs/bridges.md § Two shapes for shapes A/B.
|
|
14
14
|
*/
|
|
15
15
|
import { agenticStackDescriptor } from "./agentic-stack.js";
|
|
16
16
|
import { claudeProjectMemoryBridge } from "./claude-project.js";
|
package/dist/bridges/discover.js
CHANGED
|
@@ -53,7 +53,7 @@ export async function loadCodePlugin(discovered, opts = {}) {
|
|
|
53
53
|
field: "exports",
|
|
54
54
|
expected: "named `bridge` export or default export implementing MemoryBridge",
|
|
55
55
|
got: typeof mod === "object" && mod !== null ? `exports=${Object.keys(mod).join(",") || "(empty)"}` : typeof mod,
|
|
56
|
-
hint: `flair-bridge-<name> packages must export \`bridge\` (or default-export) a MemoryBridge. See
|
|
56
|
+
hint: `flair-bridge-<name> packages must export \`bridge\` (or default-export) a MemoryBridge. See docs/bridges.md § Shape B`,
|
|
57
57
|
});
|
|
58
58
|
}
|
|
59
59
|
// Validate the shape. Keep this minimal — we trust the plugin author
|
package/dist/bridges/scaffold.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Bridge scaffolding — emits a working starter for a new bridge.
|
|
3
3
|
*
|
|
4
|
-
* From
|
|
4
|
+
* From docs/bridges.md § Two shapes: "Scaffold & test in-tree. `flair bridge
|
|
5
5
|
* scaffold <name>` emits a working starter; `flair bridge test <name>`
|
|
6
6
|
* runs a round-trip diff." This file handles the `scaffold` half.
|
|
7
7
|
*
|
|
@@ -34,7 +34,7 @@ function fileDescriptorTemplate(name) {
|
|
|
34
34
|
# Generated by \`flair bridge scaffold ${name} --file\`.
|
|
35
35
|
# Edit to match your foreign system, then run \`flair bridge test ${name}\`.
|
|
36
36
|
#
|
|
37
|
-
# Spec: see
|
|
37
|
+
# Spec: see docs/bridges.md § The memory record and § Shape A.
|
|
38
38
|
name: ${name}
|
|
39
39
|
version: 1
|
|
40
40
|
kind: file
|
|
@@ -90,7 +90,7 @@ function apiIndexTemplate(name) {
|
|
|
90
90
|
* flair-bridge-${name}
|
|
91
91
|
*
|
|
92
92
|
* Generated by \`flair bridge scaffold ${name} --api\`.
|
|
93
|
-
* Spec: see
|
|
93
|
+
* Spec: see docs/bridges.md § The memory record and § Shape B.
|
|
94
94
|
*
|
|
95
95
|
* After editing, run:
|
|
96
96
|
* flair bridge test ${name}
|
package/dist/bridges/types.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Types for the Flair memory bridge plugin system.
|
|
3
3
|
*
|
|
4
|
-
* Mirrors the contract in
|
|
4
|
+
* Mirrors the contract in docs/bridges.md. These types are the
|
|
5
5
|
* public surface a bridge author (human or agent) targets.
|
|
6
6
|
*
|
|
7
7
|
* IMPORTANT: the YAML bridge format (shape A) also conforms to these types
|
package/dist/cli.js
CHANGED
|
@@ -384,6 +384,33 @@ export function detectOpsApiAllInterfacesBind(portValue) {
|
|
|
384
384
|
}
|
|
385
385
|
return { allInterfaces: true, boundHost: null };
|
|
386
386
|
}
|
|
387
|
+
/**
|
|
388
|
+
* Decide the source `flair init` should use for the admin password when no
|
|
389
|
+
* explicit `--admin-pass` / `--admin-pass-file` / env var was given
|
|
390
|
+
* (flair#827).
|
|
391
|
+
*
|
|
392
|
+
* Before this existed, init ALWAYS generated a fresh random password and
|
|
393
|
+
* overwrote `~/.flair/admin-pass` on every run — including a re-run against
|
|
394
|
+
* an install that was already bootstrapped and working (e.g. following
|
|
395
|
+
* `flair doctor`'s ops-bind finding, whose only prescribed remedy is
|
|
396
|
+
* re-running `flair init`). Harper's `HDB_ADMIN_PASSWORD` env var only seeds
|
|
397
|
+
* a brand-new install's user record — it does NOT rotate an existing user's
|
|
398
|
+
* stored password hash on every boot. So overwriting the file desynced it
|
|
399
|
+
* from what Harper actually had persisted, and the very next ops-API call in
|
|
400
|
+
* that SAME init run (seeding the agent) failed with a 401 "Login failed",
|
|
401
|
+
* breaking working auth on an install that had nothing wrong with its
|
|
402
|
+
* credentials.
|
|
403
|
+
*
|
|
404
|
+
* An admin-pass file that already exists on disk IS the working install's
|
|
405
|
+
* password — `flair init` is the only thing that ever writes it — so reuse
|
|
406
|
+
* it instead of generating a new one. That makes re-init idempotent: safe to
|
|
407
|
+
* run again at any time without risking the instance's auth. Deliberately
|
|
408
|
+
* rotating the admin password is a separate operation, not a `flair init`
|
|
409
|
+
* side effect.
|
|
410
|
+
*/
|
|
411
|
+
export function resolveInitAdminPasswordSource(adminPassFileExists) {
|
|
412
|
+
return adminPassFileExists ? "reuse-existing" : "generate-new";
|
|
413
|
+
}
|
|
387
414
|
// ─── Ops-socket permission posture (flair#763) ─────────────────────────────────
|
|
388
415
|
//
|
|
389
416
|
// The ops API domain socket (dataDir/operations-server) is protected by a
|
|
@@ -1846,6 +1873,39 @@ const __pkgVersion = (() => {
|
|
|
1846
1873
|
return "unknown";
|
|
1847
1874
|
}
|
|
1848
1875
|
})();
|
|
1876
|
+
/**
|
|
1877
|
+
* The `@tpsdev-ai/flair-mcp` spec written into a client's MCP config.
|
|
1878
|
+
*
|
|
1879
|
+
* PINNED, deliberately. A bare `npx -y @tpsdev-ai/flair-mcp` re-resolves to
|
|
1880
|
+
* whatever is currently published on EVERY agent session — so a single bad
|
|
1881
|
+
* publish (stolen credentials, a malicious commit that clears review, or a
|
|
1882
|
+
* compromised dependency of the MCP package) reaches every wired user
|
|
1883
|
+
* silently, with no lockfile and no review step in the path. The postmark-mcp
|
|
1884
|
+
* incident was exactly this shape: a legitimate publish by the legitimate
|
|
1885
|
+
* owner, propagating for 16 days before anyone noticed. Worse, a yank does
|
|
1886
|
+
* not help — unpinned clients keep resolving latest.
|
|
1887
|
+
*
|
|
1888
|
+
* Our publish side is already hardened (OIDC staged publish, human 2FA at the
|
|
1889
|
+
* release gate), but that defends against credential theft, not against a bad
|
|
1890
|
+
* version being published legitimately. The consumer side is where that gap
|
|
1891
|
+
* closes, and pinning is what closes it: a wired client keeps running the
|
|
1892
|
+
* exact version that was current when it was wired, and moving forward
|
|
1893
|
+
* becomes a deliberate act.
|
|
1894
|
+
*
|
|
1895
|
+
* flair and flair-mcp ship in version lockstep from this monorepo, so the
|
|
1896
|
+
* running CLI's own version is the correct pin. `flair init` re-run rewires
|
|
1897
|
+
* to the then-current version; see the upgrade-rewire follow-up issue for
|
|
1898
|
+
* making `flair upgrade` do the same.
|
|
1899
|
+
*
|
|
1900
|
+
* Falls back to the unpinned spec only when the version can't be read, which
|
|
1901
|
+
* is the same condition under which `--version` reports "unknown" — a broken
|
|
1902
|
+
* install, where a working MCP wiring matters more than a precise pin.
|
|
1903
|
+
*/
|
|
1904
|
+
export function mcpServerSpec(version = __pkgVersion) {
|
|
1905
|
+
return version && version !== "unknown"
|
|
1906
|
+
? `@tpsdev-ai/flair-mcp@${version}`
|
|
1907
|
+
: "@tpsdev-ai/flair-mcp";
|
|
1908
|
+
}
|
|
1849
1909
|
const program = new Command();
|
|
1850
1910
|
program.name("flair").version(__pkgVersion, "-v, --version");
|
|
1851
1911
|
// ─── CLI↔server version handshake (flair#695 §B) ────────────────────────────
|
|
@@ -2117,9 +2177,10 @@ program
|
|
|
2117
2177
|
selectedClients.push(clientOpt);
|
|
2118
2178
|
}
|
|
2119
2179
|
// Admin password: determine from opts, env, or generate
|
|
2120
|
-
// Priority: 1) --admin-pass-file, 2) env vars, 3) generate new
|
|
2180
|
+
// Priority: 1) --admin-pass-file, 2) env vars, 3) reuse existing file, 4) generate new
|
|
2121
2181
|
let adminPass;
|
|
2122
2182
|
let passwordSource = "generated";
|
|
2183
|
+
let reusedExistingAdminPass = false;
|
|
2123
2184
|
// Warn if --admin-pass is passed inline (not from env)
|
|
2124
2185
|
if (shouldShowInlineSecretWarning(opts.adminPass, false, new Set(["--admin-pass"]), "--admin-pass")) {
|
|
2125
2186
|
console.error("warning: --admin-pass passed inline. Consider --admin-pass-file <path> or FLAIR_ADMIN_PASS env " +
|
|
@@ -2151,34 +2212,65 @@ program
|
|
|
2151
2212
|
passwordSource = "env"; // Treat same as env for display purposes
|
|
2152
2213
|
}
|
|
2153
2214
|
else {
|
|
2154
|
-
// Generate new password and write to file atomically
|
|
2155
|
-
adminPass = Buffer.from(nacl.randomBytes(18)).toString("base64url");
|
|
2156
|
-
passwordSource = "generated";
|
|
2157
|
-
// Atomic write: create temp file in same dir, then rename
|
|
2158
2215
|
const flairDir = join(homedir(), ".flair");
|
|
2159
|
-
mkdirSync(flairDir, { recursive: true });
|
|
2160
2216
|
const adminPassPath = join(flairDir, "admin-pass");
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2217
|
+
passwordSource = "generated";
|
|
2218
|
+
if (resolveInitAdminPasswordSource(existsSync(adminPassPath)) === "reuse-existing") {
|
|
2219
|
+
// flair#827: an admin-pass file already on disk means a PRIOR `flair
|
|
2220
|
+
// init` already bootstrapped Harper's admin user with this password.
|
|
2221
|
+
// HDB_ADMIN_PASSWORD only seeds a brand-new install — Harper does
|
|
2222
|
+
// NOT rotate an existing user's stored password hash from env on
|
|
2223
|
+
// every boot. Generating and overwriting the file here would desync
|
|
2224
|
+
// it from what Harper actually has persisted, breaking ops-API auth
|
|
2225
|
+
// (401 "Login failed") on THIS SAME init run (the agent-seeding call
|
|
2226
|
+
// below) without fixing whatever the re-run was meant to fix — e.g.
|
|
2227
|
+
// `flair doctor`'s ops-bind finding, whose only prescribed remedy is
|
|
2228
|
+
// re-running `flair init`. Re-init must be idempotent here: reuse
|
|
2229
|
+
// the existing password so it's always safe to re-run against a
|
|
2230
|
+
// working install. Rotating the admin password on purpose is a
|
|
2231
|
+
// separate, deliberate operation (see the ops runbook), not a side
|
|
2232
|
+
// effect of re-init.
|
|
2233
|
+
try {
|
|
2234
|
+
adminPass = readAdminPassFileSecure(adminPassPath);
|
|
2235
|
+
}
|
|
2236
|
+
catch (err) {
|
|
2237
|
+
console.error(`Error: ${err.message}`);
|
|
2238
|
+
process.exit(1);
|
|
2239
|
+
}
|
|
2240
|
+
reusedExistingAdminPass = true;
|
|
2167
2241
|
}
|
|
2168
|
-
|
|
2169
|
-
//
|
|
2242
|
+
else {
|
|
2243
|
+
// Generate new password and write to file atomically
|
|
2244
|
+
adminPass = Buffer.from(nacl.randomBytes(18)).toString("base64url");
|
|
2245
|
+
// Atomic write: create temp file in same dir, then rename
|
|
2246
|
+
mkdirSync(flairDir, { recursive: true });
|
|
2247
|
+
const tempPath = mkdtempSync(join(flairDir, ".admin-pass.tmp-"));
|
|
2248
|
+
const finalTempPath = join(tempPath, "admin-pass");
|
|
2170
2249
|
try {
|
|
2250
|
+
writeFileSync(finalTempPath, adminPass + "\n", { mode: 0o600 });
|
|
2251
|
+
renameSync(finalTempPath, adminPassPath);
|
|
2171
2252
|
rmSync(tempPath, { recursive: true, force: true });
|
|
2172
2253
|
}
|
|
2173
|
-
catch {
|
|
2174
|
-
|
|
2254
|
+
catch (err) {
|
|
2255
|
+
// Clean up temp dir on failure
|
|
2256
|
+
try {
|
|
2257
|
+
rmSync(tempPath, { recursive: true, force: true });
|
|
2258
|
+
}
|
|
2259
|
+
catch { }
|
|
2260
|
+
throw err;
|
|
2261
|
+
}
|
|
2175
2262
|
}
|
|
2176
2263
|
}
|
|
2177
2264
|
const adminUser = DEFAULT_ADMIN_USER;
|
|
2178
|
-
// If we generated the password, report where it
|
|
2265
|
+
// If we generated (or reused) the password, report where it lives
|
|
2179
2266
|
if (passwordSource === "generated") {
|
|
2180
2267
|
const adminPassPath = join(homedir(), ".flair", "admin-pass");
|
|
2181
|
-
|
|
2268
|
+
if (reusedExistingAdminPass) {
|
|
2269
|
+
console.log(`Reusing existing admin password from: ${adminPassPath} (flair#827: re-init never rotates it — see the ops runbook to change it deliberately)`);
|
|
2270
|
+
}
|
|
2271
|
+
else {
|
|
2272
|
+
console.log(`Admin password saved to: ${adminPassPath}`);
|
|
2273
|
+
}
|
|
2182
2274
|
}
|
|
2183
2275
|
// Check Node.js version
|
|
2184
2276
|
const major = parseInt(process.version.slice(1), 10);
|
|
@@ -2515,7 +2607,8 @@ program
|
|
|
2515
2607
|
}
|
|
2516
2608
|
// ── MCP client wiring ────────────────────────────────────────────────
|
|
2517
2609
|
// The full one-command front door: detect installed MCP clients and wire
|
|
2518
|
-
// each to the zero-install `npx -y @tpsdev-ai/flair-mcp
|
|
2610
|
+
// each to the zero-install `npx -y @tpsdev-ai/flair-mcp@<version>` server
|
|
2611
|
+
// (pinned — see mcpServerSpec()). Claude
|
|
2519
2612
|
// Code is auto-wired into ~/.claude.json (the only client the CLI can
|
|
2520
2613
|
// safely modify); other clients get copy-paste snippets. `--no-mcp`
|
|
2521
2614
|
// skips wiring entirely; `--client <name>` targets one client; the
|
|
@@ -2551,7 +2644,7 @@ program
|
|
|
2551
2644
|
const flairMcpConfig = {
|
|
2552
2645
|
type: "stdio",
|
|
2553
2646
|
command: "npx",
|
|
2554
|
-
args: ["-y",
|
|
2647
|
+
args: ["-y", mcpServerSpec()],
|
|
2555
2648
|
// flair#718 authorship-provenance: each client's wired env block
|
|
2556
2649
|
// gets its OWN FLAIR_CLIENT label (never the shared mcpEnv
|
|
2557
2650
|
// object directly — that would stamp the same label into every
|
|
@@ -2640,7 +2733,9 @@ program
|
|
|
2640
2733
|
if (!opts.skipSmoke && !noMcp && clientOpt !== "none" && wiringResults.length > 0) {
|
|
2641
2734
|
console.log("\n Smoke-testing MCP server...");
|
|
2642
2735
|
try {
|
|
2643
|
-
|
|
2736
|
+
// Same spec that gets WIRED above — the smoke test must exercise the
|
|
2737
|
+
// exact version the user will run, not whatever npm resolves latest to.
|
|
2738
|
+
const mcpProc = spawn("npx", ["-y", mcpServerSpec()], {
|
|
2644
2739
|
env: { ...process.env, FLAIR_AGENT_ID: agentId, FLAIR_URL: httpUrl },
|
|
2645
2740
|
stdio: ["pipe", "pipe", "pipe"],
|
|
2646
2741
|
});
|
|
@@ -5863,8 +5958,7 @@ export function describeReflectError(message) {
|
|
|
5863
5958
|
return { kind: "other", text: message };
|
|
5864
5959
|
}
|
|
5865
5960
|
// ─── flair rem rapid ──────────────────────────────────────────────────────────
|
|
5866
|
-
// Executes by default (
|
|
5867
|
-
// issue #707): distills server-side via /ReflectMemories execute:true and
|
|
5961
|
+
// Executes by default (§3C, issue #707): distills server-side via /ReflectMemories execute:true and
|
|
5868
5962
|
// stages MemoryCandidate rows, printing a staged-candidate summary. --prompt-only
|
|
5869
5963
|
// preserves the pre-#710 handoff behavior byte-for-byte, for the bring-your-
|
|
5870
5964
|
// own-model workflow.
|
|
@@ -9707,7 +9801,13 @@ program
|
|
|
9707
9801
|
// 3b. Ops API bind (flair#670) — report-only finding, never auto-fixed.
|
|
9708
9802
|
// Rebinding the ops API requires a Harper restart to take effect, so
|
|
9709
9803
|
// `doctor --fix` deliberately does not touch it here; the fix is
|
|
9710
|
-
// `flair init` (re-run) or a manual
|
|
9804
|
+
// `flair init` (re-run, then `flair restart` to apply it) or a manual
|
|
9805
|
+
// harper-config.yaml edit + restart. flair#827: re-running `flair init`
|
|
9806
|
+
// used to regenerate ~/.flair/admin-pass unconditionally, desyncing it
|
|
9807
|
+
// from Harper's already-persisted credential and breaking admin auth on
|
|
9808
|
+
// the very re-run this remedy prescribed — resolveInitAdminPasswordSource
|
|
9809
|
+
// (see its doc comment) now reuses the existing password instead, so this
|
|
9810
|
+
// remedy is safe to follow on a working install.
|
|
9711
9811
|
try {
|
|
9712
9812
|
const harperConfigPath = join(defaultDataDir(), "harper-config.yaml");
|
|
9713
9813
|
if (existsSync(harperConfigPath)) {
|
|
@@ -9716,7 +9816,7 @@ program
|
|
|
9716
9816
|
const bind = detectOpsApiAllInterfacesBind(opsPortValue);
|
|
9717
9817
|
if (bind.allInterfaces) {
|
|
9718
9818
|
console.log(` ${render.icons.error} Ops API bound to ${render.wrap(render.c.bold, "all interfaces")} (${render.wrap(render.c.dim, String(opsPortValue))})`);
|
|
9719
|
-
console.log(` ${render.wrap(render.c.dim, "Single-host installs don't need this reachable off-box. Fix:")} flair init ${render.wrap(render.c.dim, "(rebinds to loopback + domain socket on
|
|
9819
|
+
console.log(` ${render.wrap(render.c.dim, "Single-host installs don't need this reachable off-box. Fix:")} flair init && flair restart ${render.wrap(render.c.dim, "(rebinds to loopback + domain socket; re-init reuses your existing admin password, so this is safe on a running install — pass --ops-bind for deliberate remote admin)")}`);
|
|
9720
9820
|
issues++;
|
|
9721
9821
|
}
|
|
9722
9822
|
}
|
|
@@ -12082,7 +12182,7 @@ soul.command("list")
|
|
|
12082
12182
|
// Slice 1: discovery + scaffold. Slice 2: YAML runtime + `import` for Shape A
|
|
12083
12183
|
// + agentic-stack reference adapter as a built-in.
|
|
12084
12184
|
// `test` and `export` are still stubbed; Shape B (npm code plugins) too.
|
|
12085
|
-
// See
|
|
12185
|
+
// See docs/bridges.md.
|
|
12086
12186
|
const bridge = program.command("bridge").description("Manage memory bridges (import/export between Flair and foreign systems)");
|
|
12087
12187
|
bridge
|
|
12088
12188
|
.command("list")
|
package/dist/rem/runner.js
CHANGED
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
* - Steps 1, 2 shipped in slice-1 PR-1 (#414).
|
|
21
21
|
* - Step 3 (maintenance) shipped in a prior slice-2 PR — fills
|
|
22
22
|
* `archived`/`expired` in the audit row.
|
|
23
|
-
* - Step 4: per
|
|
24
|
-
*
|
|
23
|
+
* - Step 4: per §3B (issue #707), "Deferred from parent § 4 step 4" —
|
|
24
|
+
* trust tiers aren't derivable yet
|
|
25
25
|
* (that's the emergent-trust arc). The input filter stays as today (own
|
|
26
26
|
* agent, non-archived, non-permanent, scope window); the safety net for
|
|
27
27
|
* un-tiered input is structural — candidates are staged, never
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* reflection prompt for a human/agent to run through their own LLM, or (b)
|
|
6
6
|
* with execute:true, runs the distillation server-side via Harper's models
|
|
7
7
|
* facade and stages the results as MemoryCandidate rows for review. See
|
|
8
|
-
*
|
|
8
|
+
* issue #707 (§3A).
|
|
9
9
|
*
|
|
10
10
|
* Request:
|
|
11
11
|
* agentId string — which agent to reflect on
|
|
@@ -34,6 +34,17 @@
|
|
|
34
34
|
* (and its ownership gate) — its own auth model is verified-agent +
|
|
35
35
|
* within-org + explicitly NO ownership requirement.
|
|
36
36
|
*
|
|
37
|
+
* NO READ-SCOPE GATE — INTENTIONAL ASYMMETRY with citation-on-write
|
|
38
|
+
* (flair#775): this endpoint deliberately does NOT validate reported ids
|
|
39
|
+
* against the caller's read scope. Usage feedback is the cross-agent
|
|
40
|
+
* contract described above (agent B reports using agent A's memory
|
|
41
|
+
* regardless of A's visibility setting), and the no-enumeration response
|
|
42
|
+
* below already denies existence probing on this surface. Citation-on-write
|
|
43
|
+
* (./usage-recording.ts's recordCitations()) DOES scope-gate each cited id —
|
|
44
|
+
* it is a separate write surface with a narrower threat model; see that
|
|
45
|
+
* module's doc. Do not "unify" the two: the difference is a K&S binding
|
|
46
|
+
* condition on the flair#775 locked design, not drift.
|
|
47
|
+
*
|
|
37
48
|
* WHY THIS ISN'T A @table-BACKED RESOURCE: the actual dedup ledger (one row
|
|
38
49
|
* per (agentId, memoryId) contribution) lives in the `MemoryUsage` table
|
|
39
50
|
* (schemas/memory.graphql), guarded by resources/MemoryUsage.ts. But this
|
|
@@ -164,7 +175,10 @@ export class RecordUsage extends Resource {
|
|
|
164
175
|
// ./usage-recording.ts's recordUsageContribution() — a shared,
|
|
165
176
|
// single-implementation extraction (also used by citation-on-write,
|
|
166
177
|
// resources/Memory.ts's post()/put()). Byte-identical behavior to
|
|
167
|
-
// the former private _recordOne() this replaced.
|
|
178
|
+
// the former private _recordOne() this replaced. Deliberately NO
|
|
179
|
+
// read-scope check on memoryId here (flair#775 asymmetry — see the
|
|
180
|
+
// module doc's NO READ-SCOPE GATE paragraph; citation-on-write's
|
|
181
|
+
// recordCitations() is the surface that scope-gates).
|
|
168
182
|
await recordUsageContribution(ctx, agentId, memoryId, attribution, now);
|
|
169
183
|
}
|
|
170
184
|
catch (err) {
|
|
@@ -107,9 +107,6 @@
|
|
|
107
107
|
* still a separate, deliberate step from shipping this code — see the PR
|
|
108
108
|
* description for that distinction.
|
|
109
109
|
*/
|
|
110
|
-
import { join } from "node:path";
|
|
111
|
-
import { existsSync } from "node:fs";
|
|
112
|
-
import { homedir } from "node:os";
|
|
113
110
|
const VALID_INPUT_TYPES = new Set(["document", "query"]);
|
|
114
111
|
/**
|
|
115
112
|
* THE GATE (flair#504 Phase 2 — flipped ON). A module-level constant, not an
|
|
@@ -239,42 +236,15 @@ async function getModelsApi() {
|
|
|
239
236
|
/**
|
|
240
237
|
* Resolve the directory the embeddings model lives in / downloads into.
|
|
241
238
|
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
* downloaded into the package dir before this fix;
|
|
250
|
-
* never used as a download target on fresh installs.
|
|
251
|
-
* 4. ~/.flair/data/models — last-resort default when ROOTPATH is unset.
|
|
252
|
-
*
|
|
253
|
-
* The chosen dir is always writable, so the embeddings engine can download the
|
|
254
|
-
* model on first use without hitting EACCES on a root-owned package dir.
|
|
255
|
-
*
|
|
256
|
-
* Called by `resources/embeddings-boot.ts` (flair#694) to build the
|
|
257
|
-
* `modelsDir` it passes to harper-fabric-embeddings' `register()` — both run
|
|
258
|
-
* in the SAME process now (unlike the retired src/cli.ts-side computation,
|
|
259
|
-
* which duplicated this exact default because it ran in a separate process
|
|
260
|
-
* that set the resulting value via an env var). Still exported and tested
|
|
261
|
-
* independently (test/unit/embeddings-models-dir.test.ts) as the single
|
|
262
|
-
* documented source of truth for this default.
|
|
239
|
+
* Implementation moved to `resources/models-dir.ts` (flair#815) so the
|
|
240
|
+
* reranker can share the exact same resolution without importing this module
|
|
241
|
+
* (several unit-isolated tests `mock.module()` this file wholesale — see
|
|
242
|
+
* models-dir.ts's header). Re-exported here so existing consumers keep their
|
|
243
|
+
* import site: `resources/embeddings-boot.ts` (flair#694), which builds the
|
|
244
|
+
* `modelsDir` it passes to harper-fabric-embeddings' `register()`, and
|
|
245
|
+
* test/unit/embeddings-models-dir.test.ts, which pins the resolution order.
|
|
263
246
|
*/
|
|
264
|
-
export
|
|
265
|
-
const override = process.env.FLAIR_MODELS_DIR;
|
|
266
|
-
if (override)
|
|
267
|
-
return override;
|
|
268
|
-
const rootPath = process.env.ROOTPATH;
|
|
269
|
-
if (rootPath)
|
|
270
|
-
return join(rootPath, "models");
|
|
271
|
-
// Backward compat: a prior (writable) install may have the model cached in
|
|
272
|
-
// the package dir. Reuse it rather than re-downloading — but only if present.
|
|
273
|
-
const cwdModels = join(process.cwd(), "models");
|
|
274
|
-
if (existsSync(cwdModels))
|
|
275
|
-
return cwdModels;
|
|
276
|
-
return join(homedir(), ".flair", "data", "models");
|
|
277
|
-
}
|
|
247
|
+
export { resolveModelsDir } from "./models-dir.js";
|
|
278
248
|
// Kern: "Boot probe MUST have a 5-10s timeout ... on timeout set mode='none'
|
|
279
249
|
// + log a warning. A hung probe ... must never block Harper boot."
|
|
280
250
|
const PROBE_TIMEOUT_MS = 8_000;
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
* version bump + a new FlairClient method, out of scope for this query-only
|
|
36
36
|
* slice.
|
|
37
37
|
*/
|
|
38
|
+
import { resolveVersion } from "./version.js";
|
|
38
39
|
const H = {};
|
|
39
40
|
const LOADERS = {
|
|
40
41
|
SemanticSearch: async () => (await import("./SemanticSearch.js")).SemanticSearch,
|
|
@@ -248,7 +249,14 @@ async function bootstrap(agent, args) {
|
|
|
248
249
|
// ONLY when requested so a plain bootstrap delegates a byte-identical body.
|
|
249
250
|
if (args?.abstain === true)
|
|
250
251
|
body.abstain = true;
|
|
251
|
-
|
|
252
|
+
// flair#831 — attach the running Flair version to the RESPONSE (not the
|
|
253
|
+
// delegated request body) so the calling agent learns the server version
|
|
254
|
+
// on its very first call.
|
|
255
|
+
const result = unwrap(await h.post(body));
|
|
256
|
+
if (result && typeof result === "object" && !Array.isArray(result)) {
|
|
257
|
+
return { ...result, flairVersion: resolveVersion() };
|
|
258
|
+
}
|
|
259
|
+
return result;
|
|
252
260
|
}
|
|
253
261
|
async function soulSet(agent, args) {
|
|
254
262
|
const Cls = await handler("Soul");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// ─── Memory Reflection — pure logic for /ReflectMemories ────────────────────
|
|
2
2
|
// Pure helpers backing resources/MemoryReflect.ts (FLAIR-NIGHTLY-REM slice 2,
|
|
3
|
-
// §3A — see
|
|
3
|
+
// §3A — see issue #707).
|
|
4
4
|
//
|
|
5
5
|
// Same split as resources/memory-consolidate-lib.ts: importing MemoryReflect.ts
|
|
6
6
|
// pulls in the Harper runtime (`databases`/`Resource`/`models`, storage init),
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* models-dir.ts — the ONE place that decides where model GGUFs live.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from embeddings-provider.ts (flair#815) so the reranker
|
|
5
|
+
* (rerank-provider.ts) can share the exact same resolution WITHOUT importing
|
|
6
|
+
* embeddings-provider: several unit-isolated tests `mock.module()` the whole
|
|
7
|
+
* embeddings-provider module (with only the named exports THEY consume), and
|
|
8
|
+
* bun's module cache is process-wide — so any new cross-module import of
|
|
9
|
+
* embeddings-provider from another resource makes those mocks incomplete and
|
|
10
|
+
* kills unrelated test files at module load. This module is tiny, pure
|
|
11
|
+
* node-stdlib, and never mocked, so both providers (and any future model
|
|
12
|
+
* consumer) can depend on it safely.
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync } from "node:fs";
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the directory model GGUFs live in / download into — shared by the
|
|
19
|
+
* embedding engine (embeddings-boot.ts's `register()` options) and the
|
|
20
|
+
* cross-encoder reranker (rerank-provider.ts, flair#815 — it used to hardcode
|
|
21
|
+
* `<cwd>/models`, silently failing open wherever Harper's cwd wasn't the
|
|
22
|
+
* models location).
|
|
23
|
+
*
|
|
24
|
+
* Resolution order (everything writable, never the read-only package dir):
|
|
25
|
+
* 1. FLAIR_MODELS_DIR — explicit operator/docker override.
|
|
26
|
+
* 2. <ROOTPATH>/models — Harper's data dir (Flair passes ROOTPATH =
|
|
27
|
+
* ~/.flair/data when it spawns Harper). User-
|
|
28
|
+
* owned and writable even on sudo-global installs.
|
|
29
|
+
* 3. <cwd>/models — ONLY if a model already lives there. Backward
|
|
30
|
+
* compat for existing writable installs that
|
|
31
|
+
* downloaded into the package dir before this fix;
|
|
32
|
+
* never used as a download target on fresh installs.
|
|
33
|
+
* 4. ~/.flair/data/models — last-resort default when ROOTPATH is unset.
|
|
34
|
+
*
|
|
35
|
+
* The chosen dir is always writable, so the embeddings engine can download the
|
|
36
|
+
* model on first use without hitting EACCES on a root-owned package dir.
|
|
37
|
+
*
|
|
38
|
+
* Tested independently (test/unit/embeddings-models-dir.test.ts, via
|
|
39
|
+
* embeddings-provider's re-export) as the single documented source of truth
|
|
40
|
+
* for this default.
|
|
41
|
+
*/
|
|
42
|
+
export function resolveModelsDir() {
|
|
43
|
+
const override = process.env.FLAIR_MODELS_DIR;
|
|
44
|
+
if (override)
|
|
45
|
+
return override;
|
|
46
|
+
const rootPath = process.env.ROOTPATH;
|
|
47
|
+
if (rootPath)
|
|
48
|
+
return join(rootPath, "models");
|
|
49
|
+
// Backward compat: a prior (writable) install may have the model cached in
|
|
50
|
+
// the package dir. Reuse it rather than re-downloading — but only if present.
|
|
51
|
+
const cwdModels = join(process.cwd(), "models");
|
|
52
|
+
if (existsSync(cwdModels))
|
|
53
|
+
return cwdModels;
|
|
54
|
+
return join(homedir(), ".flair", "data", "models");
|
|
55
|
+
}
|
|
@@ -127,8 +127,8 @@
|
|
|
127
127
|
*
|
|
128
128
|
* `remEligible` — MUST be `false` for every entry in v1 (typed as the
|
|
129
129
|
* literal `false`, not `boolean`, so a stray `true` is a compile error, not
|
|
130
|
-
* just a runtime policy mistake). REM's nightly gather step
|
|
131
|
-
*
|
|
130
|
+
* just a runtime policy mistake). REM's nightly gather step (see docs/rem.md)
|
|
131
|
+
* is hardcoded to `GET /Memory?agentId=…`
|
|
132
132
|
* — single table, single agent, by construction. There is no multi-table
|
|
133
133
|
* distillation input path to opt into yet; the field exists so a future
|
|
134
134
|
* REM generalization doesn't require a breaking schema change to every
|
|
@@ -70,10 +70,16 @@
|
|
|
70
70
|
* No Ollama (no logprobs, no rerank endpoint — verified), no network hop, no
|
|
71
71
|
* new auth boundary.
|
|
72
72
|
*
|
|
73
|
-
* GGUF files are NOT committed; they are provisioned into models
|
|
74
|
-
* embedding GGUF
|
|
73
|
+
* GGUF files are NOT committed; they are provisioned into the models dir
|
|
74
|
+
* alongside the embedding GGUF — resolved via the SAME `resolveModelsDir()`
|
|
75
|
+
* the embedding engine uses (FLAIR_MODELS_DIR → <ROOTPATH>/models →
|
|
76
|
+
* <cwd>/models → ~/.flair/data/models; flair#815 — this file used to hardcode
|
|
77
|
+
* <cwd>/models, so any deployment whose cwd wasn't the models location failed
|
|
78
|
+
* init and silently fell open to vector order). See docs/rerank-provisioning.md
|
|
79
|
+
* for download sources.
|
|
75
80
|
*/
|
|
76
81
|
import { join } from "node:path";
|
|
82
|
+
import { resolveModelsDir } from "./models-dir.js";
|
|
77
83
|
// Known reranker models → GGUF filename + inference mode. jina is the DEFAULT
|
|
78
84
|
// (working — see file header, flair#811): its rank-pooling path completes
|
|
79
85
|
// inside Harper. qwen3 is EXPERIMENTAL: its generative path is validated
|
|
@@ -181,6 +187,20 @@ function resolveModelKey() {
|
|
|
181
187
|
return requested;
|
|
182
188
|
return DEFAULT_MODEL;
|
|
183
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* Absolute path the reranker GGUF is expected at: `<models dir>/<file>`,
|
|
192
|
+
* where the models dir is the shared `resolveModelsDir()` (models-dir.ts,
|
|
193
|
+
* same resolution the embedding engine uses) — the single documented source
|
|
194
|
+
* of truth (FLAIR_MODELS_DIR override, ROOTPATH/models, cwd/models backward
|
|
195
|
+
* compat, ~/.flair/data/models default).
|
|
196
|
+
* flair#815: this used to be a hardcoded `<cwd>/models/<file>`, which broke
|
|
197
|
+
* (fail-open, silently) in any deployment where Harper's cwd wasn't the
|
|
198
|
+
* models location — including the recall harness's ephemeral Harpers.
|
|
199
|
+
* Exported for unit testing (see test/unit/rerank-provider.test.ts).
|
|
200
|
+
*/
|
|
201
|
+
export function resolveRerankModelPath(file) {
|
|
202
|
+
return join(resolveModelsDir(), file);
|
|
203
|
+
}
|
|
184
204
|
/**
|
|
185
205
|
* Decide whether `ensureInit()` needs to (re)run the engine init sequence —
|
|
186
206
|
* the flair#811 point-3 fix. Pure so the decision matrix is directly
|
|
@@ -234,9 +254,9 @@ async function ensureInit() {
|
|
|
234
254
|
const spec = MODELS[_modelKey];
|
|
235
255
|
_mode = spec.mode;
|
|
236
256
|
const { existsSync } = await import("node:fs");
|
|
237
|
-
const modelPath =
|
|
257
|
+
const modelPath = resolveRerankModelPath(spec.file);
|
|
238
258
|
if (!existsSync(modelPath)) {
|
|
239
|
-
throw new Error(`reranker GGUF not found:
|
|
259
|
+
throw new Error(`reranker GGUF not found: ${modelPath} (provision per docs/rerank-provisioning.md; set FLAIR_MODELS_DIR to override the models dir)`);
|
|
240
260
|
}
|
|
241
261
|
// Dynamic import — deferred to avoid Harper 5.x VM linker race (same reason
|
|
242
262
|
// embeddings-provider.ts defers harper-fabric-embeddings).
|
|
@@ -26,10 +26,28 @@
|
|
|
26
26
|
* RecordUsage.post()'s loop, parameterized so Memory.ts can drive it from a
|
|
27
27
|
* write body instead of a dedicated POST body. It NEVER reads the ledger for
|
|
28
28
|
* authority — it only writes contributions; `usedMemoryIds` must never enter
|
|
29
|
-
* an access/scope/attribution/dedup decision (flair#744 slice A invariant 3
|
|
29
|
+
* an access/scope/attribution/dedup decision (flair#744 slice A invariant 3;
|
|
30
|
+
* the flair#775 read-scope gate below is the REVERSE direction — the writer's
|
|
31
|
+
* scope vets the cited ids, the cited ids never widen anything).
|
|
32
|
+
*
|
|
33
|
+
* recordCitations() additionally validates each cited id against the
|
|
34
|
+
* WRITER's own read-scope before crediting (flair#775, a Kern+Sherlock
|
|
35
|
+
* binding condition on the locked design): citing a memory the writer cannot
|
|
36
|
+
* read is silently dropped, uniformly with a nonexistent id — never an
|
|
37
|
+
* error, never a distinguishable code path (an error, or any response/shape
|
|
38
|
+
* difference, would leak "that id exists but you can't see it").
|
|
39
|
+
* POST /RecordUsage is DELIBERATELY not scope-gated — its module doc
|
|
40
|
+
* establishes usage feedback as a cross-agent contract (agent B reports
|
|
41
|
+
* using agent A's memory regardless of A's visibility setting);
|
|
42
|
+
* citation-on-write is a separate write surface with a narrower threat
|
|
43
|
+
* model (the ids ride along on the WRITER's own memory-creation call, so
|
|
44
|
+
* crediting an unreadable id would let a writer both probe for and boost
|
|
45
|
+
* memories it cannot see). So the scope gate lives HERE, not retrofitted
|
|
46
|
+
* onto recordUsageContribution() — do not "unify" the two surfaces.
|
|
30
47
|
*/
|
|
31
48
|
import { databases } from "@harperfast/harper";
|
|
32
49
|
import { withDetachedTxn } from "./table-helpers.js";
|
|
50
|
+
import { resolveReadScope } from "./memory-read-scope.js";
|
|
33
51
|
/**
|
|
34
52
|
* Per-call cap on ids credited in one batch — shared by RecordUsage.post()'s
|
|
35
53
|
* validated `memoryIds` body (rejects a batch over the cap with a 400) and
|
|
@@ -106,6 +124,17 @@ export async function recordUsageContribution(ctx, agentId, memoryId, attributio
|
|
|
106
124
|
return; // deleted between the checks above and now — no-op
|
|
107
125
|
await withDetachedTxn(ctx, () => databases.flair.Memory.put({ ...fresh, usageCount: (fresh.usageCount ?? 0) + 1 }));
|
|
108
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Default record fetch for recordCitations()'s read-scope gate — the RAW
|
|
129
|
+
* Memory table (bypassing the Memory RESOURCE class's own read wrapper; the
|
|
130
|
+
* same trusted-internal-caller pattern recordUsageContribution() uses) so
|
|
131
|
+
* `scope.isAllowed` runs against the raw stored record. Never throws: a
|
|
132
|
+
* fetch failure reads as "not found", which the gate silently drops —
|
|
133
|
+
* identical to a nonexistent id.
|
|
134
|
+
*/
|
|
135
|
+
async function fetchMemoryForScopeCheck(ctx, memoryId) {
|
|
136
|
+
return withDetachedTxn(ctx, () => databases.flair.Memory.get(memoryId)).catch(() => null);
|
|
137
|
+
}
|
|
109
138
|
/**
|
|
110
139
|
* Batch citation helper — credits every id in `usedMemoryIds` through
|
|
111
140
|
* `recordFn` (the real `recordUsageContribution` by default), one contribution
|
|
@@ -129,16 +158,30 @@ export async function recordUsageContribution(ctx, agentId, memoryId, attributio
|
|
|
129
158
|
* validated request body, so an oversized list is trimmed rather than
|
|
130
159
|
* rejected (unlike RecordUsage.post()'s validated `memoryIds`, which
|
|
131
160
|
* 400s over the same cap).
|
|
161
|
+
* - Each cited id is validated against the WRITER's read scope before it
|
|
162
|
+
* is credited (flair#775 slice 1, K&S binding condition — see the module
|
|
163
|
+
* doc above): the scope is resolved ONCE per batch via resolveReadScope
|
|
164
|
+
* (the same single source every cross-agent Memory read path uses), then
|
|
165
|
+
* each id gets one raw fetch + one in-process `scope.isAllowed` check.
|
|
166
|
+
* Not-found and out-of-scope take the SAME silent-drop branch — there is
|
|
167
|
+
* no structurally distinguishable code path, error, or response
|
|
168
|
+
* difference between them that a caller could use to probe whether
|
|
169
|
+
* another agent's private id exists.
|
|
132
170
|
* - Each id is credited independently: one id throwing never stops the
|
|
133
171
|
* rest, and the failure is logged server-side, never surfaced to the
|
|
134
172
|
* caller (the write already committed by the time this runs).
|
|
135
173
|
*
|
|
136
|
-
* `agentId` passed to `recordFn`
|
|
137
|
-
* auth context, never anything derived from
|
|
138
|
-
* caller-supplied input (flair#744 slice A
|
|
139
|
-
* behalf of another identity).
|
|
174
|
+
* `agentId` passed to `recordFn` (and to the scope resolution) is ALWAYS
|
|
175
|
+
* `auth.agentId` — the resolved auth context, never anything derived from
|
|
176
|
+
* `usedMemoryIds` or any other caller-supplied input (flair#744 slice A
|
|
177
|
+
* invariant 4: no forging on behalf of another identity).
|
|
178
|
+
*
|
|
179
|
+
* `recordFn` / `fetchFn` / `scopeFn` are unit-test injection seams
|
|
180
|
+
* (test/unit/usage-recording.test.ts) — production callers pass none of
|
|
181
|
+
* them and always get the real recordUsageContribution / raw-table fetch /
|
|
182
|
+
* resolveReadScope.
|
|
140
183
|
*/
|
|
141
|
-
export async function recordCitations(ctx, auth, usedMemoryIds, now, recordFn = recordUsageContribution) {
|
|
184
|
+
export async function recordCitations(ctx, auth, usedMemoryIds, now, recordFn = recordUsageContribution, fetchFn = fetchMemoryForScopeCheck, scopeFn = resolveReadScope) {
|
|
142
185
|
if (auth.kind !== "agent")
|
|
143
186
|
return;
|
|
144
187
|
if (!Array.isArray(usedMemoryIds) ||
|
|
@@ -147,8 +190,31 @@ export async function recordCitations(ctx, auth, usedMemoryIds, now, recordFn =
|
|
|
147
190
|
return;
|
|
148
191
|
}
|
|
149
192
|
const ids = [...new Set(usedMemoryIds)].slice(0, MAX_USAGE_IDS_PER_CALL);
|
|
193
|
+
// Resolve the WRITER's read scope ONCE per batch. Fail CLOSED: if scope
|
|
194
|
+
// resolution itself fails, drop the whole batch rather than credit
|
|
195
|
+
// unvetted ids — citations are advisory signal, so losing a batch is
|
|
196
|
+
// strictly safer than crediting an id the writer may not be able to read.
|
|
197
|
+
let scope;
|
|
198
|
+
try {
|
|
199
|
+
scope = await scopeFn(auth.agentId);
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
console.error("recordCitations: read-scope resolution failed — batch dropped (no-op)", { err });
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
150
205
|
for (const id of ids) {
|
|
151
206
|
try {
|
|
207
|
+
// flair#775 slice 1 read-scope gate. The raw fetch + in-process
|
|
208
|
+
// predicate is deliberately ONE branch for both "doesn't exist" and
|
|
209
|
+
// "exists but out of the writer's read scope" — uniform silent drop
|
|
210
|
+
// (see the module doc). recordUsageContribution's own existence
|
|
211
|
+
// re-check makes this a second point-lookup of the same record;
|
|
212
|
+
// accepted — keeping the shared ledger core byte-identical for
|
|
213
|
+
// RecordUsage.post() is worth two point-lookups on a ≤20-id advisory
|
|
214
|
+
// batch.
|
|
215
|
+
const record = await fetchFn(ctx, id);
|
|
216
|
+
if (!record || !scope.isAllowed(record))
|
|
217
|
+
continue;
|
|
152
218
|
await recordFn(ctx, auth.agentId, id, undefined, now);
|
|
153
219
|
}
|
|
154
220
|
catch (err) {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* version.ts — shared runtime-version resolver.
|
|
3
|
+
*
|
|
4
|
+
* Reads the running @tpsdev-ai/flair version from the bundled package.json.
|
|
5
|
+
* `process.env.npm_package_version` is only populated inside `npm run`, so
|
|
6
|
+
* reading package.json relative to THIS running module is the only way to
|
|
7
|
+
* report the version of the code that's actually executing.
|
|
8
|
+
*
|
|
9
|
+
* Extracted from the duplicated copies in resources/Presence.ts and
|
|
10
|
+
* resources/AdminInstance.ts (flair#831). Those modules still carry their
|
|
11
|
+
* own copies for now — migrating them is a follow-up.
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
14
|
+
import { join, dirname } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
export function resolveVersion() {
|
|
17
|
+
try {
|
|
18
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const candidates = [
|
|
20
|
+
join(here, "..", "..", "package.json"),
|
|
21
|
+
join(here, "..", "package.json"),
|
|
22
|
+
];
|
|
23
|
+
for (const p of candidates) {
|
|
24
|
+
if (existsSync(p)) {
|
|
25
|
+
const pkg = JSON.parse(readFileSync(p, "utf-8"));
|
|
26
|
+
if (pkg.version)
|
|
27
|
+
return pkg.version;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch { /* fall through */ }
|
|
32
|
+
return process.env.npm_package_version ?? "dev";
|
|
33
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[0.001, "o", " Flair \u00b7 same identity, every orchestrator\r\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n"]
|
|
4
4
|
[1.3, "o", "$ # One agent identity. One memory store. Three orchestrators.\r\n"]
|
|
5
5
|
[1.2, "o", "$ flair init --client all --agent flint\r\n"]
|
|
6
|
-
[0.5, "o", " \u001b[32m\u2713\u001b[0m Claude Code wired (~/.claude
|
|
6
|
+
[0.5, "o", " \u001b[32m\u2713\u001b[0m Claude Code wired (~/.claude.json)\r\n"]
|
|
7
7
|
[0.18, "o", " \u001b[32m\u2713\u001b[0m Codex CLI wired (~/.codex/config.toml)\r\n"]
|
|
8
8
|
[0.18, "o", " \u001b[32m\u2713\u001b[0m Gemini CLI wired (~/.gemini/settings.json)\r\n\r\n"]
|
|
9
9
|
[1.0, "o", " Each MCP client now connects to: http://127.0.0.1:9926\r\n"]
|
package/docs/bridges.md
CHANGED
|
@@ -101,7 +101,7 @@ A few things worth knowing:
|
|
|
101
101
|
- **`--agent` is required** unless your descriptor maps an `agentId` column. If you forget, `flair bridge import` errors with a one-line operator-pointer hint plus a structured `BridgeRuntimeError` JSON on stderr.
|
|
102
102
|
- **`--dry-run` is your friend.** Validates the descriptor, parses every record, applies the mapping, but skips the PUT. Use it to confirm the count and check a few records before committing.
|
|
103
103
|
- **Output is throttled** to one progress line every 2 seconds (or every 25 records, whichever comes first), so big imports don't flood your terminal.
|
|
104
|
-
- **Errors are structured.** Every error includes `bridge`, `op`, `path`, `record`, `field`, `expected`, `got`, `hint` (per [
|
|
104
|
+
- **Errors are structured.** Every error includes `bridge`, `op`, `path`, `record`, `field`, `expected`, `got`, `hint` (per [Error format](#error-format) below). The `hint` is the part you act on; the rest is for an LLM to self-correct without operator help.
|
|
105
105
|
|
|
106
106
|
## Shape A — Declarative YAML
|
|
107
107
|
|
|
@@ -286,6 +286,6 @@ If you want an agent to write a bridge for you, here's the one-shot prompt:
|
|
|
286
286
|
|
|
287
287
|
That's the bar. If an agent can't ship a working bridge from this doc plus the scaffold, the doc is the bug.
|
|
288
288
|
|
|
289
|
-
##
|
|
289
|
+
## Authority
|
|
290
290
|
|
|
291
|
-
|
|
291
|
+
This document is the authoritative contract for the bridge plugin system: the record schema, both plugin shapes, discovery, distribution, trust, round-trip testing, and the error format are all specified above. There is no separate design spec — the original planning document was retired once the system shipped, and its design-rationale and milestone-scoping sections are preserved in git history rather than maintained here.
|
package/docs/mcp-clients.md
CHANGED
|
@@ -38,9 +38,11 @@ Flair runs as a local server at `http://127.0.0.1:19926` by default. The MCP ser
|
|
|
38
38
|
|
|
39
39
|
Pick whichever you use. The MCP server is the same package; only the config syntax differs.
|
|
40
40
|
|
|
41
|
+
> **Pin the version.** The snippets below use the bare package name for readability. `flair init` wires clients to a **pinned** spec (`@tpsdev-ai/flair-mcp@<version>`) on purpose: an unpinned reference re-resolves to whatever is currently published on every agent session, so any future publish reaches your machine silently. If you wire by hand, append the version you intend to run — `@tpsdev-ai/flair-mcp@0.28.0` — and bump it deliberately. `flair init` is the easier path and does this for you.
|
|
42
|
+
|
|
41
43
|
### Claude Code
|
|
42
44
|
|
|
43
|
-
The canonical approach is the `claude mcp add` CLI (writes to `~/.claude
|
|
45
|
+
The canonical approach is the `claude mcp add` CLI (writes to `~/.claude.json`):
|
|
44
46
|
|
|
45
47
|
```bash
|
|
46
48
|
claude mcp add flair --scope user \
|
package/docs/n8n.md
CHANGED
|
@@ -93,7 +93,7 @@ If any of those don't hold, use Flair's CLI / SDK clients (which support per-age
|
|
|
93
93
|
|
|
94
94
|
## Get By Tag — coming soon
|
|
95
95
|
|
|
96
|
-
The Flair Search node currently exposes Semantic Search and Get By Subject. **Get By Tag** is deferred until `flair-client.memory.list` exposes a `tags` filter
|
|
96
|
+
The Flair Search node currently exposes Semantic Search and Get By Subject. **Get By Tag** is deferred until `flair-client.memory.list` exposes a `tags` filter. Workaround for now: use Semantic Search and let the model filter results by tags in the response.
|
|
97
97
|
|
|
98
98
|
## Worked examples
|
|
99
99
|
|
|
@@ -117,6 +117,5 @@ Same Flair instance, same memories, different surfaces.
|
|
|
117
117
|
|
|
118
118
|
## See also
|
|
119
119
|
|
|
120
|
-
- [Spec — `@tpsdev-ai/n8n-nodes-flair`](https://github.com/tpsdev-ai/flair/blob/main/specs/N8N-NODE-q3qf.md) — implementation plan, design decisions, anti-patterns
|
|
121
120
|
- [Bridges](./bridges.md) — how Flair memories flow between hosts and instances
|
|
122
121
|
- [Federation](./federation.md) — hub-and-spoke replication
|
package/docs/notes/rem-ux.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# REM UX — trigger model, attach semantics, review loop
|
|
2
2
|
|
|
3
|
-
> Design note accompanying REM slice 2 (#707). Describes the intended user experience of in-process distillation so the CLI/docs surfaces stay coherent as the feature grows.
|
|
3
|
+
> Design note accompanying REM slice 2 (#707). Describes the intended user experience of in-process distillation so the CLI/docs surfaces stay coherent as the feature grows. See [`docs/rem.md`](../rem.md) for the REM configuration and command reference.
|
|
4
4
|
|
|
5
5
|
## Triggers — three, nothing implicit
|
|
6
6
|
|
package/docs/rem.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
REM (Reflect · Extract · Merge) is Flair's memory-curation cycle: it reads an agent's recent memories, distills them into candidate insights, and stages those candidates for explicit human/agent review — nothing is ever auto-promoted. `flair rem rapid` runs it on demand; `flair rem nightly enable` runs it on a schedule. See [`docs/notes/rem-ux.md`](notes/rem-ux.md) for the full trigger model, locality guarantees, and the review-loop UX this page's commands feed into.
|
|
4
4
|
|
|
5
|
+
> **⚠️ Prerequisite: a configured generative backend.** All REM commands (`rapid`, `nightly`, `candidates`, `promote`, `reject`) require Harper's `models.generate()` to be wired — without it, REM calls fail with `Reflection error: No generative backend configured`. Set up a backend first (see [Configuration](#configuration) below) before running any REM command. The fastest path is Ollama with a non-thinking model, which needs zero credentials and keeps all traffic local.
|
|
6
|
+
|
|
5
7
|
## Configuration
|
|
6
8
|
|
|
7
9
|
Distillation runs **server-side**, via Harper's model-access API (`models.generate()`). Flair ships zero provider code — which backend answers a REM call is entirely a Harper `models:` configuration decision.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tpsdev-ai/flair",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"packageManager": "bun@1.3.10",
|
|
5
5
|
"description": "Identity, memory, and soul for AI agents. Cryptographic identity (Ed25519), semantic memory with local embeddings, and persistent personality — all in a single process.",
|
|
6
6
|
"type": "module",
|
package/schemas/memory.graphql
CHANGED
|
@@ -44,13 +44,17 @@ type Memory @table(database: "flair") {
|
|
|
44
44
|
usageCount: Int # flair#683: verified-USE signal, distinct from retrievalCount (a
|
|
45
45
|
# search HIT). Additive/nullable — absent reads as 0, existing rows
|
|
46
46
|
# unaffected. NEVER auto-incremented on search/retrieval; the ONLY
|
|
47
|
-
# writer is
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
#
|
|
47
|
+
# writer is resources/usage-recording.ts's recordUsageContribution()
|
|
48
|
+
# — the shared ledger core behind BOTH usage surfaces: POST
|
|
49
|
+
# /RecordUsage (resources/RecordUsage.ts) and citation-on-write
|
|
50
|
+
# (resources/Memory.ts post()/put() → recordCitations(), which also
|
|
51
|
+
# validates each cited id against the writer's read scope,
|
|
52
|
+
# flair#775). It does a targeted get-then-put increment on this
|
|
53
|
+
# field alone (never through Memory.put() — see RecordUsage.ts's
|
|
54
|
+
# module doc for why: usage feedback writes to ANOTHER agent's
|
|
55
|
+
# memory, and Memory's ownership check would 403 every legit
|
|
56
|
+
# call). Drives usageBoost in resources/scoring.ts, which replaces
|
|
57
|
+
# retrievalBoost in compositeScore (retrievalCount was the
|
|
54
58
|
# contaminated "retrieval counted as usage" signal root-caused in
|
|
55
59
|
# flair#623).
|
|
56
60
|
promotionStatus: String # null | "pending" | "approved" | "rejected"
|
|
@@ -155,9 +159,12 @@ type MemoryGrant @table(database: "flair") @export {
|
|
|
155
159
|
# contributed to this memory's usageCount" is a single point-lookup, not a
|
|
156
160
|
# query. This is what makes the anti-gaming dedup rule ("each (agent, memory)
|
|
157
161
|
# pair contributes at most 1 to usageCount" — Sherlock) enforceable: the
|
|
158
|
-
#
|
|
159
|
-
#
|
|
160
|
-
#
|
|
162
|
+
# shared ledger core (resources/usage-recording.ts's recordUsageContribution(),
|
|
163
|
+
# reached via POST /RecordUsage and via citation-on-write's recordCitations())
|
|
164
|
+
# only bumps Memory.usageCount when a NEW row is created here; a repeat call
|
|
165
|
+
# from the same agent for the same memory is a no-op against a row that
|
|
166
|
+
# already exists. resources/MemoryUsage.ts guards this table's own HTTP
|
|
167
|
+
# surface (it is not the writer).
|
|
161
168
|
#
|
|
162
169
|
# `attribution` is OPAQUE — never parsed, never fed to an LLM, never rendered
|
|
163
170
|
# — a free-text "what used this" hint the caller supplies, sanitized
|
|
@@ -172,7 +179,7 @@ type MemoryUsage @table(database: "flair") {
|
|
|
172
179
|
}
|
|
173
180
|
|
|
174
181
|
# MemoryCandidate — staged distillations from the FLAIR-NIGHTLY-REM cycle.
|
|
175
|
-
# Per
|
|
182
|
+
# Per docs/rem.md. Slice 1 of ops-2qq adds the schema +
|
|
176
183
|
# `flair rem candidates` listing command. Future slices wire the nightly
|
|
177
184
|
# cycle that populates this table and the promote/reject endpoints.
|
|
178
185
|
#
|