rocket-engine 0.0.2b1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (241) hide show
  1. rocket_engine/__init__.py +8 -0
  2. rocket_engine/__main__.py +12 -0
  3. rocket_engine/adapters/README.md +54 -0
  4. rocket_engine/adapters/__init__.py +7 -0
  5. rocket_engine/adapters/api.py +39 -0
  6. rocket_engine/adapters/base.py +46 -0
  7. rocket_engine/adapters/cace/__init__.py +163 -0
  8. rocket_engine/adapters/circulax/README.md +10 -0
  9. rocket_engine/adapters/circulax/__init__.py +112 -0
  10. rocket_engine/adapters/circulax/_driver.py +190 -0
  11. rocket_engine/adapters/circulax/adapter.py +538 -0
  12. rocket_engine/adapters/circulax/results.py +291 -0
  13. rocket_engine/adapters/cli/README.md +34 -0
  14. rocket_engine/adapters/cli/__init__.py +305 -0
  15. rocket_engine/adapters/connectivity/__init__.py +196 -0
  16. rocket_engine/adapters/connectivity/results.py +214 -0
  17. rocket_engine/adapters/cosim/README.md +9 -0
  18. rocket_engine/adapters/cosim/__init__.py +1390 -0
  19. rocket_engine/adapters/cosim/results.py +214 -0
  20. rocket_engine/adapters/devsim/__init__.py +171 -0
  21. rocket_engine/adapters/digital_flow/README.md +9 -0
  22. rocket_engine/adapters/digital_flow/__init__.py +1121 -0
  23. rocket_engine/adapters/digital_flow/results.py +227 -0
  24. rocket_engine/adapters/digital_flow/state.py +462 -0
  25. rocket_engine/adapters/drc/README.klayout.md +7 -0
  26. rocket_engine/adapters/drc/README.md +7 -0
  27. rocket_engine/adapters/drc/__init__.py +1 -0
  28. rocket_engine/adapters/drc/klayout.py +952 -0
  29. rocket_engine/adapters/drc/magic.py +287 -0
  30. rocket_engine/adapters/drc/results.py +184 -0
  31. rocket_engine/adapters/drc/results_kl.py +188 -0
  32. rocket_engine/adapters/elmer/__init__.py +163 -0
  33. rocket_engine/adapters/envelope.py +120 -0
  34. rocket_engine/adapters/equivalence/__init__.py +156 -0
  35. rocket_engine/adapters/errors.py +181 -0
  36. rocket_engine/adapters/extract_magic/README.md +15 -0
  37. rocket_engine/adapters/extract_magic/__init__.py +425 -0
  38. rocket_engine/adapters/extract_magic/results.py +209 -0
  39. rocket_engine/adapters/formal_sby/README.md +7 -0
  40. rocket_engine/adapters/formal_sby/__init__.py +773 -0
  41. rocket_engine/adapters/formal_sby/formal_edge.py +195 -0
  42. rocket_engine/adapters/formal_sby/results.py +185 -0
  43. rocket_engine/adapters/gdsfactory/__init__.py +194 -0
  44. rocket_engine/adapters/gdsfactory/_driver.py +459 -0
  45. rocket_engine/adapters/gdsfactory/adapter.py +609 -0
  46. rocket_engine/adapters/gdsfactory/results.py +181 -0
  47. rocket_engine/adapters/hdl_sim/README.md +7 -0
  48. rocket_engine/adapters/hdl_sim/__init__.py +369 -0
  49. rocket_engine/adapters/hdl_sim/ghdl.py +155 -0
  50. rocket_engine/adapters/hdl_sim/results.py +178 -0
  51. rocket_engine/adapters/hdl_sim/verilator.py +167 -0
  52. rocket_engine/adapters/http/README.md +15 -0
  53. rocket_engine/adapters/http/__init__.py +458 -0
  54. rocket_engine/adapters/http/events.py +63 -0
  55. rocket_engine/adapters/ipc/README.md +9 -0
  56. rocket_engine/adapters/ipc/__init__.py +111 -0
  57. rocket_engine/adapters/lvs_netgen/README.md +20 -0
  58. rocket_engine/adapters/lvs_netgen/__init__.py +889 -0
  59. rocket_engine/adapters/lvs_netgen/results.py +168 -0
  60. rocket_engine/adapters/mcp/README.md +19 -0
  61. rocket_engine/adapters/mcp/__init__.py +85 -0
  62. rocket_engine/adapters/mcp/api-schema.json +6107 -0
  63. rocket_engine/adapters/mcp/pull.py +246 -0
  64. rocket_engine/adapters/mcp/server.py +794 -0
  65. rocket_engine/adapters/meep/__init__.py +167 -0
  66. rocket_engine/adapters/meow/README.md +7 -0
  67. rocket_engine/adapters/meow/__init__.py +190 -0
  68. rocket_engine/adapters/meow/_driver.py +227 -0
  69. rocket_engine/adapters/meow/_envelope.py +108 -0
  70. rocket_engine/adapters/meow/adapter.py +491 -0
  71. rocket_engine/adapters/meow/results.py +218 -0
  72. rocket_engine/adapters/mutation/__init__.py +192 -0
  73. rocket_engine/adapters/ngspice/README.md +28 -0
  74. rocket_engine/adapters/ngspice/__init__.py +627 -0
  75. rocket_engine/adapters/ngspice/results.py +215 -0
  76. rocket_engine/adapters/postlayout/README.md +28 -0
  77. rocket_engine/adapters/postlayout/__init__.py +17 -0
  78. rocket_engine/adapters/postlayout/adapter.py +1062 -0
  79. rocket_engine/adapters/postlayout/results.py +330 -0
  80. rocket_engine/adapters/postlayout/translate.py +71 -0
  81. rocket_engine/adapters/profile.py +220 -0
  82. rocket_engine/adapters/sax/README.md +23 -0
  83. rocket_engine/adapters/sax/__init__.py +516 -0
  84. rocket_engine/adapters/sax/_driver.py +148 -0
  85. rocket_engine/adapters/sax/results.py +291 -0
  86. rocket_engine/adapters/stdio_console.py +1151 -0
  87. rocket_engine/adapters/syntax/README.md +5 -0
  88. rocket_engine/adapters/syntax/__init__.py +366 -0
  89. rocket_engine/adapters/syntax/results.py +181 -0
  90. rocket_engine/adapters/synth_yosys/README.md +7 -0
  91. rocket_engine/adapters/synth_yosys/__init__.py +655 -0
  92. rocket_engine/adapters/synth_yosys/amaranth.py +120 -0
  93. rocket_engine/adapters/synth_yosys/ghdl.py +128 -0
  94. rocket_engine/adapters/synth_yosys/results.py +173 -0
  95. rocket_engine/adapters/timing_sta/README.md +9 -0
  96. rocket_engine/adapters/timing_sta/__init__.py +726 -0
  97. rocket_engine/adapters/view_sync/__init__.py +185 -0
  98. rocket_engine/adapters/view_sync/results.py +189 -0
  99. rocket_engine/adapters/vlsir/__init__.py +142 -0
  100. rocket_engine/adapters/xschem/__init__.py +440 -0
  101. rocket_engine/adapters/xschem/deck.py +266 -0
  102. rocket_engine/clients/__init__.py +5 -0
  103. rocket_engine/clients/runtime.py +255 -0
  104. rocket_engine/composition.py +2279 -0
  105. rocket_engine/execution/README.md +30 -0
  106. rocket_engine/execution/__init__.py +150 -0
  107. rocket_engine/execution/apig/__init__.py +1 -0
  108. rocket_engine/execution/apig/pool.py +463 -0
  109. rocket_engine/execution/apig/submit.py +124 -0
  110. rocket_engine/execution/block_runs_scope.py +479 -0
  111. rocket_engine/execution/coalesce.py +414 -0
  112. rocket_engine/execution/console/__init__.py +1 -0
  113. rocket_engine/execution/console/apply_session.py +318 -0
  114. rocket_engine/execution/console/lifecycle.py +265 -0
  115. rocket_engine/execution/dispatch.py +205 -0
  116. rocket_engine/execution/errors.py +31 -0
  117. rocket_engine/execution/ports.py +797 -0
  118. rocket_engine/execution/pyexec/__init__.py +1 -0
  119. rocket_engine/execution/pyexec/apply_context.py +547 -0
  120. rocket_engine/execution/pyexec/approvals.py +173 -0
  121. rocket_engine/execution/pyexec/runner.py +404 -0
  122. rocket_engine/execution/pyexec/support.py +464 -0
  123. rocket_engine/execution/python_exec.py +897 -0
  124. rocket_engine/execution/queue.py +85 -0
  125. rocket_engine/execution/replay_safe.py +601 -0
  126. rocket_engine/execution/routing/__init__.py +1 -0
  127. rocket_engine/execution/routing/calls.py +364 -0
  128. rocket_engine/execution/routing/namespace.py +785 -0
  129. rocket_engine/execution/routing/ports.py +102 -0
  130. rocket_engine/execution/routing/tools.py +346 -0
  131. rocket_engine/execution/runner.py +311 -0
  132. rocket_engine/execution/runtime.py +1149 -0
  133. rocket_engine/execution/service.py +279 -0
  134. rocket_engine/execution/worker.py +464 -0
  135. rocket_engine/interfaces/README.md +26 -0
  136. rocket_engine/interfaces/__init__.py +58 -0
  137. rocket_engine/interfaces/apig_assets.py +407 -0
  138. rocket_engine/interfaces/celllink.py +461 -0
  139. rocket_engine/interfaces/documents.py +92 -0
  140. rocket_engine/interfaces/envelopes.py +131 -0
  141. rocket_engine/interfaces/errors.py +16 -0
  142. rocket_engine/interfaces/layout.py +410 -0
  143. rocket_engine/interfaces/layout_schema.yaml +278 -0
  144. rocket_engine/interfaces/paths.py +223 -0
  145. rocket_engine/interfaces/pdks.py +550 -0
  146. rocket_engine/interfaces/ports.py +72 -0
  147. rocket_engine/interfaces/references.py +115 -0
  148. rocket_engine/interfaces/refs.py +85 -0
  149. rocket_engine/interfaces/runparams.py +304 -0
  150. rocket_engine/interfaces/setup.py +552 -0
  151. rocket_engine/interfaces/setup_catalogue.yaml +243 -0
  152. rocket_engine/interfaces/setup_probes.py +66 -0
  153. rocket_engine/interfaces/vocabulary.py +58 -0
  154. rocket_engine/interfaces/yaml_dump.py +31 -0
  155. rocket_engine/library/README.md +22 -0
  156. rocket_engine/library/__init__.py +13 -0
  157. rocket_engine/library/apig_catalog.py +371 -0
  158. rocket_engine/library/catalog.py +185 -0
  159. rocket_engine/library/errors.py +16 -0
  160. rocket_engine/mutate/__init__.py +776 -0
  161. rocket_engine/mutate/operators.py +700 -0
  162. rocket_engine/ops/__init__.py +1 -0
  163. rocket_engine/ops/deferred.py +279 -0
  164. rocket_engine/ops/editors.py +404 -0
  165. rocket_engine/ops/evidence.py +194 -0
  166. rocket_engine/ops/gates.py +262 -0
  167. rocket_engine/ops/ports.py +76 -0
  168. rocket_engine/ops/queries.py +417 -0
  169. rocket_engine/ops/runs.py +553 -0
  170. rocket_engine/ops/submit.py +614 -0
  171. rocket_engine/project/README.md +27 -0
  172. rocket_engine/project/__init__.py +6 -0
  173. rocket_engine/project/canvas/__init__.py +55 -0
  174. rocket_engine/project/canvas/compile.py +109 -0
  175. rocket_engine/project/canvas/integrate.py +48 -0
  176. rocket_engine/project/canvas/schema.py +241 -0
  177. rocket_engine/project/canvas/templates.py +46 -0
  178. rocket_engine/project/canvas/validate.py +603 -0
  179. rocket_engine/project/edit/__init__.py +5 -0
  180. rocket_engine/project/edit/cas.py +87 -0
  181. rocket_engine/project/edit/transactions.py +282 -0
  182. rocket_engine/project/errors.py +16 -0
  183. rocket_engine/project/frontend13.py +685 -0
  184. rocket_engine/project/geometry/__init__.py +23 -0
  185. rocket_engine/project/geometry/ops.py +451 -0
  186. rocket_engine/project/geometry/snap.py +103 -0
  187. rocket_engine/project/ip/__init__.py +1 -0
  188. rocket_engine/project/ip/blocks.py +195 -0
  189. rocket_engine/project/ip/entry.schema.json +78 -0
  190. rocket_engine/project/ip/lifecycle.py +402 -0
  191. rocket_engine/project/ip/verbs.py +123 -0
  192. rocket_engine/project/layers/__init__.py +6 -0
  193. rocket_engine/project/layers/bindings.py +115 -0
  194. rocket_engine/project/layers/catalog.py +228 -0
  195. rocket_engine/project/layers/presets.py +43 -0
  196. rocket_engine/project/ledger/__init__.py +1 -0
  197. rocket_engine/project/ledger/docs.py +167 -0
  198. rocket_engine/project/ledger/entities.py +176 -0
  199. rocket_engine/project/ledger/events.py +29 -0
  200. rocket_engine/project/ledger/runs.py +257 -0
  201. rocket_engine/project/ledger/shared.py +174 -0
  202. rocket_engine/project/ledger/snapshots.py +385 -0
  203. rocket_engine/project/ledger/wires.py +552 -0
  204. rocket_engine/project/process/__init__.py +14 -0
  205. rocket_engine/project/process/model.py +193 -0
  206. rocket_engine/project/process/recipes.py +121 -0
  207. rocket_engine/project/schematic/__init__.py +1 -0
  208. rocket_engine/project/schematic/hierarchy.py +312 -0
  209. rocket_engine/project/schematic/instances.py +393 -0
  210. rocket_engine/project/schematic/sheets.py +198 -0
  211. rocket_engine/project/service.py +2941 -0
  212. rocket_engine/project/symbols/__init__.py +1 -0
  213. rocket_engine/project/symbols/cells.py +324 -0
  214. rocket_engine/project/symbols/linkage.py +356 -0
  215. rocket_engine/project/symbols/symbols.py +352 -0
  216. rocket_engine/project/templates/__init__.py +303 -0
  217. rocket_engine/project/templates/errors.py +25 -0
  218. rocket_engine/project/testbench/__init__.py +49 -0
  219. rocket_engine/project/testbench/blocks.py +619 -0
  220. rocket_engine/project/testbench/templates.py +134 -0
  221. rocket_engine/stores/README.md +12 -0
  222. rocket_engine/stores/__init__.py +6 -0
  223. rocket_engine/stores/ecs.py +1508 -0
  224. rocket_engine/stores/layers.py +1023 -0
  225. rocket_engine/stores/settings.py +1188 -0
  226. rocket_engine/stores/wireplace.py +1324 -0
  227. rocket_engine/workflows/README.md +18 -0
  228. rocket_engine/workflows/__init__.py +53 -0
  229. rocket_engine/workflows/capability_plan.py +102 -0
  230. rocket_engine/workflows/completion.py +228 -0
  231. rocket_engine/workflows/connectgraph.py +734 -0
  232. rocket_engine/workflows/connectgraph_emit.py +336 -0
  233. rocket_engine/workflows/gates.py +144 -0
  234. rocket_engine/workflows/photonic_wire.py +76 -0
  235. rocket_engine/workflows/stages.py +324 -0
  236. rocket_engine/workflows/viewsync.py +399 -0
  237. rocket_engine-0.0.2b1.dist-info/METADATA +200 -0
  238. rocket_engine-0.0.2b1.dist-info/RECORD +241 -0
  239. rocket_engine-0.0.2b1.dist-info/WHEEL +4 -0
  240. rocket_engine-0.0.2b1.dist-info/entry_points.txt +2 -0
  241. rocket_engine-0.0.2b1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,1149 @@
1
+ """Deferred execution runtime (API-V1-CORE, ASYNC-SUBMIT, PUSH-EVENTS).
2
+
3
+ One EngineRuntime owns the ledger-facing admission state the
4
+ composition root will delegate to: durable request identity,
5
+ revision-pinned coalescing, advisory capacity, and ordered
6
+ lifecycle events. Submit acknowledges promptly with a run id and
7
+ no verdict; the deferred handle watches the run to completion
8
+ and every lifecycle step is observable through watch plus push
9
+ listeners.
10
+
11
+ Engine.submit integration seam (surface lane owns
12
+ composition.py, vocabulary.py, submit.py — this module never
13
+ imports them):
14
+ - Call: route solver operations to
15
+ `EngineRuntime.submit(operation, arguments, request_id)`
16
+ instead of the inline drain, returning the receipt
17
+ (`admitted`, `run_id`, `request_id`, `snapshot_id`,
18
+ `revision`, `event_cursor`) as the prompt acknowledgement.
19
+ - Mapping: merge the terminal completion back with
20
+ `execution.apig.submit.merge_ack_completion(ack, completion)`
21
+ so the ack shape plus verdict keeps the one run id; serve
22
+ `list_survivors`, `resume_run`, `discard_run`, `cancel_job`,
23
+ and `get_events_since` from `boot_report`, `resume`,
24
+ `discard`, `cancel`, and `watch`.
25
+
26
+ Caution: Runs never execute on acknowledgement — pump
27
+ explicitly or construct with auto_dispatch. A disconnect after
28
+ acknowledgement never discards the request: the request/run
29
+ mapping stays queryable until resume or discard decides it.
30
+ """
31
+
32
+ import shutil
33
+ import threading
34
+ import uuid
35
+
36
+ from .coalesce import Coalescer, content_key
37
+ from .errors import ExecutionError
38
+ from .ports import PortBus
39
+ from .queue import DeferredQueue
40
+
41
+ #: Operations that never coalesce: mutating scripts and editor
42
+ #: transactions execute fresh even with identical content.
43
+ NEVER_COALESCE = frozenset({"python_execute", "write_doc",
44
+ "delete_doc", "apply_mutations",
45
+ "write_settings", "rewind"})
46
+
47
+ #: Lifecycle kinds every run emits, in order (frontend-tooling
48
+ #: spec prompt-ack plus job_queued/started/progress/finished).
49
+ LIFECYCLE_ORDER = ("job_queued", "job_started", "job_progress",
50
+ "job_finished")
51
+
52
+ #: Interruptions past this count quarantine the run key until an
53
+ #: explicit quarantine override resumes it.
54
+ QUARANTINE_AFTER = 2
55
+
56
+ #: Retained event window: cursors at or below the floor fail
57
+ #: with cursor_expired plus the resubscribe path, never gaps.
58
+ RETAINED_EVENTS = 4096
59
+
60
+
61
+ def probe_host_tool(name, reason):
62
+ """Probe one host tool without executing anything.
63
+
64
+ Args:
65
+ name: Binary to resolve on PATH.
66
+ reason: BLOCKED-needs-host reason stamped when the
67
+ binary is absent.
68
+
69
+ Returns:
70
+ Probe with available, name, path when found, else the
71
+ reason and a BLOCKED verdict — loud, never a
72
+ faked PASS.
73
+ """
74
+ path = shutil.which(name)
75
+ if path is not None:
76
+ return {"available": True, "name": name, "path": path}
77
+ return {"available": False, "name": name, "path": None,
78
+ "reason": reason, "verdict": "BLOCKED-needs-host"}
79
+
80
+
81
+ class DeferredHandle:
82
+ """Prompt acknowledgement watching one run to completion.
83
+
84
+ Args:
85
+ runtime: EngineRuntime the run was admitted to.
86
+ run_id: Admitted run id shared by every coalesced caller.
87
+ receipt: Prompt acknowledgement (admitted, run_id,
88
+ request_id, snapshot_id, revision, event_cursor —
89
+ no verdict until the run finishes).
90
+ """
91
+
92
+ def __init__(self, runtime, run_id, receipt):
93
+ self._runtime = runtime
94
+ self._run_id = run_id
95
+ self._receipt = dict(receipt)
96
+
97
+ @property
98
+ def run_id(self):
99
+ """Admitted run id for this handle.
100
+
101
+ Returns:
102
+ Run id shared by every coalesced caller.
103
+ """
104
+ return self._run_id
105
+
106
+ @property
107
+ def receipt(self):
108
+ """Prompt acknowledgement for this handle.
109
+
110
+ Returns:
111
+ Receipt copy (admitted plus identity fields, never
112
+ a verdict before completion).
113
+ """
114
+ return dict(self._receipt)
115
+
116
+ def poll(self):
117
+ """Read the current run state without blocking.
118
+
119
+ Returns:
120
+ Status with run_id, operation, state, and verdict
121
+ once judged.
122
+ """
123
+ return self._runtime.status(self._run_id)
124
+
125
+ def wait(self, timeout=None):
126
+ """Block until the run reaches a terminal state.
127
+
128
+ Args:
129
+ timeout: Maximum seconds to wait; waits with the
130
+ run when omitted.
131
+
132
+ Returns:
133
+ Terminal completion for the run.
134
+
135
+ Raises:
136
+ TimeoutError: The wait lapsed before the run
137
+ finished.
138
+ """
139
+ return self._runtime.wait_for(self._run_id, timeout=timeout)
140
+
141
+
142
+ class EngineRuntime:
143
+ """Deferred execution runtime: prompt ack, capacity, events.
144
+
145
+ Args:
146
+ project_id: Project this runtime admits runs for.
147
+ runner: Callable `(operation, arguments, snapshot_id,
148
+ request_id, run_id)` returning a completion mapping.
149
+ Runner exceptions become failed completions, never
150
+ raised, and the drain continues.
151
+ max_inflight: Maximum queued plus running runs;
152
+ admissions past it are refused retryable with
153
+ admitted False, never queued, never dropped
154
+ silently.
155
+ auto_dispatch: Dispatch admitted runs on a daemon
156
+ worker without a client round trip; False leaves
157
+ dispatch to explicit pump (deterministic tests).
158
+ schemas: Optional operation to allowed-argument-names
159
+ mapping for C01 request validation; None checks
160
+ shapes only.
161
+ operations: Optional known-operation names for C01
162
+ validation; None accepts any string name and
163
+ leaves catalog validation to the Engine seam.
164
+ """
165
+
166
+ def __init__(self, project_id, runner, max_inflight=64,
167
+ auto_dispatch=True, schemas=None,
168
+ operations=None):
169
+ self._project_id = project_id
170
+ self._runner = runner
171
+ self._max_inflight = max_inflight
172
+ self._auto_dispatch = auto_dispatch
173
+ self._schemas = dict(schemas) if schemas else {}
174
+ self._operations = (frozenset(operations)
175
+ if operations is not None else None)
176
+ self._lock = threading.RLock()
177
+ self._queue = DeferredQueue(max_depth=max_inflight)
178
+ self._coalescer = Coalescer()
179
+ self._ports = PortBus()
180
+ self._requests = {}
181
+ self._runs = {}
182
+ self._events = []
183
+ self._seq_floor = 0
184
+ self._revision = 0
185
+ self._paused = False
186
+ self._listeners = []
187
+ self._interruptions = {}
188
+ self._quarantined = set()
189
+ self._pumping = False
190
+
191
+ def submit(self, operation, arguments, request_id=None):
192
+ """Admit one run and acknowledge promptly without verdict.
193
+
194
+ Reused request ids replay their stored receipt without
195
+ re-executing (identical content) or fail with
196
+ request_conflict (changed content). Equivalent
197
+ in-flight observational runs share one execution across
198
+ callers; scripts and edits always execute fresh. Past
199
+ capacity the receipt refuses retryable with admitted
200
+ False and no run is queued.
201
+
202
+ Args:
203
+ operation: Cataloged operation name.
204
+ arguments: Operation arguments mapping.
205
+ request_id: Client envelope id; minted when omitted.
206
+
207
+ Returns:
208
+ DeferredHandle watching the admitted (or shared, or
209
+ refused) run. Refused handles carry admitted
210
+ False, code, retryable, depth, and bound.
211
+
212
+ Raises:
213
+ ExecutionError: invalid_command for a malformed
214
+ call, unsupported_operation for an unknown
215
+ name, request_conflict for a reused envelope id
216
+ with new content.
217
+ """
218
+ request_id = str(request_id or uuid.uuid4().hex)
219
+ arguments = dict(arguments or {})
220
+ self.validate_request(operation, arguments)
221
+ with self._lock:
222
+ stored = self._requests.get(request_id)
223
+ if stored is not None:
224
+ if (stored["operation"] != operation or
225
+ stored["arguments"] != arguments):
226
+ raise ExecutionError(
227
+ "request_conflict",
228
+ f"Request {request_id} reused with new content.",
229
+ )
230
+ return DeferredHandle(
231
+ self, stored["run_id"], stored["receipt"])
232
+ key = content_key(operation, arguments)
233
+ if operation not in NEVER_COALESCE:
234
+ with self._coalescer.guard:
235
+ hit = self._coalescer.lookup(key,
236
+ self._revision)
237
+ if hit is not None:
238
+ waiter = self._coalescer.attach(
239
+ hit["run_id"], request_id)
240
+ receipt = self._ack(hit["run_id"],
241
+ request_id,
242
+ coalesced=True)
243
+ self._record_request(request_id,
244
+ operation,
245
+ arguments, key,
246
+ hit["run_id"],
247
+ receipt)
248
+ return DeferredHandle(self,
249
+ hit["run_id"],
250
+ receipt)
251
+ if self._inflight_locked() >= self._max_inflight:
252
+ self._paused = True
253
+ self._emit_locked("admission_paused", None,
254
+ {"depth": self._inflight_locked(),
255
+ "bound": self._max_inflight})
256
+ return DeferredHandle(self, "", {
257
+ "admitted": False, "code": "pool_full",
258
+ "retryable": True, "request_id": request_id,
259
+ "depth": self._inflight_locked(),
260
+ "bound": self._max_inflight,
261
+ "message": "Admission saturated: drain before "
262
+ "submitting more.",
263
+ })
264
+ run_id = uuid.uuid4().hex
265
+ snapshot_id = f"snapshot_{self._revision}"
266
+ record = {
267
+ "run_id": run_id, "operation": operation,
268
+ "arguments": dict(arguments),
269
+ "snapshot_id": snapshot_id,
270
+ "revision": self._revision,
271
+ "ingredients": {"revision": self._revision,
272
+ "operation": operation,
273
+ "arguments": dict(arguments)},
274
+ "request_ids": [request_id],
275
+ "state": "queued", "verdict": None,
276
+ "done": threading.Event(),
277
+ }
278
+ self._runs[run_id] = record
279
+ receipt = self._ack(run_id, request_id,
280
+ snapshot_id=snapshot_id)
281
+ self._record_request(request_id, operation,
282
+ arguments, key, run_id,
283
+ receipt)
284
+ if operation not in NEVER_COALESCE:
285
+ with self._coalescer.guard:
286
+ self._coalescer.register(key,
287
+ self._revision,
288
+ request_id, run_id,
289
+ snapshot_id)
290
+ self._queue.put({"run_id": run_id,
291
+ "request_id": request_id,
292
+ "operation": operation,
293
+ "arguments": dict(arguments),
294
+ "snapshot_id": snapshot_id})
295
+ self._emit_locked("job_queued", run_id, {})
296
+ kick = self._auto_dispatch
297
+ if kick:
298
+ self._kick_worker()
299
+ return DeferredHandle(self, run_id, receipt)
300
+
301
+ def validate_request(self, operation, arguments):
302
+ """Validate one call against the C01 request schema.
303
+
304
+ Args:
305
+ operation: Cataloged operation name.
306
+ arguments: Operation arguments mapping.
307
+
308
+ Returns:
309
+ True when the call is well formed.
310
+
311
+ Raises:
312
+ ExecutionError: invalid_command for a non-mapping
313
+ payload or an unknown semantic field,
314
+ unsupported_operation for an unknown name.
315
+ """
316
+ if not isinstance(arguments, dict):
317
+ raise ExecutionError(
318
+ "invalid_command",
319
+ f"Arguments must be a mapping, got {arguments!r}.",
320
+ )
321
+ if (self._operations is not None and
322
+ operation not in self._operations):
323
+ raise ExecutionError(
324
+ "unsupported_operation",
325
+ f"Unsupported operation: {operation}.",
326
+ )
327
+ allowed = self._schemas.get(operation)
328
+ if allowed is not None:
329
+ unknown = [key for key in arguments
330
+ if key not in allowed]
331
+ if unknown:
332
+ raise ExecutionError(
333
+ "invalid_command",
334
+ f"Unknown fields for {operation}: "
335
+ f"{sorted(unknown)}.",
336
+ fields=[{"fields": sorted(unknown)}],
337
+ )
338
+ return True
339
+
340
+ def pump(self, limit=None):
341
+ """Dispatch queued runs oldest first through the runner.
342
+
343
+ Args:
344
+ limit: Maximum entries to drain; the whole backlog
345
+ when omitted.
346
+
347
+ Returns:
348
+ Completions in dispatch order.
349
+ """
350
+ entries = self._queue.take_all()
351
+ if limit is not None:
352
+ requeue = entries[max(0, limit):]
353
+ entries = entries[:max(0, limit)]
354
+ for entry in reversed(requeue):
355
+ self._queue.put(entry)
356
+ completions = []
357
+ for entry in entries:
358
+ completions.append(self._drain_one(entry))
359
+ with self._lock:
360
+ if (self._paused and self._inflight_locked() <
361
+ self._max_inflight):
362
+ self._paused = False
363
+ self._emit_locked("admission_resumed", None,
364
+ {"depth":
365
+ self._inflight_locked()})
366
+ return completions
367
+
368
+ def status(self, run_id):
369
+ """Read one run's state, verdict, and operation.
370
+
371
+ Args:
372
+ run_id: Run to inspect.
373
+
374
+ Returns:
375
+ Status with run_id, operation, state, and verdict
376
+ once judged.
377
+
378
+ Raises:
379
+ ExecutionError: unknown_run for an untracked id.
380
+ """
381
+ with self._lock:
382
+ try:
383
+ record = self._runs[run_id]
384
+ except KeyError:
385
+ raise ExecutionError(
386
+ "unknown_run", f"Unknown run: {run_id}."
387
+ ) from None
388
+ out = {"run_id": run_id,
389
+ "operation": record["operation"],
390
+ "state": record["state"]}
391
+ if record["verdict"] is not None:
392
+ out["verdict"] = dict(record["verdict"])
393
+ return out
394
+
395
+ def wait_for(self, run_id, timeout=None):
396
+ """Block until one run reaches a terminal state.
397
+
398
+ Args:
399
+ run_id: Run to wait for.
400
+ timeout: Maximum seconds to wait; waits with the
401
+ run when omitted.
402
+
403
+ Returns:
404
+ Terminal completion for the run.
405
+
406
+ Raises:
407
+ ExecutionError: unknown_run for an untracked id.
408
+ TimeoutError: The wait lapsed before the run
409
+ finished.
410
+ """
411
+ with self._lock:
412
+ try:
413
+ record = self._runs[run_id]
414
+ except KeyError:
415
+ raise ExecutionError(
416
+ "unknown_run", f"Unknown run: {run_id}."
417
+ ) from None
418
+ done = record["done"]
419
+ if not done.wait(timeout):
420
+ raise TimeoutError(
421
+ f"Run did not finish in time: {run_id}."
422
+ )
423
+ return self._completion_locked(run_id)
424
+
425
+ def cancel(self, run_id, actor="unknown"):
426
+ """Cancel one queued or running run with attribution.
427
+
428
+ Cancelling a finished run truthfully returns its
429
+ terminal state, never a fake cancelled. Cancellation
430
+ needs no capacity slot: control stays responsive while
431
+ admission is saturated.
432
+
433
+ Args:
434
+ run_id: Run to cancel.
435
+ actor: Attribution for the lifecycle event.
436
+
437
+ Returns:
438
+ Outcome with run_id, state, actor, and verdict
439
+ when judged.
440
+
441
+ Raises:
442
+ ExecutionError: unknown_run for an untracked id.
443
+ """
444
+ with self._lock:
445
+ try:
446
+ record = self._runs[run_id]
447
+ except KeyError:
448
+ raise ExecutionError(
449
+ "unknown_run", f"Unknown run: {run_id}."
450
+ ) from None
451
+ if record["state"] not in ("queued", "running"):
452
+ out = {"run_id": run_id,
453
+ "state": record["state"], "actor": actor}
454
+ if record["verdict"] is not None:
455
+ out["verdict"] = dict(record["verdict"])
456
+ return out
457
+ entry = self._queue.take(run_id)
458
+ record["state"] = "cancelled"
459
+ record["verdict"] = {"status": "NOT_RUN",
460
+ "reason": "cancelled"}
461
+ if entry is not None:
462
+ self._coalescer.discard(run_id)
463
+ else:
464
+ record["cancel_requested"] = True
465
+ self._emit_locked("job_cancelled", run_id,
466
+ {"actor": actor})
467
+ record["done"].set()
468
+ return {"run_id": run_id, "state": "cancelled",
469
+ "actor": actor,
470
+ "verdict": dict(record["verdict"])}
471
+
472
+ def report_progress(self, run_id, fraction):
473
+ """Report one job_progress step for a live run.
474
+
475
+ Progress for a queued run is buffered and flushed in
476
+ order right after job_started, so the log always reads
477
+ queued, started, progress, finished. Progress for a
478
+ terminal run is refused instead of logged as a lie.
479
+
480
+ Args:
481
+ run_id: Live run to report progress for.
482
+ fraction: Completion fraction between 0 and 1.
483
+
484
+ Returns:
485
+ Emitted (or buffered) progress event.
486
+
487
+ Raises:
488
+ ExecutionError: unknown_run for an untracked id,
489
+ already_decided for a terminal run.
490
+ """
491
+ with self._lock:
492
+ try:
493
+ record = self._runs[run_id]
494
+ except KeyError:
495
+ raise ExecutionError(
496
+ "unknown_run", f"Unknown run: {run_id}."
497
+ ) from None
498
+ if record["state"] not in ("queued", "running"):
499
+ raise ExecutionError(
500
+ "already_decided",
501
+ f"Run already {record['state']}: {run_id}.",
502
+ )
503
+ record["progressed"] = True
504
+ if record["state"] == "queued":
505
+ pending = record.setdefault(
506
+ "pending_progress", [])
507
+ event = {"seq": None, "kind": "job_progress",
508
+ "run_id": run_id, "fraction": fraction,
509
+ "buffered": True}
510
+ pending.append(fraction)
511
+ return event
512
+ return self._emit_locked("job_progress", run_id,
513
+ {"fraction": fraction})
514
+
515
+ def request_ids_for(self, run_id):
516
+ """List the request ids mapped to one run, owner first.
517
+
518
+ Args:
519
+ run_id: Run to inspect.
520
+
521
+ Returns:
522
+ Mapped request ids; empty once the run is unknown.
523
+ """
524
+ with self._lock:
525
+ record = self._runs.get(run_id)
526
+ if record is None:
527
+ return []
528
+ return list(record["request_ids"])
529
+
530
+ def watch(self, cursor):
531
+ """Read ordered lifecycle events after a cursor.
532
+
533
+ Args:
534
+ cursor: Opaque position from a previous read; 0
535
+ replays the retained log.
536
+
537
+ Returns:
538
+ Events appended after the cursor, oldest first,
539
+ plus the current cursor.
540
+
541
+ Raises:
542
+ ExecutionError: cursor_expired when the cursor is
543
+ behind the retained floor or past the head —
544
+ re-read the snapshot and resubscribe.
545
+ """
546
+ with self._lock:
547
+ if not isinstance(cursor, int) or cursor < 0:
548
+ raise ExecutionError(
549
+ "invalid_command",
550
+ f"Invalid event cursor: {cursor!r}.",
551
+ )
552
+ head = self._seq_floor + len(self._events)
553
+ if cursor < self._seq_floor or cursor > head:
554
+ raise ExecutionError(
555
+ "cursor_expired",
556
+ f"Cursor {cursor} expired: re-read the "
557
+ f"snapshot and resubscribe from {head}.",
558
+ )
559
+ events = [dict(event) for event in
560
+ self._events[cursor - self._seq_floor:]]
561
+ return {"events": events, "cursor": head}
562
+
563
+ def add_listener(self, callback):
564
+ """Subscribe one push listener to lifecycle events.
565
+
566
+ Args:
567
+ callback: Called synchronously with each emitted
568
+ event mapping. Listener exceptions are
569
+ contained so one bad subscriber never breaks
570
+ admission or dispatch.
571
+
572
+ Returns:
573
+ Unsubscribe callable removing the listener.
574
+ """
575
+ with self._lock:
576
+ self._listeners.append(callback)
577
+
578
+ def unsubscribe():
579
+ """Remove the subscribed push listener.
580
+
581
+ Returns:
582
+ True when the listener was still subscribed.
583
+ """
584
+ with self._lock:
585
+ if callback in self._listeners:
586
+ self._listeners.remove(callback)
587
+ return True
588
+ return False
589
+
590
+ return unsubscribe
591
+
592
+ def pause_visible(self):
593
+ """Check whether admission saturation is signalled.
594
+
595
+ Returns:
596
+ True while a refusal is outstanding against the
597
+ bound; False once capacity frees.
598
+ """
599
+ with self._lock:
600
+ return self._paused
601
+
602
+ def attach_port(self, name, push=True, max_depth=None):
603
+ """Attach one notify port to the lifecycle event log.
604
+
605
+ Every appended lifecycle event fans out to the port's
606
+ async queue; the writer's path never waits for it.
607
+
608
+ Args:
609
+ name: Port name (stdio, mcp, ide, loopback-http).
610
+ push: True for async push delivery; False leaves
611
+ the port pull-only over watch.
612
+ max_depth: Per-port queue ceiling; the bus default
613
+ when omitted.
614
+
615
+ Returns:
616
+ Attached port descriptor.
617
+
618
+ Raises:
619
+ ExecutionError: invalid_command for a bad name,
620
+ port_exists for an attached name.
621
+ """
622
+ return self._ports.attach(name, push=push,
623
+ max_depth=max_depth)
624
+
625
+ def detach_port(self, name):
626
+ """Detach one notify port; its backlog is dropped.
627
+
628
+ Args:
629
+ name: Attached port name to remove.
630
+
631
+ Returns:
632
+ Detached port descriptor.
633
+
634
+ Raises:
635
+ ExecutionError: unknown_port for a detached name.
636
+ """
637
+ return self._ports.detach(name)
638
+
639
+ def port_state(self, name):
640
+ """Read one port's engine-side bookkeeping.
641
+
642
+ Args:
643
+ name: Attached port name.
644
+
645
+ Returns:
646
+ Descriptor with name, push, enabled, state,
647
+ resync_required, backlog, cursor, acked, and
648
+ max_depth.
649
+
650
+ Raises:
651
+ ExecutionError: unknown_port for a detached name.
652
+ """
653
+ return self._ports.port_state(name)
654
+
655
+ def port_pending(self, name):
656
+ """Read one port's queued lifecycle events.
657
+
658
+ Args:
659
+ name: Attached port name.
660
+
661
+ Returns:
662
+ Queued event copies, oldest first.
663
+
664
+ Raises:
665
+ ExecutionError: unknown_port for a detached name.
666
+ """
667
+ return self._ports.pending(name)
668
+
669
+ def port_ack(self, name, seq):
670
+ """Acknowledge delivery through one ledger position.
671
+
672
+ Args:
673
+ name: Attached port name.
674
+ seq: Lifecycle position acknowledged.
675
+
676
+ Returns:
677
+ Ack record with port, acked position, and backlog.
678
+
679
+ Raises:
680
+ ExecutionError: unknown_port for a detached name,
681
+ invalid_command for a non-integer position.
682
+ """
683
+ return self._ports.ack(name, seq)
684
+
685
+ def port_drain(self, name, handler, limit=None,
686
+ guarded=True):
687
+ """Deliver one port's backlog through a handler.
688
+
689
+ Args:
690
+ name: Attached port name.
691
+ handler: Callable taking one event mapping.
692
+ limit: Maximum events to deliver; the whole
693
+ backlog when omitted.
694
+ guarded: True applies each position once through
695
+ the port's bound guard.
696
+
697
+ Returns:
698
+ Drain record with delivered and applied counts,
699
+ the acked position, and handler errors.
700
+
701
+ Raises:
702
+ ExecutionError: unknown_port for a detached name.
703
+ """
704
+ return self._ports.drain(name, handler, limit=limit,
705
+ guarded=guarded)
706
+
707
+ def port_evaluate(self):
708
+ """Run the staged no-ack ladder over every port.
709
+
710
+ Args:
711
+ None.
712
+
713
+ Returns:
714
+ Directive mappings in attach order.
715
+ """
716
+ return self._ports.port_evaluate()
717
+
718
+ def port_resync(self, name, head=None):
719
+ """Converge one port onto the current lifecycle head.
720
+
721
+ Args:
722
+ name: Attached port name.
723
+ head: Position to converge on; the bus head when
724
+ omitted.
725
+
726
+ Returns:
727
+ Resync receipt with port, cursor, and state.
728
+
729
+ Raises:
730
+ ExecutionError: unknown_port for a detached name,
731
+ invalid_command for a non-integer head.
732
+ """
733
+ return self._ports.port_resync(name, head=head)
734
+
735
+ def set_port_enabled(self, name, enabled, local=False):
736
+ """Enable or disable one port live (local-only).
737
+
738
+ Args:
739
+ name: Attached port name.
740
+ enabled: True for live delivery, False to park
741
+ the port (backlog dropped, resync flagged).
742
+ local: Caller asserts a local caller; anything
743
+ else is refused loudly.
744
+
745
+ Returns:
746
+ Updated port descriptor.
747
+
748
+ Raises:
749
+ ExecutionError: unknown_port for a detached name,
750
+ remote_toggle_denied for a non-local caller.
751
+ """
752
+ return self._ports.set_enabled(name, enabled,
753
+ local=local)
754
+
755
+ def set_port_profile(self, names, local=False):
756
+ """Swap the live port profile (local-only toggle).
757
+
758
+ Args:
759
+ names: Port names forming the live profile.
760
+ local: Caller asserts a local caller; anything
761
+ else is refused loudly.
762
+
763
+ Returns:
764
+ Profile mapping with the enabled names sorted.
765
+
766
+ Raises:
767
+ ExecutionError: remote_toggle_denied for a
768
+ non-local caller, invalid_command for a bad
769
+ profile.
770
+ """
771
+ return self._ports.set_profile(names, local=local)
772
+
773
+ def apply_edit(self):
774
+ """Advance the revision cursor for one editor commit.
775
+
776
+ Editor commits ride their own lane: they never wait
777
+ behind solver capacity and only move the coalescing
778
+ cursor so later submissions execute fresh.
779
+
780
+ Returns:
781
+ New revision.
782
+ """
783
+ with self._lock:
784
+ self._revision += 1
785
+ return self._revision
786
+
787
+ def boot_report(self):
788
+ """Report unfinished survivors without executing any.
789
+
790
+ Returns:
791
+ Survivors (admitted but unfinished run ids),
792
+ interrupted runs, and quarantined runs.
793
+ """
794
+ with self._lock:
795
+ survivors = [run_id for run_id, record in
796
+ self._runs.items()
797
+ if record["state"] in ("queued",
798
+ "running")]
799
+ return {"survivors": survivors,
800
+ "interrupted": sorted(
801
+ run_id for run_id, count in
802
+ self._interruptions.items() if count),
803
+ "quarantined": sorted(self._quarantined)}
804
+
805
+ def resume(self, run_id, override_quarantine=False):
806
+ """Explicitly resume one survivor through the runner.
807
+
808
+ Args:
809
+ run_id: Survivor to execute now.
810
+ override_quarantine: Required to resume a run
811
+ quarantined after repeated interruptions.
812
+
813
+ Returns:
814
+ Terminal completion for the run.
815
+
816
+ Raises:
817
+ ExecutionError: unknown_run for an untracked id,
818
+ poison_quarantined without an explicit
819
+ override, already_decided for a discarded run.
820
+ """
821
+ with self._lock:
822
+ try:
823
+ record = self._runs[run_id]
824
+ except KeyError:
825
+ raise ExecutionError(
826
+ "unknown_run", f"Unknown run: {run_id}."
827
+ ) from None
828
+ if record["state"] == "discarded":
829
+ raise ExecutionError(
830
+ "already_decided",
831
+ f"Run already discarded: {run_id}.",
832
+ )
833
+ if (run_id in self._quarantined and
834
+ not override_quarantine):
835
+ raise ExecutionError(
836
+ "poison_quarantined",
837
+ f"Run quarantined after repeated "
838
+ f"interruptions: {run_id}.",
839
+ )
840
+ if record["state"] not in ("queued", "running"):
841
+ return self._completion_locked(run_id)
842
+ entry = self._queue.take(run_id)
843
+ if entry is None:
844
+ entry = {"run_id": run_id,
845
+ "request_id":
846
+ record["request_ids"][0],
847
+ "operation": record["operation"],
848
+ "arguments": dict(
849
+ record["arguments"]),
850
+ "snapshot_id":
851
+ record["snapshot_id"]}
852
+ return self._drain_one(entry)
853
+
854
+ def discard(self, run_id):
855
+ """Explicitly discard one survivor without executing it.
856
+
857
+ Args:
858
+ run_id: Survivor to discard.
859
+
860
+ Returns:
861
+ Discard record with run_id and discarded state.
862
+
863
+ Raises:
864
+ ExecutionError: unknown_run for an untracked id.
865
+ """
866
+ with self._lock:
867
+ try:
868
+ record = self._runs[run_id]
869
+ except KeyError:
870
+ raise ExecutionError(
871
+ "unknown_run", f"Unknown run: {run_id}."
872
+ ) from None
873
+ self._queue.take(run_id)
874
+ record["state"] = "discarded"
875
+ record["done"].set()
876
+ self._coalescer.discard(run_id)
877
+ self._emit_locked("job_cancelled", run_id,
878
+ {"reason": "discarded"})
879
+ return {"run_id": run_id, "state": "discarded"}
880
+
881
+ def note_interruption(self, run_id):
882
+ """Record one ambiguous interruption toward quarantine.
883
+
884
+ Args:
885
+ run_id: Run interrupted before a terminal state.
886
+
887
+ Returns:
888
+ Interruption count for the run.
889
+ """
890
+ with self._lock:
891
+ count = self._interruptions.get(run_id, 0) + 1
892
+ self._interruptions[run_id] = count
893
+ if count >= QUARANTINE_AFTER:
894
+ self._quarantined.add(run_id)
895
+ return count
896
+
897
+ def check_drift(self, run_id, current):
898
+ """Compare pinned ingredients against current inputs.
899
+
900
+ Args:
901
+ run_id: Run whose pinned ingredients to check.
902
+ current: Current input mapping with revision,
903
+ operation, and arguments.
904
+
905
+ Returns:
906
+ Drift report with drifted, changed fields, pinned
907
+ snapshot, and current snapshot.
908
+
909
+ Raises:
910
+ ExecutionError: unknown_run for an untracked id.
911
+ """
912
+ with self._lock:
913
+ try:
914
+ record = self._runs[run_id]
915
+ except KeyError:
916
+ raise ExecutionError(
917
+ "unknown_run", f"Unknown run: {run_id}."
918
+ ) from None
919
+ pinned = record["ingredients"]
920
+ changed = sorted(
921
+ key for key in ("revision", "operation",
922
+ "arguments")
923
+ if pinned.get(key) != current.get(key))
924
+ return {"run_id": run_id, "drifted": bool(changed),
925
+ "changed": changed,
926
+ "pinned": dict(pinned), "current": dict(current)}
927
+
928
+ def _inflight_locked(self):
929
+ """Count queued plus running runs under the lock.
930
+
931
+ Returns:
932
+ Undrained run count.
933
+ """
934
+ return sum(1 for record in self._runs.values()
935
+ if record["state"] in ("queued", "running"))
936
+
937
+ def _ack(self, run_id, request_id, coalesced=False,
938
+ snapshot_id=None):
939
+ """Build the prompt acknowledgement receipt.
940
+
941
+ Args:
942
+ run_id: Admitted run id.
943
+ request_id: Client envelope id.
944
+ coalesced: True when the caller joined a shared run.
945
+ snapshot_id: Evidence snapshot the run pins.
946
+
947
+ Returns:
948
+ Receipt with admitted, identity fields, queued
949
+ state, revision, and event cursor — never a
950
+ verdict.
951
+ """
952
+ ack = {"admitted": True, "project_id":
953
+ self._project_id, "run_id": run_id,
954
+ "request_id": request_id,
955
+ "snapshot_id": snapshot_id or
956
+ f"snapshot_{self._revision}",
957
+ "state": "queued", "revision": self._revision,
958
+ "event_cursor": self._seq_floor +
959
+ len(self._events)}
960
+ if coalesced:
961
+ ack["coalesced"] = True
962
+ return ack
963
+
964
+ def _record_request(self, request_id, operation, arguments,
965
+ key, run_id, receipt):
966
+ """Persist the durable request/run identity mapping.
967
+
968
+ Args:
969
+ request_id: Client envelope id.
970
+ operation: Admitted operation name.
971
+ arguments: Admitted arguments copy.
972
+ key: Content key for the submission.
973
+ run_id: Admitted run id.
974
+ receipt: Prompt acknowledgement for replays.
975
+ """
976
+ self._requests[request_id] = {
977
+ "run_id": run_id, "operation": operation,
978
+ "arguments": dict(arguments), "key": key,
979
+ "receipt": dict(receipt),
980
+ }
981
+ record = self._runs.get(run_id)
982
+ if (record is not None and request_id not in
983
+ record["request_ids"]):
984
+ record["request_ids"].append(request_id)
985
+
986
+ def _emit_locked(self, kind, run_id, data):
987
+ """Append one lifecycle event and push to listeners.
988
+
989
+ Args:
990
+ kind: Lifecycle kind (job_queued, job_started,
991
+ job_progress, job_finished, job_cancelled,
992
+ admission_paused, admission_resumed).
993
+ run_id: Run the event belongs to, if any.
994
+ data: Event payload fields.
995
+
996
+ Returns:
997
+ Emitted event with seq, kind, run_id, and cursor.
998
+ """
999
+ seq = self._seq_floor + len(self._events) + 1
1000
+ event = {"seq": seq, "kind": kind, "run_id": run_id,
1001
+ "cursor": seq}
1002
+ event.update(dict(data or {}))
1003
+ record = self._runs.get(run_id) if run_id else None
1004
+ if record is not None:
1005
+ event.setdefault("operation",
1006
+ record["operation"])
1007
+ if record["request_ids"]:
1008
+ event.setdefault("request_id",
1009
+ record["request_ids"][0])
1010
+ self._events.append(event)
1011
+ if len(self._events) > RETAINED_EVENTS:
1012
+ overflow = len(self._events) - RETAINED_EVENTS
1013
+ del self._events[:overflow]
1014
+ self._seq_floor += overflow
1015
+ for callback in list(self._listeners):
1016
+ try:
1017
+ callback(dict(event))
1018
+ except Exception: # noqa: BLE001 — contained
1019
+ continue
1020
+ # Port fan-out rides the same appended event: ingest is
1021
+ # non-blocking and quarantines slow ports instead of
1022
+ # stalling the writer, so emission never fails here.
1023
+ self._ports.ingest([event])
1024
+ return dict(event)
1025
+
1026
+ def _completion_locked(self, run_id):
1027
+ """Build the terminal completion for a finished run.
1028
+
1029
+ Args:
1030
+ run_id: Run to report.
1031
+
1032
+ Returns:
1033
+ Completion with run_id, state, operation, and
1034
+ verdict when judged.
1035
+ """
1036
+ record = self._runs[run_id]
1037
+ out = {"run_id": run_id, "state": record["state"],
1038
+ "operation": record["operation"]}
1039
+ if record["verdict"] is not None:
1040
+ out["verdict"] = dict(record["verdict"])
1041
+ if record.get("reason") is not None:
1042
+ out["reason"] = record["reason"]
1043
+ return out
1044
+
1045
+ def _drain_one(self, entry):
1046
+ """Run one queued entry through the runner, contained.
1047
+
1048
+ Args:
1049
+ entry: Queue entry with run_id, request_id,
1050
+ operation, arguments, and snapshot_id.
1051
+
1052
+ Returns:
1053
+ Terminal completion for the run.
1054
+ """
1055
+ run_id = entry["run_id"]
1056
+ with self._lock:
1057
+ record = self._runs.get(run_id)
1058
+ if record is None or record["state"] != "queued":
1059
+ return self._completion_locked(run_id)
1060
+ record["state"] = "running"
1061
+ self._emit_locked("job_started", run_id, {})
1062
+ pending = record.pop("pending_progress", [])
1063
+ for fraction in pending:
1064
+ self._emit_locked("job_progress", run_id,
1065
+ {"fraction": fraction})
1066
+ if not record.get("progressed"):
1067
+ self._emit_locked("job_progress", run_id,
1068
+ {"fraction": 0.0,
1069
+ "synthetic": True})
1070
+ arguments = dict(entry["arguments"])
1071
+ snapshot_id = entry["snapshot_id"]
1072
+ request_id = entry["request_id"]
1073
+ operation = entry["operation"]
1074
+ try:
1075
+ completion = self._runner(operation, arguments,
1076
+ snapshot_id, request_id,
1077
+ run_id)
1078
+ except Exception as exc: # noqa: BLE001 — recorded
1079
+ completion = {"run_id": run_id, "state": "failed",
1080
+ "operation": operation,
1081
+ "error": f"{type(exc).__name__}: {exc}",
1082
+ "verdict": {"status": "NOT_RUN",
1083
+ "reason": str(exc)}}
1084
+ with self._lock:
1085
+ record = self._runs.get(run_id)
1086
+ if record is not None and record["state"] == \
1087
+ "running":
1088
+ if record.get("cancel_requested"):
1089
+ record["state"] = "cancelled"
1090
+ record["verdict"] = {"status": "NOT_RUN",
1091
+ "reason": "cancelled"}
1092
+ self._emit_locked("job_cancelled", run_id,
1093
+ {})
1094
+ else:
1095
+ record["state"] = completion.get(
1096
+ "state", "completed")
1097
+ record["verdict"] = completion.get(
1098
+ "verdict")
1099
+ if completion.get("reason") is not None:
1100
+ record["reason"] = completion[
1101
+ "reason"]
1102
+ self._emit_locked("job_finished", run_id,
1103
+ {"verdict":
1104
+ record["verdict"]})
1105
+ record["done"].set()
1106
+ if run_id in self._runs:
1107
+ self._coalescer.complete(
1108
+ run_id, self._completion_locked(run_id))
1109
+ if (self._paused and self._inflight_locked() <
1110
+ self._max_inflight):
1111
+ self._paused = False
1112
+ self._emit_locked("admission_resumed", None,
1113
+ {"depth":
1114
+ self._inflight_locked()})
1115
+ return self._completion_locked(run_id)
1116
+
1117
+ def _kick_worker(self):
1118
+ """Start the background dispatcher when idle.
1119
+
1120
+ Returns:
1121
+ True when a worker was started.
1122
+ """
1123
+ with self._lock:
1124
+ if self._pumping or not self._queue.pending():
1125
+ return False
1126
+ self._pumping = True
1127
+
1128
+ def _work():
1129
+ try:
1130
+ while self._queue.pending():
1131
+ entries = self._queue.take_all()
1132
+ for entry in entries:
1133
+ self._drain_one(entry)
1134
+ finally:
1135
+ with self._lock:
1136
+ self._pumping = False
1137
+ if (self._paused and
1138
+ self._inflight_locked() <
1139
+ self._max_inflight):
1140
+ self._paused = False
1141
+ self._emit_locked("admission_resumed",
1142
+ None, {"depth":
1143
+ self._inflight_locked()})
1144
+
1145
+ worker = threading.Thread(target=_work, daemon=True,
1146
+ name="engine-runtime-drain")
1147
+ worker.start()
1148
+ return True
1149
+