attocode-code-intel 0.1.0__py3-none-any.whl
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.
- attocode_code_intel-0.1.0.dist-info/METADATA +221 -0
- attocode_code_intel-0.1.0.dist-info/RECORD +345 -0
- attocode_code_intel-0.1.0.dist-info/WHEEL +4 -0
- attocode_code_intel-0.1.0.dist-info/entry_points.txt +2 -0
- attocode_code_intel-0.1.0.dist-info/licenses/LICENSE +21 -0
- attocode_intel/GUIDELINES.md +432 -0
- attocode_intel/__init__.py +52 -0
- attocode_intel/_internal/__init__.py +1 -0
- attocode_intel/_internal/errors/__init__.py +126 -0
- attocode_intel/_internal/integrations/__init__.py +1 -0
- attocode_intel/_internal/integrations/context/__init__.py +1 -0
- attocode_intel/_internal/integrations/context/architecture_drift.py +354 -0
- attocode_intel/_internal/integrations/context/ast_chunker.py +202 -0
- attocode_intel/_internal/integrations/context/ast_service.py +1483 -0
- attocode_intel/_internal/integrations/context/blob_oid.py +423 -0
- attocode_intel/_internal/integrations/context/cache_manifest.py +221 -0
- attocode_intel/_internal/integrations/context/cas.py +466 -0
- attocode_intel/_internal/integrations/context/code_analyzer.py +528 -0
- attocode_intel/_internal/integrations/context/codebase_ast.py +1461 -0
- attocode_intel/_internal/integrations/context/codebase_context.py +1736 -0
- attocode_intel/_internal/integrations/context/cross_mode.py +263 -0
- attocode_intel/_internal/integrations/context/cross_references.py +616 -0
- attocode_intel/_internal/integrations/context/embedding_rotation.py +597 -0
- attocode_intel/_internal/integrations/context/embeddings.py +282 -0
- attocode_intel/_internal/integrations/context/frecency.py +427 -0
- attocode_intel/_internal/integrations/context/fuzzy.py +320 -0
- attocode_intel/_internal/integrations/context/hierarchical_explorer.py +331 -0
- attocode_intel/_internal/integrations/context/hydration.py +90 -0
- attocode_intel/_internal/integrations/context/index_repair.py +87 -0
- attocode_intel/_internal/integrations/context/index_store.py +586 -0
- attocode_intel/_internal/integrations/context/memory_store.py +602 -0
- attocode_intel/_internal/integrations/context/module_resolution.py +52 -0
- attocode_intel/_internal/integrations/context/query_constraints.py +361 -0
- attocode_intel/_internal/integrations/context/query_history.py +417 -0
- attocode_intel/_internal/integrations/context/reranker.py +351 -0
- attocode_intel/_internal/integrations/context/semantic_search.py +2931 -0
- attocode_intel/_internal/integrations/context/syntax_evidence.py +94 -0
- attocode_intel/_internal/integrations/context/systemone_ranker.py +245 -0
- attocode_intel/_internal/integrations/context/temporal_coupling.py +411 -0
- attocode_intel/_internal/integrations/context/trigram_index.py +820 -0
- attocode_intel/_internal/integrations/context/trigram_regex.py +171 -0
- attocode_intel/_internal/integrations/context/ts_parser.py +2199 -0
- attocode_intel/_internal/integrations/context/vector_store.py +795 -0
- attocode_intel/_internal/integrations/feature_flags/__init__.py +357 -0
- attocode_intel/_internal/integrations/lsp/__init__.py +1 -0
- attocode_intel/_internal/integrations/lsp/client.py +1140 -0
- attocode_intel/_internal/integrations/mcp/__init__.py +1 -0
- attocode_intel/_internal/integrations/mcp/client.py +253 -0
- attocode_intel/_internal/integrations/security/__init__.py +1 -0
- attocode_intel/_internal/integrations/security/custom_rules.py +176 -0
- attocode_intel/_internal/integrations/security/dataflow.py +522 -0
- attocode_intel/_internal/integrations/security/dependency_audit.py +303 -0
- attocode_intel/_internal/integrations/security/matcher.py +44 -0
- attocode_intel/_internal/integrations/security/patterns.py +724 -0
- attocode_intel/_internal/integrations/security/scanner.py +351 -0
- attocode_intel/_internal/integrations/utilities/__init__.py +1 -0
- attocode_intel/_internal/integrations/utilities/ignore.py +64 -0
- attocode_intel/_internal/integrations/utilities/token_estimate.py +51 -0
- attocode_intel/_internal/tricks/__init__.py +1 -0
- attocode_intel/_internal/tricks/recursive_context.py +357 -0
- attocode_intel/_internal/types/__init__.py +1 -0
- attocode_intel/_internal/types/events.py +289 -0
- attocode_intel/_internal/types/severity.py +15 -0
- attocode_intel/_shared.py +341 -0
- attocode_intel/analysis_coverage.py +56 -0
- attocode_intel/api/__init__.py +1 -0
- attocode_intel/api/access.py +32 -0
- attocode_intel/api/app.py +247 -0
- attocode_intel/api/auth/__init__.py +128 -0
- attocode_intel/api/auth/api_keys.py +61 -0
- attocode_intel/api/auth/context.py +34 -0
- attocode_intel/api/auth/jwt.py +66 -0
- attocode_intel/api/auth/oauth.py +146 -0
- attocode_intel/api/auth/passwords.py +15 -0
- attocode_intel/api/deps.py +351 -0
- attocode_intel/api/knowledge_bridge.py +13 -0
- attocode_intel/api/mcp_transport.py +51 -0
- attocode_intel/api/middleware.py +411 -0
- attocode_intel/api/models.py +698 -0
- attocode_intel/api/providers/__init__.py +1 -0
- attocode_intel/api/providers/db_provider.py +1106 -0
- attocode_intel/api/providers/local_provider.py +476 -0
- attocode_intel/api/providers/remote_provider.py +740 -0
- attocode_intel/api/repository_source.py +21 -0
- attocode_intel/api/routes/__init__.py +1 -0
- attocode_intel/api/routes/_pin_helper.py +158 -0
- attocode_intel/api/routes/activity.py +148 -0
- attocode_intel/api/routes/adr.py +102 -0
- attocode_intel/api/routes/analysis.py +515 -0
- attocode_intel/api/routes/api_keys.py +170 -0
- attocode_intel/api/routes/auth.py +428 -0
- attocode_intel/api/routes/branches.py +262 -0
- attocode_intel/api/routes/cross_repo_search.py +198 -0
- attocode_intel/api/routes/embeddings.py +386 -0
- attocode_intel/api/routes/files.py +132 -0
- attocode_intel/api/routes/files_v2.py +388 -0
- attocode_intel/api/routes/gc.py +322 -0
- attocode_intel/api/routes/git_v2.py +433 -0
- attocode_intel/api/routes/graph.py +174 -0
- attocode_intel/api/routes/graph_viz.py +160 -0
- attocode_intel/api/routes/health.py +62 -0
- attocode_intel/api/routes/history.py +175 -0
- attocode_intel/api/routes/jobs.py +84 -0
- attocode_intel/api/routes/learning.py +205 -0
- attocode_intel/api/routes/lsp.py +272 -0
- attocode_intel/api/routes/notify.py +623 -0
- attocode_intel/api/routes/operations.py +27 -0
- attocode_intel/api/routes/orgs.py +924 -0
- attocode_intel/api/routes/preferences.py +100 -0
- attocode_intel/api/routes/presence.py +42 -0
- attocode_intel/api/routes/projects.py +116 -0
- attocode_intel/api/routes/repos.py +47 -0
- attocode_intel/api/routes/search.py +180 -0
- attocode_intel/api/routes/snapshots.py +614 -0
- attocode_intel/api/routes/webhooks.py +313 -0
- attocode_intel/api/routes/websocket.py +116 -0
- attocode_intel/api/static/graph.html +239 -0
- attocode_intel/api/utils.py +96 -0
- attocode_intel/artifacts/__init__.py +45 -0
- attocode_intel/artifacts/hashing.py +230 -0
- attocode_intel/artifacts/provenance.py +190 -0
- attocode_intel/artifacts/retrieval_pin.py +159 -0
- attocode_intel/audit.py +59 -0
- attocode_intel/bug_finder.py +206 -0
- attocode_intel/bundle.py +80 -0
- attocode_intel/catalog.py +242 -0
- attocode_intel/change_review.py +75 -0
- attocode_intel/cli.py +3062 -0
- attocode_intel/community.py +63 -0
- attocode_intel/confidence/__init__.py +144 -0
- attocode_intel/confidence/jev.py +234 -0
- attocode_intel/confidence/llm.py +330 -0
- attocode_intel/confidence/redact.py +44 -0
- attocode_intel/confidence/settings.py +74 -0
- attocode_intel/config.py +256 -0
- attocode_intel/crypto.py +31 -0
- attocode_intel/db/__init__.py +7 -0
- attocode_intel/db/base.py +33 -0
- attocode_intel/db/engine.py +61 -0
- attocode_intel/db/models.py +529 -0
- attocode_intel/entrypoint.py +185 -0
- attocode_intel/focused_evidence.py +130 -0
- attocode_intel/freshness.py +154 -0
- attocode_intel/gateway.py +734 -0
- attocode_intel/git/__init__.py +7 -0
- attocode_intel/git/credentials.py +75 -0
- attocode_intel/git/manager.py +493 -0
- attocode_intel/git/models.py +96 -0
- attocode_intel/git/storage.py +98 -0
- attocode_intel/graph_query_parser.py +761 -0
- attocode_intel/helpers.py +712 -0
- attocode_intel/indexing/__init__.py +29 -0
- attocode_intel/indexing/debouncer.py +136 -0
- attocode_intel/indexing/delta_indexer.py +151 -0
- attocode_intel/indexing/full_indexer.py +323 -0
- attocode_intel/indexing/incremental.py +240 -0
- attocode_intel/indexing/parser.py +403 -0
- attocode_intel/indexing/summarizer.py +264 -0
- attocode_intel/installer.py +1896 -0
- attocode_intel/knowledge.py +269 -0
- attocode_intel/local_knowledge.py +77 -0
- attocode_intel/migrations/alembic.ini +36 -0
- attocode_intel/migrations/env.py +71 -0
- attocode_intel/migrations/script.py.mako +27 -0
- attocode_intel/migrations/versions/001_initial_schema.py +219 -0
- attocode_intel/migrations/versions/002_branch_version_and_embeddings.py +61 -0
- attocode_intel/migrations/versions/003_embedding_uniqueness_and_cascade.py +51 -0
- attocode_intel/migrations/versions/004_audit_events.py +38 -0
- attocode_intel/migrations/versions/005_user_preferences.py +30 -0
- attocode_intel/migrations/versions/006_google_oauth.py +27 -0
- attocode_intel/migrations/versions/007_pgvector_embeddings.py +28 -0
- attocode_intel/migrations/versions/008_symbol_references.py +37 -0
- attocode_intel/migrations/versions/009_webhook_secret_encryption.py +25 -0
- attocode_intel/migrations/versions/010_add_commits_table.py +40 -0
- attocode_intel/migrations/versions/011_commit_changed_files.py +25 -0
- attocode_intel/migrations/versions/012_branch_merged_at.py +25 -0
- attocode_intel/migrations/versions/013_revoked_tokens.py +32 -0
- attocode_intel/migrations/versions/014_blame_hunks.py +40 -0
- attocode_intel/migrations/versions/015_commit_file_stats.py +40 -0
- attocode_intel/migrations/versions/016_embedding_provenance_columns.py +179 -0
- attocode_intel/migrations/versions/017_provenance_table.py +102 -0
- attocode_intel/migrations/versions/018_repo_snapshots.py +178 -0
- attocode_intel/migrations/versions/019_symref_caller.py +42 -0
- attocode_intel/migrations/versions/020_shared_knowledge.py +29 -0
- attocode_intel/onboarding.py +258 -0
- attocode_intel/output.py +178 -0
- attocode_intel/precision.py +133 -0
- attocode_intel/presence.py +160 -0
- attocode_intel/probe.py +110 -0
- attocode_intel/project_dir.py +97 -0
- attocode_intel/pubsub.py +113 -0
- attocode_intel/query_ranking.py +242 -0
- attocode_intel/readiness.py +972 -0
- attocode_intel/redis.py +51 -0
- attocode_intel/reference_pages.py +60 -0
- attocode_intel/remote.py +131 -0
- attocode_intel/repo_ranker.py +246 -0
- attocode_intel/request_context.py +82 -0
- attocode_intel/rules/__init__.py +28 -0
- attocode_intel/rules/ci.py +320 -0
- attocode_intel/rules/combinators.py +293 -0
- attocode_intel/rules/enricher.py +127 -0
- attocode_intel/rules/evolution.py +452 -0
- attocode_intel/rules/executor.py +531 -0
- attocode_intel/rules/filters/__init__.py +5 -0
- attocode_intel/rules/filters/pipeline.py +151 -0
- attocode_intel/rules/formatter.py +198 -0
- attocode_intel/rules/hygiene.py +242 -0
- attocode_intel/rules/importers/__init__.py +1 -0
- attocode_intel/rules/importers/semgrep.py +382 -0
- attocode_intel/rules/loader.py +292 -0
- attocode_intel/rules/marketplace.py +375 -0
- attocode_intel/rules/metavar.py +181 -0
- attocode_intel/rules/model.py +317 -0
- attocode_intel/rules/packs/__init__.py +1 -0
- attocode_intel/rules/packs/community/_license_templates/Apache-2.0.txt +176 -0
- attocode_intel/rules/packs/community/_license_templates/MIT.txt +21 -0
- attocode_intel/rules/packs/community/bandit-python/LICENSE +176 -0
- attocode_intel/rules/packs/community/bandit-python/NOTICE +13 -0
- attocode_intel/rules/packs/community/bandit-python/PORTING.md +23 -0
- attocode_intel/rules/packs/community/bandit-python/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B105-hardcoded-password.yaml +23 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B303-weak-md5.yaml +21 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B304-weak-des.yaml +20 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B307-use-of-eval.yaml +22 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B324-weak-sha1.yaml +21 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B501-no-cert-validation.yaml +23 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B602-shell-true.yaml +22 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B608-sqli.yaml +22 -0
- attocode_intel/rules/packs/community/eslint-typescript/LICENSE +21 -0
- attocode_intel/rules/packs/community/eslint-typescript/NOTICE +13 -0
- attocode_intel/rules/packs/community/eslint-typescript/PORTING.md +23 -0
- attocode_intel/rules/packs/community/eslint-typescript/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/eqeqeq.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-eval.yaml +22 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-implied-eval.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-new-func.yaml +21 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-throw-literal.yaml +19 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-var.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-with.yaml +18 -0
- attocode_intel/rules/packs/community/gosec-go/LICENSE +176 -0
- attocode_intel/rules/packs/community/gosec-go/NOTICE +13 -0
- attocode_intel/rules/packs/community/gosec-go/PORTING.md +23 -0
- attocode_intel/rules/packs/community/gosec-go/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G101-hardcoded-credentials.yaml +22 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G102-bind-all-interfaces.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G103-unsafe-block.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G201-sql-format.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G304-file-inclusion.yaml +23 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G401-weak-cipher.yaml +20 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G402-tls-min-version.yaml +20 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G501-md5-import.yaml +21 -0
- attocode_intel/rules/packs/examples/cpp/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/cpp/rules/security.yaml +90 -0
- attocode_intel/rules/packs/examples/cpp/rules/structural.yaml +42 -0
- attocode_intel/rules/packs/examples/csharp/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/csharp/rules/security.yaml +69 -0
- attocode_intel/rules/packs/examples/csharp/rules/structural.yaml +42 -0
- attocode_intel/rules/packs/examples/go/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/go/rules/performance.yaml +136 -0
- attocode_intel/rules/packs/examples/go/rules/security.yaml +48 -0
- attocode_intel/rules/packs/examples/go/rules/structural.yaml +58 -0
- attocode_intel/rules/packs/examples/go/taint/sanitizers.yaml +24 -0
- attocode_intel/rules/packs/examples/go/taint/sinks.yaml +51 -0
- attocode_intel/rules/packs/examples/go/taint/sources.yaml +26 -0
- attocode_intel/rules/packs/examples/java/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/java/rules/correctness.yaml +55 -0
- attocode_intel/rules/packs/examples/java/rules/structural.yaml +48 -0
- attocode_intel/rules/packs/examples/java/taint/sinks.yaml +37 -0
- attocode_intel/rules/packs/examples/java/taint/sources.yaml +21 -0
- attocode_intel/rules/packs/examples/kotlin/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/kotlin/rules/correctness.yaml +50 -0
- attocode_intel/rules/packs/examples/kotlin/rules/structural.yaml +38 -0
- attocode_intel/rules/packs/examples/php/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/php/rules/security.yaml +66 -0
- attocode_intel/rules/packs/examples/php/rules/structural.yaml +41 -0
- attocode_intel/rules/packs/examples/python/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/python/rules/performance.yaml +90 -0
- attocode_intel/rules/packs/examples/python/rules/structural.yaml +55 -0
- attocode_intel/rules/packs/examples/ruby/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/ruby/rules/security.yaml +73 -0
- attocode_intel/rules/packs/examples/ruby/rules/structural.yaml +41 -0
- attocode_intel/rules/packs/examples/rust/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/rust/rules/correctness.yaml +51 -0
- attocode_intel/rules/packs/examples/rust/rules/structural.yaml +49 -0
- attocode_intel/rules/packs/examples/typescript/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/typescript/rules/performance.yaml +70 -0
- attocode_intel/rules/packs/examples/typescript/rules/structural.yaml +48 -0
- attocode_intel/rules/packs/pack_loader.py +244 -0
- attocode_intel/rules/plugins/__init__.py +1 -0
- attocode_intel/rules/plugins/plugin_loader.py +132 -0
- attocode_intel/rules/profiling.py +332 -0
- attocode_intel/rules/registry.py +213 -0
- attocode_intel/rules/sarif.py +187 -0
- attocode_intel/rules/synthesis.py +420 -0
- attocode_intel/rules/testing.py +326 -0
- attocode_intel/server.py +734 -0
- attocode_intel/service.py +3416 -0
- attocode_intel/sharing.py +57 -0
- attocode_intel/storage/__init__.py +6 -0
- attocode_intel/storage/branch_overlay.py +352 -0
- attocode_intel/storage/content_store.py +220 -0
- attocode_intel/storage/dependency_store.py +143 -0
- attocode_intel/storage/diff_engine.py +187 -0
- attocode_intel/storage/embedding_store.py +594 -0
- attocode_intel/storage/provenance_store.py +137 -0
- attocode_intel/storage/security_scanner_db.py +164 -0
- attocode_intel/storage/symbol_store.py +160 -0
- attocode_intel/structural_evidence.py +144 -0
- attocode_intel/symbol_inspection.py +141 -0
- attocode_intel/symbol_links.py +167 -0
- attocode_intel/telemetry.py +42 -0
- attocode_intel/test_ranking.py +73 -0
- attocode_intel/testing/__init__.py +43 -0
- attocode_intel/testing/fixtures.py +122 -0
- attocode_intel/testing/helpers.py +75 -0
- attocode_intel/testing/mocks.py +99 -0
- attocode_intel/tools/__init__.py +2 -0
- attocode_intel/tools/adr_tools.py +628 -0
- attocode_intel/tools/analysis_tools.py +382 -0
- attocode_intel/tools/composite_tools.py +695 -0
- attocode_intel/tools/cross_mode_tools.py +173 -0
- attocode_intel/tools/dead_code_tools.py +582 -0
- attocode_intel/tools/distill_tools.py +353 -0
- attocode_intel/tools/frecency_tools.py +220 -0
- attocode_intel/tools/fuzzy_tools.py +255 -0
- attocode_intel/tools/history_tools.py +486 -0
- attocode_intel/tools/learning_tools.py +222 -0
- attocode_intel/tools/lsp_tools.py +218 -0
- attocode_intel/tools/maintenance_tools.py +1662 -0
- attocode_intel/tools/navigation_tools.py +220 -0
- attocode_intel/tools/overlay_tools.py +540 -0
- attocode_intel/tools/pin_store.py +432 -0
- attocode_intel/tools/pin_tools.py +266 -0
- attocode_intel/tools/query_constraints_tools.py +262 -0
- attocode_intel/tools/query_history_tools.py +158 -0
- attocode_intel/tools/readiness_tools.py +44 -0
- attocode_intel/tools/rule_tools.py +992 -0
- attocode_intel/tools/search_tools.py +770 -0
- attocode_intel/tools/snapshot_tools.py +624 -0
- attocode_intel/workers/__init__.py +1 -0
- attocode_intel/workers/job_utils.py +111 -0
- attocode_intel/workers/jobs.py +573 -0
- attocode_intel/workers/run.py +30 -0
- attocode_intel/workers/settings.py +61 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Utility helpers for ARQ job implementations and enqueuing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import time
|
|
7
|
+
import uuid
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
_recent_enqueues: dict[str, float] = {}
|
|
16
|
+
_DEDUP_WINDOW_S = 60
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def enqueue_embedding_job(repo_id: str, branch_name: str = "main") -> bool:
|
|
20
|
+
"""Enqueue a generate_embeddings job via ARQ.
|
|
21
|
+
|
|
22
|
+
Returns True if enqueued, False if skipped (dedup or no Redis).
|
|
23
|
+
Silently returns False if Redis is not configured — graceful degradation.
|
|
24
|
+
"""
|
|
25
|
+
from attocode_intel.config import CodeIntelConfig
|
|
26
|
+
|
|
27
|
+
config = CodeIntelConfig.from_env()
|
|
28
|
+
if not config.redis_url:
|
|
29
|
+
logger.debug("Redis not configured, skipping embedding job enqueue")
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
# Dedup: skip if same repo+branch was enqueued in last 60s
|
|
33
|
+
key = f"{repo_id}:{branch_name}"
|
|
34
|
+
now = time.monotonic()
|
|
35
|
+
if key in _recent_enqueues and now - _recent_enqueues[key] < _DEDUP_WINDOW_S:
|
|
36
|
+
logger.debug("Skipping duplicate embedding enqueue for %s", key)
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
from arq import create_pool
|
|
41
|
+
from arq.connections import RedisSettings
|
|
42
|
+
|
|
43
|
+
pool = await create_pool(RedisSettings.from_dsn(config.redis_url))
|
|
44
|
+
await pool.enqueue_job("generate_embeddings", repo_id, branch_name)
|
|
45
|
+
await pool.close()
|
|
46
|
+
_recent_enqueues[key] = now
|
|
47
|
+
logger.info("Enqueued embedding job for repo %s branch %s", repo_id, branch_name)
|
|
48
|
+
return True
|
|
49
|
+
except Exception:
|
|
50
|
+
logger.warning("Failed to enqueue embedding job", exc_info=True)
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
async def get_repo_or_error(
|
|
55
|
+
repo_id: str, session: AsyncSession,
|
|
56
|
+
):
|
|
57
|
+
"""Look up a Repository by ID or return an error dict.
|
|
58
|
+
|
|
59
|
+
Returns (repo, None) on success, or (None, {"error": ...}) on failure.
|
|
60
|
+
"""
|
|
61
|
+
from sqlalchemy import select
|
|
62
|
+
|
|
63
|
+
from attocode_intel.db.models import Repository
|
|
64
|
+
|
|
65
|
+
result = await session.execute(
|
|
66
|
+
select(Repository).where(Repository.id == uuid.UUID(repo_id))
|
|
67
|
+
)
|
|
68
|
+
repo = result.scalar_one_or_none()
|
|
69
|
+
if repo is None:
|
|
70
|
+
return None, {"error": f"Repository {repo_id} not found"}
|
|
71
|
+
return repo, None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
async def get_branch_or_error(
|
|
75
|
+
repo_id: uuid.UUID, branch_name: str, session: AsyncSession,
|
|
76
|
+
):
|
|
77
|
+
"""Look up a Branch by repo ID and name, or return an error dict.
|
|
78
|
+
|
|
79
|
+
Returns (branch, None) on success, or (None, {"error": ...}) on failure.
|
|
80
|
+
"""
|
|
81
|
+
from sqlalchemy import select
|
|
82
|
+
|
|
83
|
+
from attocode_intel.db.models import Branch
|
|
84
|
+
|
|
85
|
+
result = await session.execute(
|
|
86
|
+
select(Branch).where(Branch.repo_id == repo_id, Branch.name == branch_name)
|
|
87
|
+
)
|
|
88
|
+
branch = result.scalar_one_or_none()
|
|
89
|
+
if branch is None:
|
|
90
|
+
return None, {"error": f"Branch {branch_name} not found"}
|
|
91
|
+
return branch, None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
async def resolve_repo_credential(repo_id, session: AsyncSession):
|
|
95
|
+
"""Fetch and decrypt RepoCredential for a repo.
|
|
96
|
+
|
|
97
|
+
Returns a Credential instance or None if no credential is configured.
|
|
98
|
+
"""
|
|
99
|
+
from sqlalchemy import select
|
|
100
|
+
|
|
101
|
+
from attocode_intel.crypto import decrypt_credential
|
|
102
|
+
from attocode_intel.db.models import RepoCredential
|
|
103
|
+
from attocode_intel.git.credentials import Credential
|
|
104
|
+
|
|
105
|
+
result = await session.execute(
|
|
106
|
+
select(RepoCredential).where(RepoCredential.repo_id == repo_id).limit(1)
|
|
107
|
+
)
|
|
108
|
+
repo_cred = result.scalar_one_or_none()
|
|
109
|
+
if repo_cred is None:
|
|
110
|
+
return None
|
|
111
|
+
return Credential(cred_type=repo_cred.cred_type, value=decrypt_credential(repo_cred.encrypted_value))
|
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
"""ARQ job implementations for background processing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def index_repository(ctx: dict, repo_id: str, branch_name: str = "main") -> dict:
|
|
12
|
+
"""Full index of a repository branch.
|
|
13
|
+
|
|
14
|
+
Timeout: 30min, Retries: 2, Priority: medium
|
|
15
|
+
"""
|
|
16
|
+
from attocode_intel.db.engine import get_session
|
|
17
|
+
from attocode_intel.db.models import Branch, IndexingJob
|
|
18
|
+
from attocode_intel.pubsub import publish_event
|
|
19
|
+
|
|
20
|
+
logger.info("Starting full index for repo %s branch %s", repo_id, branch_name)
|
|
21
|
+
await publish_event(repo_id, "index.started", {"branch": branch_name})
|
|
22
|
+
|
|
23
|
+
async for session in get_session():
|
|
24
|
+
try:
|
|
25
|
+
from sqlalchemy import select
|
|
26
|
+
|
|
27
|
+
from attocode_intel.workers.job_utils import get_repo_or_error
|
|
28
|
+
|
|
29
|
+
repo, err = await get_repo_or_error(repo_id, session)
|
|
30
|
+
if err:
|
|
31
|
+
return err
|
|
32
|
+
|
|
33
|
+
# Create/update job record
|
|
34
|
+
job = IndexingJob(
|
|
35
|
+
repo_id=repo.id,
|
|
36
|
+
job_type="full_index",
|
|
37
|
+
status="running",
|
|
38
|
+
branch_name=branch_name,
|
|
39
|
+
started_at=datetime.now(timezone.utc),
|
|
40
|
+
)
|
|
41
|
+
session.add(job)
|
|
42
|
+
await session.flush()
|
|
43
|
+
|
|
44
|
+
# Get or create branch
|
|
45
|
+
branch_result = await session.execute(
|
|
46
|
+
select(Branch).where(Branch.repo_id == repo.id, Branch.name == branch_name)
|
|
47
|
+
)
|
|
48
|
+
branch = branch_result.scalar_one_or_none()
|
|
49
|
+
if branch is None:
|
|
50
|
+
branch = Branch(
|
|
51
|
+
repo_id=repo.id,
|
|
52
|
+
name=branch_name,
|
|
53
|
+
is_default=(branch_name == repo.default_branch),
|
|
54
|
+
)
|
|
55
|
+
session.add(branch)
|
|
56
|
+
await session.flush()
|
|
57
|
+
|
|
58
|
+
# Progress callback
|
|
59
|
+
async def on_progress(data: dict) -> None:
|
|
60
|
+
await publish_event(repo_id, "index.progress", data)
|
|
61
|
+
|
|
62
|
+
# Clone if needed, then index
|
|
63
|
+
from attocode_intel.api.deps import get_config
|
|
64
|
+
from attocode_intel.git.manager import GitRepoManager
|
|
65
|
+
from attocode_intel.indexing.full_indexer import FullIndexer
|
|
66
|
+
|
|
67
|
+
config = get_config()
|
|
68
|
+
git_mgr = GitRepoManager(config.git_clone_dir, config.git_ssh_key_path)
|
|
69
|
+
|
|
70
|
+
# Clone the repo if it has a clone_url and hasn't been cloned yet
|
|
71
|
+
if repo.clone_url and not repo.clone_path:
|
|
72
|
+
import asyncio
|
|
73
|
+
|
|
74
|
+
from attocode_intel.workers.job_utils import resolve_repo_credential
|
|
75
|
+
|
|
76
|
+
credential = await resolve_repo_credential(repo.id, session)
|
|
77
|
+
loop = asyncio.get_running_loop()
|
|
78
|
+
clone_path = await loop.run_in_executor(
|
|
79
|
+
None, git_mgr.clone, repo.clone_url, repo_id, credential,
|
|
80
|
+
)
|
|
81
|
+
repo.clone_path = clone_path
|
|
82
|
+
await session.flush()
|
|
83
|
+
logger.info("Cloned %s to %s", repo.clone_url, clone_path)
|
|
84
|
+
|
|
85
|
+
# Detect default branch from cloned repo
|
|
86
|
+
branches = git_mgr.list_branches(repo_id)
|
|
87
|
+
default = next((b for b in branches if b.is_default), None)
|
|
88
|
+
if default and default.name != repo.default_branch:
|
|
89
|
+
repo.default_branch = default.name
|
|
90
|
+
if branch_name == "main" and default.name != "main":
|
|
91
|
+
branch_name = default.name
|
|
92
|
+
branch.name = default.name
|
|
93
|
+
branch.is_default = True
|
|
94
|
+
await session.flush()
|
|
95
|
+
|
|
96
|
+
# Determine the working path for indexing
|
|
97
|
+
work_path = repo.clone_path or repo.local_path
|
|
98
|
+
if not work_path:
|
|
99
|
+
return {"error": "No clone_path or local_path for repo"}
|
|
100
|
+
|
|
101
|
+
# Guard: if the path doesn't exist on this machine, skip
|
|
102
|
+
# (local_path is a client-side path that the server can't access)
|
|
103
|
+
import os
|
|
104
|
+
|
|
105
|
+
if not os.path.exists(work_path):
|
|
106
|
+
logger.warning(
|
|
107
|
+
"Skipping full index for repo %s: path %s not accessible (remote client path?)",
|
|
108
|
+
repo_id, work_path,
|
|
109
|
+
)
|
|
110
|
+
repo.index_status = "pending_client"
|
|
111
|
+
await session.commit()
|
|
112
|
+
return {"skipped": True, "reason": f"Path {work_path} not accessible from server"}
|
|
113
|
+
|
|
114
|
+
# Register path override so git operations use the correct directory
|
|
115
|
+
git_mgr.register_path(repo_id, work_path)
|
|
116
|
+
|
|
117
|
+
# Advisory lock: prevent concurrent indexing of the same branch
|
|
118
|
+
from attocode_intel.indexing.incremental import IncrementalPipeline
|
|
119
|
+
|
|
120
|
+
await IncrementalPipeline.acquire_branch_lock(session, branch.id)
|
|
121
|
+
|
|
122
|
+
indexer = FullIndexer(session, git_mgr, progress_callback=lambda d: None)
|
|
123
|
+
|
|
124
|
+
stats = await indexer.index(repo_id, branch.id, ref=branch_name)
|
|
125
|
+
|
|
126
|
+
# Update records
|
|
127
|
+
branch.last_indexed_at = datetime.now(timezone.utc)
|
|
128
|
+
repo.index_status = "indexed"
|
|
129
|
+
repo.last_indexed_at = datetime.now(timezone.utc)
|
|
130
|
+
job.status = "completed"
|
|
131
|
+
job.completed_at = datetime.now(timezone.utc)
|
|
132
|
+
job.result = stats
|
|
133
|
+
|
|
134
|
+
await session.commit()
|
|
135
|
+
await publish_event(repo_id, "index.completed", stats)
|
|
136
|
+
|
|
137
|
+
# Auto-trigger embedding generation after successful index
|
|
138
|
+
from attocode_intel.workers.job_utils import enqueue_embedding_job
|
|
139
|
+
await enqueue_embedding_job(repo_id, branch_name)
|
|
140
|
+
|
|
141
|
+
return stats
|
|
142
|
+
|
|
143
|
+
except Exception as e:
|
|
144
|
+
logger.exception("Error indexing repo %s", repo_id)
|
|
145
|
+
try:
|
|
146
|
+
repo.index_status = "failed"
|
|
147
|
+
job.status = "failed"
|
|
148
|
+
job.completed_at = datetime.now(timezone.utc)
|
|
149
|
+
job.error = str(e)
|
|
150
|
+
job.result = {"error": str(e)}
|
|
151
|
+
await session.commit()
|
|
152
|
+
except Exception:
|
|
153
|
+
logger.warning("Failed to persist error state for repo %s", repo_id)
|
|
154
|
+
await publish_event(repo_id, "index.failed", {"error": str(e)})
|
|
155
|
+
return {"error": str(e)}
|
|
156
|
+
|
|
157
|
+
return {"error": "No session available"}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
async def index_branch_delta(
|
|
161
|
+
ctx: dict,
|
|
162
|
+
repo_id: str,
|
|
163
|
+
branch_name: str,
|
|
164
|
+
from_ref: str,
|
|
165
|
+
to_ref: str,
|
|
166
|
+
) -> dict:
|
|
167
|
+
"""Delta index of changed files between two refs.
|
|
168
|
+
|
|
169
|
+
Timeout: 10min, Retries: 3, Priority: medium
|
|
170
|
+
"""
|
|
171
|
+
from attocode_intel.db.engine import get_session
|
|
172
|
+
from attocode_intel.db.models import IndexingJob
|
|
173
|
+
from attocode_intel.pubsub import publish_event
|
|
174
|
+
|
|
175
|
+
logger.info("Starting delta index for repo %s: %s..%s", repo_id, from_ref, to_ref)
|
|
176
|
+
await publish_event(repo_id, "index.started", {"branch": branch_name, "type": "delta"})
|
|
177
|
+
|
|
178
|
+
async for session in get_session():
|
|
179
|
+
try:
|
|
180
|
+
from attocode_intel.workers.job_utils import get_branch_or_error, get_repo_or_error
|
|
181
|
+
|
|
182
|
+
repo, err = await get_repo_or_error(repo_id, session)
|
|
183
|
+
if err:
|
|
184
|
+
return err
|
|
185
|
+
|
|
186
|
+
branch, err = await get_branch_or_error(repo.id, branch_name, session)
|
|
187
|
+
if err:
|
|
188
|
+
return err
|
|
189
|
+
|
|
190
|
+
job = IndexingJob(
|
|
191
|
+
repo_id=repo.id,
|
|
192
|
+
job_type="delta_index",
|
|
193
|
+
status="running",
|
|
194
|
+
branch_name=branch_name,
|
|
195
|
+
started_at=datetime.now(timezone.utc),
|
|
196
|
+
)
|
|
197
|
+
session.add(job)
|
|
198
|
+
await session.flush()
|
|
199
|
+
|
|
200
|
+
from attocode_intel.api.deps import get_config
|
|
201
|
+
from attocode_intel.git.manager import GitRepoManager
|
|
202
|
+
from attocode_intel.indexing.delta_indexer import DeltaIndexer
|
|
203
|
+
|
|
204
|
+
config = get_config()
|
|
205
|
+
git_mgr = GitRepoManager(config.git_clone_dir, config.git_ssh_key_path)
|
|
206
|
+
|
|
207
|
+
# Fetch latest commits for bare clones
|
|
208
|
+
if repo.clone_url and repo.clone_path:
|
|
209
|
+
import asyncio
|
|
210
|
+
|
|
211
|
+
from attocode_intel.workers.job_utils import resolve_repo_credential
|
|
212
|
+
|
|
213
|
+
credential = await resolve_repo_credential(repo.id, session)
|
|
214
|
+
loop = asyncio.get_running_loop()
|
|
215
|
+
await loop.run_in_executor(None, git_mgr.fetch, str(repo.id), credential)
|
|
216
|
+
|
|
217
|
+
# Advisory lock: prevent concurrent indexing of the same branch
|
|
218
|
+
from attocode_intel.indexing.incremental import IncrementalPipeline
|
|
219
|
+
|
|
220
|
+
await IncrementalPipeline.acquire_branch_lock(session, branch.id)
|
|
221
|
+
|
|
222
|
+
indexer = DeltaIndexer(session, git_mgr)
|
|
223
|
+
|
|
224
|
+
stats = await indexer.index(repo_id, branch.id, from_ref, to_ref)
|
|
225
|
+
|
|
226
|
+
branch.head_commit = to_ref
|
|
227
|
+
branch.last_indexed_at = datetime.now(timezone.utc)
|
|
228
|
+
job.status = "completed"
|
|
229
|
+
job.completed_at = datetime.now(timezone.utc)
|
|
230
|
+
job.result = stats
|
|
231
|
+
|
|
232
|
+
await session.commit()
|
|
233
|
+
await publish_event(repo_id, "index.completed", stats)
|
|
234
|
+
return stats
|
|
235
|
+
|
|
236
|
+
except Exception as e:
|
|
237
|
+
logger.exception("Error delta-indexing repo %s", repo_id)
|
|
238
|
+
try:
|
|
239
|
+
job.status = "failed"
|
|
240
|
+
job.completed_at = datetime.now(timezone.utc)
|
|
241
|
+
job.error = str(e)
|
|
242
|
+
job.result = {"error": str(e)}
|
|
243
|
+
await session.commit()
|
|
244
|
+
except Exception:
|
|
245
|
+
logger.warning("Failed to persist error state for delta-index repo %s", repo_id)
|
|
246
|
+
await publish_event(repo_id, "index.failed", {"error": str(e)})
|
|
247
|
+
return {"error": str(e)}
|
|
248
|
+
|
|
249
|
+
return {"error": "No session available"}
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
async def generate_embeddings(ctx: dict, repo_id: str, branch_name: str = "main") -> dict:
|
|
253
|
+
"""Generate embeddings for all files in a branch.
|
|
254
|
+
|
|
255
|
+
Timeout: 20min, Retries: 2, Priority: low
|
|
256
|
+
|
|
257
|
+
Pipeline:
|
|
258
|
+
1. Resolve branch manifest (all content_shas)
|
|
259
|
+
2. Check which shas already have embeddings
|
|
260
|
+
3. For each missing sha: read content, embed, store with vector
|
|
261
|
+
4. Batch 32 files at a time for memory efficiency
|
|
262
|
+
"""
|
|
263
|
+
from attocode_intel.db.engine import get_session
|
|
264
|
+
from attocode_intel.db.models import FileContent
|
|
265
|
+
from attocode_intel.pubsub import publish_event
|
|
266
|
+
|
|
267
|
+
logger.info("Starting embedding generation for repo %s branch %s", repo_id, branch_name)
|
|
268
|
+
await publish_event(repo_id, "embeddings.started", {"branch": branch_name})
|
|
269
|
+
|
|
270
|
+
async for session in get_session():
|
|
271
|
+
try:
|
|
272
|
+
from sqlalchemy import select
|
|
273
|
+
|
|
274
|
+
from attocode_intel.workers.job_utils import get_branch_or_error, get_repo_or_error
|
|
275
|
+
|
|
276
|
+
# 1. Get repo + branch
|
|
277
|
+
repo, err = await get_repo_or_error(repo_id, session)
|
|
278
|
+
if err:
|
|
279
|
+
return err
|
|
280
|
+
|
|
281
|
+
branch, err = await get_branch_or_error(repo.id, branch_name, session)
|
|
282
|
+
if err:
|
|
283
|
+
return err
|
|
284
|
+
|
|
285
|
+
# 2. Create embedding provider
|
|
286
|
+
from attocode_intel.api.deps import get_config
|
|
287
|
+
from attocode_intel._internal.integrations.context.embeddings import create_embedding_provider
|
|
288
|
+
|
|
289
|
+
config = get_config()
|
|
290
|
+
try:
|
|
291
|
+
provider = create_embedding_provider(config.embedding_model)
|
|
292
|
+
except (ImportError, RuntimeError) as exc:
|
|
293
|
+
return {"error": str(exc)}
|
|
294
|
+
if provider.name == "none":
|
|
295
|
+
return {"error": "No embedding provider available", "hint": "Install sentence-transformers or set OPENAI_API_KEY"}
|
|
296
|
+
|
|
297
|
+
dim = provider.dimension()
|
|
298
|
+
|
|
299
|
+
# 3. Ensure vector column exists with the right dimension
|
|
300
|
+
from attocode_intel.storage.embedding_store import (
|
|
301
|
+
EmbeddingStore,
|
|
302
|
+
ensure_vector_columns,
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
await ensure_vector_columns(session, primary_dim=dim)
|
|
306
|
+
|
|
307
|
+
# 4. Resolve branch manifest
|
|
308
|
+
from attocode_intel.storage.branch_overlay import BranchOverlay
|
|
309
|
+
|
|
310
|
+
overlay = BranchOverlay(session)
|
|
311
|
+
manifest = await overlay.resolve_manifest(branch.id)
|
|
312
|
+
all_shas = set(manifest.values())
|
|
313
|
+
|
|
314
|
+
if not all_shas:
|
|
315
|
+
return {"embedded": 0, "skipped": 0, "total": 0, "model": provider.name, "dimension": dim}
|
|
316
|
+
|
|
317
|
+
# 5. Find which SHAs need embedding
|
|
318
|
+
store = EmbeddingStore(session)
|
|
319
|
+
existing_shas = await store.batch_has_embeddings(all_shas, provider.name)
|
|
320
|
+
missing_shas = all_shas - existing_shas
|
|
321
|
+
|
|
322
|
+
embedded_count = 0
|
|
323
|
+
skipped_count = len(existing_shas)
|
|
324
|
+
batch_size = 32
|
|
325
|
+
|
|
326
|
+
# 6. Process in batches
|
|
327
|
+
sha_list = list(missing_shas)
|
|
328
|
+
for i in range(0, len(sha_list), batch_size):
|
|
329
|
+
batch_shas = sha_list[i:i + batch_size]
|
|
330
|
+
|
|
331
|
+
# Read content for this batch
|
|
332
|
+
content_result = await session.execute(
|
|
333
|
+
select(FileContent).where(FileContent.sha256.in_(batch_shas))
|
|
334
|
+
)
|
|
335
|
+
contents = {fc.sha256: fc for fc in content_result.scalars()}
|
|
336
|
+
|
|
337
|
+
texts = []
|
|
338
|
+
shas_in_batch = []
|
|
339
|
+
for sha in batch_shas:
|
|
340
|
+
fc = contents.get(sha)
|
|
341
|
+
if fc is None:
|
|
342
|
+
continue
|
|
343
|
+
try:
|
|
344
|
+
text_content = fc.content.decode("utf-8", errors="replace")
|
|
345
|
+
text_content = text_content.replace('\x00', '')
|
|
346
|
+
except Exception:
|
|
347
|
+
continue
|
|
348
|
+
# Truncate very large files to avoid OOM in embedding model
|
|
349
|
+
if len(text_content) > 100_000:
|
|
350
|
+
text_content = text_content[:100_000]
|
|
351
|
+
texts.append(text_content)
|
|
352
|
+
shas_in_batch.append(sha)
|
|
353
|
+
|
|
354
|
+
if not texts:
|
|
355
|
+
continue
|
|
356
|
+
|
|
357
|
+
# Embed the batch (CPU-bound, run in executor to avoid blocking event loop)
|
|
358
|
+
try:
|
|
359
|
+
import asyncio
|
|
360
|
+
loop = asyncio.get_running_loop()
|
|
361
|
+
vectors = await loop.run_in_executor(None, provider.embed, texts)
|
|
362
|
+
except Exception:
|
|
363
|
+
logger.exception("Embedding failed for batch starting at index %d", i)
|
|
364
|
+
continue
|
|
365
|
+
|
|
366
|
+
# Store embeddings with vectors
|
|
367
|
+
for sha, text_content, vector in zip(shas_in_batch, texts, vectors):
|
|
368
|
+
await store.upsert_embeddings(sha, [{
|
|
369
|
+
"embedding_model": provider.name,
|
|
370
|
+
"chunk_text": text_content[:500], # Store truncated preview
|
|
371
|
+
"chunk_type": "file",
|
|
372
|
+
"vector": vector,
|
|
373
|
+
}])
|
|
374
|
+
embedded_count += 1
|
|
375
|
+
|
|
376
|
+
# Publish progress
|
|
377
|
+
await publish_event(repo_id, "embeddings.progress", {
|
|
378
|
+
"branch": branch_name,
|
|
379
|
+
"embedded": embedded_count,
|
|
380
|
+
"total": len(missing_shas),
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
await session.commit()
|
|
384
|
+
|
|
385
|
+
stats = {
|
|
386
|
+
"embedded": embedded_count,
|
|
387
|
+
"skipped": skipped_count,
|
|
388
|
+
"total": len(all_shas),
|
|
389
|
+
"model": provider.name,
|
|
390
|
+
"dimension": dim,
|
|
391
|
+
}
|
|
392
|
+
await publish_event(repo_id, "embeddings.completed", stats)
|
|
393
|
+
logger.info("Embedding generation complete for repo %s: %s", repo_id, stats)
|
|
394
|
+
return stats
|
|
395
|
+
|
|
396
|
+
except Exception as e:
|
|
397
|
+
logger.exception("Error generating embeddings for repo %s", repo_id)
|
|
398
|
+
await publish_event(repo_id, "embeddings.failed", {"error": str(e)})
|
|
399
|
+
return {"error": str(e)}
|
|
400
|
+
|
|
401
|
+
return {"error": "No session available"}
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
async def cleanup_stale_branches(ctx: dict) -> dict:
|
|
405
|
+
"""Cron job: remove branch overlays for branches that no longer exist.
|
|
406
|
+
|
|
407
|
+
Runs every 6 hours. For local repos: compares DB branches against git.
|
|
408
|
+
For remote repos: cleans up merged branches (merged_at > 7 days ago)
|
|
409
|
+
and inactive branches (no overlay changes and no commits in 30 days).
|
|
410
|
+
"""
|
|
411
|
+
from datetime import timedelta
|
|
412
|
+
|
|
413
|
+
from sqlalchemy import func as sa_func
|
|
414
|
+
from sqlalchemy import select
|
|
415
|
+
|
|
416
|
+
from attocode_intel.api.deps import get_config
|
|
417
|
+
from attocode_intel.db.engine import get_session
|
|
418
|
+
from attocode_intel.db.models import Branch, BranchFile, Commit, Repository
|
|
419
|
+
from attocode_intel.git.manager import GitRepoManager
|
|
420
|
+
|
|
421
|
+
logger.info("Running stale branch cleanup")
|
|
422
|
+
cleaned = 0
|
|
423
|
+
|
|
424
|
+
async for session in get_session():
|
|
425
|
+
config = get_config()
|
|
426
|
+
git_mgr = GitRepoManager(config.git_clone_dir, config.git_ssh_key_path)
|
|
427
|
+
now = datetime.now(timezone.utc)
|
|
428
|
+
|
|
429
|
+
result = await session.execute(select(Repository))
|
|
430
|
+
repos = result.scalars().all()
|
|
431
|
+
|
|
432
|
+
for repo in repos:
|
|
433
|
+
if repo.clone_path:
|
|
434
|
+
# Local/cloned repos: compare against git branches
|
|
435
|
+
try:
|
|
436
|
+
git_branches = {b.name for b in git_mgr.list_branches(str(repo.id))}
|
|
437
|
+
except (FileNotFoundError, ValueError):
|
|
438
|
+
continue
|
|
439
|
+
|
|
440
|
+
branch_result = await session.execute(
|
|
441
|
+
select(Branch).where(Branch.repo_id == repo.id)
|
|
442
|
+
)
|
|
443
|
+
for db_branch in branch_result.scalars():
|
|
444
|
+
if db_branch.name not in git_branches and not db_branch.is_default:
|
|
445
|
+
logger.info(
|
|
446
|
+
"Removing stale branch %s from repo %s",
|
|
447
|
+
db_branch.name, repo.id,
|
|
448
|
+
)
|
|
449
|
+
await session.delete(db_branch)
|
|
450
|
+
cleaned += 1
|
|
451
|
+
else:
|
|
452
|
+
# Remote repos: clean up merged + inactive branches
|
|
453
|
+
branch_result = await session.execute(
|
|
454
|
+
select(Branch).where(Branch.repo_id == repo.id)
|
|
455
|
+
)
|
|
456
|
+
for db_branch in branch_result.scalars():
|
|
457
|
+
if db_branch.is_default:
|
|
458
|
+
continue
|
|
459
|
+
|
|
460
|
+
# Clean up merged branches older than retention threshold
|
|
461
|
+
if db_branch.merged_at and (now - db_branch.merged_at) > timedelta(days=config.gc_merged_branch_retention_days):
|
|
462
|
+
logger.info(
|
|
463
|
+
"Removing merged branch %s from repo %s (merged %s)",
|
|
464
|
+
db_branch.name, repo.id, db_branch.merged_at,
|
|
465
|
+
)
|
|
466
|
+
await session.delete(db_branch)
|
|
467
|
+
cleaned += 1
|
|
468
|
+
continue
|
|
469
|
+
|
|
470
|
+
# Clean up inactive branches: no overlay changes AND no commits in retention period
|
|
471
|
+
if not db_branch.merged_at:
|
|
472
|
+
overlay_count_result = await session.execute(
|
|
473
|
+
select(sa_func.count()).select_from(
|
|
474
|
+
select(BranchFile.path)
|
|
475
|
+
.where(BranchFile.branch_id == db_branch.id)
|
|
476
|
+
.subquery()
|
|
477
|
+
)
|
|
478
|
+
)
|
|
479
|
+
overlay_count = overlay_count_result.scalar() or 0
|
|
480
|
+
|
|
481
|
+
if overlay_count == 0:
|
|
482
|
+
cutoff = now - timedelta(days=config.gc_inactive_branch_retention_days)
|
|
483
|
+
recent_commit_result = await session.execute(
|
|
484
|
+
select(sa_func.count()).select_from(
|
|
485
|
+
select(Commit.id)
|
|
486
|
+
.where(
|
|
487
|
+
Commit.repo_id == repo.id,
|
|
488
|
+
Commit.branch_name == db_branch.name,
|
|
489
|
+
Commit.created_at > cutoff,
|
|
490
|
+
)
|
|
491
|
+
.subquery()
|
|
492
|
+
)
|
|
493
|
+
)
|
|
494
|
+
recent_commits = recent_commit_result.scalar() or 0
|
|
495
|
+
if recent_commits == 0:
|
|
496
|
+
logger.info(
|
|
497
|
+
"Removing inactive branch %s from repo %s",
|
|
498
|
+
db_branch.name, repo.id,
|
|
499
|
+
)
|
|
500
|
+
await session.delete(db_branch)
|
|
501
|
+
cleaned += 1
|
|
502
|
+
|
|
503
|
+
await session.commit()
|
|
504
|
+
|
|
505
|
+
logger.info("Stale branch cleanup complete: removed %d branches", cleaned)
|
|
506
|
+
return {"cleaned": cleaned}
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
async def prune_expired_revocations(ctx: dict) -> dict:
|
|
510
|
+
"""Cron job: prune expired token revocations.
|
|
511
|
+
|
|
512
|
+
Runs every 24 hours. Removes entries whose expires_at has passed
|
|
513
|
+
(tokens that are already expired don't need blocklist entries).
|
|
514
|
+
"""
|
|
515
|
+
from sqlalchemy import delete
|
|
516
|
+
|
|
517
|
+
from attocode_intel.db.engine import get_session
|
|
518
|
+
from attocode_intel.db.models import RevokedToken
|
|
519
|
+
|
|
520
|
+
logger.info("Running token revocation cleanup")
|
|
521
|
+
|
|
522
|
+
async for session in get_session():
|
|
523
|
+
result = await session.execute(
|
|
524
|
+
delete(RevokedToken).where(
|
|
525
|
+
RevokedToken.expires_at < datetime.now(timezone.utc)
|
|
526
|
+
)
|
|
527
|
+
)
|
|
528
|
+
pruned = result.rowcount
|
|
529
|
+
await session.commit()
|
|
530
|
+
logger.info("Pruned %d expired token revocations", pruned)
|
|
531
|
+
return {"pruned": pruned}
|
|
532
|
+
|
|
533
|
+
return {"pruned": 0}
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
async def gc_unreferenced_content(ctx: dict) -> dict:
|
|
537
|
+
"""Cron job: garbage collect unreferenced file_contents.
|
|
538
|
+
|
|
539
|
+
Runs every 24 hours.
|
|
540
|
+
"""
|
|
541
|
+
from attocode_intel.api.deps import get_config
|
|
542
|
+
from attocode_intel.db.engine import get_session
|
|
543
|
+
from attocode_intel.storage.content_store import ContentStore
|
|
544
|
+
|
|
545
|
+
logger.info("Running content GC")
|
|
546
|
+
config = get_config()
|
|
547
|
+
async for session in get_session():
|
|
548
|
+
store = ContentStore(session)
|
|
549
|
+
count = await store.gc_unreferenced(min_age_minutes=config.gc_content_min_age_minutes)
|
|
550
|
+
await session.commit()
|
|
551
|
+
return {"removed": count}
|
|
552
|
+
return {"removed": 0}
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
async def gc_orphaned_embeddings(ctx: dict) -> dict:
|
|
556
|
+
"""Cron job: garbage collect orphaned embeddings.
|
|
557
|
+
|
|
558
|
+
Deletes embeddings whose content_sha is not referenced by any branch manifest.
|
|
559
|
+
Runs daily at 3:30am.
|
|
560
|
+
"""
|
|
561
|
+
from attocode_intel.api.deps import get_config
|
|
562
|
+
from attocode_intel.db.engine import get_session
|
|
563
|
+
from attocode_intel.storage.embedding_store import EmbeddingStore
|
|
564
|
+
|
|
565
|
+
logger.info("Running orphaned embeddings GC")
|
|
566
|
+
config = get_config()
|
|
567
|
+
async for session in get_session():
|
|
568
|
+
store = EmbeddingStore(session)
|
|
569
|
+
count = await store.gc_orphaned(min_age_minutes=config.gc_content_min_age_minutes)
|
|
570
|
+
await session.commit()
|
|
571
|
+
logger.info("Orphaned embeddings GC complete: removed %d embeddings", count)
|
|
572
|
+
return {"removed": count}
|
|
573
|
+
return {"removed": 0}
|