claude-smart 0.2.42 → 0.2.43
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/.claude-plugin/marketplace.json +3 -3
- package/README.md +1 -1
- package/package.json +9 -3
- package/plugin/.claude-plugin/plugin.json +9 -3
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/README.md +2 -2
- package/plugin/pyproject.toml +2 -2
- package/plugin/scripts/_lib.sh +91 -0
- package/plugin/scripts/backend-service.sh +1 -0
- package/plugin/scripts/cli.sh +1 -0
- package/plugin/scripts/codex-hook.js +72 -4
- package/plugin/scripts/dashboard-build.sh +1 -0
- package/plugin/scripts/dashboard-service.sh +1 -0
- package/plugin/scripts/hook_entry.sh +1 -0
- package/plugin/scripts/smart-install.sh +1 -0
- package/plugin/src/claude_smart/context_format.py +11 -12
- package/plugin/src/claude_smart/cs_cite.py +26 -12
- package/plugin/src/claude_smart/ids.py +13 -5
- package/plugin/uv.lock +1 -1
- package/plugin/vendor/reflexio/.env.example +53 -0
- package/plugin/vendor/reflexio/LICENSE +201 -0
- package/plugin/vendor/reflexio/README.md +338 -0
- package/plugin/vendor/reflexio/pyproject.toml +271 -0
- package/plugin/vendor/reflexio/reflexio/README.md +184 -0
- package/plugin/vendor/reflexio/reflexio/__init__.py +166 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/README.md +109 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/backends.py +175 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/bench.py +642 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/embed_cache.py +330 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/report.py +317 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/results/report.md +43 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/results/results.json +4478 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/scenarios.py +134 -0
- package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/seed.py +255 -0
- package/plugin/vendor/reflexio/reflexio/cli/README.md +287 -0
- package/plugin/vendor/reflexio/reflexio/cli/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/cli/__main__.py +56 -0
- package/plugin/vendor/reflexio/reflexio/cli/_client.py +86 -0
- package/plugin/vendor/reflexio/reflexio/cli/app.py +127 -0
- package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +266 -0
- package/plugin/vendor/reflexio/reflexio/cli/codex_auth.py +503 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/admin_cmd.py +65 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/agent_playbooks.py +503 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/api.py +114 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/auth.py +109 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/config_cmd.py +511 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/doctor.py +127 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/embeddings.py +53 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/interactions.py +478 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/profiles.py +303 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/services.py +289 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +961 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/shortcuts.py +285 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/status_cmd.py +143 -0
- package/plugin/vendor/reflexio/reflexio/cli/commands/user_playbooks.py +373 -0
- package/plugin/vendor/reflexio/reflexio/cli/env_loader.py +284 -0
- package/plugin/vendor/reflexio/reflexio/cli/errors.py +217 -0
- package/plugin/vendor/reflexio/reflexio/cli/log_format.py +247 -0
- package/plugin/vendor/reflexio/reflexio/cli/output.py +867 -0
- package/plugin/vendor/reflexio/reflexio/cli/paths.py +41 -0
- package/plugin/vendor/reflexio/reflexio/cli/run_services.py +391 -0
- package/plugin/vendor/reflexio/reflexio/cli/state.py +204 -0
- package/plugin/vendor/reflexio/reflexio/cli/stop_services.py +96 -0
- package/plugin/vendor/reflexio/reflexio/cli/utils.py +329 -0
- package/plugin/vendor/reflexio/reflexio/client/__init__.py +3 -0
- package/plugin/vendor/reflexio/reflexio/client/cache.py +150 -0
- package/plugin/vendor/reflexio/reflexio/client/client.py +2613 -0
- package/plugin/vendor/reflexio/reflexio/defaults.py +23 -0
- package/plugin/vendor/reflexio/reflexio/integrations/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/.clawhubignore +7 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/README.md +274 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/TESTING.md +517 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/hook/handler.js +473 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package-lock.json +2156 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package.json +18 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/handler.ts +241 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/setup.ts +140 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/index.ts +130 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/publish.ts +113 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/search.ts +52 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/server.ts +103 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/sqlite-buffer.ts +156 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/user-id.ts +134 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/openclaw.plugin.json +41 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/package.json +17 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/rules/reflexio.md +24 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/reflexio/SKILL.md +48 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/publish_clawhub.sh +278 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/references/HOOK.md +164 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/install.sh +36 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/uninstall.sh +35 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/publish.test.ts +27 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/search.test.ts +31 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/server.test.ts +42 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/setup.test.ts +49 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/sqlite-buffer.test.ts +91 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/user-id.test.ts +50 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tsconfig.json +16 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/types/openclaw.d.ts +230 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/vitest.config.ts +13 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/README.md +120 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/TESTING.md +168 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/package-lock.json +1657 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/package.json +16 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/HEARTBEAT.md +6 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/README.md +84 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/SKILL.md +194 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/_meta.json +6 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/agents/reflexio-extractor.md +45 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/hook/handler.ts +214 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/hook/setup.ts +55 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/index.ts +327 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/consolidate.ts +233 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/dedup.ts +80 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/io.ts +155 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/openclaw-cli.ts +67 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/search.ts +33 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/write-playbook.ts +76 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/write-profile.ts +79 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/openclaw.plugin.json +46 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/package.json +18 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/README.md +36 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/full_consolidation.md +56 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/playbook_extraction.md +217 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/profile_extraction.md +132 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/skills/reflexio-consolidate/SKILL.md +33 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/skills/reflexio-embedded/SKILL.md +194 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/HOOK.md +18 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/architecture.md +49 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/comparison.md +31 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/future-work.md +47 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/porting-notes.md +52 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/scripts/install.sh +52 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/scripts/uninstall.sh +36 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/consolidate.test.ts +135 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/dedup.test.ts +104 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/io.test.ts +175 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/search.test.ts +66 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/smoke-test.ts +140 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/write-playbook.test.ts +93 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/write-profile.test.ts +174 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tsconfig.json +16 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/types/openclaw.d.ts +230 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/vitest.config.ts +7 -0
- package/plugin/vendor/reflexio/reflexio/lib/__init__.py +23 -0
- package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +310 -0
- package/plugin/vendor/reflexio/reflexio/lib/_base.py +225 -0
- package/plugin/vendor/reflexio/reflexio/lib/_config.py +83 -0
- package/plugin/vendor/reflexio/reflexio/lib/_dashboard.py +266 -0
- package/plugin/vendor/reflexio/reflexio/lib/_generation.py +176 -0
- package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +334 -0
- package/plugin/vendor/reflexio/reflexio/lib/_operations.py +153 -0
- package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +545 -0
- package/plugin/vendor/reflexio/reflexio/lib/_reflection.py +52 -0
- package/plugin/vendor/reflexio/reflexio/lib/_search.py +167 -0
- package/plugin/vendor/reflexio/reflexio/lib/_storage_labels.py +103 -0
- package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +288 -0
- package/plugin/vendor/reflexio/reflexio/lib/reflexio_lib.py +27 -0
- package/plugin/vendor/reflexio/reflexio/models/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/braintrust_schema.py +141 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/common.py +41 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/__init__.py +3 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +1103 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +63 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +487 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +28 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/pending_tool_call_schema.py +83 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +766 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/service_schemas.py +9 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/stall_state_schema.py +32 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/__init__.py +3 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +177 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +129 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/enums.py +25 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/validators.py +280 -0
- package/plugin/vendor/reflexio/reflexio/models/config_schema.py +908 -0
- package/plugin/vendor/reflexio/reflexio/models/py.typed +0 -0
- package/plugin/vendor/reflexio/reflexio/server/OVERVIEW.md +90 -0
- package/plugin/vendor/reflexio/reflexio/server/README.md +616 -0
- package/plugin/vendor/reflexio/reflexio/server/__init__.py +210 -0
- package/plugin/vendor/reflexio/reflexio/server/__main__.py +132 -0
- package/plugin/vendor/reflexio/reflexio/server/_auth.py +25 -0
- package/plugin/vendor/reflexio/reflexio/server/api.py +2714 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/account_api.py +143 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/health_api.py +91 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/pending_tool_call_api.py +572 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/precondition_checks.py +66 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +540 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +50 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/stall_state_api.py +100 -0
- package/plugin/vendor/reflexio/reflexio/server/cache/__init__.py +15 -0
- package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +208 -0
- package/plugin/vendor/reflexio/reflexio/server/correlation.py +46 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/__init__.py +30 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +110 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/image_utils.py +55 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +1595 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/llm_utils.py +112 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +469 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +1122 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_stream_parser.py +197 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +210 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +213 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/nomic_embedding_provider.py +255 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/rerank/__init__.py +6 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +177 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/rerank/llm_reranker.py +148 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/tools.py +699 -0
- package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +179 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/_dispatchers.py +54 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/README.md +121 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/agent_success_evaluation/v1.0.0.prompt.md +58 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/agent_success_evaluation_with_comparison/v1.0.0.prompt.md +76 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/answer_synthesis/v1.5.2.prompt.md +88 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/compress_session_for_query/v1.3.0.prompt.md +31 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.0.0.prompt.md +20 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.0.0.prompt.md +53 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.1.0.prompt.md +57 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.2.0.prompt.md +68 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.3.0.prompt.md +70 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.4.0.prompt.md +77 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.5.0.prompt.md +82 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.6.0.prompt.md +83 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.1.0.prompt.md +193 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.2.0.prompt.md +206 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v1.0.0-deprecated.prompt.md +66 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v1.0.0.prompt.md +43 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v1.1.0.prompt.md +46 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.0.0-deprecated.prompt.md +64 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.0.0.prompt.md +39 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.1.0.prompt.md +39 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.2.0.prompt.md +47 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.0.prompt.md +58 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.0.2.prompt.md +254 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.1.0.prompt.md +274 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.0.prompt.md +279 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v1.0.0.prompt.md +73 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v2.0.0.prompt.md +86 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.0.0.prompt.md +97 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.1.0.prompt.md +119 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.2.0.prompt.md +123 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.3.0.prompt.md +127 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.0.0.prompt.md +14 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.1.0.prompt.md +24 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.2.0.prompt.md +29 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main_expert/v1.0.0.prompt.md +11 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main_expert/v1.1.0.prompt.md +21 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main_expert/v1.2.0.prompt.md +25 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_optimizer_judge/v1.0.0.prompt.md +37 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_optimizer_judge/v1.1.0.prompt.md +40 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_optimizer_judge/v1.2.0.prompt.md +36 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate/v1.0.0.prompt.md +45 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate/v2.0.0.prompt.md +81 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate/v3.0.0.prompt.md +80 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate_expert/v1.0.0.prompt.md +34 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_deduplication/v1.0.0.prompt.md +116 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_should_generate/v1.0.0.prompt.md +33 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_should_generate_override/v1.0.0.prompt.md +16 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.0.0.prompt.md +140 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.1.0.prompt.md +160 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_main/v1.0.0.prompt.md +14 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v1.0.0.prompt.md +19 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/rerank_relevance/v1.1.0.prompt.md +44 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.0.0.prompt.md +43 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_content_evaluation/v1.0.0.prompt.md +33 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_evaluation/prompt_evaluation_dataset/feedback_extraction_main_v1.jsonl +10 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_evaluation/prompt_evaluation_dataset/profile_update_main_v1.jsonl +10 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_manager.py +280 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_schema.py +11 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/_eval_health.py +131 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_constants.py +60 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_service.py +228 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_utils.py +87 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluator.py +372 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/delayed_group_evaluator.py +156 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/group_evaluation_runner.py +340 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +471 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +1626 -0
- package/plugin/vendor/reflexio/reflexio/server/services/braintrust/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/server/services/braintrust/_cron.py +196 -0
- package/plugin/vendor/reflexio/reflexio/server/services/braintrust/_encryption.py +101 -0
- package/plugin/vendor/reflexio/reflexio/server/services/braintrust/client.py +167 -0
- package/plugin/vendor/reflexio/reflexio/server/services/braintrust/service.py +281 -0
- package/plugin/vendor/reflexio/reflexio/server/services/configurator/base_configurator.py +179 -0
- package/plugin/vendor/reflexio/reflexio/server/services/configurator/config_storage.py +62 -0
- package/plugin/vendor/reflexio/reflexio/server/services/configurator/configurator.py +87 -0
- package/plugin/vendor/reflexio/reflexio/server/services/configurator/local_file_config_storage.py +187 -0
- package/plugin/vendor/reflexio/reflexio/server/services/configurator/test_config_storage.py +162 -0
- package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +112 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/distribution.py +33 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/eval_sampler.py +126 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/group_aggregation.py +192 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/hero_state.py +75 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/rule_attribution.py +97 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +515 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/shadow_aggregation.py +90 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/agent_run_records.py +91 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/invariants.py +303 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +25 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/pending_tool_call_dispatch.py +351 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/plan.py +138 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/prior_answer_search.py +217 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +468 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +171 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +777 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/tools.py +1125 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extractor_config_utils.py +91 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extractor_interaction_utils.py +251 -0
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +689 -0
- package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +835 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +89 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_aggregator.py +1388 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_consolidator.py +960 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_extractor.py +436 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_generation_service.py +808 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_constants.py +28 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_utils.py +362 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/__init__.py +24 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/assistant_webhook.py +246 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/gepa_adapter.py +291 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/judge.py +97 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/models.py +96 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +645 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/rollout.py +35 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scenario_resolver.py +93 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scheduler.py +174 -0
- package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/__init__.py +26 -0
- package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_document_expander.py +179 -0
- package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_query_reformulator.py +297 -0
- package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_deduplicator.py +741 -0
- package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_extractor.py +462 -0
- package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service.py +734 -0
- package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service_utils.py +290 -0
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/__init__.py +17 -0
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_extractor.py +247 -0
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service.py +800 -0
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service_utils.py +146 -0
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +70 -0
- package/plugin/vendor/reflexio/reflexio/server/services/search/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/server/services/service_utils.py +671 -0
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/judge.py +184 -0
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/outcome.py +81 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/constants.py +2 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +11 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +154 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/retention_mixin.py +155 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +59 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +1253 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +1945 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +600 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_operations.py +346 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +1378 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +747 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +263 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +193 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_share_links.py +166 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_stall_state.py +217 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +153 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +372 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_base.py +71 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +235 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +170 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +677 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_profiles.py +250 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +154 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +130 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_share_links.py +93 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_stall_state.py +76 -0
- package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +568 -0
- package/plugin/vendor/reflexio/reflexio/server/site_var/README.md +77 -0
- package/plugin/vendor/reflexio/reflexio/server/site_var/feature_flags.py +116 -0
- package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_manager.py +263 -0
- package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/feature_flags.json +13 -0
- package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/llm_model_setting.json +7 -0
- package/plugin/vendor/reflexio/reflexio/server/tracing.py +158 -0
- package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +113 -0
- package/plugin/vendor/reflexio/reflexio/server/uvicorn_logging.py +76 -0
- package/plugin/vendor/reflexio/reflexio/test_support/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/test_support/llm_fixtures.py +62 -0
- package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +242 -0
- package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +129 -0
- package/plugin/vendor/reflexio/reflexio/test_support/skip_decorators.py +43 -0
|
@@ -0,0 +1,835 @@
|
|
|
1
|
+
"""Centralized manager for all _operation_state table interactions.
|
|
2
|
+
|
|
3
|
+
Consolidates 5 use cases:
|
|
4
|
+
1. Progress tracking (rerun + manual batch operations)
|
|
5
|
+
2. Concurrency lock (atomic lock with request queuing)
|
|
6
|
+
3. Extractor bookmark (track last-processed interactions per extractor)
|
|
7
|
+
4. Aggregator bookmark (track last-processed user_playbook_id per aggregator)
|
|
8
|
+
5. Simple lock (non-queuing lock for cleanup operations)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import time
|
|
13
|
+
from datetime import UTC, datetime
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from reflexio.models.api_schema.internal_schema import RequestInteractionDataModel
|
|
17
|
+
from reflexio.models.api_schema.service_schemas import Interaction, OperationStatus
|
|
18
|
+
from reflexio.server.services.storage.storage_base import BaseStorage
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# Stale lock timeout - if generation started > 5 min ago and still "in_progress", assume it crashed
|
|
23
|
+
GENERATION_STALE_LOCK_SECONDS = 300
|
|
24
|
+
|
|
25
|
+
# Stale batch progress timeout - if batch operation started > 10 min ago and still IN_PROGRESS, auto-recover
|
|
26
|
+
BATCH_STALE_PROGRESS_SECONDS = 600
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class OperationStateManager:
|
|
30
|
+
"""Centralized manager for all _operation_state table interactions.
|
|
31
|
+
|
|
32
|
+
Provides methods for progress tracking, concurrency locks, extractor bookmarks,
|
|
33
|
+
aggregator bookmarks, and simple locks.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
storage: Storage instance with operation state methods
|
|
37
|
+
org_id: Organization identifier
|
|
38
|
+
service_name: Name of the service (e.g., "profile_generation", "playbook_extractor")
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, storage: BaseStorage, org_id: str, service_name: str):
|
|
42
|
+
self.storage = storage
|
|
43
|
+
self.org_id = org_id
|
|
44
|
+
self.service_name = service_name
|
|
45
|
+
|
|
46
|
+
# ── Key Builders (private) ──
|
|
47
|
+
|
|
48
|
+
def _progress_key(self) -> str:
|
|
49
|
+
"""Build progress tracking key.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
str: Key in format '{service_name}::{org_id}::progress'
|
|
53
|
+
"""
|
|
54
|
+
return f"{self.service_name}::{self.org_id}::progress"
|
|
55
|
+
|
|
56
|
+
def _cancellation_key(self) -> str:
|
|
57
|
+
"""Build cancellation flag key.
|
|
58
|
+
|
|
59
|
+
Uses a separate row from progress to avoid lost-update race conditions
|
|
60
|
+
where progress updates overwrite the cancellation flag.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
str: Key in format '{service_name}::{org_id}::cancellation'
|
|
64
|
+
"""
|
|
65
|
+
return f"{self.service_name}::{self.org_id}::cancellation"
|
|
66
|
+
|
|
67
|
+
def _lock_key(self, scope_id: str | None = None) -> str:
|
|
68
|
+
"""Build concurrency lock key.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
scope_id: Optional scope identifier (e.g., user_id for profile generation)
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
str: Key in format '{service_name}::{org_id}[::scope_id]::lock'
|
|
75
|
+
"""
|
|
76
|
+
if scope_id:
|
|
77
|
+
return f"{self.service_name}::{self.org_id}::{scope_id}::lock"
|
|
78
|
+
return f"{self.service_name}::{self.org_id}::lock"
|
|
79
|
+
|
|
80
|
+
def _bookmark_key(
|
|
81
|
+
self,
|
|
82
|
+
name: str,
|
|
83
|
+
scope_id: str | None = None,
|
|
84
|
+
version: str | None = None,
|
|
85
|
+
) -> str:
|
|
86
|
+
"""Build bookmark key for extractor/aggregator state.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
name: Extractor or aggregator name
|
|
90
|
+
scope_id: Optional scope identifier (e.g., user_id)
|
|
91
|
+
version: Optional version identifier (for aggregator)
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
str: Key in format '{service_name}::{org_id}[::scope_id]::{name}[::version]'
|
|
95
|
+
"""
|
|
96
|
+
parts = [self.service_name, self.org_id]
|
|
97
|
+
if scope_id:
|
|
98
|
+
parts.append(scope_id)
|
|
99
|
+
parts.append(name)
|
|
100
|
+
if version:
|
|
101
|
+
parts.append(version)
|
|
102
|
+
return "::".join(parts)
|
|
103
|
+
|
|
104
|
+
# ── Use Case 1: Progress Tracking ──
|
|
105
|
+
# (Batch operations: rerun + manual)
|
|
106
|
+
|
|
107
|
+
def check_in_progress(self) -> str | None:
|
|
108
|
+
"""Check if there's an existing in-progress operation.
|
|
109
|
+
|
|
110
|
+
If the operation has been in progress for longer than BATCH_STALE_PROGRESS_SECONDS,
|
|
111
|
+
auto-marks it as FAILED and returns None to allow new operations to proceed.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Error message if operation is in progress, None otherwise
|
|
115
|
+
"""
|
|
116
|
+
key = self._progress_key()
|
|
117
|
+
existing_state_entry = self.storage.get_operation_state(key)
|
|
118
|
+
if existing_state_entry:
|
|
119
|
+
existing_state = existing_state_entry.get(
|
|
120
|
+
"operation_state", existing_state_entry
|
|
121
|
+
)
|
|
122
|
+
if existing_state.get("status") == OperationStatus.IN_PROGRESS.value:
|
|
123
|
+
# Check if the operation is stale
|
|
124
|
+
started_at = existing_state.get("started_at")
|
|
125
|
+
if started_at is None:
|
|
126
|
+
# Legacy state without started_at — treat as legitimately in-progress
|
|
127
|
+
return (
|
|
128
|
+
f"A {self.service_name} operation is already in progress. "
|
|
129
|
+
"Please wait for it to complete."
|
|
130
|
+
)
|
|
131
|
+
current_time = int(datetime.now(UTC).timestamp())
|
|
132
|
+
elapsed = current_time - started_at
|
|
133
|
+
|
|
134
|
+
if elapsed > BATCH_STALE_PROGRESS_SECONDS:
|
|
135
|
+
logger.warning(
|
|
136
|
+
"Stale %s batch operation detected (started %d seconds ago), "
|
|
137
|
+
"auto-marking as FAILED to allow recovery",
|
|
138
|
+
self.service_name,
|
|
139
|
+
elapsed,
|
|
140
|
+
)
|
|
141
|
+
existing_state["status"] = OperationStatus.FAILED.value
|
|
142
|
+
existing_state["completed_at"] = current_time
|
|
143
|
+
existing_state["error_message"] = (
|
|
144
|
+
f"Auto-recovered: operation was stuck IN_PROGRESS for {elapsed}s "
|
|
145
|
+
f"(threshold: {BATCH_STALE_PROGRESS_SECONDS}s)"
|
|
146
|
+
)
|
|
147
|
+
self.storage.update_operation_state(key, existing_state)
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
f"A {self.service_name} operation is already in progress. "
|
|
152
|
+
"Please wait for it to complete."
|
|
153
|
+
)
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
def initialize_progress(
|
|
157
|
+
self,
|
|
158
|
+
total_users: int,
|
|
159
|
+
request_params: dict,
|
|
160
|
+
extra_stats: dict | None = None,
|
|
161
|
+
) -> None:
|
|
162
|
+
"""Initialize operation state with IN_PROGRESS status.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
total_users: Total number of users to process
|
|
166
|
+
request_params: Original request parameters for reference
|
|
167
|
+
extra_stats: Optional additional stats fields to include
|
|
168
|
+
"""
|
|
169
|
+
stats = {
|
|
170
|
+
"total_interactions_processed": 0,
|
|
171
|
+
"total_generated": 0,
|
|
172
|
+
}
|
|
173
|
+
if extra_stats:
|
|
174
|
+
stats.update(extra_stats)
|
|
175
|
+
|
|
176
|
+
key = self._progress_key()
|
|
177
|
+
initial_state = {
|
|
178
|
+
"service_name": self.service_name,
|
|
179
|
+
"status": OperationStatus.IN_PROGRESS.value,
|
|
180
|
+
"started_at": int(datetime.now(UTC).timestamp()),
|
|
181
|
+
"completed_at": None,
|
|
182
|
+
"total_users": total_users,
|
|
183
|
+
"processed_users": 0,
|
|
184
|
+
"failed_users": 0,
|
|
185
|
+
"current_user_id": None,
|
|
186
|
+
"processed_user_ids": [],
|
|
187
|
+
"failed_user_ids": [],
|
|
188
|
+
"request_params": request_params,
|
|
189
|
+
"stats": stats,
|
|
190
|
+
"error_message": None,
|
|
191
|
+
"progress_percentage": 0.0,
|
|
192
|
+
}
|
|
193
|
+
self.storage.upsert_operation_state(key, initial_state)
|
|
194
|
+
|
|
195
|
+
# Clear any stale cancellation flag from a previous operation
|
|
196
|
+
cancel_key = self._cancellation_key()
|
|
197
|
+
self.storage.upsert_operation_state(
|
|
198
|
+
cancel_key, {"cancellation_requested": False}
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
def set_current_item(self, item_id: str) -> None:
|
|
202
|
+
"""Set the current item being processed.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
item_id: Item ID currently being processed
|
|
206
|
+
"""
|
|
207
|
+
key = self._progress_key()
|
|
208
|
+
state_entry = self.storage.get_operation_state(key)
|
|
209
|
+
current_state = (
|
|
210
|
+
state_entry.get("operation_state", state_entry) if state_entry else {}
|
|
211
|
+
)
|
|
212
|
+
current_state["current_user_id"] = item_id
|
|
213
|
+
self.storage.update_operation_state(key, current_state)
|
|
214
|
+
|
|
215
|
+
def update_progress(
|
|
216
|
+
self,
|
|
217
|
+
item_id: str,
|
|
218
|
+
count: int,
|
|
219
|
+
success: bool,
|
|
220
|
+
total_users: int,
|
|
221
|
+
error: str | None = None,
|
|
222
|
+
) -> None:
|
|
223
|
+
"""Update operation state after processing a user.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
item_id: User ID that was processed
|
|
227
|
+
count: Number of interactions processed for this user
|
|
228
|
+
success: Whether processing succeeded
|
|
229
|
+
total_users: Total users being processed (for percentage calculation)
|
|
230
|
+
error: Error message if processing failed
|
|
231
|
+
"""
|
|
232
|
+
key = self._progress_key()
|
|
233
|
+
state_entry = self.storage.get_operation_state(key)
|
|
234
|
+
current_state = (
|
|
235
|
+
state_entry.get("operation_state", state_entry) if state_entry else {}
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
if success:
|
|
239
|
+
current_state["processed_users"] += 1
|
|
240
|
+
current_state["processed_user_ids"].append(item_id)
|
|
241
|
+
current_state["stats"]["total_interactions_processed"] += count
|
|
242
|
+
else:
|
|
243
|
+
current_state["failed_users"] += 1
|
|
244
|
+
current_state["failed_user_ids"].append(
|
|
245
|
+
{"user_id": item_id, "error": error}
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
current_state["current_user_id"] = None
|
|
249
|
+
current_state["progress_percentage"] = (
|
|
250
|
+
current_state["processed_users"] / total_users
|
|
251
|
+
) * 100
|
|
252
|
+
|
|
253
|
+
self.storage.update_operation_state(key, current_state)
|
|
254
|
+
|
|
255
|
+
def finalize_progress(self, total_processed: int, total_generated: int = 0) -> None:
|
|
256
|
+
"""Mark operation as COMPLETED and finalize state.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
total_processed: Total number of items processed
|
|
260
|
+
total_generated: Total number of profiles or playbooks generated
|
|
261
|
+
"""
|
|
262
|
+
key = self._progress_key()
|
|
263
|
+
state_entry = self.storage.get_operation_state(key)
|
|
264
|
+
final_state = (
|
|
265
|
+
state_entry.get("operation_state", state_entry) if state_entry else {}
|
|
266
|
+
)
|
|
267
|
+
final_state["status"] = OperationStatus.COMPLETED.value
|
|
268
|
+
final_state["completed_at"] = int(datetime.now(UTC).timestamp())
|
|
269
|
+
final_state["progress_percentage"] = 100.0
|
|
270
|
+
final_state["stats"]["total_interactions_processed"] = total_processed
|
|
271
|
+
final_state["stats"]["total_generated"] = total_generated
|
|
272
|
+
self.storage.update_operation_state(key, final_state)
|
|
273
|
+
|
|
274
|
+
def mark_progress_failed(self, error_message: str) -> None:
|
|
275
|
+
"""Mark operation as FAILED with error message.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
error_message: Error description
|
|
279
|
+
"""
|
|
280
|
+
try:
|
|
281
|
+
key = self._progress_key()
|
|
282
|
+
state_entry = self.storage.get_operation_state(key)
|
|
283
|
+
if state_entry:
|
|
284
|
+
failed_state = state_entry.get("operation_state", state_entry)
|
|
285
|
+
failed_state["status"] = OperationStatus.FAILED.value
|
|
286
|
+
failed_state["completed_at"] = int(datetime.now(UTC).timestamp())
|
|
287
|
+
failed_state["error_message"] = error_message
|
|
288
|
+
self.storage.update_operation_state(key, failed_state)
|
|
289
|
+
except Exception: # noqa: S110
|
|
290
|
+
pass # Ignore errors updating state during exception handling
|
|
291
|
+
|
|
292
|
+
def get_progress(self) -> dict | None:
|
|
293
|
+
"""Get the current progress state.
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
The progress state dict, or None if no state exists
|
|
297
|
+
"""
|
|
298
|
+
key = self._progress_key()
|
|
299
|
+
state_entry = self.storage.get_operation_state(key)
|
|
300
|
+
if state_entry:
|
|
301
|
+
return state_entry.get("operation_state", state_entry)
|
|
302
|
+
return None
|
|
303
|
+
|
|
304
|
+
def request_cancellation(self) -> bool:
|
|
305
|
+
"""Request cancellation of an in-progress operation.
|
|
306
|
+
|
|
307
|
+
Writes the cancellation flag to a separate DB row from the progress state
|
|
308
|
+
to avoid lost-update race conditions where concurrent progress updates
|
|
309
|
+
overwrite the flag.
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
bool: True if cancellation was requested (operation was in progress), False otherwise
|
|
313
|
+
"""
|
|
314
|
+
# Verify the operation is actually in progress
|
|
315
|
+
progress_key = self._progress_key()
|
|
316
|
+
state_entry = self.storage.get_operation_state(progress_key)
|
|
317
|
+
if not state_entry:
|
|
318
|
+
return False
|
|
319
|
+
|
|
320
|
+
state = state_entry.get("operation_state", state_entry)
|
|
321
|
+
if state.get("status") != OperationStatus.IN_PROGRESS.value:
|
|
322
|
+
return False
|
|
323
|
+
|
|
324
|
+
# Write cancellation flag to a separate row to avoid race conditions
|
|
325
|
+
cancel_key = self._cancellation_key()
|
|
326
|
+
self.storage.upsert_operation_state(
|
|
327
|
+
cancel_key, {"cancellation_requested": True}
|
|
328
|
+
)
|
|
329
|
+
logger.info(
|
|
330
|
+
"Cancellation requested for %s (org=%s)", self.service_name, self.org_id
|
|
331
|
+
)
|
|
332
|
+
return True
|
|
333
|
+
|
|
334
|
+
def is_cancellation_requested(self) -> bool:
|
|
335
|
+
"""Check if cancellation has been requested for the current operation.
|
|
336
|
+
|
|
337
|
+
Reads from a separate cancellation row to avoid race conditions with
|
|
338
|
+
progress updates.
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
bool: True if cancellation was requested
|
|
342
|
+
"""
|
|
343
|
+
cancel_key = self._cancellation_key()
|
|
344
|
+
cancel_entry = self.storage.get_operation_state(cancel_key)
|
|
345
|
+
if not cancel_entry:
|
|
346
|
+
return False
|
|
347
|
+
cancel_state = cancel_entry.get("operation_state", cancel_entry)
|
|
348
|
+
return cancel_state.get("cancellation_requested", False)
|
|
349
|
+
|
|
350
|
+
def mark_cancelled(self) -> None:
|
|
351
|
+
"""Mark the current operation as CANCELLED.
|
|
352
|
+
|
|
353
|
+
Sets status to CANCELLED in the progress row and clears the separate
|
|
354
|
+
cancellation flag row.
|
|
355
|
+
"""
|
|
356
|
+
# Update progress state to CANCELLED
|
|
357
|
+
key = self._progress_key()
|
|
358
|
+
state_entry = self.storage.get_operation_state(key)
|
|
359
|
+
if not state_entry:
|
|
360
|
+
return
|
|
361
|
+
|
|
362
|
+
state = state_entry.get("operation_state", state_entry)
|
|
363
|
+
state["status"] = OperationStatus.CANCELLED.value
|
|
364
|
+
state["completed_at"] = int(datetime.now(UTC).timestamp())
|
|
365
|
+
self.storage.update_operation_state(key, state)
|
|
366
|
+
|
|
367
|
+
# Clear the separate cancellation flag
|
|
368
|
+
cancel_key = self._cancellation_key()
|
|
369
|
+
self.storage.upsert_operation_state(
|
|
370
|
+
cancel_key, {"cancellation_requested": False}
|
|
371
|
+
)
|
|
372
|
+
logger.info(
|
|
373
|
+
"Operation marked as cancelled for %s (org=%s)",
|
|
374
|
+
self.service_name,
|
|
375
|
+
self.org_id,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
# ── Use Case 2: Concurrency Lock ──
|
|
379
|
+
# (Atomic lock with request queuing for generation services)
|
|
380
|
+
|
|
381
|
+
def acquire_lock(
|
|
382
|
+
self,
|
|
383
|
+
request_id: str,
|
|
384
|
+
scope_id: str | None = None,
|
|
385
|
+
stale_seconds: int = GENERATION_STALE_LOCK_SECONDS,
|
|
386
|
+
payload: dict | None = None,
|
|
387
|
+
) -> bool:
|
|
388
|
+
"""Atomically check and acquire in-progress lock.
|
|
389
|
+
|
|
390
|
+
Uses a single atomic database operation to prevent race conditions where
|
|
391
|
+
multiple requests could both acquire the lock simultaneously.
|
|
392
|
+
|
|
393
|
+
If a valid in-progress operation exists, appends the request to the
|
|
394
|
+
``pending_request_queue`` (along with its ``payload``) so the running
|
|
395
|
+
operation drains queued requests one at a time after it completes. If
|
|
396
|
+
no valid lock exists or the lock is stale, acquires the lock.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
request_id: Current request ID
|
|
400
|
+
scope_id: Optional scope identifier (e.g., user_id)
|
|
401
|
+
stale_seconds: Seconds after which a lock is considered stale
|
|
402
|
+
payload: Optional serialized request payload to enqueue with this
|
|
403
|
+
request when the lock is held by someone else. Required for the
|
|
404
|
+
rerun loop to operate on the SAME interactions the original
|
|
405
|
+
publish enqueued, not whatever the bookmark currently points at
|
|
406
|
+
(R2 / reflexio-enterprise#59).
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
bool: True if lock acquired (proceed with generation), False if skipped
|
|
410
|
+
"""
|
|
411
|
+
state_key = self._lock_key(scope_id)
|
|
412
|
+
# Pass ``payload`` as a kwarg so storage backends that haven't been
|
|
413
|
+
# updated to the new signature still error loudly rather than silently
|
|
414
|
+
# dropping it on the floor.
|
|
415
|
+
result = self.storage.try_acquire_in_progress_lock(
|
|
416
|
+
state_key,
|
|
417
|
+
request_id,
|
|
418
|
+
stale_seconds,
|
|
419
|
+
payload=payload,
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
acquired = result.get("acquired", False)
|
|
423
|
+
|
|
424
|
+
if acquired:
|
|
425
|
+
logger.info(
|
|
426
|
+
"Acquired in-progress lock for %s: state_key=%s, request_id=%s",
|
|
427
|
+
self.service_name,
|
|
428
|
+
state_key,
|
|
429
|
+
request_id,
|
|
430
|
+
)
|
|
431
|
+
else:
|
|
432
|
+
logger.info(
|
|
433
|
+
"Skipping %s - another operation is in progress (state_key=%s). "
|
|
434
|
+
"Enqueued request_id=%s for drain on release",
|
|
435
|
+
self.service_name,
|
|
436
|
+
state_key,
|
|
437
|
+
request_id,
|
|
438
|
+
)
|
|
439
|
+
return acquired
|
|
440
|
+
|
|
441
|
+
def release_lock(
|
|
442
|
+
self,
|
|
443
|
+
request_id: str,
|
|
444
|
+
scope_id: str | None = None,
|
|
445
|
+
) -> str | None:
|
|
446
|
+
"""Release the in-progress lock and check if a new request came in.
|
|
447
|
+
|
|
448
|
+
If a pending request exists (different from current), returns its ID so
|
|
449
|
+
the caller can re-run. Otherwise clears the lock.
|
|
450
|
+
|
|
451
|
+
Args:
|
|
452
|
+
request_id: The request ID of the current operation
|
|
453
|
+
scope_id: Optional scope identifier (e.g., user_id)
|
|
454
|
+
|
|
455
|
+
Returns:
|
|
456
|
+
Optional[str]: pending_request_id if a new request needs processing, None otherwise
|
|
457
|
+
"""
|
|
458
|
+
state_key = self._lock_key(scope_id)
|
|
459
|
+
state_record = self.storage.get_operation_state(state_key)
|
|
460
|
+
|
|
461
|
+
if not state_record:
|
|
462
|
+
return None
|
|
463
|
+
|
|
464
|
+
# Extract operation_state from the record (storage returns nested structure)
|
|
465
|
+
state = (
|
|
466
|
+
state_record.get("operation_state", {})
|
|
467
|
+
if isinstance(state_record.get("operation_state"), dict)
|
|
468
|
+
else state_record
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
pending_request_id = state.get("pending_request_id")
|
|
472
|
+
# Backward compatibility for locks written before current_request_id was introduced.
|
|
473
|
+
current_request_id = state.get("current_request_id") or state.get("request_id")
|
|
474
|
+
|
|
475
|
+
# Only process if we still own the lock
|
|
476
|
+
if current_request_id == request_id:
|
|
477
|
+
if pending_request_id and pending_request_id != request_id:
|
|
478
|
+
# Another request came in, transfer ownership and signal re-run
|
|
479
|
+
self.storage.upsert_operation_state(
|
|
480
|
+
state_key,
|
|
481
|
+
{
|
|
482
|
+
"in_progress": True,
|
|
483
|
+
"started_at": int(time.time()),
|
|
484
|
+
"current_request_id": pending_request_id,
|
|
485
|
+
"pending_request_id": None,
|
|
486
|
+
},
|
|
487
|
+
)
|
|
488
|
+
logger.info(
|
|
489
|
+
"New request %s came in during %s, will re-run (state_key=%s)",
|
|
490
|
+
pending_request_id,
|
|
491
|
+
self.service_name,
|
|
492
|
+
state_key,
|
|
493
|
+
)
|
|
494
|
+
return pending_request_id
|
|
495
|
+
# No pending request, clear the lock
|
|
496
|
+
self.storage.upsert_operation_state(
|
|
497
|
+
state_key,
|
|
498
|
+
{
|
|
499
|
+
"in_progress": False,
|
|
500
|
+
"current_request_id": None,
|
|
501
|
+
"pending_request_id": None,
|
|
502
|
+
},
|
|
503
|
+
)
|
|
504
|
+
logger.info(
|
|
505
|
+
"Released in-progress lock for %s: state_key=%s, request_id=%s",
|
|
506
|
+
self.service_name,
|
|
507
|
+
state_key,
|
|
508
|
+
request_id,
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
return None
|
|
512
|
+
|
|
513
|
+
def clear_lock(self, scope_id: str | None = None) -> None:
|
|
514
|
+
"""Clear the in-progress state (used for error cleanup).
|
|
515
|
+
|
|
516
|
+
Also drops any queued pending requests — they were tied to the now-failed
|
|
517
|
+
run's context and should not be drained against fresh state.
|
|
518
|
+
|
|
519
|
+
Args:
|
|
520
|
+
scope_id: Optional scope identifier (e.g., user_id)
|
|
521
|
+
"""
|
|
522
|
+
state_key = self._lock_key(scope_id)
|
|
523
|
+
self.storage.upsert_operation_state(
|
|
524
|
+
state_key,
|
|
525
|
+
{
|
|
526
|
+
"in_progress": False,
|
|
527
|
+
"current_request_id": None,
|
|
528
|
+
"pending_request_id": None,
|
|
529
|
+
"pending_request_queue": [],
|
|
530
|
+
},
|
|
531
|
+
)
|
|
532
|
+
logger.debug(
|
|
533
|
+
"Cleared in-progress lock for %s: state_key=%s",
|
|
534
|
+
self.service_name,
|
|
535
|
+
state_key,
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
def release_lock_pop_queue(
|
|
539
|
+
self,
|
|
540
|
+
request_id: str,
|
|
541
|
+
scope_id: str | None = None,
|
|
542
|
+
) -> dict | None:
|
|
543
|
+
"""Release the lock and pop the next queued pending request, if any.
|
|
544
|
+
|
|
545
|
+
FIFO drain — preserves order so blocked publishes are processed in the
|
|
546
|
+
order they arrived. Replaces the older single-slot ``pending_request_id``
|
|
547
|
+
last-wins behaviour that silently dropped earlier blocked publishes
|
|
548
|
+
(R2 / reflexio-enterprise#59).
|
|
549
|
+
|
|
550
|
+
On a legacy state row (written by a pre-fix server before redeploy)
|
|
551
|
+
the queue is missing but ``pending_request_id`` may be set; we surface
|
|
552
|
+
that as a queue entry with ``payload=None`` so the caller falls back
|
|
553
|
+
to the original request rather than dropping it on the floor.
|
|
554
|
+
|
|
555
|
+
Args:
|
|
556
|
+
request_id: The request ID of the current operation (the holder)
|
|
557
|
+
scope_id: Optional scope identifier (e.g., user_id)
|
|
558
|
+
|
|
559
|
+
Returns:
|
|
560
|
+
dict | None: ``{"request_id": str, "payload": dict | None}`` for
|
|
561
|
+
the next queued request, or None if the queue is empty / we're
|
|
562
|
+
not the lock holder.
|
|
563
|
+
"""
|
|
564
|
+
state_key = self._lock_key(scope_id)
|
|
565
|
+
state_record = self.storage.get_operation_state(state_key)
|
|
566
|
+
|
|
567
|
+
if not state_record:
|
|
568
|
+
return None
|
|
569
|
+
|
|
570
|
+
state = (
|
|
571
|
+
state_record.get("operation_state", {})
|
|
572
|
+
if isinstance(state_record.get("operation_state"), dict)
|
|
573
|
+
else state_record
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
# Backward compatibility for locks written before current_request_id was introduced.
|
|
577
|
+
current_request_id = state.get("current_request_id") or state.get("request_id")
|
|
578
|
+
|
|
579
|
+
# Only drain if we still own the lock.
|
|
580
|
+
if current_request_id != request_id:
|
|
581
|
+
return None
|
|
582
|
+
|
|
583
|
+
queue = list(state.get("pending_request_queue") or [])
|
|
584
|
+
next_entry: dict | None = None
|
|
585
|
+
|
|
586
|
+
if queue:
|
|
587
|
+
head = queue.pop(0)
|
|
588
|
+
if isinstance(head, dict) and "request_id" in head:
|
|
589
|
+
next_entry = {
|
|
590
|
+
"request_id": head["request_id"],
|
|
591
|
+
"payload": head.get("payload"),
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if next_entry is None:
|
|
595
|
+
# Legacy fallback: storage row lacks the queue field but the old
|
|
596
|
+
# pending_request_id slot was set by an in-flight pre-fix server.
|
|
597
|
+
legacy_pending = state.get("pending_request_id")
|
|
598
|
+
if legacy_pending and legacy_pending != request_id:
|
|
599
|
+
next_entry = {"request_id": legacy_pending, "payload": None}
|
|
600
|
+
|
|
601
|
+
if next_entry is None:
|
|
602
|
+
# No more pending work — clear the lock.
|
|
603
|
+
self.storage.upsert_operation_state(
|
|
604
|
+
state_key,
|
|
605
|
+
{
|
|
606
|
+
"in_progress": False,
|
|
607
|
+
"current_request_id": None,
|
|
608
|
+
"pending_request_id": None,
|
|
609
|
+
"pending_request_queue": [],
|
|
610
|
+
},
|
|
611
|
+
)
|
|
612
|
+
logger.info(
|
|
613
|
+
"Released in-progress lock for %s: state_key=%s, request_id=%s",
|
|
614
|
+
self.service_name,
|
|
615
|
+
state_key,
|
|
616
|
+
request_id,
|
|
617
|
+
)
|
|
618
|
+
return None
|
|
619
|
+
|
|
620
|
+
# Transfer ownership to the popped request — it becomes the new holder.
|
|
621
|
+
# Mirror the head request_id into the legacy single slot for the
|
|
622
|
+
# release window during a server upgrade.
|
|
623
|
+
new_holder_request_id = next_entry["request_id"]
|
|
624
|
+
legacy_mirror = queue[0]["request_id"] if queue else None
|
|
625
|
+
self.storage.upsert_operation_state(
|
|
626
|
+
state_key,
|
|
627
|
+
{
|
|
628
|
+
"in_progress": True,
|
|
629
|
+
"started_at": int(time.time()),
|
|
630
|
+
"current_request_id": new_holder_request_id,
|
|
631
|
+
"pending_request_id": legacy_mirror,
|
|
632
|
+
"pending_request_queue": queue,
|
|
633
|
+
},
|
|
634
|
+
)
|
|
635
|
+
logger.info(
|
|
636
|
+
"Drained queued request %s on release of %s (state_key=%s, remaining=%d)",
|
|
637
|
+
new_holder_request_id,
|
|
638
|
+
self.service_name,
|
|
639
|
+
state_key,
|
|
640
|
+
len(queue),
|
|
641
|
+
)
|
|
642
|
+
return next_entry
|
|
643
|
+
|
|
644
|
+
# ── Use Case 3: Extractor Bookmark ──
|
|
645
|
+
# (Track last-processed interactions per extractor)
|
|
646
|
+
|
|
647
|
+
def get_extractor_state_with_new_interactions(
|
|
648
|
+
self,
|
|
649
|
+
extractor_name: str,
|
|
650
|
+
user_id: str | None = None,
|
|
651
|
+
sources: list[str] | None = None,
|
|
652
|
+
) -> tuple[dict, list[RequestInteractionDataModel]]:
|
|
653
|
+
"""Get extractor operation state and new interactions since last run.
|
|
654
|
+
|
|
655
|
+
Args:
|
|
656
|
+
extractor_name: Name of the extractor
|
|
657
|
+
user_id: Optional user ID for user-level extractors
|
|
658
|
+
sources: Optional source filter list
|
|
659
|
+
|
|
660
|
+
Returns:
|
|
661
|
+
Tuple of (state_dict, new_interactions_list)
|
|
662
|
+
"""
|
|
663
|
+
state_key = self._bookmark_key(extractor_name, scope_id=user_id)
|
|
664
|
+
return self.storage.get_operation_state_with_new_request_interaction(
|
|
665
|
+
state_key, user_id, sources
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
def update_extractor_bookmark(
|
|
669
|
+
self,
|
|
670
|
+
extractor_name: str,
|
|
671
|
+
processed_interactions: list[Interaction],
|
|
672
|
+
user_id: str | None = None,
|
|
673
|
+
) -> None:
|
|
674
|
+
"""Update operation state for an extractor after processing.
|
|
675
|
+
|
|
676
|
+
Args:
|
|
677
|
+
extractor_name: Name of the extractor
|
|
678
|
+
processed_interactions: Interactions that were processed
|
|
679
|
+
user_id: Optional user ID for user-level extractors
|
|
680
|
+
"""
|
|
681
|
+
if not processed_interactions:
|
|
682
|
+
return
|
|
683
|
+
|
|
684
|
+
state_key = self._bookmark_key(extractor_name, scope_id=user_id)
|
|
685
|
+
|
|
686
|
+
last_processed_ids = [
|
|
687
|
+
interaction.interaction_id for interaction in processed_interactions
|
|
688
|
+
]
|
|
689
|
+
last_processed_timestamp = max(
|
|
690
|
+
(
|
|
691
|
+
interaction.created_at
|
|
692
|
+
for interaction in processed_interactions
|
|
693
|
+
if interaction.created_at is not None
|
|
694
|
+
),
|
|
695
|
+
default=None,
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
state_payload: dict[str, Any] = {
|
|
699
|
+
"last_processed_interaction_ids": last_processed_ids,
|
|
700
|
+
}
|
|
701
|
+
if last_processed_timestamp is not None:
|
|
702
|
+
state_payload["last_processed_timestamp"] = last_processed_timestamp
|
|
703
|
+
|
|
704
|
+
self.storage.upsert_operation_state(state_key, state_payload)
|
|
705
|
+
|
|
706
|
+
# ── Use Case 4: Aggregator Bookmark ──
|
|
707
|
+
# (Track last-processed user_playbook_id per aggregator)
|
|
708
|
+
|
|
709
|
+
def get_aggregator_bookmark(self, name: str, version: str) -> int | None:
|
|
710
|
+
"""Get the last processed user_playbook_id for an aggregator.
|
|
711
|
+
|
|
712
|
+
Args:
|
|
713
|
+
name: Aggregator/playbook name
|
|
714
|
+
version: Agent version
|
|
715
|
+
|
|
716
|
+
Returns:
|
|
717
|
+
Last processed user_playbook_id, or None if no state exists
|
|
718
|
+
"""
|
|
719
|
+
state_key = self._bookmark_key(name, version=version)
|
|
720
|
+
record = self.storage.get_operation_state(state_key)
|
|
721
|
+
if record:
|
|
722
|
+
state = record.get("operation_state", {})
|
|
723
|
+
if isinstance(state, dict):
|
|
724
|
+
return state.get("last_processed_user_playbook_id")
|
|
725
|
+
return None
|
|
726
|
+
|
|
727
|
+
def update_aggregator_bookmark(
|
|
728
|
+
self, name: str, version: str, last_processed_id: int
|
|
729
|
+
) -> None:
|
|
730
|
+
"""Update the aggregator bookmark with the highest user_playbook_id processed.
|
|
731
|
+
|
|
732
|
+
Args:
|
|
733
|
+
name: Aggregator/playbook name
|
|
734
|
+
version: Agent version
|
|
735
|
+
last_processed_id: The highest user_playbook_id that was processed
|
|
736
|
+
"""
|
|
737
|
+
state_key = self._bookmark_key(name, version=version)
|
|
738
|
+
state = {"last_processed_user_playbook_id": last_processed_id}
|
|
739
|
+
self.storage.upsert_operation_state(state_key, state)
|
|
740
|
+
logger.info(
|
|
741
|
+
"Updated aggregator bookmark for '%s' v%s with last_processed_user_playbook_id: %d",
|
|
742
|
+
name,
|
|
743
|
+
version,
|
|
744
|
+
last_processed_id,
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
# ── Use Case 4b: Aggregator Cluster Fingerprints ──
|
|
748
|
+
# (Track cluster fingerprints for change detection)
|
|
749
|
+
|
|
750
|
+
def get_cluster_fingerprints(self, name: str, version: str) -> dict:
|
|
751
|
+
"""
|
|
752
|
+
Get stored cluster fingerprints for an aggregator.
|
|
753
|
+
|
|
754
|
+
Args:
|
|
755
|
+
name: Aggregator/playbook name
|
|
756
|
+
version: Agent version
|
|
757
|
+
|
|
758
|
+
Returns:
|
|
759
|
+
dict: Mapping of fingerprint_hash to {"agent_playbook_id": int, "user_playbook_ids": list[int]}.
|
|
760
|
+
Returns empty dict if no state exists.
|
|
761
|
+
"""
|
|
762
|
+
state_key = self._bookmark_key(name, version=version) + "::clusters"
|
|
763
|
+
record = self.storage.get_operation_state(state_key)
|
|
764
|
+
if record:
|
|
765
|
+
state = record.get("operation_state", {})
|
|
766
|
+
if isinstance(state, dict):
|
|
767
|
+
return state.get("cluster_fingerprints", {})
|
|
768
|
+
return {}
|
|
769
|
+
|
|
770
|
+
def update_cluster_fingerprints(
|
|
771
|
+
self, name: str, version: str, fingerprints: dict
|
|
772
|
+
) -> None:
|
|
773
|
+
"""
|
|
774
|
+
Store cluster fingerprint mapping for an aggregator.
|
|
775
|
+
|
|
776
|
+
Args:
|
|
777
|
+
name: Aggregator/playbook name
|
|
778
|
+
version: Agent version
|
|
779
|
+
fingerprints: Mapping of fingerprint_hash to {"agent_playbook_id": int, "user_playbook_ids": list[int]}
|
|
780
|
+
"""
|
|
781
|
+
state_key = self._bookmark_key(name, version=version) + "::clusters"
|
|
782
|
+
state = {"cluster_fingerprints": fingerprints}
|
|
783
|
+
self.storage.upsert_operation_state(state_key, state)
|
|
784
|
+
logger.info(
|
|
785
|
+
"Updated cluster fingerprints for '%s' v%s with %d clusters",
|
|
786
|
+
name,
|
|
787
|
+
version,
|
|
788
|
+
len(fingerprints),
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
# ── Use Case 5: Simple Lock ──
|
|
792
|
+
# (Non-queuing lock for cleanup operations)
|
|
793
|
+
|
|
794
|
+
def acquire_simple_lock(self, stale_seconds: int = 300) -> bool:
|
|
795
|
+
"""Acquire a simple non-queuing lock.
|
|
796
|
+
|
|
797
|
+
Checks if a lock is already held. If the lock is stale (older than
|
|
798
|
+
stale_seconds), it will be overridden.
|
|
799
|
+
|
|
800
|
+
Args:
|
|
801
|
+
stale_seconds: Seconds after which a lock is considered stale
|
|
802
|
+
|
|
803
|
+
Returns:
|
|
804
|
+
bool: True if lock acquired, False if another operation holds it
|
|
805
|
+
"""
|
|
806
|
+
state_key = self._lock_key()
|
|
807
|
+
state = self.storage.get_operation_state(state_key)
|
|
808
|
+
current_time = int(time.time())
|
|
809
|
+
|
|
810
|
+
if state and state.get("in_progress", False):
|
|
811
|
+
started_at = state.get("started_at", 0)
|
|
812
|
+
if current_time - started_at < stale_seconds:
|
|
813
|
+
logger.info(
|
|
814
|
+
"Skipping %s - another operation is in progress", self.service_name
|
|
815
|
+
)
|
|
816
|
+
return False
|
|
817
|
+
logger.warning(
|
|
818
|
+
"Stale %s lock detected (started %d seconds ago), proceeding",
|
|
819
|
+
self.service_name,
|
|
820
|
+
current_time - started_at,
|
|
821
|
+
)
|
|
822
|
+
|
|
823
|
+
# Acquire lock
|
|
824
|
+
self.storage.upsert_operation_state(
|
|
825
|
+
state_key, {"in_progress": True, "started_at": current_time}
|
|
826
|
+
)
|
|
827
|
+
return True
|
|
828
|
+
|
|
829
|
+
def release_simple_lock(self) -> None:
|
|
830
|
+
"""Release the simple lock."""
|
|
831
|
+
state_key = self._lock_key()
|
|
832
|
+
self.storage.upsert_operation_state(
|
|
833
|
+
state_key,
|
|
834
|
+
{"in_progress": False, "completed_at": int(time.time())},
|
|
835
|
+
)
|