claude-smart 0.2.41 → 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.
Files changed (396) hide show
  1. package/.claude-plugin/marketplace.json +17 -0
  2. package/README.md +1 -1
  3. package/bin/claude-smart.js +86 -48
  4. package/package.json +10 -3
  5. package/plugin/.claude-plugin/plugin.json +9 -3
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/README.md +2 -2
  8. package/plugin/dashboard/next.config.ts +9 -1
  9. package/plugin/pyproject.toml +2 -2
  10. package/plugin/scripts/_lib.sh +91 -0
  11. package/plugin/scripts/backend-service.sh +46 -15
  12. package/plugin/scripts/cli.sh +29 -1
  13. package/plugin/scripts/codex-hook.js +72 -4
  14. package/plugin/scripts/dashboard-build.sh +1 -0
  15. package/plugin/scripts/dashboard-service.sh +1 -0
  16. package/plugin/scripts/ensure-plugin-root.sh +7 -14
  17. package/plugin/scripts/hook_entry.sh +1 -0
  18. package/plugin/scripts/smart-install.sh +18 -2
  19. package/plugin/src/claude_smart/cli.py +72 -38
  20. package/plugin/src/claude_smart/context_format.py +11 -12
  21. package/plugin/src/claude_smart/cs_cite.py +26 -12
  22. package/plugin/src/claude_smart/ids.py +13 -5
  23. package/plugin/uv.lock +1 -1
  24. package/plugin/vendor/reflexio/.env.example +53 -0
  25. package/plugin/vendor/reflexio/LICENSE +201 -0
  26. package/plugin/vendor/reflexio/README.md +338 -0
  27. package/plugin/vendor/reflexio/pyproject.toml +271 -0
  28. package/plugin/vendor/reflexio/reflexio/README.md +184 -0
  29. package/plugin/vendor/reflexio/reflexio/__init__.py +166 -0
  30. package/plugin/vendor/reflexio/reflexio/benchmarks/__init__.py +1 -0
  31. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/README.md +109 -0
  32. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/__init__.py +1 -0
  33. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/backends.py +175 -0
  34. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/bench.py +642 -0
  35. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/embed_cache.py +330 -0
  36. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/report.py +317 -0
  37. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/results/report.md +43 -0
  38. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/results/results.json +4478 -0
  39. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/scenarios.py +134 -0
  40. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/seed.py +255 -0
  41. package/plugin/vendor/reflexio/reflexio/cli/README.md +287 -0
  42. package/plugin/vendor/reflexio/reflexio/cli/__init__.py +0 -0
  43. package/plugin/vendor/reflexio/reflexio/cli/__main__.py +56 -0
  44. package/plugin/vendor/reflexio/reflexio/cli/_client.py +86 -0
  45. package/plugin/vendor/reflexio/reflexio/cli/app.py +127 -0
  46. package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +266 -0
  47. package/plugin/vendor/reflexio/reflexio/cli/codex_auth.py +503 -0
  48. package/plugin/vendor/reflexio/reflexio/cli/commands/__init__.py +0 -0
  49. package/plugin/vendor/reflexio/reflexio/cli/commands/admin_cmd.py +65 -0
  50. package/plugin/vendor/reflexio/reflexio/cli/commands/agent_playbooks.py +503 -0
  51. package/plugin/vendor/reflexio/reflexio/cli/commands/api.py +114 -0
  52. package/plugin/vendor/reflexio/reflexio/cli/commands/auth.py +109 -0
  53. package/plugin/vendor/reflexio/reflexio/cli/commands/config_cmd.py +511 -0
  54. package/plugin/vendor/reflexio/reflexio/cli/commands/doctor.py +127 -0
  55. package/plugin/vendor/reflexio/reflexio/cli/commands/embeddings.py +53 -0
  56. package/plugin/vendor/reflexio/reflexio/cli/commands/interactions.py +478 -0
  57. package/plugin/vendor/reflexio/reflexio/cli/commands/profiles.py +303 -0
  58. package/plugin/vendor/reflexio/reflexio/cli/commands/services.py +289 -0
  59. package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +961 -0
  60. package/plugin/vendor/reflexio/reflexio/cli/commands/shortcuts.py +285 -0
  61. package/plugin/vendor/reflexio/reflexio/cli/commands/status_cmd.py +143 -0
  62. package/plugin/vendor/reflexio/reflexio/cli/commands/user_playbooks.py +373 -0
  63. package/plugin/vendor/reflexio/reflexio/cli/env_loader.py +284 -0
  64. package/plugin/vendor/reflexio/reflexio/cli/errors.py +217 -0
  65. package/plugin/vendor/reflexio/reflexio/cli/log_format.py +247 -0
  66. package/plugin/vendor/reflexio/reflexio/cli/output.py +867 -0
  67. package/plugin/vendor/reflexio/reflexio/cli/paths.py +41 -0
  68. package/plugin/vendor/reflexio/reflexio/cli/run_services.py +391 -0
  69. package/plugin/vendor/reflexio/reflexio/cli/state.py +204 -0
  70. package/plugin/vendor/reflexio/reflexio/cli/stop_services.py +96 -0
  71. package/plugin/vendor/reflexio/reflexio/cli/utils.py +329 -0
  72. package/plugin/vendor/reflexio/reflexio/client/__init__.py +3 -0
  73. package/plugin/vendor/reflexio/reflexio/client/cache.py +150 -0
  74. package/plugin/vendor/reflexio/reflexio/client/client.py +2613 -0
  75. package/plugin/vendor/reflexio/reflexio/defaults.py +23 -0
  76. package/plugin/vendor/reflexio/reflexio/integrations/__init__.py +0 -0
  77. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/.clawhubignore +7 -0
  78. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/README.md +274 -0
  79. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/TESTING.md +517 -0
  80. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/hook/handler.js +473 -0
  81. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package-lock.json +2156 -0
  82. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package.json +18 -0
  83. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/handler.ts +241 -0
  84. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/setup.ts +140 -0
  85. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/index.ts +130 -0
  86. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/publish.ts +113 -0
  87. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/search.ts +52 -0
  88. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/server.ts +103 -0
  89. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/sqlite-buffer.ts +156 -0
  90. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/user-id.ts +134 -0
  91. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/openclaw.plugin.json +41 -0
  92. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/package.json +17 -0
  93. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/rules/reflexio.md +24 -0
  94. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/reflexio/SKILL.md +48 -0
  95. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/publish_clawhub.sh +278 -0
  96. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/references/HOOK.md +164 -0
  97. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/install.sh +36 -0
  98. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/uninstall.sh +35 -0
  99. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/publish.test.ts +27 -0
  100. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/search.test.ts +31 -0
  101. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/server.test.ts +42 -0
  102. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/setup.test.ts +49 -0
  103. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/sqlite-buffer.test.ts +91 -0
  104. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/user-id.test.ts +50 -0
  105. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tsconfig.json +16 -0
  106. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/types/openclaw.d.ts +230 -0
  107. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/vitest.config.ts +13 -0
  108. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/README.md +120 -0
  109. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/TESTING.md +168 -0
  110. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/package-lock.json +1657 -0
  111. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/package.json +16 -0
  112. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/HEARTBEAT.md +6 -0
  113. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/README.md +84 -0
  114. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/SKILL.md +194 -0
  115. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/_meta.json +6 -0
  116. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/agents/reflexio-extractor.md +45 -0
  117. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/hook/handler.ts +214 -0
  118. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/hook/setup.ts +55 -0
  119. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/index.ts +327 -0
  120. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/consolidate.ts +233 -0
  121. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/dedup.ts +80 -0
  122. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/io.ts +155 -0
  123. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/openclaw-cli.ts +67 -0
  124. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/search.ts +33 -0
  125. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/write-playbook.ts +76 -0
  126. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/write-profile.ts +79 -0
  127. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/openclaw.plugin.json +46 -0
  128. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/package.json +18 -0
  129. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/README.md +36 -0
  130. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/full_consolidation.md +56 -0
  131. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/playbook_extraction.md +217 -0
  132. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/profile_extraction.md +132 -0
  133. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/skills/reflexio-consolidate/SKILL.md +33 -0
  134. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/skills/reflexio-embedded/SKILL.md +194 -0
  135. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/HOOK.md +18 -0
  136. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/architecture.md +49 -0
  137. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/comparison.md +31 -0
  138. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/future-work.md +47 -0
  139. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/porting-notes.md +52 -0
  140. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/scripts/install.sh +52 -0
  141. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/scripts/uninstall.sh +36 -0
  142. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/consolidate.test.ts +135 -0
  143. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/dedup.test.ts +104 -0
  144. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/io.test.ts +175 -0
  145. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/search.test.ts +66 -0
  146. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/smoke-test.ts +140 -0
  147. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/write-playbook.test.ts +93 -0
  148. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/write-profile.test.ts +174 -0
  149. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tsconfig.json +16 -0
  150. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/types/openclaw.d.ts +230 -0
  151. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/vitest.config.ts +7 -0
  152. package/plugin/vendor/reflexio/reflexio/lib/__init__.py +23 -0
  153. package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +310 -0
  154. package/plugin/vendor/reflexio/reflexio/lib/_base.py +225 -0
  155. package/plugin/vendor/reflexio/reflexio/lib/_config.py +83 -0
  156. package/plugin/vendor/reflexio/reflexio/lib/_dashboard.py +266 -0
  157. package/plugin/vendor/reflexio/reflexio/lib/_generation.py +176 -0
  158. package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +334 -0
  159. package/plugin/vendor/reflexio/reflexio/lib/_operations.py +153 -0
  160. package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +545 -0
  161. package/plugin/vendor/reflexio/reflexio/lib/_reflection.py +52 -0
  162. package/plugin/vendor/reflexio/reflexio/lib/_search.py +167 -0
  163. package/plugin/vendor/reflexio/reflexio/lib/_storage_labels.py +103 -0
  164. package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +288 -0
  165. package/plugin/vendor/reflexio/reflexio/lib/reflexio_lib.py +27 -0
  166. package/plugin/vendor/reflexio/reflexio/models/__init__.py +0 -0
  167. package/plugin/vendor/reflexio/reflexio/models/api_schema/__init__.py +0 -0
  168. package/plugin/vendor/reflexio/reflexio/models/api_schema/braintrust_schema.py +141 -0
  169. package/plugin/vendor/reflexio/reflexio/models/api_schema/common.py +41 -0
  170. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/__init__.py +3 -0
  171. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +1103 -0
  172. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +63 -0
  173. package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +487 -0
  174. package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +28 -0
  175. package/plugin/vendor/reflexio/reflexio/models/api_schema/pending_tool_call_schema.py +83 -0
  176. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +766 -0
  177. package/plugin/vendor/reflexio/reflexio/models/api_schema/service_schemas.py +9 -0
  178. package/plugin/vendor/reflexio/reflexio/models/api_schema/stall_state_schema.py +32 -0
  179. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/__init__.py +3 -0
  180. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +177 -0
  181. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +129 -0
  182. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/enums.py +25 -0
  183. package/plugin/vendor/reflexio/reflexio/models/api_schema/validators.py +280 -0
  184. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +908 -0
  185. package/plugin/vendor/reflexio/reflexio/models/py.typed +0 -0
  186. package/plugin/vendor/reflexio/reflexio/server/OVERVIEW.md +90 -0
  187. package/plugin/vendor/reflexio/reflexio/server/README.md +616 -0
  188. package/plugin/vendor/reflexio/reflexio/server/__init__.py +210 -0
  189. package/plugin/vendor/reflexio/reflexio/server/__main__.py +132 -0
  190. package/plugin/vendor/reflexio/reflexio/server/_auth.py +25 -0
  191. package/plugin/vendor/reflexio/reflexio/server/api.py +2714 -0
  192. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/account_api.py +143 -0
  193. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/health_api.py +91 -0
  194. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/pending_tool_call_api.py +572 -0
  195. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/precondition_checks.py +66 -0
  196. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +540 -0
  197. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +50 -0
  198. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/stall_state_api.py +100 -0
  199. package/plugin/vendor/reflexio/reflexio/server/cache/__init__.py +15 -0
  200. package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +208 -0
  201. package/plugin/vendor/reflexio/reflexio/server/correlation.py +46 -0
  202. package/plugin/vendor/reflexio/reflexio/server/llm/__init__.py +30 -0
  203. package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +110 -0
  204. package/plugin/vendor/reflexio/reflexio/server/llm/image_utils.py +55 -0
  205. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +1595 -0
  206. package/plugin/vendor/reflexio/reflexio/server/llm/llm_utils.py +112 -0
  207. package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +469 -0
  208. package/plugin/vendor/reflexio/reflexio/server/llm/providers/__init__.py +1 -0
  209. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +1122 -0
  210. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_stream_parser.py +197 -0
  211. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +210 -0
  212. package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +213 -0
  213. package/plugin/vendor/reflexio/reflexio/server/llm/providers/nomic_embedding_provider.py +255 -0
  214. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/__init__.py +6 -0
  215. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +177 -0
  216. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/llm_reranker.py +148 -0
  217. package/plugin/vendor/reflexio/reflexio/server/llm/tools.py +699 -0
  218. package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +179 -0
  219. package/plugin/vendor/reflexio/reflexio/server/prompt/__init__.py +0 -0
  220. package/plugin/vendor/reflexio/reflexio/server/prompt/_dispatchers.py +54 -0
  221. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/README.md +121 -0
  222. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/agent_success_evaluation/v1.0.0.prompt.md +58 -0
  223. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/agent_success_evaluation_with_comparison/v1.0.0.prompt.md +76 -0
  224. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/answer_synthesis/v1.5.2.prompt.md +88 -0
  225. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/compress_session_for_query/v1.3.0.prompt.md +31 -0
  226. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.0.0.prompt.md +20 -0
  227. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.0.0.prompt.md +53 -0
  228. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.1.0.prompt.md +57 -0
  229. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.2.0.prompt.md +68 -0
  230. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.3.0.prompt.md +70 -0
  231. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.4.0.prompt.md +77 -0
  232. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.5.0.prompt.md +82 -0
  233. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.6.0.prompt.md +83 -0
  234. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.1.0.prompt.md +193 -0
  235. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.2.0.prompt.md +206 -0
  236. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v1.0.0-deprecated.prompt.md +66 -0
  237. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v1.0.0.prompt.md +43 -0
  238. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v1.1.0.prompt.md +46 -0
  239. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.0.0-deprecated.prompt.md +64 -0
  240. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.0.0.prompt.md +39 -0
  241. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.1.0.prompt.md +39 -0
  242. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.2.0.prompt.md +47 -0
  243. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.0.prompt.md +58 -0
  244. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.0.2.prompt.md +254 -0
  245. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.1.0.prompt.md +274 -0
  246. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.0.prompt.md +279 -0
  247. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v1.0.0.prompt.md +73 -0
  248. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v2.0.0.prompt.md +86 -0
  249. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.0.0.prompt.md +97 -0
  250. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.1.0.prompt.md +119 -0
  251. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.2.0.prompt.md +123 -0
  252. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.3.0.prompt.md +127 -0
  253. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.0.0.prompt.md +14 -0
  254. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.1.0.prompt.md +24 -0
  255. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.2.0.prompt.md +29 -0
  256. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main_expert/v1.0.0.prompt.md +11 -0
  257. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main_expert/v1.1.0.prompt.md +21 -0
  258. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main_expert/v1.2.0.prompt.md +25 -0
  259. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_optimizer_judge/v1.0.0.prompt.md +37 -0
  260. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_optimizer_judge/v1.1.0.prompt.md +40 -0
  261. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_optimizer_judge/v1.2.0.prompt.md +36 -0
  262. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate/v1.0.0.prompt.md +45 -0
  263. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate/v2.0.0.prompt.md +81 -0
  264. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate/v3.0.0.prompt.md +80 -0
  265. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate_expert/v1.0.0.prompt.md +34 -0
  266. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_deduplication/v1.0.0.prompt.md +116 -0
  267. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_should_generate/v1.0.0.prompt.md +33 -0
  268. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_should_generate_override/v1.0.0.prompt.md +16 -0
  269. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.0.0.prompt.md +140 -0
  270. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.1.0.prompt.md +160 -0
  271. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_main/v1.0.0.prompt.md +14 -0
  272. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v1.0.0.prompt.md +19 -0
  273. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/rerank_relevance/v1.1.0.prompt.md +44 -0
  274. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.0.0.prompt.md +43 -0
  275. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_content_evaluation/v1.0.0.prompt.md +33 -0
  276. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_evaluation/prompt_evaluation_dataset/feedback_extraction_main_v1.jsonl +10 -0
  277. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_evaluation/prompt_evaluation_dataset/profile_update_main_v1.jsonl +10 -0
  278. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_manager.py +280 -0
  279. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_schema.py +11 -0
  280. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/_eval_health.py +131 -0
  281. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_constants.py +60 -0
  282. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_service.py +228 -0
  283. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_utils.py +87 -0
  284. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluator.py +372 -0
  285. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/delayed_group_evaluator.py +156 -0
  286. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/group_evaluation_runner.py +340 -0
  287. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +471 -0
  288. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +1626 -0
  289. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/__init__.py +0 -0
  290. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/_cron.py +196 -0
  291. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/_encryption.py +101 -0
  292. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/client.py +167 -0
  293. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/service.py +281 -0
  294. package/plugin/vendor/reflexio/reflexio/server/services/configurator/base_configurator.py +179 -0
  295. package/plugin/vendor/reflexio/reflexio/server/services/configurator/config_storage.py +62 -0
  296. package/plugin/vendor/reflexio/reflexio/server/services/configurator/configurator.py +87 -0
  297. package/plugin/vendor/reflexio/reflexio/server/services/configurator/local_file_config_storage.py +187 -0
  298. package/plugin/vendor/reflexio/reflexio/server/services/configurator/test_config_storage.py +162 -0
  299. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +112 -0
  300. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/__init__.py +0 -0
  301. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/distribution.py +33 -0
  302. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/eval_sampler.py +126 -0
  303. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/group_aggregation.py +192 -0
  304. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/hero_state.py +75 -0
  305. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/rule_attribution.py +97 -0
  306. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +515 -0
  307. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/shadow_aggregation.py +90 -0
  308. package/plugin/vendor/reflexio/reflexio/server/services/extraction/__init__.py +0 -0
  309. package/plugin/vendor/reflexio/reflexio/server/services/extraction/agent_run_records.py +91 -0
  310. package/plugin/vendor/reflexio/reflexio/server/services/extraction/invariants.py +303 -0
  311. package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +25 -0
  312. package/plugin/vendor/reflexio/reflexio/server/services/extraction/pending_tool_call_dispatch.py +351 -0
  313. package/plugin/vendor/reflexio/reflexio/server/services/extraction/plan.py +138 -0
  314. package/plugin/vendor/reflexio/reflexio/server/services/extraction/prior_answer_search.py +217 -0
  315. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +468 -0
  316. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +171 -0
  317. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +777 -0
  318. package/plugin/vendor/reflexio/reflexio/server/services/extraction/tools.py +1125 -0
  319. package/plugin/vendor/reflexio/reflexio/server/services/extractor_config_utils.py +91 -0
  320. package/plugin/vendor/reflexio/reflexio/server/services/extractor_interaction_utils.py +251 -0
  321. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +689 -0
  322. package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +835 -0
  323. package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +89 -0
  324. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_aggregator.py +1388 -0
  325. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_consolidator.py +960 -0
  326. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_extractor.py +436 -0
  327. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_generation_service.py +808 -0
  328. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_constants.py +28 -0
  329. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_utils.py +362 -0
  330. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/__init__.py +24 -0
  331. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/assistant_webhook.py +246 -0
  332. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/gepa_adapter.py +291 -0
  333. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/judge.py +97 -0
  334. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/models.py +96 -0
  335. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +645 -0
  336. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/rollout.py +35 -0
  337. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scenario_resolver.py +93 -0
  338. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scheduler.py +174 -0
  339. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/__init__.py +26 -0
  340. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_document_expander.py +179 -0
  341. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_query_reformulator.py +297 -0
  342. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_deduplicator.py +741 -0
  343. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_extractor.py +462 -0
  344. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service.py +734 -0
  345. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service_utils.py +290 -0
  346. package/plugin/vendor/reflexio/reflexio/server/services/reflection/__init__.py +17 -0
  347. package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_extractor.py +247 -0
  348. package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service.py +800 -0
  349. package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service_utils.py +146 -0
  350. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/__init__.py +0 -0
  351. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +70 -0
  352. package/plugin/vendor/reflexio/reflexio/server/services/search/__init__.py +0 -0
  353. package/plugin/vendor/reflexio/reflexio/server/services/service_utils.py +671 -0
  354. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/__init__.py +1 -0
  355. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/judge.py +184 -0
  356. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/outcome.py +81 -0
  357. package/plugin/vendor/reflexio/reflexio/server/services/storage/constants.py +2 -0
  358. package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +11 -0
  359. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +154 -0
  360. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention_mixin.py +155 -0
  361. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +59 -0
  362. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +1253 -0
  363. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +1945 -0
  364. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +600 -0
  365. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_operations.py +346 -0
  366. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +1378 -0
  367. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +747 -0
  368. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +263 -0
  369. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +193 -0
  370. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_share_links.py +166 -0
  371. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_stall_state.py +217 -0
  372. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +153 -0
  373. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +372 -0
  374. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_base.py +71 -0
  375. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +235 -0
  376. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +170 -0
  377. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +677 -0
  378. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_profiles.py +250 -0
  379. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +154 -0
  380. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +130 -0
  381. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_share_links.py +93 -0
  382. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_stall_state.py +76 -0
  383. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +568 -0
  384. package/plugin/vendor/reflexio/reflexio/server/site_var/README.md +77 -0
  385. package/plugin/vendor/reflexio/reflexio/server/site_var/feature_flags.py +116 -0
  386. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_manager.py +263 -0
  387. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/feature_flags.json +13 -0
  388. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/llm_model_setting.json +7 -0
  389. package/plugin/vendor/reflexio/reflexio/server/tracing.py +158 -0
  390. package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +113 -0
  391. package/plugin/vendor/reflexio/reflexio/server/uvicorn_logging.py +76 -0
  392. package/plugin/vendor/reflexio/reflexio/test_support/__init__.py +1 -0
  393. package/plugin/vendor/reflexio/reflexio/test_support/llm_fixtures.py +62 -0
  394. package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +242 -0
  395. package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +129 -0
  396. 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
+ )