moflo 4.13.2 → 4.13.3

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.
@@ -20,7 +20,7 @@ import { existsSync, readFileSync } from 'fs';
20
20
  import { mofloInternalURL } from './lib/moflo-resolve.mjs';
21
21
  import { memoryDbPath, hnswIndexPath, findProjectRoot } from './lib/moflo-paths.mjs';
22
22
  import { openBackend } from './lib/get-backend.mjs';
23
- import { PENDING_EMBEDDING_WHERE } from './lib/embedding-backlog.mjs';
23
+ import { DELIBERATELY_UNEMBEDDED_WHERE, EMBEDDABLE_WHERE, PENDING_EMBEDDING_WHERE } from './lib/embedding-backlog.mjs';
24
24
  const FASTEMBED_INLINE = 'dist/src/cli/embeddings/fastembed-inline/index.js';
25
25
  const BRIDGE_CORE = 'dist/src/cli/memory/bridge-core.js';
26
26
  const HNSW_PERSISTENCE = 'dist/src/cli/memory/hnsw-persistence.js';
@@ -100,12 +100,12 @@ function saveDb(db) {
100
100
  // The backlog clause is imported, not restated (#1383): the indexer gate asks
101
101
  // this exact question to decide whether to run this script at all, and a gate
102
102
  // that approximates its step's own query is how new chunks got left unembedded
103
- // in the first place. `PENDING_EMBEDDING_WHERE` already carries `status =
104
- // 'active'`, so the non-forced branch below replaces the base clause rather
105
- // than appending to it.
103
+ // in the first place. Both clauses carry `status = 'active'` and exclude rows
104
+ // deliberately left unembedded (ephemeral namespaces, opt-outs — #1492), so
105
+ // `--force` re-embeds everything embeddable without resurrecting those.
106
106
  function getEntriesNeedingEmbeddings(db, namespace, forceAll) {
107
107
  let sql = `SELECT id, key, namespace, content FROM memory_entries WHERE `
108
- + (forceAll ? `status = 'active'` : PENDING_EMBEDDING_WHERE);
108
+ + (forceAll ? EMBEDDABLE_WHERE : PENDING_EMBEDDING_WHERE);
109
109
  const params = [];
110
110
 
111
111
  if (namespace) {
@@ -136,7 +136,7 @@ function getNamespaceStats(db) {
136
136
  namespace,
137
137
  COUNT(*) as total,
138
138
  SUM(CASE WHEN embedding IS NOT NULL AND embedding != '' THEN 1 ELSE 0 END) as vectorized,
139
- SUM(CASE WHEN embedding IS NULL OR embedding = '' THEN 1 ELSE 0 END) as missing
139
+ SUM(CASE WHEN (embedding IS NULL OR embedding = '') AND NOT ${DELIBERATELY_UNEMBEDDED_WHERE} THEN 1 ELSE 0 END) as missing
140
140
  FROM memory_entries
141
141
  WHERE status = 'active'
142
142
  GROUP BY namespace
@@ -26,16 +26,65 @@ import { existsSync } from 'node:fs';
26
26
  import { openBackendSync } from './get-backend.mjs';
27
27
  import { memoryDbPath } from './moflo-paths.mjs';
28
28
 
29
- /**
30
- * Rows `build-embeddings` will pick up on its next run. Active rows whose
31
- * embedding is absent — either NULL (never written) or empty string (a
32
- * producer that failed and wrote a placeholder).
33
- */
34
29
  /** Retry budget for the probe's read-only open. See {@link hasPendingEmbeddings}. */
35
30
  const PROBE_BUSY_TIMEOUT_MS = 2000;
36
31
 
32
+ /**
33
+ * Mirrors of `EPHEMERAL_NAMESPACES` / `EPHEMERAL_NAMESPACE_PREFIXES` /
34
+ * `EMBEDDING_MODEL_OPT_OUT` in `src/cli/memory/bridge-embedder.ts`. `bin/`
35
+ * cannot import TypeScript source, so the values are restated here and
36
+ * `tests/bin/embedding-backfill-ephemeral-1492.test.ts` fails if the two
37
+ * copies drift.
38
+ */
39
+ export const EPHEMERAL_NAMESPACES = Object.freeze([
40
+ 'hive-mind',
41
+ 'tasklist',
42
+ 'epic-state',
43
+ 'test-bridge-fix',
44
+ 'swarm-agents',
45
+ 'swarm-topology',
46
+ 'swarm-tasks',
47
+ 'swarm-consensus',
48
+ ]);
49
+ export const EPHEMERAL_NAMESPACE_PREFIXES = Object.freeze([]);
50
+ export const EMBEDDING_MODEL_OPT_OUT = 'none';
51
+
52
+ const sqlString = (s) => `'${s.replace(/'/g, "''")}'`;
53
+
54
+ /**
55
+ * Rows written deliberately without an embedding — ephemeral run state and
56
+ * explicit opt-outs. The writers leave `embedding` NULL for these, which is
57
+ * indistinguishable from "not embedded yet" unless every backfill excludes
58
+ * them (#1492). Inlined literals rather than bindings so the clause drops into
59
+ * any statement unchanged; every value is a compile-time constant above.
60
+ */
61
+ export const DELIBERATELY_UNEMBEDDED_WHERE = (() => {
62
+ const clauses = [];
63
+ if (EPHEMERAL_NAMESPACES.length > 0) {
64
+ clauses.push(`namespace IN (${EPHEMERAL_NAMESPACES.map(sqlString).join(', ')})`);
65
+ }
66
+ for (const prefix of EPHEMERAL_NAMESPACE_PREFIXES) clauses.push(`namespace LIKE ${sqlString(`${prefix}%`)}`);
67
+ clauses.push(`COALESCE(embedding_model, '') = ${sqlString(EMBEDDING_MODEL_OPT_OUT)}`);
68
+ // COALESCE to a strict 0/1: `namespace` is nullable and `NULL IN (...)` is
69
+ // NULL, so every negated use below would silently drop a NULL-namespace row.
70
+ return `COALESCE((${clauses.join(' OR ')}), 0)`;
71
+ })();
72
+
73
+ /**
74
+ * Every row a backfill may embed: active, and not deliberately unembedded.
75
+ * `build-embeddings --force` selects on this alone.
76
+ */
77
+ export const EMBEDDABLE_WHERE = `status = 'active' AND NOT ${DELIBERATELY_UNEMBEDDED_WHERE}`;
78
+
79
+ /**
80
+ * Rows `build-embeddings` will pick up on its next run. Embeddable rows whose
81
+ * embedding is absent — either NULL (never written) or empty string (a
82
+ * producer that failed and wrote a placeholder). The embedding test leads so
83
+ * the steady-state probe — nearly every row embedded — rejects each row before
84
+ * evaluating the exclusion.
85
+ */
37
86
  export const PENDING_EMBEDDING_WHERE =
38
- `status = 'active' AND (embedding IS NULL OR embedding = '')`;
87
+ `(embedding IS NULL OR embedding = '') AND ${EMBEDDABLE_WHERE}`;
39
88
 
40
89
  /**
41
90
  * Does the memory DB hold rows that still need embedding?
@@ -2438,6 +2438,12 @@ try {
2438
2438
  `${plural(result.superseded, 'stale learnings row')} whose verdict already exists in the verify namespace`,
2439
2439
  );
2440
2440
  }
2441
+ if (result?.stripped > 0) {
2442
+ emitMutation(
2443
+ 'unindexed run records',
2444
+ `${plural(result.stripped, 'row')} of run state removed from semantic search`,
2445
+ );
2446
+ }
2441
2447
  }
2442
2448
  } catch (err) {
2443
2449
  // Non-fatal — leftover rows just sit until the next session retries.
@@ -17,7 +17,7 @@
17
17
  * row on the canonical label; this check verifies it actually did.
18
18
  *
19
19
  * Story #729 carve-out: ephemeral-namespace rows (tasklist, hive-mind,
20
- * epic-state, test-bridge-fix, plus EPHEMERAL_NAMESPACE_PREFIXES) are
20
+ * epic-state, swarm-*, …, plus EPHEMERAL_NAMESPACE_PREFIXES) are
21
21
  * intentionally written with `embedding IS NULL AND embedding_model IS
22
22
  * NULL`. They are excluded from the count so they don't trip branch (4)
23
23
  * "unrecognised embedding_model" on every publish — see bridge-embedder.ts
@@ -33,7 +33,7 @@ import { existsSync } from 'fs';
33
33
  import { CANONICAL_EMBEDDING_MODEL } from '../embeddings/migration/types.js';
34
34
  import { memoryDbCandidatePaths } from '../services/moflo-paths.js';
35
35
  import { openDaemonDatabase } from '../memory/daemon-backend.js';
36
- import { EPHEMERAL_NAMESPACES, EPHEMERAL_NAMESPACE_PREFIXES, } from '../memory/bridge-embedder.js';
36
+ import { ephemeralNamespaceSql } from '../memory/bridge-embedder.js';
37
37
  import { resolveStateRoot } from '../services/project-root.js';
38
38
  /**
39
39
  * Known neural-model labels that all share the all-MiniLM-L6-v2 384-dim
@@ -183,21 +183,9 @@ async function loadModelGroups(dbPath) {
183
183
  // embedding_model IS NULL`. Without this exclusion every spell run that
184
184
  // logs to `tasklist` re-trips branch (4) "unrecognised embedding_model"
185
185
  // on the next publish, even though the writer is doing the right thing.
186
- const ephemeralNames = [...EPHEMERAL_NAMESPACES];
187
- const ephemeralPrefixes = [...EPHEMERAL_NAMESPACE_PREFIXES];
188
- const matchClauses = [];
189
- const params = [];
190
- if (ephemeralNames.length > 0) {
191
- matchClauses.push(`namespace IN (${ephemeralNames.map(() => '?').join(', ')})`);
192
- params.push(...ephemeralNames);
193
- }
194
- for (const prefix of ephemeralPrefixes) {
195
- matchClauses.push(`namespace LIKE ?`);
196
- params.push(`${prefix}%`);
197
- }
198
- const ephemeralExclusion = matchClauses.length > 0
199
- ? `AND NOT (embedding IS NULL AND embedding_model IS NULL AND (${matchClauses.join(' OR ')}))`
200
- : '';
186
+ const ephemeral = ephemeralNamespaceSql();
187
+ const params = ephemeral.params;
188
+ const ephemeralExclusion = `AND NOT (embedding IS NULL AND embedding_model IS NULL AND ${ephemeral.sql})`;
201
189
  const sql = `SELECT
202
190
  COALESCE(embedding_model, 'NULL') AS model,
203
191
  COUNT(*) AS n,
@@ -1888,9 +1888,13 @@ const rebuildIndexCommand = {
1888
1888
  output.writeln(output.bold('Rebuilding Embedding Index'));
1889
1889
  output.writeln(output.dim('─'.repeat(50)));
1890
1890
  const { db, dbPath } = await openDb(cwd);
1891
- // Build query
1892
- let sql = `SELECT id, key, namespace, content FROM memory_entries WHERE status = 'active'`;
1893
- const params = [];
1891
+ // Build query. Rows deliberately left unembedded (ephemeral namespaces,
1892
+ // explicit opt-outs) are excluded even under --force: NULL there means
1893
+ // "never embed", not "not embedded yet" (#1492).
1894
+ const { backfillExclusionSql } = await import('../memory/bridge-embedder.js');
1895
+ const exclusion = backfillExclusionSql();
1896
+ let sql = `SELECT id, key, namespace, content FROM memory_entries WHERE status = 'active' ${exclusion.sql}`;
1897
+ const params = [...exclusion.params];
1894
1898
  if (!forceAll) {
1895
1899
  sql += ` AND (embedding IS NULL OR embedding = '')`;
1896
1900
  }
@@ -53,6 +53,18 @@ export const EMBEDDING_MODEL_LEGACY_DEFAULT = 'local';
53
53
  * - `tasklist` — Spell run records (sp-*) written by spells/core/runner.ts + daemon-dashboard.ts
54
54
  * - `epic-state` — Epic progress (epic-N, story-M) written by commands/epic.ts
55
55
  * - `test-bridge-fix` — Single 2026-04-23 row left over from a one-off test
56
+ * - `swarm-agents`, `swarm-topology`, `swarm-tasks`, `swarm-consensus` —
57
+ * coordinator state written through by `swarm/swarm-persistence.ts` and
58
+ * read back by key on hydrate, never by similarity (#1492). Embed-skip
59
+ * ONLY: they are deliberately absent from the purge sets below, because the
60
+ * swarm must survive an MCP-server restart (Story #806).
61
+ *
62
+ * The rule is enforced at write time AND at backfill time. The writers leave
63
+ * `embedding` NULL; every backfill selector (`bin/build-embeddings.mjs`, the
64
+ * backlog probe in `bin/lib/embedding-backlog.mjs`, `flo memory rebuild-index`)
65
+ * excludes these rows via {@link backfillExclusionSql} or its `bin/` mirror —
66
+ * otherwise NULL reads as "not embedded yet" and the backfill embeds them
67
+ * anyway (#1492). The `bin/` copy is parity-guarded against this set.
56
68
  *
57
69
  * Membership is also extended by {@link EPHEMERAL_NAMESPACE_PREFIXES} for
58
70
  * dynamic-name namespaces (e.g. `doctor-memprobe-<persona>`). Most callers
@@ -70,6 +82,10 @@ export const EPHEMERAL_NAMESPACES = new Set([
70
82
  'tasklist',
71
83
  'epic-state',
72
84
  'test-bridge-fix',
85
+ 'swarm-agents',
86
+ 'swarm-topology',
87
+ 'swarm-tasks',
88
+ 'swarm-consensus',
73
89
  ]);
74
90
  /**
75
91
  * Prefix patterns that extend {@link EPHEMERAL_NAMESPACES} for namespaces
@@ -146,6 +162,47 @@ export function isEphemeralNamespace(namespace) {
146
162
  }
147
163
  return false;
148
164
  }
165
+ /**
166
+ * SQL match for a namespace set: exact `names` or any `prefixes` (as
167
+ * `LIKE 'p%'`). Returned parenthesised with positional bindings so callers can
168
+ * wrap it in `NOT (...)` or use it as a positive filter; an empty set matches
169
+ * nothing.
170
+ */
171
+ export function namespaceMatchSql(names, prefixes) {
172
+ const exact = [...names];
173
+ const likes = [...prefixes].map((p) => `${p}%`);
174
+ const clauses = [];
175
+ // Bare column, not COALESCE'd, so positive filters (the session-start purge)
176
+ // keep using the namespace index. Negate via `NOT COALESCE(match, 0)`.
177
+ if (exact.length > 0)
178
+ clauses.push(`namespace IN (${exact.map(() => '?').join(', ')})`);
179
+ for (let i = 0; i < likes.length; i++)
180
+ clauses.push('namespace LIKE ?');
181
+ return {
182
+ sql: clauses.length > 0 ? `(${clauses.join(' OR ')})` : '(0)',
183
+ params: [...exact, ...likes],
184
+ };
185
+ }
186
+ /** {@link namespaceMatchSql} over the ephemeral (embed-skip) namespaces. */
187
+ export function ephemeralNamespaceSql() {
188
+ return namespaceMatchSql(EPHEMERAL_NAMESPACES, EPHEMERAL_NAMESPACE_PREFIXES);
189
+ }
190
+ /**
191
+ * `AND ...` fragment every embedding backfill appends to its selector (#1492):
192
+ * skip ephemeral namespaces and rows a writer explicitly opted out of
193
+ * (`embedding_model = 'none'`). Both write a NULL `embedding` that means
194
+ * "deliberately unembedded", which the backfill must not read as "pending".
195
+ * Mirrored in `bin/lib/embedding-backlog.mjs` (parity-guarded).
196
+ */
197
+ export function backfillExclusionSql() {
198
+ const ephemeral = ephemeralNamespaceSql();
199
+ return {
200
+ // COALESCE: `namespace` is nullable and `NOT (NULL IN (...))` is NULL,
201
+ // which would silently drop a NULL-namespace row from the backfill.
202
+ sql: `AND NOT COALESCE(${ephemeral.sql}, 0) AND COALESCE(embedding_model, '') <> ?`,
203
+ params: [...ephemeral.params, EMBEDDING_MODEL_OPT_OUT],
204
+ };
205
+ }
149
206
  /**
150
207
  * Return `true` if a namespace should be hard-purged on session start —
151
208
  * either an exact member of {@link PURGE_ON_SESSION_START_NAMESPACES} or one
@@ -26,6 +26,16 @@
26
26
  * it on every session start, leaving the tab permanently empty. Trim
27
27
  * instead so users see recent history without unbounded growth.
28
28
  *
29
+ * 4. **Strip vectors** from surviving rows in any ephemeral namespace
30
+ * ({@link ephemeralNamespaceSql} — `tasklist`, `swarm-*`, …). Their writers
31
+ * leave `embedding` NULL, but until #1492 the background backfill read that
32
+ * NULL as "pending" and embedded them, so run records ranked in ordinary
33
+ * `memory_search` results. The backfill no longer does; this heals the rows
34
+ * every existing install already carries, and re-heals if anything embeds
35
+ * them again. Rows are kept — only their vector is cleared, back to the
36
+ * shape the writer produced. The next index chain drops them from the HNSW
37
+ * sidecar (the DB write invalidates the `hnsw-rebuild` fingerprint).
38
+ *
29
39
  * All passes share the file open + final VACUUM + atomic write, so disk I/O
30
40
  * is the same as before. Writes back to disk only when something changed.
31
41
  *
@@ -38,7 +48,7 @@
38
48
  * @module cli/services/ephemeral-namespace-purge
39
49
  */
40
50
  /* eslint-disable @typescript-eslint/no-explicit-any */
41
- import { PURGE_ON_SESSION_START_NAMESPACES, PURGE_ON_SESSION_START_PREFIXES, TASKLIST_RETENTION_CAP, VERIFY_RECORD_NAMESPACE, VERIFY_RETENTION_CAP, } from '../memory/bridge-embedder.js';
51
+ import { ephemeralNamespaceSql, namespaceMatchSql, PURGE_ON_SESSION_START_NAMESPACES, PURGE_ON_SESSION_START_PREFIXES, TASKLIST_RETENTION_CAP, VERIFY_RECORD_NAMESPACE, VERIFY_RETENTION_CAP, } from '../memory/bridge-embedder.js';
42
52
  import { memoryDbPath } from './moflo-paths.js';
43
53
  import { openDaemonDatabase } from '../memory/daemon-backend.js';
44
54
  import { resolveStateRoot } from './project-root.js';
@@ -56,7 +66,7 @@ export async function purgeEphemeralNamespaces(options = {}) {
56
66
  const fs = await import('fs');
57
67
  const path = await import('path');
58
68
  const nothingToDo = {
59
- purged: 0, trimmed: 0, relocated: 0, superseded: 0,
69
+ purged: 0, trimmed: 0, relocated: 0, superseded: 0, stripped: 0,
60
70
  };
61
71
  const dbPath = path.resolve(options.dbPath ?? memoryDbPath(resolveStateRoot()));
62
72
  if (!fs.existsSync(dbPath))
@@ -77,18 +87,11 @@ export async function purgeEphemeralNamespaces(options = {}) {
77
87
  // Purge match shape: exact namespace IN (...) OR namespace LIKE 'prefix-%'.
78
88
  // The prefix clause covers runtime-suffixed namespaces like
79
89
  // `doctor-memprobe-<persona>` whose set of suffixes isn't known upfront.
80
- const namespaces = Array.from(PURGE_ON_SESSION_START_NAMESPACES);
81
- const prefixes = Array.from(PURGE_ON_SESSION_START_PREFIXES);
82
90
  const caps = [
83
91
  ['tasklist', options.tasklistRetentionCap ?? TASKLIST_RETENTION_CAP],
84
92
  [VERIFY_RECORD_NAMESPACE, options.verifyRetentionCap ?? VERIFY_RETENTION_CAP],
85
93
  ];
86
- const exactClause = namespaces.length
87
- ? `namespace IN (${namespaces.map(() => '?').join(', ')})`
88
- : '0';
89
- const prefixClause = prefixes.map(() => 'namespace LIKE ?').join(' OR ');
90
- const purgeWhere = prefixClause ? `(${exactClause} OR ${prefixClause})` : exactClause;
91
- const purgeBindings = [...namespaces, ...prefixes.map((p) => `${p}%`)];
94
+ const { sql: purgeWhere, params: purgeBindings } = namespaceMatchSql(PURGE_ON_SESSION_START_NAMESPACES, PURGE_ON_SESSION_START_PREFIXES);
92
95
  // GLOB, not LIKE: SQLite's LIKE is case-INSENSITIVE for ASCII, so
93
96
  // `key LIKE 'verify:%'` would also sweep a `Verify:...` row that
94
97
  // `gate.cjs`'s `record-verify-outcome` — which tests
@@ -98,6 +101,8 @@ export async function purgeEphemeralNamespaces(options = {}) {
98
101
  // so the only wildcard in the pattern is the trailing `*`.
99
102
  const strayVerifyWhere = 'namespace = ? AND key GLOB ?';
100
103
  const strayVerifyBindings = [LEARNINGS_NAMESPACE, `${VERIFY_RECORD_NAMESPACE}:*`];
104
+ const ephemeral = ephemeralNamespaceSql();
105
+ const embeddedEphemeralWhere = `${ephemeral.sql} AND embedding IS NOT NULL`;
101
106
  // EVERY column needs a distinct alias. `exec` maps each row to an object
102
107
  // keyed by column name, so two columns that SQLite names identically
103
108
  // collapse into one and silently shift every later index. Unaliased
@@ -107,11 +112,13 @@ export async function purgeEphemeralNamespaces(options = {}) {
107
112
  const countRows = db.exec(`SELECT
108
113
  (SELECT COUNT(*) FROM memory_entries WHERE ${purgeWhere}) AS purgeable,
109
114
  (SELECT COUNT(*) FROM memory_entries WHERE ${strayVerifyWhere}) AS relocatable,
110
- ${caps.map((_, i) => `(SELECT COUNT(*) FROM memory_entries WHERE namespace = ?) AS capTotal${i}`).join(',\n ')}`, [...purgeBindings, ...strayVerifyBindings, ...caps.map(([ns]) => ns)]);
115
+ (SELECT COUNT(*) FROM memory_entries WHERE ${embeddedEphemeralWhere}) AS strippable,
116
+ ${caps.map((_, i) => `(SELECT COUNT(*) FROM memory_entries WHERE namespace = ?) AS capTotal${i}`).join(',\n ')}`, [...purgeBindings, ...strayVerifyBindings, ...ephemeral.params, ...caps.map(([ns]) => ns)]);
111
117
  const counts = countRows[0]?.values?.[0] ?? [];
112
118
  const purgeable = Number(counts[0] ?? 0);
113
119
  const relocatable = Number(counts[1] ?? 0);
114
- const capTotals = caps.map((_, i) => Number(counts[i + 2] ?? 0));
120
+ const strippable = Number(counts[2] ?? 0);
121
+ const capTotals = caps.map((_, i) => Number(counts[i + 3] ?? 0));
115
122
  let purged = 0;
116
123
  if (purgeable > 0) {
117
124
  db.run(`DELETE FROM memory_entries WHERE ${purgeWhere}`, purgeBindings);
@@ -136,6 +143,15 @@ export async function purgeEphemeralNamespaces(options = {}) {
136
143
  db.run(`UPDATE memory_entries SET namespace = ? WHERE ${strayVerifyWhere}`, [VERIFY_RECORD_NAMESPACE, ...strayVerifyBindings]);
137
144
  relocated = db.getRowsModified?.() ?? 0;
138
145
  }
146
+ // After the purge, so rows it already deleted are not counted twice. Model
147
+ // goes back to NULL — the exact shape the ephemeral writers produce.
148
+ let stripped = 0;
149
+ if (strippable > 0) {
150
+ db.run(`UPDATE memory_entries
151
+ SET embedding = NULL, embedding_model = NULL, embedding_dimensions = NULL
152
+ WHERE ${embeddedEphemeralWhere}`, ephemeral.params);
153
+ stripped = db.getRowsModified?.() ?? 0;
154
+ }
139
155
  let trimmed = 0;
140
156
  for (const [i, [ns, cap]] of caps.entries()) {
141
157
  // Rows just relocated into `verify` count toward its cap in this same run.
@@ -154,16 +170,18 @@ export async function purgeEphemeralNamespaces(options = {}) {
154
170
  )`, [ns, ns, cap]);
155
171
  trimmed += db.getRowsModified?.() ?? 0;
156
172
  }
157
- if (purged === 0 && trimmed === 0 && relocated === 0 && superseded === 0)
173
+ if (purged === 0 && trimmed === 0 && relocated === 0 && superseded === 0 && stripped === 0) {
158
174
  return nothingToDo;
175
+ }
159
176
  // VACUUM only after a DELETE actually freed pages. A relocation is an
160
- // UPDATE — it reclaims nothing, so VACUUMing for it would rewrite the whole
161
- // file (60+ MB on a populated store) in the foreground of session start for
162
- // no benefit. Has to run outside any open transaction; node:sqlite/sql.js
177
+ // UPDATE that reclaims nothing; a vector strip frees some pages, but later
178
+ // writes reuse them. VACUUMing for either would rewrite the whole file
179
+ // (60+ MB on a populated store) in the foreground of session start for no
180
+ // real benefit. Has to run outside any open transaction; node:sqlite/sql.js
163
181
  // both auto-commit each `db.run`, so this is safe to chain.
164
182
  if (purged > 0 || trimmed > 0 || superseded > 0)
165
183
  db.run('VACUUM');
166
- return { purged, trimmed, relocated, superseded };
184
+ return { purged, trimmed, relocated, superseded, stripped };
167
185
  }
168
186
  finally {
169
187
  db.close();
@@ -2,5 +2,5 @@
2
2
  * Auto-generated by build. Do not edit manually.
3
3
  * Source of truth: root package.json → scripts/sync-version.mjs
4
4
  */
5
- export const VERSION = '4.13.2';
5
+ export const VERSION = '4.13.3';
6
6
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moflo",
3
- "version": "4.13.2",
3
+ "version": "4.13.3",
4
4
  "description": "MoFlo — AI agent orchestration for Claude Code. A standalone, opinionated toolkit with semantic memory, learned routing, gates, spells, and the /flo issue-execution skill.",
5
5
  "main": "dist/src/cli/index.js",
6
6
  "type": "module",
@@ -99,7 +99,7 @@
99
99
  "@typescript-eslint/parser": "^8.65.0",
100
100
  "eslint": "^10.8.0",
101
101
  "glob": "^11.1.0",
102
- "moflo": "^4.13.1",
102
+ "moflo": "^4.13.2",
103
103
  "tsx": "^4.21.0",
104
104
  "typescript": "^5.9.3",
105
105
  "vitest": "^4.0.0"