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,1125 @@
1
+ #!/usr/bin/env python3
2
+ """Durable source-CMS service for automatic localization lifecycle delivery.
3
+
4
+ The source website owns three independent SQLite connections and the configured
5
+ HTTPS client. One tick performs at most one network operation, prioritizing
6
+ removals before new changes and lifecycle polling. Successful change dispatches
7
+ are reconciled into the lifecycle monitor before more network work is started,
8
+ so a crash between remote acceptance and local registration is recoverable.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import importlib.util
14
+ import json
15
+ import math
16
+ import re
17
+ import sqlite3
18
+ import sys
19
+ import time
20
+ from dataclasses import asdict, dataclass
21
+ from pathlib import Path
22
+ from typing import Any, Callable, Mapping
23
+
24
+
25
+ SCHEMA = "blun.cms-source-service-tick.v1"
26
+ HEALTH_SCHEMA = "blun.cms-source-service-health.v3"
27
+ STATUS_SCHEMA = "blun.cms-source-service-status.v3"
28
+ TOKEN = re.compile(r"^[A-Za-z0-9_.:-]{1,256}$")
29
+ ERROR_CODE = re.compile(r"^[a-z][a-z0-9_.-]{0,127}$")
30
+
31
+
32
+ def _load_module(name: str, path: Path):
33
+ spec = importlib.util.spec_from_file_location(name, path)
34
+ if spec is None or spec.loader is None:
35
+ raise RuntimeError(f"cannot load source service dependency: {path.name}")
36
+ module = importlib.util.module_from_spec(spec)
37
+ sys.modules[spec.name] = module
38
+ spec.loader.exec_module(module)
39
+ return module
40
+
41
+
42
+ _ROOT = Path(__file__).resolve().parents[1]
43
+ _LIFECYCLE = _load_module(
44
+ "blun_website_localization_cms_source_service_lifecycle",
45
+ _ROOT / "integrations" / "website_localization_cms_lifecycle_monitor.py",
46
+ )
47
+ _DISPATCH = _LIFECYCLE._DISPATCH
48
+ _REMOVAL = _load_module(
49
+ "blun_website_localization_cms_source_service_removal",
50
+ _ROOT / "integrations" / "website_localization_cms_removal_dispatch.py",
51
+ )
52
+ _NOTIFICATION = _load_module(
53
+ "blun_website_localization_cms_source_service_terminal_notification",
54
+ _ROOT / "integrations" / "website_localization_cms_terminal_notification.py",
55
+ )
56
+ _PROCESSING = _load_module(
57
+ "blun_website_localization_cms_source_service_terminal_processing",
58
+ _ROOT / "integrations" / "website_localization_cms_terminal_processing_monitor.py",
59
+ )
60
+
61
+
62
+ class CMSSourceServiceBlocked(RuntimeError):
63
+ """Stable source-service failure containing no customer content."""
64
+
65
+ def __init__(self, code: str):
66
+ if not isinstance(code, str) or ERROR_CODE.fullmatch(code) is None:
67
+ code = "source_service.blocked"
68
+ super().__init__(code)
69
+ self.code = code
70
+
71
+
72
+ @dataclass(frozen=True)
73
+ class CMSSourceTickOutcome:
74
+ schema: str
75
+ phase: str
76
+ status: str
77
+ operation: str | None = None
78
+ event_id: str | None = None
79
+ request_id: str | None = None
80
+ attempt: int | None = None
81
+ error_code: str | None = None
82
+
83
+ def as_payload(self) -> dict[str, Any]:
84
+ return asdict(self)
85
+
86
+
87
+ @dataclass(frozen=True)
88
+ class CMSSourceServiceHealth:
89
+ schema: str
90
+ status: str
91
+ pending_lifecycle_registrations: int
92
+ pending_terminal_notifications: int
93
+ pending_terminal_processing: int
94
+ changes: Mapping[str, Any]
95
+ removals: Mapping[str, Any]
96
+ lifecycle: Mapping[str, Any]
97
+ notifications: Mapping[str, Any]
98
+ terminal_processing: Mapping[str, Any]
99
+ error_code: str | None = None
100
+
101
+ def as_payload(self) -> dict[str, Any]:
102
+ return {
103
+ "schema": self.schema,
104
+ "status": self.status,
105
+ "pending_lifecycle_registrations": (
106
+ self.pending_lifecycle_registrations
107
+ ),
108
+ "pending_terminal_notifications": (
109
+ self.pending_terminal_notifications
110
+ ),
111
+ "pending_terminal_processing": self.pending_terminal_processing,
112
+ "changes": dict(self.changes),
113
+ "removals": dict(self.removals),
114
+ "lifecycle": dict(self.lifecycle),
115
+ "notifications": dict(self.notifications),
116
+ "terminal_processing": dict(self.terminal_processing),
117
+ "error_code": self.error_code,
118
+ }
119
+
120
+
121
+ @dataclass(frozen=True)
122
+ class CMSSourceServiceStatus:
123
+ schema: str
124
+ event_id: str
125
+ site_id: str
126
+ website_version: str
127
+ source_sequence: int
128
+ change_sha256: str
129
+ dispatch_status: str
130
+ dispatch_attempts: int
131
+ dispatch_max_attempts: int
132
+ dispatch_error_code: str | None
133
+ plan_id: str | None
134
+ job_count: int | None
135
+ lifecycle_state: str | None
136
+ lifecycle_poll_attempts: int
137
+ lifecycle_error_code: str | None
138
+ remote_status: str | None
139
+ lifecycle_sha256: str | None
140
+ required_locales: tuple[str, ...]
141
+ approved_locales: tuple[str, ...]
142
+ blocked_locales: tuple[tuple[str, str], ...]
143
+ queue_counts: Mapping[str, int]
144
+ notification_state: str
145
+ notification_id: str | None
146
+ notification_sha256: str | None
147
+ notification_attempts: int
148
+ notification_max_attempts: int | None
149
+ notification_error_code: str | None
150
+ terminal_processing_state: str
151
+ terminal_processing_poll_attempts: int
152
+ terminal_processing_failures: int
153
+ terminal_processing_error_code: str | None
154
+ receiver_processing_state: str | None
155
+ receiver_processing_attempts: int | None
156
+ receiver_processing_max_attempts: int | None
157
+ receiver_processing_error_code: str | None
158
+ receiver_processed_at: float | None
159
+
160
+ def as_payload(self) -> dict[str, Any]:
161
+ return {
162
+ "schema": self.schema,
163
+ "event_id": self.event_id,
164
+ "site_id": self.site_id,
165
+ "website_version": self.website_version,
166
+ "source_sequence": self.source_sequence,
167
+ "change_sha256": self.change_sha256,
168
+ "dispatch_status": self.dispatch_status,
169
+ "dispatch_attempts": self.dispatch_attempts,
170
+ "dispatch_max_attempts": self.dispatch_max_attempts,
171
+ "dispatch_error_code": self.dispatch_error_code,
172
+ "plan_id": self.plan_id,
173
+ "job_count": self.job_count,
174
+ "lifecycle_state": self.lifecycle_state,
175
+ "lifecycle_poll_attempts": self.lifecycle_poll_attempts,
176
+ "lifecycle_error_code": self.lifecycle_error_code,
177
+ "remote_status": self.remote_status,
178
+ "lifecycle_sha256": self.lifecycle_sha256,
179
+ "required_locales": list(self.required_locales),
180
+ "approved_locales": list(self.approved_locales),
181
+ "blocked_locales": [list(item) for item in self.blocked_locales],
182
+ "queue_counts": dict(self.queue_counts),
183
+ "notification_state": self.notification_state,
184
+ "notification_id": self.notification_id,
185
+ "notification_sha256": self.notification_sha256,
186
+ "notification_attempts": self.notification_attempts,
187
+ "notification_max_attempts": self.notification_max_attempts,
188
+ "notification_error_code": self.notification_error_code,
189
+ "terminal_processing_state": self.terminal_processing_state,
190
+ "terminal_processing_poll_attempts": (
191
+ self.terminal_processing_poll_attempts
192
+ ),
193
+ "terminal_processing_failures": self.terminal_processing_failures,
194
+ "terminal_processing_error_code": self.terminal_processing_error_code,
195
+ "receiver_processing_state": self.receiver_processing_state,
196
+ "receiver_processing_attempts": self.receiver_processing_attempts,
197
+ "receiver_processing_max_attempts": (
198
+ self.receiver_processing_max_attempts
199
+ ),
200
+ "receiver_processing_error_code": self.receiver_processing_error_code,
201
+ "receiver_processed_at": self.receiver_processed_at,
202
+ }
203
+
204
+
205
+ def _identifier(value: Any, code: str) -> str:
206
+ if not isinstance(value, str) or TOKEN.fullmatch(value) is None:
207
+ raise CMSSourceServiceBlocked(code)
208
+ return value
209
+
210
+
211
+ def _positive_duration(value: Any, code: str) -> float:
212
+ if (
213
+ isinstance(value, bool)
214
+ or not isinstance(value, (int, float))
215
+ or not math.isfinite(float(value))
216
+ or float(value) <= 0
217
+ or float(value) > 86_400
218
+ ):
219
+ raise CMSSourceServiceBlocked(code)
220
+ return float(value)
221
+
222
+
223
+ def _positive_integer(value: Any, code: str, maximum: int) -> int:
224
+ if (
225
+ isinstance(value, bool)
226
+ or not isinstance(value, int)
227
+ or not 1 <= value <= maximum
228
+ ):
229
+ raise CMSSourceServiceBlocked(code)
230
+ return value
231
+
232
+
233
+ def _safe_code(error: Exception, fallback: str) -> str:
234
+ code = getattr(error, "code", None)
235
+ return code if isinstance(code, str) and ERROR_CODE.fullmatch(code) else fallback
236
+
237
+
238
+ def _health_payload(value: Any) -> dict[str, Any]:
239
+ try:
240
+ payload = asdict(value)
241
+ except (TypeError, ValueError):
242
+ raise CMSSourceServiceBlocked("source_service.health_invalid") from None
243
+ if (
244
+ not isinstance(payload, dict)
245
+ or payload.get("status") not in {"ok", "blocked"}
246
+ ):
247
+ raise CMSSourceServiceBlocked("source_service.health_invalid")
248
+ return payload
249
+
250
+
251
+ class CMSLocalizationSourceService:
252
+ """Compose durable source outboxes and lifecycle monitoring into one loop."""
253
+
254
+ def __init__(
255
+ self,
256
+ change_connection: sqlite3.Connection,
257
+ removal_connection: sqlite3.Connection,
258
+ lifecycle_connection: sqlite3.Connection,
259
+ client: Any,
260
+ *,
261
+ change_worker_id: str,
262
+ removal_worker_id: str,
263
+ lifecycle_worker_id: str,
264
+ terminal_notifier: Callable[[Mapping[str, Any]], Any] | None = None,
265
+ terminal_status_reader: Callable[[str, str], Any] | None = None,
266
+ notification_worker_id: str | None = None,
267
+ clock: Callable[[], float | int] = time.time,
268
+ change_lease_seconds: float | int = 600,
269
+ removal_lease_seconds: float | int = 600,
270
+ lifecycle_lease_seconds: float | int = 600,
271
+ notification_lease_seconds: float | int = 600,
272
+ terminal_processing_lease_seconds: float | int = 600,
273
+ max_lifecycle_failures: int = 5,
274
+ max_notification_attempts: int = 5,
275
+ max_terminal_processing_failures: int = 5,
276
+ dispatch_base_delay_seconds: float | int = 5,
277
+ dispatch_max_delay_seconds: float | int = 300,
278
+ lifecycle_poll_interval_seconds: float | int = 30,
279
+ lifecycle_base_delay_seconds: float | int = 5,
280
+ lifecycle_max_delay_seconds: float | int = 300,
281
+ notification_base_delay_seconds: float | int = 5,
282
+ notification_max_delay_seconds: float | int = 300,
283
+ terminal_processing_poll_interval_seconds: float | int = 30,
284
+ terminal_processing_base_delay_seconds: float | int = 5,
285
+ terminal_processing_max_delay_seconds: float | int = 300,
286
+ ):
287
+ connections = (change_connection, removal_connection, lifecycle_connection)
288
+ if not all(isinstance(item, sqlite3.Connection) for item in connections):
289
+ raise CMSSourceServiceBlocked("source_service.connection_invalid")
290
+ if len({id(item) for item in connections}) != len(connections):
291
+ raise CMSSourceServiceBlocked("source_service.connection_reused")
292
+ methods = (
293
+ "submit_change", "cancel", "request_tombstone", "lifecycle",
294
+ )
295
+ if any(not callable(getattr(client, name, None)) for name in methods):
296
+ raise CMSSourceServiceBlocked("source_service.client_invalid")
297
+ if (terminal_notifier is None) != (notification_worker_id is None):
298
+ raise CMSSourceServiceBlocked("source_service.notification_invalid")
299
+ if terminal_notifier is not None and not callable(terminal_notifier):
300
+ raise CMSSourceServiceBlocked("source_service.notification_invalid")
301
+ if terminal_status_reader is None and terminal_notifier is not None:
302
+ candidate = getattr(terminal_notifier, "status", None)
303
+ terminal_status_reader = candidate if callable(candidate) else None
304
+ if terminal_status_reader is not None and (
305
+ terminal_notifier is None or not callable(terminal_status_reader)
306
+ ):
307
+ raise CMSSourceServiceBlocked("source_service.processing_monitor_invalid")
308
+ if not callable(clock):
309
+ raise CMSSourceServiceBlocked("source_service.clock_invalid")
310
+ timeout = getattr(client, "timeout", None)
311
+ if (
312
+ timeout is not None
313
+ and (
314
+ isinstance(timeout, bool)
315
+ or not isinstance(timeout, (int, float))
316
+ or not math.isfinite(float(timeout))
317
+ or float(timeout) <= 0
318
+ )
319
+ ):
320
+ raise CMSSourceServiceBlocked("source_service.client_invalid")
321
+
322
+ self.change_worker_id = _identifier(
323
+ change_worker_id, "source_service.worker_invalid",
324
+ )
325
+ self.removal_worker_id = _identifier(
326
+ removal_worker_id, "source_service.worker_invalid",
327
+ )
328
+ self.lifecycle_worker_id = _identifier(
329
+ lifecycle_worker_id, "source_service.worker_invalid",
330
+ )
331
+ self.notification_worker_id = (
332
+ None
333
+ if notification_worker_id is None
334
+ else _identifier(
335
+ notification_worker_id, "source_service.worker_invalid",
336
+ )
337
+ )
338
+ self.change_lease_seconds = _positive_duration(
339
+ change_lease_seconds, "source_service.lease_invalid",
340
+ )
341
+ self.removal_lease_seconds = _positive_duration(
342
+ removal_lease_seconds, "source_service.lease_invalid",
343
+ )
344
+ self.lifecycle_lease_seconds = _positive_duration(
345
+ lifecycle_lease_seconds, "source_service.lease_invalid",
346
+ )
347
+ self.notification_lease_seconds = _positive_duration(
348
+ notification_lease_seconds, "source_service.lease_invalid",
349
+ )
350
+ self.terminal_processing_lease_seconds = _positive_duration(
351
+ terminal_processing_lease_seconds, "source_service.lease_invalid",
352
+ )
353
+ if timeout is not None and any(
354
+ lease <= float(timeout) for lease in (
355
+ self.change_lease_seconds,
356
+ self.removal_lease_seconds,
357
+ self.lifecycle_lease_seconds,
358
+ self.notification_lease_seconds,
359
+ self.terminal_processing_lease_seconds,
360
+ )
361
+ ):
362
+ raise CMSSourceServiceBlocked("source_service.lease_too_short")
363
+ self.max_lifecycle_failures = _positive_integer(
364
+ max_lifecycle_failures,
365
+ "source_service.max_lifecycle_failures_invalid",
366
+ _LIFECYCLE.MAX_FAILURES,
367
+ )
368
+ self.max_notification_attempts = _positive_integer(
369
+ max_notification_attempts,
370
+ "source_service.max_notification_attempts_invalid",
371
+ 20,
372
+ )
373
+ self.max_terminal_processing_failures = _positive_integer(
374
+ max_terminal_processing_failures,
375
+ "source_service.max_terminal_processing_failures_invalid",
376
+ 20,
377
+ )
378
+ self.clock = clock
379
+ self.client = client
380
+ self.terminal_notifier = terminal_notifier
381
+ self.terminal_status_reader = terminal_status_reader
382
+
383
+ # All configuration is validated before the first schema write.
384
+ for value, code in (
385
+ (dispatch_base_delay_seconds, "source_service.delay_invalid"),
386
+ (dispatch_max_delay_seconds, "source_service.delay_invalid"),
387
+ (lifecycle_poll_interval_seconds, "source_service.delay_invalid"),
388
+ (lifecycle_base_delay_seconds, "source_service.delay_invalid"),
389
+ (lifecycle_max_delay_seconds, "source_service.delay_invalid"),
390
+ (notification_base_delay_seconds, "source_service.delay_invalid"),
391
+ (notification_max_delay_seconds, "source_service.delay_invalid"),
392
+ (terminal_processing_poll_interval_seconds, "source_service.delay_invalid"),
393
+ (terminal_processing_base_delay_seconds, "source_service.delay_invalid"),
394
+ (terminal_processing_max_delay_seconds, "source_service.delay_invalid"),
395
+ ):
396
+ _positive_duration(value, code)
397
+ if float(dispatch_base_delay_seconds) > float(dispatch_max_delay_seconds):
398
+ raise CMSSourceServiceBlocked("source_service.delay_invalid")
399
+ if float(lifecycle_base_delay_seconds) > float(lifecycle_max_delay_seconds):
400
+ raise CMSSourceServiceBlocked("source_service.delay_invalid")
401
+ if float(notification_base_delay_seconds) > float(notification_max_delay_seconds):
402
+ raise CMSSourceServiceBlocked("source_service.delay_invalid")
403
+ if (
404
+ float(terminal_processing_base_delay_seconds)
405
+ > float(terminal_processing_max_delay_seconds)
406
+ ):
407
+ raise CMSSourceServiceBlocked("source_service.delay_invalid")
408
+
409
+ self.changes = _DISPATCH.DurableCMSChangeDispatcher(
410
+ change_connection,
411
+ base_delay_seconds=dispatch_base_delay_seconds,
412
+ max_delay_seconds=dispatch_max_delay_seconds,
413
+ )
414
+ self.removals = _REMOVAL.DurableCMSRemovalDispatcher(
415
+ removal_connection,
416
+ base_delay_seconds=dispatch_base_delay_seconds,
417
+ max_delay_seconds=dispatch_max_delay_seconds,
418
+ )
419
+ self.lifecycle_monitor = _LIFECYCLE.DurableCMSLifecycleMonitor(
420
+ lifecycle_connection,
421
+ poll_interval_seconds=lifecycle_poll_interval_seconds,
422
+ base_delay_seconds=lifecycle_base_delay_seconds,
423
+ max_delay_seconds=lifecycle_max_delay_seconds,
424
+ )
425
+ self.notifications = (
426
+ None
427
+ if terminal_notifier is None
428
+ else _NOTIFICATION.DurableCMSTerminalNotifier(
429
+ lifecycle_connection,
430
+ base_delay_seconds=notification_base_delay_seconds,
431
+ max_delay_seconds=notification_max_delay_seconds,
432
+ )
433
+ )
434
+ self.terminal_processing = (
435
+ None
436
+ if terminal_status_reader is None
437
+ else _PROCESSING.DurableTerminalProcessingMonitor(
438
+ lifecycle_connection,
439
+ poll_interval_seconds=terminal_processing_poll_interval_seconds,
440
+ base_delay_seconds=terminal_processing_base_delay_seconds,
441
+ max_delay_seconds=terminal_processing_max_delay_seconds,
442
+ )
443
+ )
444
+
445
+ def __repr__(self) -> str:
446
+ return "CMSLocalizationSourceService(configured=True)"
447
+
448
+ def _now(self) -> float:
449
+ try:
450
+ value = self.clock()
451
+ except Exception:
452
+ raise CMSSourceServiceBlocked("source_service.clock_invalid") from None
453
+ if (
454
+ isinstance(value, bool)
455
+ or not isinstance(value, (int, float))
456
+ or not math.isfinite(float(value))
457
+ or float(value) < 0
458
+ ):
459
+ raise CMSSourceServiceBlocked("source_service.clock_invalid")
460
+ return float(value)
461
+
462
+ def enqueue_change(
463
+ self,
464
+ change: Mapping[str, Any],
465
+ *,
466
+ max_attempts: int = 5,
467
+ ) -> Any:
468
+ return self.changes.enqueue(
469
+ change, max_attempts=max_attempts, now=self._now(),
470
+ )
471
+
472
+ def enqueue_removal(
473
+ self,
474
+ request: Mapping[str, Any],
475
+ *,
476
+ max_attempts: int = 5,
477
+ ) -> Any:
478
+ return self.removals.enqueue(
479
+ request, max_attempts=max_attempts, now=self._now(),
480
+ )
481
+
482
+ def status(self, event_id: str, site_id: str) -> CMSSourceServiceStatus:
483
+ """Return a content-free snapshot without repairing or leasing work."""
484
+ event_id = _identifier(event_id, "source_service.status_invalid")
485
+ site_id = _identifier(site_id, "source_service.status_invalid")
486
+ now = self._now()
487
+ row = self.changes.connection.execute(
488
+ "SELECT * FROM cms_source_change_outbox WHERE event_id = ?",
489
+ (event_id,),
490
+ ).fetchone()
491
+ if row is None:
492
+ raise CMSSourceServiceBlocked("source_service.status_not_found")
493
+ try:
494
+ self.changes._validated_row(row)
495
+ change = json.loads(row["payload_json"])
496
+ except Exception as error:
497
+ raise CMSSourceServiceBlocked(
498
+ "source_service.change_state_invalid"
499
+ ) from error
500
+ if (
501
+ not isinstance(change, Mapping)
502
+ or change.get("event_id") != event_id
503
+ or change.get("site_id") != site_id
504
+ ):
505
+ # Deliberately hide whether the event exists for another site.
506
+ raise CMSSourceServiceBlocked("source_service.status_not_found")
507
+
508
+ try:
509
+ dispatch = self.changes.status(event_id, now=now)
510
+ lifecycle = self.lifecycle_monitor.status(event_id, now=now)
511
+ except Exception as error:
512
+ if getattr(error, "code", None) == "lifecycle_monitor.event_missing":
513
+ lifecycle = None
514
+ else:
515
+ raise CMSSourceServiceBlocked(
516
+ "source_service.status_blocked"
517
+ ) from error
518
+ if lifecycle is not None and not self._matching_registration(
519
+ change,
520
+ dispatch,
521
+ lifecycle,
522
+ self.max_lifecycle_failures,
523
+ ):
524
+ raise CMSSourceServiceBlocked(
525
+ "source_service.lifecycle_binding_mismatch"
526
+ )
527
+
528
+ notification = None
529
+ terminal_processing = None
530
+ if self.notifications is None:
531
+ notification_state = "disabled"
532
+ elif lifecycle is None or lifecycle.state != "terminal":
533
+ notification_state = "awaiting_terminal"
534
+ else:
535
+ try:
536
+ notification = self.notifications.status(event_id, now=now)
537
+ except Exception as error:
538
+ if getattr(error, "code", None) != "terminal_notification.missing":
539
+ raise CMSSourceServiceBlocked(
540
+ "source_service.notification_state_invalid"
541
+ ) from error
542
+ notification_state = "awaiting_registration"
543
+ else:
544
+ if notification.payload_sha256 != _NOTIFICATION._payload(
545
+ lifecycle
546
+ )[2]:
547
+ raise CMSSourceServiceBlocked(
548
+ "source_service.notification_binding_mismatch"
549
+ )
550
+ notification_state = notification.status
551
+
552
+ if self.terminal_processing is None:
553
+ terminal_processing_state = "disabled"
554
+ elif notification is None or notification.status != "succeeded":
555
+ terminal_processing_state = "awaiting_notification"
556
+ else:
557
+ try:
558
+ terminal_processing = self.terminal_processing.status(
559
+ event_id, now=now,
560
+ )
561
+ except Exception as error:
562
+ if getattr(error, "code", None) != (
563
+ "terminal_processing_monitor.missing"
564
+ ):
565
+ raise CMSSourceServiceBlocked(
566
+ "source_service.processing_state_invalid"
567
+ ) from error
568
+ terminal_processing_state = "awaiting_registration"
569
+ else:
570
+ if (
571
+ terminal_processing.notification_id
572
+ != notification.notification_id
573
+ or terminal_processing.site_id != site_id
574
+ or terminal_processing.notification_sha256
575
+ != notification.payload_sha256
576
+ ):
577
+ raise CMSSourceServiceBlocked(
578
+ "source_service.processing_binding_mismatch"
579
+ )
580
+ terminal_processing_state = terminal_processing.status
581
+
582
+ return CMSSourceServiceStatus(
583
+ schema=STATUS_SCHEMA,
584
+ event_id=event_id,
585
+ site_id=site_id,
586
+ website_version=change["website_version"],
587
+ source_sequence=change["source_sequence"],
588
+ change_sha256=dispatch.payload_sha256,
589
+ dispatch_status=dispatch.status,
590
+ dispatch_attempts=dispatch.attempts,
591
+ dispatch_max_attempts=dispatch.max_attempts,
592
+ dispatch_error_code=dispatch.last_error_code,
593
+ plan_id=dispatch.remote_plan_id,
594
+ job_count=dispatch.remote_job_count,
595
+ lifecycle_state=None if lifecycle is None else lifecycle.state,
596
+ lifecycle_poll_attempts=(
597
+ 0 if lifecycle is None else lifecycle.poll_attempts
598
+ ),
599
+ lifecycle_error_code=(
600
+ None if lifecycle is None else lifecycle.last_error_code
601
+ ),
602
+ remote_status=None if lifecycle is None else lifecycle.remote_status,
603
+ lifecycle_sha256=(
604
+ None if lifecycle is None else lifecycle.lifecycle_sha256
605
+ ),
606
+ required_locales=(
607
+ () if lifecycle is None else lifecycle.required_locales
608
+ ),
609
+ approved_locales=(
610
+ () if lifecycle is None else lifecycle.approved_locales
611
+ ),
612
+ blocked_locales=(
613
+ () if lifecycle is None else lifecycle.blocked_locales
614
+ ),
615
+ queue_counts={} if lifecycle is None else lifecycle.queue_counts,
616
+ notification_state=notification_state,
617
+ notification_id=(
618
+ None if notification is None else notification.notification_id
619
+ ),
620
+ notification_sha256=(
621
+ None if notification is None else notification.payload_sha256
622
+ ),
623
+ notification_attempts=(
624
+ 0 if notification is None else notification.attempts
625
+ ),
626
+ notification_max_attempts=(
627
+ None if notification is None else notification.max_attempts
628
+ ),
629
+ notification_error_code=(
630
+ None if notification is None else notification.last_error_code
631
+ ),
632
+ terminal_processing_state=terminal_processing_state,
633
+ terminal_processing_poll_attempts=(
634
+ 0 if terminal_processing is None
635
+ else terminal_processing.poll_attempts
636
+ ),
637
+ terminal_processing_failures=(
638
+ 0 if terminal_processing is None else terminal_processing.failures
639
+ ),
640
+ terminal_processing_error_code=(
641
+ None if terminal_processing is None
642
+ else terminal_processing.last_error_code
643
+ ),
644
+ receiver_processing_state=(
645
+ None if terminal_processing is None
646
+ else terminal_processing.receiver_status
647
+ ),
648
+ receiver_processing_attempts=(
649
+ None if terminal_processing is None
650
+ else terminal_processing.receiver_attempts
651
+ ),
652
+ receiver_processing_max_attempts=(
653
+ None if terminal_processing is None
654
+ else terminal_processing.receiver_max_attempts
655
+ ),
656
+ receiver_processing_error_code=(
657
+ None if terminal_processing is None
658
+ else terminal_processing.receiver_error_code
659
+ ),
660
+ receiver_processed_at=(
661
+ None if terminal_processing is None
662
+ else terminal_processing.receiver_processed_at
663
+ ),
664
+ )
665
+
666
+ @staticmethod
667
+ def _matching_registration(
668
+ change: Mapping[str, Any], dispatch: Any, lifecycle: Any,
669
+ max_lifecycle_failures: int,
670
+ ) -> bool:
671
+ return (
672
+ lifecycle.event_id == dispatch.event_id
673
+ and lifecycle.site_id == change.get("site_id")
674
+ and lifecycle.plan_id == dispatch.remote_plan_id
675
+ and lifecycle.website_version == change.get("website_version")
676
+ and lifecycle.source_sequence == change.get("source_sequence")
677
+ and lifecycle.job_count == dispatch.remote_job_count
678
+ and lifecycle.change_sha256 == dispatch.payload_sha256
679
+ and lifecycle.max_consecutive_failures == max_lifecycle_failures
680
+ )
681
+
682
+ def _reconcile_lifecycle(
683
+ self, now: float, *, register: bool,
684
+ ) -> tuple[int, Any | None]:
685
+ pending = 0
686
+ registered = None
687
+ rows = self.changes.connection.execute(
688
+ "SELECT * FROM cms_source_change_outbox "
689
+ "WHERE status = 'succeeded' ORDER BY created_at, event_id"
690
+ ).fetchall()
691
+ for row in rows:
692
+ self.changes._validated_row(row)
693
+ dispatch = self.changes.status(row["event_id"], now=now)
694
+ try:
695
+ change = json.loads(row["payload_json"])
696
+ except (TypeError, json.JSONDecodeError, RecursionError):
697
+ raise CMSSourceServiceBlocked(
698
+ "source_service.change_state_invalid"
699
+ ) from None
700
+ lifecycle = None
701
+ try:
702
+ lifecycle = self.lifecycle_monitor.status(
703
+ dispatch.event_id, now=now,
704
+ )
705
+ except Exception as error:
706
+ if getattr(error, "code", None) != "lifecycle_monitor.event_missing":
707
+ raise
708
+ pending += 1
709
+ if register and registered is None:
710
+ lifecycle = self.lifecycle_monitor.register(
711
+ change,
712
+ dispatch,
713
+ max_consecutive_failures=self.max_lifecycle_failures,
714
+ now=now,
715
+ )
716
+ registered = lifecycle
717
+ pending -= 1
718
+ if lifecycle is not None and not self._matching_registration(
719
+ change,
720
+ dispatch,
721
+ lifecycle,
722
+ self.max_lifecycle_failures,
723
+ ):
724
+ raise CMSSourceServiceBlocked(
725
+ "source_service.lifecycle_binding_mismatch"
726
+ )
727
+ return pending, registered
728
+
729
+ def _reconcile_notifications(
730
+ self, now: float, *, register: bool,
731
+ ) -> tuple[int, Any | None]:
732
+ if self.notifications is None:
733
+ return 0, None
734
+ pending = 0
735
+ registered = None
736
+ rows = self.lifecycle_monitor.connection.execute(
737
+ "SELECT * FROM cms_source_lifecycle_monitor "
738
+ "WHERE state = 'terminal' ORDER BY created_at, event_id"
739
+ ).fetchall()
740
+ for row in rows:
741
+ self.lifecycle_monitor._validated_row(row)
742
+ lifecycle = self.lifecycle_monitor.status(row["event_id"], now=now)
743
+ current = None
744
+ try:
745
+ current = self.notifications.status(lifecycle.event_id, now=now)
746
+ except Exception as error:
747
+ if getattr(error, "code", None) != "terminal_notification.missing":
748
+ raise
749
+ pending += 1
750
+ if register and registered is None:
751
+ current = self.notifications.register(
752
+ lifecycle,
753
+ max_attempts=self.max_notification_attempts,
754
+ now=now,
755
+ )
756
+ registered = current
757
+ pending -= 1
758
+ if current is None:
759
+ continue
760
+ if current.payload_sha256 != _NOTIFICATION._payload(lifecycle)[2]:
761
+ raise CMSSourceServiceBlocked(
762
+ "source_service.notification_binding_mismatch"
763
+ )
764
+ return pending, registered
765
+
766
+ def _reconcile_terminal_processing(
767
+ self, now: float, *, register: bool,
768
+ ) -> tuple[int, Any | None]:
769
+ if self.terminal_processing is None or self.notifications is None:
770
+ return 0, None
771
+ pending = 0
772
+ registered = None
773
+ rows = self.notifications.connection.execute(
774
+ "SELECT * FROM cms_source_terminal_notification "
775
+ "WHERE status = 'succeeded' ORDER BY created_at, event_id"
776
+ ).fetchall()
777
+ for row in rows:
778
+ self.notifications._validated_row(row)
779
+ notification = self.notifications.status(row["event_id"], now=now)
780
+ current = None
781
+ try:
782
+ current = self.terminal_processing.status(
783
+ notification.event_id, now=now,
784
+ )
785
+ except Exception as error:
786
+ if getattr(error, "code", None) != (
787
+ "terminal_processing_monitor.missing"
788
+ ):
789
+ raise
790
+ pending += 1
791
+ if register and registered is None:
792
+ payload = json.loads(row["payload_json"])
793
+ current = self.terminal_processing.register(
794
+ payload,
795
+ notification.payload_sha256,
796
+ max_failures=self.max_terminal_processing_failures,
797
+ now=now,
798
+ )
799
+ registered = current
800
+ pending -= 1
801
+ if current is None:
802
+ continue
803
+ payload = json.loads(row["payload_json"])
804
+ if (
805
+ current.notification_id != notification.notification_id
806
+ or current.site_id != payload.get("site_id")
807
+ or current.notification_sha256 != notification.payload_sha256
808
+ or current.max_failures
809
+ != self.max_terminal_processing_failures
810
+ ):
811
+ raise CMSSourceServiceBlocked(
812
+ "source_service.processing_binding_mismatch"
813
+ )
814
+ return pending, registered
815
+
816
+ @staticmethod
817
+ def _outcome(phase: str, status: str, **values: Any) -> CMSSourceTickOutcome:
818
+ return CMSSourceTickOutcome(SCHEMA, phase, status, **values)
819
+
820
+ def run_once(self) -> CMSSourceTickOutcome:
821
+ """Advance at most one external operation and one local handoff."""
822
+ try:
823
+ now = self._now()
824
+ except Exception as error:
825
+ return self._outcome(
826
+ "source", "blocked",
827
+ error_code=_safe_code(error, "source_service.clock_invalid"),
828
+ )
829
+
830
+ try:
831
+ removal = self.removals.run_once(
832
+ self.client,
833
+ self.removal_worker_id,
834
+ now=now,
835
+ lease_seconds=self.removal_lease_seconds,
836
+ )
837
+ except Exception as error:
838
+ return self._outcome(
839
+ "removal", "blocked",
840
+ error_code=_safe_code(error, "source_service.removal_blocked"),
841
+ )
842
+ if removal is not None:
843
+ return self._outcome(
844
+ "removal", removal.status,
845
+ operation=removal.operation,
846
+ event_id=removal.event_id,
847
+ request_id=removal.request_id,
848
+ attempt=removal.attempt,
849
+ error_code=removal.error_code,
850
+ )
851
+
852
+ try:
853
+ _pending, registered = self._reconcile_lifecycle(now, register=True)
854
+ except Exception as error:
855
+ return self._outcome(
856
+ "lifecycle_registration", "blocked",
857
+ error_code=_safe_code(
858
+ error, "source_service.lifecycle_registration_blocked",
859
+ ),
860
+ )
861
+ if registered is not None:
862
+ return self._outcome(
863
+ "lifecycle_registration", "registered",
864
+ event_id=registered.event_id,
865
+ )
866
+
867
+ if self.notifications is not None:
868
+ try:
869
+ _pending, registered = self._reconcile_notifications(
870
+ now, register=True,
871
+ )
872
+ except Exception as error:
873
+ return self._outcome(
874
+ "notification_registration", "blocked",
875
+ error_code=_safe_code(
876
+ error,
877
+ "source_service.notification_registration_blocked",
878
+ ),
879
+ )
880
+ if registered is not None:
881
+ return self._outcome(
882
+ "notification_registration", "registered",
883
+ event_id=registered.event_id,
884
+ )
885
+ if self.terminal_processing is not None:
886
+ try:
887
+ _pending, processing_registered = (
888
+ self._reconcile_terminal_processing(now, register=True)
889
+ )
890
+ except Exception as error:
891
+ return self._outcome(
892
+ "terminal_processing_registration", "blocked",
893
+ error_code=_safe_code(
894
+ error,
895
+ "source_service.processing_registration_blocked",
896
+ ),
897
+ )
898
+ if processing_registered is not None:
899
+ return self._outcome(
900
+ "terminal_processing_registration", "registered",
901
+ event_id=processing_registered.event_id,
902
+ )
903
+ try:
904
+ processing = self.terminal_processing.run_once(
905
+ self.terminal_status_reader,
906
+ self.notification_worker_id,
907
+ now=now,
908
+ lease_seconds=self.terminal_processing_lease_seconds,
909
+ )
910
+ except Exception as error:
911
+ return self._outcome(
912
+ "terminal_processing", "blocked",
913
+ error_code=_safe_code(
914
+ error, "source_service.processing_blocked",
915
+ ),
916
+ )
917
+ if processing is not None:
918
+ return self._outcome(
919
+ "terminal_processing", processing.status,
920
+ event_id=processing.event_id,
921
+ request_id=processing.notification_id,
922
+ attempt=processing.attempt,
923
+ error_code=processing.error_code,
924
+ )
925
+ try:
926
+ notification = self.notifications.run_once(
927
+ self.terminal_notifier,
928
+ self.notification_worker_id,
929
+ now=now,
930
+ lease_seconds=self.notification_lease_seconds,
931
+ )
932
+ except Exception as error:
933
+ return self._outcome(
934
+ "notification", "blocked",
935
+ error_code=_safe_code(
936
+ error, "source_service.notification_blocked",
937
+ ),
938
+ )
939
+ if notification is not None:
940
+ return self._outcome(
941
+ "notification", notification.status,
942
+ event_id=notification.event_id,
943
+ request_id=notification.notification_id,
944
+ attempt=notification.attempt,
945
+ error_code=notification.error_code,
946
+ )
947
+
948
+ try:
949
+ change = self.changes.run_once(
950
+ self.client,
951
+ self.change_worker_id,
952
+ now=now,
953
+ lease_seconds=self.change_lease_seconds,
954
+ )
955
+ except Exception as error:
956
+ return self._outcome(
957
+ "change", "blocked",
958
+ error_code=_safe_code(error, "source_service.change_blocked"),
959
+ )
960
+ if change is not None:
961
+ if change.status == "succeeded":
962
+ try:
963
+ _pending, registered = self._reconcile_lifecycle(
964
+ now, register=True,
965
+ )
966
+ except Exception as error:
967
+ return self._outcome(
968
+ "lifecycle_registration", "blocked",
969
+ event_id=change.event_id,
970
+ error_code=_safe_code(
971
+ error,
972
+ "source_service.lifecycle_registration_blocked",
973
+ ),
974
+ )
975
+ if registered is None:
976
+ return self._outcome(
977
+ "lifecycle_registration", "blocked",
978
+ event_id=change.event_id,
979
+ error_code="source_service.lifecycle_registration_missing",
980
+ )
981
+ return self._outcome(
982
+ "change", change.status,
983
+ event_id=change.event_id,
984
+ attempt=change.attempt,
985
+ error_code=change.error_code,
986
+ )
987
+
988
+ try:
989
+ lifecycle = self.lifecycle_monitor.run_once(
990
+ self.client,
991
+ self.lifecycle_worker_id,
992
+ now=now,
993
+ lease_seconds=self.lifecycle_lease_seconds,
994
+ )
995
+ except Exception as error:
996
+ return self._outcome(
997
+ "lifecycle", "blocked",
998
+ error_code=_safe_code(error, "source_service.lifecycle_blocked"),
999
+ )
1000
+ if lifecycle is not None:
1001
+ return self._outcome(
1002
+ "lifecycle", lifecycle.state,
1003
+ event_id=lifecycle.event_id,
1004
+ attempt=lifecycle.attempt,
1005
+ error_code=lifecycle.error_code,
1006
+ )
1007
+ return self._outcome("idle", "idle")
1008
+
1009
+ def run_forever(
1010
+ self,
1011
+ stop: Callable[[], bool],
1012
+ *,
1013
+ sleeper: Callable[[float], Any] = time.sleep,
1014
+ active_delay_seconds: float | int = 0.05,
1015
+ idle_delay_seconds: float | int = 1,
1016
+ blocked_delay_seconds: float | int = 5,
1017
+ ) -> None:
1018
+ if not callable(stop) or not callable(sleeper):
1019
+ raise CMSSourceServiceBlocked("source_service.loop_invalid")
1020
+ active = _positive_duration(
1021
+ active_delay_seconds, "source_service.delay_invalid",
1022
+ )
1023
+ idle = _positive_duration(
1024
+ idle_delay_seconds, "source_service.delay_invalid",
1025
+ )
1026
+ blocked = _positive_duration(
1027
+ blocked_delay_seconds, "source_service.delay_invalid",
1028
+ )
1029
+ while not stop():
1030
+ outcome = self.run_once()
1031
+ delay = (
1032
+ blocked if outcome.status in {"blocked", "failed", "retry_wait"}
1033
+ else idle if outcome.status == "idle"
1034
+ else active
1035
+ )
1036
+ sleeper(delay)
1037
+
1038
+ def health(self) -> CMSSourceServiceHealth:
1039
+ try:
1040
+ now = self._now()
1041
+ changes = _health_payload(self.changes.health(now=now))
1042
+ removals = _health_payload(self.removals.health(now=now))
1043
+ lifecycle = _health_payload(self.lifecycle_monitor.health(now=now))
1044
+ pending, _registered = self._reconcile_lifecycle(
1045
+ now, register=False,
1046
+ )
1047
+ notification_pending, _notification_registered = (
1048
+ self._reconcile_notifications(now, register=False)
1049
+ )
1050
+ processing_pending, _processing_registered = (
1051
+ self._reconcile_terminal_processing(now, register=False)
1052
+ )
1053
+ notifications = (
1054
+ {}
1055
+ if self.notifications is None
1056
+ else _health_payload(self.notifications.health(now=now))
1057
+ )
1058
+ terminal_processing = (
1059
+ {}
1060
+ if self.terminal_processing is None
1061
+ else _health_payload(self.terminal_processing.health(now=now))
1062
+ )
1063
+ processing_incomplete = processing_pending + (
1064
+ 0
1065
+ if not terminal_processing
1066
+ else sum(
1067
+ terminal_processing["counts"][state]
1068
+ for state in ("pending", "leased", "watching", "retry_wait")
1069
+ )
1070
+ )
1071
+ except Exception as error:
1072
+ return CMSSourceServiceHealth(
1073
+ HEALTH_SCHEMA,
1074
+ "blocked",
1075
+ 0,
1076
+ 0,
1077
+ 0,
1078
+ {},
1079
+ {},
1080
+ {},
1081
+ {},
1082
+ {},
1083
+ _safe_code(error, "source_service.health_blocked"),
1084
+ )
1085
+ blocked = any(
1086
+ part["status"] == "blocked"
1087
+ for part in (changes, removals, lifecycle)
1088
+ )
1089
+ notification_blocked = (
1090
+ bool(notifications) and notifications["status"] == "blocked"
1091
+ )
1092
+ processing_blocked = (
1093
+ bool(terminal_processing)
1094
+ and terminal_processing["status"] == "blocked"
1095
+ )
1096
+ blocked = blocked or notification_blocked or processing_blocked
1097
+ status = (
1098
+ "blocked" if blocked
1099
+ else "degraded"
1100
+ if pending or notification_pending or processing_incomplete
1101
+ else "ok"
1102
+ )
1103
+ error_code = (
1104
+ "source_service.component_blocked" if blocked
1105
+ else "source_service.lifecycle_registration_pending"
1106
+ if pending
1107
+ else "source_service.notification_registration_pending"
1108
+ if notification_pending
1109
+ else "source_service.processing_registration_pending"
1110
+ if processing_incomplete
1111
+ else None
1112
+ )
1113
+ return CMSSourceServiceHealth(
1114
+ HEALTH_SCHEMA,
1115
+ status,
1116
+ pending,
1117
+ notification_pending,
1118
+ processing_incomplete,
1119
+ changes,
1120
+ removals,
1121
+ lifecycle,
1122
+ notifications,
1123
+ terminal_processing,
1124
+ error_code,
1125
+ )