ltcai 8.9.0 → 9.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 (208) hide show
  1. package/README.md +81 -58
  2. package/auto_setup.py +7 -904
  3. package/desktop/electron/README.md +9 -0
  4. package/desktop/electron/main.cjs +5 -3
  5. package/docs/CHANGELOG.md +221 -238
  6. package/docs/COMMUNITY_AND_PLUGINS.md +3 -2
  7. package/docs/DEVELOPMENT.md +53 -14
  8. package/docs/LEGACY_COMPATIBILITY.md +4 -4
  9. package/docs/ONBOARDING.md +10 -2
  10. package/docs/OPERATIONS.md +8 -4
  11. package/docs/PRODUCT_DIRECTION_REVIEW.md +4 -0
  12. package/docs/ROADMAP_RECOMMENDATIONS.md +61 -36
  13. package/docs/TRUST_MODEL.md +5 -2
  14. package/docs/WHY_LATTICE.md +15 -10
  15. package/docs/WORKFLOW_DESIGNER.md +22 -0
  16. package/docs/kg-schema.md +13 -2
  17. package/docs/mcp-tools.md +17 -6
  18. package/docs/privacy.md +19 -3
  19. package/docs/public-deploy.md +32 -3
  20. package/docs/security-model.md +46 -11
  21. package/lattice_brain/__init__.py +1 -1
  22. package/lattice_brain/archive.py +4 -14
  23. package/lattice_brain/context.py +66 -9
  24. package/lattice_brain/embeddings.py +38 -2
  25. package/lattice_brain/graph/_kg_common.py +27 -462
  26. package/lattice_brain/graph/_kg_constants.py +243 -0
  27. package/lattice_brain/graph/_kg_fsutil.py +293 -0
  28. package/lattice_brain/graph/discovery.py +0 -948
  29. package/lattice_brain/graph/discovery_index.py +1126 -0
  30. package/lattice_brain/graph/documents.py +44 -9
  31. package/lattice_brain/graph/ingest.py +47 -20
  32. package/lattice_brain/graph/provenance.py +13 -6
  33. package/lattice_brain/graph/retrieval.py +141 -610
  34. package/lattice_brain/graph/retrieval_docgen.py +243 -0
  35. package/lattice_brain/graph/retrieval_vector.py +460 -0
  36. package/lattice_brain/graph/store.py +6 -0
  37. package/lattice_brain/ingestion.py +69 -4
  38. package/lattice_brain/portability.py +29 -23
  39. package/lattice_brain/quality.py +98 -4
  40. package/lattice_brain/runtime/agent_runtime.py +169 -7
  41. package/lattice_brain/runtime/hooks.py +30 -13
  42. package/lattice_brain/runtime/multi_agent.py +27 -8
  43. package/lattice_brain/runtime/statuses.py +10 -0
  44. package/lattice_brain/utils.py +46 -0
  45. package/lattice_brain/workflow.py +1 -5
  46. package/latticeai/__init__.py +1 -1
  47. package/latticeai/api/admin.py +1 -1
  48. package/latticeai/api/agent_registry.py +10 -8
  49. package/latticeai/api/agents.py +22 -3
  50. package/latticeai/api/auth.py +78 -16
  51. package/latticeai/api/browser.py +303 -34
  52. package/latticeai/api/chat.py +355 -952
  53. package/latticeai/api/chat_agent_http.py +356 -0
  54. package/latticeai/api/chat_contracts.py +54 -0
  55. package/latticeai/api/chat_documents.py +256 -0
  56. package/latticeai/api/chat_helpers.py +250 -0
  57. package/latticeai/api/chat_history.py +99 -0
  58. package/latticeai/api/chat_intents.py +302 -0
  59. package/latticeai/api/chat_stream.py +115 -0
  60. package/latticeai/api/computer_use.py +211 -40
  61. package/latticeai/api/health.py +9 -3
  62. package/latticeai/api/hooks.py +17 -6
  63. package/latticeai/api/knowledge_graph.py +78 -14
  64. package/latticeai/api/local_files.py +7 -2
  65. package/latticeai/api/marketplace.py +11 -0
  66. package/latticeai/api/mcp.py +102 -23
  67. package/latticeai/api/models.py +72 -33
  68. package/latticeai/api/network.py +5 -5
  69. package/latticeai/api/permissions.py +70 -29
  70. package/latticeai/api/plugins.py +21 -7
  71. package/latticeai/api/portability.py +5 -5
  72. package/latticeai/api/realtime.py +33 -5
  73. package/latticeai/api/setup.py +2 -2
  74. package/latticeai/api/static_routes.py +123 -24
  75. package/latticeai/api/tools.py +97 -14
  76. package/latticeai/api/workflow_designer.py +37 -0
  77. package/latticeai/api/workspace.py +2 -1
  78. package/latticeai/app_factory.py +185 -405
  79. package/latticeai/core/agent.py +19 -9
  80. package/latticeai/core/agent_registry.py +1 -5
  81. package/latticeai/core/config.py +23 -3
  82. package/latticeai/core/context_builder.py +21 -3
  83. package/latticeai/core/invitations.py +1 -4
  84. package/latticeai/core/io_utils.py +45 -0
  85. package/latticeai/core/legacy_compatibility.py +24 -3
  86. package/latticeai/core/local_embeddings.py +2 -4
  87. package/latticeai/core/marketplace.py +33 -2
  88. package/latticeai/core/mcp_catalog.py +450 -0
  89. package/latticeai/core/mcp_registry.py +2 -441
  90. package/latticeai/core/plugins.py +15 -0
  91. package/latticeai/core/policy.py +1 -1
  92. package/latticeai/core/realtime.py +38 -15
  93. package/latticeai/core/sessions.py +7 -2
  94. package/latticeai/core/timeutil.py +10 -0
  95. package/latticeai/core/tool_registry.py +90 -18
  96. package/latticeai/core/users.py +5 -14
  97. package/latticeai/core/workspace_graph_trace.py +31 -5
  98. package/latticeai/core/workspace_memory.py +3 -1
  99. package/latticeai/core/workspace_os.py +18 -7
  100. package/latticeai/core/workspace_os_utils.py +2 -21
  101. package/latticeai/core/workspace_permissions.py +2 -1
  102. package/latticeai/core/workspace_plugins.py +1 -1
  103. package/latticeai/core/workspace_runs.py +14 -5
  104. package/latticeai/core/workspace_skills.py +1 -1
  105. package/latticeai/core/workspace_snapshots.py +2 -1
  106. package/latticeai/core/workspace_timeline.py +2 -1
  107. package/latticeai/integrations/telegram_bot.py +96 -40
  108. package/latticeai/models/model_providers.py +111 -0
  109. package/latticeai/models/router.py +189 -173
  110. package/latticeai/runtime/access_runtime.py +62 -4
  111. package/latticeai/runtime/audit_runtime.py +27 -16
  112. package/latticeai/runtime/automation_runtime.py +22 -7
  113. package/latticeai/runtime/brain_runtime.py +19 -7
  114. package/latticeai/runtime/chat_wiring.py +2 -0
  115. package/latticeai/runtime/config_runtime.py +58 -26
  116. package/latticeai/runtime/context_runtime.py +16 -4
  117. package/latticeai/runtime/history_runtime.py +163 -0
  118. package/latticeai/runtime/hooks_runtime.py +1 -1
  119. package/latticeai/runtime/model_wiring.py +33 -5
  120. package/latticeai/runtime/namespace_runtime.py +163 -0
  121. package/latticeai/runtime/network_config_runtime.py +56 -0
  122. package/latticeai/runtime/platform_runtime_wiring.py +4 -3
  123. package/latticeai/runtime/review_wiring.py +1 -1
  124. package/latticeai/runtime/router_registration.py +34 -1
  125. package/latticeai/runtime/security_runtime.py +110 -13
  126. package/latticeai/runtime/sso_config_runtime.py +128 -0
  127. package/latticeai/runtime/stages.py +27 -0
  128. package/latticeai/runtime/user_key_runtime.py +106 -0
  129. package/latticeai/server_app.py +5 -1
  130. package/latticeai/services/architecture_readiness.py +74 -5
  131. package/latticeai/services/brain_automation.py +3 -26
  132. package/latticeai/services/chat_service.py +205 -15
  133. package/latticeai/services/local_knowledge.py +423 -0
  134. package/latticeai/services/memory_service.py +268 -21
  135. package/latticeai/services/model_engines.py +48 -33
  136. package/latticeai/services/model_errors.py +17 -0
  137. package/latticeai/services/model_loading.py +28 -25
  138. package/latticeai/services/model_runtime.py +228 -162
  139. package/latticeai/services/p_reinforce.py +22 -3
  140. package/latticeai/services/platform_runtime.py +92 -24
  141. package/latticeai/services/product_readiness.py +19 -17
  142. package/latticeai/services/review_queue.py +76 -11
  143. package/latticeai/services/router_context.py +1 -0
  144. package/latticeai/services/run_executor.py +25 -9
  145. package/latticeai/services/search_service.py +203 -28
  146. package/latticeai/services/setup_detection.py +80 -0
  147. package/latticeai/services/tool_dispatch.py +28 -5
  148. package/latticeai/services/triggers.py +53 -4
  149. package/latticeai/services/upload_service.py +13 -2
  150. package/latticeai/setup/__init__.py +25 -0
  151. package/latticeai/setup/auto_setup.py +857 -0
  152. package/latticeai/setup/wizard.py +1264 -0
  153. package/latticeai/tools/__init__.py +278 -0
  154. package/{tools → latticeai/tools}/commands.py +67 -7
  155. package/{tools → latticeai/tools}/computer.py +1 -1
  156. package/{tools → latticeai/tools}/documents.py +1 -1
  157. package/{tools → latticeai/tools}/filesystem.py +3 -3
  158. package/latticeai/tools/knowledge.py +178 -0
  159. package/{tools → latticeai/tools}/local_files.py +7 -1
  160. package/{tools → latticeai/tools}/network.py +0 -1
  161. package/local_knowledge_api.py +4 -342
  162. package/package.json +22 -2
  163. package/scripts/brain_quality_eval.py +3 -1
  164. package/scripts/bump_version.py +3 -0
  165. package/scripts/capture_release_evidence.mjs +180 -0
  166. package/scripts/check_current_release_docs.mjs +142 -0
  167. package/scripts/check_i18n_literals.mjs +107 -31
  168. package/scripts/check_openapi_drift.mjs +56 -0
  169. package/scripts/export_openapi.py +67 -7
  170. package/scripts/i18n_literal_allowlist.json +22 -24
  171. package/scripts/run_integration_tests.mjs +72 -10
  172. package/scripts/validate_release_artifacts.py +49 -7
  173. package/scripts/wheel_smoke.py +5 -0
  174. package/setup_wizard.py +3 -1304
  175. package/src-tauri/Cargo.lock +1 -1
  176. package/src-tauri/Cargo.toml +1 -1
  177. package/src-tauri/tauri.conf.json +1 -1
  178. package/static/app/asset-manifest.json +11 -11
  179. package/static/app/assets/Act-Bzz0bUyW.js +1 -0
  180. package/static/app/assets/Brain-Dj2J20YA.js +321 -0
  181. package/static/app/assets/Capture-CqlEl1Ga.js +1 -0
  182. package/static/app/assets/Library-B03FP1Yx.js +1 -0
  183. package/static/app/assets/System-80lHW0Ux.js +1 -0
  184. package/static/app/assets/index-BiMofBTM.js +17 -0
  185. package/static/app/assets/index-Bmx9rzTc.css +2 -0
  186. package/static/app/assets/primitives-Q1A96_7v.js +1 -0
  187. package/static/app/assets/textarea-D13RtnTo.js +1 -0
  188. package/static/app/index.html +2 -2
  189. package/static/css/tokens.css +4 -2
  190. package/static/sw.js +1 -1
  191. package/tools/__init__.py +21 -269
  192. package/docs/CODE_REVIEW_2026-07-06.md +0 -764
  193. package/latticeai/runtime/app_context_runtime.py +0 -13
  194. package/latticeai/runtime/sso_runtime.py +0 -52
  195. package/latticeai/runtime/tail_wiring.py +0 -21
  196. package/scripts/com.pts.claudecode.discord.plist +0 -31
  197. package/scripts/pts-claudecode-discord-bridge.mjs +0 -207
  198. package/scripts/start-pts-claudecode-discord.sh +0 -51
  199. package/static/app/assets/Act-fZokUnC0.js +0 -1
  200. package/static/app/assets/Brain-DtyuWubr.js +0 -321
  201. package/static/app/assets/Capture-D5KV3Cu7.js +0 -1
  202. package/static/app/assets/Library-C9kyFkSt.js +0 -1
  203. package/static/app/assets/System-VbChmX7r.js +0 -1
  204. package/static/app/assets/index-DCh5AoXt.css +0 -2
  205. package/static/app/assets/index-DPdcPoF0.js +0 -17
  206. package/static/app/assets/primitives-DFeanEV6.js +0 -1
  207. package/static/app/assets/textarea-CD8UNKIy.js +0 -1
  208. package/tools/knowledge.py +0 -95
@@ -22,11 +22,12 @@ underlying store, and missing stores surface as ``unavailable``.
22
22
  from __future__ import annotations
23
23
 
24
24
  import json
25
- from datetime import datetime
25
+ import logging
26
26
  from pathlib import Path
27
27
  from typing import Any, Dict, List, Optional
28
28
 
29
29
  from latticeai.core.workspace_os_utils import _file_size
30
+ from latticeai.core.timeutil import now_iso as _now
30
31
 
31
32
  # Personal workspace memory kinds (from WorkspaceOS.MEMORY_KINDS).
32
33
  WORKSPACE_KINDS = (
@@ -40,10 +41,11 @@ WORKSPACE_KINDS = (
40
41
  )
41
42
 
42
43
  TIERS = ("workspace", "project", "agent", "conversation", "graph", "vector")
44
+ LOGGER = logging.getLogger(__name__)
43
45
 
44
46
 
45
- def _now() -> str:
46
- return datetime.now().isoformat(timespec="seconds")
47
+ class MemoryServiceError(RuntimeError):
48
+ """Raised when a configured memory backend cannot be read reliably."""
47
49
 
48
50
 
49
51
  class MemoryService:
@@ -70,20 +72,23 @@ class MemoryService:
70
72
  def _workspace_memories(self, *, user_email: Optional[str], workspace_id: Optional[str]) -> List[Dict[str, Any]]:
71
73
  try:
72
74
  return list(self._store.list_memories(user_email=user_email, workspace_id=workspace_id).get("memories", []))
73
- except Exception:
74
- return []
75
+ except Exception as exc:
76
+ LOGGER.exception("workspace memory read failed")
77
+ raise MemoryServiceError("workspace memory backend unavailable") from exc
75
78
 
76
79
  def _all_memories(self) -> List[Dict[str, Any]]:
77
80
  try:
78
81
  return list(self._store.list_memories().get("memories", []))
79
- except Exception:
80
- return []
82
+ except Exception as exc:
83
+ LOGGER.exception("global memory read failed")
84
+ raise MemoryServiceError("memory backend unavailable") from exc
81
85
 
82
86
  def _snapshots(self, *, workspace_id: Optional[str]) -> List[Dict[str, Any]]:
83
87
  try:
84
88
  return list(self._store.list_memory_snapshots(workspace_id=workspace_id, limit=200).get("snapshots", []))
85
- except Exception:
86
- return []
89
+ except Exception as exc:
90
+ LOGGER.exception("memory snapshot read failed")
91
+ raise MemoryServiceError("memory snapshot backend unavailable") from exc
87
92
 
88
93
  def _conversations(self) -> List[Dict[str, Any]]:
89
94
  if self._conversation_store is not None:
@@ -92,15 +97,17 @@ class MemoryService:
92
97
  for item in self._conversation_store.history():
93
98
  grouped.setdefault(item.get("conversation_id") or "legacy-previous-history", []).append(item)
94
99
  return [{"id": conv_id, "messages": msgs} for conv_id, msgs in grouped.items()]
95
- except Exception:
96
- return []
100
+ except Exception as exc:
101
+ LOGGER.exception("conversation store read failed")
102
+ raise MemoryServiceError("conversation backend unavailable") from exc
97
103
  if not self._history_file.exists():
98
104
  return []
99
105
  try:
100
106
  with open(self._history_file, "r", encoding="utf-8") as fh:
101
107
  data = json.load(fh)
102
- except Exception:
103
- return []
108
+ except (OSError, json.JSONDecodeError, TypeError, ValueError) as exc:
109
+ LOGGER.exception("legacy conversation history read failed")
110
+ raise MemoryServiceError("conversation history is unreadable") from exc
104
111
  if isinstance(data, dict):
105
112
  convs = data.get("conversations")
106
113
  if isinstance(convs, list):
@@ -136,6 +143,7 @@ class MemoryService:
136
143
  try:
137
144
  return self._kg.stats()
138
145
  except Exception:
146
+ LOGGER.exception("knowledge graph stats read failed")
139
147
  return None
140
148
 
141
149
  def _kg_index(self) -> Optional[Dict[str, Any]]:
@@ -144,6 +152,7 @@ class MemoryService:
144
152
  try:
145
153
  return self._kg.index_status()
146
154
  except Exception:
155
+ LOGGER.exception("knowledge graph index status read failed")
147
156
  return None
148
157
 
149
158
  # ── Memory Manager: sources / usage / health ──────────────────────────
@@ -271,6 +280,7 @@ class MemoryService:
271
280
  memory_count = len(memory_ids) + len(snaps) + len(convs)
272
281
  return {
273
282
  "sources": sources,
283
+ "recent_memories": self._manager_recent_memories([*ws_mem, *project_mem], limit=8),
274
284
  "tiers": list(TIERS),
275
285
  "usage": {"total_items": total_items, "total_bytes": total_bytes, "sources": len(sources)},
276
286
  "brain_readiness": self._brain_readiness(
@@ -284,6 +294,23 @@ class MemoryService:
284
294
  "generated_at": _now(),
285
295
  }
286
296
 
297
+ @staticmethod
298
+ def _manager_recent_memories(memories: List[Dict[str, Any]], *, limit: int = 8) -> List[Dict[str, Any]]:
299
+ rows: List[Dict[str, Any]] = []
300
+ for item in memories[: max(1, limit)]:
301
+ metadata = item.get("metadata") if isinstance(item.get("metadata"), dict) else {}
302
+ rows.append({
303
+ "id": item.get("id") or "",
304
+ "kind": item.get("kind") or "memory",
305
+ "content": str(item.get("content") or "")[:320],
306
+ "tags": item.get("tags") if isinstance(item.get("tags"), list) else [],
307
+ "metadata": metadata,
308
+ "workspace_id": item.get("workspace_id") or "personal",
309
+ "created_at": item.get("created_at"),
310
+ "updated_at": item.get("updated_at"),
311
+ })
312
+ return rows
313
+
287
314
  def brain_quality_summary(self, *, user_email: Optional[str] = None, workspace_id: Optional[str] = None) -> Dict[str, Any]:
288
315
  """Return the backend-owned Brain readiness signal for API consumers."""
289
316
  return self.manager(user_email=user_email, workspace_id=workspace_id)["brain_readiness"]
@@ -338,6 +365,22 @@ class MemoryService:
338
365
  has_recall=bool(recall_items),
339
366
  graph_concepts=graph_concepts,
340
367
  )
368
+ suggested_questions = self._brain_brief_suggested_questions(
369
+ focus=focus,
370
+ has_durable_evidence=bool(proofs.get("has_durable_evidence")),
371
+ has_recall=bool(recall_items),
372
+ graph_concepts=graph_concepts,
373
+ conversations=conversations,
374
+ )
375
+ proactive_actions = self._brain_brief_proactive_actions(
376
+ focus=focus,
377
+ state=state,
378
+ has_durable_evidence=bool(proofs.get("has_durable_evidence")),
379
+ has_recall=bool(recall_items),
380
+ graph_concepts=graph_concepts,
381
+ vector_items=vector_items,
382
+ healthy_sources=healthy_sources,
383
+ )
341
384
  return {
342
385
  "status": state,
343
386
  "score": int(readiness.get("score") or 0),
@@ -345,6 +388,8 @@ class MemoryService:
345
388
  "body_key": f"brain.brief.body.{state if state in {'quiet', 'forming', 'alive'} else 'quiet'}",
346
389
  "focus": focus,
347
390
  "next_actions": actions,
391
+ "suggested_questions": suggested_questions,
392
+ "proactive_actions": proactive_actions,
348
393
  "evidence": [
349
394
  {
350
395
  "id": "durable",
@@ -501,6 +546,183 @@ class MemoryService:
501
546
  })
502
547
  return sorted(actions, key=lambda item: int(item.get("priority") or 0), reverse=True)[:4]
503
548
 
549
+ @staticmethod
550
+ def _brain_brief_proactive_actions(
551
+ *,
552
+ focus: Dict[str, Any],
553
+ state: str,
554
+ has_durable_evidence: bool,
555
+ has_recall: bool,
556
+ graph_concepts: int,
557
+ vector_items: int,
558
+ healthy_sources: int,
559
+ ) -> List[Dict[str, Any]]:
560
+ """Return concrete, one-click actions Brain can proactively suggest."""
561
+ focus_title = str(focus.get("title") or "").strip() or "Brain"
562
+ focus_detail = str(focus.get("detail") or "").strip()
563
+ actions: List[Dict[str, Any]] = []
564
+ if not has_durable_evidence:
565
+ actions.append({
566
+ "id": "proactive_add_source",
567
+ "intent": "route",
568
+ "label_key": "brain.proactive.addSource.label",
569
+ "detail_key": "brain.proactive.addSource.detail",
570
+ "route": "/capture",
571
+ "prompt": "Add a useful source to my Brain and explain what it learned.",
572
+ "priority": 100,
573
+ })
574
+ actions.append({
575
+ "id": "proactive_seed_memory",
576
+ "intent": "ask",
577
+ "label_key": "brain.proactive.seed.label",
578
+ "detail_key": "brain.proactive.seed.detail",
579
+ "prompt": "Help me seed my Brain with the most useful personal context to remember.",
580
+ "priority": 90,
581
+ })
582
+ return actions
583
+
584
+ if has_recall:
585
+ actions.append({
586
+ "id": "proactive_evidence_review",
587
+ "intent": "ask",
588
+ "label_key": "brain.proactive.evidence.label",
589
+ "detail_key": "brain.proactive.evidence.detail",
590
+ "prompt": (
591
+ f"Review the evidence Brain has for {focus_title}. "
592
+ "Separate confirmed facts, weak signals, contradictions, and next checks."
593
+ ),
594
+ "priority": 100,
595
+ "context": {"focus": focus_title, "detail": focus_detail},
596
+ })
597
+ actions.append({
598
+ "id": "proactive_delegate",
599
+ "intent": "delegate",
600
+ "label_key": "brain.proactive.delegate.label",
601
+ "detail_key": "brain.proactive.delegate.detail",
602
+ "prompt": (
603
+ f"Turn {focus_title} into an execution plan, verify the known context, "
604
+ "and return concrete next steps with risks."
605
+ ),
606
+ "priority": 95,
607
+ "context": {"focus": focus_title, "detail": focus_detail},
608
+ })
609
+ actions.append({
610
+ "id": "proactive_review_draft",
611
+ "intent": "review",
612
+ "label_key": "brain.proactive.review.label",
613
+ "detail_key": "brain.proactive.review.detail",
614
+ "prompt": (
615
+ f"Create a reviewable task from Brain's current focus: {focus_title}. "
616
+ f"{focus_detail[:240]}"
617
+ ).strip(),
618
+ "priority": 90,
619
+ "context": {"focus": focus_title, "detail": focus_detail},
620
+ })
621
+
622
+ if graph_concepts > 0:
623
+ actions.append({
624
+ "id": "proactive_map_connections",
625
+ "intent": "route",
626
+ "label_key": "brain.proactive.map.label",
627
+ "detail_key": "brain.proactive.map.detail",
628
+ "route": "/knowledge-graph",
629
+ "prompt": f"Map the strongest Knowledge Graph connections around {focus_title}.",
630
+ "priority": 82,
631
+ "context": {"focus": focus_title, "graph_concepts": graph_concepts},
632
+ })
633
+
634
+ if state == "alive" and vector_items > 0 and healthy_sources > 0:
635
+ actions.append({
636
+ "id": "proactive_weekly_brief",
637
+ "intent": "review",
638
+ "label_key": "brain.proactive.weekly.label",
639
+ "detail_key": "brain.proactive.weekly.detail",
640
+ "prompt": (
641
+ "Prepare a weekly Brain review: what changed, what decisions are pending, "
642
+ "what should be delegated, and what evidence is stale."
643
+ ),
644
+ "priority": 78,
645
+ "context": {"vector_items": vector_items, "healthy_sources": healthy_sources},
646
+ })
647
+
648
+ return sorted(actions, key=lambda item: int(item.get("priority") or 0), reverse=True)[:4]
649
+
650
+ @staticmethod
651
+ def _brain_brief_suggested_questions(
652
+ *,
653
+ focus: Dict[str, Any],
654
+ has_durable_evidence: bool,
655
+ has_recall: bool,
656
+ graph_concepts: int,
657
+ conversations: int,
658
+ ) -> List[Dict[str, Any]]:
659
+ """Return reusable, localized UI prompt descriptors for the Brain home."""
660
+ focus_title = str(focus.get("title") or "").strip()
661
+ focus_kind = str(focus.get("kind") or "empty")
662
+ questions: List[Dict[str, Any]] = []
663
+
664
+ if not has_durable_evidence or focus_kind == "empty":
665
+ questions.extend([
666
+ {
667
+ "id": "start_brain",
668
+ "label_key": "brain.suggestion.start.label",
669
+ "detail_key": "brain.suggestion.start.detail",
670
+ "prompt_key": "brain.suggestion.start.prompt",
671
+ "params": {},
672
+ "priority": 10,
673
+ },
674
+ {
675
+ "id": "add_context",
676
+ "label_key": "brain.suggestion.context.label",
677
+ "detail_key": "brain.suggestion.context.detail",
678
+ "prompt_key": "brain.suggestion.context.prompt",
679
+ "params": {},
680
+ "priority": 9,
681
+ },
682
+ ])
683
+ return questions
684
+
685
+ questions.append({
686
+ "id": "focus_next",
687
+ "label_key": "brain.suggestion.focus.label",
688
+ "detail_key": "brain.suggestion.focus.detail",
689
+ "prompt_key": "brain.suggestion.focus.prompt",
690
+ "params": {"focus": focus_title or "Brain"},
691
+ "priority": 10,
692
+ })
693
+
694
+ if has_recall:
695
+ questions.append({
696
+ "id": "evidence_check",
697
+ "label_key": "brain.suggestion.evidence.label",
698
+ "detail_key": "brain.suggestion.evidence.detail",
699
+ "prompt_key": "brain.suggestion.evidence.prompt",
700
+ "params": {"focus": focus_title or "this topic"},
701
+ "priority": 9,
702
+ })
703
+
704
+ if graph_concepts > 0:
705
+ questions.append({
706
+ "id": "graph_connections",
707
+ "label_key": "brain.suggestion.graph.label",
708
+ "detail_key": "brain.suggestion.graph.detail",
709
+ "prompt_key": "brain.suggestion.graph.prompt",
710
+ "params": {"focus": focus_title or "Knowledge Graph"},
711
+ "priority": 8,
712
+ })
713
+
714
+ if conversations > 0:
715
+ questions.append({
716
+ "id": "conversation_followup",
717
+ "label_key": "brain.suggestion.history.label",
718
+ "detail_key": "brain.suggestion.history.detail",
719
+ "prompt_key": "brain.suggestion.history.prompt",
720
+ "params": {"focus": focus_title or "recent conversations"},
721
+ "priority": 7,
722
+ })
723
+
724
+ return sorted(questions, key=lambda item: int(item.get("priority") or 0), reverse=True)[:4]
725
+
504
726
  def brain_proof(
505
727
  self,
506
728
  *,
@@ -634,9 +856,12 @@ class MemoryService:
634
856
 
635
857
  results: List[Dict[str, Any]] = []
636
858
 
859
+ errors: List[Dict[str, str]] = []
637
860
  try:
638
861
  mem = self._store.search_memories(q, user_email=user_email, limit=limit, workspace_id=workspace_id).get("memories", [])
639
- except Exception:
862
+ except Exception as exc:
863
+ LOGGER.exception("workspace memory search failed")
864
+ errors.append({"source": "workspace", "detail": str(exc)})
640
865
  mem = []
641
866
  for m in mem:
642
867
  matched = _matched_terms(m.get("content"), " ".join(m.get("tags") or []), m.get("kind"))
@@ -654,8 +879,15 @@ class MemoryService:
654
879
  if self._enable_graph and q:
655
880
  try:
656
881
  # KnowledgeGraph.search returns {"query": ..., "matches": [...]}.
657
- hits = self._kg.search(q, limit).get("matches", [])
658
- except Exception:
882
+ search_kwargs = (
883
+ {"allowed_workspaces": {workspace_id}}
884
+ if workspace_id is not None
885
+ else {}
886
+ )
887
+ hits = self._kg.search(q, limit, **search_kwargs).get("matches", [])
888
+ except Exception as exc:
889
+ LOGGER.exception("knowledge graph memory search failed")
890
+ errors.append({"source": "graph", "detail": str(exc)})
659
891
  hits = []
660
892
  for hit in hits[:limit]:
661
893
  matched = _matched_terms(hit.get("title"), hit.get("name"), hit.get("summary"), hit.get("content"))
@@ -685,6 +917,8 @@ class MemoryService:
685
917
  "results": results[: max(1, min(limit, 100))],
686
918
  "count": len(results),
687
919
  "source": "live",
920
+ "status": "degraded" if errors else "ok",
921
+ "errors": errors,
688
922
  "quality_gate": {
689
923
  "candidates": candidates,
690
924
  "passed": len(results),
@@ -736,6 +970,7 @@ class MemoryService:
736
970
  if m.get("id")
737
971
  }
738
972
  removed: List[str] = []
973
+ failed: List[Dict[str, str]] = []
739
974
  skipped: List[str] = []
740
975
  target_ids: List[str] = []
741
976
  seen: set = set()
@@ -756,11 +991,15 @@ class MemoryService:
756
991
  try:
757
992
  self._store.delete_memory(mid)
758
993
  removed.append(mid)
759
- except Exception:
760
- continue
994
+ except Exception as exc:
995
+ LOGGER.exception("memory deletion failed for %s", mid)
996
+ failed.append({"id": mid, "detail": str(exc)})
761
997
  result: Dict[str, Any] = {"removed": removed, "count": len(removed)}
762
998
  if skipped:
763
999
  result["skipped"] = skipped
1000
+ if failed:
1001
+ result["failed"] = failed
1002
+ result["status"] = "partial" if removed else "error"
764
1003
  return result
765
1004
 
766
1005
  def compact(
@@ -772,6 +1011,7 @@ class MemoryService:
772
1011
  """Dedupe workspace memories with identical (kind, content)."""
773
1012
  seen: set = set()
774
1013
  removed: List[str] = []
1014
+ failed: List[Dict[str, str]] = []
775
1015
  # Oldest first so the first occurrence (oldest) is kept.
776
1016
  memories = list(reversed(self._workspace_memories(user_email=user_email, workspace_id=workspace_id)))
777
1017
  for m in memories:
@@ -781,11 +1021,18 @@ class MemoryService:
781
1021
  try:
782
1022
  self._store.delete_memory(m["id"])
783
1023
  removed.append(m["id"])
784
- except Exception:
785
- continue
1024
+ except Exception as exc:
1025
+ LOGGER.exception("memory compaction deletion failed for %s", m["id"])
1026
+ failed.append({"id": m["id"], "detail": str(exc)})
786
1027
  else:
787
1028
  seen.add(key)
788
- return {"compacted": len(removed), "removed": removed, "remaining": len(seen)}
1029
+ return {
1030
+ "compacted": len(removed),
1031
+ "removed": removed,
1032
+ "remaining": len(seen),
1033
+ "failed": failed,
1034
+ "status": "partial" if failed and removed else "error" if failed else "ok",
1035
+ }
789
1036
 
790
1037
  def rebuild(self, target: str = "vector") -> Dict[str, Any]:
791
1038
  if target in {"vector", "index", "vector_index"}: