@tpsdev-ai/flair 0.53.0 → 0.54.2
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 +4 -1
- package/dist/build-info.json +3 -3
- package/dist/cli.js +1791 -15648
- package/dist/commands/agent.js +453 -0
- package/dist/commands/attention.js +121 -0
- package/dist/commands/backup.js +115 -0
- package/dist/commands/bootstrap.js +91 -0
- package/dist/commands/bridge.js +608 -0
- package/dist/commands/deploy.js +180 -0
- package/dist/commands/doctor.js +1665 -0
- package/dist/commands/export.js +110 -0
- package/dist/commands/federation.js +1575 -0
- package/dist/commands/fleet.js +73 -0
- package/dist/commands/grant.js +109 -0
- package/dist/commands/hook.js +193 -0
- package/dist/commands/idp.js +193 -0
- package/dist/commands/import.js +134 -0
- package/dist/commands/init.js +1203 -0
- package/dist/commands/inspect.js +45 -0
- package/dist/commands/keys.js +187 -0
- package/dist/commands/mcp.js +707 -0
- package/dist/commands/memory.js +501 -0
- package/dist/commands/migrate-harness-memory.js +270 -0
- package/dist/commands/orgevent.js +138 -0
- package/dist/commands/presence.js +76 -0
- package/dist/commands/principal.js +338 -0
- package/dist/commands/quality.js +1164 -0
- package/dist/commands/reembed.js +296 -0
- package/dist/commands/relationship.js +76 -0
- package/dist/commands/rem.js +1048 -0
- package/dist/commands/restore.js +130 -0
- package/dist/commands/search.js +244 -0
- package/dist/commands/service.js +315 -0
- package/dist/commands/session.js +184 -0
- package/dist/commands/soul.js +155 -0
- package/dist/commands/status.js +931 -0
- package/dist/commands/test.js +93 -0
- package/dist/commands/uninstall.js +143 -0
- package/dist/commands/upgrade.js +1628 -0
- package/dist/commands/workspace.js +114 -0
- package/dist/deploy.js +24 -0
- package/dist/engine-version.js +12 -4
- package/dist/fabric-npm-install.js +87 -0
- package/dist/fabric-upgrade.js +30 -15
- package/dist/federation-verify.js +498 -0
- package/dist/fleet-verify.js +144 -21
- package/dist/install/clients.js +167 -0
- package/dist/lib/auth-resolve.js +76 -1
- package/dist/lib/daemon-liveness.js +131 -2
- package/dist/lib/doctor-config-path.js +61 -0
- package/dist/lib/doctor-federation-driver.js +189 -0
- package/dist/lib/doctor-run.js +40 -0
- package/dist/lib/entity-vocab-cli.js +3 -3
- package/dist/lib/federation-pair-identity.js +47 -0
- package/dist/lib/launchd-repair.js +5 -4
- package/dist/lib/npm-registry.js +578 -0
- package/dist/lib/ops-api-bind.js +115 -0
- package/dist/lib/owned-pins.js +219 -0
- package/dist/lib/uninstall-purge.js +218 -0
- package/dist/rem/restore.js +8 -10
- package/dist/resources/AgentReadPosition.js +74 -0
- package/dist/resources/Federation.js +8 -2
- package/dist/resources/Memory.js +4 -3
- package/dist/resources/MemoryBootstrap.js +41 -25
- package/dist/resources/MemoryCandidate.js +5 -6
- package/dist/resources/OrgEventCatchup.js +126 -47
- package/dist/resources/agent-read-position-lib.js +83 -0
- package/dist/resources/agent-read-position.js +120 -0
- package/dist/resources/embeddings-boot.js +32 -0
- package/dist/resources/federation-peer-liveness.js +73 -0
- package/dist/resources/health.js +68 -19
- package/dist/resources/mcp-tools.js +48 -279
- package/dist/resources/memory-visibility.js +3 -3
- package/dist/resources/migration-boot.js +59 -18
- package/dist/resources/migrations/embedding-stamp.js +20 -1
- package/dist/resources/migrations/recheck.js +43 -0
- package/dist/resources/migrations/runner.js +6 -1
- package/dist/resources/migrations/stamp-outstanding.js +171 -0
- package/dist/resources/migrations/visibility-backfill.js +2 -2
- package/dist/resources/org-event-catchup-lib.js +47 -0
- package/dist/resources/record-owner-guard.js +1 -0
- package/dist/resources/tool-descriptors/index.js +669 -0
- package/dist/stamp-migration-verify.js +163 -0
- package/dist/stamp-outstanding.js +144 -0
- package/dist/version-check.js +29 -8
- package/docs/api-reference.md +4 -2
- package/docs/deploying-on-fabric.md +11 -10
- package/docs/deployment.md +3 -1
- package/docs/federation.md +19 -0
- package/docs/hosted-on-fabric.md +3 -3
- package/docs/quickstart.md +2 -1
- package/docs/releasing.md +20 -6
- package/docs/spoke-bringup.md +10 -5
- package/docs/standalone-local.md +3 -1
- package/docs/upgrade.md +25 -6
- package/package.json +4 -4
- package/schemas/agent.graphql +15 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stamp-migration-verify.ts — post-deploy / `upgrade --target` check that
|
|
3
|
+
* embedding-stamp actually converged (flair#1073).
|
|
4
|
+
*
|
|
5
|
+
* Route verify (`verifyDeployServing`) proves the component is serving.
|
|
6
|
+
* That is not enough: a Fabric upgrade can serve new writes (prefixed
|
|
7
|
+
* stamps) while pre-flip rows stay stale, and `flair status` used to
|
|
8
|
+
* report only a generic mixed-models warning. This polls authenticated
|
|
9
|
+
* /HealthDetail until the corpus is a single current space (and
|
|
10
|
+
* embedding-stamp is not halted/failed), the same way route verify polls
|
|
11
|
+
* until /Memory is non-404.
|
|
12
|
+
*/
|
|
13
|
+
import { EMBEDDING_STAMP_ID, stampMigrationConverged, } from "./stamp-outstanding.js";
|
|
14
|
+
export const DEFAULT_STAMP_VERIFY_TIMEOUT_MS = 600_000;
|
|
15
|
+
export const STAMP_VERIFY_POLL_INTERVAL_MS = 2_000;
|
|
16
|
+
function basicAuthHeader(user, password) {
|
|
17
|
+
return "Basic " + Buffer.from(`${user}:${password}`, "utf8").toString("base64");
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Parse /HealthDetail into a verify snapshot. `memories: null` (HealthDetail
|
|
21
|
+
* 200 after the Memory walk threw) and a missing `modelCounts` field stay
|
|
22
|
+
* `undefined` — that is unread, not an empty corpus. An explicit
|
|
23
|
+
* `modelCounts: {}` is the valid empty-store case (Bugbot Medium on #1606).
|
|
24
|
+
*/
|
|
25
|
+
export function snapshotFromHealthDetail(body) {
|
|
26
|
+
const rec = body && typeof body === "object" ? body : {};
|
|
27
|
+
const memories = rec.memories && typeof rec.memories === "object" ? rec.memories : null;
|
|
28
|
+
const migrationsBlock = rec.migrations && typeof rec.migrations === "object" ? rec.migrations : {};
|
|
29
|
+
const modelCounts = memories && memories.modelCounts && typeof memories.modelCounts === "object"
|
|
30
|
+
? memories.modelCounts
|
|
31
|
+
: undefined;
|
|
32
|
+
const rawList = Array.isArray(migrationsBlock.migrations) ? migrationsBlock.migrations : undefined;
|
|
33
|
+
const migrations = rawList
|
|
34
|
+
?.filter((m) => !!m && typeof m === "object" && typeof m.id === "string")
|
|
35
|
+
.map((m) => ({
|
|
36
|
+
id: m.id,
|
|
37
|
+
state: String(m.state ?? ""),
|
|
38
|
+
rowsDone: typeof m.rowsDone === "number" ? m.rowsDone : undefined,
|
|
39
|
+
rowsRemaining: typeof m.rowsRemaining === "number" ? m.rowsRemaining : undefined,
|
|
40
|
+
reason: typeof m.reason === "string" ? m.reason : undefined,
|
|
41
|
+
}));
|
|
42
|
+
const warnings = Array.isArray(rec.warnings)
|
|
43
|
+
? rec.warnings
|
|
44
|
+
.map((w) => (w && typeof w === "object" && typeof w.message === "string" ? w.message : null))
|
|
45
|
+
.filter((m) => m !== null)
|
|
46
|
+
: undefined;
|
|
47
|
+
return {
|
|
48
|
+
modelCounts,
|
|
49
|
+
cyclePhase: typeof migrationsBlock.cyclePhase === "string" ? migrationsBlock.cyclePhase : undefined,
|
|
50
|
+
lastCycleError: typeof migrationsBlock.lastCycleError === "string" ? migrationsBlock.lastCycleError : migrationsBlock.lastCycleError === null ? null : undefined,
|
|
51
|
+
migrations,
|
|
52
|
+
warnings,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Read the current-space id from the HealthDetail warning or from the
|
|
57
|
+
* dominant +searchprefix / gguf: stamp. Fallback: the most common real
|
|
58
|
+
* stamp — verify then treats any other space as stale.
|
|
59
|
+
*/
|
|
60
|
+
export function inferCurrentModelId(snapshot) {
|
|
61
|
+
const counts = snapshot.modelCounts ?? {};
|
|
62
|
+
let best = null;
|
|
63
|
+
let prefixed = null;
|
|
64
|
+
for (const [stamp, n] of Object.entries(counts)) {
|
|
65
|
+
if (stamp === "hash-512d" || typeof n !== "number" || n <= 0)
|
|
66
|
+
continue;
|
|
67
|
+
if (!best || n > best.n)
|
|
68
|
+
best = { stamp, n };
|
|
69
|
+
if (stamp.includes("+searchprefix") && (!prefixed || n > prefixed.n))
|
|
70
|
+
prefixed = { stamp, n };
|
|
71
|
+
}
|
|
72
|
+
return prefixed?.stamp ?? best?.stamp ?? "nomic-embed-text-v1.5-Q4_K_M+searchprefix";
|
|
73
|
+
}
|
|
74
|
+
export function evaluateStampSnapshot(snapshot) {
|
|
75
|
+
// Unread corpus (HealthDetail omitted modelCounts, including memories: null)
|
|
76
|
+
// is not an empty store. Reject before stampMigrationConverged can treat
|
|
77
|
+
// `undefined` as `{}` and report success (Bugbot Medium on #1606).
|
|
78
|
+
if (snapshot.modelCounts === undefined) {
|
|
79
|
+
return {
|
|
80
|
+
converged: false,
|
|
81
|
+
detail: "HealthDetail did not include memories.modelCounts",
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const outstandingWarning = snapshot.warnings?.find((w) => w.includes(`migration '${EMBEDDING_STAMP_ID}' is outstanding`) || w.includes("duplicate detection is inactive"));
|
|
85
|
+
if (outstandingWarning) {
|
|
86
|
+
return { converged: false, detail: outstandingWarning };
|
|
87
|
+
}
|
|
88
|
+
const counts = snapshot.modelCounts;
|
|
89
|
+
const realStamps = Object.entries(counts).filter(([k, n]) => k !== "hash-512d" && typeof n === "number" && n > 0);
|
|
90
|
+
// Production getModelId() stamps +searchprefix. A corpus that is entirely
|
|
91
|
+
// on the pre-flip bare id is the #1073 failure mode — do not treat a
|
|
92
|
+
// single-space bare corpus as converged just because inferCurrentModelId
|
|
93
|
+
// would pick that majority stamp as "current."
|
|
94
|
+
if (realStamps.length > 0 && realStamps.every(([k]) => !k.includes("+searchprefix"))) {
|
|
95
|
+
const list = realStamps.map(([k, n]) => `${k}:${n}`).join(", ");
|
|
96
|
+
return {
|
|
97
|
+
converged: false,
|
|
98
|
+
detail: `migration '${EMBEDDING_STAMP_ID}' is outstanding — corpus has no +searchprefix stamp yet (${list})`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const currentModelId = inferCurrentModelId(snapshot);
|
|
102
|
+
const migration = snapshot.migrations?.find((m) => m.id === EMBEDDING_STAMP_ID);
|
|
103
|
+
return stampMigrationConverged({
|
|
104
|
+
modelCounts: counts,
|
|
105
|
+
currentModelId,
|
|
106
|
+
migration,
|
|
107
|
+
cyclePhase: snapshot.cyclePhase,
|
|
108
|
+
lastCycleError: snapshot.lastCycleError,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
async function readHealthDetail(baseUrl, auth, fetchImpl) {
|
|
112
|
+
const url = `${baseUrl.replace(/\/+$/, "")}/HealthDetail`;
|
|
113
|
+
try {
|
|
114
|
+
const res = await fetchImpl(url, {
|
|
115
|
+
headers: { Authorization: auth, Accept: "application/json" },
|
|
116
|
+
signal: AbortSignal.timeout(15_000),
|
|
117
|
+
});
|
|
118
|
+
if (!res.ok) {
|
|
119
|
+
return { ok: false, status: res.status, error: `GET /HealthDetail returned ${res.status}` };
|
|
120
|
+
}
|
|
121
|
+
const body = await res.json();
|
|
122
|
+
return { ok: true, status: res.status, snapshot: snapshotFromHealthDetail(body) };
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
return { ok: false, status: 0, error: err?.message ?? String(err) };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Poll /HealthDetail until embedding-stamp has converged, or throw.
|
|
130
|
+
* Skipped by the caller when verify is disabled (`--no-verify`).
|
|
131
|
+
*/
|
|
132
|
+
export async function verifyStampMigrationConverged(o) {
|
|
133
|
+
const { baseUrl, fabricUser, fabricPassword, timeoutMs = DEFAULT_STAMP_VERIFY_TIMEOUT_MS, pollIntervalMs = STAMP_VERIFY_POLL_INTERVAL_MS, fetchImpl = fetch, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), onProgress, } = o;
|
|
134
|
+
const auth = basicAuthHeader(fabricUser, fabricPassword);
|
|
135
|
+
onProgress?.(`verifying ${EMBEDDING_STAMP_ID} converged on ${baseUrl}...`);
|
|
136
|
+
const deadline = Date.now() + timeoutMs;
|
|
137
|
+
let last = await readHealthDetail(baseUrl, auth, fetchImpl);
|
|
138
|
+
for (;;) {
|
|
139
|
+
if (last.ok && last.snapshot) {
|
|
140
|
+
const verdict = evaluateStampSnapshot(last.snapshot);
|
|
141
|
+
if (verdict.converged) {
|
|
142
|
+
onProgress?.(`embedding-stamp converged`);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
onProgress?.(verdict.detail);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
onProgress?.(last.error ?? "HealthDetail not readable yet");
|
|
149
|
+
}
|
|
150
|
+
if (Date.now() >= deadline)
|
|
151
|
+
break;
|
|
152
|
+
await sleep(Math.min(pollIntervalMs, Math.max(0, deadline - Date.now())));
|
|
153
|
+
last = await readHealthDetail(baseUrl, auth, fetchImpl);
|
|
154
|
+
}
|
|
155
|
+
if (!last.ok) {
|
|
156
|
+
throw new Error(`deploy verification: embedding-stamp did not converge within ${timeoutMs}ms — ` +
|
|
157
|
+
`could not read /HealthDetail (${last.error ?? `HTTP ${last.status}`}). ` +
|
|
158
|
+
`The component may still be restarting; re-run without --no-verify, or ` +
|
|
159
|
+
`\`flair status --json\` and look for migration '${EMBEDDING_STAMP_ID}'.`);
|
|
160
|
+
}
|
|
161
|
+
const verdict = last.snapshot ? evaluateStampSnapshot(last.snapshot) : { detail: "no HealthDetail snapshot" };
|
|
162
|
+
throw new Error(`deploy verification: embedding-stamp did not converge within ${timeoutMs}ms — ${verdict.detail}`);
|
|
163
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stamp-outstanding.ts — CLI-side copy of the corpus-derived
|
|
3
|
+
* "is embedding-stamp still pending?" helper (flair#1073).
|
|
4
|
+
*
|
|
5
|
+
* INLINED, not imported from resources/: cross-boundary imports from src/
|
|
6
|
+
* into resources/ do not survive npm packaging. tsconfig.cli.json compiles
|
|
7
|
+
* with `rootDir: "src"`, so dist/cli.js has no resources/ module it can
|
|
8
|
+
* resolve at the same relative path. Same reason as entity-vocab-cli.ts
|
|
9
|
+
* and the getModelId() literals in src/cli.ts.
|
|
10
|
+
*
|
|
11
|
+
* Canonical module: resources/migrations/stamp-outstanding.ts (Harper
|
|
12
|
+
* HealthDetail / embedding-stamp). The two files MUST stay in sync —
|
|
13
|
+
* test/unit/stamp-outstanding-cli-parity.test.ts pins export names and a
|
|
14
|
+
* known-answer table. Drift fails CI rather than shipping.
|
|
15
|
+
*/
|
|
16
|
+
export const EMBEDDING_STAMP_ID = "embedding-stamp";
|
|
17
|
+
const NON_SPACE_STAMPS = new Set(["hash-512d"]);
|
|
18
|
+
const DEFAULT_ENGINE = "gguf";
|
|
19
|
+
export function normalizeStampForOutstanding(stamp) {
|
|
20
|
+
if (stamp == null)
|
|
21
|
+
return null;
|
|
22
|
+
const s = String(stamp).trim();
|
|
23
|
+
if (s === "" || NON_SPACE_STAMPS.has(s))
|
|
24
|
+
return null;
|
|
25
|
+
return s.includes(":") ? s : `${DEFAULT_ENGINE}:${s}`;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Current-space id for a client that only has `modelCounts` (no live
|
|
29
|
+
* `getModelId()`). Prefers an existing `+searchprefix` stamp so a healthy
|
|
30
|
+
* bare+`gguf:` pair of the SAME space does not invent a foreign current.
|
|
31
|
+
* A uniformly pre-flip corpus has no suffix — append `+searchprefix` so
|
|
32
|
+
* those rows read as stale.
|
|
33
|
+
*/
|
|
34
|
+
export function resolveCurrentModelId(modelCounts, explicit) {
|
|
35
|
+
const trimmed = explicit?.trim();
|
|
36
|
+
if (trimmed)
|
|
37
|
+
return trimmed;
|
|
38
|
+
let prefixed;
|
|
39
|
+
let anyReal;
|
|
40
|
+
for (const [stamp, n] of Object.entries(modelCounts)) {
|
|
41
|
+
if (NON_SPACE_STAMPS.has(stamp) || typeof n !== "number" || n <= 0)
|
|
42
|
+
continue;
|
|
43
|
+
anyReal ??= stamp;
|
|
44
|
+
if (stamp.includes("+searchprefix")) {
|
|
45
|
+
if (!prefixed || stamp.includes(":"))
|
|
46
|
+
prefixed = stamp;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (prefixed)
|
|
50
|
+
return prefixed;
|
|
51
|
+
if (anyReal) {
|
|
52
|
+
const bare = anyReal.includes(":") ? anyReal.slice(anyReal.indexOf(":") + 1) : anyReal;
|
|
53
|
+
return `${DEFAULT_ENGINE}:${bare}+searchprefix`;
|
|
54
|
+
}
|
|
55
|
+
return `${DEFAULT_ENGINE}:nomic-embed-text-v1.5-Q4_K_M+searchprefix`;
|
|
56
|
+
}
|
|
57
|
+
export function countStampSpaces(modelCounts, currentModelId) {
|
|
58
|
+
const currentSpace = normalizeStampForOutstanding(currentModelId);
|
|
59
|
+
let currentCount = 0;
|
|
60
|
+
let staleCount = 0;
|
|
61
|
+
const staleStamps = [];
|
|
62
|
+
for (const [raw, n] of Object.entries(modelCounts)) {
|
|
63
|
+
if (typeof n !== "number" || n <= 0)
|
|
64
|
+
continue;
|
|
65
|
+
const space = normalizeStampForOutstanding(raw);
|
|
66
|
+
if (space === null)
|
|
67
|
+
continue;
|
|
68
|
+
if (currentSpace !== null && space === currentSpace) {
|
|
69
|
+
currentCount += n;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
staleCount += n;
|
|
73
|
+
staleStamps.push(`${raw}:${n}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { currentCount, staleCount, staleStamps, currentSpace };
|
|
77
|
+
}
|
|
78
|
+
function runnerAnnotation(input) {
|
|
79
|
+
const mig = input.migration;
|
|
80
|
+
const phase = input.cyclePhase;
|
|
81
|
+
if (input.lastCycleError) {
|
|
82
|
+
return ` last boot cycle failed (${input.lastCycleError})`;
|
|
83
|
+
}
|
|
84
|
+
if (phase === "idle") {
|
|
85
|
+
return " migration boot cycle never fired on this instance — no migration will run until this is resolved";
|
|
86
|
+
}
|
|
87
|
+
if (mig?.state === "running" || mig?.state === "checking" || mig?.state === "preflight" || mig?.state === "snapshotting" || mig?.state === "completing") {
|
|
88
|
+
const rem = typeof mig.rowsRemaining === "number" ? `, ${mig.rowsRemaining} remaining` : "";
|
|
89
|
+
return ` in progress (${mig.state}${rem})`;
|
|
90
|
+
}
|
|
91
|
+
if (mig?.state === "halted" || mig?.state === "failed") {
|
|
92
|
+
return ` ${mig.state}${mig.reason ? `: ${mig.reason}` : ""} — see \`flair doctor\``;
|
|
93
|
+
}
|
|
94
|
+
if (mig?.state === "completed" || phase === "done") {
|
|
95
|
+
return " the last cycle marked it complete without converging; it will retry automatically";
|
|
96
|
+
}
|
|
97
|
+
if (input.audience === "client") {
|
|
98
|
+
return " the Harper process applies it on its next migration cycle — this CLI does not";
|
|
99
|
+
}
|
|
100
|
+
return " it will apply automatically on this process's next migration cycle";
|
|
101
|
+
}
|
|
102
|
+
export function describeStampOutstanding(input) {
|
|
103
|
+
const { currentCount, staleCount, staleStamps } = countStampSpaces(input.modelCounts, input.currentModelId);
|
|
104
|
+
if (staleCount === 0) {
|
|
105
|
+
return { outstanding: false, staleCount: 0, currentCount };
|
|
106
|
+
}
|
|
107
|
+
const list = staleStamps.join(", ");
|
|
108
|
+
const warning = `migration '${EMBEDDING_STAMP_ID}' is outstanding ` +
|
|
109
|
+
`(${staleCount} row${staleCount === 1 ? "" : "s"} still on ${list}` +
|
|
110
|
+
`${currentCount > 0 ? `, ${currentCount} already current` : ""}) — ` +
|
|
111
|
+
`cross-model search is unreliable and duplicate detection is inactive ` +
|
|
112
|
+
`until the re-embed completes;` +
|
|
113
|
+
runnerAnnotation(input);
|
|
114
|
+
return {
|
|
115
|
+
outstanding: true,
|
|
116
|
+
migrationId: EMBEDDING_STAMP_ID,
|
|
117
|
+
staleCount,
|
|
118
|
+
currentCount,
|
|
119
|
+
staleStamps,
|
|
120
|
+
warning,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
export function stampMigrationConverged(input) {
|
|
124
|
+
const described = describeStampOutstanding(input);
|
|
125
|
+
if (described.outstanding) {
|
|
126
|
+
return { converged: false, detail: described.warning };
|
|
127
|
+
}
|
|
128
|
+
const mig = input.migration;
|
|
129
|
+
if (mig?.state === "halted" || mig?.state === "failed") {
|
|
130
|
+
return {
|
|
131
|
+
converged: false,
|
|
132
|
+
detail: `migration '${EMBEDDING_STAMP_ID}' ${mig.state}${mig.reason ? `: ${mig.reason}` : ""}`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (mig &&
|
|
136
|
+
(mig.state === "running" ||
|
|
137
|
+
mig.state === "checking" ||
|
|
138
|
+
mig.state === "preflight" ||
|
|
139
|
+
mig.state === "snapshotting" ||
|
|
140
|
+
mig.state === "completing")) {
|
|
141
|
+
return { converged: false, detail: `migration '${EMBEDDING_STAMP_ID}' still ${mig.state}` };
|
|
142
|
+
}
|
|
143
|
+
return { converged: true, detail: "embedding-stamp converged (corpus is a single current space)" };
|
|
144
|
+
}
|
package/dist/version-check.js
CHANGED
|
@@ -32,6 +32,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
32
32
|
import { homedir } from "node:os";
|
|
33
33
|
import { dirname, join } from "node:path";
|
|
34
34
|
import { parseSemverCore } from "./fabric-upgrade.js";
|
|
35
|
+
import { fetchLatestVersion } from "./lib/npm-registry.js";
|
|
35
36
|
export const FLAIR_PKG_NAME = "@tpsdev-ai/flair";
|
|
36
37
|
export const DEFAULT_CACHE_PATH = join(homedir(), ".flair", ".version-check-cache.json");
|
|
37
38
|
/** How long a cached "latest" answer is trusted before we re-hit the registry. */
|
|
@@ -65,19 +66,39 @@ function writeCacheFile(path, entry) {
|
|
|
65
66
|
}
|
|
66
67
|
async function defaultFetchLatest(timeoutMs) {
|
|
67
68
|
try {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
// flair#1688: honour the registry npm is configured to use (scoped
|
|
70
|
+
// `@scope:registry`, project/user/global .npmrc, env `npm_config_registry`)
|
|
71
|
+
// rather than a hardcoded public host — a private mirror must not be
|
|
72
|
+
// silently bypassed by the update check.
|
|
73
|
+
//
|
|
74
|
+
// flair#1692: `fetchLatestVersion` validates the URL scheme, refuses
|
|
75
|
+
// redirects, applies npm's transport where a bare fetch cannot, and —
|
|
76
|
+
// critically — validates the returned value as strict semver before it can
|
|
77
|
+
// ever be used as an install spec.
|
|
78
|
+
const result = await fetchLatestVersion(FLAIR_PKG_NAME, { timeoutMs });
|
|
79
|
+
if (result.kind === "ok")
|
|
80
|
+
return result.version;
|
|
81
|
+
if (result.kind === "invalid") {
|
|
82
|
+
// A hostile/compromised registry can return a URL or tag as `latest`;
|
|
83
|
+
// npm would install `pkg@<url>` as a remote tarball. Refuse, and say so
|
|
84
|
+
// rather than silently treating it as "no update".
|
|
85
|
+
console.error(`flair version check: the configured registry returned a non-semver "latest" (${JSON.stringify(result.value)}) ` +
|
|
86
|
+
`from ${result.registry.url} — refusing to use it. Check \`registry\` / \`@scope:registry\` (source: ${result.registry.source}).`);
|
|
72
87
|
return null;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
88
|
+
}
|
|
89
|
+
if (result.kind === "refused") {
|
|
90
|
+
console.error(result.message);
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
77
93
|
// Offline, DNS failure, timeout, registry 5xx, bad JSON — all the same:
|
|
78
94
|
// we couldn't determine "latest" over the network this time.
|
|
79
95
|
return null;
|
|
80
96
|
}
|
|
97
|
+
catch {
|
|
98
|
+
// Defense-in-depth: the helper already swallows its failure modes. This
|
|
99
|
+
// guards the status/doctor never-throws contract even if it does not.
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
81
102
|
}
|
|
82
103
|
export function defaultVersionCheckDeps() {
|
|
83
104
|
return {
|
package/docs/api-reference.md
CHANGED
|
@@ -154,7 +154,8 @@ Skill-tagged Memory rows embed from `trigger` (the recall signal), not
|
|
|
154
154
|
| GET / POST / PUT | `/WorkspaceState` | Ed25519 | Owner-only. POST stamps `agentId`; PUT rejects a mismatch. |
|
|
155
155
|
| GET | `/WorkspaceLatest` | Ed25519 | Latest workspace row for the caller. |
|
|
156
156
|
| GET / POST / PUT | `/OrgEvent` | Ed25519 | Any verified agent reads every event. Writes stamp `authorId`. |
|
|
157
|
-
| GET | `/OrgEventCatchup` | Ed25519 | Catch-up feed for the caller. |
|
|
157
|
+
| GET / POST | `/OrgEventCatchup` | Ed25519 | Catch-up feed for the caller. `since` is optional (defaults to the per-agent watermark). GET pages; POST acks `{ position }` (advance-on-ack). |
|
|
158
|
+
| GET / POST | `/AgentReadPosition` | Ed25519 | Owner-only watermark (`stream`, default `org-event`). Foundation for light-comms catch-up. |
|
|
158
159
|
| POST | `/OrgEventMaintenance` | Ed25519 / admin | Expire / sweep org events. |
|
|
159
160
|
| POST | `/AttentionQuery` | Ed25519 | Cross-table “what touches entity E”. Entity strings: [docs/entity-vocabulary.md](entity-vocabulary.md). |
|
|
160
161
|
|
|
@@ -163,7 +164,7 @@ Skill-tagged Memory rows embed from `trigger` (the recall signal), not
|
|
|
163
164
|
| Method | Path | Auth | Notes |
|
|
164
165
|
|--------|------|------|-------|
|
|
165
166
|
| GET | `/FederationInstance` | Admin Basic | Local instance identity (CLI / admin). Peers do not call this during pair. |
|
|
166
|
-
| POST | `/FederationPair` | Pairing token + body-sig | Public at the Harper role gate. Handler validates token, signature, anti-replay. Fabric uses the bootstrap-user triple from `flair federation token`. |
|
|
167
|
+
| POST | `/FederationPair` | Pairing token + body-sig | Public at the Harper role gate. Handler validates token, signature, anti-replay. Response includes `instance {id, publicKey}` when this hub has an Instance row; `instance` is null if it does not (flair#839). The spoke CLI must not store an empty hub key (flair#822). Fabric uses the bootstrap-user triple from `flair federation token`. |
|
|
167
168
|
| POST | `/FederationSync` | Peer body-sig | Public at the role gate. Merge Memory / Soul / Agent / Relationship (and classifier-ready Message). Originator + per-record signature checks. |
|
|
168
169
|
| GET | `/FederationPeers` | Admin Basic | Known peers. |
|
|
169
170
|
| GET / write | `/Instance` | Read: Ed25519. Write: admin | Instance row (`flair_…` id, role hub/spoke). |
|
|
@@ -355,6 +356,7 @@ ed25519 / idp) and **Integration** (legacy platform connection).
|
|
|
355
356
|
| **Asset** | memory.graphql | yes | Blob (`contentType`, `data`) owned by `agentId`, linked by `memoryId` |
|
|
356
357
|
| **WorkspaceState** | workspace.graphql | yes | Current work (`ref`, `provider`, `phase`, `entities`) |
|
|
357
358
|
| **OrgEvent** | event.graphql | yes | Org-visible event (`authorId`, `kind`, `summary`, `entities`) |
|
|
359
|
+
| **AgentReadPosition** | agent.graphql | no | Per-agent watermark (`agentId`, `stream`, `position`). HTTP via `/AgentReadPosition`, not raw-table REST. |
|
|
358
360
|
| **Message** | message.graphql | yes | Signed envelope (`from`, `to`, `threadId`, `seq`, `state`, `signature`) |
|
|
359
361
|
| **OAuthClient** | oauth.graphql | yes | Registered OAuth clients |
|
|
360
362
|
| **OAuthAuthCode** | oauth.graphql | no | Single-use codes + PKCE |
|
|
@@ -241,7 +241,9 @@ Provision once, from one place, before serving multi-region traffic: step 2's
|
|
|
241
241
|
| `flair fleet verify --target <url>` | health, auth, version across origin + Flair peers |
|
|
242
242
|
| `flair federation status\|verify\|reachability --target <url>` | peer table, sync recency, probes |
|
|
243
243
|
|
|
244
|
-
`fleet verify` exit codes: 1 origin failed, 2 peer version
|
|
244
|
+
`fleet verify` exit codes: 1 origin failed, 2 reachable peer diverged (wrong version), 3 reachable peer unreachable/auth-failed. Unverifiable peers (no endpoint on file) warn and exit 0.
|
|
245
|
+
|
|
246
|
+
`federation verify` is a different question (did this canary land). Exit 0 includes unverifiable peers (401/403, unreachable); exit 1 is a reachable peer missing the canary. It pushes before probing so a spoke with no sync daemon can still check.
|
|
245
247
|
|
|
246
248
|
> **A credential mismatch renders as an empty section, not an error.** `flair status`
|
|
247
249
|
> reads `/HealthDetail` with `FLAIR_ADMIN_PASS` / `HDB_ADMIN_PASSWORD` / a pinned agent
|
|
@@ -299,17 +301,16 @@ without this re-flip will appear healthy (`/Health` green) while its MCP tools a
|
|
|
299
301
|
dark to every connected client. If you rely on MCP, add the re-flip to your upgrade
|
|
300
302
|
runbook.
|
|
301
303
|
|
|
302
|
-
###
|
|
304
|
+
### npm cache on the node
|
|
303
305
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
306
|
+
`flair deploy` and `flair upgrade --target` pass Harper an `install_command` that runs
|
|
307
|
+
`npm install --cache <tmp>` and deletes that directory when the install finishes. The
|
|
308
|
+
node's `~/.npm/_cacache` is no longer the install cache, so repeated deploys do not
|
|
309
|
+
accumulate tarballs ([flair#886](https://github.com/tpsdev-ai/flair/issues/886)).
|
|
307
310
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
obvious fix isn't available — and clearing the cache needs node access this shape
|
|
312
|
-
doesn't give you.
|
|
311
|
+
Harper still consults `install_command` only when `node_modules` is absent — that is
|
|
312
|
+
exactly when a fresh extract runs the install, which is every deploy (Harper replaces
|
|
313
|
+
the component directory). There is nothing for an operator to clean.
|
|
313
314
|
|
|
314
315
|
### Backup and rollback
|
|
315
316
|
|
package/docs/deployment.md
CHANGED
|
@@ -194,6 +194,7 @@ These are read by the Harper process at boot (same places as the table above: la
|
|
|
194
194
|
| Variable | Default | What it does |
|
|
195
195
|
|----------|---------|--------------|
|
|
196
196
|
| `FLAIR_EMBED_THREADS` | `max(1, availableParallelism() − 1)` | CPU threads for in-process embedding (harper-fabric-embeddings / llama.cpp). Host-aware so a 4-core box does not inherit HFE's fixed 6, and an 8-vCPU ingest host is not stuck at 6 idle cores. One core is left for Harper's event loop and the OS. `availableParallelism()` respects a container CPU quota. Set a positive integer to pin. Invalid values fall back to the default. |
|
|
197
|
+
| `FLAIR_EMBED_GPU_LAYERS` | unset (HFE default `0`, CPU only) | Layers to offload to the GPU. **Unset omits the field** so harper-fabric-embeddings keeps its own default of 0. This is a pin for measurement (the ingest-throughput bench, flair#1436), not a new product default — #1437 is the detect-and-default decision. `0` = CPU; `99` = full offload (Metal on mac-arm64). Invalid values fall through to omit. |
|
|
197
198
|
| `FLAIR_HYBRID_RETRIEVAL` | `true` | Hybrid BM25 + vector retrieval. Set `false` / `0` / `off` to revert to the legacy HNSW + keyword-bump path. |
|
|
198
199
|
| `FLAIR_MODELS_DIR` | `<data-dir>/models` | Directory the embedding GGUF is loaded from (and downloaded into on first boot). Point this at a pre-seeded directory to skip the HuggingFace download; see [troubleshooting.md](troubleshooting.md). |
|
|
199
200
|
|
|
@@ -216,6 +217,7 @@ Always backup before upgrades.
|
|
|
216
217
|
## Uninstall
|
|
217
218
|
|
|
218
219
|
```bash
|
|
219
|
-
flair uninstall
|
|
220
|
+
flair uninstall # stop the server and remove the launchd/systemd service; keep data and keys
|
|
221
|
+
flair uninstall --purge # also remove ~/.flair (data, keys, secrets), schedulers, and client wiring
|
|
220
222
|
npm uninstall -g @tpsdev-ai/flair
|
|
221
223
|
```
|
package/docs/federation.md
CHANGED
|
@@ -120,6 +120,24 @@ knob. The first sync runs immediately on enable.
|
|
|
120
120
|
`flair federation watch` is unchanged and still the right tool for an
|
|
121
121
|
interactive "watch it sync while I debug" session.
|
|
122
122
|
|
|
123
|
+
### Did the canary actually land?
|
|
124
|
+
|
|
125
|
+
`flair federation verify` writes a tagged memory, **pushes it** (so a
|
|
126
|
+
freshly paired spoke with no sync daemon can still pass), then probes each
|
|
127
|
+
peer. It uses the same couldn't-check-≠-failed split as `flair fleet verify`
|
|
128
|
+
(flair#988 / #823):
|
|
129
|
+
|
|
130
|
+
| What the probe saw | Verdict | Exit |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| Canary found | OK | 0 |
|
|
133
|
+
| HTTP 401/403, unreachable, or no endpoint | UNVERIFIABLE (warning) | 0 |
|
|
134
|
+
| Reachable peer answered 200 without the canary after a successful push | FAIL | 1 |
|
|
135
|
+
| Revoked peer | UNVERIFIABLE (warning; not probed) | 0 |
|
|
136
|
+
|
|
137
|
+
A 401 is "could not authenticate to that peer," not "sync failed." Do not
|
|
138
|
+
treat unverifiable as a pass that hides a reachable peer on the wrong side
|
|
139
|
+
of the canary.
|
|
140
|
+
|
|
123
141
|
**Credentials.** The scheduler never writes a password into a unit file. It
|
|
124
142
|
stores the *path* given to `--admin-pass-file` (defaulting to
|
|
125
143
|
`~/.flair/admin-pass` when that exists) and the CLI reads the file at run time,
|
|
@@ -197,6 +215,7 @@ Records with `updatedAt` more than 5 minutes in the future are rejected. This pr
|
|
|
197
215
|
| `flair federation sync disable [--remove-shim]` | Remove the scheduled sync driver |
|
|
198
216
|
| `flair federation sync status` | Show whether the driver is installed and genuinely active |
|
|
199
217
|
| `flair federation watch [--interval <s>]` | Run sync in a foreground loop for an interactive session (default 30s) |
|
|
218
|
+
| `flair federation verify [--wait <s>] [--admin-pass <pass>]` | Write a canary, push it, and check each peer. Flag/file admin credentials authenticate the admin-gated peer listing. 401/403, unreachable, and revoked are UNVERIFIABLE (warning, exit 0); a reachable peer missing the canary still FAILs (exit 1). |
|
|
200
219
|
| `flair federation reachability` | Probe local instance + each paired peer (read-only) |
|
|
201
220
|
| `flair federation token [--ttl <min>]` | Generate a one-time pairing token triple (hub only) |
|
|
202
221
|
|
package/docs/hosted-on-fabric.md
CHANGED
|
@@ -155,7 +155,7 @@ flair status --target https://<cluster>.<org>.harperfabric.com
|
|
|
155
155
|
flair fleet verify --target https://<cluster>.<org>.harperfabric.com
|
|
156
156
|
```
|
|
157
157
|
|
|
158
|
-
`fleet verify` checks health, auth, and version across the origin node plus every Flair federation peer on file. Exit codes: 0 = all verified, 1 = origin failed, 2 = peer
|
|
158
|
+
`fleet verify` checks health, auth, and version across the origin node plus every Flair federation peer on file. Exit codes: 0 = all probed nodes verified (unverifiable peers warn, do not fail), 1 = origin failed, 2 = reachable peer diverged, 3 = reachable peer unreachable/auth-failed.
|
|
159
159
|
|
|
160
160
|
> **A credential mismatch renders as an empty section.** `flair status` reads `/HealthDetail` with `FLAIR_ADMIN_PASS` / `HDB_ADMIN_PASSWORD` / a pinned agent key — **not** the `FABRIC_*` credentials. On failure it renders blank.
|
|
161
161
|
|
|
@@ -241,9 +241,9 @@ Fabric gives you N regional nodes running one component — **not** N Flair inst
|
|
|
241
241
|
|
|
242
242
|
`flair status` reports usage for two directories: no free space, no total, no quota. An instance can hit its quota with nothing saying so. The one indirect signal is a migration halting for space.
|
|
243
243
|
|
|
244
|
-
###
|
|
244
|
+
### npm cache is ephemeral per deploy
|
|
245
245
|
|
|
246
|
-
|
|
246
|
+
`flair deploy` / `flair upgrade --target` run the node's `npm install` against a temporary cache and delete it afterwards, so hub quota no longer grows with every install ([flair#886](https://github.com/tpsdev-ai/flair/issues/886)).
|
|
247
247
|
|
|
248
248
|
---
|
|
249
249
|
|
package/docs/quickstart.md
CHANGED
|
@@ -202,7 +202,8 @@ Soul entries and relevant memories, in one block sized to a token budget. Paste
|
|
|
202
202
|
flair stop # stop the service, keep data
|
|
203
203
|
flair restart # restart
|
|
204
204
|
flair uninstall # remove the service, keep data and keys
|
|
205
|
-
flair uninstall --purge # remove
|
|
205
|
+
flair uninstall --purge # remove data, keys, secrets, schedulers, and client wiring
|
|
206
|
+
# (the npm package is left; `npm uninstall -g @tpsdev-ai/flair`)
|
|
206
207
|
```
|
|
207
208
|
|
|
208
209
|
All reversible. Your memories aren't locked in.
|
package/docs/releasing.md
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# Releasing Flair
|
|
2
2
|
|
|
3
|
-
Flair publishes
|
|
3
|
+
Flair publishes nine workspace packages to npm under `@tpsdev-ai/*`. Releases are
|
|
4
4
|
**tokenless** and **staged**: CI authenticates to npm with a short-lived OIDC token
|
|
5
5
|
(no `NPM_TOKEN` lives anywhere) and submits each package to npm's **staging** area.
|
|
6
6
|
A maintainer then approves the staged tarballs on npmjs.com with 2FA to make them live.
|
|
7
7
|
|
|
8
8
|
> `flair-bench` is version-bumped and tagged in lockstep with the other 7, and stages in
|
|
9
9
|
> its own step in CI for [historical reasons](#flair-bench-bootstrap-one-time-done). That
|
|
10
|
-
> step is no longer allowed to fail:
|
|
10
|
+
> step is no longer allowed to fail: every already-published package must stage
|
|
11
|
+
> for a release to pass.
|
|
11
12
|
|
|
12
13
|
```
|
|
13
14
|
merge release PR ──▶ push tag v0.11.0 ──▶ CI stages all packages ──▶ npm staging
|
|
@@ -145,11 +146,15 @@ Leave `npm publish` **unchecked** under allowed actions. This structurally preve
|
|
|
145
146
|
CI/OIDC identity from publishing anything live directly — the only path to live is the
|
|
146
147
|
human 2FA approval of a staged package.
|
|
147
148
|
|
|
148
|
-
Packages: `flair-client`, `flair-mcp`, `flair`,
|
|
149
|
-
`n8n-nodes-flair`, `langgraph-flair`, `flair-bench`.
|
|
149
|
+
Packages: `flair-client`, `flair-mcp`, `flair`,
|
|
150
|
+
`openclaw-flair`, `pi-flair`, `n8n-nodes-flair`, `langgraph-flair`, `flair-bench`.
|
|
150
151
|
|
|
151
|
-
>
|
|
152
|
-
>
|
|
152
|
+
> `flair-tool-descriptors` is private and never published. Since flair#1683 it is a
|
|
153
|
+
> **build-time source**: `scripts/vendor-tool-descriptors.mjs` copies it into each
|
|
154
|
+
> consumer's own tree at prebuild (`resources/tool-descriptors/` for `flair`,
|
|
155
|
+
> `packages/flair-mcp/src/tool-descriptors/` for `flair-mcp`) and the consumers
|
|
156
|
+
> import it by relative path. Nothing declares or bundles it as a dependency —
|
|
157
|
+
> 0.54.1's `bundleDependencies` broke fresh global installs (flair#1681 → #1683).
|
|
153
158
|
|
|
154
159
|
### `flair-bench` bootstrap (one-time, done)
|
|
155
160
|
|
|
@@ -188,6 +193,15 @@ review. Because the release is triggered by a tag push, its deployment policy mu
|
|
|
188
193
|
**`v*` tags** (Settings → Environments → `release` → Deployment branches and tags →
|
|
189
194
|
Selected branches and tags → add tag rule `v*`).
|
|
190
195
|
|
|
196
|
+
### Required status checks (ruleset)
|
|
197
|
+
|
|
198
|
+
Add **`First-publish preflight`** to the main branch ruleset's required status
|
|
199
|
+
checks (repo settings → Rules → main). This is a repo-settings act, not a code
|
|
200
|
+
change: until it is listed, the release-PR job is **advisory only** and a red
|
|
201
|
+
first-publish preflight can be merged past. The tag-triggered
|
|
202
|
+
`release-publish.yml` also runs the same check before staging, so a hazard is
|
|
203
|
+
still stopped on the normal release path either way.
|
|
204
|
+
|
|
191
205
|
### Approver 2FA
|
|
192
206
|
|
|
193
207
|
The maintainer who approves staged packages must have 2FA enabled on their npm account.
|
package/docs/spoke-bringup.md
CHANGED
|
@@ -138,8 +138,8 @@ The `--admin-pass` is required so the CLI can write the hub as a local `Peer` re
|
|
|
138
138
|
|
|
139
139
|
1. The spoke POSTs a signed pairing request to the hub's `/FederationPair` endpoint.
|
|
140
140
|
2. The bootstrap user authenticates at the Harper platform layer (works on standalone and Fabric deployments alike).
|
|
141
|
-
3. The hub validates the one-time token, verifies the Ed25519 signature, and creates a `Peer` record.
|
|
142
|
-
4. The spoke writes a `Peer` record pointing to the hub so sync knows where to push.
|
|
141
|
+
3. The hub validates the one-time token, verifies the Ed25519 signature, and creates a `Peer` record. The response includes the hub's `instance {id, publicKey}` when the hub has a FederationInstance row.
|
|
142
|
+
4. The spoke writes a `Peer` record pointing to the hub so sync knows where to push. A missing hub `publicKey` is an error, never an empty string. That does not create the hub's Instance row.
|
|
143
143
|
|
|
144
144
|
---
|
|
145
145
|
|
|
@@ -156,13 +156,18 @@ Syncing to hub: <hub-instance-id>...
|
|
|
156
156
|
✅ Synced 0 records (0 skipped) in 45ms
|
|
157
157
|
```
|
|
158
158
|
|
|
159
|
-
Then verify
|
|
159
|
+
Then verify the pairing. `federation verify` pushes the canary itself — you
|
|
160
|
+
do not need the systemd timer from §7 first:
|
|
160
161
|
|
|
161
162
|
```bash
|
|
163
|
+
flair federation verify --admin-pass "$FLAIR_ADMIN_PASS"
|
|
162
164
|
flair federation reachability
|
|
163
165
|
```
|
|
164
166
|
|
|
165
|
-
|
|
167
|
+
`verify` writes a tagged memory, syncs it, and checks each peer. HTTP 401/403,
|
|
168
|
+
an unreachable peer, or a revoked leftover row is UNVERIFIABLE (a warning),
|
|
169
|
+
not FAIL. A reachable peer that is missing the canary still fails.
|
|
170
|
+
`reachability` should report `OK` for local and the hub.
|
|
166
171
|
|
|
167
172
|
---
|
|
168
173
|
|
|
@@ -290,7 +295,7 @@ flair federation pair https://fabric-node.example.com:19926/<instance> \
|
|
|
290
295
|
| Hub: mint token | `flair federation token --admin-pass <pass> > triple.json` |
|
|
291
296
|
| Spoke: pair | `flair federation pair <hub-url> --token-from ./triple.json --admin-pass <pass>` |
|
|
292
297
|
| Spoke: sync | `flair federation sync --admin-pass <pass>` |
|
|
293
|
-
| Spoke: verify | `flair federation reachability` |
|
|
298
|
+
| Spoke: verify | `flair federation verify --admin-pass <pass>` then `flair federation reachability` |
|
|
294
299
|
| Watch loop | `flair federation watch --interval 30` |
|
|
295
300
|
|
|
296
301
|
---
|
package/docs/standalone-local.md
CHANGED
|
@@ -57,7 +57,8 @@ flair status # check everything is working
|
|
|
57
57
|
flair stop # stop the service (keeps data)
|
|
58
58
|
flair restart # restart the service
|
|
59
59
|
flair uninstall # remove the service (keeps data + keys)
|
|
60
|
-
flair uninstall --purge # remove
|
|
60
|
+
flair uninstall --purge # remove data, keys, secrets, schedulers, and client wiring
|
|
61
|
+
# (the npm package is left; `npm uninstall -g @tpsdev-ai/flair`)
|
|
61
62
|
```
|
|
62
63
|
|
|
63
64
|
On macOS the service is a launchd plist at `~/Library/LaunchAgents/ai.tpsdev.flair.plist`. On Linux it is a systemd unit at `~/.config/systemd/user/flair.service`. Both auto-start on login/boot and restart on crash.
|
|
@@ -108,6 +109,7 @@ logging:
|
|
|
108
109
|
| `FLAIR_KEY_PASSPHRASE` | Passphrase for AES-256-GCM encryption of federation private-key seeds. | Set explicitly for production federation deployments. |
|
|
109
110
|
| `FLAIR_URL` | Override the Flair base URL for CLI commands (points to a remote instance). | When connecting from a different machine. |
|
|
110
111
|
| `FLAIR_EMBED_THREADS` | CPU threads for in-process embedding. Default is `max(1, availableParallelism() − 1)` — host-aware, one core left for Harper. | Pin a positive integer on a dedicated ingest host, or when the default leaves cores idle / oversubscribed. See [deployment.md](deployment.md#performance-related-environment-variables). |
|
|
112
|
+
| `FLAIR_EMBED_GPU_LAYERS` | GPU layers to offload. Unset leaves HFE's default of 0 (CPU only) — this is a pin, not a new default. | Only to measure Metal offload (`99`) or pin CPU (`0`). See [deployment.md](deployment.md#performance-related-environment-variables) and #1437. |
|
|
111
113
|
| `FLAIR_HYBRID_RETRIEVAL` | Hybrid BM25 + vector retrieval (default on). | Set `false` to revert to HNSW-only. |
|
|
112
114
|
| `FLAIR_MODELS_DIR` | Directory the embedding GGUF is loaded from. | When the model lives outside `<data-dir>/models`. |
|
|
113
115
|
|