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.
- package/README.md +93 -148
- package/bin/ltcai.js +234 -24
- package/docs/CHANGELOG.md +119 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +8 -6
- package/docs/ENTERPRISE.md +2 -1
- package/docs/MULTI_AGENT_RUNTIME.md +12 -5
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +6 -3
- package/docs/REALTIME_COLLABORATION.md +1 -1
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/WORKFLOW_DESIGNER.md +3 -2
- package/docs/kg-schema.md +1 -1
- package/docs/v11.6.0_ONE_DOOR_PLAN.md +170 -0
- package/lattice_brain/__init__.py +42 -79
- package/lattice_brain/graph/__init__.py +9 -24
- package/lattice_brain/graph/_kg_common/__init__.py +13 -24
- package/lattice_brain/ingestion/__init__.py +19 -58
- package/lattice_brain/ingestion/pipeline.py +20 -398
- package/lattice_brain/multimodal/__init__.py +7 -18
- package/lattice_brain/multimodal/images.py +6 -264
- package/lattice_brain/multimodal/video.py +8 -247
- package/lattice_brain/runtime/__init__.py +8 -79
- package/lattice_brain/runtime/hooks.py +22 -584
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/agent_worker_seam.py +7 -80
- package/latticeai/api/health.py +6 -20
- package/latticeai/api/local_files.py +16 -613
- package/latticeai/api/models.py +12 -87
- package/latticeai/api/search.py +17 -255
- package/latticeai/api/tools.py +69 -750
- package/latticeai/api/voice_capture.py +8 -70
- package/latticeai/api/worker_compute.py +842 -0
- package/latticeai/api/worker_seams.py +216 -0
- package/latticeai/app_factory.py +23 -232
- package/latticeai/cli/entrypoint.py +36 -249
- package/latticeai/core/agent_permission.py +16 -91
- package/latticeai/core/messages.py +89 -531
- package/latticeai/runtime/access_runtime.py +0 -14
- package/latticeai/runtime/bootstrap.py +10 -21
- package/latticeai/runtime/brain_runtime.py +35 -43
- package/latticeai/runtime/build_phases/__init__.py +21 -37
- package/latticeai/runtime/build_phases/features.py +99 -389
- package/latticeai/runtime/build_phases/foundation.py +82 -392
- package/latticeai/runtime/build_phases/web.py +91 -408
- package/latticeai/runtime/build_phases/worker_profile.py +244 -0
- package/latticeai/runtime/lifespan_runtime.py +7 -16
- package/latticeai/runtime/platform_services_runtime.py +1 -15
- package/latticeai/runtime/runtime_context.py +13 -130
- package/latticeai/runtime/security_runtime.py +10 -103
- package/latticeai/services/architecture_readiness.py +82 -43
- package/latticeai/services/model_runtime/__init__.py +2 -11
- package/latticeai/services/model_runtime/service.py +4 -28
- package/latticeai/services/p_reinforce.py +10 -261
- package/latticeai/services/product_readiness.py +31 -38
- package/latticeai/services/search_service.py +37 -795
- package/latticeai/services/tool_dispatch.py +30 -386
- package/latticeai/services/voice_capture.py +13 -107
- package/latticeai/tools/__init__.py +22 -49
- package/latticeai/tools/commands.py +0 -163
- package/latticeai/tools/computer.py +0 -39
- package/latticeai/tools/documents.py +1 -134
- package/latticeai/tools/filesystem.py +1 -247
- package/latticeai/tools/knowledge.py +1 -52
- package/latticeai/tools/local_files.py +0 -20
- package/latticeai/worker_app.py +75 -0
- package/package.json +2 -3
- package/requirements.txt +0 -5
- package/scripts/agent_eval.py +16 -28
- package/scripts/brain_quality_eval.py +20 -183
- package/scripts/bump_version.py +5 -5
- package/scripts/check_current_release_docs.mjs +7 -4
- package/scripts/check_openapi_drift.mjs +10 -0
- package/scripts/check_server_i18n.mjs +2 -18
- package/scripts/compose_openapi.py +377 -0
- package/scripts/export_openapi.py +32 -4
- package/scripts/gen_messages_catalog_fixture.py +302 -0
- package/scripts/gen_openapi_fragments.py +365 -0
- package/scripts/gen_redact_fixture.py +196 -0
- package/scripts/gen_worker_allowlist_fixture.py +115 -0
- package/scripts/generate_agent_parity_fixtures.py +33 -14
- package/scripts/openapi_route_families.json +2161 -0
- package/scripts/release_screen_claims.json +54 -0
- package/scripts/run_integration_tests.mjs +134 -29
- package/scripts/run_sidecar_e2e.mjs +104 -11
- package/scripts/wheel_smoke.py +32 -22
- package/src-tauri/Cargo.lock +375 -9
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/src/backend.rs +151 -45
- package/src-tauri/src/main.rs +18 -18
- package/src-tauri/src/topology.rs +58 -88
- package/src-tauri/tauri.conf.json +1 -2
- package/static/app/asset-manifest.json +41 -41
- package/static/app/assets/{Act-DcQizkl1.js → Act-BPcVAbOL.js} +1 -1
- package/static/app/assets/AdminConsole-Bw1ATQL0.js +1 -0
- package/static/app/assets/{Brain-3VCSHFcn.js → Brain-CT92Kos0.js} +2 -2
- package/static/app/assets/{BrainHome-Qm8eaztx.js → BrainHome-CFBkt1K_.js} +1 -1
- package/static/app/assets/{BrainSignals-DS9BtKOW.js → BrainSignals-ReLWF2H8.js} +1 -1
- package/static/app/assets/Capture-BsTokYkk.js +1 -0
- package/static/app/assets/{Chronicle-BGvuAchH.js → Chronicle-B6f0T9id.js} +1 -1
- package/static/app/assets/{CommandPalette-Bqhm0Urn.js → CommandPalette-CuvjTv1u.js} +1 -1
- package/static/app/assets/{Library-BV6NnF0a.js → Library-BGJbG9Hd.js} +1 -1
- package/static/app/assets/{LivingBrain-GzenJchP.js → LivingBrain-DGYK_Jsa.js} +1 -1
- package/static/app/assets/{ProductFlow-DEP6-vML.js → ProductFlow-DXBC6brE.js} +1 -1
- package/static/app/assets/{ReviewCard-CNZ7XjWG.js → ReviewCard-HXRle3qq.js} +2 -2
- package/static/app/assets/System-CMHSO9qM.js +1 -0
- package/static/app/assets/arrow-left-BfmkskWx.js +1 -0
- package/static/app/assets/{bot-B_K1Tdmw.js → bot-Cn8bWRuq.js} +1 -1
- package/static/app/assets/{brain-DWyaV1L1.js → brain-CQJberbE.js} +1 -1
- package/static/app/assets/{button-aTn4s84A.js → button-Ct9f2_oT.js} +1 -1
- package/static/app/assets/circle-check-DruOxB-4.js +1 -0
- package/static/app/assets/{circle-pause-xKgeGXkT.js → circle-pause-CmzC_apg.js} +1 -1
- package/static/app/assets/{circle-play-DkT6tYPX.js → circle-play-D8mW2aQ7.js} +1 -1
- package/static/app/assets/{cpu-85xYObUC.js → cpu-DZcdd0PZ.js} +1 -1
- package/static/app/assets/{download-B5Fm7YXo.js → download-bv1KEPGQ.js} +1 -1
- package/static/app/assets/{folder-open-kk2Xa52u.js → folder-open-d-Pip5gr.js} +1 -1
- package/static/app/assets/{hard-drive-DkA3zBW_.js → hard-drive-D20iavUb.js} +1 -1
- package/static/app/assets/index-D9x-kSNy.css +2 -0
- package/static/app/assets/{index-BMPdTmlY.js → index-Do83hDzJ.js} +3 -3
- package/static/app/assets/{input-B0nRf2jO.js → input-BLXVNmj1.js} +1 -1
- package/static/app/assets/{link-2-Dwb4gnTc.js → link-2-BPJOFlAy.js} +1 -1
- package/static/app/assets/{permissionCopy-CQDUBrOZ.js → permissionCopy-ChdJd493.js} +1 -1
- package/static/app/assets/primitives-Cv5tbZBY.js +1 -0
- package/static/app/assets/search-CT9aho2j.js +1 -0
- package/static/app/assets/{share-2-BsrxFglO.js → share-2-YNX_NtMU.js} +1 -1
- package/static/app/assets/{shield-alert-5BStfp2_.js → shield-alert-DuQ3zrVL.js} +1 -1
- package/static/app/assets/{textarea-Cg8IUA-k.js → textarea-DqwLnli4.js} +1 -1
- package/static/app/assets/{useFocusTrap-CYKvE46M.js → useFocusTrap-ZVI98jaW.js} +1 -1
- package/static/app/assets/{useMutation-CSn9t1op.js → useMutation-CVC4qv_D.js} +1 -1
- package/static/app/assets/{useQuery-CY2OI2uy.js → useQuery-C7BeG4HU.js} +1 -1
- package/static/app/assets/{utils-Ddol2RWD.js → utils-CiFtIdZq.js} +1 -1
- package/static/app/assets/{workspace-BqDwOz_p.js → workspace-DQz9vIId.js} +1 -1
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/lattice_brain/archive.py +0 -522
- package/lattice_brain/context.py +0 -325
- package/lattice_brain/conversations.py +0 -382
- package/lattice_brain/core.py +0 -82
- package/lattice_brain/graph/_kg_contract.py +0 -249
- package/lattice_brain/graph/curator.py +0 -676
- package/lattice_brain/graph/discovery.py +0 -595
- package/lattice_brain/graph/discovery_index/__init__.py +0 -35
- package/lattice_brain/graph/discovery_index/cleanup.py +0 -182
- package/lattice_brain/graph/discovery_index/extract.py +0 -137
- package/lattice_brain/graph/discovery_index/scan.py +0 -411
- package/lattice_brain/graph/discovery_index/upsert.py +0 -495
- package/lattice_brain/graph/documents.py +0 -380
- package/lattice_brain/graph/fusion.py +0 -395
- package/lattice_brain/graph/identity.py +0 -175
- package/lattice_brain/graph/image_vectors.py +0 -230
- package/lattice_brain/graph/ingest.py +0 -829
- package/lattice_brain/graph/network.py +0 -205
- package/lattice_brain/graph/proactive.py +0 -724
- package/lattice_brain/graph/projection/__init__.py +0 -42
- package/lattice_brain/graph/projection/curation.py +0 -500
- package/lattice_brain/graph/projection/v2_schema.py +0 -518
- package/lattice_brain/graph/provenance.py +0 -524
- package/lattice_brain/graph/rerank.py +0 -163
- package/lattice_brain/graph/retrieval/__init__.py +0 -54
- package/lattice_brain/graph/retrieval/context.py +0 -197
- package/lattice_brain/graph/retrieval/graph_view.py +0 -319
- package/lattice_brain/graph/retrieval/hybrid.py +0 -488
- package/lattice_brain/graph/retrieval/maintenance.py +0 -121
- package/lattice_brain/graph/retrieval/signals.py +0 -95
- package/lattice_brain/graph/retrieval_docgen.py +0 -253
- package/lattice_brain/graph/retrieval_policy.py +0 -180
- package/lattice_brain/graph/retrieval_reads.py +0 -769
- package/lattice_brain/graph/retrieval_vector/__init__.py +0 -42
- package/lattice_brain/graph/retrieval_vector/fingerprint.py +0 -97
- package/lattice_brain/graph/retrieval_vector/indexing.py +0 -347
- package/lattice_brain/graph/retrieval_vector/search.py +0 -560
- package/lattice_brain/graph/retrieval_vector/status.py +0 -374
- package/lattice_brain/graph/schema.py +0 -792
- package/lattice_brain/graph/store.py +0 -268
- package/lattice_brain/graph/vector_index/__init__.py +0 -85
- package/lattice_brain/graph/vector_index/base.py +0 -167
- package/lattice_brain/graph/vector_index/brute_force.py +0 -110
- package/lattice_brain/graph/vector_index/hnsw.py +0 -290
- package/lattice_brain/graph/vector_index/jobs.py +0 -287
- package/lattice_brain/graph/vector_index/quantized.py +0 -148
- package/lattice_brain/graph/vector_index/selector.py +0 -161
- package/lattice_brain/graph/write_master.py +0 -308
- package/lattice_brain/ingestion/_contract.py +0 -90
- package/lattice_brain/ingestion/folder_scan.py +0 -57
- package/lattice_brain/ingestion/folders.py +0 -258
- package/lattice_brain/ingestion/jobs_api.py +0 -107
- package/lattice_brain/ingestion/routing.py +0 -295
- package/lattice_brain/ingestion_jobs.py +0 -380
- package/lattice_brain/memory.py +0 -75
- package/lattice_brain/portability/__init__.py +0 -90
- package/lattice_brain/portability/_contract.py +0 -42
- package/lattice_brain/portability/backups.py +0 -338
- package/lattice_brain/portability/bundles.py +0 -136
- package/lattice_brain/portability/constants.py +0 -93
- package/lattice_brain/portability/fsops.py +0 -138
- package/lattice_brain/portability/service.py +0 -41
- package/lattice_brain/portability/sharing.py +0 -710
- package/lattice_brain/quality.py +0 -543
- package/lattice_brain/retrieval_benchmark_fixtures.py +0 -92
- package/lattice_brain/runtime/agent_runtime.py +0 -859
- package/lattice_brain/runtime/contracts.py +0 -460
- package/lattice_brain/runtime/multi_agent.py +0 -942
- package/lattice_brain/runtime/statuses.py +0 -10
- package/lattice_brain/sealed_box.py +0 -240
- package/lattice_brain/self_model.py +0 -675
- package/lattice_brain/sensitivity.py +0 -94
- package/lattice_brain/storage/__init__.py +0 -22
- package/lattice_brain/storage/base.py +0 -100
- package/lattice_brain/storage/docker.py +0 -105
- package/lattice_brain/storage/factory.py +0 -31
- package/lattice_brain/storage/migration.py +0 -191
- package/lattice_brain/storage/postgres.py +0 -123
- package/lattice_brain/storage/sqlite.py +0 -143
- package/lattice_brain/synthesis.py +0 -824
- package/lattice_brain/workflow.py +0 -497
- package/latticeai/api/admin.py +0 -471
- package/latticeai/api/agent_registry.py +0 -105
- package/latticeai/api/agents.py +0 -228
- package/latticeai/api/auth.py +0 -383
- package/latticeai/api/automation_intelligence.py +0 -401
- package/latticeai/api/brain_intelligence.py +0 -199
- package/latticeai/api/browser.py +0 -493
- package/latticeai/api/change_proposals.py +0 -89
- package/latticeai/api/chat.py +0 -572
- package/latticeai/api/chat_agent_http.py +0 -892
- package/latticeai/api/chat_contracts.py +0 -73
- package/latticeai/api/chat_documents.py +0 -276
- package/latticeai/api/chat_helpers.py +0 -460
- package/latticeai/api/chat_history.py +0 -101
- package/latticeai/api/chat_hybrid.py +0 -113
- package/latticeai/api/chat_intents.py +0 -646
- package/latticeai/api/chat_stream.py +0 -216
- package/latticeai/api/chronicle.py +0 -63
- package/latticeai/api/command_center.py +0 -51
- package/latticeai/api/computer_use.py +0 -474
- package/latticeai/api/evidence_actions.py +0 -48
- package/latticeai/api/features.py +0 -70
- package/latticeai/api/funnel_metrics.py +0 -31
- package/latticeai/api/garden.py +0 -34
- package/latticeai/api/hooks.py +0 -165
- package/latticeai/api/index_jobs.py +0 -145
- package/latticeai/api/invitations.py +0 -100
- package/latticeai/api/knowledge_graph.py +0 -536
- package/latticeai/api/marketplace.py +0 -105
- package/latticeai/api/mcp.py +0 -482
- package/latticeai/api/memory.py +0 -270
- package/latticeai/api/network.py +0 -81
- package/latticeai/api/network_boundary.py +0 -225
- package/latticeai/api/permission_mode.py +0 -61
- package/latticeai/api/permissions.py +0 -436
- package/latticeai/api/plugins.py +0 -126
- package/latticeai/api/portability.py +0 -391
- package/latticeai/api/project_sessions.py +0 -114
- package/latticeai/api/realtime.py +0 -118
- package/latticeai/api/review_queue.py +0 -364
- package/latticeai/api/security_dashboard.py +0 -604
- package/latticeai/api/setup.py +0 -319
- package/latticeai/api/static_routes.py +0 -354
- package/latticeai/api/ui_redirects.py +0 -26
- package/latticeai/api/workflow_designer.py +0 -394
- package/latticeai/api/workspace.py +0 -856
- package/latticeai/api/workspace_scope.py +0 -125
- package/latticeai/core/agent/__init__.py +0 -93
- package/latticeai/core/agent/_contract.py +0 -79
- package/latticeai/core/agent/context.py +0 -57
- package/latticeai/core/agent/deps.py +0 -125
- package/latticeai/core/agent/execution.py +0 -622
- package/latticeai/core/agent/planning.py +0 -145
- package/latticeai/core/agent/recovery.py +0 -157
- package/latticeai/core/agent/runtime.py +0 -210
- package/latticeai/core/agent/verification.py +0 -231
- package/latticeai/core/agent_eval.py +0 -739
- package/latticeai/core/agent_helpers.py +0 -493
- package/latticeai/core/agent_profiles.py +0 -110
- package/latticeai/core/agent_prompts.py +0 -171
- package/latticeai/core/agent_registry.py +0 -232
- package/latticeai/core/agent_state.py +0 -41
- package/latticeai/core/agent_trace.py +0 -104
- package/latticeai/core/artifact_ledger.py +0 -109
- package/latticeai/core/audit.py +0 -260
- package/latticeai/core/builtin_hooks.py +0 -105
- package/latticeai/core/context_builder.py +0 -394
- package/latticeai/core/document_generator.py +0 -103
- package/latticeai/core/enterprise.py +0 -154
- package/latticeai/core/enterprise_admin.py +0 -158
- package/latticeai/core/file_generation/__init__.py +0 -115
- package/latticeai/core/file_generation/bundles.py +0 -76
- package/latticeai/core/file_generation/extraction.py +0 -154
- package/latticeai/core/file_generation/inference.py +0 -235
- package/latticeai/core/file_generation/orchestration.py +0 -152
- package/latticeai/core/file_generation/prompting.py +0 -117
- package/latticeai/core/file_generation/repair.py +0 -114
- package/latticeai/core/file_generation/sanitize.py +0 -61
- package/latticeai/core/file_generation/validation.py +0 -201
- package/latticeai/core/invitations.py +0 -132
- package/latticeai/core/legacy_compatibility.py +0 -243
- package/latticeai/core/logging_safety.py +0 -46
- package/latticeai/core/marketplace.py +0 -293
- package/latticeai/core/mcp_catalog.py +0 -452
- package/latticeai/core/mcp_registry.py +0 -506
- package/latticeai/core/network_boundary.py +0 -168
- package/latticeai/core/oidc.py +0 -208
- package/latticeai/core/plugins.py +0 -432
- package/latticeai/core/product_hardening.py +0 -218
- package/latticeai/core/project_sessions.py +0 -337
- package/latticeai/core/realtime.py +0 -238
- package/latticeai/core/run_explain.py +0 -426
- package/latticeai/core/run_store.py +0 -252
- package/latticeai/core/timezones.py +0 -80
- package/latticeai/core/workspace_computer_memory.py +0 -84
- package/latticeai/core/workspace_graph_trace.py +0 -155
- package/latticeai/core/workspace_indexing.py +0 -102
- package/latticeai/core/workspace_memory.py +0 -77
- package/latticeai/core/workspace_onboarding.py +0 -104
- package/latticeai/core/workspace_os.py +0 -978
- package/latticeai/core/workspace_os_constants.py +0 -126
- package/latticeai/core/workspace_os_state.py +0 -180
- package/latticeai/core/workspace_os_utils.py +0 -103
- package/latticeai/core/workspace_permissions.py +0 -101
- package/latticeai/core/workspace_plugins.py +0 -97
- package/latticeai/core/workspace_relationships.py +0 -99
- package/latticeai/core/workspace_reorganization.py +0 -335
- package/latticeai/core/workspace_review_items.py +0 -112
- package/latticeai/core/workspace_runs.py +0 -726
- package/latticeai/core/workspace_skills.py +0 -109
- package/latticeai/core/workspace_snapshots.py +0 -198
- package/latticeai/core/workspace_timeline.py +0 -110
- package/latticeai/integrations/__init__.py +0 -0
- package/latticeai/integrations/telegram_bot/__init__.py +0 -123
- package/latticeai/integrations/telegram_bot/__main__.py +0 -17
- package/latticeai/integrations/telegram_bot/config.py +0 -86
- package/latticeai/integrations/telegram_bot/dispatch.py +0 -311
- package/latticeai/integrations/telegram_bot/flows.py +0 -478
- package/latticeai/integrations/telegram_bot/helpers.py +0 -322
- package/latticeai/integrations/telegram_bot/screens.py +0 -394
- package/latticeai/runtime/audit_runtime.py +0 -76
- package/latticeai/runtime/automation_runtime.py +0 -81
- package/latticeai/runtime/chat_wiring.py +0 -141
- package/latticeai/runtime/context_runtime.py +0 -66
- package/latticeai/runtime/feature_toggle_wiring.py +0 -157
- package/latticeai/runtime/history_runtime.py +0 -163
- package/latticeai/runtime/history_writer.py +0 -138
- package/latticeai/runtime/hooks_runtime.py +0 -77
- package/latticeai/runtime/model_wiring.py +0 -68
- package/latticeai/runtime/namespace_runtime.py +0 -163
- package/latticeai/runtime/network_boundary_wiring.py +0 -117
- package/latticeai/runtime/network_config_runtime.py +0 -56
- package/latticeai/runtime/permission_mode_wiring.py +0 -112
- package/latticeai/runtime/persistence_runtime.py +0 -159
- package/latticeai/runtime/platform_runtime_wiring.py +0 -89
- package/latticeai/runtime/review_wiring.py +0 -42
- package/latticeai/runtime/router_registration.py +0 -693
- package/latticeai/runtime/service_singletons.py +0 -55
- package/latticeai/runtime/sso_config_runtime.py +0 -128
- package/latticeai/runtime/user_key_runtime.py +0 -106
- package/latticeai/runtime/web_runtime.py +0 -92
- package/latticeai/server_app.py +0 -51
- package/latticeai/services/app_context.py +0 -130
- package/latticeai/services/automation_execution.py +0 -266
- package/latticeai/services/automation_intelligence.py +0 -614
- package/latticeai/services/brain_automation.py +0 -191
- package/latticeai/services/brain_intelligence/__init__.py +0 -58
- package/latticeai/services/brain_intelligence/_contract.py +0 -71
- package/latticeai/services/brain_intelligence/consistency.py +0 -193
- package/latticeai/services/brain_intelligence/constants.py +0 -47
- package/latticeai/services/brain_intelligence/digest.py +0 -258
- package/latticeai/services/brain_intelligence/health.py +0 -331
- package/latticeai/services/brain_intelligence/proposals.py +0 -259
- package/latticeai/services/brain_intelligence/sampling.py +0 -84
- package/latticeai/services/brain_intelligence/service.py +0 -48
- package/latticeai/services/change_proposals.py +0 -471
- package/latticeai/services/chat_service.py +0 -243
- package/latticeai/services/chronicle.py +0 -555
- package/latticeai/services/cloud_egress_audit.py +0 -85
- package/latticeai/services/cloud_extraction.py +0 -129
- package/latticeai/services/cloud_streaming.py +0 -268
- package/latticeai/services/cloud_token_guard.py +0 -84
- package/latticeai/services/command_center.py +0 -548
- package/latticeai/services/evidence_actions.py +0 -258
- package/latticeai/services/feature_toggles.py +0 -502
- package/latticeai/services/folder_watch.py +0 -520
- package/latticeai/services/funnel_metrics.py +0 -307
- package/latticeai/services/hybrid_chat.py +0 -316
- package/latticeai/services/hybrid_context.py +0 -228
- package/latticeai/services/hybrid_policy.py +0 -129
- package/latticeai/services/interop_bridges.py +0 -978
- package/latticeai/services/local_knowledge.py +0 -465
- package/latticeai/services/memory_service/__init__.py +0 -52
- package/latticeai/services/memory_service/_contract.py +0 -100
- package/latticeai/services/memory_service/brief.py +0 -431
- package/latticeai/services/memory_service/constants.py +0 -57
- package/latticeai/services/memory_service/maintenance.py +0 -138
- package/latticeai/services/memory_service/manager.py +0 -186
- package/latticeai/services/memory_service/proof.py +0 -136
- package/latticeai/services/memory_service/recall.py +0 -225
- package/latticeai/services/memory_service/service.py +0 -48
- package/latticeai/services/memory_service/stores.py +0 -110
- package/latticeai/services/mode_store.py +0 -132
- package/latticeai/services/model_recommendation.py +0 -224
- package/latticeai/services/model_runtime/cloud.py +0 -87
- package/latticeai/services/network_boundary_service.py +0 -117
- package/latticeai/services/obsidian_bridge.py +0 -609
- package/latticeai/services/openai_compatible_adapter.py +0 -101
- package/latticeai/services/permission_mode_service.py +0 -122
- package/latticeai/services/platform_runtime.py +0 -366
- package/latticeai/services/review_queue.py +0 -380
- package/latticeai/services/router_context.py +0 -59
- package/latticeai/services/run_executor.py +0 -387
- package/latticeai/services/self_model_service.py +0 -171
- package/latticeai/services/setup_detection.py +0 -147
- package/latticeai/services/triggers.py +0 -378
- package/latticeai/services/upload_service.py +0 -172
- package/latticeai/services/workspace_service.py +0 -165
- package/latticeai/setup/__init__.py +0 -25
- package/latticeai/setup/auto_setup.py +0 -846
- package/latticeai/setup/demo_corpus.py +0 -98
- package/latticeai/setup/wizard/__init__.py +0 -126
- package/latticeai/setup/wizard/catalog.py +0 -175
- package/latticeai/setup/wizard/detect.py +0 -302
- package/latticeai/setup/wizard/install.py +0 -348
- package/latticeai/setup/wizard/paths.py +0 -165
- package/latticeai/setup/wizard/plans.py +0 -74
- package/latticeai/setup/wizard/recommend.py +0 -320
- package/scripts/bench_agent_smoke.py +0 -409
- package/scripts/bench_models.py +0 -540
- package/scripts/bench_vector_index.py +0 -295
- package/scripts/funnel_soft_gate.py +0 -192
- package/scripts/generate_agent_loop_fixtures.py +0 -994
- package/scripts/generate_rust_parity_fixtures.py +0 -908
- package/scripts/migrate_brain_storage.py +0 -57
- package/scripts/parity_fixture_corpus_context.py +0 -162
- package/scripts/parity_fixture_corpus_docgen.py +0 -341
- package/scripts/profile_kg.py +0 -355
- package/server.py +0 -30
- package/static/app/assets/AdminConsole-cf4npybT.js +0 -1
- package/static/app/assets/Capture-DiQ219jW.js +0 -1
- package/static/app/assets/System-CieofHQa.js +0 -1
- package/static/app/assets/arrow-left-kfsrk0mv.js +0 -1
- package/static/app/assets/circle-check-qqLug9nU.js +0 -1
- package/static/app/assets/index-DxmOfNRi.css +0 -2
- package/static/app/assets/primitives-SNp0LRJz.js +0 -1
- 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
|
-
]
|