@wipcomputer/memory-crystal 0.7.11 → 0.7.13

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/CHANGELOG.md CHANGED
@@ -10,6 +10,176 @@
10
10
 
11
11
 
12
12
 
13
+
14
+
15
+ ## 0.7.13 (2026-03-13)
16
+
17
+ # Dev Update: Orphan Cleanup, DELETE Trigger, Doctor Fix
18
+
19
+ **Date:** 2026-03-13
20
+ **Author:** CC-Mini
21
+ **Session:** memory-db-fix
22
+
23
+ ---
24
+
25
+ ## What Happened
26
+
27
+ Parker ran the Memory Crystal install prompt and `crystal doctor` reported "Embeddings: FAILING ... no provider configured in env." Investigation revealed two separate issues:
28
+
29
+ ### Issue 1: Doctor False Positive
30
+
31
+ `checkEmbeddingProvider()` in `doctor.ts` only checked `process.env.OPENAI_API_KEY`. But the cron job and hooks resolve the key from 1Password via the SA token at `~/.openclaw/secrets/op-sa-token`. The doctor didn't know about this path.
32
+
33
+ **Fix:** Added `checkOpEmbeddings()` helper to `doctor.ts` that checks for the SA token file, then does a live `op read` to verify it works. Doctor now reports `ok: openai (via 1Password)` instead of `fail`.
34
+
35
+ ### Issue 2: Orphaned Vectors and FTS Entries
36
+
37
+ On 2026-03-11, 141,652 bulk file-scan chunks were correctly deleted from the `chunks` table. Parker said: "Why are we indexing documents?" These were raw file scans (Python venv packages, TypeScript source, vendor code) with no conversational context.
38
+
39
+ The deletion used raw SQL (`DELETE FROM chunks WHERE agent_id = 'system'`). But Memory Crystal had no DELETE trigger. The corresponding entries in `chunks_vec` (sqlite-vec) and `chunks_fts` (FTS5) were left orphaned.
40
+
41
+ **Impact:**
42
+ - 141,651 orphaned vectors (~875 MB)
43
+ - 141,652 orphaned FTS entries
44
+ - ~7% of search queries hit phantom results (silently filtered out)
45
+ - Database: 1.96 GB (should have been ~1 GB)
46
+
47
+ **Fix (three parts):**
48
+
49
+ 1. **DELETE trigger** added to `initChunksTables()` in `core.ts`:
50
+ ```sql
51
+ CREATE TRIGGER IF NOT EXISTS chunks_cleanup AFTER DELETE ON chunks
52
+ BEGIN
53
+ DELETE FROM chunks_vec WHERE chunk_id = OLD.id;
54
+ INSERT INTO chunks_fts(chunks_fts, rowid, text) VALUES('delete', OLD.id, OLD.text);
55
+ END;
56
+ ```
57
+
58
+ 2. **`cleanOrphans()` method** added to Crystal class in `core.ts`. Counts orphaned vec/FTS entries, deletes vec orphans in batches of 1000, rebuilds FTS5 from scratch.
59
+
60
+ 3. **`crystal cleanup` CLI command** added to `cli.ts`. Handles the full workflow: backup, pause cron, clean orphans, VACUUM, resume cron. Supports `--dry-run`.
61
+
62
+ **Cleanup results:**
63
+ - 141,651 orphaned vectors removed
64
+ - FTS rebuilt from 73,813 chunks in 5.7s
65
+ - Database: 1.96 GB -> 1.45 GB (525 MB saved)
66
+ - Verification: chunks = FTS entries = 73,813. Match: YES
67
+ - Zero orphans remaining
68
+
69
+ ### Side Discovery: Plaintext SA Token
70
+
71
+ During investigation, discovered that `~/.openclaw/secrets/op-sa-token` is a plaintext 1Password SA token readable by any process running as `lesa`. This is the bootstrap credential that unlocks all secrets. Bug report filed. Long-term fix: Lesa iOS app with remote biometrics (product doc written).
72
+
73
+ ### Product Rule Established
74
+
75
+ Memory Crystal indexes conversations only. File content that appears in conversation turns (agent reads a file, discusses it) is captured as part of the conversation. Raw directory scanning without conversational context is not what Memory Crystal is for.
76
+
77
+ ## Files Changed
78
+
79
+ | File | Change |
80
+ |------|--------|
81
+ | `src/core.ts` | Added `chunks_cleanup` DELETE trigger, `cleanOrphans()` method |
82
+ | `src/cli.ts` | Added `crystal cleanup` command, updated imports and USAGE |
83
+ | `src/doctor.ts` | Added `checkOpEmbeddings()` for 1Password detection |
84
+ | `ai/product/bugs/2026-03-13--orphaned-vectors-and-fts-after-bulk-delete.md` | Bug report |
85
+
86
+ ## Related (wip-secrets-ios-private)
87
+
88
+ | File | What |
89
+ |------|------|
90
+ | `ai/product/product-ideas/lesa-app-remote-biometrics.md` | Lesa app: remote biometrics for agent computers |
91
+ | `ai/product/bugs/2026-03-13--plaintext-sa-token-on-disk.md` | Plaintext SA token bug report |
92
+
93
+ ## Status
94
+
95
+ - Code deployed and running (cleanup already executed)
96
+ - Not yet committed / PR'd / released
97
+ - Needs: branch, commit, PR, merge, `wip-release patch`
98
+
99
+ ## 0.7.12 (2026-03-13)
100
+
101
+ # Dev Update: Orphan Cleanup, DELETE Trigger, Doctor Fix
102
+
103
+ **Date:** 2026-03-13
104
+ **Author:** CC-Mini
105
+ **Session:** memory-db-fix
106
+
107
+ ---
108
+
109
+ ## What Happened
110
+
111
+ Parker ran the Memory Crystal install prompt and `crystal doctor` reported "Embeddings: FAILING ... no provider configured in env." Investigation revealed two separate issues:
112
+
113
+ ### Issue 1: Doctor False Positive
114
+
115
+ `checkEmbeddingProvider()` in `doctor.ts` only checked `process.env.OPENAI_API_KEY`. But the cron job and hooks resolve the key from 1Password via the SA token at `~/.openclaw/secrets/op-sa-token`. The doctor didn't know about this path.
116
+
117
+ **Fix:** Added `checkOpEmbeddings()` helper to `doctor.ts` that checks for the SA token file, then does a live `op read` to verify it works. Doctor now reports `ok: openai (via 1Password)` instead of `fail`.
118
+
119
+ ### Issue 2: Orphaned Vectors and FTS Entries
120
+
121
+ On 2026-03-11, 141,652 bulk file-scan chunks were correctly deleted from the `chunks` table. Parker said: "Why are we indexing documents?" These were raw file scans (Python venv packages, TypeScript source, vendor code) with no conversational context.
122
+
123
+ The deletion used raw SQL (`DELETE FROM chunks WHERE agent_id = 'system'`). But Memory Crystal had no DELETE trigger. The corresponding entries in `chunks_vec` (sqlite-vec) and `chunks_fts` (FTS5) were left orphaned.
124
+
125
+ **Impact:**
126
+ - 141,651 orphaned vectors (~875 MB)
127
+ - 141,652 orphaned FTS entries
128
+ - ~7% of search queries hit phantom results (silently filtered out)
129
+ - Database: 1.96 GB (should have been ~1 GB)
130
+
131
+ **Fix (three parts):**
132
+
133
+ 1. **DELETE trigger** added to `initChunksTables()` in `core.ts`:
134
+ ```sql
135
+ CREATE TRIGGER IF NOT EXISTS chunks_cleanup AFTER DELETE ON chunks
136
+ BEGIN
137
+ DELETE FROM chunks_vec WHERE chunk_id = OLD.id;
138
+ INSERT INTO chunks_fts(chunks_fts, rowid, text) VALUES('delete', OLD.id, OLD.text);
139
+ END;
140
+ ```
141
+
142
+ 2. **`cleanOrphans()` method** added to Crystal class in `core.ts`. Counts orphaned vec/FTS entries, deletes vec orphans in batches of 1000, rebuilds FTS5 from scratch.
143
+
144
+ 3. **`crystal cleanup` CLI command** added to `cli.ts`. Handles the full workflow: backup, pause cron, clean orphans, VACUUM, resume cron. Supports `--dry-run`.
145
+
146
+ **Cleanup results:**
147
+ - 141,651 orphaned vectors removed
148
+ - FTS rebuilt from 73,813 chunks in 5.7s
149
+ - Database: 1.96 GB -> 1.45 GB (525 MB saved)
150
+ - Verification: chunks = FTS entries = 73,813. Match: YES
151
+ - Zero orphans remaining
152
+
153
+ ### Side Discovery: Plaintext SA Token
154
+
155
+ During investigation, discovered that `~/.openclaw/secrets/op-sa-token` is a plaintext 1Password SA token readable by any process running as `lesa`. This is the bootstrap credential that unlocks all secrets. Bug report filed. Long-term fix: Lesa iOS app with remote biometrics (product doc written).
156
+
157
+ ### Product Rule Established
158
+
159
+ Memory Crystal indexes conversations only. File content that appears in conversation turns (agent reads a file, discusses it) is captured as part of the conversation. Raw directory scanning without conversational context is not what Memory Crystal is for.
160
+
161
+ ## Files Changed
162
+
163
+ | File | Change |
164
+ |------|--------|
165
+ | `src/core.ts` | Added `chunks_cleanup` DELETE trigger, `cleanOrphans()` method |
166
+ | `src/cli.ts` | Added `crystal cleanup` command, updated imports and USAGE |
167
+ | `src/doctor.ts` | Added `checkOpEmbeddings()` for 1Password detection |
168
+ | `ai/product/bugs/2026-03-13--orphaned-vectors-and-fts-after-bulk-delete.md` | Bug report |
169
+
170
+ ## Related (wip-secrets-ios-private)
171
+
172
+ | File | What |
173
+ |------|------|
174
+ | `ai/product/product-ideas/lesa-app-remote-biometrics.md` | Lesa app: remote biometrics for agent computers |
175
+ | `ai/product/bugs/2026-03-13--plaintext-sa-token-on-disk.md` | Plaintext SA token bug report |
176
+
177
+ ## Status
178
+
179
+ - Code deployed and running (cleanup already executed)
180
+ - Not yet committed / PR'd / released
181
+ - Needs: branch, commit, PR, merge, `wip-release patch`
182
+
13
183
  ## 0.7.11 (2026-03-13)
14
184
 
15
185
  # Dev Update: Orphan Cleanup, DELETE Trigger, Doctor Fix
package/dist/installer.js CHANGED
@@ -38,7 +38,7 @@ function getRepoRoot() {
38
38
  if (existsSync(pkgPath)) {
39
39
  try {
40
40
  const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
41
- if (pkg.name === "memory-crystal") return dir;
41
+ if (pkg.name === "@wipcomputer/memory-crystal") return dir;
42
42
  } catch {
43
43
  }
44
44
  }
@@ -46,13 +46,26 @@ function getRepoRoot() {
46
46
  }
47
47
  return dirname(thisDir);
48
48
  }
49
+ function getLatestNpmVersion() {
50
+ const names = ["@wipcomputer/memory-crystal", "memory-crystal"];
51
+ for (const name of names) {
52
+ try {
53
+ const v = execSync(`npm view ${name} version 2>/dev/null`, { encoding: "utf-8", timeout: 1e4 }).trim();
54
+ if (v) return v;
55
+ } catch {
56
+ }
57
+ }
58
+ return null;
59
+ }
49
60
  function detectInstallState() {
50
61
  const ldmExtDir = join(LDM_ROOT, "extensions", "memory-crystal");
51
62
  const ocExtDir = join(OC_ROOT, "extensions", "memory-crystal");
52
63
  const paths = ldmPaths();
53
64
  const installedVersion = readVersion(join(ldmExtDir, "package.json"));
54
65
  const repoRoot = getRepoRoot();
55
- const repoVersion = readVersion(join(repoRoot, "package.json")) || "0.0.0";
66
+ let repoVersion = readVersion(join(repoRoot, "package.json")) || "0.0.0";
67
+ const npmVersion = getLatestNpmVersion();
68
+ if (npmVersion && npmVersion > repoVersion) repoVersion = npmVersion;
56
69
  const ccHookDeployed = existsSync(join(ldmExtDir, "dist", "cc-hook.js"));
57
70
  let ccHookConfigured = false;
58
71
  try {
@@ -355,6 +368,18 @@ function ldmCliAvailable() {
355
368
  return false;
356
369
  }
357
370
  }
371
+ function bootstrapLdmOs(steps) {
372
+ try {
373
+ steps.push("Installing LDM OS infrastructure...");
374
+ execSync("npm install -g @wipcomputer/wip-ldm-os", { stdio: "pipe", timeout: 12e4 });
375
+ execSync("ldm --version", { stdio: "pipe", timeout: 5e3 });
376
+ steps.push("LDM OS installed.");
377
+ return true;
378
+ } catch {
379
+ steps.push("LDM OS install skipped (npm offline or permissions issue). Using standalone.");
380
+ return false;
381
+ }
382
+ }
358
383
  function runLdmInstall(repoDir) {
359
384
  const steps = [];
360
385
  try {
@@ -393,7 +418,25 @@ async function runInstallOrUpdate(options) {
393
418
  steps: [`Already at v${state.repoVersion}. Nothing to do.`]
394
419
  };
395
420
  }
396
- const hasLdmCli = ldmCliAvailable();
421
+ if (isUpdate && state.installedVersion) {
422
+ const npmV = getLatestNpmVersion();
423
+ if (npmV && npmV > state.installedVersion) {
424
+ steps.push(`Upgrading v${state.installedVersion} -> v${npmV} via npm...`);
425
+ try {
426
+ execSync("npm install -g @wipcomputer/memory-crystal 2>&1", { encoding: "utf-8", timeout: 6e4, stdio: "pipe" });
427
+ steps.push(`Installed @wipcomputer/memory-crystal@${npmV}`);
428
+ steps.push("Restarting init with updated code...");
429
+ execSync("crystal init", { stdio: "inherit", timeout: 12e4 });
430
+ return { action: "updated", version: npmV, deployedTo: ["global", "ldm", "openclaw"], steps };
431
+ } catch (err) {
432
+ steps.push(`npm upgrade failed: ${err.message}. Continuing with local code.`);
433
+ }
434
+ }
435
+ }
436
+ let hasLdmCli = ldmCliAvailable();
437
+ if (!hasLdmCli) {
438
+ hasLdmCli = bootstrapLdmOs(steps);
439
+ }
397
440
  let ldmDelegated = false;
398
441
  if (hasLdmCli) {
399
442
  steps.push("LDM OS detected. Using ldm install for deployment...");
@@ -590,7 +633,7 @@ async function runInstallOrUpdate(options) {
590
633
  }
591
634
  if (hasLdmCli) {
592
635
  steps.push('Tip: Run "ldm install" to see more components you can add.');
593
- } else {
636
+ } else if (!ldmDelegated) {
594
637
  steps.push("Tip: Install LDM OS for more components: npm install -g @wipcomputer/wip-ldm-os");
595
638
  }
596
639
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/memory-crystal",
3
- "version": "0.7.11",
3
+ "version": "0.7.13",
4
4
  "description": "Sovereign memory system — local-first with ephemeral encrypted relay. Your memory, your machine, your rules.",
5
5
  "type": "module",
6
6
  "main": "dist/core.js",
@@ -86,6 +86,24 @@ Before installing, explain exactly what will be created:
86
86
  >
87
87
  > Nothing gets installed outside this folder. Nothing phones home. Want me to go ahead?"
88
88
 
89
+ ### Step 0: Upgrade if already installed
90
+
91
+ If `crystal` is already on PATH, check if it's current before running init:
92
+
93
+ ```bash
94
+ crystal status 2>/dev/null | head -1
95
+ npm view @wipcomputer/memory-crystal version 2>/dev/null
96
+ ```
97
+
98
+ If npm version is newer, upgrade first. If installed under the old unscoped name (`memory-crystal`), switch to the scoped name:
99
+
100
+ ```bash
101
+ npm uninstall -g memory-crystal 2>/dev/null
102
+ npm install -g @wipcomputer/memory-crystal
103
+ ```
104
+
105
+ Then proceed to Step 1.
106
+
89
107
  ### Step 1: Initialize
90
108
 
91
109
  ```bash