ltcai 11.2.0 → 11.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 (247) hide show
  1. package/README.md +46 -53
  2. package/docs/CHANGELOG.md +61 -0
  3. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  4. package/docs/DEVELOPMENT.md +1 -1
  5. package/docs/MULTI_AGENT_RUNTIME.md +1 -1
  6. package/docs/ONBOARDING.md +1 -1
  7. package/docs/OPERATIONS.md +6 -2
  8. package/docs/PERMISSION_MODE.md +1 -1
  9. package/docs/TRUST_MODEL.md +1 -1
  10. package/docs/WHY_LATTICE.md +1 -1
  11. package/docs/kg-schema.md +2 -2
  12. package/docs/v11.3.0_PLAN.md +202 -0
  13. package/docs/v11.4.0_RUST_FOUNDATION_PLAN.md +176 -0
  14. package/lattice_brain/__init__.py +1 -1
  15. package/lattice_brain/graph/_kg_common/__init__.py +287 -0
  16. package/lattice_brain/graph/_kg_common/extraction.py +516 -0
  17. package/lattice_brain/graph/_kg_common/relations.py +161 -0
  18. package/lattice_brain/graph/_kg_common/text.py +479 -0
  19. package/lattice_brain/graph/discovery_index/__init__.py +35 -0
  20. package/lattice_brain/graph/discovery_index/cleanup.py +182 -0
  21. package/lattice_brain/graph/discovery_index/extract.py +137 -0
  22. package/lattice_brain/graph/discovery_index/scan.py +411 -0
  23. package/lattice_brain/graph/discovery_index/upsert.py +495 -0
  24. package/lattice_brain/graph/projection/__init__.py +42 -0
  25. package/lattice_brain/graph/projection/curation.py +500 -0
  26. package/lattice_brain/graph/{projection.py → projection/v2_schema.py} +15 -477
  27. package/lattice_brain/graph/retrieval/__init__.py +54 -0
  28. package/lattice_brain/graph/retrieval/context.py +197 -0
  29. package/lattice_brain/graph/retrieval/graph_view.py +319 -0
  30. package/lattice_brain/graph/retrieval/hybrid.py +488 -0
  31. package/lattice_brain/graph/retrieval/maintenance.py +121 -0
  32. package/lattice_brain/graph/retrieval/signals.py +95 -0
  33. package/lattice_brain/graph/retrieval_vector/__init__.py +42 -0
  34. package/lattice_brain/graph/retrieval_vector/fingerprint.py +97 -0
  35. package/lattice_brain/graph/retrieval_vector/indexing.py +347 -0
  36. package/lattice_brain/graph/retrieval_vector/search.py +560 -0
  37. package/lattice_brain/graph/retrieval_vector/status.py +374 -0
  38. package/lattice_brain/ingestion/__init__.py +130 -0
  39. package/lattice_brain/ingestion/_contract.py +90 -0
  40. package/lattice_brain/ingestion/constants.py +127 -0
  41. package/lattice_brain/ingestion/folder_scan.py +57 -0
  42. package/lattice_brain/ingestion/folders.py +258 -0
  43. package/lattice_brain/ingestion/hashing.py +26 -0
  44. package/lattice_brain/ingestion/jobs_api.py +107 -0
  45. package/lattice_brain/ingestion/models.py +80 -0
  46. package/lattice_brain/ingestion/pipeline.py +486 -0
  47. package/lattice_brain/ingestion/quality.py +209 -0
  48. package/lattice_brain/ingestion/routing.py +295 -0
  49. package/lattice_brain/multimodal/__init__.py +164 -0
  50. package/lattice_brain/multimodal/audio.py +77 -0
  51. package/lattice_brain/multimodal/common.py +118 -0
  52. package/lattice_brain/multimodal/images.py +498 -0
  53. package/lattice_brain/multimodal/ports.py +169 -0
  54. package/lattice_brain/multimodal/video.py +410 -0
  55. package/lattice_brain/portability/__init__.py +90 -0
  56. package/lattice_brain/portability/_contract.py +42 -0
  57. package/lattice_brain/portability/backups.py +338 -0
  58. package/lattice_brain/portability/bundles.py +136 -0
  59. package/lattice_brain/portability/constants.py +93 -0
  60. package/lattice_brain/portability/fsops.py +138 -0
  61. package/lattice_brain/portability/service.py +41 -0
  62. package/lattice_brain/{portability.py → portability/sharing.py} +44 -677
  63. package/lattice_brain/runtime/__init__.py +1 -1
  64. package/lattice_brain/runtime/multi_agent.py +1 -1
  65. package/latticeai/__init__.py +1 -1
  66. package/latticeai/api/chronicle.py +63 -0
  67. package/latticeai/core/agent/__init__.py +93 -0
  68. package/latticeai/core/agent/_contract.py +79 -0
  69. package/latticeai/core/agent/context.py +57 -0
  70. package/latticeai/core/agent/deps.py +125 -0
  71. package/latticeai/core/agent/execution.py +622 -0
  72. package/latticeai/core/agent/planning.py +145 -0
  73. package/latticeai/core/agent/recovery.py +157 -0
  74. package/latticeai/core/agent/runtime.py +210 -0
  75. package/latticeai/core/agent/verification.py +231 -0
  76. package/latticeai/core/embedding_providers/__init__.py +151 -0
  77. package/latticeai/core/embedding_providers/base.py +199 -0
  78. package/latticeai/core/embedding_providers/captions.py +162 -0
  79. package/latticeai/core/embedding_providers/profiles.py +126 -0
  80. package/latticeai/core/embedding_providers/text.py +350 -0
  81. package/latticeai/core/embedding_providers/vision.py +352 -0
  82. package/latticeai/core/file_generation/__init__.py +115 -0
  83. package/latticeai/core/file_generation/bundles.py +76 -0
  84. package/latticeai/core/file_generation/extraction.py +154 -0
  85. package/latticeai/core/file_generation/inference.py +235 -0
  86. package/latticeai/core/file_generation/orchestration.py +152 -0
  87. package/latticeai/core/file_generation/prompting.py +117 -0
  88. package/latticeai/core/file_generation/repair.py +114 -0
  89. package/latticeai/core/file_generation/sanitize.py +61 -0
  90. package/latticeai/core/file_generation/validation.py +201 -0
  91. package/latticeai/core/legacy_compatibility.py +1 -1
  92. package/latticeai/core/marketplace.py +1 -1
  93. package/latticeai/core/messages.py +9 -0
  94. package/latticeai/core/workspace_os_constants.py +1 -1
  95. package/latticeai/integrations/telegram_bot/__init__.py +123 -0
  96. package/latticeai/integrations/telegram_bot/__main__.py +17 -0
  97. package/latticeai/integrations/telegram_bot/config.py +86 -0
  98. package/latticeai/integrations/telegram_bot/dispatch.py +311 -0
  99. package/latticeai/integrations/telegram_bot/flows.py +478 -0
  100. package/latticeai/integrations/telegram_bot/helpers.py +322 -0
  101. package/latticeai/integrations/telegram_bot/screens.py +394 -0
  102. package/latticeai/models/router/__init__.py +88 -0
  103. package/latticeai/models/router/_contract.py +66 -0
  104. package/latticeai/models/router/branding.py +56 -0
  105. package/latticeai/models/router/catalog.py +69 -0
  106. package/latticeai/models/router/documents.py +199 -0
  107. package/latticeai/models/router/errors.py +37 -0
  108. package/latticeai/models/router/generation.py +258 -0
  109. package/latticeai/models/router/loading.py +291 -0
  110. package/latticeai/models/router/local_models.py +85 -0
  111. package/latticeai/models/router/registry.py +147 -0
  112. package/latticeai/runtime/build_phases/__init__.py +82 -0
  113. package/latticeai/runtime/build_phases/features.py +407 -0
  114. package/latticeai/runtime/build_phases/foundation.py +555 -0
  115. package/latticeai/runtime/build_phases/web.py +492 -0
  116. package/latticeai/runtime/runtime_context.py +1 -0
  117. package/latticeai/services/architecture_readiness.py +48 -19
  118. package/latticeai/services/brain_intelligence/__init__.py +58 -0
  119. package/latticeai/services/brain_intelligence/_contract.py +71 -0
  120. package/latticeai/services/brain_intelligence/consistency.py +193 -0
  121. package/latticeai/services/brain_intelligence/constants.py +47 -0
  122. package/latticeai/services/brain_intelligence/digest.py +258 -0
  123. package/latticeai/services/brain_intelligence/health.py +331 -0
  124. package/latticeai/services/brain_intelligence/proposals.py +264 -0
  125. package/latticeai/services/brain_intelligence/sampling.py +84 -0
  126. package/latticeai/services/brain_intelligence/service.py +48 -0
  127. package/latticeai/services/chronicle.py +557 -0
  128. package/latticeai/services/memory_service/__init__.py +52 -0
  129. package/latticeai/services/memory_service/_contract.py +100 -0
  130. package/latticeai/services/memory_service/brief.py +431 -0
  131. package/latticeai/services/memory_service/constants.py +57 -0
  132. package/latticeai/services/memory_service/maintenance.py +138 -0
  133. package/latticeai/services/memory_service/manager.py +186 -0
  134. package/latticeai/services/memory_service/proof.py +136 -0
  135. package/latticeai/services/memory_service/recall.py +225 -0
  136. package/latticeai/services/memory_service/service.py +48 -0
  137. package/latticeai/services/memory_service/stores.py +110 -0
  138. package/latticeai/services/model_runtime/__init__.py +322 -0
  139. package/latticeai/services/model_runtime/cloud.py +87 -0
  140. package/latticeai/services/model_runtime/download.py +282 -0
  141. package/latticeai/services/model_runtime/engines.py +341 -0
  142. package/latticeai/services/model_runtime/loading.py +178 -0
  143. package/latticeai/services/model_runtime/service.py +129 -0
  144. package/latticeai/services/model_runtime/state.py +131 -0
  145. package/latticeai/services/model_runtime/status.py +255 -0
  146. package/latticeai/services/product_readiness.py +15 -7
  147. package/latticeai/setup/wizard/__init__.py +126 -0
  148. package/latticeai/setup/wizard/catalog.py +172 -0
  149. package/latticeai/setup/wizard/detect.py +323 -0
  150. package/latticeai/setup/wizard/install.py +348 -0
  151. package/latticeai/setup/wizard/paths.py +168 -0
  152. package/latticeai/setup/wizard/plans.py +74 -0
  153. package/latticeai/setup/wizard/recommend.py +320 -0
  154. package/package.json +6 -2
  155. package/scripts/bump_version.py +14 -0
  156. package/scripts/capture_release_evidence.mjs +33 -21
  157. package/scripts/check_current_release_docs.mjs +1 -1
  158. package/scripts/check_i18n_namespace_coverage.mjs +41 -4
  159. package/scripts/check_max_file_lines.mjs +102 -0
  160. package/scripts/check_release_evidence_bound.mjs +30 -15
  161. package/scripts/check_screenshot_pixel_delta.py +34 -4
  162. package/scripts/check_server_i18n.mjs +1 -0
  163. package/scripts/generate_rust_parity_fixtures.py +562 -0
  164. package/scripts/lib/mock_server_fingerprint.mjs +94 -0
  165. package/scripts/release_screen_claims.json +31 -2
  166. package/src-tauri/Cargo.lock +361 -3
  167. package/src-tauri/Cargo.toml +6 -1
  168. package/src-tauri/src/backend.rs +349 -0
  169. package/src-tauri/src/folder.rs +33 -0
  170. package/src-tauri/src/main.rs +97 -399
  171. package/src-tauri/tauri.conf.json +1 -1
  172. package/static/app/asset-manifest.json +41 -37
  173. package/static/app/assets/Act-yYpYnn0v.js +1 -0
  174. package/static/app/assets/AdminConsole-DL3Cr5pL.js +1 -0
  175. package/static/app/assets/{Brain-tuhI4sOC.js → Brain-C1HBN0Wf.js} +2 -2
  176. package/static/app/assets/BrainHome-DoXRhUUC.js +2 -0
  177. package/static/app/assets/BrainSignals-6yR6ir5t.js +1 -0
  178. package/static/app/assets/Capture-CFIRsFNE.js +1 -0
  179. package/static/app/assets/Chronicle-BZbEgiwN.js +1 -0
  180. package/static/app/assets/CommandPalette-D2pMxC2I.js +1 -0
  181. package/static/app/assets/Library-DwO3yZST.js +1 -0
  182. package/static/app/assets/{LivingBrain-DBwhto14.js → LivingBrain-Jn1GK0-S.js} +1 -1
  183. package/static/app/assets/ProductFlow-B-w1R4Oo.js +1 -0
  184. package/static/app/assets/ReviewCard-6B27X8Vg.js +3 -0
  185. package/static/app/assets/System-DW8F-2xL.js +1 -0
  186. package/static/app/assets/arrow-left-DXvKg9U6.js +1 -0
  187. package/static/app/assets/{bot-Cia42c2h.js → bot-IM_E_Y12.js} +1 -1
  188. package/static/app/assets/brain-Ci1CkWjM.js +1 -0
  189. package/static/app/assets/{button-2j2Ijzgq.js → button-COwyqfHM.js} +1 -1
  190. package/static/app/assets/circle-check-DfInj-qD.js +1 -0
  191. package/static/app/assets/{circle-pause-BEFeWpVW.js → circle-pause-DEM4A1Y5.js} +1 -1
  192. package/static/app/assets/{circle-play-ujXMcHxl.js → circle-play-C9djDuLd.js} +1 -1
  193. package/static/app/assets/{cpu-k4awryFq.js → cpu-DFdo1gw-.js} +1 -1
  194. package/static/app/assets/{download-DFbLJ_ig.js → download-SnJL6oqk.js} +1 -1
  195. package/static/app/assets/{folder-open-7y_b6xkM.js → folder-open-CqZeDkjE.js} +1 -1
  196. package/static/app/assets/{hard-drive-Bidh02Kr.js → hard-drive-j1jJXYYf.js} +1 -1
  197. package/static/app/assets/{index-DwDl9-8Y.css → index-BLPb5lmE.css} +1 -1
  198. package/static/app/assets/index-_u5iUHDr.js +10 -0
  199. package/static/app/assets/input-B0lPdRQZ.js +1 -0
  200. package/static/app/assets/link-2-CoFbooHS.js +1 -0
  201. package/static/app/assets/{permissionCopy-Bpb83Hx9.js → permissionCopy-BsyLxtao.js} +1 -1
  202. package/static/app/assets/primitives-DEbN-d6p.js +1 -0
  203. package/static/app/assets/search-BybIWPNd.js +1 -0
  204. package/static/app/assets/{share-2-BH1M-WNi.js → share-2-CVtZ_ewX.js} +1 -1
  205. package/static/app/assets/{shield-alert-BlKdBXcG.js → shield-alert-CBi2GNWM.js} +1 -1
  206. package/static/app/assets/{textarea-CCWbUfFB.js → textarea-DNMpB5ih.js} +1 -1
  207. package/static/app/assets/{useFocusTrap-YdHQ7pJ1.js → useFocusTrap-C83t3GXF.js} +1 -1
  208. package/static/app/assets/useMutation-DtbJDoyz.js +1 -0
  209. package/static/app/assets/{useQuery-CXQiwbVT.js → useQuery-Dcp1OChy.js} +1 -1
  210. package/static/app/assets/utils-BlZr7Pd4.js +4 -0
  211. package/static/app/assets/workspace-jJY4RuAV.js +1 -0
  212. package/static/app/index.html +4 -4
  213. package/static/sw.js +1 -1
  214. package/lattice_brain/graph/_kg_common.py +0 -1331
  215. package/lattice_brain/graph/discovery_index.py +0 -1141
  216. package/lattice_brain/graph/retrieval.py +0 -1120
  217. package/lattice_brain/graph/retrieval_vector.py +0 -1293
  218. package/lattice_brain/ingestion.py +0 -1525
  219. package/lattice_brain/multimodal.py +0 -1258
  220. package/latticeai/core/agent.py +0 -1465
  221. package/latticeai/core/embedding_providers.py +0 -1196
  222. package/latticeai/core/file_generation.py +0 -1047
  223. package/latticeai/integrations/telegram_bot.py +0 -1390
  224. package/latticeai/models/router.py +0 -1007
  225. package/latticeai/runtime/build_phases.py +0 -1450
  226. package/latticeai/services/brain_intelligence.py +0 -1083
  227. package/latticeai/services/memory_service.py +0 -1177
  228. package/latticeai/services/model_runtime.py +0 -1281
  229. package/latticeai/setup/wizard.py +0 -1310
  230. package/static/app/assets/Act-AWf0SAKp.js +0 -1
  231. package/static/app/assets/AdminConsole-D0u8Tiyj.js +0 -1
  232. package/static/app/assets/BrainHome-Ts7G_Ila.js +0 -2
  233. package/static/app/assets/BrainSignals-jMYgQ2Ar.js +0 -1
  234. package/static/app/assets/Capture-CqOSzyPr.js +0 -1
  235. package/static/app/assets/CommandPalette-DC0Bzh-I.js +0 -1
  236. package/static/app/assets/Library-CX-bbhmK.js +0 -1
  237. package/static/app/assets/ProductFlow-BHA2cfKI.js +0 -1
  238. package/static/app/assets/ReviewCard-BUhCKRNM.js +0 -3
  239. package/static/app/assets/System-Bu2t5hn1.js +0 -1
  240. package/static/app/assets/arrow-left-Dzwa5zRb.js +0 -1
  241. package/static/app/assets/brain-DJMoqrwx.js +0 -1
  242. package/static/app/assets/index-BpYkzcVm.js +0 -10
  243. package/static/app/assets/input-DSlJJxRs.js +0 -1
  244. package/static/app/assets/primitives-BCx6TvfG.js +0 -1
  245. package/static/app/assets/search-Cgy8cCFJ.js +0 -1
  246. package/static/app/assets/utils-zqPZJxdx.js +0 -4
  247. package/static/app/assets/workspace-DXTihhfU.js +0 -1
@@ -0,0 +1,114 @@
1
+ """Deterministic scaffolds — step 5, the last resort after the retries.
2
+
3
+ The user asked for a file, so the request must still end in a well-formed file
4
+ of the target type, never an error. Nothing here calls a model: whatever the
5
+ model did produce is salvaged where it can be, and honestly labelled where it
6
+ cannot.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import ast
12
+ import html as html_lib
13
+ import json
14
+ import re
15
+
16
+ from .extraction import _ext, _slice_html_document, _slice_json_document
17
+ from .validation import looks_like_refusal
18
+
19
+
20
+ def repair_file_content(content: str, target_path: str, user_request: str) -> str:
21
+ """Turn whatever the model produced into a valid file of the target type.
22
+
23
+ This is the last resort after retries: the user asked for a file, so the
24
+ request must still end in a well-formed file, never an error.
25
+ """
26
+ ext = _ext(target_path)
27
+ salvage = content.strip()
28
+ if looks_like_refusal(salvage):
29
+ salvage = ""
30
+
31
+ if ext in (".html", ".htm"):
32
+ return _repair_html(salvage, user_request)
33
+ if ext == ".json":
34
+ sliced = _slice_json_document(salvage)
35
+ if sliced is not None:
36
+ return sliced
37
+ return json.dumps(
38
+ {"request": user_request, "content": salvage},
39
+ ensure_ascii=False,
40
+ indent=2,
41
+ )
42
+ if ext == ".py" and salvage:
43
+ # The repair guarantee for Python is parseability: unparseable output
44
+ # is preserved honestly as a commented-out draft, never as a broken
45
+ # module the user has to debug.
46
+ try:
47
+ ast.parse(salvage)
48
+ return salvage
49
+ except SyntaxError:
50
+ commented = "\n".join(f"# {line}" for line in salvage.splitlines())
51
+ return (
52
+ f"# TODO: model produced invalid Python for: {user_request}\n"
53
+ "# The draft below is preserved as comments — fix and uncomment.\n"
54
+ f"{commented}\n"
55
+ )
56
+ if salvage:
57
+ return salvage
58
+ # Nothing usable at all — leave an honest placeholder in the right format.
59
+ comment = {
60
+ ".py": "# TODO: model produced no usable content for: ",
61
+ ".js": "// TODO: model produced no usable content for: ",
62
+ ".jsx": "// TODO: model produced no usable content for: ",
63
+ ".ts": "// TODO: model produced no usable content for: ",
64
+ ".tsx": "// TODO: model produced no usable content for: ",
65
+ ".css": "/* TODO: model produced no usable content for: ",
66
+ ".sh": "# TODO: model produced no usable content for: ",
67
+ ".sql": "-- TODO: model produced no usable content for: ",
68
+ }.get(ext, "")
69
+ if ext == ".css":
70
+ return f"{comment}{user_request} */\n"
71
+ if comment:
72
+ return f"{comment}{user_request}\n"
73
+ return f"{user_request}\n"
74
+
75
+
76
+ def _repair_html(salvage: str, user_request: str) -> str:
77
+ lower = salvage.lower()
78
+ if "<html" in lower or "<!doctype" in lower:
79
+ # A real document that is merely truncated — close it.
80
+ doc = _slice_html_document(salvage)
81
+ low = doc.lower()
82
+ if "</body>" not in low and "<body" in low:
83
+ doc += "\n</body>"
84
+ if "</html>" not in low:
85
+ doc += "\n</html>"
86
+ return doc
87
+ if re.search(r"<\w+[^>]*>", salvage):
88
+ body = salvage # an HTML fragment — embed as-is
89
+ elif salvage:
90
+ body = "\n".join(
91
+ f" <p>{html_lib.escape(line)}</p>"
92
+ for line in salvage.splitlines()
93
+ if line.strip()
94
+ )
95
+ else:
96
+ body = f" <p>{html_lib.escape(user_request)}</p>"
97
+ title = html_lib.escape(user_request[:60] or "Generated page")
98
+ return (
99
+ "<!DOCTYPE html>\n"
100
+ "<html lang=\"ko\">\n"
101
+ "<head>\n"
102
+ " <meta charset=\"utf-8\">\n"
103
+ " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n"
104
+ f" <title>{title}</title>\n"
105
+ " <style>\n"
106
+ " body { font-family: system-ui, sans-serif; margin: 2rem auto; "
107
+ "max-width: 720px; line-height: 1.6; padding: 0 1rem; }\n"
108
+ " </style>\n"
109
+ "</head>\n"
110
+ "<body>\n"
111
+ f"{body}\n"
112
+ "</body>\n"
113
+ "</html>"
114
+ )
@@ -0,0 +1,61 @@
1
+ """The single write-side guarantee for model-produced file content.
2
+
3
+ ``sanitize_write_content`` is the one door every write entry point goes
4
+ through — the direct chat path, the agent JSON loop, the tools layer — so a
5
+ fenced or chatty payload can never be persisted verbatim no matter which
6
+ surface produced it.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any, Dict, Tuple
12
+
13
+ from .extraction import extract_file_content
14
+ from .repair import repair_file_content
15
+ from .validation import validate_file_content
16
+
17
+ # Extensions the Brain UI can render inline (preview) after creation.
18
+ PREVIEWABLE_EXTENSIONS = frozenset({
19
+ ".html", ".htm", ".md", ".markdown", ".txt", ".json", ".css", ".js",
20
+ ".csv", ".py", ".yaml", ".yml", ".xml", ".sql", ".sh",
21
+ ".jsx", ".ts", ".tsx", ".vue", ".svelte",
22
+ })
23
+
24
+
25
+ def sanitize_write_content(
26
+ target_path: str,
27
+ content: Any,
28
+ user_request: str = "",
29
+ ) -> Tuple[str, Dict[str, Any]]:
30
+ """Single write-side guarantee for model-produced file content.
31
+
32
+ The direct chat path already runs the full generate→validate→repair
33
+ pipeline, but the agent JSON loop historically wrote ``args.content``
34
+ verbatim — weak models routinely put fenced/chatty payloads there. This
35
+ conservative sanitizer closes that gap for *any* write entry point:
36
+
37
+ 1. content that already validates is returned byte-for-byte unchanged
38
+ (trusted/user-authored content is never mangled);
39
+ 2. otherwise the extraction pass strips fences/think-blocks/chat noise
40
+ and is used only when the extracted payload validates;
41
+ 3. otherwise deterministic repair guarantees a structurally valid file.
42
+
43
+ Empty content is left untouched (creating an empty file is a legitimate,
44
+ intentional action — e.g. ``__init__.py``). Returns ``(content, meta)``
45
+ where meta is ``{"sanitized": bool, "repaired": bool, "reason": str}``.
46
+ """
47
+ raw = str(content or "")
48
+ if not raw.strip():
49
+ return raw, {"sanitized": False, "repaired": False, "reason": "empty"}
50
+ ok, reason = validate_file_content(raw, target_path)
51
+ if ok:
52
+ return raw, {"sanitized": False, "repaired": False, "reason": "ok"}
53
+ extracted = extract_file_content(raw, target_path)
54
+ if extracted:
55
+ extracted_ok, _ = validate_file_content(extracted, target_path)
56
+ if extracted_ok:
57
+ return extracted, {"sanitized": True, "repaired": False, "reason": reason}
58
+ repaired = repair_file_content(
59
+ extracted or raw, target_path, user_request or f"content for {target_path}"
60
+ )
61
+ return repaired, {"sanitized": True, "repaired": True, "reason": reason}
@@ -0,0 +1,201 @@
1
+ """Per-extension structural checks — step 3 of the pipeline.
2
+
3
+ Answers one question about an already-extracted payload: *is this a file of the
4
+ requested type?* HTML document shape, JSON parses, CSS has rule blocks, Python
5
+ parses, braced code is balanced, components close their blocks — and, for prose
6
+ types with no grammar to check, that the reply is not merely talk about the
7
+ file.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import ast
13
+ import json
14
+ import re
15
+ from typing import List, Tuple
16
+
17
+ from .extraction import _ext
18
+
19
+ _REFUSAL_RE = re.compile(
20
+ r"(i can('|no)?t|i'?m (sorry|unable)|as an ai|cannot assist"
21
+ r"|죄송(하지만|합니다)|할 수 없|불가능합니다|도와드릴 수 없)",
22
+ re.IGNORECASE,
23
+ )
24
+
25
+
26
+ def looks_like_refusal(content: str) -> bool:
27
+ head = content[:300]
28
+ return bool(_REFUSAL_RE.search(head)) and len(content) < 600
29
+
30
+
31
+ # Braced code types validated structurally (balanced delimiters, no fences).
32
+ _BRACED_CODE_EXTENSIONS = frozenset({".js", ".jsx", ".ts", ".tsx"})
33
+ # Single-file components validated by their block tags being closed.
34
+ _COMPONENT_EXTENSIONS = frozenset({".vue", ".svelte"})
35
+
36
+
37
+ def _strip_code_literals(text: str) -> str:
38
+ """Remove string literals and comments so delimiter counting stays honest.
39
+
40
+ A cheap single-pass scanner (not a parser): quotes ('', "", ``), line
41
+ comments (//) and block comments (/* */) commonly contain lone braces
42
+ that would otherwise false-flag valid JS/TS as unbalanced.
43
+ """
44
+ out: List[str] = []
45
+ i, n = 0, len(text)
46
+ while i < n:
47
+ ch = text[i]
48
+ nxt = text[i + 1] if i + 1 < n else ""
49
+ if ch == "\\":
50
+ i += 2 # escaped char (inside or outside a literal — always skip)
51
+ continue
52
+ if ch in ("'", '"', "`"):
53
+ quote = ch
54
+ i += 1
55
+ while i < n:
56
+ if text[i] == "\\":
57
+ i += 2
58
+ continue
59
+ if text[i] == quote:
60
+ i += 1
61
+ break
62
+ i += 1
63
+ continue
64
+ if ch == "/" and nxt == "/":
65
+ while i < n and text[i] != "\n":
66
+ i += 1
67
+ continue
68
+ if ch == "/" and nxt == "*":
69
+ end = text.find("*/", i + 2)
70
+ i = n if end < 0 else end + 2
71
+ continue
72
+ out.append(ch)
73
+ i += 1
74
+ return "".join(out)
75
+
76
+
77
+ def _check_balanced_delimiters(content: str) -> Tuple[bool, str]:
78
+ """Lenient count-based balance check for braced code (js/ts family).
79
+
80
+ Only *counts* are compared, never ordering, so valid-but-unusual code is
81
+ not rejected; a truncated file with a dangling ``{`` still fails.
82
+ """
83
+ stripped = _strip_code_literals(content)
84
+ for opener, closer, label in (("{", "}", "braces"), ("(", ")", "parentheses"), ("[", "]", "brackets")):
85
+ if stripped.count(opener) != stripped.count(closer):
86
+ return False, f"unbalanced {label} ({opener}{closer}) — the file looks truncated"
87
+ return True, "ok"
88
+
89
+
90
+ def _check_component_blocks(content: str) -> Tuple[bool, str]:
91
+ """Vue/Svelte SFC sanity: every opened block tag must be closed."""
92
+ lower = content.lower()
93
+ for tag in ("template", "script", "style"):
94
+ opened = len(re.findall(rf"<{tag}(?:\s[^>]*)?>", lower))
95
+ closed = lower.count(f"</{tag}>")
96
+ if opened != closed:
97
+ return False, f"<{tag}> block is not closed — the component looks truncated"
98
+ return True, "ok"
99
+
100
+
101
+ def validate_file_content(content: str, target_path: str) -> Tuple[bool, str]:
102
+ """Structural sanity check per file type. Returns (ok, reason)."""
103
+ if not content.strip():
104
+ return False, "empty output"
105
+ if looks_like_refusal(content):
106
+ return False, "the reply was a refusal/chat message, not file content"
107
+
108
+ ext = _ext(target_path)
109
+ if ext in (".html", ".htm"):
110
+ lower = content.lower()
111
+ if "<html" not in lower and "<!doctype" not in lower:
112
+ return False, "not a complete HTML document (missing <!DOCTYPE html>/<html>)"
113
+ if "</html>" not in lower:
114
+ return False, "HTML document is truncated (missing </html>)"
115
+ # A document that merely *contains* html somewhere is not a valid
116
+ # file payload: fenced/chat-wrapped replies must fail here so the
117
+ # extraction pass gets a chance to slice out the real document.
118
+ stripped = content.lstrip()
119
+ if not (stripped.lower().startswith("<!doctype") or stripped.lower().startswith("<html")):
120
+ return False, "HTML document is wrapped in prose or fences"
121
+ if "```" in content:
122
+ return False, "output still contains Markdown fences"
123
+ return True, "ok"
124
+ if ext == ".json":
125
+ try:
126
+ json.loads(content)
127
+ except (ValueError, TypeError) as exc:
128
+ return False, f"invalid JSON: {exc}"
129
+ return True, "ok"
130
+ if ext == ".css":
131
+ if "```" in content:
132
+ return False, "output still contains Markdown fences"
133
+ if "{" not in content or "}" not in content:
134
+ return False, "no CSS rule blocks found"
135
+ return True, "ok"
136
+ if ext == ".py":
137
+ if "```" in content:
138
+ return False, "output still contains Markdown fences"
139
+ try:
140
+ ast.parse(content)
141
+ except SyntaxError as exc:
142
+ return False, f"invalid Python syntax: {exc.msg} (line {exc.lineno})"
143
+ return True, "ok"
144
+ if ext in _BRACED_CODE_EXTENSIONS:
145
+ if "```" in content:
146
+ return False, "output still contains Markdown fences"
147
+ return _check_balanced_delimiters(content)
148
+ if ext in _COMPONENT_EXTENSIONS:
149
+ if "```" in content:
150
+ return False, "output still contains Markdown fences"
151
+ return _check_component_blocks(content)
152
+ if ext in (".sh", ".sql"):
153
+ if "```" in content:
154
+ return False, "output still contains Markdown fences"
155
+ return True, "ok"
156
+ # Prose types (.md, .txt, .csv, …) have no grammar to check, which used to
157
+ # mean *nothing* was checked: a 1–4B model that answered "Sure! Here is the
158
+ # document you asked for:" and stopped had its sentence saved as the file,
159
+ # because the fence stripper only removes conversational lines it can
160
+ # recognise and the length guard on `looks_like_refusal` lets a wordy
161
+ # refusal through. The two checks below are the only ones that generalise
162
+ # without inventing a grammar: it must not still be wearing fences, and it
163
+ # must not be *only* an answer about the file.
164
+ if "```" in content:
165
+ return False, "output still contains Markdown fences"
166
+ if _looks_like_commentary(content):
167
+ return False, "the reply talks about the file instead of being the file"
168
+ return True, "ok"
169
+
170
+
171
+ # Openers that mean "I am about to give you the thing" — if the whole reply is
172
+ # one of these, the thing never arrived.
173
+ _COMMENTARY_RE = re.compile(
174
+ r"^\s*("
175
+ r"(sure|of course|certainly|okay|ok|alright|here|below|the following)\b"
176
+ r"|i('ve| have| will|'ll)\b"
177
+ r"|(물론|네[,!. ]|알겠|다음은|아래(는|의)?|요청하신|원하시는)"
178
+ r")",
179
+ re.IGNORECASE,
180
+ )
181
+
182
+
183
+ def _looks_like_commentary(content: str) -> bool:
184
+ """True when the reply reads as an answer *about* a file, not the file.
185
+
186
+ Deliberately conservative — a long document that merely opens with "The
187
+ following" is a document. Only a short reply that both opens
188
+ conversationally and never grows into content is rejected, so a real file
189
+ is never thrown away to catch a chat line.
190
+ """
191
+ stripped = content.strip()
192
+ if len(stripped) > 400:
193
+ return False
194
+ if not _COMMENTARY_RE.match(stripped):
195
+ return False
196
+ # Structure means content arrived after the preamble: a heading, a list, a
197
+ # table row, a delimiter, or simply several lines of body text.
198
+ body = stripped.split("\n", 1)[1].strip() if "\n" in stripped else ""
199
+ if re.search(r"^\s*(#{1,6}\s|[-*+]\s|\d+[.)]\s|\||>)", body, re.MULTILINE):
200
+ return False
201
+ return len(body) < 120
@@ -13,7 +13,7 @@ from dataclasses import dataclass
13
13
  from pathlib import Path
14
14
  from typing import Any, Dict, List
15
15
 
16
- LEGACY_COMPATIBILITY_VERSION = "11.2.0"
16
+ LEGACY_COMPATIBILITY_VERSION = "11.4.0"
17
17
 
18
18
 
19
19
  @dataclass(frozen=True)
@@ -10,7 +10,7 @@ from __future__ import annotations
10
10
  from copy import deepcopy
11
11
  from typing import Any, Dict, List, Optional
12
12
 
13
- MARKETPLACE_VERSION = "11.2.0"
13
+ MARKETPLACE_VERSION = "11.4.0"
14
14
  TEMPLATE_KINDS = ("plugin", "workflow", "agent", "ingestion_bridge")
15
15
 
16
16
 
@@ -522,6 +522,15 @@ MESSAGES: Dict[str, Dict[str, str]] = {
522
522
  "ko": "디렉터리를 찾을 수 없습니다.",
523
523
  "en": "That directory was not found.",
524
524
  },
525
+ # ── chronicle ───────────────────────────────────────────────────────
526
+ "chronicle.bad_date": {
527
+ "ko": "날짜를 읽을 수 없습니다. 2026-08-11 처럼 연-월-일로 적어 주세요.",
528
+ "en": "That date could not be read. Write it as year-month-day, like 2026-08-11.",
529
+ },
530
+ "chronicle.bad_timestamp": {
531
+ "ko": "시점을 읽을 수 없습니다. 2026-08-11T09:00:00 처럼 적어 주세요.",
532
+ "en": "That moment could not be read. Write it like 2026-08-11T09:00:00.",
533
+ },
525
534
  # ── MCP / setup ─────────────────────────────────────────────────────
526
535
  "mcp.connector_not_found": {
527
536
  "ko": "커넥터를 찾을 수 없습니다.",
@@ -10,7 +10,7 @@ from __future__ import annotations
10
10
 
11
11
  from typing import Dict
12
12
 
13
- WORKSPACE_OS_VERSION = "11.2.0"
13
+ WORKSPACE_OS_VERSION = "11.4.0"
14
14
 
15
15
  # Workspace types separate single-user Personal workspaces from shared
16
16
  # Organization workspaces. Both keep the same local-first JSON store; the type
@@ -0,0 +1,123 @@
1
+ """Telegram bridge — the phone-side surface of a local Lattice AI.
2
+
3
+ The bot mirrors the web app's capabilities onto a chat: server status, models,
4
+ Knowledge Graph counts, screenshots, history, document capture, the Review
5
+ Center, and the full human-in-the-loop agent handshake. It is fail-closed by
6
+ construction — no bot token, no chat allowlist, or no explicitly provisioned
7
+ server capability and the loop refuses to start.
8
+
9
+ Split into cohesive submodules in v11.3.0 (no behaviour change): ``config``
10
+ (environment, endpoints, logger and the authenticated server client),
11
+ ``helpers`` (chat-id registry, Telegram API, network, agent artifacts),
12
+ ``screens`` (the menu commands that report on the running system), ``flows``
13
+ (ask → render → approve, including the pending-plan state), and ``dispatch``
14
+ (command/callback routing and the poll loop). ``__main__`` keeps
15
+ ``python -m latticeai.integrations.telegram_bot`` working. This module
16
+ re-exports every name the single file exposed, so
17
+ ``latticeai.integrations.telegram_bot.X`` keeps working.
18
+
19
+ Stubbing note: rebinding one of these *here* changes only this module's name.
20
+ The submodule that calls it holds its own reference, so a test standing in for
21
+ ``CHAT_IDS_FILE``/``TOKEN``/``AGENT_WORKSPACE`` patches
22
+ ``…telegram_bot.helpers``, for ``_server_client``/``_bot_pending_plans``
23
+ ``…telegram_bot.flows`` (or ``…screens`` for a screen), and for ``get_updates``
24
+ ``…telegram_bot.dispatch``.
25
+ """
26
+
27
+ # The single file had no ``__all__``, so its public surface was "every module
28
+ # global" — including the names it imported for its own use. Every re-export
29
+ # below therefore uses the redundant-alias form: it reproduces exactly that
30
+ # surface, and it marks each name as deliberate rather than a leftover import.
31
+ from latticeai.cli.runtime import _load_env_file as _load_env_file
32
+ from latticeai.core.io_utils import atomic_write_json as atomic_write_json
33
+ from latticeai.core.logging_safety import (
34
+ install_sensitive_log_filter as install_sensitive_log_filter,
35
+ )
36
+ from latticeai.core.logging_safety import safe_log_text as safe_log_text
37
+ from latticeai.core.quiet import quiet as quiet
38
+
39
+ from .config import AGENT_RESUME_URL as AGENT_RESUME_URL
40
+ from .config import AGENT_URL as AGENT_URL
41
+ from .config import AGENT_WORKSPACE as AGENT_WORKSPACE
42
+ from .config import API_URL as API_URL
43
+ from .config import BASE_URL as BASE_URL
44
+ from .config import CHAT_IDS_FILE as CHAT_IDS_FILE
45
+ from .config import CHAT_URL as CHAT_URL
46
+ from .config import DATA_DIR as DATA_DIR
47
+ from .config import GRAPH_STATS_URL as GRAPH_STATS_URL
48
+ from .config import HISTORY_URL as HISTORY_URL
49
+ from .config import INVITE_CODE as INVITE_CODE
50
+ from .config import MAX_TELEGRAM_FILE_BYTES as MAX_TELEGRAM_FILE_BYTES
51
+ from .config import MCP_TOOLS_URL as MCP_TOOLS_URL
52
+ from .config import MODELS_URL as MODELS_URL
53
+ from .config import PROPOSALS_URL as PROPOSALS_URL
54
+ from .config import PUBLIC_WEB_URL as PUBLIC_WEB_URL
55
+ from .config import SERVER_PORT as SERVER_PORT
56
+ from .config import STATUS_URL as STATUS_URL
57
+ from .config import TOKEN as TOKEN
58
+ from .config import UPLOAD_DOC_URL as UPLOAD_DOC_URL
59
+ from .config import _get_server_session as _get_server_session
60
+ from .config import _server_client as _server_client
61
+ from .config import env_value as env_value
62
+ from .config import load_env_file as load_env_file
63
+ from .config import logger as logger
64
+ from .dispatch import HELP_TEXT as HELP_TEXT
65
+ from .dispatch import _log_task_exception as _log_task_exception
66
+ from .dispatch import handle_callback_query as handle_callback_query
67
+ from .dispatch import handle_command as handle_command
68
+ from .dispatch import run_bot as run_bot
69
+ from .flows import _approval_pause_id as _approval_pause_id
70
+ from .flows import _bot_pending_plans as _bot_pending_plans
71
+ from .flows import _is_approval_pause as _is_approval_pause
72
+ from .flows import _resume_payload as _resume_payload
73
+ from .flows import ask_ai as ask_ai
74
+ from .flows import format_artifact_card as format_artifact_card
75
+ from .flows import format_grounding_badge as format_grounding_badge
76
+ from .flows import format_proposals as format_proposals
77
+ from .flows import format_run_explanation as format_run_explanation
78
+ from .flows import handle_plan_callback as handle_plan_callback
79
+ from .flows import handle_proposal_callback as handle_proposal_callback
80
+ from .flows import process_ai_request as process_ai_request
81
+ from .flows import send_artifact_card as send_artifact_card
82
+ from .flows import send_grounding_badge as send_grounding_badge
83
+ from .flows import send_plan_for_approval as send_plan_for_approval
84
+ from .flows import send_run_explanation as send_run_explanation
85
+ from .flows import show_review_center as show_review_center
86
+ from .helpers import allowed_chat_ids as allowed_chat_ids
87
+ from .helpers import answer_callback as answer_callback
88
+ from .helpers import broadcast_web_chat as broadcast_web_chat
89
+ from .helpers import collect_generated_files as collect_generated_files
90
+ from .helpers import collect_preview_urls as collect_preview_urls
91
+ from .helpers import download_as_base64 as download_as_base64
92
+ from .helpers import download_telegram_file as download_telegram_file
93
+ from .helpers import edit_message as edit_message
94
+ from .helpers import get_graph_url as get_graph_url
95
+ from .helpers import get_lan_ip as get_lan_ip
96
+ from .helpers import get_updates as get_updates
97
+ from .helpers import get_web_url as get_web_url
98
+ from .helpers import is_chat_allowed as is_chat_allowed
99
+ from .helpers import load_chat_ids as load_chat_ids
100
+ from .helpers import parse_allowed_chat_ids as parse_allowed_chat_ids
101
+ from .helpers import register_chat_id as register_chat_id
102
+ from .helpers import resolve_workspace_file as resolve_workspace_file
103
+ from .helpers import save_chat_ids as save_chat_ids
104
+ from .helpers import send_chat_action as send_chat_action
105
+ from .helpers import send_document as send_document
106
+ from .helpers import send_generated_files as send_generated_files
107
+ from .helpers import send_message as send_message
108
+ from .helpers import send_photo as send_photo
109
+ from .helpers import send_preview_links as send_preview_links
110
+ from .screens import MAIN_MENU as MAIN_MENU
111
+ from .screens import _mac_ram_used_gb as _mac_ram_used_gb
112
+ from .screens import _unload_all_report as _unload_all_report
113
+ from .screens import clear_server_history as clear_server_history
114
+ from .screens import do_unload_model as do_unload_model
115
+ from .screens import process_document_file as process_document_file
116
+ from .screens import send_mcp_tools as send_mcp_tools
117
+ from .screens import send_web_link as send_web_link
118
+ from .screens import show_graph_stats as show_graph_stats
119
+ from .screens import show_history_summary as show_history_summary
120
+ from .screens import show_menu as show_menu
121
+ from .screens import show_model_info as show_model_info
122
+ from .screens import show_status as show_status
123
+ from .screens import take_screenshot as take_screenshot
@@ -0,0 +1,17 @@
1
+ """Script entry: ``python -m latticeai.integrations.telegram_bot``.
2
+
3
+ The single file carried this block under ``if __name__ == "__main__"``. A
4
+ package cannot: ``__init__`` is never ``__main__``. Moving it here verbatim is
5
+ what keeps the ``-m`` invocation working after the v11.3.0 split.
6
+ """
7
+
8
+ import asyncio
9
+
10
+ from latticeai.core.quiet import quiet
11
+ from latticeai.integrations.telegram_bot import run_bot
12
+
13
+ if __name__ == "__main__":
14
+ try:
15
+ asyncio.run(run_bot())
16
+ except KeyboardInterrupt:
17
+ quiet()
@@ -0,0 +1,86 @@
1
+ """Environment, endpoints, logger, and the bot's server capability.
2
+
3
+ Every value the bridge reads from the environment is resolved here, once, at
4
+ import — the module *is* the bot's configuration surface. Importing it has the
5
+ same side effects the single file had at its own import: the sensitive-log
6
+ filter is installed, ``.env`` is loaded, and the data directory is provisioned.
7
+
8
+ Security boundary (unchanged since 11.0.1): :func:`_server_client` is the only
9
+ authenticated client. It carries the explicitly provisioned bot-to-server
10
+ bearer capability and is used for Lattice-server calls alone — never for
11
+ api.telegram.org, which always gets the plain ``httpx.AsyncClient`` the poll
12
+ loop owns.
13
+
14
+ Stubbing note: a constant is read through the globals of the module that *uses*
15
+ it, so a test standing in for ``TOKEN``/``CHAT_IDS_FILE``/``AGENT_WORKSPACE``
16
+ patches ``helpers``/``screens``/``flows``/``dispatch``, not this module.
17
+ """
18
+
19
+ import logging
20
+ import os
21
+ from pathlib import Path
22
+
23
+ import httpx
24
+
25
+ from latticeai.cli.runtime import _load_env_file
26
+ from latticeai.core.logging_safety import install_sensitive_log_filter
27
+
28
+ install_sensitive_log_filter()
29
+
30
+ def load_env_file(path=".env"):
31
+ # Single source of truth: shared with ltcai_cli via latticeai.cli.runtime.
32
+ _load_env_file(Path(path))
33
+
34
+ load_env_file()
35
+
36
+ def env_value(primary: str, default: str = "") -> str:
37
+ return os.getenv(primary) or default
38
+
39
+ TOKEN = env_value("LATTICEAI_TELEGRAM_BOT_TOKEN")
40
+ API_URL = f"https://api.telegram.org/bot{TOKEN}"
41
+ SERVER_PORT = int(env_value("LATTICEAI_SERVER_PORT", "4825"))
42
+ BASE_URL = env_value("LATTICEAI_BASE_URL", env_value("LATTICEAI_SERVER_URL", f"http://127.0.0.1:{SERVER_PORT}")).rstrip("/")
43
+ CHAT_URL = f"{BASE_URL}/chat"
44
+ AGENT_URL = f"{BASE_URL}/agent"
45
+ MCP_TOOLS_URL = f"{BASE_URL}/mcp/tools"
46
+ HISTORY_URL = f"{BASE_URL}/history"
47
+ STATUS_URL = f"{BASE_URL}/status"
48
+ MODELS_URL = f"{BASE_URL}/models"
49
+ GRAPH_STATS_URL = f"{BASE_URL}/knowledge-graph/stats"
50
+ UPLOAD_DOC_URL = f"{BASE_URL}/upload/document"
51
+ PROPOSALS_URL = f"{BASE_URL}/api/proposals"
52
+
53
+ AGENT_RESUME_URL = f"{BASE_URL}/agent/resume"
54
+ AGENT_WORKSPACE = Path(env_value("LATTICEAI_AGENT_ROOT", "agent_workspace")).resolve()
55
+
56
+ MAX_TELEGRAM_FILE_BYTES = 45 * 1024 * 1024
57
+ INVITE_CODE = env_value("LATTICEAI_INVITE_CODE")
58
+ PUBLIC_WEB_URL = env_value("LATTICEAI_PUBLIC_URL")
59
+ DATA_DIR = Path(env_value("LATTICEAI_DATA_DIR", str(Path.home() / ".ltcai")))
60
+ DATA_DIR.mkdir(parents=True, exist_ok=True)
61
+ CHAT_IDS_FILE = Path(env_value("LATTICEAI_TELEGRAM_CHATS_FILE", str(DATA_DIR / "telegram_chats.json")))
62
+
63
+ logging.basicConfig(level=logging.INFO)
64
+ logger = logging.getLogger(__name__)
65
+
66
+ # ── Server session auth ───────────────────────────────────────────────────────
67
+
68
+ def _get_server_session() -> str:
69
+ """Return only the explicitly provisioned bot-to-server token.
70
+
71
+ Web sessions are hashed at rest and belong to interactive users. Scanning
72
+ ``sessions.json`` cannot recover a usable token and would couple the bot to
73
+ whichever administrator happened to be logged in most recently.
74
+ """
75
+ return env_value("LATTICEAI_SERVER_SESSION_TOKEN").strip()
76
+
77
+ def _server_client(**kwargs) -> httpx.AsyncClient:
78
+ """Return a server client authenticated by an explicit bearer capability."""
79
+ token = _get_server_session()
80
+ if not token:
81
+ raise RuntimeError(
82
+ "LATTICEAI_SERVER_SESSION_TOKEN is required for Telegram server API calls."
83
+ )
84
+ headers = dict(kwargs.pop("headers", {}) or {})
85
+ headers["Authorization"] = f"Bearer {token}"
86
+ return httpx.AsyncClient(headers=headers, **kwargs)