@spine-event-engine/server 2.0.0-snapshot.24 → 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 (286) hide show
  1. package/README.md +38 -11
  2. package/REFERENCE.md +201 -130
  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/{bus/saved-dispatcher-binding.js → agent/agent-live-inputs.js} +2 -14
  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 +17 -4
  68. package/dist/bus/event-bus.d.ts.map +1 -1
  69. package/dist/bus/event-bus.js +42 -12
  70. package/dist/bus/event-bus.js.map +1 -1
  71. package/dist/context/bounded-context.d.ts +140 -87
  72. package/dist/context/bounded-context.d.ts.map +1 -1
  73. package/dist/context/bounded-context.js +702 -329
  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 +22 -13
  78. package/dist/context/entity-inbox.js.map +1 -1
  79. package/dist/context/local-inbox-handoff.d.ts +34 -34
  80. package/dist/context/local-inbox-handoff.d.ts.map +1 -1
  81. package/dist/context/local-inbox-handoff.js +108 -150
  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 +10 -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 +1 -1
  104. package/dist/delivery/delivery-monitor.js +1 -1
  105. package/dist/delivery/delivery-run-control.d.ts +6 -0
  106. package/dist/delivery/delivery-run-control.d.ts.map +1 -1
  107. package/dist/delivery/delivery-run-control.js +10 -0
  108. package/dist/delivery/delivery-run-control.js.map +1 -1
  109. package/dist/delivery/delivery-supervisor.d.ts.map +1 -1
  110. package/dist/delivery/delivery-supervisor.js +28 -4
  111. package/dist/delivery/delivery-supervisor.js.map +1 -1
  112. package/dist/delivery/delivery-tenant.d.ts +17 -0
  113. package/dist/delivery/delivery-tenant.d.ts.map +1 -0
  114. package/dist/delivery/delivery-tenant.js +50 -0
  115. package/dist/delivery/delivery-tenant.js.map +1 -0
  116. package/dist/delivery/delivery.d.ts +96 -2
  117. package/dist/delivery/delivery.d.ts.map +1 -1
  118. package/dist/delivery/delivery.js +693 -53
  119. package/dist/delivery/delivery.js.map +1 -1
  120. package/dist/delivery/inbox-records.d.ts.map +1 -1
  121. package/dist/delivery/inbox-records.js +9 -16
  122. package/dist/delivery/inbox-records.js.map +1 -1
  123. package/dist/delivery/inbox-storage.d.ts +6 -0
  124. package/dist/delivery/inbox-storage.d.ts.map +1 -1
  125. package/dist/delivery/inbox-storage.js +43 -6
  126. package/dist/delivery/inbox-storage.js.map +1 -1
  127. package/dist/delivery/inbox.d.ts.map +1 -1
  128. package/dist/delivery/inbox.js +3 -2
  129. package/dist/delivery/inbox.js.map +1 -1
  130. package/dist/delivery/reception-observation.d.ts +12 -4
  131. package/dist/delivery/reception-observation.d.ts.map +1 -1
  132. package/dist/delivery/reception-observation.js +14 -3
  133. package/dist/delivery/reception-observation.js.map +1 -1
  134. package/dist/delivery/sharded-work-registry.d.ts +6 -0
  135. package/dist/delivery/sharded-work-registry.d.ts.map +1 -1
  136. package/dist/delivery/sharded-work-registry.js +41 -4
  137. package/dist/delivery/sharded-work-registry.js.map +1 -1
  138. package/dist/entity/entity-storage-descriptor.d.ts.map +1 -1
  139. package/dist/entity/entity-storage-descriptor.js +7 -4
  140. package/dist/entity/entity-storage-descriptor.js.map +1 -1
  141. package/dist/index.d.ts +4 -2
  142. package/dist/index.d.ts.map +1 -1
  143. package/dist/index.js +2 -1
  144. package/dist/index.js.map +1 -1
  145. package/dist/integration/external-messages.d.ts.map +1 -1
  146. package/dist/integration/external-messages.js +9 -15
  147. package/dist/integration/external-messages.js.map +1 -1
  148. package/dist/integration/integration-broker.d.ts.map +1 -1
  149. package/dist/integration/integration-broker.js +112 -6
  150. package/dist/integration/integration-broker.js.map +1 -1
  151. package/dist/integration/third-party-context.d.ts.map +1 -1
  152. package/dist/integration/third-party-context.js +7 -10
  153. package/dist/integration/third-party-context.js.map +1 -1
  154. package/dist/repository/primitive-id.js +26 -11
  155. package/dist/repository/primitive-id.js.map +1 -1
  156. package/dist/repository/repository.d.ts +32 -60
  157. package/dist/repository/repository.d.ts.map +1 -1
  158. package/dist/repository/repository.js +1446 -1787
  159. package/dist/repository/repository.js.map +1 -1
  160. package/dist/runtime/runtime.d.ts +13 -0
  161. package/dist/runtime/runtime.d.ts.map +1 -1
  162. package/dist/runtime/runtime.js +18 -0
  163. package/dist/runtime/runtime.js.map +1 -1
  164. package/dist/runtime/signal-metadata.d.ts.map +1 -1
  165. package/dist/runtime/signal-metadata.js +6 -6
  166. package/dist/runtime/signal-metadata.js.map +1 -1
  167. package/dist/runtime/signal-publisher.d.ts +0 -6
  168. package/dist/runtime/signal-publisher.d.ts.map +1 -1
  169. package/dist/runtime/signal-publisher.js +1 -9
  170. package/dist/runtime/signal-publisher.js.map +1 -1
  171. package/dist/runtime/standalone-handler-runtime.d.ts +0 -7
  172. package/dist/runtime/standalone-handler-runtime.d.ts.map +1 -1
  173. package/dist/runtime/standalone-handler-runtime.js +13 -99
  174. package/dist/runtime/standalone-handler-runtime.js.map +1 -1
  175. package/dist/server/managed-delivery-gate.d.ts +34 -0
  176. package/dist/server/managed-delivery-gate.d.ts.map +1 -0
  177. package/dist/server/managed-delivery-gate.js +70 -0
  178. package/dist/server/managed-delivery-gate.js.map +1 -0
  179. package/dist/server/managed-server-application.d.ts.map +1 -1
  180. package/dist/server/managed-server-application.js +7 -4
  181. package/dist/server/managed-server-application.js.map +1 -1
  182. package/dist/server/server-environment.d.ts +203 -25
  183. package/dist/server/server-environment.d.ts.map +1 -1
  184. package/dist/server/server-environment.js +661 -133
  185. package/dist/server/server-environment.js.map +1 -1
  186. package/dist/server/server-log.d.ts +7 -0
  187. package/dist/server/server-log.d.ts.map +1 -1
  188. package/dist/server/server-log.js +14 -1
  189. package/dist/server/server-log.js.map +1 -1
  190. package/dist/server/server.d.ts +18 -32
  191. package/dist/server/server.d.ts.map +1 -1
  192. package/dist/server/server.js +50 -204
  193. package/dist/server/server.js.map +1 -1
  194. package/dist/services/spine-services.d.ts +1 -1
  195. package/dist/services/spine-services.d.ts.map +1 -1
  196. package/dist/services/spine-services.js +236 -335
  197. package/dist/services/spine-services.js.map +1 -1
  198. package/dist/stand/event-origin-filter.js +3 -3
  199. package/dist/stand/event-origin-filter.js.map +1 -1
  200. package/dist/stand/stand.d.ts +14 -0
  201. package/dist/stand/stand.d.ts.map +1 -1
  202. package/dist/stand/stand.js +53 -9
  203. package/dist/stand/stand.js.map +1 -1
  204. package/dist/stand/subscription-observer.d.ts.map +1 -1
  205. package/dist/stand/subscription-observer.js +54 -40
  206. package/dist/stand/subscription-observer.js.map +1 -1
  207. package/dist/stand/subscription-registry.d.ts +6 -2
  208. package/dist/stand/subscription-registry.d.ts.map +1 -1
  209. package/dist/stand/subscription-registry.js +15 -11
  210. package/dist/stand/subscription-registry.js.map +1 -1
  211. package/dist/stand/subscription-runtime.d.ts +5 -3
  212. package/dist/stand/subscription-runtime.d.ts.map +1 -1
  213. package/dist/stand/subscription-runtime.js +52 -9
  214. package/dist/stand/subscription-runtime.js.map +1 -1
  215. package/dist/testing/index.d.ts.map +1 -1
  216. package/dist/testing/index.js +4 -3
  217. package/dist/testing/index.js.map +1 -1
  218. package/dist/tsconfig.tsbuildinfo +1 -1
  219. package/package.json +14 -14
  220. package/dist/agent/agent-admission.d.ts +0 -80
  221. package/dist/agent/agent-admission.d.ts.map +0 -1
  222. package/dist/agent/agent-admission.js +0 -127
  223. package/dist/agent/agent-admission.js.map +0 -1
  224. package/dist/agent/agent-ai-runtime.d.ts +0 -76
  225. package/dist/agent/agent-ai-runtime.d.ts.map +0 -1
  226. package/dist/agent/agent-ai-runtime.js +0 -1269
  227. package/dist/agent/agent-ai-runtime.js.map +0 -1
  228. package/dist/agent/agent-execution-fault.d.ts +0 -18
  229. package/dist/agent/agent-execution-fault.d.ts.map +0 -1
  230. package/dist/agent/agent-execution-fault.js.map +0 -1
  231. package/dist/agent/agent-execution-session.d.ts +0 -103
  232. package/dist/agent/agent-execution-session.d.ts.map +0 -1
  233. package/dist/agent/agent-execution-session.js +0 -238
  234. package/dist/agent/agent-execution-session.js.map +0 -1
  235. package/dist/agent/agent-interaction-audit.d.ts +0 -58
  236. package/dist/agent/agent-interaction-audit.d.ts.map +0 -1
  237. package/dist/agent/agent-interaction-audit.js +0 -114
  238. package/dist/agent/agent-interaction-audit.js.map +0 -1
  239. package/dist/agent/agent-interaction-events.d.ts +0 -40
  240. package/dist/agent/agent-interaction-events.d.ts.map +0 -1
  241. package/dist/agent/agent-interaction-events.js +0 -129
  242. package/dist/agent/agent-interaction-events.js.map +0 -1
  243. package/dist/agent/agent-mcp-budget.d.ts +0 -125
  244. package/dist/agent/agent-mcp-budget.d.ts.map +0 -1
  245. package/dist/agent/agent-mcp-budget.js +0 -566
  246. package/dist/agent/agent-mcp-budget.js.map +0 -1
  247. package/dist/agent/agent-mcp-host.d.ts +0 -125
  248. package/dist/agent/agent-mcp-host.d.ts.map +0 -1
  249. package/dist/agent/agent-mcp-host.js.map +0 -1
  250. package/dist/agent/agent-mcp-runtime.d.ts.map +0 -1
  251. package/dist/agent/agent-mcp-runtime.js.map +0 -1
  252. package/dist/agent/agent-read-runtime.d.ts +0 -70
  253. package/dist/agent/agent-read-runtime.d.ts.map +0 -1
  254. package/dist/agent/agent-read-runtime.js +0 -320
  255. package/dist/agent/agent-read-runtime.js.map +0 -1
  256. package/dist/agent/agent-revisions.d.ts +0 -35
  257. package/dist/agent/agent-revisions.d.ts.map +0 -1
  258. package/dist/agent/agent-revisions.js +0 -133
  259. package/dist/agent/agent-revisions.js.map +0 -1
  260. package/dist/agent/agent-scheduler.d.ts +0 -99
  261. package/dist/agent/agent-scheduler.d.ts.map +0 -1
  262. package/dist/agent/agent-scheduler.js +0 -398
  263. package/dist/agent/agent-scheduler.js.map +0 -1
  264. package/dist/bus/saved-dispatcher-binding.d.ts +0 -53
  265. package/dist/bus/saved-dispatcher-binding.d.ts.map +0 -1
  266. package/dist/bus/saved-dispatcher-binding.js.map +0 -1
  267. package/dist/delivery/delivery-run-coordinator.d.ts +0 -218
  268. package/dist/delivery/delivery-run-coordinator.d.ts.map +0 -1
  269. package/dist/delivery/delivery-run-coordinator.js +0 -609
  270. package/dist/delivery/delivery-run-coordinator.js.map +0 -1
  271. package/dist/delivery/parked-delivery-obligations.d.ts +0 -182
  272. package/dist/delivery/parked-delivery-obligations.d.ts.map +0 -1
  273. package/dist/delivery/parked-delivery-obligations.js +0 -390
  274. package/dist/delivery/parked-delivery-obligations.js.map +0 -1
  275. package/dist/server/environment-attachment.d.ts +0 -356
  276. package/dist/server/environment-attachment.d.ts.map +0 -1
  277. package/dist/server/environment-attachment.js +0 -2062
  278. package/dist/server/environment-attachment.js.map +0 -1
  279. package/dist/server/environment-delivery-records.d.ts +0 -77
  280. package/dist/server/environment-delivery-records.d.ts.map +0 -1
  281. package/dist/server/environment-delivery-records.js +0 -282
  282. package/dist/server/environment-delivery-records.js.map +0 -1
  283. package/dist/server/environment-delivery-worker.d.ts +0 -161
  284. package/dist/server/environment-delivery-worker.d.ts.map +0 -1
  285. package/dist/server/environment-delivery-worker.js +0 -973
  286. 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,12 +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";
26
28
  import { receptionObservation } from "./reception-observation.js";
29
+ import { AgentDeliveryScope } from "./agent-delivery-scope.js";
30
+ import { emitServerError } from "../server/server-log.js";
27
31
  const deliveryFailureLimit = 100;
28
32
  const controlledMessageSettlers = new WeakMap();
33
+ const dispatchNotAdmitted = Symbol("Delivery recipient closed before dispatch");
29
34
  /**
30
- * Executes direct Inbox delivery for one complete worker identity.
35
+ * Runs one retained direct-delivery worker and its durable Inbox.
31
36
  */
32
37
  export class Delivery {
33
38
  // prettier-ignore
@@ -64,18 +69,146 @@ export class Delivery {
64
69
  */
65
70
  shards;
66
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
+ }
67
197
  /**
68
198
  * Creates direct delivery from immutable options.
69
199
  *
70
200
  * @param options The delivery configuration.
71
201
  */
72
202
  constructor(options) {
203
+ if (options.context.multitenant)
204
+ throw new TypeError("Delivery requires a single-tenant storage context.");
73
205
  if (options.worker !== undefined &&
74
206
  options.node !== undefined &&
75
207
  options.worker.nodeId?.value !== options.node) {
76
208
  throw new Error("Delivery worker node must match the configured delivery node.");
77
209
  }
78
- this.context = Delivery.#contextSnapshot(options.context);
210
+ this.context = _a.#contextSnapshot(options.context);
211
+ this.#logger = options.logger;
79
212
  this.storageFactory = options.storageFactory;
80
213
  this.strategy = options.strategy ?? { shardCount: 1, shardFor: () => ShardIndex.single() };
81
214
  this.worker =
@@ -120,14 +253,13 @@ export class Delivery {
120
253
  */
121
254
  runControlled(options) {
122
255
  const shard = options.shard;
123
- const drainOptions = {
124
- onMessage: options.onMessage,
125
- ...(options.acceptMessage === undefined ? {} : { acceptMessage: options.acceptMessage }),
126
- operation: { signal: options.signal },
127
- };
256
+ const drainOptions = this.#controlledOptions(options);
128
257
  if (options.onMessageSettled !== undefined)
129
258
  controlledMessageSettlers.set(drainOptions, options.onMessageSettled);
130
- return this.drain(shard, drainOptions)
259
+ const completed = this.#replay === undefined
260
+ ? this.drain(shard, drainOptions)
261
+ : this.#startShared(shard, drainOptions);
262
+ return completed
131
263
  .then((run) => {
132
264
  options.onDrainResult?.(run);
133
265
  return Object.freeze({
@@ -144,6 +276,20 @@ export class Delivery {
144
276
  controlledMessageSettlers.delete(drainOptions);
145
277
  });
146
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 } };
292
+ }
147
293
  /**
148
294
  * Delivers the selected message's shard for local handoff integration.
149
295
  *
@@ -167,22 +313,148 @@ export class Delivery {
167
313
  : { name: context.name, multitenant: false });
168
314
  }
169
315
  async #drainDirect(message, input) {
170
- let acknowledged = false;
171
- const run = await this.drain(message.shard, {
172
- onMessage: input.onMessage,
173
- ...(input.acceptMessage === undefined ? {} : { acceptMessage: input.acceptMessage }),
174
- onDelivered: (next) => {
175
- acknowledged ||=
176
- next.id.value === message.id.value && next.id.shard.key() === message.id.shard.key();
177
- input.onDelivered?.(next);
178
- },
179
- onDuplicateRemoved: (next) => {
180
- acknowledged ||=
181
- next.id.value === message.id.value && next.id.shard.key() === message.id.shard.key();
182
- 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);
183
404
  },
184
405
  });
185
- 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
+ };
186
458
  }
187
459
  /**
188
460
  * Executes one owned shard drain while containing monitor and endpoint failures.
@@ -191,8 +463,37 @@ export class Delivery {
191
463
  * @param options The endpoint and optional operation signal.
192
464
  * @returns The terminal direct-delivery result.
193
465
  */
194
- async drain(shard, options) {
195
- 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))
196
497
  return result("STOPPED");
197
498
  let session;
198
499
  try {
@@ -206,11 +507,41 @@ export class Delivery {
206
507
  await safely(async () => (await this.#monitor.onShardAlreadyPicked(new AlreadyPickedUp(shard))).execute());
207
508
  return result("SKIPPED");
208
509
  }
209
- if (options.operation?.signal?.aborted) {
210
- 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.");
211
526
  return result(released ? "STOPPED" : "FAILED");
212
527
  }
213
- 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({
214
545
  inbox: this.inbox,
215
546
  monitor: this.#monitor,
216
547
  options,
@@ -218,22 +549,58 @@ export class Delivery {
218
549
  session,
219
550
  shards: this.shards,
220
551
  shard,
221
- }).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;
222
569
  }
223
570
  }
571
+ _a = Delivery;
224
572
  /**
225
573
  * Processes inbox pages while retaining one shard session.
226
574
  */
227
575
  class DeliveryDrain {
228
576
  input;
229
577
  #statistics = counts();
578
+ #agentController = new AbortController();
579
+ #liveAgents = new Set();
230
580
  #failures = [];
231
581
  #deduplication;
232
582
  #blockedTargets = new Set();
583
+ #capacityBlockedTargets = new Set();
233
584
  #removedInPage = new Set();
585
+ #messageOutcomes = new WeakMap();
586
+ #wakeGeneration = 0;
587
+ #wakeGate = Promise.withResolvers();
588
+ #sessionSerial = Promise.resolve();
589
+ #renewalTimer;
234
590
  #current;
235
591
  #ownershipLost = false;
236
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
+ }
237
604
  /**
238
605
  * Creates a drain for one acquired shard session.
239
606
  *
@@ -244,6 +611,84 @@ class DeliveryDrain {
244
611
  this.#current = input.session;
245
612
  this.#deduplication = new DeliveryDeduplication(input.inbox);
246
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
+ }
247
692
  /**
248
693
  * Executes delivery and releases the shard session.
249
694
  *
@@ -255,9 +700,12 @@ class DeliveryDrain {
255
700
  run = (await this.#canStart()) ? await this.#readPages() : this.#complete("STOPPED");
256
701
  }
257
702
  catch (error) {
703
+ this.#reading = false;
704
+ this.stop(error);
258
705
  await this.#release();
259
706
  throw error;
260
707
  }
708
+ this.#reading = false;
261
709
  return (await this.#release()) ? run : this.#complete("FAILED");
262
710
  }
263
711
  /**
@@ -266,7 +714,20 @@ class DeliveryDrain {
266
714
  * @returns Whether the session was released.
267
715
  */
268
716
  async #release() {
269
- 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.");
270
731
  if (released)
271
732
  await safely(() => this.#completeMonitoring());
272
733
  return released;
@@ -289,14 +750,19 @@ class DeliveryDrain {
289
750
  async #readPages() {
290
751
  let after;
291
752
  let deliveredInScan = false;
753
+ let scanGeneration = this.#wakeGeneration;
292
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;
293
758
  const messages = await this.#readPage(after);
294
759
  this.#removedInPage.clear();
295
760
  if (messages.length === 0) {
296
- if (!deliveredInScan)
761
+ if (await this.#emptyPageDrained(deliveredInScan, scanGeneration, wake))
297
762
  return this.#complete("DRAINED");
298
763
  after = undefined;
299
764
  deliveredInScan = false;
765
+ scanGeneration = this.#wakeGeneration;
300
766
  continue;
301
767
  }
302
768
  const deliveredBefore = this.#statistics.delivered;
@@ -306,10 +772,11 @@ class DeliveryDrain {
306
772
  return this.#complete("STOPPED");
307
773
  deliveredInScan ||= this.#statistics.delivered !== deliveredBefore;
308
774
  if (messages.length < this.input.pageSize) {
309
- if (!deliveredInScan)
775
+ if (await this.#shortPageDrained(deliveredInScan, scanGeneration, wake))
310
776
  return this.#complete("DRAINED");
311
777
  after = undefined;
312
778
  deliveredInScan = false;
779
+ scanGeneration = this.#wakeGeneration;
313
780
  continue;
314
781
  }
315
782
  const survivor = messages.findLast((message) => !this.#removedInPage.has(message));
@@ -317,6 +784,51 @@ class DeliveryDrain {
317
784
  after = this.#continuation([survivor]);
318
785
  }
319
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
+ }
320
832
  /**
321
833
  * Reads the next inbox page after an optional continuation.
322
834
  *
@@ -354,7 +866,7 @@ class DeliveryDrain {
354
866
  * @returns Whether processing may continue.
355
867
  */
356
868
  async #processMessage(message, page) {
357
- if (this.input.options.operation?.signal?.aborted)
869
+ if (this.#stopped || this.input.options.operation?.signal?.aborted)
358
870
  return false;
359
871
  if (this.#shouldSkip(message))
360
872
  return true;
@@ -365,8 +877,11 @@ class DeliveryDrain {
365
877
  this.#statistics.processed += 1;
366
878
  if (!(await this.#mayProcess()))
367
879
  return false;
368
- if (page.isDuplicate(message))
369
- return await this.#removeDuplicate(message, target);
880
+ if (page.isDuplicate(message)) {
881
+ const continued = await this.#removeDuplicate(message, target);
882
+ this.#reportOutcome(message);
883
+ return continued;
884
+ }
370
885
  return await this.#deliver(message, target);
371
886
  }
372
887
  finally {
@@ -391,7 +906,11 @@ class DeliveryDrain {
391
906
  * @returns The target type and ID key.
392
907
  */
393
908
  #targetKey(message) {
394
- 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
+ ]);
395
914
  }
396
915
  /**
397
916
  * Checks monitoring and verifies that the shard session remains current.
@@ -414,6 +933,7 @@ class DeliveryDrain {
414
933
  if (!(await this.input.inbox.removeDuplicate(message, this.input.options.operation)))
415
934
  throw new Error("Inbox duplicate was not removed.");
416
935
  this.#removedInPage.add(message);
936
+ this.#messageOutcomes.set(message, { kind: "acknowledged", agent: false });
417
937
  this.input.options.onDuplicateRemoved?.(message);
418
938
  }
419
939
  catch (error) {
@@ -432,8 +952,18 @@ class DeliveryDrain {
432
952
  async #deliver(message, target) {
433
953
  const acknowledgement = { completed: false, exhausted: false };
434
954
  await this.#processAttempt(message, target, acknowledgement);
955
+ this.#reportOutcome(message);
435
956
  return !this.#ownershipLost && !this.input.options.operation?.signal?.aborted;
436
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
+ }
437
967
  /**
438
968
  * Executes one dispatch attempt and the monitor-selected response to a receptor failure.
439
969
  *
@@ -444,24 +974,86 @@ class DeliveryDrain {
444
974
  */
445
975
  async #processAttempt(message, target, acknowledgement) {
446
976
  const observation = receptionObservation.create();
977
+ const generation = this.#wakeGeneration;
447
978
  try {
979
+ const agent = await this.#dispatch(message, observation);
980
+ if (agent === dispatchNotAdmitted) {
981
+ acknowledgement.notAdmitted = true;
982
+ return;
983
+ }
448
984
  this.#statistics.accepted += 1;
449
- await this.#dispatch(message, observation);
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
+ }
450
998
  }
451
999
  catch (error) {
452
- if (this.#ownershipLost || this.input.options.operation?.signal?.aborted) {
453
- this.#recordFailure(message, error);
1000
+ await this.#handleReceptionFailure(message, target, acknowledgement, observation, error);
1001
+ if (this.#ownershipLost || this.input.options.operation?.signal?.aborted)
454
1002
  return;
455
- }
456
- if (!receptionObservation.isReceptorFailure(observation, error))
457
- throw error;
458
- this.#recordFailure(message, error);
459
- const reception = new FailedReception(message, error, () => this.#acknowledgeOnce(message, acknowledgement), () => this.#processAttempt(message, target, acknowledgement));
460
- const action = await this.input.monitor.onReceptionFailure(reception);
461
- await action.execute();
462
1003
  }
463
1004
  await this.#completeAttempt(message, target, acknowledgement);
464
1005
  }
1006
+ /**
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.
1028
+ */
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;
1052
+ this.#recordFailure(message, error);
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
+ }
465
1057
  /**
466
1058
  * Marks completed processing once, including nested repeat recovery.
467
1059
  *
@@ -473,6 +1065,7 @@ class DeliveryDrain {
473
1065
  async #completeAttempt(message, target, acknowledgement) {
474
1066
  if (acknowledgement.completed ||
475
1067
  acknowledgement.exhausted ||
1068
+ acknowledgement.notAdmitted ||
476
1069
  this.input.options.operation?.signal?.aborted)
477
1070
  return;
478
1071
  if (!(await this.#validate()))
@@ -540,13 +1133,49 @@ class DeliveryDrain {
540
1133
  * @returns Completion after endpoint dispatch.
541
1134
  */
542
1135
  async #dispatch(message, observation) {
543
- await receptionObservation.run(observation, () => withDeliveryCommitFence(async () => {
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 () => {
544
1142
  if (!(await this.#validate()))
545
1143
  throw new Error("Shard ownership was lost.");
546
1144
  }, () => {
1145
+ if (this.input.options.acceptMessage?.(message) === false) {
1146
+ admission.rejected = true;
1147
+ return Promise.resolve();
1148
+ }
547
1149
  observation.entered = true;
548
1150
  return Promise.resolve(this.input.options.onMessage(message));
549
- }));
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();
550
1179
  }
551
1180
  /**
552
1181
  * Marks a message delivered and updates local counts and callbacks.
@@ -559,6 +1188,7 @@ class DeliveryDrain {
559
1188
  throw new Error("Inbox message was not marked delivered.");
560
1189
  this.#deduplication.recordDelivered(message);
561
1190
  this.#statistics.delivered += 1;
1191
+ this.#messageOutcomes.set(message, { kind: "acknowledged", agent: false });
562
1192
  try {
563
1193
  this.input.options.onDelivered?.(message);
564
1194
  }
@@ -632,12 +1262,18 @@ class DeliveryDrain {
632
1262
  * @returns Whether the shard session remains valid.
633
1263
  */
634
1264
  async #validate() {
635
- const current = await safelyValue(() => this.input.shards.validateOwnership(this.#current, this.input.options.operation), undefined);
636
- if (current === undefined)
637
- this.#ownershipLost = true;
638
- else
639
- this.#current = current;
640
- 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
+ });
641
1277
  }
642
1278
  /**
643
1279
  * Records an ephemeral delivery failure up to the configured limit.
@@ -647,6 +1283,8 @@ class DeliveryDrain {
647
1283
  */
648
1284
  #recordFailure(message, error) {
649
1285
  this.#statistics.failed += 1;
1286
+ if (this.#messageOutcomes.get(message)?.kind !== "acknowledged")
1287
+ this.#messageOutcomes.set(message, { kind: "failed", error });
650
1288
  if (this.#failures.length < deliveryFailureLimit)
651
1289
  this.#failures.push(Object.freeze({ message: snapshot(message), error }));
652
1290
  this.#failureLimitReached = this.#failures.length === deliveryFailureLimit;
@@ -658,6 +1296,7 @@ class DeliveryDrain {
658
1296
  * @returns The immutable delivery result.
659
1297
  */
660
1298
  #complete(status) {
1299
+ this.#reading = false;
661
1300
  return result(status, this.#statistics, this.#failures);
662
1301
  }
663
1302
  /**
@@ -848,6 +1487,7 @@ class RecentDeliveries {
848
1487
  */
849
1488
  static key(message) {
850
1489
  return JSON.stringify([
1490
+ deliveryTenantKey(message),
851
1491
  message.signalId,
852
1492
  message.inboxId.targetTypeUrl,
853
1493
  InboxTargets.key(message.inboxId.targetId),