blun-king-cli 9.1.597 → 9.1.600

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 (106) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +17 -1
  3. package/agent-spine-plugin/CHANGELOG.md +1 -1
  4. package/agent-spine-plugin/docs/host-integration.md +1 -15
  5. package/agent-spine-plugin/docs/preflight-recall.md +1 -1
  6. package/agent-spine-plugin/scripts/check-install.js +1 -6
  7. package/agent-spine-plugin/scripts/hermetic-process.js +8 -0
  8. package/agent-spine-plugin/scripts/release-check.js +2 -3
  9. package/agent-spine-plugin/scripts/run-tests-hermetic.js +8 -11
  10. package/agent-spine-plugin/src/hook.js +40 -40
  11. package/agent-spine-plugin/src/lib/briefing.js +6 -3
  12. package/agent-spine-plugin/src/lib/hook-context.js +63 -64
  13. package/agent-spine-plugin/src/lib/hook-output.js +115 -94
  14. package/agent-spine-plugin/src/lib/hook-timeline.js +20 -11
  15. package/agent-spine-plugin/src/lib/host-instruction-budget.js +8 -14
  16. package/agent-spine-plugin/src/lib/mcp-timeline-tools.js +0 -7
  17. package/agent-spine-plugin/src/lib/pre-answer-timeline-recall.js +104 -0
  18. package/agent-spine-plugin/src/lib/session-timeline-codex.js +0 -4
  19. package/agent-spine-plugin/src/lib/session-timeline-enrollment-source.js +0 -3
  20. package/agent-spine-plugin/src/lib/session-timeline-enrollment-storage.js +0 -4
  21. package/agent-spine-plugin/src/lib/session-timeline-enrollment-transport.js +0 -2
  22. package/agent-spine-plugin/src/lib/session-timeline-enrollment.js +0 -22
  23. package/agent-spine-plugin/src/lib/session-timeline-host-origin.js +0 -4
  24. package/agent-spine-plugin/src/lib/session-timeline-prior.js +42 -50
  25. package/agent-spine-plugin/src/lib/session-timeline-provider.js +0 -3
  26. package/agent-spine-plugin/src/lib/session-timeline-query.js +0 -1
  27. package/agent-spine-plugin/src/lib/session-timeline-sid-acl.js +0 -4
  28. package/agent-spine-plugin/src/lib/session-timeline-source.js +0 -3
  29. package/agent-spine-plugin/src/lib/session-timeline-transport.js +0 -8
  30. package/agent-spine-plugin/src/lib/session-timeline-windows-acl.js +0 -3
  31. package/agent-spine-plugin/src/lib/session-timeline.js +370 -408
  32. package/agent-spine-plugin/src/lib/source-roots.js +26 -16
  33. package/agent-spine-plugin/src/lib/timeline-user-feedback.js +0 -1
  34. package/bin/core-bootstrap.js +18 -10
  35. package/bin/launcher-mode.js +5 -0
  36. package/bin/launcher-runtime.js +27 -0
  37. package/bin/verify-bundled-agent-sources.cjs +28 -2
  38. package/blun.mjs +798 -143
  39. package/bundled-agent-sources.json +192 -155
  40. package/package.json +1 -1
  41. package/standard-skills/translate-native/README.md +1334 -1
  42. package/standard-skills/translate-native/SKILL.md +1 -1
  43. package/standard-skills/translate-native/VERSION +1 -1
  44. package/standard-skills/translate-native/docs/PREMORTEM.md +901 -0
  45. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION.md +208 -41
  46. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_API.md +1319 -77
  47. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_EVIDENCE_HTTP.md +66 -11
  48. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_HTTP_PROVIDER.md +19 -1
  49. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_RECEIPT_VERIFIER_HTTP.md +24 -6
  50. package/standard-skills/translate-native/integrations/commercial_localization_profile.py +23 -0
  51. package/standard-skills/translate-native/integrations/website_localization.py +234 -10
  52. package/standard-skills/translate-native/integrations/website_localization_benchmark.py +39 -3
  53. package/standard-skills/translate-native/integrations/website_localization_cms.py +512 -10
  54. package/standard-skills/translate-native/integrations/website_localization_cms_client.py +116 -0
  55. package/standard-skills/translate-native/integrations/website_localization_cms_receiver.py +101 -11
  56. package/standard-skills/translate-native/integrations/website_localization_cms_receiver_runtime.py +12 -1
  57. package/standard-skills/translate-native/integrations/website_localization_cms_receiver_store.py +186 -24
  58. package/standard-skills/translate-native/integrations/website_localization_cms_source_client.py +64 -1
  59. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery.py +528 -5
  60. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_auth.py +918 -0
  61. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_auth_runtime.py +609 -0
  62. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_client.py +885 -0
  63. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_http.py +1244 -0
  64. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_runtime.py +220 -0
  65. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_sidecar_adapter.py +331 -0
  66. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_capabilities_http.py +444 -0
  67. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_client.py +673 -0
  68. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch.py +1317 -0
  69. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch_client.py +826 -0
  70. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch_http.py +1152 -0
  71. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch_openapi.py +896 -0
  72. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch_runtime.py +621 -0
  73. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_http.py +1180 -0
  74. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_runtime.py +1479 -0
  75. package/standard-skills/translate-native/integrations/website_localization_cms_source_http.py +133 -17
  76. package/standard-skills/translate-native/integrations/website_localization_cms_source_runtime.py +319 -4
  77. package/standard-skills/translate-native/integrations/website_localization_cms_source_service.py +72 -2
  78. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_http.py +13 -0
  79. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver.py +56 -13
  80. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver_openapi.py +335 -0
  81. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver_runtime.py +66 -1
  82. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_processing_monitor.py +73 -7
  83. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_receiver_client.py +93 -9
  84. package/standard-skills/translate-native/integrations/website_localization_evidence_http.py +221 -11
  85. package/standard-skills/translate-native/integrations/website_localization_health.py +6 -0
  86. package/standard-skills/translate-native/integrations/website_localization_http_provider.py +1 -1
  87. package/standard-skills/translate-native/integrations/website_localization_quality_profiles.py +350 -0
  88. package/standard-skills/translate-native/integrations/website_localization_queue.py +126 -56
  89. package/standard-skills/translate-native/integrations/website_localization_receipt_verifier_http.py +166 -13
  90. package/standard-skills/translate-native/integrations/website_localization_release.py +423 -17
  91. package/standard-skills/translate-native/integrations/website_localization_release_coordinator.py +168 -13
  92. package/standard-skills/translate-native/integrations/website_localization_runner.py +10 -0
  93. package/standard-skills/translate-native/integrations/website_localization_worker.py +213 -9
  94. package/standard-skills/translate-native/provenance.json +2 -2
  95. package/standard-skills/translate-native/references/commercial-localization.md +96 -17
  96. package/standard-skills/translate-native/scripts/blun_language_guard.py +3 -1
  97. package/standard-skills/translate-native/scripts/check_commercial_review.py +69 -9
  98. package/standard-skills/translate-native/scripts/commercial_localization_profile.py +829 -31
  99. package/standard-tools/language-guard/VERSION +1 -1
  100. package/standard-tools/language-guard/blun_language_guard.py +3 -1
  101. package/standard-tools/language-guard/check_commercial_review.py +69 -9
  102. package/standard-tools/language-guard/commercial_localization_profile.py +829 -31
  103. package/standard-tools/language-guard/provenance.json +2 -2
  104. package/standard-tools/manifest.json +1 -1
  105. package/worker-host.mjs +215 -82
  106. package/agent-spine-plugin/scripts/check-install-king.js +0 -37
@@ -0,0 +1,621 @@
1
+ #!/usr/bin/env python3
2
+ """Process-owned runtime for the durable public submission dispatcher.
3
+
4
+ The composition root validates the client, worker policy, and existing SQLite
5
+ generation before it creates or mutates durable state. One runtime owns one
6
+ connection, guards its file identity around every operation, serializes access,
7
+ and can supervise one non-daemon delivery worker.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import importlib.util
13
+ import math
14
+ import os
15
+ import sqlite3
16
+ import sys
17
+ import threading
18
+ import time
19
+ from pathlib import Path
20
+ from typing import Any, Callable, Mapping
21
+
22
+
23
+ def _load_module(name: str, path: Path):
24
+ spec = importlib.util.spec_from_file_location(name, path)
25
+ if spec is None or spec.loader is None:
26
+ raise RuntimeError(f"cannot load submission runtime dependency: {path.name}")
27
+ module = importlib.util.module_from_spec(spec)
28
+ sys.modules[spec.name] = module
29
+ spec.loader.exec_module(module)
30
+ return module
31
+
32
+
33
+ _ROOT = Path(__file__).resolve().parents[1]
34
+ _DISPATCH = _load_module(
35
+ "blun_website_localization_submission_dispatch_runtime_store",
36
+ _ROOT
37
+ / "integrations"
38
+ / "website_localization_cms_source_delivery_submission_dispatch.py",
39
+ )
40
+ _FILE_RUNTIME = _load_module(
41
+ "blun_website_localization_submission_dispatch_runtime_file",
42
+ _ROOT / "integrations" / "website_localization_cms_source_delivery_runtime.py",
43
+ )
44
+ _HTTP = _load_module(
45
+ "blun_website_localization_submission_dispatch_runtime_http",
46
+ _ROOT
47
+ / "integrations"
48
+ / "website_localization_cms_source_delivery_submission_dispatch_http.py",
49
+ )
50
+
51
+
52
+ class CMSSourceDeliverySubmissionDispatchRuntimeBlocked(RuntimeError):
53
+ """Stable runtime failure without paths, content, or private exceptions."""
54
+
55
+ def __init__(self, code: str):
56
+ super().__init__(code)
57
+ self.code = code
58
+
59
+
60
+ def _blocked(code: str) -> CMSSourceDeliverySubmissionDispatchRuntimeBlocked:
61
+ return CMSSourceDeliverySubmissionDispatchRuntimeBlocked(
62
+ "source_delivery_submission_dispatch_runtime." + code
63
+ )
64
+
65
+
66
+ def _store_failure(error: Exception) -> CMSSourceDeliverySubmissionDispatchRuntimeBlocked:
67
+ code = getattr(error, "code", "")
68
+ if code.endswith("idempotency_collision"):
69
+ return _blocked("idempotency_collision")
70
+ if code.endswith("submission_missing"):
71
+ return _blocked("submission_missing")
72
+ if code.endswith("lifecycle_not_accepted"):
73
+ return _blocked("lifecycle_not_accepted")
74
+ if code.endswith("commercial_profile_invalid"):
75
+ return _blocked("commercial_profile_invalid")
76
+ if code.endswith(("request_invalid", "attempts_invalid", "identity_invalid")):
77
+ return _blocked("request_invalid")
78
+ return _blocked("outbox_blocked")
79
+
80
+
81
+ def _file_failure(error: Exception) -> CMSSourceDeliverySubmissionDispatchRuntimeBlocked:
82
+ code = getattr(error, "code", "")
83
+ suffix = code.rsplit(".", 1)[-1]
84
+ allowed = {
85
+ "database_path_invalid",
86
+ "database_parent_unavailable",
87
+ "database_parent_unsafe",
88
+ "database_unavailable",
89
+ "database_unsafe",
90
+ "database_create_failed",
91
+ "sqlite_timeout_invalid",
92
+ }
93
+ return _blocked(suffix if suffix in allowed else "database_blocked")
94
+
95
+
96
+ def _duration(value: Any, code: str, *, maximum: float = 86_400) -> float:
97
+ if (
98
+ isinstance(value, bool)
99
+ or not isinstance(value, (int, float))
100
+ or not math.isfinite(float(value))
101
+ or not 0 < float(value) <= maximum
102
+ ):
103
+ raise _blocked(code)
104
+ return float(value)
105
+
106
+
107
+ def _now(clock: Callable[[], float | int]) -> float:
108
+ try:
109
+ value = clock()
110
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
111
+ raise ValueError
112
+ result = float(value)
113
+ if result < 0 or not math.isfinite(result):
114
+ raise ValueError
115
+ return result
116
+ except Exception:
117
+ raise _blocked("clock_invalid") from None
118
+
119
+
120
+ def _client_digest(client: Any) -> str:
121
+ value = getattr(client, "expected_capabilities_sha256", None)
122
+ if (
123
+ not callable(getattr(client, "submit_change", None))
124
+ or not callable(getattr(client, "submit_removal", None))
125
+ or not isinstance(value, str)
126
+ or _DISPATCH.SHA256.fullmatch(value) is None
127
+ ):
128
+ raise _blocked("configuration_invalid")
129
+ return value
130
+
131
+
132
+ def _preflight(
133
+ client: Any,
134
+ worker_id: Any,
135
+ lease_seconds: Any,
136
+ clock: Callable[[], float | int],
137
+ options: Mapping[str, Any],
138
+ ) -> tuple[str, float, tuple[tuple[Any, ...], ...]]:
139
+ if not callable(clock):
140
+ raise _blocked("configuration_invalid")
141
+ digest = _client_digest(client)
142
+ lease = _duration(lease_seconds, "lease_invalid")
143
+ now = _now(clock)
144
+ connection = sqlite3.connect(":memory:")
145
+ try:
146
+ dispatcher = _DISPATCH.DurableCMSSourceDeliverySubmissionDispatcher(
147
+ connection, digest, **dict(options)
148
+ )
149
+ dispatcher.run_once(client, worker_id, now=now, lease_seconds=lease)
150
+ dispatcher.health(now=now)
151
+ schema = tuple(tuple(row) for row in connection.execute(
152
+ "SELECT type, name, tbl_name, sql FROM sqlite_master "
153
+ "WHERE name NOT LIKE 'sqlite_%' ORDER BY type, name"
154
+ ).fetchall())
155
+ except CMSSourceDeliverySubmissionDispatchRuntimeBlocked:
156
+ raise
157
+ except Exception as error:
158
+ raise _blocked("configuration_invalid") from error
159
+ finally:
160
+ connection.close()
161
+ return digest, lease, schema
162
+
163
+
164
+ def _preflight_existing(
165
+ path: str,
166
+ digest: str,
167
+ options: Mapping[str, Any],
168
+ expected_schema: tuple[tuple[Any, ...], ...],
169
+ now: float,
170
+ ) -> None:
171
+ if path == ":memory:" or not os.path.exists(path):
172
+ return
173
+ connection = None
174
+ try:
175
+ connection = sqlite3.connect(
176
+ Path(path).as_uri() + "?mode=ro",
177
+ uri=True,
178
+ timeout=0,
179
+ isolation_level=None,
180
+ )
181
+ connection.row_factory = sqlite3.Row
182
+ actual = tuple(tuple(row) for row in connection.execute(
183
+ "SELECT type, name, tbl_name, sql FROM sqlite_master "
184
+ "WHERE name NOT LIKE 'sqlite_%' ORDER BY type, name"
185
+ ).fetchall())
186
+ if not actual:
187
+ return
188
+ if actual != expected_schema:
189
+ if _DISPATCH._is_empty_legacy_v1(connection, digest):
190
+ return
191
+ raise _blocked("database_schema_altered")
192
+ dispatcher = object.__new__(
193
+ _DISPATCH.DurableCMSSourceDeliverySubmissionDispatcher
194
+ )
195
+ dispatcher.connection = connection
196
+ dispatcher.expected_capabilities_sha256 = digest
197
+ dispatcher.base_delay_seconds = float(options["base_delay_seconds"])
198
+ dispatcher.max_delay_seconds = float(options["max_delay_seconds"])
199
+ dispatcher.health(now=now)
200
+ except CMSSourceDeliverySubmissionDispatchRuntimeBlocked:
201
+ raise
202
+ except Exception as error:
203
+ if isinstance(error, _DISPATCH.CMSSourceDeliverySubmissionDispatchBlocked):
204
+ raise _blocked("database_generation_invalid") from error
205
+ raise _blocked("database_preflight_failed") from error
206
+ finally:
207
+ if connection is not None:
208
+ connection.close()
209
+
210
+
211
+ class DurableCMSSourceDeliverySubmissionDispatchRuntime:
212
+ """One process-owned, guarded, thread-safe caller submission runtime."""
213
+
214
+ def __init__(
215
+ self,
216
+ connection: sqlite3.Connection,
217
+ guard: Callable[[], None],
218
+ dispatcher: Any,
219
+ client: Any,
220
+ *,
221
+ worker_id: str,
222
+ lease_seconds: float,
223
+ clock: Callable[[], float | int],
224
+ expected_capabilities_sha256: str,
225
+ http_authenticator: Callable[[dict[str, Any]], Any] | None = None,
226
+ ):
227
+ self._connection = connection
228
+ self._guard = guard
229
+ self._dispatcher = dispatcher
230
+ self._client = client
231
+ self._worker_id = worker_id
232
+ self._lease_seconds = lease_seconds
233
+ self._clock = clock
234
+ self._expected_capabilities_sha256 = expected_capabilities_sha256
235
+ self._lock = threading.RLock()
236
+ self._closed = False
237
+ self._owner_pid = os.getpid()
238
+ self._worker_stop = threading.Event()
239
+ self._worker_thread: threading.Thread | None = None
240
+ self._worker_state = "unmanaged"
241
+ self._worker_error_code: str | None = None
242
+ self.http = (
243
+ None
244
+ if http_authenticator is None
245
+ else _HTTP.build_submission_dispatch_http(self, http_authenticator)
246
+ )
247
+
248
+ def __repr__(self) -> str:
249
+ return f"DurableCMSSourceDeliverySubmissionDispatchRuntime(state={self.state!r})"
250
+
251
+ def __enter__(self) -> "DurableCMSSourceDeliverySubmissionDispatchRuntime":
252
+ self._assert_owner()
253
+ if self._closed:
254
+ raise _blocked("closed")
255
+ return self
256
+
257
+ def __exit__(self, *_exc: Any) -> None:
258
+ self.close()
259
+
260
+ def _assert_owner(self) -> None:
261
+ if os.getpid() != self._owner_pid:
262
+ raise _blocked("foreign_process")
263
+
264
+ def _guard_client(self) -> None:
265
+ if _client_digest(self._client) != self._expected_capabilities_sha256:
266
+ raise _blocked("capability_drift")
267
+
268
+ def _call(self, name: str, *args: Any, **kwargs: Any) -> Any:
269
+ self._assert_owner()
270
+ with self._lock:
271
+ if self._closed:
272
+ raise _blocked("closed")
273
+ try:
274
+ self._guard()
275
+ self._guard_client()
276
+ result = getattr(self._dispatcher, name)(*args, **kwargs)
277
+ self._guard()
278
+ return result
279
+ except CMSSourceDeliverySubmissionDispatchRuntimeBlocked:
280
+ raise
281
+ except _FILE_RUNTIME.DurableCMSSourceDeliveryRuntimeBlocked as error:
282
+ raise _file_failure(error) from error
283
+ except _DISPATCH.CMSSourceDeliverySubmissionDispatchBlocked as error:
284
+ raise _store_failure(error) from error
285
+ except Exception as error:
286
+ raise _blocked("outbox_blocked") from error
287
+
288
+ def _require_worker_locked(self) -> None:
289
+ if self._worker_state == "unmanaged":
290
+ return
291
+ if (
292
+ self._worker_state != "running"
293
+ or self._worker_error_code is not None
294
+ or self._worker_thread is None
295
+ or not self._worker_thread.is_alive()
296
+ ):
297
+ raise _blocked("worker_not_ready")
298
+
299
+ def enqueue(
300
+ self,
301
+ payload: Mapping[str, Any],
302
+ *,
303
+ source_max_attempts: int = 5,
304
+ delivery_max_attempts: int = 5,
305
+ client_max_attempts: int = 5,
306
+ ) -> Any:
307
+ self._assert_owner()
308
+ with self._lock:
309
+ self._require_worker_locked()
310
+ return self._call(
311
+ "enqueue",
312
+ payload,
313
+ source_max_attempts=source_max_attempts,
314
+ delivery_max_attempts=delivery_max_attempts,
315
+ client_max_attempts=client_max_attempts,
316
+ now=_now(self._clock),
317
+ )
318
+
319
+ def status(self, operation: str, request_id: str) -> Any:
320
+ return self._call("status", operation, request_id, now=_now(self._clock))
321
+
322
+ def lifecycle(self, operation: str, request_id: str) -> Any:
323
+ return self._call(
324
+ "lifecycle", self._client, operation, request_id,
325
+ now=_now(self._clock),
326
+ )
327
+
328
+ def commercial_profile(self) -> Any:
329
+ return self._call("commercial_profile", self._client)
330
+
331
+ def health(self) -> Any:
332
+ return self._call("health", now=_now(self._clock))
333
+
334
+ def run_once(self) -> Any:
335
+ return self._call(
336
+ "run_once",
337
+ self._client,
338
+ self._worker_id,
339
+ now=_now(self._clock),
340
+ lease_seconds=self._lease_seconds,
341
+ )
342
+
343
+ @property
344
+ def expected_capabilities_sha256(self) -> str:
345
+ """Return the locally verified public website contract pin."""
346
+
347
+ self._assert_owner()
348
+ with self._lock:
349
+ if self._closed:
350
+ raise _blocked("closed")
351
+ try:
352
+ self._guard()
353
+ self._guard_client()
354
+ except _FILE_RUNTIME.DurableCMSSourceDeliveryRuntimeBlocked as error:
355
+ raise _file_failure(error) from error
356
+ return self._expected_capabilities_sha256
357
+
358
+ @staticmethod
359
+ def _loop_delays(
360
+ active_delay_seconds: Any,
361
+ idle_delay_seconds: Any,
362
+ blocked_delay_seconds: Any,
363
+ ) -> tuple[float, float, float]:
364
+ return tuple(
365
+ _duration(value, "loop_invalid", maximum=300)
366
+ for value in (
367
+ active_delay_seconds,
368
+ idle_delay_seconds,
369
+ blocked_delay_seconds,
370
+ )
371
+ )
372
+
373
+ def _managed_worker(
374
+ self,
375
+ stop: threading.Event,
376
+ delays: tuple[float, float, float],
377
+ ) -> None:
378
+ try:
379
+ active, idle, blocked = delays
380
+ while not stop.is_set():
381
+ outcome = self.run_once()
382
+ if outcome is None:
383
+ delay = idle
384
+ elif outcome.status in {"retry_wait", "failed"}:
385
+ delay = blocked
386
+ else:
387
+ delay = active
388
+ stop.wait(delay)
389
+ except Exception:
390
+ with self._lock:
391
+ self._worker_error_code = (
392
+ "source_delivery_submission_dispatch_runtime.worker_blocked"
393
+ )
394
+ self._worker_state = "failed"
395
+ return
396
+ with self._lock:
397
+ if self._worker_state in {"running", "stopping"}:
398
+ self._worker_state = "stopped"
399
+
400
+ def start_worker(
401
+ self,
402
+ *,
403
+ active_delay_seconds: float | int = 0.05,
404
+ idle_delay_seconds: float | int = 1,
405
+ blocked_delay_seconds: float | int = 5,
406
+ ) -> None:
407
+ delays = self._loop_delays(
408
+ active_delay_seconds, idle_delay_seconds, blocked_delay_seconds
409
+ )
410
+ self._assert_owner()
411
+ with self._lock:
412
+ if self._closed:
413
+ raise _blocked("closed")
414
+ if self._worker_state == "running":
415
+ return
416
+ if self._worker_state in {"stopping", "failed"}:
417
+ raise _blocked("worker_blocked")
418
+ self._guard()
419
+ stop = threading.Event()
420
+ thread = threading.Thread(
421
+ target=self._managed_worker,
422
+ args=(stop, delays),
423
+ name="cms-public-submission-worker",
424
+ daemon=False,
425
+ )
426
+ self._worker_stop = stop
427
+ self._worker_thread = thread
428
+ self._worker_error_code = None
429
+ self._worker_state = "running"
430
+ try:
431
+ thread.start()
432
+ except Exception as error:
433
+ self._worker_thread = None
434
+ self._worker_state = "failed"
435
+ self._worker_error_code = (
436
+ "source_delivery_submission_dispatch_runtime.worker_blocked"
437
+ )
438
+ raise _blocked("worker_blocked") from error
439
+
440
+ def stop_worker(self, *, timeout_seconds: float | int = 30) -> None:
441
+ timeout = _duration(timeout_seconds, "worker_timeout_invalid", maximum=300)
442
+ self._assert_owner()
443
+ thread = self._worker_thread
444
+ state = self._worker_state
445
+ if thread is None or state in {"unmanaged", "stopped"}:
446
+ return
447
+ if thread is threading.current_thread():
448
+ raise _blocked("worker_stop_invalid")
449
+ if state != "failed":
450
+ self._worker_state = "stopping"
451
+ self._worker_stop.set()
452
+ thread.join(timeout)
453
+ if thread.is_alive():
454
+ raise _blocked("worker_stop_timeout")
455
+ with self._lock:
456
+ if self._worker_state == "stopping":
457
+ self._worker_state = "stopped"
458
+
459
+ def worker_readiness(self) -> dict[str, Any]:
460
+ self._assert_owner()
461
+ with self._lock:
462
+ state = "closed" if self._closed else self._worker_state
463
+ thread = self._worker_thread
464
+ alive = thread is not None and thread.is_alive()
465
+ error_code = self._worker_error_code
466
+ if state != "running" or not alive:
467
+ return {
468
+ "schema": "blun.cms-public-submission-worker-readiness.v1",
469
+ "status": "not_ready",
470
+ "worker_state": state,
471
+ "outbox_status": None,
472
+ "error_code": error_code or (
473
+ "source_delivery_submission_dispatch_runtime.worker_not_ready"
474
+ ),
475
+ "capabilities_sha256": self._expected_capabilities_sha256,
476
+ }
477
+ try:
478
+ health = self.health()
479
+ except CMSSourceDeliverySubmissionDispatchRuntimeBlocked:
480
+ return {
481
+ "schema": "blun.cms-public-submission-worker-readiness.v1",
482
+ "status": "not_ready",
483
+ "worker_state": "running",
484
+ "outbox_status": "blocked",
485
+ "error_code": (
486
+ "source_delivery_submission_dispatch_runtime.outbox_blocked"
487
+ ),
488
+ "capabilities_sha256": self._expected_capabilities_sha256,
489
+ }
490
+ ready = health.status == "ok"
491
+ return {
492
+ "schema": "blun.cms-public-submission-worker-readiness.v1",
493
+ "status": "ready" if ready else "not_ready",
494
+ "worker_state": "running",
495
+ "outbox_status": health.status,
496
+ "error_code": None if ready else (
497
+ "source_delivery_submission_dispatch_runtime.outbox_blocked"
498
+ ),
499
+ "capabilities_sha256": self._expected_capabilities_sha256,
500
+ }
501
+
502
+ def close(self, *, worker_timeout_seconds: float | int = 30) -> None:
503
+ self._assert_owner()
504
+ self.stop_worker(timeout_seconds=worker_timeout_seconds)
505
+ with self._lock:
506
+ if self._closed:
507
+ return
508
+ try:
509
+ self._connection.close()
510
+ except sqlite3.Error as error:
511
+ raise _blocked("close_failed") from error
512
+ self._closed = True
513
+
514
+ @property
515
+ def state(self) -> str:
516
+ if os.getpid() != self._owner_pid:
517
+ return "foreign-process"
518
+ with self._lock:
519
+ return "closed" if self._closed else "open"
520
+
521
+ @property
522
+ def worker_state(self) -> str:
523
+ if os.getpid() != self._owner_pid:
524
+ return "foreign-process"
525
+ with self._lock:
526
+ return "closed" if self._closed else self._worker_state
527
+
528
+
529
+ def open_durable_cms_source_delivery_submission_dispatch(
530
+ database: str | os.PathLike[str],
531
+ client: Any,
532
+ *,
533
+ worker_id: str,
534
+ clock: Callable[[], float | int] = time.time,
535
+ sqlite_timeout_seconds: float | int = 5,
536
+ lease_seconds: float | int = 600,
537
+ base_delay_seconds: float | int = 5,
538
+ max_delay_seconds: float | int = 300,
539
+ http_authenticator: Callable[[dict[str, Any]], Any] | None = None,
540
+ ) -> DurableCMSSourceDeliverySubmissionDispatchRuntime:
541
+ """Validate everything, then open one guarded caller-owned outbox."""
542
+
543
+ if http_authenticator is not None and not callable(http_authenticator):
544
+ raise _blocked("configuration_invalid")
545
+ options = {
546
+ "base_delay_seconds": base_delay_seconds,
547
+ "max_delay_seconds": max_delay_seconds,
548
+ }
549
+ digest, lease, schema = _preflight(
550
+ client, worker_id, lease_seconds, clock, options
551
+ )
552
+ try:
553
+ path = _FILE_RUNTIME._database_path(database)
554
+ timeout = _FILE_RUNTIME._sqlite_timeout(sqlite_timeout_seconds)
555
+ guard = _FILE_RUNTIME._prepare_database_file(path)
556
+ _preflight_existing(path, digest, options, schema, _now(clock))
557
+ guard()
558
+ except CMSSourceDeliverySubmissionDispatchRuntimeBlocked:
559
+ raise
560
+ except _FILE_RUNTIME.DurableCMSSourceDeliveryRuntimeBlocked as error:
561
+ raise _file_failure(error) from error
562
+ connection = None
563
+ try:
564
+ connection = sqlite3.connect(
565
+ path,
566
+ timeout=timeout,
567
+ isolation_level=None,
568
+ check_same_thread=False,
569
+ )
570
+ guard()
571
+ dispatcher = _DISPATCH.DurableCMSSourceDeliverySubmissionDispatcher(
572
+ connection, digest, **options
573
+ )
574
+ guard()
575
+ except Exception as error:
576
+ if connection is not None:
577
+ try:
578
+ connection.close()
579
+ except sqlite3.Error:
580
+ pass
581
+ if isinstance(error, _FILE_RUNTIME.DurableCMSSourceDeliveryRuntimeBlocked):
582
+ raise _file_failure(error) from error
583
+ if isinstance(error, _DISPATCH.CMSSourceDeliverySubmissionDispatchBlocked):
584
+ raise _store_failure(error) from error
585
+ raise _blocked("initialization_failed") from error
586
+ return DurableCMSSourceDeliverySubmissionDispatchRuntime(
587
+ connection,
588
+ guard,
589
+ dispatcher,
590
+ client,
591
+ worker_id=worker_id,
592
+ lease_seconds=lease,
593
+ clock=clock,
594
+ expected_capabilities_sha256=digest,
595
+ http_authenticator=http_authenticator,
596
+ )
597
+
598
+
599
+ def open_hosted_cms_source_delivery_submission_dispatch(
600
+ *args: Any,
601
+ active_delay_seconds: float | int = 0.05,
602
+ idle_delay_seconds: float | int = 1,
603
+ blocked_delay_seconds: float | int = 5,
604
+ **kwargs: Any,
605
+ ) -> DurableCMSSourceDeliverySubmissionDispatchRuntime:
606
+ """Open the guarded caller outbox and start its supervised worker."""
607
+
608
+ delays = DurableCMSSourceDeliverySubmissionDispatchRuntime._loop_delays(
609
+ active_delay_seconds, idle_delay_seconds, blocked_delay_seconds
610
+ )
611
+ runtime = open_durable_cms_source_delivery_submission_dispatch(*args, **kwargs)
612
+ try:
613
+ runtime.start_worker(
614
+ active_delay_seconds=delays[0],
615
+ idle_delay_seconds=delays[1],
616
+ blocked_delay_seconds=delays[2],
617
+ )
618
+ except Exception:
619
+ runtime.close()
620
+ raise
621
+ return runtime