ltcai 4.0.1 → 4.2.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 (192) hide show
  1. package/README.md +33 -24
  2. package/desktop/electron/main.cjs +44 -0
  3. package/docs/CHANGELOG.md +84 -0
  4. package/docs/V4_1_FRONTEND_ARCHITECTURE_REVIEW.md +65 -0
  5. package/docs/V4_1_FRONTEND_MIGRATION_REPORT.md +70 -0
  6. package/docs/V4_1_VALIDATION_REPORT.md +47 -0
  7. package/docs/V4_2_BRAIN_CORE_ARCHITECTURE.md +97 -0
  8. package/docs/V4_2_STORAGE_MIGRATION_REPORT.md +91 -0
  9. package/docs/V4_2_VALIDATION_REPORT.md +89 -0
  10. package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +31 -26
  11. package/frontend/index.html +24 -0
  12. package/frontend/openapi.json +14436 -0
  13. package/frontend/src/App.tsx +184 -0
  14. package/frontend/src/api/client.ts +320 -0
  15. package/frontend/src/api/openapi.ts +16921 -0
  16. package/frontend/src/components/primitives.tsx +204 -0
  17. package/frontend/src/components/ui/badge.tsx +27 -0
  18. package/frontend/src/components/ui/button.tsx +37 -0
  19. package/frontend/src/components/ui/card.tsx +22 -0
  20. package/frontend/src/components/ui/input.tsx +16 -0
  21. package/frontend/src/components/ui/textarea.tsx +16 -0
  22. package/frontend/src/lib/utils.ts +33 -0
  23. package/frontend/src/main.tsx +23 -0
  24. package/frontend/src/pages/Act.tsx +245 -0
  25. package/frontend/src/pages/Ask.tsx +200 -0
  26. package/frontend/src/pages/Brain.tsx +267 -0
  27. package/frontend/src/pages/Capture.tsx +158 -0
  28. package/frontend/src/pages/Library.tsx +187 -0
  29. package/frontend/src/pages/System.tsx +378 -0
  30. package/frontend/src/routes.ts +85 -0
  31. package/frontend/src/store/appStore.ts +54 -0
  32. package/frontend/src/styles.css +107 -0
  33. package/kg_schema.py +1 -1
  34. package/knowledge_graph.py +4 -4
  35. package/lattice_brain/__init__.py +70 -0
  36. package/lattice_brain/_kg_common.py +1 -0
  37. package/lattice_brain/archive.py +133 -0
  38. package/lattice_brain/context.py +3 -0
  39. package/lattice_brain/conversations.py +3 -0
  40. package/lattice_brain/core.py +82 -0
  41. package/lattice_brain/discovery.py +1 -0
  42. package/lattice_brain/documents.py +1 -0
  43. package/lattice_brain/embeddings.py +82 -0
  44. package/lattice_brain/identity.py +13 -0
  45. package/lattice_brain/ingest.py +1 -0
  46. package/lattice_brain/memory.py +3 -0
  47. package/lattice_brain/network.py +1 -0
  48. package/lattice_brain/projection.py +1 -0
  49. package/lattice_brain/provenance.py +1 -0
  50. package/lattice_brain/retrieval.py +1 -0
  51. package/lattice_brain/schema.py +1 -0
  52. package/lattice_brain/storage/__init__.py +22 -0
  53. package/lattice_brain/storage/base.py +72 -0
  54. package/lattice_brain/storage/docker.py +105 -0
  55. package/lattice_brain/storage/factory.py +31 -0
  56. package/lattice_brain/storage/migration.py +190 -0
  57. package/lattice_brain/storage/postgres.py +123 -0
  58. package/lattice_brain/storage/sqlite.py +128 -0
  59. package/lattice_brain/store.py +3 -0
  60. package/lattice_brain/write_master.py +1 -0
  61. package/latticeai/__init__.py +1 -1
  62. package/latticeai/api/portability.py +69 -0
  63. package/latticeai/api/setup.py +5 -4
  64. package/latticeai/api/static_routes.py +4 -4
  65. package/latticeai/app_factory.py +17 -10
  66. package/latticeai/brain/__init__.py +6 -6
  67. package/latticeai/brain/_kg_common.py +1 -1
  68. package/latticeai/brain/network.py +1 -1
  69. package/latticeai/brain/retrieval.py +15 -0
  70. package/latticeai/brain/store.py +22 -6
  71. package/latticeai/core/config.py +8 -0
  72. package/latticeai/core/marketplace.py +1 -1
  73. package/latticeai/core/multi_agent.py +1 -1
  74. package/latticeai/core/workspace_os.py +1 -1
  75. package/latticeai/services/kg_portability.py +82 -1
  76. package/package.json +55 -15
  77. package/scripts/build_frontend_assets.mjs +38 -0
  78. package/scripts/bump_version.py +4 -1
  79. package/scripts/export_openapi.py +31 -0
  80. package/scripts/lint_frontend.mjs +91 -0
  81. package/scripts/migrate_brain_storage.py +53 -0
  82. package/scripts/run_python.mjs +47 -0
  83. package/scripts/wheel_smoke.py +3 -0
  84. package/src-tauri/Cargo.lock +4833 -0
  85. package/src-tauri/Cargo.toml +19 -0
  86. package/src-tauri/build.rs +3 -0
  87. package/src-tauri/capabilities/default.json +7 -0
  88. package/src-tauri/src/main.rs +78 -0
  89. package/src-tauri/tauri.conf.json +39 -0
  90. package/static/app/asset-manifest.json +32 -0
  91. package/static/app/assets/core-CwxXejkd.js +2 -0
  92. package/static/app/assets/core-CwxXejkd.js.map +1 -0
  93. package/static/app/assets/index-CDjiH_se.css +2 -0
  94. package/static/app/assets/index-C_HAkbAg.js +333 -0
  95. package/static/app/assets/index-C_HAkbAg.js.map +1 -0
  96. package/static/app/index.html +25 -0
  97. package/static/manifest.json +2 -2
  98. package/static/sw.js +4 -4
  99. package/scripts/build_v3_assets.mjs +0 -170
  100. package/scripts/lint_v3.mjs +0 -120
  101. package/static/v3/asset-manifest.json +0 -63
  102. package/static/v3/css/lattice.base.49deefb5.css +0 -128
  103. package/static/v3/css/lattice.base.css +0 -128
  104. package/static/v3/css/lattice.components.cde18231.css +0 -472
  105. package/static/v3/css/lattice.components.css +0 -472
  106. package/static/v3/css/lattice.shell.29d36d85.css +0 -452
  107. package/static/v3/css/lattice.shell.css +0 -452
  108. package/static/v3/css/lattice.tokens.304cbc40.css +0 -135
  109. package/static/v3/css/lattice.tokens.css +0 -135
  110. package/static/v3/css/lattice.views.0a18b6c5.css +0 -360
  111. package/static/v3/css/lattice.views.css +0 -360
  112. package/static/v3/index.html +0 -68
  113. package/static/v3/js/app.c5c80c46.js +0 -26
  114. package/static/v3/js/app.js +0 -26
  115. package/static/v3/js/core/api.ba0fbf14.js +0 -625
  116. package/static/v3/js/core/api.js +0 -625
  117. package/static/v3/js/core/components.f25b3b93.js +0 -230
  118. package/static/v3/js/core/components.js +0 -230
  119. package/static/v3/js/core/dom.a2773eb0.js +0 -148
  120. package/static/v3/js/core/dom.js +0 -148
  121. package/static/v3/js/core/i18n.880e1fec.js +0 -575
  122. package/static/v3/js/core/i18n.js +0 -575
  123. package/static/v3/js/core/router.584570f2.js +0 -37
  124. package/static/v3/js/core/router.js +0 -37
  125. package/static/v3/js/core/routes.37522821.js +0 -101
  126. package/static/v3/js/core/routes.js +0 -101
  127. package/static/v3/js/core/shell.e3f6bbfa.js +0 -420
  128. package/static/v3/js/core/shell.js +0 -420
  129. package/static/v3/js/core/store.7b2aa044.js +0 -123
  130. package/static/v3/js/core/store.js +0 -123
  131. package/static/v3/js/views/account.eff40715.js +0 -143
  132. package/static/v3/js/views/account.js +0 -143
  133. package/static/v3/js/views/activity.0d271ef9.js +0 -67
  134. package/static/v3/js/views/activity.js +0 -67
  135. package/static/v3/js/views/admin-audit.660a1fb1.js +0 -185
  136. package/static/v3/js/views/admin-audit.js +0 -185
  137. package/static/v3/js/views/admin-permissions.a7ae5f09.js +0 -177
  138. package/static/v3/js/views/admin-permissions.js +0 -177
  139. package/static/v3/js/views/admin-policies.3658fd86.js +0 -102
  140. package/static/v3/js/views/admin-policies.js +0 -102
  141. package/static/v3/js/views/admin-private-vpc.7d342d36.js +0 -135
  142. package/static/v3/js/views/admin-private-vpc.js +0 -135
  143. package/static/v3/js/views/admin-security.07c66b72.js +0 -180
  144. package/static/v3/js/views/admin-security.js +0 -180
  145. package/static/v3/js/views/admin-users.f7ac7b43.js +0 -166
  146. package/static/v3/js/views/admin-users.js +0 -166
  147. package/static/v3/js/views/agents.17c5288d.js +0 -564
  148. package/static/v3/js/views/agents.js +0 -564
  149. package/static/v3/js/views/chat.e250e2cc.js +0 -624
  150. package/static/v3/js/views/chat.js +0 -624
  151. package/static/v3/js/views/files.adad14c1.js +0 -365
  152. package/static/v3/js/views/files.js +0 -365
  153. package/static/v3/js/views/graph-canvas.17c15d65.js +0 -509
  154. package/static/v3/js/views/graph-canvas.js +0 -509
  155. package/static/v3/js/views/home.24f8b8ae.js +0 -200
  156. package/static/v3/js/views/home.js +0 -200
  157. package/static/v3/js/views/hooks.37895880.js +0 -220
  158. package/static/v3/js/views/hooks.js +0 -220
  159. package/static/v3/js/views/hybrid-search.2fb63ed9.js +0 -194
  160. package/static/v3/js/views/hybrid-search.js +0 -194
  161. package/static/v3/js/views/knowledge-graph.4d09c537.js +0 -529
  162. package/static/v3/js/views/knowledge-graph.js +0 -529
  163. package/static/v3/js/views/marketplace.ab0583d4.js +0 -141
  164. package/static/v3/js/views/marketplace.js +0 -141
  165. package/static/v3/js/views/mcp.99b5c6a7.js +0 -114
  166. package/static/v3/js/views/mcp.js +0 -114
  167. package/static/v3/js/views/memory.4ebdf474.js +0 -147
  168. package/static/v3/js/views/memory.js +0 -147
  169. package/static/v3/js/views/models.a1ffa147.js +0 -256
  170. package/static/v3/js/views/models.js +0 -256
  171. package/static/v3/js/views/my-computer.d9d9ae1c.js +0 -463
  172. package/static/v3/js/views/my-computer.js +0 -463
  173. package/static/v3/js/views/network.52a4f181.js +0 -97
  174. package/static/v3/js/views/network.js +0 -97
  175. package/static/v3/js/views/pipeline.c522f1ce.js +0 -157
  176. package/static/v3/js/views/pipeline.js +0 -157
  177. package/static/v3/js/views/planning.4876fd77.js +0 -174
  178. package/static/v3/js/views/planning.js +0 -174
  179. package/static/v3/js/views/runs.b63b2afa.js +0 -144
  180. package/static/v3/js/views/runs.js +0 -144
  181. package/static/v3/js/views/settings.b7140634.js +0 -317
  182. package/static/v3/js/views/settings.js +0 -317
  183. package/static/v3/js/views/skills.c6c2f965.js +0 -109
  184. package/static/v3/js/views/skills.js +0 -109
  185. package/static/v3/js/views/snapshots.6f5db095.js +0 -135
  186. package/static/v3/js/views/snapshots.js +0 -135
  187. package/static/v3/js/views/tools.e4f11276.js +0 -108
  188. package/static/v3/js/views/tools.js +0 -108
  189. package/static/v3/js/views/workflows.7752225a.js +0 -213
  190. package/static/v3/js/views/workflows.js +0 -213
  191. package/static/v3/js/views/workspace-admin.c466029b.js +0 -156
  192. package/static/v3/js/views/workspace-admin.js +0 -156
@@ -28,6 +28,28 @@ class RestoreRequest(BaseModel):
28
28
  verify: bool = True
29
29
 
30
30
 
31
+ class EncryptedArchiveRequest(BaseModel):
32
+ path: Optional[str] = None
33
+ passphrase: str
34
+
35
+
36
+ class EncryptedRestoreRequest(BaseModel):
37
+ path: str
38
+ passphrase: str
39
+
40
+
41
+ class DockerPostgresRequest(BaseModel):
42
+ consent: bool = False
43
+ dry_run: bool = False
44
+ port: int = 5432
45
+
46
+
47
+ class SQLiteToPostgresRequest(BaseModel):
48
+ dsn: str
49
+ schema_name: str = "lattice_brain"
50
+ dry_run: bool = True
51
+
52
+
31
53
  def create_portability_router(
32
54
  *,
33
55
  service: Any,
@@ -46,6 +68,12 @@ def create_portability_router(
46
68
  _require_service()
47
69
  return service.snapshot_metadata()
48
70
 
71
+ @router.get("/api/brain/storage")
72
+ async def brain_storage_status(request: Request):
73
+ require_user(request)
74
+ _require_service()
75
+ return service.storage_status()
76
+
49
77
  @router.get("/api/knowledge-graph/provenance")
50
78
  async def recent_provenance(request: Request, limit: int = 50, source_type: Optional[str] = None):
51
79
  """Recent ingestions (provenance trail) for the ingestion-sources UI."""
@@ -90,4 +118,45 @@ def create_portability_router(
90
118
  except (ValueError, FileNotFoundError) as exc:
91
119
  raise HTTPException(status_code=400, detail=str(exc))
92
120
 
121
+ @router.post("/api/knowledge-graph/archive")
122
+ async def encrypted_archive(req: EncryptedArchiveRequest, request: Request):
123
+ require_admin(request)
124
+ _require_service()
125
+ try:
126
+ return service.encrypted_archive(req.path, passphrase=req.passphrase)
127
+ except (ValueError, FileNotFoundError) as exc:
128
+ raise HTTPException(status_code=400, detail=str(exc))
129
+
130
+ @router.post("/api/knowledge-graph/archive/restore")
131
+ async def restore_encrypted_archive(req: EncryptedRestoreRequest, request: Request):
132
+ require_admin(request)
133
+ _require_service()
134
+ try:
135
+ return service.restore_encrypted_archive(req.path, passphrase=req.passphrase)
136
+ except (ValueError, FileNotFoundError) as exc:
137
+ raise HTTPException(status_code=400, detail=str(exc))
138
+
139
+ @router.post("/api/brain/storage/postgres/docker")
140
+ async def setup_postgres_docker(req: DockerPostgresRequest, request: Request):
141
+ require_admin(request)
142
+ _require_service()
143
+ return service.postgres_docker_setup(
144
+ consent=req.consent,
145
+ dry_run=req.dry_run,
146
+ port=req.port,
147
+ )
148
+
149
+ @router.post("/api/brain/storage/migrate-postgres")
150
+ async def migrate_sqlite_to_postgres(req: SQLiteToPostgresRequest, request: Request):
151
+ require_admin(request)
152
+ _require_service()
153
+ try:
154
+ return service.migrate_sqlite_to_postgres(
155
+ dsn=req.dsn,
156
+ schema=req.schema_name,
157
+ dry_run=req.dry_run,
158
+ )
159
+ except (ValueError, FileNotFoundError, RuntimeError) as exc:
160
+ raise HTTPException(status_code=400, detail=str(exc))
161
+
93
162
  return router
@@ -19,15 +19,16 @@ from latticeai.models.router import parse_model_ref
19
19
  from setup_wizard import get_recommendations, install_stream, open_url, scan_environment
20
20
 
21
21
 
22
+ class SetupInstallRequest(BaseModel):
23
+ items: List[Dict]
24
+
25
+
22
26
  def create_setup_router(*, model_router, require_user) -> APIRouter:
23
27
  api_router = APIRouter()
24
28
  router = model_router
25
29
 
26
30
  # ── Setup Wizard ─────────────────────────────────────────────────────────────
27
-
28
- class SetupInstallRequest(BaseModel):
29
- items: List[Dict]
30
-
31
+
31
32
  def setup_auto_state() -> Dict[str, object]:
32
33
  """Return the PPT-aligned zero-config setup state used by setup UI/API."""
33
34
  profile = auto_setup_probe()
@@ -84,7 +84,7 @@ def create_static_routes_router(
84
84
  raise HTTPException(status_code=404)
85
85
  return FileResponse(str(p), media_type="application/manifest+json")
86
86
 
87
- @api_router.api_route("/favicon.ico", methods=["GET", "HEAD"])
87
+ @api_router.api_route("/favicon.ico", methods=["GET", "HEAD"], include_in_schema=False)
88
88
  async def favicon():
89
89
  ico = STATIC_DIR / "favicon.ico"
90
90
  png = STATIC_DIR / "icons" / "favicon-32.png"
@@ -112,10 +112,10 @@ def create_static_routes_router(
112
112
 
113
113
  @api_router.get("/app")
114
114
  async def app_shell(request: Request):
115
- """v3 single-page workspace shell (token-native design system)."""
116
- page = STATIC_DIR / "v3" / "index.html"
115
+ """React desktop single-page workspace shell."""
116
+ page = STATIC_DIR / "app" / "index.html"
117
117
  if not page.exists():
118
- raise HTTPException(status_code=404, detail="v3 shell not found.")
118
+ raise HTTPException(status_code=404, detail="React shell not found.")
119
119
  return ui_file_response(page)
120
120
 
121
121
 
@@ -60,7 +60,7 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
60
60
  from pydantic import BaseModel
61
61
 
62
62
  from latticeai.models.router import LLMRouter, normalize_branding
63
- from knowledge_graph import KnowledgeGraphStore, set_llm_router
63
+ from knowledge_graph import set_llm_router
64
64
  from local_knowledge_api import LocalKnowledgeWatcher
65
65
  from latticeai.core.security import (
66
66
  hash_password,
@@ -161,11 +161,12 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
161
161
  from latticeai.api.portability import create_portability_router
162
162
  from latticeai.services.memory_service import MemoryService
163
163
  from latticeai.services.ingestion import IngestionItem, IngestionPipeline
164
- from latticeai.brain.conversations import ConversationStore
165
- from latticeai.brain.context import ContextAssembler
166
- from latticeai.brain.memory import BrainMemory
167
- from latticeai.brain.identity import DeviceIdentity
168
- from latticeai.brain.network import BrainNetwork
164
+ from lattice_brain import BrainCore, ConversationStore
165
+ from lattice_brain.storage import storage_from_env
166
+ from lattice_brain.context import ContextAssembler
167
+ from lattice_brain.memory import BrainMemory
168
+ from lattice_brain.identity import DeviceIdentity
169
+ from lattice_brain.network import BrainNetwork
169
170
  from latticeai.api.network import create_network_router
170
171
  from latticeai.services.kg_portability import KGPortabilityService
171
172
  # The aliased names below look unused but are part of the legacy
@@ -342,16 +343,22 @@ def _build(config: "Optional[Config]" = None) -> Dict[str, Any]:
342
343
  )
343
344
  if EMBEDDER.fell_back:
344
345
  logging.warning("Embedding provider %s unavailable: %s", EMBEDDER.requested, EMBEDDER.detail)
345
- KNOWLEDGE_GRAPH = KnowledgeGraphStore(
346
- DATA_DIR / "knowledge_graph.sqlite",
347
- DATA_DIR / "knowledge_graph_blobs",
346
+ STORAGE_ENGINE = storage_from_env(os.environ, data_dir=DATA_DIR) if ENABLE_GRAPH else None
347
+ BRAIN_CORE = BrainCore.from_paths(
348
+ DATA_DIR,
348
349
  embedder=EMBEDDER.provider,
350
+ storage_engine=STORAGE_ENGINE,
349
351
  ) if ENABLE_GRAPH else None
352
+ KNOWLEDGE_GRAPH = BRAIN_CORE.knowledge if BRAIN_CORE is not None else None
350
353
  # ── v4 durable conversation store: unbounded episodic memory in the same
351
354
  # SQLite file as the graph (kg_portability backup/restore covers it for
352
355
  # free). Legacy chat_history.json is imported once, idempotently, and the
353
356
  # file is left untouched on disk as the import source.
354
- CONVERSATIONS = ConversationStore(DATA_DIR / "knowledge_graph.sqlite")
357
+ CONVERSATIONS = (
358
+ BRAIN_CORE.conversations
359
+ if BRAIN_CORE is not None
360
+ else ConversationStore(DATA_DIR / "knowledge_graph.sqlite")
361
+ )
355
362
  CONVERSATIONS.import_legacy_json(HISTORY_FILE)
356
363
  # Hooks registry is constructed here (ahead of the watcher) so folder-watch
357
364
  # reindexes can fire the pre_index/post_index lifecycle hooks.
@@ -1,18 +1,18 @@
1
- """latticeai.brain the durable substrate of the Digital Brain.
2
-
3
- v4 home for the brain's storage modules. The knowledge-graph store itself
4
- still lives in the root ``knowledge_graph`` module pending its decomposition
5
- (T3d); new brain components land here first.
6
- """
1
+ """Compatibility namespace for the standalone :mod:`lattice_brain` package."""
7
2
 
3
+ from lattice_brain.core import BrainCore, BrainCoreConfig
8
4
  from latticeai.brain.context import AssembledContext, ContextAssembler, ContextSection
9
5
  from latticeai.brain.conversations import ConversationStore
10
6
  from latticeai.brain.memory import BrainMemory
7
+ from latticeai.brain.store import KnowledgeGraphStore
11
8
 
12
9
  __all__ = [
13
10
  "AssembledContext",
11
+ "BrainCore",
12
+ "BrainCoreConfig",
14
13
  "BrainMemory",
15
14
  "ContextAssembler",
16
15
  "ContextSection",
17
16
  "ConversationStore",
17
+ "KnowledgeGraphStore",
18
18
  ]
@@ -33,7 +33,7 @@ except Exception: # pragma: no cover - v2 schema is optional at import time
33
33
  EdgeType = None # type: ignore[assignment]
34
34
  _exec_script = None # type: ignore[assignment]
35
35
 
36
- from latticeai.core.local_embeddings import LocalEmbeddingModel
36
+ from lattice_brain.embeddings import LocalEmbeddingModel
37
37
 
38
38
  # Default read source for the graph queries: v2 reconstruction views.
39
39
  # Override with LATTICEAI_KG_READ_V2=0 to fall back to the legacy tables.
@@ -23,7 +23,7 @@ import uuid
23
23
  from pathlib import Path
24
24
  from typing import Any, Dict, List, Optional
25
25
 
26
- from latticeai.brain.identity import DeviceIdentity, fingerprint_of, verify_signature
26
+ from lattice_brain.identity import DeviceIdentity, fingerprint_of, verify_signature
27
27
 
28
28
  PEER_AUTH_WINDOW_SECONDS = 300
29
29
  _NONCE_CACHE_MAX = 4096
@@ -813,6 +813,15 @@ class KnowledgeGraphRetrievalMixin:
813
813
  raise
814
814
 
815
815
  def index_status(self) -> Dict[str, Any]:
816
+ storage_capabilities = None
817
+ try:
818
+ storage_capabilities = self.storage_engine.capabilities().as_dict()
819
+ except Exception as exc:
820
+ storage_capabilities = {
821
+ "engine": "sqlite",
822
+ "available": False,
823
+ "reason": str(exc),
824
+ }
816
825
  with self._connect() as conn:
817
826
  vector_counts = {
818
827
  row["item_type"]: row["count"]
@@ -864,6 +873,12 @@ class KnowledgeGraphRetrievalMixin:
864
873
  # Honest capability report: trigram FTS5 keyword index, or
865
874
  # LIKE-scan fallback when this SQLite build lacks it.
866
875
  "fts_enabled": bool(getattr(self, "_fts_enabled", False)),
876
+ "engine": storage_capabilities,
877
+ "vector_search_backend": (
878
+ storage_capabilities.get("vector_backend")
879
+ if isinstance(storage_capabilities, dict)
880
+ else "bruteforce-cosine"
881
+ ),
867
882
  },
868
883
  "source_items": len(source_items),
869
884
  "indexed_items": sum(vector_counts.values()),
@@ -21,11 +21,31 @@ class KnowledgeGraphStore(
21
21
  KnowledgeGraphDocumentsMixin,
22
22
  KnowledgeGraphRetrievalMixin,
23
23
  ):
24
- def __init__(self, db_path: Path, blob_dir: Path, embedder: Any = None):
24
+ def __init__(
25
+ self,
26
+ db_path: Path,
27
+ blob_dir: Path,
28
+ embedder: Any = None,
29
+ storage_engine: Any = None,
30
+ ):
25
31
  self.db_path = Path(db_path)
26
32
  self.blob_dir = Path(blob_dir)
27
33
  self.db_path.parent.mkdir(parents=True, exist_ok=True)
28
34
  self.blob_dir.mkdir(parents=True, exist_ok=True)
35
+ if storage_engine is None:
36
+ from lattice_brain.storage import SQLiteEngine
37
+
38
+ storage_engine = SQLiteEngine(self.db_path)
39
+ storage_caps = storage_engine.capabilities()
40
+ if not storage_caps.available:
41
+ raise RuntimeError(storage_caps.reason or "Brain storage is unavailable.")
42
+ if storage_caps.engine != "sqlite":
43
+ raise RuntimeError(
44
+ "KnowledgeGraphStore currently requires SQLiteEngine. "
45
+ "Explicit non-SQLite storage must use the migration/scale tooling; "
46
+ "no SQLite fallback is attempted."
47
+ )
48
+ self.storage_engine = storage_engine
29
49
  # The embedder is swappable behind a fixed interface
30
50
  # (model_id/dim/embed/encode/decode/similarity). Defaults to the
31
51
  # deterministic, offline hash model so the store works with no config;
@@ -49,11 +69,7 @@ class KnowledgeGraphStore(
49
69
  return ("nodes", "edges")
50
70
 
51
71
  def _connect(self) -> sqlite3.Connection:
52
- conn = sqlite3.connect(str(self.db_path))
53
- conn.row_factory = sqlite3.Row
54
- conn.execute("PRAGMA journal_mode=WAL")
55
- conn.execute("PRAGMA foreign_keys=ON")
56
- return conn
72
+ return self.storage_engine.connect()
57
73
 
58
74
  def _init_db(self) -> None:
59
75
  with self._connect() as conn:
@@ -104,6 +104,11 @@ class Config:
104
104
  embedding_timeout: int
105
105
  embedding_custom_target: str
106
106
 
107
+ # ── brain storage ───────────────────────────────────────────────
108
+ storage_engine: str
109
+ postgres_dsn: str
110
+ postgres_schema: str
111
+
107
112
  # ── SSO / OIDC ──────────────────────────────────────────────────
108
113
  sso_discovery_url: str
109
114
  sso_client_id: str
@@ -185,6 +190,9 @@ class Config:
185
190
  embedding_dim=_int(env, "LATTICEAI_VECTOR_DIM", 0),
186
191
  embedding_timeout=_int(env, "LATTICEAI_EMBEDDING_TIMEOUT", 30),
187
192
  embedding_custom_target=_value(env, "LATTICEAI_EMBEDDING_CUSTOM_TARGET", ""),
193
+ storage_engine=_value(env, "LATTICEAI_STORAGE_ENGINE", "sqlite").strip().lower() or "sqlite",
194
+ postgres_dsn=_value(env, "LATTICEAI_POSTGRES_DSN", ""),
195
+ postgres_schema=_value(env, "LATTICEAI_POSTGRES_SCHEMA", "lattice_brain"),
188
196
  sso_discovery_url=_value(env, "OIDC_DISCOVERY_URL", ""),
189
197
  sso_client_id=_value(env, "OIDC_CLIENT_ID", ""),
190
198
  sso_client_secret=_value(env, "OIDC_CLIENT_SECRET", ""),
@@ -11,7 +11,7 @@ from copy import deepcopy
11
11
  from typing import Any, Dict, List, Optional
12
12
 
13
13
 
14
- MARKETPLACE_VERSION = "4.0.1"
14
+ MARKETPLACE_VERSION = "4.2.0"
15
15
  TEMPLATE_KINDS = ("plugin", "workflow", "agent")
16
16
 
17
17
 
@@ -14,7 +14,7 @@ from datetime import datetime
14
14
  from typing import Any, Callable, Dict, List, Optional
15
15
 
16
16
 
17
- MULTI_AGENT_VERSION = "4.0.1"
17
+ MULTI_AGENT_VERSION = "4.2.0"
18
18
 
19
19
  AGENT_ROLES = ("researcher", "planner", "executor", "reviewer", "release")
20
20
  CORE_PIPELINE = ("planner", "executor", "reviewer")
@@ -19,7 +19,7 @@ from pathlib import Path
19
19
  from typing import Any, Callable, Dict, Iterable, List, Optional
20
20
 
21
21
 
22
- WORKSPACE_OS_VERSION = "4.0.1"
22
+ WORKSPACE_OS_VERSION = "4.2.0"
23
23
 
24
24
  # Workspace types separate single-user Personal workspaces from shared
25
25
  # Organization workspaces. Both keep the same local-first JSON store; the type
@@ -22,6 +22,13 @@ from datetime import datetime, timezone
22
22
  from pathlib import Path
23
23
  from typing import Any, Dict, Optional
24
24
 
25
+ from lattice_brain.archive import BrainArchivePaths, EncryptedBrainArchive
26
+ from lattice_brain.storage import (
27
+ DockerPostgresWizard,
28
+ PostgresEngine,
29
+ SQLiteToPostgresMigrator,
30
+ )
31
+
25
32
  FORMAT = "latticeai.kg.export"
26
33
  FORMAT_VERSION = 1
27
34
  BACKUP_FORMAT = "latticeai.kg.backup"
@@ -95,7 +102,7 @@ class KGPortabilityService:
95
102
  origin = "unsigned-legacy"
96
103
  signature = artifact.get("signature")
97
104
  if signature:
98
- from latticeai.brain.identity import verify_manifest
105
+ from lattice_brain.identity import verify_manifest
99
106
 
100
107
  if not verify_manifest(artifact.get("header") or {}, signature):
101
108
  raise ValueError("Bundle signature verification failed — refusing to import.")
@@ -189,6 +196,36 @@ class KGPortabilityService:
189
196
  "nodes": sum(stats.get("nodes", {}).values()),
190
197
  }
191
198
 
199
+ # ── encrypted .latticebrain archive ───────────────────────────────────
200
+ def encrypted_archive(self, dest_path=None, *, passphrase: str) -> Dict[str, Any]:
201
+ self._require()
202
+ self._exports_dir.mkdir(parents=True, exist_ok=True)
203
+ dest = Path(dest_path) if dest_path else self._exports_dir / f"brain-{_stamp()}.latticebrain"
204
+ archive = EncryptedBrainArchive(
205
+ BrainArchivePaths(
206
+ db_path=Path(self._kg.db_path),
207
+ blob_dir=Path(self._kg.blob_dir),
208
+ )
209
+ )
210
+ return archive.create(dest, passphrase=passphrase)
211
+
212
+ def restore_encrypted_archive(self, archive_path, *, passphrase: str) -> Dict[str, Any]:
213
+ self._require()
214
+ archive = EncryptedBrainArchive(
215
+ BrainArchivePaths(
216
+ db_path=Path(self._kg.db_path),
217
+ blob_dir=Path(self._kg.blob_dir),
218
+ )
219
+ )
220
+ return archive.restore(
221
+ Path(archive_path),
222
+ passphrase=passphrase,
223
+ target=BrainArchivePaths(
224
+ db_path=Path(self._kg.db_path),
225
+ blob_dir=Path(self._kg.blob_dir),
226
+ ),
227
+ )
228
+
192
229
  # ── status surface ───────────────────────────────────────────────────────
193
230
  def snapshot_metadata(self) -> Dict[str, Any]:
194
231
  if not self.available():
@@ -198,8 +235,52 @@ class KGPortabilityService:
198
235
  **self._kg.schema_versions(),
199
236
  "stats": self._kg.stats(),
200
237
  "provenance": self._kg.provenance_stats(),
238
+ "storage": (
239
+ self._kg.storage_engine.capabilities().as_dict()
240
+ if getattr(self._kg, "storage_engine", None) is not None
241
+ else {"engine": "sqlite", "available": True}
242
+ ),
201
243
  }
202
244
 
245
+ def storage_status(self) -> Dict[str, Any]:
246
+ if not self.available():
247
+ return {"available": False}
248
+ return {
249
+ "available": True,
250
+ "active": (
251
+ self._kg.storage_engine.capabilities().as_dict()
252
+ if getattr(self._kg, "storage_engine", None) is not None
253
+ else {"engine": "sqlite", "available": True}
254
+ ),
255
+ "postgres": PostgresEngine("", schema="lattice_brain").capabilities().as_dict(),
256
+ }
257
+
258
+ def postgres_docker_setup(
259
+ self,
260
+ *,
261
+ consent: bool,
262
+ dry_run: bool = False,
263
+ port: int = 5432,
264
+ ) -> Dict[str, Any]:
265
+ wizard = DockerPostgresWizard(self._data_dir / "postgres", port=port)
266
+ return wizard.start(consent=consent, dry_run=dry_run)
267
+
268
+ def migrate_sqlite_to_postgres(
269
+ self,
270
+ *,
271
+ dsn: str,
272
+ schema: str = "lattice_brain",
273
+ dry_run: bool = True,
274
+ ) -> Dict[str, Any]:
275
+ self._require()
276
+ if not dsn:
277
+ raise ValueError("Postgres DSN is required for SQLite to Postgres migration.")
278
+ migrator = SQLiteToPostgresMigrator(
279
+ Path(self._kg.db_path),
280
+ PostgresEngine(dsn, schema=schema),
281
+ )
282
+ return migrator.migrate(dry_run=dry_run)
283
+
203
284
  def recent_ingestions(self, *, limit: int = 50, source_type: Optional[str] = None) -> Dict[str, Any]:
204
285
  """Recent provenance records (newest first) for the ingestion-sources UI."""
205
286
  if not self.available():
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ltcai",
3
- "version": "4.0.1",
3
+ "version": "4.2.0",
4
4
  "description": "Lattice AI — local-first Digital Brain Platform (knowledge graph, durable memory, hybrid search, agents, signed brain exchange)",
5
5
  "homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
6
6
  "repository": {
@@ -18,29 +18,36 @@
18
18
  "start": "LTCAI",
19
19
  "dev": "python3 ltcai_cli.py --reload",
20
20
  "build": "npm run build:assets && npm run build:python",
21
- "build:assets": "node scripts/build_v3_assets.mjs",
22
- "build:python": "python3 -m build",
23
- "check:python": "python3 scripts/check_python.py",
24
- "lint": "node --check tests/visual/mock_server.cjs && node --check tests/visual/v3.spec.js && npm run lint:v3",
25
- "lint:v3": "node scripts/lint_v3.mjs",
26
- "typecheck": "cd vscode-extension && npm run build",
27
- "test": "python3 -m pytest tests/ -v",
28
- "test:unit": "python3 -m pytest tests/unit/ -v",
29
- "test:integration": "python3 -m pytest tests/integration/ -v",
21
+ "frontend:dev": "vite --host 127.0.0.1",
22
+ "frontend:openapi": "node scripts/run_python.mjs scripts/export_openapi.py frontend/openapi.json && npx openapi-typescript frontend/openapi.json -o frontend/src/api/openapi.ts",
23
+ "build:assets": "vite build && node scripts/build_frontend_assets.mjs",
24
+ "build:python": "node scripts/run_python.mjs -m build",
25
+ "check:python": "node scripts/run_python.mjs scripts/check_python.py",
26
+ "lint": "node --check tests/visual/mock_server.cjs && node --check tests/visual/v3.spec.js && npm run lint:frontend",
27
+ "lint:frontend": "node scripts/lint_frontend.mjs",
28
+ "typecheck": "npm run typecheck:frontend && cd vscode-extension && npm run build",
29
+ "typecheck:frontend": "npx tsc -p tsconfig.json --noEmit",
30
+ "test": "node scripts/run_python.mjs -m pytest tests/ -v",
31
+ "test:unit": "node scripts/run_python.mjs -m pytest tests/unit/ -v",
32
+ "test:integration": "node scripts/run_python.mjs -m pytest tests/integration/ -v",
30
33
  "test:visual": "playwright test",
31
34
  "capture:workspace": "node scripts/capture/capture_workspace.js",
32
35
  "capture:graph": "node scripts/capture/capture_graph.js",
33
36
  "capture:skills": "node scripts/capture/capture_skills.js",
34
37
  "capture:enterprise": "node scripts/capture/capture_enterprise.js",
35
38
  "capture:onboarding": "node scripts/capture/capture_onboarding.js",
39
+ "desktop:tauri": "tauri dev",
40
+ "desktop:tauri:build": "tauri build",
41
+ "desktop:tauri:check": "cd src-tauri && cargo check",
42
+ "desktop:electron": "electron desktop/electron/main.cjs",
36
43
  "package:vsix": "node scripts/build_vsix.mjs",
37
44
  "release:artifacts": "npm run build:assets && npm run build:python && npm pack && npm run package:vsix",
38
- "release:validate": "python3 scripts/validate_release_artifacts.py $npm_package_version --require-vsix --require-tgz",
45
+ "release:validate": "node scripts/run_python.mjs scripts/validate_release_artifacts.py $npm_package_version --require-vsix --require-tgz",
39
46
  "publish:npm": "npm pack && npm publish ltcai-$npm_package_version.tgz --access public",
40
- "publish:pypi": "npm run build:python && python3 -m twine upload --skip-existing dist/ltcai-$npm_package_version.tar.gz dist/ltcai-$npm_package_version-py3-none-any.whl",
47
+ "publish:pypi": "npm run build:python && node scripts/run_python.mjs -m twine upload --skip-existing dist/ltcai-$npm_package_version.tar.gz dist/ltcai-$npm_package_version-py3-none-any.whl",
41
48
  "publish:vscode": "cd vscode-extension && npm run package:vsix && npm run publish:vscode",
42
49
  "publish:openvsx": "cd vscode-extension && npm run package:vsix && npm run publish:openvsx",
43
- "publish:all": "npm run release:artifacts && npm run release:validate && npm publish ltcai-$npm_package_version.tgz --access public && python3 -m twine upload --skip-existing dist/ltcai-$npm_package_version.tar.gz dist/ltcai-$npm_package_version-py3-none-any.whl && cd vscode-extension && npm run publish:vscode && npm run publish:openvsx"
50
+ "publish:all": "npm run release:artifacts && npm run release:validate && npm publish ltcai-$npm_package_version.tgz --access public && node scripts/run_python.mjs -m twine upload --skip-existing dist/ltcai-$npm_package_version.tar.gz dist/ltcai-$npm_package_version-py3-none-any.whl && cd vscode-extension && npm run publish:vscode && npm run publish:openvsx"
44
51
  },
45
52
  "keywords": [
46
53
  "ltcai",
@@ -73,12 +80,14 @@
73
80
  "tools/",
74
81
  "mcp_registry.py",
75
82
  "latticeai/**/*.py",
83
+ "lattice_brain/**/*.py",
76
84
  "skills/",
77
85
  "static/favicon.ico",
78
86
  "static/manifest.json",
79
87
  "static/sw.js",
80
88
  "static/css/",
81
- "static/v3/",
89
+ "frontend/",
90
+ "static/app/",
82
91
  "static/icons/",
83
92
  "plugins/",
84
93
  "scripts/",
@@ -89,6 +98,12 @@
89
98
  "setup_wizard.py",
90
99
  "knowledge_graph_api.py",
91
100
  "static/vendor/",
101
+ "src-tauri/",
102
+ "!src-tauri/gen/",
103
+ "!src-tauri/gen/**",
104
+ "!src-tauri/target/",
105
+ "!src-tauri/target/**",
106
+ "desktop/electron/",
92
107
  "docs/*.md",
93
108
  "!docs/assets/",
94
109
  "!docs/images/"
@@ -97,6 +112,31 @@
97
112
  "access": "public"
98
113
  },
99
114
  "devDependencies": {
100
- "@playwright/test": "^1.60.0"
115
+ "@playwright/test": "^1.60.0",
116
+ "@tailwindcss/postcss": "^4.3.0",
117
+ "@tanstack/react-query": "^5.101.0",
118
+ "@tauri-apps/api": "^2.0.0",
119
+ "@tauri-apps/cli": "^2.0.0",
120
+ "@types/cytoscape": "^3.21.9",
121
+ "@types/react": "^19.2.17",
122
+ "@types/react-dom": "^19.2.3",
123
+ "@vitejs/plugin-react": "^6.0.2",
124
+ "autoprefixer": "^10.5.0",
125
+ "class-variance-authority": "^0.7.1",
126
+ "clsx": "^2.1.1",
127
+ "cytoscape": "^3.34.0",
128
+ "electron": "^42.4.0",
129
+ "lucide-react": "^1.17.0",
130
+ "openapi-fetch": "^0.17.0",
131
+ "openapi-typescript": "^7.13.0",
132
+ "postcss": "^8.5.15",
133
+ "react": "^19.2.7",
134
+ "react-dom": "^19.2.7",
135
+ "reactflow": "^11.11.4",
136
+ "tailwind-merge": "^3.6.0",
137
+ "tailwindcss": "^4.3.0",
138
+ "typescript": "^5.9.3",
139
+ "vite": "^8.0.16",
140
+ "zustand": "^5.0.14"
101
141
  }
102
142
  }
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+
5
+ const repo = join(import.meta.dirname, "..");
6
+ const appDir = join(repo, "static", "app");
7
+ const nestedViteManifest = join(appDir, ".vite", "asset-manifest.json");
8
+ const publicManifest = join(appDir, "asset-manifest.json");
9
+ const pkg = JSON.parse(readFileSync(join(repo, "package.json"), "utf8"));
10
+
11
+ const viteManifest = existsSync(nestedViteManifest) ? nestedViteManifest : publicManifest;
12
+ if (!existsSync(viteManifest)) {
13
+ console.error("Vite manifest missing. Run `vite build` before build_frontend_assets.mjs.");
14
+ process.exit(1);
15
+ }
16
+
17
+ const raw = JSON.parse(readFileSync(viteManifest, "utf8"));
18
+ const assets = {};
19
+ for (const [key, value] of Object.entries(raw)) {
20
+ if (value && typeof value === "object") {
21
+ if (value.file) assets[key] = `/static/app/${value.file}`;
22
+ for (const css of value.css || []) assets[css] = `/static/app/${css}`;
23
+ for (const asset of value.assets || []) assets[asset] = `/static/app/${asset}`;
24
+ }
25
+ }
26
+
27
+ const manifest = {
28
+ version: pkg.version,
29
+ generated_at: "vite",
30
+ entrypoints: {
31
+ app: assets["frontend/index.html"] || "/static/app/index.html",
32
+ },
33
+ assets,
34
+ vite: raw,
35
+ };
36
+
37
+ writeFileSync(publicManifest, JSON.stringify(manifest, null, 2) + "\n", "utf8");
38
+ console.log(`wrote static/app/asset-manifest.json with ${Object.keys(assets).length} assets`);
@@ -23,6 +23,7 @@ REPO = Path(__file__).resolve().parents[1]
23
23
  # (path, kind, pattern) — pattern groups: (prefix, version)
24
24
  TARGETS = [
25
25
  ("latticeai/__init__.py", "regex", r'(__version__ = ")([^"]+)(")'),
26
+ ("lattice_brain/__init__.py", "regex", r'(__version__ = ")([^"]+)(")'),
26
27
  ("latticeai/core/workspace_os.py", "regex", r'(WORKSPACE_OS_VERSION = ")([^"]+)(")'),
27
28
  ("latticeai/core/marketplace.py", "regex", r'(MARKETPLACE_VERSION = ")([^"]+)(")'),
28
29
  ("latticeai/core/multi_agent.py", "regex", r'(MULTI_AGENT_VERSION = ")([^"]+)(")'),
@@ -31,7 +32,9 @@ TARGETS = [
31
32
  ("package-lock.json", "package-lock", None),
32
33
  ("vscode-extension/package.json", "json", "version"),
33
34
  ("vscode-extension/package-lock.json", "package-lock", None),
34
- ("static/v3/asset-manifest.json", "json", "version"),
35
+ ("src-tauri/Cargo.toml", "regex", r'(^version = ")([^"]+)(")'),
36
+ ("src-tauri/tauri.conf.json", "json", "version"),
37
+ ("static/app/asset-manifest.json", "json", "version"),
35
38
  ]
36
39
 
37
40