@tpsdev-ai/flair 0.52.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.
- package/README.md +4 -1
- package/dist/build-info.json +3 -3
- package/dist/cli.js +1914 -15328
- 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 +1654 -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 +914 -0
- package/dist/commands/test.js +93 -0
- package/dist/commands/uninstall.js +143 -0
- package/dist/commands/upgrade.js +1592 -0
- package/dist/commands/workspace.js +114 -0
- package/dist/deploy.js +24 -0
- package/dist/fabric-npm-install.js +87 -0
- 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 +199 -0
- package/dist/lib/ops-api-bind.js +115 -0
- package/dist/lib/owned-pins.js +219 -0
- package/dist/lib/stabilize-mqtt-network.js +123 -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 +43 -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/stamp-migration-verify.js +163 -0
- package/dist/stamp-outstanding.js +144 -0
- 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 +15 -7
- package/docs/spoke-bringup.md +10 -5
- package/docs/standalone-local.md +3 -1
- package/docs/upgrade.md +25 -6
- package/node_modules/@tpsdev-ai/flair-tool-descriptors/LICENSE +19 -0
- package/node_modules/@tpsdev-ai/flair-tool-descriptors/README.md +22 -0
- package/node_modules/@tpsdev-ai/flair-tool-descriptors/dist/index.d.ts +70 -0
- package/node_modules/@tpsdev-ai/flair-tool-descriptors/dist/index.js +665 -0
- package/node_modules/@tpsdev-ai/flair-tool-descriptors/package.json +46 -0
- package/package.json +9 -4
- package/schemas/agent.graphql +15 -0
- package/templates/launchd/start-flair-with-admin-pass.sh +73 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* uninstall-purge.ts — flair#853
|
|
3
|
+
*
|
|
4
|
+
* `flair uninstall --purge` used to delete only `~/.flair/data` and
|
|
5
|
+
* `~/.flair/keys`, then print "Flair fully purged". That left the admin
|
|
6
|
+
* password, backups/logs/upgrade-snapshots, the REM nightly shim, client
|
|
7
|
+
* MCP/hook wiring, and the systemd/launchd scheduler units — a secret and
|
|
8
|
+
* a job that can resurrect on reinstall.
|
|
9
|
+
*
|
|
10
|
+
* This module is the purge body. The CLI stops the Harper service, then
|
|
11
|
+
* calls `purgeFlairInstall`. Docs say purge removes everything including
|
|
12
|
+
* data and keys; this does that, plus secrets, schedulers, and client
|
|
13
|
+
* wiring. The npm package is an intentional leftover: this CLI cannot
|
|
14
|
+
* uninstall itself. The report always names leftovers instead of claiming
|
|
15
|
+
* "fully purged" when anything remains.
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, rmSync } from "node:fs";
|
|
18
|
+
import { homedir } from "node:os";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import { ALL_CLIENTS } from "../install/clients.js";
|
|
21
|
+
import { uninstallContinuityHooks, uninstallHook, harnessSupportsContinuity, hookBackupPath, hookSettingsPath, SUPPORTED_HARNESSES, } from "../hook-install.js";
|
|
22
|
+
import { disableScheduler as disableRemScheduler, LAUNCHD_LABEL as REM_LAUNCHD_LABEL, SYSTEMD_SERVICE_UNIT as REM_SYSTEMD_SERVICE, SYSTEMD_TIMER_UNIT as REM_SYSTEMD_TIMER, } from "../rem/scheduler.js";
|
|
23
|
+
import { disableScheduler as disableFederationScheduler, LAUNCHD_LABEL as FED_LAUNCHD_LABEL, SYSTEMD_SERVICE_UNIT as FED_SYSTEMD_SERVICE, SYSTEMD_TIMER_UNIT as FED_SYSTEMD_TIMER, } from "../federation/scheduler.js";
|
|
24
|
+
/** The published CLI package. Named here so the leftover line cannot drift
|
|
25
|
+
* from the remedy command. */
|
|
26
|
+
export const FLAIR_NPM_PACKAGE = "@tpsdev-ai/flair";
|
|
27
|
+
function resolveHome(homeDir) {
|
|
28
|
+
return homeDir ?? process.env.HOME ?? process.env.USERPROFILE ?? homedir();
|
|
29
|
+
}
|
|
30
|
+
function withHome(homeDir, fn) {
|
|
31
|
+
const prev = process.env.HOME;
|
|
32
|
+
process.env.HOME = homeDir;
|
|
33
|
+
try {
|
|
34
|
+
return fn();
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
if (prev === undefined)
|
|
38
|
+
delete process.env.HOME;
|
|
39
|
+
else
|
|
40
|
+
process.env.HOME = prev;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function displayUnderHome(homeDir, absPath) {
|
|
44
|
+
if (absPath.startsWith(homeDir))
|
|
45
|
+
return "~" + absPath.slice(homeDir.length);
|
|
46
|
+
return absPath;
|
|
47
|
+
}
|
|
48
|
+
function rmPath(absPath, removed, leftovers, display) {
|
|
49
|
+
if (!existsSync(absPath))
|
|
50
|
+
return;
|
|
51
|
+
try {
|
|
52
|
+
rmSync(absPath, { recursive: true, force: true });
|
|
53
|
+
removed.push(display);
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
57
|
+
leftovers.push({
|
|
58
|
+
path: display,
|
|
59
|
+
kind: "failed",
|
|
60
|
+
reason: `could not remove: ${reason}`,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function schedulerPaths(homeDir, spec) {
|
|
65
|
+
return [
|
|
66
|
+
join(homeDir, ".flair", "bin", spec.shimName),
|
|
67
|
+
join(homeDir, "Library", "LaunchAgents", `${spec.launchdLabel}.plist`),
|
|
68
|
+
join(homeDir, ".config", "systemd", "user", spec.systemdTimer),
|
|
69
|
+
join(homeDir, ".config", "systemd", "user", spec.systemdService),
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
function disableAndSweepScheduler(homeDir, skipUnload, kind, removed, leftovers) {
|
|
73
|
+
const spec = kind === "rem"
|
|
74
|
+
? {
|
|
75
|
+
shimName: "flair-rem-nightly",
|
|
76
|
+
launchdLabel: REM_LAUNCHD_LABEL,
|
|
77
|
+
systemdTimer: REM_SYSTEMD_TIMER,
|
|
78
|
+
systemdService: REM_SYSTEMD_SERVICE,
|
|
79
|
+
disable: disableRemScheduler,
|
|
80
|
+
}
|
|
81
|
+
: {
|
|
82
|
+
shimName: "flair-federation-sync",
|
|
83
|
+
launchdLabel: FED_LAUNCHD_LABEL,
|
|
84
|
+
systemdTimer: FED_SYSTEMD_TIMER,
|
|
85
|
+
systemdService: FED_SYSTEMD_SERVICE,
|
|
86
|
+
disable: disableFederationScheduler,
|
|
87
|
+
};
|
|
88
|
+
const paths = schedulerPaths(homeDir, spec);
|
|
89
|
+
try {
|
|
90
|
+
spec.disable({
|
|
91
|
+
skipUnload,
|
|
92
|
+
removeShim: true,
|
|
93
|
+
shimPathOverride: paths[0],
|
|
94
|
+
launchdPlistOverride: paths[1],
|
|
95
|
+
systemdTimerOverride: paths[2],
|
|
96
|
+
systemdServiceOverride: paths[3],
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// File sweep below still runs — unload failure must not keep units on disk.
|
|
101
|
+
}
|
|
102
|
+
for (const abs of paths) {
|
|
103
|
+
rmPath(abs, removed, leftovers, displayUnderHome(homeDir, abs));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* `uninstallHook` / `uninstallContinuityHooks` copy the settings file to a
|
|
108
|
+
* sibling `.bak` whenever it exists — including on a no-op. Purge must not
|
|
109
|
+
* leave that copy (or a pre-existing `.bak` it just overwrote). flair#853
|
|
110
|
+
* Bugbot: only deleting the bak after an actual remove left
|
|
111
|
+
* `~/.claude/settings.json.bak` / `~/.codex/hooks.json.bak` behind.
|
|
112
|
+
*/
|
|
113
|
+
function sweepHookBackup(settingsPath, reportedBackup, homeDir, removed, leftovers) {
|
|
114
|
+
const candidates = new Set();
|
|
115
|
+
if (reportedBackup)
|
|
116
|
+
candidates.add(reportedBackup);
|
|
117
|
+
candidates.add(hookBackupPath(settingsPath));
|
|
118
|
+
for (const bak of candidates) {
|
|
119
|
+
rmPath(bak, removed, leftovers, displayUnderHome(homeDir, bak));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Remove Flair-owned state under `homeDir`: the `~/.flair` tree (data, keys,
|
|
124
|
+
* admin-pass, backups, logs, snapshots, shims), REM + federation scheduler
|
|
125
|
+
* units on both platforms, SessionStart/continuity hooks, and MCP/native
|
|
126
|
+
* client wiring. The npm package is listed as an intentional leftover
|
|
127
|
+
* unless `omitNpmLeftover` is set.
|
|
128
|
+
*/
|
|
129
|
+
export function purgeFlairInstall(opts = {}) {
|
|
130
|
+
const homeDir = resolveHome(opts.homeDir);
|
|
131
|
+
const skipUnload = !!opts.skipSchedulerUnload;
|
|
132
|
+
const removed = [];
|
|
133
|
+
const leftovers = [];
|
|
134
|
+
disableAndSweepScheduler(homeDir, skipUnload, "rem", removed, leftovers);
|
|
135
|
+
disableAndSweepScheduler(homeDir, skipUnload, "federation", removed, leftovers);
|
|
136
|
+
for (const harness of SUPPORTED_HARNESSES) {
|
|
137
|
+
const hook = uninstallHook({ homeDir, harness });
|
|
138
|
+
if (!hook.ok) {
|
|
139
|
+
leftovers.push({ path: hook.path, kind: "failed", reason: hook.message });
|
|
140
|
+
}
|
|
141
|
+
else if (hook.delta && hook.delta.action !== "noop") {
|
|
142
|
+
removed.push(displayUnderHome(homeDir, hook.path) + " (SessionStart hook)");
|
|
143
|
+
}
|
|
144
|
+
sweepHookBackup(hook.path, hook.backupPath, homeDir, removed, leftovers);
|
|
145
|
+
if (!harnessSupportsContinuity(harness))
|
|
146
|
+
continue;
|
|
147
|
+
const continuity = uninstallContinuityHooks({ homeDir, harness });
|
|
148
|
+
if (!continuity.ok) {
|
|
149
|
+
leftovers.push({ path: continuity.path, kind: "failed", reason: continuity.message });
|
|
150
|
+
}
|
|
151
|
+
else if (continuity.actions && (continuity.actions.PostToolUse === "remove" || continuity.actions.Stop === "remove")) {
|
|
152
|
+
removed.push(displayUnderHome(homeDir, continuity.path) + " (continuity hooks)");
|
|
153
|
+
}
|
|
154
|
+
sweepHookBackup(continuity.path, continuity.backupPath, homeDir, removed, leftovers);
|
|
155
|
+
}
|
|
156
|
+
// Pre-existing sibling backups (or a harness we skipped) must not survive.
|
|
157
|
+
for (const harness of SUPPORTED_HARNESSES) {
|
|
158
|
+
sweepHookBackup(hookSettingsPath(homeDir, harness), null, homeDir, removed, leftovers);
|
|
159
|
+
}
|
|
160
|
+
const unwireResults = withHome(homeDir, () => ALL_CLIENTS.map((c) => c.unwire()));
|
|
161
|
+
for (const u of unwireResults) {
|
|
162
|
+
if (!u.ok) {
|
|
163
|
+
leftovers.push({ path: u.message, kind: "failed", reason: u.message });
|
|
164
|
+
}
|
|
165
|
+
else if (u.removed) {
|
|
166
|
+
removed.push(u.message);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const flairDir = join(homeDir, ".flair");
|
|
170
|
+
rmPath(flairDir, removed, leftovers, "~/.flair");
|
|
171
|
+
if (existsSync(flairDir)) {
|
|
172
|
+
leftovers.push({
|
|
173
|
+
path: "~/.flair",
|
|
174
|
+
kind: "failed",
|
|
175
|
+
reason: "directory still present after purge",
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
if (!opts.omitNpmLeftover) {
|
|
179
|
+
leftovers.push({
|
|
180
|
+
path: FLAIR_NPM_PACKAGE,
|
|
181
|
+
kind: "intentional",
|
|
182
|
+
reason: "this CLI cannot uninstall itself",
|
|
183
|
+
remedy: `npm uninstall -g ${FLAIR_NPM_PACKAGE}`,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return { removed, leftovers };
|
|
187
|
+
}
|
|
188
|
+
export function formatPurgeReport(result) {
|
|
189
|
+
const lines = [];
|
|
190
|
+
for (const item of result.removed) {
|
|
191
|
+
lines.push(`✅ Removed: ${item}`);
|
|
192
|
+
}
|
|
193
|
+
const failed = result.leftovers.filter((l) => l.kind === "failed");
|
|
194
|
+
const intentional = result.leftovers.filter((l) => l.kind === "intentional");
|
|
195
|
+
for (const item of failed) {
|
|
196
|
+
lines.push(`⚠️ Left (failed): ${item.path} — ${item.reason}`);
|
|
197
|
+
}
|
|
198
|
+
if (failed.length === 0 && intentional.length === 0) {
|
|
199
|
+
lines.push("");
|
|
200
|
+
lines.push("🗑️ Flair fully purged");
|
|
201
|
+
return { lines, claimedFullyPurged: true };
|
|
202
|
+
}
|
|
203
|
+
lines.push("");
|
|
204
|
+
lines.push("🗑️ Flair purged (data, keys, secrets, schedulers, and client wiring)");
|
|
205
|
+
if (intentional.length > 0) {
|
|
206
|
+
lines.push("");
|
|
207
|
+
lines.push("Intentionally left:");
|
|
208
|
+
for (const item of intentional) {
|
|
209
|
+
lines.push(` • ${item.path} — ${item.reason}`);
|
|
210
|
+
if (item.remedy)
|
|
211
|
+
lines.push(` Remove with: ${item.remedy}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return { lines, claimedFullyPurged: false };
|
|
215
|
+
}
|
|
216
|
+
export function purgeHadFailures(result) {
|
|
217
|
+
return result.leftovers.some((l) => l.kind === "failed");
|
|
218
|
+
}
|
package/dist/rem/restore.js
CHANGED
|
@@ -41,14 +41,12 @@ function asArray(raw) {
|
|
|
41
41
|
function soulWrite(opts) {
|
|
42
42
|
return opts.soulApiCall ?? opts.apiCall;
|
|
43
43
|
}
|
|
44
|
-
async function listAgentCandidates(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
get_attributes: ["id", "claim"],
|
|
51
|
-
}));
|
|
44
|
+
async function listAgentCandidates(opsSearch, agentId) {
|
|
45
|
+
if (!opsSearch)
|
|
46
|
+
return [];
|
|
47
|
+
return asArray(await opsSearch("MemoryCandidate", [
|
|
48
|
+
{ search_attribute: "agentId", search_type: "equals", search_value: agentId },
|
|
49
|
+
], ["id", "claim"]));
|
|
52
50
|
}
|
|
53
51
|
function parseJsonlSafe(text) {
|
|
54
52
|
if (!text.trim())
|
|
@@ -142,7 +140,7 @@ export async function applySnapshot(opts) {
|
|
|
142
140
|
try {
|
|
143
141
|
const currentMem = asArray(await opts.apiCall("GET", `/Memory?agentId=${encodeURIComponent(opts.agentId)}`));
|
|
144
142
|
const currentSouls = asArray(await opts.apiCall("GET", `/Soul?agentId=${encodeURIComponent(opts.agentId)}`));
|
|
145
|
-
const currentCandidates = await listAgentCandidates(opts.
|
|
143
|
+
const currentCandidates = await listAgentCandidates(opts.opsSearch, opts.agentId);
|
|
146
144
|
result.deleted.memories = currentMem.length;
|
|
147
145
|
result.deleted.souls = currentSouls.length;
|
|
148
146
|
result.deleted.candidates = currentCandidates.length;
|
|
@@ -186,7 +184,7 @@ export async function applySnapshot(opts) {
|
|
|
186
184
|
// so a leftover row 403s Soul PUT after operator auth succeeds.
|
|
187
185
|
let currentCandidates = [];
|
|
188
186
|
try {
|
|
189
|
-
currentCandidates = await listAgentCandidates(opts.
|
|
187
|
+
currentCandidates = await listAgentCandidates(opts.opsSearch, opts.agentId);
|
|
190
188
|
}
|
|
191
189
|
catch (err) {
|
|
192
190
|
errors.push(`fetch-candidates: ${err?.message ?? String(err)}`);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentReadPosition.ts — HTTP surface for the per-agent read-position primitive.
|
|
3
|
+
*
|
|
4
|
+
* GET /AgentReadPosition/{agentId}?stream=org-event
|
|
5
|
+
* POST /AgentReadPosition/{agentId} { stream, position }
|
|
6
|
+
*
|
|
7
|
+
* Owner-scoped: an agent reads/advances only its own watermark. Admins and
|
|
8
|
+
* internal calls may act on any agent. The table itself is not @export.
|
|
9
|
+
*/
|
|
10
|
+
import { Resource } from "harper";
|
|
11
|
+
import { allowVerified, resolveAgentAuth } from "./agent-auth.js";
|
|
12
|
+
import { ORG_EVENT_STREAM } from "./agent-read-position-lib.js";
|
|
13
|
+
import { advanceReadPosition, defaultReadPositionTable, getReadPosition, } from "./agent-read-position.js";
|
|
14
|
+
function pathAgentId(pathInfo, resource) {
|
|
15
|
+
return ((typeof pathInfo === "object" && pathInfo !== null ? pathInfo.id : null) ??
|
|
16
|
+
(typeof pathInfo === "string" ? pathInfo : null) ??
|
|
17
|
+
resource?.getId?.() ??
|
|
18
|
+
null);
|
|
19
|
+
}
|
|
20
|
+
function queryValue(pathInfo, name) {
|
|
21
|
+
if (typeof pathInfo !== "object" || pathInfo === null)
|
|
22
|
+
return null;
|
|
23
|
+
return pathInfo.conditions?.find((c) => c.attribute === name)?.value ?? null;
|
|
24
|
+
}
|
|
25
|
+
function denyOwner() {
|
|
26
|
+
return new Response(JSON.stringify({ error: "forbidden: can only read or advance your own watermark" }), { status: 403, headers: { "Content-Type": "application/json" } });
|
|
27
|
+
}
|
|
28
|
+
function ownerDenied(auth, agentId) {
|
|
29
|
+
if (auth.kind === "anonymous")
|
|
30
|
+
return true;
|
|
31
|
+
if (auth.kind === "agent" && !auth.isAdmin && auth.agentId !== agentId)
|
|
32
|
+
return true;
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
export class AgentReadPosition extends Resource {
|
|
36
|
+
async allowRead() {
|
|
37
|
+
return allowVerified(this.getContext?.());
|
|
38
|
+
}
|
|
39
|
+
async allowCreate() {
|
|
40
|
+
return allowVerified(this.getContext?.());
|
|
41
|
+
}
|
|
42
|
+
async get(pathInfo) {
|
|
43
|
+
const auth = await resolveAgentAuth(this.getContext?.());
|
|
44
|
+
const agentId = pathAgentId(pathInfo, this);
|
|
45
|
+
if (!agentId) {
|
|
46
|
+
return new Response(JSON.stringify({ error: "agentId required in path: GET /AgentReadPosition/{agentId}" }), { status: 400, headers: { "Content-Type": "application/json" } });
|
|
47
|
+
}
|
|
48
|
+
if (ownerDenied(auth, agentId))
|
|
49
|
+
return denyOwner();
|
|
50
|
+
const stream = queryValue(pathInfo, "stream") || ORG_EVENT_STREAM;
|
|
51
|
+
const ctx = this.getContext?.();
|
|
52
|
+
const position = await getReadPosition(defaultReadPositionTable(), agentId, stream, ctx);
|
|
53
|
+
return { agentId, stream, position };
|
|
54
|
+
}
|
|
55
|
+
async post(content, pathInfo) {
|
|
56
|
+
const auth = await resolveAgentAuth(this.getContext?.());
|
|
57
|
+
const agentId = pathAgentId(pathInfo, this) ??
|
|
58
|
+
(content && typeof content === "object" ? content.agentId : null) ??
|
|
59
|
+
null;
|
|
60
|
+
if (!agentId) {
|
|
61
|
+
return new Response(JSON.stringify({ error: "agentId required in path: POST /AgentReadPosition/{agentId}" }), { status: 400, headers: { "Content-Type": "application/json" } });
|
|
62
|
+
}
|
|
63
|
+
if (ownerDenied(auth, agentId))
|
|
64
|
+
return denyOwner();
|
|
65
|
+
const stream = (content && typeof content === "object" && content.stream) || ORG_EVENT_STREAM;
|
|
66
|
+
const position = content && typeof content === "object" ? String(content.position ?? "") : "";
|
|
67
|
+
if (!position) {
|
|
68
|
+
return new Response(JSON.stringify({ error: "position required to advance watermark" }), { status: 400, headers: { "Content-Type": "application/json" } });
|
|
69
|
+
}
|
|
70
|
+
const ctx = this.getContext?.();
|
|
71
|
+
const result = await advanceReadPosition(defaultReadPositionTable(), agentId, stream, position, ctx);
|
|
72
|
+
return { agentId, stream, ...result };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -109,7 +109,9 @@ export function noteFederationMergedMemory(recordTable, mergedData) {
|
|
|
109
109
|
* AdminInstance.ts/AdminDashboard.ts: this is an admin-view endpoint (peers
|
|
110
110
|
* never call it during pairing — FederationPair.post() reads the Instance
|
|
111
111
|
* table directly server-side to hand a peer our identity; this HTTP GET is
|
|
112
|
-
* CLI tooling only).
|
|
112
|
+
* CLI tooling only). The spoke pair CLI must not GET this path to fill a
|
|
113
|
+
* missing pair publicKey (flair#822): allowRead is allowAdmin, and get()
|
|
114
|
+
* find-or-creates an Instance (that would invent a hub row; #839).
|
|
113
115
|
*/
|
|
114
116
|
export class FederationInstance extends Resource {
|
|
115
117
|
async allowRead() {
|
|
@@ -325,7 +327,11 @@ export class FederationPair extends Resource {
|
|
|
325
327
|
console.warn(`[federation] drop_user failed: ${err?.message} — cleanup sweep will retry`);
|
|
326
328
|
}
|
|
327
329
|
}
|
|
328
|
-
// Return our own identity for the peer to record
|
|
330
|
+
// Return our own identity for the peer to record. Already
|
|
331
|
+
// `{ id, publicKey, role }` when an Instance row exists (flair#213).
|
|
332
|
+
// `instance: null` means the hub has no FederationInstance — that is
|
|
333
|
+
// #839, not a pair-response-shape bug. The spoke must ERROR, never
|
|
334
|
+
// store publicKey:"" (flair#822).
|
|
329
335
|
let ourInstance = null;
|
|
330
336
|
try {
|
|
331
337
|
for await (const i of databases.flair.Instance.search()) {
|
package/dist/resources/Memory.js
CHANGED
|
@@ -269,9 +269,10 @@ async function runDedupGate(ctx, content) {
|
|
|
269
269
|
// miss a near-duplicate during that window. Bounded (the re-embed pass is
|
|
270
270
|
// batched and finishes in minutes), self-healing once the pass completes,
|
|
271
271
|
// and a missed dedup is a duplicate row, not data loss — quality, not
|
|
272
|
-
// correctness.
|
|
273
|
-
//
|
|
274
|
-
//
|
|
272
|
+
// correctness. flair#1073: if that window lasts days (boot cycle marked
|
|
273
|
+
// embedding-stamp complete without converging), /HealthDetail names the
|
|
274
|
+
// outstanding migration and that duplicate detection is inactive — do
|
|
275
|
+
// not treat a long-lived split as this documented transient.
|
|
275
276
|
let embedding = Array.isArray(content.embedding) ? content.embedding : null;
|
|
276
277
|
if (!embedding) {
|
|
277
278
|
try {
|
|
@@ -19,6 +19,9 @@ import { hybridEnabled } from "./bm25.js";
|
|
|
19
19
|
import { buildTrustBlock } from "./trust-block.js";
|
|
20
20
|
import { bestSemanticSimilarity, evaluateAbstention } from "./abstention.js";
|
|
21
21
|
import { estimateTokens } from "./token-estimate.js";
|
|
22
|
+
import { initialPosition, ORG_EVENT_STREAM } from "./agent-read-position-lib.js";
|
|
23
|
+
import { defaultReadPositionTable, ensureReadPosition } from "./agent-read-position.js";
|
|
24
|
+
import { catchupSeekTimestamp, isCatchupEligible } from "./org-event-catchup-lib.js";
|
|
22
25
|
/**
|
|
23
26
|
* POST /MemoryBootstrap
|
|
24
27
|
*
|
|
@@ -188,11 +191,11 @@ function buildTrustEntry(m, section) {
|
|
|
188
191
|
: undefined;
|
|
189
192
|
return { id: m.id, section, ...block, ...(matchQualityNote ? { matchQualityNote } : {}) };
|
|
190
193
|
}
|
|
191
|
-
// flair#1199/#1206 — the default cap on how many org events bootstrap
|
|
192
|
-
// Overridable per-request via `maxEvents`.
|
|
193
|
-
// #
|
|
194
|
-
//
|
|
195
|
-
//
|
|
194
|
+
// flair#1199/#1206 — the default DISPLAY cap on how many org events bootstrap
|
|
195
|
+
// ships. Overridable per-request via `maxEvents`. This is no longer a silent
|
|
196
|
+
// drop (flair#931): leftover events set eventsHasMore / eventsRemaining so the
|
|
197
|
+
// caller pages /OrgEventCatchup. Event slots are scarce AND (as of #1199)
|
|
198
|
+
// token-charged; the shared tokenBudget is the harder ceiling.
|
|
196
199
|
const MAX_ORG_EVENTS = 10;
|
|
197
200
|
// ─── Bootstrap scale fix (flair-bootstrap-scale-fix) tunables ───────────────
|
|
198
201
|
//
|
|
@@ -440,6 +443,12 @@ export class BootstrapMemories extends Resource {
|
|
|
440
443
|
// skipped events — never a gap derived from some larger tally, which
|
|
441
444
|
// could imply the existence of rows the caller was not allowed to see.
|
|
442
445
|
let eventsBudgetTruncated = 0;
|
|
446
|
+
// flair#931 — watermark path. Always present on the response so a
|
|
447
|
+
// display cap (maxEvents / budget) is never a silent drop: the agent
|
|
448
|
+
// pages /OrgEventCatchup until drained, then acks.
|
|
449
|
+
let eventWatermark = null;
|
|
450
|
+
let eventsHasMore = false;
|
|
451
|
+
let eventsRemaining = 0;
|
|
443
452
|
const leanMemory = (m, section) => ({
|
|
444
453
|
id: m.id,
|
|
445
454
|
content: m.content,
|
|
@@ -1293,28 +1302,27 @@ export class BootstrapMemories extends Resource {
|
|
|
1293
1302
|
// dependency — WorkspaceState/OrgEvent/Presence being briefly
|
|
1294
1303
|
// unavailable must not break bootstrap's core memory context.
|
|
1295
1304
|
}
|
|
1296
|
-
// --- 5.
|
|
1305
|
+
// --- 5. OrgEvents for this agent (per-agent watermark, flair#931) ---
|
|
1297
1306
|
try {
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1307
|
+
// Optional lastBootAt remains a createdAt floor (legacy / probe), never
|
|
1308
|
+
// the primary cursor. Default path is the durable watermark: "now" on
|
|
1309
|
+
// first seen (or FLAIR_CATCHUP_BACKFILL_MS), then position > watermark.
|
|
1310
|
+
// The old 24h window + silent slice(0, 10) is retired as the primary path.
|
|
1311
|
+
const sinceFloor = data?.lastBootAt ? new Date(data.lastBootAt).toISOString() : null;
|
|
1312
|
+
eventWatermark = await ensureReadPosition(defaultReadPositionTable(), agentId, ORG_EVENT_STREAM, initialPosition(), ctx);
|
|
1313
|
+
const seekTs = catchupSeekTimestamp(eventWatermark, sinceFloor);
|
|
1302
1314
|
const eventResults = [];
|
|
1303
|
-
// Seek the indexed
|
|
1315
|
+
// Seek the indexed floor before materializing events. Keep detail
|
|
1304
1316
|
// for no-op suppression/dedup even when the response omits it. No limit here:
|
|
1305
|
-
// expiry, targeting, dedup and budget admission must run before the cap.
|
|
1317
|
+
// expiry, targeting, dedup and budget admission must run before the display cap.
|
|
1306
1318
|
const recentEvents = withDetachedTxn(ctx, () => databases.flair.OrgEvent.search({
|
|
1307
|
-
|
|
1319
|
+
...(seekTs
|
|
1320
|
+
? { conditions: [{ attribute: "createdAt", comparator: "greater_than_equal", value: seekTs }] }
|
|
1321
|
+
: {}),
|
|
1308
1322
|
select: ["id", "kind", "summary", "detail", "targetIds", "createdAt", "expiresAt", "scope"],
|
|
1309
1323
|
}));
|
|
1310
1324
|
for await (const event of recentEvents) {
|
|
1311
|
-
if (!event
|
|
1312
|
-
continue;
|
|
1313
|
-
if (event.expiresAt && new Date(event.expiresAt) < new Date())
|
|
1314
|
-
continue;
|
|
1315
|
-
const targets = event.targetIds;
|
|
1316
|
-
const isRelevant = !targets || targets.length === 0 || targets.includes(agentId);
|
|
1317
|
-
if (!isRelevant)
|
|
1325
|
+
if (!isCatchupEligible(event, { participantId: agentId, after: eventWatermark, since: sinceFloor }))
|
|
1318
1326
|
continue;
|
|
1319
1327
|
// flair#1200 — suppress zero-row no-op auto-heal migration events at
|
|
1320
1328
|
// render. On a healthy store every boot emits a "migration graph-heal
|
|
@@ -1365,8 +1373,10 @@ export class BootstrapMemories extends Resource {
|
|
|
1365
1373
|
const dedupedEvents = [...eventBySignature.values()]
|
|
1366
1374
|
.sort((a, b) => (b.createdAt || "").localeCompare(a.createdAt || ""));
|
|
1367
1375
|
for (const evt of dedupedEvents) {
|
|
1368
|
-
if (sections.events.length >= eventCap)
|
|
1369
|
-
|
|
1376
|
+
if (sections.events.length >= eventCap) {
|
|
1377
|
+
eventsRemaining++;
|
|
1378
|
+
continue;
|
|
1379
|
+
}
|
|
1370
1380
|
// The structured object a connector actually reads (flair#1206). Lean by
|
|
1371
1381
|
// default; `detail` (the verbose migration-internals/summary-restating
|
|
1372
1382
|
// JSON) only when explicitly requested. Optional fields are omitted when
|
|
@@ -1389,6 +1399,7 @@ export class BootstrapMemories extends Resource {
|
|
|
1389
1399
|
// can say so instead of claiming "empty by design" (#1182 contract).
|
|
1390
1400
|
if (cost > tokenBudget) {
|
|
1391
1401
|
eventsBudgetTruncated++;
|
|
1402
|
+
eventsRemaining++;
|
|
1392
1403
|
continue;
|
|
1393
1404
|
}
|
|
1394
1405
|
const elapsed = Date.now() - new Date(evt.createdAt).getTime();
|
|
@@ -1400,9 +1411,10 @@ export class BootstrapMemories extends Resource {
|
|
|
1400
1411
|
includedEvents.push(structured);
|
|
1401
1412
|
tokenBudget -= cost;
|
|
1402
1413
|
}
|
|
1414
|
+
eventsHasMore = eventsRemaining > 0;
|
|
1403
1415
|
}
|
|
1404
1416
|
catch {
|
|
1405
|
-
// non-fatal: OrgEvent table may not exist yet
|
|
1417
|
+
// non-fatal: OrgEvent / AgentReadPosition table may not exist yet
|
|
1406
1418
|
}
|
|
1407
1419
|
// flair#1207 — derive the own-memory counters from the unique-id sets so the
|
|
1408
1420
|
// invariant memoriesIncluded + memoriesTruncated <= memoriesAvailable holds.
|
|
@@ -1615,8 +1627,9 @@ export class BootstrapMemories extends Resource {
|
|
|
1615
1627
|
const eventsHint = includedEvents.length === 0
|
|
1616
1628
|
? (eventsBudgetTruncated > 0
|
|
1617
1629
|
? `No org events fit the token budget: ${eventsBudgetTruncated} relevant event(s) cleared the `
|
|
1618
|
-
+ "
|
|
1619
|
-
|
|
1630
|
+
+ "watermark (or optional lastBootAt floor) but were budget-truncated. Raise maxTokens "
|
|
1631
|
+
+ "or GET /OrgEventCatchup to page the rest."
|
|
1632
|
+
: "No org events after your watermark were relevant to you (org-wide, or targeted at you) "
|
|
1620
1633
|
+ "after zero-row no-op auto-heal filtering. This container is present-but-empty by design, not dropped.")
|
|
1621
1634
|
: undefined;
|
|
1622
1635
|
// teammateFindings: [] — name WHICH legitimate empty this is (no task → no
|
|
@@ -1657,6 +1670,9 @@ export class BootstrapMemories extends Resource {
|
|
|
1657
1670
|
// delivered. Deduped (#1200) and targetIds-scoped (same set as the prose
|
|
1658
1671
|
// "## Recent Org Events" lines), so count/charge/delivery all agree.
|
|
1659
1672
|
events: includedEvents,
|
|
1673
|
+
eventWatermark,
|
|
1674
|
+
eventsHasMore,
|
|
1675
|
+
eventsRemaining,
|
|
1660
1676
|
...(currentTaskHint ? { currentTaskHint } : {}),
|
|
1661
1677
|
...(predictedHint ? { predictedHint } : {}),
|
|
1662
1678
|
// flair#1182 (0.44.11) — empty-container hints, present only when the
|
|
@@ -83,12 +83,11 @@ export class MemoryCandidate extends databases.flair.MemoryCandidate {
|
|
|
83
83
|
return memoryCandidateByIdReadGate.call(this, target, (t) => super.get(t));
|
|
84
84
|
}
|
|
85
85
|
/**
|
|
86
|
-
* Scope collection reads to the caller's own candidates.
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* "or"` in the request body cannot boolean-inject past the owner scope.
|
|
86
|
+
* Scope collection reads to the caller's own candidates. Agent-facing
|
|
87
|
+
* collection reads (`GET /MemoryCandidate`) reach this handler — the agentId
|
|
88
|
+
* condition is wrapped as the OUTERMOST `and` block (Relationship.ts's same
|
|
89
|
+
* "Security Critical" discipline) so a caller-supplied `operator: "or"` in
|
|
90
|
+
* the query cannot boolean-inject past the owner scope.
|
|
92
91
|
*/
|
|
93
92
|
async search(query) {
|
|
94
93
|
const ctx = this.getContext?.();
|