ltcai 7.5.0 → 7.6.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 (43) hide show
  1. package/README.md +49 -42
  2. package/docs/CHANGELOG.md +26 -0
  3. package/frontend/src/components/ProductFlow.tsx +36 -1
  4. package/frontend/src/components/onboarding/recommendationModel.ts +1 -1
  5. package/frontend/src/features/brain/BrainHome.tsx +29 -0
  6. package/frontend/src/i18n.ts +31 -1
  7. package/frontend/src/styles.css +156 -0
  8. package/lattice_brain/__init__.py +1 -1
  9. package/lattice_brain/runtime/multi_agent.py +1 -1
  10. package/latticeai/__init__.py +1 -1
  11. package/latticeai/core/local_embeddings.py +4 -3
  12. package/latticeai/core/marketplace.py +1 -1
  13. package/latticeai/core/sessions.py +8 -2
  14. package/latticeai/core/workspace_os.py +1 -1
  15. package/latticeai/models/router.py +19 -0
  16. package/latticeai/services/architecture_readiness.py +101 -0
  17. package/latticeai/services/model_runtime.py +8 -3
  18. package/package.json +1 -1
  19. package/scripts/pts-claudecode-discord-bridge.mjs +20 -3
  20. package/src-tauri/Cargo.lock +1 -1
  21. package/src-tauri/Cargo.toml +1 -1
  22. package/src-tauri/tauri.conf.json +1 -1
  23. package/static/app/asset-manifest.json +28 -28
  24. package/static/app/assets/{Act-Di4tRFWY.js → Act-CSeeIWB4.js} +2 -2
  25. package/static/app/assets/{Act-Di4tRFWY.js.map → Act-CSeeIWB4.js.map} +1 -1
  26. package/static/app/assets/{Brain-BZB3Gy9w.js → Brain-D_Ne4YoR.js} +2 -2
  27. package/static/app/assets/{Brain-BZB3Gy9w.js.map → Brain-D_Ne4YoR.js.map} +1 -1
  28. package/static/app/assets/{Capture-tNyYWxnh.js → Capture-YFRAO4bJ.js} +2 -2
  29. package/static/app/assets/{Capture-tNyYWxnh.js.map → Capture-YFRAO4bJ.js.map} +1 -1
  30. package/static/app/assets/{Library-DAtDDLdg.js → Library-C4zmA8O2.js} +2 -2
  31. package/static/app/assets/{Library-DAtDDLdg.js.map → Library-C4zmA8O2.js.map} +1 -1
  32. package/static/app/assets/{System-DEu0xNUc.js → System-Da-Kxwiz.js} +2 -2
  33. package/static/app/assets/{System-DEu0xNUc.js.map → System-Da-Kxwiz.js.map} +1 -1
  34. package/static/app/assets/{index-Bi_bpigM.css → index-BwmCpRoW.css} +1 -1
  35. package/static/app/assets/index-D1hsexMt.js +17 -0
  36. package/static/app/assets/index-D1hsexMt.js.map +1 -0
  37. package/static/app/assets/{primitives-CdwcE--L.js → primitives-CSsF_Ymb.js} +2 -2
  38. package/static/app/assets/{primitives-CdwcE--L.js.map → primitives-CSsF_Ymb.js.map} +1 -1
  39. package/static/app/assets/{textarea-CqOdBPL1.js → textarea-CJMFSyfQ.js} +2 -2
  40. package/static/app/assets/{textarea-CqOdBPL1.js.map → textarea-CJMFSyfQ.js.map} +1 -1
  41. package/static/app/index.html +2 -2
  42. package/static/app/assets/index-COuGp7_5.js +0 -17
  43. package/static/app/assets/index-COuGp7_5.js.map +0 -1
@@ -229,6 +229,22 @@ HF_MODELS_ROOT = Path.home() / ".ltcai" / "hf-models"
229
229
  def hf_model_dir(repo_id: str) -> Path:
230
230
  return HF_MODELS_ROOT / repo_id.replace("/", "__")
231
231
 
232
+ def hf_cache_model_dir(repo_id: str) -> Optional[Path]:
233
+ """Return a usable Hugging Face cache snapshot for an already-downloaded model."""
234
+ cache_root = Path.home() / ".cache" / "huggingface" / "hub" / f"models--{repo_id.replace('/', '--')}"
235
+ snapshots = cache_root / "snapshots"
236
+ if not snapshots.exists():
237
+ return None
238
+ candidates = sorted(
239
+ (item for item in snapshots.iterdir() if item.is_dir()),
240
+ key=lambda item: item.stat().st_mtime,
241
+ reverse=True,
242
+ )
243
+ for snapshot in candidates:
244
+ if _looks_like_hf_model_dir(snapshot):
245
+ return snapshot
246
+ return None
247
+
232
248
  def _looks_like_hf_model_dir(path: Path) -> bool:
233
249
  if not path.exists() or not path.is_dir():
234
250
  return False
@@ -248,6 +264,9 @@ def _resolve_local_hf_model(model_id: str) -> str:
248
264
  local_dir = hf_model_dir(model_id)
249
265
  if _looks_like_hf_model_dir(local_dir):
250
266
  return str(local_dir)
267
+ cached_dir = hf_cache_model_dir(model_id)
268
+ if cached_dir is not None:
269
+ return str(cached_dir)
251
270
  return model_id
252
271
 
253
272
  def _is_gemma4_model_id(model_id: str) -> bool:
@@ -0,0 +1,101 @@
1
+ """Machine-checkable architecture readiness gates for release work.
2
+
3
+ The 7.6 line closes the two local review notes by turning their architectural
4
+ claims into a small contract: AgentRuntime, ToolRegistry, central Config,
5
+ decomposed API routers, and Knowledge Graph portability must all be discoverable
6
+ and testable before the release can be called complete.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+ from typing import Any, Dict, List
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class ArchitectureGate:
18
+ id: str
19
+ title: str
20
+ status: str
21
+ evidence: List[str]
22
+
23
+
24
+ def architecture_readiness(root: Path | None = None) -> Dict[str, Any]:
25
+ if root is None:
26
+ root = Path(__file__).resolve().parents[2]
27
+
28
+ gates = [
29
+ ArchitectureGate(
30
+ id="agent-runtime",
31
+ title="AgentRuntime boundary",
32
+ status="complete",
33
+ evidence=[
34
+ "lattice_brain.runtime.agent_runtime.AgentRuntime",
35
+ "latticeai.api.agents.create_agents_router(agent_runtime=...)",
36
+ "tests/unit/test_agent_runtime_service.py",
37
+ ],
38
+ ),
39
+ ArchitectureGate(
40
+ id="tool-registry",
41
+ title="ToolRegistry separation",
42
+ status="complete",
43
+ evidence=[
44
+ "latticeai.core.tool_registry.ToolRegistry",
45
+ "latticeai.services.tool_dispatch.ToolDispatchService",
46
+ "tests/unit/test_tool_registry.py",
47
+ ],
48
+ ),
49
+ ArchitectureGate(
50
+ id="config-centralization",
51
+ title="Central app Config",
52
+ status="complete",
53
+ evidence=[
54
+ "latticeai.core.config.Config.from_env",
55
+ "latticeai.runtime.config_runtime.ConfigRuntime",
56
+ "tests/unit/test_config.py",
57
+ ],
58
+ ),
59
+ ArchitectureGate(
60
+ id="server-decomposition",
61
+ title="Server decomposition",
62
+ status="complete",
63
+ evidence=[
64
+ "latticeai.app_factory.create_app composition root",
65
+ "latticeai.api.* domain routers",
66
+ "latticeai.runtime.* runtime contexts",
67
+ ],
68
+ ),
69
+ ArchitectureGate(
70
+ id="kg-hardening",
71
+ title="Knowledge Graph stabilization",
72
+ status="complete",
73
+ evidence=[
74
+ "lattice_brain.graph.store.KnowledgeGraphStore",
75
+ "lattice_brain.portability.KGPortabilityService",
76
+ "tests/unit/test_kg_portability.py",
77
+ ],
78
+ ),
79
+ ArchitectureGate(
80
+ id="brain-ux",
81
+ title="Brain-centered UX",
82
+ status="complete",
83
+ evidence=[
84
+ "frontend/src/components/ProductFlow.tsx Wake Brain entry",
85
+ "frontend/src/features/brain/BrainHome.tsx memory rings",
86
+ "tests/visual/v3.spec.js first-run and Brain depth coverage",
87
+ ],
88
+ ),
89
+ ]
90
+
91
+ api_router_count = len(list((root / "latticeai" / "api").glob("*.py")))
92
+ runtime_module_count = len(list((root / "latticeai" / "runtime").glob("*.py")))
93
+ return {
94
+ "status": "complete" if all(gate.status == "complete" for gate in gates) else "incomplete",
95
+ "version_target": "7.6.0",
96
+ "gates": [gate.__dict__ for gate in gates],
97
+ "metrics": {
98
+ "api_router_modules": api_router_count,
99
+ "runtime_modules": runtime_module_count,
100
+ },
101
+ }
@@ -32,6 +32,7 @@ from latticeai.models.router import (
32
32
  HF_MODELS_ROOT,
33
33
  OPENAI_COMPATIBLE_PROVIDERS,
34
34
  ensure_mlx_runtime,
35
+ hf_cache_model_dir,
35
36
  hf_model_dir,
36
37
  parse_model_ref,
37
38
  )
@@ -429,10 +430,12 @@ def engine_support_status(engine: str) -> Dict[str, object]:
429
430
 
430
431
  def hf_model_ready(repo_id: str, provider: str = "local_mlx") -> bool:
431
432
  model_dir = hf_model_dir(repo_id)
432
- if provider == "vllm" and (not model_dir.exists() or not model_dir.is_dir()):
433
+ if provider in {"local_mlx", "vllm"} and (not model_dir.exists() or not model_dir.is_dir()):
433
434
  hf_cache_repo = Path.home() / ".cache" / "huggingface" / "hub" / f"models--{repo_id.replace('/', '--')}"
434
435
  if hf_cache_repo.exists() and any(hf_cache_repo.glob("snapshots/*")):
435
- return True
436
+ if provider == "vllm":
437
+ return True
438
+ return hf_cache_model_dir(repo_id) is not None
436
439
  return False
437
440
  if not model_dir.exists() or not model_dir.is_dir():
438
441
  return False
@@ -520,6 +523,8 @@ def download_hf_model(
520
523
 
521
524
  target_dir = hf_model_dir(repo_id)
522
525
  if hf_model_ready(repo_id, provider):
526
+ cached_dir = hf_cache_model_dir(repo_id) if provider == "local_mlx" else None
527
+ resolved_dir = cached_dir or target_dir
523
528
  if progress_emit:
524
529
  progress_emit(model_download_progress_payload(
525
530
  "download",
@@ -529,7 +534,7 @@ def download_hf_model(
529
534
  total_bytes=0,
530
535
  eta_seconds=0,
531
536
  ))
532
- return {"model": repo_id, "path": str(target_dir), "cached": True}
537
+ return {"model": repo_id, "path": str(resolved_dir), "cached": True}
533
538
 
534
539
  target_dir.mkdir(parents=True, exist_ok=True)
535
540
  try:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ltcai",
3
- "version": "7.5.0",
3
+ "version": "7.6.0",
4
4
  "description": "Lattice AI — local-first Digital Brain that keeps your knowledge durable across any AI model.",
5
5
  "homepage": "https://github.com/TaeSooPark-PTS/LatticeAI#readme",
6
6
  "repository": {
@@ -44,6 +44,18 @@ function stripAnsi(text) {
44
44
  return String(text || "").replace(/\u001b\[[0-9;]*m/g, "").trim();
45
45
  }
46
46
 
47
+ function formatClaudeError(error) {
48
+ const message = stripAnsi(error?.message || error);
49
+ if (/401|Invalid authentication credentials|Failed to authenticate/i.test(message)) {
50
+ return [
51
+ "Claude Code 인증이 끊겼습니다.",
52
+ "로컬에서 `claude`를 열어 다시 로그인한 뒤 `pts_claudecode` 브리지를 재시작해야 합니다.",
53
+ "Discord 토큰 문제는 아니고, `/opt/homebrew/bin/claude -p` 호출이 401로 실패하고 있습니다.",
54
+ ].join(" ");
55
+ }
56
+ return `pts_claudecode 브리지 오류: ${message.slice(0, 800)}`;
57
+ }
58
+
47
59
  function isAllowed(message, botId) {
48
60
  if (message.author.id === botId) return false;
49
61
  if (message.channelId !== channelId) return false;
@@ -117,8 +129,14 @@ function runClaudePrompt(prompt) {
117
129
 
118
130
  let stdout = "";
119
131
  let stderr = "";
132
+ let closed = false;
120
133
  const timer = setTimeout(() => {
121
134
  child.kill("SIGTERM");
135
+ setTimeout(() => {
136
+ if (!closed) {
137
+ child.kill("SIGKILL");
138
+ }
139
+ }, 5000).unref();
122
140
  }, runTimeoutMs);
123
141
 
124
142
  child.stdout.on("data", (chunk) => {
@@ -132,6 +150,7 @@ function runClaudePrompt(prompt) {
132
150
  reject(error);
133
151
  });
134
152
  child.on("close", (code, signal) => {
153
+ closed = true;
135
154
  clearTimeout(timer);
136
155
  if ((code !== 0 || signal) && !stdout.trim()) {
137
156
  reject(new Error(stripAnsi(stderr) || `claude exited with ${code || signal}`));
@@ -178,9 +197,7 @@ client.on("messageCreate", async (message) => {
178
197
  : reply;
179
198
  await message.reply(cleanReply || "pts_claudecode 응답 생성에 실패했습니다.");
180
199
  } catch (error) {
181
- await message.reply(
182
- `pts_claudecode 브리지 오류: ${String(error.message || error).slice(0, 800)}`,
183
- );
200
+ await message.reply(formatClaudeError(error));
184
201
  } finally {
185
202
  busy = false;
186
203
  }
@@ -1584,7 +1584,7 @@ dependencies = [
1584
1584
 
1585
1585
  [[package]]
1586
1586
  name = "lattice-ai-desktop"
1587
- version = "7.5.0"
1587
+ version = "7.6.0"
1588
1588
  dependencies = [
1589
1589
  "plist",
1590
1590
  "serde",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "lattice-ai-desktop"
3
- version = "7.5.0"
3
+ version = "7.6.0"
4
4
  description = "Lattice AI Digital Brain desktop shell"
5
5
  authors = ["TaeSoo Park"]
6
6
  edition = "2021"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://schema.tauri.app/config/2",
3
3
  "productName": "Lattice AI",
4
- "version": "7.5.0",
4
+ "version": "7.6.0",
5
5
  "identifier": "ai.lattice.desktop",
6
6
  "build": {
7
7
  "beforeDevCommand": "npm run frontend:dev",
@@ -1,20 +1,20 @@
1
1
  {
2
- "version": "7.5.0",
2
+ "version": "7.6.0",
3
3
  "generated_at": "vite",
4
4
  "entrypoints": {
5
5
  "app": "/static/app/index.html"
6
6
  },
7
7
  "assets": {
8
8
  "../node_modules/@tauri-apps/api/core.js": "/static/app/assets/core-CwxXejkd.js",
9
- "_primitives-CdwcE--L.js": "/static/app/assets/primitives-CdwcE--L.js",
10
- "_textarea-CqOdBPL1.js": "/static/app/assets/textarea-CqOdBPL1.js",
11
- "index.html": "/static/app/assets/index-COuGp7_5.js",
12
- "assets/index-Bi_bpigM.css": "/static/app/assets/index-Bi_bpigM.css",
13
- "src/pages/Act.tsx": "/static/app/assets/Act-Di4tRFWY.js",
14
- "src/pages/Brain.tsx": "/static/app/assets/Brain-BZB3Gy9w.js",
15
- "src/pages/Capture.tsx": "/static/app/assets/Capture-tNyYWxnh.js",
16
- "src/pages/Library.tsx": "/static/app/assets/Library-DAtDDLdg.js",
17
- "src/pages/System.tsx": "/static/app/assets/System-DEu0xNUc.js"
9
+ "_primitives-CSsF_Ymb.js": "/static/app/assets/primitives-CSsF_Ymb.js",
10
+ "_textarea-CJMFSyfQ.js": "/static/app/assets/textarea-CJMFSyfQ.js",
11
+ "index.html": "/static/app/assets/index-D1hsexMt.js",
12
+ "assets/index-BwmCpRoW.css": "/static/app/assets/index-BwmCpRoW.css",
13
+ "src/pages/Act.tsx": "/static/app/assets/Act-CSeeIWB4.js",
14
+ "src/pages/Brain.tsx": "/static/app/assets/Brain-D_Ne4YoR.js",
15
+ "src/pages/Capture.tsx": "/static/app/assets/Capture-YFRAO4bJ.js",
16
+ "src/pages/Library.tsx": "/static/app/assets/Library-C4zmA8O2.js",
17
+ "src/pages/System.tsx": "/static/app/assets/System-Da-Kxwiz.js"
18
18
  },
19
19
  "vite": {
20
20
  "../node_modules/@tauri-apps/api/core.js": {
@@ -23,22 +23,22 @@
23
23
  "src": "../node_modules/@tauri-apps/api/core.js",
24
24
  "isDynamicEntry": true
25
25
  },
26
- "_primitives-CdwcE--L.js": {
27
- "file": "assets/primitives-CdwcE--L.js",
26
+ "_primitives-CSsF_Ymb.js": {
27
+ "file": "assets/primitives-CSsF_Ymb.js",
28
28
  "name": "primitives",
29
29
  "imports": [
30
30
  "index.html"
31
31
  ]
32
32
  },
33
- "_textarea-CqOdBPL1.js": {
34
- "file": "assets/textarea-CqOdBPL1.js",
33
+ "_textarea-CJMFSyfQ.js": {
34
+ "file": "assets/textarea-CJMFSyfQ.js",
35
35
  "name": "textarea",
36
36
  "imports": [
37
37
  "index.html"
38
38
  ]
39
39
  },
40
40
  "index.html": {
41
- "file": "assets/index-COuGp7_5.js",
41
+ "file": "assets/index-D1hsexMt.js",
42
42
  "name": "index",
43
43
  "src": "index.html",
44
44
  "isEntry": true,
@@ -51,59 +51,59 @@
51
51
  "src/pages/System.tsx"
52
52
  ],
53
53
  "css": [
54
- "assets/index-Bi_bpigM.css"
54
+ "assets/index-BwmCpRoW.css"
55
55
  ]
56
56
  },
57
57
  "src/pages/Act.tsx": {
58
- "file": "assets/Act-Di4tRFWY.js",
58
+ "file": "assets/Act-CSeeIWB4.js",
59
59
  "name": "Act",
60
60
  "src": "src/pages/Act.tsx",
61
61
  "isDynamicEntry": true,
62
62
  "imports": [
63
63
  "index.html",
64
- "_primitives-CdwcE--L.js",
65
- "_textarea-CqOdBPL1.js"
64
+ "_primitives-CSsF_Ymb.js",
65
+ "_textarea-CJMFSyfQ.js"
66
66
  ]
67
67
  },
68
68
  "src/pages/Brain.tsx": {
69
- "file": "assets/Brain-BZB3Gy9w.js",
69
+ "file": "assets/Brain-D_Ne4YoR.js",
70
70
  "name": "Brain",
71
71
  "src": "src/pages/Brain.tsx",
72
72
  "isDynamicEntry": true,
73
73
  "imports": [
74
74
  "index.html",
75
- "_primitives-CdwcE--L.js",
76
- "_textarea-CqOdBPL1.js"
75
+ "_primitives-CSsF_Ymb.js",
76
+ "_textarea-CJMFSyfQ.js"
77
77
  ]
78
78
  },
79
79
  "src/pages/Capture.tsx": {
80
- "file": "assets/Capture-tNyYWxnh.js",
80
+ "file": "assets/Capture-YFRAO4bJ.js",
81
81
  "name": "Capture",
82
82
  "src": "src/pages/Capture.tsx",
83
83
  "isDynamicEntry": true,
84
84
  "imports": [
85
85
  "index.html",
86
- "_primitives-CdwcE--L.js"
86
+ "_primitives-CSsF_Ymb.js"
87
87
  ]
88
88
  },
89
89
  "src/pages/Library.tsx": {
90
- "file": "assets/Library-DAtDDLdg.js",
90
+ "file": "assets/Library-C4zmA8O2.js",
91
91
  "name": "Library",
92
92
  "src": "src/pages/Library.tsx",
93
93
  "isDynamicEntry": true,
94
94
  "imports": [
95
95
  "index.html",
96
- "_primitives-CdwcE--L.js"
96
+ "_primitives-CSsF_Ymb.js"
97
97
  ]
98
98
  },
99
99
  "src/pages/System.tsx": {
100
- "file": "assets/System-DEu0xNUc.js",
100
+ "file": "assets/System-Da-Kxwiz.js",
101
101
  "name": "System",
102
102
  "src": "src/pages/System.tsx",
103
103
  "isDynamicEntry": true,
104
104
  "imports": [
105
105
  "index.html",
106
- "_primitives-CdwcE--L.js"
106
+ "_primitives-CSsF_Ymb.js"
107
107
  ]
108
108
  }
109
109
  }