alysis-code 0.13.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (458) hide show
  1. alysis_code/__init__.py +3 -0
  2. alysis_code/__main__.py +4 -0
  3. alysis_code/_build_info.py +14 -0
  4. alysis_code/account_login.py +468 -0
  5. alysis_code/agent/README.md +35 -0
  6. alysis_code/agent/__init__.py +11 -0
  7. alysis_code/agent/acceptance_contract.py +2217 -0
  8. alysis_code/agent/blast_radius.py +1403 -0
  9. alysis_code/agent/cache_keepalive.py +227 -0
  10. alysis_code/agent/completion_certificate.py +366 -0
  11. alysis_code/agent/completion_gate.py +306 -0
  12. alysis_code/agent/empty_response_stall.py +403 -0
  13. alysis_code/agent/errors.py +28 -0
  14. alysis_code/agent/llm_calls.py +475 -0
  15. alysis_code/agent/mutation_classification.py +227 -0
  16. alysis_code/agent/prompt_context.py +2508 -0
  17. alysis_code/agent/read_ledger.py +253 -0
  18. alysis_code/agent/regression_baseline.py +642 -0
  19. alysis_code/agent/reproduction_first.py +610 -0
  20. alysis_code/agent/sensitive_output.py +629 -0
  21. alysis_code/agent/session.py +3218 -0
  22. alysis_code/agent/steering.py +191 -0
  23. alysis_code/agent/subagent_execution.py +5177 -0
  24. alysis_code/agent/subagent_workspace.py +666 -0
  25. alysis_code/agent/tools_assembly.py +4728 -0
  26. alysis_code/agent/turn/__init__.py +101 -0
  27. alysis_code/agent/turn/core.py +8483 -0
  28. alysis_code/agent/turn/events.py +113 -0
  29. alysis_code/agent/turn/exploration.py +590 -0
  30. alysis_code/agent/turn/interventions.py +65 -0
  31. alysis_code/agent/turn/read_cache.py +420 -0
  32. alysis_code/agent/turn/snapshot.py +179 -0
  33. alysis_code/agent/turn_contract.py +661 -0
  34. alysis_code/agent/turn_path.py +129 -0
  35. alysis_code/agent/verification.py +2885 -0
  36. alysis_code/agent/verification_commands.py +512 -0
  37. alysis_code/agent/verification_evidence.py +738 -0
  38. alysis_code/agent_loop.py +693 -0
  39. alysis_code/agent_runtimes/__init__.py +51 -0
  40. alysis_code/agent_runtimes/base.py +114 -0
  41. alysis_code/agent_runtimes/builtins.py +129 -0
  42. alysis_code/agent_runtimes/codex_cli.py +664 -0
  43. alysis_code/agent_runtimes/host.py +263 -0
  44. alysis_code/agent_runtimes/registry.py +64 -0
  45. alysis_code/agent_runtimes/service.py +150 -0
  46. alysis_code/agentbox_client.py +416 -0
  47. alysis_code/agentbox_integration.py +310 -0
  48. alysis_code/alysis_cloud.py +152 -0
  49. alysis_code/approval_scope.py +276 -0
  50. alysis_code/assets/README.md +33 -0
  51. alysis_code/assets/__init__.py +126 -0
  52. alysis_code/assets/asset_read_core.py +281 -0
  53. alysis_code/assets/budget_allocator.py +456 -0
  54. alysis_code/assets/comprehender.py +759 -0
  55. alysis_code/assets/index.py +654 -0
  56. alysis_code/assets/ingestion.py +275 -0
  57. alysis_code/assets/legacy_migration.py +413 -0
  58. alysis_code/assets/models.py +263 -0
  59. alysis_code/assets/ocr.py +239 -0
  60. alysis_code/assets/owl/ascii/f-000.txt +13 -0
  61. alysis_code/assets/owl/ascii/f-001.txt +13 -0
  62. alysis_code/assets/owl/ascii/f-002.txt +13 -0
  63. alysis_code/assets/owl/ascii/f-003.txt +13 -0
  64. alysis_code/assets/owl/ascii/f-004.txt +13 -0
  65. alysis_code/assets/owl/ascii/f-005.txt +13 -0
  66. alysis_code/assets/owl/ascii/f-006.txt +13 -0
  67. alysis_code/assets/owl/ascii/f-007.txt +13 -0
  68. alysis_code/assets/owl/ascii/f-008.txt +13 -0
  69. alysis_code/assets/owl/ascii/f-009.txt +13 -0
  70. alysis_code/assets/owl/ascii/f-010.txt +13 -0
  71. alysis_code/assets/owl/ascii/f-011.txt +13 -0
  72. alysis_code/assets/owl/ascii/f-012.txt +13 -0
  73. alysis_code/assets/owl/ascii/f-013.txt +13 -0
  74. alysis_code/assets/owl/ascii/f-014.txt +13 -0
  75. alysis_code/assets/owl/ascii/f-015.txt +13 -0
  76. alysis_code/assets/owl/ascii/f-016.txt +13 -0
  77. alysis_code/assets/owl/ascii/f-017.txt +13 -0
  78. alysis_code/assets/owl/ascii/f-018.txt +13 -0
  79. alysis_code/assets/owl/ascii/f-019.txt +13 -0
  80. alysis_code/assets/owl/ascii/f-020.txt +13 -0
  81. alysis_code/assets/owl/index.html +98 -0
  82. alysis_code/assets/owl/show-owl.sh +761 -0
  83. alysis_code/assets/paths.py +49 -0
  84. alysis_code/assets/plan_binding.py +326 -0
  85. alysis_code/assets/planner_context.py +466 -0
  86. alysis_code/assets/planner_tools.py +184 -0
  87. alysis_code/assets/prompts.py +101 -0
  88. alysis_code/assets/replanner_context.py +239 -0
  89. alysis_code/assets/surface.py +521 -0
  90. alysis_code/assets/untrusted_content.py +48 -0
  91. alysis_code/assets/usage_logger.py +94 -0
  92. alysis_code/assets/worker_mirror.py +428 -0
  93. alysis_code/assets/worker_section.py +303 -0
  94. alysis_code/assets/worker_tools.py +468 -0
  95. alysis_code/atomic_io.py +83 -0
  96. alysis_code/auth_diagnostics.py +272 -0
  97. alysis_code/background_runner.py +366 -0
  98. alysis_code/branding.py +270 -0
  99. alysis_code/budget_policy.py +390 -0
  100. alysis_code/build_identity.py +465 -0
  101. alysis_code/builtin_hooks/__init__.py +7 -0
  102. alysis_code/builtin_hooks/notify_done_windows.py +65 -0
  103. alysis_code/bwrap_etc.py +76 -0
  104. alysis_code/cancellation.py +41 -0
  105. alysis_code/capabilities.py +137 -0
  106. alysis_code/chatgpt_codex_static_provider.py +133 -0
  107. alysis_code/cli.py +51 -0
  108. alysis_code/cli_impl/__init__.py +1 -0
  109. alysis_code/cli_impl/assets_cli.py +537 -0
  110. alysis_code/cli_impl/assets_modal.py +412 -0
  111. alysis_code/cli_impl/chat/__init__.py +156 -0
  112. alysis_code/cli_impl/chat/commands.py +2616 -0
  113. alysis_code/cli_impl/chat/loop.py +4508 -0
  114. alysis_code/cli_impl/chat/mid_turn_policy.py +125 -0
  115. alysis_code/cli_impl/chat/rendering.py +444 -0
  116. alysis_code/cli_impl/chat/state.py +124 -0
  117. alysis_code/cli_impl/chat_resume.py +830 -0
  118. alysis_code/cli_impl/chat_slash_completer.py +258 -0
  119. alysis_code/cli_impl/commands/__init__.py +11 -0
  120. alysis_code/cli_impl/commands/_shared.py +89 -0
  121. alysis_code/cli_impl/commands/auth.py +623 -0
  122. alysis_code/cli_impl/commands/chat_resume_helpers.py +1531 -0
  123. alysis_code/cli_impl/commands/chat_state.py +158 -0
  124. alysis_code/cli_impl/commands/chat_status.py +1248 -0
  125. alysis_code/cli_impl/commands/chat_terminal.py +942 -0
  126. alysis_code/cli_impl/commands/chat_tui_panels.py +1018 -0
  127. alysis_code/cli_impl/commands/cli_common.py +1223 -0
  128. alysis_code/cli_impl/commands/cli_surface.py +77 -0
  129. alysis_code/cli_impl/commands/config.py +131 -0
  130. alysis_code/cli_impl/commands/conventions.py +85 -0
  131. alysis_code/cli_impl/commands/execution_helpers.py +350 -0
  132. alysis_code/cli_impl/commands/extensions.py +401 -0
  133. alysis_code/cli_impl/commands/forge.py +1282 -0
  134. alysis_code/cli_impl/commands/forge_asset_view.py +121 -0
  135. alysis_code/cli_impl/commands/forge_helpers.py +1215 -0
  136. alysis_code/cli_impl/commands/hooks.py +737 -0
  137. alysis_code/cli_impl/commands/ide_bridge.py +31 -0
  138. alysis_code/cli_impl/commands/mcp.py +700 -0
  139. alysis_code/cli_impl/commands/profile.py +453 -0
  140. alysis_code/cli_impl/commands/prompt_helpers.py +307 -0
  141. alysis_code/cli_impl/commands/report.py +88 -0
  142. alysis_code/cli_impl/commands/root.py +1118 -0
  143. alysis_code/cli_impl/commands/sandbox.py +184 -0
  144. alysis_code/cli_impl/commands/server.py +54 -0
  145. alysis_code/cli_impl/commands/sessions.py +252 -0
  146. alysis_code/cli_impl/commands/skills.py +404 -0
  147. alysis_code/cli_impl/commands/startup.py +946 -0
  148. alysis_code/cli_impl/commands/tools.py +335 -0
  149. alysis_code/cli_impl/commands/update.py +364 -0
  150. alysis_code/cli_impl/commands/welcome.py +972 -0
  151. alysis_code/cli_impl/config_menu.py +3882 -0
  152. alysis_code/cli_impl/forge.py +4509 -0
  153. alysis_code/cli_impl/forge_recovery.py +485 -0
  154. alysis_code/cli_impl/setup_wizard.py +2409 -0
  155. alysis_code/cli_impl/tui/__init__.py +58 -0
  156. alysis_code/cli_impl/tui/app.py +4551 -0
  157. alysis_code/cli_impl/tui/config.py +32 -0
  158. alysis_code/cli_impl/tui/config_flow.py +2754 -0
  159. alysis_code/cli_impl/tui/config_overlay.py +566 -0
  160. alysis_code/cli_impl/tui/content.py +78 -0
  161. alysis_code/cli_impl/tui/footer.py +218 -0
  162. alysis_code/cli_impl/tui/forge_status.py +136 -0
  163. alysis_code/cli_impl/tui/markdown.py +244 -0
  164. alysis_code/cli_impl/tui/owl.py +109 -0
  165. alysis_code/cli_impl/tui/plan_meta.py +477 -0
  166. alysis_code/cli_impl/tui/setup_app.py +519 -0
  167. alysis_code/cli_impl/tui/setup_flow.py +1622 -0
  168. alysis_code/cli_impl/tui/state.py +101 -0
  169. alysis_code/cli_impl/tui/subagent_identity.py +66 -0
  170. alysis_code/cli_impl/tui/subagent_panel.py +186 -0
  171. alysis_code/cli_impl/tui/surface.py +796 -0
  172. alysis_code/cli_impl/tui/transcript.py +514 -0
  173. alysis_code/cli_impl/tui/update_prompt.py +79 -0
  174. alysis_code/cli_impl/tui/workspace_guard.py +384 -0
  175. alysis_code/clipboard.py +172 -0
  176. alysis_code/code_review.py +1211 -0
  177. alysis_code/compaction/__init__.py +28 -0
  178. alysis_code/compaction/conversation_compactor.py +2932 -0
  179. alysis_code/compaction/importance.py +177 -0
  180. alysis_code/compaction/settings.py +297 -0
  181. alysis_code/compaction/tool_output_offload.py +447 -0
  182. alysis_code/config.py +3509 -0
  183. alysis_code/conflict_auto_resolver.py +895 -0
  184. alysis_code/context/__init__.py +1 -0
  185. alysis_code/context/tool_schema_budgeter.py +220 -0
  186. alysis_code/crash_diagnostics.py +282 -0
  187. alysis_code/custom_tools/README.md +34 -0
  188. alysis_code/custom_tools/__init__.py +43 -0
  189. alysis_code/custom_tools/discovery.py +903 -0
  190. alysis_code/custom_tools/runtime.py +1516 -0
  191. alysis_code/custom_tools/session.py +227 -0
  192. alysis_code/custom_tools/trust.py +232 -0
  193. alysis_code/diff_paths.py +113 -0
  194. alysis_code/direction_change.py +293 -0
  195. alysis_code/dispatch_timing.py +306 -0
  196. alysis_code/durable_service_manager.py +1236 -0
  197. alysis_code/edit_discipline.py +659 -0
  198. alysis_code/error_text.py +73 -0
  199. alysis_code/execution_budget.py +411 -0
  200. alysis_code/execution_context.py +915 -0
  201. alysis_code/execution_deadline.py +1065 -0
  202. alysis_code/execution_shared.py +1904 -0
  203. alysis_code/extensions/README.md +30 -0
  204. alysis_code/extensions/__init__.py +93 -0
  205. alysis_code/extensions/activation.py +138 -0
  206. alysis_code/extensions/install.py +1436 -0
  207. alysis_code/extensions/manifest.py +487 -0
  208. alysis_code/extensions/models.py +74 -0
  209. alysis_code/extensions/paths.py +56 -0
  210. alysis_code/extensions/registry.json +4 -0
  211. alysis_code/extensions/registry.py +52 -0
  212. alysis_code/extensions/state.py +83 -0
  213. alysis_code/extensions/workspace_trust.py +101 -0
  214. alysis_code/failed_task_evidence.py +369 -0
  215. alysis_code/failure_category.py +315 -0
  216. alysis_code/feedback_report.py +1647 -0
  217. alysis_code/file_classification.py +485 -0
  218. alysis_code/forge.py +2064 -0
  219. alysis_code/forge_completion.py +362 -0
  220. alysis_code/forge_events.py +475 -0
  221. alysis_code/frontmatter_utils.py +95 -0
  222. alysis_code/git_evidence.py +1181 -0
  223. alysis_code/git_ops.py +560 -0
  224. alysis_code/git_safe.py +62 -0
  225. alysis_code/git_worktrees.py +190 -0
  226. alysis_code/hooks/README.md +33 -0
  227. alysis_code/hooks/__init__.py +67 -0
  228. alysis_code/hooks/audit.py +171 -0
  229. alysis_code/hooks/config.py +225 -0
  230. alysis_code/hooks/dispatcher.py +1110 -0
  231. alysis_code/hooks/models.py +447 -0
  232. alysis_code/hooks/trust.py +202 -0
  233. alysis_code/host_actions.py +543 -0
  234. alysis_code/host_browser.py +103 -0
  235. alysis_code/ide/__init__.py +5 -0
  236. alysis_code/ide/activity_events.py +399 -0
  237. alysis_code/ide/approvals.py +337 -0
  238. alysis_code/ide/artifacts.py +153 -0
  239. alysis_code/ide/browser_egress_proxy.py +1076 -0
  240. alysis_code/ide/cdp_websocket_transport.py +1192 -0
  241. alysis_code/ide/change_ledger.py +1721 -0
  242. alysis_code/ide/context_blocks.py +979 -0
  243. alysis_code/ide/event_stream.py +531 -0
  244. alysis_code/ide/forge_protocol.py +3112 -0
  245. alysis_code/ide/forge_request_ledger.py +737 -0
  246. alysis_code/ide/health.py +965 -0
  247. alysis_code/ide/managed_browser.py +2251 -0
  248. alysis_code/ide/management_protocol.py +3414 -0
  249. alysis_code/ide/mcp_oauth_coordinator.py +744 -0
  250. alysis_code/ide/mcp_oauth_lifecycle.py +1504 -0
  251. alysis_code/ide/prompt_queue.py +1070 -0
  252. alysis_code/ide/protocol.py +191 -0
  253. alysis_code/ide/resumable_swarm.py +1543 -0
  254. alysis_code/ide/session_search.py +295 -0
  255. alysis_code/ide/stdio_bridge.py +9935 -0
  256. alysis_code/ide/structured_state.py +1579 -0
  257. alysis_code/ide/swarm_protocol.py +816 -0
  258. alysis_code/integration_gate.py +506 -0
  259. alysis_code/interactive_input_guard.py +39 -0
  260. alysis_code/interactive_plan_mode.py +26 -0
  261. alysis_code/internal_artifacts.py +179 -0
  262. alysis_code/knowledge_base.py +1409 -0
  263. alysis_code/knowledge_capture.py +1190 -0
  264. alysis_code/knowledge_librarian.py +605 -0
  265. alysis_code/language_policy.py +34 -0
  266. alysis_code/litellm_static_provider.py +535 -0
  267. alysis_code/llm/__init__.py +1 -0
  268. alysis_code/llm/anthropic_messages.py +2288 -0
  269. alysis_code/llm/base.py +71 -0
  270. alysis_code/llm/cache_capabilities.py +985 -0
  271. alysis_code/llm/cache_control_blocks.py +244 -0
  272. alysis_code/llm/cache_policy.py +388 -0
  273. alysis_code/llm/factory.py +373 -0
  274. alysis_code/llm/gemini_generate_content.py +2652 -0
  275. alysis_code/llm/gemini_interactions.py +739 -0
  276. alysis_code/llm/metadata.py +450 -0
  277. alysis_code/llm/openai_compat.py +2947 -0
  278. alysis_code/llm/openai_responses.py +2604 -0
  279. alysis_code/llm/protocols.py +609 -0
  280. alysis_code/llm/provider_limits.py +525 -0
  281. alysis_code/llm/request_plan.py +389 -0
  282. alysis_code/llm/request_shape.py +238 -0
  283. alysis_code/llm/streaming.py +108 -0
  284. alysis_code/llm/temperature_compat.py +78 -0
  285. alysis_code/llm/types.py +195 -0
  286. alysis_code/llm/usage_normalization.py +222 -0
  287. alysis_code/llm_error_display.py +315 -0
  288. alysis_code/logging_redaction.py +326 -0
  289. alysis_code/managed_host_deadline.py +196 -0
  290. alysis_code/mcp/README.md +33 -0
  291. alysis_code/mcp/__init__.py +24 -0
  292. alysis_code/mcp/client.py +1137 -0
  293. alysis_code/mcp/config.py +597 -0
  294. alysis_code/mcp/errors.py +113 -0
  295. alysis_code/mcp/forge_scope.py +154 -0
  296. alysis_code/mcp/jsonrpc.py +214 -0
  297. alysis_code/mcp/manager.py +2308 -0
  298. alysis_code/mcp/models.py +666 -0
  299. alysis_code/mcp/oauth.py +972 -0
  300. alysis_code/mcp/oauth_runtime.py +310 -0
  301. alysis_code/mcp/oauth_store.py +276 -0
  302. alysis_code/mcp/prompts.py +329 -0
  303. alysis_code/mcp/resources.py +295 -0
  304. alysis_code/mcp/roots.py +106 -0
  305. alysis_code/mcp/server_requests.py +75 -0
  306. alysis_code/mcp/token_store.py +859 -0
  307. alysis_code/mcp/transport_http.py +1338 -0
  308. alysis_code/mcp/transport_stdio.py +1267 -0
  309. alysis_code/mcp/untrusted_content.py +119 -0
  310. alysis_code/merge_conflict_reviewer.py +729 -0
  311. alysis_code/model_catalog/__init__.py +1 -0
  312. alysis_code/model_catalog/chatgpt_codex_subscription_snapshot.json +186 -0
  313. alysis_code/model_catalog/litellm_model_prices_snapshot.json +44715 -0
  314. alysis_code/model_catalog/litellm_model_prices_snapshot.meta.json +17 -0
  315. alysis_code/model_metadata_policy.py +223 -0
  316. alysis_code/model_metadata_utils.py +103 -0
  317. alysis_code/model_registry.py +1420 -0
  318. alysis_code/model_router.py +147 -0
  319. alysis_code/permission_policy.py +1016 -0
  320. alysis_code/personas.py +451 -0
  321. alysis_code/pipeline_facts.py +233 -0
  322. alysis_code/plan_assistant.py +4763 -0
  323. alysis_code/plan_mode.py +393 -0
  324. alysis_code/plan_reconciliation.py +1228 -0
  325. alysis_code/plan_repair.py +652 -0
  326. alysis_code/plan_validation.py +1099 -0
  327. alysis_code/planning_constraints.py +904 -0
  328. alysis_code/policy.py +95 -0
  329. alysis_code/preview_server.py +457 -0
  330. alysis_code/process_reaping.py +566 -0
  331. alysis_code/profile_presets.py +1834 -0
  332. alysis_code/profiles.py +666 -0
  333. alysis_code/provider_auth/__init__.py +29 -0
  334. alysis_code/provider_auth/base.py +99 -0
  335. alysis_code/provider_auth/openai_codex.py +951 -0
  336. alysis_code/provider_auth/registry.py +76 -0
  337. alysis_code/provider_auth/store.py +125 -0
  338. alysis_code/provider_diagnostics.py +1209 -0
  339. alysis_code/provider_model_catalog.py +685 -0
  340. alysis_code/provider_telemetry.py +1699 -0
  341. alysis_code/provider_url.py +75 -0
  342. alysis_code/reasoning_contracts.py +911 -0
  343. alysis_code/remote_sync.py +350 -0
  344. alysis_code/replanning.py +1195 -0
  345. alysis_code/repo_scan.py +1152 -0
  346. alysis_code/request_estimation.py +296 -0
  347. alysis_code/review_gate.py +617 -0
  348. alysis_code/run_lock.py +1141 -0
  349. alysis_code/run_outcome.py +58 -0
  350. alysis_code/run_provenance.py +774 -0
  351. alysis_code/run_state.py +445 -0
  352. alysis_code/runtime_artifacts.py +116 -0
  353. alysis_code/runtime_context_features.py +78 -0
  354. alysis_code/runtime_kind.py +52 -0
  355. alysis_code/safety/__init__.py +11 -0
  356. alysis_code/safety/mcp_sanitize.py +29 -0
  357. alysis_code/safety/safe_http.py +297 -0
  358. alysis_code/safety/subagent_report.py +184 -0
  359. alysis_code/sandbox_doctor.py +682 -0
  360. alysis_code/sandbox_runner.py +1025 -0
  361. alysis_code/sandbox_settings.py +423 -0
  362. alysis_code/serialized_paths.py +355 -0
  363. alysis_code/server/__init__.py +3 -0
  364. alysis_code/server/app.py +367 -0
  365. alysis_code/server/auth.py +34 -0
  366. alysis_code/server/job_config.py +30 -0
  367. alysis_code/server/settings.py +215 -0
  368. alysis_code/server/store.py +193 -0
  369. alysis_code/server/worker_runner.py +657 -0
  370. alysis_code/service_persistence.py +355 -0
  371. alysis_code/session_artifacts.py +108 -0
  372. alysis_code/session_metrics.py +331 -0
  373. alysis_code/session_store.py +624 -0
  374. alysis_code/skills/README.md +34 -0
  375. alysis_code/skills/__init__.py +104 -0
  376. alysis_code/skills/conventions.py +84 -0
  377. alysis_code/skills/discovery.py +176 -0
  378. alysis_code/skills/eval_models.py +232 -0
  379. alysis_code/skills/eval_runner.py +372 -0
  380. alysis_code/skills/evals.py +1344 -0
  381. alysis_code/skills/install.py +293 -0
  382. alysis_code/skills/loader.py +118 -0
  383. alysis_code/skills/matching.py +103 -0
  384. alysis_code/skills/models.py +71 -0
  385. alysis_code/skills/paths.py +56 -0
  386. alysis_code/skills/prompting.py +500 -0
  387. alysis_code/skills/scaffold.py +142 -0
  388. alysis_code/skills/state.py +441 -0
  389. alysis_code/skills/transactions.py +125 -0
  390. alysis_code/skills/validation.py +304 -0
  391. alysis_code/step_budget.py +238 -0
  392. alysis_code/subagent_labels.py +49 -0
  393. alysis_code/subagents.py +1072 -0
  394. alysis_code/surface/__init__.py +80 -0
  395. alysis_code/surface/base.py +305 -0
  396. alysis_code/surface/console.py +387 -0
  397. alysis_code/surface/events.py +372 -0
  398. alysis_code/surface/hidden_surface.py +529 -0
  399. alysis_code/surface/noop_surface.py +219 -0
  400. alysis_code/surface/rich_surface.py +1555 -0
  401. alysis_code/surface/styles.py +67 -0
  402. alysis_code/surface/theme.py +455 -0
  403. alysis_code/surface/types.py +100 -0
  404. alysis_code/swarm_backend.py +926 -0
  405. alysis_code/swarm_orchestrator.py +4020 -0
  406. alysis_code/swarm_scheduler.py +441 -0
  407. alysis_code/swarm_trace.py +429 -0
  408. alysis_code/swarm_worker.py +2119 -0
  409. alysis_code/swarm_write_guard.py +348 -0
  410. alysis_code/task_dependencies.py +170 -0
  411. alysis_code/task_readiness.py +992 -0
  412. alysis_code/task_scope.py +2148 -0
  413. alysis_code/terminal_manager.py +762 -0
  414. alysis_code/terminal_ownership.py +460 -0
  415. alysis_code/text_normalization.py +30 -0
  416. alysis_code/token_budget.py +97 -0
  417. alysis_code/tools/README.md +34 -0
  418. alysis_code/tools/__init__.py +1 -0
  419. alysis_code/tools/artifacts.py +127 -0
  420. alysis_code/tools/availability.py +188 -0
  421. alysis_code/tools/fs.py +1456 -0
  422. alysis_code/tools/git.py +461 -0
  423. alysis_code/tools/history.py +229 -0
  424. alysis_code/tools/http_timeout.py +78 -0
  425. alysis_code/tools/image_generation.py +552 -0
  426. alysis_code/tools/registry.py +2936 -0
  427. alysis_code/tools/repo_map.py +476 -0
  428. alysis_code/tools/search.py +563 -0
  429. alysis_code/tools/shell.py +135 -0
  430. alysis_code/tools/symbols.py +1350 -0
  431. alysis_code/tools/test_discovery.py +643 -0
  432. alysis_code/tools/web.py +482 -0
  433. alysis_code/tools/web_search.py +2012 -0
  434. alysis_code/tools/web_search_dashscope.py +557 -0
  435. alysis_code/tools/web_search_ddgs.py +221 -0
  436. alysis_code/tools/web_search_provider_adapters.py +1429 -0
  437. alysis_code/tools/web_search_tavily.py +194 -0
  438. alysis_code/updates.py +933 -0
  439. alysis_code/usage_tracker.py +1990 -0
  440. alysis_code/verification_command_analysis.py +1004 -0
  441. alysis_code/verification_contract.py +574 -0
  442. alysis_code/verification_failure_summary.py +273 -0
  443. alysis_code/verification_repair.py +385 -0
  444. alysis_code/verify_gate.py +3129 -0
  445. alysis_code/web_research.py +1872 -0
  446. alysis_code/web_search_adapters.py +66 -0
  447. alysis_code/web_search_policy.py +27 -0
  448. alysis_code/workspace_binding.py +389 -0
  449. alysis_code/workspace_binding_ui.py +408 -0
  450. alysis_code/workspace_context.py +273 -0
  451. alysis_code/workspace_isolation.py +138 -0
  452. alysis_code/workspace_provisioning.py +455 -0
  453. alysis_code-0.13.0.dist-info/METADATA +507 -0
  454. alysis_code-0.13.0.dist-info/RECORD +458 -0
  455. alysis_code-0.13.0.dist-info/WHEEL +4 -0
  456. alysis_code-0.13.0.dist-info/entry_points.txt +3 -0
  457. alysis_code-0.13.0.dist-info/licenses/LICENSE +176 -0
  458. alysis_code-0.13.0.dist-info/licenses/NOTICE +4 -0
@@ -0,0 +1,1070 @@
1
+ """Durable, fenced prompt queue for IDE clients.
2
+
3
+ The queue intentionally owns no execution threads. A bridge enqueues a JSON
4
+ payload, claims the next item for a session, and completes (or fails) that item
5
+ with the returned opaque lease token. SQLite ``BEGIN IMMEDIATE`` transactions
6
+ and a per-session partial unique index prevent two bridge processes from
7
+ holding live work for the same session at once.
8
+
9
+ Prompt payloads are never included in dataclass representations or exception
10
+ messages. Storage uses JSON rather than pickle so reopening a database cannot
11
+ execute serialized code.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import hashlib
17
+ import json
18
+ import math
19
+ import os
20
+ import re
21
+ import sqlite3
22
+ import threading
23
+ import time
24
+ import uuid
25
+ from collections.abc import Callable, Iterable, Mapping
26
+ from contextlib import contextmanager
27
+ from dataclasses import dataclass, field
28
+ from enum import Enum
29
+ from pathlib import Path
30
+ from typing import Any, TypeAlias
31
+
32
+ from ..branding import canonical_user_data_dir, env_get
33
+
34
+ JsonScalar: TypeAlias = None | bool | int | float | str
35
+ JsonValue: TypeAlias = JsonScalar | list["JsonValue"] | dict[str, "JsonValue"]
36
+
37
+ SCHEMA_VERSION = 2
38
+ DEFAULT_LEASE_SECONDS = 120.0
39
+ MIN_LEASE_SECONDS = 1.0
40
+ MAX_LEASE_SECONDS = 24 * 60 * 60.0
41
+ _IDENTIFIER_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:@/-]*$")
42
+ _ERROR_CODE_RE = re.compile(r"^[a-z][a-z0-9_.-]*$")
43
+
44
+
45
+ class PromptState(str, Enum):
46
+ PENDING = "pending"
47
+ RUNNING = "running"
48
+ COMPLETED = "completed"
49
+ CANCELLED = "cancelled"
50
+ FAILED = "failed"
51
+
52
+
53
+ TERMINAL_STATES = frozenset({PromptState.COMPLETED, PromptState.CANCELLED, PromptState.FAILED})
54
+
55
+
56
+ class PromptQueueError(RuntimeError):
57
+ """Base error with messages safe to surface or log."""
58
+
59
+
60
+ class PromptQueueValidationError(PromptQueueError, ValueError):
61
+ pass
62
+
63
+
64
+ class PromptQueueCapacityError(PromptQueueError):
65
+ pass
66
+
67
+
68
+ class PromptIdempotencyConflict(PromptQueueError):
69
+ pass
70
+
71
+
72
+ class PromptNotFound(PromptQueueError):
73
+ pass
74
+
75
+
76
+ class PromptStateError(PromptQueueError):
77
+ pass
78
+
79
+
80
+ class PromptLeaseLost(PromptQueueError):
81
+ pass
82
+
83
+
84
+ class PromptQueueStorageError(PromptQueueError):
85
+ pass
86
+
87
+
88
+ @dataclass(frozen=True, slots=True)
89
+ class PromptQueueConfig:
90
+ max_payload_bytes: int = 1024 * 1024
91
+ max_json_depth: int = 24
92
+ max_json_nodes: int = 50_000
93
+ max_outstanding_per_session: int = 100
94
+ max_list_limit: int = 500
95
+ busy_timeout_seconds: float = 10.0
96
+
97
+ def __post_init__(self) -> None:
98
+ positive_ints = (
99
+ self.max_payload_bytes,
100
+ self.max_json_depth,
101
+ self.max_json_nodes,
102
+ self.max_outstanding_per_session,
103
+ self.max_list_limit,
104
+ )
105
+ if any(
106
+ isinstance(value, bool) or not isinstance(value, int) or value <= 0
107
+ for value in positive_ints
108
+ ):
109
+ raise PromptQueueValidationError("Queue limits must be positive integers.")
110
+ if (
111
+ isinstance(self.busy_timeout_seconds, bool)
112
+ or not isinstance(self.busy_timeout_seconds, (int, float))
113
+ or not math.isfinite(self.busy_timeout_seconds)
114
+ or self.busy_timeout_seconds <= 0
115
+ or self.busy_timeout_seconds > 60
116
+ ):
117
+ raise PromptQueueValidationError("Queue busy timeout is outside the allowed range.")
118
+
119
+
120
+ @dataclass(frozen=True, slots=True)
121
+ class PromptQueueItem:
122
+ prompt_id: str
123
+ sequence: int
124
+ session_id: str = field(repr=False)
125
+ idempotency_key: str = field(repr=False)
126
+ payload: dict[str, JsonValue] = field(repr=False)
127
+ state: PromptState
128
+ created_at: float
129
+ updated_at: float
130
+ attempts: int
131
+ lease_expires_at: float | None = field(default=None, repr=False)
132
+ execution_started_at: float | None = None
133
+ terminal_at: float | None = None
134
+ error_code: str | None = None
135
+
136
+
137
+ @dataclass(frozen=True, slots=True)
138
+ class EnqueueResult:
139
+ item: PromptQueueItem
140
+ created: bool
141
+
142
+
143
+ @dataclass(frozen=True, slots=True)
144
+ class PromptLease:
145
+ item: PromptQueueItem
146
+ lease_token: str = field(repr=False)
147
+ owner_id: str = field(repr=False)
148
+ expires_at: float
149
+
150
+
151
+ def default_prompt_queue_path() -> Path:
152
+ """Return the user-data queue path, deliberately outside any workspace."""
153
+
154
+ override = str(env_get("ALYSIS_DATA_DIR") or "").strip()
155
+ data_dir = Path(override).expanduser() if override else canonical_user_data_dir()
156
+ return data_dir / "ide" / "prompt-queue.sqlite3"
157
+
158
+
159
+ class DurablePromptQueue:
160
+ """SQLite-backed ordered prompt queue.
161
+
162
+ Connections are short-lived and are never shared between threads. SQLite
163
+ serializes writers across processes; an in-process lock also prevents
164
+ schema setup and writes from needlessly contending in one bridge.
165
+ """
166
+
167
+ def __init__(
168
+ self,
169
+ path: str | os.PathLike[str] | None = None,
170
+ *,
171
+ config: PromptQueueConfig | None = None,
172
+ clock: Callable[[], float] = time.time,
173
+ id_factory: Callable[[], str] | None = None,
174
+ ) -> None:
175
+ self.path = Path(path) if path is not None else default_prompt_queue_path()
176
+ self.config = config or PromptQueueConfig()
177
+ self._clock = clock
178
+ self._id_factory = id_factory or (lambda: uuid.uuid4().hex)
179
+ self._lock = threading.RLock()
180
+ self._prepare_storage()
181
+
182
+ def enqueue(
183
+ self,
184
+ *,
185
+ session_id: str,
186
+ idempotency_key: str,
187
+ payload: Mapping[str, Any],
188
+ ) -> EnqueueResult:
189
+ """Append a prompt, or return its prior row for an identical retry.
190
+
191
+ Reusing ``(session_id, idempotency_key)`` with a different payload is
192
+ rejected. No payload fragments appear in that error.
193
+ """
194
+
195
+ session_id = _validate_identifier(session_id, "session", max_length=256)
196
+ idempotency_key = _validate_identifier(idempotency_key, "idempotency", max_length=256)
197
+ payload_json, normalized_payload = _serialize_payload(payload, self.config)
198
+ payload_sha256 = hashlib.sha256(payload_json.encode("utf-8")).hexdigest()
199
+
200
+ with self._lock, self._transaction() as connection:
201
+ existing = connection.execute(
202
+ """
203
+ SELECT * FROM prompt_queue_items
204
+ WHERE session_id = ? AND idempotency_key = ?
205
+ """,
206
+ (session_id, idempotency_key),
207
+ ).fetchone()
208
+ if existing is not None:
209
+ if existing["payload_sha256"] != payload_sha256:
210
+ raise PromptIdempotencyConflict(
211
+ "Idempotency key was already used for a different prompt."
212
+ )
213
+ return EnqueueResult(item=_item_from_row(existing), created=False)
214
+
215
+ outstanding = connection.execute(
216
+ """
217
+ SELECT COUNT(*) FROM prompt_queue_items
218
+ WHERE session_id = ? AND state IN ('pending', 'running')
219
+ """,
220
+ (session_id,),
221
+ ).fetchone()[0]
222
+ if outstanding >= self.config.max_outstanding_per_session:
223
+ raise PromptQueueCapacityError("Session prompt queue is full.")
224
+
225
+ prompt_id = _validate_identifier(self._id_factory(), "generated prompt", max_length=256)
226
+ now = self._now()
227
+ try:
228
+ connection.execute(
229
+ """
230
+ INSERT INTO prompt_queue_items (
231
+ prompt_id, session_id, idempotency_key,
232
+ payload_json, payload_sha256, state,
233
+ created_at, updated_at
234
+ ) VALUES (?, ?, ?, ?, ?, 'pending', ?, ?)
235
+ """,
236
+ (
237
+ prompt_id,
238
+ session_id,
239
+ idempotency_key,
240
+ payload_json,
241
+ payload_sha256,
242
+ now,
243
+ now,
244
+ ),
245
+ )
246
+ except sqlite3.IntegrityError:
247
+ # Another process may have inserted the same idempotency key
248
+ # after our first SELECT. Inspect it without exposing values.
249
+ existing = connection.execute(
250
+ """
251
+ SELECT * FROM prompt_queue_items
252
+ WHERE session_id = ? AND idempotency_key = ?
253
+ """,
254
+ (session_id, idempotency_key),
255
+ ).fetchone()
256
+ if existing is None:
257
+ raise PromptQueueStorageError("Could not persist queued prompt.") from None
258
+ if existing["payload_sha256"] != payload_sha256:
259
+ raise PromptIdempotencyConflict(
260
+ "Idempotency key was already used for a different prompt."
261
+ ) from None
262
+ return EnqueueResult(item=_item_from_row(existing), created=False)
263
+
264
+ row = connection.execute(
265
+ "SELECT * FROM prompt_queue_items WHERE prompt_id = ?", (prompt_id,)
266
+ ).fetchone()
267
+ if row is None: # defensive; insert and lookup share a transaction
268
+ raise PromptQueueStorageError("Could not load queued prompt.")
269
+ item = _item_from_row(row, payload=normalized_payload)
270
+ return EnqueueResult(item=item, created=True)
271
+
272
+ def get(self, *, session_id: str, prompt_id: str) -> PromptQueueItem | None:
273
+ session_id = _validate_identifier(session_id, "session", max_length=256)
274
+ prompt_id = _validate_identifier(prompt_id, "prompt", max_length=256)
275
+ with self._connection() as connection:
276
+ row = connection.execute(
277
+ """
278
+ SELECT * FROM prompt_queue_items
279
+ WHERE session_id = ? AND prompt_id = ?
280
+ """,
281
+ (session_id, prompt_id),
282
+ ).fetchone()
283
+ return None if row is None else _item_from_row(row)
284
+
285
+ def list(
286
+ self,
287
+ *,
288
+ session_id: str,
289
+ states: Iterable[PromptState | str] | None = None,
290
+ limit: int = 100,
291
+ after_sequence: int = 0,
292
+ ) -> list[PromptQueueItem]:
293
+ session_id = _validate_identifier(session_id, "session", max_length=256)
294
+ if isinstance(limit, bool) or not isinstance(limit, int):
295
+ raise PromptQueueValidationError("Queue list limit must be an integer.")
296
+ if limit < 1 or limit > self.config.max_list_limit:
297
+ raise PromptQueueValidationError("Queue list limit is outside the allowed range.")
298
+ if isinstance(after_sequence, bool) or not isinstance(after_sequence, int):
299
+ raise PromptQueueValidationError("Queue cursor must be an integer.")
300
+ if after_sequence < 0:
301
+ raise PromptQueueValidationError("Queue cursor cannot be negative.")
302
+
303
+ state_values = _normalize_states(states)
304
+ sql = "SELECT * FROM prompt_queue_items WHERE session_id = ? AND sequence > ?"
305
+ params: list[Any] = [session_id, after_sequence]
306
+ if state_values:
307
+ placeholders = ", ".join("?" for _ in state_values)
308
+ sql += f" AND state IN ({placeholders})"
309
+ params.extend(state_values)
310
+ sql += " ORDER BY sequence ASC LIMIT ?"
311
+ params.append(limit)
312
+ with self._connection() as connection:
313
+ rows = connection.execute(sql, params).fetchall()
314
+ return [_item_from_row(row) for row in rows]
315
+
316
+ def is_reclaimable(self, *, session_id: str, prompt_id: str) -> bool:
317
+ """Return whether a logical session may claim this outstanding prompt."""
318
+
319
+ session_id = _validate_identifier(session_id, "session", max_length=256)
320
+ prompt_id = _validate_identifier(prompt_id, "prompt", max_length=256)
321
+ now = self._now()
322
+ with self._connection() as connection:
323
+ row = connection.execute(
324
+ """
325
+ SELECT state, lease_expires_at FROM prompt_queue_items
326
+ WHERE session_id = ? AND prompt_id = ?
327
+ """,
328
+ (session_id, prompt_id),
329
+ ).fetchone()
330
+ if row is None:
331
+ return False
332
+ return row["state"] == PromptState.PENDING.value or (
333
+ row["state"] == PromptState.RUNNING.value
334
+ and row["lease_expires_at"] is not None
335
+ and float(row["lease_expires_at"]) <= now
336
+ )
337
+
338
+ def claim_next(
339
+ self,
340
+ *,
341
+ session_id: str,
342
+ owner_id: str,
343
+ lease_seconds: float = DEFAULT_LEASE_SECONDS,
344
+ ) -> PromptLease | None:
345
+ """Atomically claim the oldest eligible prompt for a session.
346
+
347
+ A non-expired running item blocks later prompts, preserving session
348
+ order. An expired running item is reclaimed with a new fencing token
349
+ and an incremented attempt count.
350
+ """
351
+
352
+ session_id = _validate_identifier(session_id, "session", max_length=256)
353
+ owner_id = _validate_identifier(owner_id, "owner", max_length=256)
354
+ lease_seconds = _validate_lease_seconds(lease_seconds)
355
+ now = self._now()
356
+ expires_at = now + lease_seconds
357
+ lease_token = uuid.uuid4().hex
358
+
359
+ with self._lock, self._transaction() as connection:
360
+ active = connection.execute(
361
+ """
362
+ SELECT 1 FROM prompt_queue_items
363
+ WHERE session_id = ? AND state = 'running'
364
+ AND lease_expires_at > ?
365
+ LIMIT 1
366
+ """,
367
+ (session_id, now),
368
+ ).fetchone()
369
+ if active is not None:
370
+ return None
371
+
372
+ row = connection.execute(
373
+ """
374
+ SELECT * FROM prompt_queue_items
375
+ WHERE session_id = ?
376
+ AND (
377
+ state = 'pending'
378
+ OR (state = 'running' AND lease_expires_at <= ?)
379
+ )
380
+ ORDER BY sequence ASC
381
+ LIMIT 1
382
+ """,
383
+ (session_id, now),
384
+ ).fetchone()
385
+ if row is None:
386
+ return None
387
+
388
+ connection.execute(
389
+ """
390
+ UPDATE prompt_queue_items
391
+ SET state = 'running', lease_token = ?, lease_owner = ?,
392
+ lease_session_id = ?, lease_expires_at = ?, attempts = attempts + 1,
393
+ updated_at = ?, terminal_at = NULL, error_code = NULL
394
+ WHERE sequence = ?
395
+ """,
396
+ (lease_token, owner_id, session_id, expires_at, now, row["sequence"]),
397
+ )
398
+ claimed = connection.execute(
399
+ "SELECT * FROM prompt_queue_items WHERE sequence = ?",
400
+ (row["sequence"],),
401
+ ).fetchone()
402
+ if claimed is None:
403
+ raise PromptQueueStorageError("Could not load claimed prompt.")
404
+ return PromptLease(
405
+ item=_item_from_row(claimed),
406
+ lease_token=lease_token,
407
+ owner_id=owner_id,
408
+ expires_at=expires_at,
409
+ )
410
+
411
+ def renew(
412
+ self,
413
+ *,
414
+ session_id: str,
415
+ prompt_id: str,
416
+ lease_token: str,
417
+ lease_seconds: float = DEFAULT_LEASE_SECONDS,
418
+ ) -> PromptLease:
419
+ session_id, prompt_id, lease_token = _validate_lease_identifiers(
420
+ session_id, prompt_id, lease_token
421
+ )
422
+ lease_seconds = _validate_lease_seconds(lease_seconds)
423
+ now = self._now()
424
+ expires_at = now + lease_seconds
425
+ with self._lock, self._transaction() as connection:
426
+ row = self._require_live_lease(
427
+ connection,
428
+ session_id=session_id,
429
+ prompt_id=prompt_id,
430
+ lease_token=lease_token,
431
+ now=now,
432
+ )
433
+ connection.execute(
434
+ """
435
+ UPDATE prompt_queue_items
436
+ SET lease_expires_at = ?, updated_at = ?
437
+ WHERE sequence = ?
438
+ """,
439
+ (expires_at, now, row["sequence"]),
440
+ )
441
+ renewed = connection.execute(
442
+ "SELECT * FROM prompt_queue_items WHERE sequence = ?", (row["sequence"],)
443
+ ).fetchone()
444
+ assert renewed is not None
445
+ return PromptLease(
446
+ item=_item_from_row(renewed),
447
+ lease_token=lease_token,
448
+ owner_id=renewed["lease_owner"],
449
+ expires_at=expires_at,
450
+ )
451
+
452
+ def complete(self, *, session_id: str, prompt_id: str, lease_token: str) -> PromptQueueItem:
453
+ return self._finish(
454
+ session_id=session_id,
455
+ prompt_id=prompt_id,
456
+ lease_token=lease_token,
457
+ state=PromptState.COMPLETED,
458
+ error_code=None,
459
+ )
460
+
461
+ def mark_execution_started(
462
+ self, *, session_id: str, prompt_id: str, lease_token: str
463
+ ) -> PromptQueueItem:
464
+ """Durably cross the at-most-once execution boundary for a live lease.
465
+
466
+ The marker belongs to the prompt identity rather than a SessionStore.
467
+ It therefore survives old-session to new-session rebinding. An expired
468
+ prompt carrying this marker must be failed as indeterminate instead of
469
+ being executed automatically again.
470
+ """
471
+
472
+ session_id, prompt_id, lease_token = _validate_lease_identifiers(
473
+ session_id, prompt_id, lease_token
474
+ )
475
+ now = self._now()
476
+ with self._lock, self._transaction() as connection:
477
+ row = self._require_live_lease(
478
+ connection,
479
+ session_id=session_id,
480
+ prompt_id=prompt_id,
481
+ lease_token=lease_token,
482
+ now=now,
483
+ )
484
+ if row["execution_started_at"] is not None:
485
+ raise PromptStateError("Prompt execution has already started.")
486
+ connection.execute(
487
+ """
488
+ UPDATE prompt_queue_items
489
+ SET execution_started_at = ?, updated_at = ?
490
+ WHERE sequence = ?
491
+ """,
492
+ (now, now, row["sequence"]),
493
+ )
494
+ started = connection.execute(
495
+ "SELECT * FROM prompt_queue_items WHERE sequence = ?", (row["sequence"],)
496
+ ).fetchone()
497
+ assert started is not None
498
+ return _item_from_row(started)
499
+
500
+ def fail(
501
+ self,
502
+ *,
503
+ session_id: str,
504
+ prompt_id: str,
505
+ lease_token: str,
506
+ error_code: str = "execution_failed",
507
+ ) -> PromptQueueItem:
508
+ if (
509
+ not isinstance(error_code, str)
510
+ or len(error_code) > 64
511
+ or not _ERROR_CODE_RE.fullmatch(error_code)
512
+ ):
513
+ raise PromptQueueValidationError("Prompt failure code is invalid.")
514
+ return self._finish(
515
+ session_id=session_id,
516
+ prompt_id=prompt_id,
517
+ lease_token=lease_token,
518
+ state=PromptState.FAILED,
519
+ error_code=error_code,
520
+ )
521
+
522
+ def release(self, *, session_id: str, prompt_id: str, lease_token: str) -> PromptQueueItem:
523
+ """Return a live claim to pending during a graceful bridge shutdown."""
524
+
525
+ session_id, prompt_id, lease_token = _validate_lease_identifiers(
526
+ session_id, prompt_id, lease_token
527
+ )
528
+ now = self._now()
529
+ with self._lock, self._transaction() as connection:
530
+ row = self._require_live_lease(
531
+ connection,
532
+ session_id=session_id,
533
+ prompt_id=prompt_id,
534
+ lease_token=lease_token,
535
+ now=now,
536
+ )
537
+ connection.execute(
538
+ """
539
+ UPDATE prompt_queue_items
540
+ SET state = 'pending', lease_token = NULL, lease_owner = NULL,
541
+ lease_session_id = NULL, lease_expires_at = NULL, updated_at = ?
542
+ WHERE sequence = ?
543
+ """,
544
+ (now, row["sequence"]),
545
+ )
546
+ released = connection.execute(
547
+ "SELECT * FROM prompt_queue_items WHERE sequence = ?", (row["sequence"],)
548
+ ).fetchone()
549
+ assert released is not None
550
+ return _item_from_row(released)
551
+
552
+ def delete_pending(self, *, session_id: str, prompt_id: str) -> PromptQueueItem:
553
+ """Cancel, but retain, a pending prompt for audit and idempotency."""
554
+
555
+ session_id = _validate_identifier(session_id, "session", max_length=256)
556
+ prompt_id = _validate_identifier(prompt_id, "prompt", max_length=256)
557
+ now = self._now()
558
+ with self._lock, self._transaction() as connection:
559
+ row = connection.execute(
560
+ """
561
+ SELECT * FROM prompt_queue_items
562
+ WHERE session_id = ? AND prompt_id = ?
563
+ """,
564
+ (session_id, prompt_id),
565
+ ).fetchone()
566
+ if row is None:
567
+ raise PromptNotFound("Queued prompt was not found.")
568
+ if row["state"] != PromptState.PENDING.value:
569
+ raise PromptStateError("Only pending prompts can be deleted.")
570
+ connection.execute(
571
+ """
572
+ UPDATE prompt_queue_items
573
+ SET state = 'cancelled', updated_at = ?, terminal_at = ?
574
+ WHERE sequence = ?
575
+ """,
576
+ (now, now, row["sequence"]),
577
+ )
578
+ cancelled = connection.execute(
579
+ "SELECT * FROM prompt_queue_items WHERE sequence = ?", (row["sequence"],)
580
+ ).fetchone()
581
+ assert cancelled is not None
582
+ return _item_from_row(cancelled)
583
+
584
+ def cancel_pending_for_session(self, *, session_id: str) -> int:
585
+ """Atomically cancel every pending prompt for an explicit session."""
586
+
587
+ session_id = _validate_identifier(session_id, "session", max_length=256)
588
+ now = self._now()
589
+ with self._lock, self._transaction() as connection:
590
+ cursor = connection.execute(
591
+ """
592
+ UPDATE prompt_queue_items
593
+ SET state = 'cancelled', updated_at = ?, terminal_at = ?
594
+ WHERE session_id = ? AND state = 'pending'
595
+ """,
596
+ (now, now, session_id),
597
+ )
598
+ return max(0, int(cursor.rowcount))
599
+
600
+ def cancel_claimed(
601
+ self, *, session_id: str, prompt_id: str, lease_token: str
602
+ ) -> PromptQueueItem:
603
+ """Mark work cancelled after its fenced executor has stopped."""
604
+
605
+ return self._finish(
606
+ session_id=session_id,
607
+ prompt_id=prompt_id,
608
+ lease_token=lease_token,
609
+ state=PromptState.CANCELLED,
610
+ error_code="cancelled",
611
+ )
612
+
613
+ def rebind_recoverable(
614
+ self, *, source_session_id: str, target_session_id: str
615
+ ) -> dict[str, int]:
616
+ """Rebind outstanding work while preserving a live executor's fence.
617
+
618
+ ``session_id`` is the logical queue owner shown to the resumed client;
619
+ ``lease_session_id`` remains the session accepted from the executor
620
+ holding the opaque lease. Moving a live row is therefore safe: it blocks
621
+ later target-session prompts, while only the old fenced worker can renew
622
+ or finish it. Expired rows become pending under the new session.
623
+ """
624
+
625
+ source = _validate_identifier(source_session_id, "source session", max_length=256)
626
+ target = _validate_identifier(target_session_id, "target session", max_length=256)
627
+ if source == target:
628
+ return {"pending": 0, "recovered": 0, "active": 0}
629
+ now = self._now()
630
+ with self._lock, self._transaction() as connection:
631
+ conflict = connection.execute(
632
+ """
633
+ SELECT 1
634
+ FROM prompt_queue_items source
635
+ JOIN prompt_queue_items target
636
+ ON target.session_id = ?
637
+ AND target.idempotency_key = source.idempotency_key
638
+ WHERE source.session_id = ?
639
+ AND source.state IN ('pending', 'running')
640
+ LIMIT 1
641
+ """,
642
+ (target, source),
643
+ ).fetchone()
644
+ if conflict is not None:
645
+ raise PromptIdempotencyConflict(
646
+ "Recoverable session contains a conflicting queued prompt."
647
+ )
648
+ target_running = connection.execute(
649
+ """
650
+ SELECT 1 FROM prompt_queue_items
651
+ WHERE session_id = ? AND state = 'running'
652
+ LIMIT 1
653
+ """,
654
+ (target,),
655
+ ).fetchone()
656
+ source_running = connection.execute(
657
+ """
658
+ SELECT 1 FROM prompt_queue_items
659
+ WHERE session_id = ? AND state = 'running'
660
+ LIMIT 1
661
+ """,
662
+ (source,),
663
+ ).fetchone()
664
+ if target_running is not None and source_running is not None:
665
+ raise PromptStateError("Target session already has active queued work.")
666
+
667
+ pending = connection.execute(
668
+ """
669
+ UPDATE prompt_queue_items
670
+ SET session_id = ?, updated_at = ?
671
+ WHERE session_id = ? AND state = 'pending'
672
+ """,
673
+ (target, now, source),
674
+ ).rowcount
675
+ recovered = connection.execute(
676
+ """
677
+ UPDATE prompt_queue_items
678
+ SET session_id = ?, state = 'pending', lease_token = NULL,
679
+ lease_owner = NULL, lease_session_id = NULL,
680
+ lease_expires_at = NULL, updated_at = ?
681
+ WHERE session_id = ? AND state = 'running' AND lease_expires_at <= ?
682
+ """,
683
+ (target, now, source, now),
684
+ ).rowcount
685
+ active = connection.execute(
686
+ """
687
+ UPDATE prompt_queue_items
688
+ SET session_id = ?, updated_at = ?
689
+ WHERE session_id = ? AND state = 'running' AND lease_expires_at > ?
690
+ """,
691
+ (target, now, source, now),
692
+ ).rowcount
693
+ return {
694
+ "pending": max(0, pending),
695
+ "recovered": max(0, recovered),
696
+ "active": max(0, active),
697
+ }
698
+
699
+ def _finish(
700
+ self,
701
+ *,
702
+ session_id: str,
703
+ prompt_id: str,
704
+ lease_token: str,
705
+ state: PromptState,
706
+ error_code: str | None,
707
+ ) -> PromptQueueItem:
708
+ session_id, prompt_id, lease_token = _validate_lease_identifiers(
709
+ session_id, prompt_id, lease_token
710
+ )
711
+ now = self._now()
712
+ with self._lock, self._transaction() as connection:
713
+ row = self._require_live_lease(
714
+ connection,
715
+ session_id=session_id,
716
+ prompt_id=prompt_id,
717
+ lease_token=lease_token,
718
+ now=now,
719
+ )
720
+ connection.execute(
721
+ """
722
+ UPDATE prompt_queue_items
723
+ SET state = ?, lease_token = NULL, lease_owner = NULL,
724
+ lease_session_id = NULL, lease_expires_at = NULL,
725
+ updated_at = ?, terminal_at = ?,
726
+ error_code = ?
727
+ WHERE sequence = ?
728
+ """,
729
+ (state.value, now, now, error_code, row["sequence"]),
730
+ )
731
+ finished = connection.execute(
732
+ "SELECT * FROM prompt_queue_items WHERE sequence = ?", (row["sequence"],)
733
+ ).fetchone()
734
+ assert finished is not None
735
+ return _item_from_row(finished)
736
+
737
+ def _require_live_lease(
738
+ self,
739
+ connection: sqlite3.Connection,
740
+ *,
741
+ session_id: str,
742
+ prompt_id: str,
743
+ lease_token: str,
744
+ now: float,
745
+ ) -> sqlite3.Row:
746
+ row = connection.execute(
747
+ """
748
+ SELECT * FROM prompt_queue_items
749
+ WHERE prompt_id = ?
750
+ """,
751
+ (prompt_id,),
752
+ ).fetchone()
753
+ if row is None:
754
+ raise PromptNotFound("Queued prompt was not found.")
755
+ if (
756
+ session_id not in {row["session_id"], row["lease_session_id"]}
757
+ or row["state"] != PromptState.RUNNING.value
758
+ or row["lease_token"] != lease_token
759
+ or row["lease_expires_at"] is None
760
+ or row["lease_expires_at"] <= now
761
+ ):
762
+ raise PromptLeaseLost("Prompt lease is no longer valid.")
763
+ return row
764
+
765
+ def _now(self) -> float:
766
+ try:
767
+ now = float(self._clock())
768
+ except (TypeError, ValueError, OverflowError) as exc:
769
+ raise PromptQueueValidationError("Queue clock returned an invalid value.") from exc
770
+ if not math.isfinite(now) or now < 0:
771
+ raise PromptQueueValidationError("Queue clock returned an invalid value.")
772
+ return now
773
+
774
+ def _prepare_storage(self) -> None:
775
+ try:
776
+ self.path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
777
+ _restrict_permissions(self.path.parent, 0o700)
778
+ with self._connection() as connection:
779
+ version = int(connection.execute("PRAGMA user_version").fetchone()[0])
780
+ if version not in (0, 1, SCHEMA_VERSION):
781
+ raise PromptQueueStorageError(
782
+ "Prompt queue database schema is not supported by this version."
783
+ )
784
+ connection.execute("PRAGMA journal_mode = WAL")
785
+ connection.execute("PRAGMA synchronous = FULL")
786
+ connection.executescript(
787
+ """
788
+ CREATE TABLE IF NOT EXISTS prompt_queue_items (
789
+ sequence INTEGER PRIMARY KEY AUTOINCREMENT,
790
+ prompt_id TEXT NOT NULL UNIQUE,
791
+ session_id TEXT NOT NULL,
792
+ idempotency_key TEXT NOT NULL,
793
+ payload_json TEXT NOT NULL,
794
+ payload_sha256 TEXT NOT NULL,
795
+ state TEXT NOT NULL CHECK (
796
+ state IN ('pending', 'running', 'completed', 'cancelled', 'failed')
797
+ ),
798
+ created_at REAL NOT NULL,
799
+ updated_at REAL NOT NULL,
800
+ attempts INTEGER NOT NULL DEFAULT 0 CHECK (attempts >= 0),
801
+ lease_token TEXT,
802
+ lease_owner TEXT,
803
+ lease_session_id TEXT,
804
+ lease_expires_at REAL,
805
+ execution_started_at REAL,
806
+ terminal_at REAL,
807
+ error_code TEXT,
808
+ UNIQUE (session_id, idempotency_key),
809
+ CHECK (
810
+ (state = 'running' AND lease_token IS NOT NULL
811
+ AND lease_owner IS NOT NULL AND lease_expires_at IS NOT NULL)
812
+ OR
813
+ (state != 'running' AND lease_token IS NULL
814
+ AND lease_owner IS NULL AND lease_expires_at IS NULL)
815
+ )
816
+ );
817
+ CREATE INDEX IF NOT EXISTS prompt_queue_session_order
818
+ ON prompt_queue_items (session_id, sequence);
819
+ CREATE INDEX IF NOT EXISTS prompt_queue_session_state_order
820
+ ON prompt_queue_items (session_id, state, sequence);
821
+ CREATE UNIQUE INDEX IF NOT EXISTS prompt_queue_one_running_per_session
822
+ ON prompt_queue_items (session_id) WHERE state = 'running';
823
+ """
824
+ )
825
+ columns = {
826
+ str(row["name"])
827
+ for row in connection.execute("PRAGMA table_info(prompt_queue_items)")
828
+ }
829
+ if "lease_session_id" not in columns:
830
+ connection.execute(
831
+ "ALTER TABLE prompt_queue_items ADD COLUMN lease_session_id TEXT"
832
+ )
833
+ if "execution_started_at" not in columns:
834
+ connection.execute(
835
+ "ALTER TABLE prompt_queue_items ADD COLUMN execution_started_at REAL"
836
+ )
837
+ connection.execute(
838
+ """
839
+ UPDATE prompt_queue_items
840
+ SET lease_session_id = session_id
841
+ WHERE state = 'running' AND lease_session_id IS NULL
842
+ """
843
+ )
844
+ connection.execute(f"PRAGMA user_version = {SCHEMA_VERSION}")
845
+ _restrict_permissions(self.path, 0o600)
846
+ except PromptQueueError:
847
+ raise
848
+ except (OSError, sqlite3.Error):
849
+ raise PromptQueueStorageError("Could not initialize prompt queue storage.") from None
850
+
851
+ @contextmanager
852
+ def _connection(self) -> Any:
853
+ connection: sqlite3.Connection | None = None
854
+ try:
855
+ connection = sqlite3.connect(
856
+ self.path,
857
+ timeout=self.config.busy_timeout_seconds,
858
+ isolation_level=None,
859
+ )
860
+ connection.row_factory = sqlite3.Row
861
+ busy_timeout_ms = int(self.config.busy_timeout_seconds * 1000)
862
+ connection.execute(f"PRAGMA busy_timeout = {busy_timeout_ms}")
863
+ connection.execute("PRAGMA foreign_keys = ON")
864
+ except sqlite3.Error:
865
+ if connection is not None:
866
+ try:
867
+ connection.close()
868
+ except sqlite3.Error:
869
+ pass
870
+ raise PromptQueueStorageError("Could not open prompt queue storage.") from None
871
+ try:
872
+ yield connection
873
+ finally:
874
+ try:
875
+ connection.close()
876
+ except sqlite3.Error:
877
+ pass
878
+
879
+ @contextmanager
880
+ def _transaction(self) -> Any:
881
+ with self._connection() as connection:
882
+ try:
883
+ connection.execute("BEGIN IMMEDIATE")
884
+ yield connection
885
+ connection.execute("COMMIT")
886
+ except PromptQueueError:
887
+ _rollback_quietly(connection)
888
+ raise
889
+ except sqlite3.Error:
890
+ _rollback_quietly(connection)
891
+ raise PromptQueueStorageError("Prompt queue operation failed.") from None
892
+ except BaseException:
893
+ _rollback_quietly(connection)
894
+ raise
895
+
896
+
897
+ def _serialize_payload(
898
+ payload: Mapping[str, Any], config: PromptQueueConfig
899
+ ) -> tuple[str, dict[str, JsonValue]]:
900
+ if not isinstance(payload, Mapping):
901
+ raise PromptQueueValidationError("Prompt payload must be a JSON object.")
902
+ normalized = _normalize_json(payload, config=config)
903
+ if not isinstance(normalized, dict): # Mapping always normalizes to a dict
904
+ raise PromptQueueValidationError("Prompt payload must be a JSON object.")
905
+ try:
906
+ serialized = json.dumps(
907
+ normalized,
908
+ ensure_ascii=False,
909
+ allow_nan=False,
910
+ separators=(",", ":"),
911
+ sort_keys=True,
912
+ )
913
+ except (TypeError, ValueError):
914
+ raise PromptQueueValidationError("Prompt payload is not valid JSON.") from None
915
+ if len(serialized.encode("utf-8")) > config.max_payload_bytes:
916
+ raise PromptQueueValidationError("Prompt payload exceeds the queue size limit.")
917
+ return serialized, normalized
918
+
919
+
920
+ def _normalize_json(value: Any, *, config: PromptQueueConfig) -> JsonValue:
921
+ nodes = 0
922
+
923
+ def visit(item: Any, depth: int) -> JsonValue:
924
+ nonlocal nodes
925
+ nodes += 1
926
+ if nodes > config.max_json_nodes:
927
+ raise PromptQueueValidationError("Prompt payload has too many JSON values.")
928
+ if depth > config.max_json_depth:
929
+ raise PromptQueueValidationError("Prompt payload is nested too deeply.")
930
+ if item is None or isinstance(item, (bool, str)):
931
+ return item
932
+ if isinstance(item, int):
933
+ return item
934
+ if isinstance(item, float):
935
+ if not math.isfinite(item):
936
+ raise PromptQueueValidationError("Prompt payload contains an invalid number.")
937
+ return item
938
+ if isinstance(item, Mapping):
939
+ result: dict[str, JsonValue] = {}
940
+ for key, child in item.items():
941
+ if not isinstance(key, str):
942
+ raise PromptQueueValidationError("Prompt payload object keys must be strings.")
943
+ if len(key) > 512 or _contains_control_character(key):
944
+ raise PromptQueueValidationError("Prompt payload object key is invalid.")
945
+ result[key] = visit(child, depth + 1)
946
+ return result
947
+ if isinstance(item, (list, tuple)):
948
+ return [visit(child, depth + 1) for child in item]
949
+ raise PromptQueueValidationError("Prompt payload contains a non-JSON value.")
950
+
951
+ return visit(value, 0)
952
+
953
+
954
+ def _item_from_row(
955
+ row: sqlite3.Row, *, payload: dict[str, JsonValue] | None = None
956
+ ) -> PromptQueueItem:
957
+ if payload is None:
958
+ try:
959
+ decoded = json.loads(row["payload_json"])
960
+ except (TypeError, ValueError, json.JSONDecodeError):
961
+ raise PromptQueueStorageError("Stored prompt payload is invalid.") from None
962
+ if not isinstance(decoded, dict):
963
+ raise PromptQueueStorageError("Stored prompt payload is invalid.")
964
+ payload = decoded
965
+ return PromptQueueItem(
966
+ prompt_id=row["prompt_id"],
967
+ sequence=int(row["sequence"]),
968
+ session_id=row["session_id"],
969
+ idempotency_key=row["idempotency_key"],
970
+ payload=payload,
971
+ state=PromptState(row["state"]),
972
+ created_at=float(row["created_at"]),
973
+ updated_at=float(row["updated_at"]),
974
+ attempts=int(row["attempts"]),
975
+ lease_expires_at=(
976
+ None if row["lease_expires_at"] is None else float(row["lease_expires_at"])
977
+ ),
978
+ execution_started_at=(
979
+ None if row["execution_started_at"] is None else float(row["execution_started_at"])
980
+ ),
981
+ terminal_at=None if row["terminal_at"] is None else float(row["terminal_at"]),
982
+ error_code=row["error_code"],
983
+ )
984
+
985
+
986
+ def _normalize_states(states: Iterable[PromptState | str] | None) -> list[str]:
987
+ if states is None:
988
+ return []
989
+ if isinstance(states, (str, bytes)):
990
+ raise PromptQueueValidationError("Queue states must be a collection.")
991
+ normalized: list[str] = []
992
+ try:
993
+ for state in states:
994
+ normalized.append(PromptState(state).value)
995
+ except (TypeError, ValueError):
996
+ raise PromptQueueValidationError("Queue state filter is invalid.") from None
997
+ return list(dict.fromkeys(normalized))
998
+
999
+
1000
+ def _validate_identifier(value: Any, kind: str, *, max_length: int) -> str:
1001
+ if not isinstance(value, str):
1002
+ raise PromptQueueValidationError(f"Prompt {kind} identifier must be a string.")
1003
+ if not value or len(value) > max_length or not _IDENTIFIER_RE.fullmatch(value):
1004
+ raise PromptQueueValidationError(f"Prompt {kind} identifier is invalid.")
1005
+ return value
1006
+
1007
+
1008
+ def _validate_lease_identifiers(
1009
+ session_id: Any, prompt_id: Any, lease_token: Any
1010
+ ) -> tuple[str, str, str]:
1011
+ return (
1012
+ _validate_identifier(session_id, "session", max_length=256),
1013
+ _validate_identifier(prompt_id, "prompt", max_length=256),
1014
+ _validate_identifier(lease_token, "lease", max_length=256),
1015
+ )
1016
+
1017
+
1018
+ def _validate_lease_seconds(value: Any) -> float:
1019
+ if isinstance(value, bool):
1020
+ raise PromptQueueValidationError("Prompt lease duration is invalid.")
1021
+ try:
1022
+ parsed = float(value)
1023
+ except (TypeError, ValueError, OverflowError):
1024
+ raise PromptQueueValidationError("Prompt lease duration is invalid.") from None
1025
+ if not math.isfinite(parsed) or parsed < MIN_LEASE_SECONDS or parsed > MAX_LEASE_SECONDS:
1026
+ raise PromptQueueValidationError("Prompt lease duration is outside the allowed range.")
1027
+ return parsed
1028
+
1029
+
1030
+ def _contains_control_character(value: str) -> bool:
1031
+ return any(ord(char) < 32 or ord(char) == 127 for char in value)
1032
+
1033
+
1034
+ def _restrict_permissions(path: Path, mode: int) -> None:
1035
+ if os.name == "nt" or not path.exists():
1036
+ return
1037
+ try:
1038
+ path.chmod(mode)
1039
+ except OSError:
1040
+ # Directory ownership/ACLs may be managed by the host. SQLite remains
1041
+ # usable; deployments can enforce their own ACL policy.
1042
+ pass
1043
+
1044
+
1045
+ def _rollback_quietly(connection: sqlite3.Connection) -> None:
1046
+ try:
1047
+ connection.execute("ROLLBACK")
1048
+ except sqlite3.Error:
1049
+ pass
1050
+
1051
+
1052
+ __all__ = [
1053
+ "DEFAULT_LEASE_SECONDS",
1054
+ "DurablePromptQueue",
1055
+ "EnqueueResult",
1056
+ "PromptIdempotencyConflict",
1057
+ "PromptLease",
1058
+ "PromptLeaseLost",
1059
+ "PromptNotFound",
1060
+ "PromptQueueCapacityError",
1061
+ "PromptQueueConfig",
1062
+ "PromptQueueError",
1063
+ "PromptQueueItem",
1064
+ "PromptQueueStorageError",
1065
+ "PromptQueueValidationError",
1066
+ "PromptState",
1067
+ "PromptStateError",
1068
+ "TERMINAL_STATES",
1069
+ "default_prompt_queue_path",
1070
+ ]