@tpsdev-ai/flair 0.53.0 → 0.54.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +1654 -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 +914 -0
  37. package/dist/commands/test.js +93 -0
  38. package/dist/commands/uninstall.js +143 -0
  39. package/dist/commands/upgrade.js +1592 -0
  40. package/dist/commands/workspace.js +114 -0
  41. package/dist/deploy.js +24 -0
  42. package/dist/fabric-npm-install.js +87 -0
  43. package/dist/federation-verify.js +498 -0
  44. package/dist/fleet-verify.js +144 -21
  45. package/dist/install/clients.js +167 -0
  46. package/dist/lib/auth-resolve.js +76 -1
  47. package/dist/lib/daemon-liveness.js +131 -2
  48. package/dist/lib/doctor-config-path.js +61 -0
  49. package/dist/lib/doctor-federation-driver.js +189 -0
  50. package/dist/lib/doctor-run.js +40 -0
  51. package/dist/lib/entity-vocab-cli.js +3 -3
  52. package/dist/lib/federation-pair-identity.js +47 -0
  53. package/dist/lib/launchd-repair.js +5 -4
  54. package/dist/lib/ops-api-bind.js +115 -0
  55. package/dist/lib/owned-pins.js +219 -0
  56. package/dist/lib/uninstall-purge.js +218 -0
  57. package/dist/rem/restore.js +8 -10
  58. package/dist/resources/AgentReadPosition.js +74 -0
  59. package/dist/resources/Federation.js +8 -2
  60. package/dist/resources/Memory.js +4 -3
  61. package/dist/resources/MemoryBootstrap.js +41 -25
  62. package/dist/resources/MemoryCandidate.js +5 -6
  63. package/dist/resources/OrgEventCatchup.js +126 -47
  64. package/dist/resources/agent-read-position-lib.js +83 -0
  65. package/dist/resources/agent-read-position.js +120 -0
  66. package/dist/resources/embeddings-boot.js +32 -0
  67. package/dist/resources/federation-peer-liveness.js +73 -0
  68. package/dist/resources/health.js +68 -19
  69. package/dist/resources/mcp-tools.js +43 -279
  70. package/dist/resources/memory-visibility.js +3 -3
  71. package/dist/resources/migration-boot.js +59 -18
  72. package/dist/resources/migrations/embedding-stamp.js +20 -1
  73. package/dist/resources/migrations/recheck.js +43 -0
  74. package/dist/resources/migrations/runner.js +6 -1
  75. package/dist/resources/migrations/stamp-outstanding.js +171 -0
  76. package/dist/resources/migrations/visibility-backfill.js +2 -2
  77. package/dist/resources/org-event-catchup-lib.js +47 -0
  78. package/dist/resources/record-owner-guard.js +1 -0
  79. package/dist/stamp-migration-verify.js +163 -0
  80. package/dist/stamp-outstanding.js +144 -0
  81. package/docs/api-reference.md +4 -2
  82. package/docs/deploying-on-fabric.md +11 -10
  83. package/docs/deployment.md +3 -1
  84. package/docs/federation.md +19 -0
  85. package/docs/hosted-on-fabric.md +3 -3
  86. package/docs/quickstart.md +2 -1
  87. package/docs/releasing.md +15 -7
  88. package/docs/spoke-bringup.md +10 -5
  89. package/docs/standalone-local.md +3 -1
  90. package/docs/upgrade.md +25 -6
  91. package/node_modules/@tpsdev-ai/flair-tool-descriptors/LICENSE +19 -0
  92. package/node_modules/@tpsdev-ai/flair-tool-descriptors/README.md +22 -0
  93. package/node_modules/@tpsdev-ai/flair-tool-descriptors/dist/index.d.ts +70 -0
  94. package/node_modules/@tpsdev-ai/flair-tool-descriptors/dist/index.js +665 -0
  95. package/node_modules/@tpsdev-ai/flair-tool-descriptors/package.json +46 -0
  96. package/package.json +9 -4
  97. package/schemas/agent.graphql +15 -0
@@ -0,0 +1,498 @@
1
+ /**
2
+ * federation-verify.ts — `flair federation verify` (flair#823)
3
+ *
4
+ * End-to-end canary: write a tagged memory locally, push it (bring-up has
5
+ * no sync daemon yet), then probe each peer. Same class as fleet-verify /
6
+ * flair#988: separate "couldn't check" from "verified wrong."
7
+ *
8
+ * UNVERIFIABLE (warning, exit 0): HTTP 401/403, unreachable, revoked,
9
+ * no endpoint, or we could not inject the canary while lastSyncAt is
10
+ * still fresh.
11
+ * FAIL (exit 1): a reachable, authenticated peer is missing the canary
12
+ * after a successful push — or lastSyncAt is stale when we could not
13
+ * inject and the peer answered 200 without the tag.
14
+ * OK (exit 0): the canary was found.
15
+ *
16
+ * Revoked peers are UNVERIFIABLE (listed, not probed, not FAIL) — Cos
17
+ * mid-flight: same couldn't-check bucket as 401 / unreachable. Authenticating
18
+ * the probe may later use #822's publicKey; this module does not wait on it.
19
+ * Do NOT always exit 0 — a verified-wrong reachable peer still fails.
20
+ */
21
+ import { validatePeerEndpoint } from "./fleet-verify.js";
22
+ import { DEFAULT_INTERVAL_SECONDS, freshnessWindowMs, } from "./federation/scheduler.js";
23
+ export const FED_VERIFY_EXIT_OK = 0;
24
+ export const FED_VERIFY_EXIT_DIVERGED = 1;
25
+ export function defaultFreshnessMs() {
26
+ return freshnessWindowMs(DEFAULT_INTERVAL_SECONDS);
27
+ }
28
+ export function lastSyncFreshness(lastSyncAt, nowMs, windowMs) {
29
+ if (!lastSyncAt)
30
+ return { fresh: false, ageMs: null };
31
+ const t = Date.parse(lastSyncAt);
32
+ if (!Number.isFinite(t))
33
+ return { fresh: false, ageMs: null };
34
+ const ageMs = nowMs - t;
35
+ return { fresh: ageMs <= windowMs, ageMs };
36
+ }
37
+ /**
38
+ * After the wait window, a peer answered 200 but never showed the canary.
39
+ * A successful push means we verified the write is absent → FAIL.
40
+ * A failed/skipped push plus a fresh lastSyncAt is "couldn't inject,"
41
+ * not "verified diverged." A stale reachable peer still FAILs.
42
+ */
43
+ export function classifyMissingAfterWindow(input) {
44
+ const { fresh, ageMs } = lastSyncFreshness(input.lastSyncAt, input.nowMs, input.freshnessMs);
45
+ if (input.pushed) {
46
+ return {
47
+ status: "fail",
48
+ detail: `timeout — reachable peer did not have the canary after push within ${input.waitSeconds}s`,
49
+ };
50
+ }
51
+ if (fresh) {
52
+ const age = ageMs == null ? "unknown" : `${Math.floor(ageMs / 1000)}s ago`;
53
+ return {
54
+ status: "unverifiable",
55
+ detail: `could not push canary; lastSyncAt is fresh (${age}) — could not confirm this write propagated`,
56
+ };
57
+ }
58
+ const age = ageMs == null ? "never" : `${Math.floor(ageMs / 1000)}s ago`;
59
+ return {
60
+ status: "fail",
61
+ detail: `timeout — reachable peer missing canary; lastSyncAt stale/absent (${age})`,
62
+ };
63
+ }
64
+ /** Immediate classification of a single HTTP status from the read-back probe. */
65
+ export function classifyProbeHttpStatus(status) {
66
+ if (status === 401 || status === 403)
67
+ return "auth";
68
+ if (status >= 200 && status < 300)
69
+ return "found-ok";
70
+ return "other";
71
+ }
72
+ export function authUnverifiableDetail(status) {
73
+ return `HTTP ${status} — could not authenticate to peer (couldn't check, not a sync failure)`;
74
+ }
75
+ /**
76
+ * Revoked rows are decommissioned. Do not HTTP-probe them, and do not FAIL
77
+ * them — Cos / flair#823: revoked = UNVERIFIABLE (warn), same bucket as
78
+ * 401 / unreachable.
79
+ */
80
+ export function selectPeersToProbe(peers, onlyId) {
81
+ const skippedRevoked = peers.filter((p) => p.status === "revoked" && (!onlyId || p.id === onlyId));
82
+ let probe = peers.filter((p) => p.status !== "revoked");
83
+ if (onlyId)
84
+ probe = probe.filter((p) => p.id === onlyId);
85
+ return { probe, skippedRevoked };
86
+ }
87
+ export function revokedAsUnverifiable(p) {
88
+ return {
89
+ id: p.id,
90
+ status: "unverifiable",
91
+ detail: "revoked — could not check (intentional, not a sync failure)",
92
+ lastSyncAt: p.lastSyncAt ?? null,
93
+ authenticated: false,
94
+ };
95
+ }
96
+ function formatUnverifiableWarning(unverifiableCount, checkedOk) {
97
+ return (`${unverifiableCount} peer(s) unverifiable — could not check ` +
98
+ `(auth refused, unreachable, revoked, or no endpoint); ` +
99
+ `${checkedOk} checked peer(s) have the memory.`);
100
+ }
101
+ /**
102
+ * Separate "couldn't check" from "verified wrong" (flair#823 / #988).
103
+ * Unverifiable peers never fail the run; a reachable missing-canary still does.
104
+ */
105
+ export function describeFederationVerify(results) {
106
+ const failed = results.filter((r) => r.status === "fail");
107
+ const unverifiable = results.filter((r) => r.status === "unverifiable");
108
+ const ok = results.filter((r) => r.status === "ok");
109
+ const warning = unverifiable.length > 0
110
+ ? formatUnverifiableWarning(unverifiable.length, ok.length)
111
+ : null;
112
+ if (results.length === 0) {
113
+ return {
114
+ kind: "empty",
115
+ exitCode: FED_VERIFY_EXIT_OK,
116
+ failedCount: 0,
117
+ unverifiableCount: 0,
118
+ okCount: 0,
119
+ summary: "no peers to probe.",
120
+ warning: null,
121
+ };
122
+ }
123
+ if (failed.length > 0) {
124
+ return {
125
+ kind: "diverged",
126
+ exitCode: FED_VERIFY_EXIT_DIVERGED,
127
+ failedCount: failed.length,
128
+ unverifiableCount: unverifiable.length,
129
+ okCount: ok.length,
130
+ summary: `FAIL: ${failed.length}/${results.length} peer(s) did not see the memory.`,
131
+ warning,
132
+ };
133
+ }
134
+ if (ok.length === 0 && unverifiable.length > 0) {
135
+ return {
136
+ kind: "unverifiable-only",
137
+ exitCode: FED_VERIFY_EXIT_OK,
138
+ failedCount: 0,
139
+ unverifiableCount: unverifiable.length,
140
+ okCount: 0,
141
+ summary: "no reachable peer was verified wrong.",
142
+ warning,
143
+ };
144
+ }
145
+ return {
146
+ kind: "ok",
147
+ exitCode: FED_VERIFY_EXIT_OK,
148
+ failedCount: 0,
149
+ unverifiableCount: unverifiable.length,
150
+ okCount: ok.length,
151
+ summary: `PASS: memory propagated to all ${ok.length} checked peer(s).`,
152
+ warning,
153
+ };
154
+ }
155
+ export function decideFederationVerifyExitCode(results) {
156
+ return describeFederationVerify(results).exitCode;
157
+ }
158
+ export function renderFederationVerifyVerdict(verdict) {
159
+ const lines = [];
160
+ if (verdict.warning) {
161
+ lines.push(`── WARNING: ${verdict.warning} ──`);
162
+ }
163
+ if (verdict.kind === "diverged") {
164
+ lines.push(`── ${verdict.summary} ──`);
165
+ lines.push("Diagnostics to run next:");
166
+ lines.push(" flair federation status # confirm peers are paired + lastSyncAt is recent");
167
+ lines.push(" flair federation reachability # confirm peers are HTTP-reachable");
168
+ lines.push(" flair federation sync # push now (verify also does this itself)");
169
+ lines.push(" curl <peer-endpoint>/Health # raw probe");
170
+ }
171
+ else if (verdict.kind !== "empty") {
172
+ lines.push(`── ${verdict.summary} ──`);
173
+ }
174
+ return lines.join("\n");
175
+ }
176
+ function defaultClock() {
177
+ return {
178
+ now: () => Date.now(),
179
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
180
+ };
181
+ }
182
+ function canaryContainsTag(data, tag) {
183
+ const results = data?.results ?? [];
184
+ return results.some((r) => (r.content ?? "").includes(tag));
185
+ }
186
+ /** Credentials must travel even when there is no --target (local flag-only). */
187
+ export function verifyApiOpts(opts) {
188
+ const apiOpts = {};
189
+ if (opts.baseUrl)
190
+ apiOpts.baseUrl = opts.baseUrl;
191
+ if (opts.explicitAdminPass)
192
+ apiOpts.explicitAdminPass = opts.explicitAdminPass;
193
+ if (opts.adminUser)
194
+ apiOpts.adminUser = opts.adminUser;
195
+ return (apiOpts.baseUrl || apiOpts.explicitAdminPass || apiOpts.adminUser) ? apiOpts : undefined;
196
+ }
197
+ export async function runFederationVerify(opts, deps) {
198
+ const log = deps.log ?? (() => { });
199
+ const err = deps.error ?? (() => { });
200
+ const clock = deps.clock ?? defaultClock();
201
+ const freshnessMs = opts.freshnessMs ?? defaultFreshnessMs();
202
+ const probeTimeoutMs = opts.probeTimeoutMs ?? 5_000;
203
+ const pollIntervalMs = opts.pollIntervalMs ?? 5_000;
204
+ const apiOpts = verifyApiOpts(opts);
205
+ log(`── flair federation verify — ${new Date(clock.now()).toISOString()} ──`);
206
+ log(`Tag: ${opts.tag}, wait window: ${opts.waitSeconds}s`);
207
+ const memId = `${opts.agentId}-${clock.now()}-fed-verify`;
208
+ const writtenAt = new Date(clock.now()).toISOString();
209
+ try {
210
+ await deps.api("PUT", `/Memory/${encodeURIComponent(memId)}`, {
211
+ id: memId,
212
+ agentId: opts.agentId,
213
+ content: `${opts.tag} — federation verify probe written at ${writtenAt}`,
214
+ // #1257: ephemeral is private-only. Private never federates. standard
215
+ // + shared is the legal federable pair. type:session lets rem light
216
+ // archive leftovers after 30d if the post-probe archive push fails.
217
+ // Federation does not propagate DELETE, so cleanup archives + syncs
218
+ // before the local delete (SemanticSearch excludes archived:true).
219
+ type: "session",
220
+ durability: "standard",
221
+ visibility: "shared",
222
+ tags: ["federation-verify", opts.tag],
223
+ createdAt: writtenAt,
224
+ }, apiOpts);
225
+ log(`1. Wrote local memory: ${memId}`);
226
+ }
227
+ catch (e) {
228
+ const message = e instanceof Error ? e.message : String(e);
229
+ err(`1. Local write FAILED: ${message}`);
230
+ return {
231
+ exitCode: FED_VERIFY_EXIT_DIVERGED,
232
+ verdict: {
233
+ kind: "diverged",
234
+ exitCode: FED_VERIFY_EXIT_DIVERGED,
235
+ failedCount: 0,
236
+ unverifiableCount: 0,
237
+ okCount: 0,
238
+ summary: `local write failed: ${message}`,
239
+ warning: null,
240
+ },
241
+ peers: [],
242
+ skippedRevoked: [],
243
+ memId: null,
244
+ cleanedUp: true,
245
+ pushed: false,
246
+ tag: opts.tag,
247
+ };
248
+ }
249
+ let pushed = false;
250
+ let cleanedUp = false;
251
+ let result = {
252
+ exitCode: FED_VERIFY_EXIT_OK,
253
+ verdict: describeFederationVerify([]),
254
+ peers: [],
255
+ skippedRevoked: [],
256
+ memId,
257
+ cleanedUp: false,
258
+ pushed: false,
259
+ tag: opts.tag,
260
+ };
261
+ try {
262
+ log(" Pushing canary via federation sync…");
263
+ const syncResult = await deps.syncOnce(opts.syncOpts);
264
+ if (syncResult.error) {
265
+ log(` Push: FAILED (${syncResult.error.message}) — probing with lastSyncAt freshness`);
266
+ }
267
+ else if (syncResult.pushed > 0) {
268
+ // A no-change sync ({ pushed: 0 }) did not inject the canary — do not
269
+ // treat that as a successful push or a missing-canary probe becomes
270
+ // a false FAIL.
271
+ pushed = true;
272
+ log(` Push: synced ${syncResult.pushed} record(s) (${syncResult.skipped} skipped)`);
273
+ }
274
+ else {
275
+ log(` Push: synced 0 records — canary was not injected; probing with lastSyncAt freshness`);
276
+ }
277
+ let listed = [];
278
+ try {
279
+ const r = await deps.api("GET", "/FederationPeers", undefined, apiOpts);
280
+ listed = r.peers ?? [];
281
+ }
282
+ catch (e) {
283
+ const message = e instanceof Error ? e.message : String(e);
284
+ err(`Failed to list peers: ${message}`);
285
+ const peers = [{
286
+ id: "(peer enumeration)",
287
+ status: "unverifiable",
288
+ detail: `GET /FederationPeers failed: ${message} — could not check any peer`,
289
+ lastSyncAt: null,
290
+ authenticated: false,
291
+ }];
292
+ const verdict = describeFederationVerify(peers);
293
+ log(renderFederationVerifyVerdict(verdict));
294
+ result = {
295
+ exitCode: verdict.exitCode,
296
+ verdict,
297
+ peers,
298
+ skippedRevoked: [],
299
+ memId,
300
+ cleanedUp: false,
301
+ pushed,
302
+ tag: opts.tag,
303
+ };
304
+ return result;
305
+ }
306
+ const selected = selectPeersToProbe(listed, opts.peerId);
307
+ const skippedRevoked = selected.skippedRevoked;
308
+ const toProbe = selected.probe;
309
+ const revokedRows = skippedRevoked.map(revokedAsUnverifiable);
310
+ for (const row of revokedRows) {
311
+ log(` ${row.id} UNVERIFIABLE (${row.detail})`);
312
+ }
313
+ if (toProbe.length === 0) {
314
+ if (revokedRows.length === 0)
315
+ log("(no peers to probe)");
316
+ const verdict = describeFederationVerify(revokedRows);
317
+ log(renderFederationVerifyVerdict(verdict));
318
+ result = {
319
+ exitCode: verdict.exitCode,
320
+ verdict,
321
+ peers: revokedRows,
322
+ skippedRevoked,
323
+ memId,
324
+ cleanedUp: false,
325
+ pushed,
326
+ tag: opts.tag,
327
+ };
328
+ return result;
329
+ }
330
+ log(`2. Probing ${toProbe.length} peer(s) over ${opts.waitSeconds}s window…`);
331
+ const started = clock.now();
332
+ const settled = new Map();
333
+ const sawAuthenticatedMissing = new Set();
334
+ const settle = (row) => {
335
+ settled.set(row.id, row);
336
+ const label = row.status === "ok" ? "OK" : row.status === "fail" ? "FAIL" : "UNVERIFIABLE";
337
+ log(` ${row.id} ${label} (${row.detail})`);
338
+ };
339
+ while (clock.now() - started < opts.waitMs && settled.size < toProbe.length) {
340
+ for (const p of toProbe) {
341
+ if (settled.has(p.id))
342
+ continue;
343
+ const endpointCheck = validatePeerEndpoint(p.endpoint);
344
+ if ("error" in endpointCheck) {
345
+ settle({
346
+ id: p.id,
347
+ status: "unverifiable",
348
+ detail: endpointCheck.error,
349
+ lastSyncAt: p.lastSyncAt ?? null,
350
+ authenticated: false,
351
+ });
352
+ continue;
353
+ }
354
+ let probeUrl;
355
+ try {
356
+ probeUrl = new URL("/SemanticSearch", endpointCheck.url);
357
+ }
358
+ catch {
359
+ settle({
360
+ id: p.id,
361
+ status: "unverifiable",
362
+ detail: `invalid endpoint URL: ${p.endpoint}`,
363
+ lastSyncAt: p.lastSyncAt ?? null,
364
+ authenticated: false,
365
+ });
366
+ continue;
367
+ }
368
+ try {
369
+ const res = await deps.fetch(probeUrl, {
370
+ method: "POST",
371
+ headers: { "Content-Type": "application/json" },
372
+ body: JSON.stringify({ q: opts.tag, limit: 5 }),
373
+ signal: AbortSignal.timeout(probeTimeoutMs),
374
+ });
375
+ const kind = classifyProbeHttpStatus(res.status);
376
+ if (kind === "auth") {
377
+ settle({
378
+ id: p.id,
379
+ status: "unverifiable",
380
+ detail: authUnverifiableDetail(res.status),
381
+ lastSyncAt: p.lastSyncAt ?? null,
382
+ authenticated: false,
383
+ });
384
+ continue;
385
+ }
386
+ if (kind !== "found-ok") {
387
+ // Retry 4xx/5xx other than 401/403 — couldn't complete this tick.
388
+ continue;
389
+ }
390
+ const data = await res.json().catch(() => ({}));
391
+ if (canaryContainsTag(data, opts.tag)) {
392
+ const elapsed = Math.floor((clock.now() - started) / 1000);
393
+ settle({
394
+ id: p.id,
395
+ status: "ok",
396
+ detail: `memory found after ${elapsed}s`,
397
+ lastSyncAt: p.lastSyncAt ?? null,
398
+ authenticated: true,
399
+ });
400
+ }
401
+ else {
402
+ sawAuthenticatedMissing.add(p.id);
403
+ }
404
+ }
405
+ catch {
406
+ // Unreachable this tick — retry until the window elapses.
407
+ }
408
+ }
409
+ if (settled.size < toProbe.length) {
410
+ await clock.sleep(pollIntervalMs);
411
+ }
412
+ }
413
+ for (const p of toProbe) {
414
+ if (settled.has(p.id))
415
+ continue;
416
+ if (sawAuthenticatedMissing.has(p.id)) {
417
+ const classified = classifyMissingAfterWindow({
418
+ pushed,
419
+ lastSyncAt: p.lastSyncAt,
420
+ nowMs: clock.now(),
421
+ freshnessMs,
422
+ waitSeconds: opts.waitSeconds,
423
+ });
424
+ settle({
425
+ id: p.id,
426
+ status: classified.status,
427
+ detail: classified.detail,
428
+ lastSyncAt: p.lastSyncAt ?? null,
429
+ authenticated: true,
430
+ });
431
+ }
432
+ else {
433
+ settle({
434
+ id: p.id,
435
+ status: "unverifiable",
436
+ detail: `unreachable — no successful probe within ${opts.waitSeconds}s (couldn't check, not a sync failure)`,
437
+ lastSyncAt: p.lastSyncAt ?? null,
438
+ authenticated: false,
439
+ });
440
+ }
441
+ }
442
+ const peers = [...revokedRows, ...toProbe.map((p) => settled.get(p.id))];
443
+ const verdict = describeFederationVerify(peers);
444
+ log(renderFederationVerifyVerdict(verdict));
445
+ result = {
446
+ exitCode: verdict.exitCode,
447
+ verdict,
448
+ peers,
449
+ skippedRevoked,
450
+ memId,
451
+ cleanedUp: false,
452
+ pushed,
453
+ tag: opts.tag,
454
+ };
455
+ return result;
456
+ }
457
+ finally {
458
+ // Federation merge is LWW put-by-id — local DELETE never reaches the
459
+ // hub. If we injected the canary, archive it and push that update so
460
+ // peer SemanticSearch (archived not_equal true) stops returning it.
461
+ if (pushed) {
462
+ try {
463
+ await deps.api("PUT", `/Memory/${encodeURIComponent(memId)}`, {
464
+ id: memId,
465
+ agentId: opts.agentId,
466
+ content: `${opts.tag} — federation verify probe written at ${writtenAt}`,
467
+ type: "session",
468
+ durability: "standard",
469
+ visibility: "shared",
470
+ tags: ["federation-verify", opts.tag],
471
+ createdAt: writtenAt,
472
+ archived: true,
473
+ archivedAt: new Date(clock.now()).toISOString(),
474
+ }, apiOpts);
475
+ const archiveSync = await deps.syncOnce(opts.syncOpts);
476
+ if (archiveSync.error) {
477
+ log(`4. Cleanup: archive push FAILED (${archiveSync.error.message}) — canary may linger on peers`);
478
+ }
479
+ else {
480
+ log(`4. Cleanup: pushed archive of ${memId} (peers drop it from search)`);
481
+ }
482
+ }
483
+ catch (e) {
484
+ const message = e instanceof Error ? e.message : String(e);
485
+ log(`4. Cleanup: could not archive canary for peer cleanup (${message})`);
486
+ }
487
+ }
488
+ try {
489
+ await deps.api("DELETE", `/Memory/${encodeURIComponent(memId)}`, undefined, apiOpts);
490
+ cleanedUp = true;
491
+ log(`4. Cleanup: deleted local memory ${memId}`);
492
+ }
493
+ catch {
494
+ log(`4. Cleanup: could NOT delete local memory ${memId} (manual cleanup needed)`);
495
+ }
496
+ result.cleanedUp = cleanedUp;
497
+ }
498
+ }