@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.
Files changed (97) hide show
  1. package/README.md +4 -1
  2. package/dist/build-info.json +3 -3
  3. package/dist/cli.js +1791 -15648
  4. package/dist/commands/agent.js +453 -0
  5. package/dist/commands/attention.js +121 -0
  6. package/dist/commands/backup.js +115 -0
  7. package/dist/commands/bootstrap.js +91 -0
  8. package/dist/commands/bridge.js +608 -0
  9. package/dist/commands/deploy.js +180 -0
  10. package/dist/commands/doctor.js +1665 -0
  11. package/dist/commands/export.js +110 -0
  12. package/dist/commands/federation.js +1575 -0
  13. package/dist/commands/fleet.js +73 -0
  14. package/dist/commands/grant.js +109 -0
  15. package/dist/commands/hook.js +193 -0
  16. package/dist/commands/idp.js +193 -0
  17. package/dist/commands/import.js +134 -0
  18. package/dist/commands/init.js +1203 -0
  19. package/dist/commands/inspect.js +45 -0
  20. package/dist/commands/keys.js +187 -0
  21. package/dist/commands/mcp.js +707 -0
  22. package/dist/commands/memory.js +501 -0
  23. package/dist/commands/migrate-harness-memory.js +270 -0
  24. package/dist/commands/orgevent.js +138 -0
  25. package/dist/commands/presence.js +76 -0
  26. package/dist/commands/principal.js +338 -0
  27. package/dist/commands/quality.js +1164 -0
  28. package/dist/commands/reembed.js +296 -0
  29. package/dist/commands/relationship.js +76 -0
  30. package/dist/commands/rem.js +1048 -0
  31. package/dist/commands/restore.js +130 -0
  32. package/dist/commands/search.js +244 -0
  33. package/dist/commands/service.js +315 -0
  34. package/dist/commands/session.js +184 -0
  35. package/dist/commands/soul.js +155 -0
  36. package/dist/commands/status.js +931 -0
  37. package/dist/commands/test.js +93 -0
  38. package/dist/commands/uninstall.js +143 -0
  39. package/dist/commands/upgrade.js +1628 -0
  40. package/dist/commands/workspace.js +114 -0
  41. package/dist/deploy.js +24 -0
  42. package/dist/engine-version.js +12 -4
  43. package/dist/fabric-npm-install.js +87 -0
  44. package/dist/fabric-upgrade.js +30 -15
  45. package/dist/federation-verify.js +498 -0
  46. package/dist/fleet-verify.js +144 -21
  47. package/dist/install/clients.js +167 -0
  48. package/dist/lib/auth-resolve.js +76 -1
  49. package/dist/lib/daemon-liveness.js +131 -2
  50. package/dist/lib/doctor-config-path.js +61 -0
  51. package/dist/lib/doctor-federation-driver.js +189 -0
  52. package/dist/lib/doctor-run.js +40 -0
  53. package/dist/lib/entity-vocab-cli.js +3 -3
  54. package/dist/lib/federation-pair-identity.js +47 -0
  55. package/dist/lib/launchd-repair.js +5 -4
  56. package/dist/lib/npm-registry.js +578 -0
  57. package/dist/lib/ops-api-bind.js +115 -0
  58. package/dist/lib/owned-pins.js +219 -0
  59. package/dist/lib/uninstall-purge.js +218 -0
  60. package/dist/rem/restore.js +8 -10
  61. package/dist/resources/AgentReadPosition.js +74 -0
  62. package/dist/resources/Federation.js +8 -2
  63. package/dist/resources/Memory.js +4 -3
  64. package/dist/resources/MemoryBootstrap.js +41 -25
  65. package/dist/resources/MemoryCandidate.js +5 -6
  66. package/dist/resources/OrgEventCatchup.js +126 -47
  67. package/dist/resources/agent-read-position-lib.js +83 -0
  68. package/dist/resources/agent-read-position.js +120 -0
  69. package/dist/resources/embeddings-boot.js +32 -0
  70. package/dist/resources/federation-peer-liveness.js +73 -0
  71. package/dist/resources/health.js +68 -19
  72. package/dist/resources/mcp-tools.js +48 -279
  73. package/dist/resources/memory-visibility.js +3 -3
  74. package/dist/resources/migration-boot.js +59 -18
  75. package/dist/resources/migrations/embedding-stamp.js +20 -1
  76. package/dist/resources/migrations/recheck.js +43 -0
  77. package/dist/resources/migrations/runner.js +6 -1
  78. package/dist/resources/migrations/stamp-outstanding.js +171 -0
  79. package/dist/resources/migrations/visibility-backfill.js +2 -2
  80. package/dist/resources/org-event-catchup-lib.js +47 -0
  81. package/dist/resources/record-owner-guard.js +1 -0
  82. package/dist/resources/tool-descriptors/index.js +669 -0
  83. package/dist/stamp-migration-verify.js +163 -0
  84. package/dist/stamp-outstanding.js +144 -0
  85. package/dist/version-check.js +29 -8
  86. package/docs/api-reference.md +4 -2
  87. package/docs/deploying-on-fabric.md +11 -10
  88. package/docs/deployment.md +3 -1
  89. package/docs/federation.md +19 -0
  90. package/docs/hosted-on-fabric.md +3 -3
  91. package/docs/quickstart.md +2 -1
  92. package/docs/releasing.md +20 -6
  93. package/docs/spoke-bringup.md +10 -5
  94. package/docs/standalone-local.md +3 -1
  95. package/docs/upgrade.md +25 -6
  96. package/package.json +4 -4
  97. package/schemas/agent.graphql +15 -0
@@ -175,6 +175,23 @@ export function resolveEmbedThreads(env = process.env, cores = availableParallel
175
175
  const safeCores = Number.isFinite(cores) && cores >= 1 ? Math.floor(cores) : 1;
176
176
  return Math.max(1, safeCores - 1);
177
177
  }
178
+ /**
179
+ * llama.cpp GPU-layer offload passed to HFE `register({config:{gpuLayers}})`.
180
+ *
181
+ * DEFAULT IS UNCHANGED. Unset / empty / non-integer / negative → `undefined`,
182
+ * and the register() call OMITS the field so HFE keeps its own default of 0
183
+ * (CPU only). This is a measurement pin for the ingest-throughput bench
184
+ * (flair#1436 / #1437), not a product default change. #1437 is the decision
185
+ * about whether to detect-and-default gpuLayers; this function must not
186
+ * pre-empt it.
187
+ *
188
+ * Override: `FLAIR_EMBED_GPU_LAYERS` — a non-negative integer, env-only
189
+ * (same persist-path reason as `FLAIR_EMBED_THREADS`). 0 = CPU only; 99 =
190
+ * full offload (the Metal cell). Invalid values fall through to omit.
191
+ */
192
+ export function resolveEmbedGpuLayers(env = process.env) {
193
+ return parseNonNegativeInt(env.FLAIR_EMBED_GPU_LAYERS);
194
+ }
178
195
  function parsePositiveInt(raw) {
179
196
  if (raw == null)
180
197
  return undefined;
@@ -186,6 +203,17 @@ function parsePositiveInt(raw) {
186
203
  return undefined;
187
204
  return n;
188
205
  }
206
+ function parseNonNegativeInt(raw) {
207
+ if (raw == null)
208
+ return undefined;
209
+ const trimmed = raw.trim();
210
+ if (trimmed === "")
211
+ return undefined;
212
+ const n = Number(trimmed);
213
+ if (!Number.isInteger(n) || n < 0)
214
+ return undefined;
215
+ return n;
216
+ }
189
217
  let registered = false;
190
218
  /**
191
219
  * Register the embedding backend. Idempotent within a process (mirrors
@@ -200,6 +228,9 @@ export async function registerEmbeddingsBackend() {
200
228
  const { register } = await import("harper-fabric-embeddings");
201
229
  const modelPath = benchModelPathOverride();
202
230
  const threads = resolveEmbedThreads();
231
+ // Omit when unset so HFE's default of 0 is unchanged (flair#1436 measurement
232
+ // pin; #1437 is the default-change decision). Never pass a synthesized default.
233
+ const gpuLayers = resolveEmbedGpuLayers();
203
234
  await register({
204
235
  logicalName: LOGICAL_NAME,
205
236
  kind: "embedding",
@@ -208,6 +239,7 @@ export async function registerEmbeddingsBackend() {
208
239
  ? { modelPath, pooling: EMBEDDING_POOLING }
209
240
  : { modelName: MODEL_NAME, modelsDir: resolveModelsDir(), pooling: EMBEDDING_POOLING }),
210
241
  threads,
242
+ ...(gpuLayers !== undefined ? { gpuLayers } : {}),
211
243
  },
212
244
  });
213
245
  }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Pure classifier for federation peer liveness — no Harper imports.
3
+ *
4
+ * flair#1499: HealthDetail used to count `status === "connected"` (a value
5
+ * pairing never writes — spokes stay `paired` even after a successful
6
+ * lastSyncAt advance) and then take the oldest lastSyncAt across ALL peers,
7
+ * including revoked. A healthy hub synced a minute ago plus a revoked row
8
+ * from June therefore rendered as "0 connected" and fired
9
+ * "federation peers all disconnected >24h".
10
+ *
11
+ * Three contact states, plus revoked (which never drives the staleness
12
+ * warning). Same class as #988: missing/unreadable evidence is UNKNOWN,
13
+ * never DISCONNECTED.
14
+ *
15
+ * (a) real lastSyncAt within the window → connected
16
+ * (b) real lastSyncAt older than the window → disconnected
17
+ * (c) no parseable lastSyncAt → unknown (never rendered as (b))
18
+ * revoked stored status → revoked (excluded from the >24h warning)
19
+ */
20
+ export const PEER_STALE_MS = 24 * 3600 * 1000;
21
+ export const FEDERATION_PEERS_ALL_DISCONNECTED_WARNING = "federation peers all disconnected >24h";
22
+ /** Parse a written last-contact stamp. Missing / unparseable → null, never 0/epoch. */
23
+ export function parseLastContactMs(lastSyncAt) {
24
+ if (typeof lastSyncAt !== "string")
25
+ return null;
26
+ const trimmed = lastSyncAt.trim();
27
+ if (!trimmed)
28
+ return null;
29
+ const t = Date.parse(trimmed);
30
+ return Number.isFinite(t) ? t : null;
31
+ }
32
+ export function classifyPeerLiveness(peer, nowMs, windowMs = PEER_STALE_MS) {
33
+ if (peer.status === "revoked")
34
+ return "revoked";
35
+ const ts = parseLastContactMs(peer.lastSyncAt);
36
+ if (ts === null)
37
+ return "unknown";
38
+ // Future / equal-now stamps are contact within the window (clock skew).
39
+ if (nowMs - ts > windowMs)
40
+ return "disconnected";
41
+ return "connected";
42
+ }
43
+ export function summarizePeerLiveness(peers, nowMs, windowMs = PEER_STALE_MS) {
44
+ let connected = 0;
45
+ let disconnected = 0;
46
+ let revoked = 0;
47
+ let unknown = 0;
48
+ for (const peer of peers) {
49
+ const liveness = classifyPeerLiveness(peer, nowMs, windowMs);
50
+ if (liveness === "connected")
51
+ connected++;
52
+ else if (liveness === "disconnected")
53
+ disconnected++;
54
+ else if (liveness === "revoked")
55
+ revoked++;
56
+ else
57
+ unknown++;
58
+ }
59
+ const nonRevoked = peers.length - revoked;
60
+ return {
61
+ total: peers.length,
62
+ connected,
63
+ disconnected,
64
+ revoked,
65
+ unknown,
66
+ allNonRevokedDisconnected: nonRevoked > 0 && disconnected === nonRevoked,
67
+ };
68
+ }
69
+ export function federationPeersAllDisconnectedWarning(summary) {
70
+ if (!summary.allNonRevokedDisconnected)
71
+ return null;
72
+ return { level: "warn", message: FEDERATION_PEERS_ALL_DISCONNECTED_WARNING };
73
+ }
@@ -11,7 +11,10 @@ import { REM_DEDUP_STATS_PATH } from "./dedup-cluster.js";
11
11
  import { hybridEnabled } from "./bm25.js";
12
12
  import { bm25IndexEnabled, bm25IndexStatus } from "./bm25-index-service.js";
13
13
  import { normalizeStamp } from "./embedding-space-guard.js";
14
+ import { getModelId } from "./embeddings-provider.js";
15
+ import { describeStampOutstanding, EMBEDDING_STAMP_ID } from "./migrations/stamp-outstanding.js";
14
16
  import { buildPublicHealthBody, resolveSearchReadiness } from "./search-readiness.js";
17
+ import { classifyPeerLiveness, federationPeersAllDisconnectedWarning, summarizePeerLiveness, } from "./federation-peer-liveness.js";
15
18
  const db = databases;
16
19
  const redactHome = (p) => {
17
20
  const home = homedir();
@@ -160,6 +163,14 @@ export class HealthDetail extends Resource {
160
163
  const stats = { ok: true };
161
164
  const nowMs = Date.now();
162
165
  const warnings = [];
166
+ // flair#1073: set while walking memories, consumed after the migrations
167
+ // snapshot so the outstanding-migration warning can name runner state.
168
+ // Never copied onto `stats` — it is not part of the /HealthDetail shape.
169
+ // `memoryScanOk` is the gate (not "mixed spaces"): a uniformly pre-flip
170
+ // corpus is one space and still outstanding against getModelId().
171
+ let mixedEmbeddingSpaces = false;
172
+ let memoryScanOk = false;
173
+ let memoryModelCounts = {};
163
174
  // flair#1326: same search-ready signal as public /Health. HealthDetail
164
175
  // stays HTTP 200 (it is a stats dump, not a traffic gate); the field
165
176
  // and a warning name the lag so `flair status` / operators can see it.
@@ -258,13 +269,12 @@ export class HealthDetail extends Resource {
258
269
  // re-embed) still trips it, and that is exactly when the query-time guard
259
270
  // degrades recall to keyword-only.
260
271
  const distinctSpaces = new Set(realModels.map((k) => normalizeStamp(k)).filter((s) => s !== null));
261
- if (distinctSpaces.size > 1) {
262
- const list = realModels.map((k) => `${k}:${modelCounts[k]}`).join(", ");
263
- warnings.push({
264
- level: "warn",
265
- message: `multiple embedding models in use (${list}) — cross-model search unreliable; run: flair reembed against one model`,
266
- });
267
- }
272
+ // Always keep counts — a uniformly stale pre-flip corpus is one space
273
+ // and must still name embedding-stamp (Bugbot High on flair#1606).
274
+ memoryScanOk = true;
275
+ memoryModelCounts = modelCounts;
276
+ if (distinctSpaces.size > 1)
277
+ mixedEmbeddingSpaces = true;
268
278
  }
269
279
  catch {
270
280
  stats.memories = null;
@@ -378,11 +388,16 @@ export class HealthDetail extends Resource {
378
388
  }
379
389
  else {
380
390
  const inst = instances[0];
391
+ // flair#1499: derive connected/down from lastSyncAt, not stored
392
+ // status. Pairing writes `paired`; a recent lastSyncAt is connected.
393
+ // Revoked rows are counted separately and never drive the >24h warning.
394
+ const liveness = summarizePeerLiveness(peers, nowMs);
381
395
  const peersBlock = {
382
- total: peers.length,
383
- connected: peers.filter((p) => p.status === "connected").length,
384
- disconnected: peers.filter((p) => p.status === "disconnected").length,
385
- revoked: peers.filter((p) => p.status === "revoked").length,
396
+ total: liveness.total,
397
+ connected: liveness.connected,
398
+ disconnected: liveness.disconnected,
399
+ revoked: liveness.revoked,
400
+ unknown: liveness.unknown,
386
401
  };
387
402
  const pendingTokens = tokens.filter((t) => !t.consumedBy && t.expiresAt && new Date(t.expiresAt).getTime() > nowMs).length;
388
403
  stats.federation = {
@@ -395,17 +410,13 @@ export class HealthDetail extends Resource {
395
410
  role: p.role,
396
411
  status: p.status,
397
412
  lastSyncAt: p.lastSyncAt ?? null,
413
+ liveness: classifyPeerLiveness(p, nowMs),
398
414
  }))
399
415
  : undefined,
400
416
  };
401
- if (peers.length > 0 && peersBlock.connected === 0) {
402
- const oldest = peers
403
- .map((p) => (p.lastSyncAt ? new Date(p.lastSyncAt).getTime() : 0))
404
- .reduce((a, b) => (a === 0 ? b : b === 0 ? a : Math.min(a, b)), 0);
405
- if (oldest > 0 && nowMs - oldest > 24 * 3600 * 1000) {
406
- warnings.push({ level: "warn", message: "federation peers all disconnected >24h" });
407
- }
408
- }
417
+ const disconnectWarn = federationPeersAllDisconnectedWarning(liveness);
418
+ if (disconnectWarn)
419
+ warnings.push(disconnectWarn);
409
420
  if (pendingTokens > 0) {
410
421
  warnings.push({ level: "info", message: `${pendingTokens} pairing token(s) unconsumed` });
411
422
  }
@@ -660,9 +671,47 @@ export class HealthDetail extends Resource {
660
671
  message: "migration boot cycle never fired on this instance (cyclePhase=idle) — no migration will run until this is resolved; see `flair doctor`",
661
672
  });
662
673
  }
674
+ if (memoryScanOk) {
675
+ const stamp = snapshot.migrations.find((m) => m.id === EMBEDDING_STAMP_ID);
676
+ const outstanding = describeStampOutstanding({
677
+ modelCounts: memoryModelCounts,
678
+ currentModelId: getModelId(),
679
+ migration: stamp,
680
+ cyclePhase: snapshot.cyclePhase,
681
+ lastCycleError: snapshot.lastCycleError,
682
+ });
683
+ if (outstanding.outstanding) {
684
+ warnings.push({ level: "warn", message: outstanding.warning });
685
+ }
686
+ else if (mixedEmbeddingSpaces) {
687
+ const list = Object.entries(memoryModelCounts)
688
+ .filter(([k, n]) => k !== "hash-512d" && n > 0)
689
+ .map(([k, n]) => `${k}:${n}`)
690
+ .join(", ");
691
+ warnings.push({
692
+ level: "warn",
693
+ message: `multiple embedding models in use (${list}) — cross-model search unreliable; run: flair reembed against one model`,
694
+ });
695
+ }
696
+ }
663
697
  }
664
698
  catch {
665
699
  stats.migrations = null;
700
+ if (memoryScanOk) {
701
+ const outstanding = describeStampOutstanding({
702
+ modelCounts: memoryModelCounts,
703
+ currentModelId: getModelId(),
704
+ });
705
+ if (outstanding.outstanding) {
706
+ warnings.push({ level: "warn", message: outstanding.warning });
707
+ }
708
+ else if (mixedEmbeddingSpaces) {
709
+ warnings.push({
710
+ level: "warn",
711
+ message: `multiple embedding models in use — cross-model search unreliable; run: flair reembed against one model`,
712
+ });
713
+ }
714
+ }
666
715
  }
667
716
  // ── Disk ──
668
717
  // flair#812: same shared read-only resolution as the migrations section