@tpsdev-ai/flair 0.22.0 → 0.22.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/dist/cli.js +57 -9
- package/dist/resources/migrations/runner.js +2 -3
- package/dist/resources/migrations/space.js +111 -17
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -8131,6 +8131,46 @@ see src/fleet-verify.ts's file header for the full caveat.`)
|
|
|
8131
8131
|
}
|
|
8132
8132
|
process.exit(result.exitCode);
|
|
8133
8133
|
});
|
|
8134
|
+
// ─── flair doctor — pure summary/exit helper ─────────────────────────────────
|
|
8135
|
+
// Extracted for testability (flair#721), same pattern as formatCandidateLine /
|
|
8136
|
+
// describeReflectError above: the action callback spawns process.exit and a
|
|
8137
|
+
// long sequence of console.log side effects, which makes it high-effort/
|
|
8138
|
+
// low-value to drive directly — this is the actual decision logic. Before
|
|
8139
|
+
// #721, doctor tracked only a single `issues` counter: every detected
|
|
8140
|
+
// problem incremented it, and the final summary/exit-code read that counter
|
|
8141
|
+
// alone, with no separate record of which of those issues `--fix` actually
|
|
8142
|
+
// resolved during the same run. So a `--fix` run that interactively fixed
|
|
8143
|
+
// every issue it found still printed "N issues found — see fixes above" and
|
|
8144
|
+
// exited 1 — indistinguishable from a run that fixed nothing. This helper
|
|
8145
|
+
// takes the accumulated found/fixed counts plus whether `--fix` was passed
|
|
8146
|
+
// at all, and decides the summary line + exit code:
|
|
8147
|
+
// - 0 found → "No issues found", exit 0 (unchanged)
|
|
8148
|
+
// - found, no --fix → "N issues found — see fixes above", exit 1 (unchanged)
|
|
8149
|
+
// - found, --fix, all fixed → "N issues found, N fixed ✓", exit 0
|
|
8150
|
+
// - found, --fix, some remaining → "N issues found, M fixed, K remaining", exit 1
|
|
8151
|
+
export function summarizeDoctorRun(found, fixed, autoFix) {
|
|
8152
|
+
const plural = (n) => `issue${n === 1 ? "" : "s"}`;
|
|
8153
|
+
if (found === 0) {
|
|
8154
|
+
return { line: ` ${render.icons.ok} ${render.wrap(render.c.green, "No issues found")}`, exitCode: 0 };
|
|
8155
|
+
}
|
|
8156
|
+
if (!autoFix) {
|
|
8157
|
+
return {
|
|
8158
|
+
line: ` ${render.icons.error} ${render.wrap(render.c.red, `${found} ${plural(found)} found`)} ${render.wrap(render.c.dim, "— see fixes above")}`,
|
|
8159
|
+
exitCode: 1,
|
|
8160
|
+
};
|
|
8161
|
+
}
|
|
8162
|
+
if (fixed >= found) {
|
|
8163
|
+
return {
|
|
8164
|
+
line: ` ${render.icons.ok} ${render.wrap(render.c.green, `${found} ${plural(found)} found, ${fixed} fixed ✓`)}`,
|
|
8165
|
+
exitCode: 0,
|
|
8166
|
+
};
|
|
8167
|
+
}
|
|
8168
|
+
const remaining = found - fixed;
|
|
8169
|
+
return {
|
|
8170
|
+
line: ` ${render.icons.error} ${render.wrap(render.c.red, `${found} ${plural(found)} found, ${fixed} fixed, ${remaining} remaining`)}`,
|
|
8171
|
+
exitCode: 1,
|
|
8172
|
+
};
|
|
8173
|
+
}
|
|
8134
8174
|
// ─── flair doctor ─────────────────────────────────────────────────────────────
|
|
8135
8175
|
program
|
|
8136
8176
|
.command("doctor")
|
|
@@ -8149,6 +8189,7 @@ program
|
|
|
8149
8189
|
let effectivePort = port;
|
|
8150
8190
|
let baseUrl = `http://127.0.0.1:${port}`;
|
|
8151
8191
|
let issues = 0;
|
|
8192
|
+
let fixed = 0; // issues that --fix successfully resolved during this run (flair#721)
|
|
8152
8193
|
let harperResponding = false;
|
|
8153
8194
|
console.log(`\n${render.wrap(render.c.bold, "🩺 Flair Doctor")}\n`);
|
|
8154
8195
|
// 0. Version check (flair#587) — offline-tolerant + cached, independent
|
|
@@ -8234,6 +8275,7 @@ program
|
|
|
8234
8275
|
else {
|
|
8235
8276
|
writeConfig(discoveredPort);
|
|
8236
8277
|
console.log(` ${render.icons.ok} Updated config to port ${discoveredPort}`);
|
|
8278
|
+
fixed++;
|
|
8237
8279
|
}
|
|
8238
8280
|
}
|
|
8239
8281
|
else {
|
|
@@ -8277,6 +8319,7 @@ program
|
|
|
8277
8319
|
const { execSync } = await import("node:child_process");
|
|
8278
8320
|
execSync(`${process.argv[0]} ${process.argv[1]} restart --port ${port}`, { stdio: "inherit" });
|
|
8279
8321
|
console.log(` ${render.icons.ok} Restart attempted`);
|
|
8322
|
+
fixed++;
|
|
8280
8323
|
}
|
|
8281
8324
|
catch {
|
|
8282
8325
|
console.log(` ${render.icons.error} Restart failed — try: flair init --agent-id <your-agent>`);
|
|
@@ -8319,6 +8362,7 @@ program
|
|
|
8319
8362
|
const { execSync } = await import("node:child_process");
|
|
8320
8363
|
execSync(`${process.argv[0]} ${process.argv[1]} restart --port ${effectivePort}`, { stdio: "inherit" });
|
|
8321
8364
|
console.log(` ${render.icons.ok} Restarted onto ${__pkgVersion}`);
|
|
8365
|
+
fixed++;
|
|
8322
8366
|
}
|
|
8323
8367
|
catch {
|
|
8324
8368
|
console.log(` ${render.icons.error} Restart failed — try: flair restart`);
|
|
@@ -8418,6 +8462,7 @@ program
|
|
|
8418
8462
|
else {
|
|
8419
8463
|
(await import("node:fs")).unlinkSync(pidFile);
|
|
8420
8464
|
console.log(` ${render.icons.ok} Removed stale PID file`);
|
|
8465
|
+
fixed++;
|
|
8421
8466
|
}
|
|
8422
8467
|
}
|
|
8423
8468
|
else {
|
|
@@ -8498,6 +8543,8 @@ program
|
|
|
8498
8543
|
client.id === "gemini" ? wireGemini(wireEnv) :
|
|
8499
8544
|
wireCursor(wireEnv);
|
|
8500
8545
|
console.log(` ${wireResult.ok ? render.icons.ok : render.icons.warn} ${wireResult.message}`);
|
|
8546
|
+
if (wireResult.ok)
|
|
8547
|
+
fixed++;
|
|
8501
8548
|
}
|
|
8502
8549
|
}
|
|
8503
8550
|
}
|
|
@@ -8549,6 +8596,8 @@ program
|
|
|
8549
8596
|
else {
|
|
8550
8597
|
const fixRes = fixClaudeMdBootstrap(process.cwd());
|
|
8551
8598
|
console.log(` ${fixRes.ok ? render.icons.ok : render.icons.warn} ${fixRes.message}`);
|
|
8599
|
+
if (fixRes.ok)
|
|
8600
|
+
fixed++;
|
|
8552
8601
|
}
|
|
8553
8602
|
}
|
|
8554
8603
|
}
|
|
@@ -8576,6 +8625,8 @@ program
|
|
|
8576
8625
|
const fixAgentId = claudeCodeAgentId || opts.agent || process.env.FLAIR_AGENT_ID;
|
|
8577
8626
|
const fixRes = fixSessionStartHook(homedir(), fixAgentId);
|
|
8578
8627
|
console.log(` ${fixRes.ok ? render.icons.ok : render.icons.warn} ${fixRes.message}`);
|
|
8628
|
+
if (fixRes.ok)
|
|
8629
|
+
fixed++;
|
|
8579
8630
|
}
|
|
8580
8631
|
}
|
|
8581
8632
|
}
|
|
@@ -8726,17 +8777,14 @@ program
|
|
|
8726
8777
|
console.log(` ${render.icons.warn} Migration state check failed: ${err?.message ?? err}`);
|
|
8727
8778
|
}
|
|
8728
8779
|
}
|
|
8729
|
-
// Summary
|
|
8780
|
+
// Summary — see summarizeDoctorRun above (flair#721): distinguishes
|
|
8781
|
+
// issues --fix actually resolved this run from ones still outstanding.
|
|
8730
8782
|
console.log("");
|
|
8731
|
-
|
|
8732
|
-
|
|
8733
|
-
}
|
|
8734
|
-
else {
|
|
8735
|
-
console.log(` ${render.icons.error} ${render.wrap(render.c.red, `${issues} issue${issues > 1 ? "s" : ""} found`)} ${render.wrap(render.c.dim, "— see fixes above")}`);
|
|
8736
|
-
}
|
|
8783
|
+
const summary = summarizeDoctorRun(issues, fixed, autoFix);
|
|
8784
|
+
console.log(summary.line);
|
|
8737
8785
|
console.log("");
|
|
8738
|
-
if (
|
|
8739
|
-
process.exit(
|
|
8786
|
+
if (summary.exitCode !== 0)
|
|
8787
|
+
process.exit(summary.exitCode);
|
|
8740
8788
|
});
|
|
8741
8789
|
// ─── flair session snapshot ──────────────────────────────────────────────────
|
|
8742
8790
|
// Slice 2 of FLAIR-AGENT-CONTEXT-TIERS-B. Snapshot a
|
|
@@ -70,7 +70,6 @@ function resolveDeps(deps) {
|
|
|
70
70
|
ledgerDeps: deps.ledgerDeps ?? {},
|
|
71
71
|
batchDelayMs: deps.batchDelayMs ?? resolveTestBatchDelayMs() ?? 100,
|
|
72
72
|
initiator: deps.initiator ?? "auto",
|
|
73
|
-
headroomFloor: deps.headroomFloor,
|
|
74
73
|
};
|
|
75
74
|
}
|
|
76
75
|
// ─── space/snapshot sizing heuristics (overridable via RunnerDeps in future if ever needed) ──
|
|
@@ -256,11 +255,11 @@ async function runOneMigration(migration, envelope, deps, lock) {
|
|
|
256
255
|
}
|
|
257
256
|
const estSnapshot = estimateSnapshotBytes(posture.snapshotScope, initialRemaining);
|
|
258
257
|
const estWorkingSet = estimateWorkingSetBytes(migration.riskClass, initialRemaining);
|
|
259
|
-
let space = checkSpace({ dataDir: deps.dataDir, estimatedSnapshotBytes: estSnapshot, estimatedWorkingSetBytes: estWorkingSet
|
|
258
|
+
let space = checkSpace({ dataDir: deps.dataDir, estimatedSnapshotBytes: estSnapshot, estimatedWorkingSetBytes: estWorkingSet }, deps.spaceProbe);
|
|
260
259
|
if (!space.ok) {
|
|
261
260
|
// ── Ladder step 2: prune snapshots, then retry ──
|
|
262
261
|
pruneMigrationSnapshots(deps.snapshotRoot);
|
|
263
|
-
space = checkSpace({ dataDir: deps.dataDir, estimatedSnapshotBytes: estSnapshot, estimatedWorkingSetBytes: estWorkingSet
|
|
262
|
+
space = checkSpace({ dataDir: deps.dataDir, estimatedSnapshotBytes: estSnapshot, estimatedWorkingSetBytes: estWorkingSet }, deps.spaceProbe);
|
|
264
263
|
}
|
|
265
264
|
if (!space.ok) {
|
|
266
265
|
// ── Ladder step 5: no safe path → halt-don't-brick ──
|
|
@@ -13,9 +13,60 @@
|
|
|
13
13
|
* `estimateWorkingSetBytes` below is the caller's job (the runner computes
|
|
14
14
|
* it per risk class); this module just evaluates the estimate against real
|
|
15
15
|
* (or test-overridden) disk free/total space.
|
|
16
|
+
*
|
|
17
|
+
* flair#720 rewrote the rule this module enforces. The original shape
|
|
18
|
+
* (`projectedUsedFraction <= 0.9`, measured against TOTAL disk size) was
|
|
19
|
+
* designed for a flair-dedicated volume, but on a general-purpose machine
|
|
20
|
+
* — a personal Mac especially, where APFS purgeable space makes
|
|
21
|
+
* `statfs.bavail` understate real availability — the system volume
|
|
22
|
+
* routinely sits above 90% used already, so the fraction test failed
|
|
23
|
+
* before a single byte was written, regardless of how trivially the
|
|
24
|
+
* migration's own footprint fit (flair#720: 220 KB needed, 18.6 GB free,
|
|
25
|
+
* halted anyway). The fix keeps invariant III's "never fill past a
|
|
26
|
+
* cushion" intent but measures the migration's OWN impact plus an
|
|
27
|
+
* absolute reserve, instead of the disk's pre-existing fullness:
|
|
28
|
+
* `ok = neededBytes <= freeBytes AND (freeBytes - neededBytes) >= reserve`.
|
|
29
|
+
* See `RESERVE_MIN_BYTES` / `RESERVE_MAX_BYTES` / `RESERVE_FRACTION` below
|
|
30
|
+
* for how `reserve` is derived.
|
|
16
31
|
*/
|
|
17
32
|
import { statfsSync } from "node:fs";
|
|
18
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Reserve floor: on any disk, no migration may leave less than this much
|
|
35
|
+
* free afterward, even on a tiny volume where 5% of total would be less.
|
|
36
|
+
* 256 MiB is comfortably more than any single migration's own working set
|
|
37
|
+
* in this codebase (the largest estimate is embedding/HNSW rebuild
|
|
38
|
+
* overhead, a few KB per row) — it exists to leave room for Harper's own
|
|
39
|
+
* ordinary operation (WAL, temp files, log growth), not the migration
|
|
40
|
+
* itself.
|
|
41
|
+
*/
|
|
42
|
+
export const RESERVE_MIN_BYTES = 256 * 1024 * 1024; // 256 MiB
|
|
43
|
+
/**
|
|
44
|
+
* Reserve cap: on a large disk, 5% of total would be an unreasonably large
|
|
45
|
+
* cushion to demand (e.g. 50 GB on a 1 TB drive) for what is, worst case,
|
|
46
|
+
* a few GB of migration working set. 2 GiB caps the reserve so the rule
|
|
47
|
+
* stays proportionate to what migrations actually need, not the disk's
|
|
48
|
+
* raw size.
|
|
49
|
+
*/
|
|
50
|
+
export const RESERVE_MAX_BYTES = 2 * 1024 * 1024 * 1024; // 2 GiB
|
|
51
|
+
/**
|
|
52
|
+
* Between the floor and the cap, the reserve scales with disk size: 5% of
|
|
53
|
+
* total. This is the "proportionate cushion" middle ground — big enough to
|
|
54
|
+
* matter on a mid-size disk, small enough not to dominate on either
|
|
55
|
+
* extreme (RESERVE_MIN_BYTES / RESERVE_MAX_BYTES clamp the two ends).
|
|
56
|
+
*/
|
|
57
|
+
export const RESERVE_FRACTION = 0.05;
|
|
58
|
+
/**
|
|
59
|
+
* Production escape hatch for constrained deployments (flair#720) — e.g. a
|
|
60
|
+
* small dedicated volume where even the 256 MiB floor is more cushion than
|
|
61
|
+
* the operator can spare. When set to a finite, non-negative number, this
|
|
62
|
+
* overrides the computed reserve entirely (including the floor/cap clamp
|
|
63
|
+
* — set it to 0 to disable the reserve check outright, keeping only the
|
|
64
|
+
* `neededBytes <= freeBytes` fit test). Invalid values (non-numeric,
|
|
65
|
+
* negative, NaN, Infinity) are ignored and the computed reserve is used
|
|
66
|
+
* instead — mirrors TEST_FREE_BYTES_ENV's validation below. Unset (the
|
|
67
|
+
* default) in every normal deployment.
|
|
68
|
+
*/
|
|
69
|
+
export const RESERVE_BYTES_ENV = "FLAIR_MIGRATION_RESERVE_BYTES";
|
|
19
70
|
/**
|
|
20
71
|
* Test-only escape hatch: when set, overrides the real statfs-reported free
|
|
21
72
|
* byte count. Used by the halt-on-blocked-space integration test (real
|
|
@@ -45,29 +96,72 @@ export const defaultSpaceProbe = {
|
|
|
45
96
|
getTotalBytes: realGetTotalBytes,
|
|
46
97
|
};
|
|
47
98
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
99
|
+
* Resolves the absolute reserve a migration must leave free afterward:
|
|
100
|
+
* `RESERVE_BYTES_ENV` if set to a valid (finite, >= 0) value, else
|
|
101
|
+
* `clamp(RESERVE_FRACTION * totalBytes, RESERVE_MIN_BYTES, RESERVE_MAX_BYTES)`.
|
|
102
|
+
* Exported so tests can exercise the clamp/override logic directly without
|
|
103
|
+
* going through a full `checkSpace` call.
|
|
104
|
+
*/
|
|
105
|
+
export function resolveReserveBytes(totalBytes, env = process.env) {
|
|
106
|
+
const override = env[RESERVE_BYTES_ENV];
|
|
107
|
+
if (override !== undefined) {
|
|
108
|
+
const n = Number(override);
|
|
109
|
+
if (Number.isFinite(n) && n >= 0)
|
|
110
|
+
return n;
|
|
111
|
+
}
|
|
112
|
+
const fractional = totalBytes * RESERVE_FRACTION;
|
|
113
|
+
return Math.min(RESERVE_MAX_BYTES, Math.max(RESERVE_MIN_BYTES, fractional));
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Bytes → human readable (binary-based thresholds, decimal labels — same
|
|
117
|
+
* convention as src/render.ts's `humanBytes`, which this module can't
|
|
118
|
+
* import: resources/**\/*.ts and src/**\/*.ts are separate TypeScript
|
|
119
|
+
* project boundaries (tsconfig.json vs tsconfig.cli.json/tsconfig.check.src.json)
|
|
120
|
+
* — resources/ is Harper-loaded component code and never imports from the
|
|
121
|
+
* CLI's src/ tree. Small enough to duplicate locally rather than restructure
|
|
122
|
+
* the module boundary for one formatter.
|
|
123
|
+
*/
|
|
124
|
+
export function humanBytes(n) {
|
|
125
|
+
if (!Number.isFinite(n) || n < 0)
|
|
126
|
+
return "—";
|
|
127
|
+
if (n < 1024)
|
|
128
|
+
return `${n} B`;
|
|
129
|
+
if (n < 1024 * 1024)
|
|
130
|
+
return `${(n / 1024).toFixed(1)} KB`;
|
|
131
|
+
if (n < 1024 * 1024 * 1024)
|
|
132
|
+
return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
|
133
|
+
return `${(n / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Evaluates the pre-flight space check (flair#720's rule): fails (ok:false)
|
|
137
|
+
* if the needed bytes don't fit in free space outright, OR if spending them
|
|
138
|
+
* would leave less than `reserveBytes` free afterward. Unlike the rule this
|
|
139
|
+
* replaced, fullness that already exists on the disk before the migration
|
|
140
|
+
* runs is irrelevant — only the migration's OWN impact on free space is
|
|
141
|
+
* judged, against an absolute (not fraction-of-total) cushion.
|
|
53
142
|
*/
|
|
54
143
|
export function checkSpace(input, probe = defaultSpaceProbe) {
|
|
55
|
-
const floor = input.headroomFloor ?? DEFAULT_HEADROOM_FLOOR;
|
|
56
144
|
const freeBytes = probe.getFreeBytes(input.dataDir);
|
|
57
145
|
const totalBytes = probe.getTotalBytes(input.dataDir);
|
|
58
146
|
const neededBytes = input.estimatedSnapshotBytes + input.estimatedWorkingSetBytes;
|
|
59
|
-
const
|
|
60
|
-
const projectedUsedBytes = usedBytesNow + neededBytes;
|
|
61
|
-
const projectedUsedFraction = totalBytes > 0 ? projectedUsedBytes / totalBytes : 1;
|
|
147
|
+
const reserveBytes = resolveReserveBytes(totalBytes);
|
|
62
148
|
const fits = neededBytes <= freeBytes;
|
|
63
|
-
const
|
|
64
|
-
const
|
|
149
|
+
const remainingAfter = freeBytes - neededBytes;
|
|
150
|
+
const withinReserve = fits && remainingAfter >= reserveBytes;
|
|
151
|
+
const ok = fits && withinReserve;
|
|
65
152
|
let reason;
|
|
66
|
-
if (!
|
|
153
|
+
if (!fits) {
|
|
154
|
+
reason =
|
|
155
|
+
`need ${humanBytes(neededBytes)} free (snapshot + migration working set), have ${humanBytes(freeBytes)} — ` +
|
|
156
|
+
`short by ${humanBytes(neededBytes - freeBytes)}. Set ${RESERVE_BYTES_ENV} to lower the required post-migration ` +
|
|
157
|
+
`reserve on constrained deployments (this won't help here — the migration doesn't fit even before any reserve)`;
|
|
158
|
+
}
|
|
159
|
+
else if (!withinReserve) {
|
|
67
160
|
reason =
|
|
68
|
-
`need ${neededBytes}
|
|
69
|
-
`
|
|
70
|
-
|
|
161
|
+
`need ${humanBytes(neededBytes)} free (snapshot + migration working set); have ${humanBytes(freeBytes)}, ` +
|
|
162
|
+
`which covers it but would leave only ${humanBytes(remainingAfter)} free afterward — short of the ` +
|
|
163
|
+
`${humanBytes(reserveBytes)} minimum reserve required post-migration. Set ${RESERVE_BYTES_ENV} to a lower ` +
|
|
164
|
+
`byte count to shrink the required reserve on constrained deployments`;
|
|
71
165
|
}
|
|
72
|
-
return { ok, freeBytes, totalBytes, neededBytes,
|
|
166
|
+
return { ok, freeBytes, totalBytes, neededBytes, reserveBytes, reason };
|
|
73
167
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tpsdev-ai/flair",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.1",
|
|
4
4
|
"description": "Identity, memory, and soul for AI agents. Cryptographic identity (Ed25519), semantic memory with local embeddings, and persistent personality — all in a single process.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|