ltcai 4.3.0 → 4.3.1

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 (38) hide show
  1. package/README.md +19 -17
  2. package/bin/ltcai.js +6 -2
  3. package/docs/CHANGELOG.md +33 -3
  4. package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +16 -22
  5. package/frontend/openapi.json +11 -1
  6. package/frontend/src/App.tsx +15 -1
  7. package/frontend/src/api/client.ts +19 -1
  8. package/frontend/src/api/openapi.ts +10 -0
  9. package/frontend/src/pages/Act.tsx +63 -2
  10. package/frontend/src/pages/Library.tsx +9 -3
  11. package/lattice_brain/__init__.py +1 -1
  12. package/lattice_brain/archive.py +3 -3
  13. package/lattice_brain/storage/sqlite.py +15 -2
  14. package/latticeai/__init__.py +1 -1
  15. package/latticeai/api/agents.py +3 -1
  16. package/latticeai/api/models.py +66 -18
  17. package/latticeai/brain/projection.py +12 -2
  18. package/latticeai/brain/retrieval.py +10 -0
  19. package/latticeai/brain/store.py +6 -1
  20. package/latticeai/core/config.py +3 -1
  21. package/latticeai/core/marketplace.py +1 -1
  22. package/latticeai/core/multi_agent.py +1 -1
  23. package/latticeai/core/product_hardening.py +2 -1
  24. package/latticeai/core/workspace_os.py +1 -1
  25. package/latticeai/services/agent_runtime.py +52 -12
  26. package/latticeai/services/model_runtime.py +83 -2
  27. package/ltcai_cli.py +14 -3
  28. package/package.json +3 -2
  29. package/requirements.txt +17 -0
  30. package/src-tauri/Cargo.lock +1 -1
  31. package/src-tauri/Cargo.toml +1 -1
  32. package/src-tauri/src/main.rs +257 -25
  33. package/src-tauri/tauri.conf.json +20 -1
  34. package/static/app/asset-manifest.json +3 -3
  35. package/static/app/assets/{index-RiJTJliG.js → index-BhPuj8rT.js} +45 -45
  36. package/static/app/assets/index-BhPuj8rT.js.map +1 -0
  37. package/static/app/index.html +1 -1
  38. package/static/app/assets/index-RiJTJliG.js.map +0 -1
package/README.md CHANGED
@@ -203,31 +203,31 @@ npm run dev
203
203
 
204
204
  ## Latest Release
205
205
 
206
- ### v4.3.0 RC — Portability & Product Hardening
207
-
208
- - **Portable `.latticebrain` archives** — encrypted archives now include the
209
- brain DB, blobs, workspace state, settings, signed bundles, storage metadata,
210
- provenance, and public device identity metadata.
211
- - **Safe backup/restore flows** — archive inspect, verify, import, restore, and
212
- restore dry-run are real API-backed operations; destructive restore requires
213
- explicit admin confirmation.
214
- - **Migration safety** live SQLite-to-Postgres migration creates and verifies
215
- a pre-migration backup before copying data.
216
- - **Desktop hardening** Tauri sidecar startup, status, restart, shutdown, and
217
- loopback-only/default-off guards are hardened.
218
- - **Privacy audit surface** token presence alone no longer enables Telegram or
219
- external connectors; admin status reports storage, backup health, device
220
- identity, permissions, and opt-in integration state.
206
+ ### v4.3.1 RC — End-User Audit Repair
207
+
208
+ - **Desktop startup repair** — Tauri launches a visible app, resolves the
209
+ FastAPI sidecar from installed or bundled runtime paths, reports sidecar
210
+ status, and shuts it down on close.
211
+ - **Clean npm install repair** — npm bootstrap ships the required Python
212
+ requirements file and fails honestly if dependencies cannot be installed.
213
+ - **No default outbound model work** — Model Load refuses implicit downloads or
214
+ runtime installs unless the user gives explicit download/install consent.
215
+ - **Honest Act surfaces** — agent simulation is not recorded as real success,
216
+ and workflow create/import/export/run controls call the existing workflow API.
217
+ - **Storage/portability honesty** — sqlite-vec fallback, Postgres dependency
218
+ status, configured ports, and `.latticebrain` bundle sections match runtime
219
+ behavior.
221
220
  - **Release hardening** — exact-version validation covers wheel, sdist, npm tgz,
222
221
  VSIX, and Tauri DMG artifacts.
223
222
 
224
- See [RELEASE_NOTES_v4.3.0.md](RELEASE_NOTES_v4.3.0.md),
223
+ See [RELEASE_NOTES_v4.3.1.md](RELEASE_NOTES_v4.3.1.md),
224
+ [RELEASE_NOTES_v4.3.0.md](RELEASE_NOTES_v4.3.0.md),
225
225
  [docs/kg-schema.md](docs/kg-schema.md),
226
226
  [FEATURE_STATUS.md](FEATURE_STATUS.md).
227
227
 
228
228
  ## How it works — every source converges into the graph
229
229
 
230
- As of v4.3.0, data sources flow through the brain ingestion pipeline into
230
+ As of v4.3.1, data sources flow through the brain ingestion pipeline into
231
231
  the Knowledge Graph — no source bypasses it, none becomes an isolated silo:
232
232
 
233
233
  ```text
@@ -290,6 +290,7 @@ For the deeper design, see [ARCHITECTURE.md](ARCHITECTURE.md) and
290
290
  ### Releases
291
291
 
292
292
  - [RELEASE_NOTES.md](RELEASE_NOTES.md) — current release notes
293
+ - [RELEASE_NOTES_v4.3.1.md](RELEASE_NOTES_v4.3.1.md)
293
294
  - [RELEASE_NOTES_v4.3.0.md](RELEASE_NOTES_v4.3.0.md)
294
295
  - [RELEASE_NOTES_v4.2.0.md](RELEASE_NOTES_v4.2.0.md)
295
296
  - [RELEASE_NOTES_v4.1.0.md](RELEASE_NOTES_v4.1.0.md)
@@ -305,6 +306,7 @@ For the deeper design, see [ARCHITECTURE.md](ARCHITECTURE.md) and
305
306
 
306
307
  | Version | Theme |
307
308
  | --- | --- |
309
+ | **4.3.1** | End-User Audit Repair RC — desktop sidecar startup, npm clean install, default-off model downloads, honest agent/workflow unavailable states, configured port reporting, storage/portable archive honesty |
308
310
  | **4.3.0** | Portability & Product Hardening RC — portable `.latticebrain` archives, confirmed restore/import, pre-migration backup verification, Tauri sidecar hardening, local-only/default-off integration guards, exact-version DMG validation |
309
311
  | **4.2.0** | Brain Core & Storage Rebuild — independent `lattice_brain` package, pluggable storage layer, sqlite-vec/pgvector capability reporting, explicit Postgres migration, consent-gated Docker setup, encrypted `.latticebrain` archives |
310
312
  | **4.1.0** | Frontend & Desktop Rebuild RC — React/Vite/OpenAPI desktop SPA, Tauri 2.0 primary shell, graph-first navigation, and legacy static frontend removal |
package/bin/ltcai.js CHANGED
@@ -60,9 +60,13 @@ function ensureManagedPython() {
60
60
 
61
61
  if (!canImport(managedPython, "fastapi")) {
62
62
  const requirements = path.join(root, "requirements.txt");
63
+ if (!fs.existsSync(requirements)) {
64
+ console.error(`[LTCAI] Missing ${requirements}. The npm package is incomplete and cannot start the Python server.`);
65
+ process.exit(1);
66
+ }
63
67
  console.log("[LTCAI] Installing Python dependencies. This can take a few minutes on first run.");
64
- if (!runSync(managedPython, ["-m", "pip", "install", "--upgrade", "pip"])) return null;
65
- if (!runSync(managedPython, ["-m", "pip", "install", "-r", requirements])) return null;
68
+ if (!runSync(managedPython, ["-m", "pip", "install", "--upgrade", "pip"])) process.exit(1);
69
+ if (!runSync(managedPython, ["-m", "pip", "install", "-r", requirements])) process.exit(1);
66
70
  }
67
71
 
68
72
  return managedPython;
package/docs/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## [4.3.1] - 2026-06-12
4
+
5
+ > End-user audit repair release candidate for v4.3.0 artifacts.
6
+
7
+ ### Fixed
8
+
9
+ - Tauri desktop startup now launches a visible shell, resolves the FastAPI
10
+ sidecar through installed or bundled runtime paths, reports backend status,
11
+ writes sidecar logs, and shuts down the child process on close.
12
+ - npm clean install now ships `requirements.txt` and fails honestly when Python
13
+ dependency bootstrap cannot complete.
14
+ - Model Load refuses implicit runtime installation and model downloads by
15
+ default, returning actionable unavailable states instead of hanging or making
16
+ unauthorized outbound requests.
17
+ - Agent runtime API refuses simulation-mode execution when no LLM-backed model
18
+ is loaded, preventing fake success records.
19
+ - Workflow UI exposes real create, import, export, and run paths backed by the
20
+ existing workflow API.
21
+ - Runtime version labels, configured port reporting, SSO default redirect URI,
22
+ Postgres dependency status, sqlite-vec fallback reporting, and `.latticebrain`
23
+ bundle claims now match observed behavior.
24
+
25
+ ### Artifacts
26
+
27
+ - `dist/ltcai-4.3.1-py3-none-any.whl`
28
+ - `dist/ltcai-4.3.1.tar.gz`
29
+ - `dist/ltcai-4.3.1.vsix`
30
+ - `ltcai-4.3.1.tgz`
31
+ - `src-tauri/target/release/bundle/dmg/Lattice AI_4.3.1_aarch64.dmg`
32
+
3
33
  ## [4.3.0] - 2026-06-12
4
34
 
5
35
  > Portability & Product Hardening release candidate. v4.3.0 preserves the
@@ -10,9 +40,9 @@
10
40
  ### Added
11
41
 
12
42
  - `.latticebrain` archive format v2 with encrypted graph DB, blobs, portable
13
- JSON state, signed bundles, storage metadata, provenance, public device
14
- identity metadata, manifest hashes, inspect, verify, import, restore, and
15
- restore dry-run.
43
+ JSON state, workspace export bundles when present, storage metadata,
44
+ provenance, public device identity metadata, manifest hashes, inspect,
45
+ verify, import, restore, and restore dry-run.
16
46
  - FastAPI routes for archive inspect/verify/import, backup health, and admin
17
47
  product-hardening status.
18
48
  - Product-hardening status for local-only startup, storage mode, backup health,
@@ -5,28 +5,22 @@
5
5
  > completed analysis. **Update this file before ending any phase and before any
6
6
  > likely session/context/usage limit.**
7
7
  >
8
- > Last updated: 2026-06-12 — v4.3.0 Portability/Product Hardening RC; Remaining Gaps remain empty
8
+ > Last updated: 2026-06-12 — v4.3.1 End-User Audit Repair RC; Remaining Gaps remain empty
9
9
 
10
10
  ---
11
11
 
12
- ## 0. RELEASE STATUS (v4.3.0 RC)
13
-
14
- **v4.3.0 hardens the v4.2 Brain Core/storage architecture into a portable,
15
- user-safe desktop release candidate; implementation gaps remain empty.**
16
- Latest implementation milestone: `.latticebrain` archive format v2 is the
17
- primary portable brain format and carries encrypted graph DB, blobs, portable
18
- JSON state, signed bundles, storage metadata, provenance, public device identity
19
- metadata, manifest hashes, inspect, verify, import, restore, and dry-run restore.
20
- Restore/import requires explicit confirmation unless dry-run. Live
21
- SQLite-to-Postgres migration now creates and verifies a pre-migration backup
22
- before copying data. Tauri exposes backend status/restart/shutdown and starts
23
- the sidecar with loopback/default-off guards. Admin product hardening status
24
- reports local-only startup posture, storage, backup health, device identity,
25
- permissions, and opt-in external integration state.
26
- The v4.3.0 RC process builds validated artifacts only. It does not tag, create a
12
+ ## 0. RELEASE STATUS (v4.3.1 RC)
13
+
14
+ **v4.3.1 repairs the v4.3.0 end-user audit blockers without redesigning the
15
+ Digital Brain frontend, Brain Core, storage, or agent/workflow architecture.**
16
+ Latest implementation milestone: desktop sidecar startup/status/shutdown,
17
+ npm clean install bootstrap, default-off model downloads/engine installs,
18
+ agent simulation refusal, workflow create/import/export/run UI, configured
19
+ port reporting, Postgres dependency status, sqlite-vec fallback honesty, and
20
+ `.latticebrain` bundle-section claims now match observed runtime behavior.
21
+ The v4.3.1 RC process builds validated artifacts only. It does not tag, create a
27
22
  GitHub Release, publish to PyPI, npm Registry, VS Code Marketplace, Open VSX, or
28
23
  deploy to production targets.
29
- v4.3.0 validation report: `docs/V4_3_VALIDATION_REPORT.md`.
30
24
  Remaining implementation gaps: **none**.
31
25
  Owner-only blockers: pptx history rewrite (requires force-push/owner decision)
32
26
  and consent-gated production embedder provisioning (silent default download is
@@ -34,11 +28,11 @@ not permitted).
34
28
 
35
29
  ## Remaining Gaps
36
30
 
37
- None. v4.3.0 preserves the already-empty v4.2.0 gap list and closes the
38
- portability/product-hardening work: `.latticebrain` archives are verified,
39
- inspectable, dry-runnable, and confirmation-gated; default startup is local-only
40
- and token-inert; backup health and product hardening are admin-visible; release
41
- artifact validation includes the exact Tauri DMG. Owner-only blockers above are
31
+ None. v4.3.1 preserves the already-empty v4.3.0 gap list and closes the audit
32
+ repair work: desktop sidecar startup is visible/statused, npm clean install is
33
+ packaged, Model Load is local-only by default, agent simulation is refused as a
34
+ product success state, workflows have real create/import/export/run paths, and
35
+ storage/archive status surfaces are honest. Owner-only blockers above are
42
36
  intentionally not implementation gaps.
43
37
 
44
38
  ## 1. Program Charter (from the user's v4.0.0 directive)
@@ -1349,6 +1349,11 @@
1349
1349
  ],
1350
1350
  "title": "Adapter Path"
1351
1351
  },
1352
+ "allow_download": {
1353
+ "default": false,
1354
+ "title": "Allow Download",
1355
+ "type": "boolean"
1356
+ },
1352
1357
  "draft_model_id": {
1353
1358
  "anyOf": [
1354
1359
  {
@@ -1820,6 +1825,11 @@
1820
1825
  },
1821
1826
  "PrepareModelRequest": {
1822
1827
  "properties": {
1828
+ "allow_download": {
1829
+ "default": false,
1830
+ "title": "Allow Download",
1831
+ "type": "boolean"
1832
+ },
1823
1833
  "engine": {
1824
1834
  "anyOf": [
1825
1835
  {
@@ -3543,7 +3553,7 @@
3543
3553
  },
3544
3554
  "info": {
3545
3555
  "title": "Lattice AI Server (local)",
3546
- "version": "4.3.0"
3556
+ "version": "4.3.1"
3547
3557
  },
3548
3558
  "openapi": "3.1.0",
3549
3559
  "paths": {
@@ -82,6 +82,12 @@ export default function App() {
82
82
  const [drawer, setDrawer] = React.useState(false);
83
83
  const [palette, setPalette] = React.useState(false);
84
84
  const health = useQuery({ queryKey: ["health"], queryFn: latticeApi.health });
85
+ const desktop = useQuery({
86
+ queryKey: ["desktopBackendStatus"],
87
+ queryFn: latticeApi.desktopBackendStatus,
88
+ enabled: Boolean(window.__TAURI_INTERNALS__),
89
+ refetchInterval: 5000,
90
+ });
85
91
  const workspace = useQuery({ queryKey: ["workspaceOs"], queryFn: latticeApi.workspaceOs });
86
92
 
87
93
  React.useEffect(() => {
@@ -98,6 +104,11 @@ export default function App() {
98
104
  return () => window.removeEventListener("keydown", onKey);
99
105
  }, []);
100
106
 
107
+ const healthData = (health.data?.data || {}) as Record<string, unknown>;
108
+ const appVersion = typeof healthData.version === "string" ? healthData.version : null;
109
+ const desktopData = (desktop.data?.data || {}) as Record<string, unknown>;
110
+ const desktopError = typeof desktopData.last_error === "string" ? desktopData.last_error : desktop.data?.error;
111
+
101
112
  const rail = (
102
113
  <aside className="flex h-full w-64 shrink-0 flex-col border-r border-border bg-card">
103
114
  <div className="flex h-16 items-center gap-3 border-b border-border px-4">
@@ -134,6 +145,9 @@ export default function App() {
134
145
  </nav>
135
146
  <div className="border-t border-border p-3 text-xs text-muted-foreground">
136
147
  <div>Server: {health.data?.ok ? "online" : "unavailable"}</div>
148
+ {window.__TAURI_INTERNALS__ ? (
149
+ <div>Sidecar: {desktopData.running ? "running" : desktopError ? `unavailable (${desktopError})` : "starting"}</div>
150
+ ) : null}
137
151
  <div>Workspace: {String((workspace.data?.data as Record<string, unknown>)?.active_workspace || "local")}</div>
138
152
  </div>
139
153
  </aside>
@@ -154,7 +168,7 @@ export default function App() {
154
168
  <div className="flex min-w-0 items-center gap-2">
155
169
  <Button variant="ghost" size="icon" className="lg:hidden" onClick={() => setDrawer(true)}><Menu className="h-5 w-5" /></Button>
156
170
  <div className="min-w-0">
157
- <div className="truncate text-sm text-muted-foreground">v4.1.0 Release Candidate</div>
171
+ <div className="truncate text-sm text-muted-foreground">{appVersion ? `v${appVersion}` : "Version unavailable"}</div>
158
172
  <div className="truncate font-medium">{primaryRoutes.find((item) => item.id === route.primary)?.label}</div>
159
173
  </div>
160
174
  </div>
@@ -38,6 +38,16 @@ async function tauriBackendOrigin(): Promise<string | null> {
38
38
  return desktopBase;
39
39
  }
40
40
 
41
+ async function tauriInvoke<T>(command: string): Promise<T | null> {
42
+ if (!window.__TAURI_INTERNALS__) return null;
43
+ try {
44
+ const { invoke } = await import("@tauri-apps/api/core");
45
+ return await invoke<T>(command);
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
41
51
  async function apiBase() {
42
52
  const stateBase = useAppStore.getState().apiBase;
43
53
  if (stateBase) return stateBase;
@@ -203,6 +213,11 @@ async function streamChat(body: Record<string, unknown>, handlers: ChatEventHand
203
213
 
204
214
  export const latticeApi = {
205
215
  raw: get,
216
+ desktopBackendStatus: async (): Promise<ApiResult<Record<string, unknown>>> => {
217
+ const status = await tauriInvoke<Record<string, unknown>>("backend_status");
218
+ if (status) return { ok: true, status: 200, data: status, source: "live" };
219
+ return { ok: false, status: 0, data: {}, source: "unavailable", error: "Desktop backend status is available only inside the Tauri shell." };
220
+ },
206
221
  health: () => get("/health", {}),
207
222
  workspaceOs: () => get("/workspace/os", { counts: {}, models: {}, workspace_registry: { workspaces: [] } }),
208
223
  indexStatus: () => get("/api/index/status", {}),
@@ -248,7 +263,7 @@ export const latticeApi = {
248
263
  connectFolder: (path: string) => post("/knowledge-graph/local/index", { path, approved: true, watch_enabled: true, consent: { approved: true, source: "desktop-spa" } }, {}),
249
264
  localWatchStop: (source_id: string) => post("/knowledge-graph/local/watch/stop", { source_id }, {}),
250
265
  models: () => get("/models", { catalog: [], loaded: [], recommended: [] }),
251
- loadModel: (model_id: string, engine?: string) => post("/models/load", { model_id, engine: engine || null }, {}),
266
+ loadModel: (model_id: string, engine?: string, allow_download = false) => post("/models/load", { model_id, engine: engine || null, allow_download }, {}),
252
267
  unloadModel: (model_id: string) => del(`/models/unload/${encodeURIComponent(model_id)}`, {}),
253
268
  embeddingsStatus: () => get("/api/embeddings/status", {}),
254
269
  agentRuntime: () => get("/agents/api/runtime/status", { runtime: {}, agents: [], runs: [] }),
@@ -263,6 +278,9 @@ export const latticeApi = {
263
278
  workflowDefinitions: () => get("/workflows/api/definitions", { workflows: [] }),
264
279
  workflowRuns: () => get("/workflows/api/runs", { runs: [] }),
265
280
  workflowTriggers: () => get("/workflows/api/triggers", { armed: [] }),
281
+ createWorkflow: (body: { name: string; nodes: Array<Record<string, unknown>>; metadata?: Record<string, unknown> }) => post("/workflows/api/definitions", body, {}),
282
+ importWorkflow: (data: Record<string, unknown>) => post("/workflows/api/import", { data }, {}),
283
+ exportWorkflow: (id: string) => get(`/workflows/api/export/${encodeURIComponent(id)}`, {}),
266
284
  runWorkflow: (id: string) => post(`/workflows/api/definitions/${encodeURIComponent(id)}/run`, {}, {}),
267
285
  updateWorkflow: (id: string, body: unknown) => patch(`/workflows/api/definitions/${encodeURIComponent(id)}`, body, {}),
268
286
  stopWorkflowRun: (id: string) => post(`/workflows/api/runs/${encodeURIComponent(id)}/stop`, {}, {}),
@@ -6183,6 +6183,11 @@ export interface components {
6183
6183
  LoadModelRequest: {
6184
6184
  /** Adapter Path */
6185
6185
  adapter_path?: string | null;
6186
+ /**
6187
+ * Allow Download
6188
+ * @default false
6189
+ */
6190
+ allow_download: boolean;
6186
6191
  /** Draft Model Id */
6187
6192
  draft_model_id?: string | null;
6188
6193
  /** Engine */
@@ -6416,6 +6421,11 @@ export interface components {
6416
6421
  };
6417
6422
  /** PrepareModelRequest */
6418
6423
  PrepareModelRequest: {
6424
+ /**
6425
+ * Allow Download
6426
+ * @default false
6427
+ */
6428
+ allow_download: boolean;
6419
6429
  /** Engine */
6420
6430
  engine?: string | null;
6421
6431
  /** Model */
@@ -58,6 +58,11 @@ function AgentsPanel() {
58
58
  mutationFn: () => latticeApi.registerAgent({ name: agentName, type: "custom", capabilities: [] }),
59
59
  onSuccess: () => qc.invalidateQueries({ queryKey: ["agentRegistry"] }),
60
60
  });
61
+ const runtimeData = (runtime.data?.data || {}) as Record<string, unknown>;
62
+ const runtimeMeta = (runtimeData.runtime || {}) as Record<string, unknown>;
63
+ const runtimeReady = Boolean(runtimeMeta.ready);
64
+ const runtimeReason = String(runtimeMeta.unavailable_reason || "Load an LLM-backed model before running agents.");
65
+ const canRunAgent = Boolean(goal.trim()) && runtimeReady && !run.isPending;
61
66
  return (
62
67
  <div className="grid gap-4 xl:grid-cols-[0.9fr_1.1fr]">
63
68
  <Card>
@@ -67,7 +72,15 @@ function AgentsPanel() {
67
72
  </CardHeader>
68
73
  <CardContent className="space-y-3">
69
74
  <Textarea value={goal} onChange={(e) => setGoal(e.target.value)} placeholder="Describe the objective..." />
70
- <Button disabled={!goal.trim() || run.isPending} onClick={() => run.mutate()}><Play className="h-4 w-4" /> Run planner/executor/reviewer</Button>
75
+ {!runtimeReady ? <Badge variant="warning">{runtimeReason}</Badge> : null}
76
+ <Button
77
+ className="w-full"
78
+ variant={runtimeReady ? "default" : "outline"}
79
+ disabled={!canRunAgent}
80
+ onClick={() => run.mutate()}
81
+ >
82
+ <Play className="h-4 w-4" /> {runtimeReady ? "Run pipeline" : "Agent execution unavailable"}
83
+ </Button>
71
84
  {run.data ? <JsonView value={run.data.data || run.data.error} /> : null}
72
85
  </CardContent>
73
86
  </Card>
@@ -162,8 +175,26 @@ function RunList({ runs, kind }: { runs: Array<Record<string, unknown>>; kind: "
162
175
  }
163
176
 
164
177
  function WorkflowsPanel() {
178
+ const qc = useQueryClient();
165
179
  const defs = useQuery({ queryKey: ["workflowDefinitions"], queryFn: latticeApi.workflowDefinitions });
166
180
  const triggers = useQuery({ queryKey: ["workflowTriggers"], queryFn: latticeApi.workflowTriggers });
181
+ const [name, setName] = React.useState("Manual workflow");
182
+ const [importText, setImportText] = React.useState("");
183
+ const create = useMutation({
184
+ mutationFn: () => latticeApi.createWorkflow({
185
+ name: name.trim() || "Manual workflow",
186
+ nodes: manualWorkflowNodes(),
187
+ metadata: { created_from: "desktop-act-ui" },
188
+ }),
189
+ onSuccess: () => qc.invalidateQueries({ queryKey: ["workflowDefinitions"] }),
190
+ });
191
+ const importWorkflow = useMutation({
192
+ mutationFn: () => latticeApi.importWorkflow(JSON.parse(importText) as Record<string, unknown>),
193
+ onSuccess: () => {
194
+ setImportText("");
195
+ qc.invalidateQueries({ queryKey: ["workflowDefinitions"] });
196
+ },
197
+ });
167
198
  const workflows = asArray<Record<string, unknown>>((defs.data?.data as Record<string, unknown>)?.workflows);
168
199
  const nodes: Node[] = workflows.slice(0, 12).map((workflow, index) => ({
169
200
  id: String(workflow.id || workflow.workflow_id || index),
@@ -189,7 +220,17 @@ function WorkflowsPanel() {
189
220
  </Card>
190
221
  <DataPanel title="Definitions" result={defs.data}>
191
222
  {() => (
192
- <div className="space-y-2">
223
+ <div className="space-y-3">
224
+ <div className="grid gap-2 rounded-md border border-border p-3">
225
+ <div className="flex flex-wrap gap-2">
226
+ <Input value={name} onChange={(event) => setName(event.target.value)} placeholder="Workflow name" />
227
+ <Button disabled={create.isPending} onClick={() => create.mutate()}>Create</Button>
228
+ </div>
229
+ <Textarea value={importText} onChange={(event) => setImportText(event.target.value)} placeholder="Paste exported workflow JSON" />
230
+ <Button variant="outline" disabled={!importText.trim() || importWorkflow.isPending} onClick={() => importWorkflow.mutate()}>Import</Button>
231
+ {create.data ? <JsonView value={create.data.data || create.data.error} /> : null}
232
+ {importWorkflow.data ? <JsonView value={importWorkflow.data.data || importWorkflow.data.error} /> : null}
233
+ </div>
193
234
  {workflows.length ? workflows.map((workflow) => {
194
235
  const id = String(workflow.id || workflow.workflow_id);
195
236
  return (
@@ -197,6 +238,7 @@ function WorkflowsPanel() {
197
238
  <div className="font-medium">{String(workflow.name || id)}</div>
198
239
  <div className="mt-2 flex gap-2">
199
240
  <ActionButton label="Run" action={() => latticeApi.runWorkflow(id)} invalidate={["workflowRuns"]} />
241
+ <ActionButton label="Export" action={() => latticeApi.exportWorkflow(id)} />
200
242
  </div>
201
243
  </div>
202
244
  );
@@ -211,6 +253,25 @@ function WorkflowsPanel() {
211
253
  );
212
254
  }
213
255
 
256
+ function manualWorkflowNodes(): Array<Record<string, unknown>> {
257
+ return [
258
+ {
259
+ id: "trigger",
260
+ type: "trigger",
261
+ name: "Manual start",
262
+ config: { trigger: "manual" },
263
+ next: "output",
264
+ },
265
+ {
266
+ id: "output",
267
+ type: "output",
268
+ name: "Output",
269
+ config: { value: "Workflow completed" },
270
+ next: null,
271
+ },
272
+ ];
273
+ }
274
+
214
275
  function HooksPanel() {
215
276
  const hooks = useQuery({ queryKey: ["hooks"], queryFn: latticeApi.hooks });
216
277
  const runs = useQuery({ queryKey: ["hookRuns"], queryFn: latticeApi.hookRuns });
@@ -40,7 +40,6 @@ export function LibraryPage({ initialTab }: { initialTab?: string }) {
40
40
  }
41
41
 
42
42
  function ModelsPanel() {
43
- const qc = useQueryClient();
44
43
  const models = useQuery({ queryKey: ["models"], queryFn: latticeApi.models });
45
44
  const emb = useQuery({ queryKey: ["embeddings"], queryFn: latticeApi.embeddingsStatus });
46
45
  const catalog = [
@@ -55,18 +54,25 @@ function ModelsPanel() {
55
54
  {(catalog.length ? catalog : asArray<Record<string, unknown>>((data as Record<string, unknown>).loaded)).slice(0, 14).map((model, index) => {
56
55
  const id = String(model.id || model.model_id || model.name || index);
57
56
  const loaded = asArray<string>((data as Record<string, unknown>).loaded).includes(id) || (data as Record<string, unknown>).current === id || model.state === "loaded";
57
+ const loadId = String(model.recommended_load_id || id);
58
+ const engine = String(model.recommended_engine || model.engine || "");
59
+ const loadAvailable = Boolean(model.load_available) || loaded;
60
+ const loadStatus = String(model.load_status || (loaded ? "loaded" : "unavailable"));
61
+ const unavailableReason = String(model.unavailable_reason || "Unavailable until the backend reports a local model/runtime ready.");
58
62
  return (
59
63
  <div key={id} className="flex flex-wrap items-center justify-between gap-3 rounded-md border border-border bg-background p-3">
60
64
  <div>
61
65
  <div className="font-medium">{String(model.name || id)}</div>
62
66
  <div className="text-sm text-muted-foreground">{String(model.family || model.engine || model.recommended_engine || "local")}</div>
67
+ {!loaded && !loadAvailable ? <div className="mt-1 text-xs text-muted-foreground">{unavailableReason}</div> : null}
63
68
  </div>
64
69
  <div className="flex items-center gap-2">
65
- <Badge variant={loaded ? "success" : "muted"}>{loaded ? "loaded" : "available"}</Badge>
70
+ <Badge variant={loaded ? "success" : loadAvailable ? "muted" : "warning"}>{loaded ? "loaded" : loadStatus}</Badge>
66
71
  <ActionButton
67
72
  label={loaded ? "Unload" : "Load"}
68
- action={() => loaded ? latticeApi.unloadModel(id) : latticeApi.loadModel(id, String(model.recommended_engine || model.engine || ""))}
73
+ action={() => loaded ? latticeApi.unloadModel(loadId) : latticeApi.loadModel(loadId, engine, false)}
69
74
  invalidate={["models"]}
75
+ disabled={!loaded && !loadAvailable}
70
76
  />
71
77
  </div>
72
78
  </div>
@@ -19,7 +19,7 @@ from .storage import (
19
19
  storage_from_env,
20
20
  )
21
21
 
22
- __version__ = "4.3.0"
22
+ __version__ = "4.3.1"
23
23
 
24
24
  __all__ = [
25
25
  "AssembledContext",
@@ -1,9 +1,9 @@
1
1
  """Encrypted .latticebrain archive support.
2
2
 
3
3
  The archive is intentionally self-contained and local-only: the encrypted
4
- payload holds the SQLite brain, blob store, portable JSON state, signed export
5
- bundles, and public metadata needed to inspect/verify/restore on another
6
- machine without contacting a service.
4
+ payload holds the SQLite brain, blob store, portable JSON state, workspace
5
+ export bundles when present, and public metadata needed to inspect/verify/
6
+ restore on another machine without contacting a service.
7
7
  """
8
8
 
9
9
  from __future__ import annotations
@@ -77,7 +77,12 @@ class SQLiteEngine(StorageEngine):
77
77
  backup_restore=True,
78
78
  migrations=True,
79
79
  encrypted_archives=True,
80
- metadata={"db_path": str(self.db_path), "sqlite_vec_loaded": False},
80
+ metadata={
81
+ "db_path": str(self.db_path),
82
+ "sqlite_vec_loaded": False,
83
+ "vector_mode": "fallback",
84
+ "honest_fallback": "sqlite-vec has not been probed yet; vector search uses the real brute-force cosine fallback until sqlite-vec is loaded.",
85
+ },
81
86
  )
82
87
  # Probe on demand so status is accurate even before the graph opens.
83
88
  try:
@@ -94,7 +99,11 @@ class SQLiteEngine(StorageEngine):
94
99
  return StorageCapabilities(
95
100
  engine=self.name,
96
101
  available=True,
97
- reason=None if self._sqlite_vec_loaded else self._sqlite_vec_reason,
102
+ reason=None if self._sqlite_vec_loaded else (
103
+ f"{self._sqlite_vec_reason}; using real brute-force cosine fallback, not sqlite-vec ANN"
104
+ if self._sqlite_vec_reason
105
+ else "sqlite-vec unavailable; using real brute-force cosine fallback, not sqlite-vec ANN"
106
+ ),
98
107
  vector_backend=vector_backend,
99
108
  vector_available=True,
100
109
  backup_restore=True,
@@ -103,6 +112,10 @@ class SQLiteEngine(StorageEngine):
103
112
  metadata={
104
113
  "db_path": str(self.db_path),
105
114
  "sqlite_vec_loaded": self._sqlite_vec_loaded,
115
+ "sqlite_vec_ann_available": self._sqlite_vec_loaded,
116
+ "vector_mode": "sqlite-vec" if self._sqlite_vec_loaded else "fallback",
117
+ "degraded": not self._sqlite_vec_loaded,
118
+ "honest_fallback": None if self._sqlite_vec_loaded else "Vector search is available through the deterministic brute-force cosine backend. sqlite-vec ANN is unavailable.",
106
119
  },
107
120
  )
108
121
 
@@ -1,3 +1,3 @@
1
1
  """Lattice AI - modular server package."""
2
2
 
3
- __version__ = "4.3.0"
3
+ __version__ = "4.3.1"
@@ -46,7 +46,7 @@ def create_agents_router(
46
46
  run_executor: Any = None,
47
47
  ) -> APIRouter:
48
48
  from latticeai.core.multi_agent import AGENT_ROLES, ROLE_AGENT_IDS
49
- from latticeai.services.agent_runtime import AgentRuntime
49
+ from latticeai.services.agent_runtime import AgentRuntime, AgentRuntimeUnavailable
50
50
 
51
51
  # Single AgentRuntime boundary: the router (and via it, the frontend) talks
52
52
  # to this façade instead of reaching into the orchestrator/store directly.
@@ -186,6 +186,8 @@ def create_agents_router(
186
186
  )
187
187
  except ValueError as exc:
188
188
  raise HTTPException(status_code=400, detail=str(exc)) from exc
189
+ except AgentRuntimeUnavailable as exc:
190
+ raise HTTPException(status_code=409, detail=str(exc)) from exc
189
191
  except PermissionError as exc:
190
192
  # A pre_run hook gated this run (e.g. a policy/permission hook).
191
193
  raise HTTPException(status_code=403, detail=str(exc)) from exc