ltcai 10.9.0 → 11.0.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 (126) hide show
  1. package/README.md +46 -64
  2. package/docs/CHANGELOG.md +72 -237
  3. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  4. package/docs/DEVELOPMENT.md +1 -1
  5. package/docs/ONBOARDING.md +1 -1
  6. package/docs/OPERATIONS.md +1 -1
  7. package/docs/PERFORMANCE.md +78 -7
  8. package/docs/TRUST_MODEL.md +1 -1
  9. package/docs/WHY_LATTICE.md +1 -1
  10. package/docs/kg-schema.md +1 -1
  11. package/docs/v11.1.0_PRODUCT_INTELLIGENCE_PLAN.md +313 -0
  12. package/lattice_brain/__init__.py +1 -1
  13. package/lattice_brain/graph/_kg_contract.py +11 -0
  14. package/lattice_brain/graph/curator.py +1 -1
  15. package/lattice_brain/graph/fusion.py +184 -3
  16. package/lattice_brain/graph/proactive.py +138 -1
  17. package/lattice_brain/graph/projection.py +10 -2
  18. package/lattice_brain/graph/retrieval.py +137 -7
  19. package/lattice_brain/graph/retrieval_docgen.py +20 -20
  20. package/lattice_brain/graph/retrieval_policy.py +6 -0
  21. package/lattice_brain/graph/retrieval_reads.py +188 -1
  22. package/lattice_brain/graph/retrieval_vector.py +474 -110
  23. package/lattice_brain/graph/schema.py +125 -2
  24. package/lattice_brain/graph/vector_index/__init__.py +85 -0
  25. package/lattice_brain/graph/vector_index/base.py +170 -0
  26. package/lattice_brain/graph/vector_index/brute_force.py +114 -0
  27. package/lattice_brain/graph/vector_index/hnsw.py +293 -0
  28. package/lattice_brain/graph/vector_index/jobs.py +287 -0
  29. package/lattice_brain/graph/vector_index/quantized.py +151 -0
  30. package/lattice_brain/graph/vector_index/selector.py +131 -0
  31. package/lattice_brain/ingestion.py +50 -3
  32. package/lattice_brain/portability.py +654 -2
  33. package/lattice_brain/runtime/agent_runtime.py +1 -1
  34. package/lattice_brain/runtime/contracts.py +1 -1
  35. package/lattice_brain/runtime/multi_agent.py +1 -1
  36. package/lattice_brain/self_model.py +620 -0
  37. package/lattice_brain/synthesis.py +801 -0
  38. package/latticeai/__init__.py +1 -1
  39. package/latticeai/api/brain_intelligence.py +83 -1
  40. package/latticeai/api/chat_stream.py +4 -1
  41. package/latticeai/api/local_files.py +62 -0
  42. package/latticeai/api/models.py +1 -1
  43. package/latticeai/api/portability.py +130 -1
  44. package/latticeai/api/security_dashboard.py +48 -13
  45. package/latticeai/api/voice_capture.py +4 -1
  46. package/latticeai/api/workspace.py +11 -5
  47. package/latticeai/core/embedding_providers.py +20 -1
  48. package/latticeai/core/legacy_compatibility.py +1 -1
  49. package/latticeai/core/marketplace.py +1 -1
  50. package/latticeai/core/messages.py +14 -0
  51. package/latticeai/core/model_compat.py +2 -2
  52. package/latticeai/core/tool_registry.py +0 -7
  53. package/latticeai/core/workspace_os_constants.py +1 -1
  54. package/latticeai/core/workspace_os_utils.py +4 -50
  55. package/latticeai/core/workspace_review_items.py +12 -1
  56. package/latticeai/integrations/telegram_bot.py +28 -10
  57. package/latticeai/models/router.py +1 -1
  58. package/latticeai/runtime/access_runtime.py +1 -1
  59. package/latticeai/runtime/network_boundary_wiring.py +9 -5
  60. package/latticeai/runtime/permission_mode_wiring.py +9 -6
  61. package/latticeai/runtime/router_registration.py +3 -0
  62. package/latticeai/services/architecture_readiness.py +1 -1
  63. package/latticeai/services/brain_intelligence.py +253 -0
  64. package/latticeai/services/memory_service.py +1 -1
  65. package/latticeai/services/model_catalog.py +4 -3
  66. package/latticeai/services/model_engines.py +28 -14
  67. package/latticeai/services/obsidian_bridge.py +618 -0
  68. package/latticeai/services/product_readiness.py +5 -3
  69. package/latticeai/tools/filesystem.py +4 -1
  70. package/package.json +1 -1
  71. package/scripts/bench_vector_index.py +295 -0
  72. package/scripts/check_current_release_docs.mjs +4 -2
  73. package/scripts/release_screen_claims.json +31 -0
  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-CS9IeqUX.js → Act-D4zSxFR-.js} +1 -1
  79. package/static/app/assets/{AdminConsole-3UkIEWGA.js → AdminConsole-w5jBfPt2.js} +1 -1
  80. package/static/app/assets/{Brain-B22EmNqS.js → Brain-C2EqQg74.js} +2 -2
  81. package/static/app/assets/BrainHome-CvXS6XiQ.js +2 -0
  82. package/static/app/assets/BrainSignals-DOE_KhOU.js +1 -0
  83. package/static/app/assets/Capture-DPqpGK8d.js +1 -0
  84. package/static/app/assets/{CommandPalette-86m4FCcN.js → CommandPalette-CNf7h5fp.js} +1 -1
  85. package/static/app/assets/Library-BN0HYOfc.js +1 -0
  86. package/static/app/assets/LivingBrain-Dfq_wEDI.js +1 -0
  87. package/static/app/assets/ProductFlow-B-3O0rNV.js +1 -0
  88. package/static/app/assets/{ReviewCard-BepjSDpN.js → ReviewCard-gZ-tdqFM.js} +1 -1
  89. package/static/app/assets/System-BElUcSSw.js +1 -0
  90. package/static/app/assets/arrow-left-CFNIMjhv.js +1 -0
  91. package/static/app/assets/{bot-DQj0-LkM.js → bot--qYHMtkP.js} +1 -1
  92. package/static/app/assets/brain-DDCLjRqO.js +1 -0
  93. package/static/app/assets/{button-CmTknyAP.js → button-51Z3rsuv.js} +1 -1
  94. package/static/app/assets/{circle-pause-yTCWRziJ.js → circle-pause-CMIiMaQl.js} +1 -1
  95. package/static/app/assets/{circle-play-Ccrva84R.js → circle-play-DZoO_cfG.js} +1 -1
  96. package/static/app/assets/{cpu-CbJqWTlS.js → cpu-Bs6uc9W9.js} +1 -1
  97. package/static/app/assets/{download-CkSzbzU-.js → download-G-2olkWz.js} +1 -1
  98. package/static/app/assets/{folder-open-CKyjQ4PU.js → folder-open-CTOspnmb.js} +1 -1
  99. package/static/app/assets/{hard-drive-DAzk9um0.js → hard-drive-CewHWJhn.js} +1 -1
  100. package/static/app/assets/index-CkzokZAj.css +2 -0
  101. package/static/app/assets/{index-CxOcwsHV.js → index-D7Rr-J2Y.js} +3 -3
  102. package/static/app/assets/{input-DcMETmZ7.js → input-D4w_BZWl.js} +1 -1
  103. package/static/app/assets/{permissionCopy-BVf13_25.js → permissionCopy-CosBEXAZ.js} +1 -1
  104. package/static/app/assets/primitives-d0g9pvzS.js +1 -0
  105. package/static/app/assets/search-BLCYt75v.js +1 -0
  106. package/static/app/assets/{share-2-COWCHNZm.js → share-2-NmD7e_oV.js} +1 -1
  107. package/static/app/assets/{shield-alert-BDrvilyK.js → shield-alert-CcQeMuju.js} +1 -1
  108. package/static/app/assets/{textarea-rUmsc8cP.js → textarea-BPAJDc-0.js} +1 -1
  109. package/static/app/assets/{useFocusTrap-Bi5UY_8v.js → useFocusTrap-C7YLdTBC.js} +1 -1
  110. package/static/app/assets/{useQuery-C-AicB-3.js → useQuery-DRyD9opW.js} +1 -1
  111. package/static/app/assets/{utils-BMwWg78e.js → utils-DG1_ExrP.js} +3 -3
  112. package/static/app/assets/{workspace-Y93tls8P.js → workspace-CWVf3gsI.js} +1 -1
  113. package/static/app/index.html +4 -4
  114. package/static/sw.js +1 -1
  115. package/static/app/assets/BrainHome-CMDqgJF4.js +0 -2
  116. package/static/app/assets/BrainSignals-BeE8RJo3.js +0 -1
  117. package/static/app/assets/Capture-ZX9bQh68.js +0 -1
  118. package/static/app/assets/Library-Bhz5LUca.js +0 -1
  119. package/static/app/assets/LivingBrain-BSa0wpFG.js +0 -1
  120. package/static/app/assets/ProductFlow-IP4Q-5aQ.js +0 -1
  121. package/static/app/assets/System-Bwx1h_jT.js +0 -1
  122. package/static/app/assets/arrow-left-ig7AZU8B.js +0 -1
  123. package/static/app/assets/brain-D8OEwmVj.js +0 -1
  124. package/static/app/assets/index-BfD-jhA9.css +0 -2
  125. package/static/app/assets/primitives-CQV9Q2YM.js +0 -1
  126. package/static/app/assets/search-CXGASMMH.js +0 -1
@@ -13,8 +13,11 @@ from __future__ import annotations
13
13
 
14
14
  from typing import TYPE_CHECKING
15
15
 
16
+ from ..quiet import quiet
17
+
16
18
  # ruff: noqa: F403,F405
17
19
  from ._kg_common import * # noqa: F403,F401
20
+ from .schema import TEMPORAL_PREDICATE_SQL
18
21
 
19
22
  # The cross-mixin surface (`_connect`, `_upsert_node`, …) is declared in
20
23
  # `_kg_contract.KnowledgeGraphCore`. It is a typing-only base: at runtime this
@@ -25,6 +28,39 @@ else:
25
28
  _Core = object
26
29
 
27
30
 
31
+ def _as_of_stamp(value: Any) -> str:
32
+ """Normalize an ``as_of`` argument into the store's own timestamp format.
33
+
34
+ The store writes naive local ISO-8601 seconds (``lattice_brain.utils.now_iso``),
35
+ so validity comparisons are plain lexicographic string comparisons against
36
+ that one format. An aware datetime is converted to local time first rather
37
+ than compared with an offset suffix the stored rows never carry.
38
+ """
39
+ if isinstance(value, datetime):
40
+ moment = value.astimezone().replace(tzinfo=None) if value.tzinfo else value
41
+ return moment.isoformat(timespec="seconds")
42
+ text = str(value or "").strip()
43
+ if not text:
44
+ raise ValueError("as_of requires a timestamp")
45
+ return text
46
+
47
+
48
+ def _record_access(db_path: Any, node_id: str) -> None:
49
+ """Record one access on a node (the importance/decay input).
50
+
51
+ Runs on its own short transaction, deliberately *outside* the read that
52
+ triggered it: promoting every node read to a writer would let a concurrent
53
+ ingest's write lock fail an otherwise perfectly good read. The counter is a
54
+ nice-to-have, so every sqlite failure — a missing row, a missing column, a
55
+ busy database — is swallowed.
56
+ """
57
+ if KGStoreV2 is None:
58
+ return
59
+ try:
60
+ KGStoreV2(db_path).touch_node(node_id)
61
+ except sqlite3.Error:
62
+ quiet()
63
+
28
64
 
29
65
  class KnowledgeGraphReadsMixin(_Core):
30
66
  def list_documents(self, limit: int = 200) -> Dict[str, Any]:
@@ -173,14 +209,22 @@ class KnowledgeGraphReadsMixin(_Core):
173
209
  *,
174
210
  allowed_workspaces=None,
175
211
  include_legacy_global: bool = False,
212
+ as_of=None,
176
213
  ) -> Dict[str, Any]:
177
- """Return direct neighbors (1-hop) of a node."""
214
+ """Return direct neighbors (1-hop) of a node.
215
+
216
+ ``as_of`` is additive and defaults to ``None`` — the historical call
217
+ keeps its exact behaviour. With a timestamp, neighbors whose validity
218
+ window does not cover that instant are dropped (and with them the edges
219
+ that would dangle).
220
+ """
178
221
  if allowed_workspaces is not None and not self.filter_scoped_nodes(
179
222
  [{"id": node_id}],
180
223
  allowed_workspaces,
181
224
  include_legacy_global=include_legacy_global,
182
225
  ):
183
226
  raise ValueError(f"graph node not found: {node_id}")
227
+ stamp = None if as_of is None else _as_of_stamp(as_of)
184
228
  nt, et = self._read_tables()
185
229
  with self._connect() as conn:
186
230
  edge_rows = conn.execute(
@@ -217,6 +261,14 @@ class KnowledgeGraphReadsMixin(_Core):
217
261
  list(neighbor_ids),
218
262
  )
219
263
  ]
264
+ if stamp is not None:
265
+ valid = self._valid_node_ids_at(conn, stamp)
266
+ nodes = [node for node in nodes if str(node.get("id")) in valid]
267
+ kept = {str(node.get("id")) for node in nodes} | {node_id}
268
+ edges = [
269
+ edge for edge in edges
270
+ if edge.get("from") in kept and edge.get("to") in kept
271
+ ]
220
272
  if allowed_workspaces is not None:
221
273
  nodes = self.filter_scoped_nodes(
222
274
  nodes,
@@ -257,6 +309,10 @@ class KnowledgeGraphReadsMixin(_Core):
257
309
  f"SELECT COUNT(*) AS c FROM {et} WHERE from_node=? OR to_node=?",
258
310
  (node_id, node_id),
259
311
  ).fetchone()["c"]
312
+ # Fetching a node by id is the one unambiguous "this memory was used"
313
+ # signal the read path has; it feeds importance_report(). Recorded
314
+ # after the read transaction closes so it can never fail the read.
315
+ _record_access(self.db_path, node_id)
260
316
  node = {
261
317
  "id": row["id"],
262
318
  "type": row["type"],
@@ -274,6 +330,137 @@ class KnowledgeGraphReadsMixin(_Core):
274
330
  raise ValueError(f"graph node not found: {node_id}")
275
331
  return node
276
332
 
333
+ # ── temporal reads (v11.1.0) ─────────────────────────────────────────
334
+ #
335
+ # Validity lives on ``nodes_v2``/``edges_v2``, the authoritative v2
336
+ # projection — the same table ``workspaces_of`` already reads for scoping,
337
+ # and the only one that carries the columns whichever read mode
338
+ # ``_read_tables()`` is in. The legacy compatibility tables have no
339
+ # temporal dimension, so slicing them would silently answer "everything".
340
+
341
+ def _valid_node_ids_at(self, conn: sqlite3.Connection, stamp: str) -> set:
342
+ """Ids of nodes whose validity window covers ``stamp``."""
343
+ return {
344
+ row["id"]
345
+ for row in conn.execute(
346
+ f"SELECT id FROM nodes_v2 WHERE {TEMPORAL_PREDICATE_SQL}",
347
+ (stamp, stamp),
348
+ ).fetchall()
349
+ }
350
+
351
+ def access_stats(self, node_ids=None) -> Dict[str, Dict[str, Any]]:
352
+ """Per-node access bookkeeping read from the authoritative projection.
353
+
354
+ ``{node_id: {"accesses": float, "last_used": str | None}}`` for the ids
355
+ requested (all nodes when ``node_ids`` is ``None``). Counts come from
356
+ ``_touch_node``; a node never opened simply reports ``0.0``.
357
+ """
358
+ query = "SELECT id, importance_score, last_used FROM nodes_v2"
359
+ params: List[Any] = []
360
+ if node_ids is not None:
361
+ ids = sorted({str(node_id) for node_id in node_ids if node_id})
362
+ if not ids:
363
+ return {}
364
+ query += f" WHERE id IN ({','.join('?' * len(ids))})"
365
+ params = list(ids)
366
+ with self._connect() as conn:
367
+ return {
368
+ row["id"]: {
369
+ "accesses": float(row["importance_score"] or 0.0),
370
+ "last_used": row["last_used"],
371
+ }
372
+ for row in conn.execute(query, params).fetchall()
373
+ }
374
+
375
+ def as_of(
376
+ self,
377
+ timestamp,
378
+ *,
379
+ limit: int = 300,
380
+ allowed_workspaces=None,
381
+ include_legacy_global: bool = False,
382
+ ) -> Dict[str, Any]:
383
+ """The graph slice that was valid at ``timestamp``.
384
+
385
+ Answers "what did I know in June 2025?": nodes and edges whose
386
+ ``[valid_from, valid_to)`` window covers the instant, with
387
+ ``valid_from`` falling back to ``created_at`` so a Brain that predates
388
+ the temporal columns reads as "always was true" rather than as empty.
389
+ Edges are returned only when *both* endpoints are in the slice.
390
+ """
391
+ stamp = _as_of_stamp(timestamp)
392
+ # An explicit 0 clamps to 1 rather than re-expanding to the default:
393
+ # `limit or 300` would quietly return 300 rows to a caller who asked
394
+ # for none.
395
+ try:
396
+ limit = max(1, min(int(limit), 2000))
397
+ except (TypeError, ValueError):
398
+ limit = 300
399
+ scope_sql, scope_params = self._workspace_scope_sql(
400
+ allowed_workspaces, include_legacy_global
401
+ )
402
+ where = [TEMPORAL_PREDICATE_SQL]
403
+ params: List[Any] = [stamp, stamp]
404
+ if scope_sql is not None:
405
+ where.append(f"({scope_sql})")
406
+ params.extend(scope_params)
407
+ with self._connect() as conn:
408
+ node_rows = conn.execute(
409
+ "SELECT id, COALESCE(legacy_type, type) AS type, label AS title, "
410
+ "summary, attrs AS metadata_json, updated_at, "
411
+ "valid_from, valid_to, superseded_by "
412
+ f"FROM nodes_v2 WHERE {' AND '.join(where)} "
413
+ "ORDER BY updated_at DESC, id ASC LIMIT ?",
414
+ (*params, limit),
415
+ ).fetchall()
416
+ nodes = [
417
+ {
418
+ "id": row["id"],
419
+ "type": row["type"],
420
+ "title": row["title"],
421
+ "summary": row["summary"],
422
+ "metadata": _safe_loads(row["metadata_json"]),
423
+ "updated_at": row["updated_at"],
424
+ "valid_from": row["valid_from"],
425
+ "valid_to": row["valid_to"],
426
+ "superseded_by": row["superseded_by"],
427
+ }
428
+ for row in node_rows
429
+ ]
430
+ edges: List[Dict[str, Any]] = []
431
+ visible = sorted(str(node["id"]) for node in nodes)
432
+ if visible:
433
+ placeholders = ",".join("?" * len(visible))
434
+ edges = [
435
+ {
436
+ "id": row["id"],
437
+ "from": row["from_node"],
438
+ "to": row["to_node"],
439
+ "type": row["type"],
440
+ "weight": row["weight"],
441
+ "metadata": _safe_loads(row["metadata_json"]),
442
+ "valid_from": row["valid_from"],
443
+ "valid_to": row["valid_to"],
444
+ "superseded_by": row["superseded_by"],
445
+ }
446
+ for row in conn.execute(
447
+ "SELECT id, source AS from_node, target AS to_node, "
448
+ "COALESCE(legacy_type, type) AS type, weight, "
449
+ "metadata AS metadata_json, valid_from, valid_to, superseded_by "
450
+ f"FROM edges_v2 WHERE {TEMPORAL_PREDICATE_SQL} "
451
+ f"AND source IN ({placeholders}) AND target IN ({placeholders}) "
452
+ "ORDER BY weight DESC, created_at DESC, id ASC",
453
+ (stamp, stamp, *visible, *visible),
454
+ ).fetchall()
455
+ ]
456
+ return {
457
+ "as_of": stamp,
458
+ "nodes": nodes,
459
+ "edges": edges,
460
+ "node_count": len(nodes),
461
+ "edge_count": len(edges),
462
+ }
463
+
277
464
  def relationship_search(
278
465
  self,
279
466
  *,