ltcai 11.5.1 → 11.5.2

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 (176) hide show
  1. package/README.md +85 -53
  2. package/docs/CHANGELOG.md +54 -0
  3. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  4. package/docs/DEVELOPMENT.md +1 -1
  5. package/docs/HYBRID_CLOUD_KG_STREAMING.md +8 -4
  6. package/docs/MULTI_AGENT_RUNTIME.md +1 -1
  7. package/docs/ONBOARDING.md +1 -1
  8. package/docs/OPERATIONS.md +1 -1
  9. package/docs/TRUST_MODEL.md +1 -1
  10. package/docs/WHY_LATTICE.md +1 -1
  11. package/docs/WORKFLOW_DESIGNER.md +1 -1
  12. package/docs/architecture.md +4 -4
  13. package/docs/kg-schema.md +1 -1
  14. package/docs/v11.5.2_TIGHT_SHIP_PLAN.md +144 -0
  15. package/lattice_brain/__init__.py +1 -1
  16. package/lattice_brain/graph/proactive.py +1 -1
  17. package/lattice_brain/graph/projection/v2_schema.py +0 -28
  18. package/lattice_brain/graph/vector_index/base.py +0 -3
  19. package/lattice_brain/graph/vector_index/brute_force.py +0 -4
  20. package/lattice_brain/graph/vector_index/hnsw.py +0 -3
  21. package/lattice_brain/graph/vector_index/quantized.py +0 -3
  22. package/lattice_brain/ingestion_jobs.py +0 -3
  23. package/lattice_brain/memory.py +0 -27
  24. package/lattice_brain/portability/sharing.py +0 -4
  25. package/lattice_brain/quality.py +0 -49
  26. package/lattice_brain/runtime/agent_runtime.py +1 -1
  27. package/lattice_brain/runtime/hooks.py +0 -13
  28. package/lattice_brain/runtime/multi_agent.py +1 -1
  29. package/lattice_brain/sealed_box.py +0 -4
  30. package/latticeai/__init__.py +1 -1
  31. package/latticeai/api/admin.py +12 -12
  32. package/latticeai/api/auth.py +25 -2
  33. package/latticeai/api/chat.py +3 -3
  34. package/latticeai/api/chat_agent_http.py +3 -12
  35. package/latticeai/api/chat_helpers.py +10 -13
  36. package/latticeai/api/computer_use.py +31 -42
  37. package/latticeai/api/health.py +21 -1
  38. package/latticeai/api/local_files.py +14 -0
  39. package/latticeai/api/permissions.py +38 -12
  40. package/latticeai/api/search.py +24 -0
  41. package/latticeai/api/static_routes.py +4 -1
  42. package/latticeai/cli/runtime.py +7 -3
  43. package/latticeai/core/config.py +47 -3
  44. package/latticeai/core/csrf.py +28 -2
  45. package/latticeai/core/embedding_providers/__init__.py +3 -5
  46. package/latticeai/core/embedding_providers/base.py +1 -1
  47. package/latticeai/core/embedding_providers/text.py +1 -1
  48. package/latticeai/core/enterprise.py +1 -4
  49. package/latticeai/core/http_origin.py +146 -0
  50. package/latticeai/core/invitations.py +3 -2
  51. package/latticeai/core/io_utils.py +2 -11
  52. package/latticeai/core/legacy_compatibility.py +1 -1
  53. package/latticeai/core/marketplace.py +1 -1
  54. package/latticeai/core/messages.py +5 -0
  55. package/latticeai/core/model_compat.py +2 -8
  56. package/latticeai/core/module_probe.py +37 -0
  57. package/latticeai/core/run_explain.py +5 -2
  58. package/latticeai/core/run_store.py +2 -2
  59. package/latticeai/core/security.py +13 -0
  60. package/latticeai/core/sessions.py +3 -2
  61. package/latticeai/core/sse.py +25 -0
  62. package/latticeai/core/users.py +0 -9
  63. package/latticeai/core/workspace_graph_trace.py +2 -8
  64. package/latticeai/core/workspace_os.py +0 -10
  65. package/latticeai/core/workspace_os_constants.py +1 -1
  66. package/latticeai/core/workspace_os_utils.py +34 -0
  67. package/latticeai/core/workspace_review_items.py +2 -8
  68. package/latticeai/core/workspace_runs.py +2 -8
  69. package/latticeai/core/workspace_skills.py +2 -7
  70. package/latticeai/models/router/documents.py +0 -35
  71. package/latticeai/models/router/registry.py +0 -4
  72. package/latticeai/runtime/access_runtime.py +16 -4
  73. package/latticeai/runtime/build_phases/features.py +2 -0
  74. package/latticeai/runtime/build_phases/web.py +2 -0
  75. package/latticeai/runtime/feature_toggle_wiring.py +12 -18
  76. package/latticeai/runtime/network_boundary_wiring.py +8 -19
  77. package/latticeai/runtime/permission_mode_wiring.py +6 -13
  78. package/latticeai/runtime/router_registration.py +4 -0
  79. package/latticeai/runtime/runtime_context.py +1 -13
  80. package/latticeai/runtime/service_singletons.py +55 -0
  81. package/latticeai/services/architecture_readiness.py +1 -1
  82. package/latticeai/services/brain_intelligence/proposals.py +0 -5
  83. package/latticeai/services/change_proposals.py +2 -36
  84. package/latticeai/services/chronicle.py +4 -6
  85. package/latticeai/services/command_center.py +4 -4
  86. package/latticeai/services/evidence_actions.py +5 -2
  87. package/latticeai/services/hybrid_chat.py +2 -2
  88. package/latticeai/services/hybrid_policy.py +8 -57
  89. package/latticeai/services/mode_store.py +132 -0
  90. package/latticeai/services/model_capability_registry.py +0 -15
  91. package/latticeai/services/model_catalog.py +0 -7
  92. package/latticeai/services/model_loading.py +3 -2
  93. package/latticeai/services/model_runtime/__init__.py +0 -57
  94. package/latticeai/services/model_runtime/engines.py +27 -128
  95. package/latticeai/services/model_runtime/loading.py +2 -2
  96. package/latticeai/services/network_boundary_service.py +7 -44
  97. package/latticeai/services/permission_mode_service.py +8 -56
  98. package/latticeai/services/product_readiness.py +1 -1
  99. package/latticeai/services/setup_detection.py +67 -1
  100. package/latticeai/services/tool_dispatch.py +0 -4
  101. package/latticeai/services/upload_service.py +7 -20
  102. package/latticeai/services/workspace_service.py +0 -6
  103. package/latticeai/setup/auto_setup.py +15 -26
  104. package/latticeai/setup/wizard/catalog.py +5 -2
  105. package/latticeai/setup/wizard/detect.py +6 -27
  106. package/latticeai/setup/wizard/paths.py +2 -5
  107. package/latticeai/setup/wizard/plans.py +2 -2
  108. package/package.json +2 -4
  109. package/scripts/bump_version.py +14 -1
  110. package/scripts/check_current_release_docs.mjs +1 -1
  111. package/scripts/check_legacy_debt.mjs +1 -1
  112. package/scripts/generate_agent_loop_fixtures.py +87 -0
  113. package/scripts/generate_rust_parity_fixtures.py +3 -95
  114. package/scripts/parity_fixture_corpus_context.py +162 -0
  115. package/scripts/release_screen_claims.json +10 -0
  116. package/src-tauri/Cargo.lock +7 -7
  117. package/src-tauri/Cargo.toml +1 -1
  118. package/src-tauri/src/backend.rs +76 -2
  119. package/src-tauri/src/main.rs +13 -3
  120. package/src-tauri/tauri.conf.json +2 -2
  121. package/static/app/asset-manifest.json +40 -40
  122. package/static/app/assets/{Act-Drs_jd-O.js → Act-DcQizkl1.js} +1 -1
  123. package/static/app/assets/{AdminConsole-BqNx5oF6.js → AdminConsole-cf4npybT.js} +1 -1
  124. package/static/app/assets/{Brain-C7bdNmz-.js → Brain-3VCSHFcn.js} +1 -1
  125. package/static/app/assets/{BrainHome-DcTnINcI.js → BrainHome-Qm8eaztx.js} +1 -1
  126. package/static/app/assets/{BrainSignals-Df1MRIPD.js → BrainSignals-DS9BtKOW.js} +1 -1
  127. package/static/app/assets/{Capture-Dgsxvnbx.js → Capture-DiQ219jW.js} +1 -1
  128. package/static/app/assets/{Chronicle-BrkTxJK1.js → Chronicle-BGvuAchH.js} +1 -1
  129. package/static/app/assets/{CommandPalette-DDziw7Oj.js → CommandPalette-Bqhm0Urn.js} +1 -1
  130. package/static/app/assets/{Library-CYxeuraA.js → Library-BV6NnF0a.js} +1 -1
  131. package/static/app/assets/{LivingBrain-CNp6mvCm.js → LivingBrain-GzenJchP.js} +1 -1
  132. package/static/app/assets/{ProductFlow-BVsowu3Z.js → ProductFlow-DEP6-vML.js} +1 -1
  133. package/static/app/assets/{ReviewCard-D1LbOfJS.js → ReviewCard-CNZ7XjWG.js} +1 -1
  134. package/static/app/assets/{System-DcRAq7wj.js → System-CieofHQa.js} +1 -1
  135. package/static/app/assets/arrow-left-kfsrk0mv.js +1 -0
  136. package/static/app/assets/{bot-BK2xQ9mN.js → bot-B_K1Tdmw.js} +1 -1
  137. package/static/app/assets/{brain-BZNztFBb.js → brain-DWyaV1L1.js} +1 -1
  138. package/static/app/assets/{button-CjueubVZ.js → button-aTn4s84A.js} +1 -1
  139. package/static/app/assets/circle-check-qqLug9nU.js +1 -0
  140. package/static/app/assets/{circle-pause-HRjSmpyY.js → circle-pause-xKgeGXkT.js} +1 -1
  141. package/static/app/assets/{circle-play-CMcKDVwu.js → circle-play-DkT6tYPX.js} +1 -1
  142. package/static/app/assets/{cpu-B_MyTfYr.js → cpu-85xYObUC.js} +1 -1
  143. package/static/app/assets/{download-CTy0EByV.js → download-B5Fm7YXo.js} +1 -1
  144. package/static/app/assets/{folder-open-DF6bLhTg.js → folder-open-kk2Xa52u.js} +1 -1
  145. package/static/app/assets/{hard-drive-CpWHG77C.js → hard-drive-DkA3zBW_.js} +1 -1
  146. package/static/app/assets/{index-Dd6abJHX.js → index-BMPdTmlY.js} +3 -3
  147. package/static/app/assets/{input-bVgIRN3s.js → input-B0nRf2jO.js} +1 -1
  148. package/static/app/assets/{link-2-bg6CG5Vk.js → link-2-Dwb4gnTc.js} +1 -1
  149. package/static/app/assets/{permissionCopy-DFTf1HDZ.js → permissionCopy-CQDUBrOZ.js} +1 -1
  150. package/static/app/assets/{primitives-D7D-sQ3T.js → primitives-SNp0LRJz.js} +1 -1
  151. package/static/app/assets/search-BcHqkjoy.js +1 -0
  152. package/static/app/assets/{share-2-BLzo7U4L.js → share-2-BsrxFglO.js} +1 -1
  153. package/static/app/assets/{shield-alert-BmngqnsJ.js → shield-alert-5BStfp2_.js} +1 -1
  154. package/static/app/assets/{textarea-DOSlAQQ3.js → textarea-Cg8IUA-k.js} +1 -1
  155. package/static/app/assets/{useFocusTrap-pZhgeee4.js → useFocusTrap-CYKvE46M.js} +1 -1
  156. package/static/app/assets/{useMutation-BMDwNk4I.js → useMutation-CSn9t1op.js} +1 -1
  157. package/static/app/assets/{useQuery-CSjKttRo.js → useQuery-CY2OI2uy.js} +1 -1
  158. package/static/app/assets/{utils-D3u_yv7B.js → utils-Ddol2RWD.js} +1 -1
  159. package/static/app/assets/{workspace-H3bMjYXC.js → workspace-BqDwOz_p.js} +1 -1
  160. package/static/app/index.html +3 -3
  161. package/static/sw.js +1 -1
  162. package/desktop/electron/README.md +0 -9
  163. package/desktop/electron/main.cjs +0 -58
  164. package/desktop/electron/preload.cjs +0 -5
  165. package/latticeai/core/graph_curator.py +0 -11
  166. package/latticeai/core/hooks.py +0 -11
  167. package/latticeai/core/local_embeddings.py +0 -104
  168. package/latticeai/core/multi_agent.py +0 -11
  169. package/latticeai/core/workflow_engine.py +0 -11
  170. package/latticeai/services/ingestion.py +0 -11
  171. package/latticeai/services/kg_portability.py +0 -11
  172. package/latticeai/services/multimodal_streaming.py +0 -129
  173. package/scripts/measure_brain_home_fill.mjs +0 -141
  174. package/static/app/assets/arrow-left-BHYmOTWU.js +0 -1
  175. package/static/app/assets/circle-check-CLeWJr67.js +0 -1
  176. package/static/app/assets/search-BgSVX6NG.js +0 -1
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env python3
2
+ """The context-assembly half of the Python↔Rust parity corpus (v11.5.2).
3
+
4
+ ``scripts/generate_rust_parity_fixtures.py`` owns the store, the harness and the
5
+ goldens; this module owns the specs and runners for the two context ports, so
6
+ neither file grows past the tree's file-size ceiling. The split follows the
7
+ ``parity_fixture_corpus_docgen`` precedent: specs and their runners live side by
8
+ side, because a spec list that arrives without the runner that answers it is a
9
+ suite that silently checks nothing.
10
+
11
+ Two ports, deliberately separate:
12
+
13
+ * **``context_assemble``** pins :class:`~lattice_brain.context.ContextAssembler`
14
+ over the seams the *production* assembler supplies
15
+ (``latticeai/runtime/context_runtime.py``): memories, artifacts, hybrid
16
+ knowledge and garden notes. It does **not** supply ``recent_chat`` — that seam
17
+ has never been wired live, and a golden that pinned it claimed cross-runtime
18
+ agreement about a code path the product does not execute.
19
+ * **``recent_chat``** pins the recent-conversation transcript as the product
20
+ really builds it: ``chat.py`` prepends ``build_recent_chat_context(...)`` on
21
+ every non-hybrid turn and the agent loop injects the same text each executor
22
+ step. Its Rust twin is ``lattice_retrieval::history::recent_chat``.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from typing import Any, Callable, Dict, List
28
+
29
+ #: ``h`` is the generator's ``Harness``. It is typed ``Any`` rather than
30
+ #: imported: the harness owns the live store and the service layer, so importing
31
+ #: it here would make the corpus depend on the generator that imports *it*.
32
+
33
+ #: The organization workspace the corpus seeds; the same literal the generator
34
+ #: and the docgen corpus use.
35
+ WS_ALPHA = "ws-alpha"
36
+
37
+ CONTEXT_MEMORIES: Dict[str, Any] = {
38
+ "results": [
39
+ {"id": "mem-1", "kind": "preference", "snippet": "답변은 한국어로", "score": 0.91, "source": "workspace"},
40
+ {"id": "mem-2", "kind": None, "snippet": "", "score": 0.0, "source": "workspace"},
41
+ {"id": "mem-3", "kind": "decision", "snippet": "ranking keeps alpha fusion", "score": 0.4, "source": "personal"},
42
+ {"id": "mem-4", "kind": "fact", "snippet": "온보딩은 다섯 걸음", "score": 0.3, "source": "workspace"},
43
+ ]
44
+ }
45
+
46
+ #: A ledger with a pathless row, a non-dict row, and more than the ten-row cut.
47
+ CONTEXT_ARTIFACTS: List[Any] = [
48
+ {"path": "notes/ranking.md", "at": "2026-07-20T09:00:00", "run_id": "run-1"},
49
+ {"path": "notes/onboarding.md", "run_id": "run-2"},
50
+ {"path": "", "at": "2026-07-20T09:00:01"},
51
+ "not-a-dict",
52
+ ] + [{"path": f"out/file-{index}.md", "at": None, "run_id": f"r{index}"} for index in range(10)]
53
+
54
+ def _context_seams(h: Any, spec: Dict[str, Any]) -> Dict[str, Any]:
55
+ """The seam set for one context spec — data seams plus the real engines.
56
+
57
+ ``memories`` / ``artifacts`` / ``notes`` are *data* seams: the payload is the
58
+ spec, so both runtimes feed the assembler the same bytes and what is under
59
+ test is the assembler. ``knowledge`` is a real engine — the service-layer
60
+ hybrid search.
61
+
62
+ There is deliberately no ``recent_chat`` seam here: the production
63
+ assembler does not supply one, so a golden that did would pin agreement
64
+ about a path neither runtime reaches in the product. The recent transcript
65
+ is pinned by the ``recent_chat`` suite instead.
66
+ """
67
+ # Signatures matter: the assembler inspects them to decide which context
68
+ # fields a seam may be handed, so each one declares exactly what it accepts.
69
+ seams: Dict[str, Any] = {}
70
+ if spec.get("memories") is not None:
71
+ memories = spec["memories"]
72
+ seams["memory_recall"] = (
73
+ lambda query, *, user_email=None, workspace_id=None, limit=5: memories
74
+ )
75
+ if spec.get("artifacts") is not None:
76
+ artifacts = spec["artifacts"]
77
+ seams["recent_artifacts"] = (
78
+ lambda *, user_email=None, conversation_id=None, workspace_id=None: artifacts
79
+ )
80
+ if spec.get("knowledge", True):
81
+ # Loopback trust: no workspace scoping, exactly as on the native route.
82
+ seams["hybrid_search"] = (
83
+ lambda query, *, limit=5, user_email=None, workspace_id=None:
84
+ h.service.hybrid_search(query, limit=limit)
85
+ )
86
+ if spec.get("notes") is not None:
87
+ notes = spec["notes"]
88
+ seams["notes_context"] = lambda query, *, user_email=None, workspace_id=None: notes
89
+ return seams
90
+
91
+
92
+ def _run_context_assemble(h: Any, spec: Dict[str, Any]) -> Dict[str, Any]:
93
+ from lattice_brain.context import ContextAssembler
94
+
95
+ assembled = ContextAssembler(**_context_seams(h, spec)).assemble(
96
+ spec["query"], user_email=spec.get("user_email"),
97
+ workspace_id=spec.get("workspace_id"),
98
+ conversation_id=spec.get("conversation_id"), budget=spec.get("budget", 2000),
99
+ memory_limit=spec.get("memory_limit", 5),
100
+ knowledge_limit=spec.get("knowledge_limit", 5),
101
+ )
102
+ return {"text": assembled.text, "approx_tokens": assembled.approx_tokens,
103
+ "trace": assembled.trace()}
104
+
105
+
106
+ def _run_recent_chat(h: Any, spec: Dict[str, Any]) -> str:
107
+ """The recent-conversation transcript, exactly as ``/chat`` builds it."""
108
+ from latticeai.api.chat_helpers import build_recent_chat_context
109
+
110
+ return build_recent_chat_context(
111
+ get_history=h.history_runtime["get_history"],
112
+ limit=spec.get("limit", 10),
113
+ include_image_missing_replies=spec.get("images", True),
114
+ user_email=spec.get("user_email"),
115
+ conversation_id=spec.get("conversation_id"),
116
+ workspace_id=spec.get("workspace_id"),
117
+ )
118
+
119
+
120
+ #: The two context suites, as the generator merges them into :data:`SUITES` /
121
+ #: :data:`SUITE_RUNNERS`.
122
+ CONTEXT_SUITES: Dict[str, List[Dict[str, Any]]] = {
123
+ # 11.5.2: these pin the assembler over the seams the *production* assembler
124
+ # supplies (`latticeai/runtime/context_runtime.py`). The `recent_chat` seam
125
+ # is not one of them — it has never been wired live — so pinning it here
126
+ # claimed cross-runtime agreement about a code path the product does not
127
+ # run. The recent-conversation text itself is still pinned, in the
128
+ # `recent_chat` suite below, against the function chat really calls.
129
+ "context_assemble": [
130
+ {"key": "all_seams", "query": "회의 결정 사항", "memories": CONTEXT_MEMORIES, "artifacts": CONTEXT_ARTIFACTS, "notes": "정원 노트: 랭킹은 alpha 융합을 유지한다."},
131
+ {"key": "knowledge_only", "query": "hybrid retrieval ranking"},
132
+ {"key": "no_seams", "query": "hybrid retrieval ranking", "knowledge": False},
133
+ {"key": "memories_only", "query": "온보딩", "knowledge": False, "memories": CONTEXT_MEMORIES, "memory_limit": 2},
134
+ {"key": "artifacts_only", "query": "온보딩", "knowledge": False, "artifacts": CONTEXT_ARTIFACTS},
135
+ {"key": "notes_blank", "query": "온보딩", "knowledge": False, "notes": " "},
136
+ {"key": "budget_tiny", "query": "회의 결정 사항", "memories": CONTEXT_MEMORIES, "artifacts": CONTEXT_ARTIFACTS, "notes": "정원 노트: 랭킹은 alpha 융합을 유지한다.", "budget": 20},
137
+ {"key": "budget_one", "query": "회의 결정 사항", "memories": CONTEXT_MEMORIES, "artifacts": CONTEXT_ARTIFACTS, "notes": "노트", "budget": 1},
138
+ {"key": "budget_zero", "query": "회의 결정 사항", "memories": CONTEXT_MEMORIES, "notes": "노트", "budget": 0},
139
+ {"key": "knowledge_limit_one", "query": "hybrid retrieval ranking", "knowledge_limit": 1},
140
+ ],
141
+ # The recent-conversation transcript as the product actually builds it:
142
+ # `chat.py` prepends `build_recent_chat_context(...)` on every non-hybrid
143
+ # turn, and the agent loop injects the same text each executor step. This
144
+ # is the live twin of `lattice_retrieval::history::recent_chat`.
145
+ "recent_chat": [
146
+ {"key": "everything"},
147
+ {"key": "conversation", "conversation_id": "conv-a"},
148
+ {"key": "conversation_missing", "conversation_id": "nope"},
149
+ {"key": "personal_workspace", "workspace_id": "personal", "limit": 6},
150
+ {"key": "workspace_alpha", "workspace_id": WS_ALPHA},
151
+ {"key": "user_scoped", "user_email": "jiwon@lattice.ai", "limit": 5},
152
+ {"key": "user_and_conversation", "user_email": "jiwon@lattice.ai", "conversation_id": "conv-a"},
153
+ {"key": "limit_one", "limit": 1},
154
+ {"key": "limit_zero", "limit": 0},
155
+ {"key": "image_replies_dropped", "images": False},
156
+ ],
157
+ }
158
+
159
+ CONTEXT_RUNNERS: Dict[str, Callable[..., Any]] = {
160
+ "context_assemble": _run_context_assemble,
161
+ "recent_chat": _run_recent_chat,
162
+ }
@@ -21,6 +21,16 @@
21
21
  "specific, not less."
22
22
  ],
23
23
 
24
+ "11.5.2": {
25
+ "_why": [
26
+ "Tight Ship release: deletions, consolidation and front-door fixes —",
27
+ "backend only. The frontend bundle is untouched; no captured screen",
28
+ "changes on purpose. All thirteen measured but not gated. Known",
29
+ "jitter: 05-memory-graph moves ~2-3% between captures from the",
30
+ "cytoscape layout (11.3.0-11.5.1 observed)."
31
+ ]
32
+ },
33
+
24
34
  "11.5.1": {
25
35
  "_why": [
26
36
  "Rust Full Loop release: agent-loop orchestrator port, worker seams,",
@@ -1730,7 +1730,7 @@ dependencies = [
1730
1730
 
1731
1731
  [[package]]
1732
1732
  name = "lattice-agent"
1733
- version = "11.5.1"
1733
+ version = "11.5.2"
1734
1734
  dependencies = [
1735
1735
  "axum",
1736
1736
  "base64 0.22.1",
@@ -1746,7 +1746,7 @@ dependencies = [
1746
1746
 
1747
1747
  [[package]]
1748
1748
  name = "lattice-ai-desktop"
1749
- version = "11.5.1"
1749
+ version = "11.5.2"
1750
1750
  dependencies = [
1751
1751
  "lattice-host",
1752
1752
  "lattice-jobs",
@@ -1761,7 +1761,7 @@ dependencies = [
1761
1761
 
1762
1762
  [[package]]
1763
1763
  name = "lattice-core"
1764
- version = "11.5.1"
1764
+ version = "11.5.2"
1765
1765
  dependencies = [
1766
1766
  "blake2",
1767
1767
  "rusqlite",
@@ -1771,7 +1771,7 @@ dependencies = [
1771
1771
 
1772
1772
  [[package]]
1773
1773
  name = "lattice-host"
1774
- version = "11.5.1"
1774
+ version = "11.5.2"
1775
1775
  dependencies = [
1776
1776
  "axum",
1777
1777
  "lattice-agent",
@@ -1788,7 +1788,7 @@ dependencies = [
1788
1788
 
1789
1789
  [[package]]
1790
1790
  name = "lattice-ingest"
1791
- version = "11.5.1"
1791
+ version = "11.5.2"
1792
1792
  dependencies = [
1793
1793
  "axum",
1794
1794
  "lattice-core",
@@ -1801,7 +1801,7 @@ dependencies = [
1801
1801
 
1802
1802
  [[package]]
1803
1803
  name = "lattice-jobs"
1804
- version = "11.5.1"
1804
+ version = "11.5.2"
1805
1805
  dependencies = [
1806
1806
  "axum",
1807
1807
  "lattice-core",
@@ -1813,7 +1813,7 @@ dependencies = [
1813
1813
 
1814
1814
  [[package]]
1815
1815
  name = "lattice-retrieval"
1816
- version = "11.5.1"
1816
+ version = "11.5.2"
1817
1817
  dependencies = [
1818
1818
  "axum",
1819
1819
  "fancy-regex",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "lattice-ai-desktop"
3
- version = "11.5.1"
3
+ version = "11.5.2"
4
4
  description = "Lattice AI Digital Brain desktop shell"
5
5
  authors = ["TaeSoo Park"]
6
6
  edition = "2021"
@@ -72,6 +72,9 @@ pub struct BackendStatus {
72
72
  /// Whether the background jobs timer is running in this process
73
73
  /// (11.5.0, additive).
74
74
  pub jobs_running: bool,
75
+ /// Why the front door is not there, when this shell was supposed to serve
76
+ /// one and could not bind it (11.5.2, additive). `null` normally.
77
+ pub gateway_error: Option<String>,
75
78
  }
76
79
 
77
80
  /// The gateway this shell is serving, and how to stop it.
@@ -89,6 +92,11 @@ pub struct DesktopBackend {
89
92
  gateway: Mutex<Option<RunningGateway>>,
90
93
  /// The jobs scheduler, mounted on `/host/jobs` in the gateway topology.
91
94
  jobs: Option<Arc<lattice_jobs::Scheduler>>,
95
+ /// Why the front door is not there, when it could not be bound.
96
+ ///
97
+ /// `None` means "nothing went wrong" — including in the topologies that
98
+ /// serve no gateway at all, where the worker *is* the front door.
99
+ gateway_error: Mutex<Option<String>>,
92
100
  }
93
101
 
94
102
  impl DesktopBackend {
@@ -116,6 +124,7 @@ impl DesktopBackend {
116
124
  health_deadline,
117
125
  gateway: Mutex::new(None),
118
126
  jobs,
127
+ gateway_error: Mutex::new(None),
119
128
  })
120
129
  }
121
130
 
@@ -139,6 +148,18 @@ impl DesktopBackend {
139
148
  self.plan.topology
140
149
  }
141
150
 
151
+ /// Why the front door is unavailable, when it is.
152
+ ///
153
+ /// `Some` only when this shell was supposed to serve the gateway and could
154
+ /// not bind it — in which case [`Self::app_url`] names a port nothing will
155
+ /// ever listen on, and `main::boot` must not navigate the window into it.
156
+ pub fn gateway_error(&self) -> Option<String> {
157
+ self.gateway_error
158
+ .lock()
159
+ .expect("gateway error lock")
160
+ .clone()
161
+ }
162
+
142
163
  /// Start the front door, then the worker.
143
164
  ///
144
165
  /// The gateway comes up first on purpose: it is what the webview polls, and
@@ -171,10 +192,16 @@ impl DesktopBackend {
171
192
  let listener = match bind_loopback(addr).await {
172
193
  Ok(listener) => listener,
173
194
  Err(err) => {
174
- eprintln!(
175
- "lattice-ai-desktop: the gateway could not bind {addr}: {err} — \
195
+ let message = format!(
196
+ "the gateway could not bind {addr}: {err} — \
176
197
  set LATTICEAI_DESKTOP_DIRECT=1 to run the worker as the front door"
177
198
  );
199
+ eprintln!("lattice-ai-desktop: {message}");
200
+ // Recorded, not only logged: `plan.origin` names the port that
201
+ // just failed to bind, so navigating the window there would
202
+ // land on a connection refused with no explanation. The window
203
+ // stays on the bundled shell and reads this instead.
204
+ *self.gateway_error.lock().expect("gateway error lock") = Some(message);
178
205
  return;
179
206
  }
180
207
  };
@@ -310,6 +337,7 @@ impl DesktopBackend {
310
337
  .as_ref()
311
338
  .map(|scheduler| scheduler.is_running())
312
339
  .unwrap_or(false),
340
+ gateway_error: self.gateway_error(),
313
341
  }
314
342
  }
315
343
 
@@ -457,4 +485,50 @@ mod tests {
457
485
  shell.stop_gateway();
458
486
  shell.stop_gateway();
459
487
  }
488
+
489
+ /// A shell that never had to bind a front door has nothing to report, and
490
+ /// its origin is navigable.
491
+ #[test]
492
+ fn a_healthy_shell_names_no_gateway_error() {
493
+ for probe in [
494
+ StaticProbe::new().with_env(PORT_ENV, "41844"),
495
+ StaticProbe::new().with_env(DIRECT_ENV, "1"),
496
+ ] {
497
+ let shell = backend(probe);
498
+ assert_eq!(shell.gateway_error(), None);
499
+ let value =
500
+ serde_json::to_value(shell.render(&WorkerStatus::idle(4825, true))).expect("json");
501
+ assert!(value["gateway_error"].is_null());
502
+ }
503
+ }
504
+
505
+ /// Binding the front door is the one failure that makes `app_url` name a
506
+ /// port nothing will ever answer on. The window must read the reason rather
507
+ /// than navigate into it.
508
+ #[tokio::test]
509
+ async fn a_gateway_that_cannot_bind_is_reported_and_not_navigated_to() {
510
+ // Hold the port the shell will try to bind.
511
+ let occupied = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
512
+ let port = occupied.local_addr().expect("addr").port();
513
+ let shell = backend(StaticProbe::new().with_env(PORT_ENV, &port.to_string()));
514
+ assert_eq!(shell.topology(), Topology::Gateway);
515
+
516
+ shell.start_gateway().await;
517
+
518
+ let reason = shell.gateway_error().expect("the failure is recorded");
519
+ assert!(reason.contains("could not bind"), "{reason}");
520
+ assert!(
521
+ reason.contains("LATTICEAI_DESKTOP_DIRECT"),
522
+ "the way out is named: {reason}"
523
+ );
524
+ assert!(
525
+ shell.app_url().contains(&port.to_string()),
526
+ "app_url still names the port nothing bound — which is why boot must \
527
+ read gateway_error instead of navigating"
528
+ );
529
+ let value =
530
+ serde_json::to_value(shell.render(&WorkerStatus::idle(port, true))).expect("serialise");
531
+ assert_eq!(value["gateway_error"], reason);
532
+ drop(occupied);
533
+ }
460
534
  }
@@ -82,9 +82,19 @@ fn boot(app: &tauri::App) {
82
82
  // Waiting in both places would double the worst case.
83
83
  shell.wait_until_serving().await;
84
84
  }
85
- // Navigate either way, as the shell always has: a window showing the
86
- // browser's own "cannot connect" is more use than a window showing
87
- // nothing while a worker that will never arrive is waited for.
85
+ // One reason not to navigate, and it is not "the worker is slow": if
86
+ // this shell was supposed to serve the front door and could not bind
87
+ // it, `app_url` names a port nothing will ever listen on, and sending
88
+ // the window there replaces a readable failure with a blank "cannot
89
+ // connect". The bundled shell stays up and reads `gateway_error` out of
90
+ // `desktopBackendStatus` instead.
91
+ if let Some(reason) = shell.gateway_error() {
92
+ eprintln!("lattice-ai-desktop: staying on the bundled shell — {reason}");
93
+ return;
94
+ }
95
+ // Otherwise navigate either way, as the shell always has: a window
96
+ // showing the browser's own "cannot connect" is more use than a window
97
+ // showing nothing while a worker that will never arrive is waited for.
88
98
  if let Ok(url) = tauri::Url::parse(&shell.app_url()) {
89
99
  let _ = window.navigate(url);
90
100
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://schema.tauri.app/config/2",
3
3
  "productName": "Lattice AI",
4
- "version": "11.5.1",
4
+ "version": "11.5.2",
5
5
  "identifier": "ai.lattice.desktop",
6
6
  "build": {
7
7
  "beforeDevCommand": "npm run frontend:dev",
@@ -22,7 +22,7 @@
22
22
  }
23
23
  ],
24
24
  "security": {
25
- "csp": "default-src 'self' asset: tauri: http://127.0.0.1:*; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' asset: data: blob: http://127.0.0.1:*; font-src 'self' data:; connect-src 'self' ipc: http://127.0.0.1:* ws://127.0.0.1:*; frame-src 'none'; object-src 'none'; base-uri 'none'; form-action 'self'",
25
+ "csp": "default-src 'self' asset: tauri: http://127.0.0.1:*; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' asset: data: blob: http://127.0.0.1:*; font-src 'self' data:; connect-src 'self' ipc: http://127.0.0.1:*; frame-src 'none'; object-src 'none'; base-uri 'none'; form-action 'self'",
26
26
  "devCsp": "default-src 'self' http://127.0.0.1:* ws://127.0.0.1:*; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: http://127.0.0.1:*; font-src 'self' data:; connect-src 'self' http://127.0.0.1:* ws://127.0.0.1:*; frame-src 'none'; object-src 'none'; base-uri 'none'; form-action 'self'"
27
27
  }
28
28
  },
@@ -1,49 +1,49 @@
1
1
  {
2
- "version": "11.5.1",
2
+ "version": "11.5.2",
3
3
  "generated_at": "vite",
4
4
  "entrypoints": {
5
- "app": "/static/app/assets/index-Dd6abJHX.js"
5
+ "app": "/static/app/assets/index-BMPdTmlY.js"
6
6
  },
7
7
  "assets": {
8
8
  "../node_modules/@tauri-apps/api/core.js": "/static/app/assets/core-CwxXejkd.js",
9
- "_BrainSignals-Df1MRIPD.js": "/static/app/assets/BrainSignals-Df1MRIPD.js",
10
- "_LivingBrain-CNp6mvCm.js": "/static/app/assets/LivingBrain-CNp6mvCm.js",
11
- "_ReviewCard-D1LbOfJS.js": "/static/app/assets/ReviewCard-D1LbOfJS.js",
12
- "_arrow-left-BHYmOTWU.js": "/static/app/assets/arrow-left-BHYmOTWU.js",
13
- "_bot-BK2xQ9mN.js": "/static/app/assets/bot-BK2xQ9mN.js",
14
- "_brain-BZNztFBb.js": "/static/app/assets/brain-BZNztFBb.js",
15
- "_button-CjueubVZ.js": "/static/app/assets/button-CjueubVZ.js",
16
- "_circle-check-CLeWJr67.js": "/static/app/assets/circle-check-CLeWJr67.js",
17
- "_circle-pause-HRjSmpyY.js": "/static/app/assets/circle-pause-HRjSmpyY.js",
18
- "_circle-play-CMcKDVwu.js": "/static/app/assets/circle-play-CMcKDVwu.js",
19
- "_cpu-B_MyTfYr.js": "/static/app/assets/cpu-B_MyTfYr.js",
20
- "_download-CTy0EByV.js": "/static/app/assets/download-CTy0EByV.js",
21
- "_folder-open-DF6bLhTg.js": "/static/app/assets/folder-open-DF6bLhTg.js",
22
- "_hard-drive-CpWHG77C.js": "/static/app/assets/hard-drive-CpWHG77C.js",
23
- "_input-bVgIRN3s.js": "/static/app/assets/input-bVgIRN3s.js",
24
- "_link-2-bg6CG5Vk.js": "/static/app/assets/link-2-bg6CG5Vk.js",
25
- "_permissionCopy-DFTf1HDZ.js": "/static/app/assets/permissionCopy-DFTf1HDZ.js",
26
- "_primitives-D7D-sQ3T.js": "/static/app/assets/primitives-D7D-sQ3T.js",
27
- "_search-BgSVX6NG.js": "/static/app/assets/search-BgSVX6NG.js",
28
- "_share-2-BLzo7U4L.js": "/static/app/assets/share-2-BLzo7U4L.js",
29
- "_shield-alert-BmngqnsJ.js": "/static/app/assets/shield-alert-BmngqnsJ.js",
30
- "_textarea-DOSlAQQ3.js": "/static/app/assets/textarea-DOSlAQQ3.js",
31
- "_useFocusTrap-pZhgeee4.js": "/static/app/assets/useFocusTrap-pZhgeee4.js",
32
- "_useMutation-BMDwNk4I.js": "/static/app/assets/useMutation-BMDwNk4I.js",
33
- "_useQuery-CSjKttRo.js": "/static/app/assets/useQuery-CSjKttRo.js",
34
- "_utils-D3u_yv7B.js": "/static/app/assets/utils-D3u_yv7B.js",
35
- "_workspace-H3bMjYXC.js": "/static/app/assets/workspace-H3bMjYXC.js",
36
- "index.html": "/static/app/assets/index-Dd6abJHX.js",
9
+ "_BrainSignals-DS9BtKOW.js": "/static/app/assets/BrainSignals-DS9BtKOW.js",
10
+ "_LivingBrain-GzenJchP.js": "/static/app/assets/LivingBrain-GzenJchP.js",
11
+ "_ReviewCard-CNZ7XjWG.js": "/static/app/assets/ReviewCard-CNZ7XjWG.js",
12
+ "_arrow-left-kfsrk0mv.js": "/static/app/assets/arrow-left-kfsrk0mv.js",
13
+ "_bot-B_K1Tdmw.js": "/static/app/assets/bot-B_K1Tdmw.js",
14
+ "_brain-DWyaV1L1.js": "/static/app/assets/brain-DWyaV1L1.js",
15
+ "_button-aTn4s84A.js": "/static/app/assets/button-aTn4s84A.js",
16
+ "_circle-check-qqLug9nU.js": "/static/app/assets/circle-check-qqLug9nU.js",
17
+ "_circle-pause-xKgeGXkT.js": "/static/app/assets/circle-pause-xKgeGXkT.js",
18
+ "_circle-play-DkT6tYPX.js": "/static/app/assets/circle-play-DkT6tYPX.js",
19
+ "_cpu-85xYObUC.js": "/static/app/assets/cpu-85xYObUC.js",
20
+ "_download-B5Fm7YXo.js": "/static/app/assets/download-B5Fm7YXo.js",
21
+ "_folder-open-kk2Xa52u.js": "/static/app/assets/folder-open-kk2Xa52u.js",
22
+ "_hard-drive-DkA3zBW_.js": "/static/app/assets/hard-drive-DkA3zBW_.js",
23
+ "_input-B0nRf2jO.js": "/static/app/assets/input-B0nRf2jO.js",
24
+ "_link-2-Dwb4gnTc.js": "/static/app/assets/link-2-Dwb4gnTc.js",
25
+ "_permissionCopy-CQDUBrOZ.js": "/static/app/assets/permissionCopy-CQDUBrOZ.js",
26
+ "_primitives-SNp0LRJz.js": "/static/app/assets/primitives-SNp0LRJz.js",
27
+ "_search-BcHqkjoy.js": "/static/app/assets/search-BcHqkjoy.js",
28
+ "_share-2-BsrxFglO.js": "/static/app/assets/share-2-BsrxFglO.js",
29
+ "_shield-alert-5BStfp2_.js": "/static/app/assets/shield-alert-5BStfp2_.js",
30
+ "_textarea-Cg8IUA-k.js": "/static/app/assets/textarea-Cg8IUA-k.js",
31
+ "_useFocusTrap-CYKvE46M.js": "/static/app/assets/useFocusTrap-CYKvE46M.js",
32
+ "_useMutation-CSn9t1op.js": "/static/app/assets/useMutation-CSn9t1op.js",
33
+ "_useQuery-CY2OI2uy.js": "/static/app/assets/useQuery-CY2OI2uy.js",
34
+ "_utils-Ddol2RWD.js": "/static/app/assets/utils-Ddol2RWD.js",
35
+ "_workspace-BqDwOz_p.js": "/static/app/assets/workspace-BqDwOz_p.js",
36
+ "index.html": "/static/app/assets/index-BMPdTmlY.js",
37
37
  "assets/index-DxmOfNRi.css": "/static/app/assets/index-DxmOfNRi.css",
38
- "src/components/ProductFlow.tsx": "/static/app/assets/ProductFlow-BVsowu3Z.js",
39
- "src/features/admin/AdminConsole.tsx": "/static/app/assets/AdminConsole-BqNx5oF6.js",
40
- "src/features/brain/BrainHome.tsx": "/static/app/assets/BrainHome-DcTnINcI.js",
41
- "src/features/command/CommandPalette.tsx": "/static/app/assets/CommandPalette-DDziw7Oj.js",
42
- "src/pages/Act.tsx": "/static/app/assets/Act-Drs_jd-O.js",
43
- "src/pages/Brain.tsx": "/static/app/assets/Brain-C7bdNmz-.js",
44
- "src/pages/Capture.tsx": "/static/app/assets/Capture-Dgsxvnbx.js",
45
- "src/pages/Chronicle.tsx": "/static/app/assets/Chronicle-BrkTxJK1.js",
46
- "src/pages/Library.tsx": "/static/app/assets/Library-CYxeuraA.js",
47
- "src/pages/System.tsx": "/static/app/assets/System-DcRAq7wj.js"
38
+ "src/components/ProductFlow.tsx": "/static/app/assets/ProductFlow-DEP6-vML.js",
39
+ "src/features/admin/AdminConsole.tsx": "/static/app/assets/AdminConsole-cf4npybT.js",
40
+ "src/features/brain/BrainHome.tsx": "/static/app/assets/BrainHome-Qm8eaztx.js",
41
+ "src/features/command/CommandPalette.tsx": "/static/app/assets/CommandPalette-Bqhm0Urn.js",
42
+ "src/pages/Act.tsx": "/static/app/assets/Act-DcQizkl1.js",
43
+ "src/pages/Brain.tsx": "/static/app/assets/Brain-3VCSHFcn.js",
44
+ "src/pages/Capture.tsx": "/static/app/assets/Capture-DiQ219jW.js",
45
+ "src/pages/Chronicle.tsx": "/static/app/assets/Chronicle-BGvuAchH.js",
46
+ "src/pages/Library.tsx": "/static/app/assets/Library-BV6NnF0a.js",
47
+ "src/pages/System.tsx": "/static/app/assets/System-CieofHQa.js"
48
48
  }
49
49
  }