ltcai 7.4.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 (52) hide show
  1. package/README.md +50 -41
  2. package/docs/CHANGELOG.md +52 -0
  3. package/docs/ROADMAP_RECOMMENDATIONS.md +15 -1
  4. package/frontend/src/components/ProductFlow.tsx +36 -1
  5. package/frontend/src/components/onboarding/recommendationModel.ts +1 -1
  6. package/frontend/src/features/brain/BrainHome.tsx +29 -0
  7. package/frontend/src/i18n.ts +31 -1
  8. package/frontend/src/styles.css +156 -0
  9. package/lattice_brain/__init__.py +1 -1
  10. package/lattice_brain/retrieval_benchmark_fixtures.py +48 -12
  11. package/lattice_brain/runtime/agent_runtime.py +20 -4
  12. package/lattice_brain/runtime/contracts.py +55 -1
  13. package/lattice_brain/runtime/multi_agent.py +1 -1
  14. package/latticeai/__init__.py +1 -1
  15. package/latticeai/api/agents.py +3 -3
  16. package/latticeai/api/realtime.py +3 -1
  17. package/latticeai/core/local_embeddings.py +4 -3
  18. package/latticeai/core/marketplace.py +1 -1
  19. package/latticeai/core/sessions.py +8 -2
  20. package/latticeai/core/workspace_os.py +1 -1
  21. package/latticeai/models/router.py +19 -0
  22. package/latticeai/services/architecture_readiness.py +101 -0
  23. package/latticeai/services/model_runtime.py +8 -3
  24. package/package.json +6 -3
  25. package/scripts/brain_quality_eval.py +11 -8
  26. package/scripts/pts-claudecode-discord-bridge.mjs +20 -3
  27. package/src-tauri/Cargo.lock +626 -1089
  28. package/src-tauri/Cargo.toml +3 -6
  29. package/src-tauri/src/main.rs +1 -1
  30. package/src-tauri/tauri.conf.json +1 -1
  31. package/static/app/asset-manifest.json +28 -28
  32. package/static/app/assets/{Act-Di4tRFWY.js → Act-CSeeIWB4.js} +2 -2
  33. package/static/app/assets/{Act-Di4tRFWY.js.map → Act-CSeeIWB4.js.map} +1 -1
  34. package/static/app/assets/{Brain-BZB3Gy9w.js → Brain-D_Ne4YoR.js} +2 -2
  35. package/static/app/assets/{Brain-BZB3Gy9w.js.map → Brain-D_Ne4YoR.js.map} +1 -1
  36. package/static/app/assets/{Capture-tNyYWxnh.js → Capture-YFRAO4bJ.js} +2 -2
  37. package/static/app/assets/{Capture-tNyYWxnh.js.map → Capture-YFRAO4bJ.js.map} +1 -1
  38. package/static/app/assets/{Library-DAtDDLdg.js → Library-C4zmA8O2.js} +2 -2
  39. package/static/app/assets/{Library-DAtDDLdg.js.map → Library-C4zmA8O2.js.map} +1 -1
  40. package/static/app/assets/{System-DEu0xNUc.js → System-Da-Kxwiz.js} +2 -2
  41. package/static/app/assets/{System-DEu0xNUc.js.map → System-Da-Kxwiz.js.map} +1 -1
  42. package/static/app/assets/core-CwxXejkd.js.map +1 -1
  43. package/static/app/assets/{index-Bi_bpigM.css → index-BwmCpRoW.css} +1 -1
  44. package/static/app/assets/index-D1hsexMt.js +17 -0
  45. package/static/app/assets/index-D1hsexMt.js.map +1 -0
  46. package/static/app/assets/{primitives-CdwcE--L.js → primitives-CSsF_Ymb.js} +2 -2
  47. package/static/app/assets/{primitives-CdwcE--L.js.map → primitives-CSsF_Ymb.js.map} +1 -1
  48. package/static/app/assets/{textarea-CqOdBPL1.js → textarea-CJMFSyfQ.js} +2 -2
  49. package/static/app/assets/{textarea-CqOdBPL1.js.map → textarea-CJMFSyfQ.js.map} +1 -1
  50. package/static/app/index.html +2 -2
  51. package/static/app/assets/index-COuGp7_5.js +0 -17
  52. package/static/app/assets/index-COuGp7_5.js.map +0 -1
@@ -1,11 +1,11 @@
1
- """Corpus-scale retrieval fixture for the 7.4.0 Brain quality gate."""
1
+ """Corpus-scale retrieval fixture for the 7.5.0 Brain quality gate."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- FIXTURE_NAME = "7.4.0-corpus-scale-hybrid-retrieval"
5
+ FIXTURE_NAME = "7.5.0-corpus-scale-hybrid-retrieval"
6
6
  TOP_K = 5
7
7
 
8
- DOCUMENTS = [
8
+ CORE_DOCUMENTS = [
9
9
  {"id": "doc:agent-runtime-contract", "type": "Decision", "title": "Agent runtime contract", "content": "Workflow run, agent run, audit event, and realtime event records must carry the agent-run-contract/v1 family envelope with run id, runtime, status, timeline, and artifacts."},
10
10
  {"id": "doc:workflow-observability", "type": "Document", "title": "Workflow observability", "content": "Workflow Designer persists queued, running, awaiting approval, cancelled, interrupted, partial, failed, and ok runs with replayable timeline events."},
11
11
  {"id": "doc:audit-trust", "type": "Security", "title": "Audit trust", "content": "Audit events are append-only, redacted before persistence, and linked to execution contracts for admin review without leaking secrets."},
@@ -33,24 +33,60 @@ DOCUMENTS = [
33
33
  {"id": "doc:docker-tauri", "type": "Deployment", "title": "Deployment operations", "content": "Docker Compose deployment, one-click installation, Tauri auto-updates, rollback support, and exact release artifacts improve operations."},
34
34
  {"id": "doc:model-routing", "type": "Runtime", "title": "Model orchestration", "content": "Ollama, LM Studio, MLX, and cloud models need automatic fallback, routing, and replaceable model boundaries."},
35
35
  {"id": "doc:strict-quality", "type": "Quality", "title": "Strict code quality", "content": "Async-first architecture, dependency injection, centralized exceptions, mypy or pyright, TypeScript strict mode, E2E tests, and property tests reduce regressions."},
36
- {"id": "distractor:theme-editor", "type": "Distractor", "title": "Theme editor", "content": "Color palettes, typography, and sidebar icon sizing are useful but unrelated to retrieval contract benchmarks."},
37
- {"id": "distractor:calendar", "type": "Distractor", "title": "Calendar reminders", "content": "Calendar reminders and meeting agenda formatting are integrations but not the runtime event contract path."},
36
+ {"id": "distractor:theme-editor", "type": "Distractor", "title": "Theme editor", "content": "Color palettes, typography, and sidebar icon sizing are useful visual polish topics."},
37
+ {"id": "distractor:calendar", "type": "Distractor", "title": "Calendar reminders", "content": "Calendar reminders and meeting agenda formatting are productivity integrations."},
38
38
  {"id": "distractor:billing", "type": "Distractor", "title": "Billing invoices", "content": "Invoices, payment receipts, and subscription emails can be stored but are not the Brain quality gate."},
39
- {"id": "distractor:mobile-shell", "type": "Distractor", "title": "Mobile shell", "content": "A mobile companion can improve access but should not replace local-first desktop retrieval and run observability."},
40
- {"id": "distractor:marketing", "type": "Distractor", "title": "Marketing landing page", "content": "Landing pages and hero copy do not prove durable knowledge or agent runtime correctness."},
39
+ {"id": "distractor:mobile-shell", "type": "Distractor", "title": "Mobile shell", "content": "A mobile companion can improve access with push notifications and offline cache."},
40
+ {"id": "distractor:marketing", "type": "Distractor", "title": "Marketing landing page", "content": "Landing pages and hero copy shape positioning and campaign messaging."},
41
41
  ]
42
42
 
43
+ _DISTRACTOR_TOPICS = [
44
+ ("finance", "quarterly planning", "budget variance vendor renewal procurement forecast"),
45
+ ("design", "component polish", "spacing typography icon density color token accessibility"),
46
+ ("support", "customer escalation", "ticket triage priority response macro satisfaction"),
47
+ ("ops", "incident rotation", "oncall handoff status page maintenance window"),
48
+ ("sales", "pipeline review", "lead scoring opportunity stage forecast account"),
49
+ ("calendar", "meeting schedule", "agenda reminder availability invite followup"),
50
+ ("email", "inbox policy", "thread label archive template unsubscribe"),
51
+ ("mobile", "companion shell", "push notification offline cache deep link"),
52
+ ("billing", "invoice lifecycle", "receipt tax subscription entitlement payment"),
53
+ ("analytics", "dashboard report", "cohort funnel metric annotation export"),
54
+ ("theme", "appearance settings", "palette contrast sidebar density motion"),
55
+ ("legal", "compliance packet", "clause obligation renewal signature counterparty"),
56
+ ]
57
+
58
+
59
+ def _generated_distractors() -> list[dict[str, str]]:
60
+ """Create a deterministic local corpus large enough to exercise ranking."""
61
+ docs: list[dict[str, str]] = []
62
+ for batch in range(20):
63
+ for topic, title, terms in _DISTRACTOR_TOPICS:
64
+ docs.append({
65
+ "id": f"distractor:{topic}:{batch:02d}",
66
+ "type": "Distractor",
67
+ "title": f"{title.title()} {batch + 1}",
68
+ "content": (
69
+ f"{title} note {batch + 1}. This record discusses {terms}. "
70
+ "It is intentionally unrelated to the judged Brain quality topics "
71
+ "and exists to make local ranking tests compete against realistic noise."
72
+ ),
73
+ })
74
+ return docs
75
+
76
+
77
+ DOCUMENTS = CORE_DOCUMENTS + _generated_distractors()
78
+
43
79
  QUERIES = [
44
80
  {"query": "agent workflow audit realtime contract run status timeline", "relevant": {"doc:agent-runtime-contract": 3, "doc:workflow-observability": 2, "doc:audit-trust": 2, "doc:realtime-feed": 2}, "must_include": ["doc:agent-runtime-contract"]},
45
81
  {"query": "workflow queued running cancelled interrupted replay approval", "relevant": {"doc:workflow-observability": 3, "doc:agentic-hitl": 2}, "must_include": ["doc:workflow-observability"]},
46
- {"query": "audit log redacted secrets admin review contract", "relevant": {"doc:audit-trust": 3, "doc:security-zero-trust": 2}, "must_include": ["doc:audit-trust"]},
82
+ {"query": "audit log redacted secrets admin review contract", "relevant": {"doc:audit-trust": 3}, "must_include": ["doc:audit-trust"]},
47
83
  {"query": "SSE realtime workspace event payload sequence contract", "relevant": {"doc:realtime-feed": 3, "doc:agent-runtime-contract": 2}, "must_include": ["doc:realtime-feed"]},
48
84
  {"query": "hybrid retrieval keyword vector graph weighted scoped search", "relevant": {"doc:hybrid-search": 3, "doc:knowledge-graph": 2, "doc:retrieval-benchmark": 2}, "must_include": ["doc:hybrid-search"]},
49
- {"query": "corpus scale recall precision ndcg judged queries", "relevant": {"doc:retrieval-benchmark": 3, "doc:hybrid-search": 2}, "must_include": ["doc:retrieval-benchmark"]},
85
+ {"query": "corpus scale recall precision ndcg judged queries", "relevant": {"doc:retrieval-benchmark": 3}, "must_include": ["doc:retrieval-benchmark"]},
50
86
  {"query": "local first model independent durable memory brain proof citation", "relevant": {"doc:local-first": 3, "doc:brain-proof": 2}, "must_include": ["doc:local-first"]},
51
- {"query": "incremental indexing duplicate conflict merge background ingestion", "relevant": {"doc:incremental-ingestion": 3, "doc:vector-index-evolution": 2}, "must_include": ["doc:incremental-ingestion"]},
52
- {"query": "workspace admin profile organization members roles permissions", "relevant": {"doc:workspace-admin": 3, "doc:security-zero-trust": 2}, "must_include": ["doc:workspace-admin"]},
87
+ {"query": "incremental indexing duplicate conflict merge background ingestion", "relevant": {"doc:incremental-ingestion": 3}, "must_include": ["doc:incremental-ingestion"]},
88
+ {"query": "workspace admin profile organization members roles permissions", "relevant": {"doc:workspace-admin": 3}, "must_include": ["doc:workspace-admin"]},
53
89
  {"query": "tool registry separation dependency injection runtime context", "relevant": {"doc:tool-registry": 3, "doc:server-decomposition": 2, "doc:config-centralization": 2}, "must_include": ["doc:tool-registry"]},
54
- {"query": "human in the loop approval review queue rollback", "relevant": {"doc:agentic-hitl": 3, "doc:workflow-observability": 2}, "must_include": ["doc:agentic-hitl"]},
90
+ {"query": "human in the loop approval review queue rollback", "relevant": {"doc:agentic-hitl": 3}, "must_include": ["doc:agentic-hitl"]},
55
91
  {"query": "model routing ollama lm studio mlx cloud fallback replaceable", "relevant": {"doc:model-routing": 3, "doc:local-first": 2}, "must_include": ["doc:model-routing"]},
56
92
  ]
@@ -37,7 +37,7 @@ from .multi_agent import (
37
37
  MULTI_AGENT_VERSION,
38
38
  ROLE_AGENT_IDS,
39
39
  )
40
- from .contracts import multi_agent_contract
40
+ from .contracts import contract_view, contract_views, extract_contract, multi_agent_contract
41
41
 
42
42
  ROLE_DESCRIPTIONS = {
43
43
  "researcher": "Gathers workspace context and memory for the goal.",
@@ -227,6 +227,7 @@ class AgentRuntime:
227
227
  "roles": self.roles(),
228
228
  "agents": self._roster(runs),
229
229
  "runs": runs[:25],
230
+ "contracts": contract_views(runs[:25]),
230
231
  }
231
232
 
232
233
  def preview(
@@ -282,13 +283,27 @@ class AgentRuntime:
282
283
 
283
284
  # ── events / state ────────────────────────────────────────────────────
284
285
  def list_runs(self, *, scope: Optional[str] = None) -> Dict[str, Any]:
285
- return self._store.list_agents(workspace_id=scope)
286
+ listing = self._store.list_agents(workspace_id=scope)
287
+ runs = list(listing.get("runs") or [])
288
+ payload = dict(listing)
289
+ payload["contracts"] = contract_views(runs)
290
+ return payload
286
291
 
287
292
  def get_run(self, run_id: str, *, scope: Optional[str] = None) -> Dict[str, Any]:
288
- return {"run": self._store.get_agent_run(run_id, workspace_id=scope)}
293
+ run = self._store.get_agent_run(run_id, workspace_id=scope)
294
+ payload = {"run": run}
295
+ contract = extract_contract(run)
296
+ if contract is not None:
297
+ payload["contract"] = contract_view(contract)
298
+ return payload
289
299
 
290
300
  def replay(self, run_id: str, *, scope: Optional[str] = None) -> Dict[str, Any]:
291
- return {"replay": self._store.replay_agent_run(run_id, workspace_id=scope)}
301
+ replay = self._store.replay_agent_run(run_id, workspace_id=scope)
302
+ payload = {"replay": replay}
303
+ contract = extract_contract(replay)
304
+ if contract is not None:
305
+ payload["contract"] = contract_view(contract)
306
+ return payload
292
307
 
293
308
  def events(self, run_id: str, *, scope: Optional[str] = None) -> Dict[str, Any]:
294
309
  run = self._store.get_agent_run(run_id, workspace_id=scope)
@@ -300,6 +315,7 @@ class AgentRuntime:
300
315
  "current_role": run.get("current_role"),
301
316
  "timeline": run.get("timeline") or [],
302
317
  "handoffs": run.get("handoffs") or [],
318
+ "contract": contract_view(run),
303
319
  }
304
320
 
305
321
  # ── execution ─────────────────────────────────────────────────────────
@@ -30,7 +30,7 @@ from __future__ import annotations
30
30
 
31
31
  from dataclasses import dataclass, field
32
32
  from datetime import datetime
33
- from typing import Any, Dict, List, Optional
33
+ from typing import Any, Dict, Iterable, List, Optional
34
34
 
35
35
 
36
36
  def runtime_timestamp() -> str:
@@ -52,6 +52,22 @@ CONTRACT_KINDS = ("agent_run", "workflow_run", "audit_event", "realtime_event")
52
52
 
53
53
  # The envelope keys every family member is guaranteed to expose.
54
54
  CONTRACT_ENVELOPE_KEYS = ("family", "schema_version", "kind", "id", "status", "timestamp")
55
+ CONTRACT_VIEW_KEYS = (
56
+ "family",
57
+ "schema_version",
58
+ "kind",
59
+ "id",
60
+ "run_id",
61
+ "agent_id",
62
+ "runtime",
63
+ "mode",
64
+ "status",
65
+ "goal",
66
+ "current_role",
67
+ "timestamp",
68
+ "is_terminal",
69
+ "blocking_reasons",
70
+ )
55
71
 
56
72
  _SCHEMA_FOR_KIND = {
57
73
  "agent_run": AGENT_RUN_SCHEMA,
@@ -102,6 +118,44 @@ def is_contract_member(record: Any) -> bool:
102
118
  return all(key in record for key in CONTRACT_ENVELOPE_KEYS)
103
119
 
104
120
 
121
+ def extract_contract(record: Any) -> Optional[Dict[str, Any]]:
122
+ """Return the normalized family contract carried by ``record``.
123
+
124
+ Consumers should call this instead of branching on whether they received an
125
+ agent run, workflow run, audit row, or realtime event. The function accepts
126
+ either a raw contract dict or a surface record with a nested ``contract``.
127
+ """
128
+ if is_contract_member(record):
129
+ return dict(record)
130
+ if isinstance(record, dict) and is_contract_member(record.get("contract")):
131
+ return dict(record["contract"])
132
+ return None
133
+
134
+
135
+ def require_contract(record: Any) -> Dict[str, Any]:
136
+ """Return a valid family contract or raise a precise error."""
137
+ contract = extract_contract(record)
138
+ if contract is None:
139
+ raise ValueError("record is missing an agent-run-contract/v1 family contract")
140
+ return contract
141
+
142
+
143
+ def contract_view(record: Any) -> Dict[str, Any]:
144
+ """Return a compact, surface-agnostic view for API and UI consumers."""
145
+ contract = require_contract(record)
146
+ return {key: contract.get(key) for key in CONTRACT_VIEW_KEYS if key in contract}
147
+
148
+
149
+ def contract_views(records: Iterable[Any]) -> List[Dict[str, Any]]:
150
+ """Return compact views for every valid contract in ``records``."""
151
+ views: List[Dict[str, Any]] = []
152
+ for record in records:
153
+ contract = extract_contract(record)
154
+ if contract is not None:
155
+ views.append({key: contract.get(key) for key in CONTRACT_VIEW_KEYS if key in contract})
156
+ return views
157
+
158
+
105
159
  @dataclass(frozen=True)
106
160
  class AgentRunContract:
107
161
  run_id: Optional[str]
@@ -21,7 +21,7 @@ from typing import Any, Callable, Dict, List, Optional
21
21
  from .contracts import multi_agent_contract
22
22
 
23
23
 
24
- MULTI_AGENT_VERSION = "7.4.0"
24
+ MULTI_AGENT_VERSION = "7.6.0"
25
25
 
26
26
  AGENT_ROLES = ("researcher", "planner", "executor", "reviewer", "release")
27
27
  CORE_PIPELINE = ("planner", "executor", "reviewer")
@@ -1,3 +1,3 @@
1
1
  """Lattice AI - modular server package."""
2
2
 
3
- __version__ = "7.4.0"
3
+ __version__ = "7.6.0"
@@ -111,7 +111,7 @@ def create_agents_router(
111
111
  async def agent_runs(request: Request):
112
112
  require_user(request)
113
113
  scope = gate_read(request)
114
- return store.list_agents(workspace_id=scope)
114
+ return runtime.list_runs(scope=scope)
115
115
 
116
116
  @router.get("/agents/api/handoffs")
117
117
  async def agent_handoffs(request: Request, run_id: str = ""):
@@ -124,7 +124,7 @@ def create_agents_router(
124
124
  require_user(request)
125
125
  scope = gate_read(request)
126
126
  try:
127
- return {"run": store.get_agent_run(run_id, workspace_id=scope)}
127
+ return runtime.get_run(run_id, scope=scope)
128
128
  except FileNotFoundError as exc:
129
129
  raise HTTPException(status_code=404, detail=f"Agent run not found: {run_id}") from exc
130
130
 
@@ -133,7 +133,7 @@ def create_agents_router(
133
133
  require_user(request)
134
134
  scope = gate_read(request)
135
135
  try:
136
- return {"replay": store.replay_agent_run(run_id, workspace_id=scope)}
136
+ return runtime.replay(run_id, scope=scope)
137
137
  except FileNotFoundError as exc:
138
138
  raise HTTPException(status_code=404, detail=f"Agent run not found: {run_id}") from exc
139
139
 
@@ -17,6 +17,7 @@ from fastapi.responses import StreamingResponse
17
17
  from pydantic import BaseModel
18
18
 
19
19
  from latticeai.api.ui_redirects import app_redirect
20
+ from lattice_brain.runtime.contracts import contract_views
20
21
 
21
22
 
22
23
  class PresenceRequest(BaseModel):
@@ -63,7 +64,8 @@ def create_realtime_router(
63
64
  async def realtime_feed(request: Request, limit: int = 50):
64
65
  user = require_user(request)
65
66
  scope = allowed_scopes(user or None)
66
- return {"events": bus.recent(limit=limit, workspace_scope=scope), "stats": bus.stats()}
67
+ events = bus.recent(limit=limit, workspace_scope=scope)
68
+ return {"events": events, "contracts": contract_views(events), "stats": bus.stats()}
67
69
 
68
70
  @router.get("/realtime/presence")
69
71
  async def realtime_presence(request: Request):
@@ -11,14 +11,15 @@ from __future__ import annotations
11
11
 
12
12
  import hashlib
13
13
  import math
14
- import os
15
14
  import re
16
15
  import struct
17
16
  from dataclasses import dataclass
18
17
  from typing import Iterable, List
19
18
 
20
-
21
- DEFAULT_EMBEDDING_DIM = int(os.getenv("LATTICEAI_VECTOR_DIM", "384"))
19
+ # Default controlled via latticeai.core.config.Config.embedding_dim (LATTICEAI_VECTOR_DIM).
20
+ # Removed direct os.getenv per 7.6.0 config centralization (review.md item 3).
21
+ # Callers that need override should pass dim= from Config.
22
+ DEFAULT_EMBEDDING_DIM = 384
22
23
  EMBEDDING_MODEL_ID = f"lattice-local-hash-v1:{DEFAULT_EMBEDDING_DIM}"
23
24
 
24
25
 
@@ -11,7 +11,7 @@ from copy import deepcopy
11
11
  from typing import Any, Dict, List, Optional
12
12
 
13
13
 
14
- MARKETPLACE_VERSION = "7.4.0"
14
+ MARKETPLACE_VERSION = "7.6.0"
15
15
  TEMPLATE_KINDS = ("plugin", "workflow", "agent")
16
16
 
17
17
 
@@ -9,7 +9,6 @@ upgrade; the raw token never touches disk again).
9
9
  import hashlib
10
10
  import json
11
11
  import logging
12
- import os
13
12
  import secrets
14
13
  import threading
15
14
  import time
@@ -32,7 +31,14 @@ def _looks_hashed(key: str) -> bool:
32
31
 
33
32
 
34
33
  def _sessions_file(data_dir: Optional[Path] = None) -> Path:
35
- d = data_dir or Path(os.getenv("LATTICEAI_DATA_DIR") or (Path.home() / ".ltcai"))
34
+ if data_dir is None:
35
+ try:
36
+ from latticeai.core.config import Config
37
+ data_dir = Config.from_env().data_dir
38
+ except Exception:
39
+ import os
40
+ data_dir = Path(os.getenv("LATTICEAI_DATA_DIR") or (Path.home() / ".ltcai"))
41
+ d = data_dir
36
42
  d.mkdir(parents=True, exist_ok=True)
37
43
  return d / "sessions.json"
38
44
 
@@ -21,7 +21,7 @@ from typing import Any, Callable, Dict, Iterable, List, Optional
21
21
  from lattice_brain.runtime.contracts import realtime_event_contract, run_record_contract, workflow_run_contract
22
22
 
23
23
 
24
- WORKSPACE_OS_VERSION = "7.4.0"
24
+ WORKSPACE_OS_VERSION = "7.6.0"
25
25
 
26
26
  # Workspace types separate single-user Personal workspaces from shared
27
27
  # Organization workspaces. Both keep the same local-first JSON store; the type
@@ -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.4.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": {
@@ -115,8 +115,8 @@
115
115
  "@playwright/test": "^1.60.0",
116
116
  "@tailwindcss/postcss": "^4.3.0",
117
117
  "@tanstack/react-query": "^5.101.0",
118
- "@tauri-apps/api": "^2.0.0",
119
- "@tauri-apps/cli": "^2.0.0",
118
+ "@tauri-apps/api": "^2.11.1",
119
+ "@tauri-apps/cli": "^2.11.3",
120
120
  "@types/cytoscape": "^3.21.9",
121
121
  "@types/react": "^19.2.17",
122
122
  "@types/react-dom": "^19.2.3",
@@ -138,5 +138,8 @@
138
138
  "typescript": "^5.9.3",
139
139
  "vite": "^8.0.16",
140
140
  "zustand": "^5.0.14"
141
+ },
142
+ "overrides": {
143
+ "js-yaml": "^4.2.0"
141
144
  }
142
145
  }
@@ -16,11 +16,11 @@ REPO_ROOT = Path(__file__).resolve().parents[1]
16
16
  if str(REPO_ROOT) not in sys.path:
17
17
  sys.path.insert(0, str(REPO_ROOT))
18
18
 
19
- from knowledge_graph import KnowledgeGraphStore
20
- from lattice_brain.quality import RetrievalBenchmarkRunner
21
- from latticeai.services.search_service import SearchService
22
- from latticeai.services.memory_service import MemoryService
23
- from lattice_brain.retrieval_benchmark_fixtures import DOCUMENTS, FIXTURE_NAME, QUERIES, TOP_K
19
+ from knowledge_graph import KnowledgeGraphStore # noqa: E402
20
+ from lattice_brain.quality import RetrievalBenchmarkRunner # noqa: E402
21
+ from latticeai.services.search_service import SearchService # noqa: E402
22
+ from latticeai.services.memory_service import MemoryService # noqa: E402
23
+ from lattice_brain.retrieval_benchmark_fixtures import DOCUMENTS, FIXTURE_NAME, QUERIES, TOP_K # noqa: E402
24
24
 
25
25
 
26
26
  class _EvalStore:
@@ -173,11 +173,14 @@ def main() -> int:
173
173
 
174
174
  with tempfile.TemporaryDirectory() as tmp:
175
175
  corpus_metrics = _corpus_scale_retrieval_metrics(Path(tmp))
176
- if corpus_metrics.get("recall@5", 0.0) < 0.80:
176
+ recall_key = f"recall@{TOP_K}"
177
+ precision_key = f"precision@{TOP_K}"
178
+ ndcg_key = f"ndcg@{TOP_K}"
179
+ if corpus_metrics.get(recall_key, 0.0) < 0.80:
177
180
  return _fail(f"corpus hybrid recall below threshold: {corpus_metrics}")
178
- if corpus_metrics.get("precision@5", 0.0) < 0.25:
181
+ if corpus_metrics.get(precision_key, 0.0) < 0.25:
179
182
  return _fail(f"corpus hybrid precision below threshold: {corpus_metrics}")
180
- if corpus_metrics.get("ndcg@5", 0.0) < 0.70:
183
+ if corpus_metrics.get(ndcg_key, 0.0) < 0.70:
181
184
  return _fail(f"corpus hybrid ndcg below threshold: {corpus_metrics}")
182
185
  if corpus_metrics.get("must_include_hit_rate", 0.0) < 0.90:
183
186
  return _fail(f"corpus must-include hit rate below threshold: {corpus_metrics}")
@@ -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
  }