ltcai 11.1.0 → 11.2.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 (118) hide show
  1. package/README.md +53 -53
  2. package/docs/CHANGELOG.md +33 -0
  3. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  4. package/docs/DEVELOPMENT.md +1 -1
  5. package/docs/FEATURE_AUDIT_v11.2.0.md +393 -0
  6. package/docs/LAYOUT_REBUILD_SPEC.md +9 -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/architecture.md +6 -2
  12. package/docs/kg-schema.md +1 -1
  13. package/lattice_brain/__init__.py +1 -1
  14. package/lattice_brain/gates.py +125 -0
  15. package/lattice_brain/graph/fusion.py +35 -4
  16. package/lattice_brain/graph/projection.py +66 -8
  17. package/lattice_brain/graph/schema.py +9 -0
  18. package/lattice_brain/graph/store.py +9 -0
  19. package/lattice_brain/graph/vector_index/selector.py +32 -2
  20. package/lattice_brain/ingestion.py +175 -27
  21. package/lattice_brain/multimodal.py +525 -5
  22. package/lattice_brain/portability.py +169 -32
  23. package/lattice_brain/runtime/multi_agent.py +1 -1
  24. package/lattice_brain/sealed_box.py +244 -0
  25. package/lattice_brain/synthesis.py +24 -1
  26. package/latticeai/__init__.py +1 -1
  27. package/latticeai/api/brain_intelligence.py +4 -0
  28. package/latticeai/api/chat.py +11 -0
  29. package/latticeai/api/chat_helpers.py +16 -3
  30. package/latticeai/api/chat_hybrid.py +32 -1
  31. package/latticeai/api/features.py +70 -0
  32. package/latticeai/api/local_files.py +102 -0
  33. package/latticeai/api/portability.py +39 -4
  34. package/latticeai/api/review_queue.py +126 -0
  35. package/latticeai/api/search.py +16 -2
  36. package/latticeai/core/agent.py +55 -2
  37. package/latticeai/core/config.py +4 -1
  38. package/latticeai/core/context_builder.py +6 -3
  39. package/latticeai/core/legacy_compatibility.py +1 -1
  40. package/latticeai/core/marketplace.py +1 -1
  41. package/latticeai/core/messages.py +143 -0
  42. package/latticeai/core/model_compat.py +73 -2
  43. package/latticeai/core/workspace_os_constants.py +1 -1
  44. package/latticeai/models/model_providers.py +12 -4
  45. package/latticeai/runtime/build_phases.py +28 -0
  46. package/latticeai/runtime/chat_wiring.py +4 -0
  47. package/latticeai/runtime/feature_toggle_wiring.py +163 -0
  48. package/latticeai/runtime/router_registration.py +11 -0
  49. package/latticeai/services/app_context.py +8 -0
  50. package/latticeai/services/architecture_readiness.py +1 -1
  51. package/latticeai/services/automation_intelligence.py +22 -2
  52. package/latticeai/services/brain_intelligence.py +123 -7
  53. package/latticeai/services/command_center.py +10 -4
  54. package/latticeai/services/feature_toggles.py +502 -0
  55. package/latticeai/services/folder_watch.py +122 -1
  56. package/latticeai/services/hybrid_chat.py +56 -5
  57. package/latticeai/services/interop_bridges.py +978 -0
  58. package/latticeai/services/model_capability_registry.py +434 -261
  59. package/latticeai/services/model_catalog.py +95 -61
  60. package/latticeai/services/model_recommendation.py +18 -11
  61. package/latticeai/services/model_runtime.py +1 -1
  62. package/latticeai/services/multimodal_ports.py +26 -1
  63. package/latticeai/services/obsidian_bridge.py +16 -25
  64. package/latticeai/services/product_readiness.py +1 -1
  65. package/latticeai/services/search_service.py +149 -2
  66. package/latticeai/services/tool_dispatch.py +4 -0
  67. package/latticeai/setup/auto_setup.py +27 -30
  68. package/latticeai/setup/wizard.py +77 -44
  69. package/package.json +1 -1
  70. package/scripts/check_current_release_docs.mjs +1 -1
  71. package/scripts/check_server_i18n.mjs +1 -0
  72. package/scripts/release_screen_claims.json +13 -0
  73. package/scripts/verify_hf_model_registry.py +253 -218
  74. package/src-tauri/Cargo.lock +1 -1
  75. package/src-tauri/Cargo.toml +1 -1
  76. package/src-tauri/tauri.conf.json +1 -1
  77. package/static/app/asset-manifest.json +37 -37
  78. package/static/app/assets/{Act-D0HWqtn0.js → Act-AWf0SAKp.js} +1 -1
  79. package/static/app/assets/{AdminConsole-D-QDW-A4.js → AdminConsole-D0u8Tiyj.js} +1 -1
  80. package/static/app/assets/{Brain-CzCsI1mi.js → Brain-tuhI4sOC.js} +1 -1
  81. package/static/app/assets/BrainHome-Ts7G_Ila.js +2 -0
  82. package/static/app/assets/{BrainSignals-2dHQNkns.js → BrainSignals-jMYgQ2Ar.js} +1 -1
  83. package/static/app/assets/{Capture-CT8v1StE.js → Capture-CqOSzyPr.js} +1 -1
  84. package/static/app/assets/{CommandPalette-DoLXC2KH.js → CommandPalette-DC0Bzh-I.js} +1 -1
  85. package/static/app/assets/{Library-DDoxFE5c.js → Library-CX-bbhmK.js} +1 -1
  86. package/static/app/assets/{LivingBrain-BXMWIK_2.js → LivingBrain-DBwhto14.js} +1 -1
  87. package/static/app/assets/{ProductFlow-DOYf7JIs.js → ProductFlow-BHA2cfKI.js} +1 -1
  88. package/static/app/assets/{ReviewCard-COQsqidK.js → ReviewCard-BUhCKRNM.js} +1 -1
  89. package/static/app/assets/{System-BRllvYXd.js → System-Bu2t5hn1.js} +1 -1
  90. package/static/app/assets/arrow-left-Dzwa5zRb.js +1 -0
  91. package/static/app/assets/{bot-4BvN07ux.js → bot-Cia42c2h.js} +1 -1
  92. package/static/app/assets/brain-DJMoqrwx.js +1 -0
  93. package/static/app/assets/{button-CDjtnAoU.js → button-2j2Ijzgq.js} +1 -1
  94. package/static/app/assets/{circle-pause-D_RMn7tp.js → circle-pause-BEFeWpVW.js} +1 -1
  95. package/static/app/assets/{circle-play-B5OpB8ae.js → circle-play-ujXMcHxl.js} +1 -1
  96. package/static/app/assets/{cpu-BIlWInHf.js → cpu-k4awryFq.js} +1 -1
  97. package/static/app/assets/{download-BtjXfL3z.js → download-DFbLJ_ig.js} +1 -1
  98. package/static/app/assets/{folder-open-DefMpxI2.js → folder-open-7y_b6xkM.js} +1 -1
  99. package/static/app/assets/{hard-drive-BQ8NZVkw.js → hard-drive-Bidh02Kr.js} +1 -1
  100. package/static/app/assets/{index-0AvoEBzJ.js → index-BpYkzcVm.js} +3 -3
  101. package/static/app/assets/{index-vtEfYvQY.css → index-DwDl9-8Y.css} +1 -1
  102. package/static/app/assets/{input-B_5ZJ9oy.js → input-DSlJJxRs.js} +1 -1
  103. package/static/app/assets/{permissionCopy-BqZ5tsgL.js → permissionCopy-Bpb83Hx9.js} +1 -1
  104. package/static/app/assets/{primitives-CVwew78r.js → primitives-BCx6TvfG.js} +1 -1
  105. package/static/app/assets/search-Cgy8cCFJ.js +1 -0
  106. package/static/app/assets/{share-2-D5zg_0fY.js → share-2-BH1M-WNi.js} +1 -1
  107. package/static/app/assets/{shield-alert-B5pZzkUb.js → shield-alert-BlKdBXcG.js} +1 -1
  108. package/static/app/assets/{textarea-nEVIweKY.js → textarea-CCWbUfFB.js} +1 -1
  109. package/static/app/assets/{useFocusTrap-Cm99AHlz.js → useFocusTrap-YdHQ7pJ1.js} +1 -1
  110. package/static/app/assets/{useQuery-Dm__N6bL.js → useQuery-CXQiwbVT.js} +1 -1
  111. package/static/app/assets/{utils-DcDMoZIe.js → utils-zqPZJxdx.js} +2 -2
  112. package/static/app/assets/{workspace-LtRRSKTf.js → workspace-DXTihhfU.js} +1 -1
  113. package/static/app/index.html +4 -4
  114. package/static/sw.js +1 -1
  115. package/static/app/assets/BrainHome-Btns-_TA.js +0 -2
  116. package/static/app/assets/arrow-left-DnyMzss-.js +0 -1
  117. package/static/app/assets/brain-uMb_5hnO.js +0 -1
  118. package/static/app/assets/search-DkhnOKZt.js +0 -1
@@ -172,7 +172,10 @@ class Config:
172
172
  trusted_proxies = [item.strip() for item in _value(env, "LATTICEAI_TRUSTED_PROXIES", "").split(",") if item.strip()]
173
173
 
174
174
  public_model = _value(env, "LATTICEAI_PUBLIC_MODEL", _value(env, "LATTICEAI_DEFAULT_MODEL", "openai:gpt-4o-mini"))
175
- local_model = _value(env, "LATTICEAI_LOCAL_MODEL", "mlx-community/gemma-4-12b-it-4bit")
175
+ # Canonical HF casing (the Hub answers `gemma-4-12b-it-4bit` with
176
+ # `gemma-4-12B-it-4bit`); keeping it identical everywhere means the
177
+ # download path, the on-disk cache dir and the catalog key all agree.
178
+ local_model = _value(env, "LATTICEAI_LOCAL_MODEL", "mlx-community/gemma-4-12B-it-4bit")
176
179
 
177
180
  data_dir = Path(_value(env, "LATTICEAI_DATA_DIR", str(Path.home() / ".ltcai")))
178
181
  static_dir = Path(_value(env, "LATTICEAI_STATIC_DIR", str(base_dir / "static")))
@@ -26,7 +26,7 @@ import re
26
26
  from typing import Any, Dict, List
27
27
 
28
28
  from lattice_brain.context import approx_tokens
29
- from lattice_brain.graph.retrieval import context_quality_signal
29
+ from lattice_brain.graph.retrieval import context_quality_signal, multimodal_signal
30
30
  from lattice_brain.self_model import DEFAULT_SUMMARY_TOKENS, summary_for_prompt
31
31
 
32
32
  _CLEAN_RE = re.compile(r"\s+")
@@ -250,8 +250,11 @@ def retrieve_context_for_generation(
250
250
  "graph_edges": len(hop_data.get("edges", [])),
251
251
  "budget_trimmed": trimmed,
252
252
  },
253
- # Same honest signal chat reports for the same Brain.
254
- "context_quality": context_quality_signal("hybrid", len(results)),
253
+ # Same honest signal chat reports for the same Brain — including the
254
+ # multimodal key when the document's context really rests on pictures.
255
+ "context_quality": context_quality_signal(
256
+ "hybrid", len(results), multimodal=multimodal_signal(results)
257
+ ),
255
258
  "trace": {
256
259
  "budget_approx_tokens": budget,
257
260
  "used_approx_tokens": approx_tokens(assembled),
@@ -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.1.0"
16
+ LEGACY_COMPATIBILITY_VERSION = "11.2.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.1.0"
13
+ MARKETPLACE_VERSION = "11.2.0"
14
14
  TEMPLATE_KINDS = ("plugin", "workflow", "agent", "ingestion_bridge")
15
15
 
16
16
 
@@ -307,6 +307,31 @@ MESSAGES: Dict[str, Dict[str, str]] = {
307
307
  "ko": "'{status}' 상태의 검토 항목은 승인할 수 없습니다.",
308
308
  "en": "A review item in status '{status}' cannot be approved.",
309
309
  },
310
+ "review.bulk_ids_required": {
311
+ "ko": "한꺼번에 처리할 검토 항목을 하나 이상 골라 주세요.",
312
+ "en": "Choose at least one review item to act on.",
313
+ },
314
+ "review.bulk_too_many": {
315
+ "ko": "한 번에 최대 {cap}개까지 처리할 수 있습니다.",
316
+ "en": "At most {cap} items can be handled in one request.",
317
+ },
318
+ # ── interop bridges (Notion export / git / mail / calendar) ──────────
319
+ "ingestion.interop_path_required": {
320
+ "ko": "불러올 파일이나 폴더 경로가 필요합니다.",
321
+ "en": "A file or folder path to read is required.",
322
+ },
323
+ "ingestion.interop_unknown_source": {
324
+ "ko": "알 수 없는 연동 종류입니다: {source}",
325
+ "en": "Unknown interop source: {source}",
326
+ },
327
+ "ingestion.vault_watch_unavailable": {
328
+ "ko": "보관함 자동 확인 기능을 사용할 수 없습니다.",
329
+ "en": "Vault watch is unavailable.",
330
+ },
331
+ "ingestion.vault_watch_disabled": {
332
+ "ko": "보관함 자동 확인은 기본으로 꺼져 있습니다. 설정에서 켜야 사용할 수 있습니다.",
333
+ "en": "Vault watch is off by default; turn it on in settings to use it.",
334
+ },
310
335
  # ── projects ────────────────────────────────────────────────────────
311
336
  "project.not_found": {
312
337
  "ko": "프로젝트를 찾을 수 없습니다.",
@@ -317,6 +342,124 @@ MESSAGES: Dict[str, Dict[str, str]] = {
317
342
  "ko": "혼합 검색 정책 서비스가 설정되지 않았습니다.",
318
343
  "en": "The hybrid policy service is not configured.",
319
344
  },
345
+ # ── feature toggles ─────────────────────────────────────────────────
346
+ # The switchboard renders from the server (see
347
+ # ``latticeai/services/feature_toggles.py``), so every label and one-line
348
+ # explanation a person reads lives here. The rule for the summaries: say
349
+ # what turning it *on* does, in the words someone who never read the
350
+ # environment-variable docs would use.
351
+ "features.note": {
352
+ "ko": "모두 지금 바로 적용됩니다. 다시 시작하지 않아도 됩니다.",
353
+ "en": "Every switch here takes effect right away — no restart needed.",
354
+ },
355
+ "features.unknown": {
356
+ "ko": "그런 기능은 없습니다: {feature}",
357
+ "en": "There is no such feature: {feature}",
358
+ },
359
+ "features.invalid_value": {
360
+ "ko": "이 기능에 쓸 수 없는 값입니다: {value}",
361
+ "en": "That is not a value this feature can take: {value}",
362
+ },
363
+ "features.choice.install_required": {
364
+ "ko": "설치 필요 — {reason}",
365
+ "en": "Install required — {reason}",
366
+ },
367
+ "features.allow_multimodal.label": {
368
+ "ko": "사진·녹음도 기억하기",
369
+ "en": "Remember pictures and recordings",
370
+ },
371
+ "features.allow_multimodal.summary": {
372
+ "ko": "폴더를 읽을 때 글뿐 아니라 사진과 녹음도 함께 저장합니다.",
373
+ "en": "A folder scan stores pictures and recordings too, not just text.",
374
+ },
375
+ "features.video_ingest.label": {
376
+ "ko": "영상도 함께",
377
+ "en": "Include videos",
378
+ },
379
+ "features.video_ingest.summary": {
380
+ "ko": "사진·녹음을 켠 상태에서, 영상은 장면과 자막으로 저장합니다.",
381
+ "en": "With the switch above on, videos are stored as keyframes and subtitles.",
382
+ },
383
+ "features.vault_watch.label": {
384
+ "ko": "노트 보관함 지켜보기",
385
+ "en": "Watch my notes vault",
386
+ },
387
+ "features.vault_watch.summary": {
388
+ "ko": "밖에 있는 노트 보관함이 바뀌면 알아서 다시 읽어옵니다.",
389
+ "en": "When an outside notes vault changes, it is re-read on its own.",
390
+ },
391
+ "features.brain_network.label": {
392
+ "ko": "골라서 나누기",
393
+ "en": "Share selected knowledge",
394
+ },
395
+ "features.brain_network.summary": {
396
+ "ko": "내가 고른 기억 묶음만 다른 기기로 내보내고 받아올 수 있습니다.",
397
+ "en": "Lets you export a hand-picked slice of memory to another device, and receive one.",
398
+ },
399
+ "features.brain_network.caution": {
400
+ "ko": "이 기능만 기억을 이 컴퓨터 밖으로 내보냅니다. 받은 내용은 바로 합쳐지지 않고 검토함으로 갑니다.",
401
+ "en": "This is the one switch that sends memory off this computer. Anything received waits in the review inbox instead of merging.",
402
+ },
403
+ "features.synthesis.label": {
404
+ "ko": "스스로 정리하기",
405
+ "en": "Tidy up on its own",
406
+ },
407
+ "features.synthesis.summary": {
408
+ "ko": "자료가 쌓이면 알아서 훑어보고, 고칠 거리를 검토함에 제안합니다.",
409
+ "en": "As material piles up, the Brain reviews it and proposes tidy-ups in the review inbox.",
410
+ },
411
+ "features.auto_vector_index.label": {
412
+ "ko": "넣자마자 검색 준비",
413
+ "en": "Make new material searchable at once",
414
+ },
415
+ "features.auto_vector_index.summary": {
416
+ "ko": "새 자료를 넣으면 바로 의미 검색까지 준비합니다. 끄면 나중에 한 번에 만듭니다.",
417
+ "en": "New material is prepared for meaning-based search immediately; off means you rebuild later.",
418
+ },
419
+ "features.auto_late_fusion.label": {
420
+ "ko": "글로 사진 찾기",
421
+ "en": "Find pictures by typing",
422
+ },
423
+ "features.auto_late_fusion.summary": {
424
+ "ko": "글로 물어봐도 사진까지 함께 찾습니다. 사진을 읽는 모델이 있어야 켜집니다.",
425
+ "en": "A typed question also searches pictures — needs a vision model that shares the same space.",
426
+ },
427
+ "features.fusion_rrf.label": {
428
+ "ko": "검색 결과 합치는 방식 바꾸기",
429
+ "en": "Blend search results by rank",
430
+ },
431
+ "features.fusion_rrf.summary": {
432
+ "ko": "점수 대신 순위로 합칩니다. 검색 채널마다 점수 크기가 달라도 흔들리지 않습니다.",
433
+ "en": "Combines channels by position instead of score, so mismatched score scales stop skewing results.",
434
+ },
435
+ "features.graph_expansion.label": {
436
+ "ko": "옆에 있는 기억까지 보기",
437
+ "en": "Look at neighbouring memories",
438
+ },
439
+ "features.graph_expansion.summary": {
440
+ "ko": "찾은 기억과 바로 이어진 기억도 후보로 넣습니다. 답은 넓어지고 조금 흐려집니다.",
441
+ "en": "Adds memories one link away from a hit as candidates — wider answers, slightly less focused.",
442
+ },
443
+ "features.vector_backend.label": {
444
+ "ko": "의미 검색 방식",
445
+ "en": "Meaning-search engine",
446
+ },
447
+ "features.vector_backend.summary": {
448
+ "ko": "빠르기와 정확함 사이에서 고릅니다. 기본값은 전부 훑어보는 정확한 방식입니다.",
449
+ "en": "Trade speed against exactness. The default compares everything and is exact.",
450
+ },
451
+ "features.vector_backend.choice.brute": {
452
+ "ko": "전부 비교 (정확)",
453
+ "en": "Compare everything (exact)",
454
+ },
455
+ "features.vector_backend.choice.quantized": {
456
+ "ko": "간추려 비교 (빠름)",
457
+ "en": "Compare compressed (faster)",
458
+ },
459
+ "features.vector_backend.choice.hnsw": {
460
+ "ko": "근사 검색 (가장 빠름)",
461
+ "en": "Approximate search (fastest)",
462
+ },
320
463
  # ── models ──────────────────────────────────────────────────────────
321
464
  "models.other_user_credentials": {
322
465
  "ko": "다른 사용자의 모델 자격 증명을 사용할 수 없습니다.",
@@ -32,14 +32,54 @@ FAMILY_PATTERNS: List[Tuple[str, re.Pattern]] = [
32
32
  ("qwen", re.compile(r"qwen", re.I)),
33
33
  ("llama", re.compile(r"\bllama|meta[-_]?llama", re.I)),
34
34
  ("claude", re.compile(r"claude", re.I)),
35
+ # gpt-oss is its own local family with its own chat format, so it must be
36
+ # matched before the cloud `gpt` pattern rather than falling into it.
37
+ ("gpt_oss", re.compile(r"gpt[-_]?oss", re.I)),
35
38
  ("gpt", re.compile(r"gpt[-_]?(?:4|5)|openai", re.I)),
36
39
  ("gemini", re.compile(r"gemini", re.I)),
37
40
  ("grok", re.compile(r"grok|x[-_]?ai", re.I)),
41
+ ("lfm2", re.compile(r"\blfm[-_]?2", re.I)),
38
42
  ]
39
43
 
44
+ #: HF ``config.json`` ``model_type`` → family code. An id is a name somebody
45
+ #: chose; an architecture is what mlx-lm / mlx-vlm actually dispatches on, so it
46
+ #: is the more reliable signal when we have it. Values here are the architectures
47
+ #: pinned in the model capability registry (11.2.0) plus the ones that came
48
+ #: before, so a model already on disk keeps its profile after its generation
49
+ #: stops being offered.
50
+ ARCHITECTURE_FAMILIES: Dict[str, str] = {
51
+ "gemma2": "gemma",
52
+ "gemma3": "gemma",
53
+ "gemma4": "gemma",
54
+ "gemma4_unified": "gemma",
55
+ "qwen2_5_vl": "qwen",
56
+ "qwen3_vl": "qwen",
57
+ "qwen3_vl_moe": "qwen",
58
+ "qwen3_5": "qwen",
59
+ "qwen3_5_moe": "qwen",
60
+ "llama": "llama",
61
+ "llama4": "llama",
62
+ "mllama": "llama",
63
+ "gpt_oss": "gpt_oss",
64
+ "lfm2": "lfm2",
65
+ }
66
+
67
+
68
+ def family_for_architecture(architecture: Optional[str]) -> str:
69
+ """Map an HF ``model_type`` to a family code (``"unknown"`` when unmapped)."""
70
+ return ARCHITECTURE_FAMILIES.get(str(architecture or "").strip().lower(), "unknown")
71
+
40
72
 
41
- def detect_model_family(model_id: str) -> str:
42
- """주어진 model_id 문자열에서 family 코드를 추론한다."""
73
+ def detect_model_family(model_id: str, architecture: Optional[str] = None) -> str:
74
+ """주어진 model_id 문자열에서 family 코드를 추론한다.
75
+
76
+ ``architecture`` (HF config ``model_type``) 가 주어지고 알려진 값이면 그것을
77
+ 우선한다 — 로더가 실제로 분기하는 값이기 때문이다.
78
+ """
79
+ if architecture:
80
+ by_arch = family_for_architecture(architecture)
81
+ if by_arch != "unknown":
82
+ return by_arch
43
83
  if not model_id:
44
84
  return "unknown"
45
85
  raw = str(model_id)
@@ -96,6 +136,35 @@ FAMILY_PROFILES: Dict[str, Dict[str, Any]] = {
96
136
  "disable_draft": False,
97
137
  "postprocess": ["strip_role_tokens"],
98
138
  },
139
+ # gpt-oss speaks the "harmony" response format: the channel markers are
140
+ # stop sequences, not text, so the reply never has to be salvaged from them.
141
+ "gpt_oss": {
142
+ "family": "gpt_oss",
143
+ "supports_system": True,
144
+ "supports_vision": False,
145
+ "chat_template": "tokenizer_default",
146
+ "preferred_engines": ["local_mlx", "ollama", "llamacpp"],
147
+ "temperature": 0.2,
148
+ "top_p": 0.9,
149
+ "max_tokens": 4096,
150
+ "stop_sequences": ["<|return|>", "<|call|>", "<|endoftext|>"],
151
+ "disable_draft": False,
152
+ "postprocess": ["strip_role_tokens"],
153
+ },
154
+ # LFM2 uses a ChatML-style template; text only, so no vision claim.
155
+ "lfm2": {
156
+ "family": "lfm2",
157
+ "supports_system": True,
158
+ "supports_vision": False,
159
+ "chat_template": "tokenizer_default",
160
+ "preferred_engines": ["local_mlx", "ollama", "llamacpp"],
161
+ "temperature": 0.2,
162
+ "top_p": 0.9,
163
+ "max_tokens": 4096,
164
+ "stop_sequences": ["<|im_end|>", "<|endoftext|>"],
165
+ "disable_draft": False,
166
+ "postprocess": ["strip_role_tokens"],
167
+ },
99
168
  "unknown": {
100
169
  "family": "unknown",
101
170
  "supports_system": True,
@@ -623,9 +692,11 @@ def get_stop_sequences(model_id: str, engine: Optional[str] = None) -> List[str]
623
692
 
624
693
 
625
694
  __all__ = [
695
+ "ARCHITECTURE_FAMILIES",
626
696
  "FAMILY_PROFILES",
627
697
  "CompatProfile",
628
698
  "detect_model_family",
699
+ "family_for_architecture",
629
700
  "friendly_model_runtime_error",
630
701
  "get_model_profile",
631
702
  "model_runtime_compatibility",
@@ -10,7 +10,7 @@ from __future__ import annotations
10
10
 
11
11
  from typing import Dict
12
12
 
13
- WORKSPACE_OS_VERSION = "11.1.0"
13
+ WORKSPACE_OS_VERSION = "11.2.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
@@ -32,7 +32,11 @@ OPENAI_COMPATIBLE_PROVIDERS = {
32
32
  "xai": {
33
33
  "env_key": "XAI_API_KEY",
34
34
  "base_url": "https://api.x.ai/v1",
35
- "default_model": "grok-beta",
35
+ # ``grok-beta`` was xAI's 2024 preview id and has been decommissioned;
36
+ # a request naming it now 404s at the provider. ``grok-4.5`` is the
37
+ # current flagship on api.x.ai and is multimodal, which is also why the
38
+ # separate ``grok-vision-beta`` row below is gone rather than renamed.
39
+ "default_model": "grok-4.5",
36
40
  },
37
41
  "ollama": {
38
42
  "env_key": "OLLAMA_API_KEY",
@@ -83,7 +87,7 @@ PROVIDER_MODEL_CATALOG = {
83
87
  {"id": "anthropic/claude-haiku-4.5", "name": "Claude Haiku 4.5 via OpenRouter", "family": "Claude"},
84
88
  {"id": "qwen/qwen3-vl-235b-a22b-instruct", "name": "Qwen3-VL 235B A22B via OpenRouter", "family": "Qwen"},
85
89
  {"id": "google/gemma-4-12b-it", "name": "Gemma 4 12B via OpenRouter", "family": "Gemma"},
86
- {"id": "x-ai/grok-2", "name": "Grok 2 via OpenRouter", "family": "Grok"},
90
+ {"id": "x-ai/grok-4.5", "name": "Grok 4.5 via OpenRouter", "family": "Grok"},
87
91
  {"id": "meta-llama/llama-4-scout-17b-16e-instruct", "name": "Llama 4 Scout via OpenRouter", "family": "Llama"},
88
92
  {"id": "google/gemini-2.5-flash", "name": "Gemini 2.5 Flash via OpenRouter", "family": "Gemini"},
89
93
  ],
@@ -96,8 +100,12 @@ PROVIDER_MODEL_CATALOG = {
96
100
  {"id": "meta-llama/Llama-4-Scout-17B-16E-Instruct", "name": "Llama 4 Scout", "family": "Llama"},
97
101
  ],
98
102
  "xai": [
99
- {"id": "grok-beta", "name": "Grok Beta", "family": "Grok"},
100
- {"id": "grok-vision-beta", "name": "Grok Vision Beta", "family": "Grok"},
103
+ # One current generation only, per MODEL_POLICY's "do not keep old
104
+ # same-family generations" rule. The retired preview ids
105
+ # (grok-beta / grok-vision-beta) are not carried as fallbacks: a model
106
+ # the provider no longer serves is not compatibility, it is a 404 the
107
+ # user has to discover for themselves.
108
+ {"id": "grok-4.5", "name": "Grok 4.5", "family": "Grok"},
101
109
  ],
102
110
  }
103
111
 
@@ -621,6 +621,29 @@ def phase_domain(ctx: RuntimeContext) -> None:
621
621
  )
622
622
 
623
623
 
624
+ def self_model_port(workspace_graph: Any) -> Any:
625
+ """The agent loop's Self-Model port (v11.2.0).
626
+
627
+ 11.1.0 built ``executor_prompt_for(self_model_summary=…)`` and had nothing
628
+ to pass it; this is what the composition root passes. ``summary_for_prompt``
629
+ never raises and answers ``""`` for a Brain that knows nothing about its
630
+ owner — which produces exactly the pre-11.2.0 prompt bytes.
631
+
632
+ A module-level factory rather than a closure inside ``phase_services``
633
+ because a port that cannot be called on its own cannot be tested on its own.
634
+ """
635
+
636
+ def _summary(*, user_email: Any = None, workspace_id: Any = None) -> str:
637
+ from lattice_brain.self_model import summary_for_prompt
638
+
639
+ graph = workspace_graph()
640
+ if graph is None:
641
+ return ""
642
+ return summary_for_prompt(graph, workspace_id=workspace_id)
643
+
644
+ return _summary
645
+
646
+
624
647
  # ── phase 6b: retrieval, agent runtime, and the typed AppContext ─────────────
625
648
  def phase_services(ctx: RuntimeContext) -> None:
626
649
  """Retrieval/context assembly, the chat agent runtime, and the AppContext.
@@ -756,6 +779,9 @@ def phase_services(ctx: RuntimeContext) -> None:
756
779
  audit=ctx.append_audit_event,
757
780
  hooks=ctx.HOOKS_REGISTRY,
758
781
  brain_memory=ctx.BRAIN_MEMORY,
782
+ self_model_summary=self_model_port(
783
+ lambda: ctx.KNOWLEDGE_GRAPH if ctx.ENABLE_GRAPH else None
784
+ ),
759
785
  )
760
786
  )
761
787
 
@@ -803,6 +829,8 @@ def phase_services(ctx: RuntimeContext) -> None:
803
829
  require_graph=ctx._require_graph,
804
830
  workspace_graph=ctx._workspace_graph,
805
831
  graph_stats=ctx._graph_stats_safe,
832
+ # Provider, not a value: REVIEW_QUEUE lands two phases later.
833
+ review_queue=lambda: ctx.REVIEW_QUEUE,
806
834
  workspace_models=_workspace_models_payload,
807
835
  workspace_settings=_workspace_settings_payload,
808
836
  scan_environment=scan_environment,
@@ -25,6 +25,7 @@ def build_chat_agent_runtime_from_context(
25
25
  audit: Any,
26
26
  hooks: Any,
27
27
  brain_memory: Any,
28
+ self_model_summary: Any = None,
28
29
  ) -> Any:
29
30
  # Ensure dispatch + agent share the same autonomy dial before the runtime
30
31
  # is constructed (process-wide service; data_dir refined later at router mount).
@@ -39,6 +40,9 @@ def build_chat_agent_runtime_from_context(
39
40
  hooks=hooks,
40
41
  brain_memory=brain_memory,
41
42
  permission_mode=resolve_active_permission_mode,
43
+ # v11.2.0: a scoped resolver, not a snapshot — the profile a run sees is
44
+ # the one the Brain holds when the run starts, per user and workspace.
45
+ self_model_summary=self_model_summary,
42
46
  )
43
47
 
44
48
 
@@ -0,0 +1,163 @@
1
+ """Wire the feature switchboard into the running app (v11.2.0).
2
+
3
+ Same shape as ``permission_mode_wiring``: one process-wide service, an
4
+ idempotent router mount guarded on ``app.state``, and explicit arguments that
5
+ *rebind* an already-created service rather than being dropped — a lazy first
6
+ caller must not pin the store to a fallback data dir with no audit sink.
7
+
8
+ The one job unique to this module is :func:`bind_feature_gates`: it points each
9
+ opt-in gate's resolver at the service, which is the step that turns a persisted
10
+ preference into behaviour. Every gate is imported inside the function so
11
+ importing this module costs nothing; ``lattice_brain.synthesis`` in particular
12
+ drags in the graph layer.
13
+
14
+ Unbinding is a supported operation (:func:`unbind_feature_gates`) because these
15
+ gates are module-level singletons: a test that bound them must be able to give
16
+ them back, and "the environment answers again" has to be reachable.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import os
22
+ import threading
23
+ from pathlib import Path
24
+ from typing import Any, Callable, List, Optional, Tuple
25
+
26
+ from latticeai.services.feature_toggles import FeatureToggleService
27
+
28
+ _LOCK = threading.Lock()
29
+ _SHARED: Optional[FeatureToggleService] = None
30
+
31
+ #: ``feature id -> (module, attribute)`` for every boolean gate. Resolved lazily
32
+ #: by :func:`_boolean_gates` so the table stays readable next to the catalog it
33
+ #: mirrors, and a rename that breaks the pairing fails loudly at wiring time.
34
+ GATE_BINDINGS: Tuple[Tuple[str, str, str], ...] = (
35
+ ("allow_multimodal", "lattice_brain.ingestion", "MULTIMODAL_GATE"),
36
+ ("video_ingest", "lattice_brain.ingestion", "VIDEO_GATE"),
37
+ ("auto_vector_index", "lattice_brain.ingestion", "AUTO_VECTOR_INDEX_GATE"),
38
+ ("brain_network", "lattice_brain.portability", "BRAIN_NETWORK_GATE"),
39
+ ("synthesis", "lattice_brain.synthesis", "SYNTHESIS_GATE"),
40
+ ("fusion_rrf", "lattice_brain.graph.fusion", "FUSION_RRF_GATE"),
41
+ ("graph_expansion", "lattice_brain.graph.fusion", "GRAPH_EXPANSION_GATE"),
42
+ ("vault_watch", "latticeai.services.folder_watch", "VAULT_WATCH_GATE"),
43
+ ("auto_late_fusion", "latticeai.services.search_service", "IMAGE_QUERY_FUSION_GATE"),
44
+ )
45
+
46
+ #: The one non-boolean seam: the vector backend is a pick-one-of-three.
47
+ CHOICE_FEATURE = "vector_backend"
48
+
49
+
50
+ def _default_data_dir() -> Path:
51
+ raw = os.environ.get("LATTICEAI_DATA_DIR", "").strip()
52
+ if raw:
53
+ return Path(raw)
54
+ return Path.home() / ".ltcai"
55
+
56
+
57
+ def _boolean_gates() -> List[Tuple[str, Any]]:
58
+ """``[(feature id, gate)]`` — imports are here, not at module import."""
59
+ from importlib import import_module
60
+
61
+ return [
62
+ (feature_id, getattr(import_module(module), attribute))
63
+ for feature_id, module, attribute in GATE_BINDINGS
64
+ ]
65
+
66
+
67
+ def _bind_vector_index(resolver: Any) -> None:
68
+ from lattice_brain.graph.vector_index.selector import bind_vector_index_resolver
69
+
70
+ bind_vector_index_resolver(resolver)
71
+
72
+
73
+ def get_feature_toggle_service(
74
+ *,
75
+ data_dir: Optional[Path] = None,
76
+ audit: Optional[Callable[..., None]] = None,
77
+ ) -> FeatureToggleService:
78
+ """Process-wide singleton; explicit arguments rebind an existing one."""
79
+ global _SHARED
80
+ with _LOCK:
81
+ if _SHARED is None:
82
+ _SHARED = FeatureToggleService(
83
+ data_dir=Path(data_dir) if data_dir is not None else _default_data_dir(),
84
+ audit=audit,
85
+ )
86
+ return _SHARED
87
+ if data_dir is not None:
88
+ _SHARED.rebind_data_dir(Path(data_dir))
89
+ if audit is not None:
90
+ _SHARED.rebind_audit(audit)
91
+ return _SHARED
92
+
93
+
94
+ def bind_feature_gates(service: Optional[FeatureToggleService] = None) -> None:
95
+ """Point every opt-in gate at the switchboard.
96
+
97
+ After this, a persisted preference beats the environment variable for every
98
+ feature in the catalog — which is the whole point, since the panel is the
99
+ only control surface most people will ever use.
100
+ """
101
+ resolved = service or get_feature_toggle_service()
102
+ for feature_id, gate in _boolean_gates():
103
+ # ``gate.local`` is the fallback on purpose: the switchboard speaks only
104
+ # for switches this person actually moved. Everything else still answers
105
+ # from the gate's own override → env → default, so binding changes
106
+ # nothing at all for an install that never opened the panel.
107
+ gate.bind(resolved.resolver(feature_id, gate.local))
108
+ _bind_vector_index(resolved.choice_resolver(CHOICE_FEATURE))
109
+
110
+
111
+ def unbind_feature_gates() -> None:
112
+ """Hand every gate back to its environment variable."""
113
+ for _feature_id, gate in _boolean_gates():
114
+ gate.bind(None)
115
+ _bind_vector_index(None)
116
+
117
+
118
+ def reset_feature_toggle_service() -> None:
119
+ """Drop the singleton *and* the bindings that pointed at it (tests)."""
120
+ global _SHARED
121
+ unbind_feature_gates()
122
+ with _LOCK:
123
+ _SHARED = None
124
+
125
+
126
+ def register_features_router(
127
+ app: Any,
128
+ *,
129
+ require_user: Callable[..., str],
130
+ data_dir: Optional[Path] = None,
131
+ append_audit_event: Optional[Callable[..., None]] = None,
132
+ ) -> FeatureToggleService:
133
+ """Install GET/POST /api/features on ``app`` and bind the gates. Idempotent.
134
+
135
+ The guard is a flag on ``app.state`` rather than a scan of route paths: a
136
+ router included through fastapi >= 0.140 has no flat ``path`` to introspect,
137
+ so path scanning silently stopped seeing the mount (see
138
+ ``permission_mode_wiring`` for the same note).
139
+ """
140
+ from latticeai.api.features import create_features_router
141
+
142
+ service = get_feature_toggle_service(data_dir=data_dir, audit=append_audit_event)
143
+ bind_feature_gates(service)
144
+ state = getattr(app, "state", None)
145
+ if state is not None and getattr(state, "_ltcai_features_mounted", False):
146
+ return service
147
+ app.include_router(
148
+ create_features_router(service=service, require_user=require_user)
149
+ )
150
+ if state is not None:
151
+ state._ltcai_features_mounted = True
152
+ return service
153
+
154
+
155
+ __all__ = [
156
+ "CHOICE_FEATURE",
157
+ "GATE_BINDINGS",
158
+ "bind_feature_gates",
159
+ "get_feature_toggle_service",
160
+ "register_features_router",
161
+ "reset_feature_toggle_service",
162
+ "unbind_feature_gates",
163
+ ]
@@ -675,4 +675,15 @@ def register_review_and_brain_tail_routers(
675
675
  append_audit_event=append_audit_event,
676
676
  knowledge_graph=knowledge_graph,
677
677
  )
678
+ # The opt-in switchboard. Mounted last on purpose: binding the gates to the
679
+ # service is what makes a stored preference beat an env var, and every
680
+ # module that owns one of those gates has been imported by now.
681
+ from latticeai.runtime.feature_toggle_wiring import register_features_router
682
+
683
+ register_features_router(
684
+ app,
685
+ require_user=require_user,
686
+ data_dir=data_dir,
687
+ append_audit_event=append_audit_event,
688
+ )
678
689
  return brain_network
@@ -76,6 +76,14 @@ class AppContext:
76
76
  workspace_graph: Optional[Callable[[], Any]] = None
77
77
  graph_stats: Optional[Callable[[], dict]] = None
78
78
 
79
+ # ── review center ─────────────────────────────────────────────────────
80
+ # ``ReviewQueueService``, reached through a provider like
81
+ # ``workspace_graph`` above: the queue is built in a later runtime phase
82
+ # than this context, so a value captured here would be ``None`` forever.
83
+ # Call it per request; ``None`` means no Review Center is wired (tests,
84
+ # headless helpers) and the callers stage nothing rather than writing.
85
+ review_queue: Optional[Callable[[], Any]] = None
86
+
79
87
  # ── workspace payload providers / skills ──────────────────────────────
80
88
  workspace_models: Optional[Callable[[], dict]] = None
81
89
  workspace_settings: Optional[Callable[[], dict]] = None
@@ -16,7 +16,7 @@ from typing import Any, Dict, List
16
16
 
17
17
  from latticeai.core.legacy_compatibility import legacy_shim_report
18
18
 
19
- ARCHITECTURE_VERSION_TARGET = "11.1.0"
19
+ ARCHITECTURE_VERSION_TARGET = "11.2.0"
20
20
 
21
21
  PREFERRED_REFACTORING_ORDER = [
22
22
  "agent-runtime",