total-recall-brain 3.2.3 → 3.4.0

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 (66) hide show
  1. package/README.md +1 -1
  2. package/extension/background.js +14 -2
  3. package/extension/content-script.js +18 -1
  4. package/extension/lib/brain-client.js +86 -52
  5. package/extension/lib/preconfigured.js +5 -0
  6. package/extension/manifest.json +1 -1
  7. package/extension/options/options.html +1 -0
  8. package/extension/options/options.js +12 -6
  9. package/extension/popup/popup.html +1 -0
  10. package/extension/popup/popup.js +29 -4
  11. package/extension/sidepanel/sidepanel.html +1 -0
  12. package/extension/sidepanel/sidepanel.js +24 -6
  13. package/frontend/index.html +8 -0
  14. package/frontend/src/api.ts +1 -2
  15. package/frontend/src/pages/ChatPage.tsx +9 -6
  16. package/frontend/src/pages/IntegrationsPage.tsx +144 -3
  17. package/package.json +2 -2
  18. package/scaffold/.agent/skills/total-recall/SKILL.md +55 -6
  19. package/scaffold/.agent/skills/total-recall/memory-vault/invariants/operating-instructions.md +2 -2
  20. package/scaffold/.agent/skills/total-recall/skills/cli-agents/agents.yml +1 -6
  21. package/scaffold/.agent/skills/total-recall/skills/ssss/SKILL.md +19 -0
  22. package/scaffold/.agent/skills/total-recall/skills/ssss/references/ssss-spec.md +33 -5
  23. package/src/cli/forget.mjs +18 -12
  24. package/src/cli/ingest/index.mjs +3 -2
  25. package/src/cli/init.mjs +30 -0
  26. package/src/cli/map.mjs +2 -2
  27. package/src/cli/rebuild.mjs +1 -1
  28. package/src/cli/remember.mjs +21 -11
  29. package/src/cli/setup.mjs +10 -1
  30. package/src/cli/share.mjs +7 -2
  31. package/src/core/append-log.mjs +131 -0
  32. package/src/core/clarity-rewriter.mjs +10 -6
  33. package/src/core/conclusion-writer.mjs +3 -2
  34. package/src/core/config.mjs +6 -0
  35. package/src/core/conflict-detector.mjs +4 -2
  36. package/src/core/context-compiler.mjs +412 -0
  37. package/src/core/daemon-loop.mjs +15 -0
  38. package/src/core/dream.mjs +4 -4
  39. package/src/core/drift-detector.mjs +3 -3
  40. package/src/core/embeddings.mjs +60 -10
  41. package/src/core/fact-seeker.mjs +70 -44
  42. package/src/core/import-rules.mjs +3 -2
  43. package/src/core/inference-engine.mjs +5 -4
  44. package/src/core/migrate.mjs +4 -3
  45. package/src/core/optimizer.mjs +6 -5
  46. package/src/core/parallel-context.mjs +288 -0
  47. package/src/core/post-mortem.mjs +4 -3
  48. package/src/core/research-queue.mjs +4 -3
  49. package/src/core/runtime.mjs +2 -2
  50. package/src/core/sandbox.mjs +56 -26
  51. package/src/core/scheduler.mjs +6 -5
  52. package/src/core/schema.mjs +2 -2
  53. package/src/core/search.mjs +167 -101
  54. package/src/core/surface.mjs +203 -53
  55. package/src/core/validated-write.mjs +100 -0
  56. package/src/core/vault-cache.mjs +46 -18
  57. package/src/core/vault.mjs +17 -29
  58. package/src/core/vector-field.mjs +659 -0
  59. package/src/core/vector-store.mjs +85 -0
  60. package/src/server/api.mjs +4 -3
  61. package/src/server/index.mjs +13 -61
  62. package/src/server/rest.mjs +238 -29
  63. package/src/server/route-manifest.json +344 -77
  64. package/src/server/routes/memory.mjs +85 -39
  65. package/src/server/routes/sessions.mjs +6 -2
  66. package/src/server/routes/share.mjs +9 -2
@@ -1,79 +1,346 @@
1
1
  [
2
- { "method": "GET", "path": "/api/memory" },
3
- { "method": "GET", "path": "/api/memory/stats" },
4
- { "method": "GET", "path": "/api/memory/:slug" },
5
- { "method": "POST", "path": "/api/memory" },
6
- { "method": "PUT", "path": "/api/memory/:slug" },
7
- { "method": "PATCH", "path": "/api/memory/:slug" },
8
- { "method": "DELETE", "path": "/api/memory/:slug" },
9
- { "method": "GET", "path": "/api/keys" },
10
- { "method": "POST", "path": "/api/keys" },
11
- { "method": "DELETE", "path": "/api/keys/:id" },
12
- { "method": "GET", "path": "/api/sessions" },
13
- { "method": "GET", "path": "/api/sessions/:id" },
14
- { "method": "POST", "path": "/api/sessions/ingest" },
15
- { "method": "DELETE", "path": "/api/sessions/:id" },
16
- { "method": "POST", "path": "/api/memory/search/semantic" },
17
- { "method": "POST", "path": "/api/vault/compile" },
18
- { "method": "POST", "path": "/api/dream" },
19
- { "method": "GET", "path": "/api/vault/status" },
20
- { "method": "POST", "path": "/api/sandbox" },
21
- { "method": "GET", "path": "/api/config" },
22
- { "method": "GET", "path": "/v1/models" },
23
- { "method": "GET", "path": "/.well-known/total-recall.json" },
24
- { "method": "GET", "path": "/api" },
25
- { "method": "GET", "path": "/api/research" },
26
- { "method": "POST", "path": "/api/research" },
27
- { "method": "PATCH", "path": "/api/research/:id" },
28
- { "method": "DELETE", "path": "/api/research/:id" },
29
- { "method": "GET", "path": "/api/integrations/active" },
30
- { "method": "POST", "path": "/api/integrations/connect" },
31
- { "method": "GET", "path": "/api/import/rules" },
32
- { "method": "POST", "path": "/api/import/rules" },
33
- { "method": "GET", "path": "/api/brain/export" },
34
- { "method": "GET", "path": "/api/graph" },
35
- { "method": "GET", "path": "/api/conflicts" },
36
- { "method": "POST", "path": "/api/conflicts/resolve" },
37
- { "method": "GET", "path": "/api/files" },
38
- { "method": "GET", "path": "/api/skills" },
39
- { "method": "GET", "path": "/api/skills/search" },
40
- { "method": "POST", "path": "/api/skills/install" },
41
- { "method": "GET", "path": "/api/skills/:name" },
42
- { "method": "PUT", "path": "/api/skills/:name" },
43
- { "method": "DELETE", "path": "/api/skills/:name" },
44
- { "method": "GET", "path": "/api/logs/:type" },
45
- { "method": "GET", "path": "/api/tasks" },
46
- { "method": "POST", "path": "/api/tasks" },
47
- { "method": "GET", "path": "/api/config-json" },
48
- { "method": "POST", "path": "/api/config-json" },
49
- { "method": "GET", "path": "/api/usage" },
50
- { "method": "GET", "path": "/api/config/:name" },
51
- { "method": "PUT", "path": "/api/config/:name" },
52
- { "method": "POST", "path": "/api/capture/:source" },
53
- { "method": "POST", "path": "/auth/login" },
54
- { "method": "POST", "path": "/auth/logout" },
55
- { "method": "POST", "path": "/auth/change-password" },
56
- { "method": "GET", "path": "/auth/me" },
57
- { "method": "GET", "path": "/auth/status" },
58
- { "method": "POST", "path": "/auth/setup" },
59
- { "method": "GET", "path": "/api/tts/status" },
60
- { "method": "POST", "path": "/api/tts" },
61
- { "method": "GET", "path": "/api/instructions" },
62
- { "method": "PUT", "path": "/api/instructions" },
63
- { "method": "GET", "path": "/api/ssss" },
64
- { "method": "GET", "path": "/api/ssss/instructions" },
65
- { "method": "GET", "path": "/api/ssss/skill/ssss" },
66
- { "method": "GET", "path": "/api/ssss/spec" },
67
- { "method": "GET", "path": "/api/ssss/references" },
68
- { "method": "GET", "path": "/api/ssss/references/:name" },
69
- { "method": "GET", "path": "/api/scripts" },
70
- { "method": "GET", "path": "/api/scripts/:name" },
71
- { "method": "PUT", "path": "/api/scripts/:name" },
72
- { "method": "POST", "path": "/api/scripts/:name/run" },
73
- { "method": "GET", "path": "/api/brains" },
74
- { "method": "GET", "path": "/api/brains/:id/nodes" },
75
- { "method": "GET", "path": "/api/gemini-models" },
76
- { "method": "POST", "path": "/api/share" },
77
- { "method": "GET", "path": "/api/extension/download" },
78
- { "method": "GET", "path": "/api/extension/status" }
2
+ {
3
+ "method": "GET",
4
+ "path": "/.well-known/total-recall.json"
5
+ },
6
+ {
7
+ "method": "GET",
8
+ "path": "/api"
9
+ },
10
+ {
11
+ "method": "GET",
12
+ "path": "/api/brain/export"
13
+ },
14
+ {
15
+ "method": "GET",
16
+ "path": "/api/brains"
17
+ },
18
+ {
19
+ "method": "GET",
20
+ "path": "/api/brains/:id/nodes"
21
+ },
22
+ {
23
+ "method": "POST",
24
+ "path": "/api/capture/:source"
25
+ },
26
+ {
27
+ "method": "GET",
28
+ "path": "/api/config"
29
+ },
30
+ {
31
+ "method": "GET",
32
+ "path": "/api/config-json"
33
+ },
34
+ {
35
+ "method": "POST",
36
+ "path": "/api/config-json"
37
+ },
38
+ {
39
+ "method": "GET",
40
+ "path": "/api/config/:name"
41
+ },
42
+ {
43
+ "method": "PUT",
44
+ "path": "/api/config/:name"
45
+ },
46
+ {
47
+ "method": "GET",
48
+ "path": "/api/conflicts"
49
+ },
50
+ {
51
+ "method": "POST",
52
+ "path": "/api/conflicts/resolve"
53
+ },
54
+ {
55
+ "method": "POST",
56
+ "path": "/api/context"
57
+ },
58
+ {
59
+ "method": "GET",
60
+ "path": "/api/context/flash/health"
61
+ },
62
+ {
63
+ "method": "GET",
64
+ "path": "/api/context/preview"
65
+ },
66
+ {
67
+ "method": "POST",
68
+ "path": "/api/context/stream"
69
+ },
70
+ {
71
+ "method": "POST",
72
+ "path": "/api/dream"
73
+ },
74
+ {
75
+ "method": "GET",
76
+ "path": "/api/extension/download"
77
+ },
78
+ {
79
+ "method": "GET",
80
+ "path": "/api/extension/status"
81
+ },
82
+ {
83
+ "method": "POST",
84
+ "path": "/api/field/compile"
85
+ },
86
+ {
87
+ "method": "POST",
88
+ "path": "/api/field/sample"
89
+ },
90
+ {
91
+ "method": "GET",
92
+ "path": "/api/field/stats"
93
+ },
94
+ {
95
+ "method": "GET",
96
+ "path": "/api/files"
97
+ },
98
+ {
99
+ "method": "GET",
100
+ "path": "/api/gemini-models"
101
+ },
102
+ {
103
+ "method": "GET",
104
+ "path": "/api/graph"
105
+ },
106
+ {
107
+ "method": "GET",
108
+ "path": "/api/import/rules"
109
+ },
110
+ {
111
+ "method": "POST",
112
+ "path": "/api/import/rules"
113
+ },
114
+ {
115
+ "method": "GET",
116
+ "path": "/api/instructions"
117
+ },
118
+ {
119
+ "method": "PUT",
120
+ "path": "/api/instructions"
121
+ },
122
+ {
123
+ "method": "GET",
124
+ "path": "/api/integrations/active"
125
+ },
126
+ {
127
+ "method": "POST",
128
+ "path": "/api/integrations/connect"
129
+ },
130
+ {
131
+ "method": "GET",
132
+ "path": "/api/keys"
133
+ },
134
+ {
135
+ "method": "POST",
136
+ "path": "/api/keys"
137
+ },
138
+ {
139
+ "method": "DELETE",
140
+ "path": "/api/keys/:id"
141
+ },
142
+ {
143
+ "method": "GET",
144
+ "path": "/api/logs/:type"
145
+ },
146
+ {
147
+ "method": "GET",
148
+ "path": "/api/memory"
149
+ },
150
+ {
151
+ "method": "POST",
152
+ "path": "/api/memory"
153
+ },
154
+ {
155
+ "method": "DELETE",
156
+ "path": "/api/memory/:slug"
157
+ },
158
+ {
159
+ "method": "GET",
160
+ "path": "/api/memory/:slug"
161
+ },
162
+ {
163
+ "method": "PATCH",
164
+ "path": "/api/memory/:slug"
165
+ },
166
+ {
167
+ "method": "PUT",
168
+ "path": "/api/memory/:slug"
169
+ },
170
+ {
171
+ "method": "POST",
172
+ "path": "/api/memory/search/semantic"
173
+ },
174
+ {
175
+ "method": "GET",
176
+ "path": "/api/memory/stats"
177
+ },
178
+ {
179
+ "method": "GET",
180
+ "path": "/api/research"
181
+ },
182
+ {
183
+ "method": "POST",
184
+ "path": "/api/research"
185
+ },
186
+ {
187
+ "method": "DELETE",
188
+ "path": "/api/research/:id"
189
+ },
190
+ {
191
+ "method": "PATCH",
192
+ "path": "/api/research/:id"
193
+ },
194
+ {
195
+ "method": "POST",
196
+ "path": "/api/sandbox"
197
+ },
198
+ {
199
+ "method": "GET",
200
+ "path": "/api/scripts"
201
+ },
202
+ {
203
+ "method": "GET",
204
+ "path": "/api/scripts/:name"
205
+ },
206
+ {
207
+ "method": "PUT",
208
+ "path": "/api/scripts/:name"
209
+ },
210
+ {
211
+ "method": "POST",
212
+ "path": "/api/scripts/:name/run"
213
+ },
214
+ {
215
+ "method": "GET",
216
+ "path": "/api/sessions"
217
+ },
218
+ {
219
+ "method": "DELETE",
220
+ "path": "/api/sessions/:id"
221
+ },
222
+ {
223
+ "method": "GET",
224
+ "path": "/api/sessions/:id"
225
+ },
226
+ {
227
+ "method": "POST",
228
+ "path": "/api/sessions/ingest"
229
+ },
230
+ {
231
+ "method": "POST",
232
+ "path": "/api/share"
233
+ },
234
+ {
235
+ "method": "GET",
236
+ "path": "/api/skills"
237
+ },
238
+ {
239
+ "method": "DELETE",
240
+ "path": "/api/skills/:name"
241
+ },
242
+ {
243
+ "method": "GET",
244
+ "path": "/api/skills/:name"
245
+ },
246
+ {
247
+ "method": "PUT",
248
+ "path": "/api/skills/:name"
249
+ },
250
+ {
251
+ "method": "POST",
252
+ "path": "/api/skills/install"
253
+ },
254
+ {
255
+ "method": "GET",
256
+ "path": "/api/skills/search"
257
+ },
258
+ {
259
+ "method": "GET",
260
+ "path": "/api/ssss"
261
+ },
262
+ {
263
+ "method": "GET",
264
+ "path": "/api/ssss/instructions"
265
+ },
266
+ {
267
+ "method": "GET",
268
+ "path": "/api/ssss/references"
269
+ },
270
+ {
271
+ "method": "GET",
272
+ "path": "/api/ssss/references/:name"
273
+ },
274
+ {
275
+ "method": "GET",
276
+ "path": "/api/ssss/skill/ssss"
277
+ },
278
+ {
279
+ "method": "GET",
280
+ "path": "/api/ssss/spec"
281
+ },
282
+ {
283
+ "method": "GET",
284
+ "path": "/api/tasks"
285
+ },
286
+ {
287
+ "method": "POST",
288
+ "path": "/api/tasks"
289
+ },
290
+ {
291
+ "method": "POST",
292
+ "path": "/api/tts"
293
+ },
294
+ {
295
+ "method": "GET",
296
+ "path": "/api/tts/status"
297
+ },
298
+ {
299
+ "method": "GET",
300
+ "path": "/api/usage"
301
+ },
302
+ {
303
+ "method": "POST",
304
+ "path": "/api/vault/compact"
305
+ },
306
+ {
307
+ "method": "POST",
308
+ "path": "/api/vault/compile"
309
+ },
310
+ {
311
+ "method": "GET",
312
+ "path": "/api/vault/hash"
313
+ },
314
+ {
315
+ "method": "GET",
316
+ "path": "/api/vault/status"
317
+ },
318
+ {
319
+ "method": "POST",
320
+ "path": "/auth/change-password"
321
+ },
322
+ {
323
+ "method": "POST",
324
+ "path": "/auth/login"
325
+ },
326
+ {
327
+ "method": "POST",
328
+ "path": "/auth/logout"
329
+ },
330
+ {
331
+ "method": "GET",
332
+ "path": "/auth/me"
333
+ },
334
+ {
335
+ "method": "POST",
336
+ "path": "/auth/setup"
337
+ },
338
+ {
339
+ "method": "GET",
340
+ "path": "/auth/status"
341
+ },
342
+ {
343
+ "method": "GET",
344
+ "path": "/v1/models"
345
+ }
79
346
  ]
@@ -13,7 +13,8 @@
13
13
  import express from 'express';
14
14
  import fs from 'node:fs';
15
15
  import path from 'node:path';
16
- import { writeNode, createMemoryNode, walkMd } from '../../core/vault.mjs';
16
+ import { createMemoryNode, walkMd } from '../../core/vault.mjs';
17
+ import { writeNodeValidated } from '../../core/validated-write.mjs';
17
18
  import { getNodes, invalidate } from '../../core/vault-cache.mjs';
18
19
  import { requireAuth, requireScope } from '../auth.mjs';
19
20
  import { compileSurface } from '../../core/surface.mjs';
@@ -35,6 +36,10 @@ import {
35
36
 
36
37
  const router = express.Router();
37
38
 
39
+ /* ── OPT-6: debounce recompilation across rapid consecutive writes ── */
40
+ let recompileTimer = null;
41
+ const RECOMPILE_DEBOUNCE_MS = 2000; // 2-second quiet period
42
+
38
43
  function nodes(vaultDir = VAULT_DIR) {
39
44
  return getNodes(vaultDir);
40
45
  }
@@ -44,51 +49,70 @@ function nodes(vaultDir = VAULT_DIR) {
44
49
  * Auto-mutates the brain in real time when any fact is written, updated, or deleted!
45
50
  */
46
51
  async function triggerMutation(node, vaultDir = VAULT_DIR) {
52
+ // 1. Semantic conflict detection runs IMMEDIATELY (lightweight, per-node)
47
53
  try {
48
- // 1. Semantic conflict detection & auto-resolution (Sovereign OS Intelligence)
49
- try {
50
- const existing = nodes(vaultDir);
51
- if (node && node.type === 'memory') {
52
- detectAndResolve(node, existing, {
53
- vaultDir,
54
- inboxDir: path.join(BRAIN_DIR, 'memory-inbox'),
55
- });
56
- }
57
- } catch (conflictErr) {
58
- // Non-fatal fallback
54
+ const existing = nodes(vaultDir);
55
+ if (node && node.type === 'memory') {
56
+ detectAndResolve(node, existing, {
57
+ vaultDir,
58
+ inboxDir: path.join(BRAIN_DIR, 'memory-inbox'),
59
+ });
59
60
  }
61
+ } catch (conflictErr) {
62
+ // Non-fatal fallback
63
+ }
60
64
 
61
- // 2. Recompile instructions surface
62
- await compileSurface({
63
- vaultDir,
64
- skillsDir: SKILLS_DIR,
65
- derivedDir: DERIVED_DIR,
66
- instructionsFile: INSTRUCTIONS,
67
- });
68
-
69
- // 3. Rebuild dense embeddings index incrementally in background
65
+ // 2+3. Debounce the EXPENSIVE recompile + embeddings rebuild.
66
+ // Rapid consecutive writes accumulate; only ONE compile fires
67
+ // after a quiet period. Vault-cache is already invalidated
68
+ // immediately at each call-site via invalidate().
69
+ if (recompileTimer) clearTimeout(recompileTimer);
70
+ recompileTimer = setTimeout(async () => {
71
+ recompileTimer = null;
70
72
  try {
71
- const vaultNodes = nodes(vaultDir);
72
- await buildEmbeddingsIndex(vaultNodes, DERIVED_DIR);
73
- await buildSessionEmbeddingsIndex(SESSIONS_DIR, DERIVED_DIR);
74
- } catch (embedErr) {
75
- // Ollama/embeddings offline non-fatal
73
+ // Recompile instructions surface
74
+ await compileSurface({
75
+ vaultDir,
76
+ skillsDir: SKILLS_DIR,
77
+ derivedDir: DERIVED_DIR,
78
+ instructionsFile: INSTRUCTIONS,
79
+ });
80
+
81
+ // Rebuild dense embeddings index
82
+ try {
83
+ const vaultNodes = nodes(vaultDir);
84
+ await buildEmbeddingsIndex(vaultNodes, DERIVED_DIR);
85
+ await buildSessionEmbeddingsIndex(SESSIONS_DIR, DERIVED_DIR);
86
+ } catch (embedErr) {
87
+ // Ollama/embeddings offline non-fatal
88
+ }
89
+ } catch (err) {
90
+ // Non-fatal background log
76
91
  }
77
- } catch (err) {
78
- // Non-fatal background log
79
- }
92
+ }, RECOMPILE_DEBOUNCE_MS);
80
93
  }
81
94
 
82
95
 
83
96
  // SSSS v2 frontmatter fields we pass through verbatim from request bodies.
84
- const PASSTHROUGH_FIELDS = ['priority', 'modality', 'confidence', 'importance', 'status', 'related', 'sources'];
97
+ const PASSTHROUGH_FIELDS = [
98
+ 'priority',
99
+ 'modality',
100
+ 'confidence',
101
+ 'importance',
102
+ 'status',
103
+ 'related',
104
+ 'sources',
105
+ 'supersedes',
106
+ 'superseded_by',
107
+ 'contradicts',
108
+ ];
85
109
 
86
110
  router.get('/api/memory', requireAuth, requireScope('memory:read'), (req, res) => {
87
111
  try {
88
112
  const vaultDir = resolveVaultFromQuery(req);
89
113
  let list = nodes(vaultDir);
90
114
 
91
- const { q, category, tag, limit = '200', offset = '0' } = req.query;
115
+ const { q, category, tag, status, limit = '200', offset = '0' } = req.query;
92
116
 
93
117
  if (q) {
94
118
  const query = String(q).toLowerCase();
@@ -98,6 +122,7 @@ router.get('/api/memory', requireAuth, requireScope('memory:read'), (req, res) =
98
122
  );
99
123
  }
100
124
  if (category) list = list.filter(n => n.category === category);
125
+ if (status) list = list.filter(n => n.status === status);
101
126
  if (tag) list = list.filter(n => (n.tags || []).includes(tag));
102
127
 
103
128
  const total = list.length;
@@ -154,8 +179,15 @@ router.post('/api/memory', requireAuth, requireScope('memory:write'), (req, res)
154
179
  if (req.body[key] !== undefined) node[key] = req.body[key];
155
180
  }
156
181
 
157
- writeNode(node, vaultDir);
158
- invalidate();
182
+ const vaultResult = writeNodeValidated(node, vaultDir);
183
+ if (!vaultResult.success) {
184
+ return res.status(422).json({
185
+ error: 'Validation failed',
186
+ validation: vaultResult.validation,
187
+ repair: vaultResult.repair,
188
+ });
189
+ }
190
+ invalidate(vaultDir);
159
191
  triggerMutation(node, vaultDir);
160
192
  res.status(201).json(sanitizeNode(node));
161
193
  } catch (err) {
@@ -178,8 +210,15 @@ router.put('/api/memory/:slug', requireAuth, requireScope('memory:write'), (req,
178
210
  if (req.body[key] !== undefined) node[key] = req.body[key];
179
211
  }
180
212
 
181
- writeNode(node, vaultDir);
182
- invalidate();
213
+ const vaultResult = writeNodeValidated(node, vaultDir);
214
+ if (!vaultResult.success) {
215
+ return res.status(422).json({
216
+ error: 'Validation failed',
217
+ validation: vaultResult.validation,
218
+ repair: vaultResult.repair,
219
+ });
220
+ }
221
+ invalidate(vaultDir);
183
222
  triggerMutation(node, vaultDir);
184
223
  res.json(sanitizeNode(node));
185
224
  } catch (err) {
@@ -202,7 +241,7 @@ router.patch('/api/memory/:slug', requireAuth, requireScope('memory:write'), (re
202
241
  content: actualContent,
203
242
  });
204
243
  updated.tags = tags ?? existing.tags ?? [];
205
- updated.created_at = existing.created_at;
244
+ updated.created = existing.created;
206
245
 
207
246
  for (const key of PASSTHROUGH_FIELDS) {
208
247
  if (existing[key] !== undefined) updated[key] = existing[key];
@@ -211,8 +250,15 @@ router.patch('/api/memory/:slug', requireAuth, requireScope('memory:write'), (re
211
250
  if (req.body[key] !== undefined) updated[key] = req.body[key];
212
251
  }
213
252
 
214
- writeNode(updated, vaultDir);
215
- invalidate();
253
+ const vaultResult = writeNodeValidated(updated, vaultDir);
254
+ if (!vaultResult.success) {
255
+ return res.status(422).json({
256
+ error: 'Validation failed',
257
+ validation: vaultResult.validation,
258
+ repair: vaultResult.repair,
259
+ });
260
+ }
261
+ invalidate(vaultDir);
216
262
  triggerMutation(updated, vaultDir);
217
263
  res.json(sanitizeNode(updated));
218
264
  } catch (err) {
@@ -238,7 +284,7 @@ router.delete('/api/memory/:slug', requireAuth, requireScope('memory:write'), (r
238
284
  }
239
285
  }
240
286
  }
241
- invalidate();
287
+ invalidate(vaultDir);
242
288
  triggerMutation(null, vaultDir);
243
289
  res.json({ deleted: true, slug: req.params.slug });
244
290
  } catch (err) {
@@ -205,7 +205,12 @@ router.get('/api/sessions/:id', requireAuth, requireScope('memory:read'), (req,
205
205
  router.post('/api/sessions/ingest', ingestRateLimiter(), requireAuth, requireScope('memory:write'), (req, res) => {
206
206
  try {
207
207
  const body = req.body || {};
208
- const source = body.source || 'api';
208
+ const source = String(body.source || 'api')
209
+ .toLowerCase()
210
+ .replace(/[^a-z0-9_-]/g, '-')
211
+ .replace(/-+/g, '-')
212
+ .replace(/^-|-$/g, '')
213
+ .slice(0, 48) || 'api';
209
214
 
210
215
  let messages;
211
216
  if (body.content && typeof body.content === 'string') {
@@ -333,4 +338,3 @@ router.delete('/api/sessions/:id', requireAuth, requireScope('memory:write'), (r
333
338
  });
334
339
 
335
340
  export { router as sessionsRouter, drainActiveEmbeddings };
336
-
@@ -8,7 +8,8 @@
8
8
 
9
9
  import express from 'express';
10
10
  import crypto from 'node:crypto';
11
- import { createMemoryNode, writeNode } from '../../core/vault.mjs';
11
+ import { createMemoryNode } from '../../core/vault.mjs';
12
+ import { writeNodeValidated } from '../../core/validated-write.mjs';
12
13
  import { invalidate } from '../../core/vault-cache.mjs';
13
14
  import { addToQueue } from '../../core/research-queue.mjs';
14
15
  import { requireAuth, requireScope } from '../auth.mjs';
@@ -121,7 +122,13 @@ router.post('/api/share', requireAuth, requireScope('memory:write'), async (req,
121
122
  }];
122
123
  }
123
124
 
124
- writeNode(node, vaultDir);
125
+ const vaultResult = writeNodeValidated(node, vaultDir);
126
+ if (!vaultResult.success) {
127
+ return res.status(422).json({
128
+ error: 'Validation failed',
129
+ validation: vaultResult.validation,
130
+ });
131
+ }
125
132
  invalidate();
126
133
 
127
134
  // Mark extension as connected when share comes from the extension