@spine-event-engine/server 2.0.0-snapshot.23 → 2.0.0-snapshot.25

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 (300) hide show
  1. package/README.md +38 -11
  2. package/REFERENCE.md +278 -141
  3. package/dist/agent/agent-acceptance-receipts.d.ts +69 -0
  4. package/dist/agent/agent-acceptance-receipts.d.ts.map +1 -0
  5. package/dist/agent/agent-acceptance-receipts.js +198 -0
  6. package/dist/agent/agent-acceptance-receipts.js.map +1 -0
  7. package/dist/agent/agent-execution-capacity.d.ts +37 -9
  8. package/dist/agent/agent-execution-capacity.d.ts.map +1 -1
  9. package/dist/agent/agent-execution-capacity.js +73 -15
  10. package/dist/agent/agent-execution-capacity.js.map +1 -1
  11. package/dist/agent/agent-history.d.ts +15 -10
  12. package/dist/agent/agent-history.d.ts.map +1 -1
  13. package/dist/agent/agent-history.js +26 -14
  14. package/dist/agent/agent-history.js.map +1 -1
  15. package/dist/agent/agent-live-ai-audit.d.ts +95 -0
  16. package/dist/agent/agent-live-ai-audit.d.ts.map +1 -0
  17. package/dist/agent/agent-live-ai-audit.js +290 -0
  18. package/dist/agent/agent-live-ai-audit.js.map +1 -0
  19. package/dist/agent/agent-live-ai-runtime.d.ts +300 -0
  20. package/dist/agent/agent-live-ai-runtime.d.ts.map +1 -0
  21. package/dist/agent/agent-live-ai-runtime.js +611 -0
  22. package/dist/agent/agent-live-ai-runtime.js.map +1 -0
  23. package/dist/agent/agent-live-audit.d.ts +67 -0
  24. package/dist/agent/agent-live-audit.d.ts.map +1 -0
  25. package/dist/agent/agent-live-audit.js +121 -0
  26. package/dist/agent/agent-live-audit.js.map +1 -0
  27. package/dist/agent/agent-live-budget.d.ts +84 -0
  28. package/dist/agent/agent-live-budget.d.ts.map +1 -0
  29. package/dist/agent/agent-live-budget.js +161 -0
  30. package/dist/agent/agent-live-budget.js.map +1 -0
  31. package/dist/agent/agent-live-fault.d.ts +18 -0
  32. package/dist/agent/agent-live-fault.d.ts.map +1 -0
  33. package/dist/agent/{agent-execution-fault.js → agent-live-fault.js} +7 -7
  34. package/dist/agent/agent-live-fault.js.map +1 -0
  35. package/dist/agent/agent-live-inputs.d.ts +74 -0
  36. package/dist/agent/agent-live-inputs.d.ts.map +1 -0
  37. package/dist/agent/agent-live-inputs.js +15 -0
  38. package/dist/agent/agent-live-inputs.js.map +1 -0
  39. package/dist/agent/agent-live-interaction-events.d.ts +46 -0
  40. package/dist/agent/agent-live-interaction-events.d.ts.map +1 -0
  41. package/dist/agent/agent-live-interaction-events.js +104 -0
  42. package/dist/agent/agent-live-interaction-events.js.map +1 -0
  43. package/dist/agent/agent-live-mcp-controls.d.ts +129 -0
  44. package/dist/agent/agent-live-mcp-controls.d.ts.map +1 -0
  45. package/dist/agent/agent-live-mcp-controls.js +228 -0
  46. package/dist/agent/agent-live-mcp-controls.js.map +1 -0
  47. package/dist/agent/agent-live-mcp-host.d.ts +88 -0
  48. package/dist/agent/agent-live-mcp-host.d.ts.map +1 -0
  49. package/dist/agent/{agent-mcp-host.js → agent-live-mcp-host.js} +1 -1
  50. package/dist/agent/agent-live-mcp-host.js.map +1 -0
  51. package/dist/agent/{agent-mcp-runtime.d.ts → agent-live-mcp-runtime.d.ts} +6 -6
  52. package/dist/agent/agent-live-mcp-runtime.d.ts.map +1 -0
  53. package/dist/agent/{agent-mcp-runtime.js → agent-live-mcp-runtime.js} +13 -42
  54. package/dist/agent/agent-live-mcp-runtime.js.map +1 -0
  55. package/dist/agent/agent-live-read-runtime.d.ts +61 -0
  56. package/dist/agent/agent-live-read-runtime.d.ts.map +1 -0
  57. package/dist/agent/agent-live-read-runtime.js +121 -0
  58. package/dist/agent/agent-live-read-runtime.js.map +1 -0
  59. package/dist/agent/agent-model-selection.d.ts +21 -34
  60. package/dist/agent/agent-model-selection.d.ts.map +1 -1
  61. package/dist/agent/agent-model-selection.js +61 -78
  62. package/dist/agent/agent-model-selection.js.map +1 -1
  63. package/dist/bus/command-bus.d.ts +14 -18
  64. package/dist/bus/command-bus.d.ts.map +1 -1
  65. package/dist/bus/command-bus.js +31 -48
  66. package/dist/bus/command-bus.js.map +1 -1
  67. package/dist/bus/event-bus.d.ts +82 -104
  68. package/dist/bus/event-bus.d.ts.map +1 -1
  69. package/dist/bus/event-bus.js +87 -127
  70. package/dist/bus/event-bus.js.map +1 -1
  71. package/dist/context/bounded-context.d.ts +143 -91
  72. package/dist/context/bounded-context.d.ts.map +1 -1
  73. package/dist/context/bounded-context.js +753 -359
  74. package/dist/context/bounded-context.js.map +1 -1
  75. package/dist/context/entity-inbox.d.ts +7 -2
  76. package/dist/context/entity-inbox.d.ts.map +1 -1
  77. package/dist/context/entity-inbox.js +43 -20
  78. package/dist/context/entity-inbox.js.map +1 -1
  79. package/dist/context/local-inbox-handoff.d.ts +35 -34
  80. package/dist/context/local-inbox-handoff.d.ts.map +1 -1
  81. package/dist/context/local-inbox-handoff.js +123 -135
  82. package/dist/context/local-inbox-handoff.js.map +1 -1
  83. package/dist/context/projection-handoff.d.ts +2 -2
  84. package/dist/context/projection-handoff.d.ts.map +1 -1
  85. package/dist/context/projection-handoff.js +12 -8
  86. package/dist/context/projection-handoff.js.map +1 -1
  87. package/dist/context/tenant-index.d.ts +36 -31
  88. package/dist/context/tenant-index.d.ts.map +1 -1
  89. package/dist/context/tenant-index.js +123 -125
  90. package/dist/context/tenant-index.js.map +1 -1
  91. package/dist/context/tenant-unknown-error.d.ts +33 -0
  92. package/dist/context/tenant-unknown-error.d.ts.map +1 -0
  93. package/dist/context/tenant-unknown-error.js +75 -0
  94. package/dist/context/tenant-unknown-error.js.map +1 -0
  95. package/dist/delivery/agent-delivery-scope.d.ts +66 -0
  96. package/dist/delivery/agent-delivery-scope.d.ts.map +1 -0
  97. package/dist/delivery/agent-delivery-scope.js +44 -0
  98. package/dist/delivery/agent-delivery-scope.js.map +1 -0
  99. package/dist/delivery/delivery-builder.d.ts +25 -1
  100. package/dist/delivery/delivery-builder.d.ts.map +1 -1
  101. package/dist/delivery/delivery-builder.js +89 -0
  102. package/dist/delivery/delivery-builder.js.map +1 -1
  103. package/dist/delivery/delivery-monitor.d.ts +12 -9
  104. package/dist/delivery/delivery-monitor.d.ts.map +1 -1
  105. package/dist/delivery/delivery-monitor.js +15 -9
  106. package/dist/delivery/delivery-monitor.js.map +1 -1
  107. package/dist/delivery/delivery-ports.d.ts +6 -13
  108. package/dist/delivery/delivery-ports.d.ts.map +1 -1
  109. package/dist/delivery/delivery-run-control.d.ts +21 -0
  110. package/dist/delivery/delivery-run-control.d.ts.map +1 -1
  111. package/dist/delivery/delivery-run-control.js +10 -0
  112. package/dist/delivery/delivery-run-control.js.map +1 -1
  113. package/dist/delivery/delivery-supervisor.d.ts +33 -5
  114. package/dist/delivery/delivery-supervisor.d.ts.map +1 -1
  115. package/dist/delivery/delivery-supervisor.js +178 -26
  116. package/dist/delivery/delivery-supervisor.js.map +1 -1
  117. package/dist/delivery/delivery-tenant.d.ts +17 -0
  118. package/dist/delivery/delivery-tenant.d.ts.map +1 -0
  119. package/dist/delivery/delivery-tenant.js +50 -0
  120. package/dist/delivery/delivery-tenant.js.map +1 -0
  121. package/dist/delivery/delivery.d.ts +96 -2
  122. package/dist/delivery/delivery.d.ts.map +1 -1
  123. package/dist/delivery/delivery.js +803 -89
  124. package/dist/delivery/delivery.js.map +1 -1
  125. package/dist/delivery/inbox-records.d.ts.map +1 -1
  126. package/dist/delivery/inbox-records.js +9 -16
  127. package/dist/delivery/inbox-records.js.map +1 -1
  128. package/dist/delivery/inbox-storage.d.ts +14 -11
  129. package/dist/delivery/inbox-storage.d.ts.map +1 -1
  130. package/dist/delivery/inbox-storage.js +72 -78
  131. package/dist/delivery/inbox-storage.js.map +1 -1
  132. package/dist/delivery/inbox.d.ts +9 -12
  133. package/dist/delivery/inbox.d.ts.map +1 -1
  134. package/dist/delivery/inbox.js +13 -15
  135. package/dist/delivery/inbox.js.map +1 -1
  136. package/dist/delivery/reception-observation.d.ts +64 -0
  137. package/dist/delivery/reception-observation.d.ts.map +1 -0
  138. package/dist/delivery/reception-observation.js +78 -0
  139. package/dist/delivery/reception-observation.js.map +1 -0
  140. package/dist/delivery/sharded-work-registry.d.ts +6 -0
  141. package/dist/delivery/sharded-work-registry.d.ts.map +1 -1
  142. package/dist/delivery/sharded-work-registry.js +41 -4
  143. package/dist/delivery/sharded-work-registry.js.map +1 -1
  144. package/dist/entity/entity-storage-descriptor.d.ts.map +1 -1
  145. package/dist/entity/entity-storage-descriptor.js +7 -4
  146. package/dist/entity/entity-storage-descriptor.js.map +1 -1
  147. package/dist/index.d.ts +4 -2
  148. package/dist/index.d.ts.map +1 -1
  149. package/dist/index.js +2 -1
  150. package/dist/index.js.map +1 -1
  151. package/dist/integration/external-messages.d.ts.map +1 -1
  152. package/dist/integration/external-messages.js +9 -15
  153. package/dist/integration/external-messages.js.map +1 -1
  154. package/dist/integration/integration-broker.d.ts.map +1 -1
  155. package/dist/integration/integration-broker.js +112 -6
  156. package/dist/integration/integration-broker.js.map +1 -1
  157. package/dist/integration/third-party-context.d.ts.map +1 -1
  158. package/dist/integration/third-party-context.js +7 -10
  159. package/dist/integration/third-party-context.js.map +1 -1
  160. package/dist/repository/primitive-id.js +26 -11
  161. package/dist/repository/primitive-id.js.map +1 -1
  162. package/dist/repository/repository.d.ts +33 -62
  163. package/dist/repository/repository.d.ts.map +1 -1
  164. package/dist/repository/repository.js +1475 -1827
  165. package/dist/repository/repository.js.map +1 -1
  166. package/dist/runtime/runtime.d.ts +24 -0
  167. package/dist/runtime/runtime.d.ts.map +1 -1
  168. package/dist/runtime/runtime.js +31 -0
  169. package/dist/runtime/runtime.js.map +1 -1
  170. package/dist/runtime/signal-metadata.d.ts.map +1 -1
  171. package/dist/runtime/signal-metadata.js +8 -15
  172. package/dist/runtime/signal-metadata.js.map +1 -1
  173. package/dist/runtime/signal-publisher.d.ts +8 -36
  174. package/dist/runtime/signal-publisher.d.ts.map +1 -1
  175. package/dist/runtime/signal-publisher.js +68 -61
  176. package/dist/runtime/signal-publisher.js.map +1 -1
  177. package/dist/runtime/standalone-handler-runtime.d.ts +0 -7
  178. package/dist/runtime/standalone-handler-runtime.d.ts.map +1 -1
  179. package/dist/runtime/standalone-handler-runtime.js +14 -130
  180. package/dist/runtime/standalone-handler-runtime.js.map +1 -1
  181. package/dist/server/managed-delivery-gate.d.ts +34 -0
  182. package/dist/server/managed-delivery-gate.d.ts.map +1 -0
  183. package/dist/server/managed-delivery-gate.js +70 -0
  184. package/dist/server/managed-delivery-gate.js.map +1 -0
  185. package/dist/server/managed-server-application.d.ts +2 -2
  186. package/dist/server/managed-server-application.d.ts.map +1 -1
  187. package/dist/server/managed-server-application.js +471 -178
  188. package/dist/server/managed-server-application.js.map +1 -1
  189. package/dist/server/node-coordinator.d.ts +14 -0
  190. package/dist/server/node-coordinator.d.ts.map +1 -1
  191. package/dist/server/node-coordinator.js +266 -45
  192. package/dist/server/node-coordinator.js.map +1 -1
  193. package/dist/server/server-environment.d.ts +203 -25
  194. package/dist/server/server-environment.d.ts.map +1 -1
  195. package/dist/server/server-environment.js +661 -133
  196. package/dist/server/server-environment.js.map +1 -1
  197. package/dist/server/server-log.d.ts +7 -0
  198. package/dist/server/server-log.d.ts.map +1 -1
  199. package/dist/server/server-log.js +18 -1
  200. package/dist/server/server-log.js.map +1 -1
  201. package/dist/server/server.d.ts +18 -32
  202. package/dist/server/server.d.ts.map +1 -1
  203. package/dist/server/server.js +62 -203
  204. package/dist/server/server.js.map +1 -1
  205. package/dist/services/spine-services.d.ts +1 -1
  206. package/dist/services/spine-services.d.ts.map +1 -1
  207. package/dist/services/spine-services.js +399 -370
  208. package/dist/services/spine-services.js.map +1 -1
  209. package/dist/stand/event-origin-filter.d.ts +16 -0
  210. package/dist/stand/event-origin-filter.d.ts.map +1 -0
  211. package/dist/stand/event-origin-filter.js +124 -0
  212. package/dist/stand/event-origin-filter.js.map +1 -0
  213. package/dist/stand/stand.d.ts +14 -0
  214. package/dist/stand/stand.d.ts.map +1 -1
  215. package/dist/stand/stand.js +53 -9
  216. package/dist/stand/stand.js.map +1 -1
  217. package/dist/stand/subscription-observer.d.ts.map +1 -1
  218. package/dist/stand/subscription-observer.js +61 -40
  219. package/dist/stand/subscription-observer.js.map +1 -1
  220. package/dist/stand/subscription-registry.d.ts +6 -2
  221. package/dist/stand/subscription-registry.d.ts.map +1 -1
  222. package/dist/stand/subscription-registry.js +15 -11
  223. package/dist/stand/subscription-registry.js.map +1 -1
  224. package/dist/stand/subscription-runtime.d.ts +23 -4
  225. package/dist/stand/subscription-runtime.d.ts.map +1 -1
  226. package/dist/stand/subscription-runtime.js +84 -13
  227. package/dist/stand/subscription-runtime.js.map +1 -1
  228. package/dist/testing/index.d.ts.map +1 -1
  229. package/dist/testing/index.js +3 -2
  230. package/dist/testing/index.js.map +1 -1
  231. package/dist/tsconfig.tsbuildinfo +1 -1
  232. package/package.json +15 -15
  233. package/dist/agent/agent-admission.d.ts +0 -80
  234. package/dist/agent/agent-admission.d.ts.map +0 -1
  235. package/dist/agent/agent-admission.js +0 -127
  236. package/dist/agent/agent-admission.js.map +0 -1
  237. package/dist/agent/agent-ai-runtime.d.ts +0 -76
  238. package/dist/agent/agent-ai-runtime.d.ts.map +0 -1
  239. package/dist/agent/agent-ai-runtime.js +0 -1269
  240. package/dist/agent/agent-ai-runtime.js.map +0 -1
  241. package/dist/agent/agent-execution-fault.d.ts +0 -18
  242. package/dist/agent/agent-execution-fault.d.ts.map +0 -1
  243. package/dist/agent/agent-execution-fault.js.map +0 -1
  244. package/dist/agent/agent-execution-session.d.ts +0 -103
  245. package/dist/agent/agent-execution-session.d.ts.map +0 -1
  246. package/dist/agent/agent-execution-session.js +0 -238
  247. package/dist/agent/agent-execution-session.js.map +0 -1
  248. package/dist/agent/agent-interaction-audit.d.ts +0 -58
  249. package/dist/agent/agent-interaction-audit.d.ts.map +0 -1
  250. package/dist/agent/agent-interaction-audit.js +0 -114
  251. package/dist/agent/agent-interaction-audit.js.map +0 -1
  252. package/dist/agent/agent-interaction-events.d.ts +0 -40
  253. package/dist/agent/agent-interaction-events.d.ts.map +0 -1
  254. package/dist/agent/agent-interaction-events.js +0 -129
  255. package/dist/agent/agent-interaction-events.js.map +0 -1
  256. package/dist/agent/agent-mcp-budget.d.ts +0 -125
  257. package/dist/agent/agent-mcp-budget.d.ts.map +0 -1
  258. package/dist/agent/agent-mcp-budget.js +0 -566
  259. package/dist/agent/agent-mcp-budget.js.map +0 -1
  260. package/dist/agent/agent-mcp-host.d.ts +0 -125
  261. package/dist/agent/agent-mcp-host.d.ts.map +0 -1
  262. package/dist/agent/agent-mcp-host.js.map +0 -1
  263. package/dist/agent/agent-mcp-runtime.d.ts.map +0 -1
  264. package/dist/agent/agent-mcp-runtime.js.map +0 -1
  265. package/dist/agent/agent-read-runtime.d.ts +0 -70
  266. package/dist/agent/agent-read-runtime.d.ts.map +0 -1
  267. package/dist/agent/agent-read-runtime.js +0 -320
  268. package/dist/agent/agent-read-runtime.js.map +0 -1
  269. package/dist/agent/agent-revisions.d.ts +0 -35
  270. package/dist/agent/agent-revisions.d.ts.map +0 -1
  271. package/dist/agent/agent-revisions.js +0 -133
  272. package/dist/agent/agent-revisions.js.map +0 -1
  273. package/dist/agent/agent-scheduler.d.ts +0 -99
  274. package/dist/agent/agent-scheduler.d.ts.map +0 -1
  275. package/dist/agent/agent-scheduler.js +0 -398
  276. package/dist/agent/agent-scheduler.js.map +0 -1
  277. package/dist/bus/saved-dispatcher-binding.d.ts +0 -96
  278. package/dist/bus/saved-dispatcher-binding.d.ts.map +0 -1
  279. package/dist/bus/saved-dispatcher-binding.js +0 -35
  280. package/dist/bus/saved-dispatcher-binding.js.map +0 -1
  281. package/dist/delivery/delivery-run-coordinator.d.ts +0 -218
  282. package/dist/delivery/delivery-run-coordinator.d.ts.map +0 -1
  283. package/dist/delivery/delivery-run-coordinator.js +0 -609
  284. package/dist/delivery/delivery-run-coordinator.js.map +0 -1
  285. package/dist/delivery/parked-delivery-obligations.d.ts +0 -182
  286. package/dist/delivery/parked-delivery-obligations.d.ts.map +0 -1
  287. package/dist/delivery/parked-delivery-obligations.js +0 -390
  288. package/dist/delivery/parked-delivery-obligations.js.map +0 -1
  289. package/dist/server/environment-attachment.d.ts +0 -356
  290. package/dist/server/environment-attachment.d.ts.map +0 -1
  291. package/dist/server/environment-attachment.js +0 -2062
  292. package/dist/server/environment-attachment.js.map +0 -1
  293. package/dist/server/environment-delivery-records.d.ts +0 -77
  294. package/dist/server/environment-delivery-records.d.ts.map +0 -1
  295. package/dist/server/environment-delivery-records.js +0 -282
  296. package/dist/server/environment-delivery-records.js.map +0 -1
  297. package/dist/server/environment-delivery-worker.d.ts +0 -161
  298. package/dist/server/environment-delivery-worker.d.ts.map +0 -1
  299. package/dist/server/environment-delivery-worker.js +0 -673
  300. package/dist/server/environment-delivery-worker.js.map +0 -1
@@ -11,6 +11,7 @@
11
11
  * or implied. See the License for the specific language governing permissions and limitations under
12
12
  * the License.
13
13
  */
14
+ var _a;
14
15
  import { Time } from "@spine-event-engine/core/time";
15
16
  import { clone, create } from "@bufbuild/protobuf";
16
17
  import { AnySchema, TimestampSchema } from "@bufbuild/protobuf/wkt";
@@ -22,10 +23,16 @@ import { Inbox, InboxTargets } from "./inbox.js";
22
23
  import { InboxStorage, InboxStorageClock } from "./inbox-storage.js";
23
24
  import { ShardIndex } from "./shard-index.js";
24
25
  import { ShardedWorkRegistry } from "./sharded-work-registry.js";
26
+ import { deliveryTenantKey } from "./delivery-tenant.js";
25
27
  import { withDeliveryCommitFence } from "../repository/commit-fence.js";
28
+ import { receptionObservation } from "./reception-observation.js";
29
+ import { AgentDeliveryScope } from "./agent-delivery-scope.js";
30
+ import { emitServerError } from "../server/server-log.js";
26
31
  const deliveryFailureLimit = 100;
32
+ const controlledMessageSettlers = new WeakMap();
33
+ const dispatchNotAdmitted = Symbol("Delivery recipient closed before dispatch");
27
34
  /**
28
- * Executes direct Inbox delivery for one complete worker identity.
35
+ * Runs one retained direct-delivery worker and its durable Inbox.
29
36
  */
30
37
  export class Delivery {
31
38
  // prettier-ignore
@@ -62,18 +69,146 @@ export class Delivery {
62
69
  */
63
70
  shards;
64
71
  #monitor;
72
+ #directPending = new Map();
73
+ #logger;
74
+ #backgroundObserved = new WeakSet();
75
+ #callerObserved = new WeakSet();
76
+ #activeDirect = new Map();
77
+ #activeRuns = new Set();
78
+ #directDrains = new Map();
79
+ #pendingDirectWake = new Set();
80
+ #replay;
81
+ #acceptReplay;
82
+ #closing = false;
83
+ #startupDraining = false;
84
+ #closeAttempt;
85
+ #closingRuns;
86
+ /**
87
+ * Sets the complete direct fallback dispatcher before receiving work.
88
+ *
89
+ * @internal
90
+ * @param onReplay Callback that replays a ready Inbox route.
91
+ * @param onAccept Reports whether a live recipient currently serves the row.
92
+ */
93
+ setReplay(onReplay, onAccept) {
94
+ this.#replay = onReplay;
95
+ this.#acceptReplay = onAccept;
96
+ }
97
+ /**
98
+ * Waits for every retained direct shard run to release its session.
99
+ *
100
+ * @internal
101
+ * @returns Completion after retained shard sessions release.
102
+ */
103
+ async whenIdle() {
104
+ const failures = [];
105
+ while (this.#activeRuns.size > 0) {
106
+ const active = [...this.#activeRuns];
107
+ const settled = await Promise.allSettled(active.map(({ completed }) => completed));
108
+ settled.forEach((outcome, index) => {
109
+ const failure = active[index]?.releaseFailure;
110
+ if (failure !== undefined)
111
+ failures.push(failure);
112
+ if (outcome.status === "rejected")
113
+ failures.push(outcome.reason);
114
+ });
115
+ }
116
+ if (failures.length === 1)
117
+ throw failures[0];
118
+ if (failures.length > 1)
119
+ throw new AggregateError(failures, "Delivery idle wait failed.");
120
+ }
121
+ /**
122
+ * Stops direct admission and drains all entered shard work.
123
+ *
124
+ * @internal
125
+ * @returns Completion after admitted work and retained cleanup settle.
126
+ */
127
+ close() {
128
+ if (this.#closeAttempt !== undefined)
129
+ return this.#closeAttempt;
130
+ this.beginClose();
131
+ const active = this.#closingRuns ?? [];
132
+ this.#closeAttempt = Promise.allSettled(active.map(({ completed }) => completed)).then(async (settled) => {
133
+ const failures = [];
134
+ settled.forEach((result, index) => {
135
+ const releaseFailure = active[index]?.releaseFailure;
136
+ if (releaseFailure !== undefined)
137
+ failures.push(releaseFailure);
138
+ if (result.status === "rejected")
139
+ failures.push(result.reason);
140
+ });
141
+ const storage = await Promise.allSettled([
142
+ this.inbox instanceof Inbox ? this.inbox.storage.close() : Promise.resolve(),
143
+ this.shards instanceof ShardedWorkRegistry ? this.shards.close() : Promise.resolve(),
144
+ ]);
145
+ for (const result of storage)
146
+ if (result.status === "rejected")
147
+ failures.push(result.reason);
148
+ if (failures.length === 1)
149
+ throw failures[0];
150
+ if (failures.length)
151
+ throw new AggregateError(failures, "Delivery close failed.");
152
+ });
153
+ return this.#closeAttempt;
154
+ }
155
+ /**
156
+ * Closes admission synchronously and retains admitted release outcomes.
157
+ *
158
+ * @internal
159
+ */
160
+ beginClose() {
161
+ if (this.#closing)
162
+ return;
163
+ this.#closing = true;
164
+ this.#closingRuns ??= [...this.#activeRuns];
165
+ for (const active of this.#closingRuns)
166
+ active.drain?.stop(new Error("Delivery closed."));
167
+ }
168
+ /**
169
+ * Marks public runs as unavailable while preserving exact admitted local handoffs.
170
+ *
171
+ * @internal
172
+ */
173
+ beginStartupDrain() {
174
+ if (this.#startupDraining || this.#closing)
175
+ return;
176
+ this.#startupDraining = true;
177
+ this.#closingRuns = [...this.#activeRuns];
178
+ }
179
+ /**
180
+ * Notifies an admitted shard run to scan newly eligible durable rows.
181
+ *
182
+ * @internal
183
+ * @param shard Delivery shard identifier.
184
+ */
185
+ wake(shard) {
186
+ if (this.#closing || (this.#startupDraining && !this.#hasPending(shard.key())))
187
+ return;
188
+ const drain = this.#directDrains.get(shard.key());
189
+ if (drain !== undefined) {
190
+ if (!drain.wake())
191
+ this.#pendingDirectWake.add(shard.key());
192
+ return;
193
+ }
194
+ if (this.#replay !== undefined)
195
+ this.#observeBackground(this.#startDirect(shard), shard);
196
+ }
65
197
  /**
66
198
  * Creates direct delivery from immutable options.
67
199
  *
68
200
  * @param options The delivery configuration.
69
201
  */
70
202
  constructor(options) {
203
+ if (options.context.multitenant)
204
+ throw new TypeError("Delivery requires a single-tenant storage context.");
71
205
  if (options.worker !== undefined &&
72
206
  options.node !== undefined &&
73
207
  options.worker.nodeId?.value !== options.node) {
74
208
  throw new Error("Delivery worker node must match the configured delivery node.");
75
209
  }
76
- this.context = Delivery.#contextSnapshot(options.context);
210
+ this.context = _a.#contextSnapshot(options.context);
211
+ this.#logger = options.logger;
77
212
  this.storageFactory = options.storageFactory;
78
213
  this.strategy = options.strategy ?? { shardCount: 1, shardFor: () => ShardIndex.single() };
79
214
  this.worker =
@@ -118,19 +253,42 @@ export class Delivery {
118
253
  */
119
254
  runControlled(options) {
120
255
  const shard = options.shard;
121
- return this.drain(shard, {
122
- onMessage: options.onMessage,
123
- ...(options.acceptMessage === undefined ? {} : { acceptMessage: options.acceptMessage }),
124
- operation: { signal: options.signal },
125
- }).then((run) => Object.freeze({
126
- status: run.status === "SKIPPED"
127
- ? "SKIPPED"
128
- : run.status === "FAILED"
129
- ? "FAILED"
130
- : run.status === "STOPPED"
131
- ? "STOPPED"
132
- : "COMPLETED",
133
- }));
256
+ const drainOptions = this.#controlledOptions(options);
257
+ if (options.onMessageSettled !== undefined)
258
+ controlledMessageSettlers.set(drainOptions, options.onMessageSettled);
259
+ const completed = this.#replay === undefined
260
+ ? this.drain(shard, drainOptions)
261
+ : this.#startShared(shard, drainOptions);
262
+ return completed
263
+ .then((run) => {
264
+ options.onDrainResult?.(run);
265
+ return Object.freeze({
266
+ status: run.status === "SKIPPED"
267
+ ? "SKIPPED"
268
+ : run.status === "FAILED"
269
+ ? "FAILED"
270
+ : run.status === "STOPPED"
271
+ ? "STOPPED"
272
+ : "COMPLETED",
273
+ });
274
+ })
275
+ .finally(() => {
276
+ controlledMessageSettlers.delete(drainOptions);
277
+ });
278
+ }
279
+ /**
280
+ * Builds the exact controlled options for an endpoint or retained replay run.
281
+ * @param options Controlled run input.
282
+ * @returns The options passed to one physical shard drain.
283
+ */
284
+ #controlledOptions(options) {
285
+ return this.#replay === undefined
286
+ ? {
287
+ onMessage: options.onMessage,
288
+ ...(options.acceptMessage === undefined ? {} : { acceptMessage: options.acceptMessage }),
289
+ operation: { signal: options.signal },
290
+ }
291
+ : { ...this.#directOptions(), operation: { signal: options.signal } };
134
292
  }
135
293
  /**
136
294
  * Delivers the selected message's shard for local handoff integration.
@@ -155,22 +313,148 @@ export class Delivery {
155
313
  : { name: context.name, multitenant: false });
156
314
  }
157
315
  async #drainDirect(message, input) {
158
- let acknowledged = false;
159
- const run = await this.drain(message.shard, {
160
- onMessage: input.onMessage,
161
- ...(input.acceptMessage === undefined ? {} : { acceptMessage: input.acceptMessage }),
162
- onDelivered: (next) => {
163
- acknowledged ||=
164
- next.id.value === message.id.value && next.id.shard.key() === message.id.shard.key();
165
- input.onDelivered?.(next);
166
- },
167
- onDuplicateRemoved: (next) => {
168
- acknowledged ||=
169
- next.id.value === message.id.value && next.id.shard.key() === message.id.shard.key();
170
- input.onDuplicateRemoved?.(next);
316
+ const key = _a.#messageKey(message);
317
+ const pending = this.#directPending.get(key);
318
+ if (pending !== undefined)
319
+ return pending.completed;
320
+ const selected = Promise.withResolvers();
321
+ const settle = (outcome) => {
322
+ if (this.#directPending.get(key)?.settle !== settle)
323
+ return false;
324
+ this.#directPending.delete(key);
325
+ selected.resolve(Object.freeze({ acknowledged: outcome.kind === "acknowledged", outcome }));
326
+ return true;
327
+ };
328
+ this.#directPending.set(key, {
329
+ shardKey: message.shard.key(),
330
+ input,
331
+ settle,
332
+ completed: selected.promise,
333
+ });
334
+ const completed = this.#startDirect(message.shard);
335
+ this.#observeSelectedRun(message.shard, key, completed, settle);
336
+ return selected.promise;
337
+ }
338
+ /**
339
+ * Transfers an unresolved exact-row observer to a successor shard run.
340
+ */
341
+ #observeSelectedRun(shard, key, completed, settle) {
342
+ void completed.then((run) => {
343
+ if (!this.#directPending.has(key))
344
+ return;
345
+ const successor = this.#activeDirect.get(shard.key())?.completed;
346
+ if (successor !== undefined && successor !== completed) {
347
+ this.#observeSelectedRun(shard, key, successor, settle);
348
+ }
349
+ else
350
+ settle({ kind: "unresolved", run });
351
+ }, (error) => {
352
+ if (settle({ kind: "failed", error }))
353
+ this.#callerObserved.add(completed);
354
+ });
355
+ }
356
+ static #messageKey(message) {
357
+ return JSON.stringify([
358
+ deliveryTenantKey(message),
359
+ message.id.value,
360
+ message.id.shard.index,
361
+ message.id.shard.ofTotal,
362
+ ]);
363
+ }
364
+ /**
365
+ * Starts or wakes the retained direct run for one shard.
366
+ */
367
+ #startDirect(shard) {
368
+ if (this.#closing || (this.#startupDraining && !this.#hasPending(shard.key())))
369
+ return Promise.resolve(result("STOPPED"));
370
+ return this.#startShared(shard, this.#directOptions());
371
+ }
372
+ /**
373
+ * Checks whether an exact accepted handoff needs this shard.
374
+ * @param shardKey Canonical shard key.
375
+ * @returns Whether an exact pending observer remains.
376
+ */
377
+ #hasPending(shardKey) {
378
+ return [...this.#directPending.values()].some((pending) => pending.shardKey === shardKey);
379
+ }
380
+ /**
381
+ * Starts or joins the one retained shard run used by local posts and supervision.
382
+ * @param shard Shard selected by the accepted message or supervisor.
383
+ * @param options Complete drain callbacks for a newly admitted run.
384
+ * @returns The terminal run shared by both entry paths.
385
+ */
386
+ #startShared(shard, options) {
387
+ const key = shard.key();
388
+ if (this.#startupDraining && !this.#hasPending(key))
389
+ return Promise.resolve(result("STOPPED"));
390
+ const active = this.#activeDirect.get(key);
391
+ if (active !== undefined) {
392
+ active.wake();
393
+ return active.completed;
394
+ }
395
+ const completed = this.#trackDrain(shard, options, true);
396
+ this.#activeDirect.set(key, {
397
+ completed,
398
+ wake: () => {
399
+ const drain = this.#directDrains.get(key);
400
+ if (drain === undefined)
401
+ this.#pendingDirectWake.add(key);
402
+ else if (!drain.wake())
403
+ this.#pendingDirectWake.add(key);
171
404
  },
172
405
  });
173
- return Object.freeze({ run, acknowledged });
406
+ void completed
407
+ .finally(() => {
408
+ this.#activeDirect.delete(key);
409
+ this.#directDrains.delete(key);
410
+ if (this.#pendingDirectWake.delete(key) && !this.#closing)
411
+ this.#observeBackground(this.#startDirect(shard), shard);
412
+ })
413
+ .catch(() => undefined);
414
+ return completed;
415
+ }
416
+ /**
417
+ * Reports a detached local shard failure without changing its result.
418
+ * @param completed Detached shard run.
419
+ * @param shard Failed shard.
420
+ */
421
+ #observeBackground(completed, shard) {
422
+ if (this.#backgroundObserved.has(completed))
423
+ return;
424
+ this.#backgroundObserved.add(completed);
425
+ void completed.catch(async (error) => {
426
+ // Give exact-row observers of this rejection a chance to settle first.
427
+ await Promise.resolve();
428
+ if (this.#closing || this.#logger === undefined || this.#callerObserved.has(completed))
429
+ return;
430
+ emitServerError(this.#logger, "Background Delivery failed.", {
431
+ shardId: shard.key(),
432
+ errorName: error instanceof Error ? error.name : "UnknownError",
433
+ });
434
+ });
435
+ }
436
+ /**
437
+ * Dispatches each selected direct row through its registered endpoint.
438
+ */
439
+ #directOptions() {
440
+ const request = (message) => this.#directPending.get(_a.#messageKey(message));
441
+ return {
442
+ onMessage: (message) => {
443
+ const selected = request(message);
444
+ return selected === undefined ? this.#replay?.(message) : selected.input.onMessage(message);
445
+ },
446
+ acceptMessage: (message) => {
447
+ const selected = request(message);
448
+ return selected === undefined
449
+ ? !this.#startupDraining &&
450
+ this.#replay !== undefined &&
451
+ (this.#acceptReplay?.(message) ?? true)
452
+ : (selected.input.acceptMessage?.(message) ?? true);
453
+ },
454
+ onDelivered: (message) => request(message)?.input.onDelivered?.(message),
455
+ onDuplicateRemoved: (message) => request(message)?.input.onDuplicateRemoved?.(message),
456
+ onMessageOutcome: (message, outcome) => request(message)?.settle(outcome),
457
+ };
174
458
  }
175
459
  /**
176
460
  * Executes one owned shard drain while containing monitor and endpoint failures.
@@ -179,8 +463,37 @@ export class Delivery {
179
463
  * @param options The endpoint and optional operation signal.
180
464
  * @returns The terminal direct-delivery result.
181
465
  */
182
- async drain(shard, options) {
183
- if (options.operation?.signal?.aborted)
466
+ drain(shard, options) {
467
+ return this.#trackDrain(shard, options, false);
468
+ }
469
+ /**
470
+ * Tracks one complete physical drain before opening its shard session.
471
+ * @param shard Selected shard.
472
+ * @param options Complete drain callbacks.
473
+ * @param shared Whether the run serves the shared replay route.
474
+ * @returns Completion after shard release.
475
+ */
476
+ #trackDrain(shard, options, shared) {
477
+ if (this.#closing || (this.#startupDraining && (!shared || !this.#hasPending(shard.key()))))
478
+ return Promise.resolve(result("STOPPED"));
479
+ const completion = Promise.withResolvers();
480
+ const active = { completed: completion.promise, shared };
481
+ this.#activeRuns.add(active);
482
+ if (this.#startupDraining)
483
+ this.#closingRuns?.push(active);
484
+ void this.#drainAdmitted(shard, options, active).then(completion.resolve, completion.reject);
485
+ void active.completed.finally(() => this.#activeRuns.delete(active)).catch(() => undefined);
486
+ return active.completed;
487
+ }
488
+ /**
489
+ * Completes shard acquisition, callbacks, and release for an admitted run.
490
+ * @param shard Selected shard.
491
+ * @param options Complete drain callbacks.
492
+ * @param active Admitted run record.
493
+ * @returns Terminal result after release.
494
+ */
495
+ async #drainAdmitted(shard, options, active) {
496
+ if (this.#shouldStop(options))
184
497
  return result("STOPPED");
185
498
  let session;
186
499
  try {
@@ -194,11 +507,41 @@ export class Delivery {
194
507
  await safely(async () => (await this.#monitor.onShardAlreadyPicked(new AlreadyPickedUp(shard))).execute());
195
508
  return result("SKIPPED");
196
509
  }
197
- if (options.operation?.signal?.aborted) {
198
- const released = await safelyValue(() => this.shards.release(session), false);
510
+ if (this.#shouldStop(options))
511
+ return this.#releaseStoppedSession(session, active);
512
+ return this.#runSession(shard, options, session, active);
513
+ }
514
+ /**
515
+ * Releases a session acquired after direct admission closed.
516
+ *
517
+ * @param session The acquired session to release.
518
+ * @param active The admitted run retaining release failure.
519
+ * @returns The stopped or failed direct-delivery result.
520
+ */
521
+ async #releaseStoppedSession(session, active) {
522
+ try {
523
+ const released = await this.shards.release(session);
524
+ if (!released)
525
+ active.releaseFailure = new Error("Shard release was not confirmed.");
199
526
  return result(released ? "STOPPED" : "FAILED");
200
527
  }
201
- return new DeliveryDrain({
528
+ catch (error) {
529
+ active.releaseFailure =
530
+ error instanceof Error ? error : new Error("Shard release failed.", { cause: error });
531
+ return result("FAILED");
532
+ }
533
+ }
534
+ /**
535
+ * Reads current admission fences after asynchronous shard acquisition.
536
+ */
537
+ #shouldStop(options) {
538
+ return this.#closing || options.operation?.signal?.aborted === true;
539
+ }
540
+ /**
541
+ * Tracks an admitted direct session through its complete drain.
542
+ */
543
+ #runSession(shard, options, session, active) {
544
+ const drain = new DeliveryDrain({
202
545
  inbox: this.inbox,
203
546
  monitor: this.#monitor,
204
547
  options,
@@ -206,22 +549,58 @@ export class Delivery {
206
549
  session,
207
550
  shards: this.shards,
208
551
  shard,
209
- }).run();
552
+ });
553
+ active.drain = drain;
554
+ if (active.shared) {
555
+ this.#directDrains.set(shard.key(), drain);
556
+ if (this.#pendingDirectWake.delete(shard.key()))
557
+ drain.wake();
558
+ }
559
+ const completed = drain.run().finally(() => {
560
+ active.releaseFailure = drain.releaseFailure;
561
+ });
562
+ void completed
563
+ .finally(() => {
564
+ if (active.shared && this.#directDrains.get(shard.key()) === drain)
565
+ this.#directDrains.delete(shard.key());
566
+ })
567
+ .catch(() => undefined);
568
+ return completed;
210
569
  }
211
570
  }
571
+ _a = Delivery;
212
572
  /**
213
573
  * Processes inbox pages while retaining one shard session.
214
574
  */
215
575
  class DeliveryDrain {
216
576
  input;
217
577
  #statistics = counts();
578
+ #agentController = new AbortController();
579
+ #liveAgents = new Set();
218
580
  #failures = [];
219
581
  #deduplication;
220
582
  #blockedTargets = new Set();
583
+ #capacityBlockedTargets = new Set();
221
584
  #removedInPage = new Set();
585
+ #messageOutcomes = new WeakMap();
586
+ #wakeGeneration = 0;
587
+ #wakeGate = Promise.withResolvers();
588
+ #sessionSerial = Promise.resolve();
589
+ #renewalTimer;
222
590
  #current;
223
591
  #ownershipLost = false;
224
592
  #failureLimitReached = false;
593
+ #stopped = false;
594
+ #reading = true;
595
+ #releaseFailure;
596
+ /**
597
+ * Returns a failed session release to an active direct close.
598
+ *
599
+ * @returns The original shard-release failure, if one occurred.
600
+ */
601
+ get releaseFailure() {
602
+ return this.#releaseFailure;
603
+ }
225
604
  /**
226
605
  * Creates a drain for one acquired shard session.
227
606
  *
@@ -232,6 +611,84 @@ class DeliveryDrain {
232
611
  this.#current = input.session;
233
612
  this.#deduplication = new DeliveryDeduplication(input.inbox);
234
613
  }
614
+ /**
615
+ * Notifies a fresh durable scan without starting a second shard session.
616
+ *
617
+ * @returns Whether the wake was recorded for a live shard run.
618
+ */
619
+ wake() {
620
+ if (!this.#reading)
621
+ return false;
622
+ this.#wakeGeneration += 1;
623
+ for (const target of this.#capacityBlockedTargets)
624
+ this.#blockedTargets.delete(target);
625
+ this.#capacityBlockedTargets.clear();
626
+ this.#wakeGate.resolve(undefined);
627
+ this.#wakeGate = Promise.withResolvers();
628
+ return true;
629
+ }
630
+ /**
631
+ * Stops new work and cancels entered Agent work for explicit shutdown.
632
+ *
633
+ * @param reason Cause of cancellation or failure.
634
+ */
635
+ stop(reason) {
636
+ this.#stopped = true;
637
+ this.#agentController.abort(reason);
638
+ this.#clearRenewal();
639
+ this.wake();
640
+ }
641
+ /**
642
+ * Serializes validation, renewal, and release against one current session snapshot.
643
+ * @typeParam T Result of the admitted shard operation.
644
+ * @param onOperation Current-session operation serialized with renewal and release.
645
+ * @returns Result of the admitted session operation.
646
+ */
647
+ #sessionOperation(onOperation) {
648
+ const attempt = this.#sessionSerial.then(onOperation);
649
+ this.#sessionSerial = attempt.then(() => undefined, () => undefined);
650
+ return attempt;
651
+ }
652
+ /**
653
+ * Schedules renewal before the issued lease expires while live work remains.
654
+ */
655
+ #scheduleRenewal() {
656
+ if (this.#stopped || this.#current.kind !== "LEASED" || this.input.shards.renew === undefined)
657
+ return;
658
+ this.#clearRenewal();
659
+ const life = this.#current.expiresAt.getTime() - this.#current.pickedUpAt.getTime();
660
+ const delay = Math.max(1, Math.min(Math.floor(life / 2), this.#current.expiresAt.getTime() - Time.currentTimeMillis() - 1));
661
+ this.#renewalTimer = setTimeout(() => {
662
+ void this.#renewLease();
663
+ }, delay);
664
+ this.#renewalTimer.unref();
665
+ }
666
+ /**
667
+ * Updates one live lease and fences the run permanently on loss.
668
+ * @returns Completion of lease renewal or loss fencing.
669
+ */
670
+ async #renewLease() {
671
+ await this.#sessionOperation(async () => {
672
+ const renew = this.input.shards.renew?.bind(this.input.shards);
673
+ if (this.#current.kind !== "LEASED" || this.#stopped || renew === undefined)
674
+ return;
675
+ const renewed = await safelyValue(() => renew(this.#current), undefined);
676
+ if (renewed === undefined)
677
+ this.stop(new Error("Shard ownership was lost."));
678
+ else
679
+ this.#current = renewed;
680
+ });
681
+ if (!this.#stopped && this.#liveAgents.size > 0)
682
+ this.#scheduleRenewal();
683
+ }
684
+ /**
685
+ * Cancels the live lease timer after completion or loss.
686
+ */
687
+ #clearRenewal() {
688
+ if (this.#renewalTimer !== undefined)
689
+ clearTimeout(this.#renewalTimer);
690
+ this.#renewalTimer = undefined;
691
+ }
235
692
  /**
236
693
  * Executes delivery and releases the shard session.
237
694
  *
@@ -243,10 +700,12 @@ class DeliveryDrain {
243
700
  run = (await this.#canStart()) ? await this.#readPages() : this.#complete("STOPPED");
244
701
  }
245
702
  catch (error) {
246
- if (!(await this.#release()))
247
- return this.#complete("FAILED");
703
+ this.#reading = false;
704
+ this.stop(error);
705
+ await this.#release();
248
706
  throw error;
249
707
  }
708
+ this.#reading = false;
250
709
  return (await this.#release()) ? run : this.#complete("FAILED");
251
710
  }
252
711
  /**
@@ -255,7 +714,20 @@ class DeliveryDrain {
255
714
  * @returns Whether the session was released.
256
715
  */
257
716
  async #release() {
258
- const released = await safelyValue(() => this.input.shards.release(this.#current, this.input.options.operation), false);
717
+ if (this.#stopped || this.#ownershipLost || this.input.options.operation?.signal?.aborted)
718
+ this.#agentController.abort();
719
+ await Promise.allSettled(this.#liveAgents);
720
+ this.#clearRenewal();
721
+ let released = false;
722
+ try {
723
+ released = await this.#sessionOperation(() => this.input.shards.release(this.#current, this.input.options.operation));
724
+ }
725
+ catch (error) {
726
+ this.#releaseFailure =
727
+ error instanceof Error ? error : new Error("Shard release failed.", { cause: error });
728
+ }
729
+ if (!released && this.#releaseFailure === undefined)
730
+ this.#releaseFailure = new Error("Shard release was not confirmed.");
259
731
  if (released)
260
732
  await safely(() => this.#completeMonitoring());
261
733
  return released;
@@ -278,14 +750,19 @@ class DeliveryDrain {
278
750
  async #readPages() {
279
751
  let after;
280
752
  let deliveredInScan = false;
753
+ let scanGeneration = this.#wakeGeneration;
281
754
  for (;;) {
755
+ if (this.#stopped || this.#ownershipLost || this.input.options.operation?.signal?.aborted)
756
+ return this.#complete("STOPPED");
757
+ const wake = this.#wakeGate.promise;
282
758
  const messages = await this.#readPage(after);
283
759
  this.#removedInPage.clear();
284
760
  if (messages.length === 0) {
285
- if (!deliveredInScan)
761
+ if (await this.#emptyPageDrained(deliveredInScan, scanGeneration, wake))
286
762
  return this.#complete("DRAINED");
287
763
  after = undefined;
288
764
  deliveredInScan = false;
765
+ scanGeneration = this.#wakeGeneration;
289
766
  continue;
290
767
  }
291
768
  const deliveredBefore = this.#statistics.delivered;
@@ -295,10 +772,11 @@ class DeliveryDrain {
295
772
  return this.#complete("STOPPED");
296
773
  deliveredInScan ||= this.#statistics.delivered !== deliveredBefore;
297
774
  if (messages.length < this.input.pageSize) {
298
- if (!deliveredInScan)
775
+ if (await this.#shortPageDrained(deliveredInScan, scanGeneration, wake))
299
776
  return this.#complete("DRAINED");
300
777
  after = undefined;
301
778
  deliveredInScan = false;
779
+ scanGeneration = this.#wakeGeneration;
302
780
  continue;
303
781
  }
304
782
  const survivor = messages.findLast((message) => !this.#removedInPage.has(message));
@@ -306,6 +784,51 @@ class DeliveryDrain {
306
784
  after = this.#continuation([survivor]);
307
785
  }
308
786
  }
787
+ /**
788
+ * Waits for admitted Agent work after an empty Inbox page.
789
+ * @param deliveredInScan Whether the current scan delivered a row.
790
+ * @param generation Wake generation captured before reading the page.
791
+ * @param wake Notification that new work became ready.
792
+ * @returns Whether the empty page ends this drain.
793
+ */
794
+ async #emptyPageDrained(deliveredInScan, generation, wake) {
795
+ if (generation !== this.#wakeGeneration)
796
+ return false;
797
+ if (this.#liveAgents.size > 0) {
798
+ await Promise.race([...this.#liveAgents, wake]);
799
+ return false;
800
+ }
801
+ return !deliveredInScan && this.#endScan(generation);
802
+ }
803
+ /**
804
+ * Waits for live Agent work before deciding that a short page is drained.
805
+ * @param deliveredInScan Whether the current scan delivered a row.
806
+ * @param generation Wake generation captured before reading the page.
807
+ * @param wake Notification that new work became ready.
808
+ * @returns Whether the short page ends this drain.
809
+ */
810
+ async #shortPageDrained(deliveredInScan, generation, wake) {
811
+ if (deliveredInScan)
812
+ return false;
813
+ if (generation !== this.#wakeGeneration)
814
+ return false;
815
+ if (this.#liveAgents.size === 0)
816
+ return this.#endScan(generation);
817
+ await Promise.race([...this.#liveAgents, wake]);
818
+ return false;
819
+ }
820
+ /**
821
+ * Checks the terminal scan decision against a concurrent durable wake.
822
+ *
823
+ * @param generation Wake generation captured before reading the page.
824
+ * @returns Whether this scan may finish without losing a notification.
825
+ */
826
+ #endScan(generation) {
827
+ if (generation !== this.#wakeGeneration)
828
+ return false;
829
+ this.#reading = false;
830
+ return true;
831
+ }
309
832
  /**
310
833
  * Reads the next inbox page after an optional continuation.
311
834
  *
@@ -343,19 +866,27 @@ class DeliveryDrain {
343
866
  * @returns Whether processing may continue.
344
867
  */
345
868
  async #processMessage(message, page) {
346
- if (this.input.options.operation?.signal?.aborted)
869
+ if (this.#stopped || this.input.options.operation?.signal?.aborted)
347
870
  return false;
348
871
  if (this.#shouldSkip(message))
349
872
  return true;
350
- const target = this.#targetKey(message);
351
- if (this.#blockedTargets.has(target))
352
- return true;
353
- this.#statistics.processed += 1;
354
- if (!(await this.#mayProcess()))
355
- return false;
356
- if (page.isDuplicate(message))
357
- return this.#removeDuplicate(message, target);
358
- return this.#deliver(message, target);
873
+ try {
874
+ const target = this.#targetKey(message);
875
+ if (this.#blockedTargets.has(target))
876
+ return true;
877
+ this.#statistics.processed += 1;
878
+ if (!(await this.#mayProcess()))
879
+ return false;
880
+ if (page.isDuplicate(message)) {
881
+ const continued = await this.#removeDuplicate(message, target);
882
+ this.#reportOutcome(message);
883
+ return continued;
884
+ }
885
+ return await this.#deliver(message, target);
886
+ }
887
+ finally {
888
+ controlledMessageSettlers.get(this.input.options)?.(message);
889
+ }
359
890
  }
360
891
  /**
361
892
  * Determines whether an inbox message is ineligible for endpoint delivery.
@@ -375,7 +906,11 @@ class DeliveryDrain {
375
906
  * @returns The target type and ID key.
376
907
  */
377
908
  #targetKey(message) {
378
- return `${message.inboxId.targetTypeUrl}:${InboxTargets.key(message.inboxId.targetId)}`;
909
+ return JSON.stringify([
910
+ deliveryTenantKey(message),
911
+ message.inboxId.targetTypeUrl,
912
+ InboxTargets.key(message.inboxId.targetId),
913
+ ]);
379
914
  }
380
915
  /**
381
916
  * Checks monitoring and verifies that the shard session remains current.
@@ -395,9 +930,10 @@ class DeliveryDrain {
395
930
  */
396
931
  async #removeDuplicate(message, target) {
397
932
  try {
398
- if (!(await this.input.inbox.removeDuplicate(message, this.#current, this.input.options.operation)))
933
+ if (!(await this.input.inbox.removeDuplicate(message, this.input.options.operation)))
399
934
  throw new Error("Inbox duplicate was not removed.");
400
935
  this.#removedInPage.add(message);
936
+ this.#messageOutcomes.set(message, { kind: "acknowledged", agent: false });
401
937
  this.input.options.onDuplicateRemoved?.(message);
402
938
  }
403
939
  catch (error) {
@@ -414,35 +950,156 @@ class DeliveryDrain {
414
950
  * @returns Whether processing may continue.
415
951
  */
416
952
  async #deliver(message, target) {
953
+ const acknowledgement = { completed: false, exhausted: false };
954
+ await this.#processAttempt(message, target, acknowledgement);
955
+ this.#reportOutcome(message);
956
+ return !this.#ownershipLost && !this.input.options.operation?.signal?.aborted;
957
+ }
958
+ /**
959
+ * Records one selected row after its endpoint and monitor policy settle.
960
+ * @param message Inbox row whose settled outcome is reported.
961
+ */
962
+ #reportOutcome(message) {
963
+ const outcome = this.#messageOutcomes.get(message);
964
+ if (outcome !== undefined)
965
+ this.input.options.onMessageOutcome?.(message, outcome);
966
+ }
967
+ /**
968
+ * Executes one dispatch attempt and the monitor-selected response to a receptor failure.
969
+ *
970
+ * @param message The persisted Inbox message.
971
+ * @param target The target key used to isolate acknowledgement failure.
972
+ * @param acknowledgement Shared status across immediate repeats.
973
+ * @returns Completion of the attempt and its selected action.
974
+ */
975
+ async #processAttempt(message, target, acknowledgement) {
976
+ const observation = receptionObservation.create();
977
+ const generation = this.#wakeGeneration;
417
978
  try {
979
+ const agent = await this.#dispatch(message, observation);
980
+ if (agent === dispatchNotAdmitted) {
981
+ acknowledgement.notAdmitted = true;
982
+ return;
983
+ }
418
984
  this.#statistics.accepted += 1;
419
- await this.#dispatch(message);
420
- if (this.input.options.operation?.signal?.aborted || !(await this.#validate()))
421
- return false;
422
- await this.#markDelivered(message);
985
+ if (agent?.kind === "deferred") {
986
+ this.#recordDeferred(message, target, generation);
987
+ return;
988
+ }
989
+ if (agent?.kind === "error") {
990
+ this.#recordFailure(message, agent.error);
991
+ this.#blockedTargets.add(target);
992
+ return;
993
+ }
994
+ if (agent?.kind === "accepted") {
995
+ await this.#completeAgentAttempt(message, target, acknowledgement, agent);
996
+ return;
997
+ }
423
998
  }
424
999
  catch (error) {
425
- await this.#handleReceptionFailure(message, target, error);
426
- if (this.#ownershipLost)
427
- return false;
1000
+ await this.#handleReceptionFailure(message, target, acknowledgement, observation, error);
1001
+ if (this.#ownershipLost || this.input.options.operation?.signal?.aborted)
1002
+ return;
428
1003
  }
429
- return true;
1004
+ await this.#completeAttempt(message, target, acknowledgement);
430
1005
  }
431
1006
  /**
432
- * Records endpoint failure and runs the configured recovery action.
433
- *
434
- * @param message Supplies the failed inbox message.
435
- * @param target Identifies the message target.
436
- * @param error Supplies the endpoint or acknowledgement failure.
437
- * @returns Completion after the recovery action finishes.
1007
+ * Completes acknowledgement before starting an admitted Agent handler.
1008
+ * @param message Persisted Inbox row.
1009
+ * @param target Canonical target key.
1010
+ * @param acknowledgement Status shared with immediate repeats.
1011
+ * @param agent Admitted handler awaiting durable acknowledgement.
1012
+ * @returns Completion after acknowledgement and handler start or discard.
1013
+ */
1014
+ async #completeAgentAttempt(message, target, acknowledgement, agent) {
1015
+ await this.#completeAttempt(message, target, acknowledgement);
1016
+ if (acknowledgement.completed) {
1017
+ this.#messageOutcomes.set(message, { kind: "acknowledged", agent: true });
1018
+ this.#startAgent(message, agent);
1019
+ }
1020
+ else
1021
+ agent.discard();
1022
+ }
1023
+ /**
1024
+ * Records a capacity deferral and revisits it if release raced with dispatch.
1025
+ * @param generation Wake generation captured before admission.
1026
+ * @param message Inbox row deferred for Agent capacity.
1027
+ * @param target Canonical target key blocked in this scan.
438
1028
  */
439
- async #handleReceptionFailure(message, target, error) {
1029
+ #recordDeferred(message, target, generation) {
1030
+ this.#messageOutcomes.set(message, { kind: "deferred" });
1031
+ this.#blockedTargets.add(target);
1032
+ this.#capacityBlockedTargets.add(target);
1033
+ if (generation !== this.#wakeGeneration)
1034
+ this.wake();
1035
+ }
1036
+ /**
1037
+ * Applies the original receptor monitor only to an actual receptor failure.
1038
+ * @param acknowledgement Status shared with repeated attempts.
1039
+ * @param error Original reception failure.
1040
+ * @param message Inbox row whose receptor failed.
1041
+ * @param observation Whether the receptor callback was entered.
1042
+ * @param target Canonical target key for acknowledgement.
1043
+ * @returns Completion of the monitor-selected failure action.
1044
+ */
1045
+ async #handleReceptionFailure(message, target, acknowledgement, observation, error) {
1046
+ if (this.#ownershipLost || this.input.options.operation?.signal?.aborted) {
1047
+ this.#recordFailure(message, error);
1048
+ return;
1049
+ }
1050
+ if (!receptionObservation.isReceptorFailure(observation, error))
1051
+ throw error;
440
1052
  this.#recordFailure(message, error);
441
- const reception = new FailedReception(message, error, () => this.#acknowledge(message), () => this.#repeat(message));
442
- const action = await safelyValue(() => this.input.monitor.onReceptionFailure(reception), reception.markDelivered());
443
- if (!(await safely(() => action.execute())) &&
444
- !(await safely(() => reception.markDelivered().execute())))
1053
+ const reception = new FailedReception(message, error, () => this.#acknowledgeOnce(message, acknowledgement), () => this.#processAttempt(message, target, acknowledgement));
1054
+ const action = await this.input.monitor.onReceptionFailure(reception);
1055
+ await action.execute();
1056
+ }
1057
+ /**
1058
+ * Marks completed processing once, including nested repeat recovery.
1059
+ *
1060
+ * @param message The processed Inbox message.
1061
+ * @param target The target key used to isolate acknowledgement failure.
1062
+ * @param acknowledgement Shared status across immediate repeats.
1063
+ * @returns Completion after acknowledgement or exhausted retry containment.
1064
+ */
1065
+ async #completeAttempt(message, target, acknowledgement) {
1066
+ if (acknowledgement.completed ||
1067
+ acknowledgement.exhausted ||
1068
+ acknowledgement.notAdmitted ||
1069
+ this.input.options.operation?.signal?.aborted)
1070
+ return;
1071
+ if (!(await this.#validate()))
1072
+ return;
1073
+ try {
1074
+ await this.#acknowledgeWithRetry(message, acknowledgement);
1075
+ }
1076
+ catch (error) {
1077
+ acknowledgement.exhausted = true;
1078
+ this.#recordFailure(message, error);
445
1079
  this.#blockedTargets.add(target);
1080
+ }
1081
+ }
1082
+ /**
1083
+ * Retries only durable acknowledgement after processing has completed.
1084
+ *
1085
+ * @param message Supplies the successfully processed message.
1086
+ * @param acknowledgement Shared status across immediate repeats.
1087
+ * @returns Completion after acknowledgement.
1088
+ */
1089
+ async #acknowledgeWithRetry(message, acknowledgement) {
1090
+ let firstError;
1091
+ for (let attempt = 0; attempt < 3; attempt += 1) {
1092
+ try {
1093
+ await this.#acknowledgeOnce(message, acknowledgement);
1094
+ return;
1095
+ }
1096
+ catch (error) {
1097
+ firstError ??= error;
1098
+ if (this.#ownershipLost || this.input.options.operation?.signal?.aborted)
1099
+ break;
1100
+ }
1101
+ }
1102
+ throw firstError;
446
1103
  }
447
1104
  /**
448
1105
  * Marks a message delivered after checking the shard session.
@@ -456,26 +1113,69 @@ class DeliveryDrain {
456
1113
  await this.#markDelivered(message);
457
1114
  }
458
1115
  /**
459
- * Dispatches the message again and acknowledges it.
1116
+ * Marks a message delivered once across nested monitor-selected repeats.
460
1117
  *
461
- * @param message Supplies the inbox message to repeat.
462
- * @returns Completion after dispatch and acknowledgement.
1118
+ * @param message The message to mark delivered.
1119
+ * @param acknowledgement Shared status across immediate repeats.
1120
+ * @returns Completion after acknowledgement.
463
1121
  */
464
- async #repeat(message) {
465
- await this.#dispatch(message);
1122
+ async #acknowledgeOnce(message, acknowledgement) {
1123
+ if (acknowledgement.completed)
1124
+ return;
466
1125
  await this.#acknowledge(message);
1126
+ acknowledgement.completed = true;
467
1127
  }
468
1128
  /**
469
1129
  * Dispatches a message while validating the shard session before the call.
470
1130
  *
471
1131
  * @param message Supplies the inbox message for the endpoint.
1132
+ * @param observation Records failure origin for this attempt.
472
1133
  * @returns Completion after endpoint dispatch.
473
1134
  */
474
- async #dispatch(message) {
475
- await withDeliveryCommitFence(async () => {
1135
+ async #dispatch(message, observation) {
1136
+ const operation = this.input.options.operation?.signal;
1137
+ const signal = operation === undefined
1138
+ ? this.#agentController.signal
1139
+ : AbortSignal.any([this.#agentController.signal, operation]);
1140
+ const admission = { rejected: false };
1141
+ const outcome = await AgentDeliveryScope.run(() => receptionObservation.run(observation, () => withDeliveryCommitFence(async () => {
476
1142
  if (!(await this.#validate()))
477
1143
  throw new Error("Shard ownership was lost.");
478
- }, () => Promise.resolve(this.input.options.onMessage(message)));
1144
+ }, () => {
1145
+ if (this.input.options.acceptMessage?.(message) === false) {
1146
+ admission.rejected = true;
1147
+ return Promise.resolve();
1148
+ }
1149
+ observation.entered = true;
1150
+ return Promise.resolve(this.input.options.onMessage(message));
1151
+ })), signal);
1152
+ return admission.rejected ? dispatchNotAdmitted : outcome;
1153
+ }
1154
+ /**
1155
+ * Records admitted Agent work under this shard session after Inbox acknowledgement.
1156
+ * @param agent Accepted Agent turn and discard callback.
1157
+ * @param message Acknowledged Inbox row.
1158
+ */
1159
+ #startAgent(message, agent) {
1160
+ const operation = this.input.options.operation?.signal;
1161
+ const signal = operation === undefined
1162
+ ? this.#agentController.signal
1163
+ : AbortSignal.any([this.#agentController.signal, operation]);
1164
+ const checkSession = async () => {
1165
+ if (!(await this.#validate()))
1166
+ throw new Error("Shard ownership was lost.");
1167
+ };
1168
+ const work = Promise.resolve()
1169
+ .then(() => agent.run(signal, checkSession))
1170
+ .catch((error) => {
1171
+ this.#recordFailure(message, error);
1172
+ })
1173
+ .finally(() => {
1174
+ this.#liveAgents.delete(work);
1175
+ this.wake();
1176
+ });
1177
+ this.#liveAgents.add(work);
1178
+ this.#scheduleRenewal();
479
1179
  }
480
1180
  /**
481
1181
  * Marks a message delivered and updates local counts and callbacks.
@@ -488,7 +1188,13 @@ class DeliveryDrain {
488
1188
  throw new Error("Inbox message was not marked delivered.");
489
1189
  this.#deduplication.recordDelivered(message);
490
1190
  this.#statistics.delivered += 1;
491
- this.input.options.onDelivered?.(message);
1191
+ this.#messageOutcomes.set(message, { kind: "acknowledged", agent: false });
1192
+ try {
1193
+ this.input.options.onDelivered?.(message);
1194
+ }
1195
+ catch {
1196
+ // A post-ack observer cannot undo durable delivery or repeat processing.
1197
+ }
492
1198
  }
493
1199
  /**
494
1200
  * Removes expired delivered messages from one page when supported.
@@ -525,9 +1231,7 @@ class DeliveryDrain {
525
1231
  return false;
526
1232
  if (this.input.options.operation?.signal?.aborted || !(await this.#validate()))
527
1233
  return undefined;
528
- const removed = await this.input.inbox.removeDelivered(message, this.#current, this.input.options.operation);
529
- if (this.input.options.operation?.signal?.aborted || (!removed && !(await this.#validate())))
530
- return undefined;
1234
+ const removed = await this.input.inbox.removeDelivered(message, this.input.options.operation);
531
1235
  return removed;
532
1236
  }
533
1237
  /**
@@ -558,12 +1262,18 @@ class DeliveryDrain {
558
1262
  * @returns Whether the shard session remains valid.
559
1263
  */
560
1264
  async #validate() {
561
- const current = await safelyValue(() => this.input.shards.validateOwnership(this.#current, this.input.options.operation), undefined);
562
- if (current === undefined)
563
- this.#ownershipLost = true;
564
- else
565
- this.#current = current;
566
- return current !== undefined;
1265
+ return this.#sessionOperation(async () => {
1266
+ if (this.#stopped || this.#ownershipLost)
1267
+ return false;
1268
+ const current = await safelyValue(() => this.input.shards.validateOwnership(this.#current, this.input.options.operation), undefined);
1269
+ if (current === undefined) {
1270
+ this.#ownershipLost = true;
1271
+ this.stop(new Error("Shard ownership was lost."));
1272
+ }
1273
+ else
1274
+ this.#current = current;
1275
+ return current !== undefined;
1276
+ });
567
1277
  }
568
1278
  /**
569
1279
  * Records an ephemeral delivery failure up to the configured limit.
@@ -573,6 +1283,8 @@ class DeliveryDrain {
573
1283
  */
574
1284
  #recordFailure(message, error) {
575
1285
  this.#statistics.failed += 1;
1286
+ if (this.#messageOutcomes.get(message)?.kind !== "acknowledged")
1287
+ this.#messageOutcomes.set(message, { kind: "failed", error });
576
1288
  if (this.#failures.length < deliveryFailureLimit)
577
1289
  this.#failures.push(Object.freeze({ message: snapshot(message), error }));
578
1290
  this.#failureLimitReached = this.#failures.length === deliveryFailureLimit;
@@ -584,6 +1296,7 @@ class DeliveryDrain {
584
1296
  * @returns The immutable delivery result.
585
1297
  */
586
1298
  #complete(status) {
1299
+ this.#reading = false;
587
1300
  return result(status, this.#statistics, this.#failures);
588
1301
  }
589
1302
  /**
@@ -774,6 +1487,7 @@ class RecentDeliveries {
774
1487
  */
775
1488
  static key(message) {
776
1489
  return JSON.stringify([
1490
+ deliveryTenantKey(message),
777
1491
  message.signalId,
778
1492
  message.inboxId.targetTypeUrl,
779
1493
  InboxTargets.key(message.inboxId.targetId),