ltcai 11.5.0 → 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.
- package/README.md +86 -47
- package/docs/CHANGELOG.md +76 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/HYBRID_CLOUD_KG_STREAMING.md +8 -4
- package/docs/MULTI_AGENT_RUNTIME.md +1 -1
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +1 -1
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/WORKFLOW_DESIGNER.md +1 -1
- package/docs/architecture.md +4 -4
- package/docs/kg-schema.md +1 -1
- package/docs/v11.5.1_RUST_FULL_LOOP_PLAN.md +85 -0
- package/docs/v11.5.2_TIGHT_SHIP_PLAN.md +144 -0
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/graph/proactive.py +1 -1
- package/lattice_brain/graph/projection/v2_schema.py +0 -28
- package/lattice_brain/graph/vector_index/base.py +0 -3
- package/lattice_brain/graph/vector_index/brute_force.py +0 -4
- package/lattice_brain/graph/vector_index/hnsw.py +0 -3
- package/lattice_brain/graph/vector_index/quantized.py +0 -3
- package/lattice_brain/ingestion_jobs.py +0 -3
- package/lattice_brain/memory.py +0 -27
- package/lattice_brain/portability/sharing.py +0 -4
- package/lattice_brain/quality.py +0 -49
- package/lattice_brain/runtime/agent_runtime.py +1 -1
- package/lattice_brain/runtime/hooks.py +0 -13
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/lattice_brain/sealed_box.py +0 -4
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +12 -12
- package/latticeai/api/agent_worker_seam.py +389 -0
- package/latticeai/api/auth.py +25 -2
- package/latticeai/api/chat.py +3 -3
- package/latticeai/api/chat_agent_http.py +3 -12
- package/latticeai/api/chat_helpers.py +10 -13
- package/latticeai/api/computer_use.py +31 -42
- package/latticeai/api/health.py +21 -1
- package/latticeai/api/local_files.py +14 -0
- package/latticeai/api/permissions.py +38 -12
- package/latticeai/api/search.py +24 -0
- package/latticeai/api/static_routes.py +4 -1
- package/latticeai/cli/runtime.py +7 -3
- package/latticeai/core/config.py +47 -3
- package/latticeai/core/csrf.py +28 -2
- package/latticeai/core/embedding_providers/__init__.py +3 -5
- package/latticeai/core/embedding_providers/base.py +1 -1
- package/latticeai/core/embedding_providers/text.py +1 -1
- package/latticeai/core/enterprise.py +1 -4
- package/latticeai/core/http_origin.py +146 -0
- package/latticeai/core/invitations.py +3 -2
- package/latticeai/core/io_utils.py +2 -11
- package/latticeai/core/legacy_compatibility.py +1 -1
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/messages.py +40 -0
- package/latticeai/core/model_compat.py +2 -8
- package/latticeai/core/module_probe.py +37 -0
- package/latticeai/core/run_explain.py +5 -2
- package/latticeai/core/run_store.py +2 -2
- package/latticeai/core/security.py +13 -0
- package/latticeai/core/sessions.py +3 -2
- package/latticeai/core/sse.py +25 -0
- package/latticeai/core/users.py +0 -9
- package/latticeai/core/workspace_graph_trace.py +2 -8
- package/latticeai/core/workspace_os.py +0 -10
- package/latticeai/core/workspace_os_constants.py +1 -1
- package/latticeai/core/workspace_os_utils.py +34 -0
- package/latticeai/core/workspace_review_items.py +2 -8
- package/latticeai/core/workspace_runs.py +2 -8
- package/latticeai/core/workspace_skills.py +2 -7
- package/latticeai/models/router/documents.py +0 -35
- package/latticeai/models/router/registry.py +0 -4
- package/latticeai/runtime/access_runtime.py +16 -4
- package/latticeai/runtime/build_phases/features.py +25 -2
- package/latticeai/runtime/build_phases/web.py +2 -0
- package/latticeai/runtime/feature_toggle_wiring.py +12 -18
- package/latticeai/runtime/network_boundary_wiring.py +8 -19
- package/latticeai/runtime/permission_mode_wiring.py +6 -13
- package/latticeai/runtime/router_registration.py +4 -0
- package/latticeai/runtime/runtime_context.py +1 -13
- package/latticeai/runtime/service_singletons.py +55 -0
- package/latticeai/services/architecture_readiness.py +1 -1
- package/latticeai/services/brain_intelligence/proposals.py +0 -5
- package/latticeai/services/change_proposals.py +2 -36
- package/latticeai/services/chronicle.py +4 -6
- package/latticeai/services/command_center.py +4 -4
- package/latticeai/services/evidence_actions.py +5 -2
- package/latticeai/services/hybrid_chat.py +2 -2
- package/latticeai/services/hybrid_policy.py +8 -57
- package/latticeai/services/mode_store.py +132 -0
- package/latticeai/services/model_capability_registry.py +0 -15
- package/latticeai/services/model_catalog.py +0 -7
- package/latticeai/services/model_loading.py +3 -2
- package/latticeai/services/model_runtime/__init__.py +0 -57
- package/latticeai/services/model_runtime/engines.py +27 -128
- package/latticeai/services/model_runtime/loading.py +2 -2
- package/latticeai/services/network_boundary_service.py +7 -44
- package/latticeai/services/permission_mode_service.py +8 -56
- package/latticeai/services/product_readiness.py +1 -1
- package/latticeai/services/setup_detection.py +67 -1
- package/latticeai/services/tool_dispatch.py +0 -4
- package/latticeai/services/upload_service.py +7 -20
- package/latticeai/services/workspace_service.py +0 -6
- package/latticeai/setup/auto_setup.py +15 -26
- package/latticeai/setup/wizard/catalog.py +5 -2
- package/latticeai/setup/wizard/detect.py +6 -27
- package/latticeai/setup/wizard/paths.py +2 -5
- package/latticeai/setup/wizard/plans.py +2 -2
- package/package.json +2 -4
- package/scripts/bump_version.py +14 -1
- package/scripts/check_current_release_docs.mjs +1 -1
- package/scripts/check_legacy_debt.mjs +1 -1
- package/scripts/check_server_i18n.mjs +1 -0
- package/scripts/generate_agent_loop_fixtures.py +994 -0
- package/scripts/generate_rust_parity_fixtures.py +72 -161
- package/scripts/parity_fixture_corpus_context.py +162 -0
- package/scripts/parity_fixture_corpus_docgen.py +341 -0
- package/scripts/release_screen_claims.json +20 -0
- package/src-tauri/Cargo.lock +13 -7
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/src/backend.rs +76 -2
- package/src-tauri/src/main.rs +13 -3
- package/src-tauri/tauri.conf.json +2 -2
- package/static/app/asset-manifest.json +41 -41
- package/static/app/assets/{Act-CWnxSCgN.js → Act-DcQizkl1.js} +1 -1
- package/static/app/assets/{AdminConsole-BEQYU6kF.js → AdminConsole-cf4npybT.js} +1 -1
- package/static/app/assets/{Brain-DWu1BhFg.js → Brain-3VCSHFcn.js} +1 -1
- package/static/app/assets/{BrainHome-95Hilr9R.js → BrainHome-Qm8eaztx.js} +1 -1
- package/static/app/assets/{BrainSignals-QdeqCpAF.js → BrainSignals-DS9BtKOW.js} +1 -1
- package/static/app/assets/{Capture-BHpCxnzb.js → Capture-DiQ219jW.js} +1 -1
- package/static/app/assets/{Chronicle-B4xYKoed.js → Chronicle-BGvuAchH.js} +1 -1
- package/static/app/assets/{CommandPalette-BVXnttSz.js → CommandPalette-Bqhm0Urn.js} +1 -1
- package/static/app/assets/{Library-DgYcHome.js → Library-BV6NnF0a.js} +1 -1
- package/static/app/assets/{LivingBrain-CrJLDbf7.js → LivingBrain-GzenJchP.js} +1 -1
- package/static/app/assets/{ProductFlow-DFlScKoJ.js → ProductFlow-DEP6-vML.js} +1 -1
- package/static/app/assets/{ReviewCard-Cy5f48Pj.js → ReviewCard-CNZ7XjWG.js} +1 -1
- package/static/app/assets/{System-NF8IfhTa.js → System-CieofHQa.js} +1 -1
- package/static/app/assets/arrow-left-kfsrk0mv.js +1 -0
- package/static/app/assets/{bot-CucuhLhm.js → bot-B_K1Tdmw.js} +1 -1
- package/static/app/assets/{brain-BBnSryW_.js → brain-DWyaV1L1.js} +1 -1
- package/static/app/assets/{button-C2GUj2Ai.js → button-aTn4s84A.js} +1 -1
- package/static/app/assets/circle-check-qqLug9nU.js +1 -0
- package/static/app/assets/{circle-pause-CbkWzBmG.js → circle-pause-xKgeGXkT.js} +1 -1
- package/static/app/assets/{circle-play-7lEaqHdJ.js → circle-play-DkT6tYPX.js} +1 -1
- package/static/app/assets/{cpu-DAlCXlIy.js → cpu-85xYObUC.js} +1 -1
- package/static/app/assets/{download-RNhuuJwh.js → download-B5Fm7YXo.js} +1 -1
- package/static/app/assets/{folder-open-CLW4odzM.js → folder-open-kk2Xa52u.js} +1 -1
- package/static/app/assets/{hard-drive-NKEiDIAJ.js → hard-drive-DkA3zBW_.js} +1 -1
- package/static/app/assets/{index-DMurvUuR.js → index-BMPdTmlY.js} +3 -3
- package/static/app/assets/index-DxmOfNRi.css +2 -0
- package/static/app/assets/{input-D2UhPC1X.js → input-B0nRf2jO.js} +1 -1
- package/static/app/assets/{link-2-6amKbP_P.js → link-2-Dwb4gnTc.js} +1 -1
- package/static/app/assets/{permissionCopy-Cu9TZtdR.js → permissionCopy-CQDUBrOZ.js} +1 -1
- package/static/app/assets/{primitives-gPsccucr.js → primitives-SNp0LRJz.js} +1 -1
- package/static/app/assets/search-BcHqkjoy.js +1 -0
- package/static/app/assets/{share-2-Bau7KkPq.js → share-2-BsrxFglO.js} +1 -1
- package/static/app/assets/{shield-alert-BufNYypi.js → shield-alert-5BStfp2_.js} +1 -1
- package/static/app/assets/{textarea-BQnVWhYs.js → textarea-Cg8IUA-k.js} +1 -1
- package/static/app/assets/{useFocusTrap-B3_w60si.js → useFocusTrap-CYKvE46M.js} +1 -1
- package/static/app/assets/{useMutation-BHhCflT6.js → useMutation-CSn9t1op.js} +1 -1
- package/static/app/assets/{useQuery-rBWfI-5t.js → useQuery-CY2OI2uy.js} +1 -1
- package/static/app/assets/{utils-V_5-wxr5.js → utils-Ddol2RWD.js} +1 -1
- package/static/app/assets/{workspace-K1zjYUHj.js → workspace-BqDwOz_p.js} +1 -1
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/desktop/electron/README.md +0 -9
- package/desktop/electron/main.cjs +0 -58
- package/desktop/electron/preload.cjs +0 -5
- package/latticeai/core/graph_curator.py +0 -11
- package/latticeai/core/hooks.py +0 -11
- package/latticeai/core/local_embeddings.py +0 -104
- package/latticeai/core/multi_agent.py +0 -11
- package/latticeai/core/workflow_engine.py +0 -11
- package/latticeai/services/ingestion.py +0 -11
- package/latticeai/services/kg_portability.py +0 -11
- package/latticeai/services/multimodal_streaming.py +0 -129
- package/scripts/measure_brain_home_fill.mjs +0 -141
- package/static/app/assets/arrow-left-DwkSYrjR.js +0 -1
- package/static/app/assets/circle-check-CxOVPwYq.js +0 -1
- package/static/app/assets/index-BLPb5lmE.css +0 -2
- package/static/app/assets/search-Cj_TKk_2.js +0 -1
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""The document-generation half of the Python↔Rust parity corpus (v11.5.1).
|
|
3
|
+
|
|
4
|
+
``scripts/generate_rust_parity_fixtures.py`` owns the store and the goldens;
|
|
5
|
+
this module owns the rows and specs the **document-generation** ports need, so
|
|
6
|
+
neither file has to grow past the tree's file-size ceiling. Nothing here runs
|
|
7
|
+
anything — it is data plus one normalization helper.
|
|
8
|
+
|
|
9
|
+
Three things are shaped on purpose:
|
|
10
|
+
|
|
11
|
+
* **Every allow-listed node type appears.** ``search_for_document_generation``
|
|
12
|
+
admits fifteen types and boosts four of them; a corpus that only carried
|
|
13
|
+
``Document`` would prove the boost and the filter equally badly.
|
|
14
|
+
* **The Self-Model subgraph is real.** ``summary_for_prompt`` reads the legacy
|
|
15
|
+
``nodes`` table for ``self:%`` ids, so the profile rows are written through
|
|
16
|
+
the same write door as everything else, including the root the read excludes
|
|
17
|
+
and a ``self:`` row whose metadata carries no recognised kind (dropped).
|
|
18
|
+
* **``updated_at`` is spread against the frozen clock**, including one stamp
|
|
19
|
+
that does not parse — the recency term's ``0.0`` branch is otherwise
|
|
20
|
+
unreachable from a fixture whose every row is well formed.
|
|
21
|
+
|
|
22
|
+
:func:`normalize_multi_hop` is the one deliberate deviation, and it is a
|
|
23
|
+
*golden* normalization rather than a port divergence: see its docstring.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
29
|
+
|
|
30
|
+
WS_ALPHA = "ws-alpha"
|
|
31
|
+
WS_BETA = "ws-beta"
|
|
32
|
+
|
|
33
|
+
# ── the document-generation corpus ───────────────────────────────────────────
|
|
34
|
+
# (node_id, type, title, summary, metadata, workspace_id, updated_at)
|
|
35
|
+
DOCGEN_NODES: List[Tuple[str, str, str, str, Dict[str, Any], Optional[str], str]] = [
|
|
36
|
+
# ── the Self-Model subgraph (read by `summary_for_prompt`) ──────────────
|
|
37
|
+
# `self:root` is excluded by id, and `self:unknown:*` carries no
|
|
38
|
+
# `self_model_kind`, so `_row_to_fact` drops it — both are read branches
|
|
39
|
+
# that only exist if the fixture actually contains them.
|
|
40
|
+
("self:root", "Self", "나", "Brain이 사용자에 대해 알고 있는 사실의 뿌리입니다.",
|
|
41
|
+
{"self_model": True, "self_model_kind": "root"}, None, "2026-07-01T09:00:00"),
|
|
42
|
+
("self:decision:a1", "Decision", "문서는 한국어로 쓴다",
|
|
43
|
+
"문서는 한국어로 쓴다",
|
|
44
|
+
{"self_model": True, "self_model_kind": "decision", "origin": "user",
|
|
45
|
+
"confidence": 0.9, "signal": "ko_decision_marker", "workspace_id": WS_ALPHA},
|
|
46
|
+
WS_ALPHA, "2026-07-02T09:00:00"),
|
|
47
|
+
("self:habit:b2", "Habit", "매주 월요일 회의록을 정리한다",
|
|
48
|
+
"매주 월요일 회의록을 정리한다",
|
|
49
|
+
{"self_model": True, "self_model_kind": "habit", "origin": "extraction",
|
|
50
|
+
"confidence": 0.6, "signal": "ko_habit", "workspace_id": None},
|
|
51
|
+
None, "2026-07-03T09:00:00"),
|
|
52
|
+
("self:preference:c3", "Preference", "설명은 짧고 근거를 먼저",
|
|
53
|
+
"설명은 짧고 근거를 먼저",
|
|
54
|
+
{"self_model": True, "self_model_kind": "preference", "origin": "user",
|
|
55
|
+
"confidence": 0.8, "signal": "ko_prefer", "workspace_id": None},
|
|
56
|
+
None, "2026-07-04T09:00:00"),
|
|
57
|
+
("self:relationship:d4", "Relationship", "박민서 님과 빌드 파이프라인을 함께 본다",
|
|
58
|
+
"박민서 님과 빌드 파이프라인을 함께 본다",
|
|
59
|
+
{"self_model": True, "self_model_kind": "relationship", "origin": "extraction",
|
|
60
|
+
"confidence": 0.5, "signal": "ko_with", "workspace_id": WS_BETA},
|
|
61
|
+
WS_BETA, "2026-07-05T09:00:00"),
|
|
62
|
+
("self:trait:e5", "Self", "검색 품질 담당자",
|
|
63
|
+
"검색 품질 담당자",
|
|
64
|
+
{"self_model": True, "self_model_kind": "trait", "origin": "user",
|
|
65
|
+
"confidence": 0.7, "signal": "ko_role", "workspace_id": None},
|
|
66
|
+
None, "2026-07-06T09:00:00"),
|
|
67
|
+
("self:unknown:f6", "Concept", "kind가 없는 self 노드",
|
|
68
|
+
"self_model_kind가 없어 프로필에서 제외되는 행입니다.",
|
|
69
|
+
{"self_model": True}, None, "2026-07-07T09:00:00"),
|
|
70
|
+
# ── the proposal cluster: one hub document with more qualifying
|
|
71
|
+
# neighbours than the eight the related-concept join keeps ───────────
|
|
72
|
+
("doc:proposal", "Document", "제안서 초안: 검색 품질 개선",
|
|
73
|
+
"검색 품질 개선 제안서 초안입니다. 근거는 주간 회의 기록과 분기 리뷰 발표자료입니다.",
|
|
74
|
+
{"relative_path": "docs/proposal.md", "filename": "proposal.md", "ext": ".md"},
|
|
75
|
+
WS_ALPHA, "2026-07-30T09:00:00"),
|
|
76
|
+
("doc:release-notes", "Document", "Release notes for the retrieval port",
|
|
77
|
+
"Release notes describing the ranking port and the parity harness.",
|
|
78
|
+
{"source": "release"}, None, "2026-07-28T09:00:00"),
|
|
79
|
+
("file:meeting-minutes", "File", "meeting-minutes.md",
|
|
80
|
+
"회의록: 랭킹 개선과 온보딩 체크리스트를 확정했습니다.",
|
|
81
|
+
{"filename": "meeting-minutes.md", "relative_path": "notes/meeting-minutes.md"},
|
|
82
|
+
WS_ALPHA, "2026-08-01T11:00:00"),
|
|
83
|
+
("deck:proposal", "SlideDeck", "제안 발표자료",
|
|
84
|
+
"제안서 초안을 발표용으로 정리한 자료입니다.",
|
|
85
|
+
{"filename": "proposal.pptx", "ext": ".pptx"}, WS_BETA, "2026-07-29T09:00:00"),
|
|
86
|
+
("chat:proposal", "Chat", "제안서 관련 대화",
|
|
87
|
+
"제안서 초안을 어떻게 쓸지 논의한 대화입니다.",
|
|
88
|
+
{"conversation_id": "conv-f"}, WS_ALPHA, "2026-07-26T09:00:00"),
|
|
89
|
+
("task:proposal-review", "Task", "제안서 검토",
|
|
90
|
+
"제안서 초안을 검토하고 근거를 확인합니다.", {"status": "open"},
|
|
91
|
+
WS_ALPHA, "2026-07-27T09:00:00"),
|
|
92
|
+
("dec:doc-format", "Decision", "문서 형식은 마크다운으로 통일",
|
|
93
|
+
"문서 생성 결과는 마크다운으로 통일하기로 했습니다.", {},
|
|
94
|
+
WS_ALPHA, "2026-07-22T09:00:00"),
|
|
95
|
+
("feature:doc-generation", "Feature", "문서 생성",
|
|
96
|
+
"질문에서 문서를 만들어 주는 기능입니다.", {}, WS_ALPHA, "2026-07-25T09:00:00"),
|
|
97
|
+
("concept:proposal", "Concept", "제안서",
|
|
98
|
+
"제안서는 결정을 요청하는 문서입니다.", {}, None, "2026-07-24T09:00:00"),
|
|
99
|
+
("concept:evidence", "Concept", "근거",
|
|
100
|
+
"근거는 주장을 뒷받침하는 자료입니다.", {}, WS_ALPHA, "2026-07-23T09:00:00"),
|
|
101
|
+
("page:proposal-outline", "Page", "제안서 목차",
|
|
102
|
+
"제안서 목차 페이지입니다.", {}, None, "2026-07-21T09:00:00"),
|
|
103
|
+
("slide:proposal-1", "Slide", "제안 슬라이드 1",
|
|
104
|
+
"제안서 요약 슬라이드입니다.", {}, WS_BETA, "2026-07-19T09:00:00"),
|
|
105
|
+
("sheet:proposal-budget", "Spreadsheet", "proposal-budget.xlsx",
|
|
106
|
+
"제안서 예산 표입니다.", {"filename": "proposal-budget.xlsx", "ext": ".xlsx"},
|
|
107
|
+
WS_ALPHA, "2026-07-17T09:00:00"),
|
|
108
|
+
("img:proposal-sketch", "Image", "proposal-sketch.png",
|
|
109
|
+
"제안서 구조 스케치입니다.", {"filename": "proposal-sketch.png", "ext": ".png"},
|
|
110
|
+
WS_BETA, "2026-07-16T09:00:00"),
|
|
111
|
+
("imgtext:proposal-ocr", "ImageText", "제안서 스케치 OCR",
|
|
112
|
+
"제안서 구조: 배경, 근거, 결론.",
|
|
113
|
+
{"filename": "proposal-sketch.png", "ocr": True}, WS_BETA, "2026-07-16T09:01:00"),
|
|
114
|
+
("audio:proposal-call", "Audio", "proposal-call.m4a",
|
|
115
|
+
"제안서 관련 통화 녹음입니다.", {"filename": "proposal-call.m4a", "ext": ".m4a"},
|
|
116
|
+
WS_ALPHA, "2026-07-15T09:00:00"),
|
|
117
|
+
("code:doc-builder", "CodeFile", "context_builder.py",
|
|
118
|
+
"문서 생성 컨텍스트를 조합하는 코드입니다. 제안서 생성도 여기를 지납니다.",
|
|
119
|
+
{"filename": "context_builder.py",
|
|
120
|
+
"relative_path": "latticeai/core/context_builder.py"},
|
|
121
|
+
WS_ALPHA, "2026-07-14T09:00:00"),
|
|
122
|
+
# The recency term's `0.0` branch: a stamp `datetime.fromisoformat` refuses.
|
|
123
|
+
("doc:undated", "Document", "Undated 제안서 memo",
|
|
124
|
+
"An undated memo about 제안서; its stamp does not parse.", {},
|
|
125
|
+
None, "not-a-timestamp"),
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
# (from, to, type, weight, created_at) — appended to the base edge set.
|
|
129
|
+
DOCGEN_EDGES: List[Tuple[str, str, str, float, str]] = [
|
|
130
|
+
("doc:proposal", "concept:proposal", "mentions", 0.92, "2026-07-30T00:00:00"),
|
|
131
|
+
("doc:proposal", "concept:evidence", "mentions", 0.91, "2026-07-30T00:01:00"),
|
|
132
|
+
("doc:proposal", "feature:doc-generation", "relates_to", 0.90, "2026-07-30T00:02:00"),
|
|
133
|
+
("doc:proposal", "dec:doc-format", "relates_to", 0.89, "2026-07-30T00:03:00"),
|
|
134
|
+
("doc:proposal", "task:proposal-review", "relates_to", 0.88, "2026-07-30T00:04:00"),
|
|
135
|
+
("doc:proposal", "concept:retrieval", "mentions", 0.87, "2026-07-30T00:05:00"),
|
|
136
|
+
("doc:proposal", "concept:ranking", "mentions", 0.86, "2026-07-30T00:06:00"),
|
|
137
|
+
("doc:proposal", "feature:command-palette", "mentions", 0.85, "2026-07-30T00:07:00"),
|
|
138
|
+
("doc:proposal", "task:parity-harness", "relates_to", 0.84, "2026-07-30T00:08:00"),
|
|
139
|
+
("doc:proposal", "page:proposal-outline", "contains", 0.83, "2026-07-30T00:09:00"),
|
|
140
|
+
("deck:proposal", "doc:proposal", "relates_to", 0.82, "2026-07-29T00:00:00"),
|
|
141
|
+
("deck:proposal", "slide:proposal-1", "contains", 0.81, "2026-07-29T00:01:00"),
|
|
142
|
+
("file:meeting-minutes", "dec:doc-format", "relates_to", 0.80, "2026-07-28T00:00:00"),
|
|
143
|
+
("file:meeting-minutes", "meeting:weekly", "relates_to", 0.79, "2026-07-28T00:01:00"),
|
|
144
|
+
("chat:proposal", "doc:proposal", "mentions", 0.78, "2026-07-27T00:00:00"),
|
|
145
|
+
("img:proposal-sketch", "imgtext:proposal-ocr", "relates_to", 0.77, "2026-07-26T00:00:00"),
|
|
146
|
+
("imgtext:proposal-ocr", "concept:proposal", "mentions", 0.76, "2026-07-26T00:01:00"),
|
|
147
|
+
("audio:proposal-call", "task:proposal-review", "mentions", 0.75, "2026-07-25T00:00:00"),
|
|
148
|
+
("code:doc-builder", "feature:doc-generation", "relates_to", 0.74, "2026-07-24T00:00:00"),
|
|
149
|
+
("sheet:proposal-budget", "doc:proposal", "relates_to", 0.73, "2026-07-23T00:00:00"),
|
|
150
|
+
("doc:release-notes", "concept:ranking", "mentions", 0.72, "2026-07-22T00:00:00"),
|
|
151
|
+
("doc:undated", "concept:proposal", "mentions", 0.71, "2026-07-21T00:00:00"),
|
|
152
|
+
# The Self-Model's own shape: every fact points at the root.
|
|
153
|
+
("self:decision:a1", "self:root", "PART_OF", 1.0, "2026-07-02T00:00:00"),
|
|
154
|
+
("self:habit:b2", "self:root", "PART_OF", 1.0, "2026-07-03T00:00:00"),
|
|
155
|
+
("self:preference:c3", "self:root", "PART_OF", 1.0, "2026-07-04T00:00:00"),
|
|
156
|
+
("self:relationship:d4", "self:root", "PART_OF", 1.0, "2026-07-05T00:00:00"),
|
|
157
|
+
("self:trait:e5", "self:root", "PART_OF", 1.0, "2026-07-06T00:00:00"),
|
|
158
|
+
("self:unknown:f6", "self:root", "PART_OF", 1.0, "2026-07-07T00:00:00"),
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
# ── suite specs ──────────────────────────────────────────────────────────────
|
|
162
|
+
DOCGEN_SEARCH_SPECS: List[Dict[str, Any]] = [
|
|
163
|
+
{"key": "ko_proposal", "query": "제안서 초안"},
|
|
164
|
+
{"key": "ko_meeting", "query": "회의 결정 사항"},
|
|
165
|
+
{"key": "en_ranking", "query": "hybrid retrieval ranking"},
|
|
166
|
+
{"key": "ko_sketch", "query": "스케치"},
|
|
167
|
+
{"key": "ko_self_model", "query": "문서는 한국어로"},
|
|
168
|
+
{"key": "en_undated", "query": "Undated"},
|
|
169
|
+
{"key": "hub_deck", "query": "제안 발표자료"},
|
|
170
|
+
{"key": "tie", "query": "Tie candidate"},
|
|
171
|
+
{"key": "no_hit", "query": "zzqq wumpus nonsense"},
|
|
172
|
+
{"key": "empty_query", "query": ""},
|
|
173
|
+
{"key": "blank_query", "query": " "},
|
|
174
|
+
{"key": "limit_one", "query": "제안서", "limit": 1},
|
|
175
|
+
{"key": "limit_zero", "query": "제안서", "limit": 0},
|
|
176
|
+
{"key": "limit_over", "query": "제안서", "limit": 900},
|
|
177
|
+
{"key": "scoped_alpha", "query": "제안서", "allowed": [WS_ALPHA]},
|
|
178
|
+
{"key": "scoped_alpha_legacy", "query": "제안서", "allowed": [WS_ALPHA], "legacy": True},
|
|
179
|
+
{"key": "scoped_beta", "query": "제안서", "allowed": [WS_BETA]},
|
|
180
|
+
{"key": "scoped_empty", "query": "제안서", "allowed": []},
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
MULTI_HOP_SPECS: List[Dict[str, Any]] = [
|
|
184
|
+
*[{"key": f"hub_h{hops}", "node_ids": ["doc:proposal"], "max_hops": hops}
|
|
185
|
+
for hops in (0, 1, 2, 3)],
|
|
186
|
+
{"key": "hub_hneg", "node_ids": ["doc:proposal"], "max_hops": -1},
|
|
187
|
+
{"key": "multi_seed", "node_ids": ["doc:proposal", "dec:fusion-alpha"], "max_hops": 2},
|
|
188
|
+
{"key": "leaf", "node_ids": ["slide:proposal-1"], "max_hops": 2},
|
|
189
|
+
{"key": "isolated", "node_ids": ["tie:c"], "max_hops": 2},
|
|
190
|
+
{"key": "self_root", "node_ids": ["self:root"], "max_hops": 2},
|
|
191
|
+
{"key": "missing_seed", "node_ids": ["nope:missing"], "max_hops": 2},
|
|
192
|
+
{"key": "no_seeds", "node_ids": [], "max_hops": 2},
|
|
193
|
+
{"key": "scoped_alpha", "node_ids": ["doc:proposal"], "max_hops": 2,
|
|
194
|
+
"allowed": [WS_ALPHA]},
|
|
195
|
+
{"key": "scoped_alpha_legacy", "node_ids": ["doc:proposal"], "max_hops": 2,
|
|
196
|
+
"allowed": [WS_ALPHA], "legacy": True},
|
|
197
|
+
{"key": "scoped_beta", "node_ids": ["deck:proposal"], "max_hops": 2,
|
|
198
|
+
"allowed": [WS_BETA]},
|
|
199
|
+
{"key": "scoped_empty", "node_ids": ["doc:proposal"], "max_hops": 2, "allowed": []},
|
|
200
|
+
]
|
|
201
|
+
|
|
202
|
+
CONTEXT_DOCUMENT_SPECS: List[Dict[str, Any]] = [
|
|
203
|
+
{"key": "default", "query": "제안서 초안"},
|
|
204
|
+
{"key": "no_self_model", "query": "제안서 초안", "include_self_model": False},
|
|
205
|
+
{"key": "self_model_tokens_small", "query": "제안서 초안", "self_model_tokens": 12},
|
|
206
|
+
{"key": "budget_240", "query": "제안서 초안", "budget": 240},
|
|
207
|
+
{"key": "budget_120", "query": "제안서 초안", "budget": 120},
|
|
208
|
+
{"key": "budget_40", "query": "제안서 초안", "budget": 40},
|
|
209
|
+
{"key": "budget_one", "query": "제안서 초안", "budget": 1},
|
|
210
|
+
{"key": "budget_zero", "query": "제안서 초안", "budget": 0},
|
|
211
|
+
{"key": "hops_zero", "query": "제안서 초안", "max_hops": 0},
|
|
212
|
+
{"key": "hops_three", "query": "제안서 초안", "max_hops": 3},
|
|
213
|
+
{"key": "results_two", "query": "제안서 초안", "max_results": 2},
|
|
214
|
+
{"key": "ko_meeting", "query": "회의 결정 사항"},
|
|
215
|
+
{"key": "en_ranking", "query": "hybrid retrieval ranking"},
|
|
216
|
+
{"key": "ko_sketch", "query": "스케치"},
|
|
217
|
+
{"key": "fallback_lexical", "query": "박민서 님"},
|
|
218
|
+
{"key": "fallback_empty", "query": "zzqq wumpus nonsense"},
|
|
219
|
+
{"key": "empty_query", "query": ""},
|
|
220
|
+
{"key": "scoped_alpha", "query": "제안서 초안", "allowed": [WS_ALPHA]},
|
|
221
|
+
{"key": "scoped_alpha_legacy", "query": "제안서 초안", "allowed": [WS_ALPHA],
|
|
222
|
+
"legacy": True},
|
|
223
|
+
{"key": "scoped_empty", "query": "제안서 초안", "allowed": []},
|
|
224
|
+
]
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def normalize_multi_hop(payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
228
|
+
"""Order a ``multi_hop_context`` answer deterministically.
|
|
229
|
+
|
|
230
|
+
**This is a golden normalization, not a port divergence.** Python's
|
|
231
|
+
traversal iterates ``frontier``, a ``set`` — so the order in which the
|
|
232
|
+
nodes of one hop are appended (and therefore the order their incident
|
|
233
|
+
edges are appended) depends on string hashing, which CPython randomizes
|
|
234
|
+
per process unless ``PYTHONHASHSEED`` is pinned. The *contents* are fully
|
|
235
|
+
determined (which nodes are visited, which edges are collected, and every
|
|
236
|
+
node's hop label); only the sequence is not, so the live response ordering
|
|
237
|
+
is unspecified and there is nothing for a port to reproduce.
|
|
238
|
+
|
|
239
|
+
Both sides therefore sort before comparing: nodes by ``(hop, id)``, edges
|
|
240
|
+
by ``(from, to, type, weight)``. The Rust port emits that order natively —
|
|
241
|
+
it is deterministic where Python is not, which is a strictly stronger
|
|
242
|
+
promise and not a different answer.
|
|
243
|
+
"""
|
|
244
|
+
return {
|
|
245
|
+
"nodes": sorted(payload["nodes"], key=lambda node: (node["hop"], node["id"])),
|
|
246
|
+
"edges": sorted(
|
|
247
|
+
payload["edges"],
|
|
248
|
+
key=lambda edge: (edge["from"], edge["to"], edge["type"], edge["weight"]),
|
|
249
|
+
),
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class OrderedMultiHopStore:
|
|
254
|
+
"""The store, with ``multi_hop_context`` ordered deterministically.
|
|
255
|
+
|
|
256
|
+
``retrieve_context_for_generation`` feeds the traversal's node list into
|
|
257
|
+
``extra_nodes``, and the concept section renders the first eight of it — so
|
|
258
|
+
Python's unordered ``frontier`` iteration reaches the *rendered markdown*,
|
|
259
|
+
not only the traversal payload. Both sides therefore see the normalized
|
|
260
|
+
order (see :func:`normalize_multi_hop`); every other attribute is forwarded
|
|
261
|
+
to the real store untouched.
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
def __init__(self, store: Any):
|
|
265
|
+
self._store = store
|
|
266
|
+
|
|
267
|
+
def __getattr__(self, name: str) -> Any:
|
|
268
|
+
return getattr(self._store, name)
|
|
269
|
+
|
|
270
|
+
def multi_hop_context(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
|
271
|
+
return normalize_multi_hop(self._store.multi_hop_context(*args, **kwargs))
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _scope(spec: Dict[str, Any]) -> Dict[str, Any]:
|
|
275
|
+
"""The graph-layer scope every document-generation read takes."""
|
|
276
|
+
allowed = spec.get("allowed")
|
|
277
|
+
return {
|
|
278
|
+
"allowed_workspaces": None if allowed is None else set(allowed),
|
|
279
|
+
"include_legacy_global": spec.get("legacy", False),
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def run_docgen_search(harness: Any, spec: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
284
|
+
"""``KnowledgeGraphStore.search_for_document_generation`` for one spec."""
|
|
285
|
+
return harness.store.search_for_document_generation(
|
|
286
|
+
spec["query"], limit=spec.get("limit", 10), **_scope(spec)
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def run_multi_hop(harness: Any, spec: Dict[str, Any]) -> Dict[str, Any]:
|
|
291
|
+
"""``multi_hop_context`` for one spec, in the normalized order."""
|
|
292
|
+
return normalize_multi_hop(
|
|
293
|
+
harness.store.multi_hop_context(
|
|
294
|
+
spec["node_ids"], max_hops=spec.get("max_hops", 2), **_scope(spec)
|
|
295
|
+
)
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def run_context_document(harness: Any, spec: Dict[str, Any]) -> Dict[str, Any]:
|
|
300
|
+
"""``retrieve_context_for_generation`` plus the footnote over its sources.
|
|
301
|
+
|
|
302
|
+
The footnote is a second public entry point over the same ``sources`` list;
|
|
303
|
+
carrying it here proves that port too, without a suite of its own.
|
|
304
|
+
"""
|
|
305
|
+
from lattice_brain.self_model import DEFAULT_SUMMARY_TOKENS
|
|
306
|
+
from latticeai.core.context_builder import (
|
|
307
|
+
DEFAULT_DOCUMENT_CONTEXT_BUDGET,
|
|
308
|
+
format_sources_footnote,
|
|
309
|
+
retrieve_context_for_generation,
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
context = retrieve_context_for_generation(
|
|
313
|
+
OrderedMultiHopStore(harness.store),
|
|
314
|
+
spec["query"],
|
|
315
|
+
max_results=spec.get("max_results", 10),
|
|
316
|
+
max_hops=spec.get("max_hops", 2),
|
|
317
|
+
budget=spec.get("budget", DEFAULT_DOCUMENT_CONTEXT_BUDGET),
|
|
318
|
+
include_self_model=spec.get("include_self_model", True),
|
|
319
|
+
self_model_tokens=spec.get("self_model_tokens", DEFAULT_SUMMARY_TOKENS),
|
|
320
|
+
**_scope(spec),
|
|
321
|
+
)
|
|
322
|
+
return {
|
|
323
|
+
"context": context,
|
|
324
|
+
"sources_footnote": format_sources_footnote(context["sources"]),
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
#: The three document-generation suites, as the generator merges them into
|
|
329
|
+
#: :data:`SUITES` / :data:`SUITE_RUNNERS`. Keeping the pair side by side is what
|
|
330
|
+
#: stops a spec list from arriving without the runner that answers it.
|
|
331
|
+
DOCGEN_SUITES: Dict[str, List[Dict[str, Any]]] = {
|
|
332
|
+
"docgen_search": DOCGEN_SEARCH_SPECS,
|
|
333
|
+
"multi_hop": MULTI_HOP_SPECS,
|
|
334
|
+
"context_document": CONTEXT_DOCUMENT_SPECS,
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
DOCGEN_RUNNERS: Dict[str, Any] = {
|
|
338
|
+
"docgen_search": run_docgen_search,
|
|
339
|
+
"multi_hop": run_multi_hop,
|
|
340
|
+
"context_document": run_context_document,
|
|
341
|
+
}
|
|
@@ -21,6 +21,26 @@
|
|
|
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
|
+
|
|
34
|
+
"11.5.1": {
|
|
35
|
+
"_why": [
|
|
36
|
+
"Rust Full Loop release: agent-loop orchestrator port, worker seams,",
|
|
37
|
+
"doc-gen context builder — backend only. The frontend bundle is",
|
|
38
|
+
"untouched; no captured screen changes on purpose. All thirteen",
|
|
39
|
+
"measured but not gated. Known jitter: 05-memory-graph moves ~2-3%",
|
|
40
|
+
"between captures from the cytoscape layout (11.3.0-11.5.0 observed)."
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
|
|
24
44
|
"11.5.0": {
|
|
25
45
|
"_why": [
|
|
26
46
|
"Rust Complete release: backend/desktop-topology architecture only",
|
package/src-tauri/Cargo.lock
CHANGED
|
@@ -1730,17 +1730,23 @@ dependencies = [
|
|
|
1730
1730
|
|
|
1731
1731
|
[[package]]
|
|
1732
1732
|
name = "lattice-agent"
|
|
1733
|
-
version = "11.5.
|
|
1733
|
+
version = "11.5.2"
|
|
1734
1734
|
dependencies = [
|
|
1735
1735
|
"axum",
|
|
1736
|
+
"base64 0.22.1",
|
|
1737
|
+
"fancy-regex",
|
|
1738
|
+
"futures-core",
|
|
1739
|
+
"getrandom 0.4.3",
|
|
1740
|
+
"reqwest 0.12.28",
|
|
1736
1741
|
"serde",
|
|
1737
1742
|
"serde_json",
|
|
1743
|
+
"sha2",
|
|
1738
1744
|
"tokio",
|
|
1739
1745
|
]
|
|
1740
1746
|
|
|
1741
1747
|
[[package]]
|
|
1742
1748
|
name = "lattice-ai-desktop"
|
|
1743
|
-
version = "11.5.
|
|
1749
|
+
version = "11.5.2"
|
|
1744
1750
|
dependencies = [
|
|
1745
1751
|
"lattice-host",
|
|
1746
1752
|
"lattice-jobs",
|
|
@@ -1755,7 +1761,7 @@ dependencies = [
|
|
|
1755
1761
|
|
|
1756
1762
|
[[package]]
|
|
1757
1763
|
name = "lattice-core"
|
|
1758
|
-
version = "11.5.
|
|
1764
|
+
version = "11.5.2"
|
|
1759
1765
|
dependencies = [
|
|
1760
1766
|
"blake2",
|
|
1761
1767
|
"rusqlite",
|
|
@@ -1765,7 +1771,7 @@ dependencies = [
|
|
|
1765
1771
|
|
|
1766
1772
|
[[package]]
|
|
1767
1773
|
name = "lattice-host"
|
|
1768
|
-
version = "11.5.
|
|
1774
|
+
version = "11.5.2"
|
|
1769
1775
|
dependencies = [
|
|
1770
1776
|
"axum",
|
|
1771
1777
|
"lattice-agent",
|
|
@@ -1782,7 +1788,7 @@ dependencies = [
|
|
|
1782
1788
|
|
|
1783
1789
|
[[package]]
|
|
1784
1790
|
name = "lattice-ingest"
|
|
1785
|
-
version = "11.5.
|
|
1791
|
+
version = "11.5.2"
|
|
1786
1792
|
dependencies = [
|
|
1787
1793
|
"axum",
|
|
1788
1794
|
"lattice-core",
|
|
@@ -1795,7 +1801,7 @@ dependencies = [
|
|
|
1795
1801
|
|
|
1796
1802
|
[[package]]
|
|
1797
1803
|
name = "lattice-jobs"
|
|
1798
|
-
version = "11.5.
|
|
1804
|
+
version = "11.5.2"
|
|
1799
1805
|
dependencies = [
|
|
1800
1806
|
"axum",
|
|
1801
1807
|
"lattice-core",
|
|
@@ -1807,7 +1813,7 @@ dependencies = [
|
|
|
1807
1813
|
|
|
1808
1814
|
[[package]]
|
|
1809
1815
|
name = "lattice-retrieval"
|
|
1810
|
-
version = "11.5.
|
|
1816
|
+
version = "11.5.2"
|
|
1811
1817
|
dependencies = [
|
|
1812
1818
|
"axum",
|
|
1813
1819
|
"fancy-regex",
|
package/src-tauri/Cargo.toml
CHANGED
package/src-tauri/src/backend.rs
CHANGED
|
@@ -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
|
-
|
|
175
|
-
"
|
|
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
|
}
|
package/src-tauri/src/main.rs
CHANGED
|
@@ -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
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
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.
|
|
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
|
|
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
|
},
|