ltcai 11.5.2 → 11.7.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 (443) hide show
  1. package/README.md +93 -148
  2. package/bin/ltcai.js +234 -24
  3. package/docs/CHANGELOG.md +119 -0
  4. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  5. package/docs/DEVELOPMENT.md +8 -6
  6. package/docs/ENTERPRISE.md +2 -1
  7. package/docs/MULTI_AGENT_RUNTIME.md +12 -5
  8. package/docs/ONBOARDING.md +1 -1
  9. package/docs/OPERATIONS.md +6 -3
  10. package/docs/REALTIME_COLLABORATION.md +1 -1
  11. package/docs/TRUST_MODEL.md +1 -1
  12. package/docs/WHY_LATTICE.md +1 -1
  13. package/docs/WORKFLOW_DESIGNER.md +3 -2
  14. package/docs/kg-schema.md +1 -1
  15. package/docs/v11.6.0_ONE_DOOR_PLAN.md +170 -0
  16. package/lattice_brain/__init__.py +42 -79
  17. package/lattice_brain/graph/__init__.py +9 -24
  18. package/lattice_brain/graph/_kg_common/__init__.py +13 -24
  19. package/lattice_brain/ingestion/__init__.py +19 -58
  20. package/lattice_brain/ingestion/pipeline.py +20 -398
  21. package/lattice_brain/multimodal/__init__.py +7 -18
  22. package/lattice_brain/multimodal/images.py +6 -264
  23. package/lattice_brain/multimodal/video.py +8 -247
  24. package/lattice_brain/runtime/__init__.py +8 -79
  25. package/lattice_brain/runtime/hooks.py +22 -584
  26. package/latticeai/__init__.py +1 -1
  27. package/latticeai/api/agent_worker_seam.py +7 -80
  28. package/latticeai/api/health.py +6 -20
  29. package/latticeai/api/local_files.py +16 -613
  30. package/latticeai/api/models.py +12 -87
  31. package/latticeai/api/search.py +17 -255
  32. package/latticeai/api/tools.py +69 -750
  33. package/latticeai/api/voice_capture.py +8 -70
  34. package/latticeai/api/worker_compute.py +842 -0
  35. package/latticeai/api/worker_seams.py +216 -0
  36. package/latticeai/app_factory.py +23 -232
  37. package/latticeai/cli/entrypoint.py +36 -249
  38. package/latticeai/core/agent_permission.py +16 -91
  39. package/latticeai/core/messages.py +89 -531
  40. package/latticeai/runtime/access_runtime.py +0 -14
  41. package/latticeai/runtime/bootstrap.py +10 -21
  42. package/latticeai/runtime/brain_runtime.py +35 -43
  43. package/latticeai/runtime/build_phases/__init__.py +21 -37
  44. package/latticeai/runtime/build_phases/features.py +99 -389
  45. package/latticeai/runtime/build_phases/foundation.py +82 -392
  46. package/latticeai/runtime/build_phases/web.py +91 -408
  47. package/latticeai/runtime/build_phases/worker_profile.py +244 -0
  48. package/latticeai/runtime/lifespan_runtime.py +7 -16
  49. package/latticeai/runtime/platform_services_runtime.py +1 -15
  50. package/latticeai/runtime/runtime_context.py +13 -130
  51. package/latticeai/runtime/security_runtime.py +10 -103
  52. package/latticeai/services/architecture_readiness.py +82 -43
  53. package/latticeai/services/model_runtime/__init__.py +2 -11
  54. package/latticeai/services/model_runtime/service.py +4 -28
  55. package/latticeai/services/p_reinforce.py +10 -261
  56. package/latticeai/services/product_readiness.py +31 -38
  57. package/latticeai/services/search_service.py +37 -795
  58. package/latticeai/services/tool_dispatch.py +30 -386
  59. package/latticeai/services/voice_capture.py +13 -107
  60. package/latticeai/tools/__init__.py +22 -49
  61. package/latticeai/tools/commands.py +0 -163
  62. package/latticeai/tools/computer.py +0 -39
  63. package/latticeai/tools/documents.py +1 -134
  64. package/latticeai/tools/filesystem.py +1 -247
  65. package/latticeai/tools/knowledge.py +1 -52
  66. package/latticeai/tools/local_files.py +0 -20
  67. package/latticeai/worker_app.py +75 -0
  68. package/package.json +2 -3
  69. package/requirements.txt +0 -5
  70. package/scripts/agent_eval.py +16 -28
  71. package/scripts/brain_quality_eval.py +20 -183
  72. package/scripts/bump_version.py +5 -5
  73. package/scripts/check_current_release_docs.mjs +7 -4
  74. package/scripts/check_openapi_drift.mjs +10 -0
  75. package/scripts/check_server_i18n.mjs +2 -18
  76. package/scripts/compose_openapi.py +377 -0
  77. package/scripts/export_openapi.py +32 -4
  78. package/scripts/gen_messages_catalog_fixture.py +302 -0
  79. package/scripts/gen_openapi_fragments.py +365 -0
  80. package/scripts/gen_redact_fixture.py +196 -0
  81. package/scripts/gen_worker_allowlist_fixture.py +115 -0
  82. package/scripts/generate_agent_parity_fixtures.py +33 -14
  83. package/scripts/openapi_route_families.json +2161 -0
  84. package/scripts/release_screen_claims.json +54 -0
  85. package/scripts/run_integration_tests.mjs +134 -29
  86. package/scripts/run_sidecar_e2e.mjs +104 -11
  87. package/scripts/wheel_smoke.py +32 -22
  88. package/src-tauri/Cargo.lock +375 -9
  89. package/src-tauri/Cargo.toml +1 -1
  90. package/src-tauri/src/backend.rs +151 -45
  91. package/src-tauri/src/main.rs +18 -18
  92. package/src-tauri/src/topology.rs +58 -88
  93. package/src-tauri/tauri.conf.json +1 -2
  94. package/static/app/asset-manifest.json +41 -41
  95. package/static/app/assets/{Act-DcQizkl1.js → Act-BPcVAbOL.js} +1 -1
  96. package/static/app/assets/AdminConsole-Bw1ATQL0.js +1 -0
  97. package/static/app/assets/{Brain-3VCSHFcn.js → Brain-CT92Kos0.js} +2 -2
  98. package/static/app/assets/{BrainHome-Qm8eaztx.js → BrainHome-CFBkt1K_.js} +1 -1
  99. package/static/app/assets/{BrainSignals-DS9BtKOW.js → BrainSignals-ReLWF2H8.js} +1 -1
  100. package/static/app/assets/Capture-BsTokYkk.js +1 -0
  101. package/static/app/assets/{Chronicle-BGvuAchH.js → Chronicle-B6f0T9id.js} +1 -1
  102. package/static/app/assets/{CommandPalette-Bqhm0Urn.js → CommandPalette-CuvjTv1u.js} +1 -1
  103. package/static/app/assets/{Library-BV6NnF0a.js → Library-BGJbG9Hd.js} +1 -1
  104. package/static/app/assets/{LivingBrain-GzenJchP.js → LivingBrain-DGYK_Jsa.js} +1 -1
  105. package/static/app/assets/{ProductFlow-DEP6-vML.js → ProductFlow-DXBC6brE.js} +1 -1
  106. package/static/app/assets/{ReviewCard-CNZ7XjWG.js → ReviewCard-HXRle3qq.js} +2 -2
  107. package/static/app/assets/System-CMHSO9qM.js +1 -0
  108. package/static/app/assets/arrow-left-BfmkskWx.js +1 -0
  109. package/static/app/assets/{bot-B_K1Tdmw.js → bot-Cn8bWRuq.js} +1 -1
  110. package/static/app/assets/{brain-DWyaV1L1.js → brain-CQJberbE.js} +1 -1
  111. package/static/app/assets/{button-aTn4s84A.js → button-Ct9f2_oT.js} +1 -1
  112. package/static/app/assets/circle-check-DruOxB-4.js +1 -0
  113. package/static/app/assets/{circle-pause-xKgeGXkT.js → circle-pause-CmzC_apg.js} +1 -1
  114. package/static/app/assets/{circle-play-DkT6tYPX.js → circle-play-D8mW2aQ7.js} +1 -1
  115. package/static/app/assets/{cpu-85xYObUC.js → cpu-DZcdd0PZ.js} +1 -1
  116. package/static/app/assets/{download-B5Fm7YXo.js → download-bv1KEPGQ.js} +1 -1
  117. package/static/app/assets/{folder-open-kk2Xa52u.js → folder-open-d-Pip5gr.js} +1 -1
  118. package/static/app/assets/{hard-drive-DkA3zBW_.js → hard-drive-D20iavUb.js} +1 -1
  119. package/static/app/assets/index-D9x-kSNy.css +2 -0
  120. package/static/app/assets/{index-BMPdTmlY.js → index-Do83hDzJ.js} +3 -3
  121. package/static/app/assets/{input-B0nRf2jO.js → input-BLXVNmj1.js} +1 -1
  122. package/static/app/assets/{link-2-Dwb4gnTc.js → link-2-BPJOFlAy.js} +1 -1
  123. package/static/app/assets/{permissionCopy-CQDUBrOZ.js → permissionCopy-ChdJd493.js} +1 -1
  124. package/static/app/assets/primitives-Cv5tbZBY.js +1 -0
  125. package/static/app/assets/search-CT9aho2j.js +1 -0
  126. package/static/app/assets/{share-2-BsrxFglO.js → share-2-YNX_NtMU.js} +1 -1
  127. package/static/app/assets/{shield-alert-5BStfp2_.js → shield-alert-DuQ3zrVL.js} +1 -1
  128. package/static/app/assets/{textarea-Cg8IUA-k.js → textarea-DqwLnli4.js} +1 -1
  129. package/static/app/assets/{useFocusTrap-CYKvE46M.js → useFocusTrap-ZVI98jaW.js} +1 -1
  130. package/static/app/assets/{useMutation-CSn9t1op.js → useMutation-CVC4qv_D.js} +1 -1
  131. package/static/app/assets/{useQuery-CY2OI2uy.js → useQuery-C7BeG4HU.js} +1 -1
  132. package/static/app/assets/{utils-Ddol2RWD.js → utils-CiFtIdZq.js} +1 -1
  133. package/static/app/assets/{workspace-BqDwOz_p.js → workspace-DQz9vIId.js} +1 -1
  134. package/static/app/index.html +4 -4
  135. package/static/sw.js +1 -1
  136. package/lattice_brain/archive.py +0 -522
  137. package/lattice_brain/context.py +0 -325
  138. package/lattice_brain/conversations.py +0 -382
  139. package/lattice_brain/core.py +0 -82
  140. package/lattice_brain/graph/_kg_contract.py +0 -249
  141. package/lattice_brain/graph/curator.py +0 -676
  142. package/lattice_brain/graph/discovery.py +0 -595
  143. package/lattice_brain/graph/discovery_index/__init__.py +0 -35
  144. package/lattice_brain/graph/discovery_index/cleanup.py +0 -182
  145. package/lattice_brain/graph/discovery_index/extract.py +0 -137
  146. package/lattice_brain/graph/discovery_index/scan.py +0 -411
  147. package/lattice_brain/graph/discovery_index/upsert.py +0 -495
  148. package/lattice_brain/graph/documents.py +0 -380
  149. package/lattice_brain/graph/fusion.py +0 -395
  150. package/lattice_brain/graph/identity.py +0 -175
  151. package/lattice_brain/graph/image_vectors.py +0 -230
  152. package/lattice_brain/graph/ingest.py +0 -829
  153. package/lattice_brain/graph/network.py +0 -205
  154. package/lattice_brain/graph/proactive.py +0 -724
  155. package/lattice_brain/graph/projection/__init__.py +0 -42
  156. package/lattice_brain/graph/projection/curation.py +0 -500
  157. package/lattice_brain/graph/projection/v2_schema.py +0 -518
  158. package/lattice_brain/graph/provenance.py +0 -524
  159. package/lattice_brain/graph/rerank.py +0 -163
  160. package/lattice_brain/graph/retrieval/__init__.py +0 -54
  161. package/lattice_brain/graph/retrieval/context.py +0 -197
  162. package/lattice_brain/graph/retrieval/graph_view.py +0 -319
  163. package/lattice_brain/graph/retrieval/hybrid.py +0 -488
  164. package/lattice_brain/graph/retrieval/maintenance.py +0 -121
  165. package/lattice_brain/graph/retrieval/signals.py +0 -95
  166. package/lattice_brain/graph/retrieval_docgen.py +0 -253
  167. package/lattice_brain/graph/retrieval_policy.py +0 -180
  168. package/lattice_brain/graph/retrieval_reads.py +0 -769
  169. package/lattice_brain/graph/retrieval_vector/__init__.py +0 -42
  170. package/lattice_brain/graph/retrieval_vector/fingerprint.py +0 -97
  171. package/lattice_brain/graph/retrieval_vector/indexing.py +0 -347
  172. package/lattice_brain/graph/retrieval_vector/search.py +0 -560
  173. package/lattice_brain/graph/retrieval_vector/status.py +0 -374
  174. package/lattice_brain/graph/schema.py +0 -792
  175. package/lattice_brain/graph/store.py +0 -268
  176. package/lattice_brain/graph/vector_index/__init__.py +0 -85
  177. package/lattice_brain/graph/vector_index/base.py +0 -167
  178. package/lattice_brain/graph/vector_index/brute_force.py +0 -110
  179. package/lattice_brain/graph/vector_index/hnsw.py +0 -290
  180. package/lattice_brain/graph/vector_index/jobs.py +0 -287
  181. package/lattice_brain/graph/vector_index/quantized.py +0 -148
  182. package/lattice_brain/graph/vector_index/selector.py +0 -161
  183. package/lattice_brain/graph/write_master.py +0 -308
  184. package/lattice_brain/ingestion/_contract.py +0 -90
  185. package/lattice_brain/ingestion/folder_scan.py +0 -57
  186. package/lattice_brain/ingestion/folders.py +0 -258
  187. package/lattice_brain/ingestion/jobs_api.py +0 -107
  188. package/lattice_brain/ingestion/routing.py +0 -295
  189. package/lattice_brain/ingestion_jobs.py +0 -380
  190. package/lattice_brain/memory.py +0 -75
  191. package/lattice_brain/portability/__init__.py +0 -90
  192. package/lattice_brain/portability/_contract.py +0 -42
  193. package/lattice_brain/portability/backups.py +0 -338
  194. package/lattice_brain/portability/bundles.py +0 -136
  195. package/lattice_brain/portability/constants.py +0 -93
  196. package/lattice_brain/portability/fsops.py +0 -138
  197. package/lattice_brain/portability/service.py +0 -41
  198. package/lattice_brain/portability/sharing.py +0 -710
  199. package/lattice_brain/quality.py +0 -543
  200. package/lattice_brain/retrieval_benchmark_fixtures.py +0 -92
  201. package/lattice_brain/runtime/agent_runtime.py +0 -859
  202. package/lattice_brain/runtime/contracts.py +0 -460
  203. package/lattice_brain/runtime/multi_agent.py +0 -942
  204. package/lattice_brain/runtime/statuses.py +0 -10
  205. package/lattice_brain/sealed_box.py +0 -240
  206. package/lattice_brain/self_model.py +0 -675
  207. package/lattice_brain/sensitivity.py +0 -94
  208. package/lattice_brain/storage/__init__.py +0 -22
  209. package/lattice_brain/storage/base.py +0 -100
  210. package/lattice_brain/storage/docker.py +0 -105
  211. package/lattice_brain/storage/factory.py +0 -31
  212. package/lattice_brain/storage/migration.py +0 -191
  213. package/lattice_brain/storage/postgres.py +0 -123
  214. package/lattice_brain/storage/sqlite.py +0 -143
  215. package/lattice_brain/synthesis.py +0 -824
  216. package/lattice_brain/workflow.py +0 -497
  217. package/latticeai/api/admin.py +0 -471
  218. package/latticeai/api/agent_registry.py +0 -105
  219. package/latticeai/api/agents.py +0 -228
  220. package/latticeai/api/auth.py +0 -383
  221. package/latticeai/api/automation_intelligence.py +0 -401
  222. package/latticeai/api/brain_intelligence.py +0 -199
  223. package/latticeai/api/browser.py +0 -493
  224. package/latticeai/api/change_proposals.py +0 -89
  225. package/latticeai/api/chat.py +0 -572
  226. package/latticeai/api/chat_agent_http.py +0 -892
  227. package/latticeai/api/chat_contracts.py +0 -73
  228. package/latticeai/api/chat_documents.py +0 -276
  229. package/latticeai/api/chat_helpers.py +0 -460
  230. package/latticeai/api/chat_history.py +0 -101
  231. package/latticeai/api/chat_hybrid.py +0 -113
  232. package/latticeai/api/chat_intents.py +0 -646
  233. package/latticeai/api/chat_stream.py +0 -216
  234. package/latticeai/api/chronicle.py +0 -63
  235. package/latticeai/api/command_center.py +0 -51
  236. package/latticeai/api/computer_use.py +0 -474
  237. package/latticeai/api/evidence_actions.py +0 -48
  238. package/latticeai/api/features.py +0 -70
  239. package/latticeai/api/funnel_metrics.py +0 -31
  240. package/latticeai/api/garden.py +0 -34
  241. package/latticeai/api/hooks.py +0 -165
  242. package/latticeai/api/index_jobs.py +0 -145
  243. package/latticeai/api/invitations.py +0 -100
  244. package/latticeai/api/knowledge_graph.py +0 -536
  245. package/latticeai/api/marketplace.py +0 -105
  246. package/latticeai/api/mcp.py +0 -482
  247. package/latticeai/api/memory.py +0 -270
  248. package/latticeai/api/network.py +0 -81
  249. package/latticeai/api/network_boundary.py +0 -225
  250. package/latticeai/api/permission_mode.py +0 -61
  251. package/latticeai/api/permissions.py +0 -436
  252. package/latticeai/api/plugins.py +0 -126
  253. package/latticeai/api/portability.py +0 -391
  254. package/latticeai/api/project_sessions.py +0 -114
  255. package/latticeai/api/realtime.py +0 -118
  256. package/latticeai/api/review_queue.py +0 -364
  257. package/latticeai/api/security_dashboard.py +0 -604
  258. package/latticeai/api/setup.py +0 -319
  259. package/latticeai/api/static_routes.py +0 -354
  260. package/latticeai/api/ui_redirects.py +0 -26
  261. package/latticeai/api/workflow_designer.py +0 -394
  262. package/latticeai/api/workspace.py +0 -856
  263. package/latticeai/api/workspace_scope.py +0 -125
  264. package/latticeai/core/agent/__init__.py +0 -93
  265. package/latticeai/core/agent/_contract.py +0 -79
  266. package/latticeai/core/agent/context.py +0 -57
  267. package/latticeai/core/agent/deps.py +0 -125
  268. package/latticeai/core/agent/execution.py +0 -622
  269. package/latticeai/core/agent/planning.py +0 -145
  270. package/latticeai/core/agent/recovery.py +0 -157
  271. package/latticeai/core/agent/runtime.py +0 -210
  272. package/latticeai/core/agent/verification.py +0 -231
  273. package/latticeai/core/agent_eval.py +0 -739
  274. package/latticeai/core/agent_helpers.py +0 -493
  275. package/latticeai/core/agent_profiles.py +0 -110
  276. package/latticeai/core/agent_prompts.py +0 -171
  277. package/latticeai/core/agent_registry.py +0 -232
  278. package/latticeai/core/agent_state.py +0 -41
  279. package/latticeai/core/agent_trace.py +0 -104
  280. package/latticeai/core/artifact_ledger.py +0 -109
  281. package/latticeai/core/audit.py +0 -260
  282. package/latticeai/core/builtin_hooks.py +0 -105
  283. package/latticeai/core/context_builder.py +0 -394
  284. package/latticeai/core/document_generator.py +0 -103
  285. package/latticeai/core/enterprise.py +0 -154
  286. package/latticeai/core/enterprise_admin.py +0 -158
  287. package/latticeai/core/file_generation/__init__.py +0 -115
  288. package/latticeai/core/file_generation/bundles.py +0 -76
  289. package/latticeai/core/file_generation/extraction.py +0 -154
  290. package/latticeai/core/file_generation/inference.py +0 -235
  291. package/latticeai/core/file_generation/orchestration.py +0 -152
  292. package/latticeai/core/file_generation/prompting.py +0 -117
  293. package/latticeai/core/file_generation/repair.py +0 -114
  294. package/latticeai/core/file_generation/sanitize.py +0 -61
  295. package/latticeai/core/file_generation/validation.py +0 -201
  296. package/latticeai/core/invitations.py +0 -132
  297. package/latticeai/core/legacy_compatibility.py +0 -243
  298. package/latticeai/core/logging_safety.py +0 -46
  299. package/latticeai/core/marketplace.py +0 -293
  300. package/latticeai/core/mcp_catalog.py +0 -452
  301. package/latticeai/core/mcp_registry.py +0 -506
  302. package/latticeai/core/network_boundary.py +0 -168
  303. package/latticeai/core/oidc.py +0 -208
  304. package/latticeai/core/plugins.py +0 -432
  305. package/latticeai/core/product_hardening.py +0 -218
  306. package/latticeai/core/project_sessions.py +0 -337
  307. package/latticeai/core/realtime.py +0 -238
  308. package/latticeai/core/run_explain.py +0 -426
  309. package/latticeai/core/run_store.py +0 -252
  310. package/latticeai/core/timezones.py +0 -80
  311. package/latticeai/core/workspace_computer_memory.py +0 -84
  312. package/latticeai/core/workspace_graph_trace.py +0 -155
  313. package/latticeai/core/workspace_indexing.py +0 -102
  314. package/latticeai/core/workspace_memory.py +0 -77
  315. package/latticeai/core/workspace_onboarding.py +0 -104
  316. package/latticeai/core/workspace_os.py +0 -978
  317. package/latticeai/core/workspace_os_constants.py +0 -126
  318. package/latticeai/core/workspace_os_state.py +0 -180
  319. package/latticeai/core/workspace_os_utils.py +0 -103
  320. package/latticeai/core/workspace_permissions.py +0 -101
  321. package/latticeai/core/workspace_plugins.py +0 -97
  322. package/latticeai/core/workspace_relationships.py +0 -99
  323. package/latticeai/core/workspace_reorganization.py +0 -335
  324. package/latticeai/core/workspace_review_items.py +0 -112
  325. package/latticeai/core/workspace_runs.py +0 -726
  326. package/latticeai/core/workspace_skills.py +0 -109
  327. package/latticeai/core/workspace_snapshots.py +0 -198
  328. package/latticeai/core/workspace_timeline.py +0 -110
  329. package/latticeai/integrations/__init__.py +0 -0
  330. package/latticeai/integrations/telegram_bot/__init__.py +0 -123
  331. package/latticeai/integrations/telegram_bot/__main__.py +0 -17
  332. package/latticeai/integrations/telegram_bot/config.py +0 -86
  333. package/latticeai/integrations/telegram_bot/dispatch.py +0 -311
  334. package/latticeai/integrations/telegram_bot/flows.py +0 -478
  335. package/latticeai/integrations/telegram_bot/helpers.py +0 -322
  336. package/latticeai/integrations/telegram_bot/screens.py +0 -394
  337. package/latticeai/runtime/audit_runtime.py +0 -76
  338. package/latticeai/runtime/automation_runtime.py +0 -81
  339. package/latticeai/runtime/chat_wiring.py +0 -141
  340. package/latticeai/runtime/context_runtime.py +0 -66
  341. package/latticeai/runtime/feature_toggle_wiring.py +0 -157
  342. package/latticeai/runtime/history_runtime.py +0 -163
  343. package/latticeai/runtime/history_writer.py +0 -138
  344. package/latticeai/runtime/hooks_runtime.py +0 -77
  345. package/latticeai/runtime/model_wiring.py +0 -68
  346. package/latticeai/runtime/namespace_runtime.py +0 -163
  347. package/latticeai/runtime/network_boundary_wiring.py +0 -117
  348. package/latticeai/runtime/network_config_runtime.py +0 -56
  349. package/latticeai/runtime/permission_mode_wiring.py +0 -112
  350. package/latticeai/runtime/persistence_runtime.py +0 -159
  351. package/latticeai/runtime/platform_runtime_wiring.py +0 -89
  352. package/latticeai/runtime/review_wiring.py +0 -42
  353. package/latticeai/runtime/router_registration.py +0 -693
  354. package/latticeai/runtime/service_singletons.py +0 -55
  355. package/latticeai/runtime/sso_config_runtime.py +0 -128
  356. package/latticeai/runtime/user_key_runtime.py +0 -106
  357. package/latticeai/runtime/web_runtime.py +0 -92
  358. package/latticeai/server_app.py +0 -51
  359. package/latticeai/services/app_context.py +0 -130
  360. package/latticeai/services/automation_execution.py +0 -266
  361. package/latticeai/services/automation_intelligence.py +0 -614
  362. package/latticeai/services/brain_automation.py +0 -191
  363. package/latticeai/services/brain_intelligence/__init__.py +0 -58
  364. package/latticeai/services/brain_intelligence/_contract.py +0 -71
  365. package/latticeai/services/brain_intelligence/consistency.py +0 -193
  366. package/latticeai/services/brain_intelligence/constants.py +0 -47
  367. package/latticeai/services/brain_intelligence/digest.py +0 -258
  368. package/latticeai/services/brain_intelligence/health.py +0 -331
  369. package/latticeai/services/brain_intelligence/proposals.py +0 -259
  370. package/latticeai/services/brain_intelligence/sampling.py +0 -84
  371. package/latticeai/services/brain_intelligence/service.py +0 -48
  372. package/latticeai/services/change_proposals.py +0 -471
  373. package/latticeai/services/chat_service.py +0 -243
  374. package/latticeai/services/chronicle.py +0 -555
  375. package/latticeai/services/cloud_egress_audit.py +0 -85
  376. package/latticeai/services/cloud_extraction.py +0 -129
  377. package/latticeai/services/cloud_streaming.py +0 -268
  378. package/latticeai/services/cloud_token_guard.py +0 -84
  379. package/latticeai/services/command_center.py +0 -548
  380. package/latticeai/services/evidence_actions.py +0 -258
  381. package/latticeai/services/feature_toggles.py +0 -502
  382. package/latticeai/services/folder_watch.py +0 -520
  383. package/latticeai/services/funnel_metrics.py +0 -307
  384. package/latticeai/services/hybrid_chat.py +0 -316
  385. package/latticeai/services/hybrid_context.py +0 -228
  386. package/latticeai/services/hybrid_policy.py +0 -129
  387. package/latticeai/services/interop_bridges.py +0 -978
  388. package/latticeai/services/local_knowledge.py +0 -465
  389. package/latticeai/services/memory_service/__init__.py +0 -52
  390. package/latticeai/services/memory_service/_contract.py +0 -100
  391. package/latticeai/services/memory_service/brief.py +0 -431
  392. package/latticeai/services/memory_service/constants.py +0 -57
  393. package/latticeai/services/memory_service/maintenance.py +0 -138
  394. package/latticeai/services/memory_service/manager.py +0 -186
  395. package/latticeai/services/memory_service/proof.py +0 -136
  396. package/latticeai/services/memory_service/recall.py +0 -225
  397. package/latticeai/services/memory_service/service.py +0 -48
  398. package/latticeai/services/memory_service/stores.py +0 -110
  399. package/latticeai/services/mode_store.py +0 -132
  400. package/latticeai/services/model_recommendation.py +0 -224
  401. package/latticeai/services/model_runtime/cloud.py +0 -87
  402. package/latticeai/services/network_boundary_service.py +0 -117
  403. package/latticeai/services/obsidian_bridge.py +0 -609
  404. package/latticeai/services/openai_compatible_adapter.py +0 -101
  405. package/latticeai/services/permission_mode_service.py +0 -122
  406. package/latticeai/services/platform_runtime.py +0 -366
  407. package/latticeai/services/review_queue.py +0 -380
  408. package/latticeai/services/router_context.py +0 -59
  409. package/latticeai/services/run_executor.py +0 -387
  410. package/latticeai/services/self_model_service.py +0 -171
  411. package/latticeai/services/setup_detection.py +0 -147
  412. package/latticeai/services/triggers.py +0 -378
  413. package/latticeai/services/upload_service.py +0 -172
  414. package/latticeai/services/workspace_service.py +0 -165
  415. package/latticeai/setup/__init__.py +0 -25
  416. package/latticeai/setup/auto_setup.py +0 -846
  417. package/latticeai/setup/demo_corpus.py +0 -98
  418. package/latticeai/setup/wizard/__init__.py +0 -126
  419. package/latticeai/setup/wizard/catalog.py +0 -175
  420. package/latticeai/setup/wizard/detect.py +0 -302
  421. package/latticeai/setup/wizard/install.py +0 -348
  422. package/latticeai/setup/wizard/paths.py +0 -165
  423. package/latticeai/setup/wizard/plans.py +0 -74
  424. package/latticeai/setup/wizard/recommend.py +0 -320
  425. package/scripts/bench_agent_smoke.py +0 -409
  426. package/scripts/bench_models.py +0 -540
  427. package/scripts/bench_vector_index.py +0 -295
  428. package/scripts/funnel_soft_gate.py +0 -192
  429. package/scripts/generate_agent_loop_fixtures.py +0 -994
  430. package/scripts/generate_rust_parity_fixtures.py +0 -908
  431. package/scripts/migrate_brain_storage.py +0 -57
  432. package/scripts/parity_fixture_corpus_context.py +0 -162
  433. package/scripts/parity_fixture_corpus_docgen.py +0 -341
  434. package/scripts/profile_kg.py +0 -355
  435. package/server.py +0 -30
  436. package/static/app/assets/AdminConsole-cf4npybT.js +0 -1
  437. package/static/app/assets/Capture-DiQ219jW.js +0 -1
  438. package/static/app/assets/System-CieofHQa.js +0 -1
  439. package/static/app/assets/arrow-left-kfsrk0mv.js +0 -1
  440. package/static/app/assets/circle-check-qqLug9nU.js +0 -1
  441. package/static/app/assets/index-DxmOfNRi.css +0 -2
  442. package/static/app/assets/primitives-SNp0LRJz.js +0 -1
  443. package/static/app/assets/search-BcHqkjoy.js +0 -1
@@ -1,520 +0,0 @@
1
- """Opt-in folder watch mode for the unified ingestion pipeline (backlog #8).
2
-
3
- Watches previously-ingested folders and incrementally re-ingests new/changed
4
- files through the normal :class:`IngestionPipeline` door (same filters, same
5
- quality pipeline, same workspace scoping, same provenance).
6
-
7
- Design decisions (review §7.2 B + risk section):
8
-
9
- * **Default OFF, explicit opt-in.** A watch exists only after an explicit API
10
- call. The stored config is the durable record of that consent; ``restore()``
11
- resumes only entries persisted with ``enabled: true`` and never invents one.
12
- * **Polling with mtime snapshots**, not an OS watcher. ``watchdog`` is already
13
- a dependency, but polling is deterministic, portable, and testable
14
- (``scan_once`` runs synchronously); the interval is configurable via
15
- ``LATTICEAI_FOLDER_WATCH_INTERVAL`` (seconds, default 30).
16
- * **Reuses the folder-ingest filters** (skip dirs, ``.latticeignore``,
17
- extension allow-list, size cap) so watch mode can never ingest something a
18
- manual folder ingest would have refused.
19
- * Deleted files are *counted* but their graph nodes are not removed — node
20
- deletion is a destructive operation that stays behind explicit user flows.
21
- """
22
-
23
- from __future__ import annotations
24
-
25
- import json
26
- import os
27
- import threading
28
- import uuid
29
- from pathlib import Path
30
- from typing import Any, Dict, List, Optional
31
-
32
- from lattice_brain.gates import FeatureGate
33
- from lattice_brain.ingestion import (
34
- DEFAULT_FOLDER_EXTENSIONS,
35
- DEFAULT_MAX_FILE_BYTES,
36
- FOLDER_DEFAULT_SKIP_DIRS,
37
- FOLDER_DOCUMENT_EXTENSIONS,
38
- LATTICEIGNORE_FILENAME,
39
- IngestionItem,
40
- _load_latticeignore,
41
- _matches_ignore,
42
- )
43
- from latticeai.core.quiet import quiet
44
- from latticeai.core.timeutil import now_iso as _now_iso
45
-
46
- WATCH_INTERVAL_ENV = "LATTICEAI_FOLDER_WATCH_INTERVAL"
47
- DEFAULT_WATCH_INTERVAL_SECONDS = 30.0
48
- MAX_FILES_PER_SCAN = 200 # per-watch per-scan ingest cap (thrash guard)
49
-
50
- # ── vault watch (v11.2.0) ────────────────────────────────────────────────────
51
- #: A watched *vault* is not a watched folder. 11.1.0 refused to schedule vault
52
- #: syncs for a real reason — link edges need the node ids that only a completed
53
- #: inline ingest has, so a per-file incremental pass would report edges it never
54
- #: wrote. The resolution is not to make the bridge incremental: it is to notice
55
- #: that the vault changed and then run the **whole** bridge sync, which does
56
- #: produce those edges and is idempotent by content hash. What the snapshot buys
57
- #: is skipping the sync entirely when nothing moved.
58
- WATCH_KIND_FOLDER = "folder"
59
- WATCH_KIND_VAULT = "vault"
60
- WATCH_KINDS = (WATCH_KIND_FOLDER, WATCH_KIND_VAULT)
61
- VAULT_WATCH_ENV = "LATTICEAI_VAULT_WATCH"
62
- #: Opt-in, default off, resolved when asked so a settings toggle can move it.
63
- VAULT_WATCH_GATE = FeatureGate(
64
- VAULT_WATCH_ENV,
65
- default=False,
66
- name="vault_watch",
67
- detail=(
68
- "Watching an external vault re-syncs it in the background; it is off "
69
- "until you turn it on."
70
- ),
71
- )
72
- VAULT_WATCH_DISABLED_DETAIL = (
73
- "vault watch is off by default; turn it on to let an external vault "
74
- f"re-sync in the background ({VAULT_WATCH_ENV})"
75
- )
76
-
77
-
78
- def _default_interval() -> float:
79
- raw = os.getenv(WATCH_INTERVAL_ENV, "").strip()
80
- try:
81
- value = float(raw) if raw else DEFAULT_WATCH_INTERVAL_SECONDS
82
- except ValueError:
83
- value = DEFAULT_WATCH_INTERVAL_SECONDS
84
- return max(1.0, value)
85
-
86
-
87
- class FolderWatchService:
88
- """Poll-based incremental re-ingestion of explicitly opted-in folders."""
89
-
90
- def __init__(
91
- self,
92
- *,
93
- pipeline: Any,
94
- config_path: Path,
95
- interval_seconds: Optional[float] = None,
96
- vault_bridge: Optional[Any] = None,
97
- ) -> None:
98
- self._pipeline = pipeline
99
- # Injected rather than imported at call time so a vault watch can be
100
- # driven by a test double, and so this module keeps knowing nothing
101
- # about how a vault is parsed.
102
- self._vault_bridge = vault_bridge
103
- self._config_path = Path(config_path)
104
- self._interval = float(interval_seconds) if interval_seconds else _default_interval()
105
- self._lock = threading.Lock()
106
- self._stop = threading.Event()
107
- self._thread: Optional[threading.Thread] = None
108
- self._watches: Dict[str, Dict[str, Any]] = self._load_config()
109
-
110
- # ── persistence ──────────────────────────────────────────────────────────
111
- def _load_config(self) -> Dict[str, Dict[str, Any]]:
112
- try:
113
- payload = json.loads(self._config_path.read_text(encoding="utf-8"))
114
- except (OSError, ValueError):
115
- return {}
116
- watches = payload.get("watches")
117
- return dict(watches) if isinstance(watches, dict) else {}
118
-
119
- def _save_config(self) -> None:
120
- self._config_path.parent.mkdir(parents=True, exist_ok=True)
121
- tmp = self._config_path.with_suffix(".tmp")
122
- tmp.write_text(
123
- json.dumps({"watches": self._watches}, ensure_ascii=False, indent=2),
124
- encoding="utf-8",
125
- )
126
- tmp.replace(self._config_path)
127
-
128
- # ── public API ───────────────────────────────────────────────────────────
129
- def enable(
130
- self,
131
- path: Any,
132
- *,
133
- owner: Optional[str] = None,
134
- workspace_id: Optional[str] = None,
135
- recursive: bool = True,
136
- kind: str = WATCH_KIND_FOLDER,
137
- ) -> Dict[str, Any]:
138
- """Explicit opt-in: snapshot the folder now, ingest only future changes.
139
-
140
- The folder is assumed to have been ingested already (the UI offers
141
- watch after a folder ingest); the enable-time snapshot is the baseline,
142
- so enabling never re-ingests the whole folder.
143
-
144
- ``kind="vault"`` watches an external Obsidian vault instead, re-running
145
- the whole bridge sync when the snapshot moves. That mode carries its own
146
- opt-in gate on top of this one and refuses while it is off.
147
- """
148
- kind = str(kind or WATCH_KIND_FOLDER).strip().lower()
149
- if kind not in WATCH_KINDS:
150
- return {"status": "failed", "detail": f"unknown watch kind: {kind}"}
151
- if kind == WATCH_KIND_VAULT and not VAULT_WATCH_GATE.enabled():
152
- return {"status": "disabled", "detail": VAULT_WATCH_DISABLED_DETAIL}
153
- root = Path(path).expanduser()
154
- if not root.is_dir():
155
- return {"status": "failed", "detail": f"not a directory: {root}"}
156
- root = root.resolve()
157
- with self._lock:
158
- for watch in self._watches.values():
159
- same_scope = (watch.get("workspace_id") or None) == (workspace_id or None)
160
- if watch.get("path") == str(root) and same_scope:
161
- watch["enabled"] = True
162
- watch["kind"] = kind
163
- watch["snapshot"] = self._snapshot(root, recursive=bool(watch.get("recursive", True)))
164
- self._save_config()
165
- self._ensure_thread()
166
- return {"status": "ok", "watch": self._public_watch(watch), "already_watching": True}
167
- watch_id = f"watch_{uuid.uuid4().hex[:12]}"
168
- watch = {
169
- "id": watch_id,
170
- "path": str(root),
171
- "owner": owner,
172
- "workspace_id": workspace_id,
173
- "recursive": bool(recursive),
174
- "kind": kind,
175
- "enabled": True,
176
- "created_at": _now_iso(),
177
- "last_scan_at": None,
178
- "last_result": None,
179
- "snapshot": self._snapshot(root, recursive=bool(recursive)),
180
- }
181
- self._watches[watch_id] = watch
182
- self._save_config()
183
- self._ensure_thread()
184
- return {"status": "ok", "watch": self._public_watch(watch), "already_watching": False}
185
-
186
- def disable(
187
- self,
188
- *,
189
- watch_id: Optional[str] = None,
190
- path: Optional[str] = None,
191
- ) -> Dict[str, Any]:
192
- """Opt back out: remove the stored consent record entirely."""
193
- with self._lock:
194
- target_id = None
195
- if watch_id and watch_id in self._watches:
196
- target_id = watch_id
197
- elif path:
198
- wanted = str(Path(path).expanduser().resolve())
199
- for wid, watch in self._watches.items():
200
- if watch.get("path") == wanted:
201
- target_id = wid
202
- break
203
- if target_id is None:
204
- return {"status": "not_found"}
205
- removed = self._watches.pop(target_id)
206
- self._save_config()
207
- if not any(w.get("enabled") for w in self._watches.values()):
208
- self._stop_thread_locked()
209
- return {"status": "ok", "watch": self._public_watch(removed)}
210
-
211
- def status(self) -> Dict[str, Any]:
212
- with self._lock:
213
- watches = [self._public_watch(w) for w in self._watches.values()]
214
- polling = self._thread is not None and self._thread.is_alive()
215
- return {
216
- "enabled_count": sum(1 for w in watches if w.get("enabled")),
217
- "polling": polling,
218
- "interval_seconds": self._interval,
219
- "watches": watches,
220
- "kinds": list(WATCH_KINDS),
221
- "vault_watch": {
222
- **VAULT_WATCH_GATE.describe(),
223
- "bridge_wired": self._vault_bridge is not None,
224
- },
225
- "note": (
226
- "Watch mode is opt-in and off by default. Deleted files are "
227
- "counted but never auto-removed from the Brain."
228
- ),
229
- }
230
-
231
- def restore(self) -> Dict[str, Any]:
232
- """Resume polling for watches persisted with the explicit opt-in.
233
-
234
- Never starts anything when the stored config has no enabled entries —
235
- an empty/missing config means the user never opted in.
236
- """
237
- with self._lock:
238
- enabled = [w for w in self._watches.values() if w.get("enabled")]
239
- if enabled:
240
- self._ensure_thread()
241
- return {"restored": len(enabled), "polling": bool(enabled)}
242
-
243
- def stop_all(self) -> None:
244
- """Stop the polling thread (keeps the stored opt-in for next start)."""
245
- with self._lock:
246
- self._stop_thread_locked()
247
-
248
- def scan_once(self, watch_id: str) -> Dict[str, Any]:
249
- """Synchronously scan one watch and ingest new/changed files."""
250
- with self._lock:
251
- watch = self._watches.get(watch_id)
252
- if watch is None:
253
- return {"status": "not_found", "watch_id": watch_id}
254
- if not watch.get("enabled"):
255
- return {"status": "disabled", "watch_id": watch_id}
256
- snapshot = dict(watch.get("snapshot") or {})
257
- root = Path(watch["path"])
258
- recursive = bool(watch.get("recursive", True))
259
- owner = watch.get("owner")
260
- workspace_id = watch.get("workspace_id")
261
- kind = str(watch.get("kind") or WATCH_KIND_FOLDER)
262
-
263
- result: Dict[str, Any] = {
264
- "status": "ok",
265
- "watch_id": watch_id,
266
- "kind": kind,
267
- "new": 0,
268
- "changed": 0,
269
- "removed": 0,
270
- "ingested": 0,
271
- "duplicate": 0,
272
- "failed": 0,
273
- "errors": [],
274
- }
275
- if not root.is_dir():
276
- result.update(status="failed", detail=f"folder unavailable: {root}")
277
- self._record_scan(watch_id, result, snapshot=None)
278
- return result
279
-
280
- current = self._snapshot(root, recursive=recursive)
281
- changed_rel: List[str] = []
282
- for rel, stamp in current.items():
283
- previous = snapshot.get(rel)
284
- if previous is None:
285
- result["new"] += 1
286
- changed_rel.append(rel)
287
- elif previous != stamp:
288
- result["changed"] += 1
289
- changed_rel.append(rel)
290
- result["removed"] = sum(1 for rel in snapshot if rel not in current)
291
-
292
- if kind == WATCH_KIND_VAULT:
293
- self._vault_scan(
294
- result,
295
- root=root,
296
- changed=bool(changed_rel) or bool(result["removed"]),
297
- owner=owner,
298
- workspace_id=workspace_id,
299
- )
300
- self._record_scan(watch_id, result, snapshot=current)
301
- return result
302
-
303
- for rel in changed_rel[:MAX_FILES_PER_SCAN]:
304
- path = root / rel
305
- ext = path.suffix.lower()
306
- metadata: Dict[str, Any] = {
307
- "relative_path": rel,
308
- "folder_watch": True,
309
- "watch_id": watch_id,
310
- }
311
- if ext in FOLDER_DOCUMENT_EXTENSIONS:
312
- source_type = "pdf"
313
- else:
314
- source_type = "file"
315
- try:
316
- content = path.read_text(encoding="utf-8", errors="ignore")
317
- except OSError as exc:
318
- result["failed"] += 1
319
- if len(result["errors"]) < 20:
320
- result["errors"].append({"path": rel, "detail": f"read failed: {exc}"})
321
- continue
322
- metadata["extracted"] = {"content": content, "chars": len(content)}
323
- outcome = self._pipeline.ingest(
324
- IngestionItem(
325
- source_type=source_type,
326
- title=path.name,
327
- path=str(path),
328
- source_uri=str(path),
329
- owner=owner,
330
- workspace_id=workspace_id,
331
- metadata=metadata,
332
- ),
333
- user_email=owner,
334
- )
335
- if outcome.status == "ok":
336
- if outcome.duplicate:
337
- result["duplicate"] += 1
338
- else:
339
- result["ingested"] += 1
340
- else:
341
- result["failed"] += 1
342
- if len(result["errors"]) < 20:
343
- result["errors"].append({
344
- "path": rel,
345
- "detail": outcome.detail or outcome.status,
346
- })
347
- if len(changed_rel) > MAX_FILES_PER_SCAN:
348
- result["truncated"] = True
349
- self._record_scan(watch_id, result, snapshot=current)
350
- return result
351
-
352
- # ── internals ────────────────────────────────────────────────────────────
353
- def _vault_scan(
354
- self,
355
- result: Dict[str, Any],
356
- *,
357
- root: Path,
358
- changed: bool,
359
- owner: Optional[str],
360
- workspace_id: Optional[str],
361
- ) -> None:
362
- """Re-run the whole vault sync — but only when something actually moved.
363
-
364
- A full sync is what makes the link edges real; the snapshot is only
365
- there to avoid paying for one when the vault is untouched. The gate is
366
- re-checked here, not just at enable time, so turning vault watch off
367
- stops the background work immediately instead of at the next restart.
368
- """
369
- if not VAULT_WATCH_GATE.enabled():
370
- result.update(status="disabled", detail=VAULT_WATCH_DISABLED_DETAIL)
371
- return
372
- if not changed:
373
- result["synced"] = False
374
- result["detail"] = "vault unchanged since the last scan"
375
- return
376
- if self._vault_bridge is None:
377
- result.update(
378
- status="failed",
379
- synced=False,
380
- detail="no vault bridge is wired; the vault was not re-synced",
381
- )
382
- return
383
- try:
384
- summary = self._vault_bridge.sync(
385
- str(root), owner=owner, workspace_id=workspace_id, user_email=owner,
386
- )
387
- except Exception as exc: # noqa: BLE001 — one bad sync must not kill the poller
388
- result.update(status="failed", synced=False, detail=f"vault sync failed: {exc}")
389
- return
390
- result["synced"] = True
391
- result["ingested"] = int(summary.get("ingested") or 0)
392
- result["duplicate"] = int(summary.get("duplicate") or 0)
393
- result["failed"] = int(summary.get("failed") or 0)
394
- result["errors"] = list(summary.get("errors") or [])[:20]
395
- result["edges"] = summary.get("edges")
396
- result["status"] = "ok" if summary.get("status") in ("ok", "dry_run") else str(
397
- summary.get("status") or "ok"
398
- )
399
-
400
- def _record_scan(
401
- self,
402
- watch_id: str,
403
- result: Dict[str, Any],
404
- *,
405
- snapshot: Optional[Dict[str, Any]],
406
- ) -> None:
407
- with self._lock:
408
- watch = self._watches.get(watch_id)
409
- if watch is None:
410
- return
411
- watch["last_scan_at"] = _now_iso()
412
- watch["last_result"] = {
413
- key: result[key]
414
- for key in ("status", "new", "changed", "removed", "ingested", "duplicate", "failed")
415
- if key in result
416
- }
417
- # Watch trust UI (review Wave 1.3): keep a small sample of the
418
- # latest scan's failures so the home health card can show *why*
419
- # a watch is unhealthy, not just a count.
420
- watch["last_errors"] = list(result.get("errors") or [])[:3]
421
- if snapshot is not None:
422
- watch["snapshot"] = snapshot
423
- self._save_config()
424
-
425
- def _snapshot(self, root: Path, *, recursive: bool) -> Dict[str, List[float]]:
426
- """{relative_posix_path: [mtime, size]} using the folder-ingest filters."""
427
- patterns = _load_latticeignore(root)
428
- snapshot: Dict[str, List[float]] = {}
429
- for dirpath, dirnames, filenames in os.walk(root):
430
- current = Path(dirpath)
431
- rel_dir = current.relative_to(root)
432
- kept: List[str] = []
433
- for name in sorted(dirnames):
434
- if name in FOLDER_DEFAULT_SKIP_DIRS or name.startswith("."):
435
- continue
436
- rel = name if str(rel_dir) == "." else (rel_dir / name).as_posix()
437
- if _matches_ignore(rel, name, is_dir=True, patterns=patterns):
438
- continue
439
- kept.append(name)
440
- dirnames[:] = kept if recursive else []
441
- for name in sorted(filenames):
442
- if name == LATTICEIGNORE_FILENAME or name.startswith("."):
443
- continue
444
- path = current / name
445
- rel = name if str(rel_dir) == "." else (rel_dir / name).as_posix()
446
- if _matches_ignore(rel, name, is_dir=False, patterns=patterns):
447
- continue
448
- if path.suffix.lower() not in DEFAULT_FOLDER_EXTENSIONS:
449
- continue
450
- try:
451
- stat = path.stat()
452
- except OSError:
453
- quiet()
454
- continue
455
- if stat.st_size > DEFAULT_MAX_FILE_BYTES:
456
- continue
457
- snapshot[rel] = [round(stat.st_mtime, 3), stat.st_size]
458
- return snapshot
459
-
460
- @staticmethod
461
- def _public_watch(watch: Dict[str, Any]) -> Dict[str, Any]:
462
- return {
463
- "id": watch.get("id"),
464
- "path": watch.get("path"),
465
- "owner": watch.get("owner"),
466
- "workspace_id": watch.get("workspace_id"),
467
- "recursive": bool(watch.get("recursive", True)),
468
- "kind": str(watch.get("kind") or WATCH_KIND_FOLDER),
469
- "enabled": bool(watch.get("enabled")),
470
- "created_at": watch.get("created_at"),
471
- "last_scan_at": watch.get("last_scan_at"),
472
- "last_result": watch.get("last_result"),
473
- "last_errors": list(watch.get("last_errors") or []),
474
- "tracked_files": len(watch.get("snapshot") or {}),
475
- }
476
-
477
- def _ensure_thread(self) -> None:
478
- """Start the poller (caller holds the lock)."""
479
- if self._thread is not None and self._thread.is_alive():
480
- return
481
- self._stop = threading.Event()
482
- self._thread = threading.Thread(
483
- target=self._poll_loop, name="folder-watch-poller", daemon=True,
484
- )
485
- self._thread.start()
486
-
487
- def _stop_thread_locked(self) -> None:
488
- self._stop.set()
489
- self._thread = None
490
-
491
- def _poll_loop(self) -> None:
492
- stop = self._stop
493
- while not stop.wait(self._interval):
494
- with self._lock:
495
- enabled_ids = [
496
- wid for wid, watch in self._watches.items() if watch.get("enabled")
497
- ]
498
- if not enabled_ids:
499
- return
500
- for watch_id in enabled_ids:
501
- if stop.is_set():
502
- return
503
- try:
504
- self.scan_once(watch_id)
505
- except Exception: # noqa: BLE001 — one bad watch must not kill the poller
506
- quiet()
507
- continue
508
-
509
-
510
- __all__ = [
511
- "DEFAULT_WATCH_INTERVAL_SECONDS",
512
- "VAULT_WATCH_DISABLED_DETAIL",
513
- "VAULT_WATCH_ENV",
514
- "VAULT_WATCH_GATE",
515
- "WATCH_INTERVAL_ENV",
516
- "WATCH_KINDS",
517
- "WATCH_KIND_FOLDER",
518
- "WATCH_KIND_VAULT",
519
- "FolderWatchService",
520
- ]