blun-king-cli 9.1.587 → 9.1.588

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 (436) hide show
  1. package/CHANGELOG.md +11 -185
  2. package/LIESMICH.txt +51 -13
  3. package/README.md +44 -47
  4. package/agent-spine-plugin/.codex-plugin/plugin.json +16 -4
  5. package/agent-spine-plugin/CHANGELOG.md +37 -5
  6. package/agent-spine-plugin/README.md +3 -3
  7. package/agent-spine-plugin/blun.plugin.json +45 -10
  8. package/agent-spine-plugin/docs/artifact-evaluation.md +93 -0
  9. package/agent-spine-plugin/docs/host-integration.md +42 -27
  10. package/agent-spine-plugin/docs/preflight-recall.md +4 -2
  11. package/agent-spine-plugin/docs/session-timeline.md +97 -236
  12. package/agent-spine-plugin/docs/world-model.md +25 -0
  13. package/agent-spine-plugin/hooks/codex.json +1 -1
  14. package/agent-spine-plugin/hooks/hooks.json +1 -1
  15. package/agent-spine-plugin/package.json +1 -3
  16. package/agent-spine-plugin/scripts/check-hosts.js +3 -3
  17. package/agent-spine-plugin/scripts/release-check.js +10 -5
  18. package/agent-spine-plugin/scripts/run-checks.js +4 -1
  19. package/agent-spine-plugin/scripts/run-tests-hermetic.js +32 -6
  20. package/agent-spine-plugin/src/cli-learning.js +15 -0
  21. package/agent-spine-plugin/src/cli.js +2 -0
  22. package/agent-spine-plugin/src/hook.js +32 -32
  23. package/agent-spine-plugin/src/lib/action-lesson-recall.js +73 -8
  24. package/agent-spine-plugin/src/lib/briefing.js +146 -36
  25. package/agent-spine-plugin/src/lib/channel-continuity.js +19 -0
  26. package/agent-spine-plugin/src/lib/delivery-agent-usage.js +14 -7
  27. package/agent-spine-plugin/src/lib/gateway-group-response.js +128 -0
  28. package/agent-spine-plugin/src/lib/gateway-runs.js +24 -15
  29. package/agent-spine-plugin/src/lib/hook-briefing-use.js +13 -3
  30. package/agent-spine-plugin/src/lib/hook-context.js +16 -3
  31. package/agent-spine-plugin/src/lib/hook-output.js +129 -5
  32. package/agent-spine-plugin/src/lib/hook-timeline.js +5 -3
  33. package/agent-spine-plugin/src/lib/indexed-memory.js +2 -2
  34. package/agent-spine-plugin/src/lib/learning-artifact-evaluator.js +114 -0
  35. package/agent-spine-plugin/src/lib/learning-context.js +11 -4
  36. package/agent-spine-plugin/src/lib/learning-measurements.js +2 -2
  37. package/agent-spine-plugin/src/lib/mcp-runtime.js +89 -3
  38. package/agent-spine-plugin/src/lib/mcp-source-context.js +12 -2
  39. package/agent-spine-plugin/src/lib/mcp-timeline-tools.js +91 -8
  40. package/agent-spine-plugin/src/lib/mcp-world-tools.js +2 -2
  41. package/agent-spine-plugin/src/lib/owned-file-lock.js +20 -1
  42. package/agent-spine-plugin/src/lib/persona-runtime.js +2 -2
  43. package/agent-spine-plugin/src/lib/preflight-delivery-id.js +27 -0
  44. package/agent-spine-plugin/src/lib/preflight.js +4 -4
  45. package/agent-spine-plugin/src/lib/session-timeline-codex.js +15 -0
  46. package/agent-spine-plugin/src/lib/session-timeline-contract.js +12 -4
  47. package/agent-spine-plugin/src/lib/session-timeline-event-extract.js +36 -7
  48. package/agent-spine-plugin/src/lib/session-timeline-host-origin.js +13 -10
  49. package/agent-spine-plugin/src/lib/session-timeline-invocation.js +1 -1
  50. package/agent-spine-plugin/src/lib/session-timeline-king.js +14 -0
  51. package/agent-spine-plugin/src/lib/session-timeline-prior.js +18 -12
  52. package/agent-spine-plugin/src/lib/session-timeline-provider.js +5 -0
  53. package/agent-spine-plugin/src/lib/session-timeline-query.js +2 -0
  54. package/agent-spine-plugin/src/lib/session-timeline-results.js +35 -10
  55. package/agent-spine-plugin/src/lib/session-timeline-source-open.js +30 -0
  56. package/agent-spine-plugin/src/lib/session-timeline.js +122 -75
  57. package/agent-spine-plugin/src/lib/source-roots.js +3 -2
  58. package/agent-spine-plugin/src/lib/task-knowledge-context.js +22 -1
  59. package/agent-spine-plugin/src/lib/timeline-continuation-update.js +100 -0
  60. package/agent-spine-plugin/src/lib/timeline-tool-guard.js +30 -7
  61. package/agent-spine-plugin/src/lib/timeline-user-feedback.js +217 -0
  62. package/agent-spine-plugin/src/lib/timeline-world-capture.js +233 -0
  63. package/agent-spine-plugin/src/lib/world-knowledge.js +59 -2
  64. package/agent-spine-plugin/src/lib/world-model.js +64 -9
  65. package/agent-spine-plugin/src/worker.js +13 -1
  66. package/bin/blun.js +43 -28
  67. package/bin/core-bootstrap.js +5 -4
  68. package/bin/king.js +43 -28
  69. package/bin/launcher-mode.js +1 -10
  70. package/bin/launcher-runtime.js +128 -295
  71. package/bin/managed-node.js +0 -0
  72. package/bin/managed-plugin-selection.cjs +0 -1
  73. package/bin/native-module-repair.js +0 -0
  74. package/bin/node-runtime.js +0 -0
  75. package/bin/node-version.js +0 -0
  76. package/bin/plugin-bootstrap.js +56 -120
  77. package/bin/private-paths.js +11 -34
  78. package/bin/standard-tools-bootstrap.js +34 -114
  79. package/bin/turn-thinking-policy.cjs +3 -11
  80. package/bin/update-copy.js +200 -0
  81. package/bin/update-lease.js +0 -0
  82. package/bin/update-notice.js +136 -289
  83. package/bin/verify-agent-behavior.cjs +122 -0
  84. package/bin/verify-agent-components.cjs +104 -0
  85. package/bin/verify-bundled-agent-sources.cjs +57 -0
  86. package/blun.mjs +143076 -135288
  87. package/bundled-agent-sources.json +701 -0
  88. package/package.json +12 -15
  89. package/standard-skills/translate-native/README.md +1293 -0
  90. package/standard-skills/translate-native/SKILL.md +172 -22
  91. package/standard-skills/translate-native/VERSION +1 -1
  92. package/standard-skills/translate-native/agents/openai.yaml +18 -0
  93. package/standard-skills/translate-native/assets/icon.svg +8 -0
  94. package/standard-skills/translate-native/docs/BLUN_CODE_INTEGRATION.md +76 -0
  95. package/standard-skills/translate-native/docs/PREMORTEM.md +489 -0
  96. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION.md +2035 -0
  97. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_API.md +1302 -0
  98. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_EVIDENCE_HTTP.md +136 -0
  99. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_HEALTH_HTTP.md +130 -0
  100. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_HTTP_PROVIDER.md +175 -0
  101. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_RECEIPT_VERIFIER_HTTP.md +86 -0
  102. package/standard-skills/translate-native/integrations/AGENT_RULES.md +32 -0
  103. package/standard-skills/translate-native/integrations/adapters/blun-code-language-guard.js +514 -0
  104. package/standard-skills/translate-native/integrations/adapters/node-language-guard.js +230 -0
  105. package/standard-skills/translate-native/integrations/audit_log.py +327 -0
  106. package/standard-skills/translate-native/integrations/claude_language_hook.js +1536 -0
  107. package/standard-skills/translate-native/integrations/commercial_localization_profile.py +42 -0
  108. package/standard-skills/translate-native/integrations/delivery-policy.example.json +28 -0
  109. package/standard-skills/translate-native/integrations/enforced_delivery.py +543 -0
  110. package/standard-skills/translate-native/integrations/guard_service.py +435 -0
  111. package/standard-skills/translate-native/integrations/language_gateway.py +67 -0
  112. package/standard-skills/translate-native/integrations/mcp_auth_headers.py +198 -0
  113. package/standard-skills/translate-native/integrations/mcp_http_gateway.py +429 -0
  114. package/standard-skills/translate-native/integrations/non_language_html_entities.js +1485 -0
  115. package/standard-skills/translate-native/integrations/pre_output_guard.py +65 -0
  116. package/standard-skills/translate-native/integrations/task_router.py +101 -0
  117. package/standard-skills/translate-native/integrations/website_localization.py +401 -0
  118. package/standard-skills/translate-native/integrations/website_localization_api.py +581 -0
  119. package/standard-skills/translate-native/integrations/website_localization_benchmark.py +1885 -0
  120. package/standard-skills/translate-native/integrations/website_localization_benchmark_campaign.py +1772 -0
  121. package/standard-skills/translate-native/integrations/website_localization_benchmark_candidate.py +506 -0
  122. package/standard-skills/translate-native/integrations/website_localization_benchmark_http.py +400 -0
  123. package/standard-skills/translate-native/integrations/website_localization_benchmark_review_store.py +781 -0
  124. package/standard-skills/translate-native/integrations/website_localization_benchmark_reviewer_http.py +500 -0
  125. package/standard-skills/translate-native/integrations/website_localization_benchmark_runtime.py +1107 -0
  126. package/standard-skills/translate-native/integrations/website_localization_benchmark_suite.py +463 -0
  127. package/standard-skills/translate-native/integrations/website_localization_cms.py +2835 -0
  128. package/standard-skills/translate-native/integrations/website_localization_cms_client.py +875 -0
  129. package/standard-skills/translate-native/integrations/website_localization_cms_dispatch.py +805 -0
  130. package/standard-skills/translate-native/integrations/website_localization_cms_http.py +588 -0
  131. package/standard-skills/translate-native/integrations/website_localization_cms_lifecycle_monitor.py +991 -0
  132. package/standard-skills/translate-native/integrations/website_localization_cms_receiver.py +1441 -0
  133. package/standard-skills/translate-native/integrations/website_localization_cms_receiver_runtime.py +414 -0
  134. package/standard-skills/translate-native/integrations/website_localization_cms_receiver_store.py +1073 -0
  135. package/standard-skills/translate-native/integrations/website_localization_cms_removal_dispatch.py +865 -0
  136. package/standard-skills/translate-native/integrations/website_localization_cms_source_client.py +583 -0
  137. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery.py +964 -0
  138. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_runtime.py +665 -0
  139. package/standard-skills/translate-native/integrations/website_localization_cms_source_http.py +1153 -0
  140. package/standard-skills/translate-native/integrations/website_localization_cms_source_runtime.py +675 -0
  141. package/standard-skills/translate-native/integrations/website_localization_cms_source_service.py +1125 -0
  142. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification.py +674 -0
  143. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_http.py +444 -0
  144. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver.py +1469 -0
  145. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver_runtime.py +1142 -0
  146. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_processing_monitor.py +634 -0
  147. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_receiver_client.py +804 -0
  148. package/standard-skills/translate-native/integrations/website_localization_deepl_baseline.py +922 -0
  149. package/standard-skills/translate-native/integrations/website_localization_evidence_http.py +482 -0
  150. package/standard-skills/translate-native/integrations/website_localization_health.py +1541 -0
  151. package/standard-skills/translate-native/integrations/website_localization_health_http.py +372 -0
  152. package/standard-skills/translate-native/integrations/website_localization_http_provider.py +297 -0
  153. package/standard-skills/translate-native/integrations/website_localization_native_reference_http.py +479 -0
  154. package/standard-skills/translate-native/integrations/website_localization_native_reference_intake.py +363 -0
  155. package/standard-skills/translate-native/integrations/website_localization_native_reference_queue.py +1449 -0
  156. package/standard-skills/translate-native/integrations/website_localization_native_reference_store.py +420 -0
  157. package/standard-skills/translate-native/integrations/website_localization_quality_profiles.py +235 -0
  158. package/standard-skills/translate-native/integrations/website_localization_queue.py +671 -0
  159. package/standard-skills/translate-native/integrations/website_localization_receipt_verifier_http.py +516 -0
  160. package/standard-skills/translate-native/integrations/website_localization_release.py +928 -0
  161. package/standard-skills/translate-native/integrations/website_localization_release_coordinator.py +1008 -0
  162. package/standard-skills/translate-native/integrations/website_localization_runner.py +276 -0
  163. package/standard-skills/translate-native/integrations/website_localization_runtime.py +862 -0
  164. package/standard-skills/translate-native/integrations/website_localization_service.py +350 -0
  165. package/standard-skills/translate-native/integrations/website_localization_supervisor.py +511 -0
  166. package/standard-skills/translate-native/integrations/website_localization_worker.py +663 -0
  167. package/standard-skills/translate-native/provenance.json +3 -4
  168. package/standard-skills/translate-native/references/commercial-localization.md +177 -0
  169. package/standard-skills/translate-native/scripts/blun_language_guard.py +7 -1
  170. package/standard-skills/translate-native/scripts/check_commercial_review.py +80 -0
  171. package/standard-skills/translate-native/scripts/commercial_localization_profile.py +333 -0
  172. package/standard-tools/language-guard/LICENSE +21 -0
  173. package/standard-tools/language-guard/VERSION +1 -0
  174. package/standard-tools/language-guard/blun_language_guard.py +7 -1
  175. package/standard-tools/language-guard/check_commercial_review.py +80 -0
  176. package/standard-tools/language-guard/commercial_localization_profile.py +333 -0
  177. package/standard-tools/language-guard/language_gateway.py +62 -0
  178. package/standard-tools/language-guard/pre_output_guard.py +64 -0
  179. package/standard-tools/language-guard/provenance.json +4 -11
  180. package/standard-tools/manifest.json +34 -11
  181. package/telegram-plugin/commands/access.md +2 -10
  182. package/telegram-plugin/dist/bridge.mjs +64041 -687
  183. package/telegram-plugin/dist/mcp-server.mjs +72810 -9027
  184. package/telegram-plugin/dist/noise.mjs +28 -63511
  185. package/agent-spine-plugin/CONTRIBUTING.md +0 -52
  186. package/agent-spine-plugin/SECURITY.md +0 -47
  187. package/agent-spine-plugin/docs/assignment-continuation.md +0 -48
  188. package/agent-spine-plugin/docs/releasing.md +0 -85
  189. package/agent-spine-plugin/docs/structured-completion.md +0 -67
  190. package/bin/abort-listener-policy.cjs +0 -43
  191. package/bin/active-steer-priority-policy.cjs +0 -24
  192. package/bin/agent-api-http-adapter.mjs +0 -446
  193. package/bin/agent-api-private-http-server.mjs +0 -288
  194. package/bin/agent-api-runtime.mjs +0 -252
  195. package/bin/agent-api-service-environment.mjs +0 -236
  196. package/bin/agent-api-service-host.mjs +0 -209
  197. package/bin/agent-api-service-process.mjs +0 -171
  198. package/bin/agent-api-session-registry.mjs +0 -428
  199. package/bin/agent-api-tool-broker.cjs +0 -248
  200. package/bin/agent-api-turn-controller.mjs +0 -461
  201. package/bin/agent-api-usage-journal.cjs +0 -259
  202. package/bin/agent-resume-snapshot.cjs +0 -241
  203. package/bin/agentspine-king-goal-inbox.mjs +0 -111
  204. package/bin/agentspine-king-goal-intake.mjs +0 -106
  205. package/bin/approval-rejection-stop.cjs +0 -15
  206. package/bin/assistant-message-offload-policy.cjs +0 -284
  207. package/bin/baseline-skill-performance-policy.cjs +0 -39
  208. package/bin/bash-search-scope-policy.cjs +0 -49
  209. package/bin/codebase-search-runtime.cjs +0 -23
  210. package/bin/cognitive-action-checkpoint.cjs +0 -1104
  211. package/bin/cognitive-attention-delivery.cjs +0 -76
  212. package/bin/cognitive-attention-policy.cjs +0 -143
  213. package/bin/cognitive-attention-runtime.cjs +0 -91
  214. package/bin/cognitive-context-projection.cjs +0 -73
  215. package/bin/cognitive-cross-portal-acceptance.cjs +0 -443
  216. package/bin/cognitive-effective-view.cjs +0 -77
  217. package/bin/cognitive-focus-projection.cjs +0 -206
  218. package/bin/cognitive-focus-scope.cjs +0 -37
  219. package/bin/cognitive-goal-autostart-policy.cjs +0 -72
  220. package/bin/cognitive-goal-time-trigger-controller.cjs +0 -146
  221. package/bin/cognitive-memory-adapter.cjs +0 -282
  222. package/bin/cognitive-memory-command.cjs +0 -293
  223. package/bin/cognitive-memory-provider.cjs +0 -92
  224. package/bin/cognitive-salience-policy.cjs +0 -159
  225. package/bin/cognitive-state-store.cjs +0 -508
  226. package/bin/cognitive-turn-lifecycle.cjs +0 -624
  227. package/bin/cognitive-work-focus.cjs +0 -180
  228. package/bin/compaction-history-archive.cjs +0 -166
  229. package/bin/compaction-history-startup.cjs +0 -50
  230. package/bin/compaction-model-policy.cjs +0 -31
  231. package/bin/compaction-stage-policy.cjs +0 -21
  232. package/bin/compaction-transaction-policy.cjs +0 -122
  233. package/bin/config-write-dedup-policy.cjs +0 -27
  234. package/bin/context-budget-ledger.cjs +0 -31
  235. package/bin/context-doctor-policy.cjs +0 -70
  236. package/bin/context-insight-policy.cjs +0 -36
  237. package/bin/context-performance-policy.cjs +0 -19
  238. package/bin/context-pressure-policy.cjs +0 -20
  239. package/bin/cron-run-output.cjs +0 -45
  240. package/bin/cron-run-store.cjs +0 -145
  241. package/bin/curiosity-scout-policy.cjs +0 -49
  242. package/bin/default-model-output-budget-policy.cjs +0 -28
  243. package/bin/durable-task-resume-policy.cjs +0 -130
  244. package/bin/durable-task-resume-runtime.cjs +0 -117
  245. package/bin/durable-task-resume-store.cjs +0 -88
  246. package/bin/editable-tool-approval-policy.cjs +0 -540
  247. package/bin/editable-tool-approval-runtime.cjs +0 -99
  248. package/bin/effective-system-prompt-cache-policy.cjs +0 -33
  249. package/bin/error-memory-performance-policy.cjs +0 -113
  250. package/bin/file-observation-policy.cjs +0 -133
  251. package/bin/foreground-output-capture-policy.cjs +0 -41
  252. package/bin/generated-source-health.cjs +0 -142
  253. package/bin/glob-pattern-policy.cjs +0 -13
  254. package/bin/goal-completion-evidence-policy.cjs +0 -120
  255. package/bin/grep-output-limit-policy.cjs +0 -39
  256. package/bin/historical-media-projection-policy.cjs +0 -48
  257. package/bin/history-offload-pressure-policy.cjs +0 -33
  258. package/bin/html-to-research-markdown.cjs +0 -147
  259. package/bin/identity-context-policy.cjs +0 -764
  260. package/bin/identity-journal-policy.cjs +0 -107
  261. package/bin/input-draft-persistence.cjs +0 -77
  262. package/bin/king-tui-function-contract.json +0 -33
  263. package/bin/launcher-restart-policy.cjs +0 -150
  264. package/bin/live-response-repetition-guard.cjs +0 -196
  265. package/bin/llm-config-log-dedup-policy.cjs +0 -76
  266. package/bin/loop-event-record-policy.cjs +0 -174
  267. package/bin/managed-context-startup-policy.cjs +0 -27
  268. package/bin/media-activity-layout-policy.cjs +0 -34
  269. package/bin/media-auto-retrieval-policy.cjs +0 -90
  270. package/bin/media-result-policy.cjs +0 -59
  271. package/bin/micro-compaction-policy.cjs +0 -145
  272. package/bin/mistake-relevance-policy.cjs +0 -319
  273. package/bin/model-retry-progress-policy.cjs +0 -46
  274. package/bin/native-large-file-io.cjs +0 -42
  275. package/bin/native-runtime-cache.cjs +0 -76
  276. package/bin/natural-presence-policy.cjs +0 -28
  277. package/bin/noninteractive-shell-env-policy.cjs +0 -19
  278. package/bin/observer-hooks.cjs +0 -14
  279. package/bin/outbound-claim-provenance.cjs +0 -150
  280. package/bin/oversized-context-offload-policy.cjs +0 -86
  281. package/bin/pending-media-policy.cjs +0 -182
  282. package/bin/pending-token-estimate-policy.cjs +0 -41
  283. package/bin/personal-memory-consent-policy.cjs +0 -72
  284. package/bin/personal-memory-performance-policy.cjs +0 -12
  285. package/bin/personality-choice-policy.cjs +0 -101
  286. package/bin/personality-memory-adapter.cjs +0 -379
  287. package/bin/personality-mode.cjs +0 -46
  288. package/bin/personality-setup-policy.cjs +0 -197
  289. package/bin/proactive-compaction-policy.cjs +0 -25
  290. package/bin/profile-identity-resolution.cjs +0 -136
  291. package/bin/profile-runtime.cjs +0 -318
  292. package/bin/profile-tool-exclusion-policy.cjs +0 -37
  293. package/bin/programmatic-context-isolation.cjs +0 -25
  294. package/bin/programmatic-tool-runtime.mjs +0 -627
  295. package/bin/provider-idle-timeout-policy.cjs +0 -14
  296. package/bin/provider-model-refresh-deadline.cjs +0 -53
  297. package/bin/provider-model-refresh-policy.cjs +0 -107
  298. package/bin/rate-limit-recovery-policy.cjs +0 -47
  299. package/bin/read-batch-policy.cjs +0 -32
  300. package/bin/read-continuation-policy.cjs +0 -59
  301. package/bin/recurring-cron-history-policy.cjs +0 -124
  302. package/bin/relationship-continuity-policy.cjs +0 -143
  303. package/bin/relationship-curiosity-policy.cjs +0 -107
  304. package/bin/relationship-learning-policy.cjs +0 -168
  305. package/bin/release-artifact-freeze-policy.cjs +0 -30
  306. package/bin/reload-plugin-bootstrap.cjs +0 -18
  307. package/bin/reload-queue-policy.cjs +0 -38
  308. package/bin/repeated-assistant-response-policy.cjs +0 -232
  309. package/bin/repeated-injection-projection.cjs +0 -107
  310. package/bin/repeated-user-message-projection.cjs +0 -8
  311. package/bin/research-page-result.cjs +0 -74
  312. package/bin/retry-checkpoint-policy.cjs +0 -13
  313. package/bin/runtime-exit-ledger.cjs +0 -144
  314. package/bin/scoped-cron-run-policy.cjs +0 -358
  315. package/bin/session-checkpoint-policy.cjs +0 -25
  316. package/bin/session-compaction-policy.cjs +0 -84
  317. package/bin/session-replay-policy.cjs +0 -20
  318. package/bin/session-replay-window-policy.cjs +0 -40
  319. package/bin/session-resume-checkpoint.cjs +0 -254
  320. package/bin/session-scrollback-archive.cjs +0 -229
  321. package/bin/skill-activation-performance-policy.cjs +0 -69
  322. package/bin/skill-listing-performance-policy.cjs +0 -92
  323. package/bin/soul-organization-policy.cjs +0 -78
  324. package/bin/soul-preservation-policy.cjs +0 -20
  325. package/bin/startup-preferences.cjs +0 -131
  326. package/bin/streaming-flush-performance-policy.cjs +0 -28
  327. package/bin/structured-agent-swarm-output.cjs +0 -325
  328. package/bin/structured-subagent-output.cjs +0 -252
  329. package/bin/subagent-context-fork-policy.cjs +0 -155
  330. package/bin/subagent-max-tokens-handoff-policy.cjs +0 -69
  331. package/bin/subagent-parent-responsiveness.cjs +0 -19
  332. package/bin/subagent-skill-policy.cjs +0 -206
  333. package/bin/subagent-timeout-policy.cjs +0 -182
  334. package/bin/subagent-tool-policy.cjs +0 -60
  335. package/bin/subagent-usage-rollup-policy.cjs +0 -29
  336. package/bin/system-prompt-context-policy.cjs +0 -124
  337. package/bin/system-prompt-token-cache-policy.cjs +0 -60
  338. package/bin/telegram-addressed-focus.cjs +0 -55
  339. package/bin/telegram-addressed-priority.cjs +0 -12
  340. package/bin/telegram-approval-relay.cjs +0 -290
  341. package/bin/telegram-bot-priority.cjs +0 -17
  342. package/bin/telegram-console-status-policy.cjs +0 -174
  343. package/bin/telegram-context-projection-policy.cjs +0 -141
  344. package/bin/telegram-delivery-lifecycle.cjs +0 -125
  345. package/bin/telegram-direct-focus-policy.cjs +0 -273
  346. package/bin/telegram-mcp-compatibility.cjs +0 -49
  347. package/bin/telegram-media-delivery-policy.cjs +0 -42
  348. package/bin/telegram-private-conversation-policy.cjs +0 -185
  349. package/bin/telegram-queue-handoff-policy.cjs +0 -73
  350. package/bin/telegram-remote-status-policy.cjs +0 -120
  351. package/bin/telegram-session-queue-runtime.mjs +0 -306
  352. package/bin/telegram-text-chunk-policy.cjs +0 -63
  353. package/bin/telegram-truncated-reply-policy.cjs +0 -37
  354. package/bin/telegram-urgent-policy.cjs +0 -45
  355. package/bin/telemetry-spool-policy.cjs +0 -57
  356. package/bin/thinking-activity-status-policy.cjs +0 -132
  357. package/bin/thinking-only-guard.cjs +0 -80
  358. package/bin/todo-list-turn-policy.cjs +0 -131
  359. package/bin/tool-call-loop-policy.cjs +0 -51
  360. package/bin/tool-file-persistence.cjs +0 -141
  361. package/bin/tool-result-offload-policy.cjs +0 -359
  362. package/bin/tool-result-offload-telemetry.cjs +0 -12
  363. package/bin/tool-schema-token-cache-policy.cjs +0 -41
  364. package/bin/tool-stream-preview-policy.cjs +0 -9
  365. package/bin/tui-functional-contract.cjs +0 -55
  366. package/bin/turn-tool-performance-policy.cjs +0 -486
  367. package/bin/usage-cache-efficiency-policy.cjs +0 -26
  368. package/bin/user-home-path-policy.cjs +0 -13
  369. package/bin/user-message-offload-policy.cjs +0 -103
  370. package/bin/user-prompt-hook-origin-policy.cjs +0 -34
  371. package/bin/user-tool-record-policy.cjs +0 -7
  372. package/bin/validated-learning-insight-policy.cjs +0 -58
  373. package/bin/validated-learning-outcome-trace.cjs +0 -107
  374. package/bin/validated-learning-performance-policy.cjs +0 -53
  375. package/bin/validated-learning-signal.cjs +0 -463
  376. package/bin/windows-bash-dialect-policy.cjs +0 -25
  377. package/bin/windows-node-crash-dump.cjs +0 -110
  378. package/bin/write-continuation-policy.cjs +0 -69
  379. package/codebase-index/README.md +0 -82
  380. package/codebase-index/codebase_index.py +0 -470
  381. package/standard-skills/agent-browser/SKILL.md +0 -19
  382. package/standard-skills/agent-browser/references/runtime.md +0 -8
  383. package/standard-skills/blun-session-inspector/SKILL.md +0 -41
  384. package/standard-skills/blun-session-inspector/scripts/inspect-session.cjs +0 -437
  385. package/standard-skills/design-taste-frontend/SKILL.md +0 -1206
  386. package/standard-skills/full-output-enforcement/SKILL.md +0 -49
  387. package/standard-skills/high-end-visual-design/SKILL.md +0 -98
  388. package/standard-skills/image-to-code/SKILL.md +0 -1228
  389. package/standard-skills/industrial-brutalist-ui/SKILL.md +0 -92
  390. package/standard-skills/minimalist-ui/SKILL.md +0 -85
  391. package/standard-skills/motion-design-taste/SKILL.md +0 -74
  392. package/standard-skills/playwright-testing/SKILL.md +0 -19
  393. package/standard-skills/playwright-testing/references/runtime.md +0 -7
  394. package/standard-skills/premortem/SKILL.md +0 -148
  395. package/standard-skills/redesign-existing-projects/SKILL.md +0 -178
  396. package/standard-skills/research-evidence/SKILL.md +0 -39
  397. package/standard-skills/research-evidence/references/evidence-format.md +0 -104
  398. package/standard-skills/research-evidence/scripts/evidence-collection.cjs +0 -260
  399. package/standard-skills/research-evidence/scripts/score-report.cjs +0 -130
  400. package/standard-skills/screenshot-lesen/SKILL.md +0 -52
  401. package/standard-skills/stitch-design-taste/DESIGN.md +0 -121
  402. package/standard-skills/stitch-design-taste/SKILL.md +0 -184
  403. package/standard-skills/telegram-channel/SKILL.md +0 -18
  404. package/standard-skills/telegram-channel/references/runtime.md +0 -7
  405. package/standard-skills/venture-flywheel/SKILL.md +0 -32
  406. package/standard-skills/venture-flywheel/identity/project-identity.cjs +0 -146
  407. package/standard-skills/venture-flywheel/policy/capability-engine.cjs +0 -114
  408. package/standard-skills/venture-flywheel/policy/repository-trust.cjs +0 -229
  409. package/standard-skills/venture-flywheel/references/BEISPIELE-phase0.md +0 -146
  410. package/standard-skills/venture-flywheel/references/CAPABILITY-MAP.md +0 -34
  411. package/standard-skills/venture-flywheel/references/SPEC-phase0-identity-trust.md +0 -77
  412. package/standard-skills/venture-flywheel/references/SPEC-phase0-state-events.md +0 -93
  413. package/standard-skills/venture-flywheel/schemas/capability-decision.schema.json +0 -13
  414. package/standard-skills/venture-flywheel/schemas/execution-event.schema.json +0 -44
  415. package/standard-skills/venture-flywheel/schemas/project-identity.schema.json +0 -32
  416. package/standard-skills/venture-flywheel/schemas/repository-trust.schema.json +0 -57
  417. package/standard-skills/venture-flywheel/schemas/run-transition.schema.json +0 -59
  418. package/standard-skills/venture-flywheel/state/execution-event.cjs +0 -191
  419. package/standard-skills/venture-flywheel/state/task-state-machine.cjs +0 -190
  420. package/standard-skills/web-lesen/SKILL.md +0 -73
  421. package/standard-skills/web-lesen/scripts/crawl_public.py +0 -379
  422. package/standard-skills/windows-mcp/SKILL.md +0 -19
  423. package/standard-skills/windows-mcp/references/runtime.md +0 -9
  424. package/telegram-plugin/DELIVERY.md +0 -36
  425. package/telegram-plugin/bin/telegram-approval-relay.cjs +0 -290
  426. package/telegram-plugin/bin/telegram-console-status-policy.cjs +0 -175
  427. package/telegram-plugin/bin/telegram-delivery-lifecycle.cjs +0 -125
  428. package/telegram-plugin/bin/telegram-direct-reply-policy.cjs +0 -48
  429. package/telegram-plugin/bin/telegram-launcher-status-queue.cjs +0 -122
  430. package/telegram-plugin/bin/telegram-private-conversation-policy.cjs +0 -186
  431. package/telegram-plugin/bin/telegram-remote-status-policy.cjs +0 -121
  432. package/telegram-plugin/bin/telegram-reply-parts.cjs +0 -149
  433. package/telegram-plugin/bin/telegram-text-chunk-policy.cjs +0 -63
  434. package/telegram-plugin/bin/telegram-typing-keepalive.cjs +0 -89
  435. package/telegram-plugin/compat/mcp-server-fa511cd1.mjs +0 -73825
  436. /package/{bin → scripts}/fix-node-pty-perms.js +0 -0
@@ -0,0 +1,991 @@
1
+ #!/usr/bin/env python3
2
+ """Durable source-side monitor for accepted CMS localization changes.
3
+
4
+ The monitor starts from the exact success record produced by the durable
5
+ change dispatcher. Each leased poll asks the CMS client for a freshly signed
6
+ lifecycle view; request freshness and response validation remain owned by that
7
+ client. Only content-free identities, counts, error codes, and hashes are
8
+ retained locally.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import importlib.util
14
+ import json
15
+ import math
16
+ import secrets
17
+ import sqlite3
18
+ import sys
19
+ from contextlib import contextmanager
20
+ from dataclasses import dataclass
21
+ from pathlib import Path
22
+ from typing import Any, Iterator, Mapping
23
+
24
+
25
+ SCHEMA_VERSION = 1
26
+ MAX_FAILURES = 20
27
+ MAX_LEASE_SECONDS = 86_400.0
28
+ MAX_DELAY_SECONDS = 86_400.0
29
+ STATES = ("pending", "leased", "watching", "retry_wait", "terminal", "failed")
30
+ TERMINAL_SUCCESS = {"cancelled", "deleted", "published", "superseded"}
31
+ TERMINAL_FAILURE = {
32
+ "deletion_failed", "localization_failed", "publication_blocked",
33
+ "publication_failed",
34
+ }
35
+ REMOTE_STATUSES = TERMINAL_SUCCESS | TERMINAL_FAILURE | {
36
+ "awaiting_approval", "deleting", "processing", "publishing",
37
+ "queue_recovery", "ready",
38
+ }
39
+ _COLUMNS = (
40
+ "event_id", "site_id", "plan_id", "website_version",
41
+ "source_sequence", "job_count", "change_sha256", "binding_sha256", "state",
42
+ "poll_attempts", "consecutive_failures", "max_consecutive_failures",
43
+ "next_poll_at", "lease_owner", "lease_token", "lease_expires_at",
44
+ "last_error_code", "remote_status", "lifecycle_sha256",
45
+ "lifecycle_json",
46
+ "required_locales_json", "approved_locales_json",
47
+ "blocked_locales_json", "queue_counts_json", "delivery_json",
48
+ "tombstone_json", "created_at", "updated_at",
49
+ )
50
+ _META_COLUMNS = ("singleton", "schema_version")
51
+
52
+
53
+ def _load_module(name: str, path: Path):
54
+ spec = importlib.util.spec_from_file_location(name, path)
55
+ if spec is None or spec.loader is None:
56
+ raise RuntimeError(f"cannot load lifecycle monitor dependency: {path.name}")
57
+ module = importlib.util.module_from_spec(spec)
58
+ sys.modules[spec.name] = module
59
+ spec.loader.exec_module(module)
60
+ return module
61
+
62
+
63
+ _ROOT = Path(__file__).resolve().parents[1]
64
+ _DISPATCH = _load_module(
65
+ "blun_website_localization_cms_lifecycle_monitor_dispatch",
66
+ _ROOT / "integrations" / "website_localization_cms_dispatch.py",
67
+ )
68
+ LifecycleMonitorBlocked = _DISPATCH.CMSDispatchBlocked
69
+
70
+
71
+ @dataclass(frozen=True)
72
+ class LifecyclePollClaim:
73
+ event_id: str
74
+ site_id: str
75
+ plan_id: str
76
+ website_version: str
77
+ source_sequence: int
78
+ job_count: int
79
+ change_sha256: str
80
+ attempt: int
81
+ max_consecutive_failures: int
82
+ lease_owner: str
83
+ lease_token: str
84
+ lease_expires_at: float
85
+
86
+
87
+ @dataclass(frozen=True)
88
+ class LifecycleMonitorStatus:
89
+ event_id: str
90
+ site_id: str
91
+ plan_id: str
92
+ website_version: str
93
+ source_sequence: int
94
+ job_count: int
95
+ change_sha256: str
96
+ binding_sha256: str
97
+ state: str
98
+ poll_attempts: int
99
+ consecutive_failures: int
100
+ max_consecutive_failures: int
101
+ next_poll_at: float
102
+ lease_expires_at: float | None
103
+ lease_expired: bool
104
+ last_error_code: str | None
105
+ remote_status: str | None
106
+ lifecycle_sha256: str | None
107
+ required_locales: tuple[str, ...]
108
+ approved_locales: tuple[str, ...]
109
+ blocked_locales: tuple[tuple[str, str], ...]
110
+ queue_counts: dict[str, int]
111
+ delivery: Mapping[str, Any] | None
112
+ tombstone: Mapping[str, Any] | None
113
+
114
+
115
+ @dataclass(frozen=True)
116
+ class LifecyclePollOutcome:
117
+ event_id: str
118
+ state: str
119
+ remote_status: str | None
120
+ attempt: int
121
+ consecutive_failures: int
122
+ next_poll_at: float
123
+ error_code: str | None
124
+
125
+
126
+ @dataclass(frozen=True)
127
+ class LifecycleMonitorHealth:
128
+ status: str
129
+ counts: dict[str, int]
130
+ due: int
131
+ expired_leases: int
132
+ failed: int
133
+ remote_failures: int
134
+
135
+
136
+ def _value(value: Any, name: str) -> Any:
137
+ if isinstance(value, Mapping):
138
+ return value.get(name)
139
+ return getattr(value, name, None)
140
+
141
+
142
+ def _canonical(value: Any, code: str) -> str:
143
+ return _DISPATCH._canonical_json(value, code)
144
+
145
+
146
+ def _binding(
147
+ event_id: str,
148
+ site_id: str,
149
+ plan_id: str,
150
+ website_version: str,
151
+ source_sequence: int,
152
+ job_count: int,
153
+ change_sha256: str,
154
+ max_consecutive_failures: int,
155
+ ) -> str:
156
+ value = {
157
+ "schema": "blun.cms-source-lifecycle-monitor-binding.v1",
158
+ "event_id": event_id,
159
+ "site_id": site_id,
160
+ "plan_id": plan_id,
161
+ "website_version": website_version,
162
+ "source_sequence": source_sequence,
163
+ "job_count": job_count,
164
+ "change_sha256": change_sha256,
165
+ "max_consecutive_failures": max_consecutive_failures,
166
+ }
167
+ return _DISPATCH._hash(
168
+ _canonical(value, "lifecycle_monitor.binding_invalid")
169
+ )
170
+
171
+
172
+ def _json(value: Any, code: str) -> Any:
173
+ try:
174
+ return json.loads(value)
175
+ except (TypeError, json.JSONDecodeError, RecursionError):
176
+ raise LifecycleMonitorBlocked(code) from None
177
+
178
+
179
+ @contextmanager
180
+ def _transaction(connection: sqlite3.Connection) -> Iterator[None]:
181
+ if connection.in_transaction:
182
+ raise LifecycleMonitorBlocked("lifecycle_monitor.transaction_nested")
183
+ try:
184
+ connection.execute("BEGIN IMMEDIATE")
185
+ yield
186
+ connection.commit()
187
+ except Exception:
188
+ if connection.in_transaction:
189
+ connection.rollback()
190
+ raise
191
+
192
+
193
+ class DurableCMSLifecycleMonitor:
194
+ """Poll accepted changes until a verified terminal CMS lifecycle state."""
195
+
196
+ def __init__(
197
+ self,
198
+ connection: sqlite3.Connection,
199
+ *,
200
+ poll_interval_seconds: float | int = 30,
201
+ base_delay_seconds: float | int = 5,
202
+ max_delay_seconds: float | int = 300,
203
+ ):
204
+ if not isinstance(connection, sqlite3.Connection):
205
+ raise TypeError("connection must be sqlite3.Connection")
206
+ self.connection = connection
207
+ self.connection.row_factory = sqlite3.Row
208
+ self.poll_interval_seconds = _DISPATCH._duration(
209
+ poll_interval_seconds, "lifecycle_monitor.delay_invalid",
210
+ maximum=MAX_DELAY_SECONDS,
211
+ )
212
+ self.base_delay_seconds = _DISPATCH._duration(
213
+ base_delay_seconds, "lifecycle_monitor.delay_invalid",
214
+ maximum=MAX_DELAY_SECONDS,
215
+ )
216
+ self.max_delay_seconds = _DISPATCH._duration(
217
+ max_delay_seconds, "lifecycle_monitor.delay_invalid",
218
+ maximum=MAX_DELAY_SECONDS,
219
+ )
220
+ if self.base_delay_seconds > self.max_delay_seconds:
221
+ raise LifecycleMonitorBlocked("lifecycle_monitor.delay_invalid")
222
+ self._initialize()
223
+
224
+ def _initialize(self) -> None:
225
+ with _transaction(self.connection):
226
+ self.connection.execute("""
227
+ CREATE TABLE IF NOT EXISTS cms_source_lifecycle_monitor_meta (
228
+ singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
229
+ schema_version INTEGER NOT NULL CHECK (schema_version = 1)
230
+ )
231
+ """)
232
+ self.connection.execute("""
233
+ INSERT OR IGNORE INTO cms_source_lifecycle_monitor_meta
234
+ VALUES (1, 1)
235
+ """)
236
+ self.connection.execute("""
237
+ CREATE TABLE IF NOT EXISTS cms_source_lifecycle_monitor (
238
+ event_id TEXT PRIMARY KEY,
239
+ site_id TEXT NOT NULL,
240
+ plan_id TEXT NOT NULL,
241
+ website_version TEXT NOT NULL,
242
+ source_sequence INTEGER NOT NULL CHECK (source_sequence > 0),
243
+ job_count INTEGER NOT NULL CHECK (job_count > 0),
244
+ change_sha256 TEXT NOT NULL,
245
+ binding_sha256 TEXT NOT NULL,
246
+ state TEXT NOT NULL CHECK (
247
+ state IN ('pending', 'leased', 'watching',
248
+ 'retry_wait', 'terminal', 'failed')
249
+ ),
250
+ poll_attempts INTEGER NOT NULL CHECK (poll_attempts >= 0),
251
+ consecutive_failures INTEGER NOT NULL CHECK (
252
+ consecutive_failures >= 0
253
+ ),
254
+ max_consecutive_failures INTEGER NOT NULL CHECK (
255
+ max_consecutive_failures >= 1
256
+ AND max_consecutive_failures <= 20
257
+ ),
258
+ next_poll_at REAL NOT NULL,
259
+ lease_owner TEXT,
260
+ lease_token TEXT,
261
+ lease_expires_at REAL,
262
+ last_error_code TEXT,
263
+ remote_status TEXT,
264
+ lifecycle_sha256 TEXT,
265
+ lifecycle_json TEXT,
266
+ required_locales_json TEXT,
267
+ approved_locales_json TEXT,
268
+ blocked_locales_json TEXT,
269
+ queue_counts_json TEXT,
270
+ delivery_json TEXT,
271
+ tombstone_json TEXT,
272
+ created_at REAL NOT NULL,
273
+ updated_at REAL NOT NULL,
274
+ CHECK (
275
+ (state = 'leased' AND lease_owner IS NOT NULL
276
+ AND lease_token IS NOT NULL
277
+ AND lease_expires_at IS NOT NULL)
278
+ OR
279
+ (state <> 'leased' AND lease_owner IS NULL
280
+ AND lease_token IS NULL
281
+ AND lease_expires_at IS NULL)
282
+ )
283
+ )
284
+ """)
285
+ self.connection.execute("""
286
+ CREATE INDEX IF NOT EXISTS cms_source_lifecycle_monitor_due
287
+ ON cms_source_lifecycle_monitor (
288
+ state, next_poll_at, created_at, event_id
289
+ )
290
+ """)
291
+ self._validate_schema()
292
+
293
+ def _validate_schema(self) -> None:
294
+ meta_columns = tuple(
295
+ row["name"] for row in self.connection.execute(
296
+ "PRAGMA table_info(cms_source_lifecycle_monitor_meta)"
297
+ ).fetchall()
298
+ )
299
+ meta = self.connection.execute(
300
+ "SELECT singleton, schema_version "
301
+ "FROM cms_source_lifecycle_monitor_meta"
302
+ ).fetchall()
303
+ columns = tuple(
304
+ row["name"] for row in self.connection.execute(
305
+ "PRAGMA table_info(cms_source_lifecycle_monitor)"
306
+ ).fetchall()
307
+ )
308
+ if (
309
+ meta_columns != _META_COLUMNS
310
+ or len(meta) != 1
311
+ or tuple(meta[0]) != (1, SCHEMA_VERSION)
312
+ or columns != _COLUMNS
313
+ ):
314
+ raise LifecycleMonitorBlocked("lifecycle_monitor.schema_altered")
315
+
316
+ def register(
317
+ self,
318
+ change: Mapping[str, Any],
319
+ dispatch_status: Any,
320
+ *,
321
+ max_consecutive_failures: int = 5,
322
+ now: float | int,
323
+ ) -> LifecycleMonitorStatus:
324
+ """Bind monitoring to one validated successful dispatch result."""
325
+ self._validate_schema()
326
+ if (
327
+ isinstance(max_consecutive_failures, bool)
328
+ or not isinstance(max_consecutive_failures, int)
329
+ or not 1 <= max_consecutive_failures <= MAX_FAILURES
330
+ ):
331
+ raise LifecycleMonitorBlocked("lifecycle_monitor.max_failures_invalid")
332
+ now = _DISPATCH._timestamp(now, "lifecycle_monitor.time_invalid")
333
+ event_id, payload_json, change_sha256 = _DISPATCH._change(change)
334
+ copied = _json(payload_json, "lifecycle_monitor.change_invalid")
335
+ site_id = _DISPATCH._token(
336
+ copied.get("site_id"), "lifecycle_monitor.change_invalid",
337
+ )
338
+ website_version = _DISPATCH._token(
339
+ copied.get("website_version"), "lifecycle_monitor.change_invalid",
340
+ )
341
+ source_sequence = copied.get("source_sequence")
342
+ plan_id = _value(dispatch_status, "remote_plan_id")
343
+ job_count = _value(dispatch_status, "remote_job_count")
344
+ remote_status = _value(dispatch_status, "remote_status")
345
+ dispatch_response_sha256 = _value(dispatch_status, "response_sha256")
346
+ if (
347
+ _value(dispatch_status, "event_id") != event_id
348
+ or _value(dispatch_status, "payload_sha256") != change_sha256
349
+ or _value(dispatch_status, "status") != "succeeded"
350
+ or not isinstance(plan_id, str)
351
+ or _DISPATCH.TOKEN.fullmatch(plan_id) is None
352
+ or isinstance(job_count, bool)
353
+ or not isinstance(job_count, int)
354
+ or job_count <= 0
355
+ or remote_status not in {"enqueued", "superseded", "cancelled"}
356
+ or not isinstance(dispatch_response_sha256, str)
357
+ or _DISPATCH.SHA256.fullmatch(dispatch_response_sha256) is None
358
+ or not isinstance(source_sequence, int)
359
+ or isinstance(source_sequence, bool)
360
+ or source_sequence <= 0
361
+ ):
362
+ raise LifecycleMonitorBlocked("lifecycle_monitor.dispatch_invalid")
363
+ initial_state = "terminal" if remote_status in {
364
+ "superseded", "cancelled",
365
+ } else "pending"
366
+ initial_remote = remote_status if initial_state == "terminal" else None
367
+ binding_sha256 = _binding(
368
+ event_id, site_id, plan_id, website_version, source_sequence,
369
+ job_count, change_sha256, max_consecutive_failures,
370
+ )
371
+ identity = (
372
+ site_id, plan_id, website_version, source_sequence, job_count,
373
+ change_sha256, binding_sha256, max_consecutive_failures,
374
+ )
375
+ with _transaction(self.connection):
376
+ row = self.connection.execute(
377
+ "SELECT * FROM cms_source_lifecycle_monitor WHERE event_id = ?",
378
+ (event_id,),
379
+ ).fetchone()
380
+ if row is not None:
381
+ current = (
382
+ row["site_id"], row["plan_id"], row["website_version"],
383
+ row["source_sequence"], row["job_count"],
384
+ row["change_sha256"], row["binding_sha256"],
385
+ row["max_consecutive_failures"],
386
+ )
387
+ if current != identity:
388
+ raise LifecycleMonitorBlocked(
389
+ "lifecycle_monitor.idempotency_collision"
390
+ )
391
+ else:
392
+ self.connection.execute("""
393
+ INSERT INTO cms_source_lifecycle_monitor (
394
+ event_id, site_id, plan_id, website_version,
395
+ source_sequence, job_count, change_sha256,
396
+ binding_sha256, state,
397
+ poll_attempts, consecutive_failures,
398
+ max_consecutive_failures, next_poll_at,
399
+ remote_status, created_at, updated_at
400
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?)
401
+ """, (
402
+ event_id, site_id, plan_id, website_version,
403
+ source_sequence, job_count, change_sha256, binding_sha256,
404
+ initial_state,
405
+ max_consecutive_failures, now, initial_remote, now, now,
406
+ ))
407
+ return self.status(event_id, now=now)
408
+
409
+ def claim(
410
+ self,
411
+ worker_id: str,
412
+ *,
413
+ now: float | int,
414
+ lease_seconds: float | int = 600,
415
+ ) -> LifecyclePollClaim | None:
416
+ self._validate_schema()
417
+ worker_id = _DISPATCH._token(
418
+ worker_id, "lifecycle_monitor.worker_invalid", limit=128,
419
+ )
420
+ now = _DISPATCH._timestamp(now, "lifecycle_monitor.time_invalid")
421
+ lease_seconds = _DISPATCH._duration(
422
+ lease_seconds, "lifecycle_monitor.lease_invalid",
423
+ maximum=MAX_LEASE_SECONDS,
424
+ )
425
+ integrity_error = None
426
+ claim = None
427
+ with _transaction(self.connection):
428
+ expired = self.connection.execute("""
429
+ SELECT * FROM cms_source_lifecycle_monitor
430
+ WHERE state = 'leased' AND lease_expires_at <= ?
431
+ ORDER BY event_id
432
+ """, (now,)).fetchall()
433
+ for row in expired:
434
+ self._validated_row(row)
435
+ failures = int(row["consecutive_failures"]) + 1
436
+ terminal = failures >= int(row["max_consecutive_failures"])
437
+ delay = self._retry_delay(failures)
438
+ self.connection.execute("""
439
+ UPDATE cms_source_lifecycle_monitor
440
+ SET state = ?, consecutive_failures = ?,
441
+ next_poll_at = ?, lease_owner = NULL,
442
+ lease_token = NULL, lease_expires_at = NULL,
443
+ last_error_code = 'lease_expired', updated_at = ?
444
+ WHERE event_id = ? AND state = 'leased'
445
+ """, (
446
+ "failed" if terminal else "retry_wait", failures,
447
+ now if terminal else now + delay, now, row["event_id"],
448
+ ))
449
+ row = self.connection.execute("""
450
+ SELECT * FROM cms_source_lifecycle_monitor
451
+ WHERE state IN ('pending', 'watching', 'retry_wait')
452
+ AND next_poll_at <= ?
453
+ ORDER BY created_at, event_id LIMIT 1
454
+ """, (now,)).fetchone()
455
+ if row is not None:
456
+ try:
457
+ self._validated_row(row)
458
+ except LifecycleMonitorBlocked:
459
+ self.connection.execute("""
460
+ UPDATE cms_source_lifecycle_monitor
461
+ SET state = 'failed', last_error_code =
462
+ 'lifecycle_monitor.state_invalid', updated_at = ?
463
+ WHERE event_id = ?
464
+ """, (now, row["event_id"]))
465
+ integrity_error = "lifecycle_monitor.state_invalid"
466
+ else:
467
+ token = secrets.token_urlsafe(32)
468
+ expires = now + lease_seconds
469
+ updated = self.connection.execute("""
470
+ UPDATE cms_source_lifecycle_monitor
471
+ SET state = 'leased', poll_attempts = poll_attempts + 1,
472
+ lease_owner = ?, lease_token = ?,
473
+ lease_expires_at = ?, last_error_code = NULL,
474
+ updated_at = ?
475
+ WHERE event_id = ?
476
+ AND state IN ('pending', 'watching', 'retry_wait')
477
+ """, (worker_id, token, expires, now, row["event_id"]))
478
+ if updated.rowcount != 1:
479
+ raise LifecycleMonitorBlocked(
480
+ "lifecycle_monitor.claim_lost"
481
+ )
482
+ claim = LifecyclePollClaim(
483
+ event_id=row["event_id"], site_id=row["site_id"],
484
+ plan_id=row["plan_id"],
485
+ website_version=row["website_version"],
486
+ source_sequence=int(row["source_sequence"]),
487
+ job_count=int(row["job_count"]),
488
+ change_sha256=row["change_sha256"],
489
+ attempt=int(row["poll_attempts"]) + 1,
490
+ max_consecutive_failures=int(
491
+ row["max_consecutive_failures"]
492
+ ),
493
+ lease_owner=worker_id, lease_token=token,
494
+ lease_expires_at=expires,
495
+ )
496
+ if integrity_error is not None:
497
+ raise LifecycleMonitorBlocked(integrity_error)
498
+ return claim
499
+
500
+ def run_once(
501
+ self,
502
+ client: Any,
503
+ worker_id: str,
504
+ *,
505
+ now: float | int,
506
+ lease_seconds: float | int = 600,
507
+ ) -> LifecyclePollOutcome | None:
508
+ read = getattr(client, "lifecycle", None)
509
+ if not callable(read):
510
+ raise LifecycleMonitorBlocked("lifecycle_monitor.client_invalid")
511
+ lease_seconds = _DISPATCH._duration(
512
+ lease_seconds, "lifecycle_monitor.lease_invalid",
513
+ maximum=MAX_LEASE_SECONDS,
514
+ )
515
+ timeout = getattr(client, "timeout", None)
516
+ if timeout is not None and (
517
+ isinstance(timeout, bool)
518
+ or not isinstance(timeout, (int, float))
519
+ or not math.isfinite(float(timeout))
520
+ or timeout <= 0
521
+ ):
522
+ raise LifecycleMonitorBlocked("lifecycle_monitor.client_invalid")
523
+ if timeout is not None and lease_seconds <= float(timeout):
524
+ raise LifecycleMonitorBlocked("lifecycle_monitor.lease_too_short")
525
+ claim = self.claim(worker_id, now=now, lease_seconds=lease_seconds)
526
+ if claim is None:
527
+ return None
528
+ try:
529
+ # No request_id is supplied: the secure client creates and signs a
530
+ # fresh purpose-bound request for every durable polling attempt.
531
+ response = read(claim.event_id, claim.site_id)
532
+ status = self.complete(claim, response, now=now)
533
+ except LifecycleMonitorBlocked as error:
534
+ if error.code != "lifecycle_monitor.response_invalid":
535
+ raise
536
+ status = self.fail(claim, error.code, now=now)
537
+ except Exception as error:
538
+ if getattr(error, "cms_client_failure", False) is True:
539
+ code = getattr(error, "code", None)
540
+ retryable = getattr(error, "retryable", None)
541
+ try:
542
+ code, _ = _DISPATCH._error(code)
543
+ except LifecycleMonitorBlocked:
544
+ code, retryable = "lifecycle_monitor.client_failure", False
545
+ status = (
546
+ self.retry(claim, code, now=now)
547
+ if retryable is True
548
+ else self.fail(claim, code, now=now)
549
+ )
550
+ else:
551
+ status = self.fail(
552
+ claim, "lifecycle_monitor.client_failure", now=now,
553
+ )
554
+ return LifecyclePollOutcome(
555
+ event_id=status.event_id, state=status.state,
556
+ remote_status=status.remote_status, attempt=status.poll_attempts,
557
+ consecutive_failures=status.consecutive_failures,
558
+ next_poll_at=status.next_poll_at,
559
+ error_code=status.last_error_code,
560
+ )
561
+
562
+ def complete(
563
+ self,
564
+ claim: LifecyclePollClaim,
565
+ response: Any,
566
+ *,
567
+ now: float | int,
568
+ ) -> LifecycleMonitorStatus:
569
+ self._validate_schema()
570
+ claim = self._claim(claim)
571
+ now = _DISPATCH._timestamp(now, "lifecycle_monitor.time_invalid")
572
+ copied, response_json = self._validated_response(claim, response)
573
+ remote_status = copied["status"]
574
+ terminal = remote_status in TERMINAL_SUCCESS | TERMINAL_FAILURE
575
+ snapshot = (
576
+ _canonical(copied["required_locales"], "lifecycle_monitor.response_invalid"),
577
+ _canonical(copied["approved_locales"], "lifecycle_monitor.response_invalid"),
578
+ _canonical(copied["blocked_locales"], "lifecycle_monitor.response_invalid"),
579
+ _canonical(copied["queue_counts"], "lifecycle_monitor.response_invalid"),
580
+ _canonical(copied["delivery"], "lifecycle_monitor.response_invalid"),
581
+ _canonical(copied["tombstone"], "lifecycle_monitor.response_invalid"),
582
+ )
583
+ with _transaction(self.connection):
584
+ self._require_live_claim(claim, now)
585
+ updated = self.connection.execute("""
586
+ UPDATE cms_source_lifecycle_monitor
587
+ SET state = ?, consecutive_failures = 0,
588
+ next_poll_at = ?, lease_owner = NULL, lease_token = NULL,
589
+ lease_expires_at = NULL, last_error_code = NULL,
590
+ remote_status = ?, lifecycle_sha256 = ?, lifecycle_json = ?,
591
+ required_locales_json = ?, approved_locales_json = ?,
592
+ blocked_locales_json = ?, queue_counts_json = ?,
593
+ delivery_json = ?, tombstone_json = ?, updated_at = ?
594
+ WHERE event_id = ? AND state = 'leased'
595
+ AND lease_owner = ? AND lease_token = ?
596
+ """, (
597
+ "terminal" if terminal else "watching",
598
+ now if terminal else now + self.poll_interval_seconds,
599
+ remote_status, _DISPATCH._hash(response_json), response_json,
600
+ *snapshot, now,
601
+ claim.event_id, claim.lease_owner, claim.lease_token,
602
+ ))
603
+ if updated.rowcount != 1:
604
+ raise LifecycleMonitorBlocked(
605
+ "lifecycle_monitor.completion_lost"
606
+ )
607
+ return self.status(claim.event_id, now=now)
608
+
609
+ def retry(
610
+ self, claim: LifecyclePollClaim, error_code: str, *, now: float | int,
611
+ ) -> LifecycleMonitorStatus:
612
+ return self._finish_failure(claim, error_code, retryable=True, now=now)
613
+
614
+ def fail(
615
+ self, claim: LifecyclePollClaim, error_code: str, *, now: float | int,
616
+ ) -> LifecycleMonitorStatus:
617
+ return self._finish_failure(claim, error_code, retryable=False, now=now)
618
+
619
+ def _finish_failure(
620
+ self,
621
+ claim: LifecyclePollClaim,
622
+ error_code: str,
623
+ *,
624
+ retryable: bool,
625
+ now: float | int,
626
+ ) -> LifecycleMonitorStatus:
627
+ self._validate_schema()
628
+ claim = self._claim(claim)
629
+ error_code, _ = _DISPATCH._error(error_code)
630
+ now = _DISPATCH._timestamp(now, "lifecycle_monitor.time_invalid")
631
+ with _transaction(self.connection):
632
+ row = self._require_live_claim(claim, now)
633
+ failures = int(row["consecutive_failures"]) + 1
634
+ terminal = not retryable or failures >= int(
635
+ row["max_consecutive_failures"]
636
+ )
637
+ updated = self.connection.execute("""
638
+ UPDATE cms_source_lifecycle_monitor
639
+ SET state = ?, consecutive_failures = ?, next_poll_at = ?,
640
+ lease_owner = NULL, lease_token = NULL,
641
+ lease_expires_at = NULL, last_error_code = ?, updated_at = ?
642
+ WHERE event_id = ? AND state = 'leased'
643
+ AND lease_owner = ? AND lease_token = ?
644
+ """, (
645
+ "failed" if terminal else "retry_wait", failures,
646
+ now if terminal else now + self._retry_delay(failures),
647
+ error_code, now, claim.event_id, claim.lease_owner,
648
+ claim.lease_token,
649
+ ))
650
+ if updated.rowcount != 1:
651
+ raise LifecycleMonitorBlocked("lifecycle_monitor.failure_lost")
652
+ return self.status(claim.event_id, now=now)
653
+
654
+ def status(
655
+ self, event_id: str, *, now: float | int,
656
+ ) -> LifecycleMonitorStatus:
657
+ self._validate_schema()
658
+ event_id = _DISPATCH._token(
659
+ event_id, "lifecycle_monitor.event_invalid",
660
+ )
661
+ now = _DISPATCH._timestamp(now, "lifecycle_monitor.time_invalid")
662
+ row = self.connection.execute(
663
+ "SELECT * FROM cms_source_lifecycle_monitor WHERE event_id = ?",
664
+ (event_id,),
665
+ ).fetchone()
666
+ if row is None:
667
+ raise LifecycleMonitorBlocked("lifecycle_monitor.event_missing")
668
+ self._validated_row(row)
669
+ lease = row["lease_expires_at"]
670
+ return LifecycleMonitorStatus(
671
+ event_id=row["event_id"], site_id=row["site_id"],
672
+ plan_id=row["plan_id"], website_version=row["website_version"],
673
+ source_sequence=int(row["source_sequence"]),
674
+ job_count=int(row["job_count"]),
675
+ change_sha256=row["change_sha256"],
676
+ binding_sha256=row["binding_sha256"], state=row["state"],
677
+ poll_attempts=int(row["poll_attempts"]),
678
+ consecutive_failures=int(row["consecutive_failures"]),
679
+ max_consecutive_failures=int(row["max_consecutive_failures"]),
680
+ next_poll_at=float(row["next_poll_at"]),
681
+ lease_expires_at=None if lease is None else float(lease),
682
+ lease_expired=(
683
+ row["state"] == "leased" and float(lease) <= now
684
+ ),
685
+ last_error_code=row["last_error_code"],
686
+ remote_status=row["remote_status"],
687
+ lifecycle_sha256=row["lifecycle_sha256"],
688
+ required_locales=tuple(self._stored(row, "required_locales_json", [])),
689
+ approved_locales=tuple(self._stored(row, "approved_locales_json", [])),
690
+ blocked_locales=tuple(
691
+ tuple(item) for item in self._stored(
692
+ row, "blocked_locales_json", [],
693
+ )
694
+ ),
695
+ queue_counts=dict(self._stored(row, "queue_counts_json", {})),
696
+ delivery=self._stored(row, "delivery_json", None),
697
+ tombstone=self._stored(row, "tombstone_json", None),
698
+ )
699
+
700
+ def health(self, *, now: float | int) -> LifecycleMonitorHealth:
701
+ now = _DISPATCH._timestamp(now, "lifecycle_monitor.time_invalid")
702
+ self._validate_schema()
703
+ quick = self.connection.execute("PRAGMA quick_check").fetchall()
704
+ if len(quick) != 1 or tuple(quick[0]) != ("ok",):
705
+ raise LifecycleMonitorBlocked("lifecycle_monitor.database_integrity")
706
+ rows = self.connection.execute(
707
+ "SELECT * FROM cms_source_lifecycle_monitor ORDER BY event_id"
708
+ ).fetchall()
709
+ for row in rows:
710
+ self._validated_row(row)
711
+ counts = {state: 0 for state in STATES}
712
+ for row in rows:
713
+ counts[row["state"]] += 1
714
+ due = sum(
715
+ 1 for row in rows
716
+ if row["state"] in {"pending", "watching", "retry_wait"}
717
+ and row["next_poll_at"] <= now
718
+ )
719
+ expired = sum(
720
+ 1 for row in rows
721
+ if row["state"] == "leased" and row["lease_expires_at"] <= now
722
+ )
723
+ remote_failures = sum(
724
+ 1 for row in rows if row["remote_status"] in TERMINAL_FAILURE
725
+ )
726
+ failed = counts["failed"]
727
+ return LifecycleMonitorHealth(
728
+ status="blocked" if expired or failed or remote_failures else "ok",
729
+ counts=counts, due=due, expired_leases=expired, failed=failed,
730
+ remote_failures=remote_failures,
731
+ )
732
+
733
+ def _validated_response(
734
+ self, claim: LifecyclePollClaim, response: Any,
735
+ ) -> tuple[dict[str, Any], str]:
736
+ if not isinstance(response, Mapping):
737
+ raise LifecycleMonitorBlocked("lifecycle_monitor.response_invalid")
738
+ response_json = _canonical(
739
+ dict(response), "lifecycle_monitor.response_invalid",
740
+ )
741
+ copied = _json(response_json, "lifecycle_monitor.response_invalid")
742
+ expected = {
743
+ "schema", "request_id", "event_id", "site_id", "plan_id",
744
+ "website_version", "source_sequence", "status",
745
+ "required_locales", "approved_locales", "blocked_locales",
746
+ "queue_counts", "delivery", "tombstone",
747
+ }
748
+ client = _DISPATCH._CLIENT
749
+ if (
750
+ not isinstance(copied, dict)
751
+ or set(copied) != expected
752
+ or copied.get("schema") != client._API.LIFECYCLE_RESPONSE_SCHEMA
753
+ or copied.get("event_id") != claim.event_id
754
+ or copied.get("site_id") != claim.site_id
755
+ or copied.get("plan_id") != claim.plan_id
756
+ or copied.get("website_version") != claim.website_version
757
+ or copied.get("source_sequence") != claim.source_sequence
758
+ or copied.get("status") not in REMOTE_STATUSES - {"superseded"}
759
+ or not client.CMSLocalizationHTTPClient._valid_lifecycle(copied)
760
+ or len(copied.get("required_locales", [])) != claim.job_count
761
+ ):
762
+ raise LifecycleMonitorBlocked("lifecycle_monitor.response_invalid")
763
+ return copied, response_json
764
+
765
+ def _validated_row(self, row: sqlite3.Row) -> None:
766
+ try:
767
+ event_id = _DISPATCH._token(
768
+ row["event_id"], "lifecycle_monitor.state_invalid",
769
+ )
770
+ site_id = _DISPATCH._token(
771
+ row["site_id"], "lifecycle_monitor.state_invalid",
772
+ )
773
+ plan_id = _DISPATCH._token(
774
+ row["plan_id"], "lifecycle_monitor.state_invalid",
775
+ )
776
+ website_version = _DISPATCH._token(
777
+ row["website_version"], "lifecycle_monitor.state_invalid",
778
+ )
779
+ created = _DISPATCH._timestamp(
780
+ row["created_at"], "lifecycle_monitor.state_invalid",
781
+ )
782
+ updated = _DISPATCH._timestamp(
783
+ row["updated_at"], "lifecycle_monitor.state_invalid",
784
+ )
785
+ next_poll = _DISPATCH._timestamp(
786
+ row["next_poll_at"], "lifecycle_monitor.state_invalid",
787
+ )
788
+ except (TypeError, LifecycleMonitorBlocked):
789
+ raise LifecycleMonitorBlocked("lifecycle_monitor.state_invalid") from None
790
+ state = row["state"]
791
+ attempts = row["poll_attempts"]
792
+ failures = row["consecutive_failures"]
793
+ maximum = row["max_consecutive_failures"]
794
+ source_sequence = row["source_sequence"]
795
+ job_count = row["job_count"]
796
+ error = row["last_error_code"]
797
+ remote = row["remote_status"]
798
+ leases = (row["lease_owner"], row["lease_token"], row["lease_expires_at"])
799
+ snapshot_columns = (
800
+ "lifecycle_sha256", "lifecycle_json", "required_locales_json",
801
+ "approved_locales_json", "blocked_locales_json",
802
+ "queue_counts_json", "delivery_json", "tombstone_json",
803
+ )
804
+ snapshot = tuple(row[name] for name in snapshot_columns)
805
+ has_snapshot = all(item is not None for item in snapshot)
806
+ immediate_terminal = (
807
+ state == "terminal" and remote in {"cancelled", "superseded"}
808
+ and not any(item is not None for item in snapshot)
809
+ )
810
+ if has_snapshot:
811
+ response = self._stored(row, "lifecycle_json", None)
812
+ required = self._stored(row, "required_locales_json", None)
813
+ approved = self._stored(row, "approved_locales_json", None)
814
+ blocked = self._stored(row, "blocked_locales_json", None)
815
+ counts = self._stored(row, "queue_counts_json", None)
816
+ delivery = self._stored(row, "delivery_json", None)
817
+ tombstone = self._stored(row, "tombstone_json", None)
818
+ expected_response_keys = {
819
+ "schema", "request_id", "event_id", "site_id", "plan_id",
820
+ "website_version", "source_sequence", "status",
821
+ "required_locales", "approved_locales", "blocked_locales",
822
+ "queue_counts", "delivery", "tombstone",
823
+ }
824
+ valid_snapshot = (
825
+ isinstance(response, dict)
826
+ and set(response) == expected_response_keys
827
+ and response.get("schema")
828
+ == _DISPATCH._CLIENT._API.LIFECYCLE_RESPONSE_SCHEMA
829
+ and isinstance(response.get("request_id"), str)
830
+ and _DISPATCH.TOKEN.fullmatch(response["request_id"]) is not None
831
+ and response.get("event_id") == event_id
832
+ and response.get("site_id") == site_id
833
+ and response.get("plan_id") == plan_id
834
+ and response.get("website_version") == website_version
835
+ and response.get("source_sequence") == source_sequence
836
+ and response.get("status") == remote
837
+ and response.get("required_locales") == required
838
+ and response.get("approved_locales") == approved
839
+ and response.get("blocked_locales") == blocked
840
+ and response.get("queue_counts") == counts
841
+ and response.get("delivery") == delivery
842
+ and response.get("tombstone") == tombstone
843
+ and _DISPATCH._CLIENT.CMSLocalizationHTTPClient._valid_lifecycle(
844
+ response
845
+ )
846
+ and len(required) == job_count
847
+ and row["lifecycle_sha256"]
848
+ == _DISPATCH._hash(row["lifecycle_json"])
849
+ )
850
+ else:
851
+ valid_snapshot = not any(item is not None for item in snapshot)
852
+ if (
853
+ event_id != row["event_id"] or site_id != row["site_id"]
854
+ or plan_id != row["plan_id"]
855
+ or website_version != row["website_version"]
856
+ or state not in STATES
857
+ or isinstance(source_sequence, bool)
858
+ or not isinstance(source_sequence, int) or source_sequence <= 0
859
+ or isinstance(job_count, bool) or not isinstance(job_count, int)
860
+ or job_count <= 0
861
+ or not isinstance(row["change_sha256"], str)
862
+ or _DISPATCH.SHA256.fullmatch(row["change_sha256"]) is None
863
+ or not isinstance(row["binding_sha256"], str)
864
+ or _DISPATCH.SHA256.fullmatch(row["binding_sha256"]) is None
865
+ or row["binding_sha256"] != _binding(
866
+ event_id, site_id, plan_id, website_version,
867
+ source_sequence, job_count, row["change_sha256"], maximum,
868
+ )
869
+ or isinstance(attempts, bool) or not isinstance(attempts, int)
870
+ or attempts < 0
871
+ or isinstance(failures, bool) or not isinstance(failures, int)
872
+ or isinstance(maximum, bool) or not isinstance(maximum, int)
873
+ or not 0 <= failures <= maximum <= MAX_FAILURES or maximum < 1
874
+ or next_poll < created or updated < created
875
+ or (state == "leased") != all(item is not None for item in leases)
876
+ or (state != "leased" and any(item is not None for item in leases))
877
+ or (state == "leased" and (
878
+ _DISPATCH._token(
879
+ leases[0], "lifecycle_monitor.state_invalid", limit=128,
880
+ ) != leases[0]
881
+ or _DISPATCH._token(
882
+ leases[1], "lifecycle_monitor.state_invalid",
883
+ ) != leases[1]
884
+ or _DISPATCH._timestamp(
885
+ leases[2], "lifecycle_monitor.state_invalid",
886
+ ) <= updated
887
+ ))
888
+ or (error is not None and (
889
+ not isinstance(error, str)
890
+ or _DISPATCH.ERROR_CODE.fullmatch(error) is None
891
+ ))
892
+ or (state in {"retry_wait", "failed"}) != (error is not None)
893
+ or (remote is not None and remote not in REMOTE_STATUSES)
894
+ or not valid_snapshot
895
+ or (not has_snapshot and remote is not None and not immediate_terminal)
896
+ or (has_snapshot and (
897
+ not isinstance(row["lifecycle_sha256"], str)
898
+ or _DISPATCH.SHA256.fullmatch(row["lifecycle_sha256"]) is None
899
+ ))
900
+ or (remote is None and has_snapshot)
901
+ or (has_snapshot and remote in TERMINAL_SUCCESS | TERMINAL_FAILURE
902
+ and state != "terminal")
903
+ or (state == "watching" and (
904
+ not has_snapshot or remote in TERMINAL_SUCCESS | TERMINAL_FAILURE
905
+ ))
906
+ or (state == "terminal" and not (
907
+ immediate_terminal
908
+ or (has_snapshot and remote in TERMINAL_SUCCESS | TERMINAL_FAILURE)
909
+ ))
910
+ or (state == "pending" and (remote is not None or attempts != 0))
911
+ ):
912
+ raise LifecycleMonitorBlocked("lifecycle_monitor.state_invalid")
913
+
914
+ def _claim(self, claim: Any) -> LifecyclePollClaim:
915
+ if not isinstance(claim, LifecyclePollClaim):
916
+ raise LifecycleMonitorBlocked("lifecycle_monitor.claim_invalid")
917
+ _DISPATCH._token(claim.event_id, "lifecycle_monitor.claim_invalid")
918
+ _DISPATCH._token(claim.site_id, "lifecycle_monitor.claim_invalid")
919
+ _DISPATCH._token(claim.plan_id, "lifecycle_monitor.claim_invalid")
920
+ _DISPATCH._token(
921
+ claim.website_version, "lifecycle_monitor.claim_invalid",
922
+ )
923
+ _DISPATCH._token(
924
+ claim.lease_owner, "lifecycle_monitor.claim_invalid", limit=128,
925
+ )
926
+ _DISPATCH._token(claim.lease_token, "lifecycle_monitor.claim_invalid")
927
+ if (
928
+ isinstance(claim.source_sequence, bool)
929
+ or not isinstance(claim.source_sequence, int)
930
+ or claim.source_sequence <= 0
931
+ or isinstance(claim.job_count, bool)
932
+ or not isinstance(claim.job_count, int) or claim.job_count <= 0
933
+ or not isinstance(claim.change_sha256, str)
934
+ or _DISPATCH.SHA256.fullmatch(claim.change_sha256) is None
935
+ or isinstance(claim.attempt, bool)
936
+ or not isinstance(claim.attempt, int) or claim.attempt <= 0
937
+ or isinstance(claim.max_consecutive_failures, bool)
938
+ or not isinstance(claim.max_consecutive_failures, int)
939
+ or not 1 <= claim.max_consecutive_failures <= MAX_FAILURES
940
+ ):
941
+ raise LifecycleMonitorBlocked("lifecycle_monitor.claim_invalid")
942
+ _DISPATCH._timestamp(
943
+ claim.lease_expires_at, "lifecycle_monitor.claim_invalid",
944
+ )
945
+ return claim
946
+
947
+ def _require_live_claim(
948
+ self, claim: LifecyclePollClaim, now: float,
949
+ ) -> sqlite3.Row:
950
+ row = self.connection.execute(
951
+ "SELECT * FROM cms_source_lifecycle_monitor WHERE event_id = ?",
952
+ (claim.event_id,),
953
+ ).fetchone()
954
+ if row is None:
955
+ raise LifecycleMonitorBlocked("lifecycle_monitor.claim_missing")
956
+ self._validated_row(row)
957
+ if (
958
+ row["state"] != "leased"
959
+ or row["site_id"] != claim.site_id
960
+ or row["plan_id"] != claim.plan_id
961
+ or row["website_version"] != claim.website_version
962
+ or row["source_sequence"] != claim.source_sequence
963
+ or row["job_count"] != claim.job_count
964
+ or row["change_sha256"] != claim.change_sha256
965
+ or row["poll_attempts"] != claim.attempt
966
+ or row["max_consecutive_failures"] != claim.max_consecutive_failures
967
+ or row["lease_owner"] != claim.lease_owner
968
+ or row["lease_token"] != claim.lease_token
969
+ or row["lease_expires_at"] != claim.lease_expires_at
970
+ ):
971
+ raise LifecycleMonitorBlocked("lifecycle_monitor.claim_lost")
972
+ if row["lease_expires_at"] <= now:
973
+ raise LifecycleMonitorBlocked("lifecycle_monitor.lease_expired")
974
+ if now < row["updated_at"]:
975
+ raise LifecycleMonitorBlocked("lifecycle_monitor.clock_regressed")
976
+ return row
977
+
978
+ def _stored(self, row: sqlite3.Row, name: str, empty: Any) -> Any:
979
+ raw = row[name]
980
+ if raw is None:
981
+ return empty
982
+ value = _json(raw, "lifecycle_monitor.state_invalid")
983
+ if _canonical(value, "lifecycle_monitor.state_invalid") != raw:
984
+ raise LifecycleMonitorBlocked("lifecycle_monitor.state_invalid")
985
+ return value
986
+
987
+ def _retry_delay(self, failures: int) -> float:
988
+ return min(
989
+ self.max_delay_seconds,
990
+ self.base_delay_seconds * (2 ** max(0, failures - 1)),
991
+ )