ltcai 5.2.0 → 5.4.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 (44) hide show
  1. package/README.md +144 -164
  2. package/docs/CHANGELOG.md +63 -0
  3. package/docs/DEVELOPMENT.md +99 -0
  4. package/docs/LEGACY_COMPATIBILITY.md +55 -0
  5. package/docs/WHY_LATTICE.md +4 -3
  6. package/frontend/src/App.tsx +8 -2
  7. package/frontend/src/api/client.ts +2 -0
  8. package/frontend/src/components/FirstRunGuide.tsx +5 -5
  9. package/frontend/src/components/ProductFlow.tsx +1 -1
  10. package/frontend/src/i18n.ts +40 -40
  11. package/frontend/src/pages/Act.tsx +82 -1
  12. package/frontend/src/pages/Library.tsx +18 -6
  13. package/lattice_brain/__init__.py +1 -1
  14. package/lattice_brain/archive.py +12 -0
  15. package/lattice_brain/portability.py +14 -0
  16. package/lattice_brain/runtime/__init__.py +53 -0
  17. package/lattice_brain/runtime/agent_runtime.py +7 -0
  18. package/lattice_brain/runtime/hooks.py +6 -0
  19. package/lattice_brain/runtime/multi_agent.py +7 -2
  20. package/latticeai/__init__.py +1 -1
  21. package/latticeai/api/workflow_designer.py +60 -0
  22. package/latticeai/app_factory.py +5 -78
  23. package/latticeai/core/marketplace.py +1 -1
  24. package/latticeai/core/workspace_os.py +1 -1
  25. package/latticeai/runtime/__init__.py +2 -0
  26. package/latticeai/runtime/brain_runtime.py +41 -0
  27. package/latticeai/runtime/config_runtime.py +36 -0
  28. package/latticeai/runtime/security_runtime.py +27 -0
  29. package/latticeai/services/brain_automation.py +214 -0
  30. package/latticeai/services/model_capability_registry.py +2 -3
  31. package/latticeai/services/triggers.py +61 -4
  32. package/package.json +2 -2
  33. package/scripts/verify_hf_model_registry.py +1 -3
  34. package/src-tauri/Cargo.lock +1 -1
  35. package/src-tauri/Cargo.toml +1 -1
  36. package/src-tauri/tauri.conf.json +1 -1
  37. package/static/app/asset-manifest.json +5 -5
  38. package/static/app/assets/index-C7vzwUjU.js +16 -0
  39. package/static/app/assets/index-C7vzwUjU.js.map +1 -0
  40. package/static/app/assets/index-HN4f2wbe.css +2 -0
  41. package/static/app/index.html +2 -2
  42. package/static/app/assets/index-CQmHhk8Q.css +0 -2
  43. package/static/app/assets/index-DsnfomFs.js +0 -16
  44. package/static/app/assets/index-DsnfomFs.js.map +0 -1
@@ -20,11 +20,17 @@ from __future__ import annotations
20
20
 
21
21
  import json
22
22
  import logging
23
+ import os
23
24
  import threading
24
25
  import time
25
26
  from pathlib import Path
26
27
  from typing import Any, Callable, Dict, List, Optional
27
28
 
29
+ try:
30
+ from zoneinfo import ZoneInfo
31
+ except Exception: # pragma: no cover
32
+ ZoneInfo = None # type: ignore
33
+
28
34
  DEFAULT_TICK_SECONDS = 5.0
29
35
  MIN_INTERVAL_SECONDS = 60
30
36
 
@@ -51,6 +57,15 @@ class TriggerService:
51
57
  self._stop_event = threading.Event()
52
58
  self._thread: Optional[threading.Thread] = None
53
59
  self._lock = threading.Lock()
60
+ # LATTICE_TZ: wall-clock / display 용. interval 계산은 여전히 unix seconds (duration 기반, drift 방지).
61
+ # describe()와 이벤트에 tz 정보 노출. calendar "daily at HH:MM" semantics 는 추후 cron 확장 시 사용.
62
+ self._tz_name = os.environ.get("LATTICE_TZ") or "UTC"
63
+ self._tz = None
64
+ if ZoneInfo is not None:
65
+ try:
66
+ self._tz = ZoneInfo(self._tz_name)
67
+ except Exception:
68
+ self._tz = ZoneInfo("UTC") if ZoneInfo else None
54
69
 
55
70
  # ── durable state ──────────────────────────────────────────────────────
56
71
  def _load_state(self) -> Dict[str, Any]:
@@ -86,27 +101,37 @@ class TriggerService:
86
101
  continue
87
102
  cfg = node.get("config") or {}
88
103
  kind = str(cfg.get("trigger") or "manual")
104
+ if cfg.get("enabled") is False:
105
+ continue
89
106
  if kind in ("interval", "brain_event"):
90
107
  found.append({"workflow": wf, "node": node, "kind": kind, "config": cfg})
91
108
  return found
92
109
 
93
110
  def describe(self) -> Dict[str, Any]:
94
- """Honest status surface: what is armed, when it last fired/skipped."""
111
+ """Honest status surface: what is armed, when it last fired/skipped.
112
+ Includes LATTICE_TZ, per-trigger status (armed|degraded), consecutive_failures.
113
+ """
95
114
  state = self._load_state()
96
115
  armed = []
97
116
  for item in self._triggered_workflows():
98
117
  wf_id = item["workflow"].get("id")
118
+ entry = state.get(wf_id) or {}
119
+ fails = int(entry.get("consecutive_failures", 0))
120
+ status = "degraded" if fails >= 3 else "armed"
99
121
  armed.append({
100
122
  "workflow_id": wf_id,
101
123
  "name": item["workflow"].get("name"),
102
124
  "kind": item["kind"],
103
125
  "config": {k: v for k, v in item["config"].items() if k != "trigger"},
104
- "last_fired_at": (state.get(wf_id) or {}).get("last_fired_at"),
105
- "recent_events": (state.get(wf_id) or {}).get("events", [])[-5:],
126
+ "last_fired_at": entry.get("last_fired_at"),
127
+ "status": status,
128
+ "consecutive_failures": fails,
129
+ "recent_events": entry.get("events", [])[-5:],
106
130
  })
107
131
  return {
108
132
  "running": bool(self._thread and self._thread.is_alive()),
109
133
  "tick_seconds": self._tick,
134
+ "tz": self._tz_name,
110
135
  "armed": armed,
111
136
  }
112
137
 
@@ -133,6 +158,7 @@ class TriggerService:
133
158
  skipped += missed
134
159
  # Reset the cadence from now — no catch-up storm.
135
160
  entry["last_fired_at"] = now if last is not None else entry.get("last_fired_at")
161
+ entry["last_attempt_at"] = now
136
162
  self._save_state(state)
137
163
  return skipped
138
164
 
@@ -152,9 +178,16 @@ class TriggerService:
152
178
  if last is None:
153
179
  # First sighting arms the schedule; it fires one interval later.
154
180
  entry["last_fired_at"] = now
181
+ entry["last_attempt_at"] = now
155
182
  continue
156
183
  if now - float(last) < interval:
157
184
  continue
185
+ # Dedup guard (edge case): short cooldown + last_attempt prevents rapid re-fire on
186
+ # clock skew, tick jitter, or restart races. Interval 자체 + 이 가드로 중복 실행 방지.
187
+ last_attempt = float(entry.get("last_attempt_at") or 0)
188
+ if now - last_attempt < 10:
189
+ continue
190
+ entry["last_attempt_at"] = now
158
191
  entry["last_fired_at"] = now
159
192
  self._record_event(state, wf_id, {"type": "fired", "trigger": "interval"})
160
193
  fired += 1
@@ -181,7 +214,14 @@ class TriggerService:
181
214
  if wanted and wanted != source_type:
182
215
  continue
183
216
  wf_id = item["workflow"].get("id")
184
- state.setdefault(wf_id, {})["last_fired_at"] = self._clock()
217
+ entry = state.setdefault(wf_id, {})
218
+ now = self._clock()
219
+ # Dedup guard for brain_event too (rapid ingest burst 등).
220
+ last_attempt = float(entry.get("last_attempt_at") or 0)
221
+ if now - last_attempt < 5:
222
+ continue
223
+ entry["last_attempt_at"] = now
224
+ entry["last_fired_at"] = now
185
225
  self._record_event(state, wf_id, {
186
226
  "type": "fired", "trigger": "brain_event", "source_type": source_type,
187
227
  })
@@ -211,11 +251,28 @@ class TriggerService:
211
251
  def _run():
212
252
  try:
213
253
  self._run_workflow(workflow_id, {"__trigger__": trigger_info})
254
+ self._record_fire_outcome(workflow_id, ok=True)
214
255
  except Exception as exc:
215
256
  logging.warning("trigger run failed for %s: %s", workflow_id, exc)
257
+ self._record_fire_outcome(workflow_id, ok=False, detail=str(exc))
216
258
 
217
259
  threading.Thread(target=_run, name=f"trigger-{workflow_id}", daemon=True).start()
218
260
 
261
+ def _record_fire_outcome(self, wf_id: str, *, ok: bool, detail: str = "") -> None:
262
+ """Track consecutive launch failures for degraded status in describe().
263
+ (Deep execution failures are visible via workflow run records; 여기서는 scheduler fire 자체 실패를 카운트.)
264
+ """
265
+ with self._lock:
266
+ state = self._load_state()
267
+ entry = state.setdefault(wf_id, {})
268
+ if ok:
269
+ entry["consecutive_failures"] = 0
270
+ else:
271
+ fails = int(entry.get("consecutive_failures", 0)) + 1
272
+ entry["consecutive_failures"] = fails
273
+ self._record_event(state, wf_id, {"type": "failed", "detail": detail[:200]})
274
+ self._save_state(state)
275
+
219
276
  def start(self) -> None:
220
277
  if self._thread and self._thread.is_alive():
221
278
  return
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ltcai",
3
- "version": "5.2.0",
4
- "description": "Lattice AI — local-first Living Brain workspace (conversation, durable memory, hybrid search, agents, advanced graph exploration, portable encrypted brain archives)",
3
+ "version": "5.4.0",
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": {
7
7
  "type": "git",
@@ -26,7 +26,6 @@ from __future__ import annotations
26
26
 
27
27
  import argparse
28
28
  import json
29
- import os
30
29
  import sys
31
30
  import time
32
31
  import urllib.error
@@ -43,7 +42,6 @@ try:
43
42
  from latticeai.services.model_capability_registry import (
44
43
  get_all_capabilities,
45
44
  ModelCapability,
46
- VerificationStatus,
47
45
  )
48
46
  except Exception as e:
49
47
  print("ERROR: Could not import model_capability_registry:", e)
@@ -206,7 +204,7 @@ def main() -> int:
206
204
  args = parser.parse_args()
207
205
 
208
206
  caps = get_all_capabilities()
209
- print(f"Lattice AI 5.2.0 HF Model Registry Verifier")
207
+ print("Lattice AI 5.2.0 HF Model Registry Verifier")
210
208
  print(f"Capabilities in registry: {len(caps)}")
211
209
  print(f"Time: {datetime.now(timezone.utc).isoformat()}")
212
210
  print("-" * 88)
@@ -1654,7 +1654,7 @@ dependencies = [
1654
1654
 
1655
1655
  [[package]]
1656
1656
  name = "lattice-ai-desktop"
1657
- version = "5.2.0"
1657
+ version = "5.4.0"
1658
1658
  dependencies = [
1659
1659
  "plist",
1660
1660
  "serde",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "lattice-ai-desktop"
3
- version = "5.2.0"
3
+ version = "5.4.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": "5.2.0",
4
+ "version": "5.4.0",
5
5
  "identifier": "ai.lattice.desktop",
6
6
  "build": {
7
7
  "beforeDevCommand": "npm run frontend:dev",
@@ -1,13 +1,13 @@
1
1
  {
2
- "version": "5.2.0",
2
+ "version": "5.4.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
- "index.html": "/static/app/assets/index-DsnfomFs.js",
10
- "assets/index-CQmHhk8Q.css": "/static/app/assets/index-CQmHhk8Q.css"
9
+ "index.html": "/static/app/assets/index-C7vzwUjU.js",
10
+ "assets/index-HN4f2wbe.css": "/static/app/assets/index-HN4f2wbe.css"
11
11
  },
12
12
  "vite": {
13
13
  "../node_modules/@tauri-apps/api/core.js": {
@@ -17,7 +17,7 @@
17
17
  "isDynamicEntry": true
18
18
  },
19
19
  "index.html": {
20
- "file": "assets/index-DsnfomFs.js",
20
+ "file": "assets/index-C7vzwUjU.js",
21
21
  "name": "index",
22
22
  "src": "index.html",
23
23
  "isEntry": true,
@@ -25,7 +25,7 @@
25
25
  "../node_modules/@tauri-apps/api/core.js"
26
26
  ],
27
27
  "css": [
28
- "assets/index-CQmHhk8Q.css"
28
+ "assets/index-HN4f2wbe.css"
29
29
  ]
30
30
  }
31
31
  }