ltcai 6.7.0 → 7.1.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 (65) hide show
  1. package/README.md +29 -31
  2. package/docs/CHANGELOG.md +48 -0
  3. package/frontend/src/App.tsx +80 -0
  4. package/frontend/src/api/client.ts +11 -0
  5. package/frontend/src/components/AdminAccessGate.tsx +70 -0
  6. package/frontend/src/components/BrainConversation.tsx +232 -8
  7. package/frontend/src/components/FeedbackState.tsx +45 -0
  8. package/frontend/src/components/WorkspaceProfileSwitcher.tsx +176 -0
  9. package/frontend/src/components/onboarding/AnalysisScreen.tsx +20 -12
  10. package/frontend/src/components/onboarding/InstallScreen.tsx +55 -0
  11. package/frontend/src/components/onboarding/RecommendationScreen.tsx +55 -5
  12. package/frontend/src/components/onboarding/recommendationModel.ts +59 -2
  13. package/frontend/src/features/brain/BrainConversation.tsx +382 -7
  14. package/frontend/src/features/brain/BrainGraphLayer.tsx +258 -25
  15. package/frontend/src/features/brain/BrainHome.tsx +300 -1
  16. package/frontend/src/features/brain/brainData.ts +25 -1
  17. package/frontend/src/features/brain/graphLayout.ts +12 -1
  18. package/frontend/src/features/brain/types.ts +54 -0
  19. package/frontend/src/i18n.ts +374 -0
  20. package/frontend/src/store/appStore.ts +15 -0
  21. package/frontend/src/styles.css +1383 -41
  22. package/lattice_brain/__init__.py +1 -1
  23. package/lattice_brain/runtime/multi_agent.py +1 -1
  24. package/latticeai/__init__.py +1 -1
  25. package/latticeai/api/workspace.py +59 -0
  26. package/latticeai/core/marketplace.py +1 -1
  27. package/latticeai/core/workspace_os.py +1 -1
  28. package/package.json +1 -1
  29. package/scripts/brain_quality_eval.py +118 -0
  30. package/src-tauri/Cargo.lock +1 -1
  31. package/src-tauri/Cargo.toml +1 -1
  32. package/src-tauri/tauri.conf.json +1 -1
  33. package/static/app/asset-manifest.json +28 -28
  34. package/static/app/assets/Act-BHEb8bAM.js +2 -0
  35. package/static/app/assets/{Act-DGN37eR4.js.map → Act-BHEb8bAM.js.map} +1 -1
  36. package/static/app/assets/Brain-yvR7xkrV.js +322 -0
  37. package/static/app/assets/Brain-yvR7xkrV.js.map +1 -0
  38. package/static/app/assets/Capture-BrAmsSEH.js +2 -0
  39. package/static/app/assets/Capture-BrAmsSEH.js.map +1 -0
  40. package/static/app/assets/Library-BOVzYfxI.js +2 -0
  41. package/static/app/assets/{Library-BbagRFyd.js.map → Library-BOVzYfxI.js.map} +1 -1
  42. package/static/app/assets/System-D4WaN4kj.js +2 -0
  43. package/static/app/assets/System-D4WaN4kj.js.map +1 -0
  44. package/static/app/assets/index-BOB-W1FZ.js +17 -0
  45. package/static/app/assets/index-BOB-W1FZ.js.map +1 -0
  46. package/static/app/assets/index-DvLsGy-z.css +2 -0
  47. package/static/app/assets/primitives-C3_BfUb8.js +2 -0
  48. package/static/app/assets/primitives-C3_BfUb8.js.map +1 -0
  49. package/static/app/assets/textarea-CbvhHvzg.js +2 -0
  50. package/static/app/assets/{textarea-C2PLu6_O.js.map → textarea-CbvhHvzg.js.map} +1 -1
  51. package/static/app/index.html +2 -2
  52. package/static/app/assets/Act-DGN37eR4.js +0 -2
  53. package/static/app/assets/Brain-HDcIwPHW.js +0 -322
  54. package/static/app/assets/Brain-HDcIwPHW.js.map +0 -1
  55. package/static/app/assets/Capture--eZfb_Ex.js +0 -2
  56. package/static/app/assets/Capture--eZfb_Ex.js.map +0 -1
  57. package/static/app/assets/Library-BbagRFyd.js +0 -2
  58. package/static/app/assets/System-C6pd3Mp_.js +0 -2
  59. package/static/app/assets/System-C6pd3Mp_.js.map +0 -1
  60. package/static/app/assets/index-C950aES_.js +0 -17
  61. package/static/app/assets/index-C950aES_.js.map +0 -1
  62. package/static/app/assets/index-ztppMg0c.css +0 -2
  63. package/static/app/assets/primitives-Bcm5lZRu.js +0 -2
  64. package/static/app/assets/primitives-Bcm5lZRu.js.map +0 -1
  65. package/static/app/assets/textarea-C2PLu6_O.js +0 -2
@@ -7,7 +7,19 @@ import { t } from "@/i18n";
7
7
  import { BrainConversation } from "./BrainConversation";
8
8
  import { buildBrainProof, buildBrainReadiness, buildMemoryFragments, currentModelName, parseKnowledgeGraph } from "./brainData";
9
9
  import { DepthEmergence } from "./DepthEmergence";
10
- import { DEPTHS, type BrainDepth, type MemoryFragment, type Message } from "./types";
10
+ import {
11
+ DEPTHS,
12
+ INGESTION_STAGE_ORDER,
13
+ type BrainDepth,
14
+ type BrainProof,
15
+ type EmergenceEvent,
16
+ type IngestionPipelineStage,
17
+ type IngestionSourceType,
18
+ type IngestionState,
19
+ type MemoryFragment,
20
+ type Message,
21
+ type MessageProof,
22
+ } from "./types";
11
23
 
12
24
  export function BrainHome({
13
25
  brainState,
@@ -31,8 +43,27 @@ export function BrainHome({
31
43
  const [memoryFeedback, setMemoryFeedback] = React.useState<string | null>(null);
32
44
  const [uploadingDocument, setUploadingDocument] = React.useState(false);
33
45
  const [lastRecallQuery, setLastRecallQuery] = React.useState("");
46
+ const [ingestionStates, setIngestionStates] = React.useState<Record<IngestionSourceType, IngestionState | null>>({
47
+ file: null,
48
+ folder: null,
49
+ note: null,
50
+ web: null,
51
+ });
52
+ const [emergenceEvents, setEmergenceEvents] = React.useState<EmergenceEvent[]>([]);
34
53
  const streamRef = React.useRef<HTMLDivElement>(null);
35
54
  const recallTimerRef = React.useRef<number | null>(null);
55
+ const stageTimersRef = React.useRef<Record<IngestionSourceType, number[]>>({
56
+ file: [],
57
+ folder: [],
58
+ note: [],
59
+ web: [],
60
+ });
61
+ const pendingBaselineRef = React.useRef<
62
+ Partial<Record<IngestionSourceType, { memories: number; entities: number; label: string }>>
63
+ >({});
64
+ // Source types whose request has resolved and are awaiting count settle to record emergence.
65
+ const awaitingEmergenceRef = React.useRef<Set<IngestionSourceType>>(new Set());
66
+ const settleTimerRef = React.useRef<number | null>(null);
36
67
 
37
68
  const memoriesQ = useQuery({ queryKey: ["memoryManager"], queryFn: latticeApi.memoryManager });
38
69
  const historyQ = useQuery({ queryKey: ["chatHistory"], queryFn: latticeApi.chatHistory });
@@ -89,9 +120,156 @@ export function BrainHome({
89
120
  React.useEffect(() => {
90
121
  return () => {
91
122
  if (recallTimerRef.current !== null) window.clearTimeout(recallTimerRef.current);
123
+ for (const timers of Object.values(stageTimersRef.current)) {
124
+ for (const timer of timers) window.clearTimeout(timer);
125
+ }
126
+ };
127
+ }, []);
128
+
129
+ const clearStageTimers = React.useCallback((sourceType: IngestionSourceType) => {
130
+ for (const timer of stageTimersRef.current[sourceType]) window.clearTimeout(timer);
131
+ stageTimersRef.current[sourceType] = [];
132
+ }, []);
133
+
134
+ const setStage = React.useCallback((sourceType: IngestionSourceType, stage: IngestionPipelineStage) => {
135
+ setIngestionStates((prev) => {
136
+ const current = prev[sourceType];
137
+ if (!current) return prev;
138
+ return {
139
+ ...prev,
140
+ [sourceType]: {
141
+ ...current,
142
+ stage,
143
+ completedAt: stage === "complete" || stage === "error" ? Date.now() : current.completedAt,
144
+ },
145
+ };
146
+ });
147
+ }, []);
148
+
149
+ const beginIngestion = React.useCallback(
150
+ (sourceType: IngestionSourceType, label: string) => {
151
+ clearStageTimers(sourceType);
152
+ pendingBaselineRef.current[sourceType] = {
153
+ memories: memoryFragments.length,
154
+ entities: knowledgeConcepts.length,
155
+ label,
156
+ };
157
+ setIngestionStates((prev) => ({
158
+ ...prev,
159
+ [sourceType]: {
160
+ sourceType,
161
+ label,
162
+ stage: "preparing",
163
+ startedAt: Date.now(),
164
+ completedAt: null,
165
+ newMemories: 0,
166
+ newEntities: 0,
167
+ },
168
+ }));
169
+ // Progressive disclosure of the in-flight pipeline while the request runs.
170
+ const interim: IngestionPipelineStage[] = ["parsing", "embedding", "indexing"];
171
+ interim.forEach((stage, index) => {
172
+ const timer = window.setTimeout(() => setStage(sourceType, stage), 420 * (index + 1));
173
+ stageTimersRef.current[sourceType].push(timer);
174
+ });
175
+ },
176
+ [clearStageTimers, knowledgeConcepts.length, memoryFragments.length, setStage],
177
+ );
178
+
179
+ const resolveEmergence = React.useCallback(
180
+ (sourceType: IngestionSourceType, memoryCount: number, entityCount: number) => {
181
+ clearStageTimers(sourceType);
182
+ const baseline = pendingBaselineRef.current[sourceType];
183
+ const label = baseline?.label ?? "";
184
+ // Snapshot deltas after invalidation lands; cap at >=0 to avoid noise.
185
+ const newMemories = Math.max(0, memoryCount - (baseline?.memories ?? memoryCount));
186
+ const newEntities = Math.max(0, entityCount - (baseline?.entities ?? entityCount));
187
+ delete pendingBaselineRef.current[sourceType];
188
+ setIngestionStates((prev) => {
189
+ const current = prev[sourceType];
190
+ if (!current) return prev;
191
+ return {
192
+ ...prev,
193
+ [sourceType]: {
194
+ ...current,
195
+ stage: "complete",
196
+ completedAt: Date.now(),
197
+ newMemories,
198
+ newEntities,
199
+ },
200
+ };
201
+ });
202
+ setEmergenceEvents((events) =>
203
+ [
204
+ {
205
+ id: `${sourceType}-${Date.now()}`,
206
+ sourceType,
207
+ label,
208
+ newMemories,
209
+ newEntities,
210
+ at: Date.now(),
211
+ },
212
+ ...events,
213
+ ].slice(0, 10),
214
+ );
215
+ },
216
+ [clearStageTimers],
217
+ );
218
+
219
+ // Once a request resolves we wait for the refetched counts to settle, then record the
220
+ // real emergence delta. A fallback timer guarantees the panel never hangs on a stale count.
221
+ const markAwaitingEmergence = React.useCallback(
222
+ (sourceType: IngestionSourceType) => {
223
+ awaitingEmergenceRef.current.add(sourceType);
224
+ if (settleTimerRef.current !== null) window.clearTimeout(settleTimerRef.current);
225
+ settleTimerRef.current = window.setTimeout(() => {
226
+ for (const pending of Array.from(awaitingEmergenceRef.current)) {
227
+ resolveEmergence(pending, memoryFragments.length, knowledgeConcepts.length);
228
+ awaitingEmergenceRef.current.delete(pending);
229
+ }
230
+ }, 1600);
231
+ },
232
+ [knowledgeConcepts.length, memoryFragments.length, resolveEmergence],
233
+ );
234
+
235
+ // Flush awaiting ingestions as soon as the underlying counts change post-invalidation.
236
+ React.useEffect(() => {
237
+ if (awaitingEmergenceRef.current.size === 0) return;
238
+ for (const pending of Array.from(awaitingEmergenceRef.current)) {
239
+ const baseline = pendingBaselineRef.current[pending];
240
+ if (!baseline) {
241
+ awaitingEmergenceRef.current.delete(pending);
242
+ continue;
243
+ }
244
+ if (memoryFragments.length !== baseline.memories || knowledgeConcepts.length !== baseline.entities) {
245
+ resolveEmergence(pending, memoryFragments.length, knowledgeConcepts.length);
246
+ awaitingEmergenceRef.current.delete(pending);
247
+ }
248
+ }
249
+ }, [memoryFragments.length, knowledgeConcepts.length, resolveEmergence]);
250
+
251
+ React.useEffect(() => {
252
+ return () => {
253
+ if (settleTimerRef.current !== null) window.clearTimeout(settleTimerRef.current);
92
254
  };
93
255
  }, []);
94
256
 
257
+ const failIngestion = React.useCallback(
258
+ (sourceType: IngestionSourceType, reason: string) => {
259
+ clearStageTimers(sourceType);
260
+ delete pendingBaselineRef.current[sourceType];
261
+ setIngestionStates((prev) => {
262
+ const current = prev[sourceType];
263
+ if (!current) return prev;
264
+ return {
265
+ ...prev,
266
+ [sourceType]: { ...current, stage: "error", completedAt: Date.now(), error: reason },
267
+ };
268
+ });
269
+ },
270
+ [clearStageTimers],
271
+ );
272
+
95
273
  async function send() {
96
274
  const text = draft.trim();
97
275
  if (!text || streaming) return;
@@ -134,6 +312,7 @@ export function BrainHome({
134
312
  } else {
135
313
  setMemoryFeedback(t(language, "brain.saved", { topics: knowledgeConcepts.length, memories: memoryFragments.length }));
136
314
  setLastRecallQuery(text);
315
+ void attachAnswerProof(text);
137
316
  }
138
317
  } finally {
139
318
  setStreaming(false);
@@ -144,17 +323,35 @@ export function BrainHome({
144
323
  }
145
324
  }
146
325
 
326
+ async function attachAnswerProof(query: string) {
327
+ const proofResult = await latticeApi.memoryBrainProof(query, 4);
328
+ const proof = buildBrainProof(proofResult.data, modelName);
329
+ const messageProof = toMessageProof(proof, query, modelName);
330
+ setMessages((items) => {
331
+ const next = [...items];
332
+ for (let index = next.length - 1; index >= 0; index -= 1) {
333
+ if (next[index].role === "assistant") {
334
+ next[index] = { ...next[index], proof: messageProof };
335
+ break;
336
+ }
337
+ }
338
+ return next;
339
+ });
340
+ }
341
+
147
342
  async function uploadDocument(file: File) {
148
343
  if (uploadingDocument) return;
149
344
 
150
345
  setUploadingDocument(true);
151
346
  setMemoryFeedback(t(language, "brain.upload.pending", { name: file.name }));
152
347
  onBrainChange("recalling", 0.86);
348
+ beginIngestion("file", file.name);
153
349
 
154
350
  try {
155
351
  const result = await latticeApi.uploadDocument(file);
156
352
  if (result.error) {
157
353
  setMemoryFeedback(t(language, "brain.upload.failed", { reason: result.error }));
354
+ failIngestion("file", result.error);
158
355
  return;
159
356
  }
160
357
 
@@ -164,11 +361,93 @@ export function BrainHome({
164
361
  void qc.invalidateQueries({ queryKey: ["memoryManager"] });
165
362
  void qc.invalidateQueries({ queryKey: ["graph"] });
166
363
  void qc.invalidateQueries({ queryKey: ["memoryBrainProof"] });
364
+ markAwaitingEmergence("file");
167
365
  } finally {
168
366
  setUploadingDocument(false);
169
367
  }
170
368
  }
171
369
 
370
+ async function connectFolder(path: string) {
371
+ const target = path.trim();
372
+ if (!target) return;
373
+ setMemoryFeedback(t(language, "brain.ingest.folder.pending", { path: target }));
374
+ onBrainChange("recalling", 0.84);
375
+ beginIngestion("folder", target);
376
+ const result = await latticeApi.connectFolder(target);
377
+ if (result.error || !result.ok) {
378
+ setMemoryFeedback(t(language, "brain.ingest.folder.failed", { reason: result.error || "unavailable" }));
379
+ failIngestion("folder", result.error || "unavailable");
380
+ return;
381
+ }
382
+ setMemoryFeedback(t(language, "brain.ingest.folder.saved", { path: target }));
383
+ setLastRecallQuery(target);
384
+ triggerBrainRecall();
385
+ void refreshBrainProof(target);
386
+ markAwaitingEmergence("folder");
387
+ }
388
+
389
+ async function ingestNote(note: string) {
390
+ const content = note.trim();
391
+ if (!content) return;
392
+ setMemoryFeedback(t(language, "brain.ingest.note.pending"));
393
+ onBrainChange("recalling", 0.84);
394
+ beginIngestion("note", content.slice(0, 80));
395
+ const result = await latticeApi.ingestNote(content, content.slice(0, 80));
396
+ if (result.error || !result.ok) {
397
+ setMemoryFeedback(t(language, "brain.ingest.note.failed", { reason: result.error || "unavailable" }));
398
+ failIngestion("note", result.error || "unavailable");
399
+ return;
400
+ }
401
+ setMemoryFeedback(t(language, "brain.ingest.note.saved"));
402
+ setLastRecallQuery(content.slice(0, 120));
403
+ triggerBrainRecall();
404
+ void refreshBrainProof(content.slice(0, 120));
405
+ markAwaitingEmergence("note");
406
+ }
407
+
408
+ async function ingestWeb(url: string) {
409
+ const target = url.trim();
410
+ if (!target) return;
411
+ setMemoryFeedback(t(language, "brain.ingest.web.pending", { url: target }));
412
+ onBrainChange("recalling", 0.84);
413
+ beginIngestion("web", target);
414
+ const result = await latticeApi.browserReadUrl(target);
415
+ if (result.error || !result.ok) {
416
+ setMemoryFeedback(t(language, "brain.ingest.web.failed", { reason: result.error || "unavailable" }));
417
+ failIngestion("web", result.error || "unavailable");
418
+ return;
419
+ }
420
+ setMemoryFeedback(t(language, "brain.ingest.web.saved", { url: target }));
421
+ setLastRecallQuery(target);
422
+ triggerBrainRecall();
423
+ void refreshBrainProof(target);
424
+ markAwaitingEmergence("web");
425
+ }
426
+
427
+ async function refreshBrainProof(query = lastRecallQuery) {
428
+ await Promise.all([
429
+ qc.invalidateQueries({ queryKey: ["memoryManager"] }),
430
+ qc.invalidateQueries({ queryKey: ["graph"] }),
431
+ qc.invalidateQueries({ queryKey: ["memoryBrainProof"] }),
432
+ ]);
433
+ if (query.trim()) {
434
+ await attachAnswerProof(query);
435
+ }
436
+ }
437
+
438
+ async function verifyModelContinuity() {
439
+ const lastUserMessage = [...messages].reverse().find((message: Message) => message.role === "user");
440
+ const query = lastRecallQuery || lastUserMessage?.content || "";
441
+ if (!query.trim()) {
442
+ setMemoryFeedback(t(language, "brain.modelDemo.needQuestion"));
443
+ return;
444
+ }
445
+ setMemoryFeedback(t(language, "brain.modelDemo.checking", { model: modelName }));
446
+ await attachAnswerProof(query);
447
+ setLastRecallQuery(query);
448
+ setMemoryFeedback(t(language, "brain.modelDemo.done", { model: modelName }));
449
+ }
450
+
172
451
  function deepen() {
173
452
  setExplorationDepth((depth) => {
174
453
  const next = Math.min(5, depth + 1) as BrainDepth;
@@ -272,6 +551,8 @@ export function BrainHome({
272
551
  messages={messages}
273
552
  starterPrompts={starterPrompts}
274
553
  memoryFeedback={memoryFeedback}
554
+ ingestionStates={ingestionStates}
555
+ emergenceEvents={emergenceEvents}
275
556
  draft={draft}
276
557
  streaming={streaming}
277
558
  imageData={imageData}
@@ -285,8 +566,26 @@ export function BrainHome({
285
566
  onDraftChange={setDraft}
286
567
  onImageDataChange={setImageData}
287
568
  onUploadDocument={(file) => void uploadDocument(file)}
569
+ onConnectFolder={(path) => void connectFolder(path)}
570
+ onIngestNote={(note) => void ingestNote(note)}
571
+ onIngestWeb={(url) => void ingestWeb(url)}
572
+ onVerifyModelContinuity={() => void verifyModelContinuity()}
288
573
  onSend={() => void send()}
289
574
  />
290
575
  </main>
291
576
  );
292
577
  }
578
+
579
+ function toMessageProof(proof: BrainProof, query: string, fallbackModelName: string): MessageProof {
580
+ return {
581
+ query,
582
+ model: proof.modelContinuity.activeModel || fallbackModelName,
583
+ provenAcrossModels: proof.modelContinuity.proven && proof.claims.keepsContextAcrossModels,
584
+ citations: proof.recall.items.slice(0, 4).map((item) => ({
585
+ id: item.id,
586
+ source: item.source,
587
+ title: item.title,
588
+ snippet: item.snippet,
589
+ })),
590
+ };
591
+ }
@@ -33,7 +33,9 @@ export function parseKnowledgeGraph(data: unknown): KnowledgeGraphModel {
33
33
  const label = textValue(node, ["title", "label", "name"], id.replace(/^[^:]+:/, ""));
34
34
  const summary = textValue(node, ["summary", "description", "snippet"]) || textValue(metadata, ["summary", "description", "relative_path", "filename"]);
35
35
  const importance = clamp(numberValue(node, ["importance_norm", "importance", "score"]) || 0.5, 0.08, 1);
36
- return [{ id, label, type, summary, importance }];
36
+ const createdAt = timestampValue(node, ["created_at", "createdAt", "added_at", "addedAt", "timestamp", "updated_at", "updatedAt"])
37
+ ?? timestampValue(metadata, ["created_at", "createdAt", "added_at", "addedAt", "timestamp", "updated_at", "updatedAt"]);
38
+ return [{ id, label, type, summary, importance, ...(createdAt !== undefined ? { createdAt } : {}) }];
37
39
  }).sort((left, right) => right.importance - left.importance);
38
40
  const ids = new Set(nodes.map((node) => node.id));
39
41
  const edges = rawEdges.flatMap((edge, index): RelationshipThread[] => {
@@ -205,6 +207,28 @@ function titleValue(record: ApiRecord, keys: string[], fallback = "") {
205
207
  .replace(/\b\w/g, (character) => character.toUpperCase());
206
208
  }
207
209
 
210
+ // Parse a created/added timestamp into unix epoch milliseconds. Accepts ISO 8601
211
+ // strings, unix seconds, or unix milliseconds. Returns undefined when absent or
212
+ // unparseable so the time-exploration UI can fall back gracefully.
213
+ function timestampValue(record: ApiRecord, keys: string[]): number | undefined {
214
+ for (const key of keys) {
215
+ const value = record[key];
216
+ if (typeof value === "number" && Number.isFinite(value)) {
217
+ // Heuristic: values below ~1e12 are seconds, otherwise milliseconds.
218
+ return value < 1e12 ? value * 1000 : value;
219
+ }
220
+ if (typeof value === "string" && value.trim()) {
221
+ const numeric = Number(value);
222
+ if (Number.isFinite(numeric) && /^\d+$/.test(value.trim())) {
223
+ return numeric < 1e12 ? numeric * 1000 : numeric;
224
+ }
225
+ const parsed = Date.parse(value);
226
+ if (Number.isFinite(parsed)) return parsed;
227
+ }
228
+ }
229
+ return undefined;
230
+ }
231
+
208
232
  function numberValue(record: ApiRecord, keys: string[]) {
209
233
  for (const key of keys) {
210
234
  const value = Number(record[key]);
@@ -1,5 +1,16 @@
1
1
  import * as React from "react";
2
- import type { KnowledgeConcept } from "./types";
2
+ import type { KnowledgeConcept, RelationshipThread } from "./types";
3
+
4
+ // Return the set of node ids that share a direct (1-hop) edge with `nodeId`.
5
+ // The focused node itself is not included.
6
+ export function computeGraphNeighbors(nodeId: string, edges: RelationshipThread[]): Set<string> {
7
+ const neighbors = new Set<string>();
8
+ for (const edge of edges) {
9
+ if (edge.source === nodeId) neighbors.add(edge.target);
10
+ else if (edge.target === nodeId) neighbors.add(edge.source);
11
+ }
12
+ return neighbors;
13
+ }
3
14
 
4
15
  export function layoutGraphNodes(nodes: KnowledgeConcept[], radiusX: number, radiusY: number) {
5
16
  return nodes.map((node, index) => {
@@ -6,6 +6,19 @@ export type BrainDepth = 1 | 2 | 3 | 4 | 5;
6
6
  export type Message = {
7
7
  role: "user" | "assistant";
8
8
  content: string;
9
+ proof?: MessageProof;
10
+ };
11
+
12
+ export type MessageProof = {
13
+ query: string;
14
+ model: string;
15
+ provenAcrossModels: boolean;
16
+ citations: Array<{
17
+ id: string;
18
+ source: string;
19
+ title: string;
20
+ snippet: string;
21
+ }>;
9
22
  };
10
23
 
11
24
  export type MemoryFragment = {
@@ -20,6 +33,9 @@ export type KnowledgeConcept = {
20
33
  type: string;
21
34
  summary: string;
22
35
  importance: number;
36
+ // Optional unix epoch (ms) the concept was added to the graph. Enables
37
+ // time-based exploration; absent when the backend does not yet emit it.
38
+ createdAt?: number;
23
39
  };
24
40
 
25
41
  export type RelationshipThread = {
@@ -87,6 +103,44 @@ export type BrainProof = {
87
103
  };
88
104
  };
89
105
 
106
+ export type IngestionSourceType = "file" | "folder" | "note" | "web";
107
+
108
+ export type IngestionPipelineStage =
109
+ | "preparing"
110
+ | "parsing"
111
+ | "embedding"
112
+ | "indexing"
113
+ | "complete"
114
+ | "error";
115
+
116
+ export type IngestionState = {
117
+ sourceType: IngestionSourceType;
118
+ label: string;
119
+ stage: IngestionPipelineStage;
120
+ startedAt: number;
121
+ completedAt: number | null;
122
+ newMemories: number;
123
+ newEntities: number;
124
+ error?: string;
125
+ };
126
+
127
+ export type EmergenceEvent = {
128
+ id: string;
129
+ sourceType: IngestionSourceType;
130
+ label: string;
131
+ newMemories: number;
132
+ newEntities: number;
133
+ at: number;
134
+ };
135
+
136
+ export const INGESTION_STAGE_ORDER: IngestionPipelineStage[] = [
137
+ "preparing",
138
+ "parsing",
139
+ "embedding",
140
+ "indexing",
141
+ "complete",
142
+ ];
143
+
90
144
  export const DEPTHS: Array<{ level: BrainDepth; labelKey: string; state: BrainState }> = [
91
145
  { level: 1, labelKey: "brain.depthLabel.1", state: "idle" },
92
146
  { level: 2, labelKey: "brain.depthLabel.2", state: "recalling" },