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,737 @@
1
+ """Durable exactly-once acceptance for IDE Forge plan requests.
2
+
3
+ Only hashes of idempotency keys and request payloads are persisted. The
4
+ instruction itself is deliberately never written to this database. Workers
5
+ hold short, renewable, hashed leases so a stale process cannot publish a
6
+ result after ownership has moved. An expired *running* lease is terminally
7
+ classified as indeterminate: callers must not automatically execute it again.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import hashlib
13
+ import json
14
+ import math
15
+ import os
16
+ import re
17
+ import secrets
18
+ import sqlite3
19
+ import threading
20
+ import time
21
+ import uuid
22
+ from collections.abc import Callable, Mapping
23
+ from contextlib import contextmanager, suppress
24
+ from dataclasses import dataclass, field
25
+ from enum import Enum
26
+ from pathlib import Path
27
+ from typing import Any
28
+
29
+ from ..branding import canonical_user_data_dir, env_get
30
+
31
+ SCHEMA_VERSION = 1
32
+ DEFAULT_LEASE_SECONDS = 30.0
33
+ MIN_LEASE_SECONDS = 1.0
34
+ MAX_LEASE_SECONDS = 15 * 60.0
35
+ _IDENTIFIER_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:@/-]*$")
36
+ _ERROR_CODE_RE = re.compile(r"^[a-z][a-z0-9_.-]*$")
37
+
38
+
39
+ class ForgeRequestState(str, Enum):
40
+ QUEUED = "queued"
41
+ RUNNING = "running"
42
+ COMPLETED = "completed"
43
+ FAILED = "failed"
44
+ CANCELLED = "cancelled"
45
+ INDETERMINATE = "indeterminate"
46
+
47
+
48
+ TERMINAL_STATES = frozenset(
49
+ {
50
+ ForgeRequestState.COMPLETED,
51
+ ForgeRequestState.FAILED,
52
+ ForgeRequestState.CANCELLED,
53
+ ForgeRequestState.INDETERMINATE,
54
+ }
55
+ )
56
+
57
+
58
+ class ForgeRequestLedgerError(RuntimeError):
59
+ """Base error whose message is safe to surface or log."""
60
+
61
+
62
+ class ForgeRequestValidationError(ForgeRequestLedgerError, ValueError):
63
+ pass
64
+
65
+
66
+ class ForgeRequestIdempotencyConflict(ForgeRequestLedgerError):
67
+ pass
68
+
69
+
70
+ class ForgeRequestNotFound(ForgeRequestLedgerError):
71
+ pass
72
+
73
+
74
+ class ForgeRequestLeaseLost(ForgeRequestLedgerError):
75
+ pass
76
+
77
+
78
+ class ForgeRequestStorageError(ForgeRequestLedgerError):
79
+ pass
80
+
81
+
82
+ @dataclass(frozen=True, slots=True)
83
+ class ForgeRequestLedgerConfig:
84
+ busy_timeout_seconds: float = 10.0
85
+ lease_seconds: float = DEFAULT_LEASE_SECONDS
86
+ max_records: int = 50_000
87
+ terminal_retention_seconds: float = 90 * 24 * 60 * 60.0
88
+
89
+ def __post_init__(self) -> None:
90
+ _duration(self.busy_timeout_seconds, maximum=60.0, label="busy timeout")
91
+ _duration(self.lease_seconds, maximum=MAX_LEASE_SECONDS, label="lease")
92
+ if isinstance(self.max_records, bool) or not isinstance(self.max_records, int):
93
+ raise ForgeRequestValidationError("Forge request record limit must be an integer.")
94
+ if self.max_records < 100 or self.max_records > 1_000_000:
95
+ raise ForgeRequestValidationError(
96
+ "Forge request record limit is outside the allowed range."
97
+ )
98
+ _duration(
99
+ self.terminal_retention_seconds,
100
+ maximum=10 * 365 * 24 * 60 * 60.0,
101
+ label="terminal retention",
102
+ )
103
+
104
+
105
+ @dataclass(frozen=True, slots=True)
106
+ class ForgeRequestRecord:
107
+ job_id: str
108
+ session_id: str
109
+ workspace_root: Path = field(repr=False)
110
+ state: ForgeRequestState
111
+ created_at: float
112
+ updated_at: float
113
+ started_at: float | None = None
114
+ terminal_at: float | None = None
115
+ plan_id: str | None = None
116
+ error_code: str | None = None
117
+ attempts: int = 0
118
+
119
+
120
+ @dataclass(frozen=True, slots=True)
121
+ class ForgeDispatchLease:
122
+ job_id: str
123
+ generation: int
124
+ expires_at: float
125
+ token: str = field(repr=False)
126
+
127
+
128
+ @dataclass(frozen=True, slots=True)
129
+ class ForgeWorkerLease:
130
+ job_id: str
131
+ generation: int
132
+ expires_at: float
133
+ token: str = field(repr=False)
134
+
135
+
136
+ @dataclass(frozen=True, slots=True)
137
+ class ForgeRequestAcceptance:
138
+ record: ForgeRequestRecord
139
+ created: bool
140
+ dispatch_lease: ForgeDispatchLease | None = field(default=None, repr=False)
141
+
142
+
143
+ def default_forge_request_ledger_path() -> Path:
144
+ """Return the private user-data path, outside workspaces by default."""
145
+
146
+ override = str(env_get("ALYSIS_DATA_DIR") or "").strip()
147
+ data_dir = Path(override).expanduser() if override else canonical_user_data_dir()
148
+ return data_dir / "ide" / "forge-plan-requests.sqlite3"
149
+
150
+
151
+ class DurableForgeRequestLedger:
152
+ """SQLite-backed Forge request acceptance and fenced worker ownership."""
153
+
154
+ def __init__(
155
+ self,
156
+ path: str | os.PathLike[str] | None = None,
157
+ *,
158
+ config: ForgeRequestLedgerConfig | None = None,
159
+ clock: Callable[[], float] = time.time,
160
+ job_id_factory: Callable[[], str] | None = None,
161
+ lease_token_factory: Callable[[], str] | None = None,
162
+ ) -> None:
163
+ self.path = Path(path) if path is not None else default_forge_request_ledger_path()
164
+ self.config = config or ForgeRequestLedgerConfig()
165
+ self._clock = clock
166
+ self._job_id_factory = job_id_factory or (lambda: f"job_{uuid.uuid4().hex}")
167
+ self._lease_token_factory = lease_token_factory or (
168
+ lambda: f"lease_{secrets.token_urlsafe(32)}"
169
+ )
170
+ self._lock = threading.RLock()
171
+ self._prepare_storage()
172
+
173
+ def accept(
174
+ self,
175
+ *,
176
+ workspace_root: str | os.PathLike[str],
177
+ session_id: str,
178
+ idempotency_key: str,
179
+ payload: Mapping[str, Any],
180
+ ) -> ForgeRequestAcceptance:
181
+ """Durably accept a request, or return its stable prior job.
182
+
183
+ A stale queued dispatch can be safely fenced and redispatched because a
184
+ worker must transition it to ``running`` before invoking the planner.
185
+ A stale running request is never redispatched.
186
+ """
187
+
188
+ workspace = _workspace(workspace_root)
189
+ session = _identifier(session_id, "session", maximum=256)
190
+ key = _identifier(idempotency_key, "idempotency", maximum=256)
191
+ workspace_key = _digest(os.path.normcase(os.fspath(workspace)))
192
+ idempotency_hash = _digest(key)
193
+ payload_hash = _payload_digest(payload)
194
+ now = self._now()
195
+ duration = self.config.lease_seconds
196
+
197
+ with self._lock, self._transaction() as connection:
198
+ row = connection.execute(
199
+ """
200
+ SELECT * FROM ide_forge_plan_requests
201
+ WHERE workspace_sha256 = ? AND idempotency_sha256 = ?
202
+ """,
203
+ (workspace_key, idempotency_hash),
204
+ ).fetchone()
205
+ if row is not None:
206
+ if str(row["payload_sha256"]) != payload_hash:
207
+ raise ForgeRequestIdempotencyConflict(
208
+ "Idempotency key was already used for a different Forge plan request."
209
+ )
210
+ row = self._reconcile_expired_running(connection, row=row, now=now)
211
+ state = ForgeRequestState(str(row["state"]))
212
+ connection.execute(
213
+ """
214
+ UPDATE ide_forge_plan_requests
215
+ SET session_id = ?, updated_at = CASE
216
+ WHEN session_id = ? THEN updated_at ELSE ? END
217
+ WHERE job_id = ?
218
+ """,
219
+ (session, session, now, row["job_id"]),
220
+ )
221
+ row = self._row_by_job(connection, str(row["job_id"]))
222
+ if (
223
+ state is ForgeRequestState.QUEUED
224
+ and float(row["lease_expires_at"] or 0.0) <= now
225
+ ):
226
+ lease = self._replace_dispatch_lease(connection, row=row, now=now)
227
+ return ForgeRequestAcceptance(
228
+ record=self._record(self._row_by_job(connection, lease.job_id)),
229
+ created=False,
230
+ dispatch_lease=lease,
231
+ )
232
+ return ForgeRequestAcceptance(record=self._record(row), created=False)
233
+
234
+ job_id = _identifier(self._job_id_factory(), "generated job", maximum=128)
235
+ self._prune_for_insert(connection, now=now)
236
+ token = _lease_token(self._lease_token_factory())
237
+ generation = 1
238
+ expires_at = now + duration
239
+ try:
240
+ connection.execute(
241
+ """
242
+ INSERT INTO ide_forge_plan_requests (
243
+ job_id, workspace_sha256, workspace_root, session_id,
244
+ idempotency_sha256, payload_sha256, state, generation,
245
+ attempts, lease_token_sha256, lease_expires_at,
246
+ created_at, updated_at
247
+ ) VALUES (?, ?, ?, ?, ?, ?, 'queued', ?, 0, ?, ?, ?, ?)
248
+ """,
249
+ (
250
+ job_id,
251
+ workspace_key,
252
+ os.fspath(workspace),
253
+ session,
254
+ idempotency_hash,
255
+ payload_hash,
256
+ generation,
257
+ _digest(token),
258
+ expires_at,
259
+ now,
260
+ now,
261
+ ),
262
+ )
263
+ except sqlite3.IntegrityError:
264
+ raise ForgeRequestStorageError(
265
+ "Could not allocate a durable Forge plan request."
266
+ ) from None
267
+ row = self._row_by_job(connection, job_id)
268
+ return ForgeRequestAcceptance(
269
+ record=self._record(row),
270
+ created=True,
271
+ dispatch_lease=ForgeDispatchLease(
272
+ job_id=job_id,
273
+ generation=generation,
274
+ expires_at=expires_at,
275
+ token=token,
276
+ ),
277
+ )
278
+
279
+ def begin(self, lease: ForgeDispatchLease) -> ForgeWorkerLease:
280
+ """Claim execution before the planner is invoked."""
281
+
282
+ now = self._now()
283
+ expires_at = now + self.config.lease_seconds
284
+ with self._lock, self._transaction() as connection:
285
+ row = self._require_lease(
286
+ connection,
287
+ job_id=lease.job_id,
288
+ generation=lease.generation,
289
+ token=lease.token,
290
+ expected_state=ForgeRequestState.QUEUED,
291
+ now=now,
292
+ )
293
+ connection.execute(
294
+ """
295
+ UPDATE ide_forge_plan_requests
296
+ SET state = 'running', attempts = attempts + 1,
297
+ started_at = COALESCE(started_at, ?), updated_at = ?,
298
+ lease_expires_at = ?
299
+ WHERE job_id = ?
300
+ """,
301
+ (now, now, expires_at, row["job_id"]),
302
+ )
303
+ return ForgeWorkerLease(
304
+ job_id=lease.job_id,
305
+ generation=lease.generation,
306
+ expires_at=expires_at,
307
+ token=lease.token,
308
+ )
309
+
310
+ def renew(self, lease: ForgeWorkerLease) -> ForgeWorkerLease:
311
+ now = self._now()
312
+ expires_at = now + self.config.lease_seconds
313
+ with self._lock, self._transaction() as connection:
314
+ self._require_lease(
315
+ connection,
316
+ job_id=lease.job_id,
317
+ generation=lease.generation,
318
+ token=lease.token,
319
+ expected_state=ForgeRequestState.RUNNING,
320
+ now=now,
321
+ )
322
+ connection.execute(
323
+ """
324
+ UPDATE ide_forge_plan_requests
325
+ SET lease_expires_at = ?, updated_at = ? WHERE job_id = ?
326
+ """,
327
+ (expires_at, now, lease.job_id),
328
+ )
329
+ return ForgeWorkerLease(
330
+ job_id=lease.job_id,
331
+ generation=lease.generation,
332
+ expires_at=expires_at,
333
+ token=lease.token,
334
+ )
335
+
336
+ def complete(self, lease: ForgeWorkerLease, *, plan_id: str) -> ForgeRequestRecord:
337
+ clean_plan_id = _identifier(plan_id, "plan", maximum=128)
338
+ return self._finish(
339
+ lease,
340
+ state=ForgeRequestState.COMPLETED,
341
+ plan_id=clean_plan_id,
342
+ error_code=None,
343
+ )
344
+
345
+ def fail(self, lease: ForgeWorkerLease, *, error_code: str) -> ForgeRequestRecord:
346
+ return self._finish(
347
+ lease,
348
+ state=ForgeRequestState.FAILED,
349
+ plan_id=None,
350
+ error_code=_error_code(error_code),
351
+ )
352
+
353
+ def cancel(self, lease: ForgeWorkerLease, *, error_code: str) -> ForgeRequestRecord:
354
+ return self._finish(
355
+ lease,
356
+ state=ForgeRequestState.CANCELLED,
357
+ plan_id=None,
358
+ error_code=_error_code(error_code),
359
+ )
360
+
361
+ def reject(self, lease: ForgeDispatchLease, *, error_code: str) -> ForgeRequestRecord:
362
+ """Terminally reject accepted work that cannot be dispatched."""
363
+
364
+ now = self._now()
365
+ with self._lock, self._transaction() as connection:
366
+ self._require_lease(
367
+ connection,
368
+ job_id=lease.job_id,
369
+ generation=lease.generation,
370
+ token=lease.token,
371
+ expected_state=ForgeRequestState.QUEUED,
372
+ now=now,
373
+ )
374
+ connection.execute(
375
+ """
376
+ UPDATE ide_forge_plan_requests
377
+ SET state = 'failed', error_code = ?, terminal_at = ?, updated_at = ?,
378
+ lease_token_sha256 = NULL, lease_expires_at = NULL
379
+ WHERE job_id = ?
380
+ """,
381
+ (_error_code(error_code), now, now, lease.job_id),
382
+ )
383
+ return self._record(self._row_by_job(connection, lease.job_id))
384
+
385
+ def get(self, job_id: str) -> ForgeRequestRecord | None:
386
+ clean = _identifier(job_id, "job", maximum=128)
387
+ now = self._now()
388
+ with self._lock, self._transaction() as connection:
389
+ row = connection.execute(
390
+ "SELECT * FROM ide_forge_plan_requests WHERE job_id = ?", (clean,)
391
+ ).fetchone()
392
+ if row is None:
393
+ return None
394
+ row = self._reconcile_expired_running(connection, row=row, now=now)
395
+ return self._record(row)
396
+
397
+ def attach(
398
+ self,
399
+ *,
400
+ job_id: str,
401
+ session_id: str,
402
+ workspace_root: str | os.PathLike[str],
403
+ ) -> ForgeRequestRecord:
404
+ """Attach a recovered durable job to a live bridge session."""
405
+
406
+ clean_job = _identifier(job_id, "job", maximum=128)
407
+ clean_session = _identifier(session_id, "session", maximum=256)
408
+ workspace = _workspace(workspace_root)
409
+ workspace_key = _digest(os.path.normcase(os.fspath(workspace)))
410
+ now = self._now()
411
+ with self._lock, self._transaction() as connection:
412
+ row = self._row_by_job(connection, clean_job)
413
+ if not secrets.compare_digest(str(row["workspace_sha256"]), workspace_key):
414
+ raise ForgeRequestValidationError(
415
+ "Durable Forge plan request workspace binding does not match."
416
+ )
417
+ connection.execute(
418
+ """
419
+ UPDATE ide_forge_plan_requests SET session_id = ?, updated_at = ?
420
+ WHERE job_id = ?
421
+ """,
422
+ (clean_session, now, clean_job),
423
+ )
424
+ return self._record(self._row_by_job(connection, clean_job))
425
+
426
+ def _finish(
427
+ self,
428
+ lease: ForgeWorkerLease,
429
+ *,
430
+ state: ForgeRequestState,
431
+ plan_id: str | None,
432
+ error_code: str | None,
433
+ ) -> ForgeRequestRecord:
434
+ now = self._now()
435
+ with self._lock, self._transaction() as connection:
436
+ self._require_lease(
437
+ connection,
438
+ job_id=lease.job_id,
439
+ generation=lease.generation,
440
+ token=lease.token,
441
+ expected_state=ForgeRequestState.RUNNING,
442
+ now=now,
443
+ )
444
+ connection.execute(
445
+ """
446
+ UPDATE ide_forge_plan_requests
447
+ SET state = ?, plan_id = ?, error_code = ?, terminal_at = ?,
448
+ updated_at = ?, lease_token_sha256 = NULL,
449
+ lease_expires_at = NULL
450
+ WHERE job_id = ?
451
+ """,
452
+ (state.value, plan_id, error_code, now, now, lease.job_id),
453
+ )
454
+ return self._record(self._row_by_job(connection, lease.job_id))
455
+
456
+ def _replace_dispatch_lease(
457
+ self, connection: sqlite3.Connection, *, row: sqlite3.Row, now: float
458
+ ) -> ForgeDispatchLease:
459
+ token = _lease_token(self._lease_token_factory())
460
+ generation = int(row["generation"]) + 1
461
+ expires_at = now + self.config.lease_seconds
462
+ connection.execute(
463
+ """
464
+ UPDATE ide_forge_plan_requests
465
+ SET generation = ?, lease_token_sha256 = ?, lease_expires_at = ?,
466
+ updated_at = ? WHERE job_id = ? AND state = 'queued'
467
+ """,
468
+ (generation, _digest(token), expires_at, now, row["job_id"]),
469
+ )
470
+ return ForgeDispatchLease(
471
+ job_id=str(row["job_id"]),
472
+ generation=generation,
473
+ expires_at=expires_at,
474
+ token=token,
475
+ )
476
+
477
+ def _require_lease(
478
+ self,
479
+ connection: sqlite3.Connection,
480
+ *,
481
+ job_id: str,
482
+ generation: int,
483
+ token: str,
484
+ expected_state: ForgeRequestState,
485
+ now: float,
486
+ ) -> sqlite3.Row:
487
+ row = self._row_by_job(connection, _identifier(job_id, "job", maximum=128))
488
+ if (
489
+ str(row["state"]) != expected_state.value
490
+ or int(row["generation"]) != generation
491
+ or not secrets.compare_digest(str(row["lease_token_sha256"] or ""), _digest(token))
492
+ or float(row["lease_expires_at"] or 0.0) <= now
493
+ ):
494
+ raise ForgeRequestLeaseLost("Forge plan worker lease is no longer valid.")
495
+ return row
496
+
497
+ def _reconcile_expired_running(
498
+ self, connection: sqlite3.Connection, *, row: sqlite3.Row, now: float
499
+ ) -> sqlite3.Row:
500
+ if str(row["state"]) != ForgeRequestState.RUNNING.value:
501
+ return row
502
+ if float(row["lease_expires_at"] or 0.0) > now:
503
+ return row
504
+ connection.execute(
505
+ """
506
+ UPDATE ide_forge_plan_requests
507
+ SET state = 'indeterminate', error_code = 'worker_lease_expired',
508
+ terminal_at = ?, updated_at = ?, lease_token_sha256 = NULL,
509
+ lease_expires_at = NULL
510
+ WHERE job_id = ? AND state = 'running' AND lease_expires_at <= ?
511
+ """,
512
+ (now, now, row["job_id"], now),
513
+ )
514
+ return self._row_by_job(connection, str(row["job_id"]))
515
+
516
+ def _prune_for_insert(self, connection: sqlite3.Connection, *, now: float) -> None:
517
+ cutoff = now - self.config.terminal_retention_seconds
518
+ connection.execute(
519
+ """
520
+ DELETE FROM ide_forge_plan_requests
521
+ WHERE state IN ('completed', 'failed', 'cancelled', 'indeterminate')
522
+ AND terminal_at < ?
523
+ """,
524
+ (cutoff,),
525
+ )
526
+ count = int(
527
+ connection.execute("SELECT COUNT(*) FROM ide_forge_plan_requests").fetchone()[0]
528
+ )
529
+ overflow = count - self.config.max_records + 1
530
+ if overflow > 0:
531
+ connection.execute(
532
+ """
533
+ DELETE FROM ide_forge_plan_requests WHERE job_id IN (
534
+ SELECT job_id FROM ide_forge_plan_requests
535
+ WHERE state IN ('completed', 'failed', 'cancelled', 'indeterminate')
536
+ ORDER BY terminal_at ASC LIMIT ?
537
+ )
538
+ """,
539
+ (overflow,),
540
+ )
541
+ count = int(
542
+ connection.execute("SELECT COUNT(*) FROM ide_forge_plan_requests").fetchone()[0]
543
+ )
544
+ if count >= self.config.max_records:
545
+ raise ForgeRequestStorageError("Durable Forge plan request storage is full.")
546
+
547
+ def _record(self, row: sqlite3.Row) -> ForgeRequestRecord:
548
+ return ForgeRequestRecord(
549
+ job_id=str(row["job_id"]),
550
+ session_id=str(row["session_id"]),
551
+ workspace_root=Path(str(row["workspace_root"])),
552
+ state=ForgeRequestState(str(row["state"])),
553
+ created_at=float(row["created_at"]),
554
+ updated_at=float(row["updated_at"]),
555
+ started_at=(None if row["started_at"] is None else float(row["started_at"])),
556
+ terminal_at=(None if row["terminal_at"] is None else float(row["terminal_at"])),
557
+ plan_id=(None if row["plan_id"] is None else str(row["plan_id"])),
558
+ error_code=(None if row["error_code"] is None else str(row["error_code"])),
559
+ attempts=int(row["attempts"]),
560
+ )
561
+
562
+ def _row_by_job(self, connection: sqlite3.Connection, job_id: str) -> sqlite3.Row:
563
+ row = connection.execute(
564
+ "SELECT * FROM ide_forge_plan_requests WHERE job_id = ?", (job_id,)
565
+ ).fetchone()
566
+ if row is None:
567
+ raise ForgeRequestNotFound("Durable Forge plan request was not found.")
568
+ return row
569
+
570
+ def _prepare_storage(self) -> None:
571
+ try:
572
+ self.path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
573
+ with suppress(OSError):
574
+ self.path.parent.chmod(0o700)
575
+ with self._connection() as connection:
576
+ version = int(connection.execute("PRAGMA user_version").fetchone()[0])
577
+ if version not in {0, SCHEMA_VERSION}:
578
+ raise ForgeRequestStorageError(
579
+ "Durable Forge plan request storage schema is unsupported."
580
+ )
581
+ connection.execute(
582
+ """
583
+ CREATE TABLE IF NOT EXISTS ide_forge_plan_requests (
584
+ job_id TEXT PRIMARY KEY,
585
+ workspace_sha256 TEXT NOT NULL,
586
+ workspace_root TEXT NOT NULL,
587
+ session_id TEXT NOT NULL,
588
+ idempotency_sha256 TEXT NOT NULL,
589
+ payload_sha256 TEXT NOT NULL,
590
+ state TEXT NOT NULL CHECK (
591
+ state IN ('queued', 'running', 'completed', 'failed',
592
+ 'cancelled', 'indeterminate')
593
+ ),
594
+ generation INTEGER NOT NULL DEFAULT 1,
595
+ attempts INTEGER NOT NULL DEFAULT 0,
596
+ lease_token_sha256 TEXT,
597
+ lease_expires_at REAL,
598
+ plan_id TEXT,
599
+ error_code TEXT,
600
+ created_at REAL NOT NULL,
601
+ updated_at REAL NOT NULL,
602
+ started_at REAL,
603
+ terminal_at REAL,
604
+ UNIQUE(workspace_sha256, idempotency_sha256)
605
+ )
606
+ """
607
+ )
608
+ connection.execute(
609
+ "CREATE INDEX IF NOT EXISTS idx_ide_forge_plan_requests_state "
610
+ "ON ide_forge_plan_requests(state, lease_expires_at)"
611
+ )
612
+ connection.execute(f"PRAGMA user_version = {SCHEMA_VERSION}")
613
+ with suppress(OSError):
614
+ self.path.chmod(0o600)
615
+ except (OSError, sqlite3.Error) as exc:
616
+ raise ForgeRequestStorageError(
617
+ "Could not prepare durable Forge plan request storage."
618
+ ) from exc
619
+
620
+ @contextmanager
621
+ def _connection(self):
622
+ connection: sqlite3.Connection | None = None
623
+ try:
624
+ connection = sqlite3.connect(
625
+ self.path,
626
+ timeout=self.config.busy_timeout_seconds,
627
+ isolation_level=None,
628
+ )
629
+ connection.row_factory = sqlite3.Row
630
+ connection.execute(
631
+ f"PRAGMA busy_timeout = {int(self.config.busy_timeout_seconds * 1000)}"
632
+ )
633
+ connection.execute("PRAGMA journal_mode = WAL")
634
+ connection.execute("PRAGMA synchronous = FULL")
635
+ yield connection
636
+ except ForgeRequestLedgerError:
637
+ raise
638
+ except (OSError, sqlite3.Error) as exc:
639
+ raise ForgeRequestStorageError("Durable Forge plan request storage failed.") from exc
640
+ finally:
641
+ if connection is not None:
642
+ connection.close()
643
+
644
+ @contextmanager
645
+ def _transaction(self):
646
+ with self._connection() as connection:
647
+ connection.execute("BEGIN IMMEDIATE")
648
+ try:
649
+ yield connection
650
+ except BaseException:
651
+ connection.rollback()
652
+ raise
653
+ else:
654
+ connection.commit()
655
+
656
+ def _now(self) -> float:
657
+ try:
658
+ value = float(self._clock())
659
+ except (TypeError, ValueError, OverflowError):
660
+ raise ForgeRequestStorageError("Forge request ledger clock failed.") from None
661
+ if not math.isfinite(value) or value < 0:
662
+ raise ForgeRequestStorageError("Forge request ledger clock failed.")
663
+ return value
664
+
665
+
666
+ def _workspace(value: str | os.PathLike[str]) -> Path:
667
+ try:
668
+ path = Path(value).expanduser().resolve(strict=True)
669
+ except (OSError, RuntimeError, TypeError, ValueError):
670
+ raise ForgeRequestValidationError("Forge request workspace is invalid.") from None
671
+ if not path.is_dir():
672
+ raise ForgeRequestValidationError("Forge request workspace must be a directory.")
673
+ return path
674
+
675
+
676
+ def _identifier(value: Any, label: str, *, maximum: int) -> str:
677
+ clean = str(value or "").strip()
678
+ if not clean or len(clean) > maximum or _IDENTIFIER_RE.fullmatch(clean) is None:
679
+ raise ForgeRequestValidationError(f"Forge request {label} identifier is invalid.")
680
+ return clean
681
+
682
+
683
+ def _lease_token(value: Any) -> str:
684
+ return _identifier(value, "lease", maximum=256)
685
+
686
+
687
+ def _error_code(value: Any) -> str:
688
+ clean = str(value or "").strip()
689
+ if not clean or len(clean) > 128 or _ERROR_CODE_RE.fullmatch(clean) is None:
690
+ raise ForgeRequestValidationError("Forge request error code is invalid.")
691
+ return clean
692
+
693
+
694
+ def _duration(value: Any, *, maximum: float, label: str) -> float:
695
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
696
+ raise ForgeRequestValidationError(f"Forge request {label} must be numeric.")
697
+ clean = float(value)
698
+ if not math.isfinite(clean) or clean < MIN_LEASE_SECONDS or clean > maximum:
699
+ raise ForgeRequestValidationError(f"Forge request {label} is outside the allowed range.")
700
+ return clean
701
+
702
+
703
+ def _payload_digest(payload: Mapping[str, Any]) -> str:
704
+ try:
705
+ encoded = json.dumps(
706
+ dict(payload),
707
+ ensure_ascii=False,
708
+ allow_nan=False,
709
+ separators=(",", ":"),
710
+ sort_keys=True,
711
+ ).encode("utf-8")
712
+ except (TypeError, ValueError):
713
+ raise ForgeRequestValidationError("Forge request payload is not valid JSON.") from None
714
+ return hashlib.sha256(encoded).hexdigest()
715
+
716
+
717
+ def _digest(value: str) -> str:
718
+ return hashlib.sha256(value.encode("utf-8")).hexdigest()
719
+
720
+
721
+ __all__ = [
722
+ "DEFAULT_LEASE_SECONDS",
723
+ "DurableForgeRequestLedger",
724
+ "ForgeDispatchLease",
725
+ "ForgeRequestAcceptance",
726
+ "ForgeRequestIdempotencyConflict",
727
+ "ForgeRequestLeaseLost",
728
+ "ForgeRequestLedgerConfig",
729
+ "ForgeRequestLedgerError",
730
+ "ForgeRequestNotFound",
731
+ "ForgeRequestRecord",
732
+ "ForgeRequestState",
733
+ "ForgeRequestStorageError",
734
+ "ForgeRequestValidationError",
735
+ "ForgeWorkerLease",
736
+ "default_forge_request_ledger_path",
737
+ ]